/* The centerpiece: interactive "What if?" dashboard. Looks like a real working tool. Driven by 4 sliders that recompute KPIs + chart live. */ const R = window.Recharts; const MONTHS = ["Янв","Фев","Мар","Апр","Май","Июн","Июл","Авг","Сен","Окт","Ноя","Дек"]; /* Base business state (rounded, plausible for ~25-50 person company) */ const BASE = { monthlyRevenue: 4_200_000, // ₽ revenueGrowthMoM: 0.012, // 1.2% mom cogsRatio: 0.42, // себестоимость fixedOpex: 1_350_000, // ФОТ + аренда + прочее marketingBase: 280_000, // маркетинг avgCheck: 84_000, seasonality: [0.92,0.95,1.02,1.05,1.08,1.10,0.96,0.93,1.04,1.07,1.04,0.98], taxRate: 0.06, // УСН 6% упрощённо }; function buildSeries({ revenueDelta, marketingDelta, hires, checkDelta }) { const data = []; let cashBalance = 1_800_000; let scnCashBalance = 1_800_000; const newOpexFromHires = hires * 110_000; // средняя стоимость найма + ФОТ в месяц for (let i = 0; i < 12; i++) { const seasonalFactor = BASE.seasonality[i]; const growth = Math.pow(1 + BASE.revenueGrowthMoM, i); // base const bRev = BASE.monthlyRevenue * growth * seasonalFactor; const bCogs = bRev * BASE.cogsRatio; const bOpex = BASE.fixedOpex + BASE.marketingBase; const bGross = bRev - bCogs; const bEbit = bGross - BASE.fixedOpex - BASE.marketingBase; const bTax = bRev * BASE.taxRate; const bNet = bEbit - bTax; cashBalance += bNet; // scenario const checkBoost = 1 + (checkDelta / 100) * 0.6; // повышение чека не масштабируется один-к-одному const sRev = bRev * (1 + revenueDelta / 100) * checkBoost; const sCogs = sRev * BASE.cogsRatio; const sMarketing = BASE.marketingBase * (1 + marketingDelta / 100); const sOpex = BASE.fixedOpex + sMarketing + newOpexFromHires; const sGross = sRev - sCogs; const sEbit = sGross - sOpex; const sTax = sRev * BASE.taxRate; const sNet = sEbit - sTax; scnCashBalance += sNet; data.push({ m: MONTHS[i], base: Math.round(bNet), scn: Math.round(sNet), baseCash: Math.round(cashBalance), scnCash: Math.round(scnCashBalance), rev: Math.round(sRev), gross: Math.round(sGross), }); } return data; } function sum(arr, key) { return arr.reduce((a, r) => a + r[key], 0); } const Slider = ({ label, value, setValue, min, max, step, suffix = "%", help }) => (
{label}
{help &&
{help}
}
{value > 0 ? "+" : ""}{value}{suffix}
setValue(parseFloat(e.target.value))} aria-label={label} />
{min}{suffix}0+{max}{suffix}
); function KPI({ label, base, scn, format = fmtK, suffix = "₽", positive = true }) { const delta = scn - base; const pct = base === 0 ? 0 : (delta / Math.abs(base)) * 100; const goodSign = positive ? delta >= 0 : delta <= 0; return (
{label}
{format(scn)} {suffix}
{delta >= 0 ? "▲" : "▼"} {pct.toFixed(1).replace('.', ',')}% vs база
); } function WhatIfDashboard() { const [revenueDelta, setRevenueDelta] = useState(20); const [marketingDelta, setMarketingDelta] = useState(15); const [hires, setHires] = useState(1); const [checkDelta, setCheckDelta] = useState(8); const [tab, setTab] = useState("cash"); // cash | pl | scenarios const base = useMemo(() => buildSeries({ revenueDelta: 0, marketingDelta: 0, hires: 0, checkDelta: 0 }), []); const scn = useMemo(() => buildSeries({ revenueDelta, marketingDelta, hires, checkDelta }), [revenueDelta, marketingDelta, hires, checkDelta]); const merged = base.map((r, i) => ({ ...r, base: r.base, baseCash: r.baseCash, scnCash: scn[i].scnCash, scn: scn[i].scn, rev: scn[i].rev, gross: scn[i].gross })); const kpis = { revBase: sum(base, "base") + sum(base, "base") * 0, // placeholder for revenue (use rev from each) }; const annualRevBase = base.reduce((a, r) => a + (r.base + (BASE.fixedOpex + BASE.marketingBase) + r.base*0), 0); // not used const annualRev = { base: base.reduce((a) => a, 0) // placeholder }; // Better KPI math: recompute totals directly const totBase = base.reduce((a, r) => ({ rev: a.rev + 0, net: a.net + r.base, cash: r.baseCash }), { rev: 0, net: 0, cash: base[base.length-1].baseCash }); const totScn = scn .reduce((a, r) => ({ rev: a.rev + r.rev, net: a.net + r.scn, cash: r.scnCash }), { rev: 0, net: 0, cash: scn[scn.length-1].scnCash }); // recompute base revenue: let baseRev = 0, baseGross = 0; for (let i = 0; i < 12; i++) { const r = BASE.monthlyRevenue * Math.pow(1 + BASE.revenueGrowthMoM, i) * BASE.seasonality[i]; baseRev += r; baseGross += r * (1 - BASE.cogsRatio); } totBase.rev = baseRev; const scnGross = scn.reduce((a, r) => a + r.gross, 0); const cashFloor = (arr, key) => Math.min(...arr.map(r => r[key])); const dipBase = cashFloor(base, "baseCash"); const dipScn = cashFloor(scn, "scnCash"); /* Scenario presets */ const presets = [ { id: "base", label: "Базовый", set: () => { setRevenueDelta(0); setMarketingDelta(0); setHires(0); setCheckDelta(0); } }, { id: "growth", label: "+20% продаж", set: () => { setRevenueDelta(20); setMarketingDelta(15); setHires(1); setCheckDelta(8); } }, { id: "stress", label: "Стресс −15%", set: () => { setRevenueDelta(-15); setMarketingDelta(-10); setHires(0); setCheckDelta(-5); } }, { id: "scale", label: "Масштаб", set: () => { setRevenueDelta(40); setMarketingDelta(30); setHires(3); setCheckDelta(12); } }, ]; const activePreset = revenueDelta===0 && marketingDelta===0 && hires===0 && checkDelta===0 ? "base" : revenueDelta===20 && marketingDelta===15 && hires===1 && checkDelta===8 ? "growth" : revenueDelta===-15 && marketingDelta===-10 && hires===0 && checkDelta===-5 ? "stress" : revenueDelta===40 && marketingDelta===30 && hires===3 && checkDelta===12 ? "scale" : "custom"; return (
{/* Top chrome — looks like a real app */}
/ модель / ремонт-сервис · 2026
{["cash","pl","scenarios"].map((t) => ( ))}
{/* KPIs */}
{/* Chart + controls */}
{/* preset chips + period */}
{presets.map(p => ( ))} {activePreset === "custom" && ( · свой сценарий )}
База Сценарий
{tab === "cash" ? ( (v/1_000_000).toFixed(1) + " млн"} axisLine={false} tickLine={false} /> fmt(v) + " ₽"} labelClassName="text-mute" contentStyle={{ background: "#fff" }}/> ) : tab === "pl" ? ( (v/1_000_000).toFixed(1) + " млн"} axisLine={false} tickLine={false} /> fmt(v) + " ₽"} /> ) : ( (v/1_000_000).toFixed(0) + " млн"} axisLine={false} tickLine={false} /> fmt(v) + " ₽"} /> )}
{/* footnote */}
Модель пересчитывает 12 месяцев на лету. Это упрощённый демо-расчёт; в реальной работе модель строится на ваших данных из 1С, банков и Excel — учитывает сезонность, кассовые разрывы и налоговый режим.
{/* Controls */}
Параметры сценария
Двигайте бегунки — KPI и графики пересчитаются мгновенно.
Δ Чистая прибыль
= 0 ? "text-emerald-700" : "text-danger")}> {totScn.net - totBase.net >= 0 ? "+" : "−"}{fmt(Math.abs(totScn.net - totBase.net))} ₽
Кассовый разрыв
= 0 ? "text-emerald-700" : "text-danger")}> {dipScn >= 0 ? "не возникает" : "− " + fmtK(Math.abs(dipScn)) + " ₽"}
); } window.WhatIfDashboard = WhatIfDashboard;