From c5af2010445ccdf7c4954df9afde4fa2e84a0018 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Tue, 26 Aug 2025 12:11:21 +0000 Subject: [PATCH] added missing pages --- .../assessment/ConditionReport.tsx | 221 ++++++++++++++++++ .../[propertyId]/assessment/page.tsx | 199 ++++++++++++++++ src/app/shadcn_components/ui/accordion.tsx | 58 +++++ 3 files changed, 478 insertions(+) create mode 100644 src/app/portfolio/[slug]/building-passport/[propertyId]/assessment/ConditionReport.tsx create mode 100644 src/app/portfolio/[slug]/building-passport/[propertyId]/assessment/page.tsx create mode 100644 src/app/shadcn_components/ui/accordion.tsx diff --git a/src/app/portfolio/[slug]/building-passport/[propertyId]/assessment/ConditionReport.tsx b/src/app/portfolio/[slug]/building-passport/[propertyId]/assessment/ConditionReport.tsx new file mode 100644 index 0000000..2186454 --- /dev/null +++ b/src/app/portfolio/[slug]/building-passport/[propertyId]/assessment/ConditionReport.tsx @@ -0,0 +1,221 @@ +"use client"; + +import { + Card, + CardContent, + CardHeader, +} from "@/app/shadcn_components/ui/card"; +import { + CheckCircle, + XCircle, + HelpCircle, +} from "lucide-react"; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@/app/shadcn_components/ui/accordion"; +import clsx from "clsx"; + +function ChecklistItem({ + label, + passed, + note, + alert = false, + roomsWithIssues = [], +}: { + label: string; + passed?: boolean; + note?: string; + alert?: boolean; + roomsWithIssues?: [string, any][]; +}) { + const icon = passed === true ? ( + + ) : passed === false ? ( + + ) : ( + + ); + + return ( +
+
+ {icon} + + {label} + {note && ( + + ({note}) + + )} + +
+ + {roomsWithIssues.length > 0 && ( + + {roomsWithIssues.map(([roomName, room]: any) => ( + + {formatRoomName(roomName)} + +
+ {room.room_info?.overall_condition_of_the_room && ( +
+ Condition: {room.room_info.overall_condition_of_the_room} +
+ )} + {room.room_info?.does_the_room_have_any_defects && ( +
+ Defects: {room.room_info.does_the_room_have_any_defects} +
+ )} + {room.room_info?.ventilation_info + ?.are_there_any_visible_or_reported_signs_of_damp_mould_or_excessive_condensation_within_the_room !== + undefined && ( +
+ Damp/Mould:{" "} + {room.room_info.ventilation_info + .are_there_any_visible_or_reported_signs_of_damp_mould_or_excessive_condensation_within_the_room + ? "Yes" + : "No"} +
+ )} + {room.room_info?.windows_info?.condition_of_the_windows && ( +
+ Window Condition:{" "} + {room.room_info.windows_info.condition_of_the_windows} +
+ )} +
+
+
+ ))} +
+ )} +
+ ); +} + +function formatRoomName(name: string) { + return name + .replaceAll("_", " ") + .replace(/\b\w/g, (l) => l.toUpperCase()) + .replace("Room Info", "Room"); +} + +export default function ConditionReport({ + conditionReport, +}: { + conditionReport: { + rooms: Record; + [key: string]: any; + } +}) { + const rooms = conditionReport.rooms; + + const allRoomData = [ + ...Object.entries(rooms).filter(([k, 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]), + ]; + + const hasDampIssues = allRoomData.some( + ([, room]: any) => + room.room_info?.ventilation_info + ?.are_there_any_visible_or_reported_signs_of_damp_mould_or_excessive_condensation_within_the_room + ); + + const hasDefects = allRoomData.some( + ([, room]: any) => room.room_info?.does_the_room_have_any_defects === "Yes" + ); + + const windowsOk = 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; + }); + + const heatingWorking = + conditionReport.heating_system?.general_condition + ?.is_the_heating_system_in_working_order === true; + + const kitchenOk = rooms.kitchen?.room_info?.overall_condition_of_the_room === "Good"; + + const bathroomsOk = Array.isArray(rooms.bathrooms) && + rooms.bathrooms.length > 0 && + rooms.bathrooms.every( + (b: any) => + b?.room_info?.overall_condition_of_the_room === "Good" + ) + + const roomsWithDefects = allRoomData.filter( + ([, room]: any) => room.room_info?.does_the_room_have_any_defects === "Yes" + ); + + const roomsWithDamp = 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 + ); + + const roomsWithBadWindows = 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"; + } + ); + + return ( +
+ + + Decent Homes Checklist + + + 0} + roomsWithIssues={roomsWithDamp} + /> + + + + + + + + + +
+ ); +} diff --git a/src/app/portfolio/[slug]/building-passport/[propertyId]/assessment/page.tsx b/src/app/portfolio/[slug]/building-passport/[propertyId]/assessment/page.tsx new file mode 100644 index 0000000..7368036 --- /dev/null +++ b/src/app/portfolio/[slug]/building-passport/[propertyId]/assessment/page.tsx @@ -0,0 +1,199 @@ +import EpcCard from "@/app/components/building-passport/EpcCard"; +import FeatureTable from "@/app/components/building-passport/FeatureTable"; +import { + ConditionReportData, + PropertyDetailsEpc, + PropertyDetailsSpatial, + PropertyMeta, +} from "@/app/db/schema/property"; +import { formatDateTime } from "@/app/utils"; +import { + generalColumns, + nonInstrusiveColumns, + retrofitColumns, +} from "@/app/components/building-passport/FeatureTableColumns"; +import { + formatGeneralFeatures, + formatHeatDemandFeatures, + formatRetrofitFeatures, + getConditionReport, + getPropertyMeta, + getSpatialData, + getNonIntrusiveSurvey, + getDocument, + getEnergyAssessmentFromS3 +} from "../utils"; +import ConditionReport from "@/app/portfolio/[slug]/building-passport/[propertyId]/assessment/ConditionReport"; + +interface PropertyDetailsCardProps { + conditionReportData: PropertyDetailsEpc; + propertyMeta: PropertyMeta; + propertyDetailsSpatial: PropertyDetailsSpatial; +} + +const rowTitleStyle = "text-brandblue align-top pb-3"; +const rowValueStyle = "text-brandblue text-end pr-8 pt-1 align-top pb-3"; + +function PropertyDetailsCard({ + conditionReportData, + propertyMeta, + propertyDetailsSpatial, +}: PropertyDetailsCardProps) { + const propertyText = [propertyMeta.builtForm, propertyMeta.propertyType] + .filter(Boolean) + .join(" "); + + return ( +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
Year built:{propertyMeta.yearBuilt}
Property Type:{propertyText}
Total floor area: + {`${conditionReportData.totalFloorArea} m`} + 2 +
In conservation area: + {propertyDetailsSpatial.conservationStatus ? "Yes" : "No"} +
Is listed: + {propertyDetailsSpatial.isListedBuilding ? "Yes" : "No"} +
Is heritage: + {propertyDetailsSpatial.isHeritageBuilding ? "Yes" : "No"} +
+
+ + + + + + + + + + + + + + + + + + + +
Local Authority:{propertyMeta.localAuthority}
Constituency:{propertyMeta.constituency}
Tenure{propertyMeta.tenure}
Number of Habitable Rooms: + {propertyMeta.numberOfRooms || "unkown"} +
+
+
+ ); +} + +const formatDate = (dateString: Date) => { + const date = new Date(dateString); + return date.toLocaleDateString("en-GB", { + weekday: "long", // "Monday" through "Sunday" + year: "numeric", // "2024" + month: "long", // "January" through "December" + day: "numeric", // "1", "2", ..., "31" + }); +}; + +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); + const propertyDetailsSpatial = await getSpatialData(propertyMeta.uprn); + const generalFeatures = formatGeneralFeatures( + conditionReportData, + propertyMeta.propertyType + ); + const conditionReportMeta = await getDocument( + { uprn: String(propertyMeta.uprn), documentType: "ECO_CONDITION_REPORT" } + ); + let conditionReport = { rooms: {} }; + if (conditionReportMeta && conditionReportMeta.s3JsonUri) { + conditionReport = await getEnergyAssessmentFromS3(conditionReportMeta.s3JsonUri); + } + + console.log("conditionReport", conditionReport.rooms.kitchen) + + const nonIntrusiveSurvey = await getNonIntrusiveSurvey(propertyMeta.uprn); + + const retrofitFeatures = formatRetrofitFeatures(conditionReportData); + + const heatingDemand = formatHeatDemandFeatures(conditionReportData); + + return ( +
+
+ Last updated: {formatDateTime(propertyMeta.updatedAt)} +
+
+
+ + + +
+
+ + { + Object.keys(conditionReportMeta).length > 0 && + } + + {nonIntrusiveSurvey && ( +
+
Non-Intrusive Survey
+
+ Conducted by: {nonIntrusiveSurvey.surveyor} on{" "} + {formatDate(nonIntrusiveSurvey.surveyDate)} +
+ +
+ )} +
General Features
+ +
Existing Property Features
+ +
Heating Demand
+ +
+ ); +} diff --git a/src/app/shadcn_components/ui/accordion.tsx b/src/app/shadcn_components/ui/accordion.tsx new file mode 100644 index 0000000..7511978 --- /dev/null +++ b/src/app/shadcn_components/ui/accordion.tsx @@ -0,0 +1,58 @@ +"use client" + +import * as React from "react" +import * as AccordionPrimitive from "@radix-ui/react-accordion" +import { ChevronDown } from "lucide-react" + +import { cn } from '@/lib/utils' + +const Accordion = AccordionPrimitive.Root + +const AccordionItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AccordionItem.displayName = "AccordionItem" + +const AccordionTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + svg]:rotate-180", + className + )} + {...props} + > + {children} + + + +)) +AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName + +const AccordionContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + +
{children}
+
+)) + +AccordionContent.displayName = AccordionPrimitive.Content.displayName + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }