Garde: dept libre + liste techs complète + réordonner la rotation + éditer + 2 sem. consécutives

Fix listes vides: département = champ libre optionnel (existants OU texte), liste des techs = TOUS
(plus de désactivation sur dept). Réordonnancement de la rotation (↑/↓), édition d'une règle (crayon
→ recharge dans le formulaire → Mettre à jour). Champ « Sem. consécutives / tech » (mettre 2 = un tech
fait 2 semaines de suite). Annuler l'édition.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
louispaulb 2026-06-04 20:53:26 -04:00
parent fe60eeb485
commit 05b5b16a5d

View File

@ -272,13 +272,17 @@
<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) }} · toutes les {{ r.periodWeeks }} sem. · rotation : {{ r.techs.map(id => (techs.find(t => t.id === id) || {}).name || id).join(' → ') }}</q-item-label> <q-item-label caption>{{ gardeDowLabel(r) }} · toutes les {{ r.periodWeeks }} sem. · rotation : {{ r.techs.map(id => (techs.find(t => t.id === id) || {}).name || id).join(' → ') }}</q-item-label>
</q-item-section> </q-item-section>
<q-item-section side><q-btn flat dense round size="sm" icon="delete" color="grey-6" @click="removeGardeRule(i)" /></q-item-section> <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="delete" color="grey-6" @click="removeGardeRule(i)" />
</q-item-section>
</q-item> </q-item>
</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="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="groupOptions" emit-value map-options label="Département" style="width:170px" /> <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.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-input dense outlined type="number" min="1" v-model.number="newGardeRule.periodWeeks" label="Toutes les (sem.)" style="width:130px" /> <q-input dense outlined type="number" min="1" v-model.number="newGardeRule.periodWeeks" label="Sem. consécutives / tech" style="width:160px"><q-tooltip>2 = chaque tech fait 2 semaines de suite avant de passer au suivant</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">Jours :</span> <span class="text-caption text-grey-7 q-mr-xs">Jours :</span>
@ -286,9 +290,19 @@
<q-btn flat dense size="sm" no-caps label="Tous les soirs" @click="newGardeRule.weekdays = [1,2,3,4,5,6,0]" /> <q-btn flat dense size="sm" no-caps label="Tous les soirs" @click="newGardeRule.weekdays = [1,2,3,4,5,6,0]" />
<q-btn flat dense size="sm" no-caps label="Week-ends" @click="newGardeRule.weekdays = [5,6,0]" /> <q-btn flat dense size="sm" no-caps label="Week-ends" @click="newGardeRule.weekdays = [5,6,0]" />
</div> </div>
<q-select dense outlined multiple use-chips v-model="newGardeRule.techs" :options="deptTechs" emit-value map-options label="Techs en rotation (dans l'ordre de sélection)" class="q-mt-sm" :disable="!newGardeRule.dept" hint="L'ordre de sélection = l'ordre de la rotation" /> <q-select dense outlined multiple use-chips v-model="newGardeRule.techs" :options="techOptions" emit-value map-options label="Techs en rotation" class="q-mt-sm" hint="Ajoute les techs ; règle l'ordre ci-dessous" />
<div v-if="newGardeRule.techs.length" class="q-mt-xs">
<div class="text-caption text-grey-7">Ordre de rotation :</div>
<div v-for="(id, i) in newGardeRule.techs" :key="id" class="row items-center no-wrap">
<span class="text-caption" style="min-width:24px">{{ i + 1 }}.</span>
<span class="text-caption col">{{ techName(id) }}</span>
<q-btn flat dense round size="xs" icon="arrow_upward" :disable="i === 0" @click="moveTech(i, -1)" />
<q-btn flat dense round size="xs" icon="arrow_downward" :disable="i === newGardeRule.techs.length - 1" @click="moveTech(i, 1)" />
</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="add" label="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-space /> <q-space />
<q-btn dense unelevated color="primary" icon="auto_awesome" label="Appliquer à la semaine" @click="applyGardeRules" /> <q-btn dense unelevated color="primary" icon="auto_awesome" label="Appliquer à la semaine" @click="applyGardeRules" />
</div> </div>
@ -588,7 +602,11 @@ function loadLS () { try { demand.value = JSON.parse(localStorage.getItem(LS_DEM
// Rotation de garde par département (récurrence + rotation) // Rotation de garde par département (récurrence + rotation)
const GARDE_EPOCH = '2026-01-05' // lundi de référence pour l'index de semaine 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 deptTechs = computed(() => techs.value.filter(t => !newGardeRule.dept || t.group === newGardeRule.dept).map(t => ({ label: t.name, value: t.id }))) const groupNames = computed(() => [...new Set(techs.value.map(t => t.group).filter(Boolean))].sort())
const editingGardeId = ref(null)
function techName (id) { const t = techs.value.find(x => x.id === id); return t ? t.name : id }
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 editGardeRule (r) { Object.assign(newGardeRule, { dept: r.dept || '', shift: r.shift, weekdays: [...r.weekdays], periodWeeks: r.periodWeeks || 1, techs: [...r.techs] }); editingGardeId.value = r.id }
function d2ms (iso) { const a = iso.split('-').map(Number); return Date.UTC(a[0], a[1] - 1, a[2]) } function d2ms (iso) { const a = iso.split('-').map(Number); return Date.UTC(a[0], a[1] - 1, a[2]) }
function mondayISO (iso) { return addDaysISO(iso, -((dowOf(iso) + 6) % 7)) } function mondayISO (iso) { return addDaysISO(iso, -((dowOf(iso) + 6) % 7)) }
function weekIndex (iso) { return Math.round((d2ms(mondayISO(iso)) - d2ms(GARDE_EPOCH)) / (7 * 86400000)) } function weekIndex (iso) { return Math.round((d2ms(mondayISO(iso)) - d2ms(GARDE_EPOCH)) / (7 * 86400000)) }
@ -602,11 +620,14 @@ function rotationTech (rule, iso) {
function toggleGardeDow (v) { const i = newGardeRule.weekdays.indexOf(v); if (i >= 0) newGardeRule.weekdays.splice(i, 1); else newGardeRule.weekdays.push(v) } 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.dept || !newGardeRule.shift || !newGardeRule.techs.length || !newGardeRule.weekdays.length) { $q.notify({ type: 'warning', message: 'Département, shift, jours et techs requis' }); return } if (!newGardeRule.shift || !newGardeRule.techs.length || !newGardeRule.weekdays.length) { $q.notify({ type: 'warning', message: 'Shift, jours et techs requis (département optionnel)' }); return }
gardeRules.value = [...gardeRules.value, { id: Date.now(), dept: newGardeRule.dept, shift: newGardeRule.shift, weekdays: [...newGardeRule.weekdays], periodWeeks: newGardeRule.periodWeeks || 1, techs: [...newGardeRule.techs] }] const rule = { id: editingGardeId.value || Date.now(), dept: newGardeRule.dept || '—', shift: newGardeRule.shift, weekdays: [...newGardeRule.weekdays], periodWeeks: newGardeRule.periodWeeks || 1, techs: [...newGardeRule.techs] }
saveGarde(); newGardeRule.techs = []; $q.notify({ type: 'positive', message: 'Règle de garde ajoutée' }) if (editingGardeId.value) gardeRules.value = gardeRules.value.map(r => r.id === editingGardeId.value ? rule : r)
else gardeRules.value = [...gardeRules.value, rule]
saveGarde(); editingGardeId.value = null; newGardeRule.techs = []; newGardeRule.weekdays = []
$q.notify({ type: 'positive', message: 'Règle de garde enregistrée' })
} }
function removeGardeRule (i) { gardeRules.value = gardeRules.value.filter((_, j) => j !== i); saveGarde() } 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('') }
// 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)
function applyGardeRules () { function applyGardeRules () {