From 0a90eecc91fb4fd37fc7a0989dbb0bc4ded52e14 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Sat, 23 Aug 2025 13:35:34 +0000 Subject: [PATCH 1/4] rendered the condition report tab, only if we have the data --- .../components/building-passport/Toolbar.tsx | 18 ++++++++++++++++- src/app/db/surveyDB/schema/surveyDB.ts | 4 ++-- .../[slug]/(portfolio)/summary/page.tsx | 2 -- .../[propertyId]/documents/page.tsx | 7 +++---- .../building-passport/[propertyId]/layout.tsx | 11 ++++++++-- .../building-passport/[propertyId]/utils.ts | 20 +++++++++++++++++-- 6 files changed, 49 insertions(+), 13 deletions(-) diff --git a/src/app/components/building-passport/Toolbar.tsx b/src/app/components/building-passport/Toolbar.tsx index 57ad05d..901b8ac 100644 --- a/src/app/components/building-passport/Toolbar.tsx +++ b/src/app/components/building-passport/Toolbar.tsx @@ -16,10 +16,12 @@ import { NavigationMenuLink, } from "@/app/shadcn_components/ui/navigation-menu"; import { cva } from "class-variance-authority"; +import { getUploadedFile } from "@/app/db/surveyDB/schema/surveyDB"; interface ToolbarProps { propertyId: string; portfolioId: string; + conditionReport: getUploadedFile; } const navigationMenuTriggerStyle = cva( @@ -53,7 +55,7 @@ const navigationMenuTriggerStyle = cva( ].join(" ") ); -export function Toolbar({ propertyId, portfolioId }: ToolbarProps) { +export function Toolbar({ propertyId, portfolioId, conditionReport }: ToolbarProps) { function handleClickSettings() { console.log("Settings were clicked, implement me"); } @@ -108,6 +110,18 @@ export function Toolbar({ propertyId, portfolioId }: ToolbarProps) { ); + const conditionButton = ( + + + Condition Report + + ); + + console.log("conditionReport", conditionReport) + return ( 0 && conditionButton} {energyAssessmentsReportButton}

Summary

diff --git a/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/page.tsx b/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/page.tsx index 6811e87..1ded30a 100644 --- a/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/page.tsx +++ b/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/page.tsx @@ -2,16 +2,15 @@ import { getPropertyMeta } from "@/app/portfolio/[slug]/building-passport/[prope import { eq } from "drizzle-orm"; import { DocumentsTable } from "./DocumentsTable"; import { surveyDB } from "@/app/db/surveyDB/connection"; -import { uploaded_files } from "@/app/db/surveyDB/schema/surveyDB"; +import { uploadedFiles } from "@/app/db/surveyDB/schema/surveyDB"; import { type getUploadedFiles } from "@/app/db/surveyDB/schema/surveyDB"; -import { EmptyObject } from "react-hook-form"; async function getDocuments( uprn: number ): Promise< getUploadedFiles> { - const result = surveyDB.query.uploaded_files.findMany({ - where: eq(uploaded_files.uprn, String(uprn)), + const result = surveyDB.query.uploadedFiles.findMany({ + where: eq(uploadedFiles.uprn, String(uprn)), }); return result; diff --git a/src/app/portfolio/[slug]/building-passport/[propertyId]/layout.tsx b/src/app/portfolio/[slug]/building-passport/[propertyId]/layout.tsx index 02686b0..43f4c92 100644 --- a/src/app/portfolio/[slug]/building-passport/[propertyId]/layout.tsx +++ b/src/app/portfolio/[slug]/building-passport/[propertyId]/layout.tsx @@ -1,5 +1,5 @@ import { Toolbar } from "@/app/components/building-passport/Toolbar"; -import { getPropertyMeta } from "./utils"; +import { getPropertyMeta, getDocument } from "./utils"; import BackToPortfolioButton from "@/app/components/building-passport/BackToPortfolioButton"; import { ExclamationCircleIcon } from "@heroicons/react/24/outline"; @@ -30,6 +30,13 @@ export default async function DashboardLayout( // The layout is a server component by default so we can fetch meta data here const propertyMeta = await getPropertyMeta(params.propertyId); + // We check if we have an uploaded condition report and if so, we show the condition tab. Otherwise, we + // don't show it + const conditionReport = await getDocument( + { uprn: String(propertyMeta.uprn), documentType: "ECO_CONDITION_REPORT" } + ); + + console.log("conditionReport", conditionReport) if (!propertyId && propertyId !== "0") { throw Error("Invalid propertyId"); @@ -53,7 +60,7 @@ export default async function DashboardLayout(

{propertyMeta.postcode}

- +
{propertyMeta.detailsEpc.estimated && } {children} diff --git a/src/app/portfolio/[slug]/building-passport/[propertyId]/utils.ts b/src/app/portfolio/[slug]/building-passport/[propertyId]/utils.ts index d737e33..43bcd70 100644 --- a/src/app/portfolio/[slug]/building-passport/[propertyId]/utils.ts +++ b/src/app/portfolio/[slug]/building-passport/[propertyId]/utils.ts @@ -5,6 +5,7 @@ import { Plan, } from "@/app/db/schema/recommendations"; import { db } from "@/app/db/db"; +import { surveyDB } from "@/app/db/surveyDB/connection"; import { Feature, GeneralFeature, @@ -17,7 +18,7 @@ import { nonInstrusiveSurvey, } from "@/app/db/schema/property"; import { getRating } from "@/app/utils"; -import { eq, desc } from "drizzle-orm"; +import { eq, desc, and } from "drizzle-orm"; import { energyAssessment, EnergyAssessment, @@ -26,9 +27,9 @@ import { } from "@/app/db/schema/energy_assessments"; import { fundingPackage, - FundingPackage, FundingPackageWithMeasures } from "@/app/db/schema/funding"; +import { getUploadedFile, uploadedFiles, DB_REPORT_TYPES } from "@/app/db/surveyDB/schema/surveyDB"; type RecommendationList = { recommendation: Recommendation; @@ -49,6 +50,21 @@ export async function getPlanFunding(planId: string): Promise { + // We get the latest entry for the given UPRN and document type, by s3JsonUploadTimestamp + const data = await surveyDB.query.uploadedFiles.findFirst({ + where: and(eq(uploadedFiles.uprn, String(uprn)), eq(uploadedFiles.docType, documentType)), + orderBy: (uploadedFiles, { desc }) => [desc(uploadedFiles.s3JsonUploadTimestamp)] + }); + // We may not have an uploaded document so we return an empty array + if (!data) { + return {} as getUploadedFile; + } + return data; +} + export async function getEnergyAssessment( uprn: number ): Promise { From ce1a78005aedacf4425ea8db712eb2abdef0b1fb Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Sun, 24 Aug 2025 18:04:25 +0000 Subject: [PATCH 2/4] added decent homes reporting --- package-lock.json | 98 ++++ package.json | 1 + .../components/building-passport/Toolbar.tsx | 18 +- .../building-passport/[propertyId]/layout.tsx | 2 - .../pre-assessment-report/page.tsx | 193 ------- .../building-passport/[propertyId]/utils.ts | 30 + tailwind.config.js | 514 +++++++++++------- 7 files changed, 436 insertions(+), 420 deletions(-) delete mode 100644 src/app/portfolio/[slug]/building-passport/[propertyId]/pre-assessment-report/page.tsx diff --git a/package-lock.json b/package-lock.json index 1e485d9..6b8e824 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@headlessui/react": "^2.2.7", "@heroicons/react": "^2.0.18", "@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-dropdown-menu": "^2.0.5", @@ -2734,6 +2735,43 @@ "integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==", "license": "MIT" }, + "node_modules/@radix-ui/react-accordion": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.12.tgz", + "integrity": "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collapsible": "1.1.12", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "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-accordion/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-arrow": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", @@ -2787,6 +2825,66 @@ } } }, + "node_modules/@radix-ui/react-collapsible": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz", + "integrity": "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.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-collapsible/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-collapsible/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-collection": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", diff --git a/package.json b/package.json index 8459da7..51f4e13 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "@headlessui/react": "^2.2.7", "@heroicons/react": "^2.0.18", "@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-dropdown-menu": "^2.0.5", diff --git a/src/app/components/building-passport/Toolbar.tsx b/src/app/components/building-passport/Toolbar.tsx index 901b8ac..2468f41 100644 --- a/src/app/components/building-passport/Toolbar.tsx +++ b/src/app/components/building-passport/Toolbar.tsx @@ -63,7 +63,7 @@ export function Toolbar({ propertyId, portfolioId, conditionReport }: ToolbarPro const preAssessmentReportButton = ( Data @@ -110,18 +110,6 @@ export function Toolbar({ propertyId, portfolioId, conditionReport }: ToolbarPro ); - const conditionButton = ( - - - Condition Report - - ); - - console.log("conditionReport", conditionReport) - return ( 0 && conditionButton} - {energyAssessmentsReportButton} + {/* {energyAssessmentsReportButton} */} -
{address}
- - ); -} - -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 nonIntrusiveSurvey = await getNonIntrusiveSurvey(propertyMeta.uprn); - - const retrofitFeatures = formatRetrofitFeatures(conditionReportData); - - const heatingDemand = formatHeatDemandFeatures(conditionReportData); - - return ( -
-
Pre Assessment Report
-
- Last updated: {formatDateTime(propertyMeta.updatedAt)} -
-
-
- - - -
-
- - {nonIntrusiveSurvey && ( -
-
Non-Intrusive Survey
-
- Conducted by: {nonIntrusiveSurvey.surveyor} on{" "} - {formatDate(nonIntrusiveSurvey.surveyDate)} -
- -
- )} -
General Features
- -
Existing Property Features
- -
Heating Demand
- -
- ); -} diff --git a/src/app/portfolio/[slug]/building-passport/[propertyId]/utils.ts b/src/app/portfolio/[slug]/building-passport/[propertyId]/utils.ts index 43bcd70..d872408 100644 --- a/src/app/portfolio/[slug]/building-passport/[propertyId]/utils.ts +++ b/src/app/portfolio/[slug]/building-passport/[propertyId]/utils.ts @@ -1,3 +1,4 @@ +import S3 from "aws-sdk/clients/s3"; import { Recommendation, planRecommendations, @@ -31,6 +32,35 @@ import { } from "@/app/db/schema/funding"; import { getUploadedFile, uploadedFiles, DB_REPORT_TYPES } from "@/app/db/surveyDB/schema/surveyDB"; + +export async function getEnergyAssessmentFromS3(s3Uri: string): Promise { + const url = new URL(s3Uri); + + const bucketMatch = url.hostname.match(/^(.+)\.s3/); + const bucket = bucketMatch?.[1]; + const key = url.pathname.startsWith("/") ? url.pathname.slice(1) : url.pathname; + + if (!bucket || !key) { + throw new Error("Could not extract bucket or key from URI"); + } + + const s3 = new S3({ + region: "eu-west-2", + accessKeyId: process.env.RETROFIT_ENERGY_ASSESSMENTS_AWS_ACCESS_KEY, + secretAccessKey: process.env.ENERGY_ASSESSMENTS_AWS_SECRET, + }); + + const result = await s3 + .getObject({ + Bucket: bucket, + Key: key, + }) + .promise(); + + const body = result.Body?.toString("utf-8"); + return body ? JSON.parse(body) : null; +} + type RecommendationList = { recommendation: Recommendation; }[]; diff --git a/tailwind.config.js b/tailwind.config.js index f0f0811..b35a54e 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -12,193 +12,227 @@ module.exports = { "./node_modules/@tremor/**/*.{js,ts,jsx,tsx}", ], theme: { - transparent: "transparent", - current: "currentColor", - container: { - center: true, - padding: "2rem", - screens: { - "2xl": "1400px", - }, - }, - extend: { - backgroundImage: { - "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", - "gradient-conic": - "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", - }, - colors: { - // Tremor light mode - tremor: { - brand: { - faint: colors.blue[50], - muted: colors.blue[200], - subtle: colors.blue[400], - DEFAULT: colors.blue[500], - emphasis: colors.blue[700], - inverted: colors.white, - }, - background: { - muted: colors.gray[50], - subtle: colors.gray[100], - DEFAULT: colors.white, - emphasis: colors.gray[700], - }, - border: { - DEFAULT: colors.gray[200], - }, - ring: { - DEFAULT: colors.gray[200], - }, - content: { - subtle: colors.gray[400], - DEFAULT: colors.gray[500], - emphasis: colors.gray[700], - strong: colors.gray[900], - inverted: colors.white, - }, - }, - // Tremor dark mode - // dark mode - "dark-tremor": { - brand: { - faint: "#0B1229", - muted: colors.blue[950], - subtle: colors.blue[800], - DEFAULT: colors.blue[500], - emphasis: colors.blue[400], - inverted: colors.blue[950], - }, - background: { - muted: "#131A2B", - subtle: colors.gray[800], - DEFAULT: colors.gray[900], - emphasis: colors.gray[300], - }, - border: { - DEFAULT: colors.gray[800], - }, - ring: { - DEFAULT: colors.gray[800], - }, - content: { - subtle: colors.gray[600], - DEFAULT: colors.gray[500], - emphasis: colors.gray[200], - strong: colors.gray[50], - inverted: colors.gray[950], - }, - }, - epc_a: "#117d58", - epc_b: "#2da55c", - epc_c: "#8dbd40", - epc_d: "#f7cd14", - epc_e: "#f3a96a", - epc_f: "#ef8026", - epc_g: "#e41e3b", - brandblue: "#14163d", - hoverblue: "#3e4073", - brandtan: "#d3b488", - hovertan: "#947750", - brandgold: "#f1bb06", - hovergold: "#c79d12", - brandbrown: "#c4a47c", - brandmidblue: "#3943b7", - brandlightblue: "#00a9f4", - border: "hsl(var(--border))", - input: "hsl(var(--input))", - ring: "hsl(var(--ring))", - background: "hsl(var(--background))", - foreground: "hsl(var(--foreground))", - primary: { - DEFAULT: "hsl(var(--primary))", - foreground: "hsl(var(--primary-foreground))", - }, - secondary: { - DEFAULT: "hsl(var(--secondary))", - foreground: "hsl(var(--secondary-foreground))", - }, - destructive: { - DEFAULT: "hsl(var(--destructive))", - foreground: "hsl(var(--destructive-foreground))", - }, - muted: { - DEFAULT: "hsl(var(--muted))", - foreground: "hsl(var(--muted-foreground))", - }, - accent: { - DEFAULT: "hsl(var(--accent))", - foreground: "hsl(var(--accent-foreground))", - }, - popover: { - DEFAULT: "hsl(var(--popover))", - foreground: "hsl(var(--popover-foreground))", - }, - card: { - DEFAULT: "hsl(var(--card))", - foreground: "hsl(var(--card-foreground))", - }, - }, - textColor: { - brandblue: "#14163d", - hoverblue: "#3e4073", - brandtan: "#d3b488", - hovertan: "#947750", - brandbrown: "#c4a47c", - brandmidblue: "#3943b7", - brandlightblue: "#00a9f4", - }, - borderRadius: { - lg: `var(--radius)`, - md: `calc(var(--radius) - 2px)`, - sm: "calc(var(--radius) - 4px)", - }, - fontFamily: { - sans: ["var(--font-sans)", ...fontFamily.sans], - }, - keyframes: { - "accordion-down": { - from: { height: 0 }, - to: { height: "var(--radix-accordion-content-height)" }, - }, - "accordion-up": { - from: { height: "var(--radix-accordion-content-height)" }, - to: { height: 0 }, - }, - }, - animation: { - "accordion-down": "accordion-down 0.2s ease-out", - "accordion-up": "accordion-up 0.2s ease-out", - }, - maxWidth: { - "8xl": "90rem", - }, - boxShadow: { - // light - "tremor-input": "0 1px 2px 0 rgb(0 0 0 / 0.05)", - "tremor-card": - "0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)", - "tremor-dropdown": - "0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)", - // dark - "dark-tremor-input": "0 1px 2px 0 rgb(0 0 0 / 0.05)", - "dark-tremor-card": - "0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)", - "dark-tremor-dropdown": - "0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)", - }, - borderRadius: { - "tremor-small": "0.375rem", - "tremor-default": "0.5rem", - "tremor-full": "9999px", - }, - fontSize: { - "tremor-label": ["0.75rem", { lineHeight: "1rem" }], - "tremor-default": ["0.875rem", { lineHeight: "1.25rem" }], - "tremor-title": ["1.125rem", { lineHeight: "1.75rem" }], - "tremor-metric": ["1.875rem", { lineHeight: "2.25rem" }], - }, - }, + transparent: 'transparent', + current: 'currentColor', + container: { + center: true, + padding: '2rem', + screens: { + '2xl': '1400px' + } + }, + extend: { + backgroundImage: { + 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', + 'gradient-conic': 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))' + }, + colors: { + tremor: { + brand: { + faint: 'colors.blue[50]', + muted: 'colors.blue[200]', + subtle: 'colors.blue[400]', + DEFAULT: 'colors.blue[500]', + emphasis: 'colors.blue[700]', + inverted: 'colors.white' + }, + background: { + muted: 'colors.gray[50]', + subtle: 'colors.gray[100]', + DEFAULT: 'colors.white', + emphasis: 'colors.gray[700]' + }, + border: { + DEFAULT: 'colors.gray[200]' + }, + ring: { + DEFAULT: 'colors.gray[200]' + }, + content: { + subtle: 'colors.gray[400]', + DEFAULT: 'colors.gray[500]', + emphasis: 'colors.gray[700]', + strong: 'colors.gray[900]', + inverted: 'colors.white' + } + }, + 'dark-tremor': { + brand: { + faint: '#0B1229', + muted: 'colors.blue[950]', + subtle: 'colors.blue[800]', + DEFAULT: 'colors.blue[500]', + emphasis: 'colors.blue[400]', + inverted: 'colors.blue[950]' + }, + background: { + muted: '#131A2B', + subtle: 'colors.gray[800]', + DEFAULT: 'colors.gray[900]', + emphasis: 'colors.gray[300]' + }, + border: { + DEFAULT: 'colors.gray[800]' + }, + ring: { + DEFAULT: 'colors.gray[800]' + }, + content: { + subtle: 'colors.gray[600]', + DEFAULT: 'colors.gray[500]', + emphasis: 'colors.gray[200]', + strong: 'colors.gray[50]', + inverted: 'colors.gray[950]' + } + }, + epc_a: '#117d58', + epc_b: '#2da55c', + epc_c: '#8dbd40', + epc_d: '#f7cd14', + epc_e: '#f3a96a', + epc_f: '#ef8026', + epc_g: '#e41e3b', + brandblue: '#14163d', + hoverblue: '#3e4073', + brandtan: '#d3b488', + hovertan: '#947750', + brandgold: '#f1bb06', + hovergold: '#c79d12', + brandbrown: '#c4a47c', + brandmidblue: '#3943b7', + brandlightblue: '#00a9f4', + border: 'hsl(var(--border))', + input: 'hsl(var(--input))', + ring: 'hsl(var(--ring))', + background: 'hsl(var(--background))', + foreground: 'hsl(var(--foreground))', + primary: { + DEFAULT: 'hsl(var(--primary))', + foreground: 'hsl(var(--primary-foreground))' + }, + secondary: { + DEFAULT: 'hsl(var(--secondary))', + foreground: 'hsl(var(--secondary-foreground))' + }, + destructive: { + DEFAULT: 'hsl(var(--destructive))', + foreground: 'hsl(var(--destructive-foreground))' + }, + muted: { + DEFAULT: 'hsl(var(--muted))', + foreground: 'hsl(var(--muted-foreground))' + }, + accent: { + DEFAULT: 'hsl(var(--accent))', + foreground: 'hsl(var(--accent-foreground))' + }, + popover: { + DEFAULT: 'hsl(var(--popover))', + foreground: 'hsl(var(--popover-foreground))' + }, + card: { + DEFAULT: 'hsl(var(--card))', + foreground: 'hsl(var(--card-foreground))' + } + }, + textColor: { + brandblue: '#14163d', + hoverblue: '#3e4073', + brandtan: '#d3b488', + hovertan: '#947750', + brandbrown: '#c4a47c', + brandmidblue: '#3943b7', + brandlightblue: '#00a9f4' + }, + borderRadius: { + 'tremor-small': '0.375rem', + 'tremor-default': '0.5rem', + 'tremor-full': '9999px' + }, + fontFamily: { + sans: [ + 'var(--font-sans)', + ...fontFamily.sans + ] + }, + keyframes: { + 'accordion-down': { + from: { + height: 0 + }, + to: { + height: 'var(--radix-accordion-content-height)' + } + }, + 'accordion-up': { + from: { + height: 'var(--radix-accordion-content-height)' + }, + to: { + height: 0 + } + }, + 'accordion-down': { + from: { + height: '0' + }, + to: { + height: 'var(--radix-accordion-content-height)' + } + }, + 'accordion-up': { + from: { + height: 'var(--radix-accordion-content-height)' + }, + to: { + height: '0' + } + } + }, + animation: { + 'accordion-down': 'accordion-down 0.2s ease-out', + 'accordion-up': 'accordion-up 0.2s ease-out', + 'accordion-down': 'accordion-down 0.2s ease-out', + 'accordion-up': 'accordion-up 0.2s ease-out' + }, + maxWidth: { + '8xl': '90rem' + }, + boxShadow: { + 'tremor-input': '0 1px 2px 0 rgb(0 0 0 / 0.05)', + 'tremor-card': '0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)', + 'tremor-dropdown': '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)', + 'dark-tremor-input': '0 1px 2px 0 rgb(0 0 0 / 0.05)', + 'dark-tremor-card': '0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)', + 'dark-tremor-dropdown': '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)' + }, + fontSize: { + 'tremor-label': [ + '0.75rem', + { + lineHeight: '1rem' + } + ], + 'tremor-default': [ + '0.875rem', + { + lineHeight: '1.25rem' + } + ], + 'tremor-title': [ + '1.125rem', + { + lineHeight: '1.75rem' + } + ], + 'tremor-metric': [ + '1.875rem', + { + lineHeight: '2.25rem' + } + ] + } + } }, variants: { extend: { @@ -234,28 +268,90 @@ module.exports = { /^(fill-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/, }, // This enables the EPC colours for tremor. They're listed from EPC G -> A - ...[ - "[#e41e3b]", - "[#ef8026]", - "[#f3a96a]", - "[#f7cd14]", - "[#8dbd40]", - "[#2da55c]", - "[#117d58]", - ].flatMap((customColor) => [ - `bg-${customColor}`, - `border-${customColor}`, - `hover:bg-${customColor}`, - `hover:border-${customColor}`, - `hover:text-${customColor}`, - `fill-${customColor}`, - `ring-${customColor}`, - `stroke-${customColor}`, - `text-${customColor}`, - `ui-selected:bg-${customColor}`, - `ui-selected:border-${customColor}`, - `ui-selected:text-${customColor}`, - ]), + "bg-[#e41e3b]", + "border-[#e41e3b]", + "hover:bg-[#e41e3b]", + "hover:border-[#e41e3b]", + "hover:text-[#e41e3b]", + "fill-[#e41e3b]", + "ring-[#e41e3b]", + "stroke-[#e41e3b]", + "text-[#e41e3b]", + "ui-selected:bg-[#e41e3b]", + "ui-selected:border-[#e41e3b]", + "ui-selected:text-[#e41e3b]", + "bg-[#ef8026]", + "border-[#ef8026]", + "hover:bg-[#ef8026]", + "hover:border-[#ef8026]", + "hover:text-[#ef8026]", + "fill-[#ef8026]", + "ring-[#ef8026]", + "stroke-[#ef8026]", + "text-[#ef8026]", + "ui-selected:bg-[#ef8026]", + "ui-selected:border-[#ef8026]", + "ui-selected:text-[#ef8026]", + "bg-[#f3a96a]", + "border-[#f3a96a]", + "hover:bg-[#f3a96a]", + "hover:border-[#f3a96a]", + "hover:text-[#f3a96a]", + "fill-[#f3a96a]", + "ring-[#f3a96a]", + "stroke-[#f3a96a]", + "text-[#f3a96a]", + "ui-selected:bg-[#f3a96a]", + "ui-selected:border-[#f3a96a]", + "ui-selected:text-[#f3a96a]", + "bg-[#f7cd14]", + "border-[#f7cd14]", + "hover:bg-[#f7cd14]", + "hover:border-[#f7cd14]", + "hover:text-[#f7cd14]", + "fill-[#f7cd14]", + "ring-[#f7cd14]", + "stroke-[#f7cd14]", + "text-[#f7cd14]", + "ui-selected:bg-[#f7cd14]", + "ui-selected:border-[#f7cd14]", + "ui-selected:text-[#f7cd14]", + "bg-[#8dbd40]", + "border-[#8dbd40]", + "hover:bg-[#8dbd40]", + "hover:border-[#8dbd40]", + "hover:text-[#8dbd40]", + "fill-[#8dbd40]", + "ring-[#8dbd40]", + "stroke-[#8dbd40]", + "text-[#8dbd40]", + "ui-selected:bg-[#8dbd40]", + "ui-selected:border-[#8dbd40]", + "ui-selected:text-[#8dbd40]", + "bg-[#2da55c]", + "border-[#2da55c]", + "hover:bg-[#2da55c]", + "hover:border-[#2da55c]", + "hover:text-[#2da55c]", + "fill-[#2da55c]", + "ring-[#2da55c]", + "stroke-[#2da55c]", + "text-[#2da55c]", + "ui-selected:bg-[#2da55c]", + "ui-selected:border-[#2da55c]", + "ui-selected:text-[#2da55c]", + "bg-[#117d58]", + "border-[#117d58]", + "hover:bg-[#117d58]", + "hover:border-[#117d58]", + "hover:text-[#117d58]", + "fill-[#117d58]", + "ring-[#117d58]", + "stroke-[#117d58]", + "text-[#117d58]", + "ui-selected:bg-[#117d58]", + "ui-selected:border-[#117d58]", + "ui-selected:text-[#117d58]", ], plugins: [ function ({ addVariant }) { From c5af2010445ccdf7c4954df9afde4fa2e84a0018 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Tue, 26 Aug 2025 12:11:21 +0000 Subject: [PATCH 3/4] 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 } From 0a9772d522be4d7d338a7970a2e51f398afe15da Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Tue, 26 Aug 2025 14:00:46 +0000 Subject: [PATCH 4/4] fixed funding ui --- .../RecommendationContainer.tsx | 2 +- .../ValuationImpactComponent.tsx | 6 +-- .../assessment/ConditionReport.tsx | 40 ++++++++++++++++++- .../[propertyId]/assessment/page.tsx | 10 ++++- 4 files changed, 50 insertions(+), 8 deletions(-) diff --git a/src/app/components/building-passport/RecommendationContainer.tsx b/src/app/components/building-passport/RecommendationContainer.tsx index af9dc11..37d0901 100644 --- a/src/app/components/building-passport/RecommendationContainer.tsx +++ b/src/app/components/building-passport/RecommendationContainer.tsx @@ -320,7 +320,7 @@ export default function RecommendationContainer({ console.warn("Multiple funding packages found, using the first one."); } - const [totalFunding, setTotalFunding] = useState(funding[0].projectFunding) + const [totalFunding, setTotalFunding] = useState(funding[0]?.projectFunding || 0) const currentEpcRating = propertyMeta.currentEpcRating; const currentSapPoints = propertyMeta.currentSapPoints; diff --git a/src/app/components/building-passport/ValuationImpactComponent.tsx b/src/app/components/building-passport/ValuationImpactComponent.tsx index f1856a0..9a2d5d4 100644 --- a/src/app/components/building-passport/ValuationImpactComponent.tsx +++ b/src/app/components/building-passport/ValuationImpactComponent.tsx @@ -105,14 +105,14 @@ export default function ValuationImpactComponent({ setFundingModalIsOpen(false)} - scheme={funding.scheme} - fundingPackageMeasures={funding.fundingPackageMeasures} + scheme={funding?.scheme} + fundingPackageMeasures={funding?.fundingPackageMeasures || []} /> ); diff --git a/src/app/portfolio/[slug]/building-passport/[propertyId]/assessment/ConditionReport.tsx b/src/app/portfolio/[slug]/building-passport/[propertyId]/assessment/ConditionReport.tsx index 2186454..ddd0afa 100644 --- a/src/app/portfolio/[slug]/building-passport/[propertyId]/assessment/ConditionReport.tsx +++ b/src/app/portfolio/[slug]/building-passport/[propertyId]/assessment/ConditionReport.tsx @@ -109,14 +109,28 @@ 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 }: { conditionReport: { rooms: Record; [key: string]: any; - } + }, + totalFloorArea: number; }) { + + // 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 = [ @@ -172,6 +186,22 @@ export default function ConditionReport({ } ); + // 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); + return (
@@ -180,7 +210,7 @@ export default function ConditionReport({ 0} roomsWithIssues={roomsWithDamp} @@ -212,6 +242,12 @@ export default function ConditionReport({ passed={bathroomsOk} alert={!bathroomsOk} /> + diff --git a/src/app/portfolio/[slug]/building-passport/[propertyId]/assessment/page.tsx b/src/app/portfolio/[slug]/building-passport/[propertyId]/assessment/page.tsx index 7368036..ddce32a 100644 --- a/src/app/portfolio/[slug]/building-passport/[propertyId]/assessment/page.tsx +++ b/src/app/portfolio/[slug]/building-passport/[propertyId]/assessment/page.tsx @@ -143,7 +143,7 @@ export default async function PreAssessmentReport( conditionReport = await getEnergyAssessmentFromS3(conditionReportMeta.s3JsonUri); } - console.log("conditionReport", conditionReport.rooms.kitchen) + // console.log("conditionReport", conditionReport.rooms.utility) const nonIntrusiveSurvey = await getNonIntrusiveSurvey(propertyMeta.uprn); @@ -151,6 +151,12 @@ export default async function PreAssessmentReport( const heatingDemand = formatHeatDemandFeatures(conditionReportData); + // If total floor area is missing, we have a problem + if (conditionReportData.totalFloorArea == null) { + console.error("Total floor area is missing"); + return null; + } + return (
@@ -172,7 +178,7 @@ export default async function PreAssessmentReport(
{ - Object.keys(conditionReportMeta).length > 0 && + Object.keys(conditionReportMeta).length > 0 && } {nonIntrusiveSurvey && (