feat(booking): endpoints publics Option A (choisir un créneau proposé)
Complète le backend RDV 2 voies (les 2 modes ont maintenant leur chemin, tous gardés F + hold 5 min) : - POST /book/api/hold : hold EXCLUSIF 5 min à la sélection d'un créneau (Option A) ; release possible ; retour au pool à l'expiration (bookingSlots re-soustrait les holds vivants). - POST /book/api/submit mode:'pick' : le client confirme le créneau choisi → confirmWindow (garde F : si le ticket est déjà assigné à un autre tech dans F, enregistré en 'Proposé' au lieu d'écraser). - (mode:'rank' = Option B « le client propose 3 dispos » inchangé : fitBooking → confirmWindow.) Routes PUBLIC via le préfixe /book existant (aucun changement server.js). Vérifié live : token bidon → 404 lien invalide. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
777794cd28
commit
1fc0e903eb
|
|
@ -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' })
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user