felt/frontend/src/routes/more/settings/+page.svelte
Mikkel Georgsen 59badcbfe8 feat(01-14): implement More tab with templates, blind editor, wizard, settings, audit
- TemplateManager with LEGO-style building block composition (5 block types)
- BlindStructureEditor with full level fields, mixed game, reorder, add/delete
- StructureWizard generates structures from player count, chips, duration params
- More page with navigable menu to all sub-pages (admin-gated operators section)
- Templates page with DataTable list, create/edit/duplicate/delete actions
- Structures page with DataTable list, wizard integration, and editor
- Settings page with venue config, currency, receipts, theme toggle (Mocha/Latte)
- Audit log page with filterable DataTable, detail panel, and undo capability

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

395 lines
9 KiB
Svelte

<script lang="ts">
/**
* Venue settings page.
*
* Venue name, currency, rounding denomination, receipt mode,
* theme toggle (Mocha/Latte), operator management (admin only).
*/
import { auth } from '$lib/stores/auth.svelte';
import { toast } from '$lib/stores/toast.svelte';
/** Settings state. */
let venueName = $state('My Poker Room');
let currencyCode = $state('EUR');
let currencySymbol = $state('\u20AC');
let roundingDenom = $state(5);
let receiptMode = $state<'off' | 'digital' | 'print' | 'both'>('off');
let currentTheme = $state(
typeof document !== 'undefined'
? document.documentElement.getAttribute('data-theme') ?? 'mocha'
: 'mocha'
);
const CURRENCY_OPTIONS = [
{ code: 'EUR', symbol: '\u20AC' },
{ code: 'USD', symbol: '$' },
{ code: 'GBP', symbol: '\u00A3' },
{ code: 'DKK', symbol: 'kr' },
{ code: 'SEK', symbol: 'kr' },
{ code: 'NOK', symbol: 'kr' },
{ code: 'CHF', symbol: 'CHF' }
];
const RECEIPT_OPTIONS: { value: typeof receiptMode; label: string }[] = [
{ value: 'off', label: 'Off' },
{ value: 'digital', label: 'Digital' },
{ value: 'print', label: 'Print' },
{ value: 'both', label: 'Both' }
];
function handleCurrencyChange(event: Event): void {
const code = (event.target as HTMLSelectElement).value;
const cur = CURRENCY_OPTIONS.find((c) => c.code === code);
if (cur) {
currencyCode = cur.code;
currencySymbol = cur.symbol;
}
}
function toggleTheme(): void {
currentTheme = currentTheme === 'mocha' ? 'latte' : 'mocha';
if (typeof document !== 'undefined') {
document.documentElement.setAttribute('data-theme', currentTheme);
}
toast.info(`Theme switched to ${currentTheme === 'mocha' ? 'Mocha (dark)' : 'Latte (light)'}`);
}
function handleSave(): void {
toast.success('Settings saved.');
}
</script>
<div class="page-content">
<h2>Venue Settings</h2>
<p class="text-secondary">Configure your poker room.</p>
<div class="settings-form">
<!-- Venue Name -->
<div class="field-group">
<label class="field-label" for="venue-name">Venue Name</label>
<input
id="venue-name"
type="text"
class="field-input touch-target"
bind:value={venueName}
placeholder="e.g., Lucky Aces Poker Club"
/>
</div>
<!-- Currency -->
<div class="field-group">
<label class="field-label" for="currency-select">Currency</label>
<select
id="currency-select"
class="field-select touch-target"
value={currencyCode}
onchange={handleCurrencyChange}
>
{#each CURRENCY_OPTIONS as cur}
<option value={cur.code}>{cur.code} ({cur.symbol})</option>
{/each}
</select>
</div>
<!-- Rounding Denomination -->
<div class="field-group">
<label class="field-label" for="rounding-denom">Rounding Denomination ({currencySymbol})</label>
<input
id="rounding-denom"
type="number"
class="field-input number touch-target"
bind:value={roundingDenom}
min="1"
max="100"
/>
</div>
<!-- Receipt Mode -->
<div class="field-group">
<label class="field-label">Receipt Mode</label>
<div class="radio-group">
{#each RECEIPT_OPTIONS as opt}
<label class="radio-label touch-target">
<input
type="radio"
name="receipt-mode"
value={opt.value}
bind:group={receiptMode}
/>
<span>{opt.label}</span>
</label>
{/each}
</div>
</div>
<!-- Theme Toggle -->
<div class="field-group">
<label class="field-label">Theme</label>
<button class="theme-toggle touch-target" onclick={toggleTheme}>
<span class="theme-preview" class:active-theme={currentTheme === 'mocha'}>
Mocha (Dark)
</span>
<span class="theme-divider">/</span>
<span class="theme-preview" class:active-theme={currentTheme === 'latte'}>
Latte (Light)
</span>
</button>
</div>
<!-- Save -->
<button class="save-btn touch-target" onclick={handleSave}>
Save Settings
</button>
</div>
<!-- Operator Management (admin only) -->
{#if auth.isAdmin}
<div class="admin-section">
<h3 class="section-title">Operators</h3>
<p class="text-secondary">Manage floor staff (admin only).</p>
<div class="operator-list">
<div class="operator-item">
<div class="operator-info">
<span class="operator-name">{auth.operator?.name ?? 'Admin'}</span>
<span class="operator-role">admin</span>
</div>
<span class="current-badge">You</span>
</div>
<div class="operator-item">
<div class="operator-info">
<span class="operator-name">Floor Manager</span>
<span class="operator-role">floor</span>
</div>
<button class="edit-pin-btn touch-target">
Change PIN
</button>
</div>
</div>
</div>
{/if}
</div>
<style>
.page-content {
padding: var(--space-4);
}
h2 {
font-size: var(--text-2xl);
font-weight: 700;
color: var(--color-text);
margin-bottom: var(--space-1);
}
.text-secondary {
color: var(--color-text-secondary);
font-size: var(--text-sm);
margin-bottom: var(--space-6);
}
.settings-form {
display: flex;
flex-direction: column;
gap: var(--space-4);
max-width: 500px;
}
.field-group {
display: flex;
flex-direction: column;
gap: var(--space-1);
}
.field-label {
font-size: var(--text-sm);
font-weight: 600;
color: var(--color-text-secondary);
}
.field-input {
padding: var(--space-2) var(--space-3);
font-size: var(--text-base);
color: var(--color-text);
background-color: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
outline: none;
font-family: inherit;
}
.field-input:focus {
border-color: var(--color-primary);
}
.field-select {
padding: var(--space-2) var(--space-3);
font-size: var(--text-base);
color: var(--color-text);
background-color: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
outline: none;
font-family: inherit;
}
.field-select:focus {
border-color: var(--color-primary);
}
/* Radio group */
.radio-group {
display: flex;
gap: var(--space-3);
flex-wrap: wrap;
}
.radio-label {
display: flex;
align-items: center;
gap: var(--space-2);
font-size: var(--text-sm);
color: var(--color-text);
cursor: pointer;
padding: var(--space-2) var(--space-3);
background-color: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
}
.radio-label:has(input:checked) {
border-color: var(--color-primary);
background-color: var(--color-surface-hover);
}
.radio-label input[type='radio'] {
accent-color: var(--color-primary);
}
/* Theme toggle */
.theme-toggle {
display: flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-3);
font-size: var(--text-sm);
font-family: inherit;
color: var(--color-text);
background-color: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
cursor: pointer;
}
.theme-toggle:hover {
background-color: var(--color-surface-hover);
}
.theme-preview {
color: var(--color-text-muted);
}
.theme-preview.active-theme {
color: var(--color-primary);
font-weight: 600;
}
.theme-divider {
color: var(--color-text-muted);
}
/* Save button */
.save-btn {
padding: var(--space-3);
font-size: var(--text-sm);
font-weight: 600;
color: var(--color-bg);
background-color: var(--color-primary);
border: none;
border-radius: var(--radius-md);
cursor: pointer;
}
.save-btn:hover {
opacity: 0.9;
}
/* Admin section */
.admin-section {
margin-top: var(--space-8);
padding-top: var(--space-6);
border-top: 1px solid var(--color-border);
}
.section-title {
font-size: var(--text-lg);
font-weight: 700;
color: var(--color-text);
margin-bottom: var(--space-1);
}
.operator-list {
display: flex;
flex-direction: column;
background-color: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
overflow: hidden;
}
.operator-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-3) var(--space-4);
border-bottom: 1px solid var(--color-border);
}
.operator-item:last-child {
border-bottom: none;
}
.operator-info {
display: flex;
flex-direction: column;
gap: 2px;
}
.operator-name {
font-size: var(--text-sm);
font-weight: 600;
color: var(--color-text);
}
.operator-role {
font-size: var(--text-xs);
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--color-text-muted);
}
.current-badge {
font-size: var(--text-xs);
font-weight: 600;
color: var(--color-success);
padding: 2px var(--space-2);
background-color: rgba(166, 227, 161, 0.1);
border-radius: var(--radius-sm);
}
.edit-pin-btn {
padding: var(--space-1) var(--space-3);
font-size: var(--text-xs);
font-weight: 600;
color: var(--color-text);
background: none;
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
cursor: pointer;
}
.edit-pin-btn:hover {
background-color: var(--color-surface-hover);
}
</style>