diff --git a/src/app/api/upload/validate/route.ts b/src/app/api/upload/validate/route.ts index c0c2f5d..bb69a3b 100644 --- a/src/app/api/upload/validate/route.ts +++ b/src/app/api/upload/validate/route.ts @@ -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, + }); } diff --git a/src/app/components/portfolio/Toolbar.tsx b/src/app/components/portfolio/Toolbar.tsx index 1ba07b6..1f3b811 100644 --- a/src/app/components/portfolio/Toolbar.tsx +++ b/src/app/components/portfolio/Toolbar.tsx @@ -102,6 +102,7 @@ export function Toolbar({ portfolioId, scenarios }: ToolbarProps) { isOpen={modalIsOpen} setIsOpen={setModalIsOpen} portfolioId={portfolioId} + scenarios={scenarios} /> ); diff --git a/src/app/portfolio/[slug]/components/FormSchema.tsx b/src/app/portfolio/[slug]/components/FormSchema.tsx new file mode 100644 index 0000000..566e3a4 --- /dev/null +++ b/src/app/portfolio/[slug]/components/FormSchema.tsx @@ -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; diff --git a/src/app/portfolio/[slug]/components/InputFile.tsx b/src/app/portfolio/[slug]/components/InputFile.tsx index 49e3b20..80ff585 100644 --- a/src/app/portfolio/[slug]/components/InputFile.tsx +++ b/src/app/portfolio/[slug]/components/InputFile.tsx @@ -28,7 +28,6 @@ export function InputFile({ return (
- >; +}) { + const { control } = form; + const allMeasures = measuresList; + return ( +
+
+ + +
+ + {/* Measures grid */} +
+ {measuresList.map((measure) => ( + ( + + { + 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" + /> + + {measuresDisplayLabels[measure]} + + + )} + /> + ))} +
+
+ ); +} + +export default MeasuresCheckboxes; diff --git a/src/app/portfolio/[slug]/components/RemoteAssessmentModal.tsx b/src/app/portfolio/[slug]/components/RemoteAssessmentModal.tsx index f50ad99..faba50b 100644 --- a/src/app/portfolio/[slug]/components/RemoteAssessmentModal.tsx +++ b/src/app/portfolio/[slug]/components/RemoteAssessmentModal.tsx @@ -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; - 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>; -}) { - const { control } = form; - const allMeasures = measuresList; - return ( -
-
- - -
- - {/* Measures grid */} -
- {measuresList.map((measure) => ( - ( - - { - 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" - /> - - {measuresDisplayLabels[measure]} - - - )} - /> - ))} -
-
- ); -} diff --git a/src/app/portfolio/[slug]/components/UploadCsvModal.tsx b/src/app/portfolio/[slug]/components/UploadCsvModal.tsx index 0e3c7c1..0acc829 100644 --- a/src/app/portfolio/[slug]/components/UploadCsvModal.tsx +++ b/src/app/portfolio/[slug]/components/UploadCsvModal.tsx @@ -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 ( - - - - {selectedOption || "Select an option"} - - - - {options.map((option) => ( - - {({ active }) => ( - - )} - - ))} - - - - - ); -} - -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); + 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(""); - const [housingType, sethousingType] = useState(""); - const [goalValue, setGoalValue] = useState(""); + const scenarioOptions = useMemo( + () => + scenarios.map((s) => ({ + label: s.name || "", + value: String(s.id) || "", + disabled: false, + })), + [scenarios] + ); - const [csvFile, setCsvFile] = useState(null); - - function handleBudgeChange(e: React.ChangeEvent) { - 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 ( - <> - - setIsOpen(false)} - > + + setIsOpen(false)} + > +
-
+ - -
-
- - - - Upload Properties - - -
- Upload a csv of properties and input some details about how - you want to retrofit your properties -
- -
-
-
- + +
+ + ); }