From 9a507d35db0f63123403a34a6669aec52533693a Mon Sep 17 00:00:00 2001 From: louispaulb Date: Sat, 4 Jul 2026 08:00:46 -0400 Subject: [PATCH] =?UTF-8?q?feat(dashboard):=20bande=20d'occupation=20+=20K?= =?UTF-8?q?PI=20cliquables/tendance=20+=20garde-fou=20PPA=20;=20barres=20d?= =?UTF-8?q?e=20charge=20en-t=C3=AAte=20Planif?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - réutilisable (extrait de .pm-strip) ; heatColor déplacé dans useHelpers = source unique - Tableau de bord : bande « Charge à venir » (7 j glissants, réutilise /roster/capacity) ; clic jour → Planif ; légende + heures à répartir - KPI cliquables (→ page liée) + sous-ligne tendance/contexte (urgents, +N tickets 7 j, non assignés) - Garde-fou PPA : confirmation explicite avant prélèvement (cf. incident double-charge 2026-06) - Planif : barres verticales d'occupation dans l'en-tête desktop (hdr-vbar) + sélecteur de jour « Tournées » = OccupancyStrip (remplace les chips « lun 29/06 ») Vérifié en préview (strip, clics jour, dialog PPA annulé sans charge, nav KPI) + build vert + leak-check 0. Aucun changement hub (réutilise /roster/capacity). Co-Authored-By: Claude Opus 4.8 --- .../src/components/shared/OccupancyStrip.vue | 64 ++++++++++ apps/ops/src/composables/useHelpers.js | 10 ++ apps/ops/src/pages/DashboardPage.vue | 113 ++++++++++++++++-- apps/ops/src/pages/PlanificationPage.vue | 21 +++- 4 files changed, 193 insertions(+), 15 deletions(-) create mode 100644 apps/ops/src/components/shared/OccupancyStrip.vue diff --git a/apps/ops/src/components/shared/OccupancyStrip.vue b/apps/ops/src/components/shared/OccupancyStrip.vue new file mode 100644 index 0000000..63c5032 --- /dev/null +++ b/apps/ops/src/components/shared/OccupancyStrip.vue @@ -0,0 +1,64 @@ + + + + + diff --git a/apps/ops/src/composables/useHelpers.js b/apps/ops/src/composables/useHelpers.js index a11d3e6..37026ac 100644 --- a/apps/ops/src/composables/useHelpers.js +++ b/apps/ops/src/composables/useHelpers.js @@ -29,6 +29,16 @@ export function timeToH (t) { return h + m / 60 } +// Occupation → couleur « chaleur » (vert = libre → rouge = saturé). SOURCE UNIQUE : +// bande mobile Planif (.pm-strip), du tableau de bord, en-tête desktop Planif. +export function heatColor (ratio) { + const r = +ratio || 0 + if (r >= 1) return '#ef4444' // saturé / dépassé + if (r >= 0.75) return '#f59e0b' // chargé + if (r >= 0.45) return '#84cc16' // moyen + return '#22c55e' // libre +} + export function hToTime (h) { const totalMin = Math.round(h * 60) const hh = Math.floor(totalMin / 60) diff --git a/apps/ops/src/pages/DashboardPage.vue b/apps/ops/src/pages/DashboardPage.vue index 8ad5732..275a2ba 100644 --- a/apps/ops/src/pages/DashboardPage.vue +++ b/apps/ops/src/pages/DashboardPage.vue @@ -3,21 +3,40 @@ - +
-
+
-
+
{{ stat.value }}
{{ stat.label }}
+
{{ stat.sub }}
+
+ +
+
+
Charge à venir 7 jours
+ + +
+ +
Chargement de la charge…
+
+ libre + chargé + saturé + · {{ weekUnplaced }}h à répartir +
+
+
@@ -87,9 +106,9 @@ icon="credit_score" :loading="runningPPA" dense no-caps - @click="runPPA" + @click="confirmPPA" > - Prélève automatiquement les cartes enregistrées + Prélève les cartes enregistrées (confirmation requise)
@@ -171,23 +190,57 @@ + + diff --git a/apps/ops/src/pages/PlanificationPage.vue b/apps/ops/src/pages/PlanificationPage.vue index 4b775a1..f84d95f 100644 --- a/apps/ops/src/pages/PlanificationPage.vue +++ b/apps/ops/src/pages/PlanificationPage.vue @@ -406,6 +406,7 @@
{{ d.dow }}
{{ d.dnum }}
+ {{ estLoad(d.iso) }}/{{ visStat(d.iso).hours }}h {{ estLoad(d.iso) }} h estimées (jobs prévus) / {{ visStat(d.iso).hours }} h de shift
{{ skillFilter.length ? ('compétence(s) : ' + skillFilter.join(', ')) : 'ressources affichées' }} ({{ visibleTechs.length }} tech)
@@ -481,7 +482,7 @@
Journée : - + Positions GPS des techs (Traccar), rafraîchies ~25 s. Cliquer pour {{ showLivePos ? 'masquer' : 'afficher' }}. {{ dayRoutes.length }} tournée(s) · clic tech = zoom · clic arrêt = détail @@ -1794,7 +1795,7 @@ import * as roster from 'src/api/roster' import * as addressApi from 'src/api/address' // recherche d'adresse RQA (coords) pour le sélecteur d'emplacement import { useSSE, sendSmsViaHub } from 'src/composables/useSSE' import { MAPBOX_TOKEN } from 'src/config/erpnext' // routage routier réel (API Mapbox Matrix), déjà utilisé par le Dispatch -import { legacyDeptColor } from 'src/composables/useHelpers' // coloriage par type « comme legacy » (partagé avec le board Dispatch) +import { legacyDeptColor, heatColor } from 'src/composables/useHelpers' // coloriage par type « comme legacy » (partagé) + heatColor (source unique, aussi ) import { useUserPrefs } from 'src/composables/useUserPrefs' // préférences d'affichage par utilisateur (serveur) import { relTime } from 'src/composables/useFormatters' // temps relatif COHÉRENT (même formateur que la Boîte) import { messageIdentity, priorityMeta, PRIORITY_LEVELS } from 'src/composables/useConversationDisplay' // resolvers partagés + priorité (drapeau) réutilisée des conversations @@ -1804,6 +1805,7 @@ import TechSelect from 'src/components/shared/TechSelect.vue' import SkillSelect from 'src/components/shared/SkillSelect.vue' import TagEditor from 'src/components/shared/TagEditor.vue' // module de tags partagé (Dispatch) : condensé, création à la volée, couleurs import RouteMap from 'src/components/shared/RouteMap.vue' // carte de tournées réutilisable (revue dispatch auto + onglet Tournées) : OSRM réel, arrêts numérotés, fitTo +import OccupancyStrip from 'src/components/shared/OccupancyStrip.vue' // bande d'occupation réutilisable (sélecteur de jour Tournées + tableau de bord) import UnifiedCreateModal from 'src/components/shared/UnifiedCreateModal.vue' // création de job NATIVE OPS (work-order) — Dispatch déprécié, tout ici import SuggestSlotsDialog from 'src/modules/dispatch/components/SuggestSlotsDialog.vue' // « Trouver un créneau » → pré-remplit la création (tech+date+heure+adresse) import WeeklyScheduleEditor from 'src/components/shared/WeeklyScheduleEditor.vue' // génération de quarts hebdo par tech (modèles) — repris/amélioré depuis Dispatch @@ -1874,6 +1876,9 @@ const estLoadByDay = computed(() => { }) function estLoad (iso) { return estLoadByDay.value[iso] || 0 } function loadClass (iso) { const est = estLoad(iso), dispo = visStat(iso).hours || 0; if (!dispo) return est > 0 ? 'cap-full' : 'cap-none'; const r = est / dispo; return r > 1 ? 'cap-full' : (r > 0.85 ? 'cap-low' : 'cap-ok') } // vert/orange/rouge selon est/dispo +// Barre verticale d'occupation de l'en-tête desktop (miroir de la bande mobile) : ratio charge estimée / heures de quart. +function hdrRatio (iso) { const cap = visStat(iso).hours || 0, load = estLoad(iso) || 0; return cap > 0 ? load / cap : (load > 0 ? 1 : 0) } +function hdrFillH (iso) { return (estLoad(iso) || 0) > 0 ? Math.max(12, Math.min(100, Math.round(hdrRatio(iso) * 100))) : 0 } // Vue « jobs prévus en en-tête de jour » : jobs NON assignés ayant une date, groupés par jour, glissables sur une case // tech×jour (réutilise onJobDragStart/onCellDrop → assigne + replanifie à la date de la case). Suit le filtre compétence. const showDayJobs = ref(false) @@ -3981,7 +3986,7 @@ function hasShiftDay (techId, iso) { // ── Vue mobile « bande de jours + heat » (téléphone) : charge agrégée par jour + par tech, lecture rapide. ── const todayIso = computed(() => (nowET.value && nowET.value.iso) || new Date().toISOString().slice(0, 10)) -function heatColor (ratio) { const r = +ratio || 0; if (r >= 1) return '#ef4444'; if (r >= 0.75) return '#f59e0b'; if (r >= 0.45) return '#84cc16'; return '#22c55e' } +// heatColor → composables/useHelpers.js (source unique, partagé avec + tableau de bord) const _CAP_H = 8 // capacité indicative par tech-jour (h) pour normaliser les barres const mobileSelIso = ref(null) // Bande de jours mobile = fenêtre LONGUE (4 semaines) → carrousel horizontal CONTINU (sans flèches). @@ -4676,6 +4681,11 @@ const suggestRoutes = computed(() => { watch(() => suggestDlg.view, (v) => { if (v === 'map' && !suggestMapDay.value) suggestMapDay.value = suggestWindow.value[0] || '' }) // ── P3 — Onglet « Tournées » : tournées RÉELLES (jobs assignés, occupation) de la journée choisie, 1 couleur/tech, tracés OSRM (RouteMap). ── const routesDay = ref(null) +// Sélecteur de jour (Tournées) enrichi : bande d'occupation (barres de charge) au lieu de simples chips « lun 29/06 ». +const routeStripDays = computed(() => dayList.value.slice(0, 7).map(d => { + const load = estLoad(d.iso) || 0; const cap = visStat(d.iso).hours || 0 + return { iso: d.iso, dow: d.dow, dnum: d.dnum, weekend: d.weekend, holiday: isHoliday(d.iso), h: load, cap, ratio: hdrRatio(d.iso), over: cap > 0 && load > cap, today: d.iso === todayISO() } +})) const routesMapRef = ref(null) const dayRouteMetrics = reactive({}) const hiddenRouteTechs = ref(new Set()) // chips : techs RETIRÉS de la carte (couleur/badge conservés, réactivables d'un clic) @@ -5505,7 +5515,10 @@ onBeforeRouteLeave(() => { if (dirty.value && !window.confirm(DIRTY_MSG)) return .loc-results { max-height: 190px; overflow: auto; border-radius: 6px; } .loc-pick-link { cursor: pointer; text-decoration: underline; font-weight: 600; } /* Bande de capacité restante par jour (AM/PM/Soir) dans l'en-tête */ -.cap-strip { display: flex; gap: 2px; justify-content: center; align-items: center; margin-top: 2px; } +.cap-strip { display: flex; gap: 3px; justify-content: center; align-items: center; margin-top: 2px; } +/* Barre verticale d'occupation (miroir de la bande mobile) dans l'en-tête desktop */ +.hdr-vbar { width: 7px; height: 22px; background: #eef2f7; border-radius: 4px; overflow: hidden; display: inline-flex; align-items: flex-end; flex: 0 0 auto; } +.hdr-vfill { width: 100%; border-radius: 4px; transition: height .3s; } .cap-seg { font-size: 9px; font-weight: 700; line-height: 1; min-width: 15px; padding: 2px 2px; border-radius: 3px; text-align: center; cursor: default; } .cap-ok { background: #c8e6c9; color: #1b5e20; } .cap-low { background: #ffe0b2; color: #e65100; }