feat: appointment confirmation and cancellation (GH #98, GRO-153)

Add customer confirmation/cancellation flow for appointments:

- DB migration (0013): add confirmation_status, confirmed_at, cancelled_at,
  confirmation_token to appointments table with index on token column
- schema.ts + factories.ts + types: expose new columns and ConfirmationStatus type
- GET /api/book/confirm/:token — tokenized confirm via email link (redirects)
- GET /api/book/cancel/:token — tokenized cancel via email link, single-use token
- POST /api/appointments/:id/confirm — portal/staff confirm endpoint
- POST /api/appointments/:id/cancel — portal/staff cancel endpoint
- Reminder emails now include Confirm/Cancel CTA buttons with tokenized links
- Reminder service generates confirmation token if missing before sending
- Staff calendar shows confirmation status indicator on appointment cards
  and in the detail modal (confirmed ✓ / customer cancelled ✗)
- /booking/confirmed, /booking/cancelled, /booking/error redirect pages
- 23 new unit tests covering all new endpoints and edge cases

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Scrubs McBarkley
2026-03-24 16:02:58 +00:00
parent 75d0e4c3e6
commit d1ab91adfa
14 changed files with 736 additions and 3 deletions
+14
View File
@@ -9,6 +9,9 @@ import { BookPage } from "./pages/Book.js";
import { ReportsPage } from "./pages/Reports.js";
import { GroupBookingPage } from "./pages/GroupBooking.js";
import { SettingsPage } from "./pages/Settings.js";
import { BookingConfirmedPage } from "./pages/BookingConfirmed.js";
import { BookingCancelledPage } from "./pages/BookingCancelled.js";
import { BookingErrorPage } from "./pages/BookingError.js";
import { CustomerPortal } from "./portal/CustomerPortal.js";
import { DevLoginSelector, getDevUser } from "./pages/DevLoginSelector.js";
import { DevSessionIndicator } from "./components/DevSessionIndicator.js";
@@ -151,6 +154,17 @@ export function App() {
return <Navigate to="/login" replace />;
}
// Public booking redirect pages — no auth or portal chrome needed
if (location.pathname === "/booking/confirmed") {
return <BookingConfirmedPage />;
}
if (location.pathname === "/booking/cancelled") {
return <BookingCancelledPage />;
}
if (location.pathname === "/booking/error") {
return <BookingErrorPage />;
}
return (
<BrandingProvider>
{location.pathname.startsWith("/admin") ? (