diff --git a/apps/ops/src/pages/PlanificationPage.vue b/apps/ops/src/pages/PlanificationPage.vue index 9af830b..a31deb3 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 }} + {{ t.template_name }} @@ -90,8 +90,9 @@
+ {{ cellClipboard.length }} copié(s) Légende : - + Ppause ·libre Jmodifié (non publié) @@ -307,6 +308,7 @@ const showDemand = ref(false) const drag = reactive({ on: false, ti: 0, di: 0, moved: false, base: [] }) const justDragged = ref(false) const selection = ref([]) +const activeCell = ref(null) // dernière case cliquée {id, name, iso} — pour copier/coller au clavier sans multi-sélection const anchor = ref(null) const demand = ref([]); const holidays = ref([]); const weekTemplates = ref([]) const history = ref([]); const future = ref([]) @@ -345,6 +347,8 @@ 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 || '')) }) +// Presets « nommés » seulement (Jour/Soir/…) → barre d'assignation + légende propres, même si des modèles auto existent +const presetTemplates = computed(() => templatesRanked.value.filter(t => /[a-gi-zA-GI-Z]/.test(t.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' } } @@ -578,6 +582,7 @@ function removeShift (techId, iso, shift) { assignments.value = assignments.valu function clearLocal (techId, iso) { assignments.value = assignments.value.filter(x => !(x.tech === techId && x.date === iso)) } function onCellClick (t, d, ev, ti, di) { if (justDragged.value) { justDragged.value = false; return } + activeCell.value = { id: t.id, name: t.name, iso: d.iso } // mémorise la case pour Cmd+C/V 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 @@ -607,8 +612,19 @@ function assignBulk (tpl) { pushHistory(); for (const k of selection.value) { co 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 const cellClipboard = ref([]) -function copyCell () { const k = selection.value[0]; if (!k) { $q.notify({ type: 'warning', message: 'Sélectionne une case à copier' }); return } const [tid, iso] = k.split('|'); cellClipboard.value = cellsOf(tid, iso).map(a => a.shift); $q.notify({ type: 'positive', message: cellClipboard.value.length ? (cellClipboard.value.length + ' shift(s) copié(s) — sélectionne des cases puis Coller') : 'Case vide copiée (Coller videra les cases)' }) } -function pasteCells () { if (!selection.value.length) { $q.notify({ type: 'warning', message: 'Sélectionne les cases cibles' }); return } pushHistory(); for (const k of selection.value) { const [tid, iso] = k.split('|'); const t = techs.value.find(x => x.id === tid); if (!cellClipboard.value.length) { clearLocal(tid, iso); continue } for (const name of cellClipboard.value) { const tpl = tplByName.value[name]; if (tpl) addShift(tid, t ? t.name : tid, iso, tpl) } } selection.value = [] } +function copyCell () { + const k = selection.value[0] || (activeCell.value && (activeCell.value.id + '|' + activeCell.value.iso)) + if (!k) { $q.notify({ type: 'warning', message: 'Clique ou sélectionne une case d\'abord' }); return } + const [tid, iso] = k.split('|'); cellClipboard.value = cellsOf(tid, iso).map(a => a.shift) + $q.notify({ type: 'positive', message: cellClipboard.value.length ? (cellClipboard.value.length + ' shift(s) copié(s) — sélectionne des cases puis Coller (ou Cmd+V)') : 'Case vide copiée (Coller videra les cases)' }) +} +function pasteCells () { + const targets = selection.value.length ? selection.value.slice() : (activeCell.value ? [activeCell.value.id + '|' + activeCell.value.iso] : []) + if (!targets.length) { $q.notify({ type: 'warning', message: 'Sélectionne les cases cibles' }); return } + pushHistory() + for (const k of targets) { const [tid, iso] = k.split('|'); const t = techs.value.find(x => x.id === tid); if (!cellClipboard.value.length) { clearLocal(tid, iso); continue } for (const name of cellClipboard.value) { const tpl = tplByName.value[name]; if (tpl) addShift(tid, t ? t.name : tid, iso, tpl) } } + if (selection.value.length) selection.value = [] +} async function togglePause (t) { try { const paused = !isPaused(t); await roster.pauseTechnician(t.id, paused); t.status = paused ? 'En pause' : 'Disponible'; $q.notify({ type: 'info', message: t.name + (paused ? ' en pause' : ' réactivé') }) } catch (e) { err(e) } } function err (e) { $q.notify({ type: 'negative', message: '' + (e.message || e) }) } @@ -616,8 +632,8 @@ function err (e) { $q.notify({ type: 'negative', message: '' + (e.message || e) function onKey (e) { const k = e.key.toLowerCase() if ((e.ctrlKey || e.metaKey) && k === 'z') { e.preventDefault(); if (e.shiftKey) redo(); else undo(); return } - if ((e.ctrlKey || e.metaKey) && k === 'c' && selection.value.length) { e.preventDefault(); copyCell(); return } - if ((e.ctrlKey || e.metaKey) && k === 'v' && selection.value.length) { e.preventDefault(); pasteCells() } + if ((e.ctrlKey || e.metaKey) && k === 'c' && (selection.value.length || activeCell.value)) { e.preventDefault(); menu.show = false; copyCell(); return } + if ((e.ctrlKey || e.metaKey) && k === 'v' && (selection.value.length || activeCell.value)) { e.preventDefault(); menu.show = false; pasteCells() } } 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() })