planif: horaires (lot) — recherche/skill/tout-aucun ; grille 2 sem. par défaut (sélecteur retiré)

- WeeklyScheduleEditor (lot) : recherche par nom + chips « par compétence » (sélectionne les techs qui l'ont)
  + boutons Tout / Aucun. Passe t.skills depuis openSchedGenBulk. Avant : liste de chips sans filtre.
- Grille : 2 semaines (days=14) par défaut, sélecteur « Semaine/2 sem. » retiré (évitait les changements d'étendue accidentels).
- (Rappel : onScheduleApply recharge déjà la grille → les quarts générés en lot apparaissent après application.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
louispaulb 2026-07-07 12:56:17 -04:00
parent 712f113e29
commit 969d5af9f9
2 changed files with 27 additions and 6 deletions

View File

@ -29,6 +29,13 @@ fill(SCHEDULE_PRESETS[0].schedule)
watch(() => props.modelValue, (o) => { if (o) { fill(SCHEDULE_PRESETS[0].schedule); weeks.value = mode.value === 'recurring' ? 6 : 1; selected.value = new Set((props.techs || []).map(t => t.id)) } })
watch(mode, (m) => { weeks.value = m === 'recurring' ? 6 : 1 })
function toggleTech (id) { const s = new Set(selected.value); s.has(id) ? s.delete(id) : s.add(id); selected.value = s }
// Sélection des techs (mode lot) : recherche par nom · sélection par compétence · tout / aucun
const techSearch = ref('')
const allSkills = computed(() => { const s = new Set(); for (const t of (props.techs || [])) for (const sk of (t.skills || [])) s.add(sk); return [...s].sort() })
const filteredTechs = computed(() => { const q = techSearch.value.trim().toLowerCase(); return q ? (props.techs || []).filter(t => String(t.name || '').toLowerCase().includes(q)) : (props.techs || []) })
function selectAllShown () { const s = new Set(selected.value); for (const t of filteredTechs.value) s.add(t.id); selected.value = s }
function selectNone () { selected.value = new Set() }
function selectBySkill (sk) { const s = new Set(selected.value); for (const t of (props.techs || [])) if ((t.skills || []).includes(sk)) s.add(t.id); selected.value = s }
function dayHours (d) { if (!d || !d.on) return 0; const [h1, m1] = d.start.split(':').map(Number); const [h2, m2] = d.end.split(':').map(Number); let mn = (h2 * 60 + m2) - (h1 * 60 + m1); if (mn < 0) mn += 1440; return Math.round(mn / 60 * 10) / 10 }
const totalDays = () => DAYS.filter(([k]) => sched[k]?.on).length
@ -59,11 +66,24 @@ function submit () {
</q-btn-toggle>
<!-- Mode LOT : choisir les techniciens à qui appliquer le modèle (tous cochés par défaut) -->
<div v-if="bulk">
<div class="text-caption text-grey-7 q-mb-xs">Appliquer à ({{ selected.size }}/{{ techs.length }}) clic pour ()cocher :</div>
<div class="row q-gutter-xs" style="max-height:96px;overflow:auto">
<span v-for="t in techs" :key="t.id" class="wse-tech" :class="{ on: selected.has(t.id) }" @click="toggleTech(t.id)">
<div class="row items-center q-mb-xs">
<div class="text-caption text-grey-7">Appliquer à <b>{{ selected.size }}</b>/{{ techs.length }}</div>
<q-space />
<q-btn flat dense no-caps size="sm" color="primary" label="Tout" @click="selectAllShown" />
<q-btn flat dense no-caps size="sm" color="grey-7" label="Aucun" @click="selectNone" />
</div>
<q-input v-model="techSearch" dense outlined clearable placeholder="Chercher un nom…" class="q-mb-xs">
<template #prepend><q-icon name="search" size="18px" /></template>
</q-input>
<div v-if="allSkills.length" class="row q-gutter-xs q-mb-xs items-center">
<span class="text-caption text-grey-6">Par compétence :</span>
<span v-for="sk in allSkills" :key="sk" class="wse-skill" @click="selectBySkill(sk)"><q-icon name="add" size="11px" />{{ sk }}</span>
</div>
<div class="row q-gutter-xs" style="max-height:120px;overflow:auto">
<span v-for="t in filteredTechs" :key="t.id" class="wse-tech" :class="{ on: selected.has(t.id) }" @click="toggleTech(t.id)">
<q-icon :name="selected.has(t.id) ? 'check' : 'add'" size="12px" />{{ t.name }}
</span>
<div v-if="!filteredTechs.length" class="text-caption text-grey-5 q-pa-xs">Aucun tech pour « {{ techSearch }} »</div>
</div>
</div>
<!-- Modèles (1 clic remplit le formulaire) -->
@ -113,4 +133,6 @@ function submit () {
.wse-repos { color: #94a3b8; font-style: italic; font-size: 12.5px; margin-left: 8px; }
.wse-tech { display: inline-flex; align-items: center; gap: 3px; padding: 2px 9px; border: 1.5px solid #cbd5e1; border-radius: 12px; font-size: 12px; font-weight: 600; color: #64748b; background: #fff; cursor: pointer; user-select: none; }
.wse-tech.on { border-color: #6366f1; background: #eef2ff; color: #4338ca; }
.wse-skill { display: inline-flex; align-items: center; gap: 2px; padding: 2px 8px; border: 1px dashed #94a3b8; border-radius: 12px; font-size: 11px; font-weight: 600; color: #475569; background: #f8fafc; cursor: pointer; user-select: none; }
.wse-skill:hover { border-color: #6366f1; color: #4338ca; background: #eef2ff; }
</style>

View File

@ -44,7 +44,6 @@
<q-btn dense flat icon="keyboard_double_arrow_right" @click="navWeek(1)"><q-tooltip>Semaine suivante</q-tooltip></q-btn>
</q-btn-group>
<q-input dense outlined type="date" v-model="boardDate" style="width:150px"><q-tooltip>{{ boardView === 'kanban' ? 'Jour affiché (mode Jour)' : 'Début de la fenêtre (mode Semaine)' }}</q-tooltip></q-input>
<q-select v-show="boardView === 'grid'" dense outlined v-model="days" :options="[{ label: 'Semaine', value: 7 }, { label: '2 sem.', value: 14 }]" style="width:108px" emit-value map-options @update:model-value="onDaysChange"><q-tooltip>Étendue de la grille</q-tooltip></q-select>
<q-btn dense flat round icon="undo" :disable="!history.length" @click="undo"><q-tooltip>Annuler (Ctrl+Z)</q-tooltip></q-btn>
<q-btn dense flat round icon="redo" :disable="!future.length" @click="redo"><q-tooltip>Rétablir (Ctrl+Shift+Z)</q-tooltip></q-btn>
<q-separator vertical class="q-mx-xs" />
@ -2065,7 +2064,7 @@ const unassignedByDay = computed(() => {
const dailyStats = ref([])
const solverStats = ref(null)
const loading = ref(false); const generating = ref(false); const publishing = ref(false); const applying = ref(false)
const days = ref(7)
const days = ref(14) // 2 semaines par défaut (sélecteur retiré pour éviter les changements d'étendue accidentels)
const start = ref(thisMonday()) // défaut = semaine COURANTE (cohérent avec « Auj. ») pas la semaine suivante
const lastWeek = reactive({ start: start.value, days: days.value })
const showDemand = ref(false)
@ -2646,7 +2645,7 @@ async function applyWeekPreset (t, dows, min, max) {
// Génération de quarts HEBDO par tech (modèles + N semaines) écrit DIRECTEMENT les Shift Assignment (Publié).
const schedGenOpen = ref(false); const schedGenTechs = ref([])
function openSchedGen (t) { schedGenTechs.value = [{ id: t.id, name: t.name }]; skillMenuShown.value = false; schedGenOpen.value = true } // 1 tech
function openSchedGenBulk () { schedGenTechs.value = (visibleTechs.value || []).map(t => ({ id: t.id, name: t.name })); schedGenOpen.value = true } // LOT : tous les techs visibles
function openSchedGenBulk () { schedGenTechs.value = (visibleTechs.value || []).map(t => ({ id: t.id, name: t.name, skills: t.skills || [] })); schedGenOpen.value = true } // LOT : tous les techs visibles (+ compétences pour filtrer)
const _hmNum = (s) => { const [h, m] = String(s || '').split(':').map(Number); return (h || 0) + (m || 0) / 60 }
async function onScheduleApply ({ schedule, weeks, techIds, mode }) {
const targets = (schedGenTechs.value || []).filter(t => (techIds || []).includes(t.id)); if (!targets.length) return