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)
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
# 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: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
|
||||
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:00–20: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.
|
||||
Reference in New Issue
Block a user