diff --git a/public/index.html b/public/index.html index c9981fa..6ceeff7 100644 --- a/public/index.html +++ b/public/index.html @@ -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 `

๐Ÿ’ธ Budget Cheat Sheet

-

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.

+

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.

Confirmed roster: ${rosterCount || 'loading'} attendee${rosterCount === 1 ? '' : 's'}${groomCount ? ` ยท ${groomCount} groom${groomCount === 1 ? '' : 's'}` : ''}${bestManCount ? ` ยท ${bestManCount} best man${bestManCount === 1 ? '' : 's'}` : ''} ยท max size ${groupSizeLimit}
-
- - - - - - - - - - - ${rows.map((row) => ` - - - ${renderBudgetCell(row.budget)} - ${renderBudgetCell(row.balanced)} - ${renderBudgetCell(row.splurge)} - - `).join('')} - -
AttendeesBudget TrackBalanced TrackSplurge Track
${row.groupSize}
+
+ + +
Showing live pricing for ${selectedGroupSize} attendee${selectedGroupSize === 1 ? '' : 's'}.
+
+
+ ${[ + selectedScenarios.budget, + selectedScenarios.balanced, + selectedScenarios.splurge, + ].map((scenario) => renderBudgetCard(scenario, selectedGroupSize)).join('')}
`; @@ -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 ` +
+
+ ${selectedGroupSize} attendee${selectedGroupSize === 1 ? '' : 's'} + n/a +
+

No live budget

+
n/a
+
+ `; + } + + const note = scenario.derived + ? 'Interpolated from the latest live budget anchors.' + : 'From the latest live automation run.'; + + return ` +
+
+ ${selectedGroupSize} attendee${selectedGroupSize === 1 ? '' : 's'} + ${escapeHtml(scenario.tier)} +
+

${escapeHtml(scenario.tier)} Track

+
${escapeHtml(formatCurrency(scenario.perPerson))}
+
${escapeHtml(formatCurrency(scenario.groupTotal))} group total
+
${escapeHtml(scenario.summary || '')}
+ +
+ `; + } + // โ”€โ”€ Voting โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ function toggleVote(optionId) { openVoteConfirm(optionId);