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

90 lines
7.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.