mirror of
https://github.com/Hestia-Homes/assessment-model.git
synced 2026-06-08 11:37:25 +00:00
trying to get the skeleton working
This commit is contained in:
parent
2341741270
commit
f8ca195b83
3 changed files with 83 additions and 44 deletions
|
|
@ -1,6 +1,7 @@
|
|||
"use client";
|
||||
|
||||
import { useState, useTransition, useMemo } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import {
|
||||
Recommendation,
|
||||
RecommendationType,
|
||||
|
|
@ -97,6 +98,7 @@ export default function RecommendationContainer({
|
|||
Record<string, AugmentedRec | null>
|
||||
>({});
|
||||
|
||||
const router = useRouter();
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
// Sort: included first, then not-included, then already-installed
|
||||
|
|
@ -160,6 +162,7 @@ export default function RecommendationContainer({
|
|||
}),
|
||||
);
|
||||
await updatePlanMetrics(planId, currentSapPoints, slug, propertyId);
|
||||
router.refresh();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,14 +17,22 @@ import {
|
|||
|
||||
function getEpcHex(letter: string | null | undefined): string {
|
||||
switch (letter?.toUpperCase()) {
|
||||
case "A": return "#117d58";
|
||||
case "B": return "#2da55c";
|
||||
case "C": return "#8dbd40";
|
||||
case "D": return "#f7cd14";
|
||||
case "E": return "#f3a96a";
|
||||
case "F": return "#ef8026";
|
||||
case "G": return "#e41e3b";
|
||||
default: return "#9ca3af";
|
||||
case "A":
|
||||
return "#117d58";
|
||||
case "B":
|
||||
return "#2da55c";
|
||||
case "C":
|
||||
return "#8dbd40";
|
||||
case "D":
|
||||
return "#f7cd14";
|
||||
case "E":
|
||||
return "#f3a96a";
|
||||
case "F":
|
||||
return "#ef8026";
|
||||
case "G":
|
||||
return "#e41e3b";
|
||||
default:
|
||||
return "#9ca3af";
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -34,7 +42,15 @@ const EPC_LETTERS = ["G", "F", "E", "D", "C", "B", "A"];
|
|||
// Returns the horizontal centre position (%) of each EPC band in the 7-segment bar
|
||||
function epcToBandCenter(letter: string | null | undefined): number {
|
||||
if (!letter) return 0;
|
||||
const map: Record<string, number> = { G: 0, F: 1, E: 2, D: 3, C: 4, B: 5, A: 6 };
|
||||
const map: Record<string, number> = {
|
||||
G: 0,
|
||||
F: 1,
|
||||
E: 2,
|
||||
D: 3,
|
||||
C: 4,
|
||||
B: 5,
|
||||
A: 6,
|
||||
};
|
||||
const idx = map[letter.toUpperCase()] ?? 0;
|
||||
return ((idx + 0.5) / 7) * 100;
|
||||
}
|
||||
|
|
@ -44,12 +60,13 @@ export default async function PlanDetail(props: {
|
|||
}) {
|
||||
const params = await props.params;
|
||||
const propertyMeta = await getPropertyMeta(params.propertyId);
|
||||
const [recommendations, planMeta, installedMeasures, scenarioData] = await Promise.all([
|
||||
getRecommendations(params.planId),
|
||||
getPlanMeta(params.planId),
|
||||
getInstalledMeasuresByUprn(propertyMeta.uprn),
|
||||
getScenario(params.planId),
|
||||
]);
|
||||
const [recommendations, planMeta, installedMeasures, scenarioData] =
|
||||
await Promise.all([
|
||||
getRecommendations(params.planId),
|
||||
getPlanMeta(params.planId),
|
||||
getInstalledMeasuresByUprn(propertyMeta.uprn),
|
||||
getScenario(params.planId),
|
||||
]);
|
||||
|
||||
const currentEpc = propertyMeta.currentEpcRating;
|
||||
const targetEpc =
|
||||
|
|
@ -59,7 +76,10 @@ export default async function PlanDetail(props: {
|
|||
const valuationLabel = (() => {
|
||||
if (planMeta.valuationIncreaseAverage)
|
||||
return `+${planMeta.valuationIncreaseAverage.toFixed(1)}%`;
|
||||
if (planMeta.valuationIncreaseLowerBound && planMeta.valuationIncreaseUpperBound)
|
||||
if (
|
||||
planMeta.valuationIncreaseLowerBound &&
|
||||
planMeta.valuationIncreaseUpperBound
|
||||
)
|
||||
return `+${planMeta.valuationIncreaseLowerBound.toFixed(0)}–${planMeta.valuationIncreaseUpperBound.toFixed(0)}%`;
|
||||
if (planMeta.valuationIncrease)
|
||||
return `+${planMeta.valuationIncrease.toFixed(1)}%`;
|
||||
|
|
@ -68,21 +88,15 @@ export default async function PlanDetail(props: {
|
|||
|
||||
return (
|
||||
<div className="max-w-[1400px] mx-auto py-10 space-y-12">
|
||||
|
||||
{/* ── Header ─────────────────────────────────────────────────── */}
|
||||
<div>
|
||||
<h1 className="text-4xl md:text-5xl font-black font-manrope tracking-tighter text-brandblue mb-4">
|
||||
<h1 className="text-brandblue text-4xl md:text-5xl font-black font-manrope tracking-tighter text-brandblue mb-4">
|
||||
Plan Details: {planMeta.name ?? "Retrofit Plan"}
|
||||
</h1>
|
||||
<p className="text-gray-600 max-w-2xl text-lg leading-relaxed">
|
||||
A comprehensive transformation strategy designed to improve energy
|
||||
efficiency, reduce running costs, and maximise property value.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* ── Executive Summary Bento ──────────────────────────────────── */}
|
||||
<section className="grid grid-cols-1 md:grid-cols-4 lg:grid-cols-6 gap-6">
|
||||
|
||||
{/* EPC Rating card — redesigned with SAP IMPROVEMENT label */}
|
||||
<div className="md:col-span-2 lg:col-span-2 bg-white p-7 rounded-2xl border border-gray-100 shadow-sm flex flex-col gap-5">
|
||||
<span className="text-[10px] font-bold uppercase tracking-widest text-gray-400">
|
||||
|
|
@ -92,7 +106,9 @@ export default async function PlanDetail(props: {
|
|||
{/* Current → Target letter badges */}
|
||||
<div className="flex items-end gap-3">
|
||||
<div className="flex flex-col items-center gap-1.5">
|
||||
<span className="text-[10px] font-bold text-gray-400 uppercase tracking-widest">Current</span>
|
||||
<span className="text-[10px] font-bold text-gray-400 uppercase tracking-widest">
|
||||
Current
|
||||
</span>
|
||||
<span
|
||||
className="text-5xl font-black font-manrope leading-none"
|
||||
style={{ color: getEpcHex(currentEpc) }}
|
||||
|
|
@ -102,12 +118,21 @@ export default async function PlanDetail(props: {
|
|||
</div>
|
||||
<svg
|
||||
className="w-5 h-5 text-gray-300 mb-2 shrink-0"
|
||||
fill="none" stroke="currentColor" strokeWidth={2.5} viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2.5}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3" />
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3"
|
||||
/>
|
||||
</svg>
|
||||
<div className="flex flex-col items-center gap-1.5">
|
||||
<span className="text-[10px] font-bold text-gray-400 uppercase tracking-widest">Target</span>
|
||||
<span className="text-[10px] font-bold text-gray-400 uppercase tracking-widest">
|
||||
Target
|
||||
</span>
|
||||
<span
|
||||
className="text-7xl font-black font-manrope leading-none drop-shadow-lg"
|
||||
style={{ color: getEpcHex(targetEpc) }}
|
||||
|
|
@ -146,7 +171,8 @@ export default async function PlanDetail(props: {
|
|||
className="text-[8px] font-bold"
|
||||
style={{
|
||||
color:
|
||||
l === currentEpc?.toUpperCase() || l === targetEpc?.toUpperCase()
|
||||
l === currentEpc?.toUpperCase() ||
|
||||
l === targetEpc?.toUpperCase()
|
||||
? getEpcHex(l)
|
||||
: "#d1d5db",
|
||||
}}
|
||||
|
|
@ -175,7 +201,6 @@ export default async function PlanDetail(props: {
|
|||
|
||||
{/* 4 stat tiles — cool blue background, icon at top */}
|
||||
<div className="md:col-span-2 lg:col-span-4 grid grid-cols-2 gap-4">
|
||||
|
||||
<div className="bg-blue-50 p-5 rounded-xl flex flex-col justify-between gap-3 border border-blue-100">
|
||||
<CloudIcon className="w-8 h-8 text-brandmidblue" />
|
||||
<p className="text-2xl font-bold font-manrope text-brandblue">
|
||||
|
|
@ -221,13 +246,11 @@ export default async function PlanDetail(props: {
|
|||
Valuation Boost
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ── Financial + Recommendations ──────────────────────────────── */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-12 items-start">
|
||||
|
||||
{/* Left: Financial Overview — sticky so it stays visible while scrolling recs */}
|
||||
<div className="lg:col-span-4 space-y-6 lg:sticky lg:top-8">
|
||||
<h3 className="text-xs font-bold uppercase tracking-widest text-gray-500">
|
||||
|
|
@ -251,7 +274,8 @@ export default async function PlanDetail(props: {
|
|||
Contingency
|
||||
</p>
|
||||
<p className="text-xl font-bold font-manrope text-brandbrown">
|
||||
{planMeta.contingencyCost != null && planMeta.contingencyCost > 0
|
||||
{planMeta.contingencyCost != null &&
|
||||
planMeta.contingencyCost > 0
|
||||
? `£${formatNumber(planMeta.contingencyCost)}`
|
||||
: "—"}
|
||||
</p>
|
||||
|
|
@ -261,7 +285,9 @@ export default async function PlanDetail(props: {
|
|||
<InformationCircleIcon className="w-5 h-5 text-white/40 group-hover:text-white/80 transition-colors" />
|
||||
<div className="absolute bottom-full right-0 mb-3 w-64 bg-gray-900 text-white text-xs rounded-xl p-3.5 shadow-2xl z-20 opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none">
|
||||
<p className="leading-relaxed text-white/90">
|
||||
A contingency buffer is added on top of estimated costs to account for unexpected variations in materials or labour. Rates vary per measure type.
|
||||
A contingency buffer is added on top of estimated costs to
|
||||
account for unexpected variations in materials or labour.
|
||||
Rates vary per measure type.
|
||||
</p>
|
||||
<div className="absolute top-full right-4 border-4 border-transparent border-t-gray-900" />
|
||||
</div>
|
||||
|
|
@ -286,15 +312,18 @@ export default async function PlanDetail(props: {
|
|||
<ul className="space-y-1.5">
|
||||
<li className="text-xs text-gray-600 leading-relaxed flex items-start gap-1.5">
|
||||
<span className="text-brandblue mt-0.5 shrink-0">·</span>
|
||||
Costs modelled using current market rates and SAP 10.2 methodology.
|
||||
Costs modelled using current market rates and SAP 10.2
|
||||
methodology.
|
||||
</li>
|
||||
<li className="text-xs text-gray-600 leading-relaxed flex items-start gap-1.5">
|
||||
<span className="text-brandblue mt-0.5 shrink-0">·</span>
|
||||
Annual savings are projected based on typical occupancy patterns.
|
||||
Annual savings are projected based on typical occupancy
|
||||
patterns.
|
||||
</li>
|
||||
<li className="text-xs text-gray-600 leading-relaxed flex items-start gap-1.5">
|
||||
<span className="text-brandblue mt-0.5 shrink-0">·</span>
|
||||
Final quotes confirmed after a technical survey of the property.
|
||||
Final quotes confirmed after a technical survey of the
|
||||
property.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
|
@ -310,17 +339,23 @@ export default async function PlanDetail(props: {
|
|||
{scenarioData.name && (
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-500">Scenario</span>
|
||||
<span className="font-semibold text-gray-800">{scenarioData.name}</span>
|
||||
<span className="font-semibold text-gray-800">
|
||||
{scenarioData.name}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-500">Housing type</span>
|
||||
<span className="font-semibold text-gray-800">{scenarioData.housingType}</span>
|
||||
<span className="font-semibold text-gray-800">
|
||||
{scenarioData.housingType}
|
||||
</span>
|
||||
</div>
|
||||
{scenarioData.budget != null && (
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-500">Budget</span>
|
||||
<span className="font-semibold text-gray-800">£{formatNumber(scenarioData.budget)}</span>
|
||||
<span className="font-semibold text-gray-800">
|
||||
£{formatNumber(scenarioData.budget)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{scenarioData.goal && (
|
||||
|
|
@ -328,7 +363,9 @@ export default async function PlanDetail(props: {
|
|||
<span className="text-gray-500">Goal</span>
|
||||
<span className="font-semibold text-gray-800 text-right">
|
||||
{scenarioData.goal}
|
||||
{scenarioData.goalValue ? ` → ${scenarioData.goalValue}` : ""}
|
||||
{scenarioData.goalValue
|
||||
? ` → ${scenarioData.goalValue}`
|
||||
: ""}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"target": "es5",
|
||||
// "target": "ESNext",
|
||||
"target": "ESNext",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
|
|
@ -10,8 +9,8 @@
|
|||
"forceConsistentCasingInFileNames": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"module": "preserve",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue