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>
274 lines
11 KiB
JavaScript
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)
|
|
}
|
|
}
|