added missing pages

This commit is contained in:
Khalim Conn-Kowlessar 2025-08-26 12:11:21 +00:00
parent ce1a78005a
commit c5af201044
3 changed files with 478 additions and 0 deletions

View file

@ -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 ? (
<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">
<div className="flex items-center gap-3">
{icon}
<span className="text-sm font-medium text-foreground">
{label}
{note && (
<span className="ml-1 text-xs text-muted-foreground font-normal">
({note})
</span>
)}
</span>
</div>
{roomsWithIssues.length > 0 && (
<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>
<AccordionContent>
<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}
</div>
)}
{room.room_info?.does_the_room_have_any_defects && (
<div>
<strong>Defects:</strong> {room.room_info.does_the_room_have_any_defects}
</div>
)}
{room.room_info?.ventilation_info
?.are_there_any_visible_or_reported_signs_of_damp_mould_or_excessive_condensation_within_the_room !==
undefined && (
<div>
<strong>Damp/Mould:</strong>{" "}
{room.room_info.ventilation_info
.are_there_any_visible_or_reported_signs_of_damp_mould_or_excessive_condensation_within_the_room
? "Yes"
: "No"}
</div>
)}
{room.room_info?.windows_info?.condition_of_the_windows && (
<div>
<strong>Window Condition:</strong>{" "}
{room.room_info.windows_info.condition_of_the_windows}
</div>
)}
</div>
</AccordionContent>
</AccordionItem>
))}
</Accordion>
)}
</div>
);
}
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<string, any>;
[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 (
<div className="space-y-6 mt-8">
<Card>
<CardHeader className="text-lg font-semibold text-brandblue">
Decent Homes Checklist
</CardHeader>
<CardContent className="space-y-3">
<ChecklistItem
label="No signs of damp or mould"
passed={roomsWithDamp.length === 0}
alert={roomsWithDamp.length > 0}
roomsWithIssues={roomsWithDamp}
/>
<ChecklistItem
label={hasDefects ? "Room defects present" : "No room defects present"}
passed={!hasDefects}
alert={hasDefects}
roomsWithIssues={roomsWithDefects}
/>
<ChecklistItem
label={heatingWorking ? "Heating system operational" : "Heating system not operational"}
passed={heatingWorking}
alert={!heatingWorking}
/>
<ChecklistItem
label={windowsOk ? "Windows in good condition" : "Windows not in good condition"}
passed={windowsOk}
alert={!windowsOk}
roomsWithIssues={roomsWithBadWindows}
/>
<ChecklistItem
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"}
passed={bathroomsOk}
alert={!bathroomsOk}
/>
</CardContent>
</Card>
</div>
);
}

View file

@ -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 (
<div className="w-full flex flex-col items-center p-4 shadow rounded-md justify-start bg-gray-100">
<div className="grid grid-cols-2 gap-8 text-m w-full h-full text-sm">
<div className="border-r">
<table className="w-full ">
<tbody>
<tr>
<td className={rowTitleStyle}>Year built:</td>
<td className={rowValueStyle}>{propertyMeta.yearBuilt}</td>
</tr>
<tr>
<td className={rowTitleStyle}>Property Type:</td>
<td className={rowValueStyle}>{propertyText}</td>
</tr>
<tr>
<td className={rowTitleStyle}>Total floor area:</td>
<td className={rowValueStyle}>
{`${conditionReportData.totalFloorArea} m`}
<sup>2</sup>
</td>
</tr>
<tr>
<td className={rowTitleStyle}>In conservation area:</td>
<td className={rowValueStyle}>
{propertyDetailsSpatial.conservationStatus ? "Yes" : "No"}
</td>
</tr>
<tr>
<td className={rowTitleStyle}>Is listed:</td>
<td className={rowValueStyle}>
{propertyDetailsSpatial.isListedBuilding ? "Yes" : "No"}
</td>
</tr>
<tr>
<td className={rowTitleStyle}>Is heritage:</td>
<td className={rowValueStyle}>
{propertyDetailsSpatial.isHeritageBuilding ? "Yes" : "No"}
</td>
</tr>
</tbody>
</table>
</div>
<table className="w-full">
<tbody>
<tr>
<td className={rowTitleStyle}>Local Authority:</td>
<td className={rowValueStyle}>{propertyMeta.localAuthority}</td>
</tr>
<tr>
<td className={rowTitleStyle}>Constituency:</td>
<td className={rowValueStyle}>{propertyMeta.constituency}</td>
</tr>
<tr>
<td className={rowTitleStyle}>Tenure</td>
<td className={rowValueStyle}>{propertyMeta.tenure}</td>
</tr>
<tr>
<td className={rowTitleStyle}>Number of Habitable Rooms:</td>
<td className={rowValueStyle}>
{propertyMeta.numberOfRooms || "unkown"}
</td>
</tr>
</tbody>
</table>
</div>
</div>
);
}
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 (
<div className="leading-loose tracking-wider">
<div className="text-gray-700 text-sm mt-4">
Last updated: {formatDateTime(propertyMeta.updatedAt)}
</div>
<div className="flex flex-col items-stretch mb-4">
<div className="flex flex-row justify-start mt-4 space-x-4">
<EpcCard
epcRating={propertyMeta.currentEpcRating}
fullMargin={false}
/>
<PropertyDetailsCard
conditionReportData={conditionReportData}
propertyMeta={propertyMeta}
propertyDetailsSpatial={propertyDetailsSpatial}
/>
</div>
</div>
{
Object.keys(conditionReportMeta).length > 0 && <ConditionReport conditionReport={conditionReport} />
}
{nonIntrusiveSurvey && (
<div>
<div className="flex py-8 text-lg">Non-Intrusive Survey</div>
<div className="flex mb-2 text-sm text-gray-500">
Conducted by: {nonIntrusiveSurvey.surveyor} on{" "}
{formatDate(nonIntrusiveSurvey.surveyDate)}
</div>
<FeatureTable
data={nonIntrusiveSurvey.notes}
columns={nonInstrusiveColumns}
/>
</div>
)}
<div className="flex py-8 text-lg">General Features</div>
<FeatureTable data={generalFeatures} columns={generalColumns} />
<div className="flex py-8 text-lg">Existing Property Features</div>
<FeatureTable data={retrofitFeatures} columns={retrofitColumns} />
<div className="flex py-8 text-lg">Heating Demand</div>
<FeatureTable data={heatingDemand} columns={generalColumns} />
</div>
);
}

View file

@ -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<typeof AccordionPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
>(({ className, ...props }, ref) => (
<AccordionPrimitive.Item
ref={ref}
className={cn("border-b", className)}
{...props}
/>
))
AccordionItem.displayName = "AccordionItem"
const AccordionTrigger = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
ref={ref}
className={cn(
"flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
className
)}
{...props}
>
{children}
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
))
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
const AccordionContent = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Content
ref={ref}
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
{...props}
>
<div className={cn("pb-4 pt-0", className)}>{children}</div>
</AccordionPrimitive.Content>
))
AccordionContent.displayName = AccordionPrimitive.Content.displayName
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }