delete solar analysis page and remove solar analysis from toolbar

This commit is contained in:
Khalim Conn-Kowlessar 2026-04-08 10:51:46 +00:00
parent b9d5166e82
commit c3e657847a
2 changed files with 1 additions and 393 deletions

View file

@ -56,7 +56,7 @@ const navigationMenuTriggerStyle = cva(
"data-[active]:bg-accent/50",
"data-[state=open]:bg-gray-200",
"text-gray-900",
].join(" ")
].join(" "),
);
export function Toolbar({
@ -88,16 +88,6 @@ export function Toolbar({
</NavigationMenuLink>
);
const solarAnalysisButton = (
<NavigationMenuLink
className={navigationMenuTriggerStyle() + " ml-3 mr-2"}
href={`/portfolio/${portfolioId}/building-passport/${propertyId}/solar-analysis`}
>
<SunIcon className="h-4 w-4 mr-2" />
Solar
</NavigationMenuLink>
);
const recommendationsButton = (
<NavigationMenuLink
className={navigationMenuTriggerStyle() + " ml-3 mr-2"}
@ -136,7 +126,6 @@ export function Toolbar({
{Object.keys(decentHomes).length > 0 &&
decentHomes.uprn &&
decentHomesButton}
{solarAnalysisButton}
{recommendationsButton}
{documentsButton}
</NavigationMenuList>

View file

@ -1,381 +0,0 @@
import {
SunIcon,
BoltIcon,
ClockIcon,
Squares2X2Icon,
SparklesIcon,
MapPinIcon,
CalendarIcon,
GlobeAltIcon,
} from "@heroicons/react/24/outline";
import {
Card,
CardContent,
CardHeader,
CardTitle,
} from "@/app/shadcn_components/ui/card";
import { Badge } from "@/app/shadcn_components/ui/badge";
import { Separator } from "@/app/shadcn_components/ui/separator";
import { getPropertyMeta } from "../utils";
import { getSolarData, getSolarScenarioData } from "./utils";
import PropertyMapWrapper from "./PropertyMapWrapper";
import SolarSimulationWrapper from "./SolarSimulationWrapper";
// ── Helpers ───────────────────────────────────────────────────────────────────
function getDirectionLabel(az: number): { label: string; short: string } {
// Standard compass: 0/360=N, 90=E, 180=S, 270=W
const norm = ((az % 360) + 360) % 360;
if (norm >= 337.5 || norm < 22.5) return { short: "N", label: "North" };
if (norm < 67.5) return { short: "NE", label: "North-East" };
if (norm < 112.5) return { short: "E", label: "East" };
if (norm < 157.5) return { short: "SE", label: "South-East" };
if (norm < 202.5) return { short: "S", label: "South" };
if (norm < 247.5) return { short: "SW", label: "South-West" };
if (norm < 292.5) return { short: "W", label: "West" };
return { short: "NW", label: "North-West" };
}
// Warm = south-facing (best solar); cool = north-facing (worst)
const SEGMENT_THEMES: Record<string, {
gradient: string;
border: string;
badge: string;
label: string;
dot: string;
}> = {
S: { gradient: "from-amber-50 to-orange-50/60", border: "border-amber-200/80", badge: "bg-amber-100 text-amber-800 border-amber-300", label: "text-amber-900", dot: "bg-amber-400" },
SE: { gradient: "from-orange-50 to-amber-50/60", border: "border-orange-200/80", badge: "bg-orange-100 text-orange-800 border-orange-300", label: "text-orange-900", dot: "bg-orange-400" },
SW: { gradient: "from-orange-50 to-amber-50/60", border: "border-orange-200/80", badge: "bg-orange-100 text-orange-800 border-orange-300", label: "text-orange-900", dot: "bg-orange-400" },
E: { gradient: "from-sky-50 to-blue-50/40", border: "border-sky-200/80", badge: "bg-sky-100 text-sky-800 border-sky-300", label: "text-sky-900", dot: "bg-sky-400" },
W: { gradient: "from-sky-50 to-blue-50/40", border: "border-sky-200/80", badge: "bg-sky-100 text-sky-800 border-sky-300", label: "text-sky-900", dot: "bg-sky-400" },
N: { gradient: "from-slate-50 to-gray-50/40", border: "border-slate-200/80", badge: "bg-slate-100 text-slate-700 border-slate-300", label: "text-slate-800", dot: "bg-slate-400" },
NE: { gradient: "from-slate-50 to-gray-50/40", border: "border-slate-200/80", badge: "bg-slate-100 text-slate-700 border-slate-300", label: "text-slate-800", dot: "bg-slate-400" },
NW: { gradient: "from-slate-50 to-gray-50/40", border: "border-slate-200/80", badge: "bg-slate-100 text-slate-700 border-slate-300", label: "text-slate-800", dot: "bg-slate-400" },
};
// ── Sub-components ────────────────────────────────────────────────────────────
function PageSection({ title, description, children }: {
title: string;
description: string;
children: React.ReactNode;
}) {
return (
<section className="space-y-5">
<div>
<h2 className="text-base font-bold text-brandblue tracking-tight">{title}</h2>
<p className="text-sm text-gray-400 mt-0.5 leading-relaxed">{description}</p>
</div>
{children}
</section>
);
}
function InputRow({
icon,
label,
value,
}: {
icon: React.ReactNode;
label: string;
value: React.ReactNode;
}) {
return (
<div className="flex items-center justify-between py-3 group">
<div className="flex items-center gap-2.5">
<span className="shrink-0 w-7 h-7 rounded-md bg-brandblue/5 flex items-center justify-center text-brandblue/60 group-hover:bg-brandblue/10 transition-colors">
{icon}
</span>
<span className="text-sm text-gray-500">{label}</span>
</div>
<span className="text-sm font-semibold text-gray-800 tabular-nums">{value}</span>
</div>
);
}
function RoofSegmentCard({
index,
azimuthDegrees,
pitchDegrees,
areaMeters2,
groundAreaMeters2,
sunshineQuantiles,
planeHeightAtCenterMeters,
}: {
index: number;
azimuthDegrees: number;
pitchDegrees: number;
areaMeters2: number;
groundAreaMeters2: number;
sunshineQuantiles: number[];
planeHeightAtCenterMeters: number;
}) {
const dir = getDirectionLabel(azimuthDegrees);
const theme = SEGMENT_THEMES[dir.short] ?? SEGMENT_THEMES["N"];
const medianSunshine = sunshineQuantiles?.[4] ?? null;
const peakSunshine = sunshineQuantiles?.[8] ?? null;
return (
<div className={`rounded-2xl border bg-gradient-to-br ${theme.gradient} ${theme.border} shadow-sm overflow-hidden`}>
{/* Card header strip */}
<div className="px-5 pt-5 pb-4">
<div className="flex items-start justify-between mb-3">
<div>
<p className="text-[10px] font-bold uppercase tracking-widest text-gray-400 mb-1">
Segment {index + 1}
</p>
<h3 className={`text-lg font-bold leading-tight ${theme.label}`}>
{dir.label}
</h3>
</div>
<div className={`flex items-center gap-1 text-xs font-bold px-2.5 py-1 rounded-full border ${theme.badge}`}>
<span className={`w-1.5 h-1.5 rounded-full ${theme.dot}`} />
{dir.short}
</div>
</div>
{/* Sunshine bar */}
{medianSunshine !== null && (
<div className="mt-3">
<div className="flex items-center justify-between text-[10px] text-gray-400 mb-1">
<span>Median sunshine</span>
<span className="font-semibold text-gray-600">{Math.round(medianSunshine)} hrs/yr</span>
</div>
<div className="h-1.5 bg-white/60 rounded-full overflow-hidden">
<div
className={`h-full rounded-full ${theme.dot} opacity-70`}
style={{ width: `${Math.min(100, (medianSunshine / 1600) * 100)}%` }}
/>
</div>
</div>
)}
</div>
<Separator className="opacity-30" />
{/* Stats grid */}
<dl className="grid grid-cols-2 gap-px bg-black/5 text-sm">
{[
{ label: "Roof area", value: `${areaMeters2.toFixed(1)}` },
{ label: "Ground area", value: `${groundAreaMeters2.toFixed(1)}` },
{ label: "Pitch", value: `${pitchDegrees.toFixed(1)}°` },
{ label: "Azimuth", value: `${azimuthDegrees.toFixed(1)}°` },
{ label: "Height", value: `${planeHeightAtCenterMeters.toFixed(1)} m` },
peakSunshine !== null
? { label: "Peak (P90)", value: `${Math.round(peakSunshine)} hrs` }
: { label: "", value: "" },
].map(({ label, value }, i) =>
label ? (
<div key={i} className="bg-white/40 px-4 py-3 hover:bg-white/60 transition-colors">
<dt className="text-[10px] uppercase tracking-wide text-gray-400 font-medium mb-0.5">{label}</dt>
<dd className="font-semibold text-gray-700 tabular-nums">{value}</dd>
</div>
) : <div key={i} className="bg-white/20" />
)}
</dl>
</div>
);
}
// ── Page ──────────────────────────────────────────────────────────────────────
export default async function SolarAnalysisPage(props: {
params: Promise<{ slug: string; propertyId: string }>;
}) {
const params = await props.params;
const propertyMeta = await getPropertyMeta(params.propertyId);
const solarData = await getSolarData(Number(propertyMeta.uprn));
if (!solarData) {
return (
<div className="flex items-center justify-center min-h-[60vh]">
<div className="text-center">
<div className="w-16 h-16 rounded-2xl bg-gray-100 flex items-center justify-center mx-auto mb-4">
<SunIcon className="w-8 h-8 text-gray-300" />
</div>
<p className="text-base font-semibold text-gray-600">No Solar Data Available</p>
<p className="text-sm text-gray-400 mt-1">Please check back later for updates.</p>
</div>
</div>
);
}
const solarScenarioData = await getSolarScenarioData(String(solarData.id));
const {
panelWidthMeters,
panelHeightMeters,
panelCapacityWatts,
panelLifetimeYears,
maxSunshineHoursPerYear,
carbonOffsetFactorKgPerMwh,
roofSegmentStats,
solarPanelConfigs,
maxArrayPanelsCount,
maxArrayAreaMeters2,
} = solarData.googleApiResponse.solarPotential;
const buildingCenter = solarData.googleApiResponse.center;
const { imageryQuality, imageryDate, regionCode } = solarData.googleApiResponse;
const imageryDateStr = `${imageryDate.year}-${String(imageryDate.month).padStart(2, "0")}-${String(imageryDate.day).padStart(2, "0")}`;
const qualityColors: Record<string, string> = {
HIGH: "bg-emerald-50 text-emerald-700 border-emerald-200",
MEDIUM: "bg-amber-50 text-amber-700 border-amber-200",
LOW: "bg-gray-50 text-gray-600 border-gray-200",
};
const qualityBadge = qualityColors[imageryQuality] ?? qualityColors.LOW;
const qualityText = imageryQuality === "HIGH" ? "High quality"
: imageryQuality === "MEDIUM" ? "Medium quality"
: "Base quality";
const maxAnnualKwh = Math.round(
solarPanelConfigs[solarPanelConfigs.length - 1].yearlyEnergyDcKwh
);
return (
<div className="max-w-6xl mx-auto px-4 py-10 space-y-14">
{/* ── Hero header ─────────────────────────────────────────────────── */}
<div className="flex flex-wrap items-start justify-between gap-4">
<div>
<div className="flex flex-wrap items-center gap-2.5 mb-2">
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-brandgold/30 to-brandgold/10 border border-brandgold/30 flex items-center justify-center shrink-0">
<SunIcon className="w-5 h-5 text-brandgold" />
</div>
<h1 className="text-2xl font-bold text-brandblue tracking-tight">
Solar Potential Analysis
</h1>
<span className={`text-xs font-semibold px-2.5 py-1 rounded-full border ${qualityBadge}`}>
{qualityText}
</span>
</div>
<p className="text-sm text-gray-400 max-w-xl leading-relaxed">
Ara uses high-resolution aerial imagery and rooftop geometry data to estimate
suitable solar PV packages for retrofit plans.{" "}
{solarScenarioData.scenrioType === "building"
? "Figures are for the building as a whole."
: "Figures are for this individual unit."}
</p>
</div>
{/* Key summary pills */}
<div className="flex flex-wrap gap-2 text-xs">
{[
{ icon: <MapPinIcon className="w-3.5 h-3.5" />, text: regionCode },
{ icon: <CalendarIcon className="w-3.5 h-3.5" />, text: `Imagery: ${imageryDateStr}` },
{ icon: <BoltIcon className="w-3.5 h-3.5" />, text: `Up to ${maxAnnualKwh.toLocaleString()} kWh/yr` },
{ icon: <Squares2X2Icon className="w-3.5 h-3.5" />, text: `Up to ${maxArrayPanelsCount} panels` },
].map(({ icon, text }, i) => (
<span key={i} className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-white border border-gray-200 text-gray-500 font-medium shadow-sm">
<span className="text-brandblue/60">{icon}</span>
{text}
</span>
))}
</div>
</div>
{/* ── Map + Inputs ─────────────────────────────────────────────────── */}
<PageSection
title="Property Overview"
description="Location and the key parameters used to model this property's solar potential."
>
<div className="grid grid-cols-1 lg:grid-cols-5 gap-5">
{/* Map */}
<div className="lg:col-span-3 rounded-2xl overflow-hidden border border-gray-200 shadow-sm" style={{ height: "360px" }}>
<PropertyMapWrapper
latitude={buildingCenter.latitude}
longitude={buildingCenter.longitude}
/>
</div>
{/* Inputs card */}
<Card className="lg:col-span-2 shadow-sm border-gray-200/80">
<CardHeader className="pb-1 pt-5 px-5">
<CardTitle className="text-sm font-bold text-brandblue">Analysis Parameters</CardTitle>
<p className="text-xs text-gray-400 mt-0.5">
Inputs used to model solar output for this property.
</p>
</CardHeader>
<CardContent className="px-5 pb-5 pt-1">
<div className="divide-y divide-gray-100">
<InputRow
icon={<Squares2X2Icon className="w-3.5 h-3.5" />}
label="Panel dimensions"
value={`${panelWidthMeters} m × ${panelHeightMeters} m`}
/>
<InputRow
icon={<BoltIcon className="w-3.5 h-3.5" />}
label="Panel capacity"
value={`${panelCapacityWatts} W`}
/>
<InputRow
icon={<ClockIcon className="w-3.5 h-3.5" />}
label="Panel lifetime"
value={`${panelLifetimeYears} years`}
/>
<InputRow
icon={<SunIcon className="w-3.5 h-3.5" />}
label="Max sunshine"
value={`${Math.round(maxSunshineHoursPerYear).toLocaleString()} hrs/yr`}
/>
<InputRow
icon={<SparklesIcon className="w-3.5 h-3.5" />}
label="Carbon offset factor"
value={`${Math.round(carbonOffsetFactorKgPerMwh)} kg/MWh`}
/>
<InputRow
icon={<GlobeAltIcon className="w-3.5 h-3.5" />}
label="Maximum array"
value={`${maxArrayPanelsCount} panels · ${maxArrayAreaMeters2.toFixed(0)}`}
/>
</div>
</CardContent>
</Card>
</div>
</PageSection>
{/* ── Roof profile ─────────────────────────────────────────────────── */}
<PageSection
title="Roof Profile"
description={`${roofSegmentStats.length} roof face${roofSegmentStats.length !== 1 ? "s" : ""} identified. Each is assessed independently — south-facing segments with low pitch typically yield the highest solar output.`}
>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
{roofSegmentStats.map((seg: any, i: number) => (
<RoofSegmentCard
key={i}
index={i}
azimuthDegrees={seg.azimuthDegrees}
pitchDegrees={seg.pitchDegrees}
areaMeters2={seg.stats.areaMeters2}
groundAreaMeters2={seg.stats.groundAreaMeters2}
sunshineQuantiles={seg.stats.sunshineQuantiles}
planeHeightAtCenterMeters={seg.planeHeightAtCenterMeters}
/>
))}
</div>
</PageSection>
{/* ── Solar configurations ─────────────────────────────────────────── */}
<PageSection
title="Solar Configurations"
description="All array sizes modelled for this property. The efficiency curve reveals how output scales with system size — useful for understanding which roof faces are doing the most work."
>
<Card className="shadow-sm border-gray-200/80">
<CardContent className="pt-6 px-6 pb-6">
<SolarSimulationWrapper
solarPanelConfigs={solarPanelConfigs}
panelCapacityWatts={panelCapacityWatts}
panelLifetimeYears={panelLifetimeYears}
panelWidthMeters={panelWidthMeters}
panelHeightMeters={panelHeightMeters}
/>
</CardContent>
</Card>
</PageSection>
</div>
);
}