From dadda9fd4918ec716d1cfbb4b54832aa09fc1e4e Mon Sep 17 00:00:00 2001 From: louispaulb Date: Sat, 6 Jun 2026 10:05:45 -0400 Subject: [PATCH] =?UTF-8?q?feat(dispatch):=20coloriage=20cartes=20par=20ty?= =?UTF-8?q?pe=20legacy=20+=20n=C2=B0=20ticket=20dans=20le=20d=C3=A9tail?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - pont : stocke legacy_dept (dept osTicket) sur le Dispatch Job + backfill des 70 existants - useHelpers.jobColor/legacyDeptColor : palette comme legacy (Installation vert, Réparation or, Télé rose, Téléphonie vert pâle, Désinstallation rouge foncé) ; tech en pause = rouge vif prioritaire - store _mapJob : expose legacyDept + legacyTicketId - RightPanel : champ « Ticket legacy » (#id · dept) — le client est déjà un lien cliquable vers la fiche - doc mise à jour Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/ops/src/composables/useHelpers.js | 18 +++++++++++++++++- .../modules/dispatch/components/RightPanel.vue | 4 ++++ apps/ops/src/stores/dispatch.js | 2 ++ docs/features/legacy-dispatch-bridge.md | 10 +++++++++- services/targo-hub/lib/legacy-dispatch-sync.js | 15 +++++++++------ 5 files changed, 41 insertions(+), 8 deletions(-) diff --git a/apps/ops/src/composables/useHelpers.js b/apps/ops/src/composables/useHelpers.js index 09f5816..24e7eae 100644 --- a/apps/ops/src/composables/useHelpers.js +++ b/apps/ops/src/composables/useHelpers.js @@ -81,12 +81,28 @@ export function jobSvcCode (job) { return 'WO' } +// Couleur par TYPE de ticket, calquée sur le board legacy (osTicket). Téléphonie = vert +// PLUS PÂLE que l'installation. Ordre des tests important (téléph avant télé ; télé avant install). +export function legacyDeptColor (dept) { + if (!dept) return null + const d = String(dept).toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, '') + if (d.includes('teleph')) return '#8fce93' // Téléphonie → vert pâle + if (d.includes('tele') || d.includes('televis')) return '#ec5fb0' // Télé (install/réparation) → rose + if (d.includes('desinstall') || d.includes('retrait')) return '#c0392b' // Désinstallation → rouge foncé + if (d.includes('repar')) return '#f1c84b' // Réparation (Fibre) → jaune/or + if (d.includes('installation') || d.includes('monteur') || d.includes('fusionneur')) return '#46992f' // Installation (Fibre) → vert + return null +} + export function jobColor (job, techColors, store) { - // Tech en pause/absent (statut interne 'off') → ses jobs en ROUGE (à réassigner) + // Tech en pause/absent (statut interne 'off') → ses jobs en ROUGE vif (à réassigner) — priorité opérationnelle if (job.assignedTech && store) { const at = store.technicians.find(x => x.id === job.assignedTech) if (at && at.status === 'off') return '#e53935' } + // Type legacy (pont osTicket) → couleur « comme legacy » + const ld = legacyDeptColor(job.legacyDept) + if (ld) return ld if (SVC_COLORS[job.service_type]) return SVC_COLORS[job.service_type] const s = (job.subject||'').toLowerCase() if (s.includes('internet')) return '#3b82f6' diff --git a/apps/ops/src/modules/dispatch/components/RightPanel.vue b/apps/ops/src/modules/dispatch/components/RightPanel.vue index c9ffdbd..6ec5141 100644 --- a/apps/ops/src/modules/dispatch/components/RightPanel.vue +++ b/apps/ops/src/modules/dispatch/components/RightPanel.vue @@ -79,6 +79,10 @@ const onDeleteTag = inject('onDeleteTag') @change="emit('set-end-date', panel.data.job, $event.target.value)" style="margin-top:2px" />
Statut{{ panel.data?.job?.status }}
+
+ Ticket legacy + #{{ panel.data.job.legacyTicketId }} · {{ panel.data.job.legacyDept }} +
Note{{ panel.data.job.note }}
Tags diff --git a/apps/ops/src/stores/dispatch.js b/apps/ops/src/stores/dispatch.js index d61b15e..1edd46c 100644 --- a/apps/ops/src/stores/dispatch.js +++ b/apps/ops/src/stores/dispatch.js @@ -56,6 +56,8 @@ export const useDispatchStore = defineStore('dispatch', () => { sourceIssue: j.source_issue || null, dependsOn: j.depends_on || null, jobType: j.job_type || null, + legacyDept: j.legacy_dept || null, // département osTicket legacy → coloriage par type + legacyTicketId: j.legacy_ticket_id || null, // n° ticket legacy (affiché dans le panneau détail) parentJob: j.parent_job || null, stepOrder: j.step_order || 0, onOpenWebhook: j.on_open_webhook || null, diff --git a/docs/features/legacy-dispatch-bridge.md b/docs/features/legacy-dispatch-bridge.md index 80dc812..c66ef25 100644 --- a/docs/features/legacy-dispatch-bridge.md +++ b/docs/features/legacy-dispatch-bridge.md @@ -22,7 +22,8 @@ Override possible via `LEGACY_TARGO_STAFF_ID`. ## Mapping ticket legacy → Dispatch Job | Dispatch Job | Source legacy | Notes | |---|---|---| -| `legacy_ticket_id` | `ticket.id` | **clé d'idempotence** (lookup avant create) | +| `legacy_ticket_id` | `ticket.id` | **clé d'idempotence** (lookup avant create) ; affiché dans le panneau détail | +| `legacy_dept` | `ticket_dept.name` | département granulaire → **coloriage des cartes « comme legacy »** | | `ticket_id` | `'LEG-' + ticket.id` | nom lisible du DJ | | `subject` | `ticket.subject` | + adresse ajoutée si pas de Service Location | | `customer` | `Customer` où `legacy_account_id = ticket.account_id` | 61/70 matchés ; sinon laissé vide | @@ -40,6 +41,13 @@ Override possible via `LEGACY_TARGO_STAFF_ID`. seulement tant qu'il est encore `open` + non assigné. - **SÉQUENTIEL** (frappe_pg ne supporte pas la concurrence) — pas de `Promise.all`. +## Coloriage des cartes (comme legacy) +`jobColor()` (`apps/ops/src/composables/useHelpers.js` → `legacyDeptColor`) colore par `legacyDept` : +Installation(Fibre)/Monteur/Fusionneur → **vert** `#46992f` · Réparation(Fibre) → **or** `#f1c84b` · +Télé (install/réparation) → **rose** `#ec5fb0` · Téléphonie → **vert pâle** `#8fce93` · +Désinstallation → **rouge foncé** `#c0392b`. (Tech en pause → rouge vif, priorité.) Le store +(`_mapJob`) expose `legacyDept`/`legacyTicketId` ; le pool « non-assigné » est déjà trié par `scheduledDate`. + ## Endpoints - `GET /dispatch/legacy-sync/preview` — **dry-run, 0 écriture** : ce qui serait créé + matching client/SL + non-matchés. - `POST /dispatch/legacy-sync/run` — exécute la synchro (création/maj). Retourne `{tickets, created, updated, skipped, errors, unmatched_customer}`. diff --git a/services/targo-hub/lib/legacy-dispatch-sync.js b/services/targo-hub/lib/legacy-dispatch-sync.js index 52d0194..0e4c3b8 100644 --- a/services/targo-hub/lib/legacy-dispatch-sync.js +++ b/services/targo-hub/lib/legacy-dispatch-sync.js @@ -124,6 +124,7 @@ async function buildJob (t) { status: 'open', order_source: 'Manual', legacy_ticket_id: String(t.id), + legacy_dept: t.dept || '', // département legacy granulaire → coloriage « comme legacy » (Installation Fibre / Réparation Fibre / Télé / Téléphonie…) } const sd = tzDate(t.due_date); if (sd) payload.scheduled_date = sd const st = startTime(t.due_time); if (st) payload.start_time = st @@ -137,7 +138,7 @@ async function buildJob (t) { } async function findExisting (legacyId) { - const r = await erp.list('Dispatch Job', { filters: [['legacy_ticket_id', '=', legacyId]], fields: ['name', 'status', 'assigned_tech', 'scheduled_date'], limit: 1 }) + const r = await erp.list('Dispatch Job', { filters: [['legacy_ticket_id', '=', legacyId]], fields: ['name', 'status', 'assigned_tech', 'scheduled_date', 'legacy_dept'], limit: 1 }) return (r && r[0]) || null } @@ -153,11 +154,13 @@ async function sync ({ dryRun = false } = {}) { if (!b.matched.customer) unmatched++ const ex = await findExisting(b.legacy_id) if (ex) { - // Déjà importé : on ne touche QUE s'il est encore au pool (open + non assigné) et que la date a changé. - if (!dryRun && ex.status === 'open' && !ex.assigned_tech && b.payload.scheduled_date && b.payload.scheduled_date !== ex.scheduled_date) { - await erp.update('Dispatch Job', ex.name, { scheduled_date: b.payload.scheduled_date }) - updated++; details.push({ legacy_id: b.legacy_id, action: 'reschedule', job: ex.name, date: b.payload.scheduled_date }) - } else { skipped++ } + // Déjà importé. Backfill du département (métadonnée couleur, sans risque) + maj date SEULEMENT + // s'il est encore au pool (open + non assigné) → on ne clobbe jamais le travail du répartiteur. + const patch = {} + if (!ex.legacy_dept && b.payload.legacy_dept) patch.legacy_dept = b.payload.legacy_dept + if (ex.status === 'open' && !ex.assigned_tech && b.payload.scheduled_date && b.payload.scheduled_date !== ex.scheduled_date) patch.scheduled_date = b.payload.scheduled_date + if (!dryRun && Object.keys(patch).length) { await erp.update('Dispatch Job', ex.name, patch); updated++; details.push({ legacy_id: b.legacy_id, action: 'update', job: ex.name, patch }) } + else skipped++ } else if (dryRun) { created++; details.push({ legacy_id: b.legacy_id, action: 'would-create', subject: b.payload.subject, job_type: b.payload.job_type, dept: b.dept, scheduled_date: b.payload.scheduled_date || null, start_time: b.payload.start_time || null, customer: b.matched.customer_name, customer_matched: b.matched.customer, sl_matched: b.matched.service_location, addr: b.addr }) } else {