adding multiple missed files for splitting out recommendationModal and recommendationContainer

This commit is contained in:
Khalim Conn-Kowlessar 2023-07-28 17:30:39 +01:00
parent 9e8f548eb4
commit a653e157e0
5 changed files with 342 additions and 0 deletions

View 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>
</>
);
}

View 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>
</>
);
}

View file

@ -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;

View file

@ -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);
}

View file

@ -0,0 +1,5 @@
export interface RecommendationMetricMap {
Walls: number;
Floor: number;
Ventilation: number;
}