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:
parent
aa52a2c98c
commit
3979f79283
|
|
@ -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) — Ré-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) — Ré-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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user