fix player win % and visual overhaul

This commit is contained in:
SowinskiBraeden committed 2026-09-05 19:19:46 -07:00
1 parent 1b9f9510ec
commit 5c09266d75
16 files changed
+3123 -1490

No files matched your search

+302 -92
View File
@@ -29,73 +29,123 @@
</div>
<div class="cash-strip">
<div class="stat stat--rail" style="--rail:var(--accent);">
<span class="kicker">Invested</span>
<div class="stat__val" style="color:var(--num)">{{ session.total_invested_cents | money }}</div>
<div class="stat">
<span class="kicker">Cash in</span>
<div class="stat__val" style="color:var(--num)">{{ session.total_buy_in_cents | money }}</div>
{% if session.total_front_cents > 0 %}<div class="stat__sub">+ {{ session.total_front_cents | money }} fronted</div>{% endif %}
</div>
<div class="stat stat--rail" style="--rail:var(--rank-2);">
<span class="kicker">Cashout</span>
{% if session.total_payout_carry_in_cents > 0 or session.total_rollover_in_cents > 0 %}
<div class="stat">
<span class="kicker">Carry in</span>
<div class="stat__val" style="color:var(--rolled)">{{ (session.total_payout_carry_in_cents + session.total_rollover_in_cents) | money }}</div>
</div>
{% endif %}
<div class="stat">
<span class="kicker">Claimed</span>
<div class="stat__val" style="color:var(--num)">{{ session.total_cash_out_cents | money }}</div>
</div>
<div class="stat stat--rail" style="--rail:var(--pos);">
<div class="stat">
<span class="kicker">Paid out</span>
<div class="stat__val" style="color:var(--pos)">{{ session.total_paid_out_cents | money }}</div>
</div>
<div class="stat stat--rail" style="--rail:var(--warn);">
<span class="kicker">Open items</span>
<div class="stat__val" style="color:var(--num)">{{ (session.total_current_due_to_player_cents + session.total_current_due_to_house_cents) | money }}</div>
{% set total_unsettled = session.total_current_due_to_player_cents + session.total_current_due_to_house_cents %}
<div class="stat">
<span class="kicker">Unsettled</span>
<div class="stat__val" style="color:{{ 'var(--warn)' if total_unsettled > 0 else 'var(--muted)' }}">{{ total_unsettled | money }}</div>
{% if session.total_current_due_to_house_cents > 0 %}<div class="stat__sub" style="color:var(--neg)">{{ session.total_current_due_to_house_cents | money }} owed to house</div>{% endif %}
{% if session.total_current_due_to_player_cents > 0 %}<div class="stat__sub" style="color:var(--warn)">{{ session.total_current_due_to_player_cents | money }} owed to players</div>{% endif %}
</div>
</div>
<div class="league-manage-grid" style="margin-top:20px;align-items:stretch;">
<div class="stack">
{% if can_manage %}
<form class="panel form-card" method="post" id="appendForm" style="align-self:stretch;">
<form class="panel form-card ledger-form" method="post" id="appendForm">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div>
<p class="eyebrow">Append event</p>
<h2 class="panel__title" style="margin-top:4px;">Ledger entry</h2>
<div class="ledger-form__head">
<span class="eyebrow">Ledger entry</span>
{% if session_model.status == 'open' %}
<span class="ledger-form__status ledger-form__status--open">Live</span>
{% else %}
<span class="ledger-form__status ledger-form__status--closed">Closed</span>
{% endif %}
</div>
{% if session_model.status != 'open' %}
<div class="session-locked-banner">
<strong>Session closed</strong>
<span>Books assumed settled. Reopen to add events.</span>
<span>Reopen to add events.</span>
</div>
{% endif %}
<fieldset {% if session_model.status != 'open' %}disabled{% endif %} style="border:none;padding:0;margin:0;min-width:0;">
<fieldset {% if session_model.status != 'open' %}disabled{% endif %} style="border:none;padding:0;margin:0;min-width:0;display:grid;gap:10px;">
<label>
<span>Player</span>
<input
type="text"
id="playerSearch"
list="playerList"
placeholder="Start typing a name…"
autocomplete="off"
value="{{ players | selectattr('id', 'equalto', append_form.player_id) | map(attribute='display_name') | first | default('') }}"
>
<datalist id="playerList">
{% for player in players %}
<option value="{{ player.display_name }}"></option>
{% endfor %}
</datalist>
<div class="player-picker" id="playerPicker">
<div class="player-picker__input-wrap">
<input
type="text"
id="playerSearch"
class="player-picker__search"
placeholder="Select a player…"
autocomplete="off"
value="{{ players | selectattr('id', 'equalto', append_form.player_id) | map(attribute='display_name') | first | default('') }}"
>
<svg class="player-picker__chevron" width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden="true"><path d="M2 4l4 4 4-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
</div>
<div class="player-picker__dropdown" id="playerDropdown" hidden>
{% for player in players %}
<button type="button" class="player-picker__opt" data-name="{{ player.display_name }}" data-id="{{ player.id }}">{{ player.display_name }}</button>
{% endfor %}
</div>
</div>
<input type="hidden" name="player_id" id="playerIdInput" value="{{ append_form.player_id or '' }}">
</label>
<label>
<span>Event type</span>
<select name="event_type" id="eventTypeSelect" required>
{% for value, label in event_types %}
<option value="{{ value }}" {{ 'selected' if append_form.event_type == value else '' }}>{{ label }}</option>
{% endfor %}
</select>
</label>
<label>
<span>Amount</span>
<input type="number" step="0.01" min="0" name="amount" value="{{ append_form.amount }}" placeholder="0.00">
</label>
<div class="ledger-form__ta-row">
<div class="ledger-form__inputs">
<label>
<span>Type</span>
<select name="event_type" id="eventTypeSelect" required>
{% for value, label in event_types %}
<option value="{{ value }}" {{ 'selected' if append_form.event_type == value else '' }}>{{ label }}</option>
{% endfor %}
</select>
</label>
<label>
<span>Amount</span>
<input type="number" step="0.01" min="0" name="amount" id="amountInput" value="{{ append_form.amount }}" placeholder="0.00">
</label>
</div>
{% if default_buyin_cents > 0 or default_rebuy_cents > 0 %}
<div class="amount-quickset" id="amountQuickset">
<div class="amount-quickset__presets">
{% if default_buyin_cents > 0 %}
<button type="button" class="amount-quickset__btn"
data-event-type="buyin"
data-base="{{ '%.2f' % (default_buyin_cents / 100) }}">
<span class="amount-quickset__lbl">Buy-in</span>
<span class="amount-quickset__val">${{ '%.2f' % (default_buyin_cents / 100) }}</span>
</button>
{% endif %}
{% if default_rebuy_cents > 0 %}
<button type="button" class="amount-quickset__btn"
data-event-type="buyin"
data-base="{{ '%.2f' % (default_rebuy_cents / 100) }}">
<span class="amount-quickset__lbl">Rebuy</span>
<span class="amount-quickset__val">${{ '%.2f' % (default_rebuy_cents / 100) }}</span>
</button>
{% endif %}
</div>
<div class="amount-quickset__mults">
<button type="button" class="amount-quickpick__btn" data-mult="1">1×</button>
<button type="button" class="amount-quickpick__btn" data-mult="2">2×</button>
</div>
</div>
{% endif %}
</div>
<label>
<span>Note <span class="form-opt-hint">(optional)</span></span>
<textarea name="note" placeholder="Optional context">{{ append_form.note or '' }}</textarea>
<textarea name="note" rows="1" placeholder="Optional context" style="resize:vertical;">{{ append_form.note or '' }}</textarea>
</label>
<button class="btn btn--primary" type="submit" style="margin-top:8px;">Append event</button>
<button class="btn btn--primary ledger-form__submit" type="submit">Append event</button>
</fieldset>
</form>
{% else %}
@@ -111,16 +161,31 @@
<p class="kicker" style="margin:0;">Results</p>
<h2 class="panel__title">Player totals</h2>
</div>
<span class="panel__tag">{{ session.entries|length }} players</span>
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;">
{% set ns = namespace(playing=0, settled=0, owes=0, owed=0) %}
{% for entry in session.entries %}
{% if entry.cash_out_cents == 0 and entry.invested_cents > 0 %}{% set ns.playing = ns.playing + 1 %}
{% elif entry.current_due_to_house_cents > 0 %}{% set ns.owes = ns.owes + 1 %}
{% elif entry.current_due_to_player_cents > 0 %}{% set ns.owed = ns.owed + 1 %}
{% else %}{% set ns.settled = ns.settled + 1 %}
{% endif %}
{% endfor %}
{% if ns.playing > 0 %}<span class="ledger-tally ledger-tally--playing">{{ ns.playing }} in play</span>{% endif %}
{% if ns.owes > 0 %}<span class="ledger-tally ledger-tally--owes">{{ ns.owes }} owe</span>{% endif %}
{% if ns.owed > 0 %}<span class="ledger-tally ledger-tally--owed">{{ ns.owed }} owed</span>{% endif %}
{% if ns.settled > 0 %}<span class="ledger-tally ledger-tally--settled">{{ ns.settled }} settled</span>{% endif %}
<span class="panel__tag">{{ session.entries|length }} players</span>
</div>
</div>
{% if session.entries %}
<div class="player-ledger-list">
{% for entry in session.entries %}
{% set row_cls = 'player-ledger-row--pos' if entry.net_cents > 0 else 'player-ledger-row--neg' if entry.net_cents < 0 else '' %}
<div class="player-ledger-row {{ row_cls }}">
<div class="player-ledger-row__header">
<div class="player-ledger-row">
<div class="player-ledger-row__who">
<span class="player-ledger-row__name">{{ entry.player_name }}</span>
{% if entry.current_due_to_house_cents > 0 %}
{% if entry.cash_out_cents == 0 and entry.invested_cents > 0 %}
<span class="badge badge--playing">In play</span>
{% elif entry.current_due_to_house_cents > 0 %}
<span class="badge badge--owes">Owes {{ entry.current_due_to_house_cents | money }}</span>
{% elif entry.current_due_to_player_cents > 0 %}
<span class="badge badge--unpaid">Due {{ entry.current_due_to_player_cents | money }}</span>
@@ -128,40 +193,62 @@
<span class="badge badge--settled">Settled</span>
{% endif %}
</div>
<div class="player-ledger-row__fields">
<div class="player-ledger-row__field">
<span class="player-ledger-row__lbl">Buy in</span>
<div class="player-ledger-row__stats">
<div class="player-ledger-row__stat">
<span class="player-ledger-row__lbl">In</span>
<span class="player-ledger-row__val">{{ entry.invested_cents | money }}</span>
</div>
<div class="player-ledger-row__field">
<span class="player-ledger-row__lbl">Cashout claim</span>
<div class="player-ledger-row__stat">
<span class="player-ledger-row__lbl">Cashout</span>
<span class="player-ledger-row__val">{{ entry.cash_out_cents | money }}</span>
</div>
<div class="player-ledger-row__field">
<div class="player-ledger-row__stat">
<span class="player-ledger-row__lbl">Paid out</span>
<span class="player-ledger-row__val" style="color:var(--pos)">{{ entry.paid_out_cents | money if entry.paid_out_cents else '—' }}</span>
</div>
<div class="player-ledger-row__field player-ledger-row__field--net">
<div class="player-ledger-row__stat player-ledger-row__stat--net">
<span class="player-ledger-row__lbl">Net</span>
<span class="player-ledger-row__val {{ 'num-pos' if entry.net_cents > 0 else 'num-neg' if entry.net_cents < 0 else 'num-muted' }}">
{{ '+' if entry.net_cents > 0 else '' }}{{ entry.net_cents | money }}
</span>
<span class="player-ledger-row__val {{ 'num-pos' if entry.net_cents > 0 else 'num-neg' if entry.net_cents < 0 else 'num-muted' }}">{{ '+' if entry.net_cents > 0 else '' }}{{ entry.net_cents | money }}</span>
</div>
</div>
{% if entry.rollover_in_cents or entry.rollover_out_cents or entry.payout_carry_in_cents %}
<div class="player-ledger-row__fields player-ledger-row__fields--secondary">
<div class="player-ledger-row__field">
{% if entry.rollover_in_cents or entry.rollover_out_cents or entry.payout_carry_in_cents %}
<div class="player-ledger-row__stat">
<span class="player-ledger-row__lbl">Rolled in</span>
<span class="player-ledger-row__val" style="color:var(--rolled)">{{ entry.rollover_in_cents | money }}</span>
</div>
<div class="player-ledger-row__field">
<div class="player-ledger-row__stat">
<span class="player-ledger-row__lbl">Carry in</span>
<span class="player-ledger-row__val" style="color:var(--rolled)">{{ entry.payout_carry_in_cents | money }}</span>
</div>
<div class="player-ledger-row__field">
<span class="player-ledger-row__lbl">Rolled out</span>
<span class="player-ledger-row__val" style="color:var(--rolled)">{{ entry.rollover_out_cents | money }}</span>
</div>
{% endif %}
</div>
{% if can_manage and session_model.status == 'open' %}
<div class="player-ledger-row__actions">
{% if default_rebuy_cents > 0 %}
<form method="post" style="margin:0;">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<input type="hidden" name="player_id" value="{{ entry.player_id }}">
<input type="hidden" name="event_type" value="buyin">
<input type="hidden" name="amount" value="{{ '%.2f' % (default_rebuy_cents / 100) }}">
<button type="submit" class="btn btn--ghost">Rebuy ${{ '%.2f' % (default_rebuy_cents / 100) }}</button>
</form>
{% elif default_buyin_cents > 0 %}
<form method="post" style="margin:0;">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<input type="hidden" name="player_id" value="{{ entry.player_id }}">
<input type="hidden" name="event_type" value="buyin">
<input type="hidden" name="amount" value="{{ '%.2f' % (default_buyin_cents / 100) }}">
<button type="submit" class="btn btn--ghost">Rebuy ${{ '%.2f' % (default_buyin_cents / 100) }}</button>
</form>
{% else %}
<button type="button" class="btn btn--ghost player-row-prefill"
data-player-id="{{ entry.player_id }}"
data-player-name="{{ entry.player_name }}"
data-event-type="buyin">Rebuy</button>
{% endif %}
<button type="button" class="btn btn--ghost player-row-prefill"
data-player-id="{{ entry.player_id }}"
data-player-name="{{ entry.player_name }}"
data-event-type="cashout">Cash out</button>
</div>
{% endif %}
</div>
@@ -171,6 +258,7 @@
<p class="muted-text" style="padding:28px;">No player ledger events yet.</p>
{% endif %}
</div>
</div>
<div class="panel" style="margin-top:20px;">
@@ -315,37 +403,159 @@
</div>
<script>
const playerMap = {
{% for player in players %}"{{ player.display_name | replace('"', '\\"') }}": "{{ player.id }}"{% if not loop.last %},{% endif %}
{% endfor %}
};
const searchInput = document.getElementById('playerSearch');
const idInput = document.getElementById('playerIdInput');
const eventTypeSelect = document.getElementById('eventTypeSelect');
// --- Custom player picker ---
(function () {
const picker = document.getElementById('playerPicker');
if (!picker) return;
const searchInput = document.getElementById('playerSearch');
const idInput = document.getElementById('playerIdInput');
const dropdown = document.getElementById('playerDropdown');
const opts = dropdown ? Array.from(dropdown.querySelectorAll('.player-picker__opt')) : [];
const eventTypeSelect = document.getElementById('eventTypeSelect');
if (searchInput && idInput) {
function openDropdown() {
if (!dropdown) return;
dropdown.removeAttribute('hidden');
}
function closeDropdown() {
if (dropdown) dropdown.setAttribute('hidden', '');
}
function filterOpts(q) {
const lq = q.toLowerCase().trim();
opts.forEach(opt => {
opt.style.display = (!lq || opt.dataset.name.toLowerCase().includes(lq)) ? '' : 'none';
});
}
function selectPlayer(name, id) {
searchInput.value = name;
idInput.value = id;
filterOpts(name);
closeDropdown();
}
function clearPlayer() {
searchInput.value = '';
idInput.value = '';
filterOpts('');
}
searchInput.addEventListener('focus', () => { filterOpts(searchInput.value); openDropdown(); });
searchInput.addEventListener('input', function () {
idInput.value = playerMap[this.value.trim()] || '';
filterOpts(this.value);
openDropdown();
const matched = opts.find(o => o.dataset.name.toLowerCase() === this.value.trim().toLowerCase());
idInput.value = matched ? matched.dataset.id : '';
});
searchInput.addEventListener('change', function () {
idInput.value = playerMap[this.value.trim()] || '';
});
}
if (eventTypeSelect && searchInput && idInput) {
eventTypeSelect.addEventListener('change', function () {
if (this.value === 'note') {
searchInput.value = '';
idInput.value = '';
searchInput.placeholder = 'No player (session note)';
} else {
searchInput.placeholder = 'Start typing a name…';
searchInput.addEventListener('keydown', function (e) {
if (e.key === 'Escape') closeDropdown();
if (e.key === 'Enter') {
const visible = opts.filter(o => o.style.display !== 'none');
if (visible.length === 1) { e.preventDefault(); selectPlayer(visible[0].dataset.name, visible[0].dataset.id); }
}
});
if (eventTypeSelect.value === 'note') {
searchInput.placeholder = 'No player (session note)';
opts.forEach(opt => {
opt.addEventListener('mousedown', e => { e.preventDefault(); selectPlayer(opt.dataset.name, opt.dataset.id); });
});
document.addEventListener('click', e => { if (!picker.contains(e.target)) closeDropdown(); });
if (eventTypeSelect) {
eventTypeSelect.addEventListener('change', function () {
if (this.value === 'note') {
clearPlayer();
searchInput.placeholder = 'No player (session note)';
searchInput.disabled = true;
} else {
searchInput.placeholder = 'Search or select a player…';
searchInput.disabled = false;
}
});
if (eventTypeSelect.value === 'note') {
searchInput.placeholder = 'No player (session note)';
searchInput.disabled = true;
}
}
}
})();
// --- Quick-set amount buttons (Buy-in / Rebuy) + 1x/2x multipliers ---
(function () {
const amountInput = document.getElementById('amountInput');
const eventTypeSelect = document.getElementById('eventTypeSelect');
let quickBase = null;
let currentMult = 1;
function setAmount(val) {
if (!amountInput) return;
amountInput.value = parseFloat(val).toFixed(2);
}
function syncQuicksetActive(activeBtn) {
document.querySelectorAll('.amount-quickset__btn').forEach(b => {
b.classList.toggle('is-active', b === activeBtn);
});
}
function syncMultActive(mult) {
currentMult = mult;
document.querySelectorAll('.amount-quickpick__btn').forEach(b => {
b.classList.toggle('is-active', parseInt(b.dataset.mult) === mult);
});
}
document.querySelectorAll('.amount-quickset__btn').forEach(btn => {
btn.addEventListener('click', function () {
quickBase = parseFloat(this.dataset.base);
if (eventTypeSelect && this.dataset.eventType) {
eventTypeSelect.value = this.dataset.eventType;
}
setAmount(quickBase * currentMult);
syncQuicksetActive(this);
});
});
document.querySelectorAll('.amount-quickpick__btn').forEach(btn => {
btn.addEventListener('click', function () {
const mult = parseInt(this.dataset.mult);
syncMultActive(mult);
if (quickBase !== null) setAmount(quickBase * mult);
});
});
// Reset multiplier to 1× when user types manually
if (amountInput) {
amountInput.addEventListener('input', function () {
syncMultActive(1);
syncQuicksetActive(null);
quickBase = parseFloat(this.value) || null;
});
}
// Auto-select the Buy-in quickset button on load if buyin type and no amount
if (!amountInput || amountInput.value) return;
const buyinBtn = document.querySelector('.amount-quickset__btn[data-event-type="buyin"]');
if (buyinBtn && eventTypeSelect && eventTypeSelect.value === 'buyin') {
quickBase = parseFloat(buyinBtn.dataset.base);
setAmount(quickBase);
syncQuicksetActive(buyinBtn);
}
})();
// --- Per-player quick-action pre-fill ---
document.querySelectorAll('.player-row-prefill').forEach(function(btn) {
btn.addEventListener('click', function() {
var playerId = this.dataset.playerId;
var playerName = this.dataset.playerName;
var eventType = this.dataset.eventType;
var idInput = document.getElementById('playerIdInput');
var searchInput = document.getElementById('playerSearch');
var typeSelect = document.getElementById('eventTypeSelect');
var amountInput = document.getElementById('amountInput');
if (idInput) idInput.value = playerId;
if (searchInput) searchInput.value = playerName;
if (typeSelect) typeSelect.value = eventType;
if (amountInput) { amountInput.value = ''; amountInput.focus(); }
var form = document.getElementById('appendForm');
if (form) form.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
});
});
document.querySelectorAll('[data-modal]').forEach(function(btn) {
btn.addEventListener('click', function() {