"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>(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 (
{years.map((y) => ( ))} {rows.map((row, i) => { rowIndex = i; const isExpanded = expandedRows.has(i); if (row.isSeparator) { return ( ); } if (row.isHeader) { return ( ); } return ( <> {row.values.map((v, j) => ( ))} {row.collapsible && row.children && isExpanded && row.children.map((child, ci) => ( {child.values.map((v, j) => ( ))} ))} ); })}
Metric ({unit}) {yearPrefix}{y}
{row.label}
row.collapsible && toggleRow(i)} > {row.collapsible && ( {isExpanded ? "▼" : "▶"} )} {row.label} {row.format ? row.format(v) : n1(v)}
{child.label} {child.format ? child.format(v) : n1(v)}
); } // --------------------------------------------------------------------------- // 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, onNavigate }: { kpis: KpiSummary; scenarioId: string; onNavigate?: (sheet: string) => void }) { 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 (
onNavigate?.("generation")} /> onNavigate?.("irr")} /> onNavigate?.("irr")} /> onNavigate?.("debt")} /> onNavigate?.("debt")} /> onNavigate?.("debt")} /> onNavigate?.("debt")} /> onNavigate?.("idc")} /> onNavigate?.("irr")} /> onNavigate?.("irr")} /> {kpis.solar_y1_cuf != null && ( onNavigate?.("generation")} /> )} {kpis.wind_y1_plf != null && ( onNavigate?.("generation")} /> )} {kpis.rtc_cuf_achieved != null && ( )} {/* Add custom KPI button */} {/* Custom KPIs */} {customKpis.map((kpi: { label: string; value: string; unit: string }, i: number) => (
))}
{stmts && (

P&L Overview (₹Cr)

Closing Cash (₹Cr)

)}
); } 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 (
{sections.map((s) => (
{s.title}
{s.rows.map(({ label, value }) => ( ))}
{label} {value}
))}
); } 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; } // Hourly generation tree: Year > Month > Day > Hour (expandable) // --------------------------------------------------------------------------- const MONTH_DAYS = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; interface HourlyData { solar_hourly?: number[]; wind_hourly?: number[]; // New columns hourly_timestamps?: string[]; hourly_fy?: string[]; hourly_proj_year?: number[]; hourly_total_re?: number[]; hourly_client_end?: number[]; hourly_load?: number[]; // Profile data for detailed display hourly_solar_profile?: number[]; hourly_wind_profile?: number[]; } function HourlyGenerationSheet({ hourly, codYear, solarDCMW, windMW }: { hourly: HourlyData; codYear?: number; solarDCMW?: number; windMW?: number }) { const { solar_hourly, wind_hourly, hourly_total_re, hourly_client_end, hourly_load, hourly_solar_profile, hourly_wind_profile } = hourly; const hasSolar = solar_hourly && solar_hourly.length > 0; const hasWind = wind_hourly && wind_hourly.length > 0; const hasTotalRe = hourly_total_re && hourly_total_re.length > 0; const hasClientEnd = hourly_client_end && hourly_client_end.length > 0; const hasLoad = hourly_load && hourly_load.length > 0; const hasSolarProfile = hourly_solar_profile && hourly_solar_profile.length > 0; const hasWindProfile = hourly_wind_profile && hourly_wind_profile.length > 0; const hasData = hasSolar || hasWind || hasTotalRe; // FY labels: if COD is April 2026, FY 2026-27 = Year 1 const startYear = codYear || new Date().getFullYear(); const getFyLabel = (yearIndex: number) => { const fyStart = startYear + yearIndex; const fyEnd = fyStart + 1; return `FY ${String(fyStart).slice(-2)}-${String(fyEnd).slice(-2)}`; }; // Month names - in reverse order (April first for Indian FY starting Apr) const monthNames = ['Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec', 'Jan', 'Feb', 'Mar']; // Mapping from position 0-11 to actual month 4-12, 1-3 const monthPositions = [4, 5, 6, 7, 8, 9, 10, 11, 12, 1, 2, 3]; const getMonthLabel = (pos: number, yearIndex: number) => { const month = monthPositions[pos]; const fyStart = startYear + yearIndex; const fyEnd = fyStart + 1; const fySuffix = month <= 3 ? String(fyEnd).slice(-2) : String(fyStart).slice(-2); return `${monthNames[pos]} ${fySuffix}`; }; // Expand state: which years/months/days are expanded const [expandedYears, setExpandedYears] = useState>(new Set()); const [expandedMonths, setExpandedMonths] = useState>(new Set()); const [expandedDays, setExpandedDays] = useState>(new Set()); const toggleYear = (yr: number) => setExpandedYears((prev) => { const next = new Set(prev); if (next.has(yr)) next.delete(yr); else next.add(yr); return next; }); const toggleMonth = (key: string) => setExpandedMonths((prev) => { const next = new Set(prev); if (next.has(key)) next.delete(key); else next.add(key); return next; }); const toggleDay = (key: string) => setExpandedDays((prev) => { const next = new Set(prev); if (next.has(key)) next.delete(key); else next.add(key); return next; }); // Helper: get hourly output data for a specific year/month/day (sums) const getYearData = (year: number, isSolar: boolean) => { const data = isSolar ? solar_hourly : wind_hourly; if (!data) return []; const start = (year - 1) * 8760; return data.slice(start, start + 8760); }; const getMonthData = (year: number, month: number, isSolar: boolean) => { const yearData = getYearData(year, isSolar); if (!yearData) return []; const start = MONTH_DAYS.slice(0, month - 1).reduce((a, d) => a + d, 0) * 24; const days = MONTH_DAYS[month - 1]; return yearData.slice(start, start + days * 24); }; const getDayData = (year: number, month: number, day: number, isSolar: boolean) => { const monthData = getMonthData(year, month, isSolar); if (!monthData) return []; const start = (day - 1) * 24; return monthData.slice(start, start + 24); }; // Helper: get raw profile data (0-1 normalized) for averages const getYearProfile = (year: number, data?: number[]) => { if (!data) return []; const start = (year - 1) * 8760; return data.slice(start, start + 8760); }; const getMonthProfile = (year: number, month: number, data?: number[]) => { if (!data) return []; const yearData = getYearProfile(year, data); if (!yearData.length) return []; const start = MONTH_DAYS.slice(0, month - 1).reduce((a, d) => a + d, 0) * 24; const days = MONTH_DAYS[month - 1]; return yearData.slice(start, start + days * 24); }; const getDayProfile = (year: number, month: number, day: number, data?: number[]) => { if (!data) return []; const yearData = getYearProfile(year, data); if (!yearData.length) return []; const start = MONTH_DAYS.slice(0, month - 1).reduce((a, d) => a + d, 0) * 24; const monthData = yearData.slice(start, start + MONTH_DAYS[month - 1] * 24); const dayStart = (day - 1) * 24; return monthData.slice(dayStart, dayStart + 24); }; // Compute averages from raw profile (0-1 values) const computeYearAvgProfile = (year: number, data?: number[]) => { const d = getYearProfile(year, data); if (!d.length) return 0; return d.reduce((a, v) => a + v, 0) / d.length; }; const computeMonthAvgProfile = (year: number, month: number, data?: number[]) => { const d = getMonthProfile(year, month, data); if (!d.length) return 0; return d.reduce((a, v) => a + v, 0) / d.length; }; const computeDayAvgProfile = (year: number, month: number, day: number, data?: number[]) => { const d = getDayProfile(year, month, day, data); if (!d.length) return 0; return d.reduce((a, v) => a + v, 0) / d.length; }; // Compute totals for display const computeYearTotal = (year: number, isSolar: boolean) => { const d = getYearData(year, isSolar); return d.reduce((a, v) => a + v, 0); }; const computeMonthTotal = (year: number, month: number, isSolar: boolean) => { const d = getMonthData(year, month, isSolar); return d.reduce((a, v) => a + v, 0); }; const computeDayTotal = (year: number, month: number, day: number, isSolar: boolean) => { const d = getDayData(year, month, day, isSolar); return d.reduce((a, v) => a + v, 0); }; // Helper functions for new columns (Total RE, Client End, Load) const getYearDataNew = (year: number, data?: number[]) => { if (!data) return []; const start = (year - 1) * 8760; return data.slice(start, start + 8760); }; const computeYearTotalNew = (year: number, data?: number[]) => { const d = getYearDataNew(year, data); return d.reduce((a, v) => a + v, 0); }; const computeMonthTotalNew = (year: number, month: number, data?: number[]) => { if (!data) return 0; const yearData = getYearDataNew(year, data); if (!yearData.length) return 0; const start = MONTH_DAYS.slice(0, month - 1).reduce((a, d) => a + d, 0) * 24; const days = MONTH_DAYS[month - 1]; const monthData = yearData.slice(start, start + days * 24); return monthData.reduce((a, v) => a + v, 0); }; const computeDayTotalNew = (year: number, month: number, day: number, data?: number[]) => { if (!data) return 0; const yearData = getYearDataNew(year, data); if (!yearData.length) return 0; const start = MONTH_DAYS.slice(0, month - 1).reduce((a, d) => a + d, 0) * 24; const monthData = yearData.slice(start, start + MONTH_DAYS[month - 1] * 24); const dayStart = (day - 1) * 24; const dayData = monthData.slice(dayStart, dayStart + 24); return dayData.reduce((a, v) => a + v, 0); }; if (!hasData) { return
No hourly generation data available
; } // Build tree: Years (1-25), expandable to months, days, hours return (
{hasSolar && ● Solar} {hasWind && ● Wind} {hasTotalRe && ● Total RE} {hasClientEnd && ● Client End} {hasLoad && ● Load} Click: Year → Month → Day → Hour
{/* Column Header */}
Period {hasSolar && Solar 8760} {hasSolar && DC MW} {hasSolar && Solar MW} {hasWind && Wind 8760} {hasWind && MW} {hasWind && Wind MW} {hasTotalRe && Total RE} {hasClientEnd && Client End} {hasLoad && Load}
{/* Year rows */} {[...Array(25)].map((_, i) => { const year = i + 1; const isYearExpanded = expandedYears.has(year); const solarYr = hasSolar ? computeYearTotal(year, true) : 0; const windYr = hasWind ? computeYearTotal(year, false) : 0; const totalReYr = hasTotalRe ? computeYearTotalNew(year, hourly_total_re) : 0; const clientEndYr = hasClientEnd ? computeYearTotalNew(year, hourly_client_end) : 0; const loadYr = hasLoad ? computeYearTotalNew(year, hourly_load) : 0; // Solar 8760: average profile value (0-1), DC MW: capacity (placeholder), Solar MW: sum output const solarProfileAvg = hasSolarProfile ? computeYearAvgProfile(year, hourly_solar_profile) : 0; const windProfileAvg = hasWindProfile ? computeYearAvgProfile(year, hourly_wind_profile) : 0; const fyLabel = getFyLabel(i); return (
{/* Month rows - forward order (Apr first, then subsequent months) */} {isYearExpanded && [...Array(12)].map((_, mi) => { const pos = mi; // Forward: 0=Apr, 1=May, etc. const month = monthPositions[pos]; const monthKey = `${year}-${month}`; const isMonthExpanded = expandedMonths.has(monthKey); const solarMo = hasSolar ? computeMonthTotal(year, month, true) : 0; const windMo = hasWind ? computeMonthTotal(year, month, false) : 0; // Average profile for month const solarProfileMo = hasSolarProfile ? computeMonthAvgProfile(year, month, hourly_solar_profile) : 0; const windProfileMo = hasWindProfile ? computeMonthAvgProfile(year, month, hourly_wind_profile) : 0; const totalReMo = hasTotalRe ? computeMonthTotalNew(year, month, hourly_total_re) : 0; const clientEndMo = hasClientEnd ? computeMonthTotalNew(year, month, hourly_client_end) : 0; const loadMo = hasLoad ? computeMonthTotalNew(year, month, hourly_load) : 0; const daysInMonth = MONTH_DAYS[month - 1]; const monthLabel = getMonthLabel(pos, i); return (
{/* Day rows (expandable) */} {isMonthExpanded && [...Array(daysInMonth)].map((_, di) => { const day = di + 1; const dayKey = `${year}-${month}-${day}`; const isDayExpanded = expandedDays.has(dayKey); const solarDy = hasSolar ? computeDayTotal(year, month, day, true) : 0; const windDy = hasWind ? computeDayTotal(year, month, day, false) : 0; // Average profile for day const solarProfileDay = hasSolarProfile ? computeDayAvgProfile(year, month, day, hourly_solar_profile) : 0; const windProfileDay = hasWindProfile ? computeDayAvgProfile(year, month, day, hourly_wind_profile) : 0; const totalReDy = hasTotalRe ? computeDayTotalNew(year, month, day, hourly_total_re) : 0; const clientEndDy = hasClientEnd ? computeDayTotalNew(year, month, day, hourly_client_end) : 0; const loadDy = hasLoad ? computeDayTotalNew(year, month, day, hourly_load) : 0; return (
{/* Hour values - vertical stack */} {isDayExpanded && (
{[...Array(24)].map((_, h) => { const hour = h; const hourIdx = (day - 1) * 24 + hour; const solarHr = hasSolar ? getDayData(year, month, day, true)[hourIdx % 24] : 0; const windHr = hasWind ? getDayData(year, month, day, false)[hourIdx % 24] : 0; // Raw profile values const yearDataProfile = getYearProfile(year, hourly_solar_profile); const hourProfile = yearDataProfile.length > hourIdx ? yearDataProfile[hourIdx] : 0; const yearDataProfileW = getYearProfile(year, hourly_wind_profile); const hourProfileW = yearDataProfileW.length > hourIdx ? yearDataProfileW[hourIdx] : 0; // Total RE and Client End const yearDataNew = getYearDataNew(year, hourly_total_re); const hourDataTotalRe = yearDataNew.length > hourIdx ? yearDataNew[hourIdx] : 0; const yearDataClient = getYearDataNew(year, hourly_client_end); const hourDataClient = yearDataClient.length > hourIdx ? yearDataClient[hourIdx] : 0; const yearDataLoad = getYearDataNew(year, hourly_load); const hourDataLoad = yearDataLoad.length > hourIdx ? yearDataLoad[hourIdx] : 0; return (
{String(h).padStart(2, '0')}:00 {/* Solar 8760: raw profile, DC MW, Solar MW */} {hasSolar && {(hourProfile * 100).toFixed(1)}%} {hasSolar && {solarDCMW || '-'}} {hasSolar && {Math.round(solarHr)}} {/* Wind 8760, MW, Wind MW */} {hasWind && {(hourProfileW * 100).toFixed(1)}%} {hasWind && {windMW || '-'}} {hasWind && {Math.round(windHr)}} {/* Total RE, Client End, Load */} {hasTotalRe && {Math.round(hourDataTotalRe)}} {hasClientEnd && {Math.round(hourDataClient)}} {hasLoad && {Math.round(hourDataLoad)}}
); })}
)}
); })}
); })}
); })}
); } 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
No IDC data available
; 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 (
{/* Header Cards */}
{fundingMix.map((f) => (

{f.label}

₹{f.val.toFixed(0)}

))}
{/* Single ALL-IN-ONE Matrix Table */}

IDC Construction Phasing Matrix ({nMonths} months)

m.month)} rows={matrixRows} unit="₹ Cr" yearPrefix="M" />
); } // --------------------------------------------------------------------------- // Main workbook // --------------------------------------------------------------------------- interface Props { scenarioId: string; kpis: KpiSummary; debtScheduleJson: string | null; activeSheet: string; codYear?: number; solarDCMW?: number; windMW?: number; onNavigate?: (sheet: string) => void; } export function WorkbookView({ scenarioId, kpis, debtScheduleJson, activeSheet, codYear, solarDCMW, windMW, onNavigate }: 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 (

All monetary values in INR Crore unless noted

{activeSheet === "summary" && } {activeSheet === "pnl" && pnl.length > 0 && ( )} {activeSheet === "cfs" && cfs.length > 0 && ( )} {activeSheet === "bs" && bs.length > 0 && ( )} {activeSheet === "debt" && debtSchedule.length > 0 && ( )} {activeSheet === "irr" && } {activeSheet === "generation" && (stmts?.solar_hourly || stmts?.wind_hourly || stmts?.hourly_total_re ? ( ) : generation.length > 0 ? ( ) : (
No generation data available
))} {activeSheet === "idc" && idcPhasing && ( )} {activeSheet === "opex" && pnl.length > 0 && ( )}
); }