Add recurring series to events

A Series checkbox under When opens a panel for the schedule:
weekly on chosen weekdays, monthly by date, or monthly by weekday
position, every N weeks or months, with meeting times and an
optional meeting count. starts_on anchors the schedule and ends_on
bounds it, so effective_status needs no change.

Occurrences are derived, not stored. src/lib/eventSeries.ts builds
the schedule label shown on cards and the event page, and the
upcoming dates listed on the event page.

Migration 016 adds the columns; the CRUD engine gains a time type.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Zaldimmar 2026-09-25 04:46:49 -05:00
parent aebc1c302b
commit 5ed7994a64
11 changed files with 500 additions and 3 deletions

View file

@ -0,0 +1,73 @@
-- ═══════════════════════════════════════════════════════════════
-- EVENT SERIES
--
-- An event that meets on a schedule — a weekly class, a monthly
-- meeting — is still one row. is_series says the dates repeat; the
-- series_ columns say how. Occurrences are never stored: they are
-- a pure function of these columns plus starts_on and ends_on, and
-- the site works them out when it draws them.
--
-- The event's own dates bound the series. starts_on is the first
-- meeting and anchors everything else: which week an every-other-
-- week series is "on", which day of the month a monthly one keeps,
-- and which weekday it falls on when no day is ticked. ends_on,
-- when set, is the last day it can meet — which is also what keeps
-- effective_status in v_events right with no change to the view.
-- series_count, when set, stops it after that many meetings,
-- whichever comes first.
--
-- series_frequency:
-- weekly on the ticked weekdays, every N weeks
-- monthly_date on starts_on's day of the month (the 13th),
-- every N months; a short month uses its last day
-- monthly_weekday on starts_on's weekday position (2nd Tuesday),
-- every N months; a 5th becomes "last"
--
-- One boolean per weekday rather than a packed text column: each
-- is a checkbox the CRUD engine already knows how to validate and
-- write, and a CHECK can hold it to 0 or 1.
--
-- frequency and interval are NOT NULL with defaults so that a box
-- ticked with nothing else filled in is still a complete schedule —
-- weekly, on starts_on's weekday — and so every existing row gets
-- a valid value without a backfill. They are ignored while
-- is_series is 0.
--
-- Times are 'HH:MM', 24-hour, local to the event. The GLOB is a
-- backstop; the admin engine checks the range before it gets here.
--
-- No change to v_events: it is SELECT e.*, so the columns arrive
-- on /events and /events/:id for free.
--
-- No BEGIN...END in this file, so nothing after it is dropped by
-- the migration runner.
-- ═══════════════════════════════════════════════════════════════
ALTER TABLE events
ADD COLUMN is_series INTEGER NOT NULL DEFAULT 0 CHECK (is_series IN (0, 1));
ALTER TABLE events
ADD COLUMN series_frequency TEXT NOT NULL DEFAULT 'weekly'
CHECK (series_frequency IN ('weekly', 'monthly_date', 'monthly_weekday'));
ALTER TABLE events
ADD COLUMN series_interval INTEGER NOT NULL DEFAULT 1 CHECK (series_interval >= 1);
ALTER TABLE events ADD COLUMN series_sun INTEGER NOT NULL DEFAULT 0 CHECK (series_sun IN (0, 1));
ALTER TABLE events ADD COLUMN series_mon INTEGER NOT NULL DEFAULT 0 CHECK (series_mon IN (0, 1));
ALTER TABLE events ADD COLUMN series_tue INTEGER NOT NULL DEFAULT 0 CHECK (series_tue IN (0, 1));
ALTER TABLE events ADD COLUMN series_wed INTEGER NOT NULL DEFAULT 0 CHECK (series_wed IN (0, 1));
ALTER TABLE events ADD COLUMN series_thu INTEGER NOT NULL DEFAULT 0 CHECK (series_thu IN (0, 1));
ALTER TABLE events ADD COLUMN series_fri INTEGER NOT NULL DEFAULT 0 CHECK (series_fri IN (0, 1));
ALTER TABLE events ADD COLUMN series_sat INTEGER NOT NULL DEFAULT 0 CHECK (series_sat IN (0, 1));
ALTER TABLE events
ADD COLUMN series_start_time TEXT
CHECK (series_start_time GLOB '[0-2][0-9]:[0-5][0-9]');
ALTER TABLE events
ADD COLUMN series_end_time TEXT
CHECK (series_end_time GLOB '[0-2][0-9]:[0-5][0-9]');
ALTER TABLE events
ADD COLUMN series_count INTEGER CHECK (series_count >= 1);