From 2a7c05141ded8d88d53a6fae472f27c1b46e9c04 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Thu, 31 Jul 2025 14:55:19 +0100 Subject: [PATCH 1/4] allowing for additional goal types to be submitted --- .../[slug]/components/FormSchema.tsx | 40 ++++--- .../components/RemoteAssessmentModal.tsx | 101 +++++++++++++----- 2 files changed, 100 insertions(+), 41 deletions(-) diff --git a/src/app/portfolio/[slug]/components/FormSchema.tsx b/src/app/portfolio/[slug]/components/FormSchema.tsx index fbf3a3a8..9e281fb8 100644 --- a/src/app/portfolio/[slug]/components/FormSchema.tsx +++ b/src/app/portfolio/[slug]/components/FormSchema.tsx @@ -8,18 +8,34 @@ export const baseFormSchema = z.object({ export type BaseFormValues = z.infer; -export const RemoteAssessmentFormSchema = baseFormSchema.extend({ - scenario: z.string().min(1), - goal: z.string().min(1), - goalValue: z.string().min(1), - housingType: z.string().min(1), - addressLineOne: z.string().min(1), - postcode: z.string().min(1), - uprn: z.number().min(1, "UPRN must be a valid number"), - valuation: z.number().min(1, "Valuation must be a valid number"), - propertyType: z.string().nullable(), - builtForm: z.string().nullable(), -}); +export const RemoteAssessmentFormSchema = baseFormSchema + .extend({ + scenario: z.string().min(1), + goal: z.string().min(1), + // goalValue: z.string().min(1), + goalValue: z.string().optional(), + budget: z.number().optional(), + housingType: z.string().min(1), + addressLineOne: z.string().min(1), + postcode: z.string().min(1), + uprn: z.number().min(1, "UPRN must be a valid number"), + valuation: z.number().min(1, "Valuation must be a valid number"), + propertyType: z.string().nullable(), + builtForm: z.string().nullable(), + }) + .refine((data) => data.goal !== "Increasing EPC" || !!data.goalValue, { + path: ["goalValue"], + message: "Target EPC Rating is required when goal is Increasing EPC", + }) + .refine( + (data) => + data.goal === "Increasing EPC" || + (typeof data.budget === "number" && data.budget > 0), + { + path: ["budget"], + message: "Budget is required for this goal", + } + ); export type RemoteAssessmentFormValues = z.infer< typeof RemoteAssessmentFormSchema diff --git a/src/app/portfolio/[slug]/components/RemoteAssessmentModal.tsx b/src/app/portfolio/[slug]/components/RemoteAssessmentModal.tsx index e1b5702f..7a375390 100644 --- a/src/app/portfolio/[slug]/components/RemoteAssessmentModal.tsx +++ b/src/app/portfolio/[slug]/components/RemoteAssessmentModal.tsx @@ -121,7 +121,12 @@ const selectGoalOptions = [ { label: "Reduce energy consumption", value: "Reduce energy consumption", - disabled: true, + disabled: false, + }, + { + label: "Reduce CO2 emissions", + value: "Reduce CO2 emissions", + disabled: false, }, ]; @@ -357,6 +362,11 @@ function useCreateRemoteAssessment({ async function triggerEngine(data: RemoteAssessmentFormValues) { try { + // Goal value should not be missing at this point + if (!data.goalValue) { + throw new Error("Goal value is required"); + } + const triggerBody: EngineTriggerBody = { scenario_id: scenarioId === "__new__" ? null : scenarioId, portfolio_id: portfolioId, @@ -461,10 +471,11 @@ export default function RemoteAssessmentModal({ housingType: "", goal: "", goalValue: "", + budget: undefined, addressLineOne: "", postcode: "", - uprn: undefined, // Must match expected type - valuation: undefined, // Must match expected type + uprn: undefined, + valuation: undefined, propertyType: null, builtForm: null, measures: measuresList, @@ -474,6 +485,7 @@ export default function RemoteAssessmentModal({ const { isValid, isSubmitting } = formState; const measures = form.watch("measures"); + const goal = form.watch("goal"); const { handleSubmit: triggerAssessment, @@ -657,32 +669,63 @@ export default function RemoteAssessmentModal({ )} /> - {/* Goal Value */} - ( - - - Goal Value - - - {selectedScenario === NEW_SENTINEL ? ( - - field.onChange(opt.value) - } - /> - ) : ( - - )} - - - - )} - /> + {goal && + (goal === "Increasing EPC" ? ( + ( + + + Target EPC Rating + + + {selectedScenario === NEW_SENTINEL ? ( + + field.onChange(opt.value) + } + /> + ) : ( + + )} + + + + )} + /> + ) : ( + ( + + + Budget (£) + + + + field.onChange( + e.target.value === "" + ? undefined + : Number(e.target.value) + ) + } + className="border-brandbrown focus-visible:ring-brandbrown focus-visible:border-brandbrown" + /> + + + + )} + /> + ))} )} From 88e499ea3155802ff77867cc62e2a19b4bfa22ad Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Thu, 31 Jul 2025 15:40:20 +0100 Subject: [PATCH 2/4] Allowing carbon and energy reduction goals to be passed in the remote assessment --- .../components/RemoteAssessmentModal.tsx | 74 ++++++++++--------- 1 file changed, 40 insertions(+), 34 deletions(-) diff --git a/src/app/portfolio/[slug]/components/RemoteAssessmentModal.tsx b/src/app/portfolio/[slug]/components/RemoteAssessmentModal.tsx index 7a375390..195ff54d 100644 --- a/src/app/portfolio/[slug]/components/RemoteAssessmentModal.tsx +++ b/src/app/portfolio/[slug]/components/RemoteAssessmentModal.tsx @@ -152,7 +152,7 @@ interface EngineTriggerBody { portfolio_id: string; housing_type: string; goal: string; - goal_value: string; + goal_value: string | null; trigger_file_path: string; already_installed_file_path: string; patches_file_path: string; @@ -160,7 +160,7 @@ interface EngineTriggerBody { valuation_file_path: string; scenario_name: string; multi_plan: boolean; - budget: null; + budget: number | null; event_type: string; inclusions: (typeof measuresList)[number][]; scenario_id?: string | null; @@ -363,7 +363,7 @@ function useCreateRemoteAssessment({ async function triggerEngine(data: RemoteAssessmentFormValues) { try { // Goal value should not be missing at this point - if (!data.goalValue) { + if (data.goal === "Increasing EPC" && !data.goalValue) { throw new Error("Goal value is required"); } @@ -372,7 +372,8 @@ function useCreateRemoteAssessment({ portfolio_id: portfolioId, housing_type: data.housingType, goal: data.goal, - goal_value: data.goalValue, + // We only send goal_value if the goal is "Increasing EPC" + goal_value: data.goal, trigger_file_path: assetListFileKey, already_installed_file_path: "", patches_file_path: "", @@ -381,7 +382,8 @@ function useCreateRemoteAssessment({ scenario_name: data.scenario, inclusions: data.measures, multi_plan: true, - budget: null, + // If the goal is "Increasing EPC", we don't send a budget + budget: data.budget || null, event_type: "remote_assessment", }; @@ -669,34 +671,37 @@ export default function RemoteAssessmentModal({ )} /> - {goal && - (goal === "Increasing EPC" ? ( - ( - - - Target EPC Rating - - - {selectedScenario === NEW_SENTINEL ? ( - - field.onChange(opt.value) - } - /> - ) : ( - - )} - - - - )} - /> - ) : ( + {goal && ( + <> + {goal === "Increasing EPC" && ( + ( + + + Target EPC Rating + + + {selectedScenario === NEW_SENTINEL ? ( + + field.onChange(opt.value) + } + /> + ) : ( + + )} + + + + )} + /> + )} + + {/* ✅ Budget shows for ALL goals but is only mandatory when goal != Increasing EPC */} )} /> - ))} + + )} )} From b0129617186243730ef13f2b90dbd6931d91f38f Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Thu, 31 Jul 2025 17:07:05 +0100 Subject: [PATCH 3/4] changed goal values in remote assessment modal --- src/app/portfolio/[slug]/components/RemoteAssessmentModal.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/portfolio/[slug]/components/RemoteAssessmentModal.tsx b/src/app/portfolio/[slug]/components/RemoteAssessmentModal.tsx index 195ff54d..85daa287 100644 --- a/src/app/portfolio/[slug]/components/RemoteAssessmentModal.tsx +++ b/src/app/portfolio/[slug]/components/RemoteAssessmentModal.tsx @@ -120,12 +120,12 @@ const selectGoalOptions = [ }, { label: "Reduce energy consumption", - value: "Reduce energy consumption", + value: "Energy Savings", disabled: false, }, { label: "Reduce CO2 emissions", - value: "Reduce CO2 emissions", + value: "Reducing CO2 emissions", disabled: false, }, ]; From 550d2462dc82c8f6f4950d17224986054524153b Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Fri, 1 Aug 2025 14:27:32 +0100 Subject: [PATCH 4/4] implemented optimisation options for remote assessments --- src/app/api/plan/trigger/route.ts | 52 +++++++++++-------- .../[slug]/components/FormSchema.tsx | 4 +- .../components/RemoteAssessmentModal.tsx | 6 +-- 3 files changed, 34 insertions(+), 28 deletions(-) diff --git a/src/app/api/plan/trigger/route.ts b/src/app/api/plan/trigger/route.ts index 5c21a116..bf6be78f 100644 --- a/src/app/api/plan/trigger/route.ts +++ b/src/app/api/plan/trigger/route.ts @@ -1,30 +1,36 @@ import { NextRequest, NextResponse } from "next/server"; import { z } from "zod"; import { MeasureKeyEnum } from "@/app/db/schema/recommendations"; +import { PortfolioGoal } from "@/app/db/schema/portfolio"; -const PresignedUrlBodySchema = z.object({ - portfolio_id: z.string(), - housing_type: z.enum(["Social", "Private"]), - goal: z.enum(["Increasing EPC", "Reduce energy consumption"]), - goal_value: z.string(), - trigger_file_path: z.string(), - valuation_file_path: z.string(), - multi_plan: z.boolean().optional(), - budget: z.number().optional().nullable(), - scenario_name: z.string().optional(), - sheet_count: z.number().optional(), // Number of rows in the selected sheet - event_type: z.enum(["remote_assessment"]).optional(), - ashp_cop: z.number().optional(), - // inclusions is a list of measures, where the values are in measuresList - inclusions: z.array(MeasureKeyEnum).optional(), - exclusions: z.array(MeasureKeyEnum).optional(), - already_installed_file_path: z.string().optional(), - // optional scenario_id to link the plan to an existing scenario - scenario_id: z.string().optional().nullable(), - file_type: z.enum(["csv", "xlsx"]).optional(), // Specify the file type - file_format: z.enum(["domna_asset_list"]).optional().nullable(), // Specify the file format - sheet_name: z.string().optional().nullable(), // Specify the sheet name if applicable -}); +const PresignedUrlBodySchema = z + .object({ + portfolio_id: z.string(), + housing_type: z.enum(["Social", "Private"]), + goal: z.enum(PortfolioGoal), + goal_value: z.string().nullable(), // Nullable to handle cases where goal_value is not applicable + trigger_file_path: z.string(), + valuation_file_path: z.string(), + multi_plan: z.boolean().optional(), + budget: z.number().optional().nullable(), + scenario_name: z.string().optional(), + sheet_count: z.number().optional(), // Number of rows in the selected sheet + event_type: z.enum(["remote_assessment"]).optional(), + ashp_cop: z.number().optional(), + // inclusions is a list of measures, where the values are in measuresList + inclusions: z.array(MeasureKeyEnum).optional(), + exclusions: z.array(MeasureKeyEnum).optional(), + already_installed_file_path: z.string().optional(), + // optional scenario_id to link the plan to an existing scenario + scenario_id: z.string().optional().nullable(), + file_type: z.enum(["csv", "xlsx"]).optional(), // Specify the file type + file_format: z.enum(["domna_asset_list"]).optional().nullable(), // Specify the file format + sheet_name: z.string().optional().nullable(), // Specify the sheet name if applicable + }) + .refine((data) => data.goal !== "Increasing EPC" || !!data.goal_value, { + path: ["goal_value"], + message: "Target EPC Rating is required when goal is Increasing EPC", + }); export async function POST(request: NextRequest) { // For the moment, this api specifically handles uploads of csvs diff --git a/src/app/portfolio/[slug]/components/FormSchema.tsx b/src/app/portfolio/[slug]/components/FormSchema.tsx index 9e281fb8..1a2c36f1 100644 --- a/src/app/portfolio/[slug]/components/FormSchema.tsx +++ b/src/app/portfolio/[slug]/components/FormSchema.tsx @@ -1,6 +1,6 @@ // formSchemas.ts import * as z from "zod"; -import { MeasureKeyEnum } from "@/app/db/schema/recommendations"; +import { MeasureKeyEnum, HousingType } from "@/app/db/schema/recommendations"; export const baseFormSchema = z.object({ measures: z.array(MeasureKeyEnum).min(1, "At least one measure is required"), @@ -15,7 +15,7 @@ export const RemoteAssessmentFormSchema = baseFormSchema // goalValue: z.string().min(1), goalValue: z.string().optional(), budget: z.number().optional(), - housingType: z.string().min(1), + housingType: z.enum(HousingType), addressLineOne: z.string().min(1), postcode: z.string().min(1), uprn: z.number().min(1, "UPRN must be a valid number"), diff --git a/src/app/portfolio/[slug]/components/RemoteAssessmentModal.tsx b/src/app/portfolio/[slug]/components/RemoteAssessmentModal.tsx index 85daa287..54752650 100644 --- a/src/app/portfolio/[slug]/components/RemoteAssessmentModal.tsx +++ b/src/app/portfolio/[slug]/components/RemoteAssessmentModal.tsx @@ -119,12 +119,12 @@ const selectGoalOptions = [ disabled: false, }, { - label: "Reduce energy consumption", + label: "Energy Savings", value: "Energy Savings", disabled: false, }, { - label: "Reduce CO2 emissions", + label: "Reducing CO2 emissions", value: "Reducing CO2 emissions", disabled: false, }, @@ -373,7 +373,7 @@ function useCreateRemoteAssessment({ housing_type: data.housingType, goal: data.goal, // We only send goal_value if the goal is "Increasing EPC" - goal_value: data.goal, + goal_value: data.goalValue || null, trigger_file_path: assetListFileKey, already_installed_file_path: "", patches_file_path: "",