Planification: axe timeline adaptatif + intervalle texte + modèles triés par usage
#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) <noreply@anthropic.com>
This commit is contained in:
parent
1ab9f64b48
commit
72845e2057
|
|
@ -81,7 +81,7 @@
|
|||
|
||||
<q-banner v-if="selection.length" dense rounded class="bg-teal-1 text-teal-9 q-mb-sm">
|
||||
{{ selection.length }} cellule(s) — assigner :
|
||||
<q-btn v-for="t in templates" :key="t.name" dense unelevated size="sm" class="q-mx-xs" :style="chip(t.color)" :label="code(t)" @click="assignBulk(t)" />
|
||||
<q-btn v-for="t in templatesRanked" :key="t.name" dense unelevated size="sm" class="q-mx-xs" :style="chip(t.color)" :label="code(t)" @click="assignBulk(t)"><q-tooltip>{{ t.template_name }}</q-tooltip></q-btn>
|
||||
<q-btn dense flat size="sm" icon="layers_clear" label="Libérer" @click="clearBulk" />
|
||||
<q-btn dense flat size="sm" icon="close" label="Annuler" @click="selection = []" />
|
||||
</q-banner>
|
||||
|
|
@ -120,13 +120,12 @@
|
|||
<template v-if="cellsOf(t.id, d.iso).length">
|
||||
<div class="cell-chips">
|
||||
<span v-for="(a, ai) in cellsOf(t.id, d.iso)" :key="ai" class="code-chip" :style="chip(cellColor(a))">{{ cellCode(a) }}</span>
|
||||
<span v-if="cellOcc(t.id, d.iso)" class="cell-int">{{ cellIcon(t.id, d.iso) }}<template v-if="cellOcc(t.id, d.iso).bookableH > 0"> {{ cellOcc(t.id, d.iso).usedH }}/{{ cellOcc(t.id, d.iso).bookableH }}</template></span>
|
||||
<span v-if="cellOcc(t.id, d.iso)" class="cell-int">{{ cellLabel(t.id, d.iso) }}</span>
|
||||
</div>
|
||||
<div v-if="cellOcc(t.id, d.iso)" class="tl">
|
||||
<div v-for="(b, bi) in cellBands(t.id, d.iso)" :key="'b' + bi" class="tl-shift" :class="{ oncall: b.oncall }" :style="{ left: b.left, width: b.width }"></div>
|
||||
<div v-for="(b, bi) in cellOcc(t.id, d.iso).blocks" :key="'j' + bi" class="tl-blk" :style="blockStyle(b, cellOcc(t.id, d.iso).pct)"></div>
|
||||
<div class="tl-noon"></div>
|
||||
<q-tooltip class="bg-grey-9">{{ cellInterval(t.id, d.iso) }} · {{ cellOcc(t.id, d.iso).usedH }} h / {{ cellOcc(t.id, d.iso).shiftH }} h ({{ cellOcc(t.id, d.iso).pct }} %)</q-tooltip>
|
||||
<q-tooltip class="bg-grey-9">{{ cellInterval(t.id, d.iso) }}<template v-if="cellOcc(t.id, d.iso).bookableH"> · {{ cellOcc(t.id, d.iso).usedH }} h occupé / {{ cellOcc(t.id, d.iso).bookableH }} h offrable ({{ cellOcc(t.id, d.iso).pct }} %)</template></q-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<span v-else-if="isPaused(t)" class="code-chip" style="background:#eee;color:#999">P</span>
|
||||
|
|
@ -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; }
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user