mirror of
https://github.com/Hestia-Homes/assessment-model.git
synced 2026-06-08 11:37:25 +00:00
followed the train of functions that uploads the asset list and fixed all my assessment spelling errors
This commit is contained in:
parent
eaa9d82384
commit
b5fda31903
3 changed files with 16 additions and 498 deletions
|
|
@ -39,13 +39,13 @@ ListItem.displayName = "ListItem";
|
|||
export default function AddNewDropDown({
|
||||
isUploadCsvOpen,
|
||||
setIsUploadCsvOpen,
|
||||
isRemoteAssesmentOpen,
|
||||
setIsRemoteAssesmentOpen,
|
||||
isRemoteAssessmentOpen,
|
||||
setIsRemoteAssessmentOpen,
|
||||
}: {
|
||||
isUploadCsvOpen: boolean;
|
||||
setIsUploadCsvOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
isRemoteAssesmentOpen: boolean;
|
||||
setIsRemoteAssesmentOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
isRemoteAssessmentOpen: boolean;
|
||||
setIsRemoteAssessmentOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}) {
|
||||
function handleCickAddUnit() {
|
||||
console.log("Add unit");
|
||||
|
|
@ -55,8 +55,8 @@ export default function AddNewDropDown({
|
|||
setIsUploadCsvOpen(!isUploadCsvOpen);
|
||||
}
|
||||
|
||||
function handleClickRemoteAssesment() {
|
||||
setIsRemoteAssesmentOpen(!isRemoteAssesmentOpen);
|
||||
function handleClickRemoteAssessment() {
|
||||
setIsRemoteAssessmentOpen(!isRemoteAssessmentOpen);
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -71,11 +71,11 @@ export default function AddNewDropDown({
|
|||
<PlusIcon className="h-4 w-4 mr-2" /> Add Unit
|
||||
</div>
|
||||
</ListItem>
|
||||
<ListItem onClick={handleClickRemoteAssesment}>
|
||||
<ListItem onClick={handleClickRemoteAssessment}>
|
||||
<div className="font-medium items-center flex text-sm text-gray-900 justify-start">
|
||||
<DocumentMagnifyingGlassIcon className="h-4 w-4 mr-2" /> Remote Assesment
|
||||
<DocumentMagnifyingGlassIcon className="h-4 w-4 mr-2" /> Remote Assessment
|
||||
</div>
|
||||
Schedule a remote assesment
|
||||
Schedule a remote assessment
|
||||
</ListItem>
|
||||
<ListItem onClick={handleClickUploadCSV}>
|
||||
<div className="font-medium items-center flex text-sm text-gray-900 justify-start">
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import {
|
|||
import AddNewDropDown from "./AddNew";
|
||||
import { cva } from "class-variance-authority";
|
||||
import UploadCsvModal from "@/app/portfolio/[slug]/components/UploadCsvModal";
|
||||
import RemoteAssesmentModal from "@/app/portfolio/[slug]/components/RemoteAssesmentModal";
|
||||
import RemoteAssessmentModal from "@/app/portfolio/[slug]/components/RemoteAssessmentModal";
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
|
|
@ -41,7 +41,7 @@ export function Toolbar({ portfolioId }: ToolbarProps) {
|
|||
}
|
||||
|
||||
const [modalIsOpen, setModalIsOpen] = useState(false);
|
||||
const [isRemoteAssesmentOpen, setIsRemoteAssesmentOpen] = useState(false);
|
||||
const [isRemoteAssessmentOpen, setIsRemoteAssessmentOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<NavigationMenu>
|
||||
|
|
@ -73,13 +73,13 @@ export function Toolbar({ portfolioId }: ToolbarProps) {
|
|||
<AddNewDropDown
|
||||
isUploadCsvOpen={modalIsOpen}
|
||||
setIsUploadCsvOpen={setModalIsOpen}
|
||||
isRemoteAssesmentOpen={isRemoteAssesmentOpen}
|
||||
setIsRemoteAssesmentOpen={setIsRemoteAssesmentOpen}
|
||||
isRemoteAssessmentOpen={isRemoteAssessmentOpen}
|
||||
setIsRemoteAssessmentOpen={setIsRemoteAssessmentOpen}
|
||||
/>
|
||||
</NavigationMenuList>
|
||||
<RemoteAssesmentModal
|
||||
isOpen={isRemoteAssesmentOpen}
|
||||
setIsOpen={setIsRemoteAssesmentOpen}
|
||||
<RemoteAssessmentModal
|
||||
isOpen={isRemoteAssessmentOpen}
|
||||
setIsOpen={setIsRemoteAssessmentOpen}
|
||||
portfolioId={portfolioId}
|
||||
/>
|
||||
<UploadCsvModal
|
||||
|
|
|
|||
|
|
@ -1,482 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import { Dialog, Transition, Menu } from "@headlessui/react";
|
||||
import { useState, Fragment, useEffect, useMemo } from "react";
|
||||
import { Input } from "@/app/shadcn_components/ui/input";
|
||||
import { Button } from "@/app/shadcn_components/ui/button";
|
||||
import { Float } from "@headlessui-float/react";
|
||||
import { ChevronDownIcon } from "@heroicons/react/20/solid";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { add } from "cypress/types/lodash";
|
||||
import { post } from "cypress/types/jquery";
|
||||
|
||||
type Option = {
|
||||
label: string;
|
||||
value: string;
|
||||
disabled: boolean;
|
||||
};
|
||||
|
||||
type DropdownProps = {
|
||||
options: Option[];
|
||||
selectedOption: string;
|
||||
onSelectOption: (option: Option) => void;
|
||||
};
|
||||
|
||||
const selecthousingTypeOptions = [
|
||||
{
|
||||
label: "Social",
|
||||
value: "Social",
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
label: "Private",
|
||||
value: "Private",
|
||||
disabled: false,
|
||||
},
|
||||
];
|
||||
|
||||
const selectGoalOptions = [
|
||||
{
|
||||
label: "Increase EPC",
|
||||
value: "Increase EPC",
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
label: "Reduce energy consumption",
|
||||
value: "Reduce energy consumption",
|
||||
disabled: false, // TODO: Disable
|
||||
},
|
||||
];
|
||||
|
||||
const goalValueOptions = [
|
||||
{
|
||||
label: "C",
|
||||
value: "C",
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
label: "B",
|
||||
value: "B",
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
label: "A",
|
||||
value: "A",
|
||||
disabled: false,
|
||||
},
|
||||
];
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
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.");
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
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 GenericObject>(data: T[]): string => {
|
||||
// Get headers (keys from the first object)
|
||||
const headers = Object.keys(data[0]) as (keyof T)[];
|
||||
|
||||
// Create CSV rows
|
||||
const rows = data.map((row) =>
|
||||
headers.map((header) => row[header]).join(",")
|
||||
);
|
||||
|
||||
// Combine headers and rows into CSV string
|
||||
return [headers.join(","), ...rows].join("\n");
|
||||
};
|
||||
|
||||
function useCreateRemoteAssessment({
|
||||
portfolioId,
|
||||
uprn,
|
||||
addressLineOne,
|
||||
postcode,
|
||||
}: {
|
||||
portfolioId: string;
|
||||
uprn: number | null;
|
||||
addressLineOne: string;
|
||||
postcode: string;
|
||||
}) {
|
||||
// 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);
|
||||
|
||||
const { assetListFileKey, valuationDataFileKey } = useMemo(
|
||||
() => generateS3Keys(userId, portfolioId),
|
||||
[userId, portfolioId]
|
||||
);
|
||||
|
||||
const {
|
||||
mutate: mutateUploadAssetList,
|
||||
isLoading: uploadAssetListIsLoading,
|
||||
isError: uploadAssetListIsError,
|
||||
} = useMutation(uploadCsvToS3, {
|
||||
onSuccess: (data) => {
|
||||
console.log("WAS IT A SUCCESS?", data.success);
|
||||
console.log("TRIGGERING THE ENGINE");
|
||||
// This is where we trigger the engine!!!
|
||||
const body = {
|
||||
trigger_file_path: assetListFileKey,
|
||||
};
|
||||
// engine API call goes here
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error(error);
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
mutate: mutatePresignedUrl,
|
||||
isLoading: presignedUrlIsLoading,
|
||||
isError: presignedUrlIsError,
|
||||
} = useMutation(generatePresignedUrl, {
|
||||
onSuccess: (data) => {
|
||||
console.log(data.url);
|
||||
// On success, upload to that URL!!!!
|
||||
const assetList = [
|
||||
{
|
||||
uprn: uprn,
|
||||
address: addressLineOne,
|
||||
postcode: postcode,
|
||||
},
|
||||
];
|
||||
const assetListCsvString = convertToCSV(assetList);
|
||||
const assetListCsv = new Blob([assetListCsvString], {
|
||||
type: "text/csv",
|
||||
});
|
||||
|
||||
mutateUploadAssetList({ presignedUrl: data.url, file: assetListCsv });
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error(error);
|
||||
},
|
||||
});
|
||||
|
||||
function handleSubmit() {
|
||||
mutatePresignedUrl({ userId, portfolioId, fileKey: assetListFileKey });
|
||||
console.log("SUCCESS"); // This is where we would want to trigger some kind of use feedback
|
||||
}
|
||||
|
||||
return {
|
||||
handleSubmit,
|
||||
presignedUrlIsLoading,
|
||||
presignedUrlIsError,
|
||||
};
|
||||
}
|
||||
|
||||
export default function RemoteAssesmentModal({
|
||||
portfolioId,
|
||||
isOpen,
|
||||
setIsOpen,
|
||||
}: {
|
||||
isOpen: boolean;
|
||||
setIsOpen: (isOpen: boolean) => void;
|
||||
portfolioId: string;
|
||||
}) {
|
||||
const [scenario, setScenario] = useState<undefined | string>(undefined);
|
||||
const [housingType, sethousingType] = useState<string>("");
|
||||
const [selectedGoal, setSelectedGoal] = useState<string>("");
|
||||
const [goalValue, setGoalValue] = useState<string>("");
|
||||
const [addressLineOne, setAddressLineOne] = useState<string>("");
|
||||
const [postcode, setPostcode] = useState<string>("");
|
||||
const [uprn, setUprn] = useState<number | null>(null);
|
||||
const [valuation, setValuation] = useState<number | string | null>("");
|
||||
const [buttonDisabled, setButtonDisabled] = useState(true);
|
||||
|
||||
function handleScenarioChange(event: React.ChangeEvent<HTMLInputElement>) {
|
||||
setScenario(event.target.value);
|
||||
}
|
||||
|
||||
function handleAddressLineOneChange(
|
||||
event: React.ChangeEvent<HTMLInputElement>
|
||||
) {
|
||||
setAddressLineOne(event.target.value);
|
||||
}
|
||||
|
||||
function handlePostcodeChange(event: React.ChangeEvent<HTMLInputElement>) {
|
||||
setPostcode(event.target.value);
|
||||
}
|
||||
|
||||
function handleUprnChange(event: React.ChangeEvent<HTMLInputElement>) {
|
||||
setUprn(Number(event.target.value));
|
||||
}
|
||||
|
||||
function handleValuationChange(event: React.ChangeEvent<HTMLInputElement>) {
|
||||
setValuation(event.target.value);
|
||||
}
|
||||
|
||||
const { handleSubmit, presignedUrlIsLoading, presignedUrlIsError } =
|
||||
useCreateRemoteAssessment({
|
||||
portfolioId,
|
||||
uprn,
|
||||
addressLineOne,
|
||||
postcode,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
function handleButtonDisabled(): boolean {
|
||||
return !(
|
||||
scenario &&
|
||||
selectedGoal &&
|
||||
housingType &&
|
||||
addressLineOne &&
|
||||
postcode &&
|
||||
uprn &&
|
||||
valuation
|
||||
);
|
||||
}
|
||||
|
||||
setButtonDisabled(handleButtonDisabled());
|
||||
}, [
|
||||
scenario,
|
||||
selectedGoal,
|
||||
housingType,
|
||||
addressLineOne,
|
||||
postcode,
|
||||
uprn,
|
||||
valuation,
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
<Dialog
|
||||
as="div"
|
||||
className="relative z-10"
|
||||
onClose={() => setIsOpen(false)}
|
||||
>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div 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-1/2 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="text-lg font-medium leading-6 text-brandblue mb-3"
|
||||
>
|
||||
{scenario}
|
||||
</Dialog.Title>
|
||||
<div className="flex justify-center">
|
||||
Scenario Name
|
||||
<Input
|
||||
type="text"
|
||||
defaultValue={"Remote Assesment"}
|
||||
onChange={handleScenarioChange}
|
||||
/>
|
||||
</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)}
|
||||
/>
|
||||
</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"
|
||||
>
|
||||
Select Goal
|
||||
<span className="text-red-500">*</span>
|
||||
</label>
|
||||
<SelectDropdown
|
||||
options={selectGoalOptions}
|
||||
selectedOption={selectedGoal}
|
||||
onSelectOption={(option) => setSelectedGoal(option.value)}
|
||||
/>
|
||||
{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);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex justify-center mt-6">
|
||||
Address Line 1
|
||||
<Input type="text" onChange={handleAddressLineOneChange} />
|
||||
</div>
|
||||
<div className="flex justify-center mt-6">
|
||||
Postcode
|
||||
<Input type="text" onChange={handlePostcodeChange} />
|
||||
</div>
|
||||
<div className="flex justify-center mt-6">
|
||||
UPRN
|
||||
<Input type="text" onChange={handleUprnChange} />
|
||||
</div>
|
||||
<div className="flex justify-center mt-6">
|
||||
Valuation
|
||||
<Input type="text" onChange={handleValuationChange} />
|
||||
</div>
|
||||
<div className="flex justify-center mt-6">
|
||||
<Button
|
||||
disabled={buttonDisabled}
|
||||
onClick={() => {
|
||||
handleSubmit();
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</div>
|
||||
<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={() => setIsOpen(false)}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue