diff --git a/src/app/api/plan/trigger/route.ts b/src/app/api/plan/trigger/route.ts index 9746b392..bb18f7c8 100644 --- a/src/app/api/plan/trigger/route.ts +++ b/src/app/api/plan/trigger/route.ts @@ -19,6 +19,7 @@ const PresignedUrlBodySchema = z.object({ already_installed_file_path: z.string().optional(), // optional scenario_id to link the plan to an existing scenario scenario_id: z.string().optional().nullable(), + file_type: z.enum(["csv", "xlsx"]).optional(), // Specify the file type }); export async function POST(request: NextRequest) { @@ -54,8 +55,6 @@ export async function POST(request: NextRequest) { console.log("Triggering plan with body: ", validatedBody); const url = `${process.env.FASTAPI_API_URL}/v1/plan/trigger`; - console.log("Triggering plan with url: ", url); - console.log("Triggering plan with headers: ", headers); const response = await fetch(url, { method: "POST", diff --git a/src/app/api/upload/validate/route.ts b/src/app/api/upload/validate/route.ts index bb69a3b0..8f3526e1 100644 --- a/src/app/api/upload/validate/route.ts +++ b/src/app/api/upload/validate/route.ts @@ -10,41 +10,70 @@ export async function POST(req: NextRequest) { return NextResponse.json({ error: "No file provided" }, { status: 400 }); } - const arrayBuffer = await file.arrayBuffer(); - const workbook = XLSX.read(arrayBuffer, { type: "array" }); + const filename = file.name.toLowerCase(); + const mimeType = file.type; - const sheetNames = workbook.SheetNames; + let fileType: "csv" | "xlsx" | "unknown" = "unknown"; + + if ( + filename.endsWith(".csv") || + mimeType === "text/csv" || + mimeType === "application/vnd.ms-excel" + ) { + fileType = "csv"; + } else if ( + filename.endsWith(".xlsx") || + filename.endsWith(".xls") || + mimeType === + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" + ) { + fileType = "xlsx"; + } + + if (fileType === "unknown") { + return NextResponse.json( + { error: "Unsupported file type", file_type: fileType }, + { status: 400 } + ); + } let isStandardised = false; + let sheetNames: string[] = []; - for (const sheetName of sheetNames) { - if (sheetName !== "Standardised Asset List") continue; + // Handle CSV + if (fileType === "csv") { + const text = await file.text(); + const lines = text.split("\n").map((line) => line.split(",")); + const headers = lines[0].map((h) => h.trim()); + isStandardised = headers.includes("domna_property_id"); + } - const worksheet = workbook.Sheets[sheetName]; - const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 }) as ( - | string - | number - | null - )[][]; + // Handle Excel + if (fileType === "xlsx") { + const arrayBuffer = await file.arrayBuffer(); - const headers = jsonData.find( - (row): row is string[] => - Array.isArray(row) && - row.length > 0 && - row.every((cell) => typeof cell === "string" || cell === null) - ); + // Only load specific sheet to reduce memory usage + const workbook = XLSX.read(arrayBuffer, { + type: "array", + sheets: ["Standardised Asset List"], + }); - if (headers?.includes("domna_property_id")) { - isStandardised = true; - break; + sheetNames = workbook.SheetNames; + + const worksheet = workbook.Sheets["Standardised Asset List"]; + if (worksheet) { + const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 }); + const headers = jsonData[0] as string[]; + isStandardised = headers?.includes("domna_property_id"); } } return NextResponse.json({ message: isStandardised - ? "Standardised Asset List format detected. Please select which tab to use." + ? "Standardised Asset List format detected." : "Valid file. No standardised format detected.", - sheetNames: isStandardised ? sheetNames : undefined, isStandardised, + file_type: fileType, + ...(fileType === "xlsx" && isStandardised ? { sheetNames } : {}), }); } diff --git a/src/app/portfolio/[slug]/components/UploadCsvModal.tsx b/src/app/portfolio/[slug]/components/UploadCsvModal.tsx index 00c1b01c..bf11fdb7 100644 --- a/src/app/portfolio/[slug]/components/UploadCsvModal.tsx +++ b/src/app/portfolio/[slug]/components/UploadCsvModal.tsx @@ -51,9 +51,13 @@ const goalValueOptions = [ { label: "A", value: "A", disabled: false }, ]; -function generateS3Key(userId: string, portfolioId: string) { +function generateS3Key( + userId: string, + portfolioId: string, + fileType: "csv" | "xlsx" +) { const timestamp = new Date().toISOString().replace(/[:.-]/g, ""); - return `${userId}/${portfolioId}/${timestamp}/asset_list.csv`; + return `${userId}/${portfolioId}/${timestamp}/asset_list.${fileType}`; } async function uploadCsvToS3({ @@ -103,6 +107,7 @@ export function useUploadCsvPlan({ ashpCop, measures, onSuccessRedirect, + fileType, }: { file: File; portfolioId: string; @@ -115,13 +120,14 @@ export function useUploadCsvPlan({ selectedSheet: string; measures: (typeof measuresList)[number][]; onSuccessRedirect: (path: string) => void; + fileType: "csv" | "xlsx"; }) { const session = useSession(); const userId = String(session.data?.user.dbId); const fileKey = useMemo( - () => generateS3Key(userId, portfolioId), - [userId, portfolioId] + () => generateS3Key(userId, portfolioId, fileType), + [userId, portfolioId, fileType] ); const { mutateAsync: uploadFileToS3, isLoading: isUploadLoading } = @@ -154,6 +160,7 @@ export function useUploadCsvPlan({ event_type: "remote_assessment", sheet_name: selectedSheet, ashp_cop: ashpCop, + file_type: fileType, // Pass the file type for backend processing }; const triggerRes = await fetch("/api/plan/trigger", { @@ -211,6 +218,7 @@ export default function UploadCsvModal({ const [csvFile, setCsvFile] = useState(null); const [selectedScenario, setSelectedScenario] = useState(null); const [showMeasures, setShowMeasures] = useState(false); + const [fileType, setFileType] = useState<"csv" | "xlsx">("csv"); const scenarioOptions = useMemo( () => @@ -242,6 +250,7 @@ export default function UploadCsvModal({ setSheetNames([]); setSelectedSheet(""); } + setFileType(data.file_type); // capture file type }, onError: () => { setSheetNames([]); @@ -282,6 +291,7 @@ export default function UploadCsvModal({ ashpCop: form.watch("ashpCop"), scenarioName: form.watch("scenario"), measures: form.watch("measures"), + fileType: fileType, portfolioId, selectedSheet, onSuccessRedirect: (path) => router.push(path), @@ -496,11 +506,6 @@ export default function UploadCsvModal({ /> -
-                        {JSON.stringify(form.formState.errors, null, 2)}
-                      
-
{JSON.stringify(form.watch(), null, 2)}
-
{/* Heat Pump COP */}