feat: recurring appointments with cascading change propagation (#28)

* feat: recurring appointments with cascading change propagation

Implements GitHub issue #9 — recurring appointment scheduling with
configurable frequency and cascade edit/cancel options.

Changes:
- DB: add `recurring_series` table (frequency_weeks) and series_id /
  series_index columns on appointments (migration 0003)
- API POST /appointments: accepts optional `recurrence` object
  (frequencyWeeks + count) that creates a full series in one transaction
- API PATCH /appointments/🆔 new `cascadeMode` field
  (this_only | this_and_future | all) applies time-delta shifts and
  field updates across the series
- API DELETE /appointments/🆔 new `?cascade=` query param cancels
  this_only / this_and_future / all series members
- Frontend: booking form gains a "Recurring appointment" checkbox with
  frequency and count pickers; calendar chips show a ↻ recurring label;
  detail modal shows "Recurring series" badge and a cascade-delete radio
  picker for series appointments

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* fix: resolve TypeScript errors in recurring appointments route

Guard against possibly-undefined results from Drizzle .returning()
destructuring — use indexed access + explicit null checks instead of
array destructuring for the recurring_series insert, and add an early
throw when the series or first appointment row is missing.

Co-Authored-By: Paperclip <noreply@paperclip.ing>

---------

Co-authored-by: Groom Book CTO <cto@groombook.app>
Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit was merged in pull request #28.
This commit is contained in:
groombook-paperclip[bot]
2026-03-17 20:37:33 +00:00
committed by GitHub
parent e524099214
commit e7cf185d8c
6 changed files with 462 additions and 57 deletions
+8
View File
@@ -54,6 +54,12 @@ export interface Staff {
updatedAt: string;
}
export interface RecurringSeries {
id: string;
frequencyWeeks: number;
createdAt: string;
}
export interface Appointment {
id: string;
clientId: string;
@@ -65,6 +71,8 @@ export interface Appointment {
endTime: string;
notes: string | null;
priceCents: number | null;
seriesId: string | null;
seriesIndex: number | null;
createdAt: string;
updatedAt: string;
}