Switch budget tab to attendee dropdown
This commit is contained in:
@@ -930,6 +930,39 @@
|
|||||||
grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
|
||||||
gap: 12px;
|
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 {
|
.budget-table-wrap {
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
@@ -1586,6 +1619,7 @@
|
|||||||
priceUpdatedAt: '',
|
priceUpdatedAt: '',
|
||||||
priceHistoryRunCount: 0,
|
priceHistoryRunCount: 0,
|
||||||
sortMode: localStorage.getItem('cabo_sort_mode') || 'vote-desc',
|
sortMode: localStorage.getItem('cabo_sort_mode') || 'vote-desc',
|
||||||
|
budgetGuestCount: Number(localStorage.getItem('cabo_budget_guest_count') || 0),
|
||||||
priceSourceSelections: (() => {
|
priceSourceSelections: (() => {
|
||||||
try {
|
try {
|
||||||
return JSON.parse(localStorage.getItem('cabo_price_source_selections') || '{}');
|
return JSON.parse(localStorage.getItem('cabo_price_source_selections') || '{}');
|
||||||
@@ -1672,6 +1706,7 @@
|
|||||||
if (!res.ok) return;
|
if (!res.ok) return;
|
||||||
const payload = await res.json();
|
const payload = await res.json();
|
||||||
state.guestRoster = Array.isArray(payload.guests) ? payload.guests : [];
|
state.guestRoster = Array.isArray(payload.guests) ? payload.guests : [];
|
||||||
|
syncBudgetGuestCount();
|
||||||
renderGuestAuthOptions();
|
renderGuestAuthOptions();
|
||||||
render();
|
render();
|
||||||
} catch {
|
} catch {
|
||||||
@@ -2665,6 +2700,7 @@
|
|||||||
|
|
||||||
const groupedScenarios = getBudgetScenarioGroups();
|
const groupedScenarios = getBudgetScenarioGroups();
|
||||||
const groupSizeLimit = getBudgetGroupSizeLimit();
|
const groupSizeLimit = getBudgetGroupSizeLimit();
|
||||||
|
const selectedGroupSize = getSelectedBudgetGuestCount(groupSizeLimit);
|
||||||
const rosterCount = Array.isArray(state.guestRoster) ? state.guestRoster.length : 0;
|
const rosterCount = Array.isArray(state.guestRoster) ? state.guestRoster.length : 0;
|
||||||
const groomCount = Array.isArray(state.guestRoster)
|
const groomCount = Array.isArray(state.guestRoster)
|
||||||
? state.guestRoster.filter((guest) => guest.role === 'groom').length
|
? state.guestRoster.filter((guest) => guest.role === 'groom').length
|
||||||
@@ -2672,40 +2708,32 @@
|
|||||||
const bestManCount = Array.isArray(state.guestRoster)
|
const bestManCount = Array.isArray(state.guestRoster)
|
||||||
? state.guestRoster.filter((guest) => guest.role === 'best-man').length
|
? state.guestRoster.filter((guest) => guest.role === 'best-man').length
|
||||||
: 0;
|
: 0;
|
||||||
const rows = Array.from({ length: groupSizeLimit }, (_, index) => index + 1)
|
const selectedScenarios = {
|
||||||
.map((groupSize) => ({
|
budget: getDynamicBudgetScenario('Budget', selectedGroupSize, groupedScenarios),
|
||||||
groupSize,
|
balanced: getDynamicBudgetScenario('Balanced', selectedGroupSize, groupedScenarios),
|
||||||
budget: getDynamicBudgetScenario('Budget', groupSize, groupedScenarios),
|
splurge: getDynamicBudgetScenario('Splurge', selectedGroupSize, groupedScenarios),
|
||||||
balanced: getDynamicBudgetScenario('Balanced', groupSize, groupedScenarios),
|
};
|
||||||
splurge: getDynamicBudgetScenario('Splurge', groupSize, groupedScenarios),
|
|
||||||
}));
|
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<section class="budget-board">
|
<section class="budget-board">
|
||||||
<h2>💸 Budget Cheat Sheet</h2>
|
<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-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">
|
<div class="budget-controls">
|
||||||
<table class="budget-table">
|
<label for="budgetGuestCountSelect">Attendees</label>
|
||||||
<thead>
|
<select id="budgetGuestCountSelect" onchange="setBudgetGuestCount(this.value)">
|
||||||
<tr>
|
${Array.from({ length: groupSizeLimit }, (_, index) => index + 1).map((groupSize) => `
|
||||||
<th>Attendees</th>
|
<option value="${groupSize}"${groupSize === selectedGroupSize ? ' selected' : ''}>${groupSize} attendee${groupSize === 1 ? '' : 's'}</option>
|
||||||
<th>Budget Track</th>
|
`).join('')}
|
||||||
<th>Balanced Track</th>
|
</select>
|
||||||
<th>Splurge Track</th>
|
<div class="budget-selected-note">Showing live pricing for ${selectedGroupSize} attendee${selectedGroupSize === 1 ? '' : 's'}.</div>
|
||||||
</tr>
|
</div>
|
||||||
</thead>
|
<div class="budget-grid">
|
||||||
<tbody>
|
${[
|
||||||
${rows.map((row) => `
|
selectedScenarios.budget,
|
||||||
<tr${row.groupSize === groupSizeLimit ? ' class="is-max"' : ''}>
|
selectedScenarios.balanced,
|
||||||
<td class="budget-attendee-count">${row.groupSize}</td>
|
selectedScenarios.splurge,
|
||||||
${renderBudgetCell(row.budget)}
|
].map((scenario) => renderBudgetCard(scenario, selectedGroupSize)).join('')}
|
||||||
${renderBudgetCell(row.balanced)}
|
|
||||||
${renderBudgetCell(row.splurge)}
|
|
||||||
</tr>
|
|
||||||
`).join('')}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
`;
|
`;
|
||||||
@@ -2728,6 +2756,29 @@
|
|||||||
return Math.max(rosterCount || 0, seededMax || 0, 1);
|
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) {
|
function getDynamicBudgetScenario(tier, groupSize, groupedScenarios) {
|
||||||
const scenarios = groupedScenarios[tier] || [];
|
const scenarios = groupedScenarios[tier] || [];
|
||||||
if (!scenarios.length) {
|
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 ────────────────────────────────────────────────
|
// ── Voting ────────────────────────────────────────────────
|
||||||
function toggleVote(optionId) {
|
function toggleVote(optionId) {
|
||||||
openVoteConfirm(optionId);
|
openVoteConfirm(optionId);
|
||||||
|
|||||||
Reference in New Issue
Block a user