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 {