/* ============================================================
BLUEPRINTSENSES — OUTPUTS (Pro)
Ownership dashboard · Pitching deck · Plan summary · Export.
Everything reads the live plan; deck headlines are editable and
saved in plan.outputs.deck. Nothing here changes the numbers.
============================================================ */
const { useState: useStateOut } = React;
function OutputsModule({ plan, setOutputs, setActive, openIE }) {
const { t } = useLang();
const { pro } = usePro();
const [tab, setTab] = useStateOut("deck");
const TABS = [["deck", t("out.tabDeck")], ["ownership", t("out.tabOwnership")], ["summary", t("out.tabSummary")], ["export", t("out.tabExport")]];
if (!pro) {
return (
);
}
return (
{TABS.map(([k, l]) => (
))}
{tab === "deck" && }
{tab === "ownership" && }
{tab === "summary" && }
{tab === "export" && setTab("summary")} goDeck={() => setTab("deck")} />}
);
}
function LastUpdated({ plan }) {
const { t, lang } = useLang();
const d = plan.meta.updatedAt ? new Date(plan.meta.updatedAt) : null;
if (!d) return null;
return (
{t("out.lastUpdated", { d: d.toLocaleDateString(lang === "th" ? "th-TH" : "en-GB", { day: "numeric", month: "short", year: "numeric" }) })}
);
}
/* ---------- ownership dashboard ---------- */
function OwnershipDashboard({ plan, setActive }) {
const { t, lang } = useLang();
const res = computeOwnership(plan.ownership, plan.financials, t);
if (!res.rows.length) {
return (
{t("out.noOwners")}
{t("out.noOwnersSub")}
);
}
const colors = ["var(--c-food)", "var(--c-rent)", "var(--brass)", "var(--c-mkt)", "var(--green)", "var(--c-util)"];
const th = { fontSize: 10, letterSpacing: ".08em", textTransform: "uppercase", color: "var(--muted)", fontWeight: 600, padding: "0 0 8px", textAlign: "right" };
const td = { padding: "11px 0", textAlign: "right", borderTop: "1px solid var(--border)", fontVariantNumeric: "tabular-nums" };
return (
0 ? "green" : "red"} />
{t("out.whoOwns")}
{res.rows.map((r, i) =>
)}
| {t("own.party")} | {t("out.putIn")} | % |
{t("own.perMonth")} | {t("own.year1")} | {t("own.stakeholderPayback")} | {t("out.return3y")} |
{res.rows.map((r, i) => (
| {r.name || t("own.unnamed")} {optLabel(lang, r.role)} |
{fmtBaht(r.cash)}{r.inKindValue > 0 && + {fmtBaht(r.inKindValue)} {t("out.inKindShort")} } |
{fmtPct(r.share, 1)} |
{fmtBaht(r.monthly)} |
{fmtBaht(r.year1)} |
{r.cash > 0 ? (r.paybackMonth ? t("proj.monthN", { n: r.paybackMonth }) : t("proj.notIn36")) : "—"} |
= 1 ? "var(--green)" : "var(--ink)" }}>{r.multiple !== null ? r.multiple.toFixed(2) + "×" : "—"} |
))}
{t("out.ownFootnote")}
);
}
/* ---------- pitching deck ---------- */
function Slide({ n, kicker, title, onTitle, children, preview }) {
return (
{kicker}
{onTitle && !preview
?
onTitle(e.target.value)} className="serif" aria-label="Slide title"
style={{ fontSize: "clamp(18px, 3vw, 32px)", border: "none", outline: "none", background: "transparent", padding: 0, marginTop: 6, color: "var(--ink)", fontFamily: "Newsreader, serif", width: "100%" }} />
:
{title}
}
{children}
{n}
);
}
function DeckFigure({ label, value, tone }) {
return (
);
}
function PitchDeck({ plan, setOutputs, preview }) {
const { t, lang } = useLang();
const ol = (v) => optLabel(lang, v);
const idea = plan.idea, fin = plan.financials;
const live = computeFin(fin, t);
const proj = computeProjection(fin, t, "planned");
const own = computeOwnership(plan.ownership, fin, t);
const deck = (plan.outputs && plan.outputs.deck) || {};
const title = (k, def) => (deck[k] !== undefined ? deck[k] : def);
const setTitle = (k) => (v) => setOutputs({ deck: { ...deck, [k]: v } });
const name = idea.conceptName || t("pitch.untitledConcept");
const per = (idea.pro || {}).persona || {};
const neg = (n) => `${n < 0 ? "−" : ""}${fmtBaht(Math.abs(n))}`;
const txt = { fontSize: "clamp(11px, 1.55vw, 16px)", lineHeight: 1.5, margin: "0 0 1.2%" };
const grid = { display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: "2.5%" };
const flags = computeIdea(idea, t, lang).flags.concat(live.flags).filter((f) => f.tone !== "green").slice(0, 3);
const slides = [
{idea.tagline &&
“{idea.tagline}”
}
{t("deck.coverSub")}
,
{t("deck.conceptLine", { who: idea.targetCustomer.map(ol).join(", ").toLowerCase() || "—", loc: (ol(idea.locationType) || "—").toLowerCase(), spend: fmtBaht(ideaAvgSpend(idea)) })}
{t("deck.formatLine", { seats: idea.format.seats || "—", sqm: idea.format.sqm || "—", hours: fmtHours(idea.hours) || "—", svc: (ol(idea.format.serviceStyle) || "—").toLowerCase() })}
{idea.usp && {idea.usp}
}
,
{per.name ? (
{per.name}{per.age ? `, ${per.age}` : ""}{per.occupation ? ` · ${per.occupation}` : ""}
{per.story &&
{per.story}
}
{per.wants &&
{t("concept.wants")}: {per.wants}
}
) : {idea.targetCustomer.map(ol).join(", ") || "—"}
}
{(idea.pro || {}).valuePromise && {t("concept.pro.valuePromise")}: {idea.pro.valuePromise}
}
,
= 0 ? "green" : "red"} />
,
,
{own.rows.length ? own.rows.map((r) => (
{r.name || t("own.unnamed")}{fmtBaht(r.cash)}{fmtPct(r.share, 1)}
{r.cash > 0 && r.paybackMonth ? t("deck.backIn", { n: r.paybackMonth }) : ""}
)) : {t("deck.noOwners")}
}
,
{flags.length ? flags.map((f, i) => {i + 1}. {f.text}
) : {t("pitch.noFlags")}
}
,
];
if (preview) return {slides.slice(0, 2)}
;
return (
{t("out.deckNote")}
{Object.keys(deck).length > 0 && }
{slides}
);
}
/* ---------- export ---------- */
function xmlEsc(s) { return String(s).replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); }
function sheet(name, rows) {
const cell = (v) => typeof v === "number" && isFinite(v)
? `${Math.round(v * 100) / 100} | `
: `${xmlEsc(v === null || v === undefined ? "" : v)} | `;
return `${rows.map((r) => `${r.map(cell).join("")}
`).join("")}
`;
}
function buildWorkbook(plan, t, lang) {
const idea = plan.idea, fin = plan.financials;
const live = computeFin(fin, t);
const proj = computeProjection(fin, t, fin.projScenario || "planned");
const own = computeOwnership(plan.ownership, fin, t);
const ol = (v) => optLabel(lang, v);
const summary = [
[t("xl.concept"), idea.conceptName], [t("concept.slogan"), idea.tagline], [t("concept.restaurantType"), ol(idea.category)], [t("concept.cuisineType"), ol(idea.cuisine)],
[t("concept.areaType"), ol(idea.locationType)], [t("idea.seats"), idea.format.seats], ["m²", idea.format.sqm], [t("concept.openingHours"), fmtHours(idea.hours)],
[t("concept.avgSpend"), ideaAvgSpend(idea)], [], [t("pitch.totalToOpen"), live.investment], [t("fin2.salesMo"), live.sales],
[t("fin.ebitdaMo"), live.profit], [t("pitch.profitMoAfterTax"), live.netProfit], [t("fin.netMargin"), live.netMargin], [t("fin.breakEvenMo"), live.breakEven],
[t("fin2.invPayback") + " (mo)", isFinite(live.paybackAfterTax) ? live.paybackAfterTax : ""], [t("fin2.cashPayback") + " (mo)", proj.moneyBackMonth || ""],
[t("proj.lowestCash"), proj.lowestCash], [t("fin.theVerdict"), live.verdict.label],
];
const inv = [[t("xl.category"), t("xl.amount")], ...INV_FIELDS.map(([k, lk]) => [t("fin." + lk), live.invVals[k]]), [t("fin.totalToOpen"), live.investment]];
(Object.keys(fin.invItems || {})).forEach((k) => (fin.invItems[k] || []).forEach((it) => inv.push([" · " + it.name, (it.qty || 0) * (it.price || 0)])));
const cost = [[t("xl.category"), t("xl.amount")], [t("fin.foodDrink"), live.foodCost], ...COST_FIELDS.map(([k, lk]) => [t("fin." + lk), live.costs[k] || 0]), [t("fin.totalMonthlyCost"), live.totalCost]];
const pj = [[t("xl.month"), t("xl.calMonth"), t("fin2.salesMo"), t("fin2.netMo"), t("proj.legendCash"), t("proj.legendCum")],
...proj.months.map((m) => [m.m, MONTHS_EN[m.cal], m.sales, m.net, m.cash, m.cum])];
const ow = [[t("own.party"), t("xl.role"), t("own.cashIn"), t("own.inKindValue"), "%", t("own.perMonth"), t("own.year1"), t("own.stakeholderPayback") + " (mo)"],
...own.rows.map((r) => [r.name, ol(r.role), r.cash, r.inKindValue, r.share, r.monthly, r.year1, r.paybackMonth || ""])];
return `\n\n`
+ sheet(t("xl.sSummary"), summary) + sheet(t("xl.sInvestment"), inv) + sheet(t("xl.sCost"), cost) + sheet(t("xl.sProjection"), pj) + sheet(t("xl.sOwnership"), ow)
+ ``;
}
function ExportPanel({ plan, openIE, goSummary, goDeck }) {
const { t, lang } = useLang();
const fileBase = (plan.idea.conceptName || "blueprintsenses-plan").replace(/[^\p{L}\p{N}]+/gu, "-").toLowerCase();
const excel = () => {
const blob = new Blob([buildWorkbook(plan, t, lang)], { type: "application/vnd.ms-excel" });
const a = document.createElement("a");
a.href = URL.createObjectURL(blob); a.download = fileBase + ".xls"; a.click();
setTimeout(() => URL.revokeObjectURL(a.href), 1000);
};
const pdf = (go) => { go(); setTimeout(() => window.print(), 250); };
const rows = [
["out.pdfPlan", "out.pdfPlanSub", () => pdf(goSummary)],
["out.pdfDeck", "out.pdfDeckSub", () => pdf(goDeck)],
["out.excel", "out.excelSub", excel],
["out.json", "out.jsonSub", openIE],
];
return (
{rows.map(([k, sub, fn], i) => (
))}
);
}
Object.assign(window, { OutputsModule, PitchDeck, buildWorkbook });