basic setup for remote assessment live

This commit is contained in:
Khalim Conn-Kowlessar 2025-10-21 12:49:36 +00:00
parent 45c95340c8
commit 54ccbf93dd
7 changed files with 76 additions and 1001 deletions

View file

@ -14,7 +14,6 @@ import {
import AddNewDropDown from "./AddNew";
import { cva } from "class-variance-authority";
import UploadCsvModal from "@/app/portfolio/[slug]/components/UploadCsvModal";
import RemoteAssessmentModal from "@/app/portfolio/[slug]/components/RemoteAssessmentModal";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { ScenarioSelect } from "@/app/db/schema/recommendations";
@ -103,12 +102,6 @@ export function Toolbar({ portfolioId, scenarios }: ToolbarProps) {
setIsRemoteAssessmentOpen={setIsRemoteAssessmentOpen}
/>
</NavigationMenuList>
<RemoteAssessmentModal
isOpen={isRemoteAssessmentOpen}
setIsOpen={setIsRemoteAssessmentOpen}
portfolioId={portfolioId}
scenarios={scenarios}
/>
<UploadCsvModal
isOpen={modalIsOpen}
setIsOpen={setModalIsOpen}

View file

@ -19,9 +19,9 @@ export const RemoteAssessmentFormSchema = baseFormSchema
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(),
valuation: z.number().min(1, "Valuation must be a valid number").optional(),
propertyType: z.string().nullable().optional(),
builtForm: z.string().nullable().optional(),
})
.refine((data) => data.goal !== "Increasing EPC" || !!data.goalValue, {
path: ["goalValue"],

View file

@ -1,948 +0,0 @@
"use client";
import {
Dialog,
DialogBackdrop,
DialogPanel,
DialogTitle,
Transition,
TransitionChild,
} from "@headlessui/react";
import { Fragment, useMemo } from "react";
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 { zodResolver } from "@hookform/resolvers/zod";
import {
FormField,
FormItem,
FormLabel,
FormControl,
FormMessage,
FormDescription,
} from "@/app/shadcn_components/ui/form";
import { useToast } from "@/app/hooks/use-toast";
import { ScenarioSelect } from "@/app/db/schema/recommendations";
import { useState } from "react";
import {
SelectScenarioDropdown,
SelectDropdown,
} from "./RemoteAssessmentDropdowns";
import MeasuresCheckboxes from "./MeasuresCheckboxes";
import { measuresList } from "@/app/db/schema/recommendations";
import {
RemoteAssessmentFormSchema,
RemoteAssessmentFormValues,
} from "./FormSchema";
type Option = {
label: string;
value: string;
disabled?: boolean;
};
type DropdownProps = {
options: Option[];
selectedOption: string;
onSelectOption: (option: Option) => void;
width?: string;
};
// Extend the existing props
type OptionalDropdownProps = Omit<DropdownProps, "selectedOption"> & {
selectedOption: string | null | undefined;
};
const selecthousingTypeOptions = [
{
label: "Social",
value: "Social",
disabled: false,
},
{
label: "Private",
value: "Private",
disabled: false,
},
];
const propertyTypeOptions = [
{
label: "House",
value: "House",
disabled: false,
},
{
label: "Flat",
value: "Flat",
disabled: false,
},
{
label: "Bungalow",
value: "bungalow",
disabled: false,
},
{
label: "Maisonette",
value: "Maisonette",
disabled: false,
},
{
label: "Other",
value: "Other",
disabled: false,
},
];
const builtFormOptions = [
{
label: "Detached",
value: "Detached",
disabled: false,
},
{
label: "Semi-Detached",
value: "Semi-Detached",
disabled: false,
},
{
label: "Mid-Terrace",
value: "Mid-Terrace",
disabled: false,
},
{
label: "End-Terrace",
value: "End-Terrace",
disabled: false,
},
];
const selectGoalOptions = [
{
label: "Increasing EPC",
value: "Increasing EPC",
disabled: false,
},
{
label: "Energy Savings",
value: "Energy Savings",
disabled: false,
},
{
label: "Reducing CO2 emissions",
value: "Reducing CO2 emissions",
disabled: false,
},
];
const goalValueOptions = [
{
label: "C",
value: "C",
disabled: false,
},
{
label: "B",
value: "B",
disabled: false,
},
{
label: "A",
value: "A",
disabled: false,
},
];
interface EngineTriggerBody {
portfolio_id: string;
housing_type: string;
goal: string;
goal_value: string | null;
trigger_file_path: string;
already_installed_file_path: string;
patches_file_path: string;
non_invasive_recommendations_file_path: string;
valuation_file_path: string;
scenario_name: string;
multi_plan: boolean;
budget: number | null;
event_type: string;
inclusions: (typeof measuresList)[number][];
scenario_id?: string | null;
}
async function uploadCsvToS3({
presignedUrl,
file,
}: {
presignedUrl: string;
file: Blob;
}) {
try {
const response = await fetch(presignedUrl, {
method: "PUT",
body: file,
headers: { "Content-Type": "text/csv" },
});
if (!response.ok) {
console.error(response);
throw new Error("Network response was not ok");
}
} catch (error) {
console.error(error);
throw new Error("Upload failed.");
}
console.log("File uploaded successfully");
return { success: true };
}
async function generatePresignedUrl({
userId,
portfolioId,
fileKey,
}: {
userId: string;
portfolioId: string;
fileKey: string;
}) {
// fileKey is a location in S3 where we want to upload the file
const response = await fetch("/api/upload/csv", {
method: "POST",
body: JSON.stringify({
userId,
portfolioId,
fileKey,
}),
});
if (!response.ok) {
throw new Error("Failed to generate presigned url");
}
const data = await response.json();
data.fileKey = fileKey;
return data;
}
function generateS3Keys(userId: string, portfolioId: string) {
const timestamp = new Date().toISOString().replace(/[:.-]/g, "");
const assetListFileKey = `${userId}/${portfolioId}/${timestamp}/asset_list.csv`;
const valuationDataFileKey = `${userId}/${portfolioId}/${timestamp}/valuation_data.csv`;
return { assetListFileKey, valuationDataFileKey };
}
type GenericObject = Record<string, any>;
const convertToCSV = <T extends Record<string, any>>(data: T[]): string => {
if (data.length === 0) return "";
const headers = Object.keys(data[0]) as (keyof T)[];
const escape = (value: any): string => {
if (value == null) return "";
const str = String(value);
// Check if field contains special characters
if (/[",\n]/.test(str)) {
// Escape double quotes and wrap in quotes
return `"${str.replace(/"/g, '""')}"`;
}
return str;
};
const rows = data.map((row) =>
headers.map((header) => escape(row[header])).join(",")
);
return [headers.join(","), ...rows].join("\n");
};
function useCreateRemoteAssessment({
portfolioId,
uprn,
addressLineOne,
postcode,
valuation,
propertyType,
builtForm,
measures,
scenarioId,
}: {
portfolioId: string;
uprn: number | undefined | null;
addressLineOne: string;
postcode: string;
valuation: number | undefined | null;
measures: (typeof measuresList)[number][];
propertyType?: string | null;
builtForm?: string | null;
scenarioId?: string | null;
}) {
// 1) We want to upload the asset data. To do this, we format the asset data, generate a presigned URL, and upload the data to S3.
// 2) We then want to upload valuation data. To do this, we format the valuation data, generate a presigned URL, and upload the data to S3.
// 3) Trigger the engine!!!! This is an api at /api/plan/trigger with our body that we looked at in Miro
// Set up the mutation with react-query, to generate a presigned URL
const session = useSession();
const userId = String(session.data?.user.dbId);
if (uprn === undefined || valuation === undefined) {
throw new Error("UPRN and valuation must be provided");
}
const { assetListFileKey, valuationDataFileKey } = useMemo(
() => generateS3Keys(userId, portfolioId),
[userId, portfolioId]
);
const {
mutate: mutateUploadFile,
isLoading: uploadFileIsLoading,
isError: uploadFileIsError,
} = useMutation(uploadCsvToS3, {
onSuccess: (data) => {
// Callback for successful mutation
console.log("Files uploaded successfully");
// Trigger the engine here if needed
},
onError: (error) => {
// Callback for failed mutation
console.error("Error uploading files:", error);
},
});
const {
mutate: mutatePresignedUrl,
isLoading: presignedUrlIsLoading,
isError: presignedUrlIsError,
} = useMutation(generatePresignedUrl, {
onSuccess: (data) => {
// console.log(data.url);
// // On success, upload to that URL!!!!
let csvFile: Blob = new Blob();
if (data.fileKey === assetListFileKey) {
const assetList = [
{
uprn: uprn,
address: addressLineOne,
postcode: postcode,
property_type: propertyType,
built_form: builtForm,
},
];
csvFile = new Blob([convertToCSV(assetList)], {
type: "text/csv",
});
} else if (data.fileKey === valuationDataFileKey) {
const valuationData = [
{
uprn: uprn,
valuation: valuation,
},
];
csvFile = new Blob([convertToCSV(valuationData)], {
type: "text/csv",
});
}
mutateUploadFile({
file: csvFile,
presignedUrl: data.url,
});
},
onError: (error) => {
console.error(error);
},
});
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,
// 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: "",
non_invasive_recommendations_file_path: "",
valuation_file_path: valuationDataFileKey,
scenario_name: data.scenario,
inclusions: data.measures,
multi_plan: true,
// If the goal is "Increasing EPC", we don't send a budget
budget: data.budget || null,
event_type: "remote_assessment",
};
console.log("Triggering engine with body:", triggerBody);
const response = await fetch("/api/plan/trigger", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(triggerBody),
});
if (!response.ok) {
throw new Error("Failed to trigger engine");
}
} catch (error) {
console.error("Error triggering engine:", error);
throw error;
}
}
async function handleSubmit(formData: RemoteAssessmentFormValues) {
try {
await Promise.all([
mutatePresignedUrl({
userId,
portfolioId,
fileKey: assetListFileKey,
}),
mutatePresignedUrl({
userId,
portfolioId,
fileKey: valuationDataFileKey,
}),
]);
await triggerEngine(formData);
} catch (error) {
console.error("Error in submission process:", error);
}
}
return {
handleSubmit,
triggerEngine,
mutateUploadFile,
presignedUrlIsLoading,
presignedUrlIsError,
uploadFileIsLoading,
uploadFileIsError,
};
}
export default function RemoteAssessmentModal({
isOpen,
setIsOpen,
portfolioId,
scenarios,
}: {
isOpen: boolean;
setIsOpen: (open: boolean) => void;
portfolioId: string;
scenarios: ScenarioSelect[];
}) {
const NEW_SENTINEL = "__new__";
const [selectedScenario, setSelectedScenario] = useState<string | null>(null);
const { toast } = useToast();
const [showMeasures, setShowMeasures] = useState(false);
const scenarioOptions: Option[] = useMemo(
() => [
...scenarios.map((s) => ({
label: s.name || "",
value: String(s.id) || "",
disabled: false,
})),
],
[scenarios]
);
const form = useForm<RemoteAssessmentFormValues>({
resolver: zodResolver(RemoteAssessmentFormSchema),
mode: "onChange",
defaultValues: {
scenario: "",
housingType: "",
goal: "",
goalValue: "",
budget: undefined,
addressLineOne: "",
postcode: "",
uprn: undefined,
valuation: undefined,
propertyType: null,
builtForm: null,
measures: measuresList,
},
});
const { reset, setValue, formState } = form;
const { isValid, isSubmitting } = formState;
const measures = form.watch("measures");
const goal = form.watch("goal");
const {
handleSubmit: triggerAssessment,
presignedUrlIsLoading,
presignedUrlIsError,
} = useCreateRemoteAssessment({
portfolioId,
uprn: form.watch("uprn") ?? null,
addressLineOne: form.watch("addressLineOne"),
postcode: form.watch("postcode"),
valuation: form.watch("valuation") ?? null,
propertyType: form.watch("propertyType"),
builtForm: form.watch("builtForm"),
measures: measures,
scenarioId: selectedScenario,
});
const onSelectScenario = (opt: Option) => {
setSelectedScenario(opt.value);
if (opt.value === NEW_SENTINEL) {
reset({
...form.getValues(),
scenario: "",
housingType: "",
goal: "",
goalValue: "",
});
} else {
const picked = scenarios.find((s) => String(s.id) === opt.value);
if (!picked) return;
setValue("scenario", picked.name || "");
setValue("housingType", picked.housingType);
setValue("goal", picked.goal);
setValue("goalValue", picked.goalValue || "");
}
};
const onSubmit = form.handleSubmit(async (data) => {
await triggerAssessment(data);
form.reset();
setIsOpen(false);
toast({ title: "Remote assessment sent" });
});
return (
<Transition show={isOpen} as={Fragment}>
<Dialog
as="div"
className="fixed inset-0 z-10 overflow-y-auto"
onClose={() => setIsOpen(false)}
>
<div className="min-h-screen px-4 text-center">
<TransitionChild
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<DialogBackdrop className="fixed inset-0 bg-black/25" />
</TransitionChild>
{/* Spacer for centering */}
<span
className="inline-block h-screen align-middle"
aria-hidden="true"
>
&#8203;
</span>
<TransitionChild
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<DialogPanel className="inline-block w-full max-w-2xl p-6 my-8 overflow-visible text-left align-middle transition-all transform bg-white shadow-xl rounded-2xl">
<DialogTitle className="text-lg font-medium">
Remote Assessment Details
</DialogTitle>
<FormProvider {...form}>
<form onSubmit={onSubmit} className="space-y-6 mt-4">
{/* Scenario selector */}
<FormItem>
<FormLabel>Select scenario</FormLabel>
<FormControl>
<SelectScenarioDropdown
scenarios={scenarioOptions}
selectedValue={selectedScenario}
onSelect={onSelectScenario}
/>
</FormControl>
</FormItem>
{selectedScenario !== null && (
<>
<div className="grid grid-cols-2 gap-4">
{/* Scenario Name */}
<FormField
control={form.control}
name="scenario"
render={({ field }) => (
<FormItem>
<FormLabel className="text-gray-800">
Scenario Name
</FormLabel>
<FormControl>
<Input
{...field}
disabled={selectedScenario !== NEW_SENTINEL}
placeholder="Scenario name"
className="border-brandbrown focus-visible:ring-brandbrown focus-visible:border-brandbrown"
/>
</FormControl>
<FormMessage className="text-brandbrown" />
</FormItem>
)}
/>
{/* Housing Type */}
<FormField
control={form.control}
name="housingType"
render={({ field }) => (
<FormItem>
<FormLabel className="text-gray-800">
Housing Type
</FormLabel>
<FormControl>
{selectedScenario === NEW_SENTINEL ? (
<SelectDropdown
options={selecthousingTypeOptions}
selectedOption={field.value}
onSelectOption={(o) =>
field.onChange(o.value)
}
/>
) : (
<Input value={field.value} disabled />
)}
</FormControl>
<FormMessage className="text-brandbrown" />
</FormItem>
)}
/>
</div>
<div className="grid grid-cols-2 gap-4">
{/* Goal */}
<FormField
control={form.control}
name="goal"
render={({ field }) => (
<FormItem>
<FormLabel className="text-gray-800">
Goal
</FormLabel>
<FormControl>
{selectedScenario === NEW_SENTINEL ? (
<SelectDropdown
options={selectGoalOptions}
selectedOption={field.value}
onSelectOption={(o) =>
field.onChange(o.value)
}
/>
) : (
<Input value={field.value} disabled />
)}
</FormControl>
<FormMessage className="text-brandbrown" />
</FormItem>
)}
/>
{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>
)}
/>
)}
{/* ✅ 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">
{/* We mark budget as (optional) when the goal is increasing EPC*/}
Budget (£){" "}
{goal === "Increasing EPC" && (
<span className="text-sm text-gray-500">
(optional)
</span>
)}
</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>
</>
)}
<FormField
control={form.control}
name="addressLineOne"
render={({ field }) => (
<FormItem>
<FormLabel className="text-gray-800">Address</FormLabel>
<FormControl>
<Input
placeholder="Enter address"
{...field}
className="border-brandbrown focus-visible:ring-brandbrown focus-visible:border-brandbrown"
/>
</FormControl>
<FormMessage className="text-brandbrown" />
</FormItem>
)}
/>
<FormField
control={form.control}
name="postcode"
render={({ field }) => (
<FormItem>
<FormLabel className="text-gray-800">
Postcode
</FormLabel>
<FormControl>
<Input
placeholder="Enter postcode"
{...field}
className="border-brandbrown focus-visible:ring-brandbrown focus-visible:border-brandbrown"
/>
</FormControl>
<FormMessage className="text-brandbrown" />
</FormItem>
)}
/>
<FormField
control={form.control}
name="uprn"
render={({ field }) => (
<FormItem>
<FormLabel className="text-gray-800">UPRN</FormLabel>
<FormControl>
<Input
type="number"
placeholder="Enter UPRN"
{...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>
)}
/>
<FormField
control={form.control}
name="valuation"
render={({ field }) => (
<FormItem>
<FormLabel className="text-gray-800">
Valuation
</FormLabel>
<FormDescription>
The valuation can be found at{" "}
<a
href={`https://www.zoopla.co.uk/property/uprn/${form.watch(
"uprn"
)}/`}
target="_blank"
rel="noreferrer"
>
zoopla property page
</a>
</FormDescription>
<FormControl>
<Input
type="number"
placeholder="Enter valuation"
{...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 className="flex flex-col gap-2">
<p className="text-sm text-gray-600">
<strong>Optional:</strong> Property Type and Built Form
are only required if no EPC is available.
</p>
<div className="flex gap-4">
<FormField
control={form.control}
name="propertyType"
render={({ field }) => (
<FormItem className="w-full">
<FormLabel>Property Type</FormLabel>
<FormControl>
<SelectDropdown
options={propertyTypeOptions}
selectedOption={field.value || ""}
onSelectOption={(o) => field.onChange(o.value)}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="builtForm"
render={({ field }) => (
<FormItem className="w-full">
<FormLabel>Built Form</FormLabel>
<FormControl>
<SelectDropdown
options={builtFormOptions}
selectedOption={field.value || ""}
onSelectOption={(o) => field.onChange(o.value)}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</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 form={form} />}
</div>
<div className="flex justify-end gap-4">
<Button
type="button"
variant="outline"
onClick={() => setIsOpen(false)}
>
Cancel
</Button>
<Button
type="submit"
disabled={
!isValid || presignedUrlIsLoading || isSubmitting
}
>
{isSubmitting || presignedUrlIsLoading
? "Submitting…"
: "Submit"}
</Button>
</div>
{presignedUrlIsError && (
<p className="text-red-500">Error uploading files</p>
)}
</form>
</FormProvider>
</DialogPanel>
</TransitionChild>
</div>
</Dialog>
</Transition>
);
}
function setIsOpen(arg0: boolean) {
throw new Error("Function not implemented.");
}

View file

@ -15,10 +15,13 @@ import {
export default function AddressSearch({
onAddressSelect,
onPostcodeSelect,
postcode,
}: {
onAddressSelect?: (address: string | null) => void;
onAddressSelect: (address: string | null) => void;
onPostcodeSelect: (postcode: string) => void;
postcode: string;
}) {
const [postcode, setPostcode] = useState("");
const [addresses, setAddresses] = useState<string[]>([]);
const [selectedAddress, setSelectedAddress] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
@ -71,7 +74,7 @@ export default function AddressSearch({
<Input
placeholder="Enter postcode"
value={postcode}
onChange={(e) => setPostcode(e.target.value.toUpperCase())}
onChange={(e) => onPostcodeSelect(e.target.value.toUpperCase())}
className="text-lg"
/>
<Button

View file

@ -3,8 +3,10 @@
import { useState } from "react";
import AddressSearch from "./AddressSearch";
import ScenarioSetup from "./ScenarioSetup";
import RunAssessment from "./RunAssessment";
import { ScenarioSelect } from "@/app/db/schema/recommendations";
import { measuresList } from "@/app/db/schema/recommendations";
import useCreateRemoteAssessment from "./useCreateRemoteAssessment";
import { RemoteAssessmentFormValues } from "@/app/portfolio/[slug]/components/FormSchema";
export default function RemoteAssessmentClient({
portfolioId,
@ -14,6 +16,24 @@ export default function RemoteAssessmentClient({
scenarios: ScenarioSelect[];
}) {
const [selectedAddress, setSelectedAddress] = useState<string | null>(null);
const [selectedPostcode, setSelectedPostcode] = useState<string>("");
const { handleSubmit: submitAssessment, isUploading } =
useCreateRemoteAssessment({
portfolioId,
uprn: 1,
addressLineOne: selectedAddress || "",
postcode: selectedPostcode,
valuation: null,
propertyType: null,
builtForm: null,
measures: measuresList,
});
async function onSubmitRemoteAssessment(values: RemoteAssessmentFormValues) {
console.log("🚀 Submitting remote assessment:", values);
await submitAssessment(values);
}
return (
<div className="max-w-4xl mx-auto p-8 space-y-12">
@ -21,7 +41,11 @@ export default function RemoteAssessmentClient({
Remote Assessment
</h1>
<AddressSearch onAddressSelect={setSelectedAddress} />
<AddressSearch
onAddressSelect={setSelectedAddress}
onPostcodeSelect={setSelectedPostcode}
postcode={selectedPostcode}
/>
<div
className={`transition-all duration-300 ${
@ -34,6 +58,10 @@ export default function RemoteAssessmentClient({
portfolioId={portfolioId}
scenarios={scenarios}
disabled={!selectedAddress}
selectedAddress={selectedAddress}
selectedPostcode={selectedPostcode}
isSubmitting={isUploading}
onSubmitRemoteAssessment={onSubmitRemoteAssessment}
/>
</div>
@ -43,9 +71,7 @@ export default function RemoteAssessmentClient({
? "opacity-100 pointer-events-auto cursor-default"
: "opacity-50 pointer-events-none cursor-not-allowed"
}`}
>
<RunAssessment />
</div>
></div>
</div>
);
}

View file

@ -1,18 +0,0 @@
"use client";
import { Card } from "@/app/shadcn_components/ui/card";
import { Button } from "@/app/shadcn_components/ui/button";
import { Play } from "lucide-react";
export default function RunAssessment() {
return (
<Card className="p-6 opacity-50 pointer-events-none">
<h2 className="text-xl font-semibold text-brandbrown mb-4">
Step 3: Run Assessment
</h2>
<Button className="flex items-center gap-2 bg-brandblue text-white">
<Play className="w-4 h-4" /> Run Assessment
</Button>
</Card>
);
}

View file

@ -3,7 +3,7 @@
import { useState, useMemo } from "react";
import { useForm, FormProvider } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Play } from "lucide-react";
import {
FormField,
@ -41,12 +41,18 @@ export default function ScenarioSetup({
portfolioId,
scenarios,
disabled = false,
onSubmitScenario,
selectedAddress,
selectedPostcode,
isSubmitting,
onSubmitRemoteAssessment,
}: {
portfolioId: string;
scenarios: ScenarioSelect[];
disabled?: boolean;
onSubmitScenario?: (values: RemoteAssessmentFormValues) => void;
selectedAddress: string | null;
selectedPostcode: string;
isSubmitting: boolean;
onSubmitRemoteAssessment: (values: RemoteAssessmentFormValues) => void;
}) {
const NEW_SENTINEL = "__new__";
const [selectedScenario, setSelectedScenario] = useState<string | null>(null);
@ -63,10 +69,7 @@ export default function ScenarioSetup({
housingType: "Social",
addressLineOne: "",
postcode: "",
uprn: 0,
valuation: 0,
propertyType: null,
builtForm: null,
uprn: 1,
measures: measuresList,
},
});
@ -97,8 +100,10 @@ export default function ScenarioSetup({
goal: "",
goalValue: "",
budget: undefined,
valuation: undefined,
measures: measuresList, // all measures preselected
measures: measuresList,
addressLineOne: selectedAddress || "",
postcode: selectedPostcode || "",
uprn: 1, // TODO: Replace with real UPRN
});
} else {
form.reset({
@ -107,14 +112,17 @@ export default function ScenarioSetup({
goal: opt.goal || "",
goalValue: opt.goalValue || "",
budget: undefined,
valuation: undefined,
measures: measuresList,
addressLineOne: selectedAddress || "",
postcode: selectedPostcode || "",
uprn: 1,
});
}
}
function onSubmit(data: RemoteAssessmentFormValues) {
if (onSubmitScenario) onSubmitScenario(data);
console.log("form Data", data);
onSubmitRemoteAssessment(data);
console.log("Submitted scenario data:", data);
}
@ -268,14 +276,25 @@ export default function ScenarioSetup({
</button>
{showMeasures && <MeasuresCheckboxes form={form} />}
</div>
<div className="flex justify-end pt-2">
<div className="flex justify-end pt-6">
<Button
type="submit"
className="bg-brandbrown text-white hover:bg-hoverblue"
disabled={!formState.isValid}
type="button"
disabled={isSubmitting}
onClick={() =>
handleSubmit(onSubmit, (err) =>
console.log("Validation failed:", err)
)()
}
className="flex items-center gap-2 bg-brandblue text-white py-3 font-semibold hover:bg-hoverblue"
>
Save Scenario
{isSubmitting ? (
"Submitting..."
) : (
<>
<Play className="w-5 h-5" />
Run Assessment
</>
)}
</Button>
</div>
</>