The Book New wizard held raw ISO slot strings (e.g.
"2026-06-09T10:00:00.000Z") from /api/book/availability and rendered them
verbatim on the slot buttons, the Review "Date & Time" line, and the success
screen, and POSTed them straight as preferredTime. The api inserts that into
the Postgres `time` column, so a full ISO datetime is `invalid input syntax
for type time` → unhandled 500, breaking portal booking (GRO-2211).
- Add shared UTC helpers formatSlotLabel(slot) and slotToTime(slot) so the
display label and the submitted time derive from the same canonical UTC ISO
slot and never desync. Both format/extract in UTC (slots are UTC business
hours); guards tolerate already-formatted labels and HH:MM:SS values.
- BookNew: render formatSlotLabel(time) on slot buttons, the Review line, and
the confirmation; submit preferredTime: slotToTime(selectedTime) → HH:MM:SS.
- Export BookingFlow for testing.
- Tests: helper unit tests + a Book New funnel integration test asserting the
rendered slot label is "10:00 AM" (never raw ISO) and the waitlist POST body
preferredTime matches /^\d{2}:\d{2}:\d{2}$/.
GRO-2213
Co-Authored-By: Paperclip <noreply@paperclip.ing>
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>
QA regression: PR #26 removed fireEvent and waitFor from the
@testing-library/react import, breaking 21 test cases and typecheck.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Add a StatusBadge component that renders human-readable labels
(Confirmed, Pending, Waitlisted, etc.) with semantic color classes
for appointment cards in the portal. Replaces raw status strings.
- Added STATUS_LABELS map for human-readable status labels
- Updated STATUS_COLORS to use accessible amber/blue tones
- Exported StatusBadge for testing
- Added unit tests for all 7 badge states plus fallback
- Updated UAT_PLAYBOOK.md §5.12c with status badge test cases
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
QA review pointed out:
- Lint error: 'act' imported but never used in test file
- 6 test failures: date input lacked accessible label
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Both BookingFlow and RescheduleFlow in Appointments.tsx now fetch
from /api/book/availability when a date is selected, matching the
public booking wizard behavior. Loading and error states shown.
- Removed hardcoded availableTimes arrays from both flows
- Added useEffect that fetches availability on date change
- Shows "Checking availability…" while loading
- Shows error message on fetch failure
- Shows "No available slots" when API returns empty
Added tests for RescheduleFlow dynamic slot fetching covering:
loading, fetched slots, error, empty, API params, and re-fetch on
date change.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
- Copy apps/web/ with all src, components, pages, portal
- Inline packages/types/ as local packages/types module
- Add tsconfig path aliases for @groombook/types
- Port Dockerfile and CI workflow
- Image name: ghcr.io/groombook/web
Co-Authored-By: Paperclip <noreply@paperclip.ing>