Switch budget tab to attendee dropdown

This commit is contained in:
TopherMayor
2026-04-30 20:34:33 -07:00
parent 1e0a072231
commit 538de0039c

View File

@@ -930,6 +930,39 @@
grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
gap: 12px;
}
.budget-controls {
margin-top: 12px;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 10px;
}
.budget-controls label {
font-size: 0.68rem;
text-transform: uppercase;
letter-spacing: 0.8px;
color: #fbc08c;
font-weight: 700;
}
.budget-controls select {
min-width: 180px;
padding: 8px 12px;
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.12);
background: rgba(11, 13, 20, 0.9);
color: #fff;
font-size: 0.84rem;
outline: none;
cursor: pointer;
}
.budget-controls select:focus {
border-color: rgba(251, 191, 36, 0.55);
box-shadow: 0 0 0 2px rgba(251, 191, 36, 0.08);
}
.budget-selected-note {
font-size: 0.72rem;
color: #ffcf9c;
}
.budget-table-wrap {
margin-top: 16px;
overflow-x: auto;
@@ -1586,6 +1619,7 @@
priceUpdatedAt: '',
priceHistoryRunCount: 0,
sortMode: localStorage.getItem('cabo_sort_mode') || 'vote-desc',
budgetGuestCount: Number(localStorage.getItem('cabo_budget_guest_count') || 0),
priceSourceSelections: (() => {
try {
return JSON.parse(localStorage.getItem('cabo_price_source_selections') || '{}');
@@ -1672,6 +1706,7 @@
if (!res.ok) return;
const payload = await res.json();
state.guestRoster = Array.isArray(payload.guests) ? payload.guests : [];
syncBudgetGuestCount();
renderGuestAuthOptions();
render();
} catch {
@@ -2665,6 +2700,7 @@
const groupedScenarios = getBudgetScenarioGroups();
const groupSizeLimit = getBudgetGroupSizeLimit();
const selectedGroupSize = getSelectedBudgetGuestCount(groupSizeLimit);
const rosterCount = Array.isArray(state.guestRoster) ? state.guestRoster.length : 0;
const groomCount = Array.isArray(state.guestRoster)
? state.guestRoster.filter((guest) => guest.role === 'groom').length
@@ -2672,40 +2708,32 @@
const bestManCount = Array.isArray(state.guestRoster)
? state.guestRoster.filter((guest) => guest.role === 'best-man').length
: 0;
const rows = Array.from({ length: groupSizeLimit }, (_, index) => index + 1)
.map((groupSize) => ({
groupSize,
budget: getDynamicBudgetScenario('Budget', groupSize, groupedScenarios),
balanced: getDynamicBudgetScenario('Balanced', groupSize, groupedScenarios),
splurge: getDynamicBudgetScenario('Splurge', groupSize, groupedScenarios),
}));
const selectedScenarios = {
budget: getDynamicBudgetScenario('Budget', selectedGroupSize, groupedScenarios),
balanced: getDynamicBudgetScenario('Balanced', selectedGroupSize, groupedScenarios),
splurge: getDynamicBudgetScenario('Splurge', selectedGroupSize, groupedScenarios),
};
return `
<section class="budget-board">
<h2>💸 Budget Cheat Sheet</h2>
<p>These numbers recalculate from the latest automation run and scale from 1 attendee up to the full confirmed roster so we can compare Budget, Balanced, and Splurge tracks side by side.</p>
<p>Pick the attendee count you want to model. The app will show only the live Budget, Balanced, and Splurge data for that one group size.</p>
<div class="budget-stamp">Confirmed roster: ${rosterCount || 'loading'} attendee${rosterCount === 1 ? '' : 's'}${groomCount ? ` · ${groomCount} groom${groomCount === 1 ? '' : 's'}` : ''}${bestManCount ? ` · ${bestManCount} best man${bestManCount === 1 ? '' : 's'}` : ''} · max size ${groupSizeLimit}</div>
<div class="budget-table-wrap">
<table class="budget-table">
<thead>
<tr>
<th>Attendees</th>
<th>Budget Track</th>
<th>Balanced Track</th>
<th>Splurge Track</th>
</tr>
</thead>
<tbody>
${rows.map((row) => `
<tr${row.groupSize === groupSizeLimit ? ' class="is-max"' : ''}>
<td class="budget-attendee-count">${row.groupSize}</td>
${renderBudgetCell(row.budget)}
${renderBudgetCell(row.balanced)}
${renderBudgetCell(row.splurge)}
</tr>
`).join('')}
</tbody>
</table>
<div class="budget-controls">
<label for="budgetGuestCountSelect">Attendees</label>
<select id="budgetGuestCountSelect" onchange="setBudgetGuestCount(this.value)">
${Array.from({ length: groupSizeLimit }, (_, index) => index + 1).map((groupSize) => `
<option value="${groupSize}"${groupSize === selectedGroupSize ? ' selected' : ''}>${groupSize} attendee${groupSize === 1 ? '' : 's'}</option>
`).join('')}
</select>
<div class="budget-selected-note">Showing live pricing for ${selectedGroupSize} attendee${selectedGroupSize === 1 ? '' : 's'}.</div>
</div>
<div class="budget-grid">
${[
selectedScenarios.budget,
selectedScenarios.balanced,
selectedScenarios.splurge,
].map((scenario) => renderBudgetCard(scenario, selectedGroupSize)).join('')}
</div>
</section>
`;
@@ -2728,6 +2756,29 @@
return Math.max(rosterCount || 0, seededMax || 0, 1);
}
function getSelectedBudgetGuestCount(groupSizeLimit = getBudgetGroupSizeLimit()) {
const parsed = Number(state.budgetGuestCount);
if (Number.isFinite(parsed) && parsed >= 1) {
return Math.min(Math.floor(parsed), groupSizeLimit);
}
return groupSizeLimit;
}
function syncBudgetGuestCount() {
const nextCount = getSelectedBudgetGuestCount(getBudgetGroupSizeLimit());
state.budgetGuestCount = nextCount;
localStorage.setItem('cabo_budget_guest_count', String(nextCount));
}
function setBudgetGuestCount(value) {
const nextCount = Number.parseInt(value, 10);
if (!Number.isFinite(nextCount) || nextCount < 1) return;
state.budgetGuestCount = Math.min(nextCount, getBudgetGroupSizeLimit());
localStorage.setItem('cabo_budget_guest_count', String(state.budgetGuestCount));
render();
}
function getDynamicBudgetScenario(tier, groupSize, groupedScenarios) {
const scenarios = groupedScenarios[tier] || [];
if (!scenarios.length) {
@@ -2811,6 +2862,44 @@
`;
}
function renderBudgetCard(scenario, selectedGroupSize) {
if (!scenario) {
return `
<article class="budget-card">
<div class="budget-meta">
<span>${selectedGroupSize} attendee${selectedGroupSize === 1 ? '' : 's'}</span>
<span class="budget-tier budget-tier-pill">n/a</span>
</div>
<h3>No live budget</h3>
<div class="budget-price">n/a</div>
</article>
`;
}
const note = scenario.derived
? 'Interpolated from the latest live budget anchors.'
: 'From the latest live automation run.';
return `
<article class="budget-card">
<div class="budget-meta">
<span>${selectedGroupSize} attendee${selectedGroupSize === 1 ? '' : 's'}</span>
<span class="budget-tier ${scenario.tier.toLowerCase()}">${escapeHtml(scenario.tier)}</span>
</div>
<h3>${escapeHtml(scenario.tier)} Track</h3>
<div class="budget-price">${escapeHtml(formatCurrency(scenario.perPerson))}</div>
<div class="budget-total">${escapeHtml(formatCurrency(scenario.groupTotal))} group total</div>
<div class="budget-summary">${escapeHtml(scenario.summary || '')}</div>
<ul>
${[
...(Array.isArray(scenario.notes) ? scenario.notes.slice(0, 4) : []),
note,
].filter(Boolean).map((item) => `<li>${escapeHtml(item)}</li>`).join('')}
</ul>
</article>
`;
}
// ── Voting ────────────────────────────────────────────────
function toggleVote(optionId) {
openVoteConfirm(optionId);