Planification: menu de case court (2 raccourcis + slider en haut) — Appliquer toujours atteignable
- Menu réduit: 2 raccourcis (Normal 8–17, Soir 16–20) + slider d'ajustement remontés EN HAUT (près du clic) ; Appliquer dans la rangée du slider → reste visible même si la case est en bas (max-height 85vh + flip auto Quasar). Retrait de la longue liste des modèles. - quickShift(min,max) + applyWindow() factorisés. Shifts en place + Copier/Coller/Vider en icônes compactes sous le slider. Nettoyage cellCode/cellColor/codeByShift/colorByShift/addFromMenu inutilisés. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ece0ccb6ff
commit
9261692c7f
|
|
@ -253,32 +253,36 @@
|
|||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<q-menu v-model="menu.show" :target="menu.target" anchor="bottom left" self="top left">
|
||||
<q-list dense style="min-width:260px;user-select:none;-webkit-user-select:none">
|
||||
<q-item-label header>{{ menu.tech && menu.tech.name }} — {{ menu.day && menu.day.dnum }}</q-item-label>
|
||||
<q-item v-for="a in menuCellShifts" :key="'c' + a.shift">
|
||||
<q-item-section avatar><span class="code-chip" :style="chip(cellColor(a))">{{ cellCode(a) }}</span></q-item-section>
|
||||
<q-item-section>{{ a.shift_name || a.shift }} <span class="text-grey-6">{{ a.hours }}h</span></q-item-section>
|
||||
<q-item-section side><q-btn flat dense round size="sm" icon="close" color="grey-7" @click="removeShiftFromMenu(a)"><q-tooltip>Retirer</q-tooltip></q-btn></q-item-section>
|
||||
</q-item>
|
||||
<q-separator />
|
||||
<q-item-label header>Ajouter un shift</q-item-label>
|
||||
<q-item v-for="t in templates" :key="t.name" clickable v-close-popup @click="addFromMenu(t)"><q-item-section avatar><span class="code-chip" :style="chip(t.color)">{{ code(t) }}</span></q-item-section><q-item-section>{{ t.template_name }}</q-item-section></q-item>
|
||||
<q-item v-if="menuCellShifts.length" clickable v-close-popup @click="clearOne"><q-item-section class="text-grey-7">Libérer tout</q-item-section></q-item>
|
||||
<q-separator />
|
||||
<q-item clickable v-close-popup @click="copyFromMenu"><q-item-section avatar><q-icon name="content_copy" size="18px" /></q-item-section><q-item-section>Copier cette case</q-item-section></q-item>
|
||||
<q-item clickable v-close-popup @click="pasteFromMenu" :disable="!cellClipboard.length"><q-item-section avatar><q-icon name="content_paste" size="18px" /></q-item-section><q-item-section>Coller{{ cellClipboard.length ? ' (' + cellClipboard.length + ')' : '' }}</q-item-section></q-item>
|
||||
<q-separator />
|
||||
<q-item-label header>Ajuster l'horaire (glisser)</q-item-label>
|
||||
<div class="q-px-md q-pb-sm" style="width:260px" @click.stop @mousedown.stop>
|
||||
<q-menu v-model="menu.show" :target="menu.target" anchor="bottom left" self="top left" max-height="85vh">
|
||||
<q-list dense style="width:262px;user-select:none;-webkit-user-select:none">
|
||||
<q-item-label header class="q-py-xs">{{ menu.tech && menu.tech.name }} — {{ menu.day && menu.day.dnum }}</q-item-label>
|
||||
<!-- Raccourcis (près du clic) -->
|
||||
<div class="row q-gutter-xs q-px-sm q-pb-xs">
|
||||
<q-btn dense unelevated size="sm" color="primary" label="Normal 8–17" class="col" @click="quickShift(8, 17)" />
|
||||
<q-btn dense unelevated size="sm" color="deep-purple-5" label="Soir 16–20" class="col" @click="quickShift(16, 20)" />
|
||||
</div>
|
||||
<!-- Slider d'ajustement (en haut = proche du clic ; Appliquer dans la même rangée) -->
|
||||
<div class="q-px-md q-pb-sm" @click.stop @mousedown.stop>
|
||||
<q-range v-model="menuRange" :min="0" :max="24" :step="0.5" snap color="primary" class="q-mt-sm" />
|
||||
<div class="row items-center no-wrap q-gutter-sm q-mt-xs">
|
||||
<div class="row items-center no-wrap q-gutter-sm">
|
||||
<span class="text-caption text-weight-bold">{{ fmtH(menuRange.min) }}h–{{ fmtH(menuRange.max) }}h</span>
|
||||
<q-space />
|
||||
<q-toggle dense v-model="menuOnCall" :true-value="1" :false-value="0" label="Garde" color="brown" />
|
||||
<q-btn dense unelevated size="sm" color="primary" label="Appliquer" @click="applyMenuRange" />
|
||||
</div>
|
||||
</div>
|
||||
<q-separator />
|
||||
<!-- Shifts en place + actions compactes -->
|
||||
<q-item v-for="a in menuCellShifts" :key="'c' + a.shift" dense>
|
||||
<q-item-section>{{ a.shift_name || a.shift }} <span class="text-grey-6">{{ a.hours }}h</span></q-item-section>
|
||||
<q-item-section side><q-btn flat dense round size="sm" icon="close" color="grey-7" @click="removeShiftFromMenu(a)"><q-tooltip>Retirer</q-tooltip></q-btn></q-item-section>
|
||||
</q-item>
|
||||
<div class="row items-center q-px-sm q-py-xs q-gutter-sm">
|
||||
<q-btn flat dense size="sm" icon="content_copy" color="grey-8" @click="copyFromMenu"><q-tooltip>Copier la case</q-tooltip></q-btn>
|
||||
<q-btn flat dense size="sm" icon="content_paste" color="grey-8" :disable="!cellClipboard.length" @click="pasteFromMenu"><q-tooltip>Coller{{ cellClipboard.length ? ' (' + cellClipboard.length + ')' : '' }}</q-tooltip></q-btn>
|
||||
<q-space />
|
||||
<q-btn v-if="menuCellShifts.length" flat dense size="sm" icon="layers_clear" color="grey-8" label="Vider" @click="clearOne" />
|
||||
</div>
|
||||
</q-list>
|
||||
</q-menu>
|
||||
</q-page>
|
||||
|
|
@ -340,8 +344,6 @@ function dowOf (iso) { const [y, m, d] = iso.split('-').map(Number); return new
|
|||
const tplOptions = computed(() => templates.value.map(t => ({ label: t.template_name, value: t.name })))
|
||||
const techOptions = computed(() => techs.value.map(t => ({ label: t.name, value: t.id })))
|
||||
function code (t) { return (t.template_name || t.name || '?').trim()[0].toUpperCase() }
|
||||
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(() => {
|
||||
|
|
@ -350,8 +352,6 @@ const templatesRanked = computed(() => {
|
|||
})
|
||||
// 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' } }
|
||||
|
||||
// techs visibles (recherche + groupe + tri)
|
||||
|
|
@ -609,20 +609,22 @@ function selectBlock (ti, di) { const a = anchor.value; const t0 = Math.min(a.ti
|
|||
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])] }
|
||||
function maybeSelectRow (ti) { const ks = dayList.value.map(d => visibleTechs.value[ti].id + '|' + d.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])] }
|
||||
const menuCellShifts = computed(() => (menu.tech && menu.day) ? cellsOf(menu.tech.id, menu.day.iso) : [])
|
||||
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)' : '')
|
||||
// Applique une fenêtre [min,max] à la case du menu : trouve/crée un modèle auto-nommé puis remplace.
|
||||
async function applyWindow (min, max, oncall) {
|
||||
if (!menu.tech || !menu.day || max <= min) return
|
||||
const s = numToTime(min); const e = numToTime(max)
|
||||
const nm = fmtH(min) + 'h–' + fmtH(max) + 'h' + (oncall ? ' (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) { try { await roster.createTemplate({ template_name: nm, start_time: s + ':00', end_time: e + ':00', hours: calcHours(s, e), color: oncall ? '#f9a825' : '#1976d2', default_required: 1, on_call: oncall ? 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 quickShift (min, max) { return applyWindow(min, max, 0) }
|
||||
async function applyMenuRange () { return applyWindow(menuRange.value.min, menuRange.value.max, menuOnCall.value) }
|
||||
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
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user