mirror of
https://github.com/Hestia-Homes/assessment-model.git
synced 2026-06-08 11:37:25 +00:00
handling already installed measures - updated ui and adding handling for installed measures
This commit is contained in:
parent
8a08e98340
commit
bb141b759b
12 changed files with 5410 additions and 60 deletions
|
|
@ -108,9 +108,9 @@ export default function RecommendationCard({
|
|||
) as Recommendation;
|
||||
|
||||
// A recommendation type could have no default recommendation, so we need to check if it exists
|
||||
const alreadyInstalled = defaultComponent
|
||||
? defaultComponent.alreadyInstalled
|
||||
: false;
|
||||
const alreadyInstalled = recommendationData.some(
|
||||
(rec) => rec.alreadyInstalled
|
||||
);
|
||||
|
||||
const [cardComponent, setCardComponent] =
|
||||
useState<Recommendation>(defaultComponent);
|
||||
|
|
@ -131,8 +131,8 @@ export default function RecommendationCard({
|
|||
const cardClassName = alreadyInstalled
|
||||
? alreadyInstalledStyling
|
||||
: cardComponent
|
||||
? selectionStyling
|
||||
: noSelectionStyling;
|
||||
? selectionStyling
|
||||
: noSelectionStyling;
|
||||
|
||||
const optionTextClassName = alreadyInstalled
|
||||
? "text-brandgold"
|
||||
|
|
@ -141,8 +141,8 @@ export default function RecommendationCard({
|
|||
const optionsText = alreadyInstalled
|
||||
? "Already installed"
|
||||
: cardComponent
|
||||
? "Click for more options"
|
||||
: "Click to select";
|
||||
? "Click for more options"
|
||||
: "Click to select";
|
||||
|
||||
const openModal = () => {
|
||||
// If the card is already installed, we don't want to open the modal
|
||||
|
|
|
|||
|
|
@ -19,12 +19,14 @@ import {
|
|||
SecondaryEnergyEfficiencyImpactCard,
|
||||
} from "./EnergyEfficiencyImpactCard";
|
||||
import { FundingPackageWithMeasures } from "@/app/db/schema/funding";
|
||||
import { InstalledMeasureSummary } from "@/app/portfolio/[slug]/building-passport/[propertyId]/utils";
|
||||
|
||||
interface RecommendationContainerProps {
|
||||
recommendations: Recommendation[];
|
||||
propertyMeta: PropertyMeta;
|
||||
planMeta: Plan;
|
||||
funding: FundingPackageWithMeasures[];
|
||||
installedMeasures: InstalledMeasureSummary[];
|
||||
}
|
||||
|
||||
const typeToCategoryMap: { [key in RecommendationType]?: RecommendationType } =
|
||||
|
|
@ -57,7 +59,13 @@ export default function RecommendationContainer({
|
|||
propertyMeta,
|
||||
planMeta,
|
||||
funding,
|
||||
installedMeasures,
|
||||
}: RecommendationContainerProps) {
|
||||
// Get the unique types of installed measures for easy lookup
|
||||
const installedMeasureTypeSet = new Set(
|
||||
installedMeasures.map((m) => m.measureType)
|
||||
);
|
||||
|
||||
const categorizedRecommendations = recommendations.reduce(
|
||||
(acc, curr) => {
|
||||
const typeKey = curr.type as RecommendationType;
|
||||
|
|
@ -66,11 +74,28 @@ export default function RecommendationContainer({
|
|||
if (!acc[category]) {
|
||||
acc[category] = [];
|
||||
}
|
||||
acc[category].push(curr);
|
||||
|
||||
const alreadyInstalled =
|
||||
curr.measureType != null &&
|
||||
installedMeasureTypeSet.has(curr.measureType);
|
||||
|
||||
acc[category].push({
|
||||
...curr,
|
||||
alreadyInstalled: alreadyInstalled,
|
||||
sapPoints: alreadyInstalled ? 0 : curr.sapPoints,
|
||||
estimatedCost: alreadyInstalled ? 0 : curr.estimatedCost,
|
||||
co2EquivalentSavings: alreadyInstalled ? 0 : curr.co2EquivalentSavings,
|
||||
energyCostSavings: alreadyInstalled ? 0 : curr.energyCostSavings,
|
||||
kwhSavings: alreadyInstalled ? 0 : curr.kwhSavings,
|
||||
labourDays: alreadyInstalled ? 0 : curr.labourDays,
|
||||
});
|
||||
|
||||
return acc;
|
||||
},
|
||||
{} as Record<RecommendationType, (typeof recommendations)[0][]>
|
||||
{} as Record<
|
||||
RecommendationType,
|
||||
(Recommendation & { alreadyInstalled: boolean })[]
|
||||
>
|
||||
);
|
||||
|
||||
const defaultWallsRecommendations =
|
||||
|
|
|
|||
|
|
@ -70,11 +70,6 @@ export function Toolbar({
|
|||
const [openModal, setOpenModal] = useState(false);
|
||||
const [showToast, setShowToast] = useState(false);
|
||||
|
||||
console.log(propertyId, "PropertyID")
|
||||
console.log(portfolioId, "porfolio id")
|
||||
console.log(propertyMeta, "property meta")
|
||||
console.log(decentHomes, "decent homes")
|
||||
|
||||
function handleClickSettings() {
|
||||
console.log("Settings were clicked, implement me");
|
||||
}
|
||||
|
|
@ -129,8 +124,6 @@ export function Toolbar({
|
|||
</NavigationMenuLink>
|
||||
);
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center justify-between w-full">
|
||||
|
|
|
|||
9
src/app/db/migrations/0148_first_gamma_corps.sql
Normal file
9
src/app/db/migrations/0148_first_gamma_corps.sql
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
ALTER TABLE "property_details_epc" ADD COLUMN "original_co2_emissions" real;--> statement-breakpoint
|
||||
ALTER TABLE "property_details_epc" ADD COLUMN "original_primary_energy_consumption" real;--> statement-breakpoint
|
||||
ALTER TABLE "property_details_epc" ADD COLUMN "original_current_energy_demand" real;--> statement-breakpoint
|
||||
ALTER TABLE "property_details_epc" ADD COLUMN "original_current_energy_demand_heating_hotwater" real;--> statement-breakpoint
|
||||
ALTER TABLE "property_details_epc" ADD COLUMN "installed_measures_co2_adjustment" real;--> statement-breakpoint
|
||||
ALTER TABLE "property_details_epc" ADD COLUMN "installed_measures_energy_demand_adjustment" real;--> statement-breakpoint
|
||||
ALTER TABLE "property_details_epc" ADD COLUMN "installed_measures_total_energy_bill_adjustment" real;--> statement-breakpoint
|
||||
ALTER TABLE "property_details_epc" ADD COLUMN "installed_measures_heat_demand_adjustment" real;--> statement-breakpoint
|
||||
ALTER TABLE "property_details_epc" ADD COLUMN "is_epc_adjusted_for_installed_measures" boolean DEFAULT false;
|
||||
5243
src/app/db/migrations/meta/0148_snapshot.json
Normal file
5243
src/app/db/migrations/meta/0148_snapshot.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1037,6 +1037,13 @@
|
|||
"when": 1767814056667,
|
||||
"tag": "0147_confused_killer_shrike",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 148,
|
||||
"version": "7",
|
||||
"when": 1767823836420,
|
||||
"tag": "0148_first_gamma_corps",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -227,6 +227,36 @@ export const propertyDetailsEpc = pgTable(
|
|||
appliancesEnergyCostCurrent: real("appliances_cost_current"),
|
||||
gasStandingCharge: real("gas_standing_charge"),
|
||||
electricityStandingCharge: real("electricity_standing_charge"),
|
||||
|
||||
// When we have already installed measures, we will adjust the carbon, bills, kwh, heat demandto reflect this. We keep a record of
|
||||
// 1) The adjustments
|
||||
// 2) original values
|
||||
// 3) a flag to indicate whether the values have been adjusted, for easily filtering
|
||||
|
||||
// original values - we don't need bills because we don't actually adjust any of the originals we just subtract adjustments from current values
|
||||
originalCo2Emissions: real("original_co2_emissions"),
|
||||
originalPrimaryEnergyConsumption: real(
|
||||
"original_primary_energy_consumption"
|
||||
),
|
||||
originalCurrentEnergyDemand: real("original_current_energy_demand"),
|
||||
originalCurrentEnergyDemandHeatingHotwater: real(
|
||||
"original_current_energy_demand_heating_hotwater"
|
||||
),
|
||||
|
||||
// adjustment quantities
|
||||
installedMeasuresCo2Adjustment: real("installed_measures_co2_adjustment"),
|
||||
installedMeasuresEnergyDemandAdjustment: real(
|
||||
"installed_measures_energy_demand_adjustment"
|
||||
),
|
||||
installedMeasuresTotalEnergyBillAdjustment: real(
|
||||
"installed_measures_total_energy_bill_adjustment"
|
||||
),
|
||||
installedMeasuresHeatDemandAdjustment: real(
|
||||
"installed_measures_heat_demand_adjustment"
|
||||
),
|
||||
isEpcAdjustedForInstalledMeasures: boolean(
|
||||
"is_epc_adjusted_for_installed_measures"
|
||||
).default(false),
|
||||
},
|
||||
(table) => [
|
||||
uniqueIndex("uq_property_details_epc_property_portfolio").on(
|
||||
|
|
|
|||
|
|
@ -175,7 +175,6 @@ export function ReportingClientArea({
|
|||
scenarioData.n_units_upgraded,
|
||||
}
|
||||
: null;
|
||||
|
||||
// Baseline stays baseline
|
||||
const activeMetrics = baseline;
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import {
|
|||
getRecommendations,
|
||||
getPlanMeta,
|
||||
getPlanFunding,
|
||||
getInstalledMeasuresByUprn,
|
||||
} from "../../utils";
|
||||
|
||||
export default async function Recommendations(props: {
|
||||
|
|
@ -14,6 +15,7 @@ export default async function Recommendations(props: {
|
|||
const recommendations = await getRecommendations(params.planId);
|
||||
const planMeta = await getPlanMeta(params.planId);
|
||||
const funding = await getPlanFunding(params.planId);
|
||||
const installedMeasures = await getInstalledMeasuresByUprn(propertyMeta.uprn);
|
||||
|
||||
return (
|
||||
<div className="leading-loose tracking-wider">
|
||||
|
|
@ -22,6 +24,7 @@ export default async function Recommendations(props: {
|
|||
propertyMeta={propertyMeta}
|
||||
planMeta={planMeta}
|
||||
funding={funding}
|
||||
installedMeasures={installedMeasures}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -61,11 +61,9 @@ function PlanCard({
|
|||
);
|
||||
}
|
||||
|
||||
export default async function RecommendationPlans(
|
||||
props: {
|
||||
params: Promise<{ slug: string; propertyId: string }>;
|
||||
}
|
||||
) {
|
||||
export default async function RecommendationPlans(props: {
|
||||
params: Promise<{ slug: string; propertyId: string }>;
|
||||
}) {
|
||||
const params = await props.params;
|
||||
const propertyMeta = await getPropertyMeta(params.propertyId);
|
||||
const plans = await getPlans(params.propertyId);
|
||||
|
|
@ -79,22 +77,12 @@ export default async function RecommendationPlans(
|
|||
<div className="max-w-3xl">
|
||||
{plans.map((plan, index) => {
|
||||
// We accumulate the cost and the sap points for only the default recommendations
|
||||
const totalEstimatedCost = plan.planRecommendations.reduce(
|
||||
(acc, rec) => {
|
||||
if (rec.recommendation.default) {
|
||||
return acc + rec.recommendation.estimatedCost;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
0
|
||||
);
|
||||
|
||||
const totalSapPoints = plan.planRecommendations.reduce((acc, rec) => {
|
||||
if (rec.recommendation.default) {
|
||||
return acc + rec.recommendation.sapPoints;
|
||||
}
|
||||
return acc;
|
||||
}, 0);
|
||||
const totalEstimatedCost = plan.costOfWorks || 0;
|
||||
|
||||
const totalSapPoints =
|
||||
(plan.postSapPoints || propertyMeta.currentSapPoints) -
|
||||
propertyMeta.currentSapPoints;
|
||||
|
||||
// Placeholder while we return 999 for all sap points
|
||||
const expectedSapPoints = Math.min(
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import {
|
|||
planRecommendations,
|
||||
plan,
|
||||
Plan,
|
||||
installedMeasure,
|
||||
} from "@/app/db/schema/recommendations";
|
||||
import { db } from "@/app/db/db";
|
||||
import { surveyDB } from "@/app/db/surveyDB/connection";
|
||||
|
|
@ -28,17 +29,31 @@ import {
|
|||
} from "@/app/db/schema/energy_assessments";
|
||||
import {
|
||||
fundingPackage,
|
||||
FundingPackageWithMeasures
|
||||
FundingPackageWithMeasures,
|
||||
} from "@/app/db/schema/funding";
|
||||
import { getUploadedFile, uploadedFiles, DB_REPORT_TYPES } from "@/app/db/surveyDB/schema/surveyDB";
|
||||
import {
|
||||
getUploadedFile,
|
||||
uploadedFiles,
|
||||
DB_REPORT_TYPES,
|
||||
} from "@/app/db/surveyDB/schema/surveyDB";
|
||||
|
||||
export type InstalledMeasureSummary = {
|
||||
measureType: string;
|
||||
installedAt: Date | null;
|
||||
sapPoints: number | null;
|
||||
carbonSavings: number | null;
|
||||
kwhSavings: number | null;
|
||||
billSavings: number | null;
|
||||
};
|
||||
|
||||
export async function getEnergyAssessmentFromS3(s3Uri: string): Promise<any> {
|
||||
const url = new URL(s3Uri);
|
||||
|
||||
const bucketMatch = url.hostname.match(/^(.+)\.s3/);
|
||||
const bucket = bucketMatch?.[1];
|
||||
const key = url.pathname.startsWith("/") ? url.pathname.slice(1) : url.pathname;
|
||||
const key = url.pathname.startsWith("/")
|
||||
? url.pathname.slice(1)
|
||||
: url.pathname;
|
||||
|
||||
if (!bucket || !key) {
|
||||
throw new Error("Could not extract bucket or key from URI");
|
||||
|
|
@ -65,7 +80,9 @@ type RecommendationList = {
|
|||
recommendation: Recommendation;
|
||||
}[];
|
||||
|
||||
export async function getPlanFunding(planId: string): Promise<FundingPackageWithMeasures[]> {
|
||||
export async function getPlanFunding(
|
||||
planId: string
|
||||
): Promise<FundingPackageWithMeasures[]> {
|
||||
const data = await db.query.fundingPackage.findMany({
|
||||
where: eq(fundingPackage.planId, BigInt(planId)),
|
||||
with: {
|
||||
|
|
@ -80,13 +97,22 @@ export async function getPlanFunding(planId: string): Promise<FundingPackageWith
|
|||
return data;
|
||||
}
|
||||
|
||||
export async function getDocument(
|
||||
{uprn, documentType}: {uprn: string; documentType: typeof DB_REPORT_TYPES[number]}
|
||||
): Promise<getUploadedFile> {
|
||||
export async function getDocument({
|
||||
uprn,
|
||||
documentType,
|
||||
}: {
|
||||
uprn: string;
|
||||
documentType: (typeof DB_REPORT_TYPES)[number];
|
||||
}): Promise<getUploadedFile> {
|
||||
// We get the latest entry for the given UPRN and document type, by s3JsonUploadTimestamp
|
||||
const data = await surveyDB.query.uploadedFiles.findFirst({
|
||||
where: and(eq(uploadedFiles.uprn, String(uprn)), eq(uploadedFiles.docType, documentType)),
|
||||
orderBy: (uploadedFiles, { desc }) => [desc(uploadedFiles.s3JsonUploadTimestamp)]
|
||||
where: and(
|
||||
eq(uploadedFiles.uprn, String(uprn)),
|
||||
eq(uploadedFiles.docType, documentType)
|
||||
),
|
||||
orderBy: (uploadedFiles, { desc }) => [
|
||||
desc(uploadedFiles.s3JsonUploadTimestamp),
|
||||
],
|
||||
});
|
||||
// We may not have an uploaded document so we return an empty array
|
||||
if (!data) {
|
||||
|
|
@ -177,20 +203,20 @@ export async function getPlans(propertyId: string): Promise<PlanRelation[]> {
|
|||
const data = await db.query.plan.findMany({
|
||||
where: eq(plan.propertyId, BigInt(propertyId)),
|
||||
orderBy: [desc(plan.createdAt)],
|
||||
with: {
|
||||
planRecommendations: {
|
||||
columns: {},
|
||||
with: {
|
||||
recommendation: {
|
||||
columns: {
|
||||
estimatedCost: true,
|
||||
sapPoints: true,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
// with: {
|
||||
// planRecommendations: {
|
||||
// columns: {},
|
||||
// with: {
|
||||
// recommendation: {
|
||||
// columns: {
|
||||
// estimatedCost: true,
|
||||
// sapPoints: true,
|
||||
// default: true,
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
});
|
||||
|
||||
if (!data) {
|
||||
|
|
@ -456,3 +482,26 @@ export function formatHeatDemandFeatures(
|
|||
// },
|
||||
];
|
||||
}
|
||||
|
||||
export async function getInstalledMeasuresByUprn(
|
||||
uprn: number
|
||||
): Promise<InstalledMeasureSummary[]> {
|
||||
const data = await db
|
||||
.select({
|
||||
measureType: installedMeasure.measureType,
|
||||
installedAt: installedMeasure.installedAt,
|
||||
sapPoints: installedMeasure.sapPoints,
|
||||
carbonSavings: installedMeasure.carbonSavings,
|
||||
kwhSavings: installedMeasure.kwhSavings,
|
||||
billSavings: installedMeasure.billSavings,
|
||||
})
|
||||
.from(installedMeasure)
|
||||
.where(
|
||||
and(
|
||||
eq(installedMeasure.uprn, BigInt(uprn)),
|
||||
eq(installedMeasure.isActive, true)
|
||||
)
|
||||
);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -136,6 +136,10 @@ export function formatDateTime(dateTimeString: string | Date): string {
|
|||
export function formatNumber(number: number): string {
|
||||
if (number === 0) return "0";
|
||||
|
||||
if (number < 1) {
|
||||
return number.toFixed(2);
|
||||
}
|
||||
|
||||
const suffixes = ["", "k", "m", "b", "t"];
|
||||
const suffixIndex = Math.floor(Math.log10(Math.abs(number)) / 3);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue