feat/events and time blocks page - #40
Closed
ethnjs wants to merge 102 commits into
Closed
Conversation
…vents-and-time-blocks-page
…vents-and-time-blocks-page
…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
- 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
marked this pull request as ready for review
April 19, 2026 05:55
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
feat: events & time blocks page
Summary
TimeBlocksTableUI for creating/editing/deleting time blocks per tournament; time block picker in EventTable is multi-selectCategoriesTabletab with inline add, rename, and delete; color tokens wired into Cards/Timeline views; inline category creation from EventSidePanelCsvImportBarwith preview-before-import modal, template download, and export with tournament-scoped filenames; blocks column dropped from events CSV schemaPATCH /events/batchbackend endpoint saves changes in one request using no-change sentinelsEventTimelinewith zoom levels, grouping (division / category / block), color-by controls, overlapping block rendering, and smooth sticky date-header scrollingperf: virtualize event table rowscommittime_blocks,tournament_categories, andmembership_eventstables;field_type/value_typesplit oncolumn_mappings; temp membership profile fieldsbatchroute ordered before{event_id}to prevent FastAPI routing collision;tournament_idincluded in event create payload;model_fields_setused to allow clearing nullable fields via PATCH; favicon updated;parseApiErrorhelperChanged files (highlights)
frontend/app/dashboard/[tournamentId]/events/page.tsxEventTable,EventCardGrid,EventCard,EventTimeline,EventChip,EventSidePanel,EventFiltersPanel,TimeBlocksTable,CategoriesTable,CsvImportBar,DeleteBlockModalfrontend/lib/api.ts,frontend/lib/errors.ts,frontend/lib/formatters.tsevents.py(batch PATCH, query filters),time_blocks.py(new),categories.py(new)event.py(EventBatchUpdate),time_block.py(new),tournament_category.py(new)affe_time_blocks_categories_membership_events.py,23ff6e84620b_split_field_value_type.pytest_categories.py,test_time_blocks.py,test_events.py(batch endpoint)Test plan
Setup
alembic upgrade head— migration should apply cleanly with no errorsbackend/app/db/init_db.pyTime Blocks tab
DeleteBlockModalappears listing the affected events; action buttons remain visible when the event list is long (scroll inside modal)Categories tab
Events — Cards view
Events — Table view
PATCH /tournaments/{id}/events/batchfires once with array of updates; verify network tab shows single requestEvents — Timeline view
EventChipcomponents on a horizontal timeline; chips are color-coded by categoryShared filter panel
CSV import/export
--color-surfacebackground renders correctly (not transparent)Backend — batch PATCH
PATCH /tournaments/{id}/events/batchwith[{id, name: "new name"}]→ onlynamechangesPATCH /tournaments/{id}/events/batchwith field set tonull→ nullable field is clearedGET /events/batchdoes not match{event_id}route (regression check)manage_eventspermissionBackend — time blocks & categories
POST /tournaments/{id}/time_blocks→ creates block;GETlists itPATCH /tournaments/{id}/time_blocks/{id}→ updates label/timesDELETE /tournaments/{id}/time_blocks/{id}→ deletes blockPOST /tournaments/{id}/categories→ creates categoryPATCH /tournaments/{id}/categories/{id}→ renames; returns 400 when attempting to rename defaultDELETE /tournaments/{id}/categories/{id}→ returns 400 when attempting to delete defaultRegression checks
NewTournamentModal— create tournament succeeds (staleblocksfield removed from payload)useTournamenthook and API types are consistent (no TS errors in console)