From 5e18bbe3ed59f93bc5429f9efde90a8c95709066 Mon Sep 17 00:00:00 2001 From: Mikkel Georgsen Date: Sun, 1 Mar 2026 08:23:27 +0100 Subject: [PATCH] feat(01-11): implement Financials tab with prize pool, bubble prize, and chop/deal - PrizePoolCard: collapsible breakdown (entries, rebuys, add-ons, re-entries, rake, season reserve, net prize pool), guarantee indicator - TransactionList: DataTable-based with type filter chips, search, swipe-to-undo action, row click for receipt view - BubblePrize: prominent button, amount input pre-filled with buy-in, preview redistribution for top positions, confirm flow - DealFlow: 5 deal types (ICM, chip chop, even chop, custom, partial), multi-step wizard (select type > input > review proposal > confirm), info message about prize/league independence - Financials page: assembles prize pool card, payout preview table, bubble prize button, deal/chop button, transaction list - Fixed Svelte template unicode escapes (use HTML entities in templates) Co-Authored-By: Claude Opus 4.6 --- .../src/lib/components/BubblePrize.svelte | 330 +++++++++++ frontend/src/lib/components/DealFlow.svelte | 519 ++++++++++++++++++ .../src/lib/components/PrizePoolCard.svelte | 281 ++++++++++ .../src/lib/components/TransactionList.svelte | 210 +++++++ frontend/src/routes/financials/+page.svelte | 250 ++++++--- 5 files changed, 1518 insertions(+), 72 deletions(-) create mode 100644 frontend/src/lib/components/BubblePrize.svelte create mode 100644 frontend/src/lib/components/DealFlow.svelte create mode 100644 frontend/src/lib/components/PrizePoolCard.svelte create mode 100644 frontend/src/lib/components/TransactionList.svelte diff --git a/frontend/src/lib/components/BubblePrize.svelte b/frontend/src/lib/components/BubblePrize.svelte new file mode 100644 index 0000000..abd7c58 --- /dev/null +++ b/frontend/src/lib/components/BubblePrize.svelte @@ -0,0 +1,330 @@ + + +
+ {#if flowState === 'idle'} + + + {:else if flowState === 'input'} + +
+

Bubble Prize

+

Enter the bubble prize amount (pre-filled with buy-in).

+ +
+ + +
+ +
+ + +
+
+ {:else if flowState === 'preview' && preview} + +
+

Bubble Prize Preview

+

+ Bubble: {formatCurrency(preview.bubble_amount)} +

+ + + + + + + + + + + {#each preview.payouts.slice(0, 5) as p} + + + + + + {/each} + +
PositionOriginalAdjusted
{formatPosition(p.position)}{formatCurrency(p.original)} + {formatCurrency(p.adjusted)} +
+ +
+ + +
+
+ {:else if flowState === 'preview' && !preview} + +
+

Loading preview...

+
+ {:else if flowState === 'submitting'} + +
+

Adding bubble prize...

+
+ {/if} +
+ + diff --git a/frontend/src/lib/components/DealFlow.svelte b/frontend/src/lib/components/DealFlow.svelte new file mode 100644 index 0000000..360c12a --- /dev/null +++ b/frontend/src/lib/components/DealFlow.svelte @@ -0,0 +1,519 @@ + + +
+ {#if step === 'idle'} + {#if remainingPlayers >= 2} + + {/if} + {:else if step === 'type'} + +
+
+

Select Deal Type

+ +
+ +

+ Prize money and league positions are independent. +

+ +
+ {#each dealTypes as dt} + + {/each} +
+
+ {:else if step === 'input'} + +
+
+

+ {dealTypes.find((d) => d.value === selectedType)?.label ?? 'Deal'} Details +

+ +
+ + {#if selectedType === 'icm' || selectedType === 'chip_chop'} +

Enter chip stacks for each remaining player.

+

+ Chip stacks will be loaded from the tournament when connected. + Enter manual overrides if needed. +

+ {:else if selectedType === 'custom'} +

Enter payout amount for each player.

+

+ Total must equal the prize pool ({formatCurrency(prizePool)}). +

+ {:else if selectedType === 'partial_chop'} +

How much to split now?

+
+ + + + Remaining {formatCurrency(prizePool - partialSplitAmount)} stays in play. + +
+ {/if} + +
+ + +
+
+ {:else if step === 'review'} + +
+
+

Review Proposal

+ +
+ + {#if proposal} +
+ {dealTypes.find((d) => d.value === selectedType)?.label ?? selectedType} +
+ + + + + + + + + + + + {#each proposal.players as p} + + + + + + + {/each} + +
PlayerChipsOriginalProposed
{p.player_name}{formatCurrency(p.chips)}{formatCurrency(p.original_payout)}{formatCurrency(p.proposed_payout)}
+ + {#if selectedType === 'partial_chop'} +

+ Split: {formatCurrency(proposal.amount_to_split)} | + Remaining in play: {formatCurrency(proposal.amount_in_play - proposal.amount_to_split)} +

+ {/if} + +

+ Prize money and league positions are independent. +

+ +
+ + +
+ {:else} +

Calculating proposal...

+ {/if} +
+ {:else if step === 'submitting'} +
+

Applying deal...

+
+ {/if} +
+ + diff --git a/frontend/src/lib/components/PrizePoolCard.svelte b/frontend/src/lib/components/PrizePoolCard.svelte new file mode 100644 index 0000000..ef7bd95 --- /dev/null +++ b/frontend/src/lib/components/PrizePoolCard.svelte @@ -0,0 +1,281 @@ + + +
+ + + {#if expanded} +
+ + + + + + + + + + + + {#if (financials.rebuy_count ?? 0) > 0} + + + + + + + + {/if} + + {#if (financials.addon_count ?? 0) > 0} + + + + + + + + {/if} + + {#if (financials.reentry_count ?? 0) > 0} + + + + + + + + {/if} + + + + + + + + + + + + {#if financials.rake_breakdown && financials.rake_breakdown.length > 0} + {#each financials.rake_breakdown as rb} + + + + + {/each} + {/if} + + {#if financials.season_reserve > 0} + + + + + {/if} + + + + + + +
Entries{financials.buyin_count ?? 0}×{formatCurrency(financials.buyin_amount ?? 0)}{formatCurrency(financials.total_buyin)}
Rebuys{financials.rebuy_count}×{formatCurrency(financials.rebuy_amount ?? 0)}{formatCurrency(financials.total_rebuys)}
Add-ons{financials.addon_count}×{formatCurrency(financials.addon_amount ?? 0)}{formatCurrency(financials.total_addons)}
Re-entries{financials.reentry_count}×{formatCurrency(financials.reentry_amount ?? 0)}{formatCurrency(financials.total_reentries ?? 0)}
Total Contributions{formatCurrency(financials.total_collected)}
Rake-{formatCurrency(financials.house_fee)}
{rb.category}-{formatCurrency(rb.amount)}
Season Reserve-{formatCurrency(financials.season_reserve)}
Prize Pool{formatCurrency(financials.prize_pool)}
+
+ {/if} + + + {#if financials.guarantee && financials.guarantee > 0} +
0}> + {#if financials.guarantee_shortfall > 0} + Guarantee: {formatCurrency(financials.guarantee)} (house covers {formatCurrency(financials.guarantee_shortfall)}) + {:else} + Guarantee: {formatCurrency(financials.guarantee)} (met) + {/if} +
+ {/if} +
+ + diff --git a/frontend/src/lib/components/TransactionList.svelte b/frontend/src/lib/components/TransactionList.svelte new file mode 100644 index 0000000..0238307 --- /dev/null +++ b/frontend/src/lib/components/TransactionList.svelte @@ -0,0 +1,210 @@ + + +
+ +
+
+ {#each typeOptions as opt} + + {/each} +
+
+ + + String(item['id'])} + onrowclick={handleRowClick} + swipeActions={onundo + ? [ + { + id: 'undo', + label: 'Undo', + color: 'var(--color-error)', + handler: handleUndo + } + ] + : []} + /> +
+ + diff --git a/frontend/src/routes/financials/+page.svelte b/frontend/src/routes/financials/+page.svelte index b819616..d81493f 100644 --- a/frontend/src/routes/financials/+page.svelte +++ b/frontend/src/routes/financials/+page.svelte @@ -1,109 +1,215 @@
-

Financials

-

Prize pool and payout information.

- {#if tournament.financials} {@const fin = tournament.financials} -
-
- Total Buy-ins - {fin.total_buyin.toLocaleString()} -
-
- Total Rebuys - {fin.total_rebuys.toLocaleString()} -
-
- Total Add-ons - {fin.total_addons.toLocaleString()} -
-
- Prize Pool - {fin.prize_pool.toLocaleString()} -
-
- House Fee - {fin.house_fee.toLocaleString()} -
-
- Paid Positions - {fin.paid_positions} + + + + + + {#if fin.payouts && fin.payouts.length > 0} +
+

Payout Preview

+ + + + + + + + + + {#each fin.payouts as payout} + + + + + + {/each} + +
PositionPercentageAmount
+ {payout.position}{ordinalSuffix(payout.position)} + {#if payout.player_name} + ({payout.player_name}) + {/if} + + {fin.prize_pool > 0 + ? ((payout.amount / fin.prize_pool) * 100).toFixed(1) + : '0.0'}% + {formatCurrency(payout.amount)}
+ {#if fin.rounding_denomination > 0} +

+ Rounded to nearest {formatCurrency(fin.rounding_denomination)} +

+ {/if}
+ {/if} + + + {#if tournament.id} + + {/if} + + + {#if tournament.id} + + {/if} + + +
+

Transactions

+
+ {:else} + +

No financial data available yet.

{/if}