/* ============================================================ 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 (
{}} preview />
); } 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) =>
)}
{res.rows.map((r, i) => ( ))}
{t("own.party")}{t("out.putIn")}% {t("own.perMonth")}{t("own.year1")}{t("own.stakeholderPayback")}{t("out.return3y")}
{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 (
{label}
{value}
); } 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) => (
{t(k)}
{t(sub)}
))}
); } Object.assign(window, { OutputsModule, PitchDeck, buildWorkbook });