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