fix(roster): créneaux OFFERTS au client réservent le trajet autour des jobs
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 <noreply@anthropic.com>
This commit is contained in:
parent
0542e525ea
commit
6d07f070d7
|
|
@ -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: [] }
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user