From 72845e2057f57c9e4bf1248d45c8eb11c876c781 Mon Sep 17 00:00:00 2001 From: louispaulb Date: Thu, 4 Jun 2026 15:48:47 -0400 Subject: [PATCH] =?UTF-8?q?Planification:=20axe=20timeline=20adaptatif=20+?= =?UTF-8?q?=20intervalle=20texte=20+=20mod=C3=A8les=20tri=C3=A9s=20par=20u?= =?UTF-8?q?sage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #1 axe trop large (0-24) → axe ADAPTATIF calé sur l'amplitude réelle des shifts réguliers de la semaine (garde n'élargit pas) → barres plus larges, position lisible. Intervalle début–fin REMIS en texte dans la cellule (☀ 8–16 🛡️) = référence sans survol. Infobulle = capacité offrable (corrige aussi un bug: shiftH→bookableH). #2 modèles d'assignation TRIÉS PAR USAGE (les plus utilisés en premier) + infobulle nom. (Rappel: créer des modèles custom = éditeur « Types de shift ».) Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/ops/src/pages/PlanificationPage.vue | 38 ++++++++++++++---------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/apps/ops/src/pages/PlanificationPage.vue b/apps/ops/src/pages/PlanificationPage.vue index 14eedcb..9751c3b 100644 --- a/apps/ops/src/pages/PlanificationPage.vue +++ b/apps/ops/src/pages/PlanificationPage.vue @@ -81,7 +81,7 @@ {{ selection.length }} cellule(s) — assigner : - + {{ t.template_name }} @@ -120,13 +120,12 @@ P @@ -320,6 +319,11 @@ function code (t) { return (t.template_name || t.name || '?').trim()[0].toUpperC const codeByShift = computed(() => Object.fromEntries(templates.value.map(t => [t.name, code(t)]))) const colorByShift = computed(() => Object.fromEntries(templates.value.map(t => [t.name, t.color || '#1976d2']))) const tplByName = computed(() => Object.fromEntries(templates.value.map(t => [t.name, t]))) +// Modèles triés par usage (les plus utilisés en premier) — pour l'assignation rapide +const templatesRanked = computed(() => { + const cnt = {}; for (const a of assignments.value) cnt[a.shift] = (cnt[a.shift] || 0) + 1 + return templates.value.slice().sort((x, y) => (cnt[y.name] || 0) - (cnt[x.name] || 0) || (x.template_name || '').localeCompare(y.template_name || '')) +}) function cellCode (a) { return codeByShift.value[a.shift] || (a.shift_name || a.shift || '?')[0].toUpperCase() } function cellColor (a) { return a.color || colorByShift.value[a.shift] || '#1976d2' } function chip (color) { return { background: color || '#1976d2', color: '#fff' } } @@ -356,9 +360,17 @@ function stat (iso) { return statByDate.value[iso] || {} } // Micro-timeline 24 h par cellule : fenêtre(s) du shift = bande neutre, jobs pris = trait coloré. const occByTechDay = ref({}) -const AXIS_SPAN = 24 // axe 00:00 → 24:00 function hToNum (t) { if (!t) return null; const p = String(t).split(':'); return Number(p[0]) + (Number(p[1]) || 0) / 60 } -function pos (s, e) { const left = Math.max(0, s / AXIS_SPAN * 100); const width = Math.max(1.5, Math.min(100 - left, (e - s) / AXIS_SPAN * 100)); return { left: left + '%', width: width + '%' } } +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). +const axisBounds = computed(() => { + let lo = Infinity; let hi = -Infinity + for (const techId in cellsByTechDay.value) { const day = cellsByTechDay.value[techId]; for (const iso in day) for (const a of day[iso]) { const t = tplByName.value[a.shift]; if (!t || t.on_call) continue; const s = hToNum(t.start_time); const e = hToNum(t.end_time); if (s != null) lo = Math.min(lo, s); if (e != null) hi = Math.max(hi, e <= s ? 24 : e) } } + if (!isFinite(lo) || !isFinite(hi)) return { min: 7, max: 19 } + lo = Math.max(0, Math.floor(lo)); hi = Math.min(24, Math.ceil(hi)); if (hi - lo < 4) hi = Math.min(24, lo + 4) + return { min: lo, max: hi } +}) +function pos (s, e) { const b = axisBounds.value; const span = (b.max - b.min) || 24; const L = Math.max(0, (s - b.min) / span * 100); const R = Math.min(100, (e - b.min) / span * 100); return { left: L + '%', width: Math.max(1.5, R - L) + '%' } } // Bandes = chaque shift du jour. Garde (on_call) = bande hachurée (réserve, non offrable). function cellBands (techId, iso) { const out = [] @@ -371,14 +383,11 @@ function cellBands (techId, iso) { return out } function blockStyle (blk, pct) { return { ...pos(blk.s, Math.min(blk.e, 24)), background: pct == null ? '#6d4c41' : occColor(pct) } } -// Période d'après les shifts RÉGULIERS seulement (la garde ne définit pas le jour/soir offrable) -function cellPeriod (techId, iso) { - let s = Infinity; let e = -Infinity - for (const a of cellsOf(techId, iso)) { const t = tplByName.value[a.shift]; if (!t || t.on_call) continue; const st = hToNum(t.start_time); const en = hToNum(t.end_time); if (st != null) s = Math.min(s, st); if (en != null) e = Math.max(e, en) } - if (!isFinite(s)) return '' - const m = (s + e) / 2; return m < 14 ? '☀️' : m < 20 ? '🌆' : '🌙' -} -function cellIcon (techId, iso) { const o = cellOcc(techId, iso); if (!o) return ''; return (o.hasReg ? cellPeriod(techId, iso) : '') + (o.hasGarde ? '🛡️' : '') } +// Fenêtre des shifts RÉGULIERS (la garde ne compte pas comme dispo offrable) +function cellRegWindow (techId, iso) { let s = Infinity; let e = -Infinity; for (const a of cellsOf(techId, iso)) { const t = tplByName.value[a.shift]; if (!t || t.on_call) continue; const st = hToNum(t.start_time); const en = hToNum(t.end_time); if (st != null) s = Math.min(s, st); if (en != null) e = Math.max(e, en) } return isFinite(s) ? { s, e } : null } +function cellPeriod (techId, iso) { const w = cellRegWindow(techId, iso); if (!w) return ''; const m = (w.s + w.e) / 2; return m < 14 ? '☀️' : m < 20 ? '🌆' : '🌙' } +// Libellé inline = icône période + intervalle début–fin (la référence) + 🛡️ si garde +function cellLabel (techId, iso) { const o = cellOcc(techId, iso); if (!o) return ''; const w = cellRegWindow(techId, iso); let s = o.hasReg ? cellPeriod(techId, iso) : ''; if (w) s += (s ? ' ' : '') + fmtH(w.s) + '–' + fmtH(w.e); if (o.hasGarde) s += ' 🛡️'; return s || '🛡️ garde' } const occCells = computed(() => { const m = {}; const ct = cellsByTechDay.value for (const techId in ct) for (const iso in ct[techId]) { @@ -603,7 +612,6 @@ th.clk, td.clk { cursor: pointer; } .tl-shift { position: absolute; top: 0; bottom: 0; background: #ccd2d8; border-radius: 1px; } /* fenêtre dispo = neutre */ .tl-shift.oncall { background: repeating-linear-gradient(45deg, #d7ccc8 0, #d7ccc8 2px, transparent 2px, transparent 4px); box-shadow: inset 0 0 0 1px #a1887f; } /* garde = hachuré (réserve, non offrable) */ .tl-blk { position: absolute; top: 0; bottom: 0; border-radius: 1px; opacity: .95; } /* occupé = trait coloré */ -.tl-noon { position: absolute; top: 0; bottom: 0; left: 50%; width: 1px; background: rgba(0, 0, 0, .10); } tr.paused .tech-col { color: #aaa; } tfoot .sum td { background: #fafafa; font-size: 11px; color: #555; font-weight: 600; } tfoot .sum .tech-col { background: #fafafa; }