import { ref } from 'vue' import { updateTech } from 'src/api/dispatch' import { toErpStatus } from 'src/composables/useHelpers' const ABSENCE_REASONS = [ { value: 'vacation', label: 'Vacances', icon: '🏖️' }, { value: 'sick', label: 'Maladie', icon: '🤒' }, { value: 'personal', label: 'Personnel', icon: '🏠' }, { value: 'training', label: 'Formation', icon: '📚' }, { value: 'injury', label: 'Blessure', icon: '🩹' }, { value: 'other', label: 'Autre', icon: '📋' }, ] export { ABSENCE_REASONS } export function useTechManagement (store, invalidateRoutes) { const editingTech = ref(null) const newTechName = ref('') const newTechPhone = ref('') const newTechDevice = ref('') const addingTech = ref(false) // Absence modal state const absenceModalOpen = ref(false) const absenceModalTech = ref(null) const absenceForm = ref({ reason: 'vacation', from: '', until: '', jobAction: 'unassign' }) const absenceProcessing = ref(false) async function saveTechField (tech, field, value) { const trimmed = typeof value === 'string' ? value.trim() : value if (field === 'full_name') { if (!trimmed || trimmed === tech.fullName) return tech.fullName = trimmed } else if (field === 'status') { tech.status = trimmed tech.active = trimmed !== 'inactive' && trimmed !== 'off' } else if (field === 'phone') { if (trimmed === tech.phone) return tech.phone = trimmed } const saveValue = field === 'status' ? toErpStatus(trimmed) : trimmed try { await updateTech(tech.name || tech.id, { [field]: saveValue }) } catch (_e) { /* save failed */ } } async function addTech (extraFields = {}) { const name = newTechName.value.trim() if (!name || addingTech.value) return addingTech.value = true try { const tech = await store.createTechnician({ full_name: name, phone: newTechPhone.value.trim() || '', traccar_device_id: newTechDevice.value || '', ...extraFields, }) newTechName.value = '' newTechPhone.value = '' newTechDevice.value = '' if (tech.traccarDeviceId) await store.pollGps() } catch (e) { const msg = e?.message || String(e) alert('Erreur création technicien:\n' + msg.replace(/<[^>]+>/g, '')) } finally { addingTech.value = false } } // ── Absence flow ── function openAbsenceModal (tech) { absenceModalTech.value = tech const today = new Date().toISOString().slice(0, 10) absenceForm.value = { reason: tech.absenceReason || 'vacation', from: tech.absenceFrom || today, until: tech.absenceUntil || '', jobAction: 'unassign', // 'unassign' | 'reassign' } absenceModalOpen.value = true } async function confirmAbsence () { const tech = absenceModalTech.value if (!tech) return absenceProcessing.value = true try { const { reason, from, until, jobAction } = absenceForm.value // 1. Handle assigned jobs const linkedJobs = store.jobs.filter(j => j.assignedTech === tech.id) if (linkedJobs.length > 0) { if (jobAction === 'unassign') { for (const job of linkedJobs) { await store.unassignJob(job.id) } } // 'reassign' is handled by the user manually after — jobs stay visible in unassigned pool } // 2. Update tech status + absence fields tech.status = 'off' tech.active = false tech.absenceReason = reason tech.absenceFrom = from || null tech.absenceUntil = until || null tech.absenceStartTime = null tech.absenceEndTime = null await updateTech(tech.name || tech.id, { status: toErpStatus('off'), absence_reason: reason, absence_from: from || '', absence_until: until || '', absence_start_time: '', absence_end_time: '', }) absenceModalOpen.value = false } catch (e) { const msg = e?.message || String(e) alert('Erreur: ' + msg.replace(/<[^>]+>/g, '')) } absenceProcessing.value = false } async function endAbsence (tech) { tech.status = 'available' tech.active = true tech.absenceReason = '' tech.absenceFrom = null tech.absenceUntil = null tech.absenceStartTime = null tech.absenceEndTime = null try { await updateTech(tech.name || tech.id, { status: toErpStatus('available'), absence_reason: '', absence_from: '', absence_until: '', absence_start_time: '', absence_end_time: '', }) } catch (_e) { tech.status = 'off' tech.active = false } } // Permanent deactivation (non-dispatchable resource like support staff) async function deactivateTech (tech) { if (!confirm(`Désactiver ${tech.fullName} du dispatch ?\n\nLa ressource ne sera plus dispatchable mais reste dans le système.`)) return try { const linkedJobs = store.jobs.filter(j => j.assignedTech === tech.id) for (const job of linkedJobs) await store.unassignJob(job.id) tech.status = 'inactive' tech.active = false tech.absenceReason = '' tech.absenceFrom = null tech.absenceUntil = null tech.absenceStartTime = null tech.absenceEndTime = null await updateTech(tech.name || tech.id, { status: toErpStatus('inactive'), absence_reason: '', absence_from: '', absence_until: '', absence_start_time: '', absence_end_time: '' }) } catch (e) { alert('Erreur: ' + (e?.message || String(e)).replace(/<[^>]+>/g, '')) } } async function reactivateTech (tech) { tech.status = 'available' tech.active = true tech.absenceReason = '' tech.absenceFrom = null tech.absenceUntil = null tech.absenceStartTime = null tech.absenceEndTime = null try { await updateTech(tech.name || tech.id, { status: toErpStatus('available'), absence_reason: '', absence_from: '', absence_until: '', absence_start_time: '', absence_end_time: '' }) } catch (_e) { tech.status = 'inactive' tech.active = false } } async function removeTech (tech) { if (!confirm(`⚠ SUPPRIMER DÉFINITIVEMENT ${tech.fullName} ?\n\nCette action est irréversible.`)) return try { const linkedJobs = store.jobs.filter(j => j.assignedTech === tech.id) for (const job of linkedJobs) await store.unassignJob(job.id) await store.deleteTechnician(tech.id) } catch (e) { alert('Erreur: ' + (e?.message || String(e)).replace(/<[^>]+>/g, '')) } } async function saveWeeklySchedule (tech, schedule) { tech.weeklySchedule = { ...schedule } try { await updateTech(tech.name || tech.id, { weekly_schedule: JSON.stringify(schedule) }) } catch (e) { alert('Erreur: ' + (e?.message || String(e)).replace(/<[^>]+>/g, '')) } } async function saveTraccarLink (tech, deviceId) { tech.traccarDeviceId = deviceId || null tech.gpsCoords = null tech.gpsOnline = false await updateTech(tech.name || tech.id, { traccar_device_id: deviceId || '' }) await store.pollGps() invalidateRoutes() } // Persist a tech's "home base" coordinates (where they start the day from // when no live GPS is available). Coords are stored in ERPNext as // separate Float fields `latitude` / `longitude`; the dispatch store // reads them back as `tech.coords = [longitude, latitude]` (the // [lng, lat] order is what Mapbox/MapLibre + GeoJSON expect). // Route optimization is recomputed because changing the start point // shifts every tech's optimal job sequence for the day. async function saveTechHome (tech, longitude, latitude) { const lng = Number(longitude); const lat = Number(latitude) if (!Number.isFinite(lng) || !Number.isFinite(lat)) { throw new Error('Coordonnées invalides') } tech.coords = [lng, lat] await updateTech(tech.name || tech.id, { longitude: lng, latitude: lat }) invalidateRoutes() } return { editingTech, newTechName, newTechPhone, newTechDevice, addingTech, absenceModalOpen, absenceModalTech, absenceForm, absenceProcessing, saveTechField, addTech, openAbsenceModal, confirmAbsence, endAbsence, deactivateTech, reactivateTech, removeTech, saveTraccarLink, saveWeeklySchedule, saveTechHome, ABSENCE_REASONS, } }