From 5e57b72a8f04d02d8824ceb958d97a3afb1f00b8 Mon Sep 17 00:00:00 2001 From: louispaulb Date: Sat, 6 Jun 2026 12:37:45 -0400 Subject: [PATCH] =?UTF-8?q?feat(dispatch):=20couleur=20ticket=20=3D=20coul?= =?UTF-8?q?eur=20skill=20+=20fil=20complet=20du=20ticket=20+=20tri=20pool?= =?UTF-8?q?=20(date/ville/priorit=C3=A9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Couleurs liées aux skills (éditable/cohérent) : hub deptToSkill() déduit une compétence du type legacy → /roster/unassigned-jobs renvoie required_skill ; PlanificationPage colore la carte par getTagColor(required_skill) (même couleur que le chip skill) ; bordure 5px - Fil complet du ticket : hub /dispatch/legacy-sync/ticket-thread (ticket_msg + auteur staff, HTML nettoyé) ; api legacyTicketThread ; RightPanel bouton « 💬 Voir le fil / commentaires » (chargé au clic, messages+auteurs+dates) - Order-by du pool dispatch : useBottomPanel.bottomSort (date|city|priority) + dropdown ⇅ dans BottomPanel (ville = 2e segment adresse / token sujet avant |) Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/ops/src/api/roster.js | 2 + apps/ops/src/composables/useBottomPanel.js | 52 +++++++++++-------- .../dispatch/components/BottomPanel.vue | 10 +++- .../dispatch/components/RightPanel.vue | 31 ++++++++++- apps/ops/src/pages/DispatchPage.vue | 3 +- apps/ops/src/pages/PlanificationPage.vue | 8 +-- .../targo-hub/lib/legacy-dispatch-sync.js | 18 +++++++ services/targo-hub/lib/roster.js | 16 +++++- 8 files changed, 110 insertions(+), 30 deletions(-) diff --git a/apps/ops/src/api/roster.js b/apps/ops/src/api/roster.js index 5939d6b..7fc4477 100644 --- a/apps/ops/src/api/roster.js +++ b/apps/ops/src/api/roster.js @@ -93,5 +93,7 @@ export const redistributePlan = (plan) => jpost('/roster/skill-impact/redistribu export const unassignedJobs = () => jget('/roster/unassigned-jobs') // Assigner un job à un tech (date = case déposée) export const assignJob = (job, tech, date) => jpost('/roster/assign-job', { job, tech, date }) +// Fil complet d'un ticket legacy (description + commentaires/réponses des collaborateurs) — read-only +export const legacyTicketThread = (id) => jget('/dispatch/legacy-sync/ticket-thread?id=' + encodeURIComponent(id)) // Aviser le client d'un report : désassigne + SMS lien /book — { job, phone?, message? } export const notifyReschedule = (body) => jpost('/roster/job/notify-reschedule', body) diff --git a/apps/ops/src/composables/useBottomPanel.js b/apps/ops/src/composables/useBottomPanel.js index 3eec073..7a91313 100644 --- a/apps/ops/src/composables/useBottomPanel.js +++ b/apps/ops/src/composables/useBottomPanel.js @@ -7,32 +7,42 @@ export function useBottomPanel (store, todayStr, unscheduledJobs, { pushUndo, sm const bottomPanelH = ref(parseInt(localStorage.getItem('sbv2-bottomH')) || 220) watch(bottomPanelOpen, v => localStorage.setItem('sbv2-bottomPanel', v ? 'true' : 'false')) - // ── Grouped by date ────────────────────────────────────────────────────────── + // ── Tri / regroupement (date · ville · priorité) ───────────────────────────── + const bottomSort = ref(localStorage.getItem('sbv2-bottomSort') || 'date') + watch(bottomSort, v => localStorage.setItem('sbv2-bottomSort', v)) + const PRIO = { urgent: 0, high: 1, medium: 2, low: 3 } + // Ville : 2e segment de l'adresse libre, sinon 1er token du sujet avant « | » (tickets legacy), sinon vide. + function cityOf (job) { + const a = String(job.address || '') + const parts = a.split(',').map(s => s.trim()).filter(Boolean) + if (parts.length >= 2) return parts[1] + const subj = String(job.subject || ''); if (subj.includes('|')) return subj.split('|')[0].trim() + return parts[0] || '' + } const unassignedGrouped = computed(() => { const today = todayStr + const sort = bottomSort.value const jobs = unscheduledJobs.value.slice() + const byDate = (a, b) => { // ordre secondaire : date (aujourd'hui d'abord) + const da = a.scheduledDate || '9999-99-99'; const db = b.scheduledDate || '9999-99-99' + const at = da === today ? 0 : 1; const bt = db === today ? 0 : 1 + return at !== bt ? at - bt : da.localeCompare(db) + } jobs.sort((a, b) => { - const da = a.scheduledDate || '9999-99-99' - const db = b.scheduledDate || '9999-99-99' - const aToday = da === today ? 0 : 1 - const bToday = db === today ? 0 : 1 - if (aToday !== bToday) return aToday - bToday - if (da !== db) return da.localeCompare(db) - const prio = { high: 0, medium: 1, low: 2 } - return (prio[a.priority] ?? 2) - (prio[b.priority] ?? 2) + if (sort === 'priority') return (PRIO[a.priority] ?? 3) - (PRIO[b.priority] ?? 3) || byDate(a, b) + if (sort === 'city') return cityOf(a).localeCompare(cityOf(b)) || byDate(a, b) + return byDate(a, b) || ((PRIO[a.priority] ?? 3) - (PRIO[b.priority] ?? 3)) }) - const groups = [] - let currentDate = null + const keyOf = job => sort === 'priority' ? (job.priority || 'low') : sort === 'city' ? (cityOf(job) || 'Sans ville') : (job.scheduledDate || null) + const labelOf = key => { + if (sort === 'priority') return ({ urgent: '🔴 Urgent', high: '🟠 Élevée', medium: '🔵 Moyenne', low: '⚪ Basse' })[key] || key + if (sort === 'city') return key + return key === today ? "Aujourd'hui" : (key ? fmtDate(new Date(key + 'T00:00:00')) : 'Sans date') + } + const groups = []; let cur = Symbol('init') jobs.forEach(job => { - const d = job.scheduledDate || null - if (d !== currentDate) { - currentDate = d - let label = d === today ? "Aujourd'hui" : d ? d : 'Sans date' - if (d && d !== today) { - label = fmtDate(new Date(d + 'T00:00:00')) - } - groups.push({ date: d, label, jobs: [] }) - } + const k = keyOf(job) + if (k !== cur) { cur = k; groups.push({ key: String(k), date: sort === 'date' ? k : null, label: labelOf(k), jobs: [] }) } groups.at(-1).jobs.push(job) }) return groups @@ -111,7 +121,7 @@ export function useBottomPanel (store, todayStr, unscheduledJobs, { pushUndo, sm } return { - bottomPanelOpen, bottomPanelH, unassignedGrouped, startBottomResize, + bottomPanelOpen, bottomPanelH, unassignedGrouped, bottomSort, startBottomResize, bottomSelected, toggleBottomSelect, selectAllBottom, clearBottomSelect, batchAssignBottom, dispatchCriteria, dispatchCriteriaModal, saveDispatchCriteria, moveCriterion, btColWidths, btColW, startColResize, diff --git a/apps/ops/src/modules/dispatch/components/BottomPanel.vue b/apps/ops/src/modules/dispatch/components/BottomPanel.vue index be39412..2088b32 100644 --- a/apps/ops/src/modules/dispatch/components/BottomPanel.vue +++ b/apps/ops/src/modules/dispatch/components/BottomPanel.vue @@ -9,10 +9,11 @@ const props = defineProps({ unscheduledCount: Number, selected: Object, // Set dropActive: Boolean, + sort: { type: String, default: 'date' }, // tri du pool : date | city | priority }) const emit = defineEmits([ - 'update:open', 'update:height', 'resize-start', + 'update:open', 'update:height', 'resize-start', 'update:sort', 'toggle-select', 'select-all', 'clear-select', 'batch-assign', 'auto-distribute', 'open-criteria', 'row-click', 'row-dblclick', 'row-dragstart', @@ -106,6 +107,11 @@ function btLassoEnd () { +