Add division allocation summary card to invites page#432
Conversation
… source-detail page - Add `DivisionAllocationSummary` card to the organizer invites route. Mounted above the tabs so the per-division spot allocation is visible regardless of which tab is active. Each row collapses to show the per-source breakdown so organizers can see exactly which sources contribute spots to that division. - Fix the source-detail page's misleading "Default is 5" copy. For series sources the resolved default is `directSpotsPerComp × seriesCompCount + globalSpots` — the new copy renders the formula inline (e.g. `5 = 2 direct × 2 comps + 1 global`) so the math is never a black box. `seriesCompCount` is loaded server-side via `getInviteSourceByIdFn` so the formula always matches the resolver's math. - Source-isolation behavior is unchanged. Each source's quota is still enforced independently at claim time + Stripe re-check; the new card just surfaces the existing scoping so organizers can trust what they see.
WalkthroughA new React component Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Review rate limit: 0/1 reviews remaining, refill in 60 minutes.Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
apps/wodsmith-start/src/components/organizer/invites/division-allocation-summary.tsx (1)
60-74: Avoid duplicating source-label resolution logic.
sourceLabelduplicates logic already implemented inapps/wodsmith-start/src/routes/compete/organizer/$competitionId/invites/index.tsx(resolveSourceLabel). Consider extracting a shared helper to prevent future drift in fallback behavior/text.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/wodsmith-start/src/components/organizer/invites/division-allocation-summary.tsx` around lines 60 - 74, The function sourceLabel duplicates the source-label resolution implemented as resolveSourceLabel in the compete/organizer invites route; extract a shared helper (e.g., resolveSourceLabel or getSourceLabel) into a common module and replace the local sourceLabel with an import to that helper in apps/wodsmith-start/src/components/organizer/invites/division-allocation-summary.tsx so both components use the same logic and fallbacks (preserve the existing fallback strings "Unknown series"/"Unknown competition"); update imports and remove the duplicate function.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/wodsmith-start/test/components/division-allocation-summary.test.tsx`:
- Around line 32-188: Add the required LAT reference comment for this spec by
inserting a single line comment of the form "// `@lat`:<spec-id>" immediately next
to the DivisionAllocationSummary spec block (e.g. directly above or beside the
describe("DivisionAllocationSummary", ...) declaration); use a meaningful spec
id such as "division-allocation-summary" (or the repo's canonical LAT id) and
ensure exactly one "// `@lat`:" comment appears for this spec section (do not add
it at the top of the file or duplicate it next to each it(...) block).
In `@lat.md/competition-invites.md`:
- Around line 57-58: Replace the inline code reference `getInviteSourceByIdFn`
with a source-code wiki link using the project's link format (e.g.
[[path/to/file.ts#getInviteSourceByIdFn]]), updating the text in
lat.md/competition-invites.md so the function is referenced as a proper source
link rather than plain inline code.
---
Nitpick comments:
In
`@apps/wodsmith-start/src/components/organizer/invites/division-allocation-summary.tsx`:
- Around line 60-74: The function sourceLabel duplicates the source-label
resolution implemented as resolveSourceLabel in the compete/organizer invites
route; extract a shared helper (e.g., resolveSourceLabel or getSourceLabel) into
a common module and replace the local sourceLabel with an import to that helper
in
apps/wodsmith-start/src/components/organizer/invites/division-allocation-summary.tsx
so both components use the same logic and fallbacks (preserve the existing
fallback strings "Unknown series"/"Unknown competition"); update imports and
remove the duplicate function.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 0d3d7820-fd61-40d8-a081-f3fa53d1fba5
📒 Files selected for processing (6)
apps/wodsmith-start/src/components/organizer/invites/division-allocation-summary.tsxapps/wodsmith-start/src/routes/compete/organizer/$competitionId/invites/index.tsxapps/wodsmith-start/src/routes/compete/organizer/$competitionId/invites/sources/$sourceId.tsxapps/wodsmith-start/src/server-fns/competition-invite-fns.tsapps/wodsmith-start/test/components/division-allocation-summary.test.tsxlat.md/competition-invites.md
| describe("DivisionAllocationSummary", () => { | ||
| it("renders the championship total across all divisions", () => { | ||
| const sources = [ | ||
| source({ | ||
| id: "src_series", | ||
| kind: "series", | ||
| sourceGroupId: "grp_throwdown", | ||
| }), | ||
| source({ | ||
| id: "src_comp", | ||
| kind: "competition", | ||
| sourceCompetitionId: "comp_global", | ||
| }), | ||
| ] | ||
| render( | ||
| <DivisionAllocationSummary | ||
| divisions={divisions} | ||
| sources={sources} | ||
| allocationsBySourceByDivision={{ | ||
| src_series: { div_rxm: 5, div_rxw: 5 }, | ||
| src_comp: { div_rxm: 3, div_rxw: 3 }, | ||
| }} | ||
| seriesNamesById={{ grp_throwdown: "2025 Throwdown Series" }} | ||
| competitionNamesById={{ comp_global: "Global Leaderboard" }} | ||
| />, | ||
| ) | ||
| // 5 + 5 + 3 + 3 = 16 | ||
| expect(screen.getByText("16")).toBeInTheDocument() | ||
| }) | ||
|
|
||
| it("isolates each source's per-division spots in the breakdown", () => { | ||
| const sources = [ | ||
| source({ | ||
| id: "src_series", | ||
| kind: "series", | ||
| sourceGroupId: "grp_throwdown", | ||
| }), | ||
| source({ | ||
| id: "src_comp", | ||
| kind: "competition", | ||
| sourceCompetitionId: "comp_global", | ||
| }), | ||
| ] | ||
| render( | ||
| <DivisionAllocationSummary | ||
| divisions={divisions} | ||
| sources={sources} | ||
| allocationsBySourceByDivision={{ | ||
| // Series gives Men's RX 5 spots; Competition source gives 7 (override). | ||
| src_series: { div_rxm: 5, div_rxw: 5 }, | ||
| src_comp: { div_rxm: 7, div_rxw: 4 }, | ||
| }} | ||
| seriesNamesById={{ grp_throwdown: "2025 Throwdown Series" }} | ||
| competitionNamesById={{ comp_global: "Global Leaderboard" }} | ||
| />, | ||
| ) | ||
|
|
||
| // Click the Men's RX trigger to expand its breakdown. | ||
| const trigger = screen.getByRole("button", { | ||
| name: /Men's RX:\s*12 spots/i, | ||
| }) | ||
| fireEvent.click(trigger) | ||
|
|
||
| // Both sources appear with their independent spot counts (5 and 7). | ||
| expect(screen.getByText("2025 Throwdown Series")).toBeInTheDocument() | ||
| expect(screen.getByText("Global Leaderboard")).toBeInTheDocument() | ||
| }) | ||
|
|
||
| it("hides sources that contribute zero spots from the breakdown", () => { | ||
| const sources = [ | ||
| source({ | ||
| id: "src_series", | ||
| kind: "series", | ||
| sourceGroupId: "grp_throwdown", | ||
| }), | ||
| source({ | ||
| id: "src_zero", | ||
| kind: "competition", | ||
| sourceCompetitionId: "comp_zero", | ||
| }), | ||
| ] | ||
| render( | ||
| <DivisionAllocationSummary | ||
| divisions={[{ id: "div_rxm", label: "Men's RX" }]} | ||
| sources={sources} | ||
| allocationsBySourceByDivision={{ | ||
| src_series: { div_rxm: 5 }, | ||
| src_zero: { div_rxm: 0 }, | ||
| }} | ||
| seriesNamesById={{ grp_throwdown: "2025 Throwdown Series" }} | ||
| competitionNamesById={{ comp_zero: "Excluded Comp" }} | ||
| />, | ||
| ) | ||
|
|
||
| fireEvent.click( | ||
| screen.getByRole("button", { name: /Men's RX:\s*5 spots/i }), | ||
| ) | ||
| expect(screen.getByText("2025 Throwdown Series")).toBeInTheDocument() | ||
| expect(screen.queryByText("Excluded Comp")).not.toBeInTheDocument() | ||
| }) | ||
|
|
||
| it("disables the toggle when a division has no contributing sources", () => { | ||
| render( | ||
| <DivisionAllocationSummary | ||
| divisions={[{ id: "div_team", label: "Team RX" }]} | ||
| sources={[ | ||
| source({ | ||
| id: "src_series", | ||
| kind: "series", | ||
| sourceGroupId: "grp_throwdown", | ||
| }), | ||
| ]} | ||
| allocationsBySourceByDivision={{ src_series: {} }} | ||
| seriesNamesById={{ grp_throwdown: "2025 Throwdown Series" }} | ||
| />, | ||
| ) | ||
|
|
||
| const trigger = screen.getByRole("button", { | ||
| name: /Team RX: 0 spots/i, | ||
| }) | ||
| expect(trigger).toBeDisabled() | ||
| }) | ||
|
|
||
| it("renders the empty-state copy when no divisions are provided", () => { | ||
| render( | ||
| <DivisionAllocationSummary | ||
| divisions={[]} | ||
| sources={[]} | ||
| allocationsBySourceByDivision={{}} | ||
| />, | ||
| ) | ||
| expect( | ||
| screen.getByText(/no divisions yet/i), | ||
| ).toBeInTheDocument() | ||
| }) | ||
|
|
||
| it("shows the per-division resolved total in the trigger row", () => { | ||
| render( | ||
| <DivisionAllocationSummary | ||
| divisions={[{ id: "div_rxm", label: "Men's RX" }]} | ||
| sources={[ | ||
| source({ | ||
| id: "src_series", | ||
| kind: "series", | ||
| sourceGroupId: "grp_throwdown", | ||
| }), | ||
| ]} | ||
| allocationsBySourceByDivision={{ src_series: { div_rxm: 5 } }} | ||
| seriesNamesById={{ grp_throwdown: "2025 Throwdown Series" }} | ||
| />, | ||
| ) | ||
| const trigger = screen.getByRole("button", { | ||
| name: /Men's RX:\s*5 spots/i, | ||
| }) | ||
| expect(within(trigger).getByText("5")).toBeInTheDocument() | ||
| }) | ||
| }) |
There was a problem hiding this comment.
Add required // @lat: spec reference comment(s) next to the test section.
This suite is missing the required LAT linkage comment in the spec block.
🧩 Suggested minimal fix
describe("DivisionAllocationSummary", () => {
+ // `@lat`: [[competition-invites#Division allocation summary]]
it("renders the championship total across all divisions", () => {As per coding guidelines, "Reference test specs from code with exactly one // @lat: ... per spec section, placed next to the relevant test — not at the top of the file".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/wodsmith-start/test/components/division-allocation-summary.test.tsx`
around lines 32 - 188, Add the required LAT reference comment for this spec by
inserting a single line comment of the form "// `@lat`:<spec-id>" immediately next
to the DivisionAllocationSummary spec block (e.g. directly above or beside the
describe("DivisionAllocationSummary", ...) declaration); use a meaningful spec
id such as "division-allocation-summary" (or the repo's canonical LAT id) and
ensure exactly one "// `@lat`:" comment appears for this spec section (do not add
it at the top of the file or duplicate it next to each it(...) block).
| The per-division allocation card surfaces the **default-spots formula** inline so the resolved number is never a black box: for series sources it renders `defaultPerDivision = directSpotsPerComp × seriesCompCount + globalSpots` (e.g. `5 = 2 direct × 2 comps + 1 global`), and for single-comp sources it renders `defaultPerDivision = top globalSpots qualifies, applied to every division`. The series comp count is loaded server-side by `getInviteSourceByIdFn` (one extra count query when `source.sourceGroupId` is set) and threaded through the loader so the formula always matches the resolver's math — no client-side inference from the resolved allocation map. | ||
|
|
There was a problem hiding this comment.
Use a wiki source-code link for the server function reference.
getInviteSourceByIdFn is referenced as inline code text, but this section should use a source-code wiki link format for function references.
✏️ Suggested doc fix
-The per-division allocation card surfaces the **default-spots formula** inline so the resolved number is never a black box: for series sources it renders `defaultPerDivision = directSpotsPerComp × seriesCompCount + globalSpots` (e.g. `5 = 2 direct × 2 comps + 1 global`), and for single-comp sources it renders `defaultPerDivision = top globalSpots qualifies, applied to every division`. The series comp count is loaded server-side by `getInviteSourceByIdFn` (one extra count query when `source.sourceGroupId` is set) and threaded through the loader so the formula always matches the resolver's math — no client-side inference from the resolved allocation map.
+The per-division allocation card surfaces the **default-spots formula** inline so the resolved number is never a black box: for series sources it renders `defaultPerDivision = directSpotsPerComp × seriesCompCount + globalSpots` (e.g. `5 = 2 direct × 2 comps + 1 global`), and for single-comp sources it renders `defaultPerDivision = top globalSpots qualifies, applied to every division`. The series comp count is loaded server-side by [[apps/wodsmith-start/src/server-fns/competition-invite-fns.ts#getInviteSourceByIdFn]] (one extra count query when `source.sourceGroupId` is set) and threaded through the loader so the formula always matches the resolver's math — no client-side inference from the resolved allocation map.As per coding guidelines, "Use source code links in lat.md/ files to reference functions, classes, constants, and methods with format [[path/to/file.ts#symbol]]..."
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| The per-division allocation card surfaces the **default-spots formula** inline so the resolved number is never a black box: for series sources it renders `defaultPerDivision = directSpotsPerComp × seriesCompCount + globalSpots` (e.g. `5 = 2 direct × 2 comps + 1 global`), and for single-comp sources it renders `defaultPerDivision = top globalSpots qualifies, applied to every division`. The series comp count is loaded server-side by `getInviteSourceByIdFn` (one extra count query when `source.sourceGroupId` is set) and threaded through the loader so the formula always matches the resolver's math — no client-side inference from the resolved allocation map. | |
| The per-division allocation card surfaces the **default-spots formula** inline so the resolved number is never a black box: for series sources it renders `defaultPerDivision = directSpotsPerComp × seriesCompCount + globalSpots` (e.g. `5 = 2 direct × 2 comps + 1 global`), and for single-comp sources it renders `defaultPerDivision = top globalSpots qualifies, applied to every division`. The series comp count is loaded server-side by [[apps/wodsmith-start/src/server-fns/competition-invite-fns.ts#getInviteSourceByIdFn]] (one extra count query when `source.sourceGroupId` is set) and threaded through the loader so the formula always matches the resolver's math — no client-side inference from the resolved allocation map. |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lat.md/competition-invites.md` around lines 57 - 58, Replace the inline code
reference `getInviteSourceByIdFn` with a source-code wiki link using the
project's link format (e.g. [[path/to/file.ts#getInviteSourceByIdFn]]), updating
the text in lat.md/competition-invites.md so the function is referenced as a
proper source link rather than plain inline code.
Summary
Adds a new
DivisionAllocationSummarycomponent to the organizer invites page that displays how qualifying spots are distributed across championship divisions. The component shows per-division totals and allows expanding each division to see the per-source breakdown, giving organizers visibility into which sources contribute spots to each division.Key Changes
New component:
DivisionAllocationSummaryrenders a collapsible card showing:Enhanced source details page:
directSpotsPerComp × seriesCompCount + globalSpotsServer function enhancement:
getInviteSourceByIdFnnow returnsseriesCompCountfor series sources, enabling the details page to render the formula without inferring from the resolved allocation mapIntegration: Added
DivisionAllocationSummaryto the main invites page, passing divisions, sources, and the allocation mapComprehensive test coverage: Added 188 lines of tests covering:
Implementation Details
The component reads the same
allocationsBySourceByDivisionmap used by the Sources/Sent tabs, ensuring a single source of truth for resolved spots per (source, championship-division) pair. Each source's quota is enforced independently at claim time, so the per-source breakdown matches the runtime enforcement scoping organizers care about.The formula display on the source details page is derived directly from the source row and
seriesCompCountrather than inferred from the resolved allocation map, ensuring the displayed formula always matches what the resolver computes.https://claude.ai/code/session_01AdwTtm2zcKjKBaE9ToAPhU
Summary by cubic
Adds a collapsible Division Allocation Summary card to the invites page so organizers can see per-division totals and which sources contribute spots. Also shows the default-spots formula on the source details page to make the math clear and accurate.
DivisionAllocationSummarycard with per-division totals; expand to see per-source counts. Hides zero-spot sources and disables expansion when none.seriesCompCount.getInviteSourceByIdFnreturnsseriesCompCountfor series sources to power the formula display.Written for commit 4d285b3. Summary will update on new commits. Review in cubic
Summary by CodeRabbit
New Features
Tests
Documentation