Garde: moteur fiable — séquence d'étapes {tech, weeks} + semaine d'ancrage (parcours déterministe)
Refonte du principe de rotation suite aux bugs (3 sem. au lieu de 2, édition non reflétée) :
- Séquence = étapes {tech, nb de semaines} ; « 2 semaines consécutives » = weeks:2 (fini les
doublons+periodWeeks qui se multipliaient). Tours inégaux = weeks différents par étape.
- ANCRAGE explicite (semaine de départ, donnée de référence stockée) : on PARCOURT la séquence
semaine par semaine depuis l'ancrage → déterministe, reflète les éditions, pas de dérive.
- Vérifié: A×2,B,C ancré 8 juin → A,A,B,C,A,A,B,C (A toujours 2 consécutives) ; réordonner reflète.
- Aperçu et génération utilisent le même parcours. Migration auto des anciennes règles (techs+period→steps).
Rappel: après édition, re-cliquer « Générer la garde » (l'horizon est réécrit, wipe ciblé du shift).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
761498d65c
commit
e454e3f276
|
|
@ -29,7 +29,7 @@
|
||||||
</q-menu>
|
</q-menu>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
<q-btn v-if="defaultTemplate" dense flat color="amber-9" icon="star" :label="defaultTemplate.name" @click="applyDefault"><q-tooltip>Appliquer le modèle par défaut (consciente des absences)</q-tooltip></q-btn>
|
<q-btn v-if="defaultTemplate" dense flat color="amber-9" icon="star" :label="defaultTemplate.name" @click="applyDefault"><q-tooltip>Appliquer le modèle par défaut (consciente des absences)</q-tooltip></q-btn>
|
||||||
<q-btn dense outline color="brown" icon="shield" label="Garde" @click="showGarde = true"><q-tooltip>Rotation de garde par département</q-tooltip></q-btn>
|
<q-btn dense outline color="brown" icon="shield" label="Garde" @click="openGarde"><q-tooltip>Rotation de garde par département</q-tooltip></q-btn>
|
||||||
<q-btn unelevated color="primary" icon="auto_awesome" label="Générer" :loading="generating" @click="doGenerate" />
|
<q-btn unelevated color="primary" icon="auto_awesome" label="Générer" :loading="generating" @click="doGenerate" />
|
||||||
<q-checkbox v-model="notifySms" label="SMS" dense size="sm"><q-tooltip>Notifier les techs par SMS à la publication</q-tooltip></q-checkbox>
|
<q-checkbox v-model="notifySms" label="SMS" dense size="sm"><q-tooltip>Notifier les techs par SMS à la publication</q-tooltip></q-checkbox>
|
||||||
<q-btn :outline="!dirty" :unelevated="dirty" color="positive" icon="cloud_upload" :label="dirty ? ('Publier (' + dirtyCount + ')') : 'Publier'" :loading="publishing" :disable="!dirty" @click="doPublish" />
|
<q-btn :outline="!dirty" :unelevated="dirty" color="positive" icon="cloud_upload" :label="dirty ? ('Publier (' + dirtyCount + ')') : 'Publier'" :loading="publishing" :disable="!dirty" @click="doPublish" />
|
||||||
|
|
@ -270,7 +270,7 @@
|
||||||
<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) }}</q-item-label>
|
||||||
<q-item-label caption>{{ gardeDowLabel(r) }} · change tous les {{ r.periodWeeks }} {{ (r.unit || 'occ') === 'week' ? 'sem.' : 'jour(s) de garde' }} · rotation : {{ r.techs.map(id => (techs.find(t => t.id === id) || {}).name || id).join(' → ') }}</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">
|
||||||
<q-btn flat dense round size="sm" icon="edit" color="primary" @click="editGardeRule(r)"><q-tooltip>Modifier (ordre, techs, période)</q-tooltip></q-btn>
|
<q-btn flat dense round size="sm" icon="edit" color="primary" @click="editGardeRule(r)"><q-tooltip>Modifier (ordre, techs, période)</q-tooltip></q-btn>
|
||||||
|
|
@ -280,10 +280,9 @@
|
||||||
</q-list>
|
</q-list>
|
||||||
<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:180px" 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 de garde" style="width:190px" />
|
||||||
<q-select dense outlined v-model="newGardeRule.unit" :options="[{ label: 'par jour de garde', value: 'occ' }, { label: 'par semaine', value: 'week' }]" emit-value map-options label="Rotation" style="width:150px" />
|
<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-input dense outlined type="number" min="1" v-model.number="newGardeRule.periodWeeks" :label="newGardeRule.unit === 'week' ? 'Semaines / tech' : 'Jours de garde / tech'" style="width:150px"><q-tooltip>Occurrences consécutives avant de passer au tech suivant (1 = change à chaque {{ newGardeRule.unit === 'week' ? 'semaine' : 'jour de garde' }})</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>
|
||||||
|
|
@ -297,14 +296,15 @@
|
||||||
<q-select dense outlined v-model="gardePick" :options="techOptions" emit-value map-options label="Ajouter un tech à la suite" style="min-width:210px" />
|
<q-select dense outlined v-model="gardePick" :options="techOptions" emit-value map-options label="Ajouter un tech à la suite" style="min-width:210px" />
|
||||||
<q-btn dense unelevated color="grey-7" icon="add" label="Ajouter" :disable="!gardePick" @click="addTechToSeq" />
|
<q-btn dense unelevated color="grey-7" icon="add" label="Ajouter" :disable="!gardePick" @click="addTechToSeq" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="newGardeRule.techs.length" class="q-mt-xs">
|
<div v-if="newGardeRule.steps.length" class="q-mt-xs">
|
||||||
<div class="text-caption text-grey-7">Séquence de rotation ({{ newGardeRule.techs.length }} positions — doublons permis pour des tours inégaux) :</div>
|
<div class="text-caption text-grey-7">Séquence ({{ newGardeRule.steps.length }} étapes) — « sem. » = nb de semaines consécutives du tech avant de passer au suivant :</div>
|
||||||
<div v-for="(id, i) in newGardeRule.techs" :key="i" class="row items-center no-wrap q-gutter-xs q-mt-xs">
|
<div v-for="(s, i) in newGardeRule.steps" :key="i" class="row items-center no-wrap q-gutter-xs q-mt-xs">
|
||||||
<span class="text-caption text-weight-medium" style="min-width:22px">{{ i + 1 }}.</span>
|
<span class="text-caption text-weight-medium" style="min-width:22px">{{ i + 1 }}.</span>
|
||||||
<q-select dense outlined options-dense v-model="newGardeRule.techs[i]" :options="techOptions" emit-value map-options style="min-width:175px" />
|
<q-select dense outlined options-dense v-model="s.tech" :options="techOptions" emit-value map-options style="min-width:170px" />
|
||||||
|
<q-input dense outlined type="number" min="1" v-model.number="s.weeks" style="width:92px" suffix="sem." />
|
||||||
<q-btn flat dense round size="xs" icon="arrow_upward" :disable="i === 0" @click="moveTech(i, -1)"><q-tooltip>Monter</q-tooltip></q-btn>
|
<q-btn flat dense round size="xs" icon="arrow_upward" :disable="i === 0" @click="moveTech(i, -1)"><q-tooltip>Monter</q-tooltip></q-btn>
|
||||||
<q-btn flat dense round size="xs" icon="arrow_downward" :disable="i === newGardeRule.techs.length - 1" @click="moveTech(i, 1)"><q-tooltip>Descendre</q-tooltip></q-btn>
|
<q-btn flat dense round size="xs" icon="arrow_downward" :disable="i === newGardeRule.steps.length - 1" @click="moveTech(i, 1)"><q-tooltip>Descendre</q-tooltip></q-btn>
|
||||||
<q-btn flat dense round size="xs" icon="close" color="grey-6" @click="newGardeRule.techs.splice(i, 1)"><q-tooltip>Retirer</q-tooltip></q-btn>
|
<q-btn flat dense round size="xs" icon="close" color="grey-6" @click="newGardeRule.steps.splice(i, 1)"><q-tooltip>Retirer</q-tooltip></q-btn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="gardePreview.length" class="q-mt-sm bg-grey-1 rounded-borders q-pa-sm">
|
<div v-if="gardePreview.length" class="q-mt-sm bg-grey-1 rounded-borders q-pa-sm">
|
||||||
|
|
@ -315,7 +315,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="row items-center q-mt-md">
|
<div class="row items-center q-mt-md">
|
||||||
<q-btn dense unelevated color="brown" :icon="editingGardeId ? 'save' : 'add'" :label="editingGardeId ? 'Mettre à jour' : 'Ajouter la règle'" @click="addGardeRule" />
|
<q-btn dense unelevated color="brown" :icon="editingGardeId ? 'save' : 'add'" :label="editingGardeId ? 'Mettre à jour' : 'Ajouter la règle'" @click="addGardeRule" />
|
||||||
<q-btn v-if="editingGardeId" flat dense class="q-ml-xs" label="Annuler" @click="editingGardeId = null; newGardeRule.techs = []; newGardeRule.weekdays = []" />
|
<q-btn v-if="editingGardeId" flat dense class="q-ml-xs" label="Annuler" @click="editingGardeId = null; newGardeRule.steps = []; newGardeRule.weekdays = []" />
|
||||||
<q-space />
|
<q-space />
|
||||||
<q-select dense outlined v-model="gardeHorizon" :options="[4, 8, 12, 26]" emit-value map-options style="width:96px" label="semaines" />
|
<q-select dense outlined v-model="gardeHorizon" :options="[4, 8, 12, 26]" emit-value map-options style="width:96px" label="semaines" />
|
||||||
<q-btn dense unelevated color="primary" icon="event_repeat" label="Générer la garde" class="q-ml-sm" @click="applyGardeRules" />
|
<q-btn dense unelevated color="primary" icon="event_repeat" label="Générer la garde" class="q-ml-sm" @click="applyGardeRules" />
|
||||||
|
|
@ -395,7 +395,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: [], periodWeeks: 1, unit: 'week', techs: [] })
|
const newGardeRule = reactive({ dept: '', shift: '', 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)
|
||||||
|
|
@ -634,57 +634,55 @@ const GARDE_EPOCH = '2026-01-05' // lundi de référence pour l'index de semaine
|
||||||
const gardeTemplateOptions = computed(() => templates.value.slice().sort((a, b) => (b.on_call ? 1 : 0) - (a.on_call ? 1 : 0)).map(t => ({ label: t.template_name + (t.on_call ? ' 🛡️' : ''), value: t.name })))
|
const gardeTemplateOptions = computed(() => templates.value.slice().sort((a, b) => (b.on_call ? 1 : 0) - (a.on_call ? 1 : 0)).map(t => ({ label: t.template_name + (t.on_call ? ' 🛡️' : ''), value: t.name })))
|
||||||
const groupNames = computed(() => [...new Set(techs.value.map(t => t.group).filter(Boolean))].sort())
|
const groupNames = computed(() => [...new Set(techs.value.map(t => t.group).filter(Boolean))].sort())
|
||||||
const editingGardeId = ref(null); const gardePick = ref(null)
|
const editingGardeId = ref(null); const gardePick = ref(null)
|
||||||
function addTechToSeq () { if (gardePick.value) { newGardeRule.techs.push(gardePick.value); gardePick.value = null } } // doublons permis (tours inégaux)
|
function d2ms (iso) { const a = iso.split('-').map(Number); return Date.UTC(a[0], a[1] - 1, a[2]) }
|
||||||
function moveTech (i, dir) { const a = newGardeRule.techs; 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 mondayISO (iso) { return addDaysISO(iso, -((dowOf(iso) + 6) % 7)) }
|
||||||
function editGardeRule (r) { Object.assign(newGardeRule, { dept: r.dept || '', shift: r.shift, weekdays: [...r.weekdays], periodWeeks: r.periodWeeks || 1, unit: r.unit || 'week', techs: [...r.techs] }); editingGardeId.value = r.id }
|
function weekNo (iso) { return Math.round((d2ms(mondayISO(iso)) - d2ms(GARDE_EPOCH)) / (7 * 86400000)) } // n° de semaine absolu (réf. lundi)
|
||||||
|
function openGarde () { if (!newGardeRule.anchor) newGardeRule.anchor = mondayISO(start.value); showGarde.value = true }
|
||||||
|
// Séquence = étapes {tech, weeks}. Ajouter à la suite (doublons OK), réordonner, retirer.
|
||||||
|
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 editGardeRule (r) {
|
||||||
|
const steps = (r.steps && r.steps.length) ? r.steps.map(s => ({ tech: s.tech, weeks: Number(s.weeks) || 1 })) : (r.techs || []).map(t => ({ tech: t, weeks: r.periodWeeks || 1 }))
|
||||||
|
Object.assign(newGardeRule, { dept: r.dept || '', shift: r.shift, weekdays: [...(r.weekdays || [])], anchor: r.anchor || mondayISO(start.value), steps })
|
||||||
|
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]
|
||||||
function isSetActive (set) { return set.length && set.every(v => newGardeRule.weekdays.includes(v)) }
|
function isSetActive (set) { return set.length && set.every(v => newGardeRule.weekdays.includes(v)) }
|
||||||
function toggleWeekdaysSet (set) { if (isSetActive(set)) newGardeRule.weekdays = newGardeRule.weekdays.filter(v => !set.includes(v)); else newGardeRule.weekdays = [...new Set([...newGardeRule.weekdays, ...set])] }
|
function toggleWeekdaysSet (set) { if (isSetActive(set)) newGardeRule.weekdays = newGardeRule.weekdays.filter(v => !set.includes(v)); else newGardeRule.weekdays = [...new Set([...newGardeRule.weekdays, ...set])] }
|
||||||
function d2ms (iso) { const a = iso.split('-').map(Number); return Date.UTC(a[0], a[1] - 1, a[2]) }
|
function toggleGardeDow (v) { const i = newGardeRule.weekdays.indexOf(v); if (i >= 0) newGardeRule.weekdays.splice(i, 1); else newGardeRule.weekdays.push(v) }
|
||||||
function mondayISO (iso) { return addDaysISO(iso, -((dowOf(iso) + 6) % 7)) }
|
// ── Moteur de rotation : on PARCOURT la séquence semaine par semaine depuis l'ANCRAGE ──
|
||||||
function weekIndex (iso) { return Math.round((d2ms(mondayISO(iso)) - d2ms(GARDE_EPOCH)) / (7 * 86400000)) }
|
function cycleWeeks (steps) { return (steps || []).reduce((s, x) => s + (Number(x.weeks) || 1), 0) }
|
||||||
// Nb de jours de garde (matching weekdays) depuis l'époque, AVANT cette date → fait avancer la séquence jour par jour.
|
function stepTechAt (steps, w) { for (const s of steps) { const n = Number(s.weeks) || 1; if (w < n) return s.tech; w -= n } return steps[0] && steps[0].tech }
|
||||||
function occurrenceIndex (rule, iso) {
|
function rotationTech (rule, iso) {
|
||||||
const days = Math.round((d2ms(iso) - d2ms(GARDE_EPOCH)) / 86400000); if (days <= 0) return 0
|
const steps = rule.steps || []; const cyc = cycleWeeks(steps); if (!cyc) return null
|
||||||
const wd = rule.weekdays || []; let cnt = Math.floor(days / 7) * wd.length
|
const anchor = rule.anchor || mondayISO(iso)
|
||||||
const epochDow = dowOf(GARDE_EPOCH)
|
const w0 = (((weekNo(iso) - weekNo(anchor)) % cyc) + cyc) % cyc
|
||||||
for (let i = 0; i < days % 7; i++) if (wd.includes((epochDow + i) % 7)) cnt++
|
for (let k = 0; k < cyc; k++) { const id = stepTechAt(steps, (w0 + k) % cyc); if (id && !isAbsent(id, iso)) return id } // saut d'absent
|
||||||
return cnt
|
return stepTechAt(steps, w0)
|
||||||
}
|
}
|
||||||
// Index de rotation : par OCCURRENCE de garde (défaut → la séquence continue chaque jour de garde) ou par SEMAINE (bloc).
|
// Aperçu : qui est de garde, semaine par semaine, depuis l'ancrage — reflète la file en cours d'édition (ignore absences)
|
||||||
function gardeIndex (rule, iso) { return Math.floor(((rule.unit || 'week') === 'week' ? weekIndex(iso) : occurrenceIndex(rule, iso)) / (rule.periodWeeks || 1)) }
|
|
||||||
// Aperçu : qui est de garde sur les prochaines semaines (depuis la semaine affichée) — reflète la file en cours d'édition
|
|
||||||
const gardePreview = computed(() => {
|
const gardePreview = computed(() => {
|
||||||
const rule = newGardeRule; if (!rule.techs.length || !rule.weekdays.length) return []
|
const rule = newGardeRule; const cyc = cycleWeeks(rule.steps); if (!cyc || !rule.weekdays.length) return []
|
||||||
const out = []; const wk0 = mondayISO(start.value)
|
const anchor = rule.anchor || mondayISO(start.value); const out = []
|
||||||
for (let i = 0; i < 8; i++) {
|
for (let i = 0; i < Math.min(14, cyc + 4); i++) {
|
||||||
const ws = addDaysISO(wk0, i * 7); let iso = null
|
const ws = addDaysISO(mondayISO(anchor), i * 7); const w0 = (((weekNo(ws) - weekNo(anchor)) % cyc) + cyc) % cyc
|
||||||
for (let k = 0; k < 7; k++) { const d = addDaysISO(ws, k); if (rule.weekdays.includes(dowOf(d))) { iso = d; break } }
|
const id = stepTechAt(rule.steps, w0)
|
||||||
if (!iso) continue
|
|
||||||
const base = gardeIndex(rule, iso); const id = rule.techs[((base % rule.techs.length) + rule.techs.length) % rule.techs.length]
|
|
||||||
out.push({ week: ws, name: (techs.value.find(t => t.id === id) || {}).name || id })
|
out.push({ week: ws, name: (techs.value.find(t => t.id === id) || {}).name || id })
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
})
|
})
|
||||||
// Tech de garde pour une date ; saute un tech absent au profit du suivant.
|
|
||||||
function rotationTech (rule, iso) {
|
|
||||||
if (!rule.techs || !rule.techs.length) return null
|
|
||||||
const base = gardeIndex(rule, iso)
|
|
||||||
for (let k = 0; k < rule.techs.length; k++) { const id = rule.techs[((base + k) % rule.techs.length + rule.techs.length) % rule.techs.length]; if (!isAbsent(id, iso)) return id }
|
|
||||||
return rule.techs[((base % rule.techs.length) + rule.techs.length) % rule.techs.length]
|
|
||||||
}
|
|
||||||
function toggleGardeDow (v) { const i = newGardeRule.weekdays.indexOf(v); if (i >= 0) newGardeRule.weekdays.splice(i, 1); else newGardeRule.weekdays.push(v) }
|
|
||||||
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.techs.length || !newGardeRule.weekdays.length) { $q.notify({ type: 'warning', message: 'Shift, jours et techs requis (département optionnel)' }); 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], periodWeeks: newGardeRule.periodWeeks || 1, unit: newGardeRule.unit || 'occ', techs: [...newGardeRule.techs] }
|
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 })) }
|
||||||
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.techs = []; newGardeRule.weekdays = []
|
saveGarde(); editingGardeId.value = null; newGardeRule.steps = []; newGardeRule.weekdays = []
|
||||||
$q.notify({ type: 'positive', message: 'Règle de garde enregistrée' })
|
$q.notify({ type: 'positive', message: 'Règle enregistrée — clique « Générer la garde » pour l\'appliquer' })
|
||||||
}
|
}
|
||||||
function removeGardeRule (i) { gardeRules.value = gardeRules.value.filter((_, j) => j !== i); saveGarde(); if (editingGardeId.value && !gardeRules.value.some(r => r.id === editingGardeId.value)) editingGardeId.value = null }
|
function removeGardeRule (i) { gardeRules.value = gardeRules.value.filter((_, j) => j !== i); saveGarde(); if (editingGardeId.value && !gardeRules.value.some(r => r.id === editingGardeId.value)) editingGardeId.value = null }
|
||||||
function gardeDowLabel (r) { return r.weekdays.map(w => (GARDE_DOW.find(x => x.v === w) || {}).l).join('') }
|
function gardeDowLabel (r) { return (r.weekdays || []).map(w => (GARDE_DOW.find(x => x.v === w) || {}).l).join('') }
|
||||||
|
function gardeSeqLabel (r) { return (r.steps || []).map(s => ((techs.value.find(t => t.id === s.tech) || {}).name || s.tech) + (s.weeks > 1 ? ' ×' + s.weeks : '')).join(' → ') }
|
||||||
// Génère les gardes de la semaine affichée selon les règles (rotation par département)
|
// Génère les gardes de la semaine affichée selon les règles (rotation par département)
|
||||||
const gardeHorizon = ref(8) // nb de semaines à matérialiser (évènement récurrent)
|
const gardeHorizon = ref(8) // nb de semaines à matérialiser (évènement récurrent)
|
||||||
// Génère la garde sur un HORIZON (plusieurs semaines) et l'écrit directement (publié) → navigable semaine par semaine.
|
// Génère la garde sur un HORIZON (plusieurs semaines) et l'écrit directement (publié) → navigable semaine par semaine.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user