Files
famapp/docs/tasks/10-calendar-module.md
T
ginnoir 09d6adbf18 Calendars as entities, uniform widget contract, multi-dashboard tasks
Architecture refinements before module work begins:

- Calendars become first-class entities (mirror of lists). Each
  calendar has its own visibility (private | household) and is
  independently shareable. Updated CLAUDE.md data model and task 10.

- Drop the singleton/parameterized split for dashboard widgets. Every
  widget declares a configSchema + defaultConfig + resolveConfigOptions
  and every placement is an independent instance — same widgetId can
  appear multiple times on one dashboard pointed at different things.
  Updated task 04 contract; tasks 11/12 widget sections aligned.

- Add tasks 25 (multiple dashboards per user) and 26 (customizable
  layout + widget configuration). Replaces the previously-considered
  three-task plan with a cleaner two-task split that the uniform
  contract enables. Phase 3 index updated; STATUS records the rationale.

- Task 20 reframed as a single-dashboard MVP that exercises the new
  contract end-to-end before the customization layer lands.
2026-05-06 01:14:49 -05:00

4.7 KiB

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.upcomingconfig: { calendarIds: "all" | string[]; days: number }. Default { calendarIds: "all", days: 3 }. resolveConfigOptions returns { calendars: { id, name, visibility }[] }.
  • calendar.monthconfig: { 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.