From ff4bf9af6e4b5bf9d85fcda564c72e769e698fa0 Mon Sep 17 00:00:00 2001 From: Jun-te kim Date: Thu, 14 Aug 2025 17:32:09 +0000 Subject: [PATCH] save! --- src/app/db/documents_schema/documents.ts | 75 ------- src/app/db/documents_schema/relations.ts | 22 -- .../insert_data_to_uploaded_files/route.ts | 59 ++++++ .../connection.ts} | 6 +- src/app/db/surveyDB/schema/documents.ts | 55 +++++ src/app/db/surveyDB/schema/surveyDB.ts | 20 ++ src/app/db/surveyDB/utils/utility.ts | 29 +++ .../documents/DocumentSection.tsx | 90 +-------- .../[propertyId]/documents/DocumentsTable.tsx | 152 ++------------ .../[propertyId]/documents/UploadModal.tsx | 191 ++++++++++-------- .../[propertyId]/documents/page.tsx | 37 ---- 11 files changed, 293 insertions(+), 443 deletions(-) delete mode 100644 src/app/db/documents_schema/documents.ts delete mode 100644 src/app/db/documents_schema/relations.ts create mode 100644 src/app/db/surveyDB/api/insert_data_to_uploaded_files/route.ts rename src/app/db/{documents_db.ts => surveyDB/connection.ts} (73%) create mode 100644 src/app/db/surveyDB/schema/documents.ts create mode 100644 src/app/db/surveyDB/schema/surveyDB.ts create mode 100644 src/app/db/surveyDB/utils/utility.ts diff --git a/src/app/db/documents_schema/documents.ts b/src/app/db/documents_schema/documents.ts deleted file mode 100644 index 6948d09..0000000 --- a/src/app/db/documents_schema/documents.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { pgTable, uuid, text, timestamp } from "drizzle-orm/pg-core"; -import { pgEnum } from "drizzle-orm/pg-core"; -import { InferModel } from "drizzle-orm"; - -export const reportType: [string, ...string[]] = [ - "QUIDOS_PRESITE_NOTE", - "CHARTED_SURVEYOR_REPORT", - "ENERGY_PERFORMANCE_REPORT", - "U_VALUE_CALCULATOR_REPORT", - "OVERWRITING_U_VALUE_DECLARATION_FORM", - "OSMOSIS_CONDITION_PAS_2035_REPORT", - "DOMNA_CONDITION_PAS_2035_REPORT", -]; - -const reportTypeEnum = pgEnum("report_type", reportType); - -export const companyInfo = pgTable("companyinfo", { - id: uuid("id").primaryKey().defaultRandom(), - address: text("address").notNull(), - tradingName: text("trading_name").notNull(), - postCode: text("post_code").notNull(), - faxNumber: text("fax_number"), - relatedPartyDisclosure: text("related_party_disclosure"), -}); - -// --- assessorInfo table --- -export const assessorInfo = pgTable("assessorinfo", { - id: uuid("id").primaryKey().defaultRandom(), - accreditationNumber: text("accreditation_number").notNull(), - name: text("name").notNull(), - phoneNumber: text("phone_number"), - emailAddress: text("email_address"), - companyId: uuid("company_id").references(() => companyInfo.id), -}); - -// --- buildings table --- -export const buildings = pgTable("buildings", { - id: uuid("id").primaryKey().defaultRandom(), - address: text("address").notNull(), - postcode: text("postcode").notNull(), - uprn: text("UPRN").notNull(), - landlordId: text("landlord_id").notNull(), - domnaId: text("domna_id").notNull(), -}); - -// --- documents table --- -export const documents = pgTable("documents", { - id: uuid("id").primaryKey().defaultRandom(), - - authorId: uuid("assessor_id") - .notNull() - .references(() => assessorInfo.id), - createdAt: timestamp("created_at", { withTimezone: true }).notNull(), - documentType: reportTypeEnum("document_type").notNull(), - - buildingId: uuid("building_id") - .notNull() - .references(() => buildings.id), - targetTable: text("target_table").notNull(), - targetId: uuid("target_id").notNull(), -}); - -export type Building = InferModel; -export type Document = InferModel; -export type AssessorInfo = InferModel; - -export type DocumentWithAuthor = Document & { - author: AssessorInfo; -}; - -export type BuildingWithDocuments = Building & { - documents: DocumentWithAuthor[]; -}; - -export type ReportType = (typeof reportType)[number]; diff --git a/src/app/db/documents_schema/relations.ts b/src/app/db/documents_schema/relations.ts deleted file mode 100644 index 927be39..0000000 --- a/src/app/db/documents_schema/relations.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { pgTable, serial, text, integer } from "drizzle-orm/pg-core"; -import { relations } from "drizzle-orm"; -import { - buildings, - documents, - assessorInfo, -} from "@/app/db/documents_schema/documents"; - -export const buildingsRelations = relations(buildings, ({ many }) => ({ - documents: many(documents), -})); - -export const documentsRelations = relations(documents, ({ one }) => ({ - building: one(buildings, { - fields: [documents.buildingId], - references: [buildings.id], - }), - author: one(assessorInfo, { - fields: [documents.authorId], - references: [assessorInfo.id], - }), -})); diff --git a/src/app/db/surveyDB/api/insert_data_to_uploaded_files/route.ts b/src/app/db/surveyDB/api/insert_data_to_uploaded_files/route.ts new file mode 100644 index 0000000..be51420 --- /dev/null +++ b/src/app/db/surveyDB/api/insert_data_to_uploaded_files/route.ts @@ -0,0 +1,59 @@ +// app/db/surveyDB/api/insert_data_to_uploaded_files/route.ts +import { NextResponse } from "next/server"; +import { z, ZodError } from "zod"; +import { insertUploadedFile } from "../../utils/utility"; // ensure path is correct +import { ReportTypeSchema } from "../../schema/documents"; + +export const runtime = "nodejs"; + +// Helper: "" or whitespace -> undefined (so optional() can drop it) +const emptyToUndefined = (v: unknown) => { + if (typeof v === "string" && v.trim() === "") return undefined; + return v; +}; + +const BodySchema = z.object({ + s3JsonUri: z.preprocess( + emptyToUndefined, + z.string().url().optional() + ), + s3FileUri: z.string().url(), + docType: ReportTypeSchema, + // Required upload timestamp (coerce from ISO string) + s3FileUploadTimestamp: z.coerce.date(), + // Optional JSON timestamp: allow "" -> undefined, then coerce to Date + s3JsonUploadTimestamp: z.preprocess( + emptyToUndefined, + z.coerce.date().optional() + ), + uprn: z.string().min(1), +}); + +export async function POST(req: Request) { + try { + const parsed = BodySchema.parse(await req.json()); + + const row = await insertUploadedFile({ + s3JsonUri: parsed.s3JsonUri, // undefined -> util converts to null + s3FileUri: parsed.s3FileUri, + docType: parsed.docType, + s3FileUploadTimestamp: parsed.s3FileUploadTimestamp, + s3JsonUploadTimestamp: parsed.s3JsonUploadTimestamp, // undefined -> util converts to null + uprn: parsed.uprn, + }); + + return NextResponse.json(row, { status: 201 }); + } catch (e) { + if (e instanceof ZodError) { + return NextResponse.json( + { error: "Invalid payload", details: e.flatten() }, + { status: 400 } + ); + } + console.error(e); + return NextResponse.json( + { error: "Failed to insert uploaded_file table in surveyDB" }, + { status: 500 } + ); + } +} diff --git a/src/app/db/documents_db.ts b/src/app/db/surveyDB/connection.ts similarity index 73% rename from src/app/db/documents_db.ts rename to src/app/db/surveyDB/connection.ts index 9a6af2c..b575361 100644 --- a/src/app/db/documents_db.ts +++ b/src/app/db/surveyDB/connection.ts @@ -1,8 +1,7 @@ // db.ts import { drizzle } from "drizzle-orm/node-postgres"; import { Pool } from "pg"; -import * as documentsSchema from "@/app/db/documents_schema/documents"; -import * as relations from "@/app/db/documents_schema/relations"; +import * as documentsSchema from "@/app/db/surveyDB/schema/surveyDB"; export const pool = new Pool({ host: process.env.DOCUMENTS_DB_HOST, @@ -19,9 +18,8 @@ export const pool = new Pool({ const schema = { ...documentsSchema, - ...relations, }; -export const documentsDB = drizzle(pool, { +export const surveyDB = drizzle(pool, { schema: schema, }); diff --git a/src/app/db/surveyDB/schema/documents.ts b/src/app/db/surveyDB/schema/documents.ts new file mode 100644 index 0000000..4fbf05e --- /dev/null +++ b/src/app/db/surveyDB/schema/documents.ts @@ -0,0 +1,55 @@ +// Enum values copied from the backend (Drizzle + Python) +import { z } from "zod"; + +export const REPORT_TYPES = [ +// "quidos_presite_note", +// "charted_surveyor_report", +// "u_value_calculator_report", +// "overwriting_u_value_declaration_form", + "osmosis_condition_pas_2035_report", +// "warm_homes_condition_pas_2035_report", +// "energy_performance_report_with_data", + "energy_performance_report_summary_information", + "lodgement_xml_needed_for_lodgement_to_like_trademark", + "reduce_xml_needed_to_generate_full_sap_xml", + "full_xml_needed_for_co_ordination", +// "floor_plan", +// "occupancy_assessment", +] as const; + +export type ReportType = (typeof REPORT_TYPES)[number]; + +// Map reportType → title for UI +export const documentTypeTitles: Record = { +// quidos_presite_note: "RdSAP Summary Report", +// charted_surveyor_report: "Chartered Surveyor Report", +// u_value_calculator_report: "U-Value Calculator Report", +// overwriting_u_value_declaration_form: "Overwriting U-Value Declaration Form", + osmosis_condition_pas_2035_report: "Osmosis Condition Report (PAS 2035)", +// warm_homes_condition_pas_2035_report: "Warm Homes PAS 2035 Report", +// energy_performance_report_with_data: "EPC Report With Data", + energy_performance_report_summary_information: "EPC Summary Report", + lodgement_xml_needed_for_lodgement_to_like_trademark: "LIG XML", + reduce_xml_needed_to_generate_full_sap_xml: "RdSAP XML", + full_xml_needed_for_co_ordination: "Full SAP XML", +// floor_plan: "Floor Plan", +// occupancy_assessment: "Occupancy Assessment", +}; + +// Map reportType → accepted file extensions +export const documentTypeFileTypes: Record = { +// quidos_presite_note: ".pdf", +// charted_surveyor_report: ".pdf", +// u_value_calculator_report: ".pdf", +// overwriting_u_value_declaration_form: ".pdf", + osmosis_condition_pas_2035_report: ".pdf", +// warm_homes_condition_pas_2035_report: ".pdf", +// energy_performance_report_with_data: ".pdf", + energy_performance_report_summary_information: ".pdf", + lodgement_xml_needed_for_lodgement_to_like_trademark: ".xml", + reduce_xml_needed_to_generate_full_sap_xml: ".xml", + full_xml_needed_for_co_ordination: ".xml", +// floor_plan: ".pdf", +// occupancy_assessment: ".pdf", +}; +export const ReportTypeSchema = z.enum(REPORT_TYPES); \ No newline at end of file diff --git a/src/app/db/surveyDB/schema/surveyDB.ts b/src/app/db/surveyDB/schema/surveyDB.ts new file mode 100644 index 0000000..1e9f1f5 --- /dev/null +++ b/src/app/db/surveyDB/schema/surveyDB.ts @@ -0,0 +1,20 @@ +import { pgTable, uuid, text, timestamp } from "drizzle-orm/pg-core"; +import { pgEnum } from "drizzle-orm/pg-core"; + +import { REPORT_TYPES } from "./documents"; + +export const docTypeEnum = pgEnum("doc_type_enum", [...REPORT_TYPES]); + +export const uploaded_files = pgTable("uploaded_files", { + id: uuid("id").primaryKey().defaultRandom(), + + s3JsonUri: text("s3_json_uri"), + s3FileUri: text("s3_file_uri").notNull(), + + docType: docTypeEnum("doc_type").notNull(), // enum used here ✅ + + s3FileUploadTimestamp: timestamp("s3_file_upload_timestamp", { withTimezone: true }).notNull(), + s3JsonUploadTimestamp: timestamp("s3_json_upload_timestamp", { withTimezone: true }), + + uprn: text("uprn").notNull(), +}); \ No newline at end of file diff --git a/src/app/db/surveyDB/utils/utility.ts b/src/app/db/surveyDB/utils/utility.ts new file mode 100644 index 0000000..b789c9e --- /dev/null +++ b/src/app/db/surveyDB/utils/utility.ts @@ -0,0 +1,29 @@ +// insertUploadedFile.ts +import { uploaded_files } from "@/app/db/surveyDB/schema/surveyDB"; +import { surveyDB } from "../connection"; +import type { ReportType } from "../schema/documents"; + +export interface UploadedFileInput { + s3JsonUri?: string; // optional + s3FileUri: string; + docType: ReportType; + s3FileUploadTimestamp: Date; + s3JsonUploadTimestamp?: Date; // optional + uprn: string; +} + +export async function insertUploadedFile(data: UploadedFileInput) { + const [newFile] = await surveyDB + .insert(uploaded_files) + .values({ + s3JsonUri: data.s3JsonUri ?? null, // Pass null if missing + s3FileUri: data.s3FileUri, + docType: data.docType, + s3FileUploadTimestamp: data.s3FileUploadTimestamp, + s3JsonUploadTimestamp: data.s3JsonUploadTimestamp ?? null, // Pass null if missing + uprn: data.uprn, + }) + .returning(); + + return newFile; +} \ No newline at end of file diff --git a/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/DocumentSection.tsx b/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/DocumentSection.tsx index 212b39c..3cf21d8 100644 --- a/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/DocumentSection.tsx +++ b/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/DocumentSection.tsx @@ -1,49 +1,20 @@ "use client"; -import React from "react"; +import React, { useState } from "react"; import { TableCell, TableRow } from "@/app/shadcn_components/ui/table"; -import { - DocumentWithAuthor, - ReportType, -} from "@/app/db/documents_schema/documents"; import { BrandButton } from "@/app/components/Buttons"; -import { MenuButton } from "./MenuButton"; -import { useState } from "react"; import { UploadModal } from "./UploadModal"; -import { getPropertyMeta } from "../utils"; +import { documentTypeTitles, type ReportType } from "@/app/db/surveyDB/schema/documents"; -// Descriptions based on the document types -const descriptions: Record = { - QUIDOS_PRESITE_NOTE: - "Pre-site note from Quidos, detailing surveyor's findings", - CHARTED_SURVEYOR_REPORT: "Detailed report by a chartered surveyor", - ENERGY_PERFORMANCE_REPORT: "Energy performance breakdown", - U_VALUE_CALCULATOR_REPORT: "Calculated U-values for walls, floors, and roofs", - OVERWRITING_U_VALUE_DECLARATION_FORM: "Signed form for overwriting U-values", - OSMOSIS_CONDITION_PAS_2035_REPORT: - "Osmosis-generated PAS 2035 Condition Report", - DOMNA_CONDITION_PAS_2035_REPORT: - "Domna-generated PAS 2035 Condition Report" +type Props = { + reportType: ReportType; // <- the only type selector needed + uprn: string; }; -export const DocumentSection = ({ - title, - docs, - sectionKey, - documentType, - uprn, - fileTypes, -}: { - title: string; - docs: DocumentWithAuthor[]; - sectionKey: string; - documentType: ReportType; - uprn: string; - fileTypes: ".xml,.pdf" | ".xml" | ".pdf"; -}) => { +export const DocumentSection: React.FC = ({ reportType, uprn }) => { const [showUploadModal, setShowUploadModal] = useState(false); - const [expanded, setExpanded] = useState(false); - const toggle = () => setExpanded((prev) => !prev); + + const title = documentTypeTitles[reportType]; return ( <> @@ -52,18 +23,7 @@ export const DocumentSection = ({ {title} - - {docs.length > 0 ? ( - - ) : ( - No documents available - )} - + setShowUploadModal(false)} - documentType={documentType} - fileTypes={fileTypes} + documentType={reportType} // <- strong ReportType uprn={uprn} /> - - {expanded && - docs.map((doc) => ( - - - {`Uploaded: ${doc.createdAt.toLocaleDateString("en-GB")}`} -
- {descriptions[doc.documentType] ?? ""} -
-
- - - {`Created by: ${ - doc.author.emailAddress ?? "No Author Information" - }`} - - - - { - console.log("View clicked for", doc.id); - }} - onDelete={() => { - console.log("Delete clicked for", doc.id); - }} - /> - -
- ))} ); }; diff --git a/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/DocumentsTable.tsx b/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/DocumentsTable.tsx index 7fb3e3a..9661527 100644 --- a/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/DocumentsTable.tsx +++ b/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/DocumentsTable.tsx @@ -1,147 +1,27 @@ "use client"; - import React from "react"; -import { - Table, - TableBody, - TableRow, - TableCell, -} from "@/app/shadcn_components/ui/table"; -import { DocumentWithAuthor } from "@/app/db/documents_schema/documents"; +import { Table, TableBody, TableRow, TableCell } from "@/app/shadcn_components/ui/table"; import { DocumentSection } from "./DocumentSection"; +import { type ReportType, REPORT_TYPES, documentTypeFileTypes, documentTypeTitles } from "@/app/db/surveyDB/schema/documents"; -import { useMutation } from "@tanstack/react-query"; - -import { MenuButton } from "./MenuButton"; - -type Props = { - documents: DocumentWithAuthor[]; - uprn: string, - // allowedTypes: (typeof DocumentType)[number][]; // Use the union type for allowedTypes as well -}; - -// Fetch the presigned URL from the API -async function generatePresignedUrl(fileKey: string) { - const response = await fetch("/api/energy-assessment-documents", { - method: "POST", - body: JSON.stringify({ fileKey }), - }); - - if (!response.ok) { - throw new Error("Failed to generate presigned URL"); - } - - const data = await response.json(); - return data.url; -} - -export const DocumentsTable: React.FC = ({ - documents, - uprn, - // allowedTypes, -}) => { - const [expanded, setExpanded] = React.useState(false); - - // Mutation to handle the presigned URL generation - const { mutate: fetchPresignedUrl } = useMutation( - // Use the file key as the argument to generate the URL - async (fileKey: string) => await generatePresignedUrl(fileKey), - { - onSuccess: (url) => { - window.open(url, "_blank"); // Open the file in a new tab - }, - onError: (error) => { - console.error("Error generating presigned URL:", error); - }, - } - ); - - const handleDownload = () => { - // Generate URL and open in new tab - // fetchPresignedUrl(documentLocation); - console.log("Download button clicked"); - }; - - const handleUpload = () => { - // Handle the upload logic here - console.log("Upload button clicked"); - console.log("Junte was here"); - }; - - // We split out the various document types. Filter all of the quidos pre-site notes - const quidosPreSite = documents.filter( - (doc) => doc.documentType === "QUIDOS_PRESITE_NOTE" - ); - - const osmosisConditionReport = documents.filter( - (doc) => doc.documentType === "OSMOSIS_CONDITION_PAS_2035_REPORT" - ); - - const domnaConditionReport = documents.filter( - (doc) => doc.documentType === "DOMNA_CONDITION_PAS_2035_REPORT" - ); - - const floors = documents.filter((doc) => doc.documentType === "FLOOR_PLAN"); - - const occupancy = documents.filter( - (doc) => doc.documentType === "OCCUPANCY_ASSESSMENT" - ); +type Props = { uprn: string }; +export const DocumentsTable: React.FC = ({ uprn }) => { return ( - // Quidos Pre-Site Notes Row - - - - - - - - - - - - - - - - - - - - - - + {REPORT_TYPES.map((rt) => ( + + + + + + + ))}
); -}; +}; \ No newline at end of file diff --git a/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/UploadModal.tsx b/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/UploadModal.tsx index 82060d5..4619430 100644 --- a/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/UploadModal.tsx +++ b/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/UploadModal.tsx @@ -9,25 +9,19 @@ import { DialogFooter, } from "@/app/shadcn_components/ui/dialog"; import { Button } from "@/app/shadcn_components/ui/button"; -import { ReportType } from "@/app/db/documents_schema/documents"; import { Input } from "@/app/shadcn_components/ui/input"; -import { useParams } from 'next/navigation'; import { useState } from "react"; import { uploadFileToS3 } from "@/app/utils/s3"; -import { getPropertyMeta } from "../utils"; +import { documentTypeFileTypes, documentTypeTitles, ReportType } from "@/app/db/surveyDB/schema/documents"; type UploadModalProps = { open: boolean; onClose: () => void; - documentType: string; - uprn:string; - fileTypes: ".xml,.pdf" | ".xml" | ".pdf"; -}; - -const titles: Record = { - QUIDOS_PRESITE_NOTE: "RdSAP Summary Report", + documentType: ReportType; // <- strongly typed + uprn: string; }; +// Fetch presigned URL from API async function generatePresignedUrls({ path, contentType, @@ -37,88 +31,111 @@ async function generatePresignedUrls({ contentType: string; expiresInSeconds: number; }) { - const body = JSON.stringify({ - path: path, - expiresInSeconds: expiresInSeconds, - contentType: contentType - }); - const presignedResponse = await fetch("/api/upload/retrofit-energy-assessments", { + const body = JSON.stringify({ path, expiresInSeconds, contentType }); + const res = await fetch("/api/upload/retrofit-energy-assessments", { method: "POST", - headers: { "Content-Type": "application/json" }, + headers: { "Content-Type": "application/json" }, body, }); - - if (!presignedResponse.ok) { - throw new Error("Network response was not ok"); - } - - return presignedResponse.json(); + if (!res.ok) throw new Error("Failed to get presigned URL"); + return res.json() as Promise<{ url: string }>; } -export const UploadModal = ({ - open, - onClose, - documentType, - uprn, - fileTypes = ".xml,.pdf", -}: UploadModalProps) => { +// Decide content-type from file extension +function contentTypeFor(ext: string): string { + const e = ext.toLowerCase(); + if (e === "pdf") return "application/pdf"; + if (e === "xml") return "application/xml"; + return "application/octet-stream"; +} + +export const UploadModal = ({ open, onClose, documentType, uprn }: UploadModalProps) => { const [uploadFiles, setUploadFiles] = useState([]); - const [buttonDisabled, setButtonDisabled] = useState(true); + const [submitting, setSubmitting] = useState(false); - async function handleS3Upload() { - - const timestamp = new Date() - .toISOString() - .replace(/[-:]/g, "") - .replace("T", "_") - .split(".")[0]; // remove milliseconds - - const fileExtension = uploadFiles[0].name.split(".").pop() || "pdf"; - const s3Key = `documents/${uprn}/${documentType}/${timestamp}.${fileExtension}`; - - console.log("Get Presigned url in a specific bucket location") - const { url } = await generatePresignedUrls({ - path: s3Key, // path in bucket - contentType: "application/pdf", - expiresInSeconds: 5 * 60, - }); - - console.log("Retrievied url ", url); - console.log("uploading file..."); - await uploadFileToS3({ - presignedUrl: url, - file: uploadFiles[0], - contentType: "application/pdf" - }) - console.log("uploading Completed!!! Check aws"); - - onClose(); //probably khalim call back to update the front end properl - } + const accepted = documentTypeFileTypes[documentType]; // ".pdf" | ".xml" | ".xml,.pdf" + const title = documentTypeTitles[documentType]; function handleInputOnChange(e: React.ChangeEvent) { - if (e.target.files) { - const filesArray = Array.from(e.target.files); - const extensions = filesArray.map((file) => - file.name.split(".").pop()?.toLowerCase() - ); - // The valid extension are defined by filetypes e.g. ".xml,.pdf" so we split on the comma - const validExtensions = fileTypes - .split(",") - .map((ext) => ext.replace(".", "")); + if (!e.target.files?.length) { + setUploadFiles([]); + return; + } + const file = e.target.files[0]; - // Check if the files have valid extensions - const isValid = extensions.every((ext) => - validExtensions.includes(ext || "") - ); + // Validate by extension against accepted + const ext = (file.name.split(".").pop() || "").toLowerCase(); + const validExtensions = accepted.split(",").map((x) => x.replace(".", "")); + const isValid = validExtensions.includes(ext); - if (isValid) { - setUploadFiles(filesArray); - setButtonDisabled(false); - } else { - setButtonDisabled(true); + if (!isValid) { + setUploadFiles([]); + return; + } + setUploadFiles([file]); + } + + async function handleS3Upload() { + if (!uploadFiles.length) return; + setSubmitting(true); + + try { + // Timestamp like YYYYMMDD_HHMMSS + const timestamp = new Date() + .toISOString() + .replace(/[-:]/g, "") + .replace("T", "_") + .split(".")[0]; + + const file = uploadFiles[0]; + const ext = (file.name.split(".").pop() || "").toLowerCase(); + const ct = contentTypeFor(ext); + + const s3Key = `documents/${uprn}/${documentType}/${timestamp}.${ext}`; + + // 1) Get presigned URL + const { url } = await generatePresignedUrls({ + path: s3Key, + contentType: ct, + expiresInSeconds: 5 * 60, + }); + + // 2) Upload to S3 via presigned URL + await uploadFileToS3({ + presignedUrl: url, + file, + contentType: ct, + }); + + // 3) Record in DB (store durable HTTPS URL without query params) + const presigned = new URL(url); + const s3FileUri = presigned.origin + presigned.pathname; + + const res = await fetch("/db/surveyDB/api/insert_data_to_uploaded_files", { + // If you move your route to the conventional path, change to "/api/uploaded-files" + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + s3FileUri, + docType: documentType, // ReportType value + uprn, + s3FileUploadTimestamp: new Date().toISOString(), + }), + }); + + if (!res.ok) { + const err = await res.json().catch(() => ({})); + console.error("DB insert failed:", err); + throw new Error("Failed to insert uploaded file record"); } - } else { - setButtonDisabled(true); + + // Success — close the dialog and let parent refresh UI + onClose(); + } catch (err) { + console.error(err); + // You can show a toast here if you have one + } finally { + setSubmitting(false); } } @@ -128,15 +145,14 @@ export const UploadModal = ({ Upload Document - Upload an {titles[documentType]}. Once uploaded, - automated extraction can begin. + Upload a {title}. Once uploaded, automated extraction can begin.
- - diff --git a/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/page.tsx b/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/page.tsx index c8feedc..e9d61a9 100644 --- a/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/page.tsx +++ b/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/page.tsx @@ -1,42 +1,7 @@ -import { documentsDB } from "@/app/db/documents_db"; -import { - buildings, - DocumentWithAuthor, - BuildingWithDocuments, -} from "@/app/db/documents_schema/documents"; import { getPropertyMeta } from "@/app/portfolio/[slug]/building-passport/[propertyId]/utils"; import { eq } from "drizzle-orm"; import { DocumentsTable } from "./DocumentsTable"; -async function getDocuments( - uprn: number -): Promise { - const result = documentsDB.query.buildings.findFirst({ - where: eq(buildings.uprn, String(uprn)), - with: { - documents: { - with: { - author: true, // Include author information - there will only be one author per document - }, - }, - }, - }); - - // If we have no buildings, we return an empty object - if (!result) { - return { - id: "", - address: "", - postcode: "", - uprn: String(uprn), - landlordId: "", - domnaId: "", - documents: [] as DocumentWithAuthor[], - } as BuildingWithDocuments; - } - - return result; -} export default async function DocumentsPage( props: { @@ -51,7 +16,6 @@ export default async function DocumentsPage( } const propertyMeta = await getPropertyMeta(propertyId); - const documents = await getDocuments(propertyMeta.uprn); return ( <> @@ -61,7 +25,6 @@ export default async function DocumentsPage(