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 :
- {{ code(t) }}{{ t.template_name }}
+ {{ code(t) }}{{ t.template_name }}
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() })