felt/frontend/src/lib/components/AddOnFlow.svelte
Mikkel Georgsen 44b555db10 feat(01-12): implement Players tab with buy-in, bust-out, rebuy, add-on flows
- PlayerSearch: typeahead with 200ms debounce, 48px touch targets, recently active when empty
- BuyInFlow: 3-step wizard (search -> auto-seat preview -> confirm) with mini table diagram
- BustOutFlow: minimal-tap flow (table grid -> seat tap -> verify -> hitman select)
- PlayerDetail: full per-player tracking (status, chips, financials, history, undo buttons)
- RebuyFlow: quick 2-tap flow with pre-selected player support
- AddOnFlow: quick flow with mass add-on all option
- Players page: Active/Busted/All tabs, DataTable with search, swipe actions, overlay flows
- Layout: FAB actions wired to actual flows, clock pause/resume via API

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 08:23:09 +01:00

332 lines
7.8 KiB
Svelte

<script lang="ts">
import { tournament } from '$lib/stores/tournament.svelte';
import type { Player } from '$lib/stores/tournament.svelte';
import { toast } from '$lib/stores/toast.svelte';
import { api } from '$lib/api';
import PlayerSearch from './PlayerSearch.svelte';
/**
* Add-On Flow — quick flow similar to Rebuy.
*
* Features:
* - Show only during add-on window
* - "Add-On All" option for break-time mass add-ons
* - Quick flow: select player + confirm
*/
interface Props {
/** Close callback. */
onclose: () => void;
/** Pre-selected player (from player detail). */
preselectedPlayer?: Player | null;
}
let { onclose, preselectedPlayer = null }: Props = $props();
type Step = 'select' | 'confirm';
let step = $state<Step>('select');
let selectedPlayer = $state<Player | null>(null);
// Initialize from preselectedPlayer prop
$effect(() => {
if (preselectedPlayer) {
selectedPlayer = preselectedPlayer;
step = 'confirm';
}
});
let submitting = $state(false);
// Add-on config (simplified — real config from tournament)
let addonAmount = 50;
let addonChips = 15000;
/** Eligible: active players only. */
function isEligible(player: Player): boolean {
return player.status === 'active';
}
function handleSelect(player: Player): void {
selectedPlayer = player;
step = 'confirm';
}
async function confirmAddon(): Promise<void> {
if (!selectedPlayer) return;
submitting = true;
try {
await api.post(`/tournaments/${tournament.id}/addon`, {
player_id: selectedPlayer.id
});
toast.success(`${selectedPlayer.name} add-on — +${addonChips.toLocaleString()} chips`);
onclose();
} catch (err) {
toast.error(`Add-on failed: ${err instanceof Error ? err.message : 'Unknown error'}`);
} finally {
submitting = false;
}
}
async function addonAll(): Promise<void> {
submitting = true;
try {
const activePlayers = tournament.players.filter((p) => p.status === 'active');
let count = 0;
for (const player of activePlayers) {
await api.post(`/tournaments/${tournament.id}/addon`, {
player_id: player.id
});
count++;
}
toast.success(`Add-on applied to ${count} players`);
onclose();
} catch (err) {
toast.error(`Mass add-on failed: ${err instanceof Error ? err.message : 'Unknown error'}`);
} finally {
submitting = false;
}
}
function goBack(): void {
if (step === 'confirm' && !preselectedPlayer) {
step = 'select';
selectedPlayer = null;
} else {
onclose();
}
}
</script>
<div class="addon-flow">
<!-- Header -->
<div class="flow-header">
<button class="back-btn touch-target" onclick={goBack} aria-label="Go back">
<span aria-hidden="true">&larr;</span>
</button>
<h2 class="flow-title">Add-On</h2>
<button class="close-btn touch-target" onclick={onclose} aria-label="Close">
&times;
</button>
</div>
<div class="flow-content">
{#if step === 'select'}
<h3 class="step-title">Select Player</h3>
<PlayerSearch
players={tournament.players}
onselect={handleSelect}
filter={isEligible}
placeholder="Search eligible players..."
autofocus={true}
/>
<!-- Add-On All button -->
<div class="mass-addon">
<button
class="btn-addon-all touch-target"
onclick={addonAll}
disabled={submitting}
>
{submitting ? 'Processing...' : 'Add-On All Active Players'}
</button>
<p class="mass-addon-hint">
Applies add-on to all {tournament.players.filter((p) => p.status === 'active').length} active players
</p>
</div>
{:else if step === 'confirm'}
<div class="confirm-card">
<h3 class="confirm-name">{selectedPlayer?.name}</h3>
<div class="confirm-details">
<div class="confirm-row">
<span class="confirm-label">Current Chips</span>
<span class="confirm-value chips">{selectedPlayer?.chips.toLocaleString()}</span>
</div>
<div class="confirm-row">
<span class="confirm-label">Add-On Amount</span>
<span class="confirm-value currency">{addonAmount.toLocaleString()}</span>
</div>
<div class="confirm-row">
<span class="confirm-label">Chips Added</span>
<span class="confirm-value chips">+{addonChips.toLocaleString()}</span>
</div>
<div class="confirm-row">
<span class="confirm-label">Add-Ons So Far</span>
<span class="confirm-value number">{selectedPlayer?.addons ?? 0}</span>
</div>
</div>
<button
class="btn-confirm touch-target"
onclick={confirmAddon}
disabled={submitting}
>
{submitting ? 'Processing...' : 'Confirm Add-On'}
</button>
</div>
{/if}
</div>
</div>
<style>
.addon-flow {
display: flex;
flex-direction: column;
height: 100%;
background-color: var(--color-bg);
}
.flow-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-3) var(--space-4);
border-bottom: 1px solid var(--color-border);
background-color: var(--color-bg-elevated);
}
.flow-title {
font-size: var(--text-lg);
font-weight: 700;
color: var(--color-warning);
}
.back-btn, .close-btn {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
background: none;
border: none;
color: var(--color-text);
font-size: var(--text-xl);
cursor: pointer;
border-radius: var(--radius-md);
}
.back-btn:hover, .close-btn:hover {
background-color: var(--color-surface);
}
.flow-content {
flex: 1;
overflow-y: auto;
padding: var(--space-4);
display: flex;
flex-direction: column;
gap: var(--space-6);
}
.step-title {
font-size: var(--text-lg);
font-weight: 600;
color: var(--color-text);
}
/* Mass add-on */
.mass-addon {
display: flex;
flex-direction: column;
gap: var(--space-2);
align-items: center;
padding-top: var(--space-4);
border-top: 1px solid var(--color-border);
}
.btn-addon-all {
width: 100%;
padding: var(--space-3) var(--space-6);
font-size: var(--text-base);
font-weight: 600;
background-color: var(--color-warning);
color: var(--color-bg);
border: none;
border-radius: var(--radius-lg);
cursor: pointer;
transition: background-color var(--transition-fast), opacity var(--transition-fast);
}
.btn-addon-all:hover:not(:disabled) {
background-color: color-mix(in srgb, var(--color-warning) 85%, white);
}
.btn-addon-all:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.mass-addon-hint {
font-size: var(--text-xs);
color: var(--color-text-muted);
text-align: center;
}
/* Confirm card */
.confirm-card {
display: flex;
flex-direction: column;
gap: var(--space-4);
align-items: center;
padding-top: var(--space-4);
}
.confirm-name {
font-size: var(--text-2xl);
font-weight: 700;
color: var(--color-text);
}
.confirm-details {
width: 100%;
display: flex;
flex-direction: column;
background-color: var(--color-surface);
border-radius: var(--radius-lg);
border: 1px solid var(--color-border);
overflow: hidden;
}
.confirm-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--space-3) var(--space-4);
border-bottom: 1px solid var(--color-border);
}
.confirm-row:last-child {
border-bottom: none;
}
.confirm-label {
font-size: var(--text-sm);
color: var(--color-text-muted);
}
.confirm-value {
font-weight: 600;
color: var(--color-text);
}
.btn-confirm {
width: 100%;
padding: var(--space-4) var(--space-6);
font-size: var(--text-lg);
font-weight: 700;
background-color: var(--color-warning);
color: var(--color-bg);
border: none;
border-radius: var(--radius-lg);
cursor: pointer;
transition: background-color var(--transition-fast), opacity var(--transition-fast);
}
.btn-confirm:hover:not(:disabled) {
background-color: color-mix(in srgb, var(--color-warning) 85%, white);
}
.btn-confirm:disabled {
opacity: 0.6;
cursor: not-allowed;
}
</style>