Garde: génération sur un HORIZON multi-semaines (évènement récurrent) au lieu d'une seule semaine
Avant: « Appliquer à la semaine » n'écrivait qu'une semaine → 1 seul tech (rotation hebdo). Maintenant « Générer la garde » matérialise la rotation sur N semaines (4/8/12/26) d'un coup, écrit direct (publié), navigable semaine par semaine — comme un évènement récurrent. Endpoint /roster/garde/apply : wipe ciblé des shifts de garde dans l'horizon + recréation (idempotent, reflète l'édition de la séquence). Saut d'absent conservé. (source='manuel' car le Select n'autorise que solveur/manuel.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
2b71d1c78c
commit
58253d2e2f
|
|
@ -35,6 +35,7 @@ export const getCoverage = (start, days = 7) => jget(`/roster/coverage?start=${s
|
||||||
export const getStats = (start, days = 7) => jget(`/roster/stats?start=${start}&days=${days}`)
|
export const getStats = (start, days = 7) => jget(`/roster/stats?start=${start}&days=${days}`)
|
||||||
export const getOccupancy = (start, days = 7) => jget(`/roster/occupancy?start=${start}&days=${days}`)
|
export const getOccupancy = (start, days = 7) => jget(`/roster/occupancy?start=${start}&days=${days}`)
|
||||||
export const getAbsences = (start, days = 7) => jget(`/roster/absences?start=${start}&days=${days}`)
|
export const getAbsences = (start, days = 7) => jget(`/roster/absences?start=${start}&days=${days}`)
|
||||||
|
export const applyGardeHorizon = (start, weeks, assignments, shifts) => jpost('/roster/garde/apply', { start, weeks, assignments, shifts })
|
||||||
export const generate = (start, days = 7, weights) => jpost('/roster/generate', { start, days, weights })
|
export const generate = (start, days = 7, weights) => jpost('/roster/generate', { start, days, weights })
|
||||||
export const publish = (assignments) => jpost('/roster/publish', { assignments })
|
export const publish = (assignments) => jpost('/roster/publish', { assignments })
|
||||||
export const publishWeek = (start, days, assignments, notify) => jpost('/roster/publish-week', { start, days, assignments, notify })
|
export const publishWeek = (start, days, assignments, notify) => jpost('/roster/publish-week', { start, days, assignments, notify })
|
||||||
|
|
|
||||||
|
|
@ -317,8 +317,10 @@
|
||||||
<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.techs = []; newGardeRule.weekdays = []" />
|
||||||
<q-space />
|
<q-space />
|
||||||
<q-btn dense unelevated color="primary" icon="auto_awesome" label="Appliquer à la semaine" @click="applyGardeRules" />
|
<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" />
|
||||||
</div>
|
</div>
|
||||||
|
<div class="text-caption text-grey-6 q-mt-xs">Matérialise la rotation sur l'horizon (publié direct, comme un évènement récurrent). Re-générer reflète les modifs de séquence.</div>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
</q-dialog>
|
</q-dialog>
|
||||||
|
|
@ -669,19 +671,31 @@ function addGardeRule () {
|
||||||
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('') }
|
||||||
// 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 () {
|
const gardeHorizon = ref(8) // nb de semaines à matérialiser (évènement récurrent)
|
||||||
if (!gardeRules.value.length) { $q.notify({ type: 'info', message: 'Aucune règle — ouvre « Garde » pour en créer' }); return }
|
// Génère la garde sur un HORIZON (plusieurs semaines) et l'écrit directement (publié) → navigable semaine par semaine.
|
||||||
pushHistory(); let added = 0
|
async function applyGardeRules () {
|
||||||
for (const d of dayList.value) {
|
if (!gardeRules.value.length) { $q.notify({ type: 'info', message: 'Aucune règle — ajoute-en une' }); return }
|
||||||
const dow = dowOf(d.iso)
|
if (dirty.value && !window.confirm('Les modifications non publiées de la grille seront rechargées. Continuer ?')) return
|
||||||
for (const rule of gardeRules.value) {
|
const weeks = gardeHorizon.value || 8; const wk0 = mondayISO(start.value); const list = []
|
||||||
if (!rule.weekdays.includes(dow)) continue
|
for (let i = 0; i < weeks; i++) {
|
||||||
const tpl = tplByName.value[rule.shift]; if (!tpl) continue
|
const ws = addDaysISO(wk0, i * 7)
|
||||||
const id = rotationTech(rule, d.iso); if (!id) continue
|
for (let k = 0; k < 7; k++) {
|
||||||
const t = techs.value.find(x => x.id === id); addShift(id, t ? t.name : id, d.iso, tpl); added++
|
const d = addDaysISO(ws, k); const dow = dowOf(d)
|
||||||
|
for (const rule of gardeRules.value) {
|
||||||
|
if (!rule.weekdays.includes(dow)) continue
|
||||||
|
const tpl = tplByName.value[rule.shift]; if (!tpl) continue
|
||||||
|
const id = rotationTech(rule, d); if (!id) continue
|
||||||
|
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 || '' })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$q.notify({ type: 'positive', message: added + ' garde(s) appliquée(s) — Publier pour confirmer' })
|
const shifts = [...new Set(gardeRules.value.map(r => r.shift))]
|
||||||
|
try {
|
||||||
|
const r = await roster.applyGardeHorizon(wk0, weeks, list, shifts)
|
||||||
|
showGarde.value = false; await loadWeek()
|
||||||
|
$q.notify({ type: 'positive', message: `Garde générée sur ${weeks} sem. : ${r.created} assignations` + (r.deleted ? ` (${r.deleted} remplacées)` : '') + '. Navigue les semaines pour voir la rotation.', timeout: 6000 })
|
||||||
|
} catch (e) { err(e) }
|
||||||
}
|
}
|
||||||
function saveDemand () { localStorage.setItem(LS_DEMAND, JSON.stringify(demand.value)) }
|
function saveDemand () { localStorage.setItem(LS_DEMAND, JSON.stringify(demand.value)) }
|
||||||
function addDemand () { demand.value = [...demand.value, { shift: templates.value[0] && templates.value[0].name, zone: 'Montréal', skills: '', job_h: 0, weekday: 1, weekend: 0, holiday: 0 }]; saveDemand() }
|
function addDemand () { demand.value = [...demand.value, { shift: templates.value[0] && templates.value[0].name, zone: 'Montréal', skills: '', job_h: 0, weekday: 1, weekend: 0, holiday: 0 }]; saveDemand() }
|
||||||
|
|
|
||||||
|
|
@ -714,6 +714,28 @@ async function handle (req, res, method, path, url) {
|
||||||
}
|
}
|
||||||
return json(res, 200, { ok: errors === 0, created, deleted, errors, notified, unchanged })
|
return json(res, 200, { ok: errors === 0, created, deleted, errors, notified, unchanged })
|
||||||
}
|
}
|
||||||
|
// Garde : matérialiser la rotation sur un HORIZON (plusieurs semaines) — comme un évènement récurrent.
|
||||||
|
// Wipe ciblé sur les shifts de garde dans l'horizon + recréation (idempotent, reflète l'édition de la séquence).
|
||||||
|
if (path === '/roster/garde/apply' && method === 'POST') {
|
||||||
|
const b = await parseBody(req)
|
||||||
|
const dates = rangeDates(b.start, (b.weeks || 1) * 7)
|
||||||
|
const shifts = b.shifts || []
|
||||||
|
let deleted = 0
|
||||||
|
if (shifts.length) {
|
||||||
|
const existing = await erp.list('Shift Assignment', { filters: [['shift_template', 'in', shifts], ['assignment_date', 'in', dates]], fields: ['name'], limit: 3000 })
|
||||||
|
for (const a of existing) { const r = await retryWrite(() => erp.remove('Shift Assignment', a.name)); if (r.ok) deleted++ }
|
||||||
|
}
|
||||||
|
let created = 0; let errors = 0
|
||||||
|
for (const a of (b.assignments || [])) {
|
||||||
|
const r = await retryWrite(() => erp.create('Shift Assignment', {
|
||||||
|
technician: a.tech, technician_name: a.tech_name || '', assignment_date: a.date,
|
||||||
|
shift_template: a.shift, zone: a.zone || '', hours: Number(a.hours) || 0,
|
||||||
|
status: 'Publié', source: 'manuel',
|
||||||
|
}))
|
||||||
|
if (r.ok) created++; else errors++
|
||||||
|
}
|
||||||
|
return json(res, 200, { ok: errors === 0, created, deleted, errors })
|
||||||
|
}
|
||||||
// Modifier / supprimer un type de shift (Shift Template)
|
// Modifier / supprimer un type de shift (Shift Template)
|
||||||
const mTpl = path.match(/^\/roster\/template\/(.+)$/)
|
const mTpl = path.match(/^\/roster\/template\/(.+)$/)
|
||||||
if (mTpl && method === 'PUT') {
|
if (mTpl && method === 'PUT') {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user