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}
-
-
-
-
- Attendees
- Budget Track
- Balanced Track
- Splurge Track
-
-
-
- ${rows.map((row) => `
-
- ${row.groupSize}
- ${renderBudgetCell(row.budget)}
- ${renderBudgetCell(row.balanced)}
- ${renderBudgetCell(row.splurge)}
-
- `).join('')}
-
-
+
+
Attendees
+
+ ${Array.from({ length: groupSizeLimit }, (_, index) => index + 1).map((groupSize) => `
+ ${groupSize} attendee${groupSize === 1 ? '' : 's'}
+ `).join('')}
+
+
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 || '')}
+
+ ${[
+ ...(Array.isArray(scenario.notes) ? scenario.notes.slice(0, 4) : []),
+ note,
+ ].filter(Boolean).map((item) => `${escapeHtml(item)} `).join('')}
+
+
+ `;
+ }
+
// โโ Voting โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function toggleVote(optionId) {
openVoteConfirm(optionId);