fix(ops): slot finder respects untimed load (humanly possible)

suggestSlots built gaps only from timed jobs, so a tech loaded with untimed
(no start_time) legacy jobs still looked free and got offered slots. Now the
day is skipped when timed + untimed committed hours + the requested duration
exceed the shift. Also matches assigned_tech by technician_id OR docname.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
louispaulb 2026-07-09 14:43:21 -04:00
parent 7df6ad30e3
commit 04fb52426f

View File

@ -207,12 +207,12 @@ async function suggestSlots ({ duration_h = 1, latitude, longitude, after_date,
const shift = winBy[tech.technician_id + '|' + dateStr] || winBy[tech.name + '|' + dateStr]
if (!shift) continue
// Day's pinned jobs (only those with a real start_time — floating jobs
// without a time are ignored since we don't know when they'll land).
// Day's pinned jobs (only those with a real start_time) — servent à découper les trous.
// Match par technician_id OU docname (les jobs legacy portent parfois l'un ou l'autre).
const belongs = (j) => (j.assigned_tech === tech.technician_id || j.assigned_tech === tech.name) &&
j.scheduled_date === dateStr && !(ignoreReserved && j.job_type === 'Réservation')
const dayJobs = allJobs
.filter(j => j.assigned_tech === tech.technician_id &&
j.scheduled_date === dateStr && j.start_time &&
!(ignoreReserved && j.job_type === 'Réservation')) // mode urgence/réparation → les blocs RÉSERVÉS (soft) ne bloquent pas
.filter(j => belongs(j) && j.start_time)
.map(j => {
const s = timeToHours(j.start_time)
return {
@ -223,6 +223,16 @@ async function suggestSlots ({ duration_h = 1, latitude, longitude, after_date,
})
.sort((a, b) => a.start_h - b.start_h)
// « Humainement possible » : un tech ne peut pas dépasser son quart. Les jobs assignés SANS heure fixée
// (legacy osTicket) occupent quand même la journée → on retranche leur charge + celle déjà placée ; si la
// capacité restante ne suffit pas pour cette durée, PAS de créneau ce jour (sinon on suroffre un tech déjà plein).
const shiftH = shift.end_h - shift.start_h
const clampD = (j) => Math.max(0, Math.min(j.end_h, shift.end_h) - Math.max(j.start_h, shift.start_h))
const timedBusy = dayJobs.reduce((a, j) => a + clampD(j), 0)
const untimedLoad = allJobs.filter(j => belongs(j) && !j.start_time)
.reduce((a, j) => a + (parseFloat(j.duration_h) || 1), 0)
if (timedBusy + untimedLoad + duration > shiftH + 0.01) continue
// Build gaps bounded by shift_start/end (shift = quart réel du tech ce jour, résolu ci-dessus).
const gaps = []
let cursor = shift.start_h, prevCoords = homeCoords