Merge pull request #65 from Hestia-Homes/main

Additional optimisation options released
This commit is contained in:
KhalimCK 2025-08-01 15:54:23 +01:00 committed by GitHub
commit 7c8e36beae
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 141 additions and 70 deletions

View file

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

View file

@ -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"),
@ -8,18 +8,34 @@ export const baseFormSchema = z.object({
export type BaseFormValues = z.infer<typeof baseFormSchema>;
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.enum(HousingType),
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

View file

@ -119,9 +119,14 @@ const selectGoalOptions = [
disabled: false,
},
{
label: "Reduce energy consumption",
value: "Reduce energy consumption",
disabled: true,
label: "Energy Savings",
value: "Energy Savings",
disabled: false,
},
{
label: "Reducing CO2 emissions",
value: "Reducing CO2 emissions",
disabled: false,
},
];
@ -147,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;
@ -155,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;
@ -357,12 +362,18 @@ function useCreateRemoteAssessment({
async function triggerEngine(data: RemoteAssessmentFormValues) {
try {
// Goal value should not be missing at this point
if (data.goal === "Increasing EPC" && !data.goalValue) {
throw new Error("Goal value is required");
}
const triggerBody: EngineTriggerBody = {
scenario_id: scenarioId === "__new__" ? null : scenarioId,
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.goalValue || null,
trigger_file_path: assetListFileKey,
already_installed_file_path: "",
patches_file_path: "",
@ -371,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",
};
@ -461,10 +473,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 +487,7 @@ export default function RemoteAssessmentModal({
const { isValid, isSubmitting } = formState;
const measures = form.watch("measures");
const goal = form.watch("goal");
const {
handleSubmit: triggerAssessment,
@ -657,32 +671,67 @@ export default function RemoteAssessmentModal({
)}
/>
{/* Goal Value */}
<FormField
control={form.control}
name="goalValue"
render={({ field }) => (
<FormItem>
<FormLabel className="text-gray-800">
Goal Value
</FormLabel>
<FormControl>
{selectedScenario === NEW_SENTINEL ? (
<SelectDropdown
options={goalValueOptions}
selectedOption={field.value}
onSelectOption={(opt) =>
field.onChange(opt.value)
}
/>
) : (
<Input value={field.value} disabled />
{goal && (
<>
{goal === "Increasing EPC" && (
<FormField
control={form.control}
name="goalValue"
render={({ field }) => (
<FormItem>
<FormLabel className="text-gray-800">
Target EPC Rating
</FormLabel>
<FormControl>
{selectedScenario === NEW_SENTINEL ? (
<SelectDropdown
options={goalValueOptions}
selectedOption={field.value || ""}
onSelectOption={(opt) =>
field.onChange(opt.value)
}
/>
) : (
<Input value={field.value} disabled />
)}
</FormControl>
<FormMessage className="text-brandbrown" />
</FormItem>
)}
</FormControl>
<FormMessage className="text-brandbrown" />
</FormItem>
)}
/>
/>
)}
{/* ✅ Budget shows for ALL goals but is only mandatory when goal != Increasing EPC */}
<FormField
control={form.control}
name="budget"
render={({ field }) => (
<FormItem>
<FormLabel className="text-gray-800">
Budget (£)
</FormLabel>
<FormControl>
<Input
type="number"
placeholder="Enter budget"
{...field}
value={field.value ?? ""}
onChange={(e) =>
field.onChange(
e.target.value === ""
? undefined
: Number(e.target.value)
)
}
className="border-brandbrown focus-visible:ring-brandbrown focus-visible:border-brandbrown"
/>
</FormControl>
<FormMessage className="text-brandbrown" />
</FormItem>
)}
/>
</>
)}
</div>
</>
)}