gigafibre-fsm/src/stores/dispatch.js
louispaulb 859f043bb2 Refactor: extract autoDispatch, serializeAssistants, store assign logic
- Extract useAutoDispatch.js (autoDistribute + optimizeRoute)
- Add serializeAssistants() to useHelpers — removes 6 duplications
- Move smartAssign/fullUnassign into Pinia store
- Add drag-and-drop on dispatch criteria modal
- DispatchV2Page.vue: 1463 → 1385 lines

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

274 lines
11 KiB
JavaScript

// ── Dispatch store ───────────────────────────────────────────────────────────
// Shared state for both MobilePage and DispatchPage.
// All ERPNext calls go through api/dispatch.js — not here.
// ─────────────────────────────────────────────────────────────────────────────
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { fetchTechnicians, fetchJobs, updateJob, createJob as apiCreateJob, fetchTags } from 'src/api/dispatch'
import { TECH_COLORS } from 'src/config/erpnext'
import { serializeAssistants } from 'src/composables/useHelpers'
export const useDispatchStore = defineStore('dispatch', () => {
const technicians = ref([])
const jobs = ref([])
const allTags = ref([]) // { name, label, color, category }
const loading = ref(false)
const erpStatus = ref('pending') // 'pending' | 'ok' | 'error' | 'session_expired'
// ── Data transformers ────────────────────────────────────────────────────
function _mapJob (j) {
return {
id: j.ticket_id || j.name,
name: j.name, // ERPNext docname (used for PUT calls)
subject: j.subject || 'Job sans titre',
address: j.address || 'Adresse inconnue',
coords: [j.longitude || 0, j.latitude || 0],
priority: j.priority || 'low',
duration: j.duration_h || 1,
status: j.status || 'open',
assignedTech: j.assigned_tech || null,
routeOrder: j.route_order || 0,
legDist: j.leg_distance || null,
legDur: j.leg_duration || null,
scheduledDate: j.scheduled_date || null,
endDate: j.end_date || null,
startTime: j.start_time || null,
assistants: (j.assistants || []).map(a => ({ techId: a.tech_id, techName: a.tech_name, duration: a.duration_h || 0, note: a.note || '', pinned: !!a.pinned })),
tags: (j.tags || []).map(t => t.tag),
}
}
function _mapTech (t, idx) {
return {
id: t.technician_id || t.name,
name: t.name, // ERPNext docname
fullName: t.full_name || t.name,
status: t.status || '',
user: t.user || null,
colorIdx: idx % TECH_COLORS.length,
coords: [t.longitude || -73.5673, t.latitude || 45.5017],
queue: [], // filled in loadAll()
tags: (t.tags || []).map(tg => tg.tag),
}
}
// ── Loaders ──────────────────────────────────────────────────────────────
async function loadAll () {
loading.value = true
erpStatus.value = 'pending'
try {
const [rawTechs, rawJobs, rawTags] = await Promise.all([
fetchTechnicians(),
fetchJobs(),
fetchTags(),
])
allTags.value = rawTags
technicians.value = rawTechs.map(_mapTech)
jobs.value = rawJobs.map(_mapJob)
// Build each tech's ordered queue (primary + assistant jobs)
technicians.value.forEach(tech => {
tech.queue = jobs.value
.filter(j => j.assignedTech === tech.id)
.sort((a, b) => a.routeOrder - b.routeOrder)
tech.assistJobs = jobs.value
.filter(j => j.assistants.some(a => a.techId === tech.id))
})
erpStatus.value = 'ok'
} catch (e) {
erpStatus.value = e.message?.includes('session') ? 'session_expired' : 'error'
console.error('loadAll error:', e)
} finally {
loading.value = false
}
}
// Load jobs assigned to one tech — used by MobilePage
async function loadJobsForTech (techId) {
loading.value = true
try {
const raw = await fetchJobs([['assigned_tech', '=', techId]])
jobs.value = raw.map(_mapJob)
} finally {
loading.value = false
}
}
// ── Mutations (also syncs to ERPNext) ────────────────────────────────────
async function setJobStatus (jobId, status) {
const job = jobs.value.find(j => j.id === jobId)
if (!job) return
job.status = status
await updateJob(job.id, { status })
}
async function assignJobToTech (jobId, techId, routeOrder, scheduledDate) {
const job = jobs.value.find(j => j.id === jobId)
if (!job) return
// Remove from old tech queue
technicians.value.forEach(t => {
t.queue = t.queue.filter(q => q.id !== jobId)
})
// Add to new tech queue
const tech = technicians.value.find(t => t.id === techId)
if (tech) {
job.assignedTech = techId
job.routeOrder = routeOrder
job.status = 'assigned'
if (scheduledDate !== undefined) job.scheduledDate = scheduledDate
tech.queue.splice(routeOrder, 0, job)
// Re-number route_order
tech.queue.forEach((q, i) => { q.routeOrder = i })
}
const payload = {
assigned_tech: techId,
route_order: routeOrder,
status: 'assigned',
}
if (scheduledDate !== undefined) payload.scheduled_date = scheduledDate || ''
await updateJob(job.id, payload)
}
async function unassignJob (jobId) {
const job = jobs.value.find(j => j.id === jobId)
if (!job) return
technicians.value.forEach(t => { t.queue = t.queue.filter(q => q.id !== jobId) })
job.assignedTech = null
job.status = 'open'
try { await updateJob(job.name || job.id, { assigned_tech: null, status: 'open' }) } catch (_) {}
}
async function createJob (fields) {
// fields: { subject, address, duration_h, priority, assigned_tech?, scheduled_date?, start_time? }
const localId = 'WO-' + Date.now().toString(36).toUpperCase()
const job = _mapJob({
ticket_id: localId, name: localId,
subject: fields.subject || 'Nouveau travail',
address: fields.address || '',
longitude: fields.longitude || 0,
latitude: fields.latitude || 0,
duration_h: parseFloat(fields.duration_h) || 1,
priority: fields.priority || 'low',
status: fields.assigned_tech ? 'assigned' : 'open',
assigned_tech: fields.assigned_tech || null,
scheduled_date: fields.scheduled_date || null,
start_time: fields.start_time || null,
route_order: 0,
})
jobs.value.push(job)
if (fields.assigned_tech) {
const tech = technicians.value.find(t => t.id === fields.assigned_tech)
if (tech) { job.routeOrder = tech.queue.length; tech.queue.push(job) }
}
try {
const created = await apiCreateJob({
subject: job.subject,
address: job.address,
longitude: job.coords?.[0] || '',
latitude: job.coords?.[1] || '',
duration_h: job.duration,
priority: job.priority,
status: job.status,
assigned_tech: job.assignedTech || '',
scheduled_date: job.scheduledDate || '',
start_time: job.startTime || '',
})
if (created?.name) { job.id = created.name; job.name = created.name }
} catch (_) {}
return job
}
async function setJobSchedule (jobId, scheduledDate, startTime) {
const job = jobs.value.find(j => j.id === jobId)
if (!job) return
job.scheduledDate = scheduledDate || null
job.startTime = startTime !== undefined ? startTime : job.startTime
const payload = { scheduled_date: job.scheduledDate || '' }
if (startTime !== undefined) payload.start_time = startTime || ''
try { await updateJob(job.name || job.id, payload) } catch (_) {}
}
async function updateJobCoords (jobId, lng, lat) {
const job = jobs.value.find(j => j.id === jobId)
if (!job) return
job.coords = [lng, lat]
try { await updateJob(job.name || job.id, { longitude: lng, latitude: lat }) } catch (_) {}
}
async function addAssistant (jobId, techId) {
const job = jobs.value.find(j => j.id === jobId)
if (!job) return
if (job.assignedTech === techId) return // already lead
if (job.assistants.some(a => a.techId === techId)) return // already assistant
const tech = technicians.value.find(t => t.id === techId)
const entry = { techId, techName: tech?.fullName || techId, duration: job.duration, note: '', pinned: false }
job.assistants = [...job.assistants, entry]
if (tech) tech.assistJobs = jobs.value.filter(j => j.assistants.some(a => a.techId === tech.id))
try {
await updateJob(job.name || job.id, {
assistants: serializeAssistants(job.assistants),
})
} catch (_) {}
}
async function removeAssistant (jobId, techId) {
const job = jobs.value.find(j => j.id === jobId)
if (!job) return
job.assistants = job.assistants.filter(a => a.techId !== techId)
const tech = technicians.value.find(t => t.id === techId)
if (tech) tech.assistJobs = jobs.value.filter(j => j.assistants.some(a => a.techId === tech.id))
try {
await updateJob(job.name || job.id, {
assistants: serializeAssistants(job.assistants),
})
} catch (_) {}
}
async function reorderTechQueue (techId, fromIdx, toIdx) {
const tech = technicians.value.find(t => t.id === techId)
if (!tech) return
const [moved] = tech.queue.splice(fromIdx, 1)
tech.queue.splice(toIdx, 0, moved)
tech.queue.forEach((q, i) => { q.routeOrder = i })
// Sync all reordered jobs
await Promise.all(
tech.queue.map((q, i) => updateJob(q.id, { route_order: i })),
)
}
// ── Smart assign (removes circular assistant deps) ──────────────────────
function smartAssign (jobId, newTechId, dateStr) {
const job = jobs.value.find(j => j.id === jobId)
if (!job) return
if (job.assistants.some(a => a.techId === newTechId)) {
job.assistants = job.assistants.filter(a => a.techId !== newTechId)
updateJob(job.name || job.id, { assistants: serializeAssistants(job.assistants) }).catch(() => {})
}
assignJobToTech(jobId, newTechId, technicians.value.find(t => t.id === newTechId)?.queue.length || 0, dateStr)
_rebuildAssistJobs()
}
// ── Full unassign (clears assistants + unassigns) ──────────────────────
function fullUnassign (jobId) {
const job = jobs.value.find(j => j.id === jobId)
if (!job) return
if (job.assistants.length) { job.assistants = []; updateJob(job.name || job.id, { assistants: [] }).catch(() => {}) }
unassignJob(jobId)
_rebuildAssistJobs()
}
// Rebuild all tech.assistJobs references
function _rebuildAssistJobs () {
technicians.value.forEach(t => { t.assistJobs = jobs.value.filter(j => j.assistants.some(a => a.techId === t.id)) })
}
return {
technicians, jobs, allTags, loading, erpStatus,
loadAll, loadJobsForTech,
setJobStatus, assignJobToTech, unassignJob, createJob, reorderTechQueue, updateJobCoords, setJobSchedule, addAssistant, removeAssistant,
smartAssign, fullUnassign,
}
})