diff --git a/apps/ops/src/api/roster.js b/apps/ops/src/api/roster.js index 9dbfbce..e694424 100644 --- a/apps/ops/src/api/roster.js +++ b/apps/ops/src/api/roster.js @@ -36,6 +36,7 @@ export const getStats = (start, days = 7) => jget(`/roster/stats?start=${start}& 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 setAbsence = (tech, date, type, remove) => jpost('/roster/absence/set', { tech, date, type, remove }) 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 df8e981..bd3d1d0 100644 --- a/apps/ops/src/pages/PlanificationPage.vue +++ b/apps/ops/src/pages/PlanificationPage.vue @@ -344,6 +344,12 @@ + + + {{ menuIsAbsent ? "Retirer l'absence" : 'Marquer absent ce jour' }} + A + + {{ a.shift_name || a.shift }} {{ a.hours }}h @@ -465,6 +471,15 @@ const occByTechDay = ref({}) const absByTechDay = ref({}) // tech|date → type d'absence (En pause / Congé / Maladie…) → hachuré function isAbsent (techId, iso) { return !!absByTechDay.value[techId + '|' + iso] } function absenceLabel (techId, iso) { return absByTechDay.value[techId + '|' + iso] || 'Absent' } +async function reloadAbsences () { try { const r = await roster.getAbsences(start.value, days.value); absByTechDay.value = r.absences || {} } catch (e) {} } +// Bascule absence d'1 jour sur des cases (clic + « A » ou menu). Si toutes absentes → retire ; sinon marque. +async function toggleAbsentCells (targets) { + if (!targets || !targets.length) return + const allAbsent = targets.every(k => { const [tid, iso] = k.split('|'); return isAbsent(tid, iso) }) + for (const k of targets) { const [tid, iso] = k.split('|'); try { await roster.setAbsence(tid, iso, 'Congé', allAbsent) } catch (e) { err(e) } } + await reloadAbsences() + $q.notify({ type: 'info', message: allAbsent ? 'Absence retirée' : (targets.length + ' absence(s) marquée(s)') }) +} function hToNum (t) { if (!t) return null; const p = String(t).split(':'); return Number(p[0]) + (Number(p[1]) || 0) / 60 } function fmtH (h) { const hh = Math.floor(h); const mm = Math.round((h - hh) * 60); return mm ? (hh + ':' + String(mm).padStart(2, '0')) : ('' + hh) } // Axe ADAPTATIF : se cale sur l'amplitude réelle des shifts réguliers de la semaine (la garde n'élargit pas). @@ -793,6 +808,8 @@ function selectBlock (ti, di) { const a = anchor.value; const t0 = Math.min(a.ti function maybeSelectCol (di) { const ks = visibleTechs.value.map(t => t.id + '|' + dayList.value[di].iso); const all = ks.every(k => selSet.value.has(k)); selection.value = all ? selection.value.filter(k => !ks.includes(k)) : [...new Set([...selection.value, ...ks])] } function maybeSelectRow (ti) { const ks = dayList.value.map(d => visibleTechs.value[ti].id + '|' + d.iso); const all = ks.every(k => selSet.value.has(k)); selection.value = all ? selection.value.filter(k => !ks.includes(k)) : [...new Set([...selection.value, ...ks])] } const menuCellShifts = computed(() => (menu.tech && menu.day) ? cellsOf(menu.tech.id, menu.day.iso) : []) +const menuIsAbsent = computed(() => (menu.tech && menu.day) ? isAbsent(menu.tech.id, menu.day.iso) : false) +function toggleAbsentMenu () { if (menu.tech && menu.day) { toggleAbsentCells([menu.tech.id + '|' + menu.day.iso]); menu.show = false } } function removeShiftFromMenu (a) { pushHistory(); removeShift(a.tech, a.date, a.shift) } function clearOne () { if (menu.tech && menu.day) { pushHistory(); clearLocal(menu.tech.id, menu.day.iso); menu.show = false } } // Menu : copier / coller (marche au clic, sans Cmd+clic) + ajuster l'horaire au slider @@ -844,6 +861,11 @@ function onKey (e) { for (const key of targets) { const [tid, iso] = key.split('|'); clearLocal(tid, iso) } if (selection.value.length) selection.value = [] } + if (k === 'a' && !e.altKey && (selection.value.length || activeCell.value)) { // « A » = bascule absent + e.preventDefault(); menu.show = false + const targets = selection.value.length ? selection.value.slice() : [activeCell.value.id + '|' + activeCell.value.iso] + toggleAbsentCells(targets); if (selection.value.length) selection.value = [] + } } function onUnload (e) { if (dirty.value) { e.preventDefault(); e.returnValue = '' } } onMounted(async () => { loadLS(); document.addEventListener('keydown', onKey); document.addEventListener('mouseup', onUp); window.addEventListener('beforeunload', onUnload); try { await loadBase() } catch (e) { err(e) } await loadWeek() }) diff --git a/services/targo-hub/lib/roster.js b/services/targo-hub/lib/roster.js index 2938a72..0db23f8 100644 --- a/services/targo-hub/lib/roster.js +++ b/services/targo-hub/lib/roster.js @@ -566,6 +566,18 @@ async function handle (req, res, method, path, url) { if (!start) return json(res, 400, { error: 'start requis' }) return json(res, 200, { absences: await absencesByTechDay(start, days) }) } + // Absence d'UN JOUR depuis la grille (approuvée → hachurée tout de suite) ou retrait (jour unique seulement). + if (path === '/roster/absence/set' && method === 'POST') { + const b = await parseBody(req) + if (!b.tech || !b.date) return json(res, 400, { error: 'tech + date requis' }) + if (b.remove) { + const rows = await erp.list('Tech Availability', { filters: [['technician', '=', b.tech], ['from_date', '=', b.date], ['to_date', '=', b.date]], fields: ['name'], limit: 20 }) + let removed = 0; for (const r of rows) { const x = await retryWrite(() => erp.remove('Tech Availability', r.name)); if (x.ok) removed++ } + return json(res, 200, { ok: true, removed }) + } + const r = await retryWrite(() => erp.create('Tech Availability', { technician: b.tech, from_date: b.date, to_date: b.date, availability_type: b.type || 'Congé', status: 'Approuvé', reason: 'Grille' })) + return json(res, r.ok ? 200 : 500, r) + } // Prise de RDV : créneaux dispo (roster + compétence + zone) pour proposer/valider if (path === '/roster/book/slots' && method === 'GET') { if (!start) return json(res, 400, { error: 'start requis' })