# 10 — Calendar module ## Goal Implement the `calendar` module: first-class **calendars** (multiple per household, each with its own visibility) and **events** that belong to a calendar. Month/week/day views with CRUD. ## Why Calendars are entities, not a singleton concept. A user might run a _Personal_ (private) calendar, a _Family_ (household-shared) calendar, and a _Wedding planning_ calendar that gets share-linked publicly. The calendar module ships that abstraction; widgets and the share-link service then reuse it generically. ## Depends on - 04 (module loader + widget contract), 07 (household), 08 (theming) ## Scope ### Schema (`src/modules/calendar/schema.ts`) `calendars`: - `id` uuid pk - `household_id` fk - `owner_id` fk users - `name` text - `color` text nullable - `visibility` text — `'private' | 'household'`. Check constraint. - `created_at`, `updated_at` `calendar_events`: - `id` uuid pk - `calendar_id` fk calendars (events inherit household scope through the calendar) - `title` text - `start_at` timestamptz, `end_at` timestamptz - `all_day` boolean - `location` text nullable - `notes` text nullable - `owner_id` fk users - `rrule` text nullable (reserved; no UI) - `external_source` text nullable, `external_id` text nullable (reserved) - `created_at`, `updated_at` Index on `(calendar_id, start_at)` for range queries. ### Seed (in `pnpm db:seed`, idempotent) - One `Home` calendar per household, `visibility = 'household'`. - One `Personal` calendar per user on first login, `visibility = 'private'`. ### Visibility helper `canSeeCalendar(userId, calendarId)`: - `visibility = 'household'` → must be a member of the calendar's household - `visibility = 'private'` → must be the calendar's `owner_id` All event reads go through this. Cross-leakage tests prove it. ### Server (`src/modules/calendar/server/`) - `listCalendars()` — every calendar the current user can see. - `createCalendar`, `renameCalendar`, `setCalendarVisibility`, `setCalendarColor`, `deleteCalendar`. - `listEvents({ from, to, calendarIds })` — `calendarIds: 'all' | string[]`, where `'all'` resolves to every calendar the user can see; explicit ids are filtered against the visibility helper. - `createEvent`, `updateEvent`, `deleteEvent` — server actions, validate with Zod. `createEvent` requires a `calendarId` the user can write to. ### UI (`src/modules/calendar/components/`) - `/calendar` page with month / week / day toggle. Recommended lib: **FullCalendar** (`@fullcalendar/react` + day/week/month plugins). - Calendar list in a sidebar with show/hide toggles per calendar, color swatches, "+ new calendar" button. The visible set is local view state (not persisted yet — separate later concern). - Click a day cell → create-event dialog (calendar selector defaults to last-used). Click an event → edit dialog. Drag-resize updates `end_at`. - `/calendar/manage` (or modal from sidebar) — create / rename / recolor / change visibility / delete calendars. ### Manifest Entity types: - `calendar.calendar` — shareable (read by default; writes via share token possible later), searchable (name). - `calendar.event` — shareable (read-only by default), remindable, searchable (title + notes + location). Widgets (every widget configurable per the task 04 contract): - **`calendar.upcoming`** — `config: { calendarIds: "all" | string[]; days: number }`. Default `{ calendarIds: "all", days: 3 }`. `resolveConfigOptions` returns `{ calendars: { id, name, visibility }[] }`. - **`calendar.month`** — `config: { calendarIds: "all" | string[] }`. Larger default size. Quick-add: "New event" → opens create dialog with the user's last-used calendar pre-selected. "New calendar" → calendar create dialog. Nav: `/calendar`. ## Out of scope - Recurrence UI (rrule column reserved, ignored on read). - External calendar sync. - Free/busy aggregation across users. - Per-user persisted "which calendars are shown" preference (local view state for now). - Per-calendar write-share via share token (basic share link is enough for v1). ## Acceptance criteria - [ ] Default seeds create one `Home` (household) calendar and one `Personal` (private) calendar per user, idempotently. - [ ] CRUD for both calendars and events works end-to-end with optimistic updates. - [ ] Private calendars are invisible to other household members in queries, in the sidebar, and in widgets. - [ ] `listEvents({ calendarIds: 'all' })` returns events from all visible calendars; explicit ids cannot be used to leak across visibility. - [ ] Both widgets register with the shape required by task 04 — a config schema, a default config, and a `resolveConfigOptions` adapter. - [ ] One Playwright happy-path test: create calendar → create event in it → see on calendar → edit → delete event → delete calendar.