mirror of
https://github.com/Hestia-Homes/assessment-model.git
synced 2026-06-08 11:37:25 +00:00
adding multiple missed files for splitting out recommendationModal and recommendationContainer
This commit is contained in:
parent
9e8f548eb4
commit
a653e157e0
5 changed files with 342 additions and 0 deletions
112
src/app/components/building-passport/RecommendationContainer.tsx
Normal file
112
src/app/components/building-passport/RecommendationContainer.tsx
Normal file
|
|
@ -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<RecommendationMetricMap>({
|
||||
Walls: defaultWallsRecommendations.estimatedCost,
|
||||
Floor: defaultFloorRecommendations.estimatedCost,
|
||||
Ventilation: defaultVentiliationRecommendations.estimatedCost,
|
||||
});
|
||||
|
||||
const [sapMap, setSapMap] = useState<RecommendationMetricMap>({
|
||||
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 (
|
||||
<>
|
||||
<div className="mb-4 flex flex-col grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 items-stretch">
|
||||
<RecommendationCostSummaryCard
|
||||
totalEstimatedCost={totalEstimatedCost}
|
||||
totalSapPoints={totalSapPoints}
|
||||
/>
|
||||
|
||||
<table className="text-left bg-green-700 rounded-md text-gray-100">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="font-medium pl-4 py-2">Current EPC Rating:</td>
|
||||
<td className="font-bold pr-2">{currentEpcRating}</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td className="font-medium pl-4 py-2">Expected EPC Rating:</td>
|
||||
<td className="font-bold pr-2">{expectedEpcRating}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<Separator className="mb-4" />
|
||||
<div className="flex flex-col grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 items-stretch">
|
||||
{Object.entries(recommendations).map(
|
||||
([componentType, recommendationData], idx) => {
|
||||
return (
|
||||
<RecommendationCard
|
||||
key={idx}
|
||||
componentType={componentType}
|
||||
recommendationData={recommendationData}
|
||||
setCostMap={setCostMap}
|
||||
costMap={costMap}
|
||||
setTotalEstimatedCost={setTotalEstimatedCost}
|
||||
setSapMap={setSapMap}
|
||||
sapMap={sapMap}
|
||||
setTotalSapPoints={setTotalSapPoints}
|
||||
currentSapPoints={currentSapPoints}
|
||||
setExpectedEpcRating={setExpectedEpcRating}
|
||||
/>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
173
src/app/components/building-passport/RecommendationModal.tsx
Normal file
173
src/app/components/building-passport/RecommendationModal.tsx
Normal file
|
|
@ -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<SetStateAction<ComponentRecommendation>>;
|
||||
setCostMap: Dispatch<SetStateAction<RecommendationMetricMap>>;
|
||||
costMap: RecommendationMetricMap;
|
||||
setTotalEstimatedCost: Dispatch<SetStateAction<number>>;
|
||||
sapMap: RecommendationMetricMap;
|
||||
setSapMap: Dispatch<SetStateAction<RecommendationMetricMap>>;
|
||||
setTotalSapPoints: Dispatch<SetStateAction<number>>;
|
||||
currentSapPoints: number;
|
||||
setExpectedEpcRating: Dispatch<SetStateAction<string>>;
|
||||
}
|
||||
|
||||
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 (
|
||||
<>
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-10" onClose={closeModal}>
|
||||
<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"
|
||||
>
|
||||
<div className="fixed inset-0 bg-black bg-opacity-25" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 overflow-y-auto">
|
||||
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
||||
<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"
|
||||
>
|
||||
<Dialog.Panel className="w-full max-w-screen-md transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all">
|
||||
<Dialog.Title
|
||||
as="h3"
|
||||
className="text-lg font-medium leading-6 text-brandblue mb-3"
|
||||
>
|
||||
{title}
|
||||
</Dialog.Title>
|
||||
<RecommendationTable
|
||||
data={recommendationData}
|
||||
columns={uvalueColumns}
|
||||
defaultRowIndex={defaultRowIndex}
|
||||
rowSelection={rowSelection}
|
||||
setRowSelection={setRowSelection}
|
||||
setSaveButtonDisabled={setSaveButtonDisabled}
|
||||
/>
|
||||
<div className="mt-4 flex justify-end gap-2">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex justify-center rounded-md border border-transparent hover:text-red-600 bg-gray-200 px-4 py-2 text-sm font-medium text-red-600 hover:bg-gray-300 focus:outline-none focus-visible:ring-2 focus-visible:none focus-visible:ring-offset-2"
|
||||
onClick={() => {
|
||||
setRowSelection({});
|
||||
}}
|
||||
>
|
||||
Remove Recommendation
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex justify-center rounded-md border border-transparent bg-blue-100 px-4 py-2 text-sm font-medium text-blue-900 hover:bg-blue-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
||||
onClick={closeModal}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex justify-center rounded-md border border-transparent bg-brandblue px-4 py-2 text-sm font-medium text-gray-100 hover:bg-hoverblue focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-brandblue"
|
||||
onClick={saveChanges}
|
||||
disabled={saveButtonDisabled}
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -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<ComponentRecommendation>[] = [
|
||||
{
|
||||
accessorKey: "description",
|
||||
header: "Description",
|
||||
},
|
||||
{
|
||||
accessorKey: "estimatedCost",
|
||||
header: "Estimated Cost",
|
||||
cell: ({ row }) => {
|
||||
return <div>£{formatNumber(row.getValue("estimatedCost"))}</div>;
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "newUValue",
|
||||
header: "New U-Value",
|
||||
},
|
||||
{
|
||||
accessorKey: "default",
|
||||
id: "default",
|
||||
header: "Selected?",
|
||||
cell: ({ row }) => (
|
||||
<Checkbox
|
||||
checked={row.getIsSelected()}
|
||||
onCheckedChange={(value) => {
|
||||
if (value === true && !row.getIsSelected()) {
|
||||
row.toggleSelected(true);
|
||||
}
|
||||
}}
|
||||
aria-label="Select row"
|
||||
/>
|
||||
),
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
},
|
||||
];
|
||||
|
||||
export default uvalueColumns;
|
||||
|
|
@ -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);
|
||||
}
|
||||
5
src/types/recommendations.ts
Normal file
5
src/types/recommendations.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
export interface RecommendationMetricMap {
|
||||
Walls: number;
|
||||
Floor: number;
|
||||
Ventilation: number;
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue