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 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 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 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 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 solarEPC = solarItems.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 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
|
// Financing
|
||||||
const idcRate = gv(inputs, "capex", "interest_rate_annual", 0.09);
|
const idcRate = gv(inputs, "capex", "interest_rate_annual", 0.09);
|
||||||
const constrMonths = gv(inputs, "capex", "construction_months", 24);
|
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 idcCost = totalCostCr * debtFrac * idcRate * constrMonths / 12;
|
||||||
const upfrontPct = gv(inputs, "capex", "upfront_fee_pct", 0.01);
|
const upfrontPct = gv(inputs, "capex", "upfront_fee_pct", 0.01);
|
||||||
const upfrontCost = totalCostCr * debtFrac * upfrontPct;
|
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 (
|
return (
|
||||||
<div className="border border-border rounded-lg overflow-hidden">
|
<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>}
|
{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>}
|
{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>}
|
{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>}
|
{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>}
|
||||||
{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>}
|
{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>}
|
||||||
{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>}
|
{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>}
|
||||||
{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>}
|
{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">
|
<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 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>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue