feat(planif): hybrid recurring shift model — weekly_schedule pattern + materialization
The weekly_schedule pattern per tech is now the SOURCE of shifts. New
materializeShifts() (hub roster.js) generates source='pattern' Shift
Assignment rows over an N-week horizon, auto-skipping QC holidays
(holidays-qc) and vacations (Tech Availability), and PRESERVING manual
overrides (source≠'pattern') — e.g. a one-off night week. Idempotent
(re-runnable / cron-ready).
- hub: fetchTechnicians returns parsed weekly_schedule; ensureShiftTemplate
(find/create Shift Template by exact times); endpoints
POST /roster/technician/:id/weekly-schedule and /roster/materialize-shifts
(GET=dry-run, POST=apply, weeks/tech params).
- api/roster.js: setWeeklySchedule + materializeShifts.
- WeeklyScheduleEditor: mode toggle 🔁 Récurrent (base, materialized) ↔
📌 Exception (concrete override weeks); adaptive labels/horizon.
- PlanificationPage onScheduleApply branches: recurring → setWeeklySchedule
per tech + materializeShifts; exception → direct-write source='manuel'.
No ERPNext schema change (weekly_schedule + source already exist).
Dry-run against prod: 6 techs patterned → 48 shifts/2wk, 0 collisions.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>