Skip to content

feat: event assignments board, archive freeze, and overview widgets - #74

Open
ethnjs wants to merge 94 commits into
feat/member-profilesfrom
feat/assignments
Open

feat: event assignments board, archive freeze, and overview widgets#74
ethnjs wants to merge 94 commits into
feat/member-profilesfrom
feat/assignments

Conversation

@ethnjs

@ethnjs ethnjs commented Sep 11, 2026

Copy link
Copy Markdown
Owner

feat: event assignments board, archive freeze, and overview widgets

Summary

  1. Event assignments (closes feat: tournament event assignments #70): a TD can assign a member to a role on a tournament event, optionally pinned to one of that event's shifts. Only structural errors are rejected. An assignment that contradicts what the member said (unavailable, track not confirmed, already booked at that hour) is allowed and shown as a warning, because TDs override reality constantly and a hard block would make the tool unusable.
  2. Assignments board: a real dashboard route (not the design sketch) with events on one side and the member pool on the other. Members are dragged onto events, shifts, or a cosmetic track's no-shift column. The board and member card view config are saved per viewer.
  3. Events and shifts catch up: a fields group registry on the event reads, events table view state moved into display config, bulk duplicate/delete on both tables, a mass shift editor, open panels kept in the URL, and refetch on tab focus for pages that several people edit together.
  4. Archived tournaments freeze end to end: every write route rejects an archived tournament, archiving also archives its forms, and write controls are locked instead of hidden.
  5. Overview page: a tournament header card, a staff members summary widget, per-track signup cards for the viewer, and an onboarding progress ring on the roster.

What changed

Assignments backend (#70)

  • Model: TournamentEventAssignment links an event, a membership, a tournament_membership_roles row, an optional shift, and a track. Both the role and the shift go through composite FKs: (membership_role_id, membership_id) and (shift_id, track_id). Denormalized columns can't drift from their source this way, whatever the write path. A member in a role they don't hold can't be stored at all.
  • Uniqueness is enforced by two partial unique indexes (pinned vs unpinned), not by a pre-check. A check-then-insert races. A duplicate drag becomes a 409, not a 500.
  • Routes are flat under /tournaments/{id}/assignments/, because an assignment joins an event to a member and nesting it under either one would imply an ownership that doesn't exist. GET takes optional event_id / membership_id filters, so the board, member panel, and event drawer share one route. Reads need manage_events or manage_members. Writes need manage_members.
  • Rank-bound role auto-grant: the body names a role, not a join row. If the member doesn't hold that role, it's granted in the same transaction through the same rank guard the roles route uses. Unassigning never revokes the role.
  • Track on every row: a cosmetic track (Test Writing) has no shifts, so an unpinned row used to have no reliable track. Pinned rows take their track from the shift. Unpinned rows must name one.
  • Schedule edits don't lose staffing: when a shift leaves an event, its assignments are unpinned, not deleted.
  • Per-track default_role: the role the board grants when a member is dropped on that track's column.
  • Membership fields gains an assignments group, an assigned roster filter, and a card surface. Event reads get their own fields registry (event_fields.py) with the same opt-down rules as the memberships one. An event's shifts come back in schedule order, not attach order.
  • The audit log entries for assignment create/update/delete were added, then removed within this branch.

Assignments board (frontend)

  • app/dashboard/tournaments/[id]/assignments, with a sidebar entry and IconAssignments.
  • Flag rules (lib/assignments/flags.ts): unavailable, track_unconfirmed, double_booked (compared by overlapping time, not shift id), and over_staffed against volunteers_needed. Computed client-side, because every input already arrives on responses the board fetches. A field group that wasn't requested produces no flag, since "we didn't ask" must never render as "they said no".
  • Lanes: one no-shift column per cosmetic track. Chips have a role menu that is single-select with a multi-select toggle. A chip move is diffed by cell, so dropping in the same place writes nothing. Viewers can hide tracks from the event rows.
  • Member panel: a new assignments section with a timeline that shares availability's shading, a per-track chip filter applied on the server, and edits that sync with the board in both directions.
  • Perf: the chip is split into a draggable shell and a memoized body, so crossing a drop target doesn't re-render every chip. Intl.DateTimeFormat instances are cached.

Events, shifts, and shared UI

  • Events and shifts tabs inlined into their own pages. Events table view state moved into display config. Filters store selected values instead of excluded ones, and the track filter uses chips.
  • Bulk duplicate/delete on the events toolbar, and a shifts select mode with a MassShiftEditor (BulkDeleteModal, MassResultsCard).
  • usePanelUrl keeps the open event/shift/member panel in the URL. useRefetchOnFocus covers events, shifts, and the member panel.
  • Input gets a clear button (used in every search bar). Also: a ChipInput disabled state, a compact EmptyState, DockedPanel without a close button, and Popover staying put on scroll.

Archive freeze

  • core/tournament/archive.py: one archive_tournament shared by the manual route and the auto-archive job, so their side effects can't drift apart. Before this, the job didn't clear archive_override_at.
  • Archiving now archives every form in the tournament, drafts included, so nothing keeps accepting responses. Unarchiving leaves the forms archived, and the TD republishes them on purpose.
  • require_form_not_archived is called in every form write route: create, update, delete, bulk field edits, invalidate, submit/patch response, and prerequisites. It isn't built into the access dependencies because those also gate reads, and an archived tournament's forms stay readable. Sheets, admin, display config, onboarding, leaving a tournament, and ownership transfer are also rejected while archived.
  • Frontend: useArchiveLock locks write controls across events, shifts, assignments, members, forms, sheets, and roles instead of hiding them. The form builder reads a new Form.tournament_is_archived property.

Onboarding progress

  • Behavior change: archiving a form that's in the onboarding sequence is no longer blocked. Onboarding skips any step that isn't published. Only deleting an onboarding form still needs it removed from the sequence first.
  • recompute_onboarding recalculates every active member's onboarded_at whenever the live sequence changes (a step published, unpublished, added, or removed), instead of waiting for each member's next progress call. It keeps an existing completion time.
  • New onboarding membership field group (completed / total live steps, counted from responses in two queries per roster), shown as a ProgressRing column on the members table and a field on the member panel.

Overview

  • GET /members/summary/ (manage_members): counts only, never names. It's its own route rather than a roster param because it returns counts, not memberships. It returns per-track status counts, availability-option coverage (options grouping the same shifts across forms collapse into one), and onboarding counts.
  • Widgets: TournamentHeaderCard (one tile per primary track), MemberSummaryCard (status tiles and stacked availability bars with per-segment tooltips), and MySignupCards (the viewer's own status, availability on the member panel's timeline, lunch, and event preferences, per track). The setup checklist moved into overview/ behind a shared OverviewCard header.
  • MasonryGrid now packs cards into a gapless mosaic without changing their order, and the setup checklist can span columns.

Unlisted

  • db/seed_members.py: a CLI that seeds members by replaying onboarding form submissions.
  • Setup checklist computed live, with a new first-form step.
  • Forms table status actions with shared rules. Fix for stray option keys when a question's preset or type changes.
  • Sheet-config modals moved from components/ui to components/tournament/sheets.
  • Copy/placeholder tweaks, pronoun options trimmed to four, expired invites hidden, pending-delete tracks hidden from filters and display config.

Migrations

3 new revisions, so alembic upgrade head is required.

  • b570994db077: the tournament_event_assignments table, plus a unique (id, membership_id) on tournament_membership_roles for the composite FK to target.
  • c018f2ae8e71: nullable tournament_tracks.default_role_id (ON DELETE SET NULL).
  • d3b71a4c9e02: tournament_track_id becomes NOT NULL on assignments. Backfill: a pinned row takes its shift's track. An unpinned row takes its track by matching its role against the cosmetic tracks' default roles, which is ambiguous when two tracks share a default, so ORDER BY keeps it deterministic. After that the fallbacks are the event's first cosmetic track, then any of its tracks, then the tournament's first primary track. Rows that still have no track are deleted (the track invariant says there shouldn't be any). The unpinned uniqueness index now includes the track.

Test plan

  • New: backend/tests/api/tournament/test_assignments.py (CRUD, 409 on duplicates, rank-bound auto-grant, shift-on-event validation, track resolution, unpin on shift detach, permission split).
  • New: backend/tests/api/tournament/test_member_summary.py (status, availability-option, and onboarding counts).
  • Extended: test_display_config.py, test_events.py, test_memberships.py, test_setup_checklist.py, test_shifts.py, test_join_codes.py, test_onboarding.py (skipped unpublished steps, recompute on sequence change), test_core.py / test_admin.py / test_forms.py / test_sheets.py (archive freeze and form cascade), core/test_field_groups.py.
  • Manual:
    • Drag a member onto an event shift and onto a cosmetic track column. Check that the role is auto-granted and the chip lands in the right lane.
    • Assign someone who is unavailable, unconfirmed, and double-booked. Check that all three flags render and nothing is blocked.
    • Drag the same chip into the same cell. Check that no request fires.
    • Remove a shift from an event. Check that its assignments move to the no-shift lane instead of disappearing.
    • Edit an assignment from the member panel. Check that the board updates, and the other way around.
    • Archive a tournament. Check that its forms (drafts included) archive, and that write controls on every page render locked, not hidden. Unarchive it and check that the forms stay archived.
    • Archive an onboarding form. Check that members who finished the remaining steps become onboarded without calling progress again, and that the progress ring shows the new total.
    • Open the overview as a TD and as a regular member. Check that the TD sees the members widget, and that the member sees only their own signup cards.

@ethnjs ethnjs added enhancement New feature or request backend frontend database Updates database schemas or models events tournaments labels Sep 11, 2026
@vercel

vercel Bot commented Sep 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
nexus Ready Ready Preview Sep 11, 2026 5:59am UTC

@railway-app
railway-app Bot temporarily deployed to nexus / nexus-pr-74 September 11, 2026 02:25 Destroyed
@railway-app

railway-app Bot commented Sep 11, 2026

Copy link
Copy Markdown

🚅 Deployed to the nexus-pr-74 environment in nexus

Service Status Web Updated
nexus ✅ Success (View Logs) Sep 11, 2026 at 5:59 am UTC

@ethnjs ethnjs changed the title feat: tournament event assignments and the staffing board feat: event assignments board, archive freeze, and overview widgets Sep 11, 2026
@ethnjs ethnjs added the forms label Sep 11, 2026
@ethnjs
ethnjs added this pull request to stack #75 September 11, 2026 06:02
@ethnjs
ethnjs marked this pull request as ready for review September 11, 2026 06:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend database Updates database schemas or models enhancement New feature or request events forms frontend tournaments

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: tournament event assignments

1 participant