feat(planif): functional dispatch — single-day suggest, one overdue chip, sick-tech redistribute, bulk shift gen
Making the auto-dispatch match the real workflow:
- SINGLE selected day: suggestWindow is now one day (suggestDay), not
today+tomorrow. openSuggest defaults it to the board's focus day
(mobileSelIso/kbSelIso/today); config has a day picker (Aujourd'hui /
Demain / date). Overdue + undated jobs are pulled onto that day.
Re-simulating today is fine (non-destructive; apply is explicit).
- ONE "⏰ en retard" chip: assignDates collapses all past dates into a
single overdue chip (was one per date); pool filter handles __overdue__.
- SICK/absent tech: a "sick" button per review group deselects the tech
and re-optimizes → the solver redistributes their jobs across the rest
(verified: removing Anthony reassigned his jobs, Gilles picked up).
- BULK shift generation (A): WeeklyScheduleEditor gains multi-tech mode
(tech chips, all on by default); "Générer les horaires (lot)" in the
Outils + mobile menus applies a template to N techs × N weeks at once.
Verified: overdue collapses to "en retard 13"; day selector + single-day
plan (13 jobs); sick redistributes; bulk shows 56 techs / "Générer ×56".
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b1ff46e8c1
commit
c267f9a3dd
|
|
@ -6,24 +6,37 @@
|
|||
* + « nombre de semaines » pour AUTOMATISER (répéter le patron sur N semaines).
|
||||
* Émet 'apply' { schedule:[{dow,on,start,end}] (Lun..Dim), weeks } → le parent écrit les Shift Assignment.
|
||||
*/
|
||||
import { reactive, ref, watch } from 'vue'
|
||||
import { reactive, ref, watch, computed } from 'vue'
|
||||
import { SCHEDULE_PRESETS } from 'src/composables/useHelpers'
|
||||
|
||||
const props = defineProps({ modelValue: { type: Boolean, default: false }, techName: { type: String, default: '' } })
|
||||
const props = defineProps({
|
||||
modelValue: { type: Boolean, default: false },
|
||||
techName: { type: String, default: '' },
|
||||
techs: { type: Array, default: () => [] }, // [{id,name}] — si >1 : sélection multi (application en LOT)
|
||||
})
|
||||
const emit = defineEmits(['update:modelValue', 'apply'])
|
||||
|
||||
const DAYS = [['mon', 'Lun'], ['tue', 'Mar'], ['wed', 'Mer'], ['thu', 'Jeu'], ['fri', 'Ven'], ['sat', 'Sam'], ['sun', 'Dim']]
|
||||
const sched = reactive({})
|
||||
const weeks = ref(1)
|
||||
const selected = ref(new Set()) // techIds cochés (mode lot)
|
||||
const bulk = computed(() => (props.techs || []).length > 1)
|
||||
const title = computed(() => bulk.value ? ((props.techs || []).length + ' techniciens') : (props.techName || (props.techs[0] && props.techs[0].name) || ''))
|
||||
|
||||
function fill (preset) { for (const [k] of DAYS) { const s = preset[k]; sched[k] = s ? { on: true, start: s.start, end: s.end } : { on: false, start: '08:00', end: '16:00' } } }
|
||||
fill(SCHEDULE_PRESETS[0].schedule)
|
||||
watch(() => props.modelValue, (o) => { if (o) { fill(SCHEDULE_PRESETS[0].schedule); weeks.value = 1 } })
|
||||
watch(() => props.modelValue, (o) => { if (o) { fill(SCHEDULE_PRESETS[0].schedule); weeks.value = 1; selected.value = new Set((props.techs || []).map(t => t.id)) } })
|
||||
function toggleTech (id) { const s = new Set(selected.value); s.has(id) ? s.delete(id) : s.add(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
|
||||
const totalHours = () => DAYS.reduce((s, [k]) => s + dayHours(sched[k]), 0)
|
||||
function submit () { emit('apply', { schedule: DAYS.map(([k]) => ({ dow: k, ...sched[k] })), weeks: Math.max(1, Math.min(12, Number(weeks.value) || 1)) }); emit('update:modelValue', false) }
|
||||
function submit () {
|
||||
const techIds = bulk.value ? [...selected.value] : (props.techs[0] ? [props.techs[0].id] : [])
|
||||
if (!techIds.length) return
|
||||
emit('apply', { schedule: DAYS.map(([k]) => ({ dow: k, ...sched[k] })), weeks: Math.max(1, Math.min(12, Number(weeks.value) || 1)), techIds })
|
||||
emit('update:modelValue', false)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -31,11 +44,20 @@ function submit () { emit('apply', { schedule: DAYS.map(([k]) => ({ dow: k, ...s
|
|||
<q-card style="width:600px;max-width:96vw">
|
||||
<q-card-section class="row items-center q-pb-sm">
|
||||
<q-icon name="event_note" color="primary" size="24px" class="q-mr-sm" />
|
||||
<div class="text-h6">Horaire — {{ techName }}</div>
|
||||
<div class="text-h6">Horaire — {{ title }}</div>
|
||||
<q-space /><q-btn flat round dense icon="close" @click="emit('update:modelValue', false)" />
|
||||
</q-card-section>
|
||||
<q-separator />
|
||||
<q-card-section class="q-gutter-sm">
|
||||
<!-- 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 (dé)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)">
|
||||
<q-icon :name="selected.has(t.id) ? 'check' : 'add'" size="12px" />{{ t.name }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Modèles (1 clic remplit le formulaire) -->
|
||||
<div class="row q-gutter-xs">
|
||||
<span v-for="p in SCHEDULE_PRESETS" :key="p.key" class="wse-preset" @click="fill(p.schedule)">{{ p.label }}</span>
|
||||
|
|
@ -63,7 +85,7 @@ function submit () { emit('apply', { schedule: DAYS.map(([k]) => ({ dow: k, ...s
|
|||
</q-card-section>
|
||||
<q-card-actions align="right" class="q-px-md q-pb-md">
|
||||
<q-btn flat no-caps label="Annuler" color="grey-7" @click="emit('update:modelValue', false)" />
|
||||
<q-btn unelevated no-caps color="primary" icon="event_available" :label="'Générer ' + (weeks > 1 ? weeks + ' semaines' : 'la semaine')" @click="submit" />
|
||||
<q-btn unelevated no-caps color="primary" icon="event_available" :disable="bulk && !selected.size" :label="'Générer' + (bulk ? ' ×' + selected.size + ' tech' : '') + (weeks > 1 ? ' · ' + weeks + ' sem.' : '')" @click="submit" />
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
|
@ -80,4 +102,6 @@ function submit () { emit('apply', { schedule: DAYS.map(([k]) => ({ dow: k, ...s
|
|||
.wse-sep { color: #94a3b8; }
|
||||
.wse-h { color: #64748b; font-size: 12px; font-weight: 600; margin-left: auto; }
|
||||
.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; }
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
<q-item tag="label" clickable><q-item-section avatar><q-icon name="sms" /></q-item-section><q-item-section>Notifier par SMS</q-item-section><q-item-section side><q-toggle v-model="notifySms" dense /></q-item-section></q-item>
|
||||
<q-item clickable v-close-popup @click="openCreateJob"><q-item-section avatar><q-icon name="add_task" color="teal-7" /></q-item-section><q-item-section>Créer un job</q-item-section></q-item>
|
||||
<q-item clickable v-close-popup @click="showSuggestSlots = true"><q-item-section avatar><q-icon name="event_available" color="teal-8" /></q-item-section><q-item-section>Trouver un créneau</q-item-section></q-item>
|
||||
<q-item clickable v-close-popup @click="openSchedGenBulk"><q-item-section avatar><q-icon name="event_note" color="primary" /></q-item-section><q-item-section>Générer les horaires (lot)</q-item-section></q-item>
|
||||
<q-item clickable v-close-popup @click="openLegacyPush"><q-item-section avatar><q-icon name="sync_alt" color="deep-orange" /></q-item-section><q-item-section>Publier au legacy</q-item-section></q-item>
|
||||
<q-item clickable v-close-popup @click="() => guard(loadWeek)"><q-item-section avatar><q-icon name="refresh" /></q-item-section><q-item-section>Rafraîchir</q-item-section></q-item>
|
||||
</q-list>
|
||||
|
|
@ -96,6 +97,7 @@
|
|||
</q-list>
|
||||
</q-menu>
|
||||
</q-item>
|
||||
<q-item clickable v-close-popup @click="openSchedGenBulk"><q-item-section avatar><q-icon name="event_note" color="primary" /></q-item-section><q-item-section>Générer les horaires (lot)<q-item-label caption>modèle → plusieurs techs, N semaines</q-item-label></q-item-section></q-item>
|
||||
<q-item clickable v-close-popup @click="openGarde"><q-item-section avatar><q-icon name="shield" color="brown" /></q-item-section><q-item-section>Rotation de garde</q-item-section></q-item>
|
||||
<q-separator />
|
||||
<q-item clickable v-close-popup @click="openTeamEditor"><q-item-section avatar><q-icon name="speed" /></q-item-section><q-item-section>Cadence équipe</q-item-section></q-item>
|
||||
|
|
@ -1018,7 +1020,14 @@
|
|||
<q-input v-model.number="solverOpts.speedKmh" type="number" dense outlined style="width:86px" label="km/h" :min="20" :max="90"><q-tooltip>Vitesse de repli (nœuds sans coordonnées)</q-tooltip></q-input>
|
||||
<q-input v-model.number="solverOpts.maxSeconds" type="number" dense outlined style="width:92px" label="Calcul s" :min="2" :max="30"><q-tooltip>Budget de calcul par jour</q-tooltip></q-input>
|
||||
</div>
|
||||
<div class="text-caption q-mb-sm" style="color:#475569;background:#eef2ff;border-radius:6px;padding:4px 8px"><q-icon name="date_range" size="14px" class="q-mr-xs" color="primary" />Répartit sur : <b>{{ suggestWindowLabel }}</b><span class="text-grey-6"> — défaut aujourd'hui + demain ; filtre par date (chips de la liste) pour cibler d'autres jours. Les jobs datés au-delà sont laissés pour plus tard.</span></div>
|
||||
<!-- JOUR CIBLÉ (une seule journée) : les jobs en retard / sans date sont tirés sur ce jour. Re-simuler aujourd'hui = OK (non destructif). -->
|
||||
<div class="row items-center q-mb-sm" style="gap:6px;color:#475569;background:#eef2ff;border-radius:6px;padding:5px 8px">
|
||||
<q-icon name="event" size="16px" color="primary" /><span class="text-caption text-weight-medium">Répartir pour :</span>
|
||||
<q-btn dense unelevated size="sm" no-caps :color="suggestDay === todayISO() ? 'primary' : 'grey-4'" :text-color="suggestDay === todayISO() ? 'white' : 'grey-8'" label="Aujourd'hui" @click="suggestDay = todayISO()" />
|
||||
<q-btn dense unelevated size="sm" no-caps :color="suggestDay === tomorrowISO() ? 'primary' : 'grey-4'" :text-color="suggestDay === tomorrowISO() ? 'white' : 'grey-8'" label="Demain" @click="suggestDay = tomorrowISO()" />
|
||||
<q-input dense outlined type="date" v-model="suggestDay" style="width:150px" />
|
||||
<q-space /><span class="text-caption text-grey-6">jobs en retard/sans date → tirés sur ce jour</span>
|
||||
</div>
|
||||
<div class="row items-center q-mb-xs" style="gap:4px">
|
||||
<span class="text-caption text-grey-7 q-mr-xs">Rapide :</span>
|
||||
<q-btn dense flat size="sm" no-caps label="Tous" @click="suggestSelectTechs('all')" />
|
||||
|
|
@ -1085,6 +1094,8 @@
|
|||
<q-btn v-if="!g.placeholder" flat dense round size="sm" :icon="techHomes[g.techId] ? 'home' : 'business'" :color="techHomes[g.techId] ? 'teal-7' : 'blue-grey-4'" @click="openHomePicker(techById[g.techId] || { id: g.techId, name: g.techName })"><q-tooltip>Départ : {{ techHomes[g.techId] ? ('domicile — ' + (techHomes[g.techId].address || 'défini')) : 'bureau TARGO (défaut)' }} — cliquer pour définir/modifier le domicile</q-tooltip></q-btn>
|
||||
<!-- Éditer / réordonner les compétences du tech directement depuis l'optimiseur (même éditeur : chips ★ + glisser priorité) -->
|
||||
<q-btn v-if="!g.placeholder" flat dense round size="sm" icon="edit" color="grey-6" @click="openSkillEditorFor(g.techId, $event)"><q-tooltip>Compétences & niveaux de {{ g.techName }} — glisser pour l'ordre de priorité</q-tooltip></q-btn>
|
||||
<!-- Tech malade/absent → le retire ET redistribue ses jobs équitablement sur les autres (ré-optimise) -->
|
||||
<q-btn v-if="!g.placeholder" flat dense round size="sm" icon="sick" color="deep-orange-4" @click="markTechSick(g)"><q-tooltip>{{ g.techName }} malade / absent — retirer et redistribuer ses {{ g.jobs }} job(s) sur les autres</q-tooltip></q-btn>
|
||||
<!-- Assigner / fusionner TOUTE la file (marche depuis un placeholder → vrai tech, ou pour fusionner 2 files) -->
|
||||
<q-btn flat dense round size="sm" :icon="g.placeholder ? 'person_add' : 'drive_file_move'" :color="g.placeholder ? 'amber-9' : 'grey-7'"><q-tooltip>{{ g.placeholder ? 'Assigner cette tournée à un tech' : ('Déplacer / fusionner tous les jobs de ' + g.techName) }}</q-tooltip>
|
||||
<q-menu anchor="bottom right" self="top right"><q-list dense style="min-width:250px;max-height:42vh;overflow:auto">
|
||||
|
|
@ -1733,8 +1744,8 @@
|
|||
:technicians="techs" :external-tags="tagCatalog" :external-get-color="getTagColor" @created="onJobCreated" />
|
||||
<!-- « Trouver un créneau » → chips compétence (durée par défaut) + adresse → choisit tech+date+heure puis pré-remplit la création -->
|
||||
<SuggestSlotsDialog v-model="showSuggestSlots" :skills="slotSkills" @select="onSlotSelected" />
|
||||
<!-- Génération de quarts hebdo par tech (modèles + N semaines) → écrit les Shift Assignment -->
|
||||
<WeeklyScheduleEditor v-model="schedGenOpen" :tech-name="schedGenTech && schedGenTech.name" @apply="onScheduleApply" />
|
||||
<!-- Génération de quarts hebdo (modèles + N semaines + LOT multi-techs) → écrit les Shift Assignment -->
|
||||
<WeeklyScheduleEditor v-model="schedGenOpen" :techs="schedGenTechs" :tech-name="schedGenTechs[0] && schedGenTechs[0].name" @apply="onScheduleApply" />
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
|
|
@ -2336,27 +2347,30 @@ async function applyWeekPreset (t, dows, min, max) {
|
|||
$q.notify({ type: 'positive', message: t.name + ' : horaire appliqué (semaine affichée) — pense à Publier', timeout: 2500 })
|
||||
}
|
||||
// ── Génération de quarts HEBDO par tech (modèles + N semaines) — écrit DIRECTEMENT les Shift Assignment (Publié). ──
|
||||
const schedGenOpen = ref(false); const schedGenTech = ref(null)
|
||||
function openSchedGen (t) { schedGenTech.value = { id: t.id, name: t.name }; skillMenuShown.value = false; schedGenOpen.value = true }
|
||||
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
|
||||
const _hmNum = (s) => { const [h, m] = String(s || '').split(':').map(Number); return (h || 0) + (m || 0) / 60 }
|
||||
async function onScheduleApply ({ schedule, weeks }) {
|
||||
const t = schedGenTech.value; if (!t) return
|
||||
async function onScheduleApply ({ schedule, weeks, techIds }) {
|
||||
const targets = (schedGenTechs.value || []).filter(t => (techIds || []).includes(t.id)); if (!targets.length) return
|
||||
let created = 0, failed = 0; const tplByKey = {}
|
||||
const base = start.value // lundi de la semaine affichée
|
||||
for (let w = 0; w < weeks; w++) {
|
||||
for (let i = 0; i < 7; i++) {
|
||||
const day = schedule[i]; if (!day || !day.on) continue
|
||||
const sh = _hmNum(day.start), eh = _hmNum(day.end); if (!(eh > sh)) { failed++; continue }
|
||||
const key = sh + '-' + eh; let tpl = tplByKey[key]
|
||||
if (!tpl) { tpl = await ensureWindowTpl(sh, eh); tplByKey[key] = tpl }
|
||||
if (!tpl) { failed++; continue }
|
||||
const iso = addDaysISO(base, w * 7 + i)
|
||||
try { const r = await roster.createShift({ tech: t.id, tech_name: t.name, date: iso, shift: tpl.name, hours: tpl.hours || calcHours(day.start, day.end) }); if (r && (r.ok || r.existed)) created++; else failed++ } catch (e) { failed++ }
|
||||
for (const t of targets) {
|
||||
for (let w = 0; w < weeks; w++) {
|
||||
for (let i = 0; i < 7; i++) {
|
||||
const day = schedule[i]; if (!day || !day.on) continue
|
||||
const sh = _hmNum(day.start), eh = _hmNum(day.end); if (!(eh > sh)) { failed++; continue }
|
||||
const key = sh + '-' + eh; let tpl = tplByKey[key]
|
||||
if (!tpl) { tpl = await ensureWindowTpl(sh, eh); tplByKey[key] = tpl }
|
||||
if (!tpl) { failed++; continue }
|
||||
const iso = addDaysISO(base, w * 7 + i)
|
||||
try { const r = await roster.createShift({ tech: t.id, tech_name: t.name, date: iso, shift: tpl.name, hours: tpl.hours || calcHours(day.start, day.end) }); if (r && (r.ok || r.existed)) created++; else failed++ } catch (e) { failed++ }
|
||||
}
|
||||
}
|
||||
}
|
||||
try { await loadWeek() } catch (e) {} // rafraîchit la grille (semaine affichée)
|
||||
$q.notify({ type: created ? 'positive' : 'warning', icon: 'event_available', message: created + ' quart(s) publié(s) pour ' + t.name + (weeks > 1 ? ' (' + weeks + ' sem.)' : '') + (failed ? ' · ' + failed + ' ignoré(s)' : ''), timeout: 3500 })
|
||||
schedGenTech.value = null
|
||||
$q.notify({ type: created ? 'positive' : 'warning', icon: 'event_available', message: created + ' quart(s) publié(s) · ' + targets.length + ' tech' + (weeks > 1 ? ' (' + weeks + ' sem.)' : '') + (failed ? ' · ' + failed + ' ignoré(s)' : ''), timeout: 4000 })
|
||||
schedGenTechs.value = []
|
||||
}
|
||||
function skillEffOf (t, sk) { const e = t.skill_eff && t.skill_eff[sk]; return (e != null && e !== '') ? Number(e) : (Number(t.efficiency) || 1) } // facteur (pour le calcul de priorité)
|
||||
// Efficacité saisie en % de PERFORMANCE : + = plus VITE (moins de temps) · − = plus LENT. Conversion ↔ facteur.
|
||||
|
|
@ -2525,7 +2539,15 @@ const assignDateFilter = ref([]) // isos retenus (vide = toutes les dates)
|
|||
const assignDates = computed(() => {
|
||||
const t = todayISO(); const m = {}
|
||||
for (const j of assignPanel.jobs) { const d = (j.scheduled_date && j.scheduled_date !== 'Sans date') ? j.scheduled_date : 'Sans date'; m[d] = (m[d] || 0) + 1 }
|
||||
return Object.entries(m).map(([iso, n]) => ({ iso, n, label: iso === 'Sans date' ? 'Sans date' : fmtDueLabel(iso), overdue: iso !== 'Sans date' && iso < t })).sort((a, b) => String(a.iso).localeCompare(String(b.iso)))
|
||||
// Toutes les dates PASSÉES → UN SEUL chip « ⏰ en retard » (déclutter). Les jours futurs + « Sans date » restent séparés.
|
||||
const out = []; let overdueN = 0
|
||||
for (const [iso, n] of Object.entries(m)) {
|
||||
if (iso !== 'Sans date' && iso < t) { overdueN += n; continue }
|
||||
out.push({ iso, n, label: iso === 'Sans date' ? 'Sans date' : fmtDueLabel(iso), overdue: false })
|
||||
}
|
||||
out.sort((a, b) => String(a.iso).localeCompare(String(b.iso)))
|
||||
if (overdueN) out.unshift({ iso: '__overdue__', n: overdueN, label: '⏰ en retard', overdue: true })
|
||||
return out
|
||||
})
|
||||
function toggleAssignDate (iso) { const s = new Set(assignDateFilter.value); s.has(iso) ? s.delete(iso) : s.add(iso); assignDateFilter.value = [...s] }
|
||||
// Depuis l'aperçu « jobs sur la colonne du jour » → ouvre le panneau « À assigner » filtré sur ce jour.
|
||||
|
|
@ -2533,7 +2555,7 @@ async function openDayInPanel (iso) { await openAssignPanel(); assignSort.value
|
|||
const assignJobsFiltered = computed(() => {
|
||||
let arr = assignPanel.jobs
|
||||
const f = assignTypeFilter.value; if (f.length) { const set = new Set(f); arr = arr.filter(j => set.has(j.required_skill || 'autre')) }
|
||||
const df = assignDateFilter.value; if (df.length) { const ds = new Set(df); arr = arr.filter(j => ds.has((j.scheduled_date && j.scheduled_date !== 'Sans date') ? j.scheduled_date : 'Sans date')) }
|
||||
const df = assignDateFilter.value; if (df.length) { const ds = new Set(df); const t = todayISO(); arr = arr.filter(j => { const d = (j.scheduled_date && j.scheduled_date !== 'Sans date') ? j.scheduled_date : 'Sans date'; return ds.has(d) || (ds.has('__overdue__') && d !== 'Sans date' && d < t) }) } // '__overdue__' = toutes dates passées
|
||||
return arr
|
||||
})
|
||||
function toggleAssignType (k) { const i = assignTypeFilter.value.indexOf(k); if (i >= 0) assignTypeFilter.value.splice(i, 1); else assignTypeFilter.value.push(k) }
|
||||
|
|
@ -4313,23 +4335,19 @@ function buildSuggestion () {
|
|||
suggestDlg.plan = plan
|
||||
}
|
||||
function openSuggest () {
|
||||
suggestDay.value = mobileSelIso.value || kbSelIso.value || todayISO() // jour ciblé = celui sélectionné au tableau (défaut auj.)
|
||||
suggestDlg.fromSel = selectedNames.value.length > 0
|
||||
// Étape « disponibilité » d'abord : défaut = techs AVEC un quart dans la fenêtre (= disponibles) ; sinon tous les visibles.
|
||||
// Étape « disponibilité » d'abord : défaut = techs AVEC un quart CE JOUR (= disponibles) ; sinon tous les visibles.
|
||||
const techs = visibleTechs.value || []
|
||||
const withShift = techs.filter(t => (dayList.value || []).some(d => hasShiftDay(t.id, d.iso)))
|
||||
const withShift = techs.filter(t => hasShiftDay(t.id, suggestDay.value))
|
||||
const base = withShift.length ? withShift : techs
|
||||
const sel = {}; base.forEach(t => { sel[t.id] = true }); suggestDlg.techSel = sel
|
||||
suggestDlg.plan = []; suggestDlg.mode = 'config'; suggestDlg.open = true
|
||||
}
|
||||
const suggestSelCount = computed(() => Object.values(suggestDlg.techSel).filter(Boolean).length)
|
||||
// Fenêtre du dispatch auto : dates SÉLECTIONNÉES (chips) sinon aujourd'hui + demain (max) — on ne planifie pas loin (techs malades imprévus).
|
||||
const suggestWindow = computed(() => {
|
||||
const isos = (dayList.value || []).map(d => d.iso); const t = todayISO(); const tmrw = tomorrowISO()
|
||||
const sel = (assignDateFilter.value || []).filter(iso => isos.includes(iso))
|
||||
let win = sel.length ? [...sel].sort() : isos.filter(iso => iso === t || iso === tmrw)
|
||||
if (!win.length) win = isos.filter(iso => iso >= t).slice(0, 2) // repli : 2 premiers jours futurs de la grille
|
||||
return win
|
||||
})
|
||||
// Dispatch auto = UNE SEULE journée (celle sélectionnée). Les jobs en retard / sans date sont tirés sur CE jour. Non destructif : on revoit avant d'appliquer (re-simuler aujourd'hui = OK).
|
||||
const suggestDay = ref('')
|
||||
const suggestWindow = computed(() => [suggestDay.value || todayISO()])
|
||||
const windowDayLabel = iso => iso === todayISO() ? "Aujourd'hui" : (iso === tomorrowISO() ? 'Demain' : (fmtDueLabel(iso) || iso))
|
||||
const suggestWindowLabel = computed(() => {
|
||||
const w = suggestWindow.value; if (!w.length) return '—'
|
||||
|
|
@ -4375,6 +4393,13 @@ async function runSuggestion () {
|
|||
loadLegTimes().catch(() => {}) // temps de route réels entre arrêts (OSRM, non bloquant)
|
||||
}
|
||||
// Ré-optimiser depuis la revue : relance le solveur VRP en tenant compte des contraintes AM/PM/⚡ posées sur les jobs.
|
||||
// Tech malade/absent : le désélectionne puis RÉ-OPTIMISE → ses jobs sont répartis équitablement (le solveur équilibre la charge) sur les techs restants.
|
||||
async function markTechSick (g) {
|
||||
if (!g || isHoldId(g.techId)) return
|
||||
suggestDlg.techSel[g.techId] = false
|
||||
$q.notify({ type: 'info', icon: 'sick', message: g.techName + ' retiré (malade/absent) — redistribution de ses ' + g.jobs + ' job(s)…', timeout: 2200 })
|
||||
await reoptimize()
|
||||
}
|
||||
async function reoptimize () {
|
||||
suggestDlg.strategy = 'optimize'; suggestDlg.building = true
|
||||
try { await optimizeSuggestion() } catch (e) { err(e) } finally { suggestDlg.building = false }
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user