mirror of
https://github.com/Hestia-Homes/assessment-model.git
synced 2026-06-08 11:37:25 +00:00
updating upload to allow excel and csv upload
This commit is contained in:
parent
37383226dc
commit
be31116541
6 changed files with 196 additions and 43 deletions
95
package-lock.json
generated
95
package-lock.json
generated
|
|
@ -52,6 +52,7 @@
|
|||
"tailwindcss": "^3.4.3",
|
||||
"tailwindcss-animate": "^1.0.6",
|
||||
"typescript": "5.0.4",
|
||||
"xlsx": "^0.18.5",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
@ -3467,6 +3468,14 @@
|
|||
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/adler-32": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz",
|
||||
"integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/agent-base": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-5.1.1.tgz",
|
||||
|
|
@ -4182,6 +4191,18 @@
|
|||
"integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/cfb": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz",
|
||||
"integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==",
|
||||
"dependencies": {
|
||||
"adler-32": "~1.3.0",
|
||||
"crc-32": "~1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
|
|
@ -4350,6 +4371,14 @@
|
|||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/codepage": {
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz",
|
||||
"integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
|
|
@ -4435,6 +4464,17 @@
|
|||
"integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/crc-32": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
|
||||
"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
|
||||
"bin": {
|
||||
"crc32": "bin/crc32.njs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
|
||||
|
|
@ -6257,6 +6297,14 @@
|
|||
"node": ">= 0.12"
|
||||
}
|
||||
},
|
||||
"node_modules/frac": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
|
||||
"integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/fraction.js": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz",
|
||||
|
|
@ -9461,6 +9509,17 @@
|
|||
"node": ">= 10.x"
|
||||
}
|
||||
},
|
||||
"node_modules/ssf": {
|
||||
"version": "0.11.2",
|
||||
"resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
|
||||
"integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
|
||||
"dependencies": {
|
||||
"frac": "~1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/sshpk": {
|
||||
"version": "1.17.0",
|
||||
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz",
|
||||
|
|
@ -10429,6 +10488,22 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/wmf": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
|
||||
"integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/word": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz",
|
||||
"integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/word-wrap": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
|
||||
|
|
@ -10474,6 +10549,26 @@
|
|||
"async-limiter": "~1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/xlsx": {
|
||||
"version": "0.18.5",
|
||||
"resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz",
|
||||
"integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==",
|
||||
"dependencies": {
|
||||
"adler-32": "~1.3.0",
|
||||
"cfb": "~1.2.1",
|
||||
"codepage": "~1.15.0",
|
||||
"crc-32": "~1.2.1",
|
||||
"ssf": "~0.11.2",
|
||||
"wmf": "~1.0.1",
|
||||
"word": "~0.3.0"
|
||||
},
|
||||
"bin": {
|
||||
"xlsx": "bin/xlsx.njs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/xml2js": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz",
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@
|
|||
"tailwindcss": "^3.4.3",
|
||||
"tailwindcss-animate": "^1.0.6",
|
||||
"typescript": "5.0.4",
|
||||
"xlsx": "^0.18.5",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
|||
32
src/app/api/upload/validate/route.ts
Normal file
32
src/app/api/upload/validate/route.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
import * as XLSX from "xlsx";
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
console.log("Validating uploaded file...");
|
||||
const formData = await req.formData();
|
||||
const file = formData.get("file") as File;
|
||||
|
||||
if (!file) {
|
||||
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 }
|
||||
);
|
||||
}
|
||||
|
||||
// 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." });
|
||||
}
|
||||
|
|
@ -5,7 +5,11 @@ import {
|
|||
NavigationMenuLink,
|
||||
NavigationMenuTrigger,
|
||||
} from "@/app/shadcn_components/ui/navigation-menu";
|
||||
import { PlusIcon, TableCellsIcon, DocumentMagnifyingGlassIcon } from "@heroicons/react/24/outline";
|
||||
import {
|
||||
PlusIcon,
|
||||
TableCellsIcon,
|
||||
DocumentMagnifyingGlassIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
|
@ -66,22 +70,18 @@ export default function AddNewDropDown({
|
|||
</NavigationMenuTrigger>
|
||||
<NavigationMenuContent>
|
||||
<ul className="p-6 md:w-[200px] lg:w-[350px] lg:grid-cols-[.75fr_1fr] cursor-pointer">
|
||||
<ListItem onClick={handleCickAddUnit}>
|
||||
<div className="font-medium items-center flex text-sm text-gray-900 justify-start">
|
||||
<PlusIcon className="h-4 w-4 mr-2" /> Add Unit
|
||||
</div>
|
||||
</ListItem>
|
||||
<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 Assessment
|
||||
<DocumentMagnifyingGlassIcon className="h-4 w-4 mr-2" /> Remote
|
||||
Assessment
|
||||
</div>
|
||||
Schedule a remote assessment
|
||||
Run a remote assessment for a single unit
|
||||
</ListItem>
|
||||
<ListItem onClick={handleClickUploadCSV}>
|
||||
<div className="font-medium items-center flex text-sm text-gray-900 justify-start">
|
||||
<TableCellsIcon className="h-4 w-4 mr-2" /> Upload CSV
|
||||
<TableCellsIcon className="h-4 w-4 mr-2" /> File Import
|
||||
</div>
|
||||
Upload a csv of multiple units
|
||||
Upload a excel or csv file, containing multiple units
|
||||
</ListItem>
|
||||
</ul>
|
||||
</NavigationMenuContent>
|
||||
|
|
|
|||
|
|
@ -4,47 +4,42 @@ import { Input } from "@/app/shadcn_components/ui/input";
|
|||
import { Label } from "@/app/shadcn_components/ui/label";
|
||||
|
||||
export function InputFile({
|
||||
handleButtonDisabled,
|
||||
setCsvFile,
|
||||
selectedGoal,
|
||||
housingType,
|
||||
goalValue,
|
||||
onFileSelect,
|
||||
isValidating,
|
||||
}: {
|
||||
handleButtonDisabled: (
|
||||
goal?: string,
|
||||
scheme?: string,
|
||||
value?: string,
|
||||
file?: File | null
|
||||
) => void;
|
||||
setCsvFile: (file: File) => void;
|
||||
selectedGoal: string;
|
||||
housingType: string;
|
||||
goalValue: string;
|
||||
onFileSelect: (file: File) => void;
|
||||
isValidating: boolean;
|
||||
}) {
|
||||
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);
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
const validExtensions = ["csv", "xls", "xlsx"];
|
||||
const fileExtension = file.name.split(".").pop()?.toLowerCase();
|
||||
|
||||
if (!fileExtension || !validExtensions.includes(fileExtension)) {
|
||||
console.error("Unsupported file type");
|
||||
e.target.value = "";
|
||||
return;
|
||||
}
|
||||
|
||||
onFileSelect(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>
|
||||
<Label htmlFor="csv-uploader">Upload your file (CSV or Excel)</Label>
|
||||
<Input
|
||||
id="csv-uploader"
|
||||
type="file"
|
||||
accept=".csv, text/csv"
|
||||
className="cursor-pointer"
|
||||
accept=".csv, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel"
|
||||
onChange={handleOnChange}
|
||||
disabled={isValidating}
|
||||
className={`cursor-pointer ${isValidating ? "opacity-50" : ""}`}
|
||||
/>
|
||||
{isValidating && (
|
||||
<span className="text-sm text-gray-500 mt-1">Validating file…</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { ChevronDownIcon } from "@heroicons/react/20/solid";
|
|||
import { Float } from "@headlessui-float/react";
|
||||
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;
|
||||
|
|
@ -159,6 +160,38 @@ export default function UploadCsvModal({
|
|||
}
|
||||
}
|
||||
|
||||
const {
|
||||
mutate: validateFile,
|
||||
isLoading: isValidating,
|
||||
error: validationError,
|
||||
} = useMutation(
|
||||
async (file: 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");
|
||||
}
|
||||
|
||||
return response.json();
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
setButtonDisabled(false);
|
||||
},
|
||||
onError: (err) => {
|
||||
console.error(err);
|
||||
setButtonDisabled(true);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
|
|
@ -311,11 +344,8 @@ export default function UploadCsvModal({
|
|||
<div className="flex flex-col space-y-2 mt-7">
|
||||
<div className="flex space-x-2">
|
||||
<InputFile
|
||||
handleButtonDisabled={handleButtonDisabled}
|
||||
setCsvFile={setCsvFile}
|
||||
selectedGoal={selectedGoal}
|
||||
housingType={housingType}
|
||||
goalValue={goalValue}
|
||||
onFileSelect={(file) => validateFile(file)}
|
||||
isValidating={isValidating}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue