diff --git a/apps/ops/src/api/roster.js b/apps/ops/src/api/roster.js index 4eaa3b6..9dbfbce 100644 --- a/apps/ops/src/api/roster.js +++ b/apps/ops/src/api/roster.js @@ -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 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 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 publish = (assignments) => jpost('/roster/publish', { assignments }) export const publishWeek = (start, days, assignments, notify) => jpost('/roster/publish-week', { start, days, assignments, notify }) diff --git a/apps/ops/src/pages/PlanificationPage.vue b/apps/ops/src/pages/PlanificationPage.vue index d0d1bba..df8e981 100644 --- a/apps/ops/src/pages/PlanificationPage.vue +++ b/apps/ops/src/pages/PlanificationPage.vue @@ -317,8 +317,10 @@ - + + +
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.
@@ -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 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) -function applyGardeRules () { - if (!gardeRules.value.length) { $q.notify({ type: 'info', message: 'Aucune règle — ouvre « Garde » pour en créer' }); return } - pushHistory(); let added = 0 - for (const d of dayList.value) { - const dow = dowOf(d.iso) - 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.iso); if (!id) continue - const t = techs.value.find(x => x.id === id); addShift(id, t ? t.name : id, d.iso, tpl); added++ +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. +async function applyGardeRules () { + if (!gardeRules.value.length) { $q.notify({ type: 'info', message: 'Aucune règle — ajoute-en une' }); return } + if (dirty.value && !window.confirm('Les modifications non publiées de la grille seront rechargées. Continuer ?')) return + const weeks = gardeHorizon.value || 8; const wk0 = mondayISO(start.value); const list = [] + for (let i = 0; i < weeks; i++) { + const ws = addDaysISO(wk0, i * 7) + for (let k = 0; k < 7; k++) { + 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 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() } diff --git a/services/targo-hub/lib/roster.js b/services/targo-hub/lib/roster.js index edf7c8f..2938a72 100644 --- a/services/targo-hub/lib/roster.js +++ b/services/targo-hub/lib/roster.js @@ -714,6 +714,28 @@ async function handle (req, res, method, path, url) { } 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) const mTpl = path.match(/^\/roster\/template\/(.+)$/) if (mTpl && method === 'PUT') {