mirror of
https://github.com/Hestia-Homes/assessment-model.git
synced 2026-06-08 11:37:25 +00:00
Merge pull request #82 from Hestia-Homes/feature/condition
Added decent homes dashboard
This commit is contained in:
commit
0a4b2a25e3
11 changed files with 1145 additions and 216 deletions
|
|
@ -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
168
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
189
src/app/portfolio/[slug]/(portfolio)/decent-homes/page.tsx
Normal file
189
src/app/portfolio/[slug]/(portfolio)/decent-homes/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
118
src/app/shadcn_components/ui/drawer.tsx
Normal file
118
src/app/shadcn_components/ui/drawer.tsx
Normal 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,
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue