/* ============================================================
RESTO SUITE — FINANCIAL FEASIBILITY MODULE
Reads/writes: plan.financials (its own namespace only)
Reads: plan.idea (to pre-fill seats / avg check hints)
Reuses: InvCategory, SpaceCard, INV_SUGGEST, COST_SUGGEST
(investment-details.jsx), PageSummary, FinancialSummary
(financial-summary.jsx), primitives (suite-shared.jsx)
============================================================ */
const { useState: useStateF, useMemo: useMemoF } = React;
function computeFin(state, t) {
const invVals = {};
for (const k in state.investment) {
const its = (state.invItems || {})[k] || [];
invVals[k] = its.length ? its.reduce((a, i) => a + (i.qty || 0) * (i.price || 0), 0) : state.investment[k];
}
const investment = Object.values(invVals).reduce((a, b) => a + b, 0);
let sales;
if (state.revMode === "detailed") {
const d = state.detailed;
sales = d.seats * d.turns * d.avgCheck * d.openDays;
} else sales = state.monthlySales;
const foodPct = state.foodPct;
const foodCost = sales * foodPct;
const costVal = (k) => {
const its = (state.costItems || {})[k] || [];
return its.length ? its.reduce((a, i) => a + (i.qty || 0) * (i.price || 0), 0) : state[k];
};
const costs = {
staff: costVal("staff"), rent: costVal("rent"), utilities: costVal("utilities"),
marketing: costVal("marketing"), management: costVal("management") || 0, other: costVal("other"),
taxes: costVal("taxes") || 0,
};
const fixed = costs.staff + costs.rent + costs.utilities + costs.marketing + costs.management + costs.other;
const taxTotal = costs.taxes || 0;
const totalCost = foodCost + fixed + taxTotal;
const profit = sales - (foodCost + fixed); // pre-tax operating profit (EBITDA)
const margin = sales > 0 ? profit / sales : 0;
const cmRatio = 1 - foodPct;
const breakEven = cmRatio > 0 ? fixed / cmRatio : Infinity;
const openDays = state.revMode === "detailed" ? state.detailed.openDays : 26;
const breakEvenDaily = breakEven / openDays;
const payback = profit > 0 ? investment / profit : Infinity;
/* ---------- taxes: now a single manual line inside Operating Cost ---------- */
const tax = { ...((FIN_DEFAULTS && FIN_DEFAULTS.tax) || {}), ...(state.tax || {}) };
const annualGross = sales * 12;
const netSales = sales, outputVat = 0, inputVat = 0, netVat = 0, vatApplies = false;
const ssoHeads = 0, sso = 0, cit = 0;
const profitBeforeTax = profit;
const annualPBT = profit * 12;
const netProfit = sales - totalCost; // take-home after the taxes line
const netMargin = sales > 0 ? netProfit / sales : 0;
const paybackAfterTax = netProfit > 0 ? investment / netProfit : Infinity;
const share = (v) => (sales > 0 ? v / sales : 0);
const segs = [
{ key: "food", label: t("fin.segFood"), value: foodCost, pct: share(foodCost), color: "var(--c-food)" },
{ key: "staff", label: t("fin.segStaff"), value: costs.staff, pct: share(costs.staff), color: "var(--c-staff)" },
{ key: "rent", label: t("fin.segRent"), value: costs.rent, pct: share(costs.rent), color: "var(--c-rent)" },
{ key: "utilities", label: t("fin.segUtil"), value: costs.utilities, pct: share(costs.utilities), color: "var(--c-util)" },
{ key: "marketing", label: t("fin.segMkt"), value: costs.marketing, pct: share(costs.marketing), color: "var(--c-mkt)" },
{ key: "management", label: t("fin.segMgmt"), value: costs.management, pct: share(costs.management), color: "var(--c-mgmt)" },
{ key: "other", label: t("fin.segOther"), value: costs.other, pct: share(costs.other), color: "var(--c-other)" },
...(taxTotal > 0 ? [{ key: "tax", label: t("fin.segTax"), value: taxTotal, pct: share(taxTotal), color: "var(--c-tax)" }] : []),
];
let verdict;
if (sales <= 0) verdict = { key: "none", label: t("fin.verdictNone"), tone: "muted" };
else if (netProfit <= 0) verdict = { key: "loss", label: t("fin.verdictLoss"), tone: "red" };
else if (netMargin < 0.05) verdict = { key: "risky", label: t("fin.verdictRisky"), tone: "amber" };
else if (netMargin < 0.12) verdict = { key: "ok", label: t("fin.verdictOk"), tone: "amber" };
else if (netMargin < 0.20) verdict = { key: "healthy", label: t("fin.verdictHealthy"), tone: "green" };
else verdict = { key: "strong", label: t("fin.verdictStrong"), tone: "green" };
const staffPct = share(costs.staff), rentPct = share(costs.rent), primePct = foodPct + staffPct;
const flags = [];
if (sales > 0) {
if (foodPct > 0.38) flags.push({ tone: "amber", text: t("idea.flagFoodHigh", { pct: fmtPct(foodPct) }) });
else if (foodPct < 0.20) flags.push({ tone: "amber", text: t("idea.flagFoodLow", { pct: fmtPct(foodPct) }) });
else flags.push({ tone: "green", text: t("idea.flagFoodHealthy", { pct: fmtPct(foodPct) }) });
if (staffPct > 0.40) flags.push({ tone: "amber", text: t("idea.flagStaffHigh", { pct: fmtPct(staffPct) }) });
if (primePct > 0.65) flags.push({ tone: "red", text: t("idea.flagPrimeHigh", { pct: fmtPct(primePct) }) });
if (rentPct > 0.15) flags.push({ tone: "amber", text: t("idea.flagRentHigh", { pct: fmtPct(rentPct) }) });
if (profit > 0 && margin >= 0.12) flags.push({ tone: "green", text: t("idea.flagMarginGood", { pct: fmtPct(margin) }) });
if (profit <= 0) flags.push({ tone: "red", text: t("idea.flagLoss") });
if (isFinite(payback) && payback > 48) flags.push({ tone: "amber", text: t("idea.flagPaybackSlow", { months: fmtMonths(payback) }) });
if (taxTotal === 0 && profit > 0) flags.push({ tone: "amber", text: t("idea.flagNoTax") });
}
const capex = invVals.renovation + invVals.equipment + invVals.furniture + invVals.licenses + (invVals.management || 0);
const shopSqm = (state.space || {}).shopSqm || 0;
const kitchenSqm = (state.space || {}).kitchenSqm || 0;
const capexPerSqm = shopSqm > 0 ? capex / shopSqm : NaN;
const kitchenPerSqm = kitchenSqm > 0 ? invVals.equipment / kitchenSqm : NaN;
const reserveMonths = fixed > 0 ? invVals.reserve / fixed : NaN;
const detailedMode = state.revMode === "detailed";
const custDay = detailedMode ? state.detailed.seats * state.detailed.turns : NaN;
const avgCheck = detailedMode ? state.detailed.avgCheck : NaN;
return {
investment, invVals, costs, sales, foodCost, fixed, totalCost, profit, margin,
breakEven, breakEvenDaily, payback, segs, verdict, flags,
staffPct, rentPct, primePct, foodPct,
salesDaily: sales / openDays,
capex, capexPerSqm, kitchenPerSqm, reserveMonths, detailedMode, custDay, avgCheck,
tax, annualGross, vatApplies, netSales, outputVat, inputVat, netVat,
ssoHeads, sso, profitBeforeTax, annualPBT, cit, taxTotal, netProfit, netMargin, paybackAfterTax,
};
}
/* ---------- steps ---------- */
const INV_FIELDS = [
["renovation", "invRenoLabel", "invRenoHint"],
["equipment", "invEquipLabel", "invEquipHint"],
["furniture", "invFurnLabel", "invFurnHint"],
["deposit", "invDepositLabel", "invDepositHint"],
["licenses", "invLicLabel", "invLicHint"],
["management", "invMgmtLabel", "invMgmtHint"],
["inventory", "invInvLabel", "invInvHint"],
["reserve", "invReserveLabel", "invReserveHint"],
];
const COST_FIELDS = [
["staff", "costStaffLabel", "costStaffHint"],
["rent", "costRentLabel", "costRentHint"],
["utilities", "costUtilLabel", "costUtilHint"],
["marketing", "costMktLabel", "costMktHint"],
["management", "costMgmtLabel", "costMgmtHint"],
["other", "costOtherLabel", "costOtherHint"],
["taxes", "costTaxLabel", "costTaxHint"],
];
function FinStepInvestment({ state, set, live, summary }) {
const { t, lang } = useLang();
return (