From 6d07f070d74464cc22da2a8bce9fc780e964c389 Mon Sep 17 00:00:00 2001 From: louispaulb Date: Mon, 20 Jul 2026 07:41:31 -0400 Subject: [PATCH] =?UTF-8?q?fix(roster):=20cr=C3=A9neaux=20OFFERTS=20au=20c?= =?UTF-8?q?lient=20r=C3=A9servent=20le=20trajet=20autour=20des=20jobs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit techGaps prend un bufferH optionnel : rétrécit chaque trou d'un tampon de trajet (15 min) aux bords qui touchent un JOB existant (pas aux bords début/ fin de quart) → on n'offre plus au client un RDV collé entre 2 interventions. Appliqué à bookingSlots (offres client ; 0 si ignorePolicy = confirm/interne) et fitBooking (Option B : la dispo proposée par le client doit laisser le trajet). firstFitStart (drag-drop) garde bufferH=0 → placement inchangé. Vérifié live : /book garde 20 offres agrégées ; 694 créneaux/tech avec tampon vs 850 sans → les créneaux serrés entre 2 jobs ne sont plus proposés. Co-Authored-By: Claude Opus 4.8 --- services/targo-hub/lib/roster.js | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/services/targo-hub/lib/roster.js b/services/targo-hub/lib/roster.js index ec6825d..44153c5 100644 --- a/services/targo-hub/lib/roster.js +++ b/services/targo-hub/lib/roster.js @@ -781,8 +781,14 @@ async function loadBookingData (start, days) { return { asgs, techById, tplByName, booked, unavail } } +// Tampon de trajet (h) réservé de chaque côté d'un job existant pour les créneaux OFFERTS au client : on ne propose +// pas un RDV collé entre 2 interventions (le tech doit pouvoir arriver PUIS repartir). Cohérent avec dispatch.js. +const BOOKING_TRAVEL_BUF_H = 0.25 // 15 min + // Trous libres d'un tech (dans son shift, moins jobs pointés), filtrés compétence/zone. -function techGaps (a, d, skill, zone) { +// bufferH > 0 : rétrécit chaque trou du tampon de trajet aux bords qui touchent un JOB (pas aux bords début/fin de +// quart) → le RDV proposé laisse le temps du déplacement. bufferH = 0 (défaut) = comportement historique exact (drag-drop). +function techGaps (a, d, skill, zone, bufferH = 0) { const t = d.techById[a.tech]; if (!t || t.status === PAUSE_STATUS) return null if (d.unavail && d.unavail[a.tech] && d.unavail[a.tech].has(a.date)) return null // absence/congé approuvé ce jour-là → pas de créneaux if (skill && !(t.skills || []).includes(skill)) return null @@ -791,9 +797,16 @@ function techGaps (a, d, skill, zone) { if (tpl.on_call) return null // garde (sur appel) = capacité d'urgence, JAMAIS offerte au booking client const sh = timeToH(tpl.start_time) || 8; const eh = timeToH(tpl.end_time) || (sh + (Number(tpl.hours) || 8)) const day = (d.booked[a.tech + '|' + a.date] || []).slice().sort((x, y) => x.s - y.s) - let cursor = sh; const gaps = [] - for (const b of day) { if (b.s > cursor) gaps.push([cursor, b.s]); cursor = Math.max(cursor, b.e) } - if (cursor < eh) gaps.push([cursor, eh]) + let cursor = sh; const gaps = []; let prevJob = false + for (const b of day) { + if (b.s > cursor) { + const gs = cursor + (prevJob ? bufferH : 0) // repartir du job précédent + const ge = b.s - bufferH // arriver au job suivant + if (ge - gs >= 1e-6) gaps.push([gs, ge]) + } + cursor = Math.max(cursor, b.e); prevJob = true + } + if (cursor < eh) { const gs = cursor + (prevJob ? bufferH : 0); if (eh - gs >= 1e-6) gaps.push([gs, eh]) } // eh = fin de quart, pas de trajet après return { tech: t, gaps } } @@ -821,7 +834,7 @@ async function bookingSlots ({ skill, zone, duration = 1, start, days = 7, limit for (const a of d.asgs) { if (a.status === 'Annulé') continue if (offered && !offered.includes(parseISO(a.date).getUTCDay())) continue // jour non offert par la politique - const g = techGaps(a, d, skill, zone); if (!g) continue + const g = techGaps(a, d, skill, zone, ignorePolicy ? 0 : BOOKING_TRAVEL_BUF_H); if (!g) continue // offres client → réserve le trajet ; confirm/interne (ignorePolicy) → 0 for (const [gs, ge] of g.gaps) { let s = Math.max(gs, dayStart); const end = Math.min(ge, dayEnd) // borné à la plage horaire offerte while (s + dur <= end) { @@ -856,7 +869,7 @@ async function fitBooking ({ skill, zone, duration = 1, prefs = [] } = {}) { const p = prefs[i]; const ps = timeToH(p.start); const pe = ps + dur for (const a of (byDate[p.date] || [])) { if (a.status === 'Annulé') continue - const g = techGaps(a, d, skill, zone); if (!g) continue + const g = techGaps(a, d, skill, zone, BOOKING_TRAVEL_BUF_H); if (!g) continue // Option B : la dispo proposée par le client doit laisser le trajet if (g.gaps.some(([gs, ge]) => gs <= ps && ge >= pe)) { return { chosen: { rank: i + 1, date: p.date, start: p.start, end: hToTime(pe), tech: a.tech, tech_name: g.tech.name, zone: a.zone || '', shift: a.shift }, proposed: [] } }