mirror of
https://github.com/Hestia-Homes/assessment-model.git
synced 2026-06-30 12:55:02 +00:00
debugging fileupload modal
This commit is contained in:
parent
c34969a941
commit
07b6fe08ea
4 changed files with 498 additions and 168 deletions
|
|
@ -1,7 +1,14 @@
|
|||
// formSchemas.ts
|
||||
import * as z from "zod";
|
||||
import { MeasureKeyEnum } from "@/app/db/schema/recommendations";
|
||||
|
||||
export const formSchema = z.object({
|
||||
export const baseFormSchema = z.object({
|
||||
measures: z.array(MeasureKeyEnum).min(1, "At least one measure is required"),
|
||||
});
|
||||
|
||||
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),
|
||||
|
|
@ -12,7 +19,26 @@ export const formSchema = z.object({
|
|||
valuation: z.number().min(1),
|
||||
propertyType: z.string().nullable(),
|
||||
builtForm: z.string().nullable(),
|
||||
measures: z.array(MeasureKeyEnum).min(1, "Select at least one measure."),
|
||||
});
|
||||
|
||||
export type FormValues = z.infer<typeof formSchema>;
|
||||
export type RemoteAssessmentFormValues = z.infer<
|
||||
typeof RemoteAssessmentFormSchema
|
||||
>;
|
||||
|
||||
export const uploadCsvSchema = baseFormSchema.extend({
|
||||
scenario: z.string().min(1),
|
||||
goal: z.string().min(1),
|
||||
goalValue: z.string().min(1),
|
||||
housingType: z.string(),
|
||||
ashpCop: z.preprocess((val) => {
|
||||
if (val === "" || val === undefined) return undefined;
|
||||
return Number(val);
|
||||
}, z.number().min(0.1)),
|
||||
budget: z.preprocess((val) => {
|
||||
if (val === "" || val === undefined) return undefined;
|
||||
if (val === null) return null;
|
||||
return Number(val);
|
||||
}, z.union([z.number(), z.null()]).optional()),
|
||||
});
|
||||
|
||||
export type UploadCsvFormValues = z.infer<typeof uploadCsvSchema>;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { useForm } from "react-hook-form";
|
||||
import { UseFormReturn, Path } from "react-hook-form";
|
||||
import {
|
||||
FormField,
|
||||
FormItem,
|
||||
|
|
@ -10,22 +10,26 @@ import {
|
|||
measuresList,
|
||||
measuresDisplayLabels,
|
||||
} from "@/app/db/schema/recommendations";
|
||||
import { FormValues } from "./FormSchema";
|
||||
import { BaseFormValues } from "./FormSchema";
|
||||
|
||||
function MeasuresCheckboxes({
|
||||
type MeasuresCheckboxesProps<T extends BaseFormValues> = {
|
||||
form: UseFormReturn<T>;
|
||||
name?: Path<T>; // Optional override for custom field key
|
||||
};
|
||||
|
||||
function MeasuresCheckboxes<T extends BaseFormValues>({
|
||||
form,
|
||||
}: {
|
||||
form: ReturnType<typeof useForm<FormValues>>;
|
||||
}) {
|
||||
const { control } = form;
|
||||
const allMeasures = measuresList;
|
||||
name = "measures" as Path<T>,
|
||||
}: MeasuresCheckboxesProps<T>) {
|
||||
const { control, setValue } = form;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 mt-4 ">
|
||||
<div className="flex flex-col gap-4 mt-4">
|
||||
<div className="flex justify-start gap-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
form.setValue("measures", allMeasures, { shouldValidate: true })
|
||||
setValue(name, measuresList as any, { shouldValidate: true })
|
||||
}
|
||||
className="text-sm text-brandbrown underline"
|
||||
>
|
||||
|
|
@ -33,36 +37,29 @@ function MeasuresCheckboxes({
|
|||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
form.setValue("measures", [], { shouldValidate: true })
|
||||
}
|
||||
onClick={() => setValue(name, [] as any, { shouldValidate: true })}
|
||||
className="text-sm text-brandbrown underline"
|
||||
>
|
||||
Untick All
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Measures grid */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{measuresList.map((measure) => (
|
||||
<FormField
|
||||
key={measure}
|
||||
control={control}
|
||||
name="measures"
|
||||
name={name}
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex items-center space-x-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
value={measure}
|
||||
checked={field.value?.includes(measure) ?? true}
|
||||
checked={field.value?.includes(measure) ?? false}
|
||||
onChange={(e) => {
|
||||
const checked = e.target.checked;
|
||||
const current = new Set(field.value ?? []);
|
||||
if (checked) {
|
||||
current.add(measure);
|
||||
} else {
|
||||
current.delete(measure);
|
||||
}
|
||||
checked ? current.add(measure) : current.delete(measure);
|
||||
field.onChange(Array.from(current));
|
||||
}}
|
||||
className="h-4 w-4"
|
||||
|
|
|
|||
|
|
@ -25,7 +25,10 @@ import {
|
|||
} from "./RemoteAssessmentDropdowns";
|
||||
import MeasuresCheckboxes from "./MeasuresCheckboxes";
|
||||
import { measuresList } from "@/app/db/schema/recommendations";
|
||||
import { formSchema, FormValues } from "./FormSchema";
|
||||
import {
|
||||
RemoteAssessmentFormSchema,
|
||||
RemoteAssessmentFormValues,
|
||||
} from "./FormSchema";
|
||||
|
||||
type Option = {
|
||||
label: string;
|
||||
|
|
@ -348,7 +351,7 @@ function useCreateRemoteAssessment({
|
|||
},
|
||||
});
|
||||
|
||||
async function triggerEngine(data: FormValues) {
|
||||
async function triggerEngine(data: RemoteAssessmentFormValues) {
|
||||
try {
|
||||
const triggerBody: EngineTriggerBody = {
|
||||
scenario_id: scenarioId === "__new__" ? null : scenarioId,
|
||||
|
|
@ -387,7 +390,7 @@ function useCreateRemoteAssessment({
|
|||
}
|
||||
}
|
||||
|
||||
async function handleSubmit(formData: FormValues) {
|
||||
async function handleSubmit(formData: RemoteAssessmentFormValues) {
|
||||
try {
|
||||
await Promise.all([
|
||||
mutatePresignedUrl({
|
||||
|
|
@ -447,7 +450,7 @@ export default function RemoteAssessmentModal({
|
|||
);
|
||||
|
||||
const form = useForm({
|
||||
resolver: zodResolver(formSchema),
|
||||
resolver: zodResolver(RemoteAssessmentFormSchema),
|
||||
mode: "onChange",
|
||||
defaultValues: {
|
||||
scenario: "",
|
||||
|
|
@ -456,8 +459,8 @@ export default function RemoteAssessmentModal({
|
|||
goalValue: "",
|
||||
addressLineOne: "",
|
||||
postcode: "",
|
||||
uprn: 0,
|
||||
valuation: 0,
|
||||
uprn: null,
|
||||
valuation: null,
|
||||
propertyType: null as string | null,
|
||||
builtForm: null as string | null,
|
||||
measures: measuresList,
|
||||
|
|
|
|||
|
|
@ -18,8 +18,19 @@ import {
|
|||
FormItem,
|
||||
FormLabel,
|
||||
FormControl,
|
||||
FormMessage,
|
||||
} from "@/app/shadcn_components/ui/form";
|
||||
import { Input } from "@/app/shadcn_components/ui/input";
|
||||
import { measuresList } from "@/app/db/schema/recommendations";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import {
|
||||
uploadCsvSchema,
|
||||
UploadCsvFormValues,
|
||||
} from "@/app/portfolio/[slug]/components/FormSchema";
|
||||
|
||||
const NEW_SENTINEL = "__new__";
|
||||
|
||||
const selecthousingTypeOptions = [
|
||||
{ label: "Social", value: "Social", disabled: false },
|
||||
|
|
@ -27,12 +38,12 @@ const selecthousingTypeOptions = [
|
|||
];
|
||||
|
||||
const selectGoalOptions = [
|
||||
{ label: "Increase EPC", value: "Increase EPC", disabled: false },
|
||||
{
|
||||
label: "Reduce energy consumption",
|
||||
value: "Reduce energy consumption",
|
||||
disabled: false,
|
||||
},
|
||||
{ label: "Increasing EPC", value: "Increasing EPC", disabled: false },
|
||||
// {
|
||||
// label: "Reduce energy consumption",
|
||||
// value: "Reduce energy consumption",
|
||||
// disabled: false,
|
||||
// },
|
||||
];
|
||||
|
||||
const goalValueOptions = [
|
||||
|
|
@ -41,26 +52,165 @@ const goalValueOptions = [
|
|||
{ label: "A", value: "A", disabled: false },
|
||||
];
|
||||
|
||||
function generateS3Key(userId: string, portfolioId: string) {
|
||||
const timestamp = new Date().toISOString().replace(/[:.-]/g, "");
|
||||
return `${userId}/${portfolioId}/${timestamp}/asset_list.csv`;
|
||||
}
|
||||
|
||||
async function uploadCsvToS3({
|
||||
presignedUrl,
|
||||
file,
|
||||
}: {
|
||||
presignedUrl: string;
|
||||
file: File;
|
||||
}) {
|
||||
const response = await fetch(presignedUrl, {
|
||||
method: "PUT",
|
||||
body: file,
|
||||
headers: { "Content-Type": "text/csv" },
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error("Upload failed");
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
async function generatePresignedUrl({
|
||||
userId,
|
||||
portfolioId,
|
||||
fileKey,
|
||||
}: {
|
||||
userId: string;
|
||||
portfolioId: string;
|
||||
fileKey: string;
|
||||
}) {
|
||||
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();
|
||||
return { url: data.url, fileKey };
|
||||
}
|
||||
export function useUploadCsvPlan({
|
||||
file,
|
||||
portfolioId,
|
||||
goal,
|
||||
goalValue,
|
||||
housingType,
|
||||
scenarioName,
|
||||
selectedSheet,
|
||||
budget,
|
||||
ashpCop,
|
||||
measures,
|
||||
onSuccessRedirect,
|
||||
}: {
|
||||
file: File;
|
||||
portfolioId: string;
|
||||
goal: string;
|
||||
goalValue: string;
|
||||
budget: number | null;
|
||||
ashpCop: number;
|
||||
housingType: string;
|
||||
scenarioName: string;
|
||||
selectedSheet: string;
|
||||
measures: (typeof measuresList)[number][];
|
||||
onSuccessRedirect: (path: string) => void;
|
||||
}) {
|
||||
const session = useSession();
|
||||
const userId = String(session.data?.user.dbId);
|
||||
|
||||
const fileKey = useMemo(
|
||||
() => generateS3Key(userId, portfolioId),
|
||||
[userId, portfolioId]
|
||||
);
|
||||
|
||||
const { mutateAsync: uploadFileToS3, isLoading: isUploadLoading } =
|
||||
useMutation(uploadCsvToS3);
|
||||
|
||||
const { mutateAsync: getPresignedUrl, isLoading: isGeneratingUrlLoading } =
|
||||
useMutation(generatePresignedUrl);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const { url } = await getPresignedUrl({ userId, portfolioId, fileKey });
|
||||
|
||||
await uploadFileToS3({ presignedUrl: url, file });
|
||||
|
||||
const body = {
|
||||
scenario_id: scenarioName === NEW_SENTINEL ? null : scenarioName,
|
||||
portfolio_id: portfolioId,
|
||||
housing_type: housingType,
|
||||
goal: goal,
|
||||
goal_value: goalValue,
|
||||
trigger_file_path: fileKey,
|
||||
valuation_file_path: "", // Not used in this context
|
||||
already_installed_file_path: "",
|
||||
patches_file_path: "",
|
||||
non_invasive_recommendations_file_path: "",
|
||||
multi_plan: true,
|
||||
budget: budget,
|
||||
scenario_name: scenarioName,
|
||||
inclusions: measures,
|
||||
event_type: "remote_assessment",
|
||||
sheet_name: selectedSheet,
|
||||
ashp_cop: ashpCop,
|
||||
};
|
||||
|
||||
const triggerRes = await fetch("/api/plan/trigger", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (!triggerRes.ok) throw new Error("Failed to trigger engine");
|
||||
|
||||
onSuccessRedirect(`/portfolio/${portfolioId}/plan-loading`);
|
||||
} catch (err) {
|
||||
console.error("Upload or trigger failed", err);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
handleSubmit,
|
||||
isUploadLoading,
|
||||
isGeneratingUrlLoading,
|
||||
};
|
||||
}
|
||||
|
||||
export default function UploadCsvModal({
|
||||
isOpen = false,
|
||||
setIsOpen,
|
||||
portfolioId,
|
||||
scenarios,
|
||||
}: {
|
||||
isOpen?: boolean;
|
||||
setIsOpen: (open: boolean) => void;
|
||||
portfolioId: string;
|
||||
scenarios: ScenarioSelect[];
|
||||
}) {
|
||||
const form = useForm({
|
||||
const form = useForm<UploadCsvFormValues>({
|
||||
resolver: zodResolver(uploadCsvSchema),
|
||||
shouldUnregister: false,
|
||||
mode: "onChange",
|
||||
defaultValues: {
|
||||
scenario: "",
|
||||
housingType: "",
|
||||
goal: "",
|
||||
goalValue: "",
|
||||
budget: "",
|
||||
budget: null,
|
||||
measures: measuresList,
|
||||
},
|
||||
});
|
||||
const [sheetNames, setSheetNames] = useState([]);
|
||||
const { formState } = form;
|
||||
const { isValid, isSubmitting } = formState;
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const [sheetNames, setSheetNames] = useState<string[]>([]);
|
||||
const [selectedSheet, setSelectedSheet] = useState("");
|
||||
const [csvFile, setCsvFile] = useState(null);
|
||||
const [selectedScenario, setSelectedScenario] = useState(null);
|
||||
const [csvFile, setCsvFile] = useState<File | null>(null);
|
||||
const [selectedScenario, setSelectedScenario] = useState<string | null>(null);
|
||||
const [showMeasures, setShowMeasures] = useState(false);
|
||||
|
||||
const scenarioOptions = useMemo(
|
||||
|
|
@ -74,7 +224,7 @@ export default function UploadCsvModal({
|
|||
);
|
||||
|
||||
const { mutate: validateFile, isLoading: isValidating } = useMutation(
|
||||
async (file) => {
|
||||
async (file: File) => {
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
const response = await fetch("/api/upload/validate", {
|
||||
|
|
@ -101,15 +251,16 @@ export default function UploadCsvModal({
|
|||
}
|
||||
);
|
||||
|
||||
const onSelectScenario = (opt) => {
|
||||
const onSelectScenario = (opt: { value: string }) => {
|
||||
setSelectedScenario(opt.value);
|
||||
if (opt.value === "__new__") {
|
||||
if (opt.value === NEW_SENTINEL) {
|
||||
form.reset({
|
||||
...form.getValues(),
|
||||
scenario: "",
|
||||
housingType: "",
|
||||
goal: "",
|
||||
goalValue: "",
|
||||
ashpCop: 2.8, // Default COP value - eventually to come from db
|
||||
});
|
||||
} else {
|
||||
const picked = scenarios.find((s) => String(s.id) === opt.value);
|
||||
|
|
@ -121,10 +272,33 @@ export default function UploadCsvModal({
|
|||
}
|
||||
};
|
||||
|
||||
const onSubmit = form.handleSubmit((data) => {
|
||||
console.log({ ...data, csvFile, selectedSheet });
|
||||
const uploadCsvPlan = useUploadCsvPlan({
|
||||
file: csvFile!,
|
||||
goal: form.watch("goal"),
|
||||
goalValue: form.watch("goalValue"),
|
||||
housingType: form.watch("housingType"),
|
||||
budget: form.watch("budget") || null,
|
||||
ashpCop: form.watch("ashpCop"),
|
||||
scenarioName: form.watch("scenario"),
|
||||
measures: form.watch("measures"),
|
||||
portfolioId,
|
||||
selectedSheet,
|
||||
onSuccessRedirect: (path) => router.push(path),
|
||||
});
|
||||
|
||||
const onSubmit = form.handleSubmit(async () => {
|
||||
if (!csvFile || !selectedSheet) return;
|
||||
await uploadCsvPlan.handleSubmit();
|
||||
form.reset();
|
||||
});
|
||||
|
||||
console.log("csvFile:", csvFile);
|
||||
console.log("isValidating:", isValidating);
|
||||
console.log("isValid:", isValid);
|
||||
console.log("isSubmitting:", isSubmitting);
|
||||
console.log("Raw form values", form.getValues());
|
||||
console.log("Zod parse result", uploadCsvSchema.safeParse(form.getValues()));
|
||||
|
||||
return (
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
<Dialog
|
||||
|
|
@ -164,7 +338,11 @@ export default function UploadCsvModal({
|
|||
Upload Property Data
|
||||
</Dialog.Title>
|
||||
<FormProvider {...form}>
|
||||
<form onSubmit={onSubmit} className="flex flex-col gap-6 mt-6">
|
||||
<form
|
||||
onSubmit={onSubmit}
|
||||
className="flex flex-col flex-grow gap-6 mt-6"
|
||||
>
|
||||
{/* File Upload */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<InputFile
|
||||
onFileSelect={(file) => {
|
||||
|
|
@ -180,6 +358,8 @@ export default function UploadCsvModal({
|
|||
View example CSV format
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* Sheet selection */}
|
||||
{sheetNames.length > 0 && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium text-gray-700">
|
||||
|
|
@ -194,132 +374,241 @@ export default function UploadCsvModal({
|
|||
onSelectOption={(opt) => setSelectedSheet(opt.value)}
|
||||
/>
|
||||
<p className="text-xs text-gray-500">
|
||||
Make sure the selected tab uses the Standardised Asset
|
||||
List format.
|
||||
Ensure this sheet uses the Standardised Asset List
|
||||
format.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col gap-2">
|
||||
<FormItem>
|
||||
<FormLabel>Select scenario</FormLabel>
|
||||
<FormControl>
|
||||
<SelectScenarioDropdown
|
||||
scenarios={scenarioOptions}
|
||||
selectedValue={selectedScenario}
|
||||
onSelect={onSelectScenario}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
</div>
|
||||
|
||||
{/* Scenario selection */}
|
||||
<FormItem>
|
||||
<FormLabel>Select scenario</FormLabel>
|
||||
<FormControl>
|
||||
<SelectScenarioDropdown
|
||||
scenarios={scenarioOptions}
|
||||
selectedValue={selectedScenario}
|
||||
onSelect={onSelectScenario}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
|
||||
{/* Scenario definition inputs */}
|
||||
{selectedScenario !== null && (
|
||||
<>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="scenario"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="text-gray-800">
|
||||
Scenario Name
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<input
|
||||
{...field}
|
||||
disabled={selectedScenario !== "__new__"}
|
||||
placeholder="Scenario name"
|
||||
className="w-full rounded-lg border border-brandbrown bg-white px-4 py-2 text-sm shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brandbrown"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormItem>
|
||||
<FormLabel>Housing type</FormLabel>
|
||||
<FormControl>
|
||||
{selectedScenario === "__new__" ? (
|
||||
<SelectDropdown
|
||||
options={selecthousingTypeOptions}
|
||||
selectedOption={form.watch("housingType")}
|
||||
onSelectOption={(opt) =>
|
||||
form.setValue("housingType", opt.value)
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
value={form.watch("housingType")}
|
||||
disabled
|
||||
className="w-full rounded-lg border border-brandbrown bg-gray-100 px-4 py-2 text-sm"
|
||||
/>
|
||||
<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>
|
||||
)}
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
<FormItem>
|
||||
<FormLabel>Goal</FormLabel>
|
||||
<FormControl>
|
||||
{selectedScenario === "__new__" ? (
|
||||
<SelectDropdown
|
||||
options={selectGoalOptions}
|
||||
selectedOption={form.watch("goal")}
|
||||
onSelectOption={(opt) =>
|
||||
form.setValue("goal", opt.value)
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
value={form.watch("goal")}
|
||||
disabled
|
||||
className="w-full rounded-lg border border-brandbrown bg-gray-100 px-4 py-2 text-sm"
|
||||
/>
|
||||
/>
|
||||
|
||||
{/* 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>
|
||||
)}
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
{form.watch("goal") === "Increase EPC" && (
|
||||
<FormItem>
|
||||
<FormLabel>Choose a target EPC value</FormLabel>
|
||||
<FormControl>
|
||||
{selectedScenario === "__new__" ? (
|
||||
<SelectDropdown
|
||||
options={goalValueOptions}
|
||||
selectedOption={form.watch("goalValue")}
|
||||
onSelectOption={(opt) =>
|
||||
form.setValue("goalValue", opt.value)
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
value={form.watch("goalValue")}
|
||||
disabled
|
||||
className="w-full rounded-lg border border-brandbrown bg-gray-100 px-4 py-2 text-sm"
|
||||
/>
|
||||
)}
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="budget"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="text-gray-800">
|
||||
Budget (optional)
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<input
|
||||
type="number"
|
||||
placeholder="Set a budget"
|
||||
{...field}
|
||||
disabled={selectedScenario !== "__new__"}
|
||||
className={`w-full rounded-lg border ${
|
||||
selectedScenario !== "__new__"
|
||||
? "bg-gray-100"
|
||||
: "bg-white"
|
||||
} px-4 py-2 text-sm shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brandbrown`}
|
||||
/>
|
||||
</FormControl>
|
||||
</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 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 />
|
||||
)}
|
||||
</FormControl>
|
||||
<FormMessage className="text-brandbrown" />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<pre>
|
||||
{JSON.stringify(form.formState.errors, null, 2)}
|
||||
</pre>
|
||||
<pre>{JSON.stringify(form.watch(), null, 2)}</pre>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{/* Heat Pump COP */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="ashpCop"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="text-gray-800">
|
||||
Heat Pump COP
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
step="0.1"
|
||||
placeholder="e.g. 2.8"
|
||||
{...field}
|
||||
value={
|
||||
field.value === undefined ? "" : field.value
|
||||
}
|
||||
disabled={selectedScenario !== NEW_SENTINEL}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage className="text-brandbrown" />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Budget */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="budget"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="text-gray-800">
|
||||
Budget (£)
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
step="1"
|
||||
placeholder="Optional"
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
disabled={selectedScenario !== NEW_SENTINEL}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage className="text-brandbrown" />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* <FormField
|
||||
control={form.control}
|
||||
name="cop"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Heat pump COP</FormLabel>
|
||||
<FormControl>
|
||||
<input
|
||||
type="number"
|
||||
step="0.1"
|
||||
{...field}
|
||||
disabled={selectedScenario !== "__new__"}
|
||||
className={`w-full rounded-lg border ${
|
||||
selectedScenario !== "__new__"
|
||||
? "bg-gray-100"
|
||||
: "bg-white"
|
||||
} border-brandbrown px-4 py-2 text-sm shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brandbrown`}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/> */}
|
||||
|
||||
{/* <FormField
|
||||
control={form.control}
|
||||
name="budget"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Budget (optional)</FormLabel>
|
||||
<FormControl>
|
||||
<input
|
||||
type="text"
|
||||
inputMode="decimal"
|
||||
value={field.value ?? ""}
|
||||
onChange={(e) =>
|
||||
field.onChange(
|
||||
e.target.value === ""
|
||||
? null
|
||||
: parseFloat(e.target.value)
|
||||
)
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/> */}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Measures */}
|
||||
<div className="border-t pt-4 mt-6">
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -329,9 +618,12 @@ export default function UploadCsvModal({
|
|||
<span>Measures</span>
|
||||
<span>{showMeasures ? "−" : "+"}</span>
|
||||
</button>
|
||||
{showMeasures && <MeasuresCheckboxes form={form} />}
|
||||
<div className={showMeasures ? "" : "hidden"}>
|
||||
<MeasuresCheckboxes form={form} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end gap-4">
|
||||
|
||||
<div className="mt-auto pt-4 flex justify-end gap-4">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
|
|
@ -339,8 +631,20 @@ export default function UploadCsvModal({
|
|||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={!csvFile}>
|
||||
Submit
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={
|
||||
!csvFile || isValidating || !isValid || isSubmitting
|
||||
}
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="loader h-4 w-4 border-2 border-t-transparent border-white rounded-full animate-spin" />
|
||||
Submitting...
|
||||
</div>
|
||||
) : (
|
||||
"Submit"
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue