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 @@
+
+
+
+ emit('update:modelValue', v)">
+
+
+
+ Horaire — {{ techName }}
+
+
+
+
+
+
+ {{ p.label }}
+
+
+
+
+
+
+
+
+
à partir de la semaine affichée · {{ totalDays() }} j / {{ Math.round(totalHours() * 10) / 10 }}h par semaine
+
+
+
+
+
+
+
+
+
+
+
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 @@
-
-
-
+
🗓 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) }