diff --git a/apps/ops/src/pages/PlanificationPage.vue b/apps/ops/src/pages/PlanificationPage.vue index 9751c3b..82e3a52 100644 --- a/apps/ops/src/pages/PlanificationPage.vue +++ b/apps/ops/src/pages/PlanificationPage.vue @@ -137,6 +137,7 @@ 👥 Effectif{{ stat(d.iso).staff || '' }} ⏱ Heures{{ stat(d.iso).hours || '' }} + 🛡️ Garde{{ stat(d.iso).on_call || '' }} 🎫 Tickets{{ stat(d.iso).tickets || '' }} 💲 Coût ({{ Math.round(weekCost) }} $/sem){{ dayCost(d.iso) || '' }} @@ -339,7 +340,7 @@ const visibleTechs = computed(() => { const cellsByTechDay = computed(() => { const m = {}; for (const a of assignments.value) { const t = (m[a.tech] || (m[a.tech] = {})); (t[a.date] || (t[a.date] = [])).push(a) } return m }) function cellsOf (techId, iso) { return (cellsByTechDay.value[techId] && cellsByTechDay.value[techId][iso]) || [] } function isPaused (t) { return t.status === 'En pause' } -function hoursOf (techId) { let h = 0; for (const a of assignments.value) if (a.tech === techId) h += Number(a.hours) || 0; return h } +function hoursOf (techId) { let h = 0; for (const a of assignments.value) { if (a.tech !== techId) continue; const t = tplByName.value[a.shift]; if (t && t.on_call) continue; h += Number(a.hours) || 0 } return h } // garde exclue (mise en dispo, pas travaillée) const serverSet = ref(new Set()) const currentSet = computed(() => new Set(assignments.value.map(a => a.tech + '|' + a.date + '|' + a.shift))) @@ -357,6 +358,7 @@ function isSelected (techId, iso) { return selSet.value.has(techId + '|' + iso) const statByDate = computed(() => Object.fromEntries(dailyStats.value.map(s => [s.date, s]))) function stat (iso) { return statByDate.value[iso] || {} } +const hasOnCall = computed(() => dailyStats.value.some(s => s.on_call > 0)) // Micro-timeline 24 h par cellule : fenêtre(s) du shift = bande neutre, jobs pris = trait coloré. const occByTechDay = ref({}) @@ -407,7 +409,7 @@ function cellInterval (techId, iso) { // coût de main-d'œuvre (coût chargé × heures) const costByTech = computed(() => Object.fromEntries(techs.value.map(t => [t.id, t.cost_h || 0]))) -const costByDate = computed(() => { const m = {}; for (const a of assignments.value) m[a.date] = (m[a.date] || 0) + (Number(a.hours) || 0) * (costByTech.value[a.tech] || 0); return m }) +const costByDate = computed(() => { const m = {}; for (const a of assignments.value) { const t = tplByName.value[a.shift]; if (t && t.on_call) continue; m[a.date] = (m[a.date] || 0) + (Number(a.hours) || 0) * (costByTech.value[a.tech] || 0) } return m }) // garde exclue du coût de main-d'œuvre const weekCost = computed(() => Object.values(costByDate.value).reduce((s, v) => s + v, 0)) function dayCost (iso) { return Math.round(costByDate.value[iso] || 0) } diff --git a/services/targo-hub/lib/roster.js b/services/targo-hub/lib/roster.js index 65435d6..97ed86f 100644 --- a/services/targo-hub/lib/roster.js +++ b/services/targo-hub/lib/roster.js @@ -449,19 +449,25 @@ async function handlePublicBooking (req, res, method, path, url) { return json(res, 404, { error: 'not found' }) } -// Stats par jour : effectif (techs distincts), heures planifiées, tickets dispatch. +// Stats par jour : effectif (techs distincts), heures TRAVAILLÉES, tickets dispatch. +// La garde (on_call) = mise en disponibilité → exclue des heures travaillées. async function statsByDay (start, days) { const dates = rangeDates(start, days) const asgs = await fetchAssignments(start, days) + const templates = await fetchTemplates() + const onCall = new Set(templates.filter(t => t.on_call).map(t => t.name)) const jobs = await erp.list('Dispatch Job', { filters: [['scheduled_date', 'in', dates]], fields: ['name', 'scheduled_date'], limit: 3000, }) const by = {} - for (const d of dates) by[d] = { date: d, staff: new Set(), hours: 0, tickets: 0 } - for (const a of asgs) { if (a.status === 'Annulé') continue; const x = by[a.date]; if (x) { x.staff.add(a.tech); x.hours += Number(a.hours) || 0 } } + for (const d of dates) by[d] = { date: d, staff: new Set(), hours: 0, oncall: new Set(), tickets: 0 } + for (const a of asgs) { + if (a.status === 'Annulé') continue; const x = by[a.date]; if (!x) continue + if (onCall.has(a.shift)) { x.oncall.add(a.tech) } else { x.staff.add(a.tech); x.hours += Number(a.hours) || 0 } + } for (const j of jobs) { const x = by[j.scheduled_date]; if (x) x.tickets++ } - return dates.map(d => ({ date: d, staff: by[d].staff.size, hours: by[d].hours, tickets: by[d].tickets })) + return dates.map(d => ({ date: d, staff: by[d].staff.size, hours: by[d].hours, on_call: by[d].oncall.size, tickets: by[d].tickets })) } // Occupation par (technicien, jour) : Σ heures + blocs horaires des Dispatch Jobs planifiés.