Adding inclusions into body - need to pass existing scenario

This commit is contained in:
Khalim Conn-Kowlessar 2025-07-15 12:41:41 +01:00
parent 4b23c950d5
commit 1f0d4f0dcb
6 changed files with 164 additions and 24 deletions

View file

@ -1,5 +1,6 @@
import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import { MeasureKeyEnum } from "@/app/db/schema/recommendations";
const PresignedUrlBodySchema = z.object({
portfolio_id: z.string(),
@ -12,6 +13,10 @@ const PresignedUrlBodySchema = z.object({
budget: z.number().optional().nullable(),
scenario_name: z.string().optional(),
event_type: z.enum(["remote_assessment"]).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(),
});
export async function POST(request: NextRequest) {

View file

@ -13,6 +13,7 @@ import {
} from "drizzle-orm/pg-core";
import { Material, material } from "./materials";
import { InferModel } from "drizzle-orm";
import { z } from "zod";
export const recommendation = pgTable("recommendation", {
id: bigserial("id", { mode: "bigint" }).primaryKey(),
@ -232,3 +233,39 @@ export interface RecommendationWithMaterials {
totalWorkHours: number;
recommendationMaterials: RecommendationMaterialToMaterial[];
}
export const measuresDisplayLabels = {
internal_wall_insulation: "Internal Wall Insulation",
external_wall_insulation: "External Wall Insulation",
cavity_wall_insulation: "Cavity Wall Insulation",
loft_insulation: "Loft Insulation",
flat_roof_insulation: "Flat Roof Insulation",
room_roof_insulation: "Room-in-Roof Insulation",
suspended_floor_insulation: "Suspended Floor Insulation",
solid_floor_insulation: "Solid Floor Insulation",
boiler_upgrade: "Boiler Upgrade",
high_heat_retention_storage_heater: "High Heat Retention Storage Heater",
air_source_heat_pump: "Air Source Heat Pump",
secondary_heating: "Secondary Heating",
solar_pv: "Solar PV",
double_glazing: "Double Glazing",
secondary_glazing: "Secondary Glazing",
ventilation: "Ventilation",
low_energy_lighting: "Low Energy Lighting",
fireplace: "Fireplace",
hot_water_tank_insulation: "Hot Water Tank Insulation",
cylinder_thermostat: "Cylinder Thermostat",
} as const;
export type MeasureKey = keyof typeof measuresDisplayLabels;
export const measuresList: MeasureKey[] = Object.keys(
measuresDisplayLabels
) as MeasureKey[];
export const MeasureKeyEnum = z.enum([
...Object.keys(measuresDisplayLabels),
] as [
MeasureKey, // Force at least one measure key
...MeasureKey[]
]);

View file

@ -11,6 +11,7 @@ import { Toaster } from "@/app/shadcn_components/ui/toaster";
// If loading a variable font, you don't need to specify the font weight
const inter = Inter({
subsets: ["latin"],
variable: "--font-inter",
});
export const metadata = {

View file

@ -74,6 +74,12 @@ export const DocumentsTable: React.FC<Props> = ({
(doc) => doc.documentType === "OSMOSIS_CONDITION_PAS_2035_REPORT"
);
const floors = documents.filter((doc) => doc.documentType === "FLOOR_PLAN");
const occupancy = documents.filter(
(doc) => doc.documentType === "OCCUPANCY_ASSESSMENT"
);
return (
// Quidos Pre-Site Notes Row
<Table className="min-w-full table-fixed divide-y divide-gray-200 shadow overflow-hidden border-b border-gray-200 sm:rounded-lg">
@ -100,6 +106,30 @@ export const DocumentsTable: React.FC<Props> = ({
<TableRow className="hover:bg-transparent">
<TableCell colSpan={3} className="h-3 p-0" />
</TableRow>
<DocumentSection
title="Floor Plan"
docs={floors}
sectionKey="floorplan"
documentType="FLOOR_PLAN"
fileTypes=".pdf"
/>
<TableRow className="hover:bg-transparent">
<TableCell colSpan={3} className="h-3 p-0" />
</TableRow>
<DocumentSection
title="Occupancy Assessment"
docs={occupancy}
sectionKey="occupancy"
documentType="OCCUPANCY_ASSESSMENT"
fileTypes=".pdf"
/>
<TableRow className="hover:bg-transparent">
<TableCell colSpan={3} className="h-3 p-0" />
</TableRow>
</TableBody>
</Table>
);

View file

@ -61,6 +61,10 @@ export default async function DocumentsPage({
<div className="py-4">
<DocumentsTable documents={documents?.documents ?? []} />
</div>
<div className="flex items-center justify-between py-4 px-6 bg-brandblue text-white font-semibold text-lg rounded-md">
Coordination
</div>
</div>
</>
);

View file

@ -6,7 +6,7 @@ import { Input } from "@/app/shadcn_components/ui/input";
import { Button } from "@/app/shadcn_components/ui/button";
import { useMutation } from "@tanstack/react-query";
import { useSession } from "next-auth/react";
import { useForm, FormProvider } from "react-hook-form";
import { useForm, FormProvider, useFormContext } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import * as z from "zod";
import {
@ -24,11 +24,16 @@ import {
SelectScenarioDropdown,
SelectDropdown,
} from "./RemoteAssessmentDropdowns";
import {
measuresList,
measuresDisplayLabels,
MeasureKeyEnum,
} from "@/app/db/schema/recommendations";
type Option = {
label: string;
value: string;
disabled: boolean;
disabled?: boolean;
};
type DropdownProps = {
@ -152,20 +157,21 @@ interface EngineTriggerBody {
multi_plan: boolean;
budget: null;
event_type: string;
inclusions: (typeof measuresList)[number][];
}
const formSchema = z.object({
scenario: z.string().min(1, "Scenario is required"),
goal: z.string().min(1, "Goal is required"),
goalValue: z.string().min(1, "Goal value is required"),
housingType: z.string().min(1, "Housing type is required"),
addressLineOne: z.string().min(1, "Address is required"),
postcode: z.string().min(1, "Postcode is required"),
uprn: z.number().min(1, "UPRN is required"),
valuation: z.number().min(1, "Valuation is required"),
// Both property type and build form are optional
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),
valuation: z.number().min(1),
propertyType: z.string().optional().nullable(),
builtForm: z.string().optional().nullable(),
measures: z.array(MeasureKeyEnum),
});
type FormValues = z.infer<typeof formSchema>;
@ -269,12 +275,14 @@ function useCreateRemoteAssessment({
valuation,
propertyType,
builtForm,
measures,
}: {
portfolioId: string;
uprn: number | null;
addressLineOne: string;
postcode: string;
valuation: string | number | null;
measures: (typeof measuresList)[number][];
propertyType?: string | null;
builtForm?: string | null;
}) {
@ -369,11 +377,14 @@ function useCreateRemoteAssessment({
non_invasive_recommendations_file_path: "",
valuation_file_path: valuationDataFileKey,
scenario_name: data.scenario,
inclusions: data.measures,
multi_plan: true,
budget: null,
event_type: "remote_assessment",
};
console.log("Triggering engine with body:", triggerBody);
const response = await fetch("/api/plan/trigger", {
method: "POST",
headers: {
@ -437,20 +448,22 @@ export default function RemoteAssessmentModal({
const NEW_SENTINEL = "__new__";
const [selectedScenario, setSelectedScenario] = useState<string | null>(null);
const { toast } = useToast();
const [showMeasures, setShowMeasures] = useState(false);
const scenarioOptions: Option[] = useMemo(
() => [
{ label: "Create new scenario…", value: NEW_SENTINEL, disabled: false },
...scenarios.map((s) => ({
label: s.name,
value: s.name,
label: s.name || "",
value: s.name || "",
disabled: false,
})),
],
[scenarios]
);
const form = useForm<FormValues>({
console.log("Scenario options:", scenarioOptions);
const form = useForm({
resolver: zodResolver(formSchema),
defaultValues: {
scenario: "",
@ -463,6 +476,7 @@ export default function RemoteAssessmentModal({
valuation: 0,
propertyType: null,
builtForm: null,
measures: measuresList,
},
});
const { reset, setValue } = form;
@ -479,6 +493,7 @@ export default function RemoteAssessmentModal({
valuation: form.watch("valuation"),
propertyType: form.watch("propertyType"),
builtForm: form.watch("builtForm"),
measures: form.watch("measures"),
});
const onSelectScenario = (opt: Option) => {
@ -494,10 +509,10 @@ export default function RemoteAssessmentModal({
} else {
const picked = scenarios.find((s) => s.name === opt.value);
if (!picked) return;
setValue("scenario", picked.name);
setValue("scenario", picked.name || "");
setValue("housingType", picked.housingType);
setValue("goal", picked.goal);
setValue("goalValue", picked.goalValue);
setValue("goalValue", picked.goalValue || "");
}
};
@ -558,10 +573,7 @@ export default function RemoteAssessmentModal({
<FormLabel>Select scenario</FormLabel>
<FormControl>
<SelectScenarioDropdown
scenarios={scenarios.map((s) => ({
label: s.name,
value: s.name,
}))}
scenarios={scenarioOptions}
selectedValue={selectedScenario}
onSelect={onSelectScenario}
/>
@ -661,7 +673,7 @@ export default function RemoteAssessmentModal({
<FormControl>
{selectedScenario === NEW_SENTINEL ? (
<SelectDropdown
options={selecthousingTypeOptions}
options={goalValueOptions}
selectedOption={field.value}
onSelectOption={(opt) =>
field.onChange(opt.value)
@ -790,7 +802,7 @@ export default function RemoteAssessmentModal({
<FormControl>
<SelectDropdown
options={propertyTypeOptions}
selectedOption={field.value}
selectedOption={field.value || ""}
onSelectOption={(o) => field.onChange(o.value)}
/>
</FormControl>
@ -807,7 +819,7 @@ export default function RemoteAssessmentModal({
<FormControl>
<SelectDropdown
options={builtFormOptions}
selectedOption={field.value}
selectedOption={field.value || ""}
onSelectOption={(o) => field.onChange(o.value)}
/>
</FormControl>
@ -818,6 +830,19 @@ export default function RemoteAssessmentModal({
</div>
</div>
{/* Measures Section */}
<div className="border-t pt-4 mt-6">
<button
type="button"
onClick={() => setShowMeasures(!showMeasures)}
className="flex items-center justify-between w-full text-sm font-medium"
>
<span>Measures</span>
<span>{showMeasures ? "" : "+"}</span>
</button>
{showMeasures && <MeasuresCheckboxes />}
</div>
<div className="flex justify-end gap-4">
<Button
type="button"
@ -846,3 +871,41 @@ export default function RemoteAssessmentModal({
function setIsOpen(arg0: boolean) {
throw new Error("Function not implemented.");
}
function MeasuresCheckboxes() {
const { control } = useFormContext();
return (
<div className="grid grid-cols-2 gap-4 mt-4">
{measuresList.map((measure) => (
<FormField
key={measure}
control={control}
name="measures"
render={({ field }) => (
<FormItem className="flex items-center space-x-2">
<input
type="checkbox"
value={measure}
checked={field.value?.includes(measure) ?? true}
onChange={(e) => {
const checked = e.target.checked;
const current = new Set(field.value ?? []);
if (checked) {
current.add(measure);
} else {
current.delete(measure);
}
field.onChange(Array.from(current));
}}
className="h-4 w-4"
/>
<FormLabel className="text-sm">
{measuresDisplayLabels[measure]}
</FormLabel>
</FormItem>
)}
/>
))}
</div>
);
}