/** * API — ServiceRequest, ServiceBid, EquipmentInstall * * Tries Frappe custom doctypes first, falls back to Lead + localStorage * so the app works before the backend doctypes are created. */ const BASE = '' async function getCSRF () { const m = document.cookie.match(/csrftoken=([^;]+)/) if (m) return m[1] const r = await fetch('/api/method/frappe.auth.get_csrf_token', { credentials: 'include' }) const d = await r.json().catch(() => ({})) return d.csrf_token || '' } async function frappePOST (doctype, data) { const csrf = await getCSRF().catch(() => '') const r = await fetch(`${BASE}/api/resource/${encodeURIComponent(doctype)}`, { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json', 'X-Frappe-CSRF-Token': csrf }, body: JSON.stringify(data), }) if (!r.ok) throw new Error(`HTTP ${r.status}`) const body = await r.json() return body.data } async function frappePUT (doctype, name, data) { const csrf = await getCSRF().catch(() => '') const r = await fetch(`${BASE}/api/resource/${encodeURIComponent(doctype)}/${encodeURIComponent(name)}`, { method: 'PUT', credentials: 'include', headers: { 'Content-Type': 'application/json', 'X-Frappe-CSRF-Token': csrf }, body: JSON.stringify(data), }) if (!r.ok) throw new Error(`HTTP ${r.status}`) const body = await r.json() return body.data } async function frappeGET (doctype, filters = {}, fields = ['name']) { const params = new URLSearchParams({ fields: JSON.stringify(fields), filters: JSON.stringify(filters), limit: 50, }) const r = await fetch(`${BASE}/api/resource/${encodeURIComponent(doctype)}?${params}`, { credentials: 'include', }) if (!r.ok) throw new Error(`HTTP ${r.status}`) const body = await r.json() return body.data || [] } // ───────────────────────────────────────────────────────────────────────────── // ServiceRequest // ───────────────────────────────────────────────────────────────────────────── export async function createServiceRequest (data) { /** * data = { * service_type: 'internet' | 'tv' | 'telephone' | 'multi', * problem_type: string, * description: string, * address: string, * coordinates: [lng, lat], * preferred_dates: [{ date, time_slot, time_slots[] }, ...], // up to 3 * contact: { name, phone, email }, * urgency: 'normal' | 'urgent', * budget: { id, label, min, max } | null, * } */ const ref = 'SR-' + Date.now().toString(36).toUpperCase().slice(-6) // Try Frappe ServiceRequest doctype try { const doc = await frappePOST('Service Request', { customer_name: data.contact.name, phone: data.contact.phone, email: data.contact.email, service_type: data.service_type, problem_type: data.problem_type, description: data.description, address: data.address, lng: data.coordinates?.[0] || 0, lat: data.coordinates?.[1] || 0, preferred_date_1: data.preferred_dates[0]?.date || '', time_slot_1: data.preferred_dates[0]?.time_slot || '', preferred_date_2: data.preferred_dates[1]?.date || '', time_slot_2: data.preferred_dates[1]?.time_slot || '', preferred_date_3: data.preferred_dates[2]?.date || '', time_slot_3: data.preferred_dates[2]?.time_slot || '', urgency: data.urgency || 'normal', budget_label: data.budget?.label || '', budget_min: data.budget?.min || 0, budget_max: data.budget?.max || 0, status: 'New', }) return { ref: doc.name, source: 'frappe' } } catch (_) {} // Fallback: create as Frappe Lead + HD Ticket try { const notes = buildNotes(data) const doc = await frappePOST('Lead', { lead_name: data.contact.name, mobile_no: data.contact.phone, email_id: data.contact.email || '', source: 'Dispatch Booking', lead_owner: '', status: 'Open', notes, }) return { ref: doc.name, source: 'lead' } } catch (_) {} // Final fallback: localStorage const list = JSON.parse(localStorage.getItem('dispatch_service_requests') || '[]') list.push({ ref, ...data, lng: data.coordinates?.[0] || 0, lat: data.coordinates?.[1] || 0, budget_label: data.budget?.label || '', created: new Date().toISOString(), status: 'new' }) localStorage.setItem('dispatch_service_requests', JSON.stringify(list)) return { ref, source: 'local' } } function buildNotes (data) { const dates = data.preferred_dates .filter(d => d.date) .map((d, i) => ` Date ${i + 1}: ${d.date} — ${d.time_slot}`) .join('\n') return [ `SERVICE: ${data.service_type?.toUpperCase()}`, `PROBLÈME: ${data.problem_type}`, `DESCRIPTION: ${data.description}`, `ADRESSE: ${data.address}`, `URGENCE: ${data.urgency}`, '', 'DATES PRÉFÉRÉES:', dates, ].join('\n') } export async function fetchServiceRequests (status = null) { try { const filters = status ? { status } : {} return await frappeGET('Service Request', filters, [ 'name', 'customer_name', 'phone', 'service_type', 'problem_type', 'description', 'address', 'status', 'urgency', 'preferred_date_1', 'time_slot_1', 'preferred_date_2', 'time_slot_2', 'preferred_date_3', 'time_slot_3', 'confirmed_date', 'creation', 'budget_label', 'budget_min', 'budget_max', ]) } catch (_) { return JSON.parse(localStorage.getItem('dispatch_service_requests') || '[]') } } export async function updateServiceRequestStatus (name, status, confirmedDate = null) { try { const data = {} if (status) data.status = status if (confirmedDate) data.confirmed_date = confirmedDate if (Object.keys(data).length === 0) return return await frappePUT('Service Request', name, data) } catch (_) { const list = JSON.parse(localStorage.getItem('dispatch_service_requests') || '[]') const item = list.find(r => r.ref === name || r.name === name) if (item) { if (status) item.status = status if (confirmedDate) item.confirmed_date = confirmedDate } localStorage.setItem('dispatch_service_requests', JSON.stringify(list)) } } // ───────────────────────────────────────────────────────────────────────────── // ServiceBid (tech bids on a date) // ───────────────────────────────────────────────────────────────────────────── export async function createServiceBid (data) { /** * data = { request, technician, proposed_date, time_slot, estimated_duration, notes, price } */ try { return await frappePOST('Service Bid', { ...data, status: 'Pending' }) } catch (_) { const list = JSON.parse(localStorage.getItem('dispatch_service_bids') || '[]') const bid = { ref: 'BID-' + Date.now().toString(36).toUpperCase().slice(-6), ...data, status: 'pending', created: new Date().toISOString() } list.push(bid) localStorage.setItem('dispatch_service_bids', JSON.stringify(list)) return bid } } export async function fetchBidsForRequest (requestName) { try { return await frappeGET('Service Bid', { request: requestName }, [ 'name', 'technician', 'proposed_date', 'time_slot', 'estimated_duration', 'notes', 'status', 'creation', ]) } catch (_) { const list = JSON.parse(localStorage.getItem('dispatch_service_bids') || '[]') return list.filter(b => b.request === requestName) } } export async function fetchBidsForTech (techName) { try { return await frappeGET('Service Bid', { technician: techName }, [ 'name', 'request', 'proposed_date', 'time_slot', 'status', 'creation', ]) } catch (_) { const list = JSON.parse(localStorage.getItem('dispatch_service_bids') || '[]') return list.filter(b => b.technician === techName) } } export async function fetchOpenRequests () { try { return await frappeGET('Service Request', { status: ['in', ['New', 'Bidding']] }, [ 'name', 'customer_name', 'service_type', 'problem_type', 'description', 'address', 'lng', 'lat', 'urgency', 'preferred_date_1', 'time_slot_1', 'preferred_date_2', 'time_slot_2', 'preferred_date_3', 'time_slot_3', 'creation', 'budget_label', 'budget_min', 'budget_max', ]) } catch (_) { const list = JSON.parse(localStorage.getItem('dispatch_service_requests') || '[]') return list.filter(r => ['new', 'bidding'].includes(r.status)) } } export async function acceptBid (bidName, requestName, confirmedDate) { try { await frappePUT('Service Bid', bidName, { status: 'Accepted' }) await frappePUT('Service Request', requestName, { status: 'Confirmed', confirmed_date: confirmedDate }) } catch (_) {} } // ───────────────────────────────────────────────────────────────────────────── // EquipmentInstall (barcode scan on site) // ───────────────────────────────────────────────────────────────────────────── export async function createEquipmentInstall (data) { /** * data = { request, barcode, equipment_type, brand, model, notes, photo_base64 } */ try { return await frappePOST('Equipment Install', { ...data, installation_date: new Date().toISOString().split('T')[0], }) } catch (_) { const list = JSON.parse(localStorage.getItem('dispatch_equipment') || '[]') const item = { ref: 'EQ-' + Date.now().toString(36).toUpperCase().slice(-6), ...data, created: new Date().toISOString() } list.push(item) localStorage.setItem('dispatch_equipment', JSON.stringify(list)) return item } } export async function fetchEquipmentForRequest (requestName) { try { return await frappeGET('Equipment Install', { request: requestName }, [ 'name', 'barcode', 'equipment_type', 'brand', 'model', 'notes', 'installation_date', ]) } catch (_) { const list = JSON.parse(localStorage.getItem('dispatch_equipment') || '[]') return list.filter(e => e.request === requestName) } }