followed the train of functions that uploads the asset list and fixed all my assessment spelling errors

This commit is contained in:
StefanWout 2024-11-20 17:07:12 +00:00
parent eaa9d82384
commit b5fda31903
3 changed files with 16 additions and 498 deletions

View file

@ -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">

View file

@ -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

View file

@ -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>
</>
);
}