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 }} · {{ jobCity(j) }}
+ {{ j.required_skill || '—' }} · {{ j.customer_name }}
📍 {{ j.address }}
+
+ +{{ routeDayUnassigned.length - 14 }}
+
+ Répartir automatiquement les jobs de ce jour
+
Object.assign(dayRouteMetrics, m)" @stop-click="onRoutesStopClick" />
@@ -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; }