diff --git a/src/app/api/plan/trigger/route.ts b/src/app/api/plan/trigger/route.ts index 2925a61..baa49a6 100644 --- a/src/app/api/plan/trigger/route.ts +++ b/src/app/api/plan/trigger/route.ts @@ -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) { diff --git a/src/app/db/schema/recommendations.ts b/src/app/db/schema/recommendations.ts index 0f861af..1a21ac4 100644 --- a/src/app/db/schema/recommendations.ts +++ b/src/app/db/schema/recommendations.ts @@ -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[] +]); diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 4c9d8d1..5fb12ae 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -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 = { diff --git a/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/DocumentsTable.tsx b/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/DocumentsTable.tsx index 231fc35..52c40d2 100644 --- a/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/DocumentsTable.tsx +++ b/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/DocumentsTable.tsx @@ -74,6 +74,12 @@ export const DocumentsTable: React.FC = ({ (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 @@ -100,6 +106,30 @@ export const DocumentsTable: React.FC = ({ + + + + + + + + + + + +
); diff --git a/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/page.tsx b/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/page.tsx index 5faeba3..b1183ae 100644 --- a/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/page.tsx +++ b/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/page.tsx @@ -61,6 +61,10 @@ export default async function DocumentsPage({
+ +
+ Coordination +
); diff --git a/src/app/portfolio/[slug]/components/RemoteAssessmentModal.tsx b/src/app/portfolio/[slug]/components/RemoteAssessmentModal.tsx index 1e88995..81ac22c 100644 --- a/src/app/portfolio/[slug]/components/RemoteAssessmentModal.tsx +++ b/src/app/portfolio/[slug]/components/RemoteAssessmentModal.tsx @@ -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; @@ -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(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({ + 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({ Select scenario ({ - label: s.name, - value: s.name, - }))} + scenarios={scenarioOptions} selectedValue={selectedScenario} onSelect={onSelectScenario} /> @@ -661,7 +673,7 @@ export default function RemoteAssessmentModal({ {selectedScenario === NEW_SENTINEL ? ( field.onChange(opt.value) @@ -790,7 +802,7 @@ export default function RemoteAssessmentModal({ field.onChange(o.value)} /> @@ -807,7 +819,7 @@ export default function RemoteAssessmentModal({ field.onChange(o.value)} /> @@ -818,6 +830,19 @@ export default function RemoteAssessmentModal({ + {/* Measures Section */} +
+ + {showMeasures && } +
+
+ ); +}