These are planning numbers for the group to compare tracks quickly before anyone starts buying flights. They use current live price signals and bake in the shared-cost difference between 8, 10, and 12 guys.
-
- ${scenarios.map(scenario => `
-
-
- ${scenario.groupSize} guys
- ${scenario.tier}
-
- ${scenario.tier} Track
- $${scenario.perPerson.toLocaleString()}
- $${scenario.groupTotal.toLocaleString()} group total
- ${scenario.summary}
- ${scenario.notes.map(note => `- ${note}
`).join('')}
-
- `).join('')}
+
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.
+
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('')}
+
+
`;
}
+ function getBudgetScenarioGroups() {
+ const tiers = { Budget: [], Balanced: [], Splurge: [] };
+ (Array.isArray(state.budgetScenarios) ? state.budgetScenarios : []).forEach((scenario) => {
+ if (!tiers[scenario.tier]) return;
+ tiers[scenario.tier].push(scenario);
+ });
+
+ Object.values(tiers).forEach((list) => list.sort((a, b) => a.groupSize - b.groupSize));
+ return tiers;
+ }
+
+ function getBudgetGroupSizeLimit() {
+ const rosterCount = Array.isArray(state.guestRoster) ? state.guestRoster.length : 0;
+ const seededMax = Math.max(...(Array.isArray(state.budgetScenarios) ? state.budgetScenarios : []).map((scenario) => scenario.groupSize || 0), 0);
+ return Math.max(rosterCount || 0, seededMax || 0, 1);
+ }
+
+ function getDynamicBudgetScenario(tier, groupSize, groupedScenarios) {
+ const scenarios = groupedScenarios[tier] || [];
+ if (!scenarios.length) {
+ return null;
+ }
+
+ const exact = scenarios.find((scenario) => scenario.groupSize === groupSize);
+ if (exact) {
+ return exact;
+ }
+
+ const [lower, upper] = getBudgetBrackets(scenarios, groupSize);
+ const lowerSize = lower?.groupSize || upper?.groupSize || groupSize;
+ const upperSize = upper?.groupSize || lower?.groupSize || groupSize;
+ const lowerPrice = typeof lower?.perPerson === 'number' ? lower.perPerson : (typeof upper?.perPerson === 'number' ? upper.perPerson : 0);
+ const upperPrice = typeof upper?.perPerson === 'number' ? upper.perPerson : lowerPrice;
+ const ratio = lowerSize === upperSize ? 0 : (groupSize - lowerSize) / (upperSize - lowerSize);
+ const perPerson = Math.max(0, lowerPrice + ((upperPrice - lowerPrice) * ratio));
+ const roundedPerPerson = Math.round(perPerson * 100) / 100;
+ const sourceScenario = upper || lower || scenarios[0];
+ const notes = [
+ `Scaled from live ${tier.toLowerCase()} automations to ${groupSize} attendees.`,
+ ...(Array.isArray(sourceScenario?.notes) ? sourceScenario.notes.slice(0, 4) : []),
+ ];
+
+ return {
+ id: `live-${tier.toLowerCase()}-${groupSize}`,
+ tier,
+ groupSize,
+ perPerson: roundedPerPerson,
+ groupTotal: Math.round(roundedPerPerson * groupSize),
+ summary: `${groupSize} guys about ${formatCurrency(roundedPerPerson)} pp`,
+ notes,
+ derived: true,
+ };
+ }
+
+ function getBudgetBrackets(scenarios, groupSize) {
+ if (scenarios.length === 1) {
+ return [scenarios[0], scenarios[0]];
+ }
+
+ if (groupSize <= scenarios[0].groupSize) {
+ return [scenarios[0], scenarios[1]];
+ }
+
+ if (groupSize >= scenarios[scenarios.length - 1].groupSize) {
+ return [scenarios[scenarios.length - 2], scenarios[scenarios.length - 1]];
+ }
+
+ for (let index = 0; index < scenarios.length - 1; index += 1) {
+ const lower = scenarios[index];
+ const upper = scenarios[index + 1];
+ if (groupSize >= lower.groupSize && groupSize <= upper.groupSize) {
+ return [lower, upper];
+ }
+ }
+
+ return [scenarios[0], scenarios[1]];
+ }
+
+ function renderBudgetCell(scenario) {
+ if (!scenario) {
+ return '
n/a | ';
+ }
+
+ const note = scenario.derived
+ ? 'Interpolated from the latest live budget anchors.'
+ : 'From the latest live automation run.';
+
+ return `
+
+
+ ${escapeHtml(scenario.tier)}
+ ${escapeHtml(scenario.summary || `${scenario.groupSize} guys`)}
+
+ ${escapeHtml(formatCurrency(scenario.perPerson))} pp
+ ${escapeHtml(formatCurrency(scenario.groupTotal))} total
+ ${escapeHtml(note)}
+ |
+ `;
+ }
+
// โโ Voting โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function toggleVote(optionId) {
openVoteConfirm(optionId);