feat(ops): auto-detect AM/PM from ticket title + AM/PM in RDV proposal

1. Détection auto AM/PM : les tickets legacy « Note AM » / « Note PM »
   (importés tels quels) sont détectés (detectAmpm : titre + note, tokens
   AM/PM / avant-midi / après-midi, null si ambigu) et la contrainte
   horaire est PRÉ-APPLIQUÉE dans « Suggérer » (suggestDlg.jobTime seedé à
   l'ouverture, marqueur  auto_awesome sur la puce, modifiable). Le
   solveur respecte alors la fenêtre 8-12 / 12-16.
2. Fenêtre RDV « 3 dispos du client » : chaque créneau proposé a un
   sélecteur AM/PM (avant-midi/après-midi) qui règle l'heure représentative
   (AM=08:00, PM=13:00), heure exacte toujours ajustable. Aligné sur AM/PM
   du dispatch.

Vérifié live : Suggérer affiche auto_awesome + puce AM pré-cochée (jobs
« Note AM ») ; RDV → clic PM règle 13:00, AM règle 08:00 ; 0 nouvelle
erreur.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
louispaulb 2026-07-05 17:54:46 -04:00
parent aa52a2c98c
commit 3979f79283
2 changed files with 25 additions and 3 deletions

View File

@ -1204,6 +1204,7 @@
</q-item>
</q-list></q-menu>
</q-btn>
<q-icon v-if="jobTimeOf(e.jobName).auto" name="auto_awesome" size="11px" color="amber-7" class="q-mr-xs"><q-tooltip>AM/PM détecté automatiquement depuis le titre/note du ticket</q-tooltip></q-icon>
<span class="jt-chip" :class="{ on: jobTimeOf(e.jobName).ampm === 'am' }" @click="toggleJobAmpm(e.jobName, 'am')"><q-tooltip>Avant-midi (8-12) -optimiser pour appliquer</q-tooltip>AM</span>
<span class="jt-chip" :class="{ on: jobTimeOf(e.jobName).ampm === 'pm' }" @click="toggleJobAmpm(e.jobName, 'pm')"><q-tooltip>Après-midi (12-16) -optimiser pour appliquer</q-tooltip>PM</span>
<q-space />
@ -4263,6 +4264,16 @@ function legHome (g, d) { const v = (legsByKey[g.techId + '|' + d.iso] || {}).ho
function legReturn (g, d) { const v = (legsByKey[g.techId + '|' + d.iso] || {}).returnTo; return v != null ? v : null } // temps de retour au départ
// Contraintes horaires par job ( Optimiser) : AM/PM (fenêtre dure) + « tôt » (urgence début de journée). Session (clé = jobName, survit aux ré-optimisations).
const AMPM_WIN = { am: [480, 720], pm: [720, 960] } // 8-12 / 12-16 (heure d'horloge)
// Détecte une contrainte AM/PM dans le titre/note du ticket (legacy « Note AM » / « Note PM » importé tel quel).
// Renvoie 'am' | 'pm' | null (null si absent ou ambigu = les deux présents).
function detectAmpm (job) {
const t = (String(job && job.subject || '') + ' ' + String(job && (job.legacy_detail || job.notes) || '')).toLowerCase()
const am = /\bam\b|avant.?midi|\bmatin\b/.test(t)
const pm = /\bpm\b|apr[eè]s.?midi/.test(t)
if (am && !pm) return 'am'
if (pm && !am) return 'pm'
return null
}
function jobTimeOf (name) { return suggestDlg.jobTime[name] || {} }
function toggleJobAmpm (name, v) { const c = suggestDlg.jobTime[name] || {}; suggestDlg.jobTime[name] = { ...c, ampm: c.ampm === v ? null : v } }
// 🖥 « SANS DÉPLACEMENT » (à distance, ex. config boîtier tel via ACS) : persistant (store hub), exclu des tournées par l'optimiseur.
@ -4467,7 +4478,10 @@ function openSuggest () {
// Pré-sélection par COMPÉTENCE : parmi les dispos, ceux dont les compétences couvrent au moins un skill requis
// par les jobs du périmètre (jour + filtres). Toujours modifiable (le dialogue liste tous les techs, cochables) ;
// repli sur tous les dispos si aucun skill requis connu ou aucun match.
const needed = new Set((suggestJobs() || []).map(j => j.required_skill).filter(Boolean))
const scopeJobs = suggestJobs() || []
// AUTO AM/PM : pré-applique la contrainte horaire détectée dans le titre/note (« Note AM/PM » legacy) si non déjà posée.
for (const j of scopeJobs) { if (!suggestDlg.jobTime[j.name]) { const ap = detectAmpm(j); if (ap) suggestDlg.jobTime[j.name] = { ampm: ap, auto: true } } }
const needed = new Set(scopeJobs.map(j => j.required_skill).filter(Boolean))
const skilled = needed.size ? availPool.filter(t => (t.skills || []).some(s => needed.has(s))) : []
const base = skilled.length ? skilled : availPool
const sel = {}; base.forEach(t => { sel[t.id] = true }); suggestDlg.techSel = sel

View File

@ -98,7 +98,12 @@
<div v-for="(p, i) in prefs" :key="i" class="row items-center q-gutter-sm q-mb-xs">
<q-badge color="grey-7">{{ i + 1 }}</q-badge>
<q-input dense outlined type="date" v-model="p.date" style="width:160px" />
<q-input dense outlined type="time" v-model="p.start" style="width:120px" />
<q-btn-toggle v-model="p.ampm" @update:model-value="v => setPrefAmpm(p, v)" dense no-caps unelevated
toggle-color="primary" color="grey-3" text-color="grey-8"
:options="[{ label: 'AM', value: 'am' }, { label: 'PM', value: 'pm' }]" />
<q-input dense outlined type="time" v-model="p.start" style="width:110px">
<template #append><q-icon name="schedule" size="15px" color="grey-5"><q-tooltip>Heure exacte (optionnel) AM = avant-midi, PM = après-midi</q-tooltip></q-icon></template>
</q-input>
</div>
<q-btn unelevated color="primary" icon="search" label="Vérifier" :loading="fitting" class="q-mt-sm" @click="doFit" />
@ -142,7 +147,10 @@ const params = reactive({ skill: '', zone: '', duration: 1, start: todayISO(), d
// Les 3 dispos sont PRÉ-REMPLIES avec des valeurs réelles (et non des défauts navigateur vides)
// « Vérifier » fonctionne d'emblée ; l'agent ajuste selon ce que dit le client.
function plusDaysISO (iso, n) { const a = iso.split('-').map(Number); const d = new Date(Date.UTC(a[0], a[1] - 1, a[2])); d.setUTCDate(d.getUTCDate() + n); return d.toISOString().slice(0, 10) }
function defaultPrefs () { const d1 = plusDaysISO(todayISO(), 1); const d2x = plusDaysISO(todayISO(), 2); return [{ date: d1, start: '09:00' }, { date: d1, start: '13:00' }, { date: d2x, start: '09:00' }] }
function defaultPrefs () { const d1 = plusDaysISO(todayISO(), 1); const d2x = plusDaysISO(todayISO(), 2); return [{ date: d1, start: '08:00', ampm: 'am' }, { date: d1, start: '13:00', ampm: 'pm' }, { date: d2x, start: '08:00', ampm: 'am' }] }
// Choix rapide AM/PM (avant-midi / après-midi) règle l'heure de départ représentative (fenêtre 8-12 / 12-16) ;
// l'heure exacte reste ajustable. Aligné sur la contrainte AM/PM du dispatch.
function setPrefAmpm (p, v) { p.ampm = v; if (v === 'am') p.start = '08:00'; else if (v === 'pm') p.start = '13:00' }
const prefs = ref(defaultPrefs())
const fit = ref(null); const fitting = ref(false)
const slots = ref([]); const chosen = ref(null); const finding = ref(false); const searched = ref(false)