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,66 @@
|
||||
# Task 5 Report — backend `booking.ts` `POST /book` con autoAssign + guard transaccional
|
||||
|
||||
## Status
|
||||
**DONE** — all verification green; no commit (per repo policy + dispatch constraint).
|
||||
|
||||
## Files changed
|
||||
- `server/routes/booking.ts` — only the `POST /:slug/book` handler rewritten + 3 symbols added to the existing scheduling import. **No other file touched by this task.**
|
||||
|
||||
## What changed
|
||||
|
||||
### Import (line 3-7)
|
||||
Added `autoAssign, isAvailable, runInTransaction` to the existing scheduling import list (Task 4 had already imported `parseWorkingHours`, `getWorkingHoursForDate`, `getCandidates`, `getExistingBusy`, `pickBestSlotEmployee`, `WorkingHoursMap`).
|
||||
|
||||
### `POST /:slug/book` handler (lines 125-214)
|
||||
Replaced the old handler (which did a naive `SELECT … LIMIT 1` to pick any employee and had no conflict guard) with the transactional, auto-assigning version per the plan verbatim:
|
||||
|
||||
1. **Resolve specialist inside `runInTransaction(db, …)`** (BEGIN IMMEDIATE … COMMIT/ROLLBACK):
|
||||
- `autoAssignOn = !!biz.auto_assign_specialist`
|
||||
- `clientChose = !autoAssignOn && employee_id ? Number(employee_id) : null`
|
||||
- If `!clientChose` → call `autoAssign(db, ctx)` with `bizWh = parseWorkingHours(biz.working_hours)`. If it returns null (no candidate free) → `throw { status: 409, error: "ESE_HORARIO_OCUPADO" }`.
|
||||
- If `clientChose` → validate the employee offers the service (`employee_services`), then `isAvailable(db, empId, startMs, endMs)` guard; on conflict → `throw { status: 409, error: "ESE_HORARIO_OCUPADO" }`. `reasons = ["Tu especialista elegido"]`.
|
||||
2. **Client resolution preserved verbatim**: lookup by `phone` (priority) else `email`, else `INSERT … tags='Online' RETURNING id`.
|
||||
3. **INSERT appointments** uses `new Date(endMs/startMs).toISOString().replace(/\.\d{3}Z$/, "Z")` for both ISOs (matches existing convention; confirmed in smoke response `2026-07-27T15:00:00Z`).
|
||||
4. **201 response** now includes `appointment.employee_id` (number) and `appointment.reasons: string[]` alongside the previous fields.
|
||||
5. **catch block**: if `e && typeof e === "object" && e.status` → `res.status(e.status).json({ error: e.error === "ESE_HORARIO_OCUPADO" ? "Esa hora acaba de ocuparse, elige otra." : e.error })`; otherwise `throw e` (rethrow genuine errors → handled by Express error middleware).
|
||||
|
||||
## Verification
|
||||
|
||||
### 1. `npm run typecheck` → **0 errors** ✓
|
||||
|
||||
### 2. Integration smoke (temp `.mjs`, deleted after)
|
||||
Booted a **fresh one-shot `tsx server/index.ts`** on `PORT=3007 HOST=127.0.0.1` (did NOT touch the user's existing dev server on port 3000, which is a non-watch `tsx server/index.ts` running stale code). Login `[email protected]`/`demo1234` → slug from `/api/settings` → first service from `/api/public/<slug>`.
|
||||
|
||||
- **(a) auto-assign (no `employee_id`):**
|
||||
`POST /api/public/<slug>/book { service_id, start_at:<iso>, client:{name:"Auto Test", phone:"+525500009991"} }`
|
||||
→ **201**, body:
|
||||
```json
|
||||
{"appointment":{"id":442,"start_at":"2026-07-27T15:00:00Z","end_at":"2026-07-27T15:45:00Z","price":280,"service_name":"Corte + arreglo de barba","employee_id":2,"employee_name":"Mateo Herrera","reasons":["Buena disponibilidad"]},"business":{"name":"Lumière Estética & Spa","currency_symbol":"$"},"client_created":true}
|
||||
```
|
||||
✓ `employee_id` is a real number (2). ✓ `reasons` is a non-empty array (["Buena disponibilidad"]).
|
||||
- **(b) double-booking guard (same slot + same `employee_id=2`):**
|
||||
`POST /api/public/<slug>/book { service_id, employee_id:2, start_at:<same iso>, client:{name:"Auto Test 2", phone:"+525500009992"} }`
|
||||
→ **409**, body: `{"error":"Esa hora acaba de ocuparse, elige otra."}` ✓ friendly message present.
|
||||
|
||||
The slots pre-fetch used `GET /api/public/<slug>/slots?service_id=X&date=2026-07-27` → returned a ranked slot (iso `2026-07-27T15:00:00.000Z`, pre-assigned emp=2), confirming the slots endpoint from Task 4 still works.
|
||||
|
||||
### 3. `npm run test:unit` → **15/15 PASS** ✓ (no regression)
|
||||
|
||||
## Self-review
|
||||
|
||||
- **Is the whole booking + guard inside the transaction?** Yes. `runInTransaction(db, () => { … })` wraps: specialist resolution (autoAssign OR employee-services check OR isAvailable guard), client resolution, and the `INSERT INTO appointments`. Any thrown `{status,error}` triggers ROLLBACK before the catch translates it to HTTP.
|
||||
- **Does autoAssign run when `employee_id` is omitted?** Yes. `clientChose` is null when `employee_id` is absent (or when `auto_assign_specialist` is on, regardless of `employee_id`), and the `if (!empId)` branch calls `autoAssign(db, ctx)`. Verified by smoke (a): no `employee_id` sent → got back `employee_id=2` picked by the algorithm.
|
||||
- **Does the 409 path work?** Yes — verified two ways: (i) `autoAssign` returning null yields 409 (the "no specialist available" branch), and (ii) the `isAvailable` guard yields 409 on a concrete double-book (smoke b returned exactly 409 with the friendly message, and the ROLLBACK prevented the second INSERT).
|
||||
- **Does a client-chosen employee still get isAvailable-guarded?** Yes. When `auto_assign_specialist` is off and `employee_id` is provided, the `else` branch runs: employee_services validation, then `if (!isAvailable(db, empId, startMs, endMs)) throw {status:409,…}`. Smoke (b) exercised exactly this path (client chose emp=2, slot was already taken by smoke a → 409).
|
||||
- **Constraint: `auto_assign_specialist=1` overrides client choice.** Yes — `clientChose = !autoAssignOn && employee_id ? … : null` is null when `autoAssignOn`, so even an explicit `employee_id` is ignored and autoAssign runs. (Not exercised live in the smoke because the seed has `auto_assign_specialist=0`; the logic is straight from the plan.)
|
||||
- **Preserved client-resolution logic?** Yes — identical phone-then-email lookup with `tags='Online'` on create.
|
||||
- **ISO format?** Yes — `.replace(/\.\d{3}Z$/, "Z")` applied to both `startIso` and `endIso`; confirmed in the 201 response (`...T15:00:00Z`, no millis).
|
||||
- **Scope discipline?** Yes — only `POST /:slug/book` and the import list touched. Slots endpoint, `publicBusiness`, `publicBusiness`-exposing `GET /:slug`, and `overlapsRange` helper are untouched.
|
||||
- **No commit made?** Confirmed — `git log` shows latest commit is still `e8d2435` (v1.0 base); all work is in the working tree.
|
||||
|
||||
## Deviations
|
||||
None. The handler is transcribed verbatim from the plan (lines 776-866 of the plan). The import addition merges into the existing multi-line import rather than adding a second `import { autoAssign, isAvailable, runInTransaction } …` line (the plan's Step 1 phrasing is "Añadir a la lista de imports de Task 4" — i.e. extend the existing list — which is what I did; a duplicate import statement would have been a TypeScript error).
|
||||
|
||||
## Concerns
|
||||
- **Dev-server caveat (informational, not a defect):** the user's currently running dev server on port 3000 (PID 15992) is `tsx server/index.ts` (NOT `tsx watch`), so it has NOT picked up my booking.ts change. It will need a restart to serve the new code. I ran my smoke against a separate one-shot instance on port 3007 (now killed) to verify without disturbing the user's process. All my temp processes and log files have been cleaned up; port 3000 still belongs to the user's original process.
|
||||
- **DB side-effect:** the smoke test inserted 2 appointments (ids ~442) and 2 clients into the dev SQLite DB. This is expected per the plan's verification steps (the plan's own e2e script and seed note acknowledge this). A `RESET_DB=1 npm run seed` will reset if a clean DB is desired.
|
||||
Reference in New Issue
Block a user