// ── ERPNext Dispatch resource calls ───────────────────────────────────────── // All ERPNext fetch() calls live here. // Swap BASE_URL in config/erpnext.js to change the target server. // ───────────────────────────────────────────────────────────────────────────── import { BASE_URL } from 'src/config/erpnext' import { HUB_URL } from 'src/config/hub' import { authFetch } from './auth' // Créneaux disponibles (hub : gap-finding + tampon de route + classement) pour un nouveau job à une adresse. // payload = { duration_h, latitude, longitude, after_date?, limit? } → [{ tech_id, tech_name, date, start_time, end_time, travel_min, distance_km, reasons[] }] export async function suggestSlots (payload) { const res = await fetch(`${HUB_URL}/dispatch/suggest-slots`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload || {}), }) if (!res.ok) throw new Error('suggest-slots ' + res.status) const data = await res.json() return data.slots || data || [] } async function apiGet (path) { const res = await authFetch(BASE_URL + path) const data = await res.json() if (data.exc) { console.error('[apiGet] ERPNext error:', path.slice(0, 120), data.exc.slice ? data.exc.slice(0, 200) : data.exc) throw new Error(data.exc) } return data } async function apiPut (doctype, name, body) { const res = await authFetch( `${BASE_URL}/api/resource/${encodeURIComponent(doctype)}/${encodeURIComponent(name)}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }, ) const text = await res.text() if (!res.ok) console.error(`[API] PUT ${doctype}/${name} failed:`, res.status, text) let data try { data = JSON.parse(text) } catch { throw new Error(`PUT ${doctype}/${name}: invalid JSON — ${text.slice(0, 200)}`) } if (data.exc) throw new Error(data.exc) return data } // Single-call fetch with all fields + child tables async function listDocs (doctype, fields = '["*"]', filters = null, limit = 200) { let url = `/api/resource/${encodeURIComponent(doctype)}?fields=${fields}&limit_page_length=${limit}` if (filters) url += '&filters=' + encodeURIComponent(JSON.stringify(filters)) const data = await apiGet(url) return data.data || [] } // Fetch single doc (needed for child tables not returned by list) async function fetchDoc (doctype, name) { return (await apiGet(`/api/resource/${encodeURIComponent(doctype)}/${encodeURIComponent(name)}`)).data } export async function fetchTechnicians () { // Fast: single list call (no child tables = no tags) const techs = await listDocs('Dispatch Technician', '["*"]', null, 100) return techs // tags loaded lazily via loadTechTags() } // Background: fetch individual docs to get child tables (tags) export async function loadTechTags (techNames) { const results = await Promise.all(techNames.map(n => fetchDoc('Dispatch Technician', n).catch(() => null))) return results.filter(Boolean) } // Fast: single list call, no child tables (assistants/tags come empty) export async function fetchJobsFast (filters = null) { return listDocs('Dispatch Job', '["*"]', filters, 500) } // Full: adds child tables (assistants, tags) — use only for jobs that need them export async function fetchJobFull (name) { return fetchDoc('Dispatch Job', name) } // Legacy: fetches all with child tables (slow — avoid) export async function fetchJobs (filters = null) { return fetchJobsFast(filters) } export async function updateJob (name, payload) { return apiPut('Dispatch Job', name, payload) } export async function createJob (payload) { const res = await authFetch( `${BASE_URL}/api/resource/Dispatch%20Job`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }, ) const data = await res.json() if (data.exc) throw new Error(data.exc) return data.data } export async function publishJobs (jobNames) { const results = await Promise.allSettled( jobNames.map(name => apiPut('Dispatch Job', name, { published: 1 })) ) const failed = results.filter(r => r.status === 'rejected') if (failed.length) console.warn(`[publishJobs] ${failed.length}/${jobNames.length} failed`) return { published: jobNames.length - failed.length, failed: failed.length } } export async function updateTech (name, payload) { return apiPut('Dispatch Technician', name, payload) } export async function createTech (payload) { const res = await authFetch( `${BASE_URL}/api/resource/Dispatch%20Technician`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }, ) const data = await res.json() if (data.exc) throw new Error(data.exc) return data.data } export async function deleteTech (name) { const res = await authFetch( `${BASE_URL}/api/resource/Dispatch%20Technician/${encodeURIComponent(name)}`, { method: 'DELETE' }, ) if (!res.ok) { const data = await res.json().catch(() => ({})) const msg = data._server_messages ? JSON.parse(JSON.parse(data._server_messages)[0]).message : data.exception || 'Delete failed' throw new Error(msg) } } export async function fetchTags () { const data = await apiGet('/api/resource/Dispatch%20Tag?fields=["name","label","color","category"]&limit=200') return data.data || [] } export async function createTag (label, category = 'Custom', color = '#6b7280') { const res = await authFetch( `${BASE_URL}/api/resource/Dispatch%20Tag`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ label, category, color }), }, ) const data = await res.json() if (data.exc) throw new Error(data.exc) return data.data } export async function updateTag (name, payload) { return apiPut('Dispatch Tag', name, payload) } export async function renameTag (oldName, newName) { const res = await authFetch( `${BASE_URL}/api/method/frappe.client.rename_doc`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ doctype: 'Dispatch Tag', old_name: oldName, new_name: newName }), }, ) const data = await res.json() if (data.exc) throw new Error(data.exc) return data.message // returns new name } export async function deleteTag (name) { const res = await authFetch( `${BASE_URL}/api/resource/Dispatch%20Tag/${encodeURIComponent(name)}`, { method: 'DELETE' }, ) if (!res.ok) throw new Error('Delete tag failed') }