mirror of
https://github.com/Hestia-Homes/assessment-model.git
synced 2026-06-30 12:55:02 +00:00
restructuring the plan submit components
This commit is contained in:
parent
0e2ffe97c3
commit
cb7ba9e0b3
5 changed files with 247 additions and 219 deletions
|
|
@ -8,7 +8,7 @@ import {
|
|||
} from "@/app/shadcn_components/ui/navigation-menu";
|
||||
import AddNewDropDown from "./AddNew";
|
||||
import { cva } from "class-variance-authority";
|
||||
import UploadCsvModal from "./UploadCsvModal";
|
||||
import UploadCsvModal from "@/app/portfolio/[slug]/components/UploadCsvModal";
|
||||
import { useState } from "react";
|
||||
|
||||
interface ToolbarProps {
|
||||
|
|
|
|||
50
src/app/portfolio/[slug]/components/InputFile.tsx
Normal file
50
src/app/portfolio/[slug]/components/InputFile.tsx
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
"use client";
|
||||
|
||||
import { Input } from "@/app/shadcn_components/ui/input";
|
||||
import { Label } from "@/app/shadcn_components/ui/label";
|
||||
|
||||
export function InputFile({
|
||||
handleButtonDisabled,
|
||||
setCsvFile,
|
||||
selectedGoal,
|
||||
housingType,
|
||||
goalValue,
|
||||
}: {
|
||||
handleButtonDisabled: (
|
||||
goal?: string,
|
||||
scheme?: string,
|
||||
value?: string,
|
||||
file?: File | null
|
||||
) => void;
|
||||
setCsvFile: (file: File) => void;
|
||||
selectedGoal: string;
|
||||
housingType: string;
|
||||
goalValue: string;
|
||||
}) {
|
||||
function handleOnChange(e: React.ChangeEvent<HTMLInputElement>) {
|
||||
if (e.target.files) {
|
||||
// Check if files is not null
|
||||
const file = e.target.files[0];
|
||||
if (file.type !== "text/csv") {
|
||||
// Show an error message
|
||||
console.error("File is not a CSV");
|
||||
return;
|
||||
}
|
||||
setCsvFile(file); // Assuming you have a state to keep the file
|
||||
handleButtonDisabled(selectedGoal, housingType, goalValue, file);
|
||||
}
|
||||
}
|
||||
|
||||
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 csv</Label>
|
||||
<Input
|
||||
id="csv-uploader"
|
||||
type="file"
|
||||
accept=".csv, text/csv"
|
||||
className="cursor-pointer"
|
||||
onChange={handleOnChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
194
src/app/portfolio/[slug]/components/SubmitPlan.tsx
Normal file
194
src/app/portfolio/[slug]/components/SubmitPlan.tsx
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { useSession } from "next-auth/react";
|
||||
|
||||
function generateS3Key(userId: number, portfolioId: number, filename: string) {
|
||||
const timestamp = new Date().toISOString().replace(/[:.-]/g, "");
|
||||
const key = `${userId}/${portfolioId}/${filename}-${timestamp}.csv`;
|
||||
return key;
|
||||
}
|
||||
|
||||
interface UseCreatePlanProps {
|
||||
portfolioId: number;
|
||||
housingType: string;
|
||||
goal: string;
|
||||
goalValue: string;
|
||||
file: File;
|
||||
}
|
||||
|
||||
interface UseCreatePlanReturn {
|
||||
handlePlanBuild: () => void;
|
||||
isGeneratingUrlLoading: boolean;
|
||||
isUploadLoading: boolean;
|
||||
}
|
||||
|
||||
const useCreatePlan = ({
|
||||
portfolioId,
|
||||
housingType,
|
||||
goal,
|
||||
goalValue,
|
||||
file,
|
||||
}: UseCreatePlanProps): UseCreatePlanReturn => {
|
||||
const router = useRouter();
|
||||
const session = useSession();
|
||||
|
||||
const userId = session.data?.user.dbId;
|
||||
|
||||
const fileKey = generateS3Key(
|
||||
userId,
|
||||
portfolioId,
|
||||
"portfolio_plan_properties"
|
||||
);
|
||||
|
||||
const { mutate: mutateUploadCsv, isLoading: isUploadLoading } = useMutation(
|
||||
uploadCsvToS3,
|
||||
{
|
||||
onSuccess: (data) => {
|
||||
const body = JSON.stringify({
|
||||
portfolio_id: portfolioId,
|
||||
housing_type: housingType,
|
||||
goal: goal,
|
||||
goal_value: goalValue,
|
||||
trigger_file_path: fileKey,
|
||||
});
|
||||
|
||||
const response = fetch(`/api/plan/trigger`, {
|
||||
method: "POST",
|
||||
body: body,
|
||||
});
|
||||
return response;
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error(error);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const { mutate, isLoading: isGeneratingUrlLoading } = useMutation(
|
||||
generatePresignedUrl,
|
||||
{
|
||||
onSuccess: (data) => {
|
||||
try {
|
||||
const response = mutateUploadCsv({
|
||||
presignedUrl: data.url,
|
||||
file: file,
|
||||
});
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error(error);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const handlePlanBuild = () => {
|
||||
mutate({ userId: userId, portfolioId: portfolioId, fileKey: fileKey });
|
||||
router.push(`/portfolio/${portfolioId}/plan-loading`);
|
||||
};
|
||||
|
||||
return {
|
||||
handlePlanBuild,
|
||||
isGeneratingUrlLoading,
|
||||
isUploadLoading,
|
||||
};
|
||||
};
|
||||
|
||||
async function generatePresignedUrl({
|
||||
userId,
|
||||
portfolioId,
|
||||
fileKey,
|
||||
}: {
|
||||
userId: number;
|
||||
portfolioId: number;
|
||||
fileKey: string;
|
||||
}) {
|
||||
const body = JSON.stringify({
|
||||
userId: userId,
|
||||
portfolioId: portfolioId,
|
||||
fileKey: fileKey,
|
||||
});
|
||||
|
||||
const presignedResponse = await fetch(`/api/upload/csv`, {
|
||||
method: "POST",
|
||||
body: body,
|
||||
});
|
||||
if (!presignedResponse.ok) {
|
||||
throw new Error("Network response was not ok");
|
||||
}
|
||||
const presignedUrl = await presignedResponse.json();
|
||||
return presignedUrl;
|
||||
}
|
||||
|
||||
async function uploadCsvToS3({
|
||||
presignedUrl,
|
||||
file,
|
||||
}: {
|
||||
presignedUrl: string;
|
||||
file: File;
|
||||
}) {
|
||||
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 };
|
||||
}
|
||||
|
||||
export const SubmitPlan = ({
|
||||
buttonDisabled,
|
||||
goal,
|
||||
housingType,
|
||||
goalValue,
|
||||
file,
|
||||
portfolioId,
|
||||
}: {
|
||||
buttonDisabled: boolean;
|
||||
goal: string;
|
||||
housingType: string;
|
||||
goalValue: string;
|
||||
file: File;
|
||||
portfolioId: number;
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const session = useSession();
|
||||
|
||||
const { handlePlanBuild, isGeneratingUrlLoading, isUploadLoading } =
|
||||
useCreatePlan({ portfolioId, housingType, goal, goalValue, file });
|
||||
|
||||
let buttonText;
|
||||
if (isUploadLoading) {
|
||||
buttonText = "Uploading file...";
|
||||
} else if (isGeneratingUrlLoading) {
|
||||
buttonText = "Creating plan...";
|
||||
} else {
|
||||
buttonText = "Create";
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="text-white inline-flex justify-center rounded-md border border-transparent bg-brandblue px-4 py-2 text-sm font-medium text-grey-900 hover:bg-hoverblue focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 disabled:bg-gray-300 disabled:opacity-50"
|
||||
onClick={handlePlanBuild}
|
||||
disabled={buttonDisabled || isGeneratingUrlLoading || isUploadLoading}
|
||||
>
|
||||
{buttonText}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
|
@ -4,224 +4,8 @@ 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 { Input } from "@/app/shadcn_components/ui/input";
|
||||
import { Label } from "@/app/shadcn_components/ui/label";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { useSession } from "next-auth/react";
|
||||
|
||||
function generateS3Key(userId: number, portfolioId: number, filename: string) {
|
||||
const timestamp = new Date().toISOString().replace(/[:.-]/g, "");
|
||||
const key = `${userId}/${portfolioId}/${filename}-${timestamp}.csv`;
|
||||
return key;
|
||||
}
|
||||
|
||||
async function generatePresignedUrl({
|
||||
userId,
|
||||
portfolioId,
|
||||
fileKey,
|
||||
}: {
|
||||
userId: number;
|
||||
portfolioId: number;
|
||||
fileKey: string;
|
||||
}) {
|
||||
const body = JSON.stringify({
|
||||
userId: userId,
|
||||
portfolioId: portfolioId,
|
||||
fileKey: fileKey,
|
||||
});
|
||||
|
||||
const presignedResponse = await fetch(`/api/upload/csv`, {
|
||||
method: "POST",
|
||||
body: body,
|
||||
});
|
||||
if (!presignedResponse.ok) {
|
||||
throw new Error("Network response was not ok");
|
||||
}
|
||||
const presignedUrl = await presignedResponse.json();
|
||||
return presignedUrl;
|
||||
}
|
||||
|
||||
async function uploadCsvToS3({
|
||||
presignedUrl,
|
||||
file,
|
||||
}: {
|
||||
presignedUrl: string;
|
||||
file: File;
|
||||
}) {
|
||||
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 };
|
||||
}
|
||||
|
||||
export const SubmitPlan = ({
|
||||
buttonDisabled,
|
||||
goal,
|
||||
housingType,
|
||||
goalValue,
|
||||
file,
|
||||
portfolioId,
|
||||
}: {
|
||||
buttonDisabled: boolean;
|
||||
goal: string;
|
||||
housingType: string;
|
||||
goalValue: string;
|
||||
file: File;
|
||||
portfolioId: number;
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const session = useSession();
|
||||
|
||||
const userId = session.data?.user.dbId;
|
||||
|
||||
const fileKey = generateS3Key(
|
||||
userId,
|
||||
portfolioId,
|
||||
"portfolio_plan_properties"
|
||||
);
|
||||
|
||||
const { mutate: mutateUploadCsv, isLoading: isUploadLoading } = useMutation(
|
||||
uploadCsvToS3,
|
||||
{
|
||||
onSuccess: (data) => {
|
||||
const body = JSON.stringify({
|
||||
// userId: session.data?.user.dbId,
|
||||
portfolio_id: portfolioId,
|
||||
housing_type: housingType,
|
||||
goal: goal,
|
||||
goal_value: goalValue,
|
||||
trigger_file_path: fileKey,
|
||||
});
|
||||
|
||||
// After the file has been uploaded, we can trigger the job to build the plan
|
||||
const response = fetch(`/api/plan/trigger`, {
|
||||
method: "POST",
|
||||
body: body,
|
||||
});
|
||||
return response;
|
||||
},
|
||||
onError: (error) => {
|
||||
// handle error
|
||||
console.error(error);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const { mutate, isLoading } = useMutation(generatePresignedUrl, {
|
||||
onSuccess: (data) => {
|
||||
// After the presigned URL has been generated, we can upload the file to S3
|
||||
try {
|
||||
const response = mutateUploadCsv({
|
||||
presignedUrl: data.url,
|
||||
file: file,
|
||||
});
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
// handle error
|
||||
console.error(error);
|
||||
},
|
||||
});
|
||||
|
||||
if (!session.data) {
|
||||
// The user is not logged in, redirect them to sign in
|
||||
router.push("/");
|
||||
return null;
|
||||
}
|
||||
|
||||
const handlePlanBuild = () => {
|
||||
// The plan build is triggered by clicking submit which will:
|
||||
// 1) Generate a pre-signed url to upload to
|
||||
// 2) Upload the csv to the pre-signed url
|
||||
// 3) Trigger the job to build the plan
|
||||
// 4) Redirect the user to some loading page - this could be the portfolio page itself and we just trigger a regresh with skeleton cards for the properties
|
||||
mutate({ userId: userId, portfolioId: portfolioId, fileKey: fileKey });
|
||||
|
||||
// TODO: Make api call to backend service to trigger the plan build
|
||||
// Probably need to pass in the file key to mutate (define it outside)
|
||||
// because of the async
|
||||
// We could also trigger it inside of mutateUploadCsv but the nested nature is kind of ugly
|
||||
|
||||
console.log("Redirect user to loading page");
|
||||
router.push("/temp-loading");
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="text-white inline-flex justify-center rounded-md border border-transparent bg-brandblue px-4 py-2 text-sm font-medium text-grey-900 hover:bg-hoverblue focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 disabled:bg-gray-300 disabled:opacity-50"
|
||||
onClick={handlePlanBuild}
|
||||
disabled={buttonDisabled || isLoading}
|
||||
>
|
||||
{isLoading ? "Creating..." : "Create"}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export function InputFile({
|
||||
handleButtonDisabled,
|
||||
setCsvFile,
|
||||
selectedGoal,
|
||||
housingType,
|
||||
goalValue,
|
||||
}: {
|
||||
handleButtonDisabled: (
|
||||
goal?: string,
|
||||
scheme?: string,
|
||||
value?: string,
|
||||
file?: File | null
|
||||
) => void;
|
||||
setCsvFile: (file: File) => void;
|
||||
selectedGoal: string;
|
||||
housingType: string;
|
||||
goalValue: string;
|
||||
}) {
|
||||
function handleOnChange(e: React.ChangeEvent<HTMLInputElement>) {
|
||||
if (e.target.files) {
|
||||
// Check if files is not null
|
||||
const file = e.target.files[0];
|
||||
if (file.type !== "text/csv") {
|
||||
// Show an error message
|
||||
console.error("File is not a CSV");
|
||||
return;
|
||||
}
|
||||
setCsvFile(file); // Assuming you have a state to keep the file
|
||||
handleButtonDisabled(selectedGoal, housingType, goalValue, file);
|
||||
}
|
||||
}
|
||||
|
||||
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 csv</Label>
|
||||
<Input
|
||||
id="csv-uploader"
|
||||
type="file"
|
||||
accept=".csv, text/csv"
|
||||
className="cursor-pointer"
|
||||
onChange={handleOnChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
import { InputFile } from "@/app/portfolio/[slug]/components/InputFile";
|
||||
import { SubmitPlan } from "@/app/portfolio/[slug]/components/SubmitPlan";
|
||||
|
||||
type Option = {
|
||||
label: string;
|
||||
Loading…
Add table
Reference in a new issue