Files
AgendaPro/.superpowers/sdd/task-4-report.md
T
AgendaPro Dev d796538fd9 feat: rebrand AgendaPro -> AgendaMax + calendar fixes + current-week demo seed
- 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)
2026-07-27 10:11:16 -06:00

7.0 KiB
Raw Blame History

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

  1. Imports (top of file). Added scheduling imports from ../lib/scheduling.ts: parseWorkingHours, getWorkingHoursForDate, getCandidates, getExistingBusy, pickBestSlotEmployee, type WorkingHoursMap.
  2. publicBusiness(slug) now SELECTs two extra columns: auto_assign_specialist, working_hours (so the public business payload exposes them for the frontend).
  3. GET /:slug/slots computation block fully rewritten:
    • bizWh = parseWorkingHours(biz.working_hours) — real business working-hours map.
    • When employeeId is absent (useRanking = !employeeId): pre-fetches ranked CandidateInfo[] via getCandidates(...) and the per-employee busy map via getExistingBusy(...), then for every grid step calls pickBestSlotEmployee(...) to attach the best-ranked free specialist.
    • When employeeId is provided: resolves the per-employee working-hours window (falling back to business hours) and uses the existing busy list with the local overlapsRange helper.
    • Day window comes from getWorkingHoursForDate(...)no more hardcoded 09: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 <= dayEnd end-clamp, t < now+30min skip, 40-slot cap, and the response shape { slots: [{time, iso, employee_id, employee_name}], service: {id, name, price, duration_min} } are all preserved.
  4. Added a tiny module-local overlapsRange(a,b,c,d) helper (equivalent to overlaps in 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 by pickBestSlotEmployee).
  • Window strictly within the seed's Lun-Vie 09:0020: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

  1. Dropped isoDateStr from the Task-4 import list. The plan's Task-4 import block includes isoDateStr, but the Task-4 code never references it, and tsconfig.json has "noUnusedLocals": true — keeping it would fail the npm run typecheck verification step. Task 5 ("Ampliar imports de scheduling") explicitly adds more imports and can re-introduce isoDateStr at that point when it's actually used by the POST /book rewrite. Functionality identical.
  2. Renamed const any = …const anyEmp = … in the fallback query inside the candidates block. any is 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 old new Date(${date}T09:00:00) / T20:00:00 literals are gone. The window now comes from getWorkingHoursForDate(singleEmpWh | null, bizWh, dateObj), which reads parseWorkingHours(biz.working_hours). Confirmed empirically: a Saturday (closed in seed) returns 0 slots, while a Monday returns 18 slots within 09:0020:00.
  • When employeeId is null, does it use pickBestSlotEmployee? Yes. useRanking = !employeeId gates the branch; when true, the per-slot resolver is chosen = 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 + different efficiency_score / specialties) would be needed. Out of scope for Task 4.
  • biz.working_hours is returned to the frontend as a raw JSON string (not parsed). Frontend will need to JSON.parse it — that's expected per shared/types.ts (working_hours?: WorkingHoursMap | string | null) and consistent with how Settings already returns it. No action needed here.
  • A leftover orphan tsx watch process from an initial failed Start-Process attempt was killed during cleanup; the user's main npm run dev (concurrently) was left untouched and is still serving on :3000.