completed summary ui

This commit is contained in:
Khalim Conn-Kowlessar 2025-08-26 22:53:11 +00:00
parent 45f5634f03
commit 1617d69b69
6 changed files with 833 additions and 60 deletions

View file

@ -0,0 +1,276 @@
"use client";
import React, { useState } from "react";
import { Card, CardContent } from "@/app/shadcn_components/ui/card";
import { Button } from "@/app/shadcn_components/ui/button";
import {
Drawer,
DrawerContent,
DrawerHeader,
DrawerTitle,
} from "@/app/shadcn_components/ui/drawer";
interface PropertySummary {
id: bigint;
uprn: bigint;
address: string;
postcode: string;
has_condition_data: boolean;
passes_decent_homes: boolean;
category1: Record<string, boolean>;
category2: Record<string, boolean>;
}
interface Props {
data: PropertySummary[];
portfolioId: string;
}
type ReasonKey =
| "damp_passes"
| "structure_passes"
| "heating_passes"
| "sap_passes"
| "thermal_comfort_passes"
| "defects_pass"
| "windows_pass"
| "kitchen_pass"
| "bathroom_pass"
| "space_pass";
const reasonLabelMap: Record<ReasonKey, string> = {
damp_passes: "Damp",
structure_passes: "Structural Issues",
heating_passes: "Heating",
sap_passes: "SAP Rating",
thermal_comfort_passes: "Thermal Comfort",
defects_pass: "General Disrepair",
windows_pass: "Windows",
kitchen_pass: "Kitchen",
bathroom_pass: "Bathroom",
space_pass: "Space Standard",
};
const DecentHomesDashboard: React.FC<Props> = ({ data, portfolioId }) => {
console.log("Data received:", data);
const [open, setOpen] = useState(false);
const [selectedGroup, setSelectedGroup] = useState<PropertySummary[]>([]);
const [selectedTitle, setSelectedTitle] = useState("");
const total = data.length;
const passed = data.filter((p) => p.passes_decent_homes);
const failedC1 = data.filter(
(p) =>
p.has_condition_data &&
Object.values(p.category1).some((v) => v === false)
);
const failedC2 = data.filter(
(p) =>
p.has_condition_data &&
Object.values(p.category2).some((v) => v === false)
);
const missing = data.filter((p) => !p.has_condition_data);
const openDrawer = (group: PropertySummary[], title: string) => {
setSelectedGroup(group);
setSelectedTitle(title);
setOpen(true);
};
const failedReasons = new Map<string, number>();
data.forEach((p) => {
if (!p.has_condition_data || p.passes_decent_homes) return;
const reasons = Object.entries({ ...p.category1, ...p.category2 })
.filter(([_, v]) => v === false)
.map(([key]) => key);
reasons.forEach((r) => {
failedReasons.set(r, (failedReasons.get(r) ?? 0) + 1);
});
});
const sortedReasons = Array.from(failedReasons.entries()).sort(
(a, b) => b[1] - a[1]
);
const top3Reasons = sortedReasons.slice(0, 3);
const dataCompleteness = (
(data.filter((p) => p.has_condition_data).length / total) *
100
).toFixed(1);
return (
<div className="container mx-auto px-4 py-6">
<h1 className="text-2xl font-semibold mb-6">Decent Homes Summary</h1>
<div className="sm:col-span-2 rounded-xl bg-brandblue p-[3px] mb-6">
<div className="rounded-lg bg-brandbrown p-[3px]">
<div className="rounded-md bg-brandblue">
<CardContent className="relative p-8 flex flex-col items-center justify-center text-center space-y-3">
<div className="text-sm uppercase tracking-wider font-semibold text-brandbrown">
Overall Pass Rate
</div>
<div className="text-6xl font-extrabold text-white drop-shadow">
{((passed.length / total) * 100).toFixed(1)}%
</div>
<div className="text-md font-medium text-white/80">
<span className="text-brandbrown font-semibold">
{passed.length}
</span>{" "}
out of{" "}
<span className="text-brandbrown font-semibold">{total}</span>{" "}
homes meet the Decent Homes Standard
</div>
<p className="absolute bottom-4 right-4 text-white/50 italic">
Data completeness: {dataCompleteness}%
</p>
</CardContent>
</div>
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<Card
onClick={() =>
openDrawer(failedC1, "Properties with Category 1 Risks")
}
className="hover:shadow-lg cursor-pointer border border-brandblue/30 bg-brandblue/5 transition duration-150 ease-in-out hover:bg-brandblue/10"
>
<CardContent className="p-6 relative bg-brandblue hover:bg-hoverblue">
<div className="text-brandbrown font-semibold mb-2">
Category 1 Risks
</div>
<div className="text-5xl font-bold text-white text-center">
{failedC1.length}
</div>
</CardContent>
</Card>
<Card
onClick={() =>
openDrawer(failedC2, "Properties with Category 2 Risks")
}
className="hover:shadow-lg cursor-pointer border border-brandblue/30 bg-brandblue/5 transition duration-150 ease-in-out hover:bg-brandblue/10"
>
<CardContent className="p-6 relative bg-brandblue hover:bg-hoverblue">
<div className="text-brandbrown font-semibold mb-2">
Category 2 Risks
</div>
<div className="text-5xl font-bold text-white text-center">
{failedC2.length}
</div>
</CardContent>
</Card>
<Card
onClick={() =>
openDrawer(missing, "Properties Missing Condition Data")
}
className="hover:shadow-lg cursor-pointer border border-brandblue/30 bg-brandblue/5 transition duration-150 ease-in-out hover:bg-brandblue/10"
>
<CardContent className="p-6 relative bg-brandblue hover:bg-hoverblue">
<div className="text-brandbrown font-semibold mb-2">
No Condition Data
</div>
<div className="text-5xl font-bold text-white text-center">
{missing.length}
</div>
</CardContent>
</Card>
<Card
onClick={() => openDrawer(passed, "Properties Passing Decent Homes")}
className="hover:shadow-lg cursor-pointer border border-brandblue/30 bg-brandblue/5 transition duration-150 ease-in-out"
>
<CardContent className="p-6 relative bg-brandblue hover:bg-hoverblue">
<div className="text-brandbrown font-semibold mb-2">
Passes Decent Homes
</div>
<div className="text-5xl font-bold text-white text-center">
{passed.length}
</div>
</CardContent>
</Card>
</div>
<div className="mt-8">
<h2 className="text-lg font-semibold mb-4 text-brandbrown">
Top Reasons for Failing
</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
{top3Reasons.map(([reason, count]) => (
<div
key={reason}
className="bg-brandblue/10 border border-brandblue/20 rounded-lg p-6 shadow-sm flex flex-col justify-between"
>
<div>
<div className="text-red-500 text-2xl mb-2">🚫</div>
<h3 className="text-md font-semibold text-brandbrown mb-1">
{reasonLabelMap[reason as ReasonKey] ??
reason.replace(/_/g, " ")}
</h3>
<p className="text-sm text-muted-foreground">
Failing in <span className="font-medium">{count}</span>{" "}
properties
</p>
</div>
</div>
))}
</div>
</div>
<Drawer open={open} onOpenChange={setOpen}>
<DrawerContent className="p-4 w-full max-w-md ml-auto border-l h-full overflow-y-auto">
<DrawerHeader>
<DrawerTitle>{selectedTitle}</DrawerTitle>
</DrawerHeader>
<ul className="space-y-4">
{selectedGroup.map((p) => (
<li key={String(p.id)} className="border rounded p-4">
<div className="font-semibold">
{p.address} ({p.postcode})
</div>
<div className="text-sm text-muted-foreground mb-2">
UPRN: {String(p.uprn)}
</div>
{p.has_condition_data ? (
<>
{Object.entries({ ...p.category1, ...p.category2 })
.filter(([_, value]) => value === false)
.map(([key]) => (
<div key={key} className="text-red-500 text-sm">
Issue:{" "}
{reasonLabelMap[key as ReasonKey] ??
key.replace(/_/g, " ")}
</div>
))}
</>
) : (
<div className="text-orange-500 text-sm">
No condition data available
</div>
)}
<Button
variant="outline"
size="sm"
className="mt-3"
onClick={() =>
window.open(
`/portfolio/${portfolioId}/building-passport/${p.id}/assessment`,
"_blank"
)
}
>
View Property
</Button>
</li>
))}
</ul>
</DrawerContent>
</Drawer>
</div>
);
};
export default DecentHomesDashboard;

View file

@ -0,0 +1,189 @@
import { db } from "@/app/db/db";
import { property } from "@/app/db/schema/property";
import { inArray, eq, and } from "drizzle-orm";
import { surveyDB } from "@/app/db/surveyDB/connection";
import { uploadedFiles } from "@/app/db/surveyDB/schema/surveyDB";
import {
getEnergyAssessmentFromS3,
getConditionReport,
getPropertyMeta,
} from "@/app/portfolio/[slug]/building-passport/[propertyId]/utils";
import {
getAllRoomData,
getRoomsWithDamp,
getRoomsWithDefects,
getRoomsWithBadWindows,
areAllWindowsOk,
getElevationsWithIssues,
hasSufficientSpace,
meetsSapThreshold,
hasEfficientHeatingSystem,
isInsulationAdequate,
} from "@/app/portfolio/[slug]/building-passport/[propertyId]/assessment/decent_homes_utils";
import DecentHomesDashboard from "./DecentHomesDashboard";
async function getPropertiesWithUprn(
portfolioId: string,
limit = 1000,
offset = 0
): Promise<any[]> {
const data = await db.query.property.findMany({
limit,
offset,
columns: {
id: true,
uprn: true,
address: true,
postcode: true,
},
where: eq(property.portfolioId, BigInt(portfolioId)),
});
return data;
}
async function getDocumentsForUprns(
uprns: (string | number)[]
): Promise<Map<string, typeof uploadedFiles.$inferSelect>> {
const rows = await surveyDB.query.uploadedFiles.findMany({
where: and(
inArray(uploadedFiles.uprn, uprns.map(String)),
eq(uploadedFiles.docType, "ECO_CONDITION_REPORT")
),
orderBy: (uploadedFiles, { desc }) => [
desc(uploadedFiles.s3JsonUploadTimestamp),
],
});
const latestByUprn = new Map<string, (typeof rows)[number]>();
for (const row of rows) {
if (!latestByUprn.has(row.uprn)) {
latestByUprn.set(row.uprn, row);
}
}
return latestByUprn;
}
export default async function DecentHomesPage({
params,
searchParams,
}: {
params: Promise<{ slug: string }>;
searchParams: Promise<{
[key: string]: string | string[] | undefined | number;
}>;
}) {
const { slug: portfolioId } = await params;
const properties = await getPropertiesWithUprn(portfolioId, 1000, 0);
const uprns = properties.map((p) => String(p.uprn));
const documentsMap = await getDocumentsForUprns(uprns);
const summaryResults = await Promise.all(
properties.map(async (property) => {
const result: any = {
id: property.id,
uprn: property.uprn,
address: property.address,
postcode: property.postcode,
has_condition_data: false,
passes_decent_homes: false,
category1: {},
category2: {},
};
const propertyMeta = await getPropertyMeta(property.id);
const conditionReportMeta = documentsMap.get(String(propertyMeta.uprn));
if (!conditionReportMeta || !conditionReportMeta.s3JsonUri) {
return result;
}
result.has_condition_data = true;
const conditionReport = await getEnergyAssessmentFromS3(
conditionReportMeta.s3JsonUri
);
const conditionData = await getConditionReport(String(property.id));
if (!conditionReport || !conditionData) return result;
const allRoomData = getAllRoomData(conditionReport.rooms);
const roomsWithDamp = getRoomsWithDamp(allRoomData);
const roomsWithDefects = getRoomsWithDefects(allRoomData);
const roomsWithBadWindows = getRoomsWithBadWindows(allRoomData);
const windowsOk = areAllWindowsOk(allRoomData);
const elevationsWithIssues = getElevationsWithIssues(conditionReport);
const {
isSufficient: enoughSpace,
occupantsToUse,
numberOfBedrooms,
areaPerPerson,
} = hasSufficientSpace(
conditionReport.occupant_info,
conditionData.totalFloorArea ?? 0,
allRoomData
);
const sapOk = meetsSapThreshold(propertyMeta.currentSapPoints ?? 0);
const efficientHeating = hasEfficientHeatingSystem(
conditionData.heatingRating
);
const insulationOk = isInsulationAdequate(
conditionData.roofRating,
conditionData.wallsRating
);
const thermalComfortOk = efficientHeating && insulationOk;
const kitchenOk =
conditionReport.rooms.kitchen?.room_info
?.overall_condition_of_the_room === "Good";
const bathroomsOk =
Array.isArray(conditionReport.rooms.bathrooms) &&
conditionReport.rooms.bathrooms.length > 0 &&
conditionReport.rooms.bathrooms.every(
(b: any) => b?.room_info?.overall_condition_of_the_room === "Good"
);
const hasDefects = roomsWithDefects.length > 0;
result.category1 = {
damp_passes: roomsWithDamp.length === 0,
structure_passes: elevationsWithIssues.length === 0,
heating_passes: efficientHeating,
sap_passes: sapOk,
thermal_comfort_passes: thermalComfortOk,
};
result.category2 = {
defects_pass: !hasDefects,
windows_pass: windowsOk,
kitchen_pass: kitchenOk,
bathroom_pass: bathroomsOk,
space_pass: enoughSpace,
};
// Overall result
result.passes_decent_homes =
result.category1.damp_passes &&
result.category1.structure_passes &&
result.category1.heating_passes &&
result.category1.sap_passes &&
result.category1.thermal_comfort_passes &&
result.category2.defects_pass &&
result.category2.windows_pass &&
result.category2.kitchen_pass &&
result.category2.bathroom_pass &&
result.category2.space_pass;
return result;
})
);
return (
<div className="container mx-auto px-4">
<DecentHomesDashboard data={summaryResults} portfolioId={portfolioId} />
</div>
);
}

View file

@ -1,15 +1,7 @@
"use client";
import {
Card,
CardContent,
CardHeader,
} from "@/app/shadcn_components/ui/card";
import {
CheckCircle,
XCircle,
HelpCircle,
} from "lucide-react";
import { Card, CardContent, CardHeader } from "@/app/shadcn_components/ui/card";
import { CheckCircle, XCircle, HelpCircle } from "lucide-react";
import {
Accordion,
AccordionContent,
@ -32,6 +24,16 @@ import {
meetsSapThreshold,
} from "./decent_homes_utils";
type Rating = 1 | 2 | 3 | 4 | 5;
const RatingMap: Record<Rating, string> = {
1: "Very Poor",
2: "Poor",
3: "Average",
4: "Good",
5: "Very Good",
};
function ChecklistItem({
label,
passed,
@ -45,18 +47,19 @@ function ChecklistItem({
alert?: boolean;
roomsWithIssues?: [string, any][];
}) {
const icon = passed === true ? (
<CheckCircle className="text-brandbrown w-5 h-5 shrink-0" />
) : passed === false ? (
<XCircle
className={clsx(
"w-5 h-5 shrink-0",
alert ? "text-red-600" : "text-brandblue"
)}
/>
) : (
<HelpCircle className="text-brandblue w-5 h-5 shrink-0" />
);
const icon =
passed === true ? (
<CheckCircle className="text-brandbrown w-5 h-5 shrink-0" />
) : passed === false ? (
<XCircle
className={clsx(
"w-5 h-5 shrink-0",
alert ? "text-red-600" : "text-brandblue"
)}
/>
) : (
<HelpCircle className="text-brandblue w-5 h-5 shrink-0" />
);
return (
<div className="bg-muted/50 px-3 py-2 rounded-md space-y-2">
@ -73,7 +76,10 @@ function ChecklistItem({
</div>
{roomsWithIssues.length > 0 && (
<Accordion type="multiple" className="ml-6 border-l border-muted pl-4 mt-2">
<Accordion
type="multiple"
className="ml-6 border-l border-muted pl-4 mt-2"
>
{roomsWithIssues.map(([roomName, room]: any) => (
<AccordionItem key={roomName} value={roomName}>
<AccordionTrigger>{formatRoomName(roomName)}</AccordionTrigger>
@ -81,14 +87,17 @@ function ChecklistItem({
<div className="text-sm text-muted-foreground space-y-1 px-1">
{room.room_info?.overall_condition_of_the_room && (
<div>
<strong>Condition:</strong> {room.room_info.overall_condition_of_the_room}
<strong>Condition:</strong>{" "}
{room.room_info.overall_condition_of_the_room}
</div>
)}
{room.room_info?.does_the_room_have_any_defects === "Yes" && (
<div className="flex items-start gap-2 text-brandblue">
<AlertTriangle className="w-4 h-4 mt-0.5 shrink-0" />
<div>
<div><strong>Defect reported</strong></div>
<div>
<strong>Defect reported</strong>
</div>
{room.room_info?.description_of_defect && (
<div className="text-sm text-muted-foreground">
{room.room_info.description_of_defect}
@ -97,11 +106,16 @@ function ChecklistItem({
</div>
</div>
)}
{room.room_info?.ventilation_info?.location_of_any_damp_or_mould && (
{room.room_info?.ventilation_info
?.location_of_any_damp_or_mould && (
<div className="flex items-center gap-2 text-red-600">
<AlertTriangle className="w-4 h-4 shrink-0" />
<span>
<strong>Damp/Mould Location:</strong> {room.room_info.ventilation_info.location_of_any_damp_or_mould}
<strong>Damp/Mould Location:</strong>{" "}
{
room.room_info.ventilation_info
.location_of_any_damp_or_mould
}
<span className="ml-2 text-xs text-muted-foreground italic">
(Severity: Average)
</span>
@ -140,7 +154,7 @@ export default function ConditionReport({
conditionReport: {
rooms: Record<string, any>;
[key: string]: any;
},
};
totalFloorArea: number;
currentSapPoints: number;
conditionData: PropertyDetailsEpc;
@ -151,22 +165,32 @@ export default function ConditionReport({
const roomsWithBadWindows = getRoomsWithBadWindows(allRoomData);
const windowsOk = areAllWindowsOk(allRoomData);
const elevationsWithIssues = getElevationsWithIssues(conditionReport.access_and_elevations);
const elevationsWithIssues = getElevationsWithIssues(
conditionReport.access_and_elevations
);
const {
isSufficient: enoughSpace,
occupantsToUse,
numberOfBedrooms,
areaPerPerson,
} = hasSufficientSpace(conditionReport.occupant_info, totalFloorArea, allRoomData);
} = hasSufficientSpace(conditionReport, totalFloorArea, allRoomData);
const sapOk = meetsSapThreshold(currentSapPoints);
const efficientHeating = hasEfficientHeatingSystem(conditionData.heating || "");
const insulationOk = isInsulationAdequate(conditionData.heating || "", conditionData.roofRating, conditionData.wallsRating);
const efficientHeating = hasEfficientHeatingSystem(
conditionData.heatingRating
);
const insulationOk = isInsulationAdequate(
conditionData.roofRating,
conditionData.wallsRating
);
const thermalComfortOk = efficientHeating && insulationOk;
const kitchenOk = conditionReport.rooms.kitchen?.room_info?.overall_condition_of_the_room === "Good";
const bathroomsOk = Array.isArray(conditionReport.rooms.bathrooms) &&
const kitchenOk =
conditionReport.rooms.kitchen?.room_info?.overall_condition_of_the_room ===
"Good";
const bathroomsOk =
Array.isArray(conditionReport.rooms.bathrooms) &&
conditionReport.rooms.bathrooms.length > 0 &&
conditionReport.rooms.bathrooms.every(
(b: any) => b?.room_info?.overall_condition_of_the_room === "Good"
@ -210,26 +234,41 @@ export default function ConditionReport({
<ChecklistItem
label={
conditionReport.heating_system?.general_condition?.is_the_heating_system_in_working_order
conditionReport.heating_system?.general_condition
?.is_the_heating_system_in_working_order
? "Heating system operational"
: "Heating system not operational"
}
passed={conditionReport.heating_system?.general_condition?.is_the_heating_system_in_working_order}
alert={!conditionReport.heating_system?.general_condition?.is_the_heating_system_in_working_order}
passed={
conditionReport.heating_system?.general_condition
?.is_the_heating_system_in_working_order
}
alert={
!conditionReport.heating_system?.general_condition
?.is_the_heating_system_in_working_order
}
/>
<ChecklistItem
label={sapOk ? "SAP rating meets minimum threshold (≥ 35)" : "SAP rating below recommended minimum"}
label={
sapOk
? "SAP rating meets minimum threshold (≥ 35)"
: "SAP rating below recommended minimum"
}
passed={sapOk}
alert={!sapOk}
note={`SAP Points: ${currentSapPoints}`}
/>
<ChecklistItem
label={thermalComfortOk ? "Thermal comfort conditions met" : "Thermal comfort conditions not met"}
label={
thermalComfortOk
? "Thermal comfort conditions met"
: "Thermal comfort conditions not met"
}
passed={thermalComfortOk}
alert={!thermalComfortOk}
note={`Heating: ${conditionData.heating}; Roof Rating: ${conditionData.roofRating ?? "N/A"}; Wall Rating: ${conditionData.wallsRating ?? "N/A"}`}
note={`Heating: ${conditionData.heating}; Roof Rating: ${RatingMap[conditionData.roofRating as Rating] ?? "N/A"}; Wall Rating: ${RatingMap[conditionData.wallsRating as Rating] ?? "N/A"}`}
/>
</div>
@ -239,7 +278,11 @@ export default function ConditionReport({
</h3>
<ChecklistItem
label={hasDefects ? "Room defects present" : "No room defects present"}
label={
hasDefects
? "Room defects present"
: "No room defects present"
}
passed={!hasDefects}
alert={hasDefects}
roomsWithIssues={roomsWithDefects}
@ -257,13 +300,21 @@ export default function ConditionReport({
/>
<ChecklistItem
label={kitchenOk ? "Kitchen in good condition" : "Kitchen not in good condition"}
label={
kitchenOk
? "Kitchen in good condition"
: "Kitchen not in good condition"
}
passed={kitchenOk}
alert={!kitchenOk}
/>
<ChecklistItem
label={bathroomsOk ? "Bathrooms in good condition" : "Bathrooms not in good condition"}
label={
bathroomsOk
? "Bathrooms in good condition"
: "Bathrooms not in good condition"
}
passed={bathroomsOk}
alert={!bathroomsOk}
/>
@ -272,7 +323,7 @@ export default function ConditionReport({
label="Sufficient space for number of occupants"
passed={enoughSpace}
alert={!enoughSpace}
note={`${occupantsToUse} occupants, ${numberOfBedrooms} bedrooms. ${areaPerPerson}m² per person`}
note={`${occupantsToUse} occupants, ${numberOfBedrooms} bedrooms. ${areaPerPerson.toFixed(1)}m² per person`}
/>
</div>
</div>

View file

@ -0,0 +1,137 @@
// utils/decentHomeChecks.ts
export function getAllRoomData(rooms: Record<string, any>) {
return [
...Object.entries(rooms).filter(([_, v]) => v?.room_info),
...(rooms.bedrooms || []).map((b: any, i: number) => [
"Bedroom " + (i + 1),
b,
]),
...(rooms.bathrooms || []).map((b: any, i: number) => [
"Bathroom " + (i + 1),
b,
]),
];
}
export function getRoomsWithDamp(allRoomData: [string, any][]) {
return allRoomData.filter(
([, room]: any) =>
room.room_info?.ventilation_info
?.are_there_any_visible_or_reported_signs_of_damp_mould_or_excessive_condensation_within_the_room
);
}
export function getRoomsWithDefects(allRoomData: [string, any][]) {
return allRoomData.filter(
([, room]: any) => room.room_info?.does_the_room_have_any_defects === "Yes"
);
}
export function getRoomsWithBadWindows(allRoomData: [string, any][]) {
return allRoomData.filter(([, room]: any) => {
const wi = room.room_info?.windows_info;
return (
wi?.does_the_room_have_any_windows &&
wi.condition_of_the_windows !== "Good condition"
);
});
}
export function areAllWindowsOk(allRoomData: [string, any][]) {
return allRoomData.every(([, room]: any) => {
const wi = room.room_info?.windows_info;
return wi?.does_the_room_have_any_windows
? wi?.condition_of_the_windows === "Good condition"
: true;
});
}
export function getElevationsWithIssues(conditionReport: any): [string, any][] {
const frontElevation =
conditionReport.access_and_elevations?.external_elevation_front
?.external_elevation;
const applyFrontDefaults = (
label: string,
elevationKey: string
): [string, any] => {
const section = conditionReport.access_and_elevations?.[elevationKey];
if (section?.do_all_answers_for_the_front_elevation_apply_to_this_wall) {
return [label, frontElevation];
}
return [label, section?.external_elevation ?? null];
};
const allElevations: [string, any][] = [
["Front Elevation", frontElevation],
applyFrontDefaults("Back Elevation", "external_elevation_back"),
applyFrontDefaults("Gable One", "external_elevation_gable_one"),
applyFrontDefaults("Gable Two", "external_elevation_gable_two"),
];
return allElevations.filter(
([_, elevation]) =>
elevation &&
(elevation.does_any_structural_defect_need_resolving_before_retrofit ===
true ||
elevation.are_there_any_signs_of_water_penetration_caused_by_failed_rainwater_goods_or_pipework ===
true ||
elevation.are_there_any_visible_signs_of_movement === true ||
elevation.are_there_any_visible_signs_of_cracking_to_the_existing_external_finish ===
true)
);
}
export function hasSufficientSpace(
conditionReport: any,
totalFloorArea: number,
allRoomData: [string, any][]
) {
const totalOccupants =
conditionReport.occupant_info?.total_number_of_occupants ?? 0;
const totalAdults = conditionReport.occupant_info?.no_of_adult_occupants ?? 0;
const totalChildren =
conditionReport.occupant_info?.no_of_child_occupants ?? 0;
const occupantsToUse = Math.max(totalAdults + totalChildren, totalOccupants);
console.log("totalOccupants", totalOccupants);
console.log("occupantsToUse", occupantsToUse);
const numberOfBedrooms = allRoomData.filter(([label]) =>
label.includes("Bedroom")
).length;
const getRecommendedOccupants = (bedrooms: number) => {
if (bedrooms <= 0) return 0;
if (bedrooms === 1) return 2;
if (bedrooms === 2) return 4;
if (bedrooms === 3) return 6;
return 7;
};
const maxOccupants = getRecommendedOccupants(numberOfBedrooms);
const areaPerPerson = totalFloorArea / occupantsToUse;
const isSufficient = occupantsToUse <= maxOccupants && areaPerPerson >= 20;
return { isSufficient, occupantsToUse, numberOfBedrooms, areaPerPerson };
}
export function hasEfficientHeatingSystem(
heatingRating: number | null
): boolean {
return heatingRating != null && heatingRating >= 3;
}
export function isInsulationAdequate(
roofRating: number | null,
wallRating: number | null
): boolean {
return (
(roofRating != null && roofRating >= 3) ||
(wallRating != null && wallRating >= 3)
);
}
export function meetsSapThreshold(currentSapPoints: number): boolean {
return currentSapPoints >= 35;
}

View file

@ -21,7 +21,7 @@ import {
getSpatialData,
getNonIntrusiveSurvey,
getDocument,
getEnergyAssessmentFromS3
getEnergyAssessmentFromS3,
} from "../utils";
import ConditionReport from "@/app/portfolio/[slug]/building-passport/[propertyId]/assessment/ConditionReport";
@ -122,11 +122,9 @@ const formatDate = (dateString: Date) => {
});
};
export default async function PreAssessmentReport(
props: {
params: Promise<{ slug: string; propertyId: string }>;
}
) {
export default async function PreAssessmentReport(props: {
params: Promise<{ slug: string; propertyId: string }>;
}) {
const params = await props.params;
const propertyMeta = await getPropertyMeta(params.propertyId);
const conditionReportData = await getConditionReport(params.propertyId);
@ -135,14 +133,19 @@ export default async function PreAssessmentReport(
conditionReportData,
propertyMeta.propertyType
);
const conditionReportMeta = await getDocument(
{ uprn: String(propertyMeta.uprn), documentType: "ECO_CONDITION_REPORT" }
);
const conditionReportMeta = await getDocument({
uprn: String(propertyMeta.uprn),
documentType: "ECO_CONDITION_REPORT",
});
let conditionReport = { rooms: {} };
if (conditionReportMeta && conditionReportMeta.s3JsonUri) {
conditionReport = await getEnergyAssessmentFromS3(conditionReportMeta.s3JsonUri);
conditionReport = await getEnergyAssessmentFromS3(
conditionReportMeta.s3JsonUri
);
}
console.log("conditionReport", conditionReport);
const nonIntrusiveSurvey = await getNonIntrusiveSurvey(propertyMeta.uprn);
const retrofitFeatures = formatRetrofitFeatures(conditionReportData);
@ -176,15 +179,14 @@ export default async function PreAssessmentReport(
</div>
</div>
{
Object.keys(conditionReportMeta).length > 0 &&
<ConditionReport
conditionReport={conditionReport}
totalFloorArea={conditionReportData.totalFloorArea}
{Object.keys(conditionReportMeta).length > 0 && (
<ConditionReport
conditionReport={conditionReport}
totalFloorArea={conditionReportData.totalFloorArea}
currentSapPoints={propertyMeta.currentSapPoints}
conditionData={conditionReportData}
/>
}
)}
{nonIntrusiveSurvey && (
<div>

View file

@ -0,0 +1,118 @@
"use client"
import * as React from "react"
import { Drawer as DrawerPrimitive } from "vaul"
import { cn } from "@/lib/utils"
const Drawer = ({
shouldScaleBackground = true,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Root>) => (
<DrawerPrimitive.Root
shouldScaleBackground={shouldScaleBackground}
{...props}
/>
)
Drawer.displayName = "Drawer"
const DrawerTrigger = DrawerPrimitive.Trigger
const DrawerPortal = DrawerPrimitive.Portal
const DrawerClose = DrawerPrimitive.Close
const DrawerOverlay = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Overlay
ref={ref}
className={cn("fixed inset-0 z-50 bg-black/80", className)}
{...props}
/>
))
DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName
const DrawerContent = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DrawerPortal>
<DrawerOverlay />
<DrawerPrimitive.Content
ref={ref}
className={cn(
"fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background",
className
)}
{...props}
>
<div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
{children}
</DrawerPrimitive.Content>
</DrawerPortal>
))
DrawerContent.displayName = "DrawerContent"
const DrawerHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)}
{...props}
/>
)
DrawerHeader.displayName = "DrawerHeader"
const DrawerFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props}
/>
)
DrawerFooter.displayName = "DrawerFooter"
const DrawerTitle = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
DrawerTitle.displayName = DrawerPrimitive.Title.displayName
const DrawerDescription = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
DrawerDescription.displayName = DrawerPrimitive.Description.displayName
export {
Drawer,
DrawerPortal,
DrawerOverlay,
DrawerTrigger,
DrawerClose,
DrawerContent,
DrawerHeader,
DrawerFooter,
DrawerTitle,
DrawerDescription,
}