Switch budget tab to attendee dropdown
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user