/* ============================================================ BLUEPRINTSENSES — OWNERSHIP MODULE (Pro only) Reads/writes: plan.ownership. Reads plan.financials for the money each party puts in and the profit there is to share. Stakeholder model → profit sharing → management power. ============================================================ */ const { useState: useStateO } = React; const OWN_ROLES = ["Owner-operator", "Operator", "Investor", "Partner", "Chef partner"]; const OWN_MODELS = ["sole", "equal", "investorOperator"]; const pid = () => "p" + Math.random().toString(36).slice(2, 8); function modelParties(model, idea, live, t) { const inv = Math.round((live.investment || idea.founder.capital || 1000000) / 1000) * 1000; if (model === "sole") return [{ id: pid(), name: t("own.you"), role: "Owner-operator", cash: inv, inKind: "", inKindValue: 0, share: 100 }]; if (model === "equal") return [ { id: pid(), name: t("own.partnerN", { n: 1 }), role: "Partner", cash: inv / 2, inKind: "", inKindValue: 0, share: 50 }, { id: pid(), name: t("own.partnerN", { n: 2 }), role: "Partner", cash: inv / 2, inKind: "", inKindValue: 0, share: 50 }, ]; return [ { id: pid(), name: t("own.investor"), role: "Investor", cash: Math.round(inv * 0.9 / 1000) * 1000, inKind: "", inKindValue: 0, share: 60 }, { id: pid(), name: t("own.operator"), role: "Operator", cash: Math.round(inv * 0.1 / 1000) * 1000, inKind: t("own.runsShop"), inKindValue: Math.round(inv * 0.25 / 1000) * 1000, share: 40 }, ]; } /* shares, payouts and paybacks over the 36-month projection */ function computeOwnership(own, fin, t) { const live = computeFin(fin, t); const proj = computeProjection(fin, t, "planned"); const parties = own.parties || []; const contrib = (p) => (p.cash || 0) + (p.inKindValue || 0); const totalContrib = parties.reduce((a, p) => a + contrib(p), 0); const totalCash = parties.reduce((a, p) => a + (p.cash || 0), 0); const shares = parties.map((p) => (own.manualShares ? (p.share || 0) / 100 : totalContrib > 0 ? contrib(p) / totalContrib : 0)); const shareSum = shares.reduce((a, b) => a + b, 0); const pct = own.distributePct ?? 0.7; /* month by month: investors-first repays cash before anyone shares by % */ const paid = parties.map(() => 0); const repaid = parties.map(() => 0); const paybackMonth = parties.map(() => null); const year1 = parties.map(() => 0); let repayDoneMonth = own.profitRule === "investorsFirst" ? null : 0; proj.months.forEach((mo) => { let dist = Math.max(0, mo.net) * pct; if (own.profitRule === "investorsFirst" && repayDoneMonth === null) { const owed = parties.map((p, i) => Math.max(0, (p.cash || 0) - repaid[i])); const owedTotal = owed.reduce((a, b) => a + b, 0); if (owedTotal > 0) { const use = Math.min(dist, owedTotal); parties.forEach((p, i) => { const x = owedTotal > 0 ? use * owed[i] / owedTotal : 0; repaid[i] += x; paid[i] += x; if (mo.m <= 12) year1[i] += x; }); dist -= use; } const stillOwed = parties.reduce((a, p, i) => a + Math.max(0, (p.cash || 0) - repaid[i]), 0); if (stillOwed <= 0.5) repayDoneMonth = mo.m; } parties.forEach((p, i) => { const x = dist * (shareSum > 0 ? shares[i] / shareSum : 0); paid[i] += x; if (mo.m <= 12) year1[i] += x; }); parties.forEach((p, i) => { if (paybackMonth[i] === null && (p.cash || 0) > 0 && paid[i] >= p.cash) paybackMonth[i] = mo.m; }); }); const steadyDist = Math.max(0, live.netProfit) * pct; const rows = parties.map((p, i) => ({ ...p, contrib: contrib(p), share: shares[i], /* investors-first: while cash is being repaid, payouts follow cash put in, not % */ monthly: own.profitRule === "investorsFirst" && repayDoneMonth !== 0 && totalCash > 0 ? steadyDist * (p.cash || 0) / totalCash : steadyDist * shares[i], yearly: steadyDist * shares[i] * 12, year1: year1[i], total3y: paid[i], paybackMonth: paybackMonth[i], multiple: p.cash > 0 ? paid[i] / p.cash : null, })); const flags = []; const nm = (p) => p.name || t("own.unnamed"); if (own.manualShares && Math.abs(shareSum - 1) > 0.005) flags.push({ tone: "red", text: t("own.flagSum", { pct: Math.round(shareSum * 100) }) }); rows.forEach((r) => { if (!r.role) flags.push({ tone: "amber", text: t("own.flagNoRole", { name: nm(r) }) }); if (own.manualShares && r.share > 0 && r.contrib === 0) flags.push({ tone: "amber", text: t("own.flagNoContrib", { name: nm(r), pct: fmtPct(r.share) }) }); if ((r.role === "Operator" || r.role === "Owner-operator") && r.share < 0.1 && rows.length > 1) flags.push({ tone: "amber", text: t("own.flagOperatorLow", { name: nm(r), pct: fmtPct(r.share) }) }); if (r.share > 0.5 && own.majorRule === "majority" && rows.length > 1) flags.push({ tone: "amber", text: t("own.flagMajority", { name: nm(r), pct: fmtPct(r.share) }) }); if (r.cash > 0 && !r.paybackMonth) flags.push({ tone: "amber", text: t("own.flagNoPayback", { name: nm(r) }) }); }); if (parties.length && live.investment > 0 && totalCash < live.investment) flags.push({ tone: "red", text: t("own.flagGap", { cash: fmtBaht(totalCash), inv: fmtBaht(live.investment), gap: fmtBaht(live.investment - totalCash) }) }); if (own.profitRule === "investorsFirst" && repayDoneMonth === null && parties.length) flags.push({ tone: "amber", text: t("own.flagRepaySlow") }); if (!own.manager && parties.length > 1) flags.push({ tone: "amber", text: t("own.flagNoManager") }); const complete = parties.length > 0 && rows.every((r) => r.role && r.share > 0) && !!own.profitRule && (parties.length === 1 || !!own.manager) && !(own.manualShares && Math.abs(shareSum - 1) > 0.005); return { live, proj, rows, flags, totalCash, totalContrib, shareSum, repayDoneMonth, steadyDist, complete }; } function PartyCard({ p, i, own, onChange, onRemove, share }) { const { t, lang } = useLang(); return (
| {t("own.party")} | % | {t("own.perMonth")} | {t("own.year1")} | {t("own.stakeholderPayback")} |
|---|---|---|---|---|
| {r.name || t("own.unnamed")} | {fmtPct(r.share, 1)} | {fmtBaht(r.monthly)} | {fmtBaht(r.year1)} | {r.cash > 0 ? (r.paybackMonth ? t("proj.monthN", { n: r.paybackMonth }) : t("proj.notIn36")) : "—"} |