diff --git a/services/targo-hub/lib/roster.js b/services/targo-hub/lib/roster.js index 5662894..6c5c20e 100644 --- a/services/targo-hub/lib/roster.js +++ b/services/targo-hub/lib/roster.js @@ -950,6 +950,17 @@ async function handlePublicBooking (req, res, method, path, url) { const windows = await bookingSlots({ skill, duration: dur, start: todayET(), days: getBookingPolicy().horizon_days || 21, aggregate: true, limit: 60 }) return json(res, 200, { ok: true, job: { location: job.service_location || '', service_type: job.service_type || '', required_skill: skill, duration: dur, scheduled: job.scheduled_date || '' }, windows }) } + // Hold PUBLIC (Option A « on propose, le client choisit ») : à la sélection d'un créneau, on le réserve + // EXCLUSIVEMENT hold_minutes (5). Passé ce délai il retourne au pool (bookingSlots re-soustrait les holds vivants). + if (path === '/book/api/hold' && method === 'POST') { + const job = await jobByToken(token); if (!job) return json(res, 404, { ok: false, error: 'lien invalide' }) + const b = await parseBody(req); if (!b.date || !b.start) return json(res, 400, { ok: false, error: 'date + start requis' }) + const key = b.date + '|' + (String(b.start).length >= 5 ? String(b.start).slice(0, 5) : b.start) + if (b.release) { releaseHold(key); return json(res, 200, { ok: true, released: true }) } + const minutes = getBookingPolicy().hold_minutes + addHold(key, minutes) + return json(res, 200, { ok: true, minutes, expires_in_s: minutes * 60 }) + } if (path === '/book/api/submit' && method === 'POST') { const job = await jobByToken(token); if (!job) return json(res, 404, { ok: false, error: 'lien invalide' }) const b = await parseBody(req); const dur = Number(job.duration_h) || 1; const skill = skillForJob(job) @@ -959,6 +970,11 @@ async function handlePublicBooking (req, res, method, path, url) { await retryWrite(() => erp.update('Dispatch Job', job.name, { booking_prefs: JSON.stringify(b.prefs), booking_status: 'Proposé' })) return json(res, 200, { ok: true, confirmed: false, message: 'Vos disponibilités sont enregistrées — nous vous confirmerons sous peu.' }) } + // Option A : le client CHOISIT un créneau proposé → confirmation immédiate (garde F : si conflit, enregistré en Proposé). + if (b.mode === 'pick' && b.date && b.start) { + const r = await confirmWindow(job.name, b.date, b.start, dur, skill) + return json(res, 200, r) + } return json(res, 400, { ok: false, error: 'requête invalide' }) } return json(res, 404, { error: 'not found' })