From ea600d49aaa4533e54a1f386b3de36a33f383caa Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Thu, 5 Sep 2024 18:42:35 +0100 Subject: [PATCH 1/8] adding variables to capture the impact of the new recommendations in the plan --- README.md | 8 +++++ .../building-passport/RecommendationCard.tsx | 4 +++ .../RecommendationContainer.tsx | 35 +++++++++++++++++++ src/app/db/schema/recommendations.ts | 5 ++- src/types/recommendations.ts | 3 ++ 5 files changed, 54 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b58ac46..e720d02 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/src/app/components/building-passport/RecommendationCard.tsx b/src/app/components/building-passport/RecommendationCard.tsx index cace886..f53826c 100644 --- a/src/app/components/building-passport/RecommendationCard.tsx +++ b/src/app/components/building-passport/RecommendationCard.tsx @@ -17,6 +17,7 @@ const alreadyInstalledStyling = const TitleMap = { mechanical_ventilation: "Mechanical Ventilation", + trickle_vents: "Trickle Vents", sealing_open_fireplace: "Sealing Open Fireplace", low_energy_lighting: "Low Energy Lighting", // Walls @@ -33,6 +34,7 @@ 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 @@ -47,6 +49,8 @@ const TitleMap = { roof_insulation: "Roof Insulation", // Cylinder thermostat cylinder_thermostat: "Cylinder Thermostat", + // Draught proofing + draught_proofing: "Draught Proofing", }; type RecommendationCardProps = { diff --git a/src/app/components/building-passport/RecommendationContainer.tsx b/src/app/components/building-passport/RecommendationContainer.tsx index 951e6af..50cf27b 100644 --- a/src/app/components/building-passport/RecommendationContainer.tsx +++ b/src/app/components/building-passport/RecommendationContainer.tsx @@ -127,6 +127,21 @@ export default function RecommendationContainer({ (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({ wall_insulation: defaultWallsRecommendations.estimatedCost || 0, floor_insulation: defaultFloorRecommendations.estimatedCost || 0, @@ -145,6 +160,9 @@ export default function RecommendationContainer({ 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({ @@ -163,6 +181,9 @@ export default function RecommendationContainer({ 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({ @@ -181,6 +202,9 @@ export default function RecommendationContainer({ 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({ @@ -204,6 +228,10 @@ export default function RecommendationContainer({ 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] = @@ -228,6 +256,10 @@ export default function RecommendationContainer({ defaultSecondaryHeatingRecommendations.energyCostSavings || 0, cylinder_thermostat: defaultCylinderThermostatRecommendations.energyCostSavings || 0, + trickle_vents: defaultTrickleVentsRecommendations.energyCostSavings || 0, + mixed_glazing: defaultMixedGlazingRecommendations.energyCostSavings || 0, + draught_proofing: + defaultDraughtProofingRecommendations.energyCostSavings || 0, }); const [kwhSavingsMap, setKwhSavingsMap] = useState({ @@ -246,6 +278,9 @@ export default function RecommendationContainer({ 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( diff --git a/src/app/db/schema/recommendations.ts b/src/app/db/schema/recommendations.ts index b8899bb..d272b2a 100644 --- a/src/app/db/schema/recommendations.ts +++ b/src/app/db/schema/recommendations.ts @@ -185,7 +185,10 @@ export type RecommendationType = | "hot_water_tank_insulation" | "heating_control" | "secondary_heating" - | "cylinder_thermostat"; + | "cylinder_thermostat" + | "trickle_vents" + | "mixed_glazing" + | "draught_proofing"; export type UnnestedRecommendation = { quantity: number; diff --git a/src/types/recommendations.ts b/src/types/recommendations.ts index e113988..fd0f610 100644 --- a/src/types/recommendations.ts +++ b/src/types/recommendations.ts @@ -12,4 +12,7 @@ export interface RecommendationMetricMap { heating_control: number; secondary_heating: number; cylinder_thermostat: number; + trickle_vents: number; + mixed_glazing: number; + draught_proofing: number; } From b550c47e1ae34a4ba2c8e8f7f9550d9d48ae49c6 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Thu, 5 Sep 2024 18:47:31 +0100 Subject: [PATCH 2/8] Adding structure for the energy assessment page --- .../components/building-passport/Toolbar.tsx | 18 +++++++++++------- .../[propertyId]/energy-assessment/page.tsx | 18 ++++++++++++++++++ 2 files changed, 29 insertions(+), 7 deletions(-) create mode 100644 src/app/portfolio/[slug]/building-passport/[propertyId]/energy-assessment/page.tsx diff --git a/src/app/components/building-passport/Toolbar.tsx b/src/app/components/building-passport/Toolbar.tsx index 8f57d21..58866ed 100644 --- a/src/app/components/building-passport/Toolbar.tsx +++ b/src/app/components/building-passport/Toolbar.tsx @@ -39,6 +39,16 @@ export function Toolbar({ propertyId, portfolioId }: ToolbarProps) { ); + const energyAssessmentsReportButton = ( + + + Energy Assessment + + ); + const solarAnalysisButton = ( - - Plan optimiser - */} + {energyAssessmentsReportButton} +
+
+ This property does not have a Domna energy assessement +
+

Please check back later for updates.

+
+ + ); +} From 8147a0d4b738dee655ee115de4b7de467c3b53fb Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Mon, 9 Sep 2024 12:15:55 +0100 Subject: [PATCH 3/8] Added the documents table to the app - needs api setup to generate pre-signed urls --- src/app/components/Buttons.tsx | 19 +++++ src/app/db/schema/energy_assessments.ts | 8 ++ .../energy-assessment/DocumentsTable.tsx | 85 +++++++++++++++++++ .../[propertyId]/energy-assessment/page.tsx | 48 +++++++++-- .../building-passport/[propertyId]/utils.ts | 39 ++++++++- 5 files changed, 191 insertions(+), 8 deletions(-) create mode 100644 src/app/portfolio/[slug]/building-passport/[propertyId]/energy-assessment/DocumentsTable.tsx diff --git a/src/app/components/Buttons.tsx b/src/app/components/Buttons.tsx index 7da4ab2..a568800 100644 --- a/src/app/components/Buttons.tsx +++ b/src/app/components/Buttons.tsx @@ -18,3 +18,22 @@ export function TanButton({ ); } + +export function BrandBlueButton({ + label, + onClick, +}: { + label: string; + onClick: Dispatch>; +}) { + // General tan colored button + return ( + + ); +} diff --git a/src/app/db/schema/energy_assessments.ts b/src/app/db/schema/energy_assessments.ts index 44736f7..6f2a133 100644 --- a/src/app/db/schema/energy_assessments.ts +++ b/src/app/db/schema/energy_assessments.ts @@ -195,3 +195,11 @@ export const energyAssessmentDocuments = pgTable( // Types for the new table export type EnergyAssessment = InferModel; +export type EnergyAssessmentScenario = InferModel< + typeof energyAssessmentScenarios, + "select" +>; +export type EnergyAssessmentDocument = InferModel< + typeof energyAssessmentDocuments, + "select" +>; diff --git a/src/app/portfolio/[slug]/building-passport/[propertyId]/energy-assessment/DocumentsTable.tsx b/src/app/portfolio/[slug]/building-passport/[propertyId]/energy-assessment/DocumentsTable.tsx new file mode 100644 index 0000000..49fae6e --- /dev/null +++ b/src/app/portfolio/[slug]/building-passport/[propertyId]/energy-assessment/DocumentsTable.tsx @@ -0,0 +1,85 @@ +"use client"; + +import React from "react"; +import { + Table, + TableBody, + TableCell, + TableRow, +} from "@/app/shadcn_components/ui/table"; +import { BrandBlueButton } from "@/app/components/Buttons"; +import { DocumentType } from "@/app/db/schema/energy_assessments"; + +// Use the type directly from the array +type Document = { + id: bigint; + documentType: (typeof DocumentType)[number]; // Create a union type from the array + documentLocation: string; + uploadedAt: Date; +}; + +type Props = { + documents: Document[]; + allowedTypes: (typeof DocumentType)[number][]; // Use the union type for allowedTypes as well +}; + +// Descriptions based on the document types +const descriptions: { [key in (typeof DocumentType)[number]]: string } = { + EPR: "Contains current energy performance of your home, and recommendations for improvement", + "Condition Report": + "Detailed report on property condition, including photographs", + "Evidence Report": "Additional photographic evidence from the survey", + "Summary Information": "Summarized survey outputs and site notes", + "Floor Plan": "Layout of the property and room/window dimensions", + "Scenario Draft EPC": "Draft EPC based on a given scenario", + "Scenario Site Notes": "Site notes from the scenario analysis", +}; + +export const DocumentsTable: React.FC = ({ + documents, + allowedTypes, +}) => { + const relevantDocuments = documents.filter((doc) => + allowedTypes.includes(doc.documentType) + ); + + // Track displayed descriptions + const displayedDescriptions: Record<(typeof DocumentType)[number], boolean> = + allowedTypes.reduce((acc, type) => { + acc[type] = false; + return acc; + }, {} as Record<(typeof DocumentType)[number], boolean>); + + const processedDocuments = relevantDocuments.map((doc) => { + const showDescription = !displayedDescriptions[doc.documentType]; + if (showDescription) { + displayedDescriptions[doc.documentType] = true; + } + return { ...doc, showDescription }; + }); + + return ( + + + {processedDocuments.map((doc) => ( + + + {doc.documentType} + + + {doc.showDescription ? descriptions[doc.documentType] : ""} + + + + console.log(`Downloading ${doc.documentLocation}`) + } + /> + + + ))} + +
+ ); +}; diff --git a/src/app/portfolio/[slug]/building-passport/[propertyId]/energy-assessment/page.tsx b/src/app/portfolio/[slug]/building-passport/[propertyId]/energy-assessment/page.tsx index 925a9e2..936f80b 100644 --- a/src/app/portfolio/[slug]/building-passport/[propertyId]/energy-assessment/page.tsx +++ b/src/app/portfolio/[slug]/building-passport/[propertyId]/energy-assessment/page.tsx @@ -1,17 +1,51 @@ +import { + getEnergyAssessment, + getEnergyAssessmentDocuments, + getPropertyMeta, +} from "../utils"; + +// EnergyAssessmentsPage.tsx +import { DocumentsTable } from "./DocumentsTable"; + export default async function EnergyAssessmentsPage({ params, }: { params: { slug: string; propertyId: string }; }) { - // If there's no solar data, we cannot display the page + const propertyMeta = await getPropertyMeta(params.propertyId); + const ea = await getEnergyAssessment(propertyMeta.uprn); + + if (!ea) { + return ( +
+
+
+ This property does not have a Domna energy assessment +
+

Please check back later for updates.

+
+
+ ); + } + + const documents = await getEnergyAssessmentDocuments(ea.id); + + const table1AllowedTypes = [ + "EPR", + "Condition Report", + "Evidence Report", + "Summary Information", + "Floor Plan", + ]; return ( -
-
-
- This property does not have a Domna energy assessement -
-

Please check back later for updates.

+
+
Core Survey Documents
+
+
); diff --git a/src/app/portfolio/[slug]/building-passport/[propertyId]/utils.ts b/src/app/portfolio/[slug]/building-passport/[propertyId]/utils.ts index d33e968..4e0f136 100644 --- a/src/app/portfolio/[slug]/building-passport/[propertyId]/utils.ts +++ b/src/app/portfolio/[slug]/building-passport/[propertyId]/utils.ts @@ -1,4 +1,3 @@ -import { recommendation } from "../../../../db/schema/recommendations"; import { Recommendation, planRecommendations, @@ -18,11 +17,49 @@ import { import { plan, Plan } from "@/app/db/schema/recommendations"; import { getRating } from "@/app/utils"; import { eq, desc } from "drizzle-orm"; +import { + energyAssessment, + EnergyAssessment, + energyAssessmentDocuments, + EnergyAssessmentDocument, +} from "@/app/db/schema/energy_assessments"; type RecommendationList = { recommendation: Recommendation; }[]; +export async function getEnergyAssessment( + uprn: number +): Promise { + const data = await db.query.energyAssessment.findFirst({ + where: eq(energyAssessment.uprn, BigInt(uprn)), + }); + + if (!data) { + // If there's no data, we return an empty array. This signififies that no energy assessment has been conducted + return {} as EnergyAssessment; + } + + return data; +} + +export async function getEnergyAssessmentDocuments( + energyAssessmentId: bigint +): Promise { + const data = await db.query.energyAssessmentDocuments.findMany({ + where: eq( + energyAssessmentDocuments.energyAssessmentId, + BigInt(energyAssessmentId) + ), + }); + + if (!data) { + throw new Error("Network response was not ok"); + } + + return data; +} + export async function getRecommendations( planId: string ): Promise { From 07be8eaf8407e0dab6e6661e00bf8ccc589774e8 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Mon, 9 Sep 2024 14:52:34 +0100 Subject: [PATCH 4/8] presign api working but want to use temp aws credentials| --- .../api/energy-assessment-documents/route.ts | 49 +++++++++++++++++ .../energy-assessment/DocumentsTable.tsx | 54 ++++++++++++------- 2 files changed, 84 insertions(+), 19 deletions(-) create mode 100644 src/app/api/energy-assessment-documents/route.ts diff --git a/src/app/api/energy-assessment-documents/route.ts b/src/app/api/energy-assessment-documents/route.ts new file mode 100644 index 0000000..89b9140 --- /dev/null +++ b/src/app/api/energy-assessment-documents/route.ts @@ -0,0 +1,49 @@ +// pages/api/get-presigned-url.ts +import S3 from "aws-sdk/clients/s3"; +import { NextRequest, NextResponse } from "next/server"; +import { z } from "zod"; + +const PresignedUrlBodySchema = z.object({ + fileKey: z.string(), +}); + +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 { + const s3 = new S3({ + signatureVersion: "v4", + region: process.env.PRESIGN_AWS_REGION, + accessKeyId: process.env.RETROFIT_ENERGY_ASSESSMENTS_AWS_ACCESS_KEY, + secretAccessKey: process.env.ENERGY_ASSESSMENTS_AWS_SECRET, + }); + + const { fileKey } = validatedBody; + + // Presigned URL is valid for 5 minutes + const preSignedUrl = await s3.getSignedUrl("getObject", { + Bucket: process.env.RETROFIT_ENERGY_ASSESSMENTS_BUCKET, + Key: fileKey, + Expires: 5 * 60, + }); + + return new NextResponse(JSON.stringify({ url: preSignedUrl }), { + status: 200, + }); + } catch (error) { + console.error(error); + return new NextResponse(JSON.stringify({ msg: "Internal server error" }), { + status: 500, + }); + } +} diff --git a/src/app/portfolio/[slug]/building-passport/[propertyId]/energy-assessment/DocumentsTable.tsx b/src/app/portfolio/[slug]/building-passport/[propertyId]/energy-assessment/DocumentsTable.tsx index 49fae6e..05d6318 100644 --- a/src/app/portfolio/[slug]/building-passport/[propertyId]/energy-assessment/DocumentsTable.tsx +++ b/src/app/portfolio/[slug]/building-passport/[propertyId]/energy-assessment/DocumentsTable.tsx @@ -8,13 +8,14 @@ import { TableRow, } from "@/app/shadcn_components/ui/table"; import { BrandBlueButton } from "@/app/components/Buttons"; +import { useMutation } from "@tanstack/react-query"; import { DocumentType } from "@/app/db/schema/energy_assessments"; // Use the type directly from the array type Document = { id: bigint; documentType: (typeof DocumentType)[number]; // Create a union type from the array - documentLocation: string; + documentLocation: string; // S3 file key uploadedAt: Date; }; @@ -35,6 +36,21 @@ const descriptions: { [key in (typeof DocumentType)[number]]: string } = { "Scenario Site Notes": "Site notes from the scenario analysis", }; +// Fetch the presigned URL from the API +async function generatePresignedUrl(fileKey: string) { + const response = await fetch("/api/energy-assessment-documents", { + method: "POST", + body: JSON.stringify({ fileKey }), + }); + + if (!response.ok) { + throw new Error("Failed to generate presigned URL"); + } + + const data = await response.json(); + return data.url; +} + export const DocumentsTable: React.FC = ({ documents, allowedTypes, @@ -43,38 +59,38 @@ export const DocumentsTable: React.FC = ({ allowedTypes.includes(doc.documentType) ); - // Track displayed descriptions - const displayedDescriptions: Record<(typeof DocumentType)[number], boolean> = - allowedTypes.reduce((acc, type) => { - acc[type] = false; - return acc; - }, {} as Record<(typeof DocumentType)[number], boolean>); - - const processedDocuments = relevantDocuments.map((doc) => { - const showDescription = !displayedDescriptions[doc.documentType]; - if (showDescription) { - displayedDescriptions[doc.documentType] = true; + // Mutation to handle the presigned URL generation + const { mutate: fetchPresignedUrl } = useMutation( + async (fileKey: string) => await generatePresignedUrl(fileKey), + { + onSuccess: (url) => { + window.open(url, "_blank"); // Open the file in a new tab + }, + onError: (error) => { + console.error("Error generating presigned URL:", error); + }, } - return { ...doc, showDescription }; - }); + ); + + const handleDownload = (documentLocation: string) => { + fetchPresignedUrl(documentLocation); // Generate URL and open in new tab + }; return ( - {processedDocuments.map((doc) => ( + {relevantDocuments.map((doc) => ( {doc.documentType} - {doc.showDescription ? descriptions[doc.documentType] : ""} + {descriptions[doc.documentType] || ""} - console.log(`Downloading ${doc.documentLocation}`) - } + onClick={() => handleDownload(doc.documentLocation)} // Call the download handler /> From 2b17aad9a791a151e4f48a36c9f2ddd85deb1d50 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Mon, 9 Sep 2024 15:00:14 +0100 Subject: [PATCH 5/8] using rotating aws credentials --- .../api/energy-assessment-documents/route.ts | 31 ++++++++++++++++--- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/src/app/api/energy-assessment-documents/route.ts b/src/app/api/energy-assessment-documents/route.ts index 89b9140..db263ca 100644 --- a/src/app/api/energy-assessment-documents/route.ts +++ b/src/app/api/energy-assessment-documents/route.ts @@ -1,12 +1,28 @@ // 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, + }); + + // Request temporary credentials with GetSessionToken + const data = await sts.getSessionToken({ DurationSeconds: 900 }).promise(); // Token valid for 15 minutes + return data.Credentials; +} + +// API handler export async function POST(request: NextRequest) { const body = await request.json(); let validatedBody; @@ -21,27 +37,32 @@ export async function POST(request: NextRequest) { } 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: process.env.RETROFIT_ENERGY_ASSESSMENTS_AWS_ACCESS_KEY, - secretAccessKey: process.env.ENERGY_ASSESSMENTS_AWS_SECRET, + accessKeyId: credentials.AccessKeyId, + secretAccessKey: credentials.SecretAccessKey, + sessionToken: credentials.SessionToken, // Include session token }); const { fileKey } = validatedBody; - // Presigned URL is valid for 5 minutes + // 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, + Expires: 5 * 60, // URL expiration in seconds }); return new NextResponse(JSON.stringify({ url: preSignedUrl }), { status: 200, }); } catch (error) { - console.error(error); + console.error("Error generating presigned URL:", error); return new NextResponse(JSON.stringify({ msg: "Internal server error" }), { status: 500, }); From 22aea5ec6e175ce983fec78cef2f3a689b340d08 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Mon, 9 Sep 2024 15:12:41 +0100 Subject: [PATCH 6/8] fixing typescript error --- .../api/energy-assessment-documents/route.ts | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/app/api/energy-assessment-documents/route.ts b/src/app/api/energy-assessment-documents/route.ts index db263ca..86512b7 100644 --- a/src/app/api/energy-assessment-documents/route.ts +++ b/src/app/api/energy-assessment-documents/route.ts @@ -17,9 +17,20 @@ async function getTemporaryCredentials() { region: process.env.PRESIGN_AWS_REGION, }); - // Request temporary credentials with GetSessionToken - const data = await sts.getSessionToken({ DurationSeconds: 900 }).promise(); // Token valid for 15 minutes - return data.Credentials; + 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 From fd0417f3e38aa1fa7f5ac2dfc7abbb5914202118 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Mon, 9 Sep 2024 16:42:21 +0100 Subject: [PATCH 7/8] styling energy assessment page --- src/app/components/Buttons.tsx | 15 +- src/app/components/StatusBadge.tsx | 6 +- .../components/building-passport/EpcCard.tsx | 21 ++- src/app/db/schema/energy_assessments.ts | 6 + src/app/db/schema/relations.ts | 15 ++ src/app/layout.tsx | 1 - .../energy-assessment/DocumentsTable.tsx | 5 +- .../[propertyId]/energy-assessment/page.tsx | 148 +++++++++++++++++- .../building-passport/[propertyId]/utils.ts | 6 +- tailwind.config.js | 1 + 10 files changed, 203 insertions(+), 21 deletions(-) diff --git a/src/app/components/Buttons.tsx b/src/app/components/Buttons.tsx index a568800..9d2d87d 100644 --- a/src/app/components/Buttons.tsx +++ b/src/app/components/Buttons.tsx @@ -19,18 +19,27 @@ export function TanButton({ ); } -export function BrandBlueButton({ +export function BrandButton({ label, onClick, + backgroundColor, }: { label: string; onClick: Dispatch>; + backgroundColor: "brandblue" | "brandgold"; // Restrict backgroundColor to these two options }) { - // General tan colored button + // Dictionary to map background colors to hover colors + const hoverColors = { + brandblue: "hover:bg-hoverblue", + brandgold: "hover:bg-hovergold", + }; + return (
{kwh && ( - {" "} - {/* Added vertical padding to each row */} )} {carbon && ( - {" "} )} diff --git a/src/app/db/schema/energy_assessments.ts b/src/app/db/schema/energy_assessments.ts index 6f2a133..61d924e 100644 --- a/src/app/db/schema/energy_assessments.ts +++ b/src/app/db/schema/energy_assessments.ts @@ -203,3 +203,9 @@ export type EnergyAssessmentDocument = InferModel< typeof energyAssessmentDocuments, "select" >; + +// We define a type for the energyassessment docments that embeds a scenario in +// the document +export type EnergyAssessmentDocumentWithScenario = EnergyAssessmentDocument & { + scenario: EnergyAssessmentScenario | null; +}; diff --git a/src/app/db/schema/relations.ts b/src/app/db/schema/relations.ts index dfc44bd..87e7d4c 100644 --- a/src/app/db/schema/relations.ts +++ b/src/app/db/schema/relations.ts @@ -1,3 +1,7 @@ +import { + energyAssessmentDocuments, + energyAssessmentScenarios, +} from "./energy_assessments"; // This script contains ALL relations for the database, used by drizzle-orm import { relations } from "drizzle-orm"; @@ -139,3 +143,14 @@ export const nonIntrusiveSurveyNotesRelations = relations( }), }) ); + +// Define a relation from a EnergyAssessmentDocument to EnergyAssessmentScenario. This is a many to one +export const energyAssessmentDocumentsRelations = relations( + energyAssessmentDocuments, + ({ one }) => ({ + scenario: one(energyAssessmentScenarios, { + fields: [energyAssessmentDocuments.scenarioId], + references: [energyAssessmentScenarios.id], + }), + }) +); diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 1aa6c8e..c2f4e51 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -10,7 +10,6 @@ import { Inter } from "next/font/google"; // If loading a variable font, you don't need to specify the font weight const inter = Inter({ subsets: ["latin"], - display: "swap", }); export const metadata = { diff --git a/src/app/portfolio/[slug]/building-passport/[propertyId]/energy-assessment/DocumentsTable.tsx b/src/app/portfolio/[slug]/building-passport/[propertyId]/energy-assessment/DocumentsTable.tsx index 05d6318..e142b93 100644 --- a/src/app/portfolio/[slug]/building-passport/[propertyId]/energy-assessment/DocumentsTable.tsx +++ b/src/app/portfolio/[slug]/building-passport/[propertyId]/energy-assessment/DocumentsTable.tsx @@ -7,7 +7,7 @@ import { TableCell, TableRow, } from "@/app/shadcn_components/ui/table"; -import { BrandBlueButton } from "@/app/components/Buttons"; +import { BrandButton } from "@/app/components/Buttons"; import { useMutation } from "@tanstack/react-query"; import { DocumentType } from "@/app/db/schema/energy_assessments"; @@ -88,9 +88,10 @@ export const DocumentsTable: React.FC = ({ {descriptions[doc.documentType] || ""} - handleDownload(doc.documentLocation)} // Call the download handler + backgroundColor="brandgold" /> diff --git a/src/app/portfolio/[slug]/building-passport/[propertyId]/energy-assessment/page.tsx b/src/app/portfolio/[slug]/building-passport/[propertyId]/energy-assessment/page.tsx index 936f80b..092d0e4 100644 --- a/src/app/portfolio/[slug]/building-passport/[propertyId]/energy-assessment/page.tsx +++ b/src/app/portfolio/[slug]/building-passport/[propertyId]/energy-assessment/page.tsx @@ -1,11 +1,67 @@ +import { + Card, + CardContent, + CardHeader, + CardTitle, +} from "@/app/shadcn_components/ui/card"; +import EpcCard from "@/app/components/building-passport/EpcCard"; import { getEnergyAssessment, getEnergyAssessmentDocuments, getPropertyMeta, } from "../utils"; - -// EnergyAssessmentsPage.tsx import { DocumentsTable } from "./DocumentsTable"; +import { getEpcColorClass } from "@/app/utils"; + +// Helper function to clean scenario names +function cleanScenarioName(scenarioName: string | undefined) { + if (!scenarioName) { + return scenarioName; + } + + return scenarioName.startsWith("Scenario") + ? scenarioName.replace(/^Scenario\s*/i, "").trim() + : scenarioName; +} + +type InfoCardProps = { + title: string; + value: number | string; + unit: string; +}; + +export const InfoCard: React.FC = ({ title, value, unit }) => { + const isEnergyRating = title === "Energy Rating"; + const bgColorClass = isEnergyRating ? getEpcColorClass(value.toString()) : ""; // Get the EPC color if it's Energy Rating + + return ( + + + + {title} + + + +
+ {value} + {unit} +
+
+
+ ); +}; export default async function EnergyAssessmentsPage({ params, @@ -38,15 +94,93 @@ export default async function EnergyAssessmentsPage({ "Floor Plan", ]; + const scenarioAllowedTypes = ["Scenario Draft EPC", "Scenario Site Notes"]; + + // Separate core documents and scenario-specific documents + const coreDocuments = documents.filter( + (doc) => + doc.scenarioId === null && table1AllowedTypes.includes(doc.documentType) + ); + + const scenarioDocuments = documents.filter( + (doc) => + doc.scenarioId !== null && scenarioAllowedTypes.includes(doc.documentType) + ); + + // Extract unique scenarios from the documents + const scenarios = Array.from( + new Set(scenarioDocuments.map((doc) => doc.scenario?.scenarioName)) + ).filter(Boolean); // Filter out any null or undefined scenario names + return (
-
Core Survey Documents
-
- + {/* EPC Rating Card */} + + + + + + + + + + {/* Estimated Hot Water kWh */} +
+ + {/* Core Survey Documents */} +
+
+ Core Survey Documents +
+
+ +
+
+ + {/* Scenario-Specific Documents */} + {scenarios.map((scenarioName) => { + const scenarioDocs = scenarioDocuments.filter( + (doc) => doc.scenario?.scenarioName === scenarioName + ); + + return ( +
+
+ Scenario: {cleanScenarioName(scenarioName)} +
+ +
+ ); + })}
); } diff --git a/src/app/portfolio/[slug]/building-passport/[propertyId]/utils.ts b/src/app/portfolio/[slug]/building-passport/[propertyId]/utils.ts index 4e0f136..37ded61 100644 --- a/src/app/portfolio/[slug]/building-passport/[propertyId]/utils.ts +++ b/src/app/portfolio/[slug]/building-passport/[propertyId]/utils.ts @@ -22,6 +22,7 @@ import { EnergyAssessment, energyAssessmentDocuments, EnergyAssessmentDocument, + EnergyAssessmentDocumentWithScenario, } from "@/app/db/schema/energy_assessments"; type RecommendationList = { @@ -45,12 +46,15 @@ export async function getEnergyAssessment( export async function getEnergyAssessmentDocuments( energyAssessmentId: bigint -): Promise { +): Promise { const data = await db.query.energyAssessmentDocuments.findMany({ where: eq( energyAssessmentDocuments.energyAssessmentId, BigInt(energyAssessmentId) ), + with: { + scenario: true, + }, }); if (!data) { diff --git a/tailwind.config.js b/tailwind.config.js index 8e2051d..7521e9e 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -101,6 +101,7 @@ module.exports = { brandtan: "#d3b488", hovertan: "#947750", brandgold: "#f1bb06", + hovergold: "#c79d12", brandbrown: "#3d1e05", brandmidblue: "#3943b7", border: "hsl(var(--border))", From f71670305002ef1ce72d9b4d7ddaf0e11f521568 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Mon, 9 Sep 2024 16:47:08 +0100 Subject: [PATCH 8/8] removing epc card --- .../building-passport/[propertyId]/energy-assessment/page.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/portfolio/[slug]/building-passport/[propertyId]/energy-assessment/page.tsx b/src/app/portfolio/[slug]/building-passport/[propertyId]/energy-assessment/page.tsx index 092d0e4..871f9b0 100644 --- a/src/app/portfolio/[slug]/building-passport/[propertyId]/energy-assessment/page.tsx +++ b/src/app/portfolio/[slug]/building-passport/[propertyId]/energy-assessment/page.tsx @@ -4,7 +4,7 @@ import { CardHeader, CardTitle, } from "@/app/shadcn_components/ui/card"; -import EpcCard from "@/app/components/building-passport/EpcCard"; + import { getEnergyAssessment, getEnergyAssessmentDocuments, @@ -30,7 +30,7 @@ type InfoCardProps = { unit: string; }; -export const InfoCard: React.FC = ({ title, value, unit }) => { +const InfoCard: React.FC = ({ title, value, unit }) => { const isEnergyRating = title === "Energy Rating"; const bgColorClass = isEnergyRating ? getEpcColorClass(value.toString()) : ""; // Get the EPC color if it's Energy Rating
{kwh.toFixed(0)} kWh
- {carbon}t CO 2 + {carbon}t CO2