From b1ff46e8c1f797d8bedb7ae059605d55deaba6e9 Mon Sep 17 00:00:00 2001 From: louispaulb Date: Fri, 3 Jul 2026 10:37:46 -0400 Subject: [PATCH] =?UTF-8?q?feat(planif):=20weekly=20shift=20generator=20pe?= =?UTF-8?q?r=20tech=20(templates=20+=20N=20weeks)=20=E2=80=94=20reworked?= =?UTF-8?q?=20from=20Dispatch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The best schedule editor lived in the (to-be-deleted) Dispatch page. Reworked it into a shared Quasar WeeklyScheduleEditor.vue: - Reuses the shared useHelpers.SCHEDULE_PRESETS (5×8h / 4×10h / 3×12h) as one-click chips that fill a per-day form (toggle + start/end + hrs). - "Générer pour N semaine(s)" → automation: repeats the pattern over N weeks from the displayed week. - Emits the schedule; Planif writes Shift Assignments DIRECTLY (published) via ensureWindowTpl (per distinct window) + roster.createShift (idempotent), then refreshes the grid. No Publier round-trip. Wired into the tech skill dialog ("Générer l'horaire…", + a "5×8h rapide" quick path). This is what makes the créneau/skill-shift feature usable (regular shifts now exist). Dispatch's schedule modal can be retired — its useful bits (SCHEDULE_PRESETS) were already shared. Verified: editor opens per tech with presets + 7 day rows + weeks input; matches the target UX in Planification. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../shared/WeeklyScheduleEditor.vue | 83 +++++++++++++++++++ apps/ops/src/pages/PlanificationPage.vue | 35 ++++++-- 2 files changed, 113 insertions(+), 5 deletions(-) create mode 100644 apps/ops/src/components/shared/WeeklyScheduleEditor.vue diff --git a/apps/ops/src/components/shared/WeeklyScheduleEditor.vue b/apps/ops/src/components/shared/WeeklyScheduleEditor.vue new file mode 100644 index 0000000..0d73624 --- /dev/null +++ b/apps/ops/src/components/shared/WeeklyScheduleEditor.vue @@ -0,0 +1,83 @@ + + + + + diff --git a/apps/ops/src/pages/PlanificationPage.vue b/apps/ops/src/pages/PlanificationPage.vue index 2cbc997..aa4ec0f 100644 --- a/apps/ops/src/pages/PlanificationPage.vue +++ b/apps/ops/src/pages/PlanificationPage.vue @@ -790,11 +790,10 @@
Score ★ = maîtrise · Efficacité : + plus vite / plus lent pour CETTE compétence (ex. +80), vide = globale · × sur le chip = retirer.
-
🗓 Horaire — semaine affichée
-
- - - +
🗓 Horaire
+
+ Modèles (5×8h · 4×10h · 3×12h) + heures par jour + N semaines → publie les quarts + Applique 5×8h à la semaine affichée (à publier)
🏠 Domicile (départ de tournée si plus proche que le dépôt)
@@ -1734,6 +1733,8 @@ :technicians="techs" :external-tags="tagCatalog" :external-get-color="getTagColor" @created="onJobCreated" /> + + @@ -1777,6 +1778,7 @@ import TagEditor from 'src/components/shared/TagEditor.vue' // module de tags pa import RouteMap from 'src/components/shared/RouteMap.vue' // carte de tournées réutilisable (revue dispatch auto + onglet Tournées) : OSRM réel, arrêts numérotés, fitTo import UnifiedCreateModal from 'src/components/shared/UnifiedCreateModal.vue' // création de job NATIVE OPS (work-order) — Dispatch déprécié, tout ici import SuggestSlotsDialog from 'src/modules/dispatch/components/SuggestSlotsDialog.vue' // « Trouver un créneau » → pré-remplit la création (tech+date+heure+adresse) +import WeeklyScheduleEditor from 'src/components/shared/WeeklyScheduleEditor.vue' // génération de quarts hebdo par tech (modèles) — repris/amélioré depuis Dispatch const $q = useQuasar() const router = useRouter() @@ -2333,6 +2335,29 @@ async function applyWeekPreset (t, dows, min, max) { skillMenuShown.value = false $q.notify({ type: 'positive', message: t.name + ' : horaire appliqué (semaine affichée) — pense à Publier', timeout: 2500 }) } +// ── Génération de quarts HEBDO par tech (modèles + N semaines) — écrit DIRECTEMENT les Shift Assignment (Publié). ── +const schedGenOpen = ref(false); const schedGenTech = ref(null) +function openSchedGen (t) { schedGenTech.value = { id: t.id, name: t.name }; skillMenuShown.value = false; schedGenOpen.value = true } +const _hmNum = (s) => { const [h, m] = String(s || '').split(':').map(Number); return (h || 0) + (m || 0) / 60 } +async function onScheduleApply ({ schedule, weeks }) { + const t = schedGenTech.value; if (!t) return + let created = 0, failed = 0; const tplByKey = {} + const base = start.value // lundi de la semaine affichée + for (let w = 0; w < weeks; w++) { + for (let i = 0; i < 7; i++) { + const day = schedule[i]; if (!day || !day.on) continue + const sh = _hmNum(day.start), eh = _hmNum(day.end); if (!(eh > sh)) { failed++; continue } + const key = sh + '-' + eh; let tpl = tplByKey[key] + if (!tpl) { tpl = await ensureWindowTpl(sh, eh); tplByKey[key] = tpl } + if (!tpl) { failed++; continue } + const iso = addDaysISO(base, w * 7 + i) + try { const r = await roster.createShift({ tech: t.id, tech_name: t.name, date: iso, shift: tpl.name, hours: tpl.hours || calcHours(day.start, day.end) }); if (r && (r.ok || r.existed)) created++; else failed++ } catch (e) { failed++ } + } + } + try { await loadWeek() } catch (e) {} // rafraîchit la grille (semaine affichée) + $q.notify({ type: created ? 'positive' : 'warning', icon: 'event_available', message: created + ' quart(s) publié(s) pour ' + t.name + (weeks > 1 ? ' (' + weeks + ' sem.)' : '') + (failed ? ' · ' + failed + ' ignoré(s)' : ''), timeout: 3500 }) + schedGenTech.value = null +} function skillEffOf (t, sk) { const e = t.skill_eff && t.skill_eff[sk]; return (e != null && e !== '') ? Number(e) : (Number(t.efficiency) || 1) } // facteur (pour le calcul de priorité) // Efficacité saisie en % de PERFORMANCE : + = plus VITE (moins de temps) · − = plus LENT. Conversion ↔ facteur. function effPctOf (factor) { return Math.round((1 - (Number(factor) || 1)) * 100) }