-
{{ d.label }} · {{ Math.round(d.hours * 10) / 10 }}h · 🚗 ≈{{ d.km }} km / {{ d.mins }} min
+
{{ d.label }} · {{ Math.round(d.hours * 10) / 10 }}h · 🚗 ≈{{ d.km }} km / {{ d.mins }} min
+
+
+ 🔒 {{ d.prevJobs.length }} déjà assigné(s) · {{ d.prevCities.slice(0, 2).join(', ') }}
+
+
+
+
+
+
+ {{ pj.start }}
+ {{ pj.skill }}
+ {{ pj.subject }} · {{ pj.customer }}
+ {{ Math.round((pj.dur || 0) * 10) / 10 }}h
+
+
{{ ei + 1 }}
{{ e.skill }}
@@ -3915,6 +3936,7 @@ function selectAllSector (city) {
}
// ── D : SUGGESTION de répartition (glouton proximité + charge + compétence), REVUE en surcouche avant d'appliquer ──
const suggestDlg = reactive({ open: false, mode: 'config', plan: [], building: false, applying: false, fromSel: false, techSel: {}, makeShifts: true, strategy: 'optimize', view: 'list', jobTime: {} }) // défaut = solveur VRP (1re génération optimise déjà, plus de placeholders) · strategy: optimize|smart|best|balance|enough
+const prevOpen = reactive({}) // revue : sections « déjà assigné(s) » dépliées (clé techId|iso)
// Contraintes horaires par job (⚡ Optimiser) : AM/PM (fenêtre dure) + « tôt » (urgence → début de journée). Session (clé = jobName, survit aux ré-optimisations).
const AMPM_WIN = { am: [480, 720], pm: [720, 960] } // 8-12 / 12-16 (heure d'horloge)
function jobTimeOf (name) { return suggestDlg.jobTime[name] || {} }
@@ -4196,15 +4218,37 @@ const suggestGroups = computed(() => {
d.entries.push(e); d.hours += e.dur; g.hours += e.dur; g.jobs++
}
const dnumOf = iso => { const d = (dayList.value || []).find(x => x.iso === iso); return d ? (d.dow + ' ' + d.dnum) : iso.slice(5) }
+ // Ville d'un job d'occupation/legacy : sujets hétérogènes (« Ville | Client » OU « Type | Ville | Client ») → on prend le 1er segment
+ // qui RESSEMBLE à une ville (≤4 mots, sans chiffres, sans mots-métier), sinon la fin d'adresse, sinon le suffixe « … - Ville ».
+ const OCC_STOP = /bris|fibre|install|r[ée]par|modem|internet|panne|coupure|remplac|ramass|retrait|d[ée]mant|note|t[ée]l[ée]|\btv\b|wifi|borne|activation|transfert|[ée]quipement|technicien|est\.|time|urgent|\bsca\b|drop|bo[iî]tier|d[ée]roulage|registre|camping/i
+ const cityLike = s => { s = String(s || '').trim(); if (!s || /\d/.test(s)) return ''; if (s.split(/\s+/).length > 4) return ''; if (OCC_STOP.test(s)) return ''; return s }
+ const cityOfOcc = j => {
+ const parts = String(j.address || '').split(',').map(x => x.trim()).filter(Boolean)
+ if (parts.length >= 2) { const c = cityLike(parts[parts.length - 1]); if (c) return c }
+ for (const seg of String(j.subject || '').split('|')) {
+ const c = cityLike(seg); if (c) return c
+ if (seg.includes(' - ')) { const c2 = cityLike(seg.split(' - ').pop()); if (c2) return c2 }
+ }
+ return ''
+ }
return Object.values(byTech).map(g => {
- const home = techHomes.value[g.techId]; let tkm = 0
+ const home = techHomes.value[g.techId]; const isPh = isHoldId(g.techId); let tkm = 0
const days = Object.values(g.days).sort((a, b) => a.iso.localeCompare(b.iso)).map(d => {
const r = nnOrder(d.entries, home); tkm += r.km
- return { ...d, entries: r.ordered, km: r.km, mins: estTravelMin(r.km), label: dnumOf(d.iso) }
+ // Jobs DÉJÀ assignés à ce tech ce jour (occupation OPS + tickets legacy F terrain) → visibles + secteur où il est déjà
+ let prevJobs = []
+ if (!isPh) {
+ prevJobs = (((techDayOcc(g.techId, d.iso) || {}).jobs) || []).filter(j => !j.cancelled)
+ const lg = legacyLoad.value[g.techId + '|' + d.iso]
+ for (const j of ((lg && lg.jobs) || [])) if (j.field) prevJobs.push({ name: 'LEG:' + j.id, subject: j.subject, skill: j.skill || '', dur: Number(j.est_h) || 0, legacy: true })
+ }
+ const prevCities = [...new Set(prevJobs.map(cityOfOcc).filter(c => c && c !== 'Sans ville'))]
+ return { ...d, entries: r.ordered, km: r.km, mins: estTravelMin(r.km), label: dnumOf(d.iso), prevJobs, prevCities }
})
+ const sectors = [...new Set(days.flatMap(d => d.prevCities))] // secteur(s) où le tech a DÉJÀ des jobs → s'ajuster en conséquence
// Occupation : capacité (quarts réels ou 8h) + charge DÉJÀ présente sur ces jours, + les heures proposées → barre projetée.
// Occupation PAR JOUR (le vrai plafond = 8h/jour, PAS la somme des 2 jours) : on affiche le JOUR le plus chargé (contrainte qui lie).
- const placeholder = isHoldId(g.techId)
+ const placeholder = isPh
let worstPct = 0, worstProj = 0, worstCap = 8, existing = 0, over = false
if (!placeholder) for (const d of days) {
const l = techLoad(techDayOcc(g.techId, d.iso), hasShiftDay(g.techId, d.iso))
@@ -4218,7 +4262,7 @@ const suggestGroups = computed(() => {
return {
...g, dayList: days, km: Math.round(tkm * 10) / 10, mins: estTravelMin(tkm),
cap: Math.round(worstCap * 10) / 10, existing: Math.round(existing * 10) / 10, projected: Math.round(worstProj * 10) / 10,
- fillPct: Math.min(100, Math.round(worstPct)), over: !placeholder && over, placeholder, noShiftDays, skills
+ fillPct: Math.min(100, Math.round(worstPct)), over: !placeholder && over, placeholder, noShiftDays, skills, sectors
}
}).sort((a, b) => b.jobs - a.jobs)
})
@@ -5113,6 +5157,12 @@ onBeforeRouteLeave(() => { if (dirty.value && !window.confirm(DIRTY_MSG)) return
.suggest-occ-fill.high { background: #f59e0b; }
.suggest-occ-fill.over { background: #ef4444; }
.suggest-occ-txt { font-size: 11px; font-weight: 700; color: #475569; white-space: nowrap; }
+.suggest-hd-skills { display: inline-flex; align-items: center; gap: 3px; cursor: pointer; }
+.suggest-hd-skills:hover .assign-skill { filter: brightness(1.12); }
+.suggest-hd-more { font-size: 10px; font-weight: 700; color: #64748b; background: #e2e8f0; border-radius: 8px; padding: 0 5px; line-height: 15px; }
+.suggest-prev-toggle { display: inline-flex; align-items: center; gap: 2px; margin-left: 8px; font-size: 10.5px; font-weight: 600; color: #64748b; background: #f1f5f9; border: 1px solid #e2e8f0; border-radius: 8px; padding: 0 6px; cursor: pointer; user-select: none; }
+.suggest-prev-toggle:hover { border-color: #94a3b8; }
+.suggest-prev { opacity: 0.75; background: #f8fafc; border-radius: 6px; }
.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; }