diff --git a/apps/ops/src/pages/PlanificationPage.vue b/apps/ops/src/pages/PlanificationPage.vue index fb91ac9..acbb6dc 100644 --- a/apps/ops/src/pages/PlanificationPage.vue +++ b/apps/ops/src/pages/PlanificationPage.vue @@ -880,7 +880,7 @@
Jobs à assigner ({{ assignTypeFilter.length ? assignJobsFiltered.length + '/' + assignPanel.jobs.length : assignPanel.jobs.length }}) Localiser les {{ assignNoCoord }} job(s) sans coordonnées : adresse → base RQA + GPS (ils apparaissent ensuite sur la carte) - Proposer une répartition optimisée (proximité · charge · compétence){{ selectedNames.length ? ' pour la sélection' : '' }} — tu revois et ajustes avant d'appliquer + Proposer une répartition optimisée (proximité · charge · compétence){{ selectedNames.length ? ' pour la sélection lasso' : (assignDateFilter.length ? ' pour les jours cochés' : '') }} — tu revois et ajustes avant d'appliquer Carte des jobs (regrouper par région) @@ -1020,13 +1020,21 @@ Vitesse de repli (nœuds sans coordonnées) Budget de calcul par jour
- +
- Répartir pour : - - - - jobs en retard/sans date → tirés sur ce jour + Répartir : + +
Rapide : @@ -2564,10 +2572,18 @@ const assignDates = computed(() => { function toggleAssignDate (iso) { const s = new Set(assignDateFilter.value); s.has(iso) ? s.delete(iso) : s.add(iso); assignDateFilter.value = [...s] } // Depuis l'aperçu « jobs sur la colonne du jour » → ouvre le panneau « À assigner » filtré sur ce jour. async function openDayInPanel (iso) { await openAssignPanel(); assignSort.value = 'date'; assignDateFilter.value = [iso] } +// Un job correspond-il aux CHIPS de date sélectionnées ? (vide = toutes). '__overdue__' = toutes dates passées. +// Partagé par le filtre d'affichage ET le dispatch auto (« Suggérer n'utilise que les jobs des jours cochés »). +function jobMatchesDateChips (j) { + const df = assignDateFilter.value; if (!df.length) return true + const ds = new Set(df); const t = todayISO() + const d = (j.scheduled_date && j.scheduled_date !== 'Sans date') ? j.scheduled_date : 'Sans date' + return ds.has(d) || (ds.has('__overdue__') && d !== 'Sans date' && d < t) +} const assignJobsFiltered = computed(() => { let arr = assignPanel.jobs const f = assignTypeFilter.value; if (f.length) { const set = new Set(f); arr = arr.filter(j => set.has(j.required_skill || 'autre')) } - const df = assignDateFilter.value; if (df.length) { const ds = new Set(df); const t = todayISO(); arr = arr.filter(j => { const d = (j.scheduled_date && j.scheduled_date !== 'Sans date') ? j.scheduled_date : 'Sans date'; return ds.has(d) || (ds.has('__overdue__') && d !== 'Sans date' && d < t) }) } // '__overdue__' = toutes dates passées + if (assignDateFilter.value.length) arr = arr.filter(jobMatchesDateChips) return arr }) function toggleAssignType (k) { const i = assignTypeFilter.value.indexOf(k); if (i >= 0) assignTypeFilter.value.splice(i, 1); else assignTypeFilter.value.push(k) } @@ -4233,8 +4249,7 @@ function jobDur (j) { return (j && (j.est_min ? j.est_min / 60 : Number(j.durati function buildSuggestion () { const useSel = selectedNames.value.length > 0 suggestDlg.fromSel = useSel - let jobs = (assignPanel.jobs || []).filter(j => j.status !== 'On Hold') - if (useSel) jobs = jobs.filter(j => selectedJobs[j.name]) + const jobs = suggestJobs() // lasso si actif, sinon pool filtré par les chips de date (jours cochés) const days = (dayList.value || []); const dayIsos = days.map(d => d.iso) const techs = (visibleTechs.value || []).filter(t => suggestDlg.techSel[t.id]) // seulement les techs choisis (disponibles ce jour) if (!jobs.length || !dayIsos.length || !techs.length) { suggestDlg.plan = []; return } @@ -4347,19 +4362,35 @@ function buildSuggestion () { suggestDlg.plan = plan } function openSuggest () { - suggestDay.value = mobileSelIso.value || kbSelIso.value || todayISO() // jour ciblé = celui sélectionné au tableau (défaut auj.) + suggestDay.value = mobileSelIso.value || kbSelIso.value || todayISO() // repli si aucun chip coché suggestDlg.fromSel = selectedNames.value.length > 0 - // Étape « disponibilité » d'abord : défaut = techs AVEC un quart CE JOUR (= 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 techs = visibleTechs.value || [] - const withShift = techs.filter(t => hasShiftDay(t.id, suggestDay.value)) + const withShift = techs.filter(t => win.some(iso => hasShiftDay(t.id, iso))) const base = withShift.length ? withShift : techs const sel = {}; base.forEach(t => { sel[t.id] = true }); suggestDlg.techSel = sel suggestDlg.plan = []; suggestDlg.mode = 'config'; suggestDlg.open = true } const suggestSelCount = computed(() => Object.values(suggestDlg.techSel).filter(Boolean).length) -// Dispatch auto = UNE SEULE journée (celle sélectionnée). Les jobs en retard / sans date sont tirés sur CE jour. Non destructif : on revoit avant d'appliquer (re-simuler aujourd'hui = OK). +// 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. +// 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). const suggestDay = ref('') -const suggestWindow = computed(() => [suggestDay.value || todayISO()]) +const suggestWindow = computed(() => { + const f = assignDateFilter.value || [] + 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 le pool filtré par les CHIPS de date. +const suggestJobs = () => { + const useSel = selectedNames.value.length > 0 + let jobs = (assignPanel.jobs || []).filter(j => j.status !== 'On Hold') + jobs = useSel ? jobs.filter(j => selectedJobs[j.name]) : jobs.filter(jobMatchesDateChips) + return jobs +} +const suggestJobCount = computed(() => suggestJobs().length) const windowDayLabel = iso => iso === todayISO() ? "Aujourd'hui" : (iso === tomorrowISO() ? 'Demain' : (fmtDueLabel(iso) || iso)) const suggestWindowLabel = computed(() => { const w = suggestWindow.value; if (!w.length) return '—' @@ -4436,6 +4467,7 @@ async function optimizeSuggestion () { try { const r = await roster.optimizePlanHub({ dates: suggestWindow.value, tech_ids: selTechs.map(t => t.id), + job_names: suggestJobs().map(j => j.name), // ne répartir QUE les jobs des jours cochés (ou la sélection lasso) opts: { rank_weight: solverOpts.rankWeight, speed_kmh: solverOpts.speedKmh, max_seconds: solverOpts.maxSeconds, overtime_coef: solverOpts.overtimeCoef }, dur_overrides: { ...durOverride }, job_windows, }) diff --git a/services/targo-hub/lib/roster.js b/services/targo-hub/lib/roster.js index 3b3ccf6..82ddf61 100644 --- a/services/targo-hub/lib/roster.js +++ b/services/targo-hub/lib/roster.js @@ -165,6 +165,7 @@ async function optimizePlan (body) { const durOv = body.dur_overrides || {} const winOv = body.job_windows || {} // {jobName:{tw_start_min,tw_end_min}} (AM/PM) const wantTech = Array.isArray(body.tech_ids) && body.tech_ids.length ? new Set(body.tech_ids) : null + const wantJobs = Array.isArray(body.job_names) && body.job_names.length ? new Set(body.job_names) : null // ne répartir QUE ces jobs (jours cochés / sélection UI) const [pool, techsAll, templates, assignments, occ, absences] = await Promise.all([ buildUnassigned(), fetchTechnicians(), fetchTemplates(), fetchAssignments(lo, span), occupancyByTechDay(lo, span), absencesByTechDay(lo, span), @@ -197,6 +198,7 @@ async function optimizePlan (body) { // 🖥 Les jobs SANS DÉPLACEMENT (remote) sont EXCLUS des tournées : aucun arrêt, aucun épinglage d'adresse — à faire du bureau. const dset = new Set(dates); const byDay = {}; const skipped = []; const remote = [] for (const j of pool) { + if (wantJobs && !wantJobs.has(j.name)) continue // restreint aux jobs demandés (chips de date / sélection) const sched = (j.scheduled_date && /^\d{4}-\d{2}-\d{2}$/.test(String(j.scheduled_date))) ? j.scheduled_date : null if (sched && sched > hi) { skipped.push(j.name); continue } const iso = (sched && dset.has(sched)) ? sched : dates[0]