OSS-BSS-Field-Dispatch/src/api/service-request.js
louispaulb 5e6f20d871 Initial commit — dispatch app baseline before Quasar migration
Current state: custom CSS + vanilla Vue components
Architecture: modular with composables, provide/inject pattern
Ready for progressive migration to Quasar native components

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 13:35:49 -04:00

274 lines
11 KiB
JavaScript

/**
* 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)
}
}