/* ============================================================ 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 (
{i + 1}
onChange({ name: v })} placeholder={t("own.namePh")} />
{t("own.cashIn")}
onChange({ cash: v })} />
{t("own.inKind")}
onChange({ inKind: v })} placeholder={t("own.inKindPh")} />
{t("own.inKindValue")}
onChange({ inKindValue: v })} />
{t("own.ownership")}
{own.manualShares ? onChange({ share: v })} max={100} step={5} suffix="%" /> :
{fmtPct(share, 1)}
}
); } function OwnershipModule({ plan, setOwn, goOutputs }) { const { t, lang } = useLang(); const { pro } = usePro(); const own = plan.ownership; const idea = plan.idea; const res = computeOwnership(own, plan.financials, t); const [section, setSection] = useStateO(0); if (!pro) { return (
{["q1", "q2", "q3"].map((q) => )}
); } const parties = own.parties || []; const setParties = (ps) => setOwn({ parties: ps }); const upd = (id, patch) => setParties(parties.map((p) => (p.id === id ? { ...p, ...patch } : p))); const add = () => setParties([...parties, { id: pid(), name: "", role: "", cash: 0, inKind: "", inKindValue: 0, share: 0 }]); const pickModel = (m) => { const ps = modelParties(m, idea, res.live, t); setOwn({ model: m, parties: ps, manager: ps.find((p) => p.role === "Operator" || p.role === "Owner-operator")?.id || ps[0].id, profitRule: m === "investorOperator" ? "investorsFirst" : "prorata" }); }; const SECTIONS = [t("own.secModel"), t("own.secProfit"), t("own.secPower")]; const nm = (id) => (parties.find((p) => p.id === id) || {}).name || "—"; return (
{SECTIONS.map((s, i) => ( ))}
{section === 0 && (
{t("own.startFrom")}
{t("own.startFromSub")}
{OWN_MODELS.map((m) => ( ))}
{t("own.stakeholders")}
{own.manualShares ? t("own.manualNote") : t("own.autoNote")}
{parties.map((p, i) => upd(p.id, patch)} onRemove={() => setParties(parties.filter((x) => x.id !== p.id))} />)} {parties.length > 0 && (
{t("own.cashTotal")} {fmtBaht(res.totalCash)} / {fmtBaht(res.live.investment)} {t("own.toOpen")} {own.manualShares && 0.005 ? "var(--red)" : "var(--green)", fontWeight: 700 }}>Σ {Math.round(res.shareSum * 100)}%}
)}
)} {section === 1 && (
{t("own.profitRule")}
setOwn({ profitRule: v })} options={[{ v: "prorata", l: t("own.rule.prorata") }, { v: "investorsFirst", l: t("own.rule.investorsFirst") }]} />
{t("own.ruleSub." + own.profitRule)}
setOwn({ distributePct: v / 100 })} max={100} step={5} suffix="%" />
{own.profitRule === "investorsFirst" && (
)}
)} {section === 2 && (
setOwn({ signoffLimit: v })} />
v && setOwn({ majorRule: v })} renderLabel={(v) => t("own.majorRule." + v)} />
)} {res.flags.length > 0 && (

{t("own.checkSplit")}

{res.flags.map((f, i) => )}
)}
{section < 2 ? : }
); } function PayoutTable({ res }) { const { t } = useLang(); const th = { fontSize: 10, letterSpacing: ".08em", textTransform: "uppercase", color: "var(--muted)", fontWeight: 600, padding: "0 0 8px", textAlign: "right" }; const td = { padding: "10px 0", textAlign: "right", borderTop: "1px solid var(--border)", fontVariantNumeric: "tabular-nums" }; return (

{t("own.payoutTitle")}

{t("own.payoutSub")}
{res.rows.map((r) => ( ))}
{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")) : "—"}
); } function GovernanceSummary({ own, res, nm, idea }) { const { t } = useLang(); const rows = res.rows; return (
{t("own.govKicker")}

{t("own.govTitle", { name: idea.conceptName || t("pro.thisConcept") })}

  1. {t("own.govOwners")} {rows.map((r) => `${r.name || t("own.unnamed")} ${fmtPct(r.share, 1)}`).join(" · ") || "—"}
  2. {t("own.govMoney")} {rows.map((r) => `${r.name || t("own.unnamed")} ${fmtBaht(r.cash)}${r.inKind ? ` + ${r.inKind}` : ""}`).join(" · ") || "—"}
  3. {t("own.govProfit")} {t("own.govProfitText." + own.profitRule, { pct: fmtPct(own.distributePct ?? 0.7) })}
  4. {t("own.govDaily")} {t("own.govDailyText", { name: nm(own.manager), limit: fmtBaht(own.signoffLimit) })}
  5. {t("own.govMajor")} {t("own.majorRule." + own.majorRule)}
{t("own.govDisclaimer")}
); } function OwnershipPreview() { return (
{[["Mint", "60%", "฿38,400"], ["Partner", "25%", "฿16,000"], ["Investor", "15%", "฿9,600"]].map(([n, p, v]) => (
{n}{p}{v}
))}
); } Object.assign(window, { OwnershipModule, computeOwnership, OWN_ROLES });