- 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>
119 lines
3.7 KiB
TypeScript
119 lines
3.7 KiB
TypeScript
"use client";
|
|
|
|
import { useQuery } from "@tanstack/react-query";
|
|
import { getKpis, type KpiSummary } from "@/lib/api";
|
|
|
|
interface Props {
|
|
scenarioIds: string[];
|
|
scenarioNames: Record<string, string>;
|
|
}
|
|
|
|
const KPI_LABELS: { key: keyof KpiSummary; label: string; format: "pct" | "num" | "inr" }[] = [
|
|
{ key: "solved_tariff_inr_per_kwh", label: "Tariff (₹/kWh)", format: "inr" },
|
|
{ key: "equity_irr", label: "Equity IRR", format: "pct" },
|
|
{ key: "project_irr", label: "Project IRR", format: "pct" },
|
|
{ key: "min_dscr", label: "Min DSCR", format: "num" },
|
|
{ key: "avg_dscr", label: "Avg DSCR", format: "num" },
|
|
{ key: "total_capex_cr", label: "Total Capex (Cr)", format: "num" },
|
|
{ key: "lcoe_inr_per_kwh", label: "LCOE (₹/kWh)", format: "inr" },
|
|
{ key: "payback_years", label: "Payback (yrs)", format: "num" },
|
|
{ key: "solar_y1_cuf", label: "Solar Y1 CUF", format: "pct" },
|
|
{ key: "wind_y1_plf", label: "Wind Y1 PLF", format: "pct" },
|
|
{ key: "rtc_cuf_achieved", label: "RTC CUF", format: "pct" },
|
|
{ key: "total_shortfall_mwh", label: "Shortfall (MWh)", format: "num" },
|
|
{ key: "total_mcp_revenue_cr", label: "MCP Revenue (Cr)", format: "num" },
|
|
];
|
|
|
|
function fmtCell(v: number | null | undefined, format: "pct" | "num" | "inr"): string {
|
|
if (v == null) return "—";
|
|
if (format === "pct") return `${(v * 100).toFixed(1)}%`;
|
|
if (format === "inr") return `₹${v.toFixed(2)}`;
|
|
return v.toFixed(2);
|
|
}
|
|
|
|
function KpiRow({
|
|
rowKey,
|
|
label,
|
|
format,
|
|
kpiMap,
|
|
ids,
|
|
}: {
|
|
rowKey: keyof KpiSummary;
|
|
label: string;
|
|
format: "pct" | "num" | "inr";
|
|
kpiMap: Record<string, KpiSummary>;
|
|
ids: string[];
|
|
}) {
|
|
const values = ids.map((id) => kpiMap[id]?.[rowKey] as number | null | undefined);
|
|
const nums = values.filter((v): v is number => v != null);
|
|
const best = nums.length > 0 ? Math.max(...nums) : null;
|
|
|
|
return (
|
|
<tr className="border-t">
|
|
<td className="px-3 py-1.5 text-sm font-medium text-muted-foreground">{label}</td>
|
|
{ids.map((id, i) => {
|
|
const v = values[i];
|
|
const isBest = v != null && v === best && nums.length > 1;
|
|
return (
|
|
<td
|
|
key={id}
|
|
className={`px-3 py-1.5 text-sm tabular-nums text-right ${isBest ? "font-semibold text-green-700" : ""}`}
|
|
>
|
|
{fmtCell(v, format)}
|
|
</td>
|
|
);
|
|
})}
|
|
</tr>
|
|
);
|
|
}
|
|
|
|
export function ScenarioCompare({ scenarioIds, scenarioNames }: Props) {
|
|
const queries = scenarioIds.map((id) =>
|
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
useQuery({
|
|
queryKey: ["kpis", id],
|
|
queryFn: () => getKpis(id),
|
|
})
|
|
);
|
|
|
|
const kpiMap: Record<string, KpiSummary> = {};
|
|
for (let i = 0; i < scenarioIds.length; i++) {
|
|
const data = queries[i].data;
|
|
if (data) kpiMap[scenarioIds[i]] = data;
|
|
}
|
|
|
|
const isLoading = queries.some((q) => q.isLoading);
|
|
|
|
if (isLoading) {
|
|
return <div className="text-muted-foreground text-sm py-4">Loading comparison…</div>;
|
|
}
|
|
|
|
return (
|
|
<div className="overflow-x-auto border rounded">
|
|
<table className="w-full text-xs">
|
|
<thead className="bg-muted/50">
|
|
<tr>
|
|
<th className="px-3 py-2 text-left font-medium">KPI</th>
|
|
{scenarioIds.map((id) => (
|
|
<th key={id} className="px-3 py-2 text-right font-medium">
|
|
{scenarioNames[id] ?? id.slice(0, 8)}
|
|
</th>
|
|
))}
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{KPI_LABELS.map(({ key, label, format }) => (
|
|
<KpiRow
|
|
key={key}
|
|
rowKey={key}
|
|
label={label}
|
|
format={format}
|
|
kpiMap={kpiMap}
|
|
ids={scenarioIds}
|
|
/>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
);
|
|
}
|