feat: Expand Project Cost Breakdown to show all cost items
- Split Solar Land and Wind Land into separate lines - Add DSRA (Debt Service Reserve Account) - Split EPC overheads into Solar and Wind - Add EPC Margin categories (WTG Tower+BOP, Solar BOS) - Split Contingency into Solar and Wind - Add Total Hard Cost subtotal - Add financing costs (upfront fee, IDC, bank guarantee)
This commit is contained in:
parent
581eafbf36
commit
dfa5ae36d8
1 changed files with 42 additions and 8 deletions
|
|
@ -791,10 +791,16 @@ export function InputsTab({ scenarioId, inputsJson, onSaved }: Props) {
|
|||
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);
|
||||
const allLand = [...solarItems, ...windItems].filter(it => it.id.includes("land")).reduce((acc, it) => acc + (computeItemCr(it, solarDcMwp, windMw, bessMwh, 0, effectiveLandAcres) ?? 0), 0);
|
||||
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);
|
||||
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 epcAll = [...solarItems, ...windItems, ...bessItems].filter(it => it.category === "EPCOverhead").reduce((acc, it) => acc + (computeItemCr(it, solarDcMwp, windMw, bessMwh, 0, effectiveLandAcres) ?? 0), 0);
|
||||
const contAll = [...solarItems, ...windItems].filter(it => it.category === "Contingency").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
|
||||
const epcMarginWTG = [...windItems, ...bessItems].filter(it => it.category === "EPCMargin" && it.attribution === "WindOnly").reduce((acc, it) => acc + (computeItemCr(it, solarDcMwp, windMw, bessMwh, 0, effectiveLandAcres) ?? 0), 0);
|
||||
const epcMarginSolarBOS = solarItems.filter(it => it.category === "EPCMargin" && it.attribution === "SolarOnly").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);
|
||||
|
|
@ -802,6 +808,15 @@ export function InputsTab({ scenarioId, inputsJson, onSaved }: Props) {
|
|||
const idcCost = totalCostCr * debtFrac * idcRate * constrMonths / 12;
|
||||
const upfrontPct = gv(inputs, "capex", "upfront_fee_pct", 0.01);
|
||||
const upfrontCost = totalCostCr * debtFrac * upfrontPct;
|
||||
const bankGuaranteePct = gv(inputs, "capex", "bank_guarantee_pct", 0.001);
|
||||
const bankGuaranteeCost = totalCostCr * bankGuaranteePct;
|
||||
// DSRA (Debt Service Reserve Account) - typically 1-2 months of debt service
|
||||
const dsraMonths = gv(inputs, "capex", "dsra_months", 2);
|
||||
const annualDebtService = totalCostCr * debtFrac * 0.12; // approximate annual debt service
|
||||
const dsraCost = annualDebtService * dsraMonths / 12;
|
||||
|
||||
// Compute Total Hard Cost (sum of all component costs before financing)
|
||||
const totalHardCost = solarModule + solarBOS + windWTG + solarLandCost + windLandCost + bessAll + dsraCost + solarEPC + windEPC + epcMarginWTG + epcMarginSolarBOS + solarCont + windCont;
|
||||
|
||||
return (
|
||||
<div className="border border-border rounded-lg overflow-hidden">
|
||||
|
|
@ -813,13 +828,32 @@ export function InputsTab({ scenarioId, inputsJson, onSaved }: Props) {
|
|||
{solarModule > 0 && <tr><td className="px-3 py-1.5">Solar Module</td><td className="px-3 py-1.5 text-right tabular-nums">{solarModule.toFixed(2)}</td></tr>}
|
||||
{solarBOS > 0 && <tr><td className="px-3 py-1.5">Solar BoS</td><td className="px-3 py-1.5 text-right tabular-nums">{solarBOS.toFixed(2)}</td></tr>}
|
||||
{windEnabled && windWTG > 0 && <tr><td className="px-3 py-1.5">Wind WTG</td><td className="px-3 py-1.5 text-right tabular-nums">{windWTG.toFixed(2)}</td></tr>}
|
||||
{allLand > 0 && <tr><td className="px-3 py-1.5">Land Cost</td><td className="px-3 py-1.5 text-right tabular-nums">{allLand.toFixed(2)}</td></tr>}
|
||||
{bessEnabled && bessAll > 0 && <tr><td className="px-3 py-1.5">Storage (BESS)</td><td className="px-3 py-1.5 text-right tabular-nums">{bessAll.toFixed(2)}</td></tr>}
|
||||
{epcAll > 0 && <tr><td className="px-3 py-1.5">EPC Overheads</td><td className="px-3 py-1.5 text-right tabular-nums">{epcAll.toFixed(2)}</td></tr>}
|
||||
{contAll > 0 && <tr><td className="px-3 py-1.5">Contingency</td><td className="px-3 py-1.5 text-right tabular-nums">{contAll.toFixed(2)}</td></tr>}
|
||||
{solarLandCost > 0 && <tr><td className="px-3 py-1.5">Solar Land Cost</td><td className="px-3 py-1.5 text-right tabular-nums">{solarLandCost.toFixed(2)}</td></tr>}
|
||||
{windEnabled && windLandCost > 0 && <tr><td className="px-3 py-1.5">Wind Land Cost</td><td className="px-3 py-1.5 text-right tabular-nums">{windLandCost.toFixed(2)}</td></tr>}
|
||||
{bessEnabled && bessAll > 0 && <tr><td className="px-3 py-1.5">Storage</td><td className="px-3 py-1.5 text-right tabular-nums">{bessAll.toFixed(2)}</td></tr>}
|
||||
{dsraCost > 0 && <tr><td className="px-3 py-1.5">DSRA</td><td className="px-3 py-1.5 text-right tabular-nums">{dsraCost.toFixed(2)}</td></tr>}
|
||||
{solarEPC > 0 && <tr><td className="px-3 py-1.5">Solar EPC Overheads</td><td className="px-3 py-1.5 text-right tabular-nums">{solarEPC.toFixed(2)}</td></tr>}
|
||||
{windEnabled && windEPC > 0 && <tr><td className="px-3 py-1.5">Wind EPC Overheads</td><td className="px-3 py-1.5 text-right tabular-nums">{windEPC.toFixed(2)}</td></tr>}
|
||||
{epcMarginWTG > 0 && <tr><td className="px-3 py-1.5">EPC Margin - WTG Tower+BOP</td><td className="px-3 py-1.5 text-right tabular-nums">{epcMarginWTG.toFixed(2)}</td></tr>}
|
||||
{epcMarginSolarBOS > 0 && <tr><td className="px-3 py-1.5">EPC Margin - Solar BOS</td><td className="px-3 py-1.5 text-right tabular-nums">{epcMarginSolarBOS.toFixed(2)}</td></tr>}
|
||||
{solarCont > 0 && <tr><td className="px-3 py-1.5">Contingency - Solar</td><td className="px-3 py-1.5 text-right tabular-nums">{solarCont.toFixed(2)}</td></tr>}
|
||||
{windEnabled && windCont > 0 && <tr><td className="px-3 py-1.5">Contingency - Wind (in WTG cost)</td><td className="px-3 py-1.5 text-right tabular-nums">{windCont.toFixed(2)}</td></tr>}
|
||||
<tr className="border-t-2 border-border bg-muted/30">
|
||||
<td className="px-3 py-2 font-semibold">Total Hard Cost</td>
|
||||
<td className="px-3 py-2 text-right tabular-nums font-semibold">{totalHardCost.toFixed(2)}</td>
|
||||
</tr>
|
||||
<tr className="border-t border-dashed border-border/50">
|
||||
<td className="px-3 py-1.5 text-muted-foreground">Upfront Financing Fee</td><td className="px-3 py-1.5 text-right tabular-nums text-muted-foreground">{upfrontCost.toFixed(2)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="px-3 py-1.5 text-muted-foreground">Interest During Construction</td><td className="px-3 py-1.5 text-right tabular-nums text-muted-foreground">{idcCost.toFixed(2)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="px-3 py-1.5 text-muted-foreground">Bank Guarantee Cost</td><td className="px-3 py-1.5 text-right tabular-nums text-muted-foreground">{bankGuaranteeCost.toFixed(2)}</td>
|
||||
</tr>
|
||||
<tr className="border-t-2 border-border bg-muted/30">
|
||||
<td className="px-3 py-2 font-semibold">Total Project Cost</td>
|
||||
<td className="px-3 py-2 text-right tabular-nums font-bold text-primary">{(totalCostCr + idcCost + upfrontCost).toFixed(2)}</td>
|
||||
<td className="px-3 py-2 text-right tabular-nums font-bold text-primary">{(totalHardCost + upfrontCost + idcCost + bankGuaranteeCost).toFixed(2)}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue