Added reactive impact to plan page

This commit is contained in:
Khalim Conn-Kowlessar 2023-12-01 17:11:31 +00:00
parent 8e0720c3f0
commit ffc14bd630
11 changed files with 1849 additions and 117 deletions

View file

@ -0,0 +1,93 @@
interface EnergyEfficiencyImpactCardProps {
currentEpcRating: string;
expectedEpcRating: string;
currentSapPoints: number;
expectedSapPoints: number;
totalSapPoints: number;
}
export function EnergyEfficiencyImpactCard({
currentEpcRating,
expectedEpcRating,
currentSapPoints,
expectedSapPoints,
totalSapPoints,
}: EnergyEfficiencyImpactCardProps) {
return (
<div>
<table className="text-left bg-brandblue rounded-md text-gray-100 w-full">
<tbody>
<tr>
<td className="font-medium pl-4 py-2">Energy Efficiency Impact</td>
</tr>
<tr>
<td className="font-medium pl-4 py-2">Current EPC Rating:</td>
<td className="font-bold pr-2">
{currentSapPoints + " " + currentEpcRating}
</td>
</tr>
<tr>
<td className="font-medium pl-4 py-2">Expected EPC Rating:</td>
<td className="font-bold pr-2">
{Math.floor(expectedSapPoints) + " " + expectedEpcRating}
</td>
</tr>
<tr>
<td className="font-medium pl-4 py-2">
Total SAP Points Improvement:
</td>
<td className="pr-2">
{Math.round((totalSapPoints + Number.EPSILON) * 100) / 100}
</td>
</tr>
</tbody>
</table>
</div>
);
}
interface SecondaryEnergyEfficiencyImpactCardProps {
TotalCo2Savings: number;
totalEnergyCostSavings: number;
totalHeatDemandSavings: number;
}
export function SecondaryEnergyEfficiencyImpactCard({
TotalCo2Savings,
totalEnergyCostSavings,
totalHeatDemandSavings,
}: SecondaryEnergyEfficiencyImpactCardProps) {
return (
<div>
<table className="text-left bg-brandblue rounded-md text-gray-100 w-full">
<tbody>
<tr>
<td style={{ height: "48px" }}></td>
</tr>
<tr>
<td className="font-medium pl-4 py-2">
CO<sub>2</sub> Reduction
</td>
<td className=" pr-2">{TotalCo2Savings.toFixed(1)}t</td>
</tr>
<tr>
<td className="font-medium pl-4 py-2">Energy Savings</td>
<td className="pr-2">
{totalHeatDemandSavings.toFixed(0) + "kWh"}
</td>
</tr>
<tr>
<td className="font-medium pl-4 py-2">Energy Bill Savings</td>
<td className="pr-2">{"£" + Math.round(totalEnergyCostSavings)}</td>
</tr>
</tbody>
</table>
</div>
);
}

View file

@ -35,6 +35,31 @@ const TitleMap = {
roof_insulation: "Roof Insulation",
};
type RecommendationCardProps = {
componentType: RecommendationType;
recommendationData: Recommendation[];
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>>;
setTotalLabourDays: Dispatch<SetStateAction<number>>;
labourDaysMap: RecommendationMetricMap;
setLabourDaysMap: Dispatch<SetStateAction<RecommendationMetricMap>>;
setCo2SavingsMap: Dispatch<SetStateAction<RecommendationMetricMap>>;
co2SavingsMap: RecommendationMetricMap;
setTotalCo2Savings: Dispatch<SetStateAction<number>>;
setEnergyCostSavingsMap: Dispatch<SetStateAction<RecommendationMetricMap>>;
energyCostSavingsMap: RecommendationMetricMap;
setTotalEnergyCostSavings: Dispatch<SetStateAction<number>>;
setHeatDemandMap: Dispatch<SetStateAction<RecommendationMetricMap>>;
heatDemandMap: RecommendationMetricMap;
setTotalHeatDemandSavings: Dispatch<SetStateAction<number>>;
};
export default function RecommendationCard({
componentType,
recommendationData,
@ -46,18 +71,19 @@ export default function RecommendationCard({
setTotalSapPoints,
currentSapPoints,
setExpectedEpcRating,
}: {
componentType: RecommendationType;
recommendationData: Recommendation[];
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>>;
}) {
setTotalLabourDays,
labourDaysMap,
setLabourDaysMap,
setCo2SavingsMap,
co2SavingsMap,
setTotalCo2Savings,
setEnergyCostSavingsMap,
energyCostSavingsMap,
setTotalEnergyCostSavings,
setHeatDemandMap,
heatDemandMap,
setTotalHeatDemandSavings,
}: RecommendationCardProps) {
const defaultComponent = recommendationData.find(
(rec: Recommendation) => rec.default
) as Recommendation;
@ -138,6 +164,22 @@ export default function RecommendationCard({
setTotalSapPoints={setTotalSapPoints}
currentSapPoints={currentSapPoints}
setExpectedEpcRating={setExpectedEpcRating}
// Labour
setTotalLabourDays={setTotalLabourDays}
labourDaysMap={labourDaysMap}
setLabourDaysMap={setLabourDaysMap}
// Co2
setCo2SavingsMap={setCo2SavingsMap}
co2SavingsMap={co2SavingsMap}
setTotalCo2Savings={setTotalCo2Savings}
// Energy Cost
setEnergyCostSavingsMap={setEnergyCostSavingsMap}
energyCostSavingsMap={energyCostSavingsMap}
setTotalEnergyCostSavings={setTotalEnergyCostSavings}
// Heat Demand
setHeatDemandMap={setHeatDemandMap}
heatDemandMap={heatDemandMap}
setTotalHeatDemandSavings={setTotalHeatDemandSavings}
/>
</div>
);

View file

@ -5,14 +5,17 @@ import {
RecommendationType,
} from "@/app/db/schema/recommendations";
import RecommendationCard from "./RecommendationCard";
import RecommendationCostSummaryCard from "./RecommendationCostSummaryCard";
import WorksPackageCard from "./WorksPackageCard";
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]/plans/utils";
import { RecommendationMetricMap } from "@/types/recommendations";
import RecommendationEpcSummaryCard from "./RecommendationEpcSummaryCard";
import {
EnergyEfficiencyImpactCard,
SecondaryEnergyEfficiencyImpactCard,
} from "./EnergyEfficiencyImpactCard";
interface RecommendationContainerProps {
recommendations: Recommendation[];
@ -32,6 +35,15 @@ const typeToCategoryMap: { [key in RecommendationType]?: RecommendationType } =
exposed_floor_insulation: "floor_insulation",
};
const emptyImpactState = {
estimatedCost: 0,
sapPoints: 0,
labourDays: 0,
co2EquivalentSavings: 0,
energyCostSavings: 0,
heatDemand: 0,
};
export default function RecommendationContainer({
recommendations,
propertyMeta,
@ -48,37 +60,35 @@ export default function RecommendationContainer({
return acc;
}, {} as Record<RecommendationType, (typeof recommendations)[0][]>);
console.log(categorizedRecommendations);
const defaultWallsRecommendations =
categorizedRecommendations.wall_insulation?.find(
(rec: Recommendation) => rec.default
) || { estimatedCost: 0, sapPoints: 0 };
) || emptyImpactState;
const defaultFloorRecommendations =
categorizedRecommendations.floor_insulation?.find(
(rec: Recommendation) => rec.default
) || { estimatedCost: 0, sapPoints: 0 };
) || emptyImpactState;
const defaultRoofRecommendations =
categorizedRecommendations.roof_insulation?.find(
(rec: Recommendation) => rec.default
) || { estimatedCost: 0, sapPoints: 0 };
) || emptyImpactState;
const defaultVentiliationRecommendations =
categorizedRecommendations.mechanical_ventilation?.find(
(rec: Recommendation) => rec.default
) || { estimatedCost: 0, sapPoints: 0 };
) || emptyImpactState;
const defaultFireplaceRecommendations =
categorizedRecommendations.sealing_open_fireplace?.find(
(rec: Recommendation) => rec.default
) || { estimatedCost: 0, sapPoints: 0 };
) || emptyImpactState;
const defaultLightingRecommendations =
categorizedRecommendations.low_energy_lighting?.find(
(rec: Recommendation) => rec.default
) || { estimatedCost: 0, sapPoints: 0 };
) || emptyImpactState;
const [costMap, setCostMap] = useState<RecommendationMetricMap>({
wall_insulation: defaultWallsRecommendations?.estimatedCost || 0,
@ -99,6 +109,49 @@ export default function RecommendationContainer({
low_energy_lighting: defaultLightingRecommendations.sapPoints || 0,
});
const [labourDaysMap, setLabourDaysMap] = useState<RecommendationMetricMap>({
wall_insulation: defaultWallsRecommendations?.labourDays || 0,
floor_insulation: defaultFloorRecommendations.labourDays || 0,
roof_insulation: defaultRoofRecommendations.labourDays || 0,
mechanical_ventilation: defaultVentiliationRecommendations.labourDays || 0,
sealing_open_fireplace: defaultFireplaceRecommendations.labourDays || 0,
low_energy_lighting: defaultLightingRecommendations.labourDays || 0,
});
const [co2SavingsMap, setCo2SavingsMap] = useState<RecommendationMetricMap>({
wall_insulation: defaultWallsRecommendations?.co2EquivalentSavings || 0,
floor_insulation: defaultFloorRecommendations.co2EquivalentSavings || 0,
roof_insulation: defaultRoofRecommendations.co2EquivalentSavings || 0,
mechanical_ventilation:
defaultVentiliationRecommendations.co2EquivalentSavings || 0,
sealing_open_fireplace:
defaultFireplaceRecommendations.co2EquivalentSavings || 0,
low_energy_lighting:
defaultLightingRecommendations.co2EquivalentSavings || 0,
});
const [energyCostSavingsMap, setEnergyCostSavingsMap] =
useState<RecommendationMetricMap>({
wall_insulation: defaultWallsRecommendations?.energyCostSavings || 0,
floor_insulation: defaultFloorRecommendations.energyCostSavings || 0,
roof_insulation: defaultRoofRecommendations.energyCostSavings || 0,
mechanical_ventilation:
defaultVentiliationRecommendations.energyCostSavings || 0,
sealing_open_fireplace:
defaultFireplaceRecommendations.energyCostSavings || 0,
low_energy_lighting:
defaultLightingRecommendations.energyCostSavings || 0,
});
const [heatDemandMap, setHeatDemandMap] = useState<RecommendationMetricMap>({
wall_insulation: defaultWallsRecommendations?.heatDemand || 0,
floor_insulation: defaultFloorRecommendations.heatDemand || 0,
roof_insulation: defaultRoofRecommendations.heatDemand || 0,
mechanical_ventilation: defaultVentiliationRecommendations.heatDemand || 0,
sealing_open_fireplace: defaultFireplaceRecommendations.heatDemand || 0,
low_energy_lighting: defaultLightingRecommendations.heatDemand || 0,
});
const [totalEstimatedCost, setTotalEstimatedCost] = useState(
sumRecommendationMetricMap(costMap)
);
@ -107,10 +160,25 @@ export default function RecommendationContainer({
sumRecommendationMetricMap(sapMap)
);
const [totalLabourDays, setTotalLabourDays] = useState(
sumRecommendationMetricMap(labourDaysMap)
);
const [totalCo2Savings, setTotalCo2Savings] = useState(
sumRecommendationMetricMap(co2SavingsMap)
);
const [totalEnergyCostSavings, setTotalEnergyCostSavings] = useState(
sumRecommendationMetricMap(energyCostSavingsMap)
);
const [totalHeatDemandSavings, setTotalHeatDemandSavings] = useState(
sumRecommendationMetricMap(heatDemandMap)
);
const currentEpcRating = propertyMeta.currentEpcRating;
const currentSapPoints = propertyMeta.currentSapPoints;
//TODO: Use Math.min while we have dummy SAP points
const expectedSapPoints = Math.min(currentSapPoints + totalSapPoints, 100);
const [expectedEpcRating, setExpectedEpcRating] = useState(
sapToEpc(expectedSapPoints)
@ -119,14 +187,23 @@ export default function RecommendationContainer({
return (
<>
<div className="mb-4 flex-col grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 items-stretch">
<RecommendationCostSummaryCard
<WorksPackageCard
totalEstimatedCost={totalEstimatedCost}
totalLabourDays={totalLabourDays}
/>
<EnergyEfficiencyImpactCard
currentEpcRating={currentEpcRating}
expectedEpcRating={expectedEpcRating}
currentSapPoints={currentSapPoints}
expectedSapPoints={expectedSapPoints}
totalSapPoints={totalSapPoints}
/>
<RecommendationEpcSummaryCard
currentEpcRating={currentEpcRating}
expectedEpcRating={expectedEpcRating}
<SecondaryEnergyEfficiencyImpactCard
TotalCo2Savings={totalCo2Savings}
totalEnergyCostSavings={totalEnergyCostSavings}
totalHeatDemandSavings={totalHeatDemandSavings}
/>
</div>
@ -141,14 +218,32 @@ export default function RecommendationContainer({
// entires means we loose the typing on the key
componentType={componentType as RecommendationType}
recommendationData={recommendationData}
// cost
setCostMap={setCostMap}
costMap={costMap}
setTotalEstimatedCost={setTotalEstimatedCost}
// Sap
setSapMap={setSapMap}
sapMap={sapMap}
setTotalSapPoints={setTotalSapPoints}
currentSapPoints={currentSapPoints}
setExpectedEpcRating={setExpectedEpcRating}
// Labour
setTotalLabourDays={setTotalLabourDays}
labourDaysMap={labourDaysMap}
setLabourDaysMap={setLabourDaysMap}
// Co2
setCo2SavingsMap={setCo2SavingsMap}
co2SavingsMap={co2SavingsMap}
setTotalCo2Savings={setTotalCo2Savings}
// Energy Cost
setEnergyCostSavingsMap={setEnergyCostSavingsMap}
energyCostSavingsMap={energyCostSavingsMap}
setTotalEnergyCostSavings={setTotalEnergyCostSavings}
// Heat Demand
setHeatDemandMap={setHeatDemandMap}
heatDemandMap={heatDemandMap}
setTotalHeatDemandSavings={setTotalHeatDemandSavings}
/>
);
}

View file

@ -1,30 +0,0 @@
"use client";
import { formatNumber } from "@/app/utils";
export default function RecommendationCostSummaryCard({
totalEstimatedCost,
totalSapPoints,
}: {
totalEstimatedCost: number;
totalSapPoints: number;
}) {
return (
<table className="text-left bg-brandblue rounded-md text-gray-100">
<tbody>
<tr>
<td className="font-medium pl-4 py-2">Total Cost:</td>
<td className="pr-2">{"£" + formatNumber(totalEstimatedCost)}</td>
</tr>
<tr>
<td className="font-medium pl-4 py-2">
Total SAP Points Improvement:
</td>
<td className="pr-2">
{Math.round((totalSapPoints + Number.EPSILON) * 100) / 100}
</td>
</tr>
</tbody>
</table>
);
}

View file

@ -1,25 +0,0 @@
interface RecommendationEpcSummaryCardProps {
currentEpcRating: string;
expectedEpcRating: string;
}
export default function RecommendationEpcSummaryCard({
currentEpcRating,
expectedEpcRating,
}: RecommendationEpcSummaryCardProps) {
return (
<table className="text-left bg-brandblue 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>
);
}

View file

@ -22,6 +22,18 @@ interface RecommendationModalProps {
setTotalSapPoints: Dispatch<SetStateAction<number>>;
currentSapPoints: number;
setExpectedEpcRating: Dispatch<SetStateAction<string>>;
setTotalLabourDays: Dispatch<SetStateAction<number>>;
labourDaysMap: RecommendationMetricMap;
setLabourDaysMap: Dispatch<SetStateAction<RecommendationMetricMap>>;
setCo2SavingsMap: Dispatch<SetStateAction<RecommendationMetricMap>>;
co2SavingsMap: RecommendationMetricMap;
setTotalCo2Savings: Dispatch<SetStateAction<number>>;
setEnergyCostSavingsMap: Dispatch<SetStateAction<RecommendationMetricMap>>;
energyCostSavingsMap: RecommendationMetricMap;
setTotalEnergyCostSavings: Dispatch<SetStateAction<number>>;
setHeatDemandMap: Dispatch<SetStateAction<RecommendationMetricMap>>;
heatDemandMap: RecommendationMetricMap;
setTotalHeatDemandSavings: Dispatch<SetStateAction<number>>;
}
export default function RecommendationModal({
@ -38,6 +50,18 @@ export default function RecommendationModal({
setTotalSapPoints,
currentSapPoints,
setExpectedEpcRating,
setTotalLabourDays,
labourDaysMap,
setLabourDaysMap,
setCo2SavingsMap,
co2SavingsMap,
setTotalCo2Savings,
setEnergyCostSavingsMap,
energyCostSavingsMap,
setTotalEnergyCostSavings,
setHeatDemandMap,
heatDemandMap,
setTotalHeatDemandSavings,
}: RecommendationModalProps) {
const [saveButtonDisabled, setSaveButtonDisabled] = useState(true);
@ -77,8 +101,6 @@ export default function RecommendationModal({
// update the cost sum
setTotalEstimatedCost(sumRecommendationMetricMap(newCostMap));
console.log("B4", sapMap);
// Update the sap map
const newSapMap = {
...sapMap,
@ -86,11 +108,8 @@ export default function RecommendationModal({
};
setSapMap(newSapMap);
console.log("AFTER", newSapMap);
// update the sap sum
const newSapImprovement = sumRecommendationMetricMap(newSapMap);
console.log("newSapImprovement", newSapImprovement);
setTotalSapPoints(newSapImprovement);
// TODO: While we have placeholder SAP points, constrain to 100
@ -98,6 +117,52 @@ export default function RecommendationModal({
// update the expected EPC rating
setExpectedEpcRating(sapToEpc(newSapPoints));
// Update the labour days map
const newLabourDaysMap = {
...labourDaysMap,
[title]: recommendationData[newIndex]?.labourDays || 0,
};
setLabourDaysMap(newLabourDaysMap);
// update the labour days sum
setTotalLabourDays(sumRecommendationMetricMap(newLabourDaysMap));
// Update the co2 savings map
const newCo2SavingsMap = {
...co2SavingsMap,
[title]: recommendationData[newIndex]?.co2EquivalentSavings || 0,
};
setCo2SavingsMap(newCo2SavingsMap);
// update the co2 savings sum
setTotalCo2Savings(sumRecommendationMetricMap(newCo2SavingsMap));
// Update the energy cost savings map
const newEnergyCostSavingsMap = {
...energyCostSavingsMap,
[title]: recommendationData[newIndex]?.energyCostSavings || 0,
};
setEnergyCostSavingsMap(newEnergyCostSavingsMap);
// update the energy cost savings sum
setTotalEnergyCostSavings(
sumRecommendationMetricMap(newEnergyCostSavingsMap)
);
// Update the heat demand savings map
const newHeatDemandMap = {
...heatDemandMap,
[title]: recommendationData[newIndex]?.heatDemand || 0,
};
setHeatDemandMap(newHeatDemandMap);
// update the heat demand savings sum
setTotalHeatDemandSavings(sumRecommendationMetricMap(newHeatDemandMap));
}
return (

View file

@ -0,0 +1,35 @@
"use client";
import { convertDaysToWorkingWeeks, formatNumber } from "@/app/utils";
export default function WorksPackageCard({
totalEstimatedCost,
totalLabourDays,
}: {
totalEstimatedCost: number;
totalLabourDays: number;
}) {
return (
<table className="text-left bg-brandblue rounded-md text-gray-100">
<tbody>
<tr>
<td className="font-medium pl-4 py-2">Works Package</td>
</tr>
<tr>
<td className="font-medium pl-4 py-2">Total Cost:</td>
<td className="pr-2">{"£" + formatNumber(totalEstimatedCost)}</td>
</tr>
<tr>
<td className="font-medium pl-4 py-2">Trades required:</td>
<td className="pr-2">{"1-2"}</td>
</tr>
<tr>
<td className="font-medium pl-4 py-2">Estimated Duration:</td>
<td className="pr-2">{convertDaysToWorkingWeeks(totalLabourDays)}</td>
</tr>
</tbody>
</table>
);
}

View file

@ -0,0 +1,11 @@
CREATE TABLE IF NOT EXISTS "property_details_spatial" (
"id" bigserial PRIMARY KEY NOT NULL,
"uprn" bigint,
"x_coordinate" real,
"y_coordinate" real,
"latitude" real,
"longitude" real,
"conservation_status" boolean,
"is_listed_building" boolean,
"is_heritage_building" boolean
);

File diff suppressed because it is too large Load diff

View file

@ -3,7 +3,7 @@ import { getPortfolio, getProperties } from "../utils";
import DataTable from "@/app/portfolio/[slug]/components/propertyTable";
import { columns } from "@/app/portfolio/[slug]/components/propertyTableColumns";
import { PropertyWithRelations } from "@/app/db/schema/property";
import { formatNumber } from "@/app/utils";
import { formatNumber, convertDaysToWorkingWeeks } from "@/app/utils";
// We enfore caching of data for 60 seconds
export const revalidate = 60;
@ -87,35 +87,6 @@ function SummaryBox({
}
}
function convertDaysToWorkingWeeks(days: number | null) {
if (days === null) {
return "-";
}
const workingDaysPerWeek = 5;
// Convert days to working weeks
const workingWeeks = days / workingDaysPerWeek;
// Determine the range
let lowerBound = Math.floor(workingWeeks);
let upperBound = Math.ceil(workingWeeks);
// Adjust if the fraction is very small, you might not count it as a full extra week
if (workingWeeks - lowerBound < 0.2) {
upperBound = lowerBound;
}
if (lowerBound === 1 && upperBound === 1) {
return "1-2 weeks";
}
// Format the output
return lowerBound === upperBound
? `${lowerBound} weeks`
: `${lowerBound}-${upperBound} weeks`;
}
const budgetFormatted = formatBudget(budget);
const totalCostFormatted = formatMoney(totalCost);
const totalValueIncreaseFormatted = formatMoney(propertyValuationIncrease);

View file

@ -1,5 +1,38 @@
import { Rating } from "./db/schema/property";
export function convertDaysToWorkingWeeks(days: number | null) {
if (days === null) {
return "-";
}
const workingDaysPerWeek = 5;
// Convert days to working weeks
const workingWeeks = days / workingDaysPerWeek;
// Determine the range
let lowerBound = Math.floor(workingWeeks);
let upperBound = Math.ceil(workingWeeks);
// Adjust if the fraction is very small, you might not count it as a full extra week
if (workingWeeks - lowerBound < 0.2) {
upperBound = lowerBound;
}
if (lowerBound === 0 && upperBound === 1) {
return "1 week";
}
if (lowerBound === 1 && upperBound === 1) {
return "1-2 weeks";
}
// Format the output
return lowerBound === upperBound
? `${lowerBound} weeks`
: `${lowerBound}-${upperBound} weeks`;
}
export const getEpcColorClass = (letter: string) => {
switch (letter.toUpperCase()) {
case "A":