mirror of
https://github.com/Hestia-Homes/assessment-model.git
synced 2026-06-08 11:37:25 +00:00
working on the form schema for upload files
This commit is contained in:
parent
be31116541
commit
4a291017d9
7 changed files with 423 additions and 417 deletions
|
|
@ -10,23 +10,41 @@ export async function POST(req: NextRequest) {
|
|||
return NextResponse.json({ error: "No file provided" }, { status: 400 });
|
||||
}
|
||||
|
||||
console.log("MADE IT HERE 0");
|
||||
console.log(file, file.constructor.name);
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
console.log("MADE IT HERE");
|
||||
const workbook = XLSX.read(arrayBuffer, { type: "array" });
|
||||
|
||||
const sheetNames = workbook.SheetNames;
|
||||
|
||||
if (sheetNames.length > 1) {
|
||||
return NextResponse.json(
|
||||
{ error: "Multiple sheets not allowed." },
|
||||
{ status: 400 }
|
||||
let isStandardised = false;
|
||||
|
||||
for (const sheetName of sheetNames) {
|
||||
if (sheetName !== "Standardised Asset List") continue;
|
||||
|
||||
const worksheet = workbook.Sheets[sheetName];
|
||||
const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 }) as (
|
||||
| string
|
||||
| number
|
||||
| null
|
||||
)[][];
|
||||
|
||||
const headers = jsonData.find(
|
||||
(row): row is string[] =>
|
||||
Array.isArray(row) &&
|
||||
row.length > 0 &&
|
||||
row.every((cell) => typeof cell === "string" || cell === null)
|
||||
);
|
||||
|
||||
if (headers?.includes("domna_property_id")) {
|
||||
isStandardised = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: We can check if we have a standardised asset list and if so, check which tabs have that option and give the user
|
||||
// the chance to select which tab they want to use
|
||||
|
||||
return NextResponse.json({ message: "Valid file." });
|
||||
return NextResponse.json({
|
||||
message: isStandardised
|
||||
? "Standardised Asset List format detected. Please select which tab to use."
|
||||
: "Valid file. No standardised format detected.",
|
||||
sheetNames: isStandardised ? sheetNames : undefined,
|
||||
isStandardised,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -102,6 +102,7 @@ export function Toolbar({ portfolioId, scenarios }: ToolbarProps) {
|
|||
isOpen={modalIsOpen}
|
||||
setIsOpen={setModalIsOpen}
|
||||
portfolioId={portfolioId}
|
||||
scenarios={scenarios}
|
||||
/>
|
||||
</NavigationMenu>
|
||||
);
|
||||
|
|
|
|||
18
src/app/portfolio/[slug]/components/FormSchema.tsx
Normal file
18
src/app/portfolio/[slug]/components/FormSchema.tsx
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import * as z from "zod";
|
||||
import { MeasureKeyEnum } from "@/app/db/schema/recommendations";
|
||||
|
||||
export const formSchema = z.object({
|
||||
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().nullable(),
|
||||
builtForm: z.string().nullable(),
|
||||
measures: z.array(MeasureKeyEnum).min(1, "Select at least one measure."),
|
||||
});
|
||||
|
||||
export type FormValues = z.infer<typeof formSchema>;
|
||||
|
|
@ -28,7 +28,6 @@ export function InputFile({
|
|||
|
||||
return (
|
||||
<div className="grid w-full max-w-sm items-center gap-1.5 text-sm font-semibold text-gray-600">
|
||||
<Label htmlFor="csv-uploader">Upload your file (CSV or Excel)</Label>
|
||||
<Input
|
||||
id="csv-uploader"
|
||||
type="file"
|
||||
|
|
|
|||
82
src/app/portfolio/[slug]/components/MeasuresCheckboxes.tsx
Normal file
82
src/app/portfolio/[slug]/components/MeasuresCheckboxes.tsx
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
"use client";
|
||||
|
||||
import { useForm } from "react-hook-form";
|
||||
import {
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
} from "@/app/shadcn_components/ui/form";
|
||||
import {
|
||||
measuresList,
|
||||
measuresDisplayLabels,
|
||||
} from "@/app/db/schema/recommendations";
|
||||
import { FormValues } from "./FormSchema";
|
||||
|
||||
function MeasuresCheckboxes({
|
||||
form,
|
||||
}: {
|
||||
form: ReturnType<typeof useForm<FormValues>>;
|
||||
}) {
|
||||
const { control } = form;
|
||||
const allMeasures = measuresList;
|
||||
return (
|
||||
<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 })
|
||||
}
|
||||
className="text-sm text-brandbrown underline"
|
||||
>
|
||||
Select All
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
form.setValue("measures", [], { 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"
|
||||
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>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MeasuresCheckboxes;
|
||||
|
|
@ -1,14 +1,13 @@
|
|||
"use client";
|
||||
|
||||
import { Dialog, Transition, Menu } from "@headlessui/react";
|
||||
import { Dialog, Transition } 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, useFormContext } from "react-hook-form";
|
||||
import { useForm, FormProvider } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import * as z from "zod";
|
||||
import {
|
||||
FormField,
|
||||
FormItem,
|
||||
|
|
@ -24,11 +23,9 @@ import {
|
|||
SelectScenarioDropdown,
|
||||
SelectDropdown,
|
||||
} from "./RemoteAssessmentDropdowns";
|
||||
import {
|
||||
measuresList,
|
||||
measuresDisplayLabels,
|
||||
MeasureKeyEnum,
|
||||
} from "@/app/db/schema/recommendations";
|
||||
import MeasuresCheckboxes from "./MeasuresCheckboxes";
|
||||
import { measuresList } from "@/app/db/schema/recommendations";
|
||||
import { formSchema, FormValues } from "./FormSchema";
|
||||
|
||||
type Option = {
|
||||
label: string;
|
||||
|
|
@ -161,22 +158,6 @@ interface EngineTriggerBody {
|
|||
scenario_id?: string | null;
|
||||
}
|
||||
|
||||
const formSchema = z.object({
|
||||
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().nullable(),
|
||||
builtForm: z.string().nullable(),
|
||||
measures: z.array(MeasureKeyEnum).min(1, "Select at least one measure."),
|
||||
});
|
||||
|
||||
type FormValues = z.infer<typeof formSchema>;
|
||||
|
||||
async function uploadCsvToS3({
|
||||
presignedUrl,
|
||||
file,
|
||||
|
|
@ -465,8 +446,6 @@ export default function RemoteAssessmentModal({
|
|||
[scenarios]
|
||||
);
|
||||
|
||||
console.log("Scenario options:", scenarioOptions);
|
||||
|
||||
const form = useForm({
|
||||
resolver: zodResolver(formSchema),
|
||||
mode: "onChange",
|
||||
|
|
@ -888,70 +867,3 @@ export default function RemoteAssessmentModal({
|
|||
function setIsOpen(arg0: boolean) {
|
||||
throw new Error("Function not implemented.");
|
||||
}
|
||||
|
||||
function MeasuresCheckboxes({
|
||||
form,
|
||||
}: {
|
||||
form: ReturnType<typeof useForm<FormValues>>;
|
||||
}) {
|
||||
const { control } = form;
|
||||
const allMeasures = measuresList;
|
||||
return (
|
||||
<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 })
|
||||
}
|
||||
className="text-sm text-brandbrown underline"
|
||||
>
|
||||
Select All
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
form.setValue("measures", [], { 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"
|
||||
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>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,99 +1,33 @@
|
|||
"use client";
|
||||
|
||||
import { Menu, Dialog, Transition } from "@headlessui/react";
|
||||
import { Fragment, useState } from "react";
|
||||
import { ChevronDownIcon } from "@heroicons/react/20/solid";
|
||||
import { Float } from "@headlessui-float/react";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { Fragment, useMemo, useState } from "react";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { InputFile } from "@/app/portfolio/[slug]/components/InputFile";
|
||||
import { SubmitPlan } from "@/app/portfolio/[slug]/components/SubmitPlan";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
|
||||
type Option = {
|
||||
label: string;
|
||||
value: string;
|
||||
disabled: boolean;
|
||||
};
|
||||
|
||||
type DropdownProps = {
|
||||
options: Option[];
|
||||
selectedOption: string;
|
||||
onSelectOption: (option: Option) => void;
|
||||
};
|
||||
|
||||
export function SelectDropdown({
|
||||
options,
|
||||
selectedOption,
|
||||
onSelectOption,
|
||||
}: DropdownProps) {
|
||||
return (
|
||||
<Menu as="div" className="relative inline-block text-left w-full">
|
||||
<Float>
|
||||
<Menu.Button className="inline-flex justify-center w-1/2 px-4 py-2 text-sm font-medium text-white bg-brandblue rounded-md focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75">
|
||||
{selectedOption || "Select an option"}
|
||||
<ChevronDownIcon
|
||||
className="ml-2 -mr-1 h-5 w-5 text-violet-200 hover:text-violet-100"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</Menu.Button>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-200"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="transition ease-in duration-150"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items className=" origin-bottom left-0 w-56 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none">
|
||||
{options.map((option) => (
|
||||
<Menu.Item key={option.value} disabled={option.disabled}>
|
||||
{({ active }) => (
|
||||
<button
|
||||
className={`${
|
||||
active
|
||||
? "bg-brandmidblue text-white w-full"
|
||||
: "text-gray-900 w-full"
|
||||
} group flex items-center px-4 py-2 text-sm `}
|
||||
onClick={() => onSelectOption(option)}
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
)}
|
||||
</Menu.Item>
|
||||
))}
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Float>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
|
||||
const hiddenInputArrows =
|
||||
"[-moz-appearance:_textfield] [&::-webkit-inner-spin-button]:m-0 [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:m-0 [&::-webkit-outer-spin-button]:appearance-none";
|
||||
import { ScenarioSelect } from "@/app/db/schema/recommendations";
|
||||
import MeasuresCheckboxes from "./MeasuresCheckboxes";
|
||||
import { useForm, FormProvider } from "react-hook-form";
|
||||
import {
|
||||
SelectDropdown,
|
||||
SelectScenarioDropdown,
|
||||
} from "@/app/portfolio/[slug]/components/RemoteAssessmentDropdowns";
|
||||
import { Button } from "@/app/shadcn_components/ui/button";
|
||||
import {
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormControl,
|
||||
} from "@/app/shadcn_components/ui/form";
|
||||
import { measuresList } from "@/app/db/schema/recommendations";
|
||||
|
||||
const selecthousingTypeOptions = [
|
||||
{
|
||||
label: "Social",
|
||||
value: "Social",
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
label: "Private",
|
||||
value: "Private",
|
||||
disabled: false,
|
||||
},
|
||||
{ label: "Social", value: "Social", disabled: false },
|
||||
{ label: "Private", value: "Private", disabled: false },
|
||||
];
|
||||
|
||||
const selectGoalOptions = [
|
||||
// {
|
||||
// label: "None",
|
||||
// value: "None",
|
||||
// },
|
||||
{
|
||||
label: "Increase EPC",
|
||||
value: "Increase EPC",
|
||||
disabled: false,
|
||||
},
|
||||
{ label: "Increase EPC", value: "Increase EPC", disabled: false },
|
||||
{
|
||||
label: "Reduce energy consumption",
|
||||
value: "Reduce energy consumption",
|
||||
|
|
@ -102,104 +36,103 @@ const selectGoalOptions = [
|
|||
];
|
||||
|
||||
const goalValueOptions = [
|
||||
{
|
||||
label: "C",
|
||||
value: "C",
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
label: "B",
|
||||
value: "B",
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
label: "A",
|
||||
value: "A",
|
||||
disabled: false,
|
||||
},
|
||||
{ label: "C", value: "C", disabled: false },
|
||||
{ label: "B", value: "B", disabled: false },
|
||||
{ label: "A", value: "A", disabled: false },
|
||||
];
|
||||
|
||||
export default function UploadCsvModal({
|
||||
isOpen = false,
|
||||
setIsOpen,
|
||||
portfolioId,
|
||||
}: {
|
||||
isOpen?: boolean;
|
||||
setIsOpen: (isOpen: boolean) => void;
|
||||
portfolioId: string;
|
||||
scenarios,
|
||||
}) {
|
||||
const [budget, setBudget] = useState<undefined | number>(undefined);
|
||||
const form = useForm({
|
||||
defaultValues: {
|
||||
scenario: "",
|
||||
housingType: "",
|
||||
goal: "",
|
||||
goalValue: "",
|
||||
budget: "",
|
||||
measures: measuresList,
|
||||
},
|
||||
});
|
||||
const [sheetNames, setSheetNames] = useState([]);
|
||||
const [selectedSheet, setSelectedSheet] = useState("");
|
||||
const [csvFile, setCsvFile] = useState(null);
|
||||
const [selectedScenario, setSelectedScenario] = useState(null);
|
||||
const [showMeasures, setShowMeasures] = useState(false);
|
||||
|
||||
const [buttonDisabled, setButtonDisabled] = useState(true);
|
||||
const [selectedGoal, setSelectedGoal] = useState<string>("");
|
||||
const [housingType, sethousingType] = useState<string>("");
|
||||
const [goalValue, setGoalValue] = useState<string>("");
|
||||
const scenarioOptions = useMemo(
|
||||
() =>
|
||||
scenarios.map((s) => ({
|
||||
label: s.name || "",
|
||||
value: String(s.id) || "",
|
||||
disabled: false,
|
||||
})),
|
||||
[scenarios]
|
||||
);
|
||||
|
||||
const [csvFile, setCsvFile] = useState<File | null>(null);
|
||||
|
||||
function handleBudgeChange(e: React.ChangeEvent<HTMLInputElement>) {
|
||||
setBudget(e.target.valueAsNumber);
|
||||
}
|
||||
|
||||
function handleButtonDisabled(
|
||||
goal?: string,
|
||||
housetype?: string,
|
||||
value?: string,
|
||||
file?: File | null
|
||||
) {
|
||||
// This function is defined as such to accomodate for the asynchonous nature of state setting
|
||||
// The first time this is called, the setState function will be run before this but the state value
|
||||
// will not have updated yet, so we need to pass in the value as an argument to check if the value has been updated
|
||||
if (
|
||||
(goal || selectedGoal) &&
|
||||
(housetype || housingType) &&
|
||||
(value || goalValue) &&
|
||||
(file || csvFile)
|
||||
) {
|
||||
setButtonDisabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
mutate: validateFile,
|
||||
isLoading: isValidating,
|
||||
error: validationError,
|
||||
} = useMutation(
|
||||
async (file: File) => {
|
||||
const { mutate: validateFile, isLoading: isValidating } = useMutation(
|
||||
async (file) => {
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
|
||||
const response = await fetch("/api/upload/validate", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const data = await response.json();
|
||||
throw new Error(data.error || "File validation failed");
|
||||
}
|
||||
|
||||
if (!response.ok) throw new Error("File validation failed");
|
||||
return response.json();
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
setButtonDisabled(false);
|
||||
onSuccess: (data) => {
|
||||
if (data.sheetNames) {
|
||||
setSheetNames(data.sheetNames);
|
||||
setSelectedSheet(data.sheetNames[0] || "");
|
||||
} else {
|
||||
setSheetNames([]);
|
||||
setSelectedSheet("");
|
||||
}
|
||||
},
|
||||
onError: (err) => {
|
||||
console.error(err);
|
||||
setButtonDisabled(true);
|
||||
onError: () => {
|
||||
setSheetNames([]);
|
||||
setSelectedSheet("");
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const onSelectScenario = (opt) => {
|
||||
setSelectedScenario(opt.value);
|
||||
if (opt.value === "__new__") {
|
||||
form.reset({
|
||||
...form.getValues(),
|
||||
scenario: "",
|
||||
housingType: "",
|
||||
goal: "",
|
||||
goalValue: "",
|
||||
});
|
||||
} else {
|
||||
const picked = scenarios.find((s) => String(s.id) === opt.value);
|
||||
if (!picked) return;
|
||||
form.setValue("scenario", picked.name || "");
|
||||
form.setValue("housingType", picked.housingType);
|
||||
form.setValue("goal", picked.goal);
|
||||
form.setValue("goalValue", picked.goalValue || "");
|
||||
}
|
||||
};
|
||||
|
||||
const onSubmit = form.handleSubmit((data) => {
|
||||
console.log({ ...data, csvFile, selectedSheet });
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
<Dialog
|
||||
as="div"
|
||||
className="relative z-10"
|
||||
onClose={() => setIsOpen(false)}
|
||||
>
|
||||
<Transition appear 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">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
|
|
@ -209,170 +142,213 @@ export default function UploadCsvModal({
|
|||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-black bg-opacity-25" />
|
||||
<Dialog.Overlay className="fixed inset-0 bg-black bg-opacity-25" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 overflow-y-auto">
|
||||
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
||||
<Transition.Child
|
||||
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"
|
||||
>
|
||||
<Dialog.Panel className="w-full max-w-screen-md transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all">
|
||||
<Dialog.Title
|
||||
as="h3"
|
||||
className="flex justify-center text-lg font-medium leading-6 text-brandblue mb-3"
|
||||
>
|
||||
Upload Properties
|
||||
</Dialog.Title>
|
||||
|
||||
<div className="text-gray-700 mb-7 mt-7 leading-relaxed tracking-wider">
|
||||
Upload a csv of properties and input some details about how
|
||||
you want to retrofit your properties
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col">
|
||||
<label
|
||||
htmlFor="csv-upload-budget"
|
||||
className="text-sm font-semibold text-gray-600 mb-1 relative leading-relaxed tracking-wider"
|
||||
<span
|
||||
className="inline-block h-screen align-middle"
|
||||
aria-hidden="true"
|
||||
>
|
||||
​
|
||||
</span>
|
||||
<Transition.Child
|
||||
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"
|
||||
>
|
||||
<div 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">
|
||||
<Dialog.Title className="text-lg font-medium">
|
||||
Upload Property Data
|
||||
</Dialog.Title>
|
||||
<FormProvider {...form}>
|
||||
<form onSubmit={onSubmit} className="flex flex-col gap-6 mt-6">
|
||||
<div className="flex flex-col gap-2">
|
||||
<InputFile
|
||||
onFileSelect={(file) => {
|
||||
setCsvFile(file);
|
||||
validateFile(file);
|
||||
}}
|
||||
isValidating={isValidating}
|
||||
/>
|
||||
<a
|
||||
href="/example_properties.csv"
|
||||
className="text-sm text-blue-600 underline w-fit"
|
||||
>
|
||||
Budget
|
||||
</label>
|
||||
<p className="text-sm text-gray-500 mb-2 leading-relaxed tracking-wider">
|
||||
If you don't set a budget, we will aim to minimise
|
||||
cost anyway
|
||||
</p>
|
||||
<div className="flex items-center border border-gray-200 rounded-md bg-gray-100">
|
||||
<span className="mx-2 text-gray-700">£</span>
|
||||
<input
|
||||
id="csv-upload-budget"
|
||||
type="number"
|
||||
placeholder="Set a budget"
|
||||
required
|
||||
value={budget}
|
||||
onChange={(e) => handleBudgeChange(e)}
|
||||
onKeyDown={(e) =>
|
||||
(e.key === "e" || e.key === "E") && e.preventDefault()
|
||||
}
|
||||
className="p-2 focus:outline-none bg-transparent w-full"
|
||||
View example CSV format
|
||||
</a>
|
||||
</div>
|
||||
{sheetNames.length > 0 && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium text-gray-700">
|
||||
Select the sheet/tab you want to use:
|
||||
</label>
|
||||
<SelectDropdown
|
||||
options={sheetNames.map((name) => ({
|
||||
label: name,
|
||||
value: name,
|
||||
}))}
|
||||
selectedOption={selectedSheet}
|
||||
onSelectOption={(opt) => setSelectedSheet(opt.value)}
|
||||
/>
|
||||
<p className="text-xs text-gray-500">
|
||||
Make sure the selected tab uses the Standardised Asset
|
||||
List format.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col mt-6">
|
||||
<label
|
||||
htmlFor="portfolio-name"
|
||||
className="text-sm font-semibold text-gray-600 mb-1 relative leading-relaxed tracking-wider"
|
||||
>
|
||||
Housing type
|
||||
<span className="text-red-500">*</span>
|
||||
</label>
|
||||
<SelectDropdown
|
||||
options={selecthousingTypeOptions}
|
||||
selectedOption={housingType}
|
||||
onSelectOption={(option) => {
|
||||
sethousingType(option.value);
|
||||
handleButtonDisabled(
|
||||
selectedGoal,
|
||||
option.value,
|
||||
goalValue,
|
||||
csvFile
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col mt-6">
|
||||
<label className="text-sm font-semibold text-gray-600 relative leading-relaxed tracking-wider">
|
||||
Select your goal
|
||||
<span className="text-red-500">*</span>
|
||||
</label>
|
||||
<SelectDropdown
|
||||
options={selectGoalOptions}
|
||||
selectedOption={selectedGoal}
|
||||
onSelectOption={(option) => {
|
||||
setSelectedGoal(option.value);
|
||||
handleButtonDisabled(
|
||||
option.value,
|
||||
housingType,
|
||||
goalValue,
|
||||
csvFile
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{selectedGoal === "Increase EPC" && (
|
||||
<div className="flex flex-col mt-6">
|
||||
<label
|
||||
htmlFor="csv-upload-epc"
|
||||
className="text-sm font-semibold text-gray-600 relative leading-relaxed tracking-wider"
|
||||
>
|
||||
Choose a target EPC value
|
||||
<span className="text-red-500">*</span>
|
||||
</label>
|
||||
<SelectDropdown
|
||||
options={goalValueOptions}
|
||||
selectedOption={goalValue}
|
||||
onSelectOption={(option) => {
|
||||
setGoalValue(option.value);
|
||||
handleButtonDisabled(
|
||||
selectedGoal,
|
||||
housingType,
|
||||
option.value,
|
||||
csvFile
|
||||
);
|
||||
}}
|
||||
)}
|
||||
<div className="flex flex-col gap-2">
|
||||
<FormItem>
|
||||
<FormLabel>Select scenario</FormLabel>
|
||||
<FormControl>
|
||||
<SelectScenarioDropdown
|
||||
scenarios={scenarioOptions}
|
||||
selectedValue={selectedScenario}
|
||||
onSelect={onSelectScenario}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col space-y-2 mt-7">
|
||||
<div className="text-sm font-semibold text-gray-600 relative leading-relaxed tracking-wider">
|
||||
Download the example csv and fill in the details for your
|
||||
properties
|
||||
</div>
|
||||
<div className="!text-blue-500 underline">
|
||||
<a href="/example_properties.csv">Download example CSV</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col space-y-2 mt-7">
|
||||
<div className="flex space-x-2">
|
||||
<InputFile
|
||||
onFileSelect={(file) => validateFile(file)}
|
||||
isValidating={isValidating}
|
||||
{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>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex justify-end gap-2">
|
||||
<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"
|
||||
/>
|
||||
)}
|
||||
</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"
|
||||
/>
|
||||
)}
|
||||
</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 className="border-t pt-4 mt-6">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex justify-center rounded-md border border-transparent bg-blue-100 px-4 py-2 text-sm font-medium text-blue-900 hover:bg-blue-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
||||
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>
|
||||
<SubmitPlan
|
||||
buttonDisabled={buttonDisabled}
|
||||
goal={selectedGoal}
|
||||
housingType={housingType}
|
||||
goalValue={goalValue}
|
||||
portfolioId={portfolioId}
|
||||
file={csvFile as File}
|
||||
/>
|
||||
</Button>
|
||||
<Button type="submit" disabled={!csvFile}>
|
||||
Submit
|
||||
</Button>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</form>
|
||||
</FormProvider>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
</>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue