From 969d5af9f900c233af17b0510494e75189c5ba82 Mon Sep 17 00:00:00 2001 From: louispaulb Date: Tue, 7 Jul 2026 12:56:17 -0400 Subject: [PATCH] =?UTF-8?q?planif:=20horaires=20(lot)=20=E2=80=94=20recher?= =?UTF-8?q?che/skill/tout-aucun=20;=20grille=202=20sem.=20par=20d=C3=A9fau?= =?UTF-8?q?t=20(s=C3=A9lecteur=20retir=C3=A9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .../shared/WeeklyScheduleEditor.vue | 28 +++++++++++++++++-- apps/ops/src/pages/PlanificationPage.vue | 5 ++-- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/apps/ops/src/components/shared/WeeklyScheduleEditor.vue b/apps/ops/src/components/shared/WeeklyScheduleEditor.vue index aa84405..becba0d 100644 --- a/apps/ops/src/components/shared/WeeklyScheduleEditor.vue +++ b/apps/ops/src/components/shared/WeeklyScheduleEditor.vue @@ -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 () {
-
Appliquer à ({{ selected.size }}/{{ techs.length }}) — clic pour (dé)cocher :
-
- +
+
Appliquer à {{ selected.size }}/{{ techs.length }}
+ + + +
+ + + +
+ Par compétence : + {{ sk }} +
+
+ {{ t.name }} +
Aucun tech pour « {{ techSearch }} »
@@ -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; } diff --git a/apps/ops/src/pages/PlanificationPage.vue b/apps/ops/src/pages/PlanificationPage.vue index 73893bb..8ae143c 100644 --- a/apps/ops/src/pages/PlanificationPage.vue +++ b/apps/ops/src/pages/PlanificationPage.vue @@ -44,7 +44,6 @@ Semaine suivante {{ boardView === 'kanban' ? 'Jour affiché (mode Jour)' : 'Début de la fenêtre (mode Semaine)' }} - Étendue de la grille Annuler (Ctrl+Z) Rétablir (Ctrl+Shift+Z) @@ -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