From cef6507f5fb6ade55320a2371d4afb4f0253721b Mon Sep 17 00:00:00 2001 From: louispaulb Date: Sat, 4 Jul 2026 23:11:02 -0400 Subject: [PATCH] =?UTF-8?q?feat(ops):=20filter=20chips=20scope=20Sugg?= =?UTF-8?q?=C3=A9rer=20+=20skill-matched=20techs=20(Tourn=C3=A9es)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Suite au carrousel : les chips de filtre de la bande « à répartir » pilotent maintenant AUSSI « Suggérer » (« ce que je filtre = ce que je répartis »). - Chips PRIORITÉ (normalisée high/medium/low, via POOL_PRIOS) ajoutés à côté des secteurs ; filtrent la liste + la carte (routeDayUnassignedView = jour + compétence + secteur + priorité). Affichés seulement si >1 priorité présente (comme les secteurs). - suggestJobs() : en vue Tournées, le périmètre = routeDayUnassignedView EXACTEMENT (corrige l'incohérence : avant, les chips secteur étaient ignorés par Suggérer alors que l'infobulle disait « du secteur »). - openSuggest() : pré-sélection des techs par COMPÉTENCE — parmi les disponibles (quart ce jour), ceux dont t.skills couvre un required_skill des jobs du périmètre ; repli sur tous les dispos si aucun match. Toujours modifiable (le dialogue liste les 56 techs, cochables). Vérifié live (06/07) : secteur Huntingdon → vue 4 jobs → Suggérer affiche « 4 job » (avant : ignorait le secteur) + 13/56 techs pré-cochés (compétents+dispos, reste modifiable). 0 nouvelle erreur console. Co-Authored-By: Claude Opus 4.8 --- apps/ops/src/pages/PlanificationPage.vue | 31 +++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/apps/ops/src/pages/PlanificationPage.vue b/apps/ops/src/pages/PlanificationPage.vue index 4b1f1bb..6687c3c 100644 --- a/apps/ops/src/pages/PlanificationPage.vue +++ b/apps/ops/src/pages/PlanificationPage.vue @@ -503,6 +503,12 @@ + Répartir automatiquement les jobs {{ routeSectorSel.size ? 'du secteur' : 'de ce jour' }} @@ -4424,7 +4430,13 @@ function openSuggest () { const win = suggestWindow.value // fenêtre = chips de date cochés, sinon le jour du sélecteur const techs = visibleTechs.value || [] const withShift = techs.filter(t => win.some(iso => hasShiftDay(t.id, iso))) - const base = withShift.length ? withShift : techs + const availPool = withShift.length ? withShift : techs + // Pré-sélection par COMPÉTENCE : parmi les dispos, ceux dont les compétences couvrent au moins un skill requis + // par les jobs du périmètre (jour + filtres). Toujours modifiable (le dialogue liste tous les techs, cochables) ; + // repli sur tous les dispos si aucun skill requis connu ou aucun match. + const needed = new Set((suggestJobs() || []).map(j => j.required_skill).filter(Boolean)) + const skilled = needed.size ? availPool.filter(t => (t.skills || []).some(s => needed.has(s))) : [] + const base = skilled.length ? skilled : availPool const sel = {}; base.forEach(t => { sel[t.id] = true }); suggestDlg.techSel = sel suggestDlg.plan = []; suggestDlg.mode = 'config'; suggestDlg.open = true } @@ -4443,6 +4455,8 @@ const suggestWindow = computed(() => { // par les CHIPS de TYPE **et** de DATE (assignJobsFiltered). Ex. décocher « téléphonie » → non réparti aux techs terrain. const suggestJobs = () => { if (selectedNames.value.length) return (assignPanel.jobs || []).filter(j => j.status !== 'On Hold' && selectedJobs[j.name]) // sélection lasso explicite + // Vue Tournées : périmètre = EXACTEMENT la bande filtrée (jour sélectionné + compétence + secteur + priorité) → « ce que je filtre = ce que Suggérer répartit ». + if (boardView.value === 'routes') return (routeDayUnassignedView.value || []).filter(j => j.status !== 'On Hold') let jobs = (assignJobsFiltered.value || []).filter(j => j.status !== 'On Hold') // pool tel qu'affiché (type + date) // Par DÉFAUT (aucun chip de date coché) : ne répartir QUE les jobs DATÉS sur le(s) jour(s) de la fenêtre — jamais les // jobs en retard/sans date/d'autres jours (souvent faits mais pas encore fermés) qui pollueraient la simulation. @@ -4729,10 +4743,20 @@ const routeStripDays = computed(() => { const routeDayUnassigned = computed(() => unassignedByDay.value[routesDay.value] || []) // Chips SECTEUR (par ville) des non-assignés du jour → filtre la liste ET la carte. Reset au changement de jour. const routeSectorSel = ref(new Set()) -watch(routesDay, () => { routeSectorSel.value = new Set() }) +const routePrioSel = ref(new Set()) +watch(routesDay, () => { routeSectorSel.value = new Set(); routePrioSel.value = new Set() }) const routeSectors = computed(() => { const m = {}; for (const j of routeDayUnassigned.value) { const c = jobCity(j) || 'Sans secteur'; m[c] = (m[c] || 0) + 1 } return Object.entries(m).map(([sector, n]) => ({ sector, n })).sort((a, b) => b.n - a.n) }) function toggleRouteSector (s) { const set = new Set(routeSectorSel.value); if (set.has(s)) set.delete(s); else set.add(s); routeSectorSel.value = set } -const routeDayUnassignedView = computed(() => { const sel = routeSectorSel.value; return sel.size ? routeDayUnassigned.value.filter(j => sel.has(jobCity(j) || 'Sans secteur')) : routeDayUnassigned.value }) +// Chips PRIORITÉ (normalisée high/medium/low) — filtrent la liste + la carte comme les secteurs, et (avec eux) le périmètre de « Suggérer ». +const NORM_PRIO = { high: 'high', urgent: 'high', 'élevée': 'high', elevee: 'high', medium: 'medium', moyenne: 'medium', normal: 'medium', low: 'low', basse: 'low' } +const normPrio = j => NORM_PRIO[String(j.priority || 'medium').toLowerCase()] || 'medium' +const routePrios = computed(() => { const m = {}; for (const j of routeDayUnassigned.value) { const p = normPrio(j); m[p] = (m[p] || 0) + 1 } return POOL_PRIOS.filter(pp => m[pp.value]).map(pp => ({ ...pp, n: m[pp.value] })) }) +function toggleRoutePrio (p) { const set = new Set(routePrioSel.value); if (set.has(p)) set.delete(p); else set.add(p); routePrioSel.value = set } +const routeDayUnassignedView = computed(() => { + const sel = routeSectorSel.value; const ps = routePrioSel.value + if (!sel.size && !ps.size) return routeDayUnassigned.value + return routeDayUnassigned.value.filter(j => (!sel.size || sel.has(jobCity(j) || 'Sans secteur')) && (!ps.size || ps.has(normPrio(j)))) +}) const _jlat = j => (j.latitude != null ? +j.latitude : (j.lat != null ? +j.lat : null)) const _jlon = j => (j.longitude != null ? +j.longitude : (j.lon != null ? +j.lon : null)) // Gouttes carte = non-assignés du jour (filtrés secteur) qui ont des coordonnées ; couleur = priorité @@ -5695,6 +5719,7 @@ onBeforeRouteLeave(() => { if (dirty.value && !window.confirm(DIRTY_MSG)) return .rt-sect.on { background: #6366f1; border-color: #6366f1; color: #fff; } .rt-sect-n { font-weight: 800; margin-left: 2px; opacity: .85; } .rt-sect-x { border-style: dashed; color: #94a3b8; } +.rt-chip-sep { width: 1px; align-self: stretch; min-height: 16px; background: #fed7aa; margin: 0 2px; } .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; }