feat(ops): Suggérer — re-optimize existing (dispatched) jobs as a simulation
Suggérer peut servir de SIMULATION sur des jobs DÉJÀ dispatchés : nouveau toggle « Ré-optimiser aussi les jobs déjà assignés » dans la config. Quand activé, le solveur VRP reçoit AUSSI les noms des jobs assignés de la fenêtre (assignedNamesForWindow, depuis occByTechDay) en plus du pool → repropose une répartition de TOUT (existant + nouveaux jobs ajoutés). Force le solveur (l'heuristique locale n'a que le pool en mémoire). Aperçu seulement — rien n'est écrit avant « Appliquer » (qui réassigne). Vérifié : toggle rendu dans la config Suggérer, 0 nouvelle erreur. Le résultat de ré-optimisation (solveur roster-solver) est à confirmer en prod (comme les autres runs solveur). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
92c320d08f
commit
add3c08cee
|
|
@ -1038,6 +1038,11 @@
|
||||||
<q-btn-toggle v-model="suggestDlg.strategy" dense no-caps unelevated toggle-color="primary" color="grey-3" text-color="grey-8" :options="[{ label: '⚡ Optimiser', value: 'optimize' }, { label: 'Intelligent', value: 'smart' }, { label: 'Meilleurs d\'abord', value: 'best' }, { label: 'Équilibré', value: 'balance' }, { label: 'Juste ce qu\'il faut', value: 'enough' }]" />
|
<q-btn-toggle v-model="suggestDlg.strategy" dense no-caps unelevated toggle-color="primary" color="grey-3" text-color="grey-8" :options="[{ label: '⚡ Optimiser', value: 'optimize' }, { label: 'Intelligent', value: 'smart' }, { label: 'Meilleurs d\'abord', value: 'best' }, { label: 'Équilibré', value: 'balance' }, { label: 'Juste ce qu\'il faut', value: 'enough' }]" />
|
||||||
<span class="text-caption text-grey-6">{{ ({ optimize: 'solveur VRP (OR-Tools) : routes réelles minimisées + compétences + temps sur place — regroupe les secteurs, spécialistes d\'abord', smart: 'proximité + compétence + charge', best: 'concentre le travail sur les meilleurs (cascade)', balance: 'répartit également entre tous (round-robin)', enough: 'qualifié mais le moins sur-qualifié → réserve les experts (distance + temps pris en compte)' })[suggestDlg.strategy] }}</span>
|
<span class="text-caption text-grey-6">{{ ({ optimize: 'solveur VRP (OR-Tools) : routes réelles minimisées + compétences + temps sur place — regroupe les secteurs, spécialistes d\'abord', smart: 'proximité + compétence + charge', best: 'concentre le travail sur les meilleurs (cascade)', balance: 'répartit également entre tous (round-robin)', enough: 'qualifié mais le moins sur-qualifié → réserve les experts (distance + temps pris en compte)' })[suggestDlg.strategy] }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Simulation « ré-optimiser l'existant » : inclut AUSSI les jobs déjà assignés de la fenêtre (le solveur les replace) → comparer/améliorer une tournée déjà dispatchée, avec les nouveaux jobs. Rien n'est écrit avant « Appliquer ». -->
|
||||||
|
<div class="row items-center q-mb-sm" style="gap:6px">
|
||||||
|
<q-toggle v-model="suggestDlg.includeAssigned" dense size="sm" color="deep-purple" label="Ré-optimiser aussi les jobs déjà assignés (simulation)" />
|
||||||
|
<q-icon name="info" size="15px" color="grey-5"><q-tooltip class="bg-grey-9" style="font-size:11px">Inclut les {{ assignedNamesForWindow().length }} job(s) déjà assigné(s) du/des jour(s) → le solveur repropose une répartition de TOUT (existant + nouveaux). Force le solveur VRP. Aperçu seulement — « Appliquer » réassigne.</q-tooltip></q-icon>
|
||||||
|
</div>
|
||||||
<!-- Réglages optimiseur : UN COÛT par critère (pas de slider) — persistés localement -->
|
<!-- Réglages optimiseur : UN COÛT par critère (pas de slider) — persistés localement -->
|
||||||
<div v-if="suggestDlg.strategy === 'optimize'" class="row items-center q-gutter-sm q-mb-sm" style="background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px;padding:6px 10px;flex-wrap:wrap">
|
<div v-if="suggestDlg.strategy === 'optimize'" class="row items-center q-gutter-sm q-mb-sm" style="background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px;padding:6px 10px;flex-wrap:wrap">
|
||||||
<span class="text-caption text-grey-7">Coûts :</span>
|
<span class="text-caption text-grey-7">Coûts :</span>
|
||||||
|
|
@ -4242,7 +4247,13 @@ function selectAllSector (city) {
|
||||||
if (assignPanel.showMap) refreshAssignMap()
|
if (assignPanel.showMap) refreshAssignMap()
|
||||||
}
|
}
|
||||||
// ── D : SUGGESTION de répartition (glouton proximité + charge + compétence), REVUE en surcouche avant d'appliquer ──
|
// ── D : SUGGESTION de répartition (glouton proximité + charge + compétence), REVUE en surcouche avant d'appliquer ──
|
||||||
const suggestDlg = reactive({ open: false, mode: 'config', plan: [], building: false, applying: false, fromSel: false, techSel: {}, makeShifts: true, strategy: 'optimize', view: 'list', jobTime: {} }) // défaut = solveur VRP (1re génération optimise déjà, plus de placeholders) · strategy: optimize|smart|best|balance|enough
|
const suggestDlg = reactive({ open: false, mode: 'config', plan: [], building: false, applying: false, fromSel: false, techSel: {}, makeShifts: true, strategy: 'optimize', view: 'list', jobTime: {}, includeAssigned: false }) // défaut = solveur VRP · strategy: optimize|smart|best|balance|enough · includeAssigned = simulation re-optimisant AUSSI les jobs déjà assignés
|
||||||
|
// Noms des jobs DÉJÀ assignés sur la fenêtre (pour la simulation « ré-optimiser l'existant ») — depuis l'occupation.
|
||||||
|
function assignedNamesForWindow () {
|
||||||
|
const win = new Set(suggestWindow.value); const out = new Set()
|
||||||
|
for (const key in occByTechDay.value) { if (!win.has(key.split('|')[1])) continue; for (const n of (occByTechDay.value[key].jobs || [])) if (n) out.add(n) }
|
||||||
|
return [...out]
|
||||||
|
}
|
||||||
const prevOpen = reactive({}) // revue : sections « déjà assigné(s) » dépliées (clé techId|iso)
|
const prevOpen = reactive({}) // revue : sections « déjà assigné(s) » dépliées (clé techId|iso)
|
||||||
// ── Revue : détail du ticket au clic + temps révisable + temps de route ENTRE arrêts (OSRM) ──
|
// ── Revue : détail du ticket au clic + temps révisable + temps de route ENTRE arrêts (OSRM) ──
|
||||||
const durOverride = reactive({}) // jobName → heures révisées par le dispatcher (survit aux Ré-optimiser de la session)
|
const durOverride = reactive({}) // jobName → heures révisées par le dispatcher (survit aux Ré-optimiser de la session)
|
||||||
|
|
@ -4637,6 +4648,9 @@ function techQuality (t) {
|
||||||
const suggestTechsRanked = computed(() => [...(visibleTechs.value || [])].sort((a, b) => techQuality(a) - techQuality(b)))
|
const suggestTechsRanked = computed(() => [...(visibleTechs.value || [])].sort((a, b) => techQuality(a) - techQuality(b)))
|
||||||
async function runSuggestion () {
|
async function runSuggestion () {
|
||||||
if (!suggestSelCount.value) { $q.notify({ type: 'warning', message: 'Choisis au moins un technicien disponible.', timeout: 2500 }); return }
|
if (!suggestSelCount.value) { $q.notify({ type: 'warning', message: 'Choisis au moins un technicien disponible.', timeout: 2500 }); return }
|
||||||
|
// « Ré-optimiser l'existant » : le solveur récupère les données des jobs assignés par leur nom (l'heuristique locale
|
||||||
|
// n'a que le pool en mémoire) → on force le solveur VRP.
|
||||||
|
if (suggestDlg.includeAssigned) suggestDlg.strategy = 'optimize'
|
||||||
suggestDlg.building = true
|
suggestDlg.building = true
|
||||||
try {
|
try {
|
||||||
if (suggestDlg.strategy === 'optimize') await optimizeSuggestion() // solveur VRP OR-Tools (routes + compétences + temps sur place)
|
if (suggestDlg.strategy === 'optimize') await optimizeSuggestion() // solveur VRP OR-Tools (routes + compétences + temps sur place)
|
||||||
|
|
@ -4678,7 +4692,8 @@ async function optimizeSuggestion () {
|
||||||
try {
|
try {
|
||||||
const r = await roster.optimizePlanHub({
|
const r = await roster.optimizePlanHub({
|
||||||
dates: suggestWindow.value, tech_ids: selTechs.map(t => t.id),
|
dates: suggestWindow.value, tech_ids: selTechs.map(t => t.id),
|
||||||
job_names: suggestJobs().map(j => j.name), // ne répartir QUE les jobs des jours cochés (ou la sélection lasso)
|
// job_names = pool filtré (+ jobs DÉJÀ assignés de la fenêtre si « ré-optimiser l'existant ») → le solveur les replace.
|
||||||
|
job_names: [...new Set([...suggestJobs().map(j => j.name), ...(suggestDlg.includeAssigned ? assignedNamesForWindow() : [])])],
|
||||||
opts: { rank_weight: solverOpts.rankWeight, speed_kmh: solverOpts.speedKmh, max_seconds: solverOpts.maxSeconds, overtime_coef: solverOpts.overtimeCoef },
|
opts: { rank_weight: solverOpts.rankWeight, speed_kmh: solverOpts.speedKmh, max_seconds: solverOpts.maxSeconds, overtime_coef: solverOpts.overtimeCoef },
|
||||||
dur_overrides: { ...durOverride }, job_windows,
|
dur_overrides: { ...durOverride }, job_windows,
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user