feat: customer appointment confirm/cancel in portal (GRO-47) #122

Closed
opened 2026-03-26 22:04:21 +00:00 by the-dogfather-cto[bot] · 1 comment
the-dogfather-cto[bot] commented 2026-03-26 22:04:21 +00:00 (Migrated from github.com)

Summary

Customers receive email reminders with confirm/cancel links that work, but cannot confirm or cancel through the customer portal UI. This is P1 — reduces no-shows, gives groomers advance notice to fill slots.

Related Paperclip task: GRO-47

What Already Works

  • GET /api/book/confirm/:token — tokenized email link confirm (updates confirmationStatus = "confirmed")
  • GET /api/book/cancel/:token — tokenized email link cancel (updates confirmationStatus = "cancelled", nullifies token, single-use)
  • Admin calendar detail modal shows confirmationStatus with ✓/✗ indicators
  • Reminder service generates tokens and includes confirm/cancel URLs in emails

What's Missing

1. Portal API endpoints (apps/api/src/routes/portal.ts)

Add two new routes following the exact same session auth pattern as PATCH /appointments/:id/notes:

POST /api/portal/appointments/:id/confirm

  • Require valid, non-expired impersonation session (same pattern)
  • Verify appointment belongs to session's clientId
  • Reject if startTime <= now (past or in progress)
  • Reject if confirmationStatus !== "pending" (already confirmed or cancelled)
  • Reject if status === "cancelled" or status === "completed"
  • Update: confirmationStatus = "confirmed", confirmedAt = new Date(), updatedAt = new Date()
  • Return updated appointment fields

POST /api/portal/appointments/:id/cancel

  • Require valid, non-expired impersonation session (same pattern)
  • Verify appointment belongs to session's clientId
  • Reject if startTime <= now (past)
  • Reject if status === "cancelled" or status === "completed"
  • Update: status = "cancelled", confirmationStatus = "cancelled", cancelledAt = new Date(), updatedAt = new Date()
  • Return updated appointment fields

2. Portal UI (apps/web/src/portal/sections/Appointments.tsx)

In AppointmentCard expanded view:

  • Add "Confirm Appointment" button: visible when isUpcoming(appt) && !readOnly && appt.confirmationStatus === "pending". Calls POST /api/portal/appointments/:id/confirm with the impersonation session header. On success, update local state. Show loading/error state.
  • Wire up "Cancel" button: calls POST /api/portal/appointments/:id/cancel with session header. Show a confirmation dialog first. On success, update local state (remove from upcoming or mark cancelled). Show loading/error state.
  • Show confirmation status badge in expanded view (e.g., "✓ Confirmed" in green, or "Pending confirmation" in amber, only for upcoming)

Follow the same fetch/state pattern as CustomerNotesSection.

Type Notes

The Appointment type (from mockData.js) needs confirmationStatus: "pending" | "confirmed" | "cancelled" added if not already present.

Out of Scope for This PR

  • Connecting portal to real appointment data (portal currently uses mock data; the buttons should call real API but list remains mock for now)
  • Cancellation fee logic
  • Rescheduling

cc @cpfarhood

## Summary Customers receive email reminders with confirm/cancel links that work, but cannot confirm or cancel through the customer portal UI. This is P1 — reduces no-shows, gives groomers advance notice to fill slots. Related Paperclip task: GRO-47 ## What Already Works - `GET /api/book/confirm/:token` — tokenized email link confirm (updates `confirmationStatus = "confirmed"`) - `GET /api/book/cancel/:token` — tokenized email link cancel (updates `confirmationStatus = "cancelled"`, nullifies token, single-use) - Admin calendar detail modal shows `confirmationStatus` with ✓/✗ indicators - Reminder service generates tokens and includes confirm/cancel URLs in emails ## What's Missing ### 1. Portal API endpoints (`apps/api/src/routes/portal.ts`) Add two new routes following the exact same session auth pattern as `PATCH /appointments/:id/notes`: **`POST /api/portal/appointments/:id/confirm`** - Require valid, non-expired impersonation session (same pattern) - Verify appointment belongs to session's `clientId` - Reject if `startTime <= now` (past or in progress) - Reject if `confirmationStatus !== "pending"` (already confirmed or cancelled) - Reject if `status === "cancelled"` or `status === "completed"` - Update: `confirmationStatus = "confirmed"`, `confirmedAt = new Date()`, `updatedAt = new Date()` - Return updated appointment fields **`POST /api/portal/appointments/:id/cancel`** - Require valid, non-expired impersonation session (same pattern) - Verify appointment belongs to session's `clientId` - Reject if `startTime <= now` (past) - Reject if `status === "cancelled"` or `status === "completed"` - Update: `status = "cancelled"`, `confirmationStatus = "cancelled"`, `cancelledAt = new Date()`, `updatedAt = new Date()` - Return updated appointment fields ### 2. Portal UI (`apps/web/src/portal/sections/Appointments.tsx`) In `AppointmentCard` expanded view: - Add **"Confirm Appointment"** button: visible when `isUpcoming(appt) && !readOnly && appt.confirmationStatus === "pending"`. Calls `POST /api/portal/appointments/:id/confirm` with the impersonation session header. On success, update local state. Show loading/error state. - Wire up **"Cancel"** button: calls `POST /api/portal/appointments/:id/cancel` with session header. Show a confirmation dialog first. On success, update local state (remove from upcoming or mark cancelled). Show loading/error state. - Show confirmation status badge in expanded view (e.g., "✓ Confirmed" in green, or "Pending confirmation" in amber, only for upcoming) Follow the same fetch/state pattern as `CustomerNotesSection`. ## Type Notes The `Appointment` type (from `mockData.js`) needs `confirmationStatus: "pending" | "confirmed" | "cancelled"` added if not already present. ## Out of Scope for This PR - Connecting portal to real appointment data (portal currently uses mock data; the buttons should call real API but list remains mock for now) - Cancellation fee logic - Rescheduling cc @cpfarhood
the-dogfather-cto[bot] commented 2026-03-27 16:57:32 +00:00 (Migrated from github.com)

Closing — the confirm/cancel feature was merged to main via commit 9eb0c3d (included in the fix/gro66 squash). Both portal endpoints (POST /portal/appointments/:id/confirm and /cancel) are live.

Closing — the confirm/cancel feature was merged to main via commit 9eb0c3d (included in the fix/gro66 squash). Both portal endpoints (POST /portal/appointments/:id/confirm and /cancel) are live.
This repo is archived. You cannot comment on issues.
1 Participants
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: groombook/app#122