Garde: 2 horaires par règle — semaine (soir 17h-minuit) vs fin de semaine (8h-minuit)

Une règle a maintenant un shift SEMAINE + un shift FIN DE SEMAINE (optionnel). La rotation reste
UNE séquence (même tech sur la semaine) ; la génération choisit l'horaire selon le jour : weekend
(sam/dim) → shiftWeekend, sinon shift semaine. Wipe couvre les 2 shifts. Modèles semés:
« Garde soir 17h-00h » (17:00-23:59) + « Garde fin de sem. 8h-00h » (08:00-23:59), on_call.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
louispaulb 2026-06-04 22:33:07 -04:00
parent 0320c8716f
commit 70c89b2cea

View File

@ -269,7 +269,7 @@
<q-list v-if="gardeRules.length" dense bordered class="rounded-borders q-mb-md"> <q-list v-if="gardeRules.length" dense bordered class="rounded-borders q-mb-md">
<q-item v-for="(r, i) in gardeRules" :key="r.id"> <q-item v-for="(r, i) in gardeRules" :key="r.id">
<q-item-section> <q-item-section>
<q-item-label class="text-weight-medium">{{ r.dept }} · {{ shiftName(r.shift) }}</q-item-label> <q-item-label class="text-weight-medium">{{ r.dept }} · {{ shiftName(r.shift) }}<span v-if="r.shiftWeekend"> · WE : {{ shiftName(r.shiftWeekend) }}</span></q-item-label>
<q-item-label caption>{{ gardeDowLabel(r) }} · dès {{ (r.anchor || '').slice(5) }} · {{ gardeSeqLabel(r) }}</q-item-label> <q-item-label caption>{{ gardeDowLabel(r) }} · dès {{ (r.anchor || '').slice(5) }} · {{ gardeSeqLabel(r) }}</q-item-label>
</q-item-section> </q-item-section>
<q-item-section side class="row no-wrap"> <q-item-section side class="row no-wrap">
@ -281,8 +281,9 @@
<div class="text-caption text-weight-medium q-mb-xs">{{ editingGardeId ? 'Modifier la règle' : 'Nouvelle règle' }}</div> <div class="text-caption text-weight-medium q-mb-xs">{{ editingGardeId ? 'Modifier la règle' : 'Nouvelle règle' }}</div>
<div class="row q-col-gutter-sm items-end"> <div class="row q-col-gutter-sm items-end">
<q-select dense outlined v-model="newGardeRule.dept" :options="groupNames" use-input fill-input hide-selected new-value-mode="add-unique" input-debounce="0" label="Département (optionnel)" style="width:170px" hint="existant ou tape un nom" /> <q-select dense outlined v-model="newGardeRule.dept" :options="groupNames" use-input fill-input hide-selected new-value-mode="add-unique" input-debounce="0" label="Département (optionnel)" style="width:170px" hint="existant ou tape un nom" />
<q-select dense outlined v-model="newGardeRule.shift" :options="gardeTemplateOptions" emit-value map-options label="Shift de garde" style="width:190px" /> <q-select dense outlined v-model="newGardeRule.shift" :options="gardeTemplateOptions" emit-value map-options label="Shift semaine (soir)" style="width:180px" />
<q-input dense outlined type="date" v-model="newGardeRule.anchor" label="Rotation démarre la semaine du" style="width:210px"><q-tooltip>Donnée de référence : la séquence commence à cette semaine (ancrage stable dans le temps).</q-tooltip></q-input> <q-select dense outlined clearable v-model="newGardeRule.shiftWeekend" :options="gardeTemplateOptions" emit-value map-options label="Shift fin de semaine" style="width:180px" hint="optionnel — sinon = shift semaine" />
<q-input dense outlined type="date" v-model="newGardeRule.anchor" label="Rotation démarre la semaine du" style="width:200px"><q-tooltip>Donnée de référence : la séquence commence à cette semaine (ancrage stable dans le temps).</q-tooltip></q-input>
</div> </div>
<div class="q-mt-sm row items-center q-gutter-xs"> <div class="q-mt-sm row items-center q-gutter-xs">
<span class="text-caption text-grey-7 q-mr-xs">Plages (hors bureau) :</span> <span class="text-caption text-grey-7 q-mr-xs">Plages (hors bureau) :</span>
@ -395,7 +396,7 @@ const activeCell = ref(null) // dernière case cliquée {id, name, iso} — pour
const anchor = ref(null) const anchor = ref(null)
const demand = ref([]); const holidays = ref([]); const weekTemplates = ref([]) const demand = ref([]); const holidays = ref([]); const weekTemplates = ref([])
const gardeRules = ref([]); const showGarde = ref(false) const gardeRules = ref([]); const showGarde = ref(false)
const newGardeRule = reactive({ dept: '', shift: '', weekdays: [], anchor: '', steps: [] }) const newGardeRule = reactive({ dept: '', shift: '', shiftWeekend: '', weekdays: [], anchor: '', steps: [] })
const GARDE_DOW = [{ v: 1, l: 'L' }, { v: 2, l: 'M' }, { v: 3, l: 'M' }, { v: 4, l: 'J' }, { v: 5, l: 'V' }, { v: 6, l: 'S' }, { v: 0, l: 'D' }] const GARDE_DOW = [{ v: 1, l: 'L' }, { v: 2, l: 'M' }, { v: 3, l: 'M' }, { v: 4, l: 'J' }, { v: 5, l: 'V' }, { v: 6, l: 'S' }, { v: 0, l: 'D' }]
const history = ref([]); const future = ref([]) const history = ref([]); const future = ref([])
const search = ref(''); const groupFilter = ref(null); const maxHours = ref(40) const search = ref(''); const groupFilter = ref(null); const maxHours = ref(40)
@ -642,7 +643,7 @@ function openGarde () { if (!newGardeRule.anchor) newGardeRule.anchor = mondayIS
function addTechToSeq () { if (gardePick.value) { newGardeRule.steps.push({ tech: gardePick.value, weeks: 1 }); gardePick.value = null } } function addTechToSeq () { if (gardePick.value) { newGardeRule.steps.push({ tech: gardePick.value, weeks: 1 }); gardePick.value = null } }
function moveTech (i, dir) { const a = newGardeRule.steps; const j = i + dir; if (j < 0 || j >= a.length) return; const x = a[i]; a.splice(i, 1); a.splice(j, 0, x) } function moveTech (i, dir) { const a = newGardeRule.steps; const j = i + dir; if (j < 0 || j >= a.length) return; const x = a[i]; a.splice(i, 1); a.splice(j, 0, x) }
function editGardeRule (r) { function editGardeRule (r) {
Object.assign(newGardeRule, { dept: r.dept || '', shift: r.shift, weekdays: [...(r.weekdays || [])], anchor: r.anchor || mondayISO(start.value), steps: ruleSteps(r) }) Object.assign(newGardeRule, { dept: r.dept || '', shift: r.shift, shiftWeekend: r.shiftWeekend || '', weekdays: [...(r.weekdays || [])], anchor: r.anchor || mondayISO(start.value), steps: ruleSteps(r) })
editingGardeId.value = r.id editingGardeId.value = r.id
} }
const WD_SEMAINE = [1, 2, 3, 4, 5]; const WD_FINSEM = [6, 0] const WD_SEMAINE = [1, 2, 3, 4, 5]; const WD_FINSEM = [6, 0]
@ -680,7 +681,7 @@ const gardePreview = computed(() => {
function saveGarde () { localStorage.setItem(LS_GARDE, JSON.stringify(gardeRules.value)) } function saveGarde () { localStorage.setItem(LS_GARDE, JSON.stringify(gardeRules.value)) }
function addGardeRule () { function addGardeRule () {
if (!newGardeRule.shift || !newGardeRule.steps.length || !newGardeRule.weekdays.length) { $q.notify({ type: 'warning', message: 'Shift, jours et au moins un tech requis' }); return } if (!newGardeRule.shift || !newGardeRule.steps.length || !newGardeRule.weekdays.length) { $q.notify({ type: 'warning', message: 'Shift, jours et au moins un tech requis' }); return }
const rule = { id: editingGardeId.value || Date.now(), dept: newGardeRule.dept || '—', shift: newGardeRule.shift, weekdays: [...newGardeRule.weekdays], anchor: newGardeRule.anchor || mondayISO(start.value), steps: newGardeRule.steps.map(s => ({ tech: s.tech, weeks: Number(s.weeks) || 1 })) } const rule = { id: editingGardeId.value || Date.now(), dept: newGardeRule.dept || '—', shift: newGardeRule.shift, shiftWeekend: newGardeRule.shiftWeekend || '', weekdays: [...newGardeRule.weekdays], anchor: newGardeRule.anchor || mondayISO(start.value), steps: newGardeRule.steps.map(s => ({ tech: s.tech, weeks: Number(s.weeks) || 1 })) }
if (editingGardeId.value) gardeRules.value = gardeRules.value.map(r => r.id === editingGardeId.value ? rule : r) if (editingGardeId.value) gardeRules.value = gardeRules.value.map(r => r.id === editingGardeId.value ? rule : r)
else gardeRules.value = [...gardeRules.value, rule] else gardeRules.value = [...gardeRules.value, rule]
saveGarde(); editingGardeId.value = null; newGardeRule.steps = []; newGardeRule.weekdays = [] saveGarde(); editingGardeId.value = null; newGardeRule.steps = []; newGardeRule.weekdays = []
@ -700,16 +701,18 @@ async function applyGardeRules () {
const ws = addDaysISO(wk0, i * 7) const ws = addDaysISO(wk0, i * 7)
for (let k = 0; k < 7; k++) { for (let k = 0; k < 7; k++) {
const d = addDaysISO(ws, k); const dow = dowOf(d) const d = addDaysISO(ws, k); const dow = dowOf(d)
const weekend = (dow === 0 || dow === 6)
for (const rule of gardeRules.value) { for (const rule of gardeRules.value) {
if (!rule.weekdays.includes(dow)) continue if (!rule.weekdays.includes(dow)) continue
const tpl = tplByName.value[rule.shift]; if (!tpl) continue const shiftName = (weekend && rule.shiftWeekend) ? rule.shiftWeekend : rule.shift
const tpl = tplByName.value[shiftName]; if (!tpl) continue
const id = rotationTech(rule, d); if (!id) continue const id = rotationTech(rule, d); if (!id) continue
const t = techs.value.find(x => x.id === id) const t = techs.value.find(x => x.id === id)
list.push({ tech: id, tech_name: t ? t.name : id, date: d, shift: rule.shift, hours: tpl.hours || 8, zone: tpl.zone || '' }) list.push({ tech: id, tech_name: t ? t.name : id, date: d, shift: shiftName, hours: tpl.hours || 8, zone: tpl.zone || '' })
} }
} }
} }
const shifts = [...new Set(gardeRules.value.map(r => r.shift))] const shifts = [...new Set(gardeRules.value.flatMap(r => [r.shift, r.shiftWeekend].filter(Boolean)))]
try { try {
const r = await roster.applyGardeHorizon(wk0, weeks, list, shifts) const r = await roster.applyGardeHorizon(wk0, weeks, list, shifts)
showGarde.value = false; await loadWeek() showGarde.value = false; await loadWeek()