diff --git a/src/app/components/building-passport/RecommendationContainer.tsx b/src/app/components/building-passport/RecommendationContainer.tsx new file mode 100644 index 0000000..5460541 --- /dev/null +++ b/src/app/components/building-passport/RecommendationContainer.tsx @@ -0,0 +1,112 @@ +"use client"; + +import { + ComponentRecommendation, + Recommendation, +} from "@/app/db/schema/recommendations"; +import RecommendationCard from "./RecommendationCard"; +import RecommendationCostSummaryCard from "./RecommendationCostSummaryCard"; +import { Separator } from "@/app/shadcn_components/ui/separator"; +import { PropertyMeta } from "@/app/db/schema/property"; +import { sapToEpc } from "@/app/utils"; +import { useState } from "react"; +import { sumRecommendationMetricMap } from "@/app/portfolio/[slug]/building-passport/[propertyId]/recommendations/utils"; +import { RecommendationMetricMap } from "@/types/recommendations"; + +interface RecommendationContainerProps { + recommendations: Recommendation; + propertyMeta: PropertyMeta; +} + +export default function RecommendationContainer({ + recommendations, + propertyMeta, +}: RecommendationContainerProps) { + const defaultWallsRecommendations = recommendations.Walls?.find( + (rec: ComponentRecommendation) => rec.default + ) || { estimatedCost: 0, sapPoints: 0 }; + + const defaultFloorRecommendations = recommendations.Floor?.find( + (rec: ComponentRecommendation) => rec.default + ) || { estimatedCost: 0, sapPoints: 0 }; + + const defaultVentiliationRecommendations = recommendations.Ventilation?.find( + (rec: ComponentRecommendation) => rec.default + ) || { estimatedCost: 0, sapPoints: 0 }; + + const [costMap, setCostMap] = useState({ + Walls: defaultWallsRecommendations.estimatedCost, + Floor: defaultFloorRecommendations.estimatedCost, + Ventilation: defaultVentiliationRecommendations.estimatedCost, + }); + + const [sapMap, setSapMap] = useState({ + Walls: defaultWallsRecommendations.sapPoints, + Floor: defaultFloorRecommendations.sapPoints, + Ventilation: defaultVentiliationRecommendations.sapPoints, + }); + + const [totalEstimatedCost, setTotalEstimatedCost] = useState( + sumRecommendationMetricMap(costMap) + ); + + const [totalSapPoints, setTotalSapPoints] = useState( + sumRecommendationMetricMap(sapMap) + ); + + const currentEpcRating = propertyMeta.currentEpcRating; + const currentSapPoints = propertyMeta.currentSapPoints; + + const expectedSapPoints = currentSapPoints + totalSapPoints; + const [expectedEpcRating, setExpectedEpcRating] = useState( + sapToEpc(expectedSapPoints) + ); + + return ( + <> +
+ + + + + + + + + + + + + + +
Current EPC Rating:{currentEpcRating}
Expected EPC Rating:{expectedEpcRating}
+
+ + +
+ {Object.entries(recommendations).map( + ([componentType, recommendationData], idx) => { + return ( + + ); + } + )} +
+ + ); +} diff --git a/src/app/components/building-passport/RecommendationModal.tsx b/src/app/components/building-passport/RecommendationModal.tsx new file mode 100644 index 0000000..8105cbf --- /dev/null +++ b/src/app/components/building-passport/RecommendationModal.tsx @@ -0,0 +1,173 @@ +"use client"; +import { ComponentRecommendation } from "@/app/db/schema/recommendations"; +import { Dispatch, Fragment, SetStateAction, useState } from "react"; +import { Dialog, Transition } from "@headlessui/react"; +import RecommendationTable from "@/app/components/building-passport/RecommendationTable"; +import { RecommendationMetricMap } from "@/types/recommendations"; +import { sumRecommendationMetricMap } from "@/app/portfolio/[slug]/building-passport/[propertyId]/recommendations/utils"; +import uvalueColumns from "./RecommendationTableColumns"; +import { sapToEpc } from "@/app/utils"; + +interface RecommendationModalProps { + title: string; + isOpen: boolean; + setIsOpen: (isOpen: boolean) => void; + recommendationData: ComponentRecommendation[]; + setCardComponent: Dispatch>; + setCostMap: Dispatch>; + costMap: RecommendationMetricMap; + setTotalEstimatedCost: Dispatch>; + sapMap: RecommendationMetricMap; + setSapMap: Dispatch>; + setTotalSapPoints: Dispatch>; + currentSapPoints: number; + setExpectedEpcRating: Dispatch>; +} + +export default function RecommendationModal({ + title, + isOpen = false, + setIsOpen, + recommendationData, + setCardComponent, + setCostMap, + costMap, + setTotalEstimatedCost, + sapMap, + setSapMap, + setTotalSapPoints, + currentSapPoints, + setExpectedEpcRating, +}: RecommendationModalProps) { + const [saveButtonDisabled, setSaveButtonDisabled] = useState(true); + + // Find the row where default is true + const [defaultRowIndex, setDefaultRowIndex] = useState( + recommendationData.findIndex((d) => d.default) + ); + + // Initialise the state with the default row index + const [rowSelection, setRowSelection] = useState( + defaultRowIndex !== -1 ? { [defaultRowIndex]: true } : {} + ); + + function closeModal() { + setIsOpen(false); + // If the user closes the modal, re-set the state of the row selection to the default, since nothing has changed + setRowSelection({ [defaultRowIndex]: true }); + } + + function saveChanges() { + // disable the button to prevent multiple clicks + // TODO: Add a loading state to show we're saving + setSaveButtonDisabled(true); + setIsOpen(false); + // Update the card component data + const newIndex = parseInt(Object.keys(rowSelection)[0]); + + setCardComponent(recommendationData[newIndex]); + // Set the default index + setDefaultRowIndex(newIndex); + // Update the cost map + const newCostMap = { + ...costMap, + [title]: recommendationData[newIndex]?.estimatedCost || 0, + }; + setCostMap(newCostMap); + // update the cost sum + setTotalEstimatedCost(sumRecommendationMetricMap(newCostMap)); + + console.log("title", title); + // Update the sap map + const newSapMap = { + ...sapMap, + [title]: recommendationData[newIndex]?.sapPoints || 0, + }; + setSapMap(newSapMap); + + // update the sap sum + const newSapImporvement = sumRecommendationMetricMap(newSapMap); + setTotalSapPoints(newSapImporvement); + + // update the expected EPC rating + setExpectedEpcRating(sapToEpc(currentSapPoints + newSapImporvement)); + } + + return ( + <> + + + +
+ + +
+
+ + + + {title} + + +
+ + + + +
+
+
+
+
+
+
+ + ); +} diff --git a/src/app/components/building-passport/RecommendationTableColumns.tsx b/src/app/components/building-passport/RecommendationTableColumns.tsx new file mode 100644 index 0000000..2eab268 --- /dev/null +++ b/src/app/components/building-passport/RecommendationTableColumns.tsx @@ -0,0 +1,42 @@ +import { ColumnDef } from "@tanstack/react-table"; +import { Checkbox } from "@/app/shadcn_components/ui/checkbox"; +import { formatNumber } from "@/app/utils"; +import { ComponentRecommendation } from "@/app/db/schema/recommendations"; + +const uvalueColumns: ColumnDef[] = [ + { + accessorKey: "description", + header: "Description", + }, + { + accessorKey: "estimatedCost", + header: "Estimated Cost", + cell: ({ row }) => { + return
£{formatNumber(row.getValue("estimatedCost"))}
; + }, + }, + { + accessorKey: "newUValue", + header: "New U-Value", + }, + { + accessorKey: "default", + id: "default", + header: "Selected?", + cell: ({ row }) => ( + { + if (value === true && !row.getIsSelected()) { + row.toggleSelected(true); + } + }} + aria-label="Select row" + /> + ), + enableSorting: false, + enableHiding: false, + }, +]; + +export default uvalueColumns; diff --git a/src/app/portfolio/[slug]/building-passport/[propertyId]/recommendations/utils.tsx b/src/app/portfolio/[slug]/building-passport/[propertyId]/recommendations/utils.tsx new file mode 100644 index 0000000..d3f77e4 --- /dev/null +++ b/src/app/portfolio/[slug]/building-passport/[propertyId]/recommendations/utils.tsx @@ -0,0 +1,10 @@ +import { RecommendationMetricMap } from "@/types/recommendations"; + +export function sumRecommendationMetricMap( + obj: RecommendationMetricMap +): number { + // In the recommendations section of the building passport we have the cost map which + // contains the costs of the recommendations. We need to sum these costs to display + // the total cost of the recommendations + return Object.values(obj).reduce((sum, current) => sum + current, 0); +} diff --git a/src/types/recommendations.ts b/src/types/recommendations.ts new file mode 100644 index 0000000..09e7047 --- /dev/null +++ b/src/types/recommendations.ts @@ -0,0 +1,5 @@ +export interface RecommendationMetricMap { + Walls: number; + Floor: number; + Ventilation: number; +}