fix(planif): Suggérer = jobs DUS le jour sélectionné · jour partagé entre vues (session) · quarts brouillon reflétés au calendrier mois

1) Suggérer (retour terrain, ex. ticket 254403 en retard auto-dispatché) :
   - fenêtre = EXACTEMENT le jour sélectionné ; les chips de date du pool
     n'élargissent plus jamais le dispatch (elles filtrent l'affichage)
   - suggestJobs filtré à la SOURCE (couvre solveur VRP + heuristique) :
     scheduled_date === jour sélectionné ; retards/sans-date exclus —
     seule exception : sélection manuelle (lasso)
   - libellés du dialogue mis à jour (« N job(s) DUS ce jour · retards exclus »)
2) Jour sélectionné PARTAGÉ (selDay) persisté en sessionStorage (planif_sel_day) :
   écrit par bande Tournées / Jour kanban / sélecteur de date / bande mobile /
   dialogue Suggérer ; repris au changement de vue (Jour ne retombe plus sur
   « aujourd'hui », Tournées suit, Mois ancre son mois dessus)
3) Quarts créés en Semaine/Jour → calendrier mois par tech + vue Mois :
   flush du brouillon (debounce 800 ms) avant l'ouverture du dialogue horaire,
   flush au passage en vue Mois, et refresh auto des calendriers ouverts quand
   une auto-sauvegarde se termine (techSchedRefresh / monthRefresh ;
   MonthOverview.refreshKey enfin câblé du parent)

Vérifié (aperçu 9001, lecture seule) : pool 44 jobs dont 8 en retard → dialogue
n'offre que les 2 DUS le jour choisi ; 22/07 suivi Semaine→Jour→Tournées ;
Mois affiche « Août 2026 » pour selDay=2026-08-05 ; build SPA OK.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
louispaulb 2026-07-14 22:17:12 -04:00
parent 48edede793
commit c7c9f81364

View File

@ -448,7 +448,8 @@
techs en LANES horizontales à ÉCHELLE D'HEURES (réutilise cellBands + pos + axisTicks). Glisser = assigner (hub). --> techs en LANES horizontales à ÉCHELLE D'HEURES (réutilise cellBands + pos + axisTicks). Glisser = assigner (hub). -->
<!-- P3 Onglet « Tournées » : trajets RÉELS (jobs assignés) de la journée sélectionnée, 1 couleur/tech (composant RouteMap partagé, OSRM) --> <!-- P3 Onglet « Tournées » : trajets RÉELS (jobs assignés) de la journée sélectionnée, 1 couleur/tech (composant RouteMap partagé, OSRM) -->
<!-- Vue MOIS : résumé mensuel (en quart / absents) + couverture par compétence (remplace « Demande ») --> <!-- Vue MOIS : résumé mensuel (en quart / absents) + couverture par compétence (remplace « Demande ») -->
<MonthOverview v-if="boardView === 'month'" :techs="techs" :templates="templates" :anchor="start" @generated="() => guard(loadWeek)" /> <!-- anchor = jour sélectionné partagé (même mois que le jour choisi en Semaine/Jour/Tournées) ; refresh-key = recharge quand un brouillon de quart vient d'être auto-sauvegardé -->
<MonthOverview v-if="boardView === 'month'" :techs="techs" :templates="templates" :anchor="selDay || start" :refresh-key="monthRefresh" @generated="() => guard(loadWeek)" />
<div v-if="boardView === 'routes'" class="routes-wrap q-pa-sm"> <div v-if="boardView === 'routes'" class="routes-wrap q-pa-sm">
<div class="row items-center q-mb-sm" style="gap:6px;flex-wrap:wrap"> <div class="row items-center q-mb-sm" style="gap:6px;flex-wrap:wrap">
@ -911,20 +912,20 @@
<q-toggle v-model="suggestDlg.includeAssigned" dense size="sm" color="deep-purple" label="Ré-optimiser aussi les jobs déjà assignés (simulation)" /> <q-toggle v-model="suggestDlg.includeAssigned" dense size="sm" color="deep-purple" label="Ré-optimiser aussi les jobs déjà assignés (simulation)" />
<q-icon name="info" size="15px" color="grey-5"><q-tooltip class="bg-grey-9" style="font-size:11px">Inclut les {{ assignedNamesForWindow().length }} job(s) déjà assigné(s) du/des jour(s) le solveur repropose une répartition de TOUT (existant + nouveaux). Force le solveur VRP. Aperçu seulement « Appliquer » réassigne.</q-tooltip></q-icon> <q-icon name="info" size="15px" color="grey-5"><q-tooltip class="bg-grey-9" style="font-size:11px">Inclut les {{ assignedNamesForWindow().length }} job(s) déjà assigné(s) du/des jour(s) le solveur repropose une répartition de TOUT (existant + nouveaux). Force le solveur VRP. Aperçu seulement « Appliquer » réassigne.</q-tooltip></q-icon>
</div> </div>
<!-- FENÊTRE de répartition = les CHIPS de date cochés dans le pool (ou la sélection lasso). Sinon un jour choisi ici. --> <!-- FENÊTRE de répartition = LE jour sélectionné (selDay partagé entre les vues ; modifiable ici). Lasso = seule exception (jobs choisis à la main). -->
<div class="row items-center q-mb-sm" style="gap:6px;color:#475569;background:#eef2ff;border-radius:6px;padding:5px 8px"> <div class="row items-center q-mb-sm" style="gap:6px;color:#475569;background:#eef2ff;border-radius:6px;padding:5px 8px">
<q-icon name="event" size="16px" color="primary" /><span class="text-caption text-weight-medium">Répartir :</span> <q-icon name="event" size="16px" color="primary" /><span class="text-caption text-weight-medium">Répartir :</span>
<template v-if="suggestFiltered"> <template v-if="suggestFiltered">
<span class="text-caption text-weight-medium text-indigo-8">{{ suggestJobCount }} job(s) {{ suggestScope }}</span> <span class="text-caption text-weight-medium text-indigo-8">{{ suggestJobCount }} job(s) {{ suggestScope }}</span>
<q-icon name="arrow_forward" size="13px" color="grey-6" /><span class="text-caption text-grey-7">{{ suggestWindow.map(windowDayLabel).join(', ') }}</span> <q-icon name="arrow_forward" size="13px" color="grey-6" /><span class="text-caption text-grey-7">{{ suggestWindow.map(windowDayLabel).join(', ') }}</span>
<q-space /><span class="text-caption text-grey-6">{{ selectedNames.length ? 'sélection lasso' : 'ajuste les chips sous le pool' }}</span> <q-space /><span class="text-caption text-grey-6">{{ selectedNames.length ? 'sélection lasso (peut inclure des retards)' : 'chips de compétence du pool' }}</span>
</template> </template>
<template v-else> <template v-else>
<span class="text-caption">pour :</span> <span class="text-caption">pour :</span>
<q-btn dense unelevated size="sm" no-caps :color="suggestDay === todayISO() ? 'primary' : 'grey-4'" :text-color="suggestDay === todayISO() ? 'white' : 'grey-8'" label="Aujourd'hui" @click="suggestDay = todayISO()" /> <q-btn dense unelevated size="sm" no-caps :color="suggestDay === todayISO() ? 'primary' : 'grey-4'" :text-color="suggestDay === todayISO() ? 'white' : 'grey-8'" label="Aujourd'hui" @click="suggestDay = todayISO()" />
<q-btn dense unelevated size="sm" no-caps :color="suggestDay === tomorrowISO() ? 'primary' : 'grey-4'" :text-color="suggestDay === tomorrowISO() ? 'white' : 'grey-8'" label="Demain" @click="suggestDay = tomorrowISO()" /> <q-btn dense unelevated size="sm" no-caps :color="suggestDay === tomorrowISO() ? 'primary' : 'grey-4'" :text-color="suggestDay === tomorrowISO() ? 'white' : 'grey-8'" label="Demain" @click="suggestDay = tomorrowISO()" />
<q-input dense outlined type="date" v-model="suggestDay" style="width:150px" /> <q-input dense outlined type="date" v-model="suggestDay" style="width:150px" />
<q-space /><span class="text-caption text-grey-6">{{ suggestJobCount }} job(s) de ce jour · retards exclus (chip « en retard » pour les inclure)</span> <q-space /><span class="text-caption text-grey-6">{{ suggestJobCount }} job(s) DUS ce jour · retards/sans-date exclus pour en dispatcher un : le sélectionner à la main dans le pool</span>
</template> </template>
</div> </div>
<div class="row items-center q-mb-xs" style="gap:4px"> <div class="row items-center q-mb-xs" style="gap:4px">
@ -2019,9 +2020,16 @@ watch(dayMode, (on) => { if (on) showDayJobs.value = true }) // en mode Jour, af
// Réutilise tout : occupation (avec équipes via job.assist), pool unassignedJobs, onCellDrop (= roster.assignJob hub). // Réutilise tout : occupation (avec équipes via job.assist), pool unassignedJobs, onCellDrop (= roster.assignJob hub).
const { prefs: planifPrefs, save: savePlanif } = useUserPrefs('planif', { view: 'grid' }) // préférence d'affichage SERVEUR (suit l'usager entre appareils) const { prefs: planifPrefs, save: savePlanif } = useUserPrefs('planif', { view: 'grid' }) // préférence d'affichage SERVEUR (suit l'usager entre appareils)
const boardView = ref(['kanban', 'routes', 'month'].includes(planifPrefs.value.view) ? planifPrefs.value.view : 'grid') // 'grid' | 'kanban' | 'month' | 'routes' const boardView = ref(['kanban', 'routes', 'month'].includes(planifPrefs.value.view) ? planifPrefs.value.view : 'grid') // 'grid' | 'kanban' | 'month' | 'routes'
watch(boardView, (v) => { savePlanif({ view: v }); if (v === 'kanban') { kbSelIso.value = nowET.value.iso; if (!dayList.value.find(d => d.iso === kbSelIso.value)) { start.value = thisMonday(); loadWeek() } _kbDidScroll = false; nextTick(() => setTimeout(kbScrollToDefault, 160)) } else if (v === 'grid' && days.value < 7) { days.value = 7; loadWeek() } }) watch(boardView, (v) => { savePlanif({ view: v }); if (v === 'kanban') { kbSelIso.value = selDay.value || nowET.value.iso; if (!dayList.value.find(d => d.iso === kbSelIso.value)) { start.value = mondayISO(kbSelIso.value); loadWeek() } _kbDidScroll = false; nextTick(() => setTimeout(kbScrollToDefault, 160)) } else if (v === 'grid' && days.value < 7) { days.value = 7; loadWeek() } else if (v === 'month' && _draftT) { autosaveDraft() } }) // Jour reprend le jour SÉLECTIONNÉ (selDay), plus « aujourd'hui » ; Mois flush du brouillon (le calendrier lit le serveur)
watch(() => planifPrefs.value.view, (v) => { if (v && v !== boardView.value) boardView.value = v }) // sync depuis le serveur (autre appareil) une fois chargé watch(() => planifPrefs.value.view, (v) => { if (v && v !== boardView.value) boardView.value = v }) // sync depuis le serveur (autre appareil) une fois chargé
const kbSelIso = ref('') // mode Jour : jour explicitement choisi ('' = auto aujourd'hui) const kbSelIso = ref('') // mode Jour : jour explicitement choisi ('' = auto aujourd'hui)
// Jour SÉLECTIONNÉ partagé entre les vues (Semaine · Jour · Tournées · mobile) VARIABLE DE SESSION :
// choisi dans une vue mémorisé (sessionStorage, survit au refresh de l'onglet) repris par les autres vues
// au lieu de retomber sur « aujourd'hui ». Source unique aussi pour le jour cible de « Suggérer ».
const SEL_DAY_KEY = 'planif_sel_day'
const selDay = ref((() => { try { const v = sessionStorage.getItem(SEL_DAY_KEY); return (v && /^\d{4}-\d{2}-\d{2}$/.test(v)) ? v : todayISO() } catch (e) { return todayISO() } })())
function setSelDay (iso) { if (!iso || !/^\d{4}-\d{2}-\d{2}$/.test(String(iso))) return; selDay.value = iso; try { sessionStorage.setItem(SEL_DAY_KEY, iso) } catch (e) {} }
watch(kbSelIso, v => setSelDay(v))
const kanbanDay = computed(() => { // Jour : jour choisi, sinon AUJOURD'HUI (ET) si dans la fenêtre, sinon le 1er jour const kanbanDay = computed(() => { // Jour : jour choisi, sinon AUJOURD'HUI (ET) si dans la fenêtre, sinon le 1er jour
for (const c of [kbSelIso.value, nowET.value.iso]) { if (!c) continue; const d = dayList.value.find(x => x.iso === c); if (d) return d } for (const c of [kbSelIso.value, nowET.value.iso]) { if (!c) continue; const d = dayList.value.find(x => x.iso === c); if (d) return d }
return dayList.value[0] || null return dayList.value[0] || null
@ -2031,6 +2039,7 @@ const boardDate = computed({
get: () => (boardView.value === 'kanban' ? ((kanbanDay.value && kanbanDay.value.iso) || start.value) : start.value), get: () => (boardView.value === 'kanban' ? ((kanbanDay.value && kanbanDay.value.iso) || start.value) : start.value),
set: (v) => { set: (v) => {
if (!v) return if (!v) return
setSelDay(v) // le sélecteur de date de la barre choisit AUSSI le jour partagé (repris par Jour/Tournées/Suggérer)
if (boardView.value === 'kanban') { kbSelIso.value = v; if (!dayList.value.find(d => d.iso === v)) { start.value = mondayISO(v); loadWeek() } } if (boardView.value === 'kanban') { kbSelIso.value = v; if (!dayList.value.find(d => d.iso === v)) { start.value = mondayISO(v); loadWeek() } }
else if (v !== start.value) { start.value = v; onWeekChange() } else if (v !== start.value) { start.value = v; onWeekChange() }
}, },
@ -2635,7 +2644,11 @@ async function applyWeekPreset (t, dows, min, max) {
// Écran unique « Horaire » par tech (icône event de la rangée) : pause indéfinie + horaire récurrent + calendrier congés + archivage. // Écran unique « Horaire » par tech (icône event de la rangée) : pause indéfinie + horaire récurrent + calendrier congés + archivage.
const techSchedOpen = ref(false); const techSchedTech = ref(null) const techSchedOpen = ref(false); const techSchedTech = ref(null)
const techSchedRefresh = ref(0) // après une écriture de quart le calendrier par tech recharge son mois const techSchedRefresh = ref(0) // après une écriture de quart le calendrier par tech recharge son mois
function openTechSchedule (t) { techSchedTech.value = t; techSchedOpen.value = true } const monthRefresh = ref(0) // quand un brouillon vient d'être auto-sauvegardé la vue Mois recharge (les quarts créés en Semaine/Jour s'y reflètent)
// Ouvre le calendrier par tech en FLUSHANT d'abord le brouillon en attente (debounce 800 ms) : le dialogue lit le
// SERVEUR (occupation + assignations Proposé/Publié) sans flush, un quart tout juste peint n'apparaîtrait pas.
async function openTechSchedule (t) { techSchedTech.value = t; if (_draftT) { try { await autosaveDraft() } catch (e) { /* best-effort */ } } techSchedOpen.value = true }
// (le watcher « autosave terminé rafraîchir les calendriers » vit à côté de `autosaving` plus bas TDZ)
// Calendrier par tech : le menu de cellule PARTAGÉ y émet des intentions ; on les exécute avec les mêmes fonctions que la grille. // Calendrier par tech : le menu de cellule PARTAGÉ y émet des intentions ; on les exécute avec les mêmes fonctions que la grille.
// Quarts = persistés PAR DATE (createShift 'Publié' / deleteAssignment) car le calendrier est mensuel (hors fenêtre de la grille). // Quarts = persistés PAR DATE (createShift 'Publié' / deleteAssignment) car le calendrier est mensuel (hors fenêtre de la grille).
async function onTechSchedSetShift ({ techId, iso, min, max } = {}) { async function onTechSchedSetShift ({ techId, iso, min, max } = {}) {
@ -4239,6 +4252,7 @@ const todayIso = computed(() => (nowET.value && nowET.value.iso) || new Date().t
// heatColor composables/useHelpers.js (source unique, partagé avec <OccupancyStrip> + tableau de bord) // heatColor composables/useHelpers.js (source unique, partagé avec <OccupancyStrip> + tableau de bord)
const _CAP_H = 8 // capacité indicative par tech-jour (h) pour normaliser les barres const _CAP_H = 8 // capacité indicative par tech-jour (h) pour normaliser les barres
const mobileSelIso = ref(null) const mobileSelIso = ref(null)
watch(mobileSelIso, v => setSelDay(v)) // le jour touché sur mobile devient le jour partagé (repris par les autres vues)
// Bande de jours mobile = fenêtre LONGUE (4 semaines) carrousel horizontal CONTINU (sans flèches). // Bande de jours mobile = fenêtre LONGUE (4 semaines) carrousel horizontal CONTINU (sans flèches).
// L'occupation est chargée sur la même fenêtre (voir getOccupancy mobile) pour que tous ces jours aient des données. // L'occupation est chargée sur la même fenêtre (voir getOccupancy mobile) pour que tous ces jours aient des données.
const MOBILE_STRIP_DAYS = 28 const MOBILE_STRIP_DAYS = 28
@ -4619,11 +4633,9 @@ function buildSuggestion () {
else if (sched && winSet.has(sched)) cand = [sched] // daté DANS la fenêtre ce jour else if (sched && winSet.has(sched)) cand = [sched] // daté DANS la fenêtre ce jour
else if (sched && sched > winMax) continue // daté APRÈS la fenêtre PAS dispatché maintenant (on ne planifie pas loin) else if (sched && sched > winMax) continue // daté APRÈS la fenêtre PAS dispatché maintenant (on ne planifie pas loin)
else { else {
// En retard (date < fenêtre) ou sans date : NE PLUS balayer automatiquement sur le(s) jour(s) choisi(s). // En retard (date < fenêtre) ou sans date : JAMAIS auto-dispatché. UNE seule exception : la sélection
// Seulement si EXPLICITEMENT demandé : sélection lasso, ou chip « en retard » / « Sans date » coché. // manuelle explicite (lasso/cases) les chips du pool n'ouvrent plus cette porte (ticket ex. 254403).
// sélectionner un seul jour ne dispatche QUE les jobs dus ce jour-là (retour terrain : les retards ne doivent pas s'inviter). if (!useSel) continue
const wantBacklog = useSel || (floatingPool.dateFilter.value || []).includes('__overdue__') || (floatingPool.dateFilter.value || []).includes('Sans date')
if (!wantBacklog) continue
cand = win cand = win
} }
if (!cand.length) continue if (!cand.length) continue
@ -4706,8 +4718,9 @@ function buildSuggestion () {
suggestDlg.plan = plan suggestDlg.plan = plan
} }
function openSuggest () { function openSuggest () {
// Jour de base = la sélection de la vue active (strip Tournées, Jour kanban, ou bande mobile) « Suggérer se base sur la journée sélectionnée » // Jour de base = le jour SÉLECTIONNÉ PARTAGÉ (selDay synchronisé depuis la bande Tournées, le Jour kanban,
suggestDay.value = (boardView.value === 'routes' && routesDay.value) || (boardView.value === 'kanban' && kbSelIso.value) || mobileSelIso.value || routesDay.value || kbSelIso.value || todayISO() // le sélecteur de date et la bande mobile) « Suggérer se base toujours sur la journée sélectionnée ».
suggestDay.value = (boardView.value === 'routes' && routesDay.value) || (boardView.value === 'kanban' && kanbanDay.value && kanbanDay.value.iso) || selDay.value || todayISO()
suggestDlg.fromSel = selectedNames.value.length > 0 suggestDlg.fromSel = selectedNames.value.length > 0
// Étape « disponibilité » d'abord : défaut = techs AVEC un quart sur un des jours de la fenêtre (= disponibles) ; sinon tous les visibles. // Étape « disponibilité » d'abord : défaut = techs AVEC un quart sur un des jours de la fenêtre (= disponibles) ; sinon tous les visibles.
const win = suggestWindow.value // fenêtre = chips de date cochés, sinon le jour du sélecteur const win = suggestWindow.value // fenêtre = chips de date cochés, sinon le jour du sélecteur
@ -4727,48 +4740,41 @@ function openSuggest () {
suggestDlg.plan = []; suggestDlg.mode = 'config'; suggestDlg.open = true suggestDlg.plan = []; suggestDlg.mode = 'config'; suggestDlg.open = true
} }
const suggestSelCount = computed(() => Object.values(suggestDlg.techSel).filter(Boolean).length) const suggestSelCount = computed(() => Object.values(suggestDlg.techSel).filter(Boolean).length)
// Dispatch auto : la FENÊTRE = les CHIPS de date cochées dans le pool (jours à répartir). Sinon (aucun chip) le jour choisi dans le sélecteur. // Dispatch auto : la FENÊTRE = LE jour sélectionné (selDay partagé entre vues ; modifiable dans le dialogue).
// Les chips « en retard » / « Sans date » ne sont pas des jours calendaires les jobs correspondants sont placés AUJOURD'HUI. Non destructif (revue avant d'appliquer). // Changer le jour dans le dialogue met aussi à jour le jour partagé (mémoire de session). Non destructif (revue avant d'appliquer).
const suggestDay = ref('') const suggestDay = ref('')
watch(suggestDay, v => setSelDay(v))
const suggestWindow = computed(() => { const suggestWindow = computed(() => {
// Vue Tournées : la fenêtre = EXACTEMENT le jour sélectionné (autorité = la bande de dates), jamais les chips de date du pool. // RÈGLE DÉFINITIVE (retour terrain, ticket ex. 254403 en retard auto-dispatché) : la fenêtre = EXACTEMENT
// « Suggérer » ne balaie PAS les jobs en retard (date < jour choisi) sur ce jour ; seuls les jobs DUS ce jour-là sont répartis. // LE JOUR SÉLECTIONNÉ (bande Tournées / Jour kanban / sélecteur du dialogue tous synchronisés via selDay).
// Les chips de date du pool FILTRENT l'affichage mais n'élargissent JAMAIS le dispatch auto ; les jobs en
// retard ou sans date ne sont dispatchés QUE par sélection manuelle explicite (lasso/cases cochées).
if (boardView.value === 'routes' && routesDay.value) return [routesDay.value] if (boardView.value === 'routes' && routesDay.value) return [routesDay.value]
const f = floatingPool.dateFilter.value || [] return [suggestDay.value || selDay.value || todayISO()]
if (!f.length) return [suggestDay.value || todayISO()] // aucun chip coché sélecteur de jour du dialogue
const days = new Set(f.filter(iso => iso !== '__overdue__' && iso !== 'Sans date')) // jours concrets cochés (auj./futur)
if (f.includes('__overdue__') || f.includes('Sans date') || !days.size) days.add(todayISO()) // en retard / sans date placés aujourd'hui
return [...days].sort()
}) })
// Jobs que « Suggérer » doit répartir : la sélection lasso si active, sinon « ce que je vois » = le pool filtré // Jobs que « Suggérer » doit répartir : la sélection lasso si active, sinon « ce que je vois » = le pool filtré
// par les CHIPS de TYPE **et** de DATE (assignJobsFiltered). Ex. décocher « téléphonie » non réparti aux techs terrain. // par les CHIPS de TYPE **et** de DATE (assignJobsFiltered). Ex. décocher « téléphonie » non réparti aux techs terrain.
const suggestJobs = () => { const suggestJobs = () => {
if (selectedNames.value.length) return (assignPanel.jobs || []).filter(j => j.status !== 'On Hold' && selectedJobs[j.name]) // sélection lasso explicite if (selectedNames.value.length) return (assignPanel.jobs || []).filter(j => j.status !== 'On Hold' && selectedJobs[j.name]) // sélection lasso explicite = seule façon de dispatcher un retard/sans-date
// 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 ». // 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') if (boardView.value === 'routes') return (routeDayUnassignedView.value || []).filter(j => j.status !== 'On Hold')
let jobs = (floatingPool.filteredJobs.value || []).filter(j => j.status !== 'On Hold') // pool tel qu'affiché (type + date) // RÈGLE : seuls les jobs dont la DATE DUE = le jour sélectionné sont auto-dispatchés (jamais les retards,
// 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 // jamais les sans-date, jamais les autres jours ticket ex. 254403). Appliqué à la SOURCE : couvre le
// jobs en retard/sans date/d'autres jours (souvent faits mais pas encore fermés) qui pollueraient la simulation. // solveur VRP (job_names) ET l'heuristique locale. Les chips de COMPÉTENCE du pool restreignent encore
// (Pour dispatcher les retards, cocher explicitement le chip « en retard » ou « Sans date ».) // (ex. décocher « téléphonie ») ; les chips de DATE n'influencent plus le dispatch (affichage seulement).
// Filtre appliqué à la SOURCE (couvre aussi le solveur Optimiser qui prend suggestJobs()) : seuls les jobs const win = new Set(suggestWindow.value)
// datés DANS la fenêtre passent, SAUF si l'utilisateur demande explicitement les retards/sans-date. let jobs = (assignPanel.jobs || []).filter(j => j.status !== 'On Hold' && j.scheduled_date && j.scheduled_date !== 'Sans date' && win.has(j.scheduled_date))
const wantBacklog = (floatingPool.dateFilter.value || []).includes('__overdue__') || (floatingPool.dateFilter.value || []).includes('Sans date') const sk = floatingPool.skillFilter.value || []
if (!wantBacklog) { if (sk.length) jobs = jobs.filter(j => sk.includes(j.required_skill))
const win = new Set(suggestWindow.value)
jobs = jobs.filter(j => j.scheduled_date && j.scheduled_date !== 'Sans date' && win.has(j.scheduled_date))
}
return jobs return jobs
} }
const suggestJobCount = computed(() => suggestJobs().length) const suggestJobCount = computed(() => suggestJobs().length)
// Y a-t-il un filtre restreignant le dispatch ? (lasso, chips de date ou de type) // Y a-t-il un filtre restreignant le dispatch ? (lasso ou chips de TYPE les chips de date ne comptent plus : le jour vient de selDay)
const suggestFiltered = computed(() => !!(selectedNames.value.length || floatingPool.dateFilter.value.length || floatingPool.skillFilter.value.length)) const suggestFiltered = computed(() => !!(selectedNames.value.length || floatingPool.skillFilter.value.length))
// Libellé du périmètre : « sélectionnés » (lasso) · « des jours/types cochés » (chips) décrit ce qui limite le jeu de jobs. // Libellé du périmètre : « sélectionnés » (lasso) · « des types cochés » (chips de compétence).
const suggestScope = computed(() => { const suggestScope = computed(() => {
if (selectedNames.value.length) return 'sélectionnés' if (selectedNames.value.length) return 'sélectionnés'
const parts = [] return floatingPool.skillFilter.value.length ? 'des types cochés' : 'du jour'
if (floatingPool.dateFilter.value.length) parts.push('jours')
if (floatingPool.skillFilter.value.length) parts.push('types')
return parts.length ? ('des ' + parts.join(' + ') + ' cochés') : 'du pool'
}) })
const windowDayLabel = iso => iso === todayISO() ? "Aujourd'hui" : (iso === tomorrowISO() ? 'Demain' : (fmtDueLabel(iso) || iso)) const windowDayLabel = iso => iso === todayISO() ? "Aujourd'hui" : (iso === tomorrowISO() ? 'Demain' : (fmtDueLabel(iso) || iso))
const suggestWindowLabel = computed(() => { const suggestWindowLabel = computed(() => {
@ -5208,7 +5214,16 @@ async function loadRouteTrack () {
} catch (e) { routeTrack.value = null; routeTrackMsg.value = 'indispo' } } catch (e) { routeTrack.value = null; routeTrackMsg.value = 'indispo' }
} }
watch([soloRouteTech, routesDay, showRouteTrack, boardView], loadRouteTrack) watch([soloRouteTech, routesDay, showRouteTrack, boardView], loadRouteTrack)
watch(boardView, (v) => { if (v === 'routes' && (!routesDay.value || !dayList.value.some(d => d.iso === routesDay.value))) { const t = todayISO(); routesDay.value = ((dayList.value || []).find(d => d.iso >= t) || (dayList.value || [])[0] || {}).iso || t } }, { immediate: true }) watch(boardView, (v) => {
if (v !== 'routes') return
// Reprend le jour SÉLECTIONNÉ partagé (selDay) même jour que les vues Semaine/Jour. Si le jour est hors de la
// fenêtre chargée, on déplace la fenêtre (comme le fait le mode Jour) plutôt que de retomber sur aujourd'hui.
const sel = selDay.value
if (sel && (dayList.value || []).some(d => d.iso === sel)) { routesDay.value = sel; return }
if (sel && /^\d{4}-\d{2}-\d{2}$/.test(sel)) { routesDay.value = sel; start.value = mondayISO(sel); loadWeek(); return }
if (!routesDay.value || !dayList.value.some(d => d.iso === routesDay.value)) { const t = todayISO(); routesDay.value = ((dayList.value || []).find(d => d.iso >= t) || (dayList.value || [])[0] || {}).iso || t }
}, { immediate: true })
watch(routesDay, v => setSelDay(v)) // le jour cliqué dans la bande Tournées devient le jour partagé
// Un tech « couvre » une file s'il possède TOUTES les compétences requises par ses jobs (compétences vides ignorées). // Un tech « couvre » une file s'il possède TOUTES les compétences requises par ses jobs (compétences vides ignorées).
const covers = (t, skills) => (skills || []).filter(Boolean).every(s => (t.skills || []).includes(s)) const covers = (t, skills) => (skills || []).filter(Boolean).every(s => (t.skills || []).includes(s))
// Existe-t-il AU MOINS un tech capable ? Sinon (données de compétences incomplètes) on ne bloque personne (évite de piéger une file inassignable). // Existe-t-il AU MOINS un tech capable ? Sinon (données de compétences incomplètes) on ne bloque personne (évite de piéger une file inassignable).
@ -5829,6 +5844,9 @@ function onEnter (ti, di) { if (!drag.on) return; drag.moved = true; selection.v
function onUp () { if (drag.on) { drag.on = false; if (drag.moved) justDragged.value = true } } function onUp () { if (drag.on) { drag.on = false; if (drag.moved) justDragged.value = true } }
// #2 AUTO-SAVE : chaque édition de la grille est persistée en BROUILLON (statut 'Proposé') après un court debounce. Plus rien n'est « non sauvegardé ». // #2 AUTO-SAVE : chaque édition de la grille est persistée en BROUILLON (statut 'Proposé') après un court debounce. Plus rien n'est « non sauvegardé ».
const autosaving = ref(false) const autosaving = ref(false)
// Auto-save TERMINÉ rafraîchit les calendriers qui lisent le SERVEUR (dialogue horaire par tech · vue Mois) :
// les quarts créés en Semaine/Jour s'y répliquent sans fermer/rouvrir. (Watcher ICI, après la déclaration TDZ.)
watch(autosaving, (v, ov) => { if (ov && !v) { if (techSchedOpen.value) techSchedRefresh.value++; if (boardView.value === 'month') monthRefresh.value++ } })
const changeLog = ref([]) // journal de session : { id, at, text, revert?:{tech,date,before}, reverted? } const changeLog = ref([]) // journal de session : { id, at, text, revert?:{tech,date,before}, reverted? }
let _draftT = null let _draftT = null
let _chgSeq = 0 let _chgSeq = 0