handling already installed measures - updated ui and adding handling for installed measures

This commit is contained in:
Khalim Conn-Kowlessar 2026-01-07 22:17:45 +00:00
parent 8a08e98340
commit bb141b759b
12 changed files with 5410 additions and 60 deletions

View file

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

View file

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

View file

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

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

File diff suppressed because it is too large Load diff

View file

@ -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
}
]
}

View file

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

View file

@ -175,7 +175,6 @@ export function ReportingClientArea({
scenarioData.n_units_upgraded,
}
: null;
// Baseline stays baseline
const activeMetrics = baseline;

View file

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

View file

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

View file

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

View file

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