Remodel/packages/web/components/WorkbookView.tsx
Mannu e6dc39aa33 [S1-T12/T13] P&L revenue breakdown + collapsible rows + UI polish
- Engine: Add ppa_revenue_cr, mcp_revenue_cr, tariff, units to PnLRow
- Engine: Split PPA vs MCP revenue in P&L computation
- Web: Collapsible rows for PPA/MCP Revenue and Opex
- Web: Highlighted rows (Total Revenue, EBITDA, EBIT, PBT, PAT)
- Web: Units above Tariff in breakdown, bg-blue-50 highlight
- Fix sticky column z-index for horizontal scroll
- CLAUDE.md: Add project documentation

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 10:42:36 +05:30

675 lines
28 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { useState } from "react";
import { useQuery } from "@tanstack/react-query";
import {
BarChart,
Bar,
XAxis,
YAxis,
Tooltip,
ResponsiveContainer,
LineChart,
Line,
CartesianGrid,
Legend,
} from "recharts";
import {
getStatements,
type KpiSummary,
type PnLRow,
type CfsRow,
type BsRow,
type DebtYearRow,
type GenerationRow,
type IdcPhasing,
} from "@/lib/api";
import { KpiCard } from "@/components/KpiCard";
// Horizontal table
// ---------------------------------------------------------------------------
interface TableRow {
label: string;
values: (number | null)[];
isBold?: boolean;
isSeparator?: boolean;
isHeader?: boolean;
format?: (v: number | null) => string;
indent?: boolean;
collapsible?: boolean;
isHighlight?: boolean;
children?: TableRow[];
}
function n1(v: number | null) {
return v == null ? "—" : v.toFixed(1);
}
function n2(v: number | null) {
return v == null ? "—" : v.toFixed(2);
}
function pct1(v: number | null) {
return v == null ? "—" : `${(v * 100).toFixed(1)}%`;
}
function HorizontalTable({
years,
rows,
unit = "INR Cr",
yearPrefix = "Y",
}: {
years: number[];
rows: TableRow[];
unit?: string;
yearPrefix?: string;
}) {
const [expandedRows, setExpandedRows] = useState<Set<number>>(new Set());
const toggleRow = (idx: number) => {
setExpandedRows((prev) => {
const next = new Set(prev);
if (next.has(idx)) {
next.delete(idx);
} else {
next.add(idx);
}
return next;
});
};
let rowIndex = 0;
return (
<div className="overflow-x-auto rounded-lg border border-border">
<table className="text-xs border-collapse whitespace-nowrap">
<thead>
<tr className="bg-muted/70">
<th className="sticky left-0 z-20 bg-muted/70 px-3 py-2 text-left font-semibold border-r border-b min-w-[220px] text-muted-foreground">
Metric ({unit})
</th>
{years.map((y) => (
<th
key={y}
className="px-3 py-2 text-right font-semibold border-b min-w-[68px] text-muted-foreground"
>
{yearPrefix}{y}
</th>
))}
</tr>
</thead>
<tbody>
{rows.map((row, i) => {
rowIndex = i;
const isExpanded = expandedRows.has(i);
if (row.isSeparator) {
return (
<tr key={i}>
<td colSpan={years.length + 1} className="bg-border/40 h-px p-0" />
</tr>
);
}
if (row.isHeader) {
return (
<tr key={i} className="bg-primary/5">
<td
colSpan={years.length + 1}
className="sticky left-0 z-10 px-3 py-1 text-[10px] font-semibold uppercase tracking-wider text-primary/70 bg-primary/5"
>
{row.label}
</td>
</tr>
);
}
return (
<>
<tr
key={i}
className={`border-t border-border/50 transition-colors hover:bg-accent/40 ${
row.isBold ? "font-semibold bg-muted/20" : ""
}`}
>
<td
className={`sticky left-0 z-10 border-r border-border/50 px-3 py-1.5 ${
row.isHighlight ? "bg-blue-50" : "bg-background"
} ${
row.indent ? "pl-6 text-muted-foreground" : ""
} ${row.collapsible ? "cursor-pointer hover:text-primary" : ""}`}
onClick={() => row.collapsible && toggleRow(i)}
>
{row.collapsible && (
<span className="mr-0.5 text-[6px]">{isExpanded ? "▼" : "▶"}</span>
)}
{row.label}
</td>
{row.values.map((v, j) => (
<td key={j} className={`px-3 py-1.5 text-right tabular-nums ${row.isHighlight ? "bg-blue-50" : ""}`}>
{row.format ? row.format(v) : n1(v)}
</td>
))}
</tr>
{row.collapsible && row.children && isExpanded && row.children.map((child, ci) => (
<tr key={`${i}-${ci}`} className="border-t border-border/30 bg-muted/30">
<td className="sticky left-0 z-10 bg-muted/30 border-r border-border/30 px-3 pl-8 py-1.5 text-muted-foreground">
{child.label}
</td>
{child.values.map((v, j) => (
<td key={j} className="px-3 py-1.5 text-right tabular-nums text-muted-foreground">
{child.format ? child.format(v) : n1(v)}
</td>
))}
</tr>
))}
</>
);
})}
</tbody>
</table>
</div>
);
}
// ---------------------------------------------------------------------------
// Sheet content builders
// ---------------------------------------------------------------------------
function buildPnLRows(pnl: PnLRow[]): TableRow[] {
const ppaChildren: TableRow[] = [
{ label: "Units (MWh)", values: pnl.map((r) => r.ppa_units_mwh), format: (v) => v == null ? "—" : Math.round(v).toLocaleString() },
{ label: "Tariff (₹/kWh)", values: pnl.map((r) => r.ppa_tariff_inr_per_kwh) },
];
const mcpChildren: TableRow[] = [
{ label: "MCP Units (MWh)", values: pnl.map((r) => r.mcp_units_mwh), format: (v) => v == null ? "—" : Math.round(v).toLocaleString() },
];
const opexChildren: TableRow[] = [
{ label: "O&M Opex", values: pnl.map((r) => r.om_cr) },
{ label: "Insurance", values: pnl.map((r) => r.insurance_cr) },
{ label: "Land Lease", values: pnl.map((r) => r.land_lease_cr) },
{ label: "AM Fee", values: pnl.map((r) => r.am_fee_cr) },
{ label: "Misc Opex", values: pnl.map((r) => r.misc_opex_cr) },
];
return [
{
label: "PPA Revenue",
values: pnl.map((r) => r.ppa_revenue_cr),
isBold: true,
collapsible: true,
children: ppaChildren
},
{ isSeparator: true, label: "", values: [] },
{
label: "MCP Revenue",
values: pnl.map((r) => r.mcp_revenue_cr),
collapsible: true,
children: mcpChildren
},
{ isSeparator: true, label: "", values: [] },
{ label: "Total Revenue", values: pnl.map((r) => r.revenue_cr), isBold: true, isHighlight: true },
{ isSeparator: true, label: "", values: [] },
{
label: "Operating Expenditure",
values: pnl.map((r) => r.opex_total_cr),
collapsible: true,
children: opexChildren
},
{ isSeparator: true, label: "", values: [] },
{ label: "EBITDA", values: pnl.map((r) => r.ebitda_cr), isBold: true, isHighlight: true },
{ label: "Book Depreciation", values: pnl.map((r) => r.depreciation_book_cr), indent: true },
{ label: "EBIT", values: pnl.map((r) => r.ebit_cr), isBold: true, isHighlight: true, format: n2 },
{ label: "Interest", values: pnl.map((r) => r.interest_cr), indent: true },
{ label: "PBT", values: pnl.map((r) => r.pbt_cr), isBold: true, isHighlight: true, format: n2 },
{ label: "Tax", values: pnl.map((r) => r.tax_cr), indent: true },
{ label: "PAT", values: pnl.map((r) => r.pat_cr), isBold: true, isHighlight: true, format: n2 },
];
}
function buildCfsRows(cfs: CfsRow[], kpis: KpiSummary, pnl: PnLRow[]): TableRow[] {
// CFADS = CFO + Interest (add back since CFO is post-interest in this model)
const cfads = cfs.map((r, i) => r.cfo_cr + (pnl[i]?.interest_cr ?? 0));
const equityCf = cfs.map((r, i) => cfads[i] - r.debt_repayment_cr - (pnl[i]?.interest_cr ?? 0));
return [
{ isHeader: true, label: "Operating Cash Flow", values: [] },
{ label: "PAT", values: cfs.map((r) => r.pat_cr), indent: true },
{ label: "Add: Depreciation", values: cfs.map((r) => r.depreciation_cr), indent: true },
{ label: "Δ Working Capital", values: cfs.map((r) => r.delta_working_capital_cr), indent: true },
{ label: "CFO", values: cfs.map((r) => r.cfo_cr), isBold: true },
{ isSeparator: true, label: "", values: [] },
{ isHeader: true, label: "Investing Cash Flow", values: [] },
{ label: "Capex", values: cfs.map((r) => r.capex_cr), indent: true },
{ label: "CFI", values: cfs.map((r) => r.cfi_cr), isBold: true },
{ isSeparator: true, label: "", values: [] },
{ isHeader: true, label: "Financing Cash Flow", values: [] },
{ label: "Debt Drawdown", values: cfs.map((r) => r.debt_drawdown_cr), indent: true },
{ label: "Debt Repayment", values: cfs.map((r) => r.debt_repayment_cr), indent: true },
{ label: "Equity Injection", values: cfs.map((r) => r.equity_injection_cr), indent: true },
{ label: "CFF", values: cfs.map((r) => r.cff_cr), isBold: true },
{ isSeparator: true, label: "", values: [] },
{ label: "Net Cash Flow", values: cfs.map((r) => r.net_cash_flow_cr), isBold: true },
{ label: "Opening Cash", values: cfs.map((r) => r.opening_cash_cr), indent: true },
{ label: "Closing Cash", values: cfs.map((r) => r.closing_cash_cr), isBold: true },
{ isSeparator: true, label: "", values: [] },
{ isHeader: true, label: "Returns Analysis", values: [] },
{ label: "CFADS (pre-debt service)", values: cfads, isBold: true },
{ label: "Equity Free Cash Flow", values: equityCf },
{
label: `Project IRR: ${kpis.project_irr != null ? (kpis.project_irr * 100).toFixed(1) + "%" : "—"} | Equity IRR: ${kpis.equity_irr != null ? (kpis.equity_irr * 100).toFixed(1) + "%" : "—"}`,
values: [],
isBold: true,
},
];
}
function buildBsRows(bs: BsRow[]): TableRow[] {
return [
{ isHeader: true, label: "Assets", values: [] },
{ label: "Gross Block", values: bs.map((r) => r.gross_block_cr), indent: true },
{ label: "Less: Accum Depr", values: bs.map((r) => r.accumulated_depr_cr), indent: true },
{ label: "Net Block", values: bs.map((r) => r.net_block_cr), isBold: true },
{ isSeparator: true, label: "", values: [] },
{ label: "Cash & Bank", values: bs.map((r) => r.cash_cr), indent: true },
{ label: "Receivables", values: bs.map((r) => r.receivables_cr), indent: true },
{ label: "Total Assets", values: bs.map((r) => r.total_assets_cr), isBold: true },
{ isSeparator: true, label: "", values: [] },
{ isHeader: true, label: "Liabilities & Equity", values: [] },
{ label: "Equity Share Capital", values: bs.map((r) => r.equity_cr), indent: true },
{ label: "Reserves & Surplus", values: bs.map((r) => r.reserves_cr), indent: true },
{ label: "Long-term Debt", values: bs.map((r) => r.long_term_debt_cr), indent: true },
{ label: "Payables", values: bs.map((r) => r.payables_cr), indent: true },
{ label: "Total Liabilities", values: bs.map((r) => r.total_liabilities_cr), isBold: true },
];
}
function buildDebtRows(debt: DebtYearRow[]): TableRow[] {
return [
{ label: "Opening Balance", values: debt.map((r) => r.opening_balance_cr) },
{ label: "Interest", values: debt.map((r) => r.interest_cr) },
{ label: "Principal Repayment", values: debt.map((r) => r.principal_cr) },
{ label: "Total Debt Service", values: debt.map((r) => r.total_debt_service_cr), isBold: true },
{ label: "Closing Balance", values: debt.map((r) => r.closing_balance_cr), isBold: true },
{ isSeparator: true, label: "", values: [] },
{ label: "DSCR", values: debt.map((r) => r.dscr), isBold: true, format: n2 },
];
}
// ---------------------------------------------------------------------------
// Sheet views
// ---------------------------------------------------------------------------
function SummarySheet({ kpis, scenarioId }: { kpis: KpiSummary; scenarioId: string }) {
const { data: stmts } = useQuery({
queryKey: ["statements", scenarioId],
queryFn: () => getStatements(scenarioId),
});
// Custom KPIs - stored in state, persist to localStorage
const [customKpis, setCustomKpis] = useState<{ label: string; value: string; unit: string }[]>(() => {
try {
const stored = localStorage.getItem(`customKpis-${scenarioId}`);
if (!stored) return [];
const parsed = JSON.parse(stored);
if (!Array.isArray(parsed)) return [];
// Filter out invalid entries
return parsed.filter(
(k) => k && typeof k.label === "string" && typeof k.value === "string" && k.value !== "0.0" && k.value !== "null"
);
} catch {
return [];
}
});
function addCustomKpi() {
const label = prompt("Enter KPI label (e.g. O&M Cost)");
if (!label) return;
const value = prompt(`Enter value for ${label} (without unit)`);
if (!value) return;
const unit = prompt("Enter unit (e.g. Cr, %, yrs)") || "";
const newKpis = [...customKpis, { label, value, unit }];
setCustomKpis(newKpis);
try {
localStorage.setItem(`customKpis-${scenarioId}`, JSON.stringify(newKpis));
} catch {
// localStorage unavailable
}
}
function removeCustomKpi(index: number) {
const newKpis = customKpis.filter((_, i) => i !== index);
setCustomKpis(newKpis);
try {
localStorage.setItem(`customKpis-${scenarioId}`, JSON.stringify(newKpis));
} catch {
// localStorage unavailable
}
}
const pnlChart =
stmts?.pnl.map((r) => ({
year: r.year,
Revenue: r.revenue_cr,
EBITDA: r.ebitda_cr,
PAT: r.pat_cr,
})) ?? [];
const cashChart =
stmts?.cfs.map((r) => ({ year: r.year, "Closing Cash": r.closing_cash_cr })) ?? [];
return (
<div className="space-y-6">
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-3">
<KpiCard
label="Solved Tariff"
value={kpis.solved_tariff_inr_per_kwh != null ? kpis.solved_tariff_inr_per_kwh.toFixed(2) : null}
unit="₹/kWh"
highlight
/>
<KpiCard
label="Equity IRR"
value={kpis.equity_irr != null ? `${(kpis.equity_irr * 100).toFixed(1)}%` : null}
highlight
/>
<KpiCard label="Project IRR" value={kpis.project_irr != null ? `${(kpis.project_irr * 100).toFixed(1)}%` : null} />
<KpiCard label="Min DSCR" value={kpis.min_dscr?.toFixed(2) ?? null} />
<KpiCard label="Avg DSCR" value={kpis.avg_dscr?.toFixed(2) ?? null} />
<KpiCard label="Total Capex" value={kpis.total_capex_cr != null ? kpis.total_capex_cr.toFixed(1) : null} unit="Cr" />
<KpiCard label="Debt" value={kpis.debt_cr?.toFixed(1) ?? null} unit="Cr" />
<KpiCard label="IDC" value={kpis.idc_cr?.toFixed(1) ?? null} unit="Cr" />
<KpiCard label="LCOE" value={kpis.lcoe_inr_per_kwh?.toFixed(2) ?? null} unit="₹/kWh" />
<KpiCard label="Payback" value={kpis.payback_years?.toFixed(1) ?? null} unit="yrs" />
{kpis.solar_y1_cuf != null && (
<KpiCard label="Solar Y1 CUF" value={`${(kpis.solar_y1_cuf * 100).toFixed(1)}%`} />
)}
{kpis.wind_y1_plf != null && (
<KpiCard label="Wind Y1 PLF" value={`${(kpis.wind_y1_plf * 100).toFixed(1)}%`} />
)}
{kpis.rtc_cuf_achieved != null && (
<KpiCard label="RTC CUF" value={`${(kpis.rtc_cuf_achieved * 100).toFixed(1)}%`} highlight />
)}
{/* Add custom KPI button */}
<button
onClick={addCustomKpi}
className="border border-dashed border-primary/30 rounded-lg p-4 flex items-center justify-center text-sm text-primary hover:bg-primary/5 transition-colors"
>
+ Add KPI
</button>
{/* Custom KPIs */}
{customKpis.map((kpi: { label: string; value: string; unit: string }, i: number) => (
<div key={i} className="relative">
<KpiCard label={kpi.label} value={kpi.value} unit={kpi.unit} />
<button
onClick={() => removeCustomKpi(i)}
className="absolute top-1 right-2 text-muted-foreground/40 hover:text-destructive text-xs"
>
×
</button>
</div>
))}
</div>
{stmts && (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
<div className="border rounded-lg p-4">
<p className="text-sm font-medium mb-3 text-muted-foreground">P&L Overview (Cr)</p>
<ResponsiveContainer width="100%" height={200}>
<BarChart data={pnlChart} barCategoryGap="30%">
<XAxis dataKey="year" tick={{ fontSize: 10 }} />
<YAxis tick={{ fontSize: 10 }} />
<Tooltip />
<Legend iconSize={10} />
<Bar dataKey="Revenue" fill="oklch(0.52 0.22 262)" />
<Bar dataKey="EBITDA" fill="oklch(0.60 0.17 178)" />
<Bar dataKey="PAT" fill="oklch(0.70 0.18 55)" />
</BarChart>
</ResponsiveContainer>
</div>
<div className="border rounded-lg p-4">
<p className="text-sm font-medium mb-3 text-muted-foreground">Closing Cash (Cr)</p>
<ResponsiveContainer width="100%" height={200}>
<LineChart data={cashChart}>
<CartesianGrid strokeDasharray="3 3" stroke="oklch(0.90 0.01 262)" />
<XAxis dataKey="year" tick={{ fontSize: 10 }} />
<YAxis tick={{ fontSize: 10 }} />
<Tooltip />
<Line
type="monotone"
dataKey="Closing Cash"
stroke="oklch(0.52 0.22 262)"
dot={false}
strokeWidth={2}
/>
</LineChart>
</ResponsiveContainer>
</div>
</div>
)}
</div>
);
}
function IrrSheet({ kpis }: { kpis: KpiSummary }) {
const sections = [
{
title: "Returns",
rows: [
{ label: "Equity IRR (Leveraged)", value: kpis.equity_irr != null ? pct1(kpis.equity_irr) : "—" },
{ label: "Project IRR (Unlevered)", value: kpis.project_irr != null ? pct1(kpis.project_irr) : "—" },
{ label: "LCOE", value: kpis.lcoe_inr_per_kwh != null ? `${kpis.lcoe_inr_per_kwh.toFixed(2)}/kWh` : "—" },
{ label: "Payback Period", value: kpis.payback_years != null ? `${kpis.payback_years.toFixed(1)} yrs` : "—" },
],
},
{
title: "Debt Metrics",
rows: [
{ label: "Min DSCR", value: kpis.min_dscr?.toFixed(2) ?? "—" },
{ label: "Avg DSCR", value: kpis.avg_dscr?.toFixed(2) ?? "—" },
],
},
{
title: "Project Economics",
rows: [
{ label: "Solved / Fixed Tariff", value: kpis.solved_tariff_inr_per_kwh != null ? `${kpis.solved_tariff_inr_per_kwh.toFixed(2)}/kWh` : "—" },
{ label: "Total Capex", value: kpis.total_capex_cr != null ? `${kpis.total_capex_cr.toFixed(1)} Cr` : "—" },
{ label: "Debt Sized", value: kpis.debt_cr != null ? `${kpis.debt_cr.toFixed(1)} Cr` : "—" },
{ label: "IDC", value: kpis.idc_cr != null ? `${kpis.idc_cr.toFixed(1)} Cr` : "—" },
],
},
];
return (
<div className="max-w-xl space-y-4">
{sections.map((s) => (
<div key={s.title} className="border rounded-lg overflow-hidden">
<div className="bg-muted/50 px-4 py-2 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
{s.title}
</div>
<table className="w-full text-sm">
<tbody>
{s.rows.map(({ label, value }) => (
<tr key={label} className="border-t hover:bg-accent/30 transition-colors">
<td className="px-4 py-2.5 text-muted-foreground">{label}</td>
<td className="px-4 py-2.5 tabular-nums font-semibold text-right">{value}</td>
</tr>
))}
</tbody>
</table>
</div>
))}
</div>
);
}
function buildGenerationRows(gen: GenerationRow[]): TableRow[] {
const hasSolar = gen.some((r) => r.solar_mwh > 0);
const hasWind = gen.some((r) => r.wind_mwh > 0);
const rows: TableRow[] = [];
if (hasSolar) {
rows.push({ label: "Solar Generation (MWh)", values: gen.map((r) => r.solar_mwh), format: (v) => v == null ? "—" : Math.round(v).toLocaleString() });
rows.push({ label: "Solar CUF (%)", values: gen.map((r) => r.solar_cuf_pct), format: (v) => v == null ? "—" : `${v.toFixed(1)}%`, indent: true });
}
if (hasWind) {
rows.push({ label: "Wind Generation (MWh)", values: gen.map((r) => r.wind_mwh), format: (v) => v == null ? "—" : Math.round(v).toLocaleString() });
rows.push({ label: "Wind PLF (%)", values: gen.map((r) => r.wind_plf_pct), format: (v) => v == null ? "—" : `${v.toFixed(1)}%`, indent: true });
}
rows.push({ label: "Gross Total (MWh)", values: gen.map((r) => r.gross_mwh), isBold: true, format: (v) => v == null ? "—" : Math.round(v).toLocaleString() });
rows.push({ label: "Less: Aux Consumption", values: gen.map((r) => -r.aux_loss_mwh), format: (v) => v == null ? "—" : Math.round(v).toLocaleString(), indent: true });
rows.push({ label: "Less: Transmission Loss", values: gen.map((r) => -r.tx_loss_mwh), format: (v) => v == null ? "—" : Math.round(v).toLocaleString(), indent: true });
rows.push({ label: "Less: DSM Penalty", values: gen.map((r) => -r.dsm_loss_mwh), format: (v) => v == null ? "—" : Math.round(v).toLocaleString(), indent: true });
rows.push({ label: "Net Billable (MWh)", values: gen.map((r) => r.net_billable_mwh), isBold: true, format: (v) => v == null ? "—" : Math.round(v).toLocaleString() });
rows.push({ label: "Revenue (₹ Cr)", values: gen.map((r) => r.revenue_cr), isBold: true, format: n2 });
return rows;
}
function buildOpexRows(pnl: PnLRow[]): TableRow[] {
const total = pnl.map((r) => r.om_cr + r.insurance_cr + r.land_lease_cr + r.am_fee_cr + r.misc_opex_cr);
return [
{ label: "O&M (₹ Cr)", values: pnl.map((r) => r.om_cr), format: n2 },
{ label: "Insurance (₹ Cr)", values: pnl.map((r) => r.insurance_cr), format: n2, indent: true },
{ label: "Land Lease (₹ Cr)", values: pnl.map((r) => r.land_lease_cr), format: n2, indent: true },
{ label: "AM Fee (₹ Cr)", values: pnl.map((r) => r.am_fee_cr), format: n2, indent: true },
{ label: "Miscellaneous (₹ Cr)", values: pnl.map((r) => r.misc_opex_cr), format: n2, indent: true },
{ label: "Total OPEX (₹ Cr)", values: total, isBold: true, format: n2 },
{ label: "OPEX as % of Revenue", values: pnl.map((r, i) => r.revenue_cr > 0 ? total[i] / r.revenue_cr * 100 : null), format: (v) => v == null ? "—" : `${v.toFixed(1)}%`, indent: true },
];
}
function IdcSheet({ idc }: { idc: IdcPhasing }) {
if (!idc?.base_capex_cr) return <div className="text-muted-foreground p-4">No IDC data available</div>;
const months = idc.monthly ?? [];
const nMonths = months.length;
const nCols = Math.min(nMonths, 24); // Cap at 24 months for display
// Build ALL-IN-ONE matrix: both component costs AND funding sources
const monthlyRate = 1 / nMonths;
const solarPct = 0.70, windPct = 0.0, landPct = 0.10, epcPct = 0.12, contPct = 0.08;
const matrixRows: TableRow[] = [
// === Component Costs (what's being built) ===
{ isHeader: true, label: "COMPONENT COSTS", values: [] },
{ label: "Solar Capex", values: Array(nCols).fill(0).map((_, i) => months[i] ? idc.base_capex_cr * solarPct * monthlyRate : 0) },
{ label: "Wind Capex", values: Array(nCols).fill(0).map((_, i) => months[i] ? idc.base_capex_cr * windPct * monthlyRate : 0) },
{ label: "Land & Common", values: Array(nCols).fill(0).map((_, i) => months[i] ? idc.base_capex_cr * landPct * monthlyRate : 0) },
{ label: "EPC Overhead", values: Array(nCols).fill(0).map((_, i) => months[i] ? idc.base_capex_cr * epcPct * monthlyRate : 0) },
{ label: "Contingency", values: Array(nCols).fill(0).map((_, i) => months[i] ? idc.base_capex_cr * contPct * monthlyRate : 0) },
{ isSeparator: true, label: "", values: [] },
// === Funding Sources (how it's paid) ===
{ isHeader: true, label: "FUNDING SOURCES", values: [] },
{ label: "Equity Draw", values: months.slice(0, nCols).map((m) => m.equity_draw_cr) },
{ label: "Debt Draw", values: months.slice(0, nCols).map((m) => m.debt_draw_cr) },
{ label: "IDC Interest", values: months.slice(0, nCols).map((m) => m.idc_accrual_cr), indent: true },
{ isSeparator: true, label: "", values: [] },
// === Cumulative (running totals) ===
{ isHeader: true, label: "CUMULATIVE", values: [] },
{ label: "Cum. Equity", values: months.slice(0, nCols).map((m) => m.cum_equity_cr), indent: true },
{ label: "Cum. Debt", values: months.slice(0, nCols).map((m) => m.cum_debt_cr), indent: true },
{ label: "Cum. IDC", values: months.slice(0, nCols).map((m) => m.cum_idc_cr), indent: true },
{ isSeparator: true, label: "", values: [] },
{ label: "Total Project Cost", values: months.slice(0, nCols).map((m) => m.cum_tpc_cr), isBold: true },
];
// Compact header
const fundingMix = [
{ label: "Base", val: idc.base_capex_cr },
{ label: "IDC", val: idc.idc_cr },
{ label: "Total", val: idc.total_capex_cr },
{ label: "Equity", val: idc.equity_cr },
{ label: "Debt", val: idc.debt_cr },
];
return (
<div className="space-y-4">
{/* Header Cards */}
<div className="grid grid-cols-5 gap-2">
{fundingMix.map((f) => (
<div key={f.label} className="border rounded px-3 py-2 text-center">
<p className="text-[10px] text-muted-foreground">{f.label}</p>
<p className="text-sm font-bold tabular-nums">{f.val.toFixed(0)}</p>
</div>
))}
</div>
{/* Single ALL-IN-ONE Matrix Table */}
<div className="border rounded-lg overflow-hidden">
<div className="bg-muted/50 px-4 py-2 border-b border-border">
<h3 className="font-semibold text-sm">IDC Construction Phasing Matrix ({nMonths} months)</h3>
</div>
<HorizontalTable years={months.slice(0, nCols).map((m) => m.month)} rows={matrixRows} unit="₹ Cr" yearPrefix="M" />
</div>
</div>
);
}
// ---------------------------------------------------------------------------
// Main workbook
// ---------------------------------------------------------------------------
interface Props {
scenarioId: string;
kpis: KpiSummary;
debtScheduleJson: string | null;
activeSheet: string;
}
export function WorkbookView({ scenarioId, kpis, debtScheduleJson, activeSheet }: Props) {
const { data: stmts } = useQuery({
queryKey: ["statements", scenarioId],
queryFn: () => getStatements(scenarioId),
});
const debtSchedule: DebtYearRow[] = (() => {
try {
const safe = (debtScheduleJson ?? "[]")
.replace(/:\s*Infinity/g, ": null")
.replace(/:\s*-Infinity/g, ": null")
.replace(/:\s*NaN\b/g, ": null");
return JSON.parse(safe) as DebtYearRow[];
} catch {
return [];
}
})();
const pnl = stmts?.pnl ?? [];
const cfs = stmts?.cfs ?? [];
const bs = stmts?.bs ?? [];
const generation = stmts?.generation ?? [];
const idcPhasing = stmts?.idc_phasing;
const years = pnl.map((r) => r.year);
const debtYears = debtSchedule.map((r) => r.year);
const genYears = generation.map((r) => r.year);
return (
<div className="space-y-0">
<p className="text-xs text-muted-foreground mb-4">
All monetary values in INR Crore unless noted
</p>
{activeSheet === "summary" && <SummarySheet kpis={kpis} scenarioId={scenarioId} />}
{activeSheet === "pnl" && pnl.length > 0 && (
<HorizontalTable years={years} rows={buildPnLRows(pnl)} />
)}
{activeSheet === "cfs" && cfs.length > 0 && (
<HorizontalTable years={years} rows={buildCfsRows(cfs, kpis, pnl)} />
)}
{activeSheet === "bs" && bs.length > 0 && (
<HorizontalTable years={years} rows={buildBsRows(bs)} />
)}
{activeSheet === "debt" && debtSchedule.length > 0 && (
<HorizontalTable years={debtYears} rows={buildDebtRows(debtSchedule)} />
)}
{activeSheet === "irr" && <IrrSheet kpis={kpis} />}
{activeSheet === "generation" && generation.length > 0 && (
<HorizontalTable years={genYears} rows={buildGenerationRows(generation)} unit="MWh / ₹Cr" />
)}
{activeSheet === "idc" && idcPhasing && (
<IdcSheet idc={idcPhasing} />
)}
{activeSheet === "opex" && pnl.length > 0 && (
<HorizontalTable years={years} rows={buildOpexRows(pnl)} unit="INR Cr" />
)}
</div>
);
}