updating upload to allow excel and csv upload

This commit is contained in:
Khalim Conn-Kowlessar 2025-07-16 15:10:05 +01:00
parent 37383226dc
commit be31116541
6 changed files with 196 additions and 43 deletions

95
package-lock.json generated
View file

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

View file

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

View 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." });
}

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

View file

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

View file

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