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 :
+
+ {{ suggestJobCount }} job(s) {{ selectedNames.length ? 'sélectionnés' : 'des jours cochés' }}
+ {{ suggestWindow.map(windowDayLabel).join(', ') }}
+ {{ selectedNames.length ? 'sélection lasso' : 'ajuste les chips sous le pool' }}
+
+
+ pour :
+
+
+
+ jobs en retard/sans date → tirés sur ce jour
+
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]