gigafibre-fsm/apps/ops/src/composables/useTechManagement.js
louispaulb 7d7b4fdb06 feat: nested tasks, project wizard, n8n webhooks, inline task editing
Major dispatch/task system overhaul:
- Project templates with 3-step wizard (choose template → edit steps → publish)
- 4 built-in templates: phone service, fiber install, move, repair
- Nested task tree with recursive TaskNode component (parent_job hierarchy)
- n8n webhook integration (on_open_webhook, on_close_webhook per task)
- Inline task editing: status, priority, type, tech assignment, tags, delete
- Tech assignment + tags from ticket modal → jobs appear on dispatch timeline
- ERPNext custom fields: parent_job, on_open_webhook, on_close_webhook, step_order
- Refactored ClientDetailPage, ChatterPanel, DetailModal, dispatch store
- CSS consolidation, dead code cleanup, composable extraction
- Dashboard KPIs with dispatch integration

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-01 13:01:20 -04:00

75 lines
2.3 KiB
JavaScript

import { ref } from 'vue'
import { updateTech } from 'src/api/dispatch'
export function useTechManagement (store, invalidateRoutes) {
const editingTech = ref(null)
const newTechName = ref('')
const newTechPhone = ref('')
const newTechDevice = ref('')
const addingTech = 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
} else if (field === 'phone') {
if (trimmed === tech.phone) return
tech.phone = trimmed
}
try { await updateTech(tech.name || tech.id, { [field]: trimmed }) }
catch (_e) { /* save failed */ }
}
async function addTech () {
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 || '',
})
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 }
}
async function removeTech (tech) {
if (!confirm(`Supprimer ${tech.fullName} ?`)) 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) {
const msg = e?.message || String(e)
alert('Erreur suppression:\n' + msg.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()
}
return {
editingTech, newTechName, newTechPhone, newTechDevice, addingTech,
saveTechField, addTech, removeTech, saveTraccarLink,
}
}