"use client"; import { useState } from "react"; import { Button } from "@/components/ui/button"; import { updateScenarioInputs, type ScenarioInputPayload, type CostItem } from "@/lib/api"; // --------------------------------------------------------------------------- // Constants // --------------------------------------------------------------------------- type Raw = Record; // Location config with expected CUF const LOCATION_CUF: Record = { RJ: 27.5, // Rajasthan - high irradiance GJ: 26.0, // Gujarat KA: 24.5, // Karnataka AP: 25.0, // Andhra Pradesh TN: 24.0, // Tamil Nadu MP: 25.5, // Madhya Pradesh MH: 24.0, // Maharashtra HR: 23.5, // Haryana }; const LOCATIONS = [ { value: "RJ", label: "Rajasthan" }, { value: "GJ", label: "Gujarat" }, { value: "KA", label: "Karnataka" }, { value: "AP", label: "Andhra Pradesh" }, { value: "TN", label: "Tamil Nadu" }, { value: "MP", label: "Madhya Pradesh" }, { value: "MH", label: "Maharashtra" }, { value: "HR", label: "Haryana" }, { value: "custom", label: "πŸ“€ Upload Custom Profile" }, ]; const INDIAN_STATES = [ { value: "RJ", label: "Rajasthan" }, { value: "GJ", label: "Gujarat" }, { value: "KA", label: "Karnataka" }, { value: "AP", label: "Andhra Pradesh" }, { value: "TN", label: "Tamil Nadu" }, { value: "MP", label: "Madhya Pradesh" }, { value: "MH", label: "Maharashtra" }, { value: "HR", label: "Haryana" }, { value: "UP", label: "Uttar Pradesh" }, { value: "PB", label: "Punjab" }, { value: "TS", label: "Telangana" }, { value: "OR", label: "Odisha" }, ]; const DEBT_SHAPES = [ { value: "equal_principal", label: "Equal Principal" }, { value: "equal_installment", label: "Equal Installment (EMI)" }, { value: "dscr_sculpted", label: "DSCR Sculpted" }, { value: "balloon", label: "Balloon" }, ]; // Default cost items for each technology const SOLAR_DEFAULT_ITEMS: CostItem[] = [ { id: "solar_module", name: "Solar PV Module", category: "HardCost", basis: "PER_WP_DC", value: 18.0, depr_class: "Plant", attribution: "SolarOnly", phasing_id: "default" }, { id: "solar_inverter", name: "Inverter", category: "HardCost", basis: "PER_WP_DC", value: 4.0, depr_class: "Plant", attribution: "SolarOnly", phasing_id: "default" }, { id: "solar_dc_bos", name: "DC BOS (cables, mounting, earthing)", category: "HardCost", basis: "PER_WP_DC", value: 3.5, depr_class: "Plant", attribution: "SolarOnly", phasing_id: "default" }, { id: "solar_ac_bos", name: "AC BOS (transformer, switchgear)", category: "HardCost", basis: "PER_WP_DC", value: 2.5, depr_class: "Plant", attribution: "SolarOnly", phasing_id: "default" }, { id: "solar_land", name: "Land (per MW)", category: "HardCost", basis: "PER_WP_DC", value: 2.0, depr_class: "Land_NoDepr", attribution: "SolarOnly", phasing_id: "default" }, { id: "solar_land_upfront", name: "Land Lease (upfront 5yr)", category: "HardCost", basis: "PER_ACRE", value: 30, depr_class: "LandLease_Amortized", attribution: "SolarOnly", phasing_id: "default" }, { id: "solar_hr", name: "HR & Admin (pre-COD)", category: "SoftCost", basis: "PER_WP_DC", value: 0.5, depr_class: "Intangible", attribution: "SolarOnly", phasing_id: "default" }, { id: "solar_epc_oh", name: "EPC Overhead", category: "EPCOverhead", basis: "PCT_OF_HARDCOST", value: 0.03, depr_class: "Plant", attribution: "SolarOnly", phasing_id: "default" }, { id: "solar_contingency", name: "Contingency", category: "Contingency", basis: "PCT_OF_HARDCOST", value: 0.02, depr_class: "Plant", attribution: "SolarOnly", phasing_id: "default" }, ]; const WIND_DEFAULT_ITEMS: CostItem[] = [ { id: "wind_wtg", name: "WTG Supply", category: "HardCost", basis: "PER_MW_WIND", value: 5.50, depr_class: "Plant", attribution: "WindOnly", phasing_id: "default" }, { id: "wind_tower", name: "Tower", category: "HardCost", basis: "PER_MW_WIND", value: 1.20, depr_class: "Plant", attribution: "WindOnly", phasing_id: "default" }, { id: "wind_bop", name: "Balance of Plant", category: "HardCost", basis: "PER_MW_WIND", value: 0.80, depr_class: "Plant", attribution: "WindOnly", phasing_id: "default" }, { id: "wind_land", name: "Land (per MW)", category: "HardCost", basis: "PER_MW_WIND", value: 0.30, depr_class: "Land_NoDepr", attribution: "WindOnly", phasing_id: "default" }, { id: "wind_land_upfront", name: "Land Lease (upfront 5yr)", category: "HardCost", basis: "PER_ACRE", value: 30, depr_class: "LandLease_Amortized", attribution: "WindOnly", phasing_id: "default" }, { id: "wind_e_and_c", name: "Erection & Commissioning", category: "HardCost", basis: "PER_MW_WIND", value: 0.40, depr_class: "Plant", attribution: "WindOnly", phasing_id: "default" }, { id: "wind_epc_oh", name: "EPC Overhead", category: "EPCOverhead", basis: "PCT_OF_HARDCOST", value: 0.03, depr_class: "Plant", attribution: "WindOnly", phasing_id: "default" }, { id: "wind_contingency", name: "Contingency", category: "Contingency", basis: "PCT_OF_HARDCOST", value: 0.02, depr_class: "Plant", attribution: "WindOnly", phasing_id: "default" }, ]; const BESS_DEFAULT_ITEMS: CostItem[] = [ { id: "bess_supply", name: "BESS Supply (cells + BMS + PCS)", category: "HardCost", basis: "PER_MWH_BESS", value: 3.50, depr_class: "BESS", attribution: "BESSOnly", phasing_id: "default" }, { id: "bess_civil", name: "Civil & Integration", category: "HardCost", basis: "PER_MWH_BESS", value: 0.40, depr_class: "Building", attribution: "BESSOnly", phasing_id: "default" }, { id: "bess_epc_oh", name: "EPC Overhead", category: "EPCOverhead", basis: "PCT_OF_HARDCOST", value: 0.03, depr_class: "BESS", attribution: "BESSOnly", phasing_id: "default" }, ]; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- function sec(inputs: Raw, key: string): Raw { return (inputs[key] as Raw) ?? {}; } function gv(inputs: Raw, section: string, field: string, fallback: T): T { const s = sec(inputs, section); const v = s[field]; return v !== undefined && v !== null ? (v as T) : fallback; } function pct(raw: number, decimals = 4): number { return parseFloat((raw * 100).toFixed(decimals)); } function sv(inputs: Raw, section: string, field: string, value: unknown): Raw { return { ...inputs, [section]: { ...sec(inputs, section), [field]: value } }; } function deriveCodDate(project: Raw): string { if (project.cod_date) return project.cod_date as string; const year = (project.cod_year as number) ?? 2027; return `${year}-04-01`; } // Compute Cr from a cost item given current capacities (including tax) function computeItemCr( item: CostItem, dcMwp: number, windMw: number, bessMwh: number, hardCostSum: number, landAcres: number, ): number | null { let baseCr: number | null; switch (item.basis) { case "PER_WP_DC": baseCr = item.value * dcMwp * 0.1; // β‚Ή/Wp Γ— MWp Γ— 0.1 = Cr break; case "PER_MWP_DC": baseCr = item.value * dcMwp; break; case "PER_MW_WIND": baseCr = item.value * windMw; break; case "PER_MWH_BESS": baseCr = item.value * bessMwh; break; case "PCT_OF_HARDCOST": baseCr = hardCostSum > 0 ? item.value * hardCostSum : null; break; case "PER_ACRE": baseCr = item.value * (landAcres || 0) * 0.01; // Lakh/acre Γ— acres Γ— 0.01 = Cr (1 Lakh = 0.01 Cr) break; case "ABS_INR_CR": baseCr = item.value; break; default: baseCr = null; } // Add tax (GST) if applicable - default 5% for modules if (baseCr != null) { const taxPct = item.tax_pct ?? 5; return baseCr * (1 + taxPct / 100); } return baseCr; } function basisLabel(basis: CostItem["basis"]): string { switch (basis) { case "PER_WP_DC": return "β‚Ή/Wp"; case "PER_MWP_DC": return "Cr/MWp"; case "PER_MW_WIND": return "Cr/MW"; case "PER_MWH_BESS": return "Cr/MWh"; case "PCT_OF_HARDCOST": return "% of HC"; case "PER_ACRE": return "Lakh/acre"; case "ABS_INR_CR": return "β‚ΉCr"; default: return ""; } } function injectDefaultItems(inputs: Raw, solarEnabled: boolean, windEnabled: boolean, bessEnabled: boolean): Raw { const capex = sec(inputs, "capex"); const existing = (capex.cost_items as CostItem[]) ?? []; if (existing.length > 0) return inputs; const items: CostItem[] = []; if (solarEnabled) items.push(...SOLAR_DEFAULT_ITEMS); if (windEnabled) items.push(...WIND_DEFAULT_ITEMS); if (bessEnabled) items.push(...BESS_DEFAULT_ITEMS); return sv(inputs, "capex", "cost_items", items); } // --------------------------------------------------------------------------- // Field primitives // --------------------------------------------------------------------------- function Label({ text, sub }: { text: string; sub?: string }) { return (
{text} {sub && {sub}}
); } function NumField({ label, sub, value, onChange, step = 0.01, min, max, suffix, readOnly, }: { label: string; sub?: string; value: number; onChange: (v: number) => void; step?: number; min?: number; max?: number; suffix?: string; readOnly?: boolean; }) { return (
); } function DateField({ label, sub, value, onChange, badge, }: { label: string; sub?: string; value: string; onChange: (v: string) => void; badge?: string; }) { return (
onChange(e.target.value)} className="rounded border border-input bg-background px-2 py-1 text-sm focus:outline-none focus:ring-2 focus:ring-primary/30 hover:border-primary/40 transition-colors" />
); } function SelectField({ label, sub, value, onChange, options, }: { label: string; sub?: string; value: string; onChange: (v: string) => void; options: { value: string; label: string }[]; }) { return (
); } // --------------------------------------------------------------------------- // Card components // --------------------------------------------------------------------------- function CardHeader({ title, open, onToggle, rightSlot, }: { title: string; open: boolean; onToggle: () => void; rightSlot?: React.ReactNode; }) { return (
β–Ύ {title}
{rightSlot && (
e.stopPropagation()}>{rightSlot}
)}
); } function CollapsibleCard({ title, children, cols = 2, defaultOpen = true, }: { title: string; children: React.ReactNode; cols?: number; defaultOpen?: boolean; }) { const [open, setOpen] = useState(defaultOpen); return (
setOpen(!open)} /> {open && (
{children}
)}
); } function ToggleCard({ title, enabled, onToggle, children, defaultOpen = true, }: { title: string; enabled: boolean; onToggle: (v: boolean) => void; children: React.ReactNode; defaultOpen?: boolean; }) { const [open, setOpen] = useState(defaultOpen); return (
setOpen(!open)} rightSlot={ } /> {enabled && open && (
{children}
)}
); } // --------------------------------------------------------------------------- // Cost Items Table // --------------------------------------------------------------------------- function CostItemsTable({ items, onChange, dcMwp, windMw, bessMwh, landAcres, attribution, }: { items: CostItem[]; onChange: (items: CostItem[]) => void; dcMwp: number; windMw: number; bessMwh: number; landAcres: number; attribution: "SolarOnly" | "WindOnly" | "BESSOnly"; }) { const filtered = items.filter((it) => it.attribution === attribution); // Compute hard cost sum for PCT items const hardCostSum = filtered .filter((it) => it.category === "HardCost" || it.category === "SoftCost") .reduce((acc, it) => { const cr = computeItemCr(it, dcMwp, windMw, bessMwh, 0, landAcres); return acc + (cr ?? 0); }, 0); function updateItem(id: string, field: keyof CostItem, value: unknown) { onChange( items.map((it) => it.id === id ? { ...it, [field]: value } : it, ), ); } function removeItem(id: string) { onChange(items.filter((it) => it.id !== id)); } function addItem() { const newId = `custom_${attribution.toLowerCase()}_${Date.now()}`; const defaults: Record = { SolarOnly: "PER_WP_DC", WindOnly: "PER_MW_WIND", BESSOnly: "PER_MWH_BESS", }; const newItem: CostItem = { id: newId, name: "Custom Item", category: "HardCost", basis: defaults[attribution], value: 0, depr_class: attribution === "BESSOnly" ? "BESS" : "Plant", attribution, phasing_id: "default", }; onChange([...items, newItem]); } const totalCr = filtered.reduce((acc, it) => { const cr = computeItemCr(it, dcMwp, windMw, bessMwh, hardCostSum, landAcres); return acc + (cr ?? 0); }, 0); return (
{filtered.map((item) => { const cr = computeItemCr(item, dcMwp, windMw, bessMwh, hardCostSum, landAcres); return ( ); })}
Line Item Basis Rate Tax% β‚ΉCr
updateItem(item.id, "name", e.target.value)} className="w-full bg-transparent focus:outline-none focus:bg-background focus:ring-1 focus:ring-primary/30 rounded px-1" /> {basisLabel(item.basis)} { const raw = parseFloat(e.target.value) || 0; updateItem(item.id, "value", item.basis === "PCT_OF_HARDCOST" ? raw / 100 : raw); }} className="w-full text-right bg-transparent focus:outline-none focus:bg-background focus:ring-1 focus:ring-primary/30 rounded px-1 tabular-nums" /> { const num = e.target.value.replace("%", "").replace(/[^0-9.]/g, ""); updateItem(item.id, "tax_pct", parseFloat(num) || 0); }} className="w-full text-right bg-transparent focus:outline-none focus:bg-background focus:ring-1 focus:ring-primary/30 rounded px-1 tabular-nums" /> {item.basis === "PCT_OF_HARDCOST" && cr == null ? ( calc... ) : cr != null ? ( {cr.toFixed(1)} ) : ( β€” )}
Total ({attribution.replace("Only", "")}) {totalCr.toFixed(1)}
); } // --------------------------------------------------------------------------- // Main component // --------------------------------------------------------------------------- interface Props { scenarioId: string; inputsJson: string | null; onSaved: () => void; } export function InputsTab({ scenarioId, inputsJson, onSaved }: Props) { const [inputs, setInputs] = useState(() => { try { const parsed = JSON.parse(inputsJson ?? "{}") as Raw; // Always inject default cost items if missing (for old scenarios) const capex = parsed.capex as { cost_items?: CostItem[] } | undefined; const existingItems = capex?.cost_items ?? []; const items: CostItem[] = [...existingItems]; if (existingItems.length === 0) { items.push(...SOLAR_DEFAULT_ITEMS, ...WIND_DEFAULT_ITEMS, ...BESS_DEFAULT_ITEMS); } return sv({ ...parsed }, "capex", "cost_items", items); } catch { return {}; } }); const [saving, setSaving] = useState(false); const [error, setError] = useState(null); function upd(section: string, field: string, value: unknown) { setInputs((prev) => sv(prev, section, field, value)); } function setSection(key: string, value: unknown) { setInputs((prev) => ({ ...prev, [key]: value })); } function updCostItems(newItems: CostItem[]) { setInputs((prev) => sv(prev, "capex", "cost_items", newItems)); } async function handleSave() { setSaving(true); setError(null); try { await updateScenarioInputs(scenarioId, inputs as ScenarioInputPayload); onSaved(); } catch (e) { setError(e instanceof Error ? e.message : "Save failed"); } finally { setSaving(false); } } const project = sec(inputs, "project"); const solar = inputs.solar as Raw | null | undefined; const wind = inputs.wind as Raw | null | undefined; const bess = inputs.bess as Raw | null | undefined; const rtc = inputs.rtc as Raw | null | undefined; const solarEnabled = solar !== null && solar !== undefined; const windEnabled = wind !== null && wind !== undefined; const bessEnabled = bess !== null && bess !== undefined; const rtcEnabled = rtc !== null && rtc !== undefined; const plantCod = deriveCodDate(project); // Solar capacity helpers const solarAcMw = gv(inputs, "solar", "capacity_ac_mw", 100); const solarDcAcRatio = gv(inputs, "solar", "dc_ac_ratio", 1.4); const solarDcMwp = parseFloat((solarAcMw * solarDcAcRatio).toFixed(3)); const moduleType = gv(inputs, "solar", "module_type", "fixed"); const windMw = gv(inputs, "wind", "capacity_mw", 50); const bessMwh = gv(inputs, "bess", "capacity_mwh", 200); // Land calculation: 2.5ac/MWp (fixed) or 3.5ac/MWp (tracker) + 0.5ac/MW (wind) const solarLandFactor = (moduleType as string) === "tracker" ? 3.5 : 2.5; const windLandFactor = 0.5; const calculatedLandAcres = solarEnabled ? (solarDcMwp * solarLandFactor) + (windEnabled ? windMw * windLandFactor : 0) : windEnabled ? windMw * windLandFactor : 0; // Use explicit value if set, otherwise auto-calculate const explicitLandAcres = gv(inputs, "project", "land_acres", 0); const effectiveLandAcres = explicitLandAcres > 0 ? explicitLandAcres : Math.max(calculatedLandAcres, 1); const costItems = (gv(inputs, "capex", "cost_items", []) as CostItem[]); // Compute cost summary with tax breakdown function computeCostWithTax(items: CostItem[], attribution: string) { const filtered = items.filter((it) => it.attribution === attribution); const sum = filtered.reduce((acc, it) => { const cr = computeItemCr(it, solarDcMwp, windMw, bessMwh, 0, effectiveLandAcres); return acc + (cr ?? 0); }, 0); return sum; } const solarTotalCr = computeCostWithTax(costItems, "SolarOnly"); const windTotalCr = computeCostWithTax(costItems, "WindOnly"); const bessTotalCr = computeCostWithTax(costItems, "BESSOnly"); // Calculate upfront lease cost based on user inputs (acres * rate * years) const leaseRate = gv(inputs, "project", "land_lease_rate", 0.4); const leaseYears = gv(inputs, "project", "land_lease_years", 5); const upfrontLeaseCostCr = effectiveLandAcres * leaseRate * leaseYears; const totalCostCr = solarTotalCr + windTotalCr + bessTotalCr + upfrontLeaseCostCr; // COD sync helper function handlePlantCodChange(v: string) { setInputs((prev) => { let next = sv(prev, "project", "cod_date", v); const p = sec(next, "project"); // Auto-sync tech CODs if they matched old plant COD or were empty if (!p.solar_cod_date || p.solar_cod_date === plantCod) { next = sv(next, "project", "solar_cod_date", v); } if (!p.wind_cod_date || p.wind_cod_date === plantCod) { next = sv(next, "project", "wind_cod_date", v); } if (!p.bess_cod_date || p.bess_cod_date === plantCod) { next = sv(next, "project", "bess_cod_date", v); } return next; }); } // Sync DC capacity whenever AC or ratio changes function handleSolarAcChange(v: number) { const newDc = parseFloat((v * solarDcAcRatio).toFixed(3)); setInputs((prev) => { let next = sv(prev, "solar", "capacity_ac_mw", v); next = sv(next, "solar", "capacity_dc_mwp", newDc); next = sv(next, "project", "capacity_solar_mwp", newDc); return next; }); } function handleDcAcRatioChange(v: number) { const newDc = parseFloat((solarAcMw * v).toFixed(3)); setInputs((prev) => { let next = sv(prev, "solar", "dc_ac_ratio", v); next = sv(next, "solar", "capacity_dc_mwp", newDc); next = sv(next, "project", "capacity_solar_mwp", newDc); return next; }); } return (
{/* ── Project Info ─────────────────────────────────────── */} { upd("project", "state", v); // Auto-set location_id for solar/wind if (solarEnabled) upd("solar", "location_id", v); if (windEnabled) upd("wind", "location_id", v); }} options={INDIAN_STATES} /> upd("project", "solar_cod_date", v === plantCod ? null : v)} badge={(project.solar_cod_date as string) && (project.solar_cod_date as string) !== plantCod ? "custom" : "synced"} /> {windEnabled && ( upd("project", "wind_cod_date", v === plantCod ? null : v)} badge={(project.wind_cod_date as string) && (project.wind_cod_date as string) !== plantCod ? "custom" : "synced"} /> )} {bessEnabled && ( upd("project", "bess_cod_date", v === plantCod ? null : v)} badge={(project.bess_cod_date as string) && (project.bess_cod_date as string) !== plantCod ? "custom" : "synced"} /> )} {/* ── Cost Summary ────────────────────────────────────── */} {(() => { // Compute grouped from ALL cost items - don't check enablement const solarItems = costItems.filter(it => it.attribution === "SolarOnly"); const windItems = costItems.filter(it => it.attribution === "WindOnly"); const bessItems = costItems.filter(it => it.attribution === "BESSOnly"); const solarBOS = solarItems.filter(it => ["solar_inverter", "solar_dc_bos", "solar_ac_bos", "solar_hr"].includes(it.id)).reduce((acc, it) => acc + (computeItemCr(it, solarDcMwp, windMw, bessMwh, 0, effectiveLandAcres) ?? 0), 0); const solarModule = solarItems.filter(it => it.id === "solar_module").reduce((acc, it) => acc + (computeItemCr(it, solarDcMwp, windMw, bessMwh, 0, effectiveLandAcres) ?? 0), 0); const windWTG = windItems.filter(it => ["wind_wtg", "wind_tower", "wind_bop", "wind_e_and_c"].includes(it.id)).reduce((acc, it) => acc + (computeItemCr(it, solarDcMwp, windMw, bessMwh, 0, effectiveLandAcres) ?? 0), 0); // Solar land: only include items with id containing "land" AND attribution "SolarOnly" const solarLandCost = solarItems.filter(it => it.id.includes("land") && it.attribution === "SolarOnly").reduce((acc, it) => acc + (computeItemCr(it, solarDcMwp, windMw, bessMwh, 0, effectiveLandAcres) ?? 0), 0); // Wind land: only include items with id containing "land" AND attribution "WindOnly" const windLandCost = windItems.filter(it => it.id.includes("land") && it.attribution === "WindOnly").reduce((acc, it) => acc + (computeItemCr(it, solarDcMwp, windMw, bessMwh, 0, effectiveLandAcres) ?? 0), 0); const bessAll = bessItems.reduce((acc, it) => acc + (computeItemCr(it, solarDcMwp, windMw, bessMwh, 0, effectiveLandAcres) ?? 0), 0); const solarEPC = solarItems.filter(it => it.category === "EPCOverhead").reduce((acc, it) => acc + (computeItemCr(it, solarDcMwp, windMw, bessMwh, 0, effectiveLandAcres) ?? 0), 0); const windEPC = windItems.filter(it => it.category === "EPCOverhead").reduce((acc, it) => acc + (computeItemCr(it, solarDcMwp, windMw, bessMwh, 0, effectiveLandAcres) ?? 0), 0); const solarCont = solarItems.filter(it => it.category === "Contingency" && it.attribution === "SolarOnly").reduce((acc, it) => acc + (computeItemCr(it, solarDcMwp, windMw, bessMwh, 0, effectiveLandAcres) ?? 0), 0); const windCont = windItems.filter(it => it.category === "Contingency" && it.attribution === "WindOnly").reduce((acc, it) => acc + (computeItemCr(it, solarDcMwp, windMw, bessMwh, 0, effectiveLandAcres) ?? 0), 0); // EPC Margins - include items with attribution "WindOnly" OR "Common" when wind is enabled const epcMarginWTG = windEnabled ? windItems.filter(it => it.category === "EPCMargin" && (it.attribution === "WindOnly" || it.attribution === "Common")).reduce((acc, it) => acc + (computeItemCr(it, solarDcMwp, windMw, bessMwh, 0, effectiveLandAcres) ?? 0), 0) : 0; const epcMarginSolarBOS = solarItems.filter(it => it.category === "EPCMargin" && (it.attribution === "SolarOnly" || it.attribution === "Common")).reduce((acc, it) => acc + (computeItemCr(it, solarDcMwp, windMw, bessMwh, 0, effectiveLandAcres) ?? 0), 0); // Financing const idcRate = gv(inputs, "capex", "interest_rate_annual", 0.09); const constrMonths = gv(inputs, "capex", "construction_months", 24); const debtFrac = gv(inputs, "capex", "debt_fraction", 0.75); const upfrontPct = gv(inputs, "capex", "upfront_fee_pct", 0.01); const bankGuaranteePct = gv(inputs, "capex", "bank_guarantee_pct", 0.001); // DSRA (Debt Service Reserve Account) - typically 1-2 months of debt service const dsraMonths = gv(inputs, "capex", "dsra_months", 2); // First compute base hard cost without financing items const baseHardCost = solarModule + solarBOS + windWTG + solarLandCost + windLandCost + bessAll + solarEPC + windEPC + epcMarginWTG + epcMarginSolarBOS + solarCont + windCont; // Financing costs are based on base hard cost const idcCost = baseHardCost * debtFrac * idcRate * constrMonths / 12; const upfrontCost = baseHardCost * debtFrac * upfrontPct; const bankGuaranteeCost = baseHardCost * bankGuaranteePct; // DSRA: 2 months of debt service reserve (based on base hard cost) const annualDebtService = baseHardCost * debtFrac * 0.12; // approximate annual debt service at 12% rate const dsraCost = annualDebtService * dsraMonths / 12; // Compute Total Hard Cost (sum of all component costs before financing) const totalHardCost = baseHardCost; return (

Project Cost Breakdown

{windEnabled && } {windEnabled && } {bessEnabled && } {windEnabled && } {windEnabled && }
Solar Module{solarModule.toFixed(2)}
Solar BoS{solarBOS.toFixed(2)}
Wind WTG{windWTG.toFixed(2)}
Solar Land Cost{solarLandCost.toFixed(2)}
Wind Land Cost{windLandCost.toFixed(2)}
Storage{bessAll.toFixed(2)}
DSRA{dsraCost.toFixed(2)}
Solar EPC Overheads{solarEPC.toFixed(2)}
Wind EPC Overheads{windEPC.toFixed(2)}
EPC Margin - WTG Tower+BOP{epcMarginWTG.toFixed(2)}
EPC Margin - Solar BOS{epcMarginSolarBOS.toFixed(2)}
Contingency - Solar{solarCont.toFixed(2)}
Contingency - Wind (in WTG cost){windCont.toFixed(2)}
Total Hard Cost {totalHardCost.toFixed(2)}
Upfront Financing Fee{upfrontCost.toFixed(2)}
Interest During Construction{idcCost.toFixed(2)}
Bank Guarantee Cost{bankGuaranteeCost.toFixed(2)}
Total Project Cost {(totalHardCost + upfrontCost + idcCost + bankGuaranteeCost).toFixed(2)}
); })()}
{/* ── Solar Generation ─────────────────────────────────── */} setSection( "solar", on ? { location_id: (project.state as string) ?? "RJ", capacity_ac_mw: 100, dc_ac_ratio: 1.4, capacity_dc_mwp: 140, availability_fraction: 0.995, degradation_y1: 0.007, degradation_annual: 0.005, stabilization_days: 60, stabilization_energy_loss_frac: 0.20, stabilization_dsm_addon_pct: 0.005, } : null, ) } >
upd("solar", "module_type", v)} options={[ { value: "fixed", label: "Fixed" }, { value: "tracker", label: "Tracker" }, ]} /> {}} readOnly suffix="MWp" /> upd("solar", "availability_fraction", v / 100)} step={0.1} suffix="%" /> upd("solar", "degradation_y1", v / 100)} step={0.1} suffix="%" /> upd("solar", "degradation_annual", v / 100)} step={0.05} suffix="%/yr" />

Stabilisation Period

upd("solar", "stabilization_days", Math.round(v))} step={1} min={0} suffix="days" /> upd("solar", "stabilization_energy_loss_frac", v / 100)} step={1} suffix="%" /> upd("solar", "stabilization_dsm_addon_pct", v / 100)} step={0.1} suffix="%" />
{/* ── Land & Common ────────────────────────────────────── */} upd("project", "land_acres", v)} step={10} min={0} suffix="acres" /> upd("project", "land_lease_rate", v)} step={0.05} min={0} suffix="L/acre" /> upd("project", "land_lease_years", v)} step={1} min={1} max={30} suffix="yr" />
Upfront Lease Cost (included in Total Project Cost): {upfrontLeaseCostCr.toFixed(2)} Cr
{/* ── Solar Capex ──────────────────────────────────────── */} {solarEnabled && ( )} {/* ── Wind Generation ──────────────────────────────────── */} setSection( "wind", on ? { location_id: (project.state as string) ?? "RJ", capacity_mw: 50, hub_height_m: 140, availability_fraction: 0.97, wake_loss_fraction: 0.05, stabilization_days: 60, stabilization_energy_loss_frac: 0.15, stabilization_dsm_addon_pct: 0.005, } : null, ) } > {/* Row 1: Location + Capacity */} upd("wind", "location_id", v)} options={LOCATIONS} /> { upd("wind", "capacity_mw", v); setInputs((prev) => sv(prev, "project", "capacity_wind_mw", v)); }} step={5} min={1} suffix="MW" /> upd("wind", "hub_height_m", v)} step={10} min={80} suffix="m" /> {/* Row 2: Performance */} upd("wind", "availability_fraction", v / 100)} step={0.1} suffix="%" /> upd("wind", "wake_loss_fraction", v / 100)} step={0.5} suffix="%" />
{/* Row 3: Stabilization */}

Stabilisation Period

upd("wind", "stabilization_days", Math.round(v))} step={1} min={0} suffix="days" /> upd("wind", "stabilization_energy_loss_frac", v / 100)} step={1} suffix="%" /> upd("wind", "stabilization_dsm_addon_pct", v / 100)} step={0.1} suffix="%" />
{/* ── Wind Capex ───────────────────────────────────────── */} {windEnabled && ( )} {/* ── BESS ─────────────────────────────────────────────── */} setSection( "bess", on ? { capacity_mwh: gv(inputs, "project", "capacity_bess_mwh", 200), power_mw: gv(inputs, "project", "capacity_bess_mw", 50), rte: 0.85, dod: 0.85, } : null, ) } > {/* Row 1: Capacity trio */} { upd("bess", "capacity_mwh", v); setInputs((prev) => sv(prev, "project", "capacity_bess_mwh", v)); }} step={10} min={10} suffix="MWh" /> { upd("bess", "power_mw", v); setInputs((prev) => sv(prev, "project", "capacity_bess_mw", v)); }} step={5} min={5} suffix="MW" />
{/* Row 2: Performance */} upd("bess", "rte", v / 100)} step={1} suffix="%" /> upd("bess", "dod", v / 100)} step={1} suffix="%" /> {/* ── BESS Capex ───────────────────────────────────────── */} {bessEnabled && ( )} {/* ── RTC Dispatch ─────────────────────────────────────── */} setSection("rtc", on ? { rtc_mw: 50, mcp_enabled: false, initial_soc_frac: 0.5 } : null) } > upd("rtc", "rtc_mw", v)} step={5} min={0} suffix="MW" />
{/* ── Commercial ───────────────────────────────────────── */} {/* Row 1: Tariff */} upd("commercial", "tariff_inr_per_kwh", v)} step={0.05} min={1} suffix="β‚Ή/kWh" />
{/* Row 2: Losses */} upd("commercial", "aux_consumption_pct", v / 100)} step={0.1} suffix="%" /> upd("commercial", "transmission_loss_pct", v / 100)} step={0.1} suffix="%" /> upd("commercial", "dsm_loss_pct", v / 100)} step={0.1} suffix="%" /> {/* Row 3: Working Capital */} upd("commercial", "bad_debt_pct", v / 100)} step={0.1} suffix="%" /> upd("commercial", "receivable_days", v)} step={1} suffix="days" />
{/* ── Operating Expenditure ────────────────────────────── */} {/* Solar O&M */} {solarEnabled && ( <>

Solar O&M

upd("opex", "om_solar_cr_per_mw", v / 100)} step={0.1} suffix="L/MWp" /> upd("opex", "om_solar_escalation_pct", v / 100)} step={0.5} suffix="%" /> upd("opex", "om_solar_escalation_after_year", Math.round(v))} step={1} min={1} max={25} /> )} {/* Wind O&M */} {windEnabled && ( <>

Wind O&M

upd("opex", "om_wind_cr_per_mw", v / 100)} step={0.5} suffix="L/MW" /> upd("opex", "om_wind_escalation_pct", v / 100)} step={0.5} suffix="%" /> upd("opex", "om_wind_escalation_after_year", Math.round(v))} step={1} min={1} max={25} /> )} {/* BESS O&M */} {bessEnabled && ( <>

BESS O&M

upd("opex", "om_bess_pct_of_capex", v / 100)} step={0.1} suffix="%" />
)} {/* Common */}

Common

upd("opex", "insurance_pct_of_capex", v / 100)} step={0.05} suffix="%" /> upd("opex", "am_fee_pct_of_revenue", v / 100)} step={0.1} suffix="%" /> upd("opex", "misc_cr", v)} step={0.1} suffix="Cr" /> {/* ── Construction & Financing ─────────────────────────── */} {/* Row 1 */} upd("capex", "construction_months", Math.round(v))} step={1} min={6} max={60} suffix="months" /> upd("capex", "interest_rate_annual", v / 100)} step={0.1} suffix="%" />
{/* Row 2 */} upd("capex", "debt_fraction", v / 100)} step={1} suffix="%" /> upd("capex", "upfront_fee_pct", v / 100)} step={0.1} suffix="%" /> {/* ── Debt Financing ───────────────────────────────────── */} {/* Row 1 */} upd("debt", "interest_rate_annual", v / 100)} step={0.25} suffix="%" /> upd("debt", "tenor_years", Math.round(v))} step={1} min={5} max={25} suffix="years" /> upd("debt", "moratorium_years", Math.round(v))} step={1} min={0} suffix="years" /> {/* Row 2 */} upd("debt", "de_ratio", v)} step={0.25} min={0.5} /> upd("debt", "min_dscr", v)} step={0.05} min={1.0} /> upd("debt", "avg_dscr", v)} step={0.05} min={1.0} />
upd("debt", "schedule_shape", v)} options={DEBT_SHAPES} />
{/* ── Tax ──────────────────────────────────────────────── */} upd("tax", "rate", v / 100)} step={0.1} suffix="%" /> upd("tax", "wdv_plant_rate", v / 100)} step={1} suffix="%" /> upd("tax", "wdv_bess_rate", v / 100)} step={1} suffix="%" /> upd("tax", "wdv_building_rate", v / 100)} step={1} suffix="%" /> {/* ── Solver ───────────────────────────────────────────── */}
upd("solver", "mode", v)} options={[ { value: "solve_tariff", label: "Solve tariff for target Equity IRR" }, { value: "fixed_tariff", label: "Fixed tariff (compute IRR)" }, ]} />
{gv(inputs, "solver", "mode", "solve_tariff") === "solve_tariff" ? ( upd("solver", "target_equity_irr", v / 100)} step={0.5} min={5} max={40} suffix="%" /> ) : ( upd("solver", "fixed_tariff", v)} step={0.05} min={1} suffix="β‚Ή/kWh" /> )}
{error && (

{error}

)}
); }