diff --git a/apps/ops/src/pages/PlanificationPage.vue b/apps/ops/src/pages/PlanificationPage.vue
index 0756e62..189cd78 100644
--- a/apps/ops/src/pages/PlanificationPage.vue
+++ b/apps/ops/src/pages/PlanificationPage.vue
@@ -1204,6 +1204,7 @@
+ AM/PM détecté automatiquement depuis le titre/note du ticket
Avant-midi (8-12) — Ré-optimiser pour appliquerAM
Après-midi (12-16) — Ré-optimiser pour appliquerPM
@@ -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
diff --git a/apps/ops/src/pages/RendezVousPage.vue b/apps/ops/src/pages/RendezVousPage.vue
index 252c2b9..1aa49b4 100644
--- a/apps/ops/src/pages/RendezVousPage.vue
+++ b/apps/ops/src/pages/RendezVousPage.vue
@@ -98,7 +98,12 @@
{{ i + 1 }}
-
+ 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' }]" />
+
+ Heure exacte (optionnel) — AM = avant-midi, PM = après-midi
+
@@ -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)