diff --git a/apps/pwa/src/api/client.ts b/apps/pwa/src/api/client.ts index cd330f4..e997fda 100644 --- a/apps/pwa/src/api/client.ts +++ b/apps/pwa/src/api/client.ts @@ -103,3 +103,108 @@ export async function fetchEvents( // Phase 1 legacy types (CalendarEvent, EventsResponse, fetchEventsLegacy) removed in Plan 05 // when the Phase 1 broker-proof component was retired. + +// ── /api/events (write — Plan 03-05) ───────────────────────────────────────── + +/** + * Recurrence presets supported by the EventForm. + * Maps 1:1 to the RRULE frequency values the API accepts. + */ +export type RecurrencePreset = 'none' | 'daily' | 'weekly' | 'monthly' | 'yearly' + +/** + * Payload for creating or updating a calendar event. + * Mirrors the Zod schema on POST /api/events/create and PATCH /api/events/:uid/edit. + */ +export interface CreateEventPayload { + title: string + allDay: boolean + start: string // 'YYYY-MM-DD' for allDay; ISO 8601 for timed + end: string // same format as start + recurrence: RecurrencePreset + location?: string + description?: string + calendarUrl?: string // omit to use the member's default writable calendar (D-01) +} + +/** Response from POST /api/events/create and PATCH /api/events/:uid/edit */ +export interface CreateEventResponse { + uid: string +} + +/** + * A writable calendar returned by GET /api/events/writable-calendars. + * The server is the authoritative source of the writable set (D-03). + * The client never derives writability — it reads this endpoint verbatim. + */ +export interface WritableCalendar { + url: string + displayName: string + color: string + isShared: boolean +} + +/** + * Create a new calendar event. + * + * POSTs to /api/events/create and returns immediately with 202 + uid. + * The server enqueues the write to CalDAV asynchronously (D-05/D-12). + */ +export async function createEvent(payload: CreateEventPayload): Promise { + const res = await fetch('/api/events/create', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify(payload), + }) + + if (!res.ok) { + throw new Error(`POST /api/events/create failed: ${res.status}`) + } + + return res.json() as Promise +} + +/** + * Update an existing calendar event. + * + * PATCHes /api/events/:uid/edit with the updated payload. + * Returns 202 + uid; the write is enqueued asynchronously (D-05). + */ +export async function updateEvent( + uid: string, + payload: CreateEventPayload, +): Promise { + const res = await fetch(`/api/events/${uid}/edit`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify(payload), + }) + + if (!res.ok) { + throw new Error(`PATCH /api/events/${uid}/edit failed: ${res.status}`) + } + + return res.json() as Promise +} + +/** + * Fetch the authoritative list of writable calendars for the current member. + * + * The server owns the D-03 writable set (WHERE userId=currentUser.id OR isShared=1). + * The client NEVER derives writability — it reads this endpoint verbatim. + * Drives the calendar picker visibility (D-02: hidden when only 1 writable calendar). + */ +export async function fetchWritableCalendars(): Promise { + const res = await fetch('/api/events/writable-calendars', { + credentials: 'include', + }) + + if (!res.ok) { + throw new Error(`GET /api/events/writable-calendars failed: ${res.status}`) + } + + const body = (await res.json()) as { calendars: WritableCalendar[] } + return body.calendars +} diff --git a/apps/pwa/src/store/calendarStore.ts b/apps/pwa/src/store/calendarStore.ts index e266433..0ab50d0 100644 --- a/apps/pwa/src/store/calendarStore.ts +++ b/apps/pwa/src/store/calendarStore.ts @@ -35,10 +35,27 @@ export interface CalendarStore { openEventId: string | null calendarRange: { start: string; end: string } + // ── EventForm UI state (Plan 03-05) ────────────────────────────────────── + // Server state (events, calendars) lives in TanStack Query. These are pure + // UI-shape keys: whether the form is open, what mode it is in, and which + // event UID to pre-populate in edit mode. + eventFormOpen: boolean + eventFormMode: 'create' | 'edit' + eventFormUid: string | null + setSelectedView: (view: string) => void setSelectedDate: (date: string) => void setOpenEventId: (id: string | null) => void setCalendarRange: (range: { start: string; end: string }) => void + + /** + * Open or close the EventForm. + * + * @param open true to open, false to close + * @param mode 'create' (default) or 'edit' + * @param uid UID of the event to pre-populate in edit mode (null otherwise) + */ + setEventForm: (open: boolean, mode?: 'create' | 'edit', uid?: string | null) => void } // ── Helpers ──────────────────────────────────────────────────────────────── @@ -93,6 +110,11 @@ export const useCalendarStore = create((set) => ({ openEventId: null, calendarRange: initialCalendarRange(), + // EventForm UI state — closed by default, create mode, no pre-fill UID + eventFormOpen: false, + eventFormMode: 'create', + eventFormUid: null, + setSelectedView: (view: string) => { set({ selectedView: view }) // Persist to localStorage keyed by breakpoint group @@ -110,4 +132,7 @@ export const useCalendarStore = create((set) => ({ setCalendarRange: (range: { start: string; end: string }) => set({ calendarRange: range }), + + setEventForm: (open: boolean, mode: 'create' | 'edit' = 'create', uid: string | null = null) => + set({ eventFormOpen: open, eventFormMode: mode, eventFormUid: uid }), }))