import React, { useEffect, useMemo, useState } from "react";
import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Separator } from "@/components/ui/separator";
import { Badge } from "@/components/ui/badge";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
MoreHorizontal,
Copy,
Plus,
Trash2,
RotateCcw,
Download,
Settings,
Info,
Pin,
PinOff,
Printer,
ArrowRight,
Wand2,
} from "lucide-react";
import {
ResponsiveContainer,
LineChart,
Line,
XAxis,
YAxis,
CartesianGrid,
Tooltip as ReTooltip,
BarChart,
Bar,
Legend,
} from "recharts";
// =====================
// Accelerate 3.0 — Scenario Calculator (Team Tool)
// =====================
// Key rules enforced per Ken:
// - Pricing tiers are ONLY Silver / Gold / Platinum (based on annual spend minimums).
// - “+” levels are NOT pricing tiers; they only increase the dividend rate.
// - Dividend “+” levels are fixed spend bands:
// Silver+ = 15,000–19,999
// Gold+ = 30,000–34,999
// Plat+ = 50,000–74,999
// - Free goods earned quarterly by family (Silver/Gold/Platinum) with cumulative threshold gates.
// - 90-day activation target = 25% of the annual minimum for the enrolled tier.
// - Dividend earned on growth above PY baseline AND annual minimum.
// ---------- Helpers ----------
const fmtMoney = (n) =>
new Intl.NumberFormat(undefined, {
style: "currency",
currency: "USD",
maximumFractionDigits: 0,
}).format(Number.isFinite(n) ? n : 0);
const fmtPct = (n) => `${(Number.isFinite(n) ? n : 0).toFixed(1)}%`;
const STORAGE_KEY = "accelerate3_scenarios_v2";
const SETTINGS_KEY = "accelerate3_settings_v2";
const DEFAULT_SETTINGS = {
// Annual minimum spend for pricing eligibility (and enrollment tier minimum)
mins: {
Silver: 10000,
Gold: 20000,
Platinum: 35000,
},
// Free goods: family rate by pricing tier family
freeGoodsRate: {
Silver: 0.12,
Gold: 0.07,
Platinum: 0.03,
},
// Quarterly gates: cumulative % of annual min required by end of each quarter
quarterGates: {
Q1: 0.25,
Q2: 0.5,
Q3: 0.75,
Q4: 1.0,
},
// Activation: 90-day target is 25% of annual minimum
activation: {
targetPct: 0.25,
bonusAmount: 500,
// thresholds are % of target
bonusThreshold: 1.0,
noBonusFloor: 0.9,
},
// Dividend rates (growth dividend)
dividendRate: {
Silver: 0.1,
"Silver+": 0.14,
Gold: 0.16,
"Gold+": 0.18,
Platinum: 0.19,
"Platinum+": 0.2,
},
// Dividend “+” qualification spend bands (editable)
plusBands: {
// inclusive lower, inclusive upper
SilverPlus: { min: 15000, max: 19999 },
GoldPlus: { min: 30000, max: 34999 },
PlatinumPlus: { min: 50000, max: 74999 },
},
// Royalty milestone % by consecutive years and family
royalty: {
years: [3, 5, 7, 10],
rate: {
3: { Silver: 0.04, Gold: 0.05, Platinum: 0.06 },
5: { Silver: 0.05, Gold: 0.06, Platinum: 0.07 },
7: { Silver: 0.06, Gold: 0.07, Platinum: 0.08 },
10: { Silver: 0.07, Gold: 0.08, Platinum: 0.09 },
},
},
pricingReview: {
bufferDays: 30,
},
};
function loadJSON(key, fallback) {
try {
const raw = localStorage.getItem(key);
if (!raw) return fallback;
const parsed = JSON.parse(raw);
return parsed ?? fallback;
} catch {
return fallback;
}
}
function saveJSON(key, value) {
try {
localStorage.setItem(key, JSON.stringify(value));
} catch {
// ignore
}
}
function uuid() {
return Math.random().toString(16).slice(2) + Date.now().toString(16);
}
function inBand(x, band) {
const n = Number(x || 0);
return n >= band.min && n <= band.max;
}
function pricingTierFromAnnual(total, mins) {
if (total >= mins.Platinum) return "Platinum";
if (total >= mins.Gold) return "Gold";
if (total >= mins.Silver) return "Silver";
return "Not Qualified";
}
function dividendLevelFromSpend(pricingFamily, annualTotal, settings) {
// “+” levels are spend bands; they boost the dividend rate only.
if (pricingFamily === "Not Qualified") return "Not Qualified";
if (pricingFamily === "Silver") {
return inBand(annualTotal, settings.plusBands.SilverPlus) ? "Silver+" : "Silver";
}
if (pricingFamily === "Gold") {
return inBand(annualTotal, settings.plusBands.GoldPlus) ? "Gold+" : "Gold";
}
if (pricingFamily === "Platinum") {
return inBand(annualTotal, settings.plusBands.PlatinumPlus) ? "Platinum+" : "Platinum";
}
return "Not Qualified";
}
function activationStatus(memberType, enrolledTier, spend90d, settings) {
// 90-day activation is ONLY evaluated for NEW members.
if (memberType !== "New") {
return {
status: "N/A (Existing member)",
pct: 0,
target: 0,
bonus: 0,
qualified: true,
disqualified: false,
na: true,
};
}
const min = settings.mins[enrolledTier];
const target = min * settings.activation.targetPct;
const pct = target > 0 ? spend90d / target : 0;
if (pct >= settings.activation.bonusThreshold) {
return {
status: "Qualified + $500 Bonus",
pct,
target,
bonus: settings.activation.bonusAmount,
qualified: true,
disqualified: false,
na: false,
};
}
if (pct >= settings.activation.noBonusFloor) {
return {
status: "Qualified (No Bonus)",
pct,
target,
bonus: 0,
qualified: true,
disqualified: false,
na: false,
};
}
return {
status: "Disqualified (<90%)",
pct,
target,
bonus: 0,
qualified: false,
disqualified: true,
na: false,
};
}
function quarterlyFreeGoods(quarters, pricingFamily, settings) {
const min = pricingFamily === "Not Qualified" ? 0 : settings.mins[pricingFamily];
const rate = settings.freeGoodsRate[pricingFamily] ?? 0;
const qOrder = ["Q1", "Q2", "Q3", "Q4"];
let cumulative = 0;
const out = {};
for (const q of qOrder) {
const spendQ = Number(quarters[q] ?? 0);
cumulative += spendQ;
const gate = settings.quarterGates[q] ?? 0;
const required = min * gate;
const passed = cumulative >= required && min > 0;
out[q] = {
spendQ,
cumulative,
required,
passed,
earned: passed ? spendQ * rate : 0,
};
}
const totalEarned = qOrder.reduce((s, q) => s + out[q].earned, 0);
return { byQuarter: out, totalEarned, rate, min };
}
function growthDividend({ annualTotal, baselinePY, pricingFamily, dividendLevel, settings }) {
if (pricingFamily === "Not Qualified")
return { eligible: false, reason: "Below annual minimum", growth: 0, rate: 0, payout: 0 };
const min = settings.mins[pricingFamily];
if (annualTotal < min)
return { eligible: false, reason: "Below annual minimum", growth: 0, rate: 0, payout: 0 };
const growth = Math.max(0, annualTotal - baselinePY);
if (growth <= 0)
return { eligible: false, reason: "No growth vs PY baseline", growth, rate: 0, payout: 0 };
const rate = settings.dividendRate[dividendLevel] ?? 0;
return { eligible: true, reason: "Eligible", growth, rate, payout: growth * rate };
}
function royaltyPayout({ annualTotal, pricingFamily, consecutiveYears, settings }) {
const yearsList = settings.royalty.years;
const milestone = yearsList.includes(consecutiveYears) ? consecutiveYears : null;
if (!milestone) return { eligible: false, milestone: null, rate: 0, payout: 0 };
if (pricingFamily === "Not Qualified") return { eligible: false, milestone, rate: 0, payout: 0 };
const rate = settings.royalty.rate[milestone]?.[pricingFamily] ?? 0;
return { eligible: rate > 0, milestone, rate, payout: annualTotal * rate };
}
function tierAccent(tier) {
if (tier === "Silver") return "from-sky-500/15 to-sky-500/5";
if (tier === "Gold") return "from-amber-500/15 to-amber-500/5";
if (tier === "Platinum") return "from-violet-500/15 to-violet-500/5";
return "from-slate-500/10 to-slate-500/5";
}
function tierBadgeVariant(tier) {
if (tier === "Not Qualified") return "secondary";
return "default";
}
function computeGaps(annualTotal, pricingFamily, dividendLevel, settings) {
// Next pricing tier gap
const mins = settings.mins;
let nextTier = null;
let nextTierTarget = null;
if (pricingFamily === "Not Qualified") {
nextTier = "Silver";
nextTierTarget = mins.Silver;
} else if (pricingFamily === "Silver") {
nextTier = "Gold";
nextTierTarget = mins.Gold;
} else if (pricingFamily === "Gold") {
nextTier = "Platinum";
nextTierTarget = mins.Platinum;
} else {
nextTier = null;
nextTierTarget = null;
}
const gapToNextTier = nextTierTarget ? Math.max(0, nextTierTarget - annualTotal) : 0;
// Next dividend “+” gap (inside pricing family)
let nextPlusLabel = null;
let nextPlusTarget = null;
if (pricingFamily === "Silver") {
const band = settings.plusBands.SilverPlus;
if (annualTotal < band.min) {
nextPlusLabel = "Silver+";
nextPlusTarget = band.min;
} else {
nextPlusLabel = null;
nextPlusTarget = null;
}
}
if (pricingFamily === "Gold") {
const band = settings.plusBands.GoldPlus;
if (annualTotal < band.min) {
nextPlusLabel = "Gold+";
nextPlusTarget = band.min;
} else {
nextPlusLabel = null;
nextPlusTarget = null;
}
}
if (pricingFamily === "Platinum") {
const band = settings.plusBands.PlatinumPlus;
if (annualTotal < band.min) {
nextPlusLabel = "Platinum+";
nextPlusTarget = band.min;
} else {
nextPlusLabel = null;
nextPlusTarget = null;
}
}
const gapToNextPlus = nextPlusTarget ? Math.max(0, nextPlusTarget - annualTotal) : 0;
return { nextTier, gapToNextTier, nextPlusLabel, gapToNextPlus };
}
// ---------- Defaults ----------
function makeScenario(overrides = {}) {
return {
id: overrides.id ?? uuid(),
name: overrides.name ?? "Scenario",
memberType: overrides.memberType ?? "Existing",
enrolledTier: overrides.enrolledTier ?? "Silver", // impacts 90-day target
spend: {
Q1: overrides.spend?.Q1 ?? 0,
Q2: overrides.spend?.Q2 ?? 0,
Q3: overrides.spend?.Q3 ?? 0,
Q4: overrides.spend?.Q4 ?? 0,
},
spend90d: overrides.spend90d ?? 0,
baselinePY: overrides.baselinePY ?? 0,
consecutiveYears: overrides.consecutiveYears ?? 0,
notes: overrides.notes ?? "",
// Compare
pinned: overrides.pinned ?? false,
};
}
const EXAMPLES = [
makeScenario({
name: "Example — Silver office",
memberType: "New",
enrolledTier: "Silver",
spend90d: 2600,
spend: { Q1: 3200, Q2: 2800, Q3: 2500, Q4: 2200 },
baselinePY: 9000,
consecutiveYears: 3,
}),
makeScenario({
name: "Example — Gold growth push",
memberType: "Existing",
enrolledTier: "Gold",
spend90d: 5200,
spend: { Q1: 7000, Q2: 6500, Q3: 7200, Q4: 7600 },
baselinePY: 22000,
consecutiveYears: 5,
}),
makeScenario({
name: "Example — Platinum heavy",
memberType: "Existing",
enrolledTier: "Platinum",
spend90d: 9500,
spend: { Q1: 13000, Q2: 12000, Q3: 15000, Q4: 14000 },
baselinePY: 41000,
consecutiveYears: 7,
}),
];
function PrintSheet({ payload }) {
// Print-only markup
return (
Accelerate 3.0 — Scenario Summary
Internal use • Generated from Scenario Calculator
{payload.tiles.map((t) => (
{t.label}
{t.value}
{t.sub ?
{t.sub}
: null}
))}
Inputs
Member type: {payload.memberType}
Enrolled tier (90-day): {payload.enrolledTier}
90-day spend: {fmtMoney(payload.spend90d)}
PY baseline: {fmtMoney(payload.baselinePY)}
Consecutive years: {payload.consecutiveYears}
Quarterly spend: Q1 {fmtMoney(payload.spend.Q1)} • Q2 {fmtMoney(payload.spend.Q2)} • Q3 {fmtMoney(payload.spend.Q3)} • Q4 {fmtMoney(payload.spend.Q4)}
Earnings Summary
Free goods (YTD): {fmtMoney(payload.freeGoodsYTD)}
Growth dividend: {fmtMoney(payload.dividendPayout)} ({payload.dividendExplain})
Royalty: {fmtMoney(payload.royaltyPayout)} ({payload.royaltyExplain})
{payload.notes ? (
) : null}
Quarter gates & free goods
| Quarter |
Spend |
Cumulative |
Required |
Gate |
Free goods |
{payload.qRows.map((r) => (
| {r.q} |
{fmtMoney(r.spendQ)} |
{fmtMoney(r.cumulative)} |
{fmtMoney(r.required)} |
{r.passed ? "PASS" : "MISS"} |
{fmtMoney(r.earned)} |
))}
Pricing tiers: Silver/Gold/Platinum based on annual spend. “+” levels only apply to dividend rate within fixed spend bands.
);
}
function ScenarioCard({
scenario,
settings,
onChange,
onDuplicate,
onDelete,
onRename,
onReset,
onPrint,
}) {
const annualTotal = useMemo(() => Object.values(scenario.spend).reduce((s, v) => s + Number(v || 0), 0), [scenario.spend]);
const pricingFamily = useMemo(() => pricingTierFromAnnual(annualTotal, settings.mins), [annualTotal, settings.mins]);
const dividendLevel = useMemo(
() => dividendLevelFromSpend(pricingFamily, annualTotal, settings),
[pricingFamily, annualTotal, settings]
);
const activation = useMemo(
() => activationStatus(scenario.memberType, scenario.enrolledTier, Number(scenario.spend90d || 0), settings),
[scenario.memberType, scenario.enrolledTier, scenario.spend90d, settings]
);
const freeGoods = useMemo(
() => quarterlyFreeGoods(scenario.spend, pricingFamily, settings),
[scenario.spend, pricingFamily, settings]
);
const dividend = useMemo(
() =>
growthDividend({
annualTotal,
baselinePY: Number(scenario.baselinePY || 0),
pricingFamily,
dividendLevel,
settings,
}),
[annualTotal, scenario.baselinePY, pricingFamily, dividendLevel, settings]
);
const royalty = useMemo(
() =>
royaltyPayout({
annualTotal,
pricingFamily,
consecutiveYears: Number(scenario.consecutiveYears || 0),
settings,
}),
[annualTotal, pricingFamily, scenario.consecutiveYears, settings]
);
const { nextTier, gapToNextTier, nextPlusLabel, gapToNextPlus } = useMemo(
() => computeGaps(annualTotal, pricingFamily, dividendLevel, settings),
[annualTotal, pricingFamily, dividendLevel, settings]
);
const qualificationBadge = useMemo(() => {
if (pricingFamily === "Not Qualified") return { label: "Below Minimum", variant: "secondary" };
return { label: `Pricing: ${pricingFamily}`, variant: "default" };
}, [pricingFamily]);
const dividendBadge = useMemo(() => {
if (pricingFamily === "Not Qualified") return { label: "Dividend: N/A", variant: "secondary" };
return {
label: `Dividend: ${dividendLevel} (${fmtPct((settings.dividendRate[dividendLevel] ?? 0) * 100)})`,
variant: "outline",
};
}, [pricingFamily, dividendLevel, settings]);
const chartData = useMemo(() => {
const qOrder = ["Q1", "Q2", "Q3", "Q4"];
return qOrder.map((q) => {
const d = freeGoods.byQuarter[q];
return {
quarter: q,
spend: d?.spendQ ?? 0,
cumulative: d?.cumulative ?? 0,
required: d?.required ?? 0,
freeGoods: d?.earned ?? 0,
};
});
}, [freeGoods]);
const setSpend = (q, value) => {
const v = Number(value);
onChange({ ...scenario, spend: { ...scenario.spend, [q]: Number.isFinite(v) ? v : 0 } });
};
const setField = (key, value) => onChange({ ...scenario, [key]: value });
const riskNotes = useMemo(() => {
const notes = [];
if (scenario.memberType === "New") {
if (activation.disqualified)
notes.push("New-member activation: currently below 90% of the 90-day target (disqualified).");
else if (!activation.qualified) notes.push("New-member activation: not yet qualified.");
else if (activation.bonus === 0)
notes.push("New-member activation: qualified, but below 100% (no $500 bonus).");
}
if (pricingFamily !== "Not Qualified") {
const missed = Object.entries(freeGoods.byQuarter)
.filter(([q, d]) => q !== "Q4" && d && !d.passed)
.map(([q]) => q);
if (missed.length) notes.push(`Free goods gates missed so far: ${missed.join(", ")}.`);
}
if (!dividend.eligible && pricingFamily !== "Not Qualified") notes.push(`Dividend not earned: ${dividend.reason}.`);
// Plus-band note (important because Platinum+ is a fixed band)
if (pricingFamily === "Platinum") {
const b = settings.plusBands.PlatinumPlus;
if (annualTotal > b.max) {
notes.push(
`Platinum+ is defined as ${fmtMoney(b.min)}–${fmtMoney(b.max)}. This scenario is above that band, so it uses Platinum (19%) dividend rate.`
);
}
}
return notes;
}, [scenario.memberType, activation, pricingFamily, freeGoods, dividend, annualTotal, settings]);
const tileClass = `rounded-2xl bg-gradient-to-b ${tierAccent(pricingFamily)} border border-slate-200/60`;
return (
{scenario.name}
{qualificationBadge.label}
{dividendBadge.label}
{scenario.pinned ? (
Pinned
) : null}
Member: {scenario.memberType} • Enrolled tier (90-day target):{" "}
{scenario.enrolledTier}
Scenario
onDuplicate(scenario)}>
Duplicate
{
onChange({ ...scenario, pinned: !scenario.pinned });
}}
>
{scenario.pinned ? (
<>
Unpin from compare
>
) : (
<>
Pin for compare
>
)}
onReset(scenario)}>
Reset inputs
onPrint(scenario)}>
Print / Save PDF
{
const newName = prompt("Rename scenario", scenario.name);
if (newName && newName.trim()) onRename({ ...scenario, name: newName.trim() });
}}
>
Rename
onDelete(scenario)}>
Delete
{/* Inputs */}
{(["Q1", "Q2", "Q3", "Q4"]).map((q) => (
setSpend(q, e.target.value)}
min={0}
/>
))}
setField("notes", e.target.value)}
placeholder="Optional (internal only)"
/>
{/* KPI Tiles */}
Annual qualified spend
{fmtMoney(annualTotal)}
Pricing family: {pricingFamily}
Free goods (YTD earned)
{fmtMoney(freeGoods.totalEarned)}
Rate: {fmtPct(freeGoods.rate * 100)}
Growth dividend (projected)
{fmtMoney(dividend.payout)}
{dividend.eligible ? (
Growth: {fmtMoney(dividend.growth)} • Rate:{" "}
{fmtPct(dividend.rate * 100)}
) : (
{dividend.reason}
)}
{/* Gap tiles */}
Gap to next pricing tier
{nextTier ? (
<>
{fmtMoney(gapToNextTier)}
Next tier:
{nextTier}
{fmtMoney(settings.mins[nextTier])}
>
) : (
<>
—
Already at top pricing tier
>
)}
Gap to higher dividend (+)
{nextPlusLabel ? (
<>
{fmtMoney(gapToNextPlus)}
Target:
{nextPlusLabel}
{nextPlusLabel === "Silver+" ? fmtMoney(settings.plusBands.SilverPlus.min) : nextPlusLabel === "Gold+" ? fmtMoney(settings.plusBands.GoldPlus.min) : fmtMoney(settings.plusBands.PlatinumPlus.min)}
>
) : (
<>
—
Already at highest dividend rate for this pricing tier
>
)}
Royalty milestone
{royalty.milestone ? `${royalty.milestone}-Year` : "—"}
{royalty.eligible ? (
Rate: {fmtPct(royalty.rate * 100)} • Payout:{" "}
{fmtMoney(royalty.payout)}
) : (
Not eligible
)}
90-day activation
{activation.status}
{activation.na ? (
Shown only for New members.
) : (
Target: {fmtMoney(activation.target)} • Progress: {fmtPct(activation.pct * 100)}
)}
0
? "bg-emerald-600 hover:bg-emerald-600"
: "bg-slate-600 hover:bg-slate-600"
}
>
{activation.na
? "N/A"
: activation.bonus > 0
? `Bonus ${fmtMoney(activation.bonus)}`
: activation.disqualified
? "Disqualified"
: "Qualified"}
Quarter gates (cumulative)
Free goods are earned on the quarter’s spend only if the cumulative gate is met by end of that quarter.
{(["Q1", "Q2", "Q3", "Q4"]).map((q) => {
const d = freeGoods.byQuarter[q];
if (!d) return null;
return (
{q}
Required
{fmtMoney(d.required)}
{d.passed ? "Pass" : "Miss"}
Earned
{fmtMoney(d.earned)}
);
})}
Spend vs gates
(v >= 1000 ? `${Math.round(v / 1000)}k` : v)} />
[fmtMoney(v), n]} />
Free goods earned by quarter
(v >= 1000 ? `${Math.round(v / 1000)}k` : v)} />
[fmtMoney(v), n]} />
{riskNotes.length > 0 && (
Flags / coaching points
{riskNotes.map((n, idx) => (
- {n}
))}
)}
);
}
function SettingsPanel({ settings, setSettings }) {
const update = (path, value) => {
const next = structuredClone(settings);
const [a, b, c] = path;
if (c !== undefined) next[a][b][c] = value;
else next[a][b] = value;
setSettings(next);
};
return (
Admin settings (editable)
Tune rates, gates, and + bands during internal testing.
Saved locally
Annual minimums
{(["Silver", "Gold", "Platinum"]).map((k) => (
update(["mins", k], Number(e.target.value))}
/>
))}
Free goods rate
{(["Silver", "Gold", "Platinum"]).map((k) => (
update(["freeGoodsRate", k], Number(e.target.value))}
/>
))}
Enter as decimals (0.12 = 12%).
Dividend rates
{(["Silver", "Silver+", "Gold", "Gold+", "Platinum", "Platinum+"]).map((k) => (
update(["dividendRate", k], Number(e.target.value))}
/>
))}
Enter as decimals (0.18 = 18%).
Quarter gates (cumulative)
{(["Q1", "Q2", "Q3", "Q4"]).map((k) => (
update(["quarterGates", k], Number(e.target.value))}
/>
))}
0.25 = 25% of annual minimum by end of Q1, etc.
Dividend “+” spend bands
These bands only affect dividend rate — not pricing tier.
Activation rules
update(["activation", "targetPct"], Number(e.target.value))}
/>
update(["activation", "bonusAmount"], Number(e.target.value))}
/>
update(["activation", "bonusThreshold"], Number(e.target.value))}
/>
update(["activation", "noBonusFloor"], Number(e.target.value))}
/>
Example: 0.25 target, 1.0 bonus, 0.9 qualify floor.
);
}
function computeScenarioMetrics(s, settings) {
const annualTotal = Object.values(s.spend).reduce((sum, v) => sum + Number(v || 0), 0);
const pricingFamily = pricingTierFromAnnual(annualTotal, settings.mins);
const dividendLevel = dividendLevelFromSpend(pricingFamily, annualTotal, settings);
const activation = activationStatus(s.memberType, s.enrolledTier, Number(s.spend90d || 0), settings);
const freeGoods = quarterlyFreeGoods(s.spend, pricingFamily, settings);
const dividend = growthDividend({
annualTotal,
baselinePY: Number(s.baselinePY || 0),
pricingFamily,
dividendLevel,
settings,
});
const royalty = royaltyPayout({
annualTotal,
pricingFamily,
consecutiveYears: Number(s.consecutiveYears || 0),
settings,
});
const gaps = computeGaps(annualTotal, pricingFamily, dividendLevel, settings);
return { annualTotal, pricingFamily, dividendLevel, activation, freeGoods, dividend, royalty, gaps };
}
function CompareBar({ pinned, settings }) {
const rows = useMemo(() => {
return pinned.map((s) => {
const m = computeScenarioMetrics(s, settings);
return {
id: s.id,
name: s.name,
pricing: m.pricingFamily,
divLevel: m.dividendLevel,
spend: m.annualTotal,
freeGoods: m.freeGoods.totalEarned,
dividend: m.dividend.payout,
activation: m.activation.status,
};
});
}, [pinned, settings]);
if (pinned.length === 0) return null;
return (
Compare mode
Pinned scenarios (up to 4 recommended)
{pinned.length} pinned
| Scenario |
Pricing |
Dividend Level |
Annual Spend |
Free Goods (YTD) |
Dividend |
Activation |
{rows.map((r) => (
| {r.name} |
{r.pricing} |
{r.divLevel} |
{fmtMoney(r.spend)} |
{fmtMoney(r.freeGoods)} |
{fmtMoney(r.dividend)} |
{r.activation} |
))}
);
}
export default function App() {
const [settings, setSettings] = useState(DEFAULT_SETTINGS);
const [scenarios, setScenarios] = useState([
makeScenario({ name: "Silver Growth Example", enrolledTier: "Silver", memberType: "New" }),
makeScenario({ name: "Gold Growth Example", enrolledTier: "Gold", memberType: "Existing" }),
makeScenario({ name: "Platinum Growth Example", enrolledTier: "Platinum", memberType: "Existing" })
]);
const [activeTab, setActiveTab] = useState("scenarios");
// print mode
const [printPayload, setPrintPayload] = useState(null);
// Load persisted
useEffect(() => {
const loadedSettings = loadJSON(SETTINGS_KEY, DEFAULT_SETTINGS);
// shallow merge with defaults
const merged = {
...DEFAULT_SETTINGS,
...loadedSettings,
mins: { ...DEFAULT_SETTINGS.mins, ...(loadedSettings?.mins || {}) },
freeGoodsRate: { ...DEFAULT_SETTINGS.freeGoodsRate, ...(loadedSettings?.freeGoodsRate || {}) },
quarterGates: { ...DEFAULT_SETTINGS.quarterGates, ...(loadedSettings?.quarterGates || {}) },
activation: { ...DEFAULT_SETTINGS.activation, ...(loadedSettings?.activation || {}) },
dividendRate: { ...DEFAULT_SETTINGS.dividendRate, ...(loadedSettings?.dividendRate || {}) },
plusBands: { ...DEFAULT_SETTINGS.plusBands, ...(loadedSettings?.plusBands || {}) },
royalty: { ...DEFAULT_SETTINGS.royalty, ...(loadedSettings?.royalty || {}) },
pricingReview: { ...DEFAULT_SETTINGS.pricingReview, ...(loadedSettings?.pricingReview || {}) },
};
setSettings(merged);
const loadedScenarios = loadJSON(STORAGE_KEY, null);
if (Array.isArray(loadedScenarios) && loadedScenarios.length) {
// backfill pinned flag
setScenarios(loadedScenarios.map((s) => ({ ...makeScenario({ ...s, id: s.id ?? uuid() }), ...s })));
}
}, []);
useEffect(() => {
saveJSON(STORAGE_KEY, scenarios);
}, [scenarios]);
useEffect(() => {
saveJSON(SETTINGS_KEY, settings);
}, [settings]);
const pinned = useMemo(() => scenarios.filter((s) => s.pinned), [scenarios]);
const addScenario = () => {
const n = scenarios.length + 1;
setScenarios((prev) => [...prev, makeScenario({ name: `Scenario ${n}` })]);
};
const duplicateScenario = (s) => {
const copy = makeScenario({
name: `${s.name} (copy)`,
memberType: s.memberType,
enrolledTier: s.enrolledTier,
spend: { ...s.spend },
spend90d: s.spend90d,
baselinePY: s.baselinePY,
consecutiveYears: s.consecutiveYears,
notes: s.notes,
pinned: false,
});
setScenarios((prev) => [...prev, copy]);
};
const deleteScenario = (s) => setScenarios((prev) => prev.filter((x) => x.id !== s.id));
const resetScenario = (s) => {
setScenarios((prev) => prev.map((x) => (x.id === s.id ? makeScenario({ id: x.id, name: x.name }) : x)));
};
const updateScenario = (next) => setScenarios((prev) => prev.map((x) => (x.id === next.id ? next : x)));
const renameScenario = (next) => updateScenario(next);
function loadExamples() {
setScenarios([
makeScenario({ name: "Silver Growth Example", enrolledTier: "Silver", memberType: "New" }),
makeScenario({ name: "Gold Growth Example", enrolledTier: "Gold", memberType: "Existing" }),
makeScenario({ name: "Platinum Growth Example", enrolledTier: "Platinum", memberType: "Existing" }),
]);
}
const clearAll = () => {
if (!confirm("Reset ALL scenarios to blank inputs?")) return;
setScenarios((prev) => prev.map((s) => makeScenario({ id: s.id, name: s.name })));
};
const headerStats = useMemo(() => {
const totals = scenarios.map((s) => Object.values(s.spend).reduce((sum, v) => sum + Number(v || 0), 0));
const sum = totals.reduce((a, b) => a + b, 0);
const avg = totals.length ? sum / totals.length : 0;
const min = totals.length ? Math.min(...totals) : 0;
const max = totals.length ? Math.max(...totals) : 0;
return { avg, min, max };
}, [scenarios]);
const printScenario = (scenario) => {
const m = computeScenarioMetrics(scenario, settings);
const qOrder = ["Q1", "Q2", "Q3", "Q4"];
const qRows = qOrder.map((q) => ({
q,
...m.freeGoods.byQuarter[q],
}));
const tiles = [
{ label: "Annual Spend", value: fmtMoney(m.annualTotal), sub: `Pricing: ${m.pricingFamily}` },
{
label: "Dividend Level",
value: m.pricingFamily === "Not Qualified" ? "N/A" : m.dividendLevel,
sub: m.pricingFamily === "Not Qualified" ? "Below minimum" : `Rate ${fmtPct((settings.dividendRate[m.dividendLevel] || 0) * 100)}`,
},
{ label: "Free Goods (YTD)", value: fmtMoney(m.freeGoods.totalEarned), sub: `Rate ${fmtPct(m.freeGoods.rate * 100)}` },
];
setPrintPayload({
name: scenario.name,
memberType: scenario.memberType,
enrolledTier: scenario.enrolledTier,
spend90d: Number(scenario.spend90d || 0),
baselinePY: Number(scenario.baselinePY || 0),
consecutiveYears: Number(scenario.consecutiveYears || 0),
spend: scenario.spend,
freeGoodsYTD: m.freeGoods.totalEarned,
dividendPayout: m.dividend.payout,
dividendExplain: m.dividend.eligible ? `Growth ${fmtMoney(m.dividend.growth)} @ ${fmtPct(m.dividend.rate * 100)}` : m.dividend.reason,
royaltyPayout: m.royalty.payout,
royaltyExplain: m.royalty.eligible ? `${m.royalty.milestone}-year @ ${fmtPct(m.royalty.rate * 100)}` : "Not eligible",
notes: scenario.notes,
qRows,
tiles,
});
// Delay to allow payload to render, then print
setTimeout(() => window.print(), 50);
};
return (
{/* Print Sheet */}
{/* App UI (hidden in print) */}
{/* Header */}
Accelerate 3.0 — Scenario Calculator
Team Tool
Interactive multi-scenario testing. Pricing tiers are Silver/Gold/Platinum. “+” levels only increase dividend rate.
Dividend + bands: Silver+ {fmtMoney(settings.plusBands.SilverPlus.min)}–{fmtMoney(settings.plusBands.SilverPlus.max)} • Gold+{" "}
{fmtMoney(settings.plusBands.GoldPlus.min)}–{fmtMoney(settings.plusBands.GoldPlus.max)} • Platinum+{" "}
{fmtMoney(settings.plusBands.PlatinumPlus.min)}–{fmtMoney(settings.plusBands.PlatinumPlus.max)}
{/* Overview */}
Scenarios
{scenarios.length}
Avg annual spend
{fmtMoney(headerStats.avg)}
Min / Max spend
{fmtMoney(headerStats.min)} / {fmtMoney(headerStats.max)}
{/* Compare Bar */}
Scenarios
Admin Settings
{scenarios.map((s) => (
))}
Print/Save PDF: open a scenario menu → “Print / Save PDF”. In the print dialog, choose “Save as PDF”.
{/* Print CSS */}
);
}