Merge pull request #19 from Hestia-Homes/main

Dev deploy
This commit is contained in:
KhalimCK 2024-09-10 18:53:25 +01:00 committed by GitHub
commit a5b4fa2670
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
127 changed files with 87092 additions and 351 deletions

View file

@ -2,6 +2,14 @@ This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next
## Getting Started
When first getting set up you'll firstly want to install the existing dependencies. To do this, simply run
```bash
npm install
# or
yarn install
```
First, run the development server:
```bash

463
package-lock.json generated
View file

@ -21,8 +21,10 @@
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-tooltip": "^1.0.7",
"@remixicon/react": "^4.2.0",
"@tanstack/react-query": "^4.29.12",
"@tanstack/react-table": "^8.9.3",
"@tremor/react": "^3.16.0",
"@types/node": "20.2.3",
"@types/react": "18.2.7",
"@types/react-dom": "18.2.4",
@ -42,11 +44,12 @@
"react": "18.2.0",
"react-dom": "18.2.0",
"tailwind-merge": "^1.13.2",
"tailwindcss": "3.3.2",
"tailwindcss": "^3.4.3",
"tailwindcss-animate": "^1.0.6",
"typescript": "5.0.4"
},
"devDependencies": {
"@tailwindcss/forms": "^0.5.7",
"@testing-library/cypress": "^9.0.0",
"@types/pg": "^8.10.2",
"cypress": "^12.17.1",
@ -1143,10 +1146,11 @@
}
},
"node_modules/@headlessui/react": {
"version": "1.7.14",
"resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.7.14.tgz",
"integrity": "sha512-znzdq9PG8rkwcu9oQ2FwIy0ZFtP9Z7ycS+BAqJ3R5EIqC/0bJGvhT7193rFf+45i9nnPsYvCQVW4V/bB9Xc+gA==",
"version": "1.7.19",
"resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.7.19.tgz",
"integrity": "sha512-Ll+8q3OlMJfJbAKM/+/Y2q6PPYbryqNTXDbryx7SXLIDamkF6iQFbriYHga0dY44PvDhvvBWCx1Xj4U5+G4hOw==",
"dependencies": {
"@tanstack/react-virtual": "^3.0.0-beta.60",
"client-only": "^0.0.1"
},
"engines": {
@ -1157,6 +1161,17 @@
"react-dom": "^16 || ^17 || ^18"
}
},
"node_modules/@headlessui/tailwindcss": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/@headlessui/tailwindcss/-/tailwindcss-0.2.0.tgz",
"integrity": "sha512-fpL830Fln1SykOCboExsWr3JIVeQKieLJ3XytLe/tt1A0XzqUthOftDmjcCYLW62w7mQI7wXcoPXr3tZ9QfGxw==",
"engines": {
"node": ">=10"
},
"peerDependencies": {
"tailwindcss": "^3.0"
}
},
"node_modules/@heroicons/react": {
"version": "2.0.18",
"resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.0.18.tgz",
@ -2412,6 +2427,14 @@
"@babel/runtime": "^7.13.10"
}
},
"node_modules/@remixicon/react": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/@remixicon/react/-/react-4.2.0.tgz",
"integrity": "sha512-eGhKpZ88OU0qkcY9pJu6khBmItDV82nU130E6C68yc+FbljueHlUYy/4CrJsmf860RIDMay2Rpzl27OSJ81miw==",
"peerDependencies": {
"react": ">=18.2.0"
}
},
"node_modules/@rushstack/eslint-patch": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.3.0.tgz",
@ -2446,6 +2469,18 @@
"tslib": "^2.4.0"
}
},
"node_modules/@tailwindcss/forms": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.7.tgz",
"integrity": "sha512-QE7X69iQI+ZXwldE+rzasvbJiyV/ju1FGHH0Qn2W3FKbuYtqp8LKcy6iSw79fVUT5/Vvf+0XgLCeYVG+UV6hOw==",
"dev": true,
"dependencies": {
"mini-svg-data-uri": "^1.2.3"
},
"peerDependencies": {
"tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1"
}
},
"node_modules/@tanstack/query-core": {
"version": "4.29.11",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.29.11.tgz",
@ -2500,6 +2535,22 @@
"react-dom": ">=16"
}
},
"node_modules/@tanstack/react-virtual": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.3.0.tgz",
"integrity": "sha512-QFxmTSZBniq15S0vSZ55P4ToXquMXwJypPXyX/ux7sYo6a2FX3/zWoRLLc4eIOGWTjvzqcIVNKhcuFb+OZL3aQ==",
"dependencies": {
"@tanstack/virtual-core": "3.3.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/@tanstack/table-core": {
"version": "8.9.3",
"resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.9.3.tgz",
@ -2512,6 +2563,15 @@
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@tanstack/virtual-core": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.3.0.tgz",
"integrity": "sha512-A0004OAa1FcUkPHeeGoKgBrAgjH+uHdDPrw1L7RpkwnODYqRvoilqsHPs8cyTjMg1byZBbiNpQAq2TlFLIaQag==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@testing-library/cypress": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/@testing-library/cypress/-/cypress-9.0.0.tgz",
@ -2580,12 +2640,85 @@
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
"dev": true
},
"node_modules/@tremor/react": {
"version": "3.16.0",
"resolved": "https://registry.npmjs.org/@tremor/react/-/react-3.16.0.tgz",
"integrity": "sha512-wKOGTMFwBbadYolFoOKYPjvwtgHZVB/1tS0QLaNpOpoBn3Tlx/Bb2RNsLPNGCmFTb9tNfOSGoOBwd8uukWA9xg==",
"dependencies": {
"@floating-ui/react": "^0.19.2",
"@headlessui/react": "^1.7.18",
"@headlessui/tailwindcss": "^0.2.0",
"date-fns": "^2.30.0",
"react-day-picker": "^8.9.1",
"react-transition-state": "^2.1.1",
"recharts": "^2.10.3",
"tailwind-merge": "^1.14.0"
},
"peerDependencies": {
"react": "^18.0.0",
"react-dom": ">=16.6.0"
}
},
"node_modules/@types/aria-query": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.1.tgz",
"integrity": "sha512-XTIieEY+gvJ39ChLcB4If5zHtPxt3Syj5rgZR+e1ctpmK8NjPf0zFqsz4JpLJT0xla9GFDKjy8Cpu331nrmE1Q==",
"dev": true
},
"node_modules/@types/d3-array": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz",
"integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg=="
},
"node_modules/@types/d3-color": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="
},
"node_modules/@types/d3-ease": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
"integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="
},
"node_modules/@types/d3-interpolate": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
"integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
"dependencies": {
"@types/d3-color": "*"
}
},
"node_modules/@types/d3-path": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.0.tgz",
"integrity": "sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ=="
},
"node_modules/@types/d3-scale": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.8.tgz",
"integrity": "sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==",
"dependencies": {
"@types/d3-time": "*"
}
},
"node_modules/@types/d3-shape": {
"version": "3.1.6",
"resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.6.tgz",
"integrity": "sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA==",
"dependencies": {
"@types/d3-path": "*"
}
},
"node_modules/@types/d3-time": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.3.tgz",
"integrity": "sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw=="
},
"node_modules/@types/d3-timer": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="
},
"node_modules/@types/json5": {
"version": "0.0.29",
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
@ -3526,9 +3659,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001489",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001489.tgz",
"integrity": "sha512-x1mgZEXK8jHIfAxm+xgdpHpk50IN3z3q3zP261/WS+uvePxW8izXuCu6AHz0lkuYTlATDehiZ/tNyYBdSQsOUQ==",
"version": "1.0.30001641",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001641.tgz",
"integrity": "sha512-Phv5thgl67bHYo1TtMY/MurjkHhV4EDaCosezRXgZ8jzA/Ub+wjxAvbGvjoFENStinwi5kCyOYV3mi5tOGykwA==",
"funding": [
{
"type": "opencollective",
@ -4046,6 +4179,116 @@
"type": "^1.0.1"
}
},
"node_modules/d3-array": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
"integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
"dependencies": {
"internmap": "1 - 2"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-color": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-ease": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
"integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-format": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz",
"integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-interpolate": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
"dependencies": {
"d3-color": "1 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-path": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
"integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-scale": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
"integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
"dependencies": {
"d3-array": "2.10.0 - 3",
"d3-format": "1 - 3",
"d3-interpolate": "1.2.0 - 3",
"d3-time": "2.1.1 - 3",
"d3-time-format": "2 - 4"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-shape": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
"integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
"dependencies": {
"d3-path": "^3.1.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-time": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
"integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
"dependencies": {
"d3-array": "2 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-time-format": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
"integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
"dependencies": {
"d3-time": "1 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-timer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
"integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
"engines": {
"node": ">=12"
}
},
"node_modules/damerau-levenshtein": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
@ -4063,6 +4306,21 @@
"node": ">=0.10"
}
},
"node_modules/date-fns": {
"version": "2.30.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
"integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==",
"dependencies": {
"@babel/runtime": "^7.21.0"
},
"engines": {
"node": ">=0.11"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/date-fns"
}
},
"node_modules/dayjs": {
"version": "1.11.9",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.9.tgz",
@ -4085,6 +4343,11 @@
}
}
},
"node_modules/decimal.js-light": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
"integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="
},
"node_modules/deep-equal": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.1.tgz",
@ -4240,6 +4503,15 @@
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
"dev": true
},
"node_modules/dom-helpers": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
"integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
"dependencies": {
"@babel/runtime": "^7.8.7",
"csstype": "^3.0.2"
}
},
"node_modules/dotenv": {
"version": "16.3.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz",
@ -5179,6 +5451,11 @@
"integrity": "sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==",
"dev": true
},
"node_modules/eventemitter3": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="
},
"node_modules/events": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz",
@ -5291,10 +5568,18 @@
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
},
"node_modules/fast-equals": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.0.1.tgz",
"integrity": "sha512-WF1Wi8PwwSY7/6Kx0vKXtw8RwuSGoM1bvDaJbu7MxDlR1vovZjIAKrnzyrThgAjm6JDTu0fVgWXDlMGspodfoQ==",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/fast-glob": {
"version": "3.2.12",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz",
"integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==",
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
"integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==",
"dependencies": {
"@nodelib/fs.stat": "^2.0.2",
"@nodelib/fs.walk": "^1.2.3",
@ -5962,6 +6247,14 @@
"node": ">= 0.4"
}
},
"node_modules/internmap": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
"integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
"engines": {
"node": ">=12"
}
},
"node_modules/invariant": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
@ -6409,9 +6702,9 @@
"dev": true
},
"node_modules/jiti": {
"version": "1.18.2",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.18.2.tgz",
"integrity": "sha512-QAdOptna2NYiSSpv0O/BwoHBSmz4YhpzJHyi+fnMRTXFjp7B8i/YG5Z8IfusxB1ufjcD2Sre1F3R+nX3fvy7gg==",
"version": "1.21.0",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz",
"integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==",
"bin": {
"jiti": "bin/jiti.js"
}
@ -6647,8 +6940,7 @@
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"node_modules/lodash.merge": {
"version": "4.6.2",
@ -6871,6 +7163,15 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/mini-svg-data-uri": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz",
"integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==",
"dev": true,
"bin": {
"mini-svg-data-uri": "cli.js"
}
},
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@ -7982,6 +8283,19 @@
"node": ">=0.10.0"
}
},
"node_modules/react-day-picker": {
"version": "8.10.0",
"resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-8.10.0.tgz",
"integrity": "sha512-mz+qeyrOM7++1NCb1ARXmkjMkzWVh2GL9YiPbRjKe0zHccvekk4HE+0MPOZOrosn8r8zTHIIeOUXTmXRqmkRmg==",
"funding": {
"type": "individual",
"url": "https://github.com/sponsors/gpbl"
},
"peerDependencies": {
"date-fns": "^2.28.0 || ^3.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/react-dom": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
@ -8044,6 +8358,20 @@
}
}
},
"node_modules/react-smooth": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.1.tgz",
"integrity": "sha512-OE4hm7XqR0jNOq3Qmk9mFLyd6p2+j6bvbPJ7qlB7+oo0eNcL2l7WQzG6MBnT3EXY6xzkLMUBec3AfewJdA0J8w==",
"dependencies": {
"fast-equals": "^5.0.1",
"prop-types": "^15.8.1",
"react-transition-group": "^4.4.5"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/react-style-singleton": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz",
@ -8066,6 +8394,30 @@
}
}
},
"node_modules/react-transition-group": {
"version": "4.4.5",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
"integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
"dependencies": {
"@babel/runtime": "^7.5.5",
"dom-helpers": "^5.0.1",
"loose-envify": "^1.4.0",
"prop-types": "^15.6.2"
},
"peerDependencies": {
"react": ">=16.6.0",
"react-dom": ">=16.6.0"
}
},
"node_modules/react-transition-state": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/react-transition-state/-/react-transition-state-2.1.1.tgz",
"integrity": "sha512-kQx5g1FVu9knoz1T1WkapjUgFz08qQ/g1OmuWGi3/AoEFfS0kStxrPlZx81urjCXdz2d+1DqLpU6TyLW/Ro04Q==",
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@ -8112,6 +8464,44 @@
"node": ">=8.10.0"
}
},
"node_modules/recharts": {
"version": "2.12.5",
"resolved": "https://registry.npmjs.org/recharts/-/recharts-2.12.5.tgz",
"integrity": "sha512-Cy+BkqrFIYTHJCyKHJEPvbHE2kVQEP6PKbOHJ8ztRGTAhvHuUnCwDaKVb13OwRFZ0QNUk1QvGTDdgWSMbuMtKw==",
"dependencies": {
"clsx": "^2.0.0",
"eventemitter3": "^4.0.1",
"lodash": "^4.17.21",
"react-is": "^16.10.2",
"react-smooth": "^4.0.0",
"recharts-scale": "^0.4.4",
"tiny-invariant": "^1.3.1",
"victory-vendor": "^36.6.8"
},
"engines": {
"node": ">=14"
},
"peerDependencies": {
"react": "^16.0.0 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/recharts-scale": {
"version": "0.4.5",
"resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz",
"integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==",
"dependencies": {
"decimal.js-light": "^2.4.1"
}
},
"node_modules/recharts/node_modules/clsx": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz",
"integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==",
"engines": {
"node": ">=6"
}
},
"node_modules/regenerator-runtime": {
"version": "0.13.11",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
@ -8944,28 +9334,28 @@
"integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew=="
},
"node_modules/tailwind-merge": {
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-1.13.2.tgz",
"integrity": "sha512-R2/nULkdg1VR/EL4RXg4dEohdoxNUJGLMnWIQnPKL+O9Twu7Cn3Rxi4dlXkDzZrEGtR+G+psSXFouWlpTyLhCQ==",
"version": "1.14.0",
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-1.14.0.tgz",
"integrity": "sha512-3mFKyCo/MBcgyOTlrY8T7odzZFx+w+qKSMAmdFzRvqBfLlSigU6TZnlFHK0lkMwj9Bj8OYU+9yW9lmGuS0QEnQ==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/dcastil"
}
},
"node_modules/tailwindcss": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.2.tgz",
"integrity": "sha512-9jPkMiIBXvPc2KywkraqsUfbfj+dHDb+JPWtSJa9MLFdrPyazI7q6WX2sUrm7R9eVR7qqv3Pas7EvQFzxKnI6w==",
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.3.tgz",
"integrity": "sha512-U7sxQk/n397Bmx4JHbJx/iSOOv5G+II3f1kpLpY2QeUv5DcPdcTsYLlusZfq1NthHS1c1cZoyFmmkex1rzke0A==",
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
"arg": "^5.0.2",
"chokidar": "^3.5.3",
"didyoumean": "^1.2.2",
"dlv": "^1.1.3",
"fast-glob": "^3.2.12",
"fast-glob": "^3.3.0",
"glob-parent": "^6.0.2",
"is-glob": "^4.0.3",
"jiti": "^1.18.2",
"jiti": "^1.21.0",
"lilconfig": "^2.1.0",
"micromatch": "^4.0.5",
"normalize-path": "^3.0.0",
@ -8977,7 +9367,6 @@
"postcss-load-config": "^4.0.1",
"postcss-nested": "^6.0.1",
"postcss-selector-parser": "^6.0.11",
"postcss-value-parser": "^4.2.0",
"resolve": "^1.22.2",
"sucrase": "^3.32.0"
},
@ -9060,6 +9449,11 @@
"next-tick": "1"
}
},
"node_modules/tiny-invariant": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="
},
"node_modules/titleize": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/titleize/-/titleize-3.0.0.tgz",
@ -9394,6 +9788,27 @@
"extsprintf": "^1.2.0"
}
},
"node_modules/victory-vendor": {
"version": "36.9.2",
"resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz",
"integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==",
"dependencies": {
"@types/d3-array": "^3.0.3",
"@types/d3-ease": "^3.0.0",
"@types/d3-interpolate": "^3.0.1",
"@types/d3-scale": "^4.0.2",
"@types/d3-shape": "^3.1.0",
"@types/d3-time": "^3.0.0",
"@types/d3-timer": "^3.0.0",
"d3-array": "^3.1.6",
"d3-ease": "^3.0.1",
"d3-interpolate": "^3.0.1",
"d3-scale": "^4.0.2",
"d3-shape": "^3.1.0",
"d3-time": "^3.0.0",
"d3-timer": "^3.0.1"
}
},
"node_modules/wait-on": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/wait-on/-/wait-on-7.0.1.tgz",

View file

@ -27,8 +27,10 @@
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-tooltip": "^1.0.7",
"@remixicon/react": "^4.2.0",
"@tanstack/react-query": "^4.29.12",
"@tanstack/react-table": "^8.9.3",
"@tremor/react": "^3.16.0",
"@types/node": "20.2.3",
"@types/react": "18.2.7",
"@types/react-dom": "18.2.4",
@ -48,11 +50,12 @@
"react": "18.2.0",
"react-dom": "18.2.0",
"tailwind-merge": "^1.13.2",
"tailwindcss": "3.3.2",
"tailwindcss": "^3.4.3",
"tailwindcss-animate": "^1.0.6",
"typescript": "5.0.4"
},
"devDependencies": {
"@tailwindcss/forms": "^0.5.7",
"@testing-library/cypress": "^9.0.0",
"@types/pg": "^8.10.2",
"cypress": "^12.17.1",

View file

@ -0,0 +1,7 @@
{
"associatedApplications": [
{
"applicationId": "069e75ee-ba54-45ff-ba77-a06f29c0e21c"
}
]
}

BIN
public/pfp_solar_image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 952 KiB

View file

@ -1,10 +1,19 @@
import NextAuth, { NextAuthOptions } from "next-auth";
import GoogleProvider from "next-auth/providers/google";
import AzureADB2CProvider from "next-auth/providers/azure-ad-b2c";
import CredentialsProvider from "next-auth/providers/credentials";
import { db } from "@/app/db/db";
import { user as userTable, User } from "@/app/db/schema/users";
import { eq } from "drizzle-orm";
const { GOOGLE_CLIENT_ID = "", GOOGLE_CLIENT_SECRET = "" } = process.env;
const {
AZURE_AD_B2C_TENANT_NAME = "",
AZURE_AD_B2C_CLIENT_ID = "",
AZURE_AD_B2C_CLIENT_SECRET = "",
AZURE_AD_B2C_PRIMARY_USER_FLOW = "",
} = process.env;
type OauthProvider = "google";
@ -26,6 +35,51 @@ export const AuthOptions: NextAuthOptions = {
},
},
}),
AzureADB2CProvider({
tenantId: AZURE_AD_B2C_TENANT_NAME,
clientId: AZURE_AD_B2C_CLIENT_ID,
clientSecret: AZURE_AD_B2C_CLIENT_SECRET,
primaryUserFlow: AZURE_AD_B2C_PRIMARY_USER_FLOW,
authorization: {
params: {
scope: "openid profile offline_access",
prompt: "login",
},
},
}),
CredentialsProvider({
name: "Email Login",
credentials: {
email: {
label: "Email",
type: "email",
},
},
async authorize(credentials, req) {
if (!credentials || !credentials.email) {
throw new Error("Email is required");
}
const { email } = credentials;
// Query the database to find the user by email
const dbUser = await db
.select()
.from(userTable)
.where(eq(userTable.email, email));
// If the email exists, return the user object (no password check)
if (dbUser.length === 1) {
return {
id: dbUser[0].id.toString(), // Convert bigint to string to avoid serialization issues
email: dbUser[0].email,
dbId: dbUser[0].id.toString(), // Ensure dbId is added and is a string
};
}
return null;
},
}),
],
pages: {
signIn: "/",

View file

@ -0,0 +1,81 @@
// pages/api/get-presigned-url.ts
import S3 from "aws-sdk/clients/s3";
import STS from "aws-sdk/clients/sts"; // Import STS for temporary credentials
import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
// Validate the input
const PresignedUrlBodySchema = z.object({
fileKey: z.string(),
});
// Function to get temporary credentials using GetSessionToken
async function getTemporaryCredentials() {
const sts = new STS({
accessKeyId: process.env.RETROFIT_ENERGY_ASSESSMENTS_AWS_ACCESS_KEY, // Your permanent access key
secretAccessKey: process.env.ENERGY_ASSESSMENTS_AWS_SECRET, // Your permanent secret access key
region: process.env.PRESIGN_AWS_REGION,
});
try {
// Request temporary credentials with GetSessionToken
const data = await sts.getSessionToken({ DurationSeconds: 900 }).promise(); // Token valid for 15 minutes
// Check if credentials are present
if (!data.Credentials) {
throw new Error("Failed to retrieve temporary credentials");
}
return data.Credentials;
} catch (error) {
console.error("Error fetching temporary credentials:", error);
throw error;
}
}
// API handler
export async function POST(request: NextRequest) {
const body = await request.json();
let validatedBody;
try {
validatedBody = PresignedUrlBodySchema.parse(body);
} catch (error) {
console.error("Invalid input: ", error);
return new NextResponse(JSON.stringify({ msg: "Invalid input" }), {
status: 400,
});
}
try {
// Get temporary credentials using GetSessionToken
const credentials = await getTemporaryCredentials();
// Initialize S3 with temporary credentials
const s3 = new S3({
signatureVersion: "v4",
region: process.env.PRESIGN_AWS_REGION,
accessKeyId: credentials.AccessKeyId,
secretAccessKey: credentials.SecretAccessKey,
sessionToken: credentials.SessionToken, // Include session token
});
const { fileKey } = validatedBody;
// Generate presigned URL valid for 5 minutes
const preSignedUrl = await s3.getSignedUrl("getObject", {
Bucket: process.env.RETROFIT_ENERGY_ASSESSMENTS_BUCKET,
Key: fileKey,
Expires: 5 * 60, // URL expiration in seconds
});
return new NextResponse(JSON.stringify({ url: preSignedUrl }), {
status: 200,
});
} catch (error) {
console.error("Error generating presigned URL:", error);
return new NextResponse(JSON.stringify({ msg: "Internal server error" }), {
status: 500,
});
}
}

View file

@ -0,0 +1,155 @@
import { NextRequest, NextResponse } from "next/server";
import { db } from "@/app/db/db";
import { z } from "zod";
import { formatNumber } from "@/app/utils";
import { DataItem, ChartData } from "@/app/portfolio/[slug]/utils";
import { eq } from "drizzle-orm";
import { scenario } from "@/app/db/schema/recommendations";
export async function GET(
request: NextRequest,
{ params }: { params: { scenarioId: string } }
) {
const scenarioId = params.scenarioId;
const data = await db
.select({
name: scenario.name,
cost: scenario.cost,
epcBreakdownPostRetrofit: scenario.epcBreakdownPostRetrofit,
numberOfProperties: scenario.numberOfProperties,
nUnitsToRetrofit: scenario.nUnitsToRetrofit,
co2PerUnitPostRetrofit: scenario.co2PerUnitPostRetrofit,
energyBillPerUnitPostRetrofit: scenario.energyBillPerUnitPostRetrofit,
energyConsumptionPerUnitPostRetrofit:
scenario.energyConsumptionPerUnitPostRetrofit,
valuationImprovementPerUnit: scenario.valuationImprovementPerUnit,
costPerUnit: scenario.costPerUnit,
costPerCo2Saved: scenario.costPerCo2Saved,
costPerSapPoint: scenario.costPerSapPoint,
valuationReturnOnInvestment: scenario.valuationReturnOnInvestment,
})
.from(scenario)
.where(eq(scenario.id, BigInt(scenarioId)));
if (data.length === 0) {
throw new Error("Scenario not found");
}
if (data.length > 1) {
throw new Error("More than one scenario found");
}
const scenarioName = data[0].name || "Default";
// Format the data we need for the overview
const output: DataItem[] = [
{
title: "EPCs",
scenarios: [
{
scenarioName: scenarioName,
data: JSON.parse(
data[0].epcBreakdownPostRetrofit || "[]"
) as ChartData[],
},
],
},
{
title: "# Units",
scenarios: [
{
scenarioName: scenarioName,
data: String(data[0].numberOfProperties) || "",
},
],
},
{
title: "# Units to retrofit",
scenarios: [
{
scenarioName: scenarioName,
data: String(data[0].nUnitsToRetrofit) || "",
},
],
},
{
title: "CO2/unit",
scenarios: [
{
scenarioName: scenarioName,
data: data[0].co2PerUnitPostRetrofit || "",
},
],
},
{
title: "Annual energy bill/unit",
scenarios: [
{
scenarioName: scenarioName,
data: data[0].energyBillPerUnitPostRetrofit || "",
},
],
},
{
title: "Annual energy demand (kWh)/unit",
scenarios: [
{
scenarioName: scenarioName,
data: data[0].energyConsumptionPerUnitPostRetrofit || "",
},
],
},
{
title: "Cost (£)",
scenarios: [
{
scenarioName: scenarioName,
data: "£" + formatNumber(data[0].cost || 0),
},
],
},
{
title: "Cost (£)/unit",
scenarios: [
{ scenarioName: scenarioName, data: data[0].costPerUnit || "" },
],
},
{
title: "£ per CO2 reduction",
scenarios: [
{ scenarioName: scenarioName, data: data[0].costPerCo2Saved || "" },
],
},
{
title: "£ per SAP point",
scenarios: [
{ scenarioName: scenarioName, data: data[0].costPerSapPoint || "" },
],
},
{
title:
"Potential valuation improvement/unit - highly indicative EPC effect*",
scenarios: [
{
scenarioName: scenarioName,
data: data[0].valuationImprovementPerUnit || "",
},
],
},
{
title:
"Potential valuation return on investment - highly indicative EPC effect*",
scenarios: [
{
scenarioName: scenarioName,
data: data[0].valuationReturnOnInvestment || "",
},
],
},
];
return new NextResponse(JSON.stringify(output), {
status: 200,
});
}

View file

@ -34,7 +34,7 @@ export async function GET(
with: {
detailsEpc: {
columns: {
adjustedEnergyConsumption: true,
currentEnergyDemand: true,
co2Emissions: true,
estimated: true,
},
@ -42,5 +42,9 @@ export async function GET(
},
});
if (!propertyMeta) {
return new NextResponse(null, { status: 404 });
}
return new NextResponse(JSON.stringify(propertyMeta, serializeBigInt));
}

View file

@ -1,3 +1,3 @@
export default function Beta() {
return <div>This application is not ready for general usage</div>;
return <div>You do not have access to this application currently</div>;
}

View file

@ -18,3 +18,31 @@ export function TanButton({
</button>
);
}
export function BrandButton({
label,
onClick,
backgroundColor,
}: {
label: string;
onClick: Dispatch<SetStateAction<any>>;
backgroundColor: "brandblue" | "brandgold"; // Restrict backgroundColor to these two options
}) {
// Dictionary to map background colors to hover colors
const hoverColors = {
brandblue: "hover:bg-hoverblue",
brandgold: "hover:bg-hovergold",
};
return (
<button
type="button"
className={`inline-flex justify-center rounded-md border border-transparent px-4 py-2 text-sm font-medium text-white focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2
${backgroundColor === "brandblue" ? "bg-brandblue" : "bg-brandgold"}
${hoverColors[backgroundColor]}`}
onClick={onClick}
>
{label}
</button>
);
}

View file

@ -20,7 +20,11 @@ export default function StatusBadge({
return (
<HoverCard>
<HoverCardTrigger>
<Badge className={statusConfig.class}>{statusConfig.text}</Badge>
<Badge
className={`truncate overflow-hidden whitespace-nowrap ${statusConfig.class}`}
>
{statusConfig.text}
</Badge>
</HoverCardTrigger>
<HoverCardContent>
<div>
@ -60,6 +64,12 @@ const statusColor: {
hoverText: "This portfolio is currently in the assessment stage",
propertyHoverText: "This property is currently in the assessment stage",
},
survey: {
class: "bg-brandblue hover:bg-hoverblue",
text: "survey",
hoverText: "This portfolio is currently in the survey stage",
propertyHoverText: "This property is currently in the survey stage",
},
tendering: {
class: "bg-emerald-500 hover:bg-emerald-500",
text: "tendering",

View file

@ -52,13 +52,13 @@ export function EnergyEfficiencyImpactCard({
interface SecondaryEnergyEfficiencyImpactCardProps {
TotalCo2Savings: number;
totalEnergyCostSavings: number;
totalHeatDemandSavings: number;
totalKwhSavings: number;
}
export function SecondaryEnergyEfficiencyImpactCard({
TotalCo2Savings,
totalEnergyCostSavings,
totalHeatDemandSavings,
totalKwhSavings,
}: SecondaryEnergyEfficiencyImpactCardProps) {
return (
<div>
@ -77,9 +77,7 @@ export function SecondaryEnergyEfficiencyImpactCard({
<tr>
<td className="font-medium pl-4 py-2">Energy Savings</td>
<td className="pr-2">
{totalHeatDemandSavings.toFixed(0) + "kWh"}
</td>
<td className="pr-2">{totalKwhSavings.toFixed(0) + "kWh"}</td>
</tr>
<tr>

View file

@ -6,12 +6,14 @@ export default function EpcCard({
expected = false,
kwh = null,
carbon = null,
sap = null,
}: {
epcRating: string;
fullMargin: boolean;
expected?: boolean;
kwh?: number | null;
carbon?: number | null;
sap?: string | null;
}) {
let marginClass = "";
if (fullMargin) {
@ -30,28 +32,35 @@ export default function EpcCard({
return (
<div
className={
"flex flex-col items-center p-4 shadow rounded-md max-w-xl justify-start text-gray-100 " +
"flex flex-col items-center p-4 shadow rounded-md max-w-xl justify-start text-gray-100 " +
bgColour
}
>
<div className="text-xl font-bold mb-4 text-center">{title}</div>
<div className="text-6xl font-bold ">{epcRating}</div>
{/* EPC Rating and SAP Rating */}
<div className="flex items-baseline justify-center">
{/* EPC Rating */}
<div className="text-6xl font-bold">{epcRating}</div>
{/* SAP Rating (only if sap is provided) */}
{sap !== null && (
<div className="text-lg font-medium ml-2 text-gray-100">{sap}</div>
)}
</div>
{(kwh || carbon) && (
<table className="mt-6">
<tbody>
{kwh && (
<tr>
{" "}
{/* Added vertical padding to each row */}
<td className="text-gray-50">{kwh.toFixed(0)} kWh</td>
</tr>
)}
{carbon && (
<tr>
{" "}
<td className="text-gray-50 py-2">
{carbon}t CO <sub>2</sub>
{carbon}t CO<sub>2</sub>
</td>
</tr>
)}

View file

@ -16,17 +16,22 @@ import {
TableRow,
} from "@/app/shadcn_components/ui/table";
import { Feature, GeneralFeature } from "@/app/db/schema/property";
import {
Feature,
GeneralFeature,
NonInstrusiveFeature,
} from "@/app/db/schema/property";
interface DataTableProps<T extends Feature | GeneralFeature> {
interface DataTableProps<
T extends Feature | GeneralFeature | NonInstrusiveFeature
> {
columns: ColumnDef<T>[];
data: T[];
}
export default function FeatureTable<T extends Feature | GeneralFeature>({
data,
columns,
}: DataTableProps<T>) {
export default function FeatureTable<
T extends Feature | GeneralFeature | NonInstrusiveFeature
>({ data, columns }: DataTableProps<T>) {
// Initialise the table
const table = useReactTable({
@ -41,18 +46,20 @@ export default function FeatureTable<T extends Feature | GeneralFeature>({
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
);
})}
{headerGroup.headers.map((header) => (
<TableHead
key={header.id}
style={{
// Style the header to match the column width, if passed
width: `${header.getSize()}px`,
}}
>
{flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>

View file

@ -1,6 +1,11 @@
"use client";
import { Feature, GeneralFeature, Rating } from "@/app/db/schema/property";
import {
Feature,
GeneralFeature,
NonInstrusiveFeature,
Rating,
} from "@/app/db/schema/property";
import { Badge } from "@/app/shadcn_components/ui/badge";
import { ColumnDef } from "@tanstack/react-table";
@ -42,9 +47,22 @@ export const generalColumns: ColumnDef<GeneralFeature>[] = [
{
accessorKey: "feature",
header: "Feature",
size: 100,
},
{
accessorKey: "description",
header: "Description",
},
];
export const nonInstrusiveColumns: ColumnDef<NonInstrusiveFeature>[] = [
{
accessorKey: "title",
header: "Feature",
size: 100,
},
{
accessorKey: "note",
header: "Recorded Observation",
},
];

View file

@ -12,9 +12,12 @@ const selectionStyling =
"shadow active:shadow active:bg-brandmidblue w-full border rounded p-4 cursor-pointer text-gray-900 bg-gray-100 hover:bg-hoverblue hover:text-gray-100 transition-colors rounded-md flex flex-col justify-start";
const noSelectionStyling =
"shadow active:shadow active:bg-brandmidblue w-full border rounded p-4 cursor-pointer text-gray-300 bg-white hover:bg-hoverblue hover:text-gray-100 transition-colors rounded-md flex flex-col justify-start";
const alreadyInstalledStyling =
"shadow active:shadow w-full border rounded p-4 cursor-pointer text-gray-900 bg-gray-100transition-colors rounded-md flex flex-col justify-start";
const TitleMap = {
mechanical_ventilation: "Mechanical Ventilation",
trickle_vents: "Trickle Vents",
sealing_open_fireplace: "Sealing Open Fireplace",
low_energy_lighting: "Low Energy Lighting",
// Walls
@ -31,12 +34,23 @@ const TitleMap = {
exposed_floor_insulation: "Exposed Floor Insulation",
// Windows
windows_glazing: "Window Glazing",
mixed_glazing: "Mixed - Secondary and Double Glazing",
// Solar pv
solar_pv: "Solar Photovoltaic Panels System",
// Heating
heating: "Heating Systems",
heating_control: "Heating Controls",
secondary_heating: "Secondary Heating System",
// Hot water tank
hot_water_tank_insulation: "Hot Water Tank Insulation",
// Default options when no recommendation is selected
wall_insulation: "Wall Insulation",
floor_insulation: "Floor Insulation",
roof_insulation: "Roof Insulation",
// Cylinder thermostat
cylinder_thermostat: "Cylinder Thermostat",
// Draught proofing
draught_proofing: "Draught Proofing",
};
type RecommendationCardProps = {
@ -59,9 +73,9 @@ type RecommendationCardProps = {
setEnergyCostSavingsMap: Dispatch<SetStateAction<RecommendationMetricMap>>;
energyCostSavingsMap: RecommendationMetricMap;
setTotalEnergyCostSavings: Dispatch<SetStateAction<number>>;
setHeatDemandMap: Dispatch<SetStateAction<RecommendationMetricMap>>;
heatDemandMap: RecommendationMetricMap;
setTotalHeatDemandSavings: Dispatch<SetStateAction<number>>;
setKwhSavingsMap: Dispatch<SetStateAction<RecommendationMetricMap>>;
kwhSavingsMap: RecommendationMetricMap;
setTotalKwhSavings: Dispatch<SetStateAction<number>>;
};
export default function RecommendationCard({
@ -84,14 +98,19 @@ export default function RecommendationCard({
setEnergyCostSavingsMap,
energyCostSavingsMap,
setTotalEnergyCostSavings,
setHeatDemandMap,
heatDemandMap,
setTotalHeatDemandSavings,
setKwhSavingsMap,
kwhSavingsMap,
setTotalKwhSavings,
}: RecommendationCardProps) {
const defaultComponent = recommendationData.find(
(rec: Recommendation) => rec.default
) as Recommendation;
// A recommendation type could have no default recommendation, so we need to check if it exists
const alreadyInstalled = defaultComponent
? defaultComponent.alreadyInstalled
: false;
const [cardComponent, setCardComponent] =
useState<Recommendation>(defaultComponent);
@ -107,14 +126,39 @@ export default function RecommendationCard({
return TitleMap[recommendationType];
};
// Determine the className based on alreadyInstalled or cardComponent existence
const cardClassName = alreadyInstalled
? alreadyInstalledStyling
: cardComponent
? selectionStyling
: noSelectionStyling;
const optionTextClassName = alreadyInstalled
? "text-brandgold"
: "text-blue-500 hover:text-blue-300";
const optionsText = alreadyInstalled
? "Already installed"
: cardComponent
? "Click for more options"
: "Click to select";
const openModal = () => {
// If the card is already installed, we don't want to open the modal
if (alreadyInstalled) {
return;
}
setModalIsOpen(true);
};
// If the measure is already installed, we change the font colour to gold
const titleClassName = alreadyInstalled
? "text-brandgold font-bold mb-4 text-lg"
: "font-bold mb-4 text-lg";
return (
<div
className={cardComponent ? selectionStyling : noSelectionStyling}
onClick={() => {
setModalIsOpen(true);
}}
>
<h2 className="font-bold mb-4 text-lg">{getTitle()}</h2>
<div className={cardClassName} onClick={openModal}>
<h2 className={titleClassName}>{getTitle()}</h2>
<div className="mb-3">
{cardComponent ? (
cardComponent.description
@ -122,9 +166,7 @@ export default function RecommendationCard({
<div className="text-red-500">No measure selected</div>
)}
</div>
<div className="text-blue-500 hover:text-blue-300">
{cardComponent ? "Click for more options" : "Click to select"}
</div>
<div className={optionTextClassName}>{optionsText}</div>
{cardComponent ? (
<table className="w-full text-left">
<tbody>
@ -184,10 +226,10 @@ export default function RecommendationCard({
setEnergyCostSavingsMap={setEnergyCostSavingsMap}
energyCostSavingsMap={energyCostSavingsMap}
setTotalEnergyCostSavings={setTotalEnergyCostSavings}
// Heat Demand
setHeatDemandMap={setHeatDemandMap}
heatDemandMap={heatDemandMap}
setTotalHeatDemandSavings={setTotalHeatDemandSavings}
// kWh Savings
setKwhSavingsMap={setKwhSavingsMap}
kwhSavingsMap={kwhSavingsMap}
setTotalKwhSavings={setTotalKwhSavings}
/>
</div>
);

View file

@ -34,6 +34,7 @@ const typeToCategoryMap: { [key in RecommendationType]?: RecommendationType } =
solid_floor_insulation: "floor_insulation",
exposed_floor_insulation: "floor_insulation",
windows_glazing: "windows_glazing",
heating: "heating",
};
const emptyImpactState = {
@ -42,7 +43,7 @@ const emptyImpactState = {
labourDays: 0,
co2EquivalentSavings: 0,
energyCostSavings: 0,
adjustedHeatDemand: 0,
kwhSavings: 0,
};
export default function RecommendationContainer({
@ -101,6 +102,46 @@ export default function RecommendationContainer({
(rec: Recommendation) => rec.default
) || emptyImpactState;
const defaultHeatingRecommendations =
categorizedRecommendations.heating?.find(
(rec: Recommendation) => rec.default
) || emptyImpactState;
const defaultHeatingControlRecommendations =
categorizedRecommendations.heating_control?.find(
(rec: Recommendation) => rec.default
) || emptyImpactState;
const defaultHotWaterTankRecommendations =
categorizedRecommendations.hot_water_tank_insulation?.find(
(rec: Recommendation) => rec.default
) || emptyImpactState;
const defaultSecondaryHeatingRecommendations =
categorizedRecommendations.secondary_heating?.find(
(rec: Recommendation) => rec.default
) || emptyImpactState;
const defaultCylinderThermostatRecommendations =
categorizedRecommendations.cylinder_thermostat?.find(
(rec: Recommendation) => rec.default
) || emptyImpactState;
const defaultTrickleVentsRecommendations =
categorizedRecommendations.trickle_vents?.find(
(rec: Recommendation) => rec.default
) || emptyImpactState;
const defaultMixedGlazingRecommendations =
categorizedRecommendations.mixed_glazing?.find(
(rec: Recommendation) => rec.default
) || emptyImpactState;
const defaultDraughtProofingRecommendations =
categorizedRecommendations.draught_proofing?.find(
(rec: Recommendation) => rec.default
) || emptyImpactState;
const [costMap, setCostMap] = useState<RecommendationMetricMap>({
wall_insulation: defaultWallsRecommendations.estimatedCost || 0,
floor_insulation: defaultFloorRecommendations.estimatedCost || 0,
@ -111,6 +152,17 @@ export default function RecommendationContainer({
low_energy_lighting: defaultLightingRecommendations.estimatedCost || 0,
windows_glazing: defaultWindowsRecommendations.estimatedCost || 0,
solar_pv: defaultSolarRecommendations.estimatedCost || 0,
heating: defaultHeatingRecommendations.estimatedCost || 0,
hot_water_tank_insulation:
defaultHotWaterTankRecommendations.estimatedCost || 0,
heating_control: defaultHeatingControlRecommendations.estimatedCost || 0,
secondary_heating:
defaultSecondaryHeatingRecommendations.estimatedCost || 0,
cylinder_thermostat:
defaultCylinderThermostatRecommendations.estimatedCost || 0,
trickle_vents: defaultTrickleVentsRecommendations.estimatedCost || 0,
mixed_glazing: defaultMixedGlazingRecommendations.estimatedCost || 0,
draught_proofing: defaultDraughtProofingRecommendations.estimatedCost || 0,
});
const [sapMap, setSapMap] = useState<RecommendationMetricMap>({
@ -122,6 +174,16 @@ export default function RecommendationContainer({
low_energy_lighting: defaultLightingRecommendations.sapPoints || 0,
windows_glazing: defaultWindowsRecommendations.sapPoints || 0,
solar_pv: defaultSolarRecommendations.sapPoints || 0,
heating: defaultHeatingRecommendations.sapPoints || 0,
hot_water_tank_insulation:
defaultHotWaterTankRecommendations.sapPoints || 0,
heating_control: defaultHeatingControlRecommendations.sapPoints || 0,
secondary_heating: defaultSecondaryHeatingRecommendations.sapPoints || 0,
cylinder_thermostat:
defaultCylinderThermostatRecommendations.sapPoints || 0,
trickle_vents: defaultTrickleVentsRecommendations.sapPoints || 0,
mixed_glazing: defaultMixedGlazingRecommendations.sapPoints || 0,
draught_proofing: defaultDraughtProofingRecommendations.sapPoints || 0,
});
const [labourDaysMap, setLabourDaysMap] = useState<RecommendationMetricMap>({
@ -133,6 +195,16 @@ export default function RecommendationContainer({
low_energy_lighting: defaultLightingRecommendations.labourDays || 0,
windows_glazing: defaultWindowsRecommendations.labourDays || 0,
solar_pv: defaultSolarRecommendations.labourDays || 0,
heating: defaultHeatingRecommendations.labourDays || 0,
hot_water_tank_insulation:
defaultHotWaterTankRecommendations.labourDays || 0,
heating_control: defaultHeatingControlRecommendations.labourDays || 0,
secondary_heating: defaultSecondaryHeatingRecommendations.labourDays || 0,
cylinder_thermostat:
defaultCylinderThermostatRecommendations.labourDays || 0,
trickle_vents: defaultTrickleVentsRecommendations.labourDays || 0,
mixed_glazing: defaultMixedGlazingRecommendations.labourDays || 0,
draught_proofing: defaultDraughtProofingRecommendations.labourDays || 0,
});
const [co2SavingsMap, setCo2SavingsMap] = useState<RecommendationMetricMap>({
@ -147,6 +219,19 @@ export default function RecommendationContainer({
defaultLightingRecommendations.co2EquivalentSavings || 0,
windows_glazing: defaultWindowsRecommendations.co2EquivalentSavings || 0,
solar_pv: defaultSolarRecommendations.co2EquivalentSavings || 0,
heating: defaultHeatingRecommendations.co2EquivalentSavings || 0,
hot_water_tank_insulation:
defaultHotWaterTankRecommendations.co2EquivalentSavings || 0,
heating_control:
defaultHeatingControlRecommendations.co2EquivalentSavings || 0,
secondary_heating:
defaultSecondaryHeatingRecommendations.co2EquivalentSavings || 0,
cylinder_thermostat:
defaultCylinderThermostatRecommendations.co2EquivalentSavings || 0,
trickle_vents: defaultTrickleVentsRecommendations.co2EquivalentSavings || 0,
mixed_glazing: defaultMixedGlazingRecommendations.co2EquivalentSavings || 0,
draught_proofing:
defaultDraughtProofingRecommendations.co2EquivalentSavings || 0,
});
const [energyCostSavingsMap, setEnergyCostSavingsMap] =
@ -162,19 +247,40 @@ export default function RecommendationContainer({
defaultLightingRecommendations.energyCostSavings || 0,
windows_glazing: defaultWindowsRecommendations.energyCostSavings || 0,
solar_pv: defaultSolarRecommendations.energyCostSavings || 0,
heating: defaultHeatingRecommendations.energyCostSavings || 0,
hot_water_tank_insulation:
defaultHotWaterTankRecommendations.energyCostSavings || 0,
heating_control:
defaultHeatingControlRecommendations.energyCostSavings || 0,
secondary_heating:
defaultSecondaryHeatingRecommendations.energyCostSavings || 0,
cylinder_thermostat:
defaultCylinderThermostatRecommendations.energyCostSavings || 0,
trickle_vents: defaultTrickleVentsRecommendations.energyCostSavings || 0,
mixed_glazing: defaultMixedGlazingRecommendations.energyCostSavings || 0,
draught_proofing:
defaultDraughtProofingRecommendations.energyCostSavings || 0,
});
const [heatDemandMap, setHeatDemandMap] = useState<RecommendationMetricMap>({
wall_insulation: defaultWallsRecommendations.adjustedHeatDemand || 0,
floor_insulation: defaultFloorRecommendations.adjustedHeatDemand || 0,
roof_insulation: defaultRoofRecommendations.adjustedHeatDemand || 0,
mechanical_ventilation:
defaultVentiliationRecommendations.adjustedHeatDemand || 0,
sealing_open_fireplace:
defaultFireplaceRecommendations.adjustedHeatDemand || 0,
low_energy_lighting: defaultLightingRecommendations.adjustedHeatDemand || 0,
windows_glazing: defaultWindowsRecommendations.adjustedHeatDemand || 0,
solar_pv: defaultSolarRecommendations.adjustedHeatDemand || 0,
const [kwhSavingsMap, setKwhSavingsMap] = useState<RecommendationMetricMap>({
wall_insulation: defaultWallsRecommendations.kwhSavings || 0,
floor_insulation: defaultFloorRecommendations.kwhSavings || 0,
roof_insulation: defaultRoofRecommendations.kwhSavings || 0,
mechanical_ventilation: defaultVentiliationRecommendations.kwhSavings || 0,
sealing_open_fireplace: defaultFireplaceRecommendations.kwhSavings || 0,
low_energy_lighting: defaultLightingRecommendations.kwhSavings || 0,
windows_glazing: defaultWindowsRecommendations.kwhSavings || 0,
solar_pv: defaultSolarRecommendations.kwhSavings || 0,
heating: defaultHeatingRecommendations.kwhSavings || 0,
hot_water_tank_insulation:
defaultHotWaterTankRecommendations.kwhSavings || 0,
heating_control: defaultHeatingControlRecommendations.kwhSavings || 0,
secondary_heating: defaultSecondaryHeatingRecommendations.kwhSavings || 0,
cylinder_thermostat:
defaultCylinderThermostatRecommendations.kwhSavings || 0,
trickle_vents: defaultTrickleVentsRecommendations.kwhSavings || 0,
mixed_glazing: defaultMixedGlazingRecommendations.kwhSavings || 0,
draught_proofing: defaultDraughtProofingRecommendations.kwhSavings || 0,
});
const [totalEstimatedCost, setTotalEstimatedCost] = useState(
@ -197,8 +303,8 @@ export default function RecommendationContainer({
sumRecommendationMetricMap(energyCostSavingsMap)
);
const [totalHeatDemandSavings, setTotalHeatDemandSavings] = useState(
sumRecommendationMetricMap(heatDemandMap)
const [totalKwhSavings, setTotalKwhSavings] = useState(
sumRecommendationMetricMap(kwhSavingsMap)
);
const currentEpcRating = propertyMeta.currentEpcRating;
@ -228,7 +334,7 @@ export default function RecommendationContainer({
<SecondaryEnergyEfficiencyImpactCard
TotalCo2Savings={totalCo2Savings}
totalEnergyCostSavings={totalEnergyCostSavings}
totalHeatDemandSavings={totalHeatDemandSavings}
totalKwhSavings={totalKwhSavings}
/>
</div>
@ -265,10 +371,10 @@ export default function RecommendationContainer({
setEnergyCostSavingsMap={setEnergyCostSavingsMap}
energyCostSavingsMap={energyCostSavingsMap}
setTotalEnergyCostSavings={setTotalEnergyCostSavings}
// Heat Demand
setHeatDemandMap={setHeatDemandMap}
heatDemandMap={heatDemandMap}
setTotalHeatDemandSavings={setTotalHeatDemandSavings}
// kwh Savings
setKwhSavingsMap={setKwhSavingsMap}
kwhSavingsMap={kwhSavingsMap}
setTotalKwhSavings={setTotalKwhSavings}
/>
);
}

View file

@ -31,9 +31,9 @@ interface RecommendationModalProps {
setEnergyCostSavingsMap: Dispatch<SetStateAction<RecommendationMetricMap>>;
energyCostSavingsMap: RecommendationMetricMap;
setTotalEnergyCostSavings: Dispatch<SetStateAction<number>>;
setHeatDemandMap: Dispatch<SetStateAction<RecommendationMetricMap>>;
heatDemandMap: RecommendationMetricMap;
setTotalHeatDemandSavings: Dispatch<SetStateAction<number>>;
setKwhSavingsMap: Dispatch<SetStateAction<RecommendationMetricMap>>;
kwhSavingsMap: RecommendationMetricMap;
setTotalKwhSavings: Dispatch<SetStateAction<number>>;
}
const TitleMap = {
@ -70,9 +70,9 @@ export default function RecommendationModal({
setEnergyCostSavingsMap,
energyCostSavingsMap,
setTotalEnergyCostSavings,
setHeatDemandMap,
heatDemandMap,
setTotalHeatDemandSavings,
setKwhSavingsMap,
kwhSavingsMap,
setTotalKwhSavings,
}: RecommendationModalProps) {
const [saveButtonDisabled, setSaveButtonDisabled] = useState(true);
@ -165,15 +165,15 @@ export default function RecommendationModal({
);
// Update the heat demand savings map
const newHeatDemandMap = {
...heatDemandMap,
[title]: recommendationData[newIndex]?.heatDemand || 0,
const newKwhSavingsMap = {
...kwhSavingsMap,
[title]: recommendationData[newIndex]?.kwhSavings || 0,
};
setHeatDemandMap(newHeatDemandMap);
setKwhSavingsMap(newKwhSavingsMap);
// update the heat demand savings sum
setTotalHeatDemandSavings(sumRecommendationMetricMap(newHeatDemandMap));
setTotalKwhSavings(sumRecommendationMetricMap(newKwhSavingsMap));
}
return (

View file

@ -5,7 +5,7 @@ import {
NewspaperIcon,
HomeModernIcon,
WrenchScrewdriverIcon,
LightBulbIcon,
SunIcon,
} from "@heroicons/react/24/outline";
import {
NavigationMenu,
@ -29,10 +29,6 @@ export function Toolbar({ propertyId, portfolioId }: ToolbarProps) {
console.log("Settings were clicked, implement me");
}
function handleClickPortfolioPlan() {
console.log("Opt Plan was clicked, implement me");
}
const preAssessmentReportButton = (
<NavigationMenuLink
className={navigationMenuTriggerStyle() + " ml-3 mr-2"}
@ -43,6 +39,26 @@ export function Toolbar({ propertyId, portfolioId }: ToolbarProps) {
</NavigationMenuLink>
);
const energyAssessmentsReportButton = (
<NavigationMenuLink
className={navigationMenuTriggerStyle() + " ml-3 mr-2"}
href={`/portfolio/${portfolioId}/building-passport/${propertyId}/energy-assessment`}
>
<NewspaperIcon className="h-4 w-4 mr-2" />
Energy Assessment
</NavigationMenuLink>
);
const solarAnalysisButton = (
<NavigationMenuLink
className={navigationMenuTriggerStyle() + " ml-3 mr-2"}
href={`/portfolio/${portfolioId}/building-passport/${propertyId}/solar-analysis`}
>
<SunIcon className="h-4 w-4 mr-2" />
Solar Analysis
</NavigationMenuLink>
);
const recommendationsButton = (
<NavigationMenuLink
className={navigationMenuTriggerStyle() + " ml-3 mr-2"}
@ -65,14 +81,9 @@ export function Toolbar({ propertyId, portfolioId }: ToolbarProps) {
<NavigationMenuList>
{preAssessmentReportButton}
{solarAnalysisButton}
{recommendationsButton}
{/* <NavigationMenuLink
className={navigationMenuTriggerStyle() + " ml-3 mr-2"}
href={`/portfolio/${portfolioId}/building-passport/${propertyId}/plan-optimiser`}
>
<LightBulbIcon className="h-4 w-4 mr-2" />
Plan optimiser
</NavigationMenuLink> */}
{energyAssessmentsReportButton}
<NavigationMenuItem
className={navigationMenuTriggerStyle() + " ml-3 mr-2"}
onClick={handleClickSettings}

View file

@ -0,0 +1,191 @@
"use client";
import { useState } from "react";
import { convertDaysToWorkingWeeks, formatNumber } from "@/app/utils";
interface SummaryBoxProps {
scenarios: Array<{
id: bigint;
name: string;
budget: number | null;
totalCost: number | null;
co2EquivalentSavings: number | null;
propertyValuationIncrease: number | null;
energySavings: number | null;
energyCostSavings: number | null;
labourDays: number | null;
isDefault: boolean;
}>;
numProperties: number;
}
function SummaryBox({ scenarios, numProperties }: SummaryBoxProps) {
// Get the default scenario
const defaultScenario =
scenarios.find((scenario) => scenario.isDefault) || scenarios[0];
const [selectedScenarioId, setSelectedScenarioId] = useState(
Number(defaultScenario.id)
);
const [budgetFormatted, setBudgetFormatted] = useState(
formatBudget(defaultScenario.budget)
);
const [totalCostFormatted, setTotalCostFormatted] = useState(
formatMoney(defaultScenario.totalCost)
);
const [totalValueIncreaseFormatted, setTotalValueIncreaseFormatted] =
useState(formatMoney(defaultScenario.propertyValuationIncrease));
const [energyCostSavingsFormatted, setEnergyCostSavingsFormatted] = useState(
formatMoney(defaultScenario.energyCostSavings)
);
const [co2EquivalentSavingsFormatted, setCo2EquivalentSavingsFormatted] =
useState(formatCo2(defaultScenario.co2EquivalentSavings));
const [energySavingsFormatted, setEnergySavingsFormatted] = useState(
formatKwh(defaultScenario.energySavings)
);
const handleScenarioChange = (scenarioId: string) => {
setSelectedScenarioId(Number(scenarioId));
const selectedScenario = scenarios.find(
(scenario) => Number(scenario.id) === Number(scenarioId)
);
if (!selectedScenario) {
return;
}
setBudgetFormatted(formatBudget(selectedScenario.budget));
setTotalCostFormatted(formatMoney(selectedScenario.totalCost));
setTotalValueIncreaseFormatted(
formatMoney(selectedScenario.propertyValuationIncrease)
);
setEnergyCostSavingsFormatted(
formatMoney(selectedScenario.energyCostSavings)
);
setCo2EquivalentSavingsFormatted(
formatCo2(selectedScenario.co2EquivalentSavings)
);
setEnergySavingsFormatted(formatKwh(selectedScenario.energySavings));
};
function formatMoney(amount: number | null) {
if (amount === 0) {
return "-";
}
return amount === null ? "-" : "£" + formatNumber(amount);
}
function formatBudget(budget: number | null) {
return budget === null ? "Not set" : formatMoney(budget);
}
function formatHours(hours: number | null) {
return hours === null ? "0 hours" : Math.round(hours) + " hours";
}
function formatCo2(co2: number | null) {
return co2 === null ? "-" : co2.toFixed(1) + " tonnes";
}
function formatKwh(energy: number | null) {
return energy === null ? "-" : formatNumber(energy) + " kWh";
}
return (
<div className="p-6 bg-white rounded-lg leading-relaxed">
<h2 className="text-2xl font-bold mb-4 text-brandblue text-center">
Portfolio Summary
</h2>
<div className="mb-4 flex items-center justify-center">
<select
id="scenario-select"
value={selectedScenarioId}
onChange={(e) => handleScenarioChange(e.target.value)}
className="p-2 border rounded w-full"
>
{scenarios.map((scenario) => (
<option key={String(scenario.id)} value={String(scenario.id)}>
{scenario.name}
</option>
))}
</select>
</div>
<div className="grid grid-cols-1 md:grid-cols-1 gap-6">
<div className="p-4 bg-gray-50 rounded-lg">
<h3 className="text-lg font-semibold text-brandblue mb-2">
Work Package
</h3>
<table className="w-full">
<tbody>
<tr>
<td className="text-brandblue">Total Budget</td>
<td className="text-brandblue text-end">{budgetFormatted}</td>
</tr>
<tr>
<td className="text-brandblue">Total Cost</td>
<td className="text-brandblue text-end">
{totalCostFormatted}
</td>
</tr>
<tr>
<td className="text-brandblue">Total properties</td>
<td className="text-brandblue text-end">{numProperties}</td>
</tr>
</tbody>
</table>
</div>
<div className="p-4 bg-gray-50 rounded-lg">
<h3 className="text-lg font-semibold text-brandblue mb-2">
Environmental Impact
</h3>
<table className="w-full">
<tbody>
<tr>
<td className="text-brandblue">
Annual{" "}
<span>
CO<sub>2</sub>
</span>{" "}
Savings
</td>
<td className="text-brandblue text-end">
{co2EquivalentSavingsFormatted}
</td>
</tr>
<tr>
<td className="text-brandblue">Annual Energy Savings</td>
<td className="text-brandblue text-end">
{energySavingsFormatted}
</td>
</tr>
</tbody>
</table>
</div>
<div className="p-4 bg-gray-50 rounded-lg">
<h3 className="text-lg font-semibold text-brandblue mb-2">
Financial Impact
</h3>
<table className="w-full">
<tbody>
<tr>
<td className="text-brandblue">Annual Energy Bill Reduction</td>
<td className="text-brandblue text-end">
{energyCostSavingsFormatted}
</td>
</tr>
<tr>
<td className="text-brandblue">Total Value Increase</td>
<td className="text-brandblue text-end">
{totalValueIncreaseFormatted}
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
);
}
export default SummaryBox;

View file

@ -4,6 +4,7 @@ import {
Cog6ToothIcon,
CalculatorIcon,
BuildingOfficeIcon,
ChartBarIcon,
} from "@heroicons/react/24/outline";
import {
NavigationMenu,
@ -39,6 +40,10 @@ export function Toolbar({ portfolioId }: ToolbarProps) {
router.push(`/portfolio/${portfolioId}`);
}
function handleClickSummary() {
router.push(`/portfolio/${portfolioId}/summary`);
}
const [modalIsOpen, setModalIsOpen] = useState(false);
return (
@ -53,12 +58,20 @@ export function Toolbar({ portfolioId }: ToolbarProps) {
</NavigationMenuItem>
<NavigationMenuItem
className={navigationMenuTriggerStyle() + " ml-3 mr-2"}
onClick={handleClickSummary}
>
<ChartBarIcon className="h-4 w-4 mr-2" />
Summary
</NavigationMenuItem>
{/* <NavigationMenuItem
className={navigationMenuTriggerStyle() + " ml-3 mr-2"}
onClick={handleClickPortfolioPlan}
>
<CalculatorIcon className="h-4 w-4 mr-2" />
Portfolio Plan
</NavigationMenuItem>
</NavigationMenuItem> */}
<NavigationMenuItem
className={navigationMenuTriggerStyle() + " ml-3 mr-2"}

View file

@ -0,0 +1,41 @@
import { BarChart } from "@tremor/react";
const dataFormatter = (number: number) =>
Intl.NumberFormat("us").format(number).toString();
const EpcBarChart = ({
chartdata,
}: {
chartdata: {
name: string;
A?: number;
B?: number;
C?: number;
D?: number;
E?: number;
F?: number;
G?: number;
}[];
}) => (
<BarChart
className="w-64 h-40"
data={chartdata}
index="name"
categories={["G", "F", "E", "D", "C", "B", "A"]} // Each treated as a separate series
colors={[
"#e41e3b", // Color for 'G'
"#ef8026", // Color for 'F'
"#f3a96a", // Color for 'E'
"#f7cd14", // Color for 'D'
"#8dbd40", // Color for 'C'
"#2da55c", // Color for 'B'
"#117d58", // Color for 'A'
]}
valueFormatter={dataFormatter}
yAxisWidth={48}
stack={true}
showLegend={false}
/>
);
export default EpcBarChart;

View file

@ -0,0 +1,101 @@
import { Dialog, Transition } from "@headlessui/react";
import { Fragment, useState } from "react";
const SelectComparisonModal = ({
isOpen,
setIsOpen,
scenarios,
onAddColumn,
}: {
isOpen: boolean;
setIsOpen: (isOpen: boolean) => void;
scenarios: { name: string | null; id: bigint }[];
onAddColumn: (columnName: string) => void;
}) => {
const [selectedScenario, setSelectedScenario] = useState(() => {
return scenarios && scenarios.length > 0 ? scenarios[0].id.toString() : "";
});
const addColumn = () => {
onAddColumn(selectedScenario);
setIsOpen(false);
};
return (
<Transition show={isOpen} as={Fragment}>
<Dialog
as="div"
className="fixed inset-0 z-10 overflow-y-auto"
onClose={setIsOpen}
>
<div className="min-h-screen px-4 text-center">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Dialog.Overlay className="fixed inset-0 bg-black opacity-30" />
</Transition.Child>
{/* This element is to trick the browser into centering the modal contents. */}
<span
className="inline-block h-screen align-middle"
aria-hidden="true"
>
&#8203;
</span>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<div className="inline-block w-full max-w-md p-6 my-8 overflow-hidden text-left align-middle transition-all transform bg-white shadow-xl rounded-2xl">
<Dialog.Title
as="h3"
className="text-lg font-medium leading-6 text-gray-900"
>
Add Comparison
</Dialog.Title>
<div className="mt-2">
<select
className="w-full p-2 border border-gray-300 rounded-md"
value={selectedScenario}
onChange={(e) => setSelectedScenario(e.target.value)}
>
{scenarios.map((scenario) => (
<option
key={scenario.id.toString()}
value={String(scenario.id)}
>
{scenario.name}
</option>
))}
</select>
</div>
<div className="mt-4">
<button
type="button"
className="inline-flex justify-center px-4 py-2 text-sm font-medium text-blue-900 bg-blue-100 border border-transparent rounded-md hover:bg-blue-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-blue-500"
onClick={addColumn}
>
Add
</button>
</div>
</div>
</Transition.Child>
</div>
</Dialog>
</Transition>
);
};
export default SelectComparisonModal;

View file

@ -0,0 +1,245 @@
"use client";
import React, { useState } from "react";
import {
useReactTable,
flexRender,
getCoreRowModel,
ColumnDef,
} from "@tanstack/react-table";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/app/shadcn_components/ui/table";
import SelectComparisonModal from "./SelectComparisonModal";
import EpcBarChart from "./EpcBarChart";
import { DataItem, ChartData } from "@/app/portfolio/[slug]/utils";
import { UseQueryOptions, useQuery } from "@tanstack/react-query";
export async function getComparsionOverviewScenarioData(
scenarioId: string
): Promise<Promise<DataItem[]>> {
const url = process.env.URL || "" + `/api/portfolio/scenario/${scenarioId}`;
// This is the client side counterpart to getOverviewScenarioData
const response = await fetch(url, {
method: "GET",
headers: { "Content-Type": "application/json" },
next: { revalidate: 60 },
});
if (!response.ok) {
// Print the error
console.error(response.statusText);
throw new Error("Network response was not ok");
}
return response.json();
}
interface UseFetchScenarioSummaryProps {
scenarioId: string; // Assuming scenarioId can be null to handle cases before it's available
onSuccess: (data: DataItem[]) => void;
options?: Omit<UseQueryOptions<DataItem[], Error>, "queryKey" | "queryFn">;
}
export const useFetchScenarioSummary = ({
scenarioId,
onSuccess,
}: UseFetchScenarioSummaryProps) => {
return useQuery({
queryKey: ["scenarioSummary", scenarioId],
queryFn: () => getComparsionOverviewScenarioData(scenarioId as string), // Ensure non-null assertion or handle potential null inside the function
onSuccess,
enabled: scenarioId !== "no-id", // Ensure the query only runs when portfolioId is not null
});
};
const shouldHighlightRow = (title: string) => {
return [
"Potential valuation improvement/unit - highly indicative EPC effect*",
"Potential valuation return on investment - highly indicative EPC effect*",
].includes(title);
};
const formatCo2Title = (content: any) => {
if (content === "CO2/unit") {
return "CO₂/unit";
}
if (content === "£ per CO2 reduction") {
return "£ per CO₂ reduction";
}
return content;
};
const SummaryTable = ({
data,
scenarios,
}: {
data: DataItem[];
scenarios: { name: string | null; id: bigint }[];
}) => {
// TODO: Right now a user can ad multiple comparisons of the same column, we should prevent this
const initialColumnSetup = () => {
const initialColumns: ColumnDef<DataItem>[] = [
{
accessorKey: "title",
header: () => null,
cell: (info) => <b>{info.getValue() as string}</b>,
},
];
// Retrieve the scenario names
const predefinedScenarios = data[0].scenarios.map(
(scenario) => scenario.scenarioName
);
predefinedScenarios.forEach((scenarioName) => {
initialColumns.push({
id: scenarioName,
header: () => <span>{scenarioName}</span>,
accessorFn: (row) => {
const scenarioData = row.scenarios.find(
(s) => s.scenarioName === scenarioName
)?.data;
return scenarioData;
},
cell: (info) => {
const value = info.getValue();
// Render chart if data is array and matches the chart data structure, otherwise render as text
return Array.isArray(value) && value[0]?.name ? (
<EpcBarChart chartdata={value as ChartData[]} />
) : (
<span>{value as String}</span>
);
},
});
});
return initialColumns;
};
const [columns, setColumns] =
useState<ColumnDef<DataItem>[]>(initialColumnSetup);
const [managedData, setManagedData] = useState<DataItem[]>(data || []);
const [isModalOpen, setIsModalOpen] = useState(false);
const [selectedScenarioId, setSelectedScenarioId] = useState<string>("no-id");
// Fetch and handle data when a new scenario is selected
const {
isLoading,
error,
data: fetchedData,
} = useFetchScenarioSummary({
scenarioId: selectedScenarioId,
onSuccess: (fetchedData: DataItem[]) => {
const updatedData = managedData.map((item) => ({
...item,
scenarios: [
...item.scenarios,
...(fetchedData.find((fi) => fi.title === item.title)?.scenarios ||
[]),
],
}));
setManagedData(updatedData);
if (fetchedData[0] && fetchedData[0].scenarios.length > 0) {
addColumn(fetchedData[0].scenarios[0].scenarioName);
}
},
});
const table = useReactTable({
data: managedData,
columns: columns,
getCoreRowModel: getCoreRowModel(),
});
const addColumn = (scenarioName: string) => {
const newColumn: ColumnDef<DataItem> = {
id: scenarioName.toLowerCase().replace(/\s+/g, "_"),
accessorFn: (row) =>
row.scenarios.find((s) => s.scenarioName === scenarioName)?.data,
header: () => <span>{scenarioName}</span>,
cell: (info) => {
const value = info.getValue();
return Array.isArray(value) ? (
<EpcBarChart chartdata={value as ChartData[]} />
) : (
<span>{String(value)}</span>
);
},
};
setColumns((oldColumns) => [...oldColumns, newColumn]);
};
return (
<div className="my-8">
<button
onClick={() => setIsModalOpen(true)}
className="mb-4 p-2 bg-brandgold text-white rounded disabled:opacity-50"
// If scenarios is empty, we disable this button
disabled={scenarios.length === 0}
>
Add Comparison
</button>
<SelectComparisonModal
isOpen={isModalOpen}
setIsOpen={setIsModalOpen}
scenarios={scenarios}
onAddColumn={(id: string) => {
setSelectedScenarioId(id);
}}
/>
<div className="overflow-x-auto shadow-md sm:rounded-lg">
<Table className="min-w-full">
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHead key={header.id} className="px-6 py-3">
{flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
style={
shouldHighlightRow(row.original.title)
? { background: "#e1e1e1" }
: {}
}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id} className="px-6 py-4">
{flexRender(cell.column.columnDef.cell, {
...cell.getContext(),
getValue: () => formatCo2Title(cell.getValue()),
})}
</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
</div>
</div>
);
};
export default SummaryTable;

View file

@ -0,0 +1,71 @@
"use client";
import { signIn } from "next-auth/react";
import { useState, useEffect, SetStateAction } from "react";
import { Input } from "@/app/shadcn_components/ui/input";
import { Button } from "@/app/shadcn_components/ui/button";
import { ChevronRightIcon } from "@heroicons/react/20/solid";
export default function EmailSignInButton({
error: initialError,
}: {
error: string | undefined;
}) {
const [email, setEmail] = useState("");
const [error, setError] = useState(initialError);
const handleSubmit = async (e: { preventDefault: () => void }) => {
e.preventDefault();
const res = await signIn("credentials", {
email,
});
if (res?.error) {
setError("You are not a valid user.");
} else {
console.log("Login successful");
}
};
const handleEmailChange = (e: {
target: { value: SetStateAction<string> };
}) => {
setEmail(e.target.value);
if (error) {
setError(undefined); // Clear the error when the user starts typing
}
};
// Sync initial error state with server-side error prop
useEffect(() => {
setError(initialError);
}, [initialError]);
return (
<form onSubmit={handleSubmit} className="w-full">
{/* Wrapper to control width and layout */}
<div className="flex items-center w-full space-x-1">
{/* Email input field using shadcn input */}
<Input
type="email"
value={email}
onChange={handleEmailChange}
placeholder="Enter your email"
required
className="flex-1 h-10 rounded-lg border-gray-300" // Full width input
/>
<Button
type="submit"
className="h-10 w-10 bg-brandblue text-white hover:bg-hoverblue rounded-lg flex items-center justify-center" // Fixed size button
>
<ChevronRightIcon className="h-5 w-5" />
</Button>
</div>
{/* Reserve space for the error message */}
<div className="min-h-[3rem] text-center">
{error && <p className="text-red-500">You are not a valid user.</p>}
</div>
</form>
);
}

View file

@ -0,0 +1,111 @@
"use client";
import { useSearchParams } from "next/navigation";
import { signIn } from "next-auth/react";
import Button from "./Button";
const MicrosoftSignInButton = () => {
const searchParams = useSearchParams();
const callbackUrl = searchParams.get("callbackUrl");
return (
<Button
data-testid="microsoft-signin-btn"
className="text-black hover:text-gray-400 text-xl hover:bg-gray-100 rounded-lg p-0"
onClick={() => signIn("azure-ad-b2c", { callbackUrl })} // Note the provider ID "azure-ad" must match the one you've set in NextAuth config
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="100%"
height="100%"
viewBox="0 0 215 41"
>
<title>MS-SymbolLockup</title>
<rect width="100%" height="100%" fill="#2f2f2f" />
<path
d="M45.812,25.082V23.288a2.77,2.77,0,0,0,.573.4,4.484,4.484,0,0,0,.706.3,5.486,5.486,0,0,0,.745.187,3.954,3.954,0,0,0,.687.065,2.928,2.928,0,0,0,1.634-.365,1.2,1.2,0,0,0,.537-1.062,1.167,1.167,0,0,0-.178-.649,1.939,1.939,0,0,0-.5-.5,5.412,5.412,0,0,0-.757-.435q-.435-.209-.932-.436-.533-.285-.994-.578a4.285,4.285,0,0,1-.8-.648,2.724,2.724,0,0,1-.533-.8,2.6,2.6,0,0,1-.194-1.047,2.416,2.416,0,0,1,.333-1.285,2.794,2.794,0,0,1,.877-.9,4.019,4.019,0,0,1,1.239-.528,5.906,5.906,0,0,1,1.418-.172,5.692,5.692,0,0,1,2.4.374v1.721a3.817,3.817,0,0,0-2.295-.645,4.093,4.093,0,0,0-.771.074,2.335,2.335,0,0,0-.687.241,1.5,1.5,0,0,0-.494.433,1.06,1.06,0,0,0-.189.637,1.221,1.221,0,0,0,.145.608,1.573,1.573,0,0,0,.428.468,4.321,4.321,0,0,0,.688.414c.27.134.584.28.939.436q.548.285,1.034.6a4.881,4.881,0,0,1,.856.7,3.075,3.075,0,0,1,.585.846,2.493,2.493,0,0,1,.215,1.058,2.625,2.625,0,0,1-.322,1.348,2.584,2.584,0,0,1-.866.892,3.786,3.786,0,0,1-1.254.5,6.959,6.959,0,0,1-1.5.155c-.176,0-.392-.014-.647-.04s-.518-.067-.786-.117a7.75,7.75,0,0,1-.76-.187A2.373,2.373,0,0,1,45.812,25.082Z"
fill="#fff"
/>
<path
d="M55.109,16.426a1.021,1.021,0,0,1-.713-.272.891.891,0,0,1-.3-.688.917.917,0,0,1,.3-.7,1.009,1.009,0,0,1,.713-.278,1.041,1.041,0,0,1,.732.278.915.915,0,0,1,.3.7.9.9,0,0,1-.3.678A1.035,1.035,0,0,1,55.109,16.426ZM55.95,25.5h-1.7V18h1.7Z"
fill="#fff"
/>
<path
d="M64.979,24.9q0,4.131-4.146,4.131a6.166,6.166,0,0,1-2.551-.491V26.986a4.712,4.712,0,0,0,2.332.7,2.341,2.341,0,0,0,2.668-2.628V24.24h-.029a2.938,2.938,0,0,1-4.733.436,4.046,4.046,0,0,1-.837-2.684,4.738,4.738,0,0,1,.9-3.04,2.988,2.988,0,0,1,2.471-1.128,2.38,2.38,0,0,1,2.2,1.216h.029V18h1.7ZM63.3,22.064v-.973a1.91,1.91,0,0,0-.523-1.352,1.71,1.71,0,0,0-1.3-.559,1.789,1.789,0,0,0-1.51.714,3.223,3.223,0,0,0-.545,2,2.78,2.78,0,0,0,.523,1.769,1.675,1.675,0,0,0,1.385.662,1.8,1.8,0,0,0,1.426-.632A2.4,2.4,0,0,0,63.3,22.064Z"
fill="#fff"
/>
<path
d="M73.853,25.5h-1.7V21.273q0-2.1-1.483-2.1a1.616,1.616,0,0,0-1.279.582,2.167,2.167,0,0,0-.505,1.469V25.5h-1.7V18h1.7v1.245h.029a2.669,2.669,0,0,1,2.428-1.421,2.257,2.257,0,0,1,1.863.795,3.57,3.57,0,0,1,.644,2.3Z"
fill="#fff"
/>
<path
d="M80.892,16.426a1.017,1.017,0,0,1-.713-.272.889.889,0,0,1-.3-.688.915.915,0,0,1,.3-.7,1,1,0,0,1,.713-.278,1.038,1.038,0,0,1,.731.278.915.915,0,0,1,.3.7.9.9,0,0,1-.3.678A1.033,1.033,0,0,1,80.892,16.426Zm.84,9.074h-1.7V18h1.7Z"
fill="#fff"
/>
<path
d="M90.614,25.5h-1.7V21.273q0-2.1-1.483-2.1a1.62,1.62,0,0,0-1.28.582,2.167,2.167,0,0,0-.5,1.469V25.5h-1.7V18h1.7v1.245h.03a2.668,2.668,0,0,1,2.427-1.421,2.258,2.258,0,0,1,1.864.795,3.576,3.576,0,0,1,.643,2.3Z"
fill="#fff"
/>
<path
d="M106.865,18l-2.208,7.5h-1.776l-1.36-5.083a3.291,3.291,0,0,1-.1-.659h-.029a3.018,3.018,0,0,1-.132.644l-1.477,5.1H98.042l-2.2-7.5H97.6l1.36,5.405a3.308,3.308,0,0,1,.087.645H99.1a3.384,3.384,0,0,1,.117-.659L100.725,18h1.593l1.345,5.428a3.832,3.832,0,0,1,.095.644h.052a3.3,3.3,0,0,1,.109-.644L105.249,18Z"
fill="#fff"
/>
<path
d="M108.977,16.426a1.017,1.017,0,0,1-.713-.272.889.889,0,0,1-.3-.688.915.915,0,0,1,.3-.7,1,1,0,0,1,.713-.278,1.038,1.038,0,0,1,.731.278.915.915,0,0,1,.3.7.9.9,0,0,1-.3.678A1.033,1.033,0,0,1,108.977,16.426Zm.84,9.074h-1.7V18h1.7Z"
fill="#fff"
/>
<path
d="M115.979,25.42a2.944,2.944,0,0,1-1.307.248q-2.18,0-2.179-2.094V19.333h-1.25V18h1.25V16.264l1.7-.483V18h1.79v1.333h-1.79v3.75a1.478,1.478,0,0,0,.242.952,1,1,0,0,0,.8.285,1.16,1.16,0,0,0,.745-.248Z"
fill="#fff"
/>
<path
d="M124.094,25.5h-1.7V21.4q0-2.226-1.483-2.226a1.555,1.555,0,0,0-1.258.644,2.573,2.573,0,0,0-.511,1.649V25.5h-1.7V14.4h1.7v4.849h.029a2.679,2.679,0,0,1,2.428-1.421q2.492,0,2.492,3.055Z"
fill="#fff"
/>
<path
d="M141.719,25.5h-1.726V18.7q0-.835.1-2.043h-.03a6.992,6.992,0,0,1-.285.988L136.652,25.5h-1.2l-3.136-7.793a7.371,7.371,0,0,1-.277-1.047h-.029q.059.63.058,2.058V25.5h-1.608V15h2.449l2.756,7a10.415,10.415,0,0,1,.409,1.2h.036c.181-.551.327-.962.439-1.23L139.357,15h2.362Z"
fill="#fff"
/>
<path
d="M144.964,16.426a1.019,1.019,0,0,1-.713-.272.892.892,0,0,1-.3-.688.918.918,0,0,1,.3-.7,1.007,1.007,0,0,1,.713-.278,1.038,1.038,0,0,1,.731.278.911.911,0,0,1,.3.7.9.9,0,0,1-.3.678A1.033,1.033,0,0,1,144.964,16.426Zm.841,9.074h-1.7V18h1.7Z"
fill="#fff"
/>
<path
d="M153.378,25.156a4.185,4.185,0,0,1-2.127.52,3.6,3.6,0,0,1-2.69-1.044,3.7,3.7,0,0,1-1.024-2.706,4.074,4.074,0,0,1,1.1-2.978,3.93,3.93,0,0,1,2.942-1.124,4.281,4.281,0,0,1,1.806.36v1.582a2.73,2.73,0,0,0-1.667-.586,2.312,2.312,0,0,0-1.762.728,2.669,2.669,0,0,0-.687,1.908,2.54,2.54,0,0,0,.647,1.838,2.291,2.291,0,0,0,1.736.674,2.708,2.708,0,0,0,1.725-.652Z"
fill="#fff"
/>
<path
d="M159.4,19.619a1.4,1.4,0,0,0-.884-.242,1.514,1.514,0,0,0-1.258.682,3.047,3.047,0,0,0-.5,1.852V25.5h-1.7V18h1.7v1.545h.029a2.6,2.6,0,0,1,.764-1.233,1.72,1.72,0,0,1,1.151-.444,1.425,1.425,0,0,1,.7.14Z"
fill="#fff"
/>
<path
d="M163.788,25.676a3.71,3.71,0,0,1-2.767-1.051,3.8,3.8,0,0,1-1.035-2.787,3.7,3.7,0,0,1,3.985-4.014,3.581,3.581,0,0,1,2.733,1.033,3.994,3.994,0,0,1,.98,2.864,3.938,3.938,0,0,1-1.056,2.875A3.8,3.8,0,0,1,163.788,25.676Zm.08-6.5a1.932,1.932,0,0,0-1.571.7,2.913,2.913,0,0,0-.578,1.919,2.744,2.744,0,0,0,.585,1.856,1.957,1.957,0,0,0,1.564.678,1.862,1.862,0,0,0,1.539-.666,2.95,2.95,0,0,0,.537-1.9,2.99,2.99,0,0,0-.537-1.911A1.851,1.851,0,0,0,163.868,19.18Z"
fill="#fff"
/>
<path
d="M168.94,25.266V23.691a3.383,3.383,0,0,0,2.1.725q1.535,0,1.535-.908a.714.714,0,0,0-.132-.436,1.263,1.263,0,0,0-.354-.318,2.864,2.864,0,0,0-.526-.25c-.2-.072-.428-.155-.677-.248a7.074,7.074,0,0,1-.829-.389,2.526,2.526,0,0,1-.615-.465,1.758,1.758,0,0,1-.369-.59,2.168,2.168,0,0,1-.124-.769,1.775,1.775,0,0,1,.256-.955,2.224,2.224,0,0,1,.687-.7,3.294,3.294,0,0,1,.979-.425A4.49,4.49,0,0,1,172,17.824a5.163,5.163,0,0,1,1.856.315v1.487a3.127,3.127,0,0,0-1.812-.542,2.323,2.323,0,0,0-.582.066,1.477,1.477,0,0,0-.442.183.893.893,0,0,0-.285.282.677.677,0,0,0-.1.363.779.779,0,0,0,.1.41.936.936,0,0,0,.3.3,2.675,2.675,0,0,0,.482.234q.282.105.648.23a9.5,9.5,0,0,1,.866.4,2.872,2.872,0,0,1,.654.465,1.789,1.789,0,0,1,.416.6,2.034,2.034,0,0,1,.147.81,1.855,1.855,0,0,1-.263,1,2.212,2.212,0,0,1-.7.7,3.28,3.28,0,0,1-1.013.413,5.2,5.2,0,0,1-1.209.136A5.1,5.1,0,0,1,168.94,25.266Z"
fill="#fff"
/>
<path
d="M179.183,25.676a3.711,3.711,0,0,1-2.768-1.051,3.8,3.8,0,0,1-1.034-2.787,3.7,3.7,0,0,1,3.984-4.014,3.585,3.585,0,0,1,2.734,1.033,3.993,3.993,0,0,1,.979,2.864,3.934,3.934,0,0,1-1.056,2.875A3.794,3.794,0,0,1,179.183,25.676Zm.08-6.5a1.934,1.934,0,0,0-1.572.7,2.919,2.919,0,0,0-.578,1.919,2.749,2.749,0,0,0,.585,1.856,1.959,1.959,0,0,0,1.565.678,1.864,1.864,0,0,0,1.539-.666,2.956,2.956,0,0,0,.537-1.9,3,3,0,0,0-.537-1.911A1.852,1.852,0,0,0,179.263,19.18Z"
fill="#fff"
/>
<path
d="M188.787,15.781a1.523,1.523,0,0,0-.782-.2q-1.235,0-1.235,1.4V18h1.74v1.333h-1.733V25.5h-1.7V19.333H183.8V18h1.279V16.784a2.37,2.37,0,0,1,.775-1.871,2.817,2.817,0,0,1,1.937-.684,2.866,2.866,0,0,1,.994.138Z"
fill="#fff"
/>
<path
d="M193.94,25.42a2.944,2.944,0,0,1-1.307.248q-2.179,0-2.179-2.094V19.333H189.2V18h1.25V16.264l1.7-.483V18h1.79v1.333h-1.79v3.75a1.472,1.472,0,0,0,.242.952,1,1,0,0,0,.8.285,1.162,1.162,0,0,0,.745-.248Z"
fill="#fff"
/>
<rect x="13" y="11" width="9" height="9" fill="#f25022" />
<rect x="13" y="21" width="9" height="9" fill="#00a4ef" />
<rect x="23" y="11" width="9" height="9" fill="#7fba00" />
<rect x="23" y="21" width="9" height="9" fill="#ffb900" />
</svg>
</Button>
);
};
export default MicrosoftSignInButton;

View file

@ -5,6 +5,8 @@ import * as portfolioSchema from "@/app/db/schema/portfolio";
import * as propertySchema from "@/app/db/schema/property";
import * as recommendationSchema from "@/app/db/schema/recommendations";
import * as materialSchema from "@/app/db/schema/materials";
import * as solarSchema from "@/app/db/schema/solar";
import * as EnergyAssessmentsSchema from "@/app/db/schema/energy_assessments";
import * as Relations from "@/app/db/schema/relations";
export const pool = new Pool({
@ -22,7 +24,9 @@ const schema = {
...propertySchema,
...recommendationSchema,
...materialSchema,
...solarSchema,
...Relations,
...EnergyAssessmentsSchema,
};
export const db = drizzle(pool, {

View file

@ -0,0 +1 @@
ALTER TABLE "property" ADD COLUMN "current_valuation" real;

View file

@ -0,0 +1,3 @@
ALTER TABLE "plan" ADD COLUMN "valuation_increase_lower_bound" real;--> statement-breakpoint
ALTER TABLE "plan" ADD COLUMN "valuation_increase_upper_bound" real;--> statement-breakpoint
ALTER TABLE "plan" ADD COLUMN "valuation_increase_average" real;

View file

@ -0,0 +1,20 @@
CREATE TABLE IF NOT EXISTS "non_intrusive_survey" (
"id" bigserial PRIMARY KEY NOT NULL,
"uprn" bigint,
"survey_date" timestamp,
"surveyor" text
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "non_intrusive_survey_notes" (
"id" bigserial PRIMARY KEY NOT NULL,
"survey_id" bigint NOT NULL,
"title" text,
"note" text
);
--> statement-breakpoint
ALTER TABLE "recommendation" ADD COLUMN "is_override" boolean DEFAULT false;--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "non_intrusive_survey_notes" ADD CONSTRAINT "non_intrusive_survey_notes_survey_id_non_intrusive_survey_id_fk" FOREIGN KEY ("survey_id") REFERENCES "non_intrusive_survey"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;

View file

@ -0,0 +1,3 @@
ALTER TABLE "non_intrusive_survey" ALTER COLUMN "uprn" SET NOT NULL;--> statement-breakpoint
ALTER TABLE "non_intrusive_survey" ALTER COLUMN "survey_date" SET NOT NULL;--> statement-breakpoint
ALTER TABLE "non_intrusive_survey" ALTER COLUMN "surveyor" SET NOT NULL;

View file

@ -0,0 +1,2 @@
ALTER TABLE "non_intrusive_survey_notes" ALTER COLUMN "title" SET NOT NULL;--> statement-breakpoint
ALTER TABLE "non_intrusive_survey_notes" ALTER COLUMN "note" SET NOT NULL;

View file

@ -0,0 +1,2 @@
ALTER TABLE "recommendation" ADD COLUMN "already_installed" boolean DEFAULT false;--> statement-breakpoint
ALTER TABLE "recommendation" DROP COLUMN IF EXISTS "is_override";

View file

@ -0,0 +1,13 @@
ALTER TABLE "portfolio" ADD COLUMN "epc_breakdown_pre_retrofit" text;--> statement-breakpoint
ALTER TABLE "portfolio" ADD COLUMN "epc_breakdown_post_retrofit" text;--> statement-breakpoint
ALTER TABLE "portfolio" ADD COLUMN "n_units_to_retrofit" integer;--> statement-breakpoint
ALTER TABLE "portfolio" ADD COLUMN "co2_per_unit_pre_retrofit" text;--> statement-breakpoint
ALTER TABLE "portfolio" ADD COLUMN "co2_per_unit_post_retrofit" text;--> statement-breakpoint
ALTER TABLE "portfolio" ADD COLUMN "energy_bill_per_unit_pre_retrofit" text;--> statement-breakpoint
ALTER TABLE "portfolio" ADD COLUMN "energy_bill_per_unit_post_retrofit" text;--> statement-breakpoint
ALTER TABLE "portfolio" ADD COLUMN "energy_consumption_per_unit_pre_retrofit" text;--> statement-breakpoint
ALTER TABLE "portfolio" ADD COLUMN "energy_consumption_per_unit_post_retrofit" text;--> statement-breakpoint
ALTER TABLE "portfolio" ADD COLUMN "valuation_improvement_per_unit" text;--> statement-breakpoint
ALTER TABLE "portfolio" ADD COLUMN "cost_per_unit" text;--> statement-breakpoint
ALTER TABLE "portfolio" ADD COLUMN "cost_per_co2_saved" text;--> statement-breakpoint
ALTER TABLE "portfolio" ADD COLUMN "cost_per_sap_point" text;

View file

@ -0,0 +1 @@
ALTER TABLE "portfolio" ADD COLUMN "valuation_return_on_investment" text;

View file

@ -0,0 +1 @@
ALTER TABLE "material" ADD COLUMN "is_installer_quote" boolean DEFAULT false;

View file

@ -0,0 +1 @@
ALTER TABLE "recommendation" RENAME COLUMN "adjusted_heat_demand" TO "kwh_savings";

View file

@ -0,0 +1,9 @@
CREATE TABLE IF NOT EXISTS "solar" (
"id" bigserial PRIMARY KEY NOT NULL,
"longitude" real NOT NULL,
"latitude" real NOT NULL,
"uprn" text NOT NULL,
"created_at" timestamp (6) with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp (6) with time zone DEFAULT now() NOT NULL,
"api_response" jsonb NOT NULL
);

View file

@ -0,0 +1 @@
ALTER TABLE "solar" RENAME COLUMN "api_response" TO "google_api_response";

View file

@ -0,0 +1 @@
ALTER TABLE "solar" ALTER COLUMN "uprn" SET DATA TYPE bigint USING "uprn"::bigint;

View file

@ -0,0 +1,27 @@
DO $$ BEGIN
CREATE TYPE "scenario_type" AS ENUM('unit', 'building');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "solar_scenario" (
"id" bigserial PRIMARY KEY NOT NULL,
"solar_id" bigint NOT NULL,
"scenario_type" "scenario_type" NOT NULL,
"number_panels" integer NOT NULL,
"array_kwhp" integer NOT NULL,
"lifetime_dc_kwh" real NOT NULL,
"yearly_dc_kwh" real NOT NULL,
"lifetime_ac_kwh" real,
"yearly_ac_kwh" real,
"cost" real NOT NULL,
"expected_payback_years" real,
"panelled_roof_area" real NOT NULL,
"is_default" boolean NOT NULL
);
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "solar_scenario" ADD CONSTRAINT "solar_scenario_solar_id_solar_id_fk" FOREIGN KEY ("solar_id") REFERENCES "solar"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;

View file

@ -0,0 +1,115 @@
CREATE TABLE IF NOT EXISTS "energy_assessments" (
"id" bigserial PRIMARY KEY NOT NULL,
"uprn" bigint NOT NULL,
"uprn_source" text NOT NULL,
"property_type" text NOT NULL,
"building_reference_number" text,
"current_energy_efficiency" text NOT NULL,
"current_energy_rating" text NOT NULL,
"address1" text NOT NULL,
"address2" text NOT NULL,
"address3" text,
"posttown" text NOT NULL,
"postcode" text NOT NULL,
"address" text NOT NULL,
"county" text[],
"constituency" text,
"constituency_label" text,
"low_energy_fixed_light_count" text NOT NULL,
"construction_age_band" text NOT NULL,
"mainheat_energy_eff" text NOT NULL,
"windows_env_eff" text NOT NULL,
"lighting_energy_eff" text NOT NULL,
"environmentImpactPotential" text NOT NULL,
"mainheatcont_description" text NOT NULL,
"sheating_energy_eff" text NOT NULL,
"local_authority" text NOT NULL,
"local_authority_label" text NOT NULL,
"fixed_lighting_outlets_count" text NOT NULL,
"energy_tariff" text NOT NULL,
"mechanical_ventilation" text NOT NULL,
"solar_water_heating_flag" text NOT NULL,
"co2_emissions_potential" text NOT NULL,
"number_heated_rooms" text NOT NULL,
"floor_description" text NOT NULL,
"energy_consumption_potential" text NOT NULL,
"built_form" text NOT NULL,
"number_open_fireplaces" text NOT NULL,
"windows_description" text NOT NULL,
"glazed_area" text NOT NULL,
"inspection_date" timestamp (6) with time zone NOT NULL,
"mains_gas_flag" text NOT NULL,
"co2_emiss_curr_per_floor_area" text NOT NULL,
"heat_loss_corridor" text NOT NULL,
"unheated_corridor_length" text,
"flat_storey_count" text,
"roof_energy_eff" text NOT NULL,
"total_floor_area" text NOT NULL,
"environment_impact_current" text NOT NULL,
"roof_description" text NOT NULL,
"floor_energy_eff" text NOT NULL,
"number_habitable_rooms" text NOT NULL,
"hot_water_env_eff" text NOT NULL,
"mainheatc_energy_eff" text NOT NULL,
"main_fuel" text NOT NULL,
"lighting_env_eff" text NOT NULL,
"windows_energy_eff" text NOT NULL,
"floor_env_eff" text NOT NULL,
"sheating_env_eff" text NOT NULL,
"lighting_description" text NOT NULL,
"roof_env_eff" text NOT NULL,
"walls_energy_eff" text NOT NULL,
"photo_supply" text NOT NULL,
"lighting_cost_potential" text NOT NULL,
"mainheat_env_eff" text NOT NULL,
"multi_glaze_proportion" text NOT NULL,
"main_heating_controls" text NOT NULL,
"flat_top_storey" text,
"secondheat_description" text NOT NULL,
"walls_env_eff" text NOT NULL,
"transaction_type" text NOT NULL,
"extension_count" text NOT NULL,
"mainheatc_env_eff" text NOT NULL,
"lmk_key" text,
"wind_turbine_count" text NOT NULL,
"tenure" text NOT NULL,
"floor_level" text NOT NULL,
"potential_energy_efficiency" text NOT NULL,
"potential_energy_rating" text NOT NULL,
"hot_water_energy_eff" text NOT NULL,
"low_energy_lighting" text NOT NULL,
"walls_description" text NOT NULL,
"hotwater_description" text NOT NULL,
"co2_emissions_current" text NOT NULL,
"heating_cost_current" text NOT NULL,
"heating_cost_potential" text NOT NULL,
"hot_water_cost_current" text NOT NULL,
"hot_water_cost_potential" text NOT NULL,
"lighting_cost_current" text NOT NULL,
"energy_consumption_current" text NOT NULL,
"lodgement_date" timestamp (6) with time zone NOT NULL,
"lodgement_datetime" timestamp (6) with time zone NOT NULL,
"mainheat_description" text NOT NULL,
"floor_height" real NOT NULL,
"glazed_type" text NOT NULL,
"file_location" text NOT NULL,
"surveyor_name" text NOT NULL,
"surveyor_company" text NOT NULL,
"space_heating_kwh" text NOT NULL,
"water_heating_kwh" text NOT NULL,
"number_of_doors" integer NOT NULL,
"number_of_insulated_doors" integer NOT NULL,
"number_of_floors" integer NOT NULL,
"insulation_wall_area" real NOT NULL,
"heat_loss_perimeter" real NOT NULL,
"party_wall_length" real NOT NULL,
"perimeter" real NOT NULL,
"rooms_with_bath_and_or_shower" integer,
"rooms_with_mixer_shower_no_bath" integer,
"room_with_bath_and_mixer_shower" integer,
"percent_draftproofed" integer,
"has_hot_water_cylinder" boolean,
"cylinder_insulation_type" text,
"cylinder_insulation_thickness" integer,
"cylinder_thermostat" boolean
);

View file

@ -0,0 +1 @@
ALTER TABLE "energy_assessments" RENAME COLUMN "environmentImpactPotential" TO "environment_impact_potential";

View file

@ -0,0 +1 @@
ALTER TABLE "energy_assessments" ALTER COLUMN "county" SET DATA TYPE text;

View file

@ -0,0 +1 @@
ALTER TABLE "energy_assessments" ADD COLUMN "main_dwelling_ground_floor_area" real;

View file

@ -0,0 +1,2 @@
ALTER TABLE "energy_assessments" ADD COLUMN "number_of_windows" integer;--> statement-breakpoint
ALTER TABLE "energy_assessments" ADD COLUMN "windows_area" real;

View file

@ -0,0 +1,2 @@
ALTER TABLE "energy_assessments" ALTER COLUMN "lodgement_date" SET DATA TYPE date;--> statement-breakpoint
ALTER TABLE "energy_assessments" ALTER COLUMN "lodgement_datetime" SET DATA TYPE timestamp (6);

View file

@ -0,0 +1 @@
ALTER TABLE "plan" ADD COLUMN "name" text;

View file

@ -0,0 +1,20 @@
ALTER TABLE "plan" ADD COLUMN "cost" real;--> statement-breakpoint
ALTER TABLE "plan" ADD COLUMN "total_work_hours" real;--> statement-breakpoint
ALTER TABLE "plan" ADD COLUMN "energy_savings" real;--> statement-breakpoint
ALTER TABLE "plan" ADD COLUMN "co2_equivalent_savings" real;--> statement-breakpoint
ALTER TABLE "plan" ADD COLUMN "energy_cost_savings" real;--> statement-breakpoint
ALTER TABLE "plan" ADD COLUMN "epc_breakdown_pre_retrofit" text;--> statement-breakpoint
ALTER TABLE "plan" ADD COLUMN "epc_breakdown_post_retrofit" text;--> statement-breakpoint
ALTER TABLE "plan" ADD COLUMN "number_of_properties" integer;--> statement-breakpoint
ALTER TABLE "plan" ADD COLUMN "n_units_to_retrofit" integer;--> statement-breakpoint
ALTER TABLE "plan" ADD COLUMN "co2_per_unit_pre_retrofit" text;--> statement-breakpoint
ALTER TABLE "plan" ADD COLUMN "co2_per_unit_post_retrofit" text;--> statement-breakpoint
ALTER TABLE "plan" ADD COLUMN "energy_bill_per_unit_pre_retrofit" text;--> statement-breakpoint
ALTER TABLE "plan" ADD COLUMN "energy_bill_per_unit_post_retrofit" text;--> statement-breakpoint
ALTER TABLE "plan" ADD COLUMN "energy_consumption_per_unit_pre_retrofit" text;--> statement-breakpoint
ALTER TABLE "plan" ADD COLUMN "energy_consumption_per_unit_post_retrofit" text;--> statement-breakpoint
ALTER TABLE "plan" ADD COLUMN "valuation_improvement_per_unit" text;--> statement-breakpoint
ALTER TABLE "plan" ADD COLUMN "cost_per_unit" text;--> statement-breakpoint
ALTER TABLE "plan" ADD COLUMN "cost_per_co2_saved" text;--> statement-breakpoint
ALTER TABLE "plan" ADD COLUMN "cost_per_sap_point" text;--> statement-breakpoint
ALTER TABLE "plan" ADD COLUMN "valuation_return_on_investment" text;

View file

@ -0,0 +1,34 @@
DO $$ BEGIN
CREATE TYPE "housing_type" AS ENUM('Private', 'Social');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "scenario" (
"id" bigserial PRIMARY KEY NOT NULL,
"name" text,
"budget" real,
"portfolio_id" bigint NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL,
"housing_type" "housing_type" NOT NULL,
"goal" "goal" NOT NULL,
"trigger_file_path" text,
"already_installed_file_path" text,
"patches_file_path" text,
"non_invasive_recommendations_file_path" text,
"exclusions" text,
"multi_plan" boolean
);
--> statement-breakpoint
ALTER TABLE "plan" ADD COLUMN "scenario_id" bigint;--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "plan" ADD CONSTRAINT "plan_scenario_id_scenario_id_fk" FOREIGN KEY ("scenario_id") REFERENCES "scenario"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "scenario" ADD CONSTRAINT "scenario_portfolio_id_portfolio_id_fk" FOREIGN KEY ("portfolio_id") REFERENCES "portfolio"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;

View file

@ -0,0 +1,20 @@
ALTER TABLE "plan" DROP COLUMN IF EXISTS "cost";--> statement-breakpoint
ALTER TABLE "plan" DROP COLUMN IF EXISTS "total_work_hours";--> statement-breakpoint
ALTER TABLE "plan" DROP COLUMN IF EXISTS "energy_savings";--> statement-breakpoint
ALTER TABLE "plan" DROP COLUMN IF EXISTS "co2_equivalent_savings";--> statement-breakpoint
ALTER TABLE "plan" DROP COLUMN IF EXISTS "energy_cost_savings";--> statement-breakpoint
ALTER TABLE "plan" DROP COLUMN IF EXISTS "epc_breakdown_pre_retrofit";--> statement-breakpoint
ALTER TABLE "plan" DROP COLUMN IF EXISTS "epc_breakdown_post_retrofit";--> statement-breakpoint
ALTER TABLE "plan" DROP COLUMN IF EXISTS "number_of_properties";--> statement-breakpoint
ALTER TABLE "plan" DROP COLUMN IF EXISTS "n_units_to_retrofit";--> statement-breakpoint
ALTER TABLE "plan" DROP COLUMN IF EXISTS "co2_per_unit_pre_retrofit";--> statement-breakpoint
ALTER TABLE "plan" DROP COLUMN IF EXISTS "co2_per_unit_post_retrofit";--> statement-breakpoint
ALTER TABLE "plan" DROP COLUMN IF EXISTS "energy_bill_per_unit_pre_retrofit";--> statement-breakpoint
ALTER TABLE "plan" DROP COLUMN IF EXISTS "energy_bill_per_unit_post_retrofit";--> statement-breakpoint
ALTER TABLE "plan" DROP COLUMN IF EXISTS "energy_consumption_per_unit_pre_retrofit";--> statement-breakpoint
ALTER TABLE "plan" DROP COLUMN IF EXISTS "energy_consumption_per_unit_post_retrofit";--> statement-breakpoint
ALTER TABLE "plan" DROP COLUMN IF EXISTS "valuation_improvement_per_unit";--> statement-breakpoint
ALTER TABLE "plan" DROP COLUMN IF EXISTS "cost_per_unit";--> statement-breakpoint
ALTER TABLE "plan" DROP COLUMN IF EXISTS "cost_per_co2_saved";--> statement-breakpoint
ALTER TABLE "plan" DROP COLUMN IF EXISTS "cost_per_sap_point";--> statement-breakpoint
ALTER TABLE "plan" DROP COLUMN IF EXISTS "valuation_return_on_investment";

View file

@ -0,0 +1,20 @@
ALTER TABLE "scenario" ADD COLUMN "cost" real;--> statement-breakpoint
ALTER TABLE "scenario" ADD COLUMN "total_work_hours" real;--> statement-breakpoint
ALTER TABLE "scenario" ADD COLUMN "energy_savings" real;--> statement-breakpoint
ALTER TABLE "scenario" ADD COLUMN "co2_equivalent_savings" real;--> statement-breakpoint
ALTER TABLE "scenario" ADD COLUMN "energy_cost_savings" real;--> statement-breakpoint
ALTER TABLE "scenario" ADD COLUMN "epc_breakdown_pre_retrofit" text;--> statement-breakpoint
ALTER TABLE "scenario" ADD COLUMN "epc_breakdown_post_retrofit" text;--> statement-breakpoint
ALTER TABLE "scenario" ADD COLUMN "number_of_properties" integer;--> statement-breakpoint
ALTER TABLE "scenario" ADD COLUMN "n_units_to_retrofit" integer;--> statement-breakpoint
ALTER TABLE "scenario" ADD COLUMN "co2_per_unit_pre_retrofit" text;--> statement-breakpoint
ALTER TABLE "scenario" ADD COLUMN "co2_per_unit_post_retrofit" text;--> statement-breakpoint
ALTER TABLE "scenario" ADD COLUMN "energy_bill_per_unit_pre_retrofit" text;--> statement-breakpoint
ALTER TABLE "scenario" ADD COLUMN "energy_bill_per_unit_post_retrofit" text;--> statement-breakpoint
ALTER TABLE "scenario" ADD COLUMN "energy_consumption_per_unit_pre_retrofit" text;--> statement-breakpoint
ALTER TABLE "scenario" ADD COLUMN "energy_consumption_per_unit_post_retrofit" text;--> statement-breakpoint
ALTER TABLE "scenario" ADD COLUMN "valuation_improvement_per_unit" text;--> statement-breakpoint
ALTER TABLE "scenario" ADD COLUMN "cost_per_unit" text;--> statement-breakpoint
ALTER TABLE "scenario" ADD COLUMN "cost_per_co2_saved" text;--> statement-breakpoint
ALTER TABLE "scenario" ADD COLUMN "cost_per_sap_point" text;--> statement-breakpoint
ALTER TABLE "scenario" ADD COLUMN "valuation_return_on_investment" text;

View file

@ -0,0 +1 @@
ALTER TABLE "scenario" ADD COLUMN "is_default" boolean;

View file

@ -0,0 +1 @@
ALTER TABLE "scenario" ALTER COLUMN "is_default" SET NOT NULL;

View file

@ -0,0 +1,2 @@
ALTER TABLE "scenario" ADD COLUMN "property_valuation_increase" real;--> statement-breakpoint
ALTER TABLE "scenario" ADD COLUMN "labour_days" real;

View file

@ -0,0 +1 @@
ALTER TABLE "property_details_epc" RENAME COLUMN "adjusted_energy_consumption" TO "current_energy_demand";

View file

@ -0,0 +1 @@
ALTER TABLE "property_details_epc" ADD COLUMN "current_energy_demand_heating_hotwater" real;

View file

@ -0,0 +1,14 @@
DO $$ BEGIN
CREATE TYPE "document_type" AS ENUM('EPR', 'Condition Report', 'Evidence Report', 'Summary Information', 'Floor Plan', 'Scenario EPR');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "energy_assessment_documents" (
"id" bigserial PRIMARY KEY NOT NULL,
"uprn" bigint NOT NULL,
"energy_assessment_id" bigint NOT NULL,
"document_type" "document_type" NOT NULL,
"document_location" text NOT NULL,
"uploaded_at" timestamp (6) with time zone DEFAULT now() NOT NULL
);

View file

@ -0,0 +1 @@
ALTER TYPE "document_type" ADD VALUE 'Scenario Draft EPC';

View file

@ -0,0 +1 @@
ALTER TYPE "document_type" ADD VALUE 'Scenario Site Notes';

View file

@ -0,0 +1,24 @@
CREATE TABLE IF NOT EXISTS "energy_assessment_scenarios" (
"id" bigserial PRIMARY KEY NOT NULL,
"scenario_name" text NOT NULL,
"energy_assessment_id" bigint NOT NULL
);
--> statement-breakpoint
ALTER TABLE "energy_assessment_documents" ADD COLUMN "scenario_id" bigint;--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "energy_assessment_documents" ADD CONSTRAINT "energy_assessment_documents_energy_assessment_id_energy_assessments_id_fk" FOREIGN KEY ("energy_assessment_id") REFERENCES "energy_assessments"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "energy_assessment_documents" ADD CONSTRAINT "energy_assessment_documents_scenario_id_energy_assessments_id_fk" FOREIGN KEY ("scenario_id") REFERENCES "energy_assessments"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "energy_assessment_scenarios" ADD CONSTRAINT "energy_assessment_scenarios_energy_assessment_id_energy_assessments_id_fk" FOREIGN KEY ("energy_assessment_id") REFERENCES "energy_assessments"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;

View file

@ -0,0 +1,7 @@
ALTER TABLE "energy_assessment_documents" DROP CONSTRAINT "energy_assessment_documents_scenario_id_energy_assessments_id_fk";
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "energy_assessment_documents" ADD CONSTRAINT "energy_assessment_documents_scenario_id_energy_assessment_scenarios_id_fk" FOREIGN KEY ("scenario_id") REFERENCES "energy_assessment_scenarios"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;

View file

@ -0,0 +1 @@
ALTER TYPE "status" ADD VALUE 'survey';

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

Some files were not shown because too many files have changed in this diff Show more