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>
75 lines
2.3 KiB
JavaScript
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,
|
|
}
|
|
}
|