feat(dispatch): pre-wired sites — install time ÷4 (Lac des pins/Camping/Domaine Dauphinais)

estimateForJob: an INSTALLATION whose location matches a pre-wired site
(Lac des pins, Camping, Domaine Dauphinais) takes ¼ of the normal time
(infra already in place). Applied at the estimator source → flows into
est_min everywhere (greedy duration, VRP service_min, occupancy, travel).
Floored at 15 min; adds a "site pré-câblé ×¼" label. Verified: Lac des
pins install 120→30 min; normal install unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
louispaulb 2026-07-02 15:08:33 -04:00
parent 2566976191
commit 8b2d7b54e9

View File

@ -351,10 +351,20 @@ function detectCharacteristics (job, items) {
for (const a of items.filter(i => i.cat === 'addon')) if (hit(a.keywords)) picks.push({ id: a.id, qty: 1 })
return picks
}
// Sites PRÉ-CÂBLÉS : une INSTALLATION y prend ~¼ du temps normal (infra déjà en place → branchement rapide).
const FAST_INSTALL_SITES = ['lac des pins', 'camping', 'domaine dauphinais']
function estimateForJob (job, items) {
items = items || readJobChar().items
const picks = detectCharacteristics(job, items); const by = {}; for (const i of items) by[i.id] = i
return { minutes: estimateMinutes(picks, items), picks, labels: picks.map(p => (by[p.id] || {}).label).filter(Boolean) }
let minutes = estimateMinutes(picks, items)
const labels = picks.map(p => (by[p.id] || {}).label).filter(Boolean)
// Installation à un site pré-câblé → temps /4 (Lac des pins / Camping / Domaine Dauphinais).
const isInstall = picks.some(p => String(p.id).startsWith('install')) || String(job.required_skill || '').toLowerCase().includes('install')
if (isInstall && minutes > 0) {
const loc = [job.address, job.location_label, job.service_location, job.subject, job.municipalite].join(' ').toLowerCase().normalize('NFD').replace(/\p{Diacritic}/gu, '')
if (FAST_INSTALL_SITES.some(s => loc.includes(s))) { minutes = Math.max(15, Math.round(minutes / 4)); labels.push('site pré-câblé ×¼') }
}
return { minutes, picks, labels }
}
// ── #56 Politique de créneaux offerts + holds temporaires ────────────────────