mirror of
https://github.com/Hestia-Homes/assessment-model.git
synced 2026-06-08 11:37:25 +00:00
Adding inclusions into body - need to pass existing scenario
This commit is contained in:
parent
4b23c950d5
commit
1f0d4f0dcb
6 changed files with 164 additions and 24 deletions
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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[]
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue