diff --git a/apps/ops/src/components/shared/OccupancyStrip.vue b/apps/ops/src/components/shared/OccupancyStrip.vue index 7054279..bd4ef0e 100644 --- a/apps/ops/src/components/shared/OccupancyStrip.vue +++ b/apps/ops/src/components/shared/OccupancyStrip.vue @@ -10,7 +10,8 @@ @click="$emit('select', d.iso)" > - + + {{ d.unassigned }} {{ d.dow }} {{ d.dnum }} @@ -57,6 +58,8 @@ function dayTitle (d) { .os-day.holiday { background: #fff7ed; border-color: #fdba74; } .os-hol { position: absolute; top: 1px; left: 3px; color: #ea580c; font-size: 9px; line-height: 1; } .os-warn { position: absolute; top: 2px; right: 3px; color: #ef4444; font-size: 9px; line-height: 1; pointer-events: none; } +/* Badge « dus non assignés ce jour » (flottant coin haut-droit) */ +.os-unassigned { position: absolute; top: -5px; right: -5px; min-width: 16px; height: 16px; padding: 0 3px; border-radius: 8px; background: #ef4444; color: #fff; font-size: 10px; font-weight: 700; line-height: 16px; text-align: center; box-shadow: 0 1px 3px rgba(2, 6, 23, .3); pointer-events: none; } .os-dow { font-size: 10px; color: var(--ops-text-secondary, #64748b); text-transform: capitalize; } .os-num { font-size: 13px; font-weight: 600; color: var(--ops-text, #1e293b); } .os-bar { width: 10px; height: 44px; background: #eef2f7; border-radius: 5px; overflow: hidden; display: flex; align-items: flex-end; margin: 3px 0; } diff --git a/apps/ops/src/pages/PlanificationPage.vue b/apps/ops/src/pages/PlanificationPage.vue index f3b464a..a5d17ad 100644 --- a/apps/ops/src/pages/PlanificationPage.vue +++ b/apps/ops/src/pages/PlanificationPage.vue @@ -495,6 +495,17 @@ {{ hiddenRouteTechs.has(r.id) ? 'Cliquer pour RÉAFFICHER cette tournée' : 'Cliquer pour retirer cette tournée de la carte' }} + +
+ {{ routeDayUnassigned.length }} à répartir : + + {{ j.subject || j.service_type || j.name }} + {{ j.required_skill || '—' }} + + +{{ routeDayUnassigned.length - 14 }} + + Répartir automatiquement les jobs de ce jour +
@@ -4693,10 +4704,12 @@ const routeStripDays = computed(() => { for (let i = 0; i < 14; i++) { const iso = addDaysISO(start.value, i); const c = routeCapacity.value[iso] || {} const h = +c.due_h || 0; const cap = +c.cap_h || 0; const dw = dowOf(iso) - out.push({ iso, dow: FR_DOW[dw], dnum: iso.slice(8) + '/' + iso.slice(5, 7), weekend: dw === 0 || dw === 6, holiday: isHoliday(iso), h, cap, ratio: cap > 0 ? h / cap : (h > 0 ? 1 : 0), over: !!c.overbooked, today: iso === todayISO() }) + out.push({ iso, dow: FR_DOW[dw], dnum: iso.slice(8) + '/' + iso.slice(5, 7), weekend: dw === 0 || dw === 6, holiday: isHoliday(iso), h, cap, ratio: cap > 0 ? h / cap : (h > 0 ? 1 : 0), over: !!c.overbooked, today: iso === todayISO(), unassigned: (unassignedByDay.value[iso] || []).length }) } return out }) +// Jobs DUS le jour sélectionné (Tournées) mais NON assignés — mis en évidence + « Suggérer ». Respecte le filtre de compétence (unassignedByDay). +const routeDayUnassigned = computed(() => unassignedByDay.value[routesDay.value] || []) // ── Deep-link (?day=YYYY-MM-DD & ?skill=…) + action « planifier des quarts » du slot-finder ── const route = useRoute() @@ -4742,8 +4755,9 @@ function stopLivePositions () { if (_livePosTimer) { clearInterval(_livePosTimer // Positions pour la carte : masque les techs cachés (chips), colore selon leur tournée du jour (sinon gris neutre). const dayLivePositions = computed(() => { const colorByTech = {}; for (const r of (allDayRoutes.value || [])) colorByTech[r.id] = r.color + const visIds = new Set((visibleTechs.value || []).map(t => t.id)) // filtre de compétence : seules les positions des techs capables (+ non masqués) sur la carte return (livePosRaw.value || []) - .filter(p => !hiddenRouteTechs.value.has(p.techId)) + .filter(p => visIds.has(p.techId) && !hiddenRouteTechs.value.has(p.techId)) .map(p => ({ techId: p.techId, name: p.techName, lat: p.lat, lon: p.lon, time: p.time, speed: p.speed, color: colorByTech[p.techId] || '#455a64' })) }) watch(boardView, (v) => { if (v === 'routes') startLivePositions(); else stopLivePositions() }, { immediate: true }) @@ -5641,6 +5655,11 @@ onBeforeRouteLeave(() => { if (dirty.value && !window.confirm(DIRTY_MSG)) return .rt-chip.off { opacity: 0.55; border-style: dashed; } .rt-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; } .rt-n { display: inline-flex; align-items: center; justify-content: center; min-width: 17px; height: 17px; border-radius: 9px; padding: 0 4px; color: #fff; font-size: 10px; font-weight: 800; } +/* Bande « jobs dus non assignés » (vue Tournées) : chips cliquables + Suggérer */ +.rt-unassigned { display: flex; align-items: center; flex-wrap: wrap; gap: 5px; margin-bottom: 6px; padding: 6px 8px; background: #fff7ed; border: 1px solid #fed7aa; border-radius: 8px; } +.rt-ua-lbl { font-size: 12px; color: #9a3412; display: inline-flex; align-items: center; gap: 3px; } +.rt-ua-chip { display: inline-flex; align-items: center; gap: 3px; max-width: 230px; padding: 2px 8px 2px 6px; border: 1px solid #e2e8f0; border-left: 3px solid #9e9e9e; border-radius: 6px; background: #fff; font-size: 11.5px; color: #334155; cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.rt-ua-chip:hover { border-color: #6366f1; } .jt-chip { display: inline-flex; align-items: center; font-size: 9px; font-weight: 700; padding: 1px 4px; margin-right: 2px; border-radius: 6px; border: 1px solid #cbd5e1; color: #64748b; background: #fff; cursor: pointer; user-select: none; line-height: 1.4; } .jt-chip:hover { border-color: #6366f1; } .jt-chip.on { background: #6366f1; color: #fff; border-color: #6366f1; }