mirror of
https://github.com/Hestia-Homes/assessment-model.git
synced 2026-06-08 11:37:25 +00:00
added missing pages
This commit is contained in:
parent
ce1a78005a
commit
c5af201044
3 changed files with 478 additions and 0 deletions
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
58
src/app/shadcn_components/ui/accordion.tsx
Normal file
58
src/app/shadcn_components/ui/accordion.tsx
Normal 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 }
|
||||
Loading…
Add table
Reference in a new issue