Merge pull request #82 from Hestia-Homes/feature/condition

Added decent homes dashboard
This commit is contained in:
KhalimCK 2025-08-26 23:54:14 +01:00 committed by GitHub
commit 0a4b2a25e3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 1145 additions and 216 deletions

View file

@ -13,9 +13,13 @@
"customizations": {
"vscode": {
"settings": {
"files.defaultWorkspace": "/workspaces/assessment-model"
"files.defaultWorkspace": "/workspaces/assessment-model",
"editor.formatOnSave": true,
"editor.tabSize": 2,
"editor.insertSpaces": true
},
"extensions": [
"esbenp.prettier-vscode"
]
}
}

168
package-lock.json generated
View file

@ -14,7 +14,7 @@
"@hookform/resolvers": "^3.9.1",
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-dialog": "^1.0.4",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.0.5",
"@radix-ui/react-hover-card": "^1.0.6",
"@radix-ui/react-label": "^2.1.0",
@ -39,7 +39,6 @@
"drizzle-kit": "^0.31.4",
"drizzle-orm": "^0.44.3",
"esbuild": "^0.25.8",
"eslint": "8.41.0",
"eslint-config-next": "13.4.3",
"lucide-react": "^0.233.0",
"next": "^15.4.2",
@ -56,6 +55,7 @@
"tailwindcss-animate": "^1.0.6",
"tsx": "^4.20.3",
"typescript": "5.0.4",
"vaul": "^1.1.2",
"xlsx": "^0.18.5",
"zod": "^3.23.8"
},
@ -66,6 +66,8 @@
"cypress": "^14.5.3",
"cypress-social-logins": "^1.14.1",
"dotenv": "^16.3.1",
"eslint": "^8.57.1",
"prettier": "^3.6.2",
"start-server-and-test": "^2.0.0"
}
},
@ -1733,9 +1735,9 @@
}
},
"node_modules/@eslint/js": {
"version": "8.41.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.41.0.tgz",
"integrity": "sha512-LxcyMGxwmTh2lY9FwHPGWOHmYFCZvbrFCBZL4FzSSsxsRPuhrYUg/49/0KDfW8tnIEaEHtfmn6+NPN+1DqaNmA==",
"version": "8.57.1",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz",
"integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==",
"license": "MIT",
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
@ -1850,13 +1852,13 @@
}
},
"node_modules/@humanwhocodes/config-array": {
"version": "0.11.14",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
"integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==",
"version": "0.13.0",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
"integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==",
"deprecated": "Use @eslint/config-array instead",
"license": "Apache-2.0",
"dependencies": {
"@humanwhocodes/object-schema": "^2.0.2",
"@humanwhocodes/object-schema": "^2.0.3",
"debug": "^4.3.1",
"minimatch": "^3.0.5"
},
@ -2942,20 +2944,20 @@
}
},
"node_modules/@radix-ui/react-dialog": {
"version": "1.1.14",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.14.tgz",
"integrity": "sha512-+CpweKjqpzTmwRwcYECQcNYbI8V9VSQt0SNFKeEBLgfucbsLssU6Ppq7wUdNXEGb573bMjFhVjKVll8rmV6zMw==",
"version": "1.1.15",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz",
"integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.2",
"@radix-ui/primitive": "1.1.3",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-dismissable-layer": "1.1.10",
"@radix-ui/react-focus-guards": "1.1.2",
"@radix-ui/react-dismissable-layer": "1.1.11",
"@radix-ui/react-focus-guards": "1.1.3",
"@radix-ui/react-focus-scope": "1.1.7",
"@radix-ui/react-id": "1.1.1",
"@radix-ui/react-portal": "1.1.9",
"@radix-ui/react-presence": "1.1.4",
"@radix-ui/react-presence": "1.1.5",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-slot": "1.2.3",
"@radix-ui/react-use-controllable-state": "1.2.2",
@ -2977,6 +2979,78 @@
}
}
},
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/primitive": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
"integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
"license": "MIT"
},
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-dismissable-layer": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz",
"integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.3",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-callback-ref": "1.1.1",
"@radix-ui/react-use-escape-keydown": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-focus-guards": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz",
"integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-presence": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz",
"integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-direction": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
@ -5480,6 +5554,12 @@
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@ungap/structured-clone": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
"integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
"license": "ISC"
},
"node_modules/@unrs/resolver-binding-android-arm-eabi": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz",
@ -8145,28 +8225,29 @@
}
},
"node_modules/eslint": {
"version": "8.41.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.41.0.tgz",
"integrity": "sha512-WQDQpzGBOP5IrXPo4Hc0814r4/v2rrIsB0rhT7jtunIalgg6gYXWhRMOejVO8yH21T/FGaxjmFjBMNqcIlmH1Q==",
"version": "8.57.1",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz",
"integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==",
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.4.0",
"@eslint/eslintrc": "^2.0.3",
"@eslint/js": "8.41.0",
"@humanwhocodes/config-array": "^0.11.8",
"@eslint-community/regexpp": "^4.6.1",
"@eslint/eslintrc": "^2.1.4",
"@eslint/js": "8.57.1",
"@humanwhocodes/config-array": "^0.13.0",
"@humanwhocodes/module-importer": "^1.0.1",
"@nodelib/fs.walk": "^1.2.8",
"ajv": "^6.10.0",
"@ungap/structured-clone": "^1.2.0",
"ajv": "^6.12.4",
"chalk": "^4.0.0",
"cross-spawn": "^7.0.2",
"debug": "^4.3.2",
"doctrine": "^3.0.0",
"escape-string-regexp": "^4.0.0",
"eslint-scope": "^7.2.0",
"eslint-visitor-keys": "^3.4.1",
"espree": "^9.5.2",
"eslint-scope": "^7.2.2",
"eslint-visitor-keys": "^3.4.3",
"espree": "^9.6.1",
"esquery": "^1.4.2",
"esutils": "^2.0.2",
"fast-deep-equal": "^3.1.3",
@ -8176,7 +8257,6 @@
"globals": "^13.19.0",
"graphemer": "^1.4.0",
"ignore": "^5.2.0",
"import-fresh": "^3.0.0",
"imurmurhash": "^0.1.4",
"is-glob": "^4.0.0",
"is-path-inside": "^3.0.3",
@ -8186,9 +8266,8 @@
"lodash.merge": "^4.6.2",
"minimatch": "^3.1.2",
"natural-compare": "^1.4.0",
"optionator": "^0.9.1",
"optionator": "^0.9.3",
"strip-ansi": "^6.0.1",
"strip-json-comments": "^3.1.0",
"text-table": "^0.2.0"
},
"bin": {
@ -11735,6 +11814,22 @@
"node": ">= 0.8.0"
}
},
"node_modules/prettier": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
"dev": true,
"license": "MIT",
"bin": {
"prettier": "bin/prettier.cjs"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/pretty-bytes": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz",
@ -14075,6 +14170,19 @@
"uuid": "dist/bin/uuid"
}
},
"node_modules/vaul": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vaul/-/vaul-1.1.2.tgz",
"integrity": "sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-dialog": "^1.1.1"
},
"peerDependencies": {
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc"
}
},
"node_modules/verror": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",

View file

@ -20,7 +20,7 @@
"@hookform/resolvers": "^3.9.1",
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-dialog": "^1.0.4",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.0.5",
"@radix-ui/react-hover-card": "^1.0.6",
"@radix-ui/react-label": "^2.1.0",
@ -45,7 +45,6 @@
"drizzle-kit": "^0.31.4",
"drizzle-orm": "^0.44.3",
"esbuild": "^0.25.8",
"eslint": "8.41.0",
"eslint-config-next": "13.4.3",
"lucide-react": "^0.233.0",
"next": "^15.4.2",
@ -62,6 +61,7 @@
"tailwindcss-animate": "^1.0.6",
"tsx": "^4.20.3",
"typescript": "5.0.4",
"vaul": "^1.1.2",
"xlsx": "^0.18.5",
"zod": "^3.23.8"
},
@ -72,6 +72,8 @@
"cypress": "^14.5.3",
"cypress-social-logins": "^1.14.1",
"dotenv": "^16.3.1",
"eslint": "^8.57.1",
"prettier": "^3.6.2",
"start-server-and-test": "^2.0.0"
}
}

View file

@ -70,15 +70,15 @@ export function Toolbar({ propertyId, portfolioId, conditionReport }: ToolbarPro
</NavigationMenuLink>
);
const energyAssessmentsReportButton = (
<NavigationMenuLink
className={navigationMenuTriggerStyle() + " ml-3 mr-2"}
href={`/portfolio/${portfolioId}/building-passport/${propertyId}/energy-assessment`}
>
<BoltIcon className="h-4 w-4 mr-2" />
Energy Assessment
</NavigationMenuLink>
);
// const energyAssessmentsReportButton = (
// <NavigationMenuLink
// className={navigationMenuTriggerStyle() + " ml-3 mr-2"}
// href={`/portfolio/${portfolioId}/building-passport/${propertyId}/energy-assessment`}
// >
// <BoltIcon className="h-4 w-4 mr-2" />
// Energy Assessment
// </NavigationMenuLink>
// );
const documentsButton = (
<NavigationMenuLink

View file

@ -4,7 +4,7 @@ import {
Cog6ToothIcon,
BuildingOfficeIcon,
ChartBarIcon,
WrenchScrewdriverIcon,
HomeModernIcon,
} from "@heroicons/react/24/outline";
import {
NavigationMenu,
@ -43,8 +43,11 @@ export function Toolbar({ portfolioId, scenarios }: ToolbarProps) {
router.push(`/portfolio/${portfolioId}/summary`);
}
function handleClickMeasures() {
router.push(`/portfolio/${portfolioId}/measures`);
// function handleClickMeasures() {
// router.push(`/portfolio/${portfolioId}/measures`);
// }
function handleClickDecentHomes() {
router.push(`/portfolio/${portfolioId}/decent-homes`);
}
const [modalIsOpen, setModalIsOpen] = useState(false);
@ -66,16 +69,24 @@ export function Toolbar({ portfolioId, scenarios }: ToolbarProps) {
onClick={handleClickSummary}
>
<ChartBarIcon className="h-4 w-4 mr-2" />
Summary
Retrofit Summary
</NavigationMenuItem>
<NavigationMenuItem
className={navigationMenuTriggerStyle() + " ml-3 mr-2"}
onClick={handleClickDecentHomes}
>
<HomeModernIcon className="h-4 w-4 mr-2" />
Decent Homes
</NavigationMenuItem>
{/* <NavigationMenuItem
className={navigationMenuTriggerStyle() + " ml-3 mr-2"}
onClick={handleClickMeasures}
>
<WrenchScrewdriverIcon className="h-4 w-4 mr-2" />
Measures
</NavigationMenuItem>
</NavigationMenuItem> */}
<NavigationMenuItem
className={navigationMenuTriggerStyle() + " ml-3 mr-2"}

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,
@ -17,6 +9,30 @@ import {
AccordionTrigger,
} from "@/app/shadcn_components/ui/accordion";
import clsx from "clsx";
import { AlertTriangle } from "lucide-react";
import { PropertyDetailsEpc } from "@/app/db/schema/property";
import {
getAllRoomData,
getRoomsWithDamp,
getRoomsWithDefects,
getRoomsWithBadWindows,
areAllWindowsOk,
getElevationsWithIssues,
hasSufficientSpace,
hasEfficientHeatingSystem,
isInsulationAdequate,
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,
@ -31,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">
@ -59,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>
@ -67,23 +87,39 @@ 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 && (
<div>
<strong>Defects:</strong> {room.room_info.does_the_room_have_any_defects}
{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>
{room.room_info?.description_of_defect && (
<div className="text-sm text-muted-foreground">
{room.room_info.description_of_defect}
</div>
)}
</div>
</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"}
?.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
}
<span className="ml-2 text-xs text-muted-foreground italic">
(Severity: Average)
</span>
</span>
</div>
)}
{room.room_info?.windows_info?.condition_of_the_windows && (
@ -109,98 +145,57 @@ function formatRoomName(name: string) {
.replace("Room Info", "Room");
}
function getRecommendedOccupants(bedrooms: number): number {
if (bedrooms <= 0) return 0;
if (bedrooms === 1) return 2;
if (bedrooms === 2) return 4;
if (bedrooms === 3) return 6;
return 7; // 4 or more
}
export default function ConditionReport({
conditionReport,
totalFloorArea
totalFloorArea,
currentSapPoints,
conditionData,
}: {
conditionReport: {
rooms: Record<string, any>;
[key: string]: any;
},
};
totalFloorArea: number;
currentSapPoints: number;
conditionData: PropertyDetailsEpc;
}) {
const allRoomData = getAllRoomData(conditionReport.rooms);
const roomsWithDamp = getRoomsWithDamp(allRoomData);
const roomsWithDefects = getRoomsWithDefects(allRoomData);
const roomsWithBadWindows = getRoomsWithBadWindows(allRoomData);
const windowsOk = areAllWindowsOk(allRoomData);
// Documentation on decent home standards can be found here:
// https://assets.publishing.service.gov.uk/media/5a7968b740f0b63d72fc5926/138355.pdf
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 elevationsWithIssues = getElevationsWithIssues(
conditionReport.access_and_elevations
);
const hasDefects = allRoomData.some(
([, room]: any) => room.room_info?.does_the_room_have_any_defects === "Yes"
const {
isSufficient: enoughSpace,
occupantsToUse,
numberOfBedrooms,
areaPerPerson,
} = hasSufficientSpace(conditionReport, totalFloorArea, allRoomData);
const sapOk = meetsSapThreshold(currentSapPoints);
const efficientHeating = hasEfficientHeatingSystem(
conditionData.heatingRating
);
const insulationOk = isInsulationAdequate(
conditionData.roofRating,
conditionData.wallsRating
);
const thermalComfortOk = efficientHeating && insulationOk;
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 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 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";
}
);
// Check if the property has adequate space
// We've seen a case where the number of adult occupants + child occupants is greater than the total_number_of_occupants so we
// take the biggest of the two
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 numberOfBedrooms = Array.isArray(rooms.bedrooms)
? rooms.bedrooms.length
: 0;
const occupantsToUse = Math.max(totalAdults + totalChildren, totalOccupants);
const maxOccupants = getRecommendedOccupants(numberOfBedrooms);
const areaPerPerson = totalFloorArea / occupantsToUse;
const hasSufficientSpace = (occupantsToUse <= maxOccupants) && (areaPerPerson >= 20);
const hasDefects = roomsWithDefects.length > 0;
return (
<div className="space-y-6 mt-8">
@ -208,50 +203,132 @@ export default function ConditionReport({
<CardHeader className="text-lg font-semibold text-brandblue">
Decent Homes Checklist
</CardHeader>
<CardContent className="space-y-3">
<ChecklistItem
label={roomsWithDamp ? "Signs of damp or mould present": "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}
/>
<ChecklistItem
label="Sufficient space for number of occupants"
passed={hasSufficientSpace}
alert={!hasSufficientSpace}
note={`${totalOccupants} occupants, ${numberOfBedrooms} bedrooms. ${areaPerPerson}m² per person`}
/>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-3">
<h3 className="text-sm font-semibold text-muted-foreground mb-1 uppercase tracking-wide">
Category 1 Hazards
</h3>
<ChecklistItem
label={
elevationsWithIssues.length > 0
? "Structural issues identified"
: "No major structural issues identified"
}
passed={elevationsWithIssues.length === 0}
alert={elevationsWithIssues.length > 0}
roomsWithIssues={elevationsWithIssues}
/>
<ChecklistItem
label={
roomsWithDamp.length > 0
? "Signs of damp or mould present"
: "No signs of damp or mould"
}
passed={roomsWithDamp.length === 0}
alert={roomsWithDamp.length > 0}
roomsWithIssues={roomsWithDamp}
/>
<ChecklistItem
label={
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
}
/>
<ChecklistItem
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"
}
passed={thermalComfortOk}
alert={!thermalComfortOk}
note={`Heating: ${conditionData.heating}; Roof Rating: ${RatingMap[conditionData.roofRating as Rating] ?? "N/A"}; Wall Rating: ${RatingMap[conditionData.wallsRating as Rating] ?? "N/A"}`}
/>
</div>
<div className="space-y-3">
<h3 className="text-sm font-semibold text-muted-foreground mb-1 uppercase tracking-wide">
Category 2 Hazards
</h3>
<ChecklistItem
label={
hasDefects
? "Room defects present"
: "No room defects present"
}
passed={!hasDefects}
alert={hasDefects}
roomsWithIssues={roomsWithDefects}
/>
<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}
/>
<ChecklistItem
label="Sufficient space for number of occupants"
passed={enoughSpace}
alert={!enoughSpace}
note={`${occupantsToUse} occupants, ${numberOfBedrooms} bedrooms. ${areaPerPerson.toFixed(1)}m² per person`}
/>
</div>
</div>
</CardContent>
</Card>
</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,15 +133,18 @@ 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.rooms.utility)
console.log("conditionReport", conditionReport);
const nonIntrusiveSurvey = await getNonIntrusiveSurvey(propertyMeta.uprn);
@ -167,6 +168,7 @@ export default async function PreAssessmentReport(
<EpcCard
epcRating={propertyMeta.currentEpcRating}
fullMargin={false}
sap={String(propertyMeta.currentSapPoints)}
/>
<PropertyDetailsCard
@ -177,9 +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,
}