@@ -4527,6 +4536,29 @@ function suggestSelectSkill (skill) { // (dé)sélectionne TOUT le groupe de tec
techs.forEach(t => { if (allOn) delete sel[t.id]; else sel[t.id] = true })
suggestDlg.techSel = sel
}
+// ── Groupes de techs ENREGISTRÉS (ex. « Installateurs », « Épisseurs ») — sélection manuelle sauvée par utilisateur,
+// utile quand la compétence déduite du job est imprécise. Persisté serveur (useUserPrefs, suit l'usager). ──
+const { prefs: techGroupPrefs, save: saveTechGroupPrefs } = useUserPrefs('techGroups', { groups: [] })
+const techGroups = computed(() => techGroupPrefs.value.groups || [])
+function applyTechGroup (g) { // remplace la sélection par les techs du groupe (présents/visibles seulement)
+ const ids = new Set(g.ids || []); const sel = {}
+ for (const t of (visibleTechs.value || [])) if (ids.has(t.id)) sel[t.id] = true
+ suggestDlg.techSel = sel
+}
+function saveTechGroup () {
+ const ids = Object.keys(suggestDlg.techSel).filter(id => suggestDlg.techSel[id])
+ if (!ids.length) { $q.notify({ type: 'warning', message: 'Sélectionne au moins un technicien avant d’enregistrer un groupe' }); return }
+ $q.dialog({ title: 'Enregistrer le groupe', message: 'Nom du groupe (ex. Installateurs, Épisseurs)', prompt: { model: '', type: 'text' }, cancel: true }).onOk(name => {
+ name = String(name || '').trim(); if (!name) return
+ const groups = [...(techGroupPrefs.value.groups || [])].filter(g => g.name.toLowerCase() !== name.toLowerCase())
+ groups.push({ name, ids }); groups.sort((a, b) => a.name.localeCompare(b.name))
+ saveTechGroupPrefs({ groups })
+ $q.notify({ type: 'positive', message: `Groupe « ${name} » enregistré (${ids.length} tech)` })
+ })
+}
+function deleteTechGroup (g) {
+ saveTechGroupPrefs({ groups: (techGroupPrefs.value.groups || []).filter(x => x.name !== g.name) })
+}
// Tri des techs par QUALITÉ (maîtrise ↑ · vitesse ↑ · coût ↓) → « les meilleurs en premier » dans la liste de dispo.
function techQuality (t) {
const lvls = Object.values(t.skill_levels || {}); const avgLvl = lvls.length ? lvls.reduce((a, b) => a + b, 0) / lvls.length : 0
@@ -4806,7 +4838,14 @@ function onPlanShifts ({ skill, date } = {}) {
const routesMapRef = ref(null)
const dayRouteMetrics = reactive({})
const hiddenRouteTechs = ref(new Set()) // chips : techs RETIRÉS de la carte (couleur/badge conservés, réactivables d'un clic)
-function toggleRouteTech (id) { const s = new Set(hiddenRouteTechs.value); if (s.has(id)) s.delete(id); else s.add(id); hiddenRouteTechs.value = s }
+function toggleRouteTech (id) {
+ // Clic sur un tech = ISOLER sa tournée sur la carte (masquer toutes les autres) → voir une tournée précise.
+ // Re-clic sur le tech déjà seul = tout réafficher.
+ const others = allDayRoutes.value.map(r => r.id).filter(x => x !== id)
+ const soloed = others.length > 0 && !hiddenRouteTechs.value.has(id) && others.every(x => hiddenRouteTechs.value.has(x))
+ hiddenRouteTechs.value = soloed ? new Set() : new Set(others)
+}
+function showAllRouteTechs () { hiddenRouteTechs.value = new Set() }
const allDayRoutes = computed(() => { // TOUTES les tournées du jour (couleurs STABLES par index, même quand on masque)
const iso = routesDay.value; if (!iso) return []
const out = []; let ci = 0
@@ -5730,6 +5769,8 @@ onBeforeRouteLeave(() => { if (dirty.value && !window.confirm(DIRTY_MSG)) return
.assign-sla.sla-at_risk { background: #ffedd5; color: #c2410c; }
.rt-chip { display: inline-flex; align-items: center; gap: 5px; padding: 2px 8px; border: 1.5px solid #cbd5e1; border-radius: 14px; font-size: 12px; font-weight: 600; color: #334155; cursor: pointer; user-select: none; background: #fff; }
.rt-chip.off { opacity: 0.55; border-style: dashed; }
+.rt-chip-all { border-color: #94a3b8; color: #475569; gap: 3px; }
+.rt-chip-all:hover { border-color: #64748b; background: #f1f5f9; }
.rt-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
.rt-n { display: inline-flex; align-items: center; justify-content: center; min-width: 17px; height: 17px; border-radius: 9px; padding: 0 4px; color: #fff; font-size: 10px; font-weight: 800; }
/* Bande « jobs dus non assignés » (vue Tournées) : chips secteur (filtrent liste+carte) + chips job + Suggérer */
@@ -5772,6 +5813,11 @@ onBeforeRouteLeave(() => { if (dirty.value && !window.confirm(DIRTY_MSG)) return
.suggest-tech-chip:hover { background: #f8fafc; }
.suggest-tech-chip.on { border-color: #6366f1; background: #eef2ff; }
.suggest-skill-chip { border: 1px solid; border-radius: 12px; padding: 1px 9px; font-size: 11px; cursor: pointer; user-select: none; background: #fff; }
+.suggest-grp-chip { display: inline-flex; align-items: center; gap: 3px; border: 1px solid #c7d2fe; background: #eef2ff; color: #4338ca; border-radius: 12px; padding: 1px 8px; font-size: 11px; font-weight: 600; cursor: pointer; user-select: none; }
+.suggest-grp-chip:hover { border-color: #6366f1; }
+.suggest-grp-x { opacity: .55; margin-left: 1px; }
+.suggest-grp-x:hover { opacity: 1; color: #dc2626; }
+.suggest-grp-clear { border-color: #cbd5e1; background: #f8fafc; color: #64748b; }
.suggest-step { display: inline-flex; align-items: center; justify-content: center; min-width: 18px; height: 18px; border-radius: 50%; background: #e0e7ff; color: #4338ca; font-size: 10px; font-weight: 700; margin-right: 2px; flex-shrink: 0; }
.suggest-mlvl { font-size: 10px; color: #b45309; font-weight: 700; white-space: nowrap; margin-right: 3px; }
.assign-foot { border-top: 1px solid #e0e0e0; padding: 6px 9px; font-size: 11px; color: #555; line-height: 1.45; background: #fafafa; border-radius: 0 0 8px 8px; }