Skip to content

feat/events and time blocks page - #40

Closed
ethnjs wants to merge 102 commits into
stagingfrom
feat/events-and-time-blocks-page
Closed

feat/events and time blocks page#40
ethnjs wants to merge 102 commits into
stagingfrom
feat/events-and-time-blocks-page

Conversation

@ethnjs

@ethnjs ethnjs commented Apr 16, 2026

Copy link
Copy Markdown
Owner

feat: events & time blocks page

Summary

  • Events page overhaul — full multi-view events management (Cards, Table, Timeline) with side panel CRUD, inline editing, and shared cross-view filter panel
  • Time Blocks — new data model + TimeBlocksTable UI for creating/editing/deleting time blocks per tournament; time block picker in EventTable is multi-select
  • Tournament Categories — new CategoriesTable tab with inline add, rename, and delete; color tokens wired into Cards/Timeline views; inline category creation from EventSidePanel
  • CSV import/exportCsvImportBar with preview-before-import modal, template download, and export with tournament-scoped filenames; blocks column dropped from events CSV schema
  • Bulk multi-edit — select-all checkbox + per-row checkboxes drive a multi-edit side panel; batch PATCH /events/batch backend endpoint saves changes in one request using no-change sentinels
  • Timeline viewEventTimeline with zoom levels, grouping (division / category / block), color-by controls, overlapping block rendering, and smooth sticky date-header scrolling
  • Performance — event table rows virtualized; heavy view components memoized; perf: virtualize event table rows commit
  • Backend migrations — Alembic migration adds time_blocks, tournament_categories, and membership_events tables; field_type / value_type split on column_mappings; temp membership profile fields
  • DeleteBlockModal — confirmation modal when deleting a time block that still has events assigned, with action buttons always visible regardless of event list length
  • Misc fixesbatch route ordered before {event_id} to prevent FastAPI routing collision; tournament_id included in event create payload; model_fields_set used to allow clearing nullable fields via PATCH; favicon updated; parseApiError helper

Changed files (highlights)

Area Key files
Events page frontend/app/dashboard/[tournamentId]/events/page.tsx
New components EventTable, EventCardGrid, EventCard, EventTimeline, EventChip, EventSidePanel, EventFiltersPanel, TimeBlocksTable, CategoriesTable, CsvImportBar, DeleteBlockModal
API client frontend/lib/api.ts, frontend/lib/errors.ts, frontend/lib/formatters.ts
Backend routes events.py (batch PATCH, query filters), time_blocks.py (new), categories.py (new)
Backend schemas event.py (EventBatchUpdate), time_block.py (new), tournament_category.py (new)
Migrations affe_time_blocks_categories_membership_events.py, 23ff6e84620b_split_field_value_type.py
Tests test_categories.py, test_time_blocks.py, test_events.py (batch endpoint)

Test plan

Setup

  • Run alembic upgrade head — migration should apply cleanly with no errors
  • Run backend/app/db/init_db.py

Time Blocks tab

  • Create — click "Add time block", fill in label / start / end / building; row saves inline; block appears in the list
  • Edit — click a row's edit icon; change label and date; save; confirm values update without full page reload
  • Delete (no events) — delete a block with zero assigned events; block disappears immediately, no modal
  • Delete (with events) — delete a block that has events assigned; DeleteBlockModal appears listing the affected events; action buttons remain visible when the event list is long (scroll inside modal)
  • Cancel delete — dismiss modal; block is not deleted
  • Multi-select picker — in EventTable's time-block cell, confirm the dropdown is multi-select (can assign multiple blocks to one event)

Categories tab

  • Add category — inline add row; enter name; saves and appears with a color swatch
  • Rename — click a category name, edit it, press Enter / blur; name updates in place; default category cannot be renamed
  • Delete — delete a non-default category; events previously assigned re-categorize to the default
  • Default category protections — confirm the UI prevents deleting or renaming the default category (button disabled / tooltip shown)
  • Category colors — color tokens appear on EventCards and EventChips in Timeline

Events — Cards view

  • Browse — cards render with name, division, category chip, and block labels (no date/time metadata on cards)
  • Open side panel — click a card; EventSidePanel slides open with event details; animations are smooth
  • Create event — click "+ Add event"; side panel opens in add mode; "Save & Add Another" button visible only in add mode
  • Edit event — change name, division, category, building, room, volunteers needed; save; card updates
  • Inline category create — in the category dropdown inside EventSidePanel, type a new name and confirm; new category appears in the list and is selected
  • Delete event — delete from side panel; card disappears; panel closes

Events — Table view

  • Inline editing — click a cell; edit name, division, category, block, building/room; Tab/Enter commits; Escape cancels
  • Frozen first column — scroll table horizontally; event name column stays pinned; frozen-column divider renders as an inset line
  • Sticky header — scroll vertically; header row stays at top of viewport
  • Animated column labels — horizontal scroll causes header labels to clamp/animate smoothly (no jank)
  • Column widths — verify columns are appropriately sized and don't overflow horizontally
  • Select-all — check the header checkbox; all visible rows become selected; toolbar shows selected count
  • Per-row select — check individual rows; deselect header when not all selected
  • Multi-edit panel — with 2+ events selected, click "Edit X events"; multi-edit panel appears; fields show placeholder indicating mixed values; change one field; save; only changed fields are patched (no-change sentinels prevent overwriting)
  • Batch save — confirm PATCH /tournaments/{id}/events/batch fires once with array of updates; verify network tab shows single request

Events — Timeline view

  • Render — events render as EventChip components on a horizontal timeline; chips are color-coded by category
  • Overlapping blocks — events sharing a time block offset/stack instead of overlapping
  • Zoom — zoom controls change density; chips resize proportionally
  • Grouping — switch grouping between division / category / block; rows re-render correctly
  • Color-by — toggle color-by between division and category; chip colors update
  • Date header scrolling — scroll horizontally; date header scrolls smoothly with a clamped sticky effect
  • Sticky header lock — horizontal scroll does not break vertical sticky behavior of the main header

Shared filter panel

  • Open/close — filter panel icon toggles left side panel with animation
  • Search — type in the search box; events filter in real-time across all three views (Cards, Table, Timeline)
  • Division filter — select a division chip; only matching events shown
  • Category filter — select a category; filters correctly
  • Building/block filter — dropdown restricted to known options; selecting a value filters; "no results" state shows when nothing matches
  • Chip placement — active filter chips appear in the correct location (below toolbar, not inside dropdown)
  • Clear filters — clear all; full event list restores
  • Persists across tab switch — apply a filter on Cards; switch to Table; filter is still active
  • Building/block dropdown stays open — after adding a tag in the building/block filter, dropdown remains open to allow adding another

CSV import/export

  • Export — click Export → "Download CSV"; file downloads with the tournament name in the filename; columns match events schema (blocks column absent)
  • Export template — click Export → "Download Template"; empty CSV with correct headers downloads
  • Import preview — click Import → choose a valid CSV; preview modal opens showing parsed rows
  • Import confirm — confirm import; events are created and appear in the table/cards
  • Import invalid CSV — upload a file with missing required columns; error banner appears in the modal
  • Import guide popover--color-surface background renders correctly (not transparent)

Backend — batch PATCH

  • PATCH /tournaments/{id}/events/batch with [{id, name: "new name"}] → only name changes
  • PATCH /tournaments/{id}/events/batch with field set to null → nullable field is cleared
  • Route ordering: GET /events/batch does not match {event_id} route (regression check)
  • 403 returned when caller lacks manage_events permission

Backend — time blocks & categories

  • POST /tournaments/{id}/time_blocks → creates block; GET lists it
  • PATCH /tournaments/{id}/time_blocks/{id} → updates label/times
  • DELETE /tournaments/{id}/time_blocks/{id} → deletes block
  • POST /tournaments/{id}/categories → creates category
  • PATCH /tournaments/{id}/categories/{id} → renames; returns 400 when attempting to rename default
  • DELETE /tournaments/{id}/categories/{id} → returns 400 when attempting to delete default

Regression checks

  • Volunteers page — table still loads; no layout regressions from sidebar/topbar changes
  • Sheets pages — mapping table, import, alias editor still function
  • Sidebar hover expand/collapse animation unaffected
  • NewTournamentModal — create tournament succeeds (stale blocks field removed from payload)
  • Tournament type resolution — useTournament hook and API types are consistent (no TS errors in console)

nchen0124 and others added 30 commits April 7, 2026 01:32
…ip_events

- create time_blocks table with tournament FK, label, date/start/end string columns
- create tournament_categories table with unique constraint on (tournament_id, name)
- create event_time_blocks and membership_events association tables
- backfill time_blocks from tournaments.blocks JSON, event_time_blocks from events.blocks integer lists
- backfill membership_events from memberships.assigned_event_id
- add events.category_id FK, backfill from free-text events.category string (find-or-create categories)
- migrate memberships.schedule JSON to use time_block_id instead of raw block integers
- drop legacy columns: tournaments.blocks, events.blocks, events.category, memberships.assigned_event_id
… schema

- remove TournamentBlock class and blocks field from TournamentBase and TournamentUpdate (column dropped in migration)
- remove validate_block_numbers model validator that depended on blocks
- add missing TimeBlockRead and TournamentCategoryRead imports to TournamentRead
- add 19 tests covering GET/POST/PATCH/DELETE for time blocks
- cover ordering by date then start, 409 guard on delete with assigned events, permission checks (manage_events vs view_events), and cross-tournament 404
- remove stale blocks=[] kwarg from td_tournament and other_tournament conftest fixtures (column dropped in migration)
- add 17 tests covering GET/POST/DELETE for tournament categories
- cover default seeding on tournament create (via API route), seeded-category guard on delete, 409 guard when events use the category, permission checks, and cross-tournament 404
- add _seed_tournament helper to conftest and apply it to td_tournament and other_tournament fixtures to mirror the real API's seeding behavior
- import DEFAULT_CATEGORIES and TournamentCategory into conftest
- replace blocks integer list with time_block_ids in _make_event helper and all payloads
- replace category string field with category_id FK in create/update tests
- add tests for nullable division, invalid division (422), and same-name/different-division uniqueness
- add tests for list filter params: division, type, category_id
- add tests for time_block_ids on create and patch (assign, replace, clear)
- add test verifying time_block_ids from another tournament are silently ignored
- add _make_block and _get_seeded_category_id helpers
…plify events write check

- add manage_events to tournament_director in DEFAULT_POSITIONS (was implicit via manage_tournament implication)
- remove manage_tournament fallback from _require_write_permission in events route — manage_events check alone is sufficient since manage_tournament still implies it via PERMISSION_IMPLICATIONS
- remove unused MANAGE_TOURNAMENT import from events.py
- add test verifying manage_tournament implies manage_events so tournament directors can still manage events on other tournaments
- remove SAMPLE_BLOCKS constant and all tests that used the legacy blocks field (test_create_tournament_full blocks assertions, test_create_tournament_duplicate_block_numbers, test_update_tournament_add_blocks)
- fix test_create_tournament_minimal to assert time_blocks == [] and correct categories count instead of blocks == []
- update test_create_tournament_full to omit blocks from payload and assert on time_blocks and categories fields
- add test_get_tournament_response_includes_time_blocks_and_categories verifying TournamentRead shape
- import DEFAULT_CATEGORIES for assertions
…ck_id

- remove assigned_event_id from MembershipBase, MembershipUpdate schemas and _serialize/create/update route handlers
- remove Event import from memberships route (no longer used)
- update ScheduleSlot.block -> ScheduleSlot.time_block_id (FK into time_blocks table)
- update test_memberships.py: replace _make_event helper with _make_block, update schedule payloads to time_block_id format, remove assigned_event_id from full create test
- replace tournament.blocks dict access with tournament.time_blocks ORM relationship in _parse_day_string (availability date resolution)
- remove NATS_BLOCKS constant and blocks field from _make_tournament in test_sync.py (column dropped from Tournament)
- remove legacy blocks=[] JSON from Tournament constructor
- seed DEFAULT_CATEGORIES via TournamentCategory rows after tournament flush
- seed sample time blocks as TimeBlock rows instead of JSON on the tournament
- import TournamentCategory, TimeBlock, and DEFAULT_CATEGORIES
…locks

- rename t.blocks to t.time_blocks in _make_tournament helper to match the renamed ORM relationship
- reject overlapping time blocks on POST and PATCH with 409 and conflict detail
- handle midnight-spanning blocks via sub-range splitting in _intervals_overlap
- exclude the block being updated from its own overlap check (self-exclusion)
- add 6 overlap tests: overlap→409, adjacent→201, PATCH overlap→409, self-exclusion→201, midnight-spanning→201, overlap with midnight-spanning→409
- fix test_memberships.py _make_block helper to create non-overlapping blocks
- Add TimeBlock, TimeBlockCreate, TimeBlockConflict interfaces and timeBlocksApi (CRUD under /tournaments/{id}/blocks/)
- Add TournamentCategory interface and categoriesApi (list, create, delete under /tournaments/{id}/categories/)
- Update Event interface: division nullable, category_id replaces category string, time_block_ids + time_blocks[] replace blocks[]
- Add EventCreate interface for typed create/update payloads; add query param support to listByTournament
- Update Tournament.blocks → time_blocks: TimeBlock[]
- Update ScheduleSlot.block → time_block_id; Membership.assigned_event_id → event_ids: number[]
- Add 5 category color ramps (--color-cat-1 through --color-cat-5) each with main, subtle, and text variants
- Add division chip colors for B (blue), C (green), and no-division (purple)
- Add event type chip colors for standard (teal) and trial (amber)
- Token naming follows existing --color-* pattern for consistency
- Add PageHeader with title and subtitle
- Add TabBar with Timeline / Cards / Time Blocks tabs (active underline style)
- Add ImportBar with Upload CSV button, ? help icon, and disabled Google Sheets button
- Fetch events, time blocks, and categories in parallel on mount via Promise.all
- Render loading state and error banner; tab panels are placeholders pending subsequent steps
- Export fmtTime, fmtDate, fmtDateShort, catColorIndex helpers for use in child components
- Move fmtTime, fmtDate, fmtDateShort, catColorIndex out of events/page.tsx
- Add catColorVars helper that returns all three CSS var names for a category slot
- Child components import directly from @/lib/formatters instead of re-exporting through the page
- guard all create_table calls with insp.has_table() so re-running on a DB that already has the tables skips creation
- guard add_column and drop_column calls with _has_column() to handle partial migration state
- fixes failure on dev DB where time_blocks already existed but preview/prod did not have the tables
- add json.loads() with isinstance str guard for tournaments.blocks, events.blocks, and memberships.schedule backfill steps
- SQLite returns raw JSON strings via text() queries unlike Postgres which auto-deserializes
- hoist import json to top of upgrade() so it's available across all backfill steps
- wrap all add_column and drop_column calls in op.batch_alter_table() contexts
- SQLite does not support ALTER TABLE for FK constraints or column drops; batch mode uses copy-and-move
- fix UPDATE alias in downgrade (SQLite does not support table aliases in UPDATE statements)
- consolidate events batch blocks where multiple columns are dropped together
- SQLite batch mode requires FK constraints to have explicit names; inline ForeignKey raises ValueError
- drop FK from category_id and assigned_event_id add_column calls in upgrade and downgrade
- SQLite does not enforce FK constraints by default so omitting them from DDL is safe
- Create components/events/TimeBlocksTable.tsx with Label, Day, Time range, Events count, Actions columns
- Day separator rows group blocks by date using fmtDate from lib/formatters
- Events count badge shows how many events are assigned to each block
- Empty state with serif heading and contextual message for read-only vs manage mode
- Add block button in toolbar (manage mode only); Edit/Delete action buttons per row with hover states
- Wire into events page blocks tab; onAdd/onEdit/onDelete stubbed pending steps 5 and 6
- Create components/events/TimeBlocksTable.tsx with Label, Day, Time range, Events count, Actions columns
- Day separator rows group blocks by date using fmtDate from lib/formatters
- Events count badge shows how many events are assigned to each block
- Empty state with serif heading and contextual message for read-only vs manage mode
- Add block button uses Button component with IconPlus; edit/delete use Button ghost + IconEdit/IconTrash
- Fix fmtTime in lib/formatters to always show minutes (8:00 AM not 8 AM)
- Wire into events page blocks tab; onAdd/onEdit/onDelete stubbed pending steps 5 and 6
- Add InlineRow component inside TimeBlocksTable with label, date, start, end inputs
- Manages editingId and showAddRow state internally; only one row editable at a time
- Edit row pre-fills from existing block values; add row starts blank with label focused
- Enter key submits, Escape cancels; Save disabled until all four fields filled
- Error from API displayed in a row beneath the inline form
- Add/edit buttons disabled while any row is open
- Table shown even when blocks list is empty if add row is open
- Wire handleAddBlock and handleEditBlock in page.tsx calling timeBlocksApi.create/update then reloading
- Switch table to table-layout: fixed with explicit column widths (Label 20%, Day 16%, Time range 26%, Events 20%, Actions 18%)
- Column widths no longer reflow when inline add/edit row appears
- Remove yellow warning background and left border accent from inline edit row
- Restore bottom border on all inline row cells so row separator stays visible
- Time inputs use flex: 1 / minWidth: 0 to fill the fixed column without overflowing
- Create components/events/DeleteBlockModal.tsx using existing Modal component
- Shows warning banner and affected event list when block has assigned events
- Shows simple confirmation copy when block is empty
- Cancel and Delete/Delete anyway actions with loading and error states
- Add force=true query param to timeBlocksApi.delete for confirmed force-deletes
- Wire handleDeleteClick in page: attempts delete, catches 409 and opens modal with affected_events
- Wire handleDeleteConfirm: force-deletes then reloads; modal dismisses on cancel or success
- Create components/events/EventSidePanel.tsx — 400px fixed right panel
- Fields: Name (required), Category (select), Division (segmented B/C/—), Type (segmented Standard/Trial), Building/Room/Floor (row), Volunteers needed, Time block multi-select chips
- Save and Save & add another in footer; Save & add another resets form and refocuses Name
- Unsaved changes guard: close/Escape shows inline discard confirmation strip before dismissing
- Read-only mode: all inputs disabled, footer shows Close only
- Backdrop closes panel with same guard; Escape key triggers handleClose
- Add panel state and handleSaveEvent (create or update) to events/page.tsx
- Category inline create stubbed
…ts Import JSON buttons

- Replace IconSheets in Icons.tsx with updated grid design from events page
- Add IconUpload to Icons.tsx (arrow-up-from-line design)
- Remove local UploadIcon and SheetsIcon functions from events/page.tsx; import from Icons.tsx
- Add IconUpload to Import JSON button in sheets/[configId]/edit/page.tsx
- Add IconUpload to Import JSON button in sheets/new/page.tsx
ethnjs added 16 commits April 18, 2026 19:52
- Switched animated table header label movement back from left-position updates to translateX transforms
- Kept pixel snapping with Math.round on shift values to reduce blur while preserving smoother motion
- Restored will-change transform hints for more efficient scroll-time rendering
- Replaced outer sticky-cell divider shadow with inset right-edge shadow to avoid clipping
- Applied inset divider style consistently to sticky header and body first-column cells
- Restored visible first-column boundary during horizontal scrolling
…witching

- Moved event search/filter state to the events page so filters persist across timeline, cards, and table views
- Added filter modal with multi-select category (including no category), division (including no division), building, and time block filters
- Applied shared filtered dataset to all event views and added a unified search/filter/add toolbar
- Improved tab switching responsiveness with React transitions and by keeping visited tabs mounted
- Updated EventCardGrid and EventTable with hideFilters mode for compatibility with page-level filtering
…ents

- Added windowed row virtualization to EventTable with overscan and spacer rows so only visible rows render
- Memoized EventTableRow with a custom comparator to reduce row re-renders during scroll and selection updates
- Memoized EventCard to avoid unnecessary card rerenders when unrelated state changes
- Kept existing sticky header/column and header-scroll animation behavior intact while improving render performance
…lbar UX

- Removed event count and add button from the top timeline controls header to reduce duplicated actions
- Reworked shared search/filter/add bar to use predefined Input and Button UI components
- Extracted inline filters modal into EventFiltersPanel with draft state and apply-on-Done behavior
- Added improved filter UX with colored category/division tags and tokenized building/time block autocomplete inputs
- Updated page-level filtering logic to support explicit and no-value category/division matching with persisted cross-view filters
… layout

- Group search input + filter/clear buttons in a sub-flex container so they always stay side-by-side
- Convert EventFiltersPanel from a center modal to a fixed left side panel matching EventSidePanel layout and animations
- Add slide-in-from-left / fade animations with onAnimationEnd close sequencing
- Change category/division tag border-radius from 999px to var(--radius-md) to match table tag style
- Style selected building/timeblock chips with var(--radius-sm) and var(--font-mono) to match time block chips in table view
- Make building and time block suggestion lists focus-driven dropdowns hidden until input is focused
…rection

- Match building/timeblock search input style to volunteers page (var(--font-sans), 34px height, var(--radius-md), 0 12px padding)
- Move selected building and timeblock chips above their respective inputs
- Change filter panel slide direction from left to right to avoid overlapping the sidebar nav
- Remove redundant onMouseEnter/onMouseLeave border-color overrides from panel inputs
- Replace Input component with native input element in the events toolbar
- Match volunteer page style: var(--font-sans) 13px, 34px height, var(--radius-md), 0 12px padding
- Remove unused Input import
- Import IconPlus and render it at size 14 inside the Add event button
- Button sm size already has gap: 7px so no extra styling needed
… no-results state

- Guard addBuilding against values not in buildingOptions so Enter on unknown text is a no-op
- Show dropdown with 'No results' when focused and query has no matches (previously dropdown disappeared)
- Remove free-text fallback from building Enter key handler
- Remove setBuildingOpen(false) and setBlockOpen(false) from add handlers
- Input retains focus after selection (onMouseDown preventDefault), so onFocus never refires — dropdown now stays open so the user can keep selecting without clicking out and back in
…ader row

- Removed internal table viewport vertical scrolling so the table fills naturally with page scroll
- Kept sticky table header behavior while preserving horizontal overflow for wide columns
- Removed row windowing spacer logic tied to internal scroll container to match full-page scroll UX
- Fixed mojibake placeholder characters in Categories table empty count cells by replacing them with a plain dash
- Added backend PATCH route for category rename with validation and duplicate-name protection
- Added categoriesApi.update client method and wired category rename from Events page
- Implemented inline edit mode in Categories table with Save/Cancel actions
- Kept edit/delete actions visible for all rows but disabled them for default categories
- Replaced user-facing 'Seeded' wording with 'Default' in category type labels, tooltips, and backend error messages
…ategory protections

- Added PATCH category helper and full update-route tests in test_categories.py
- Covered success, duplicate-name rejection, not-found, wrong-tournament, permission, and unauthenticated paths
- Added assertion for default-category edit rejection detail message
- Verified category API test module passes with the new backend category update behavior
- delete overlap-specific 409 assertions from time block API tests

- keep remaining time block behavior coverage intact

- verify backend/tests/api/test_time_blocks.py passes (22 passed)
- cap affected events list height and enable internal scrolling in DeleteBlockModal
- add viewport-based max-height and vertical scrolling to shared Modal container
- prevent overflow cases where confirm/cancel buttons were pushed off-screen
@ethnjs
ethnjs marked this pull request as ready for review April 19, 2026 05:55
@railway-app
railway-app Bot temporarily deployed to nexus / nexus-pr-40 April 20, 2026 01:02 Destroyed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend breaking-change Removes or alters existing behavior requiring a migration, coordination, or careful rollout database Updates database schemas or models enhancement New feature or request events frontend

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants