diff --git a/apps/ops/src/pages/PlanificationPage.vue b/apps/ops/src/pages/PlanificationPage.vue
index 49c7e81..1916c3c 100644
--- a/apps/ops/src/pages/PlanificationPage.vue
+++ b/apps/ops/src/pages/PlanificationPage.vue
@@ -975,8 +975,6 @@
Stratégie :
{{ ({ smart: 'proximité + compétence + charge', best: 'concentre le travail sur les meilleurs (cascade)', balance: 'répartit également entre tous (round-robin)', enough: 'qualifié mais le moins sur-qualifié → réserve les experts (distance + temps pris en compte)' })[suggestDlg.strategy] }}
-
- Définir le niveau min par défaut de chaque compétence (partagé — s'applique au dispatch auto)
Répartit sur : {{ suggestWindowLabel }} — défaut aujourd'hui + demain ; filtre par date (chips de la liste) pour cibler d'autres jours. Les jobs datés au-delà sont laissés pour plus tard.
@@ -1064,32 +1062,6 @@
-
-
-
-
-
- Niveau requis par compétence
Niveau min par défaut pour chaque type de travail → un job hérite de ce niveau (surchargeable par job avec « niv » dans la liste).
-
-
-
-
- Aucune compétence définie sur les techniciens.
-
-
{{ sk }}
-
- Niveau requis {{ n }}
- niv {{ levelBySkill[sk] || 1 }}
-
-
-
-
-
-
-
-
-
-
@@ -2152,9 +2124,7 @@ const groupOptions = computed(() => { const s = new Set(); for (const t of techs
const allSkills = computed(() => [...new Set(techs.value.flatMap(t => t.skills || []))].sort())
// Édition des compétences dans la cellule du nom (popover) + suggestions ALIGNÉES sur les catégories de job.
const jobTypes = ref([]) // service_types distincts (catégories de job) → suggérés comme compétences
-const skillByType = ref({}) // type de job → compétence requise (politique booking, éditable)
-const levelByType = ref({}) // type de job → niveau requis 1-5 (politique booking ; service_type souvent vide)
-const levelBySkill = ref({}) // COMPÉTENCE → niveau requis 1-5 par défaut (dimension réellement peuplée sur les jobs)
+const skillByType = ref({}) // type de job → compétence requise déduite (politique booking) — sert au repli reqSkill
const skillDialog = ref(null) // tech dont on édite les compétences
const skillMenuTarget = ref(null) // élément cliqué = ancre du popover (près de la souris, sur la rangée)
const skillMenuShown = ref(false)
@@ -3911,7 +3881,7 @@ function buildSuggestion () {
const plan = []
for (const j of sorted) {
const reqSkill = j.required_skill || skillByType.value[j.service_type] || '' // compétence explicite, sinon déduite du TYPE de job
- const reqLevel = Number(j._reqLevel || j.required_level || levelBySkill.value[reqSkill] || levelByType.value[j.service_type]) || 1 // niveau : job explicite → défaut par COMPÉTENCE → par type → 1
+ const reqLevel = Number(j.required_level) || 1 // niveau imposé PAR LE JOB (persistant) — plus de défaut global par compétence
const jlat = +j.lat, jlon = +j.lon; const hasLL = isFinite(jlat) && isFinite(jlon) && (jlat || jlon)
const dur = jobDur(j)
const sched = (j.scheduled_date && j.scheduled_date !== 'Sans date' && /^\d{4}-\d{2}-\d{2}$/.test(j.scheduled_date)) ? j.scheduled_date : null
@@ -4011,18 +3981,6 @@ function techQuality (t) {
return (5 - avgLvl) * 2 + (eff - 1) * 5 + ((Number(t.cost_h) || 0) / 100) // plus BAS = meilleur
}
const suggestTechsRanked = computed(() => [...(visibleTechs.value || [])].sort((a, b) => techQuality(a) - techQuality(b)))
-// Éditeur « Exigences par type de job » : compétence + niveau requis par service_type (politique booking, partagée à toute l'équipe).
-const jobTypeReqDlg = reactive({ open: false, saving: false })
-async function saveJobTypeReq () {
- jobTypeReqDlg.saving = true
- try {
- const lbs = {}; for (const k in levelBySkill.value) if (Number(levelBySkill.value[k]) > 1) lbs[k] = Number(levelBySkill.value[k])
- await roster.savePolicy({ booking: { level_by_skill: lbs } })
- levelBySkill.value = { ...lbs }
- $q.notify({ type: 'positive', message: 'Exigences par type enregistrées — appliquées au dispatch auto', timeout: 3000 })
- jobTypeReqDlg.open = false
- } catch (e) { err(e) } finally { jobTypeReqDlg.saving = false }
-}
function runSuggestion () {
if (!suggestSelCount.value) { $q.notify({ type: 'warning', message: 'Choisis au moins un technicien disponible.', timeout: 2500 }); return }
suggestDlg.building = true; buildSuggestion(); suggestDlg.building = false
@@ -4815,7 +4773,7 @@ function onLegacyUpdate (data) {
}
const dispatchSSE = useSSE({ listeners: { 'legacy-update': onLegacyUpdate } })
-onMounted(async () => { loadLS(); document.addEventListener('keydown', onKey); document.addEventListener('mouseup', onUp); window.addEventListener('beforeunload', onUnload); try { await loadBase() } catch (e) { err(e) } await loadWeek(); loadDispatchPolicy(); try { const _h = await roster.holidays(todayISO(), addDaysISO(todayISO(), 400)); statHolidays.value = _h.holidays || [] } catch (e) { /* fériés best-effort */ } try { const _p = await roster.holidayNotice(40); holNoticePreview.value = _p.holidays || [] } catch (e) { /* préavis best-effort */ } /* dépôt + domiciles techs (origine de tournée) */ try { const m = await roster.bookMeta(); jobTypes.value = m.service_types || []; skillByType.value = m.skill_by_type || {}; levelByType.value = m.level_by_type || {}; levelBySkill.value = m.level_by_skill || {} } catch (e) { /* catégories de job pour suggestions */ } if ($q.screen.lt.md) { try { await reloadPool() } catch (e) { /* */ } } else openAssignPanel(); /* mobile : charge le pool pour la liste « À assigner » (assignation au toucher), pas de panneau flottant ; desktop : panneau ouvert */ dispatchSSE.connect(['dispatch']); /* veille Legacy temps-réel */ _nowTimer = setInterval(() => { nowTick.value++ }, 60000) /* ligne « maintenant » du board */ })
+onMounted(async () => { loadLS(); document.addEventListener('keydown', onKey); document.addEventListener('mouseup', onUp); window.addEventListener('beforeunload', onUnload); try { await loadBase() } catch (e) { err(e) } await loadWeek(); loadDispatchPolicy(); try { const _h = await roster.holidays(todayISO(), addDaysISO(todayISO(), 400)); statHolidays.value = _h.holidays || [] } catch (e) { /* fériés best-effort */ } try { const _p = await roster.holidayNotice(40); holNoticePreview.value = _p.holidays || [] } catch (e) { /* préavis best-effort */ } /* dépôt + domiciles techs (origine de tournée) */ try { const m = await roster.bookMeta(); jobTypes.value = m.service_types || []; skillByType.value = m.skill_by_type || {} } catch (e) { /* catégories de job pour suggestions */ } if ($q.screen.lt.md) { try { await reloadPool() } catch (e) { /* */ } } else openAssignPanel(); /* mobile : charge le pool pour la liste « À assigner » (assignation au toucher), pas de panneau flottant ; desktop : panneau ouvert */ dispatchSSE.connect(['dispatch']); /* veille Legacy temps-réel */ _nowTimer = setInterval(() => { nowTick.value++ }, 60000) /* ligne « maintenant » du board */ })
onUnmounted(() => { document.removeEventListener('keydown', onKey); document.removeEventListener('mouseup', onUp); window.removeEventListener('beforeunload', onUnload); if (_nowTimer) clearInterval(_nowTimer); if (_kbRO) _kbRO.disconnect() })
onBeforeRouteLeave(() => { if (dirty.value && !window.confirm(DIRTY_MSG)) return false })