diff --git a/apps/ops/src/pages/PlanificationPage.vue b/apps/ops/src/pages/PlanificationPage.vue
index 9557009..9af830b 100644
--- a/apps/ops/src/pages/PlanificationPage.vue
+++ b/apps/ops/src/pages/PlanificationPage.vue
@@ -256,14 +256,27 @@
{{ menu.tech && menu.tech.name }} — {{ menu.day && menu.day.dnum }}
{{ cellCode(a) }}
- {{ a.shift_name || a.shift }} {{ cellHours(a) }}h
+ {{ a.shift_name || a.shift }} {{ a.hours }}h
Retirer
Ajouter un shift
{{ code(t) }}{{ t.template_name }}
-
Libérer tout
+
+ Copier cette case
+ Coller{{ cellClipboard.length ? ' (' + cellClipboard.length + ')' : '' }}
+
+ Ajuster l'horaire (glisser)
+
+
+
+ {{ fmtH(menuRange.min) }}h–{{ fmtH(menuRange.max) }}h
+
+
+
+
+
@@ -549,6 +562,7 @@ function applyTemplate (tm) {
// édition + sélection
const menu = reactive({ show: false, target: null, tech: null, day: null })
+const menuRange = ref({ min: 8, max: 16 }); const menuOnCall = ref(0)
function rect (sti, sdi, eti, edi) {
const t0 = Math.min(sti, eti), t1 = Math.max(sti, eti), d0 = Math.min(sdi, edi), d1 = Math.max(sdi, edi)
const out = []
@@ -566,7 +580,10 @@ function onCellClick (t, d, ev, ti, di) {
if (justDragged.value) { justDragged.value = false; return }
if (ev.shiftKey && anchor.value) { selectBlock(ti, di); return }
if (ev.ctrlKey || ev.metaKey) { const k = t.id + '|' + d.iso; selection.value = selSet.value.has(k) ? selection.value.filter(x => x !== k) : [...selection.value, k]; anchor.value = { ti, di }; return }
- selection.value = []; anchor.value = { ti, di }; menu.tech = t; menu.day = d; menu.target = ev.currentTarget; menu.show = true
+ selection.value = []; anchor.value = { ti, di }; menu.tech = t; menu.day = d; menu.target = ev.currentTarget
+ const wr = winOf(t.id, d.iso, false); const wg = winOf(t.id, d.iso, true); const w0 = wr || wg
+ menuRange.value = w0 ? { min: w0.s, max: w0.e } : { min: 8, max: 16 }; menuOnCall.value = wr ? 0 : (wg ? 1 : 0)
+ menu.show = true
}
function selectBlock (ti, di) { const a = anchor.value; const t0 = Math.min(a.ti, ti); const t1 = Math.max(a.ti, ti); const d0 = Math.min(a.di, di); const d1 = Math.max(a.di, di); const add = []; for (let i = t0; i <= t1; i++) for (let j = d0; j <= d1; j++) add.push(visibleTechs.value[i].id + '|' + dayList.value[j].iso); selection.value = [...new Set([...selection.value, ...add])] }
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])] }
@@ -575,6 +592,17 @@ const menuCellShifts = computed(() => (menu.tech && menu.day) ? cellsOf(menu.tec
function addFromMenu (tpl) { if (menu.tech && menu.day) { pushHistory(); addShift(menu.tech.id, menu.tech.name, menu.day.iso, tpl) } }
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
+function copyFromMenu () { if (!menu.tech || !menu.day) return; cellClipboard.value = cellsOf(menu.tech.id, menu.day.iso).map(a => a.shift); $q.notify({ type: 'positive', message: cellClipboard.value.length ? (cellClipboard.value.length + ' shift(s) copié(s) — ouvre une autre case puis Coller') : 'Case vide copiée (Coller la videra)' }) }
+function pasteFromMenu () { if (!menu.tech || !menu.day) return; pushHistory(); if (!cellClipboard.value.length) { clearLocal(menu.tech.id, menu.day.iso) } else { for (const name of cellClipboard.value) { const tpl = tplByName.value[name]; if (tpl) addShift(menu.tech.id, menu.tech.name, menu.day.iso, tpl) } } menu.show = false }
+async function applyMenuRange () {
+ if (!menu.tech || !menu.day) return
+ const s = numToTime(menuRange.value.min); const e = numToTime(menuRange.value.max)
+ const nm = fmtH(menuRange.value.min) + 'h–' + fmtH(menuRange.value.max) + 'h' + (menuOnCall.value ? ' (garde)' : '')
+ let tpl = templates.value.find(t => t.template_name === nm)
+ if (!tpl) { try { await roster.createTemplate({ template_name: nm, start_time: s + ':00', end_time: e + ':00', hours: calcHours(s, e), color: menuOnCall.value ? '#6d4c41' : '#1976d2', default_required: 1, on_call: menuOnCall.value ? 1 : 0 }); await refreshTemplates(); tpl = templates.value.find(t => t.template_name === nm) } catch (e2) { err(e2); return } }
+ if (tpl) { pushHistory(); setCellReplace(menu.tech.id, menu.tech.name, menu.day.iso, tpl); menu.show = false }
+}
function assignBulk (tpl) { pushHistory(); for (const k of selection.value) { const [tid, iso] = k.split('|'); const t = techs.value.find(x => x.id === tid); addShift(tid, t ? t.name : tid, iso, tpl) } selection.value = [] }
function clearBulk () { pushHistory(); for (const k of selection.value) { const [tid, iso] = k.split('|'); clearLocal(tid, iso) } selection.value = [] }
// Copier-coller une case (bâtir l'horaire vite) : copie les shifts de la 1re case sélectionnée → colle dans les autres