diff --git a/src/app/api/upload/retrofit-data/route.ts b/src/app/api/upload/retrofit-data/route.ts new file mode 100644 index 00000000..03ea2137 --- /dev/null +++ b/src/app/api/upload/retrofit-data/route.ts @@ -0,0 +1,157 @@ +import { NextRequest, NextResponse } from "next/server"; +import { S3Client, PutObjectCommand, GetObjectCommand } from "@aws-sdk/client-s3"; +import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; +import { db } from "@/app/db/db"; +import { filesFromSurveyor } from "@/app/db/schema/files_from_surveyor"; +import { and, eq } from "drizzle-orm"; + +// ✅ Initialize S3 Client (AWS SDK v3) +const s3 = new S3Client({ + region: process.env.RETROFIT_DATA_DEV_REGION, + credentials: { + accessKeyId: process.env.RETROFIT_DATA_DEV_ACCESS_KEY!, + secretAccessKey: process.env.RETROFIT_DATA_DEV_SECRET_KEY!, + }, +}); + +// ✅ Helper: safely convert BigInt fields to string +function serializeBigInt>(obj: T): T { + return Object.fromEntries( + Object.entries(obj).map(([key, value]) => [ + key, + typeof value === "bigint" ? value.toString() : value, + ]) + ) as T; +} + +// ------------------- +// 📤 POST — Upload files to S3 + Save to DB +// ------------------- +export async function POST(request: NextRequest) { + try { + const formData = await request.formData(); + const files = formData.getAll("files") as File[]; + + const portfolioIdStr = formData.get("portfolioId") as string; + const propertyIdStr = formData.get("propertyId") as string; + + if (!portfolioIdStr || !propertyIdStr) { + return NextResponse.json( + { msg: "Missing portfolioId or propertyId" }, + { status: 400 } + ); + } + + const portfolioId = BigInt(portfolioIdStr); + const propertyId = BigInt(propertyIdStr); + + if (!files || files.length === 0) { + return NextResponse.json({ msg: "No files uploaded" }, { status: 400 }); + } + + const uploadedFiles = await Promise.all( + files.map(async (file) => { + const timestamp = Date.now(); + const safeName = file.name.replace(/\s+/g, "_"); + const fileKey = `files_from_surveyor/${portfolioId}/${propertyId}/${timestamp}-${safeName}`; + + const buffer = Buffer.from(await file.arrayBuffer()); + + await s3.send( + new PutObjectCommand({ + Bucket: process.env.RETROFIT_DATA_DEV_S3_BUCKET_NAME!, + Key: fileKey, + Body: buffer, + ContentType: file.type, + }) + ); + + const fileUrl = `https://${process.env.RETROFIT_DATA_DEV_S3_BUCKET_NAME}.s3.${process.env.RETROFIT_DATA_DEV_REGION}.amazonaws.com/${fileKey}`; + + return { fileKey, fileUrl }; + }) + ); + + const inserted = await db + .insert(filesFromSurveyor) + .values( + uploadedFiles.map((file) => ({ + portfolioId, + propertyId, + s3JsonUrl: file.fileUrl, + })) + ) + .returning(); + + // ✅ Serialize BigInts before sending JSON + const safeInserted = inserted.map(serializeBigInt); + + return NextResponse.json({ files: safeInserted }, { status: 200 }); + } catch (error) { + console.error("❌ S3 or DB Error:", error); + return NextResponse.json( + { msg: "File upload failed", error: String(error) }, + { status: 500 } + ); + } +} + +// ------------------- +// 📥 GET — Fetch uploaded files + Presigned URLs +// ------------------- +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url); + const portfolioIdStr = searchParams.get("portfolioId"); + const propertyIdStr = searchParams.get("propertyId"); + + if (!portfolioIdStr || !propertyIdStr) { + return NextResponse.json( + { msg: "Missing portfolioId or propertyId" }, + { status: 400 } + ); + } + + const portfolioId = BigInt(portfolioIdStr); + const propertyId = BigInt(propertyIdStr); + + // ✅ Correct Drizzle where syntax + const files = await db + .select() + .from(filesFromSurveyor) + .where( + and( + eq(filesFromSurveyor.portfolioId, portfolioId), + eq(filesFromSurveyor.propertyId, propertyId) + ) + ); + + // ✅ Add 30-min presigned URLs + const withSignedUrls = await Promise.all( + files.map(async (file) => { + const key = file.s3JsonUrl.split(".amazonaws.com/")[1]; + const presignedUrl = await getSignedUrl( + s3, + new GetObjectCommand({ + Bucket: process.env.RETROFIT_DATA_DEV_S3_BUCKET_NAME!, + Key: key, + }), + { expiresIn: 60 * 30 } // 30 minutes + ); + + return serializeBigInt({ + ...file, + presignedUrl, + }); + }) + ); + + return NextResponse.json({ files: withSignedUrls }, { status: 200 }); + } catch (error) { + console.error("❌ Fetch error:", error); + return NextResponse.json( + { msg: "Error fetching files", error: String(error) }, + { status: 500 } + ); + } +} diff --git a/src/app/portfolio/[slug]/building-passport/[propertyId]/upload/page.tsx b/src/app/portfolio/[slug]/building-passport/[propertyId]/upload/page.tsx new file mode 100644 index 00000000..5d74ef0c --- /dev/null +++ b/src/app/portfolio/[slug]/building-passport/[propertyId]/upload/page.tsx @@ -0,0 +1,135 @@ +"use client"; + +import React, { useState, useEffect } from "react"; +import { useParams } from "next/navigation"; + +interface FileRecord { + id: number; + s3JsonUrl: string; + portfolioId: string; + propertyId: string; + presignedUrl?: string; + createdAt?: string; +} + +const UploadPage: React.FC = () => { + const [isUploading, setIsUploading] = useState(false); + const [files, setFiles] = useState([]); + const params = useParams(); + + const portfolioId = params?.slug as string; + const propertyId = params?.propertyId as string; + + const fetchFiles = async () => { + const res = await fetch( + `/api/upload/retrofit-data?portfolioId=${portfolioId}&propertyId=${propertyId}` + ); + if (res.ok) { + const data = await res.json(); + console.log(data, "hello"); + setFiles(data.files); + } + }; + + useEffect(() => { + if (portfolioId && propertyId) { + fetchFiles(); + } + }, [portfolioId, propertyId]); + + const handleFileChange = async (e: React.ChangeEvent) => { + const files = e.target.files; + if (!files || files.length === 0) return; + + const formData = new FormData(); + formData.append("portfolioId", portfolioId); + formData.append("propertyId", propertyId); + Array.from(files).forEach((file) => formData.append("files", file)); + + try { + setIsUploading(true); + const res = await fetch("/api/upload/retrofit-data", { + method: "POST", + body: formData, + }); + if (!res.ok) throw new Error("Upload failed"); + await fetchFiles(); + alert("✅ Files uploaded successfully!"); + } catch (err) { + console.error(err); + alert("❌ Upload failed"); + } finally { + setIsUploading(false); + } + }; + + return ( +
+

Upload Retrofit Data Files

+ +
+

+ Portfolio ID: {portfolioId} +

+

+ Property ID: {propertyId} +

+
+ + + + + +
+

Uploaded Files

+ {files.length === 0 ? ( +

No files uploaded yet.

+ ) : ( + + + + + + + + + {files.map((file) => ( + + + + + ))} + +
File URLAction
+ {file.s3JsonUrl} + + +
+ )} +
+
+ ); +}; + +export default UploadPage;