Clean up the schema: drop unused tables, rename scopes, publishable awards

Migrations 020–023, with the code that reads each:

- 020 drops people_lists, people_list_members, v_chapters and
  v_person_affiliations. Nothing queried any of them.
- 021 renames event_sections to event_scopes and events.section_id
  to scope_id, finishing what 015 described. The API sends `scopes`
  and `scope_id`, and useEvents, EventListCards and EventCalendar
  take `scope`. It also inserts national/regional/partner, which only
  the retired seed ever created: a database built from migrations
  alone had no scope for the Retreats bands.
- 022 drops events.sort_order and people.sort_order. Events now sort
  by date (upcoming soonest first, past latest first, undated last)
  on /events, org pages and the countdown. People were only ever
  sorted by sort_name on the site. Every other sort_order stays.
- 023 rebuilds teams with created_at and updated_at plus a touch
  trigger, so the teams editor gets the same optimistic concurrency
  as the other entities.

Awards can be drafted: is_published (added in 011) is on both
descriptor halves with a Publishing group, and the award list, award
page, org awards and event awards leave drafts out.

The migration runner now turns foreign keys off around the per-file
transactions and runs foreign_key_check before each commit. PRAGMA
foreign_keys is a no-op inside a transaction, so 009's warning was
right and a rebuild of a referenced table (023) couldn't be written
otherwise. CLAUDE.md is updated to match.

Also removes the stray src/App.tsx.save.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Zaldimmar 2026-09-26 17:09:58 -05:00
parent 8790f861fe
commit 25e592bfea
19 changed files with 339 additions and 423 deletions

View file

@ -7,7 +7,7 @@
<EventCalendar /> everything
<EventCalendar host="northwest" /> one host's calendar
<EventCalendar section="national" /> one scope
<EventCalendar scope="national" /> one scope
<EventCalendar type={["class", "workshop"]} controls={["search"]} />
Two layers of filtering, and they answer different questions:
@ -63,7 +63,7 @@ const BODY = '#4a6b72'
const WEEKDAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
type EventCalendarProps = {
section?: string
scope?: string
host?: string
status?: EventListItem['status']
type?: EventType | EventType[]
@ -223,7 +223,7 @@ function nextDateAfter(events: EventListItem[], after: string): string | null {
/* ── Component ───────────────────────────────────────────────── */
export default function EventCalendar({
section,
scope,
host,
status,
type,
@ -231,32 +231,32 @@ export default function EventCalendar({
controls = ALL_CONTROLS,
defaultView = 'month',
}: EventCalendarProps) {
const { events: pinned, sections, loading, error } = useEvents({ section, host, status, type })
const { events: pinned, scopes, loading, error } = useEvents({ scope, host, status, type })
const today = iso(new Date())
const [month, setMonth] = useState(monthKey(today))
const [view, setView] = useState<CalendarView>(defaultView)
const [selected, setSelected] = useState<string | null>(null)
const [scope, setScope] = useState('')
const [pickedScope, setPickedScope] = useState('')
const [kind, setKind] = useState('')
const [onlineOnly, setOnlineOnly] = useState(false)
const [query, setQuery] = useState('')
const show = (control: CalendarControl) => controls.includes(control)
const showScope = show('scope') && !section
const showScope = show('scope') && !scope
const showType = show('type') && !type
const types = useMemo(() => typesPresent(pinned, EVENT_TYPES), [pinned])
const scopes = useMemo(() => {
const present = new Set(pinned.map((e) => e.section_id))
return sections.filter((s) => present.has(s.id))
}, [pinned, sections])
const scopeOptions = useMemo(() => {
const present = new Set(pinned.map((e) => e.scope_id))
return scopes.filter((s) => present.has(s.id))
}, [pinned, scopes])
const events = useMemo(() => {
const needle = query.trim().toLowerCase()
return pinned.filter((e) => {
if (scope && e.section_id !== scope) return false
if (pickedScope && e.scope_id !== pickedScope) return false
if (kind && e.event_type !== kind) return false
if (onlineOnly && !e.is_online) return false
if (needle) {
@ -274,7 +274,7 @@ export default function EventCalendar({
}
return true
})
}, [pinned, scope, kind, onlineOnly, query])
}, [pinned, pickedScope, kind, onlineOnly, query])
const weeks = useMemo(() => gridFor(month), [month])
const gridFrom = weeks[0][0]
@ -288,7 +288,7 @@ export default function EventCalendar({
)
const inMonth = occurrences.filter((o) => o.end >= monthFrom && o.start <= monthTo)
const undated = events.filter((e) => !e.starts_on).length
const filtering = Boolean(scope || kind || onlineOnly || query)
const filtering = Boolean(pickedScope || kind || onlineOnly || query)
const next = inMonth.length === 0 ? nextDateAfter(events, monthTo) : null
const onDay = (date: string) => occurrences.filter((o) => o.start <= date && o.end >= date)
@ -299,7 +299,7 @@ export default function EventCalendar({
}
const clearFilters = () => {
setScope('')
setPickedScope('')
setKind('')
setOnlineOnly(false)
setQuery('')
@ -361,13 +361,13 @@ export default function EventCalendar({
{/* ── Filters ── */}
{(showScope || showType || show('online') || show('search')) && (
<div className="mt-5 flex flex-wrap items-center gap-3">
{showScope && scopes.length > 1 && (
{showScope && scopeOptions.length > 1 && (
<FilterSelect
label="Scope"
value={scope}
onChange={setScope}
value={pickedScope}
onChange={setPickedScope}
all="All scopes"
options={scopes.map((s) => [s.id, s.name])}
options={scopeOptions.map((s) => [s.id, s.name])}
/>
)}
{showType && types.length > 1 && (