- Rebrand all user-facing text to AgendaMax - Fix sidebar scrolling: card container no longer overflows html (flex h-full min-h-0) - Fix calendar toolbar hover: active buttons keep readable contrast - Sticky calendar toolbar (month/week/day always visible on scroll) - Seed guarantees 2-4 appointments per day for current week (Mon-Sat)
7.0 KiB
Task 4 Report — backend booking.ts: slots con working_hours + pickBestSlotEmployee
Branch: feat/auto-assign-specialist
Date: 2026-07-26
Plan: docs/superpowers/plans/2026-07-26-auto-assign-specialist-booking-redesign.md (Task 4)
Files changed
server/routes/booking.ts— only file modified by this task. Diff: +56 / −31 lines (git diff --stat).
No other files touched. POST /:slug/book left exactly as-is (Task 5 territory).
What changed
- Imports (top of file). Added scheduling imports from
../lib/scheduling.ts:parseWorkingHours, getWorkingHoursForDate, getCandidates, getExistingBusy, pickBestSlotEmployee, type WorkingHoursMap. publicBusiness(slug)now SELECTs two extra columns:auto_assign_specialist, working_hours(so the public business payload exposes them for the frontend).GET /:slug/slotscomputation block fully rewritten:bizWh = parseWorkingHours(biz.working_hours)— real business working-hours map.- When
employeeIdis absent (useRanking = !employeeId): pre-fetches rankedCandidateInfo[]viagetCandidates(...)and the per-employee busy map viagetExistingBusy(...), then for every grid step callspickBestSlotEmployee(...)to attach the best-ranked free specialist. - When
employeeIdis provided: resolves the per-employee working-hours window (falling back to business hours) and uses the existing busy list with the localoverlapsRangehelper. - Day window comes from
getWorkingHoursForDate(...)— no more hardcoded09:00/20:00. - If the resolved window is
null(business closed that day) →res.json({ slots: [], service: {...} }), no throw. - 30-min grid,
t + dur*60000 <= dayEndend-clamp,t < now+30minskip, 40-slot cap, and the response shape{ slots: [{time, iso, employee_id, employee_name}], service: {id, name, price, duration_min} }are all preserved.
- Added a tiny module-local
overlapsRange(a,b,c,d)helper (equivalent tooverlapsin scheduling.ts; the plan suggested this to keep imports minimal).
Verification
npm run typecheck
> [email protected] typecheck
> tsc -b --noEmit
0 errors. ✓
Smoke test (live server on :3000, seeded business lumiere-estetica-spa)
Wrote a throwaway .mjs script (deleted after) that logs in as owner, reads /api/settings for the slug, then hits /api/public/<slug> and /api/public/<slug>/slots.
Public business now exposes the new fields:
[info] public.business.auto_assign_specialist = 0
[info] public.business.working_hours = string
[ok] publicBusiness() exposes auto_assign_specialist + working_hours
Slots — auto-assign path (no employee_id, uses pickBestSlotEmployee):
Service id=6 "Corte + arreglo de barba" (Barbería, 45 min), date 2026-07-27 (Monday, dow=1).
[info] slots status=200 count=18
[info] service in resp = { id: 6, name: 'Corte + arreglo de barba', price: 280, duration_min: 45 }
[sample] first slot = { time: '09:00 a.m.', iso: '2026-07-27T15:00:00.000Z', employee_id: 2, employee_name: 'Mateo Herrera' }
[sample] last slot = { time: '07:00 p.m.', iso: '2026-07-28T01:00:00.000Z', employee_id: 2, employee_name: 'Mateo Herrera' }
[ok] all 18 slots have employee_id+employee_name and iso within 09:00-20:00 (inWindow=true)
- 18 slots, each carries
employee_id+employee_name(resolved bypickBestSlotEmployee). - Window strictly within the seed's Lun-Vie 09:00–20:00.
- Only Mateo Herrera offers service #6 in the seed, so the ranker correctly resolves every slot to him.
Closed-day path (Saturday, default seed = closed):
[info] Saturday 2026-08-01 slots count=0 (expected 0 if Sat closed)
[ok] closed day → { slots: [] } with service object (no throw)
The endpoint returns { slots: [], service: {...} } — no exception, matching constraint #5.
Specific-employee path (employee_id=2, the only employee offering svc #6):
status: 200 count: 18
all match eid: true
first: { time: '09:00 a.m.', iso: '2026-07-27T15:00:00.000Z', employee_id: 2, employee_name: 'Mateo Herrera' }
service in resp: { id: 6, name: 'Corte + arreglo de barba', price: 280, duration_min: 45 }
Every slot's employee_id === 2; service object included.
Deviations from the plan
- Dropped
isoDateStrfrom the Task-4 import list. The plan's Task-4 import block includesisoDateStr, but the Task-4 code never references it, andtsconfig.jsonhas"noUnusedLocals": true— keeping it would fail thenpm run typecheckverification step. Task 5 ("Ampliar imports de scheduling") explicitly adds more imports and can re-introduceisoDateStrat that point when it's actually used by thePOST /bookrewrite. Functionality identical. - Renamed
const any = …→const anyEmp = …in the fallback query inside the candidates block.anyis a reserved-ish TypeScript type name and using it as a value identifier is confusing and risky under strict mode. Purely cosmetic; behavior identical.
Self-review
- Does the day window come from
working_hours(not the old hardcoded 09/20)? Yes. The oldnew Date(${date}T09:00:00)/T20:00:00literals are gone. The window now comes fromgetWorkingHoursForDate(singleEmpWh | null, bizWh, dateObj), which readsparseWorkingHours(biz.working_hours). Confirmed empirically: a Saturday (closed in seed) returns 0 slots, while a Monday returns 18 slots within 09:00–20:00. - When
employeeIdis null, does it usepickBestSlotEmployee? Yes.useRanking = !employeeIdgates the branch; when true, the per-slot resolver ischosen = pickBestSlotEmployee(candInfos, candBusyMap, bizWh, {name, category}, slotStart, slotEnd). The previous "first free employee" inner loop is removed. - Response shape preserved? Yes —
{ slots: [{time, iso, employee_id, employee_name}], service: {id, name, price, duration_min} }verified in all three paths (ranking, specific-employee, closed-day). - 30-min grid, 40-slot cap, skip-past/<30min-from-now? All preserved (loop body unchanged in spirit; observed 18 slots, well under 40).
- Closed day →
{slots: []}+ service object, no throw? Confirmed. - POST /book untouched? Yes — diff shows the entire POST handler is byte-identical to the pre-task version.
Concerns
- The seed only has one employee (Mateo Herrera) offering service #6, so the ranker resolves every slot to the same person. To genuinely exercise the multi-candidate ranking in integration, Task 7's
booking-e2e.mjs(or a manual second employee with overlapping service + differentefficiency_score/specialties) would be needed. Out of scope for Task 4. biz.working_hoursis returned to the frontend as a raw JSON string (not parsed). Frontend will need toJSON.parseit — that's expected pershared/types.ts(working_hours?: WorkingHoursMap | string | null) and consistent with how Settings already returns it. No action needed here.- A leftover orphan
tsx watchprocess from an initial failedStart-Processattempt was killed during cleanup; the user's mainnpm run dev(concurrently) was left untouched and is still serving on :3000.