src/ApplicationBundle/Modules/HoneybeeWeb/Resources/views/inc/_hb360_report_body.html.twig line 1

Open in your IDE?
  1. {# ═══════════════════════════════════════════════════════════════════════════
  2.    HB360 report body — server-rendered saved estimate + (optional) Package B
  3.    report. Used by the account page (/tools/my-estimate) and the public
  4.    share view. Context: estimate (array), report (array|null), project.
  5.    Parent page must include _hb360_tags.html.twig once (pills CSS/JS).
  6.    ═══════════════════════════════════════════════════════════════════════════ #}
  7. {% import '@HoneybeeWeb/inc/_hb360_tags.html.twig' as hb360 %}
  8. <style>
  9. .hbr-headline { background: var(--n-dark); border-radius: var(--n-radius-sm); padding: 22px 20px; text-align: center; margin-bottom: 14px; }
  10. .hbr-headline .v { font-family: 'Montserrat', sans-serif; font-weight: 900; font-size: 2.2rem; color: #fff; line-height: 1.05; }
  11. .hbr-headline .l { font-size: 12px; text-transform: uppercase; letter-spacing: .08em; color: rgba(255,255,255,.6); margin-top: 6px; }
  12. .hbr-kpis { display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; margin: 4px 0 14px; }
  13. @media (min-width: 720px) { .hbr-kpis { grid-template-columns: repeat(4, 1fr); } }
  14. .hbr-kpi { background: var(--n-cream); border: 1px solid var(--n-border); border-radius: var(--n-radius-sm); padding: 14px 16px; }
  15. .hbr-kpi .v { font-family: 'Montserrat', sans-serif; font-weight: 900; font-size: 1.3rem; color: var(--n-dark); line-height: 1.1; }
  16. .hbr-kpi .l { font-size: 12px; text-transform: uppercase; letter-spacing: .06em; color: var(--n-muted); margin: 4px 0 6px; }
  17. .hbr-sec { margin: 26px 0 10px; }
  18. .hbr-sec h3 { font-family: 'Montserrat', sans-serif; font-weight: 800; font-size: 1.05rem; color: var(--n-dark); margin: 0 0 2px; }
  19. .hbr-sec .sub { font-size: 12px; color: var(--n-muted); }
  20. .hbr-table { width: 100%; border-collapse: collapse; font-size: 13px; margin-top: 8px; }
  21. .hbr-table th { text-align: left; font-size: 11.5px; text-transform: uppercase; letter-spacing: .05em; color: var(--n-muted); padding: 7px 8px; border-bottom: 1.5px solid var(--n-border-md); }
  22. .hbr-table td { padding: 8px; border-bottom: 1px solid var(--n-border); color: var(--n-dark); }
  23. .hbr-table td.num, .hbr-table th.num { text-align: right; font-variant-numeric: tabular-nums; }
  24. .hbr-table tfoot td { font-weight: 800; border-top: 1.5px solid var(--n-border-md); border-bottom: none; }
  25. .hbr-bar { background: var(--n-amber-dim); height: 10px; border-radius: 5px; position: relative; overflow: hidden; min-width: 60px; }
  26. .hbr-bar span { position: absolute; inset: 0 auto 0 0; background: var(--n-amber); border-radius: 5px; }
  27. .hbr-verdict { border-left: 3px solid var(--n-amber); background: var(--n-cream); border-radius: var(--n-radius-sm); padding: 12px 14px; font-size: 13px; color: var(--n-dark); line-height: 1.6; margin-top: 8px; }
  28. .hbr-note { font-size: 12px; color: var(--n-muted); line-height: 1.6; margin-top: 16px; padding: 12px 14px; background: var(--n-cream); border-radius: var(--n-radius-sm); border-left: 3px solid var(--n-amber); }
  29. </style>
  30. {% set fig = estimate.figures|default({}) %}
  31. {# ── The saved estimate (Package A recap) ── #}
  32. <div class="hbr-headline">
  33.     <div class="v">€{{ estimate.annual_savings|number_format(0, ',', '.') }}</div>
  34.     <div class="l">estimated bill saving / year {{ hb360.tag('assumed', fig.annual_savings.basis|default('')) }}</div>
  35. </div>
  36. <div class="hbr-kpis">
  37.     <div class="hbr-kpi"><div class="v">{{ estimate.kwp|number_format(estimate.kwp < 100 ? 1 : 0, ',', '.') }}</div><div class="l">System size (kWp)</div>{{ hb360.tag(fig.system_kwp.tag|default('assumed'), fig.system_kwp.basis|default('')) }}</div>
  38.     <div class="hbr-kpi"><div class="v">{{ estimate.annual_gen_kwh|number_format(0, ',', '.') }}</div><div class="l">Annual yield (kWh)</div>{{ hb360.tag(fig.annual_yield.tag|default('assumed'), fig.annual_yield.basis|default('')) }}</div>
  39.     <div class="hbr-kpi"><div class="v">€{{ estimate.capex_eur|number_format(0, ',', '.') }}</div><div class="l">Indicative CAPEX</div>{{ hb360.tag(fig.capex.tag|default('assumed'), fig.capex.basis|default('')) }}</div>
  40.     <div class="hbr-kpi"><div class="v">{{ estimate.payback_years ? estimate.payback_years ~ ' yrs' : '—' }}</div><div class="l">Simple payback</div>{{ hb360.tag(fig.payback.tag|default('assumed'), fig.payback.basis|default('')) }}</div>
  41. </div>
  42. <div style="font-size:12.5px;color:var(--n-muted)">
  43.     {{ estimate.panels|number_format(0, ',', '.') }} modules · roof {{ estimate.area_m2|number_format(0, ',', '.') }} m²
  44.     {{ hb360.tag(fig.roof_area.tag|default('assumed'), fig.roof_area.basis|default('')) }} ·
  45.     sources: {{ estimate.yield_source|default('—') }}{{ estimate.roof_source is defined and estimate.roof_source ? ' · roof via ' ~ estimate.roof_source : '' }} ·
  46.     site works {{ hb360.tag('needs site survey', fig.structure.basis|default('')) }}
  47. </div>
  48. {% if report %}
  49.     {# ── Package B: full feasibility ── #}
  50.     {% set rfig = report.figures|default({}) %}
  51.     <div class="hbr-sec">
  52.         <h3>System design {{ hb360.tag(rfig.design.tag|default('assumed'), rfig.design.basis|default('')) }}</h3>
  53.         <table class="hbr-table">
  54.             <tr><td>Array</td><td class="num">{{ report.design.panels|number_format(0, ',', '.') }} × {{ report.design.module_wp }} Wp = {{ report.design.kwp|number_format(report.design.kwp < 100 ? 1 : 0, ',', '.') }} kWp</td></tr>
  55.             <tr><td>Inverter capacity (AC)</td><td class="num">{{ report.design.inverter_ac_kw|number_format(1, ',', '.') }} kW (DC/AC {{ report.design.dc_ac_ratio }})</td></tr>
  56.             <tr><td>Array area</td><td class="num">{{ report.design.array_area_m2|number_format(0, ',', '.') }} m²</td></tr>
  57.             <tr><td>Specific yield</td><td class="num">{{ report.design.specific_yield|number_format(0, ',', '.') }} kWh/kWp/yr</td></tr>
  58.         </table>
  59.     </div>
  60.     {% if report.equipment is defined and report.equipment.inverters.ok|default(false) %}
  61.     {% set eq = report.equipment %}
  62.     <div class="hbr-sec">
  63.         <h3>Equipment configuration {{ hb360.tag(rfig.equipment.tag|default('assumed'), rfig.equipment.basis|default('')) }}</h3>
  64.         <div class="sub">How to build {{ eq.inverters.target_ac_kw }} kW AC — capacity × count options, preferred brands first. {{ eq.inverters.price_note }}</div>
  65.         <div style="overflow-x:auto">
  66.             <table class="hbr-table">
  67.                 <thead><tr><th></th><th>Configuration</th><th class="num">DC/AC</th><th class="num">MPPTs</th><th class="num">€/kW</th><th class="num">Total €</th><th>Trade-off</th></tr></thead>
  68.                 <tbody>
  69.                 {% for c in eq.inverters.configs|slice(0, 6) %}
  70.                     <tr{% if c.key == eq.chosen %} style="background:var(--n-amber-dim)"{% endif %}>
  71.                         <td style="white-space:nowrap">{% if c.key == eq.chosen %}✔ chosen{% elseif not c.valid %}✕{% endif %}</td>
  72.                         <td>{{ c.brand }} {{ c.model }} <b>{{ c.unit_ac_kw }} kW × {{ c.count }}</b>{{ c.preferred ? ' ★' : '' }}
  73.                             {% if not c.valid %}<br><span style="color:#b4452f;font-size:12px">{{ c.issues|join(' · ') }}</span>{% endif %}
  74.                             {% if c.warnings %}<br><span style="color:var(--n-muted);font-size:12px">{{ c.warnings|join(' · ') }}</span>{% endif %}</td>
  75.                         <td class="num">{{ c.dc_ac_ratio }}</td>
  76.                         <td class="num">{{ c.mppt_total ?: '—' }}</td>
  77.                         <td class="num">{{ c.eur_per_kw|number_format(0, ',', '.') }}</td>
  78.                         <td class="num">{{ c.total_price_eur|number_format(0, ',', '.') }}</td>
  79.                         <td style="font-size:12px;color:var(--n-muted)">{{ c.tradeoff }}</td>
  80.                     </tr>
  81.                 {% endfor %}
  82.                 </tbody>
  83.             </table>
  84.         </div>
  85.         {% if eq.modules.ok|default(false) %}
  86.         <table class="hbr-table" style="margin-top:10px">
  87.             <thead><tr><th>Module option</th><th class="num">Count</th><th class="num">€/Wp</th><th class="num">Total €</th></tr></thead>
  88.             <tbody>
  89.             {% for m in eq.modules.options %}
  90.                 <tr><td>{{ m.brand }} {{ m.model }} {{ m.wp }} Wp{{ m.preferred ? ' ★' : '' }}</td>
  91.                     <td class="num">{{ m.count|number_format(0, ',', '.') }}</td>
  92.                     <td class="num">{{ m.eur_per_wp|number_format(3, ',', '.') }}</td>
  93.                     <td class="num">{{ m.total_price_eur|number_format(0, ',', '.') }}</td></tr>
  94.             {% endfor %}
  95.             </tbody>
  96.         </table>
  97.         {% endif %}
  98.         <div class="sub" style="font-size:12px;color:var(--n-muted);margin-top:6px">★ preferred brand · {{ eq.inverters.caveat }}</div>
  99.     </div>
  100.     {% endif %}
  101.     <div class="hbr-sec">
  102.         <h3>Month-by-month generation {{ hb360.tag(rfig.monthly.tag|default('assumed'), rfig.monthly.basis|default('')) }}</h3>
  103.         {% set maxGen = (report.monthly|map(m => m.gen_kwh)|sort)|last %}
  104.         <table class="hbr-table">
  105.             <thead><tr><th>Month</th><th style="width:40%"></th><th class="num">Generation</th><th class="num">Used on site</th><th class="num">Export</th></tr></thead>
  106.             <tbody>
  107.             {% for m in report.monthly %}
  108.                 <tr>
  109.                     <td>{{ m.month }}</td>
  110.                     <td><div class="hbr-bar"><span style="width:{{ maxGen > 0 ? (100 * m.gen_kwh / maxGen)|round : 0 }}%"></span></div></td>
  111.                     <td class="num">{{ m.gen_kwh|number_format(0, ',', '.') }}</td>
  112.                     <td class="num">{{ m.offset_kwh|number_format(0, ',', '.') }}</td>
  113.                     <td class="num">{{ m.export_kwh|number_format(0, ',', '.') }}</td>
  114.                 </tr>
  115.             {% endfor %}
  116.             </tbody>
  117.         </table>
  118.     </div>
  119.     <div class="hbr-sec">
  120.         <h3>Load match {{ hb360.tag(rfig.load_match.tag|default('assumed'), rfig.load_match.basis|default('')) }}</h3>
  121.         <table class="hbr-table">
  122.             <tr><td>Your annual consumption</td><td class="num">{{ report.load_match.annual_consumption_kwh|number_format(0, ',', '.') }} kWh</td></tr>
  123.             <tr><td>Solar used on site</td><td class="num">{{ report.load_match.offset_kwh|number_format(0, ',', '.') }} kWh ({{ report.load_match.self_consumption_pct }}% of generation)</td></tr>
  124.             {% if report.load_match.bill_coverage_pct is not null %}
  125.             <tr><td>Bill coverage</td><td class="num">{{ report.load_match.bill_coverage_pct }}%</td></tr>
  126.             {% endif %}
  127.             <tr><td>Surplus to grid {{ hb360.tag('needs utility approval', rfig.export_revenue.basis|default('')) }}</td><td class="num">{{ report.load_match.export_kwh|number_format(0, ',', '.') }} kWh</td></tr>
  128.             <tr><td>Still imported from grid</td><td class="num">{{ report.load_match.import_kwh|number_format(0, ',', '.') }} kWh</td></tr>
  129.         </table>
  130.     </div>
  131.     <div class="hbr-sec">
  132.         <h3>Battery (BESS) indication {{ hb360.tag(rfig.bess.tag|default('needs site survey'), rfig.bess.basis|default('')) }}</h3>
  133.         <div class="hbr-verdict">
  134.             {% if report.bess.recommended is same as(true) %}<b>🔋 Worth modelling.</b>{% elseif report.bess.recommended is same as(false) %}<b>Not indicated yet.</b>{% else %}<b>Needs your usage.</b>{% endif %}
  135.             {{ report.bess.verdict }}
  136.         </div>
  137.     </div>
  138.     <div class="hbr-sec">
  139.         <h3>25-year economics {{ hb360.tag(rfig.roi.tag|default('assumed'), rfig.roi.basis|default('')) }}</h3>
  140.         <div class="hbr-kpis" style="margin-top:8px">
  141.             <div class="hbr-kpi"><div class="v">{{ report.roi.payback_years ? report.roi.payback_years ~ ' yrs' : '> 25 yrs' }}</div><div class="l">Payback</div></div>
  142.             <div class="hbr-kpi"><div class="v">€{{ report.roi.net_benefit_25yr|number_format(0, ',', '.') }}</div><div class="l">25-yr net benefit</div></div>
  143.             <div class="hbr-kpi"><div class="v">€{{ report.roi.npv|number_format(0, ',', '.') }}</div><div class="l">NPV @ {{ report.roi.assumptions.discount_rate_pct }}%</div></div>
  144.             <div class="hbr-kpi"><div class="v">{{ report.load_match.self_consumption_pct }}%</div><div class="l">Self-consumption</div></div>
  145.         </div>
  146.         <table class="hbr-table">
  147.             <thead><tr><th>Year</th><th class="num">Saving</th><th class="num">O&amp;M</th><th class="num">Cumulative</th></tr></thead>
  148.             <tbody>
  149.             {% for y in report.roi.years %}
  150.                 {% if y.year in [1, 5, 10, 15, 20, 25] %}
  151.                 <tr><td>{{ y.year }}</td><td class="num">€{{ y.saving|number_format(0, ',', '.') }}</td><td class="num">€{{ y.om|number_format(0, ',', '.') }}</td><td class="num">€{{ y.cumulative|number_format(0, ',', '.') }}</td></tr>
  152.                 {% endif %}
  153.             {% endfor %}
  154.             </tbody>
  155.         </table>
  156.         <div class="sub" style="font-size:12px;color:var(--n-muted);margin-top:6px">
  157.             Assumes {{ report.roi.assumptions.degradation_pct_yr }}%/yr degradation, {{ report.roi.assumptions.tariff_escalation_pct_yr }}%/yr tariff escalation,
  158.             O&amp;M {{ report.roi.assumptions.om_pct_capex_yr }}% of CAPEX/yr. Savings from self-consumed energy only — export revenue excluded
  159.             {{ hb360.tag('needs utility approval', rfig.export_revenue.basis|default('')) }}.
  160.         </div>
  161.     </div>
  162.     <div class="hbr-sec">
  163.         <h3>Bill of quantities {{ hb360.tag(rfig.boq.tag|default('needs site survey'), rfig.boq.basis|default('')) }}</h3>
  164.         <table class="hbr-table">
  165.             <thead><tr><th>Item</th><th class="num">Qty</th><th>Unit</th><th>Note</th></tr></thead>
  166.             <tbody>
  167.             {% for b in report.boq %}
  168.                 <tr><td>{{ b.item }}</td><td class="num">{{ b.qty|number_format(0, ',', '.') }}</td><td>{{ b.unit }}</td><td style="color:var(--n-muted)">{{ b.note }}</td></tr>
  169.             {% endfor %}
  170.             </tbody>
  171.         </table>
  172.     </div>
  173.     <div class="hbr-sec">
  174.         <h3>Indicative cost split {{ hb360.tag(rfig.cost_split.tag|default('assumed'), rfig.cost_split.basis|default('')) }}</h3>
  175.         <table class="hbr-table">
  176.             <tbody>
  177.             {% for s in report.cost_split %}
  178.                 <tr><td>{{ s.block }}</td><td class="num">{{ s.share_pct }}%</td><td class="num">€{{ s.amount|number_format(0, ',', '.') }}</td></tr>
  179.             {% endfor %}
  180.             </tbody>
  181.             <tfoot><tr><td colspan="2">Indicative CAPEX (ex-VAT)</td><td class="num">€{{ report.capex_eur|number_format(0, ',', '.') }}</td></tr></tfoot>
  182.         </table>
  183.     </div>
  184.     {% if report.package_c is defined and report.package_c.ok|default(false) %}
  185.         {% set pc = report.package_c %}
  186.         {% set cfig = pc.figures|default({}) %}
  187.         <div class="hbr-sec" style="border-top:2px solid var(--n-border-md);padding-top:18px;margin-top:30px">
  188.             <h3>Package C — hybrid design (PV + battery + backup + EV)</h3>
  189.             <div class="sub">Battery absorbs the surplus first, EV charging uses what remains, the genset is capacity insurance, finance sees the whole package.</div>
  190.         </div>
  191.         <div class="hbr-sec">
  192.             <h3>Battery storage {{ hb360.tag(cfig.bess.tag|default('needs site survey'), cfig.bess.basis|default('')) }}</h3>
  193.             {% if pc.bess.ok|default(false) %}
  194.             <table class="hbr-table">
  195.                 <tr><td>Recommended size</td><td class="num">{{ pc.bess.nameplate_kwh|number_format(1, ',', '.') }} kWh / {{ pc.bess.power_kw|number_format(1, ',', '.') }} kW</td></tr>
  196.                 <tr><td>Energy shifted to evening use</td><td class="num">{{ pc.bess.shifted_kwh_yr|number_format(0, ',', '.') }} kWh/yr</td></tr>
  197.                 <tr><td>Self-consumption</td><td class="num">{{ pc.bess.self_consumption_before_pct }}% → {{ pc.bess.self_consumption_after_pct }}%</td></tr>
  198.                 <tr><td>Indicative cost / added saving</td><td class="num">€{{ pc.bess.capex_eur|number_format(0, ',', '.') }} / €{{ pc.bess.saving_eur_yr|number_format(0, ',', '.') }} per yr</td></tr>
  199.                 <tr><td>Battery payback</td><td class="num">{{ pc.bess.payback_years ? pc.bess.payback_years ~ ' yrs' : '—' }}</td></tr>
  200.             </table>
  201.             {% else %}
  202.             <div class="hbr-verdict">{{ pc.bess.reason|default('Storage not sized.') }}</div>
  203.             {% endif %}
  204.         </div>
  205.         {% if pc.evse.ok|default(false) %}
  206.         <div class="hbr-sec">
  207.             <h3>EV charging {{ hb360.tag(cfig.evse.tag|default('needs utility approval'), cfig.evse.basis|default('')) }}</h3>
  208.             <table class="hbr-table">
  209.                 <thead><tr><th>Charger</th><th class="num">Count</th><th class="num">Annual energy</th><th class="num">Indicative cost</th></tr></thead>
  210.                 <tbody>
  211.                 {% for c in pc.evse.chargers %}
  212.                     <tr><td>{{ c.type }}</td><td class="num">{{ c.count }}</td><td class="num">{{ c.annual_kwh|number_format(0, ',', '.') }} kWh</td><td class="num">€{{ c.capex_eur|number_format(0, ',', '.') }}</td></tr>
  213.                 {% endfor %}
  214.                 </tbody>
  215.             </table>
  216.             <table class="hbr-table">
  217.                 <tr><td>Added diversified peak</td><td class="num">{{ pc.evse.diversified_peak_kw|number_format(1, ',', '.') }} kW (diversity {{ pc.evse.diversity_factor }})</td></tr>
  218.                 <tr><td>Covered by PV surplus</td><td class="num">{{ pc.evse.pv_covered_kwh|number_format(0, ',', '.') }} kWh ({{ pc.evse.pv_coverage_pct }}%)</td></tr>
  219.                 <tr><td>Grid energy for charging</td><td class="num">{{ pc.evse.grid_kwh|number_format(0, ',', '.') }} kWh ≈ €{{ pc.evse.energy_cost_eur_yr|number_format(0, ',', '.') }}/yr</td></tr>
  220.             </table>
  221.             <div class="hbr-verdict">{{ pc.evse.grid_note }} {{ hb360.tag('needs utility approval') }}</div>
  222.         </div>
  223.         {% endif %}
  224.         {% if pc.genset.ok|default(false) %}
  225.         <div class="hbr-sec">
  226.             <h3>Backup genset {{ hb360.tag(cfig.genset.tag|default('needs site survey'), cfig.genset.basis|default('')) }}</h3>
  227.             <div class="hbr-verdict">{% if pc.genset.recommended %}<b>⚙ Recommended.</b>{% else %}<b>Optional.</b>{% endif %} {{ pc.genset.verdict }}</div>
  228.             <table class="hbr-table">
  229.                 <tr><td>Critical load / frame size</td><td class="num">{{ pc.genset.critical_kw|number_format(1, ',', '.') }} kW → {{ pc.genset.kva }} kVA</td></tr>
  230.                 <tr><td>Battery ride-through</td><td class="num">{{ pc.genset.bess_ride_through_hrs }} h</td></tr>
  231.                 <tr><td>Indicative cost</td><td class="num">€{{ pc.genset.capex_eur|number_format(0, ',', '.') }}</td></tr>
  232.                 <tr><td>Outage running cost</td><td class="num">€{{ pc.genset.running_cost_eur_yr|number_format(0, ',', '.') }}/yr ({{ pc.genset.fuel_l_yr|number_format(0, ',', '.') }} l diesel · €{{ pc.genset.diesel_cost_per_kwh }}/kWh vs grid €{{ pc.genset.grid_cost_per_kwh }})</td></tr>
  233.             </table>
  234.         </div>
  235.         {% endif %}
  236.         {% if pc.finance.ok|default(false) %}
  237.         <div class="hbr-sec">
  238.             <h3>Financing the package {{ hb360.tag(cfig.finance.tag|default('assumed'), cfig.finance.basis|default('')) }}</h3>
  239.             <div class="hbr-kpis" style="margin-top:8px">
  240.                 <div class="hbr-kpi"><div class="v">€{{ pc.combined.capex_eur|number_format(0, ',', '.') }}</div><div class="l">Package CAPEX</div></div>
  241.                 <div class="hbr-kpi"><div class="v">€{{ pc.combined.saving_eur_yr|number_format(0, ',', '.') }}</div><div class="l">Saving / yr</div></div>
  242.                 <div class="hbr-kpi"><div class="v">{{ pc.combined.self_sufficiency_pct is not null ? pc.combined.self_sufficiency_pct ~ '%' : '—' }}</div><div class="l">Self-sufficiency</div></div>
  243.                 <div class="hbr-kpi"><div class="v">{{ pc.finance.cash.irr_pct is not null ? pc.finance.cash.irr_pct ~ '%' : '—' }}</div><div class="l">IRR (cash)</div></div>
  244.             </div>
  245.             <table class="hbr-table">
  246.                 <tr><td>Cash purchase</td><td class="num">payback {{ pc.finance.cash.payback_years ? pc.finance.cash.payback_years ~ ' yrs' : '> ' ~ pc.finance.assumptions.years ~ ' yrs' }} · NPV €{{ pc.finance.cash.npv|number_format(0, ',', '.') }}</td></tr>
  247.                 <tr><td>{{ (pc.finance.assumptions.loan_share * 100)|round }}% loan @ {{ (pc.finance.assumptions.loan_rate * 100)|round(1) }}% / {{ pc.finance.assumptions.loan_years }} yrs</td>
  248.                     <td class="num">equity €{{ pc.finance.loan.equity|number_format(0, ',', '.') }} · instalment €{{ pc.finance.loan.annuity_per_year|number_format(0, ',', '.') }}/yr ·
  249.                     {%- if pc.finance.loan.self_financing_all_years %} saving covers the instalment for the whole term
  250.                     {%- elseif pc.finance.loan.self_financing_year1 %} saving covers the instalment in year 1, but falls short from year {{ pc.finance.loan.first_shortfall_year }}
  251.                     {%- else %} cash-positive in year {{ pc.finance.loan.cash_positive_year ?: '> ' ~ pc.finance.assumptions.years }}{% endif %}</td></tr>
  252.                 {% if pc.finance.lcoe.per_kwh is not null %}
  253.                 <tr><td>Levelized cost of your solar energy</td><td class="num">€{{ pc.finance.lcoe.per_kwh|number_format(3, ',', '.') }}/kWh {{ pc.finance.lcoe.beats_grid ? '— beats' : '— above' }} your grid tariff (€{{ pc.finance.lcoe.grid_tariff }}/kWh)</td></tr>
  254.                 {% endif %}
  255.             </table>
  256.             <div class="sub" style="font-size:12px;color:var(--n-muted);margin-top:6px">Indicative lenses — no tax, depreciation or subsidy treatment. Not an offer of finance.</div>
  257.         </div>
  258.         {% endif %}
  259.         <div class="hbr-note">{{ pc.disclaimer }}</div>
  260.     {% endif %}
  261.     {% if report.package_d is defined and report.package_d.ok|default(false) %}
  262.         {% set pd = report.package_d %}
  263.         {% set dfig = pd.figures|default({}) %}
  264.         <div class="hbr-sec" style="border-top:2px solid var(--n-border-md);padding-top:18px;margin-top:30px">
  265.             <h3>Package D — grid &amp; connection concept</h3>
  266.             <div class="sub">Regime: {{ pd.jurisdiction_hint }} · A briefing pack for the DNO enquiry and the protection study — not a design.</div>
  267.         </div>
  268.         <div class="hbr-verdict" style="border-left-color:#b4452f;background:#fdf3f0">
  269.             <b>⚠ {{ pd.engineer_handoff.headline }}</b><br>
  270.             {{ pd.engineer_handoff.next }}
  271.         </div>
  272.         <div class="hbr-sec">
  273.             <h3>Nearby grid infrastructure {{ hb360.tag(dfig.scan.tag|default('assumed'), dfig.scan.basis|default('')) }}</h3>
  274.             {% if pd.scan.ok|default(false) %}
  275.             <table class="hbr-table">
  276.                 <thead><tr><th>Asset</th><th class="num">Voltage</th><th class="num">Distance</th><th class="num">Confidence</th><th>Note</th></tr></thead>
  277.                 <tbody>
  278.                 {% for c in pd.scan.candidates %}
  279.                     <tr>
  280.                         <td>{{ c.type }}{{ c.name ? ' · ' ~ c.name : '' }}</td>
  281.                         <td class="num">{{ c.voltage_kv is not null ? c.voltage_kv ~ ' kV' : '—' }}</td>
  282.                         <td class="num">{{ c.distance_m|number_format(0, ',', '.') }} m</td>
  283.                         <td class="num">{{ (c.confidence * 100)|round }}%</td>
  284.                         <td style="color:var(--n-muted);font-size:12px">{{ c.note }}</td>
  285.                     </tr>
  286.                 {% endfor %}
  287.                 </tbody>
  288.             </table>
  289.             <div class="sub" style="font-size:12px;color:var(--n-muted);margin-top:6px">{{ pd.scan.caveat }} Source: {{ pd.scan.source }}.</div>
  290.             {% else %}
  291.             <div class="hbr-verdict">{{ pd.scan.reason }}</div>
  292.             {% endif %}
  293.             {% if pd.route %}
  294.             <table class="hbr-table">
  295.                 <tr><td>Indicative cable route to {{ pd.route.to }} {{ hb360.tag(dfig.route.tag|default('assumed'), dfig.route.basis|default('')) }}</td>
  296.                     <td class="num">{{ pd.route.route_m|number_format(0, ',', '.') }} m ≈ €{{ pd.route.cost_eur|number_format(0, ',', '.') }} ({{ pd.route.eur_per_m }} €/m)</td></tr>
  297.             </table>
  298.             {% endif %}
  299.         </div>
  300.         <div class="hbr-sec">
  301.             <h3>Connection level indication {{ hb360.tag(dfig.connection.tag|default('needs utility approval'), dfig.connection.basis|default('')) }}</h3>
  302.             <table class="hbr-table">
  303.                 <tr><td>Design capacity</td><td class="num">{{ pd.grid.design_kva|number_format(1, ',', '.') }} kVA (export {{ pd.grid.export_peak_kw }} kW / import {{ pd.grid.import_peak_kw }} kW{{ pd.grid.export_cap_pct < 100 ? ' · feed-in capped at ' ~ pd.grid.export_cap_pct ~ '%' : '' }})</td></tr>
  304.                 <tr><td>Indicated level</td><td class="num">{{ pd.grid.level }} — {{ pd.grid.level_label }} ({{ pd.grid.regime }})</td></tr>
  305.                 {% if pd.grid.transformer_kva %}
  306.                 <tr><td>Transformer concept</td><td class="num">{{ pd.grid.transformer_kva }} kVA</td></tr>
  307.                 {% endif %}
  308.                 {% if pd.grid.existing_connection_kva %}
  309.                 <tr><td>Your existing connection</td><td class="num">{{ pd.grid.existing_connection_kva }} kVA → headroom {{ pd.grid.headroom_kva }} kVA {{ pd.grid.upgrade_likely ? '· upgrade likely' : '· may suffice' }} {{ hb360.tag('needs utility approval') }}</td></tr>
  310.                 {% endif %}
  311.                 {% for it in pd.grid.budget.items %}
  312.                 <tr><td style="color:var(--n-muted)">{{ it.item }}</td><td class="num">€{{ it.eur|number_format(0, ',', '.') }}</td></tr>
  313.                 {% endfor %}
  314.                 <tr><td><b>Indicative connection budget (excl. DNO charges)</b></td><td class="num"><b>€{{ pd.grid.budget.total_eur|number_format(0, ',', '.') }}</b></td></tr>
  315.             </table>
  316.             <div class="sub" style="font-size:12px;color:var(--n-muted);margin-top:6px">{{ pd.grid.caveat }}</div>
  317.         </div>
  318.         <div class="hbr-sec">
  319.             <h3>Protection concept checklist {{ hb360.tag(dfig.protection.tag|default('needs utility approval'), dfig.protection.basis|default('')) }}</h3>
  320.             <div style="overflow-x:auto">
  321.                 <table class="hbr-table">
  322.                     <thead><tr><th>Location</th><th>Function</th><th>ANSI</th><th>Standard</th><th>Scope</th></tr></thead>
  323.                     <tbody>
  324.                     {% for row in pd.protection.rows %}
  325.                         <tr>
  326.                             <td>{{ row.location }}</td>
  327.                             <td>{{ row.function }}{{ row.note ? ' — ' : '' }}<span style="color:var(--n-muted);font-size:12px">{{ row.note }}</span></td>
  328.                             <td class="num">{{ row.ansi }}</td>
  329.                             <td style="font-size:12px">{{ row.standard }}</td>
  330.                             <td style="font-size:12px;white-space:nowrap">{{ row.scope == 'study' ? '📐 study first' : '🧑‍🔧 engineer + DNO' }}</td>
  331.                         </tr>
  332.                     {% endfor %}
  333.                     </tbody>
  334.                 </table>
  335.             </div>
  336.             <div class="hbr-verdict" style="border-left-color:#b4452f;background:#fdf3f0;margin-top:10px">
  337.                 <b>No settings, no ratings, no coordination.</b> {{ pd.protection.caveat }}
  338.             </div>
  339.         </div>
  340.         <div class="hbr-sec">
  341.             <h3>EMS / plant control functions {{ hb360.tag(dfig.ems.tag|default('assumed'), dfig.ems.basis|default('')) }}</h3>
  342.             <table class="hbr-table">
  343.                 <thead><tr><th>Function</th><th>Why</th><th>Where</th></tr></thead>
  344.                 <tbody>
  345.                 {% for fn in pd.ems.functions %}
  346.                     <tr><td>{{ fn.function }}</td><td style="color:var(--n-muted);font-size:12px">{{ fn.why }}</td><td style="font-size:12px">{{ fn.where }}</td></tr>
  347.                 {% endfor %}
  348.                 </tbody>
  349.             </table>
  350.             <table class="hbr-table">
  351.                 <thead><tr><th>Metering point</th><th>Why</th></tr></thead>
  352.                 <tbody>
  353.                 {% for m in pd.ems.metering %}
  354.                     <tr><td>{{ m.point }}</td><td style="color:var(--n-muted);font-size:12px">{{ m.why }}</td></tr>
  355.                 {% endfor %}
  356.                 </tbody>
  357.             </table>
  358.             <div class="hbr-verdict">🐝 {{ pd.ems.honeycore_note }}</div>
  359.         </div>
  360.         <div class="hbr-sec">
  361.             <h3>What to bring to the DNO enquiry</h3>
  362.             <ul style="font-size:13px;color:var(--n-dark);line-height:1.9;margin:6px 0 0;padding-left:20px">
  363.                 {% for b in pd.engineer_handoff.bring %}<li>{{ b }}</li>{% endfor %}
  364.             </ul>
  365.         </div>
  366.         <div class="hbr-note" style="border-left-color:#b4452f">{{ pd.disclaimer }}</div>
  367.     {% endif %}
  368.     {% if report.package_e is defined and report.package_e.ok|default(false) %}
  369.         {% set pe = report.package_e %}
  370.         {% set efig = pe.figures|default({}) %}
  371.         <div class="hbr-sec" style="border-top:2px solid var(--n-border-md);padding-top:18px;margin-top:30px">
  372.             <h3>Package E — execution briefing (BoM · RFQ · commissioning)</h3>
  373.             <div class="sub">Enquiry/planning pack — procurement and the binding test plan live in the real project.</div>
  374.         </div>
  375.         <div class="hbr-sec">
  376.             <h3>Graded bill of materials {{ hb360.tag(efig.bom.tag|default('assumed'), efig.bom.basis|default('')) }}</h3>
  377.             <div class="sub" style="margin-bottom:6px">Maturity ladder:
  378.                 {% for lv in pe.bom.summary.ladder %}<b>{{ lv.key }}</b> {{ lv.label }}{{ not loop.last ? ' → ' : '' }}{% endfor %}
  379.                 — this estimate reaches <b>{{ pe.bom.summary.ceiling }}</b>.
  380.             </div>
  381.             <div style="overflow-x:auto">
  382.                 <table class="hbr-table">
  383.                     <thead><tr><th>Item</th><th class="num">Qty</th><th>Unit</th><th>Maturity</th><th>Source</th><th>To advance</th></tr></thead>
  384.                     <tbody>
  385.                     {% for l in pe.bom.lines %}
  386.                         <tr>
  387.                             <td>{{ l.item }}</td><td class="num">{{ l.qty|number_format(0, ',', '.') }}</td><td>{{ l.unit }}</td>
  388.                             <td style="white-space:nowrap"><b>{{ l.level_key }}</b> <span style="color:var(--n-muted);font-size:12px">{{ l.level_label }}</span></td>
  389.                             <td style="font-size:12px;color:var(--n-muted)">{{ l.source }}</td>
  390.                             <td style="font-size:12px;color:var(--n-muted)">{{ l.to_advance }}</td>
  391.                         </tr>
  392.                     {% endfor %}
  393.                     </tbody>
  394.                 </table>
  395.             </div>
  396.             <div class="sub" style="font-size:12px;color:var(--n-muted);margin-top:6px">{{ pe.bom.caveat }}</div>
  397.         </div>
  398.         <div class="hbr-sec">
  399.             <h3>RFQ pack {{ hb360.tag(efig.rfq.tag|default('assumed'), efig.rfq.basis|default('')) }}</h3>
  400.             {% for lot in pe.rfq.lots %}
  401.                 <table class="hbr-table" style="margin-top:10px">
  402.                     <thead><tr><th colspan="3">{{ lot.label }}</th></tr></thead>
  403.                     <tbody>
  404.                     {% for line in lot.lines %}
  405.                         <tr><td>{{ line.spec }} <span style="color:var(--n-muted);font-size:12px">{{ line.maturity }}{{ line.note ? ' · ' ~ line.note : '' }}</span></td>
  406.                             <td class="num">{{ line.qty|number_format(0, ',', '.') }}</td><td>{{ line.unit }}</td></tr>
  407.                     {% endfor %}
  408.                     </tbody>
  409.                 </table>
  410.             {% endfor %}
  411.             <div class="hbr-verdict" style="margin-top:10px"><b>Ask every vendor for:</b><br>
  412.                 {% for a in pe.rfq.asks %}· {{ a }}<br>{% endfor %}
  413.             </div>
  414.         </div>
  415.         <div class="hbr-sec">
  416.             <h3>Commissioning checklist {{ hb360.tag(efig.commissioning.tag|default('needs site survey'), efig.commissioning.basis|default('')) }}</h3>
  417.             {% for st in pe.commissioning.stages %}
  418.                 <table class="hbr-table" style="margin-top:10px">
  419.                     <thead><tr><th colspan="2">{{ st.stage }}</th></tr></thead>
  420.                     <tbody>
  421.                     {% for it in st.items %}
  422.                         <tr><td>{{ it.item }}{{ it.note ? ' — ' : '' }}<span style="color:var(--n-muted);font-size:12px">{{ it.note }}</span></td>
  423.                             <td style="font-size:12px;white-space:nowrap;text-align:right">{{ it.scope }}</td></tr>
  424.                     {% endfor %}
  425.                     </tbody>
  426.                 </table>
  427.             {% endfor %}
  428.             <div class="sub" style="font-size:12px;color:var(--n-muted);margin-top:6px">{{ pe.commissioning.caveat }}</div>
  429.         </div>
  430.         <div class="hbr-note">{{ pe.disclaimer }}</div>
  431.     {% endif %}
  432.     {% if report.package_f is defined and report.package_f.ok|default(false) %}
  433.         {% set pf = report.package_f %}
  434.         <div class="hbr-sec" style="border-top:2px solid var(--n-border-md);padding-top:18px;margin-top:30px">
  435.             <h3>Package F — operate (O&amp;M · HoneyCore) {{ hb360.tag(pf.figures.om.tag|default('assumed'), pf.figures.om.basis|default('')) }}</h3>
  436.         </div>
  437.         <div class="hbr-sec">
  438.             <table class="hbr-table">
  439.                 <thead><tr><th>KPI</th><th>Target</th></tr></thead>
  440.                 <tbody>{% for k in pf.kpis %}<tr><td>{{ k.kpi }}</td><td style="font-size:12.5px">{{ k.target }}</td></tr>{% endfor %}</tbody>
  441.             </table>
  442.             <table class="hbr-table" style="margin-top:10px">
  443.                 <thead><tr><th>O&amp;M task</th><th>Frequency</th></tr></thead>
  444.                 <tbody>{% for s in pf.schedule %}<tr><td>{{ s.task }}</td><td style="font-size:12.5px">{{ s.freq }}</td></tr>{% endfor %}</tbody>
  445.             </table>
  446.             <table class="hbr-table" style="margin-top:10px">
  447.                 <thead><tr><th>Monitoring point</th><th>Why</th></tr></thead>
  448.                 <tbody>{% for m in pf.monitoring_points %}<tr><td>{{ m.point }}</td><td style="font-size:12px;color:var(--n-muted)">{{ m.why }}</td></tr>{% endfor %}</tbody>
  449.             </table>
  450.             <div class="hbr-verdict" style="margin-top:10px">🐝 <b>Closing the loop:</b> {{ pf.honeycore.story }}<br><br>
  451.                 {% for a in pf.honeycore.activation %}· {{ a }}<br>{% endfor %}
  452.             </div>
  453.             <div class="hbr-note">{{ pf.disclaimer }}</div>
  454.         </div>
  455.     {% endif %}
  456.     <div class="hbr-note">{{ report.disclaimer }}</div>
  457.     <div style="margin-top:6px">{{ hb360.legend() }}</div>
  458. {% else %}
  459.     <div class="hbr-note">{{ estimate.disclaimer|default('Indicative estimate only — not a quote.') }}</div>
  460. {% endif %}