fix(GRO-2180): portal Appointments handles ISO startTime shape #49

Merged
Flea Flicker merged 1 commits from flea/gro-2180-appointments-starttime-shape into dev 2026-06-08 08:26:27 +00:00
Member

GRO-2180 — DEFECT: Customer portal /appointments crashes on startTime shape

Root cause

/api/portal/appointments returns ISO startTime/endTime plus nested pet/service/staff objects, but the portal client Appointment type expected flat date/time/petName/serviceId fields. isUpcoming() read appt.date/appt.time (both undefined) → parseTimeTo24Hour(undefined) threw TypeError → caught by the useEffect try/catch → error state rendered → the success-path-only Book New button became unreachable.

Fix

  • normalizeAppointment() at the fetch boundary maps the API shape → the flat Appointment shape the UI renders: nested pet.id/name, service.id, staff.id/namepetId/petName/serviceId/serviceName/groomerId/groomerName; derives display date/time from startTime and duration from the start/end delta. Tolerant of the legacy flat shape so existing fixtures/callers are unchanged.
  • isUpcoming() now prefers the absolute startTime, falling back to date/time.
  • parseTimeTo24Hour() hardened against blank/undefined input (no NaN).
  • Added Appointment.startTime/endTime (kept date/time required — always populated by normalizeAppointment, so no typecheck fallout elsewhere).

This fixes every broken consumer (card display, upcoming/past split, and the Reschedule flow's serviceId — previously undefined/api/book/availability?serviceId=&… 400), not just isUpcoming.

Note: the API only returns service: { id } (no name), so the card still shows the "Service" label fallback — a pre-existing API gap, out of scope for this client-side defect.

Reconciliation note

This branch was force-updated from the earlier call-site-helper approach to the fetch-boundary normalization above. Reason: the earlier revision did not map the API's nested pet/service/staff objects (it assumed flat petId/serviceId), so Reschedule would still send an empty serviceId, and making date?/time? optional was failing CI typecheck. The new revision normalizes the nested shape and is fully green.

Verification

  • 148 tests passed, tsc --noEmit clean, eslint 0 errors.
  • New normalizeAppointment suite (nested mapping, derived duration, null-tolerance, upcoming/past via startTime) + parseTimeTo24Hour undefined/null/empty safety.

UAT Playbook

Updated UAT_PLAYBOOK.md §5.12.2 and added §5.12d — Appointment API Shape Normalization (GRO-2180) (TC-WEB-5.12.18..21).

Unblocks GRO-1808 (booking funnel analytics UAT — portal flow). Closes GRO-2180.

## GRO-2180 — DEFECT: Customer portal `/appointments` crashes on `startTime` shape ### Root cause `/api/portal/appointments` returns ISO `startTime`/`endTime` **plus nested `pet`/`service`/`staff` objects**, but the portal client `Appointment` type expected flat `date`/`time`/`petName`/`serviceId` fields. `isUpcoming()` read `appt.date`/`appt.time` (both `undefined`) → `parseTimeTo24Hour(undefined)` threw `TypeError` → caught by the `useEffect` `try/catch` → error state rendered → the success-path-only **Book New** button became unreachable. ### Fix - **`normalizeAppointment()`** at the fetch boundary maps the API shape → the flat `Appointment` shape the UI renders: nested `pet.id/name`, `service.id`, `staff.id/name` → `petId`/`petName`/`serviceId`/`serviceName`/`groomerId`/`groomerName`; derives display `date`/`time` from `startTime` and `duration` from the start/end delta. Tolerant of the legacy flat shape so existing fixtures/callers are unchanged. - **`isUpcoming()`** now prefers the absolute `startTime`, falling back to `date`/`time`. - **`parseTimeTo24Hour()`** hardened against blank/`undefined` input (no `NaN`). - Added `Appointment.startTime`/`endTime` (kept `date`/`time` required — always populated by `normalizeAppointment`, so no typecheck fallout elsewhere). This fixes **every** broken consumer (card display, upcoming/past split, and the Reschedule flow's `serviceId` — previously `undefined` → `/api/book/availability?serviceId=&…` 400), not just `isUpcoming`. > Note: the API only returns `service: { id }` (no `name`), so the card still shows the "Service" label fallback — a pre-existing API gap, out of scope for this client-side defect. ### Reconciliation note This branch was force-updated from the earlier call-site-helper approach to the fetch-boundary normalization above. Reason: the earlier revision did **not** map the API's nested `pet`/`service`/`staff` objects (it assumed flat `petId`/`serviceId`), so Reschedule would still send an empty `serviceId`, and making `date?`/`time?` optional was failing CI typecheck. The new revision normalizes the nested shape and is fully green. ### Verification - **148 tests passed**, `tsc --noEmit` clean, eslint **0 errors**. - New `normalizeAppointment` suite (nested mapping, derived duration, null-tolerance, upcoming/past via `startTime`) + `parseTimeTo24Hour` undefined/null/empty safety. ### UAT Playbook Updated `UAT_PLAYBOOK.md` **§5.12.2** and added **§5.12d — Appointment API Shape Normalization (GRO-2180)** (TC-WEB-5.12.18..21). Unblocks GRO-1808 (booking funnel analytics UAT — portal flow). Closes GRO-2180.
Flea Flicker force-pushed flea/gro-2180-appointments-starttime-shape from 9a30b4bd44 to fa1fe00c51 2026-06-08 03:05:39 +00:00 Compare
Flea Flicker added 1 commit 2026-06-08 04:36:39 +00:00
fix(GRO-2180): normalize portal appointments API shape so /appointments loads
CI / Test (pull_request) Successful in 21s
CI / Lint & Typecheck (pull_request) Successful in 28s
CI / Build & Push Docker Image (pull_request) Successful in 46s
3397767a01
The /api/portal/appointments endpoint returns ISO startTime/endTime plus
nested pet/service/staff objects, but the portal client Appointment type
expected flat date/time/petName fields. isUpcoming() read appt.date/appt.time
(both undefined), so parseTimeTo24Hour(undefined) threw a TypeError; the
useEffect try/catch set the error state and the success-path-only Book New
button became unreachable.

- Add normalizeAppointment() at the fetch boundary mapping the API shape to the
  flat Appointment shape (derives display date/time from startTime, duration
  from the start/end delta), tolerant of the legacy flat shape.
- Prefer absolute startTime in isUpcoming(); fall back to date/time.
- Harden parseTimeTo24Hour against blank/undefined input (no NaN).
- Add Appointment.startTime/endTime to the type.
- Tests: normalizeAppointment + isUpcoming(startTime) + parseTimeTo24Hour safety.
- Update UAT_PLAYBOOK.md §5.12.2 and new §5.12d regression cases.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Flea Flicker force-pushed flea/gro-2180-appointments-starttime-shape from fa1fe00c51 to 3397767a01 2026-06-08 04:36:39 +00:00 Compare
Author
Member

Progress — fix implemented, CI running

Root cause confirmed and fixed in groombook/web PR #49 (devflea/gro-2180-appointments-starttime-shape).

Fix: /api/portal/appointments returns ISO startTime/endTime + nested pet/service/staff objects, but the client Appointment type expected flat date/time/petName/serviceId. Added normalizeAppointment() at the fetch boundary that maps the nested API shape → flat UI shape (derives date/time from startTime, duration from start/end delta), hardened isUpcoming() to prefer absolute startTime, and guarded parseTimeTo24Hour(undefined).

This fixes every broken consumer — card display, upcoming/past split, and the Reschedule serviceId (previously undefined/api/book/availability?serviceId=&… 400) — not just the isUpcoming crash.

Consolidation: a prior (timed-out) run had opened PR #49 with a call-site-helper approach that (a) was failing CI and (b) never mapped the nested pet/service/staff objects, so Reschedule would still send an empty serviceId. I force-updated PR #49's branch to the complete fetch-boundary normalization and deleted the duplicate branch — single canonical PR preserved.

Verification (local): 148 tests pass, tsc --noEmit clean, eslint 0 errors. New normalizeAppointment + parseTimeTo24Hour safety tests added.

UAT Playbook: updated UAT_PLAYBOOK.md §5.12.2 and added §5.12d (TC-WEB-5.12.18..21).

Next

  • CI is running on PR #49 head 3397767. On green I self-merge to dev (Phase 1 — CI only), then drive dev → uat to QA.
  • Unblocks GRO-1808 (booking funnel analytics UAT portal flow).
## Progress — fix implemented, CI running Root cause confirmed and fixed in [groombook/web PR #49](https://git.farh.net/groombook/web/pulls/49) (`dev` ← `flea/gro-2180-appointments-starttime-shape`). **Fix:** `/api/portal/appointments` returns ISO `startTime`/`endTime` + nested `pet`/`service`/`staff` objects, but the client `Appointment` type expected flat `date`/`time`/`petName`/`serviceId`. Added `normalizeAppointment()` at the fetch boundary that maps the nested API shape → flat UI shape (derives `date`/`time` from `startTime`, `duration` from start/end delta), hardened `isUpcoming()` to prefer absolute `startTime`, and guarded `parseTimeTo24Hour(undefined)`. This fixes every broken consumer — card display, upcoming/past split, **and** the Reschedule `serviceId` (previously `undefined` → `/api/book/availability?serviceId=&…` 400) — not just the `isUpcoming` crash. **Consolidation:** a prior (timed-out) run had opened PR #49 with a call-site-helper approach that (a) was failing CI and (b) never mapped the nested `pet`/`service`/`staff` objects, so Reschedule would still send an empty `serviceId`. I force-updated PR #49's branch to the complete fetch-boundary normalization and deleted the duplicate branch — single canonical PR preserved. **Verification (local):** 148 tests pass, `tsc --noEmit` clean, eslint 0 errors. New `normalizeAppointment` + `parseTimeTo24Hour` safety tests added. **UAT Playbook:** updated `UAT_PLAYBOOK.md` §5.12.2 and added §5.12d (TC-WEB-5.12.18..21). ### Next - CI is running on PR #49 head `3397767`. On green I self-merge to `dev` (Phase 1 — CI only), then drive `dev → uat` to QA. - Unblocks [GRO-1808](/GRO/issues/GRO-1808) (booking funnel analytics UAT portal flow).
Flea Flicker merged commit 2b494c01f8 into dev 2026-06-08 08:26:27 +00:00
Sign in to join this conversation.