Skip to content

fix(assignments): send each assignment edit as one all-or-nothing request - #120

Merged
klin2686 merged 8 commits into
mainfrom
fix/assignment-placements
Oct 16, 2026
Merged

klin2686 merged 8 commits into
mainfrom
fix/assignment-placements

Conversation

@klin2686

@klin2686 klin2686 commented Oct 16, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

  1. Each edit on the assignments board and the member panel used to send one request per assignment row, so a failure partway through left the edit half-applied (for example, a member on both events after a drag). Every edit is now one request that lands entirely or not at all.
  2. Changing a track's default role now moves the affected assignments in the same request as the setting, instead of one PATCH per row afterwards, and only moves bars whose sole role is the old default.
  3. Fixes the track header showing the old default role until a page reload, which also made switching back to the previous default do nothing.
  4. Sorts an event's bars by member name, so an edited bar no longer jumps to the bottom of its event (which also broke resizing any bar but the bottom one mid-drag) and the order survives a reload.
  5. Resizing a bar now snaps its edge to the nearest shift boundary instead of always taking the whole shift the edge is in.

Closes #118

What changed

Backend

Schemas and routes

  • New PUT /tournaments/{tournament_id}/assignments/placements/ (manage_members) takes a list of AssignmentPlacement, each {membership_id, tournament_event_id, tournament_track_id, cells: [{tournament_shift_id, role_id}]}, and returns the resulting AssignmentRead rows.
  • Each placement's existing rows for that member, event and track are diffed against cells: only the difference is inserted or deleted, unchanged rows keep their ids, and empty cells clears the placement.
  • The whole list commits once; any 403, 404, 409 or 422 changes nothing, and listing the same member, event and track twice is a 422.
  • cells is an explicit list rather than shift_ids × role_ids, because a pinned and an unpinned bar can share a track with different roles, and a product would rewrite one when editing the other.
  • TournamentTrackUpdate gains reassign_assignments: bool; with a new default_role_id, PATCH .../tracks/{id}/ also moves the track's bars whose only role is the old default to the new one in the same commit.
  • reassign_assignments additionally requires manage_members, and is a 422 without a default_role_id.

Logic

  • set_placements runs every check (get_scoped_or_404, validate_shift_on_event, resolve_assignment_track, authorize_role_grant) before changing any row, so a delete earlier in the request can't change whether a later grant counts as already held.
  • New reassign_track_role in core/tournament/assignments.py rank-checks every affected member through authorize_role_grant before moving any row.
  • reassign_track_role checks per bar (one member on one event): a bar holding any other role alongside the old default was set up by hand and is left alone, so no row is ever deleted.

Frontend

  • assignmentsApi.setPlacements and placementKeyOf (in lib/assignments/board.ts) are new; TournamentTrackUpdate gains reassign_assignments.
  • Board (assignments/page.tsx): createAssignmentRow/deleteAssignmentRow are replaced by syncPlacements, which sends every placement a gesture touched as one request and swaps those placements' rows for the response.
  • handleResizeCommit, setLaneRoles, handleRemove and both handleDragEnd branches now go through syncPlacements; on error the whole gesture rolls back.
  • When two in-flight requests cover the same placement, syncPlacements applies only the newer one's response.
  • applyDefaultRoleChange sends one track PATCH with reassign_assignments instead of a PATCH per row; the confirm dialog's count and the local update use the new soleRoleBars helper in lib/assignments/board.ts, which applies the same per-bar rule.
  • applyDefaultRoleChange now also updates each event's embedded copy of the track (event.tracks), which is what the track headers render and what requestDefaultRoleChange compares against.
  • buildLanes in lib/assignments/lanes.ts takes a comparator and sorts lanes, instead of leaving them in row order, which followed write history; the board sorts by first name, last name, then membership id, and the member panel sorts its event bars by event name.
  • byName in lib/memberSort.ts is now exported for the board's comparator.
  • Board resize (TrackShiftGrid.tsx) and member panel resize (EventBar in AssignmentsSection.tsx) round the dragged edge to the nearest column boundary instead of flooring it; the existing clamp keeps at least the bar's anchor shift.
  • Member panel (AssignmentsSection.tsx): syncLane, removeLane and handleDragEnd each send one setPlacements request; a drag between tracks clears the source and fills the target in that same request.
  • The member panel's runWrite (which re-read the member after a failure to find out what half-landed) is removed, and onChanged now fires only after a successful write.

Out of scope

  • The single-row POST/PATCH/DELETE assignment routes stay; assignmentsApi.create/update/delete are now unused by the frontend but left in place.
  • Zone assignments and role grants (PATCH .../members/{id}/roles/).

Test plan

  • pytest passes locally
  • Migration included for every model change (none)
  • Clicked through the UI change in a browser

Automated

  • tests/api/tournament/test_assignments.py: TestPlacementsRoute covers adding, removing and keeping rows, stable ids, clearing, a move between events, rows outside the placement, rollback on a 403, a 409 and a 422, a repeated placement, an archived tournament, and a cross-tournament event.
  • tests/api/tournament/test_tracks.py: reassign_assignments moves only bars whose sole role is the old default on that track (per event, across shifts), leaves a bar that also holds the new role, needs manage_members, is rank-checked before anything changes, and needs a default_role_id; without the flag rows are untouched.

Manual

  • Board: drag a member onto an event, resize the bar, toggle a role, drag it to another event, and remove it; each gesture shows one PUT .../placements/ in the Network tab.
  • Board: block */assignments/placements/* in DevTools and drag a bar to another event; the bar returns to the source event and an error toast shows.
  • Board: resize, re-role and drag a bar that isn't the bottom one in its event; it stays in its alphabetical place before and after a reload, and a resize tracks the cursor until release.
  • Board and member panel: drag a bar's right edge just past a shift border and release; it snaps back, and past the shift's middle it takes the shift; the same for the left edge.
  • Board: change a track's default role from its header; only bars whose sole role was the old default move, the header shows the new role at once, and changing it back works without a reload.
  • Member panel: drag an event onto a shift, resize it, change its role, move it to another track, and remove it; each gesture is one request and the board updates to match.

@railway-app

railway-app Bot commented Oct 16, 2026

Copy link
Copy Markdown

This PR was not deployed automatically as @klin2686 does not have access to the Railway project.

In order to get automatic PR deploys, please add @klin2686 to your workspace on Railway.

@vercel

vercel Bot commented Oct 16, 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 Oct 16, 2026 5:27am UTC

@klin2686
klin2686 merged commit 8d090a6 into main Oct 16, 2026
4 checks passed

This branch was successfully deployed

1 active deployment
Preview — 04ca1000 Deployed Oct 16, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix: assignment edits that touch several rows are sent as separate requests and can half-apply

1 participant