From 8ada57165e26604517bae0b68cce983d72daba81 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Mon, 10 Nov 2025 20:53:28 +0000 Subject: [PATCH 1/3] changing name --- src/app/email_templates/magic_link.ts | 8 ++++---- src/app/onboarding/page.tsx | 2 +- src/app/portfolio/[slug]/(portfolio)/reporting/page.tsx | 0 src/app/portfolio/[slug]/(portfolio)/reporting/utils.ts | 1 + src/app/portfolio/[slug]/plan-loading/page.tsx | 2 +- .../[slug]/remote-assessment/RemoteAssessmentClient.tsx | 2 +- tailwind.config.js | 6 +++--- 7 files changed, 11 insertions(+), 10 deletions(-) create mode 100644 src/app/portfolio/[slug]/(portfolio)/reporting/page.tsx create mode 100644 src/app/portfolio/[slug]/(portfolio)/reporting/utils.ts diff --git a/src/app/email_templates/magic_link.ts b/src/app/email_templates/magic_link.ts index 33878a8..12d3f43 100644 --- a/src/app/email_templates/magic_link.ts +++ b/src/app/email_templates/magic_link.ts @@ -25,7 +25,7 @@ export async function MagicLinksEmail({ const result = await transport.sendMail({ to: identifier, from: provider.from, - subject: "Your secure Domna IQ sign-in link", + subject: "Your secure Ara sign-in link", text: plainText({ url, host }), html: domnaHtml({ url, host, brandColor, accentColor, brown, background }), }); @@ -70,13 +70,13 @@ function domnaHtml({ -

Welcome back to Domna IQ

+

Welcome back to Ara by Domna

Click below to securely sign in to your account and continue your retrofit journey.

- Sign in to Domna IQ + Sign in to Ara

If you didn’t request this email, you can safely ignore it. @@ -94,5 +94,5 @@ function domnaHtml({ } function plainText({ url, host }: { url: string; host: string }) { - return `Sign in to Domna IQ\n${url}\n\nIf you did not request this email, you can safely ignore it.\n`; + return `Sign in to Ara by Domna\n${url}\n\nIf you did not request this email, you can safely ignore it.\n`; } diff --git a/src/app/onboarding/page.tsx b/src/app/onboarding/page.tsx index 756014f..fdfabdc 100644 --- a/src/app/onboarding/page.tsx +++ b/src/app/onboarding/page.tsx @@ -176,7 +176,7 @@ export default function OnboardingPage() {

-

Welcome to Domna IQ

+

Welcome to Ara

Help us get to know you so we can tailor your experience.

diff --git a/src/app/portfolio/[slug]/(portfolio)/reporting/page.tsx b/src/app/portfolio/[slug]/(portfolio)/reporting/page.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/app/portfolio/[slug]/(portfolio)/reporting/utils.ts b/src/app/portfolio/[slug]/(portfolio)/reporting/utils.ts new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/app/portfolio/[slug]/(portfolio)/reporting/utils.ts @@ -0,0 +1 @@ + diff --git a/src/app/portfolio/[slug]/plan-loading/page.tsx b/src/app/portfolio/[slug]/plan-loading/page.tsx index be2f040..2e721e7 100644 --- a/src/app/portfolio/[slug]/plan-loading/page.tsx +++ b/src/app/portfolio/[slug]/plan-loading/page.tsx @@ -144,7 +144,7 @@ export default function LoadingPage(props: { Building your retrofit plan

- Domna IQ is analysing your data and generating your plan summary. + Ara is analysing your data and generating your plan summary.

diff --git a/src/app/portfolio/[slug]/remote-assessment/RemoteAssessmentClient.tsx b/src/app/portfolio/[slug]/remote-assessment/RemoteAssessmentClient.tsx index 389d471..e2552bc 100644 --- a/src/app/portfolio/[slug]/remote-assessment/RemoteAssessmentClient.tsx +++ b/src/app/portfolio/[slug]/remote-assessment/RemoteAssessmentClient.tsx @@ -67,7 +67,7 @@ export default function RemoteAssessmentClient({ {/* Hero text split to use horizontal space better */}

- Domna IQ analyses your property data, models retrofit options, and + Ara analyses your property data, models retrofit options, and estimates potential funding — all without an on-site survey.

diff --git a/tailwind.config.js b/tailwind.config.js index f9cbfaa..31dda4f 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -26,9 +26,9 @@ module.exports = { "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", "gradient-conic": "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", - "domna-gradient": - "linear-gradient(135deg, #14163d 0%, #2d348f 45%, #3943b7 70%, #eff6fc 100%)", - }, + "domna-gradient": + "linear-gradient(135deg, #14163d 0%, #2d348f 45%, #3943b7 70%, #eff6fc 100%)", + }, colors: { tremor: { brand: { From ad2a46c2f631a03b4bb7b57900d554fa85134b30 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Mon, 10 Nov 2025 22:33:37 +0000 Subject: [PATCH 2/3] placeholder page --- .../[slug]/(portfolio)/reporting/page.tsx | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/app/portfolio/[slug]/(portfolio)/reporting/page.tsx b/src/app/portfolio/[slug]/(portfolio)/reporting/page.tsx index e69de29..2e57954 100644 --- a/src/app/portfolio/[slug]/(portfolio)/reporting/page.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/reporting/page.tsx @@ -0,0 +1,14 @@ +export default async function ReportingPage(props: { + params: Promise<{ slug: string }>; +}) { + const params = await props.params; + const portfolioId = params.slug; + + return ( + <> +

+
Reporting Page for portfolio: {portfolioId}
+
+ + ); +} From 23f6516c20800882739b806eee7a1a2e344a757c Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Wed, 12 Nov 2025 21:28:11 +0000 Subject: [PATCH 3/3] doc storage --- src/app/db/schema/files_from_surveyor.ts | 2 + .../documents/DocumentSection.tsx | 252 +++++++++++++----- .../[propertyId]/documents/DocumentsTable.tsx | 7 +- .../documents/GenericDocumentsTable.tsx | 38 +++ .../[propertyId]/documents/page.tsx | 38 ++- .../[propertyId]/upload/page.tsx | 1 - 6 files changed, 258 insertions(+), 80 deletions(-) create mode 100644 src/app/portfolio/[slug]/building-passport/[propertyId]/documents/GenericDocumentsTable.tsx diff --git a/src/app/db/schema/files_from_surveyor.ts b/src/app/db/schema/files_from_surveyor.ts index 9bf3d5f..84bb906 100644 --- a/src/app/db/schema/files_from_surveyor.ts +++ b/src/app/db/schema/files_from_surveyor.ts @@ -13,3 +13,5 @@ export const filesFromSurveyor = pgTable("files_from_surveyor", { s3JsonUrl: text("s3_json_url").notNull(), uploadedAt: timestamp("uploaded_at").notNull().defaultNow(), }); + +export type FilesFromSurveyor = typeof filesFromSurveyor.$inferSelect; 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 0436020..b1226f2 100644 --- a/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/DocumentSection.tsx +++ b/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/DocumentSection.tsx @@ -4,82 +4,186 @@ import React from "react"; import { TableCell, TableRow } from "@/app/shadcn_components/ui/table"; import { BrandButton } from "@/app/components/Buttons"; import { UploadModal } from "./UploadModal"; -import { documentTypeTitles, type ReportType } from "@/app/db/surveyDB/schema/documents"; -import type { getUploadedFiles, getUploadedFile } from "@/app/db/surveyDB/schema/surveyDB"; +import { + documentTypeTitles, + type ReportType, +} from "@/app/db/surveyDB/schema/documents"; +import type { getUploadedFiles } from "@/app/db/surveyDB/schema/surveyDB"; +import type { FilesFromSurveyor } from "@/app/db/schema/files_from_surveyor"; +import { useMutation } from "@tanstack/react-query"; type Props = { - reportType: ReportType; + reportType: ReportType | null; uprn: string; - files: getUploadedFiles; + files: getUploadedFiles | FilesFromSurveyor[]; }; -export const DocumentSection: React.FC = ({ reportType, uprn, files }) => { +type AnyFile = Record; + +const DATE_KEYS = [ + "uploadedAt", + "s3FileUploadTimestamp", + "createdAt", + "updatedAt", + "timestamp", +] as const; + +const URL_KEYS = ["s3FileUri", "s3JsonUrl", "url", "href", "link"] as const; + +function pickFirst( + obj: AnyFile, + keys: readonly string[] +): T | undefined { + for (const k of keys) { + if (k in obj && obj[k] != null) return obj[k] as T; + } + return undefined; +} + +function asDate(val: unknown): Date | undefined { + if (!val) return undefined; + if (val instanceof Date) return val; + const d = new Date(String(val)); + return isNaN(d.getTime()) ? undefined : d; +} + +type NormalizedFile = { + url?: string; + uploadedAt?: Date; + original: AnyFile; +}; + +const NAME_KEYS = ["name", "filename", "fileName", "title"] as const; + +function normalizeFile(f: AnyFile): NormalizedFile & { name?: string } { + const url = pickFirst(f, URL_KEYS); + const dateRaw = pickFirst(f, DATE_KEYS); + const uploadedAt = asDate(dateRaw); + + let name = pickFirst(f, NAME_KEYS); + if (!name && url) { + // Extract last path segment from URL + const parts = url.split("/"); + name = decodeURIComponent(parts[parts.length - 1]); + } + + return { url, uploadedAt, name, original: f }; +} + +function getLatestNormalized(files: AnyFile[]): NormalizedFile | null { + if (!Array.isArray(files) || files.length === 0) return null; + const normalized = files.map(normalizeFile); + + // Prefer items that have a valid uploadedAt. If none have a date, fall back to first. + const withDate = normalized.filter((n) => n.uploadedAt); + if (withDate.length === 0) return normalized[0]; + + return withDate.reduce((acc, cur) => + cur.uploadedAt!.getTime() > acc.uploadedAt!.getTime() ? cur : acc + ); +} + +function formatWhen(d: Date | string) { + const date = new Date(d); + const yyyy = date.getUTCFullYear(); + const mm = String(date.getUTCMonth() + 1).padStart(2, "0"); + const dd = String(date.getUTCDate()).padStart(2, "0"); + const hh = String(date.getUTCHours()).padStart(2, "0"); + const min = String(date.getUTCMinutes()).padStart(2, "0"); + return `${dd} ${["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"][date.getUTCMonth()]} ${yyyy}, ${hh}:${min} UTC`; +} + +export function usePresignedUrl() { + return useMutation({ + mutationFn: async (key: string) => { + const res = await fetch("/api/sign-s3-url", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ key }), + }); + + if (!res.ok) { + throw new Error("Failed to get presigned URL"); + } + + const data = await res.json(); + return data.url as string; + }, + }); +} + +export const DocumentSection: React.FC = ({ + reportType, + uprn, + files, +}) => { const [showUploadModal, setShowUploadModal] = React.useState(false); const router = useRouter(); - console.log("files", files) + const latest = React.useMemo( + () => getLatestNormalized(files as unknown as AnyFile[]), + [files] + ); - // const latestFile = React.useMemo(() => { - // if (!files?.length) return null; - // return files.reduce((acc, cur) => { - // const accTime = new Date(acc.s3FileUploadTimestamp as any).getTime(); - // const curTime = new Date(cur.s3FileUploadTimestamp as any).getTime(); - // return curTime > accTime ? cur : acc; - // }, files[0]); - // }, [files]); + const title = + reportType && documentTypeTitles[reportType] !== "Other" + ? documentTypeTitles[reportType] + : latest?.url + ? decodeURIComponent(latest.url.split("/").pop() || "") + : "Other"; -// const latestFile = React.useMemo(() => { -// console.log("Recomputing latestFile from", files); -// if (!Array.isArray(files) || files.length === 0) return null; - -// return files.reduce((acc, cur) => { -// const accTime = new Date(acc.s3FileUploadTimestamp).getTime(); -// const curTime = new Date(cur.s3FileUploadTimestamp).getTime(); -// return curTime > accTime ? cur : acc; -// }, files[0]); -// }, [JSON.stringify(files)]); - -const latestFile = files.length > 0 ? files.reduce((acc, cur) => { - return new Date(cur.s3FileUploadTimestamp).getTime() > new Date(acc.s3FileUploadTimestamp).getTime() ? cur : acc; -}) : null; - - const formatWhen = (d: string | Date) => - new Intl.DateTimeFormat(undefined, { - year: "numeric", - month: "short", - day: "2-digit", - hour: "2-digit", - minute: "2-digit", - timeZone: "UTC" - }).format(new Date(d)); - - const title = documentTypeTitles[reportType]; const count = files.length; - - const latestUpload = latestFile ? String(formatWhen(latestFile.s3FileUploadTimestamp)) : ""; - // console.log("latestFile", latestFile) + + const latestUpload = latest?.uploadedAt ? formatWhen(latest.uploadedAt) : ""; + + const { mutateAsync: getSignedUrl, isPending } = usePresignedUrl(); + + const handleViewFile = async (fileUrl: string) => { + try { + // Extract S3 key from the full URL + const bucketDomain = "retrofit-data-dev.s3.eu-west-2.amazonaws.com/"; + const key = fileUrl.split(bucketDomain)[1]; + if (!key) throw new Error("Invalid S3 file URL"); + + const signedUrl = await getSignedUrl(key); + window.open(signedUrl, "_blank", "noopener,noreferrer"); + } catch (err) { + console.error("Failed to open presigned URL:", err); + alert("Could not open file."); + } + }; + return ( <> - + {title} - {latestFile ? ( + {latest ? (
- - View latest file - - {/*
- uploaded {latestUpload} -
*/} + {latest.url ? ( + + ) : ( + Latest file has no URL + )} + {latestUpload && ( +
+ uploaded {latestUpload} +
+ )}
{count} file{count !== 1 && "s"} on record @@ -90,22 +194,26 @@ const latestFile = files.length > 0 ? files.reduce((acc, cur) => { )} - - setShowUploadModal(true)} - backgroundColor="brandblue" - /> - { - setShowUploadModal(false); - router.refresh(); - }} - documentType={reportType} - uprn={uprn} - /> - + {reportType ? ( + + setShowUploadModal(true)} + backgroundColor="brandblue" + /> + { + setShowUploadModal(false); + router.refresh(); + }} + documentType={reportType} + uprn={uprn} + /> + + ) : ( + + )} ); 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 a6956fc..b85c421 100644 --- a/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/DocumentsTable.tsx +++ b/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/DocumentsTable.tsx @@ -10,7 +10,7 @@ import { DocumentSection } from "./DocumentSection"; import { type ReportType, REPORT_TYPES, - dbLabelToReportType, // <-- import the map + dbLabelToReportType, } from "@/app/db/surveyDB/schema/documents"; import type { getUploadedFile } from "@/app/db/surveyDB/schema/surveyDB"; @@ -27,7 +27,7 @@ export const DocumentsTable: React.FC = ({ const map: Partial> = {}; for (const file of uploadedFilesData ?? []) { - const uiKey = dbLabelToReportType[file.docType]; // map DB → UI + const uiKey = dbLabelToReportType[file.docType]; // map DB → UI. We may not have a docType for surveyor files if (!uiKey) continue; // unknown/legacy type? skip safely (map[uiKey] ??= []).push(file); @@ -45,14 +45,11 @@ export const DocumentsTable: React.FC = ({ return map; }, [uploadedFilesData]); - console.log("filesByType", filesByType); - return ( {REPORT_TYPES.map((reportType) => { const filesForType = filesByType[reportType] ?? []; - console.log("reportType", reportType); return ( = ({ uprn, files }) => { + return ( +
+ + {files.map((file, index) => { + return ( + + + + + + + ); + })} + +
+ ); +}; 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 799142a..4901fa0 100644 --- a/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/page.tsx +++ b/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/page.tsx @@ -1,7 +1,11 @@ import { getPropertyMeta } from "@/app/portfolio/[slug]/building-passport/[propertyId]/utils"; -import { eq } from "drizzle-orm"; +import { and, eq } from "drizzle-orm"; import { DocumentsTable } from "./DocumentsTable"; +import { GenericDocumentsTable } from "./GenericDocumentsTable"; import { surveyDB } from "@/app/db/surveyDB/connection"; +import { db } from "@/app/db/db"; +import { filesFromSurveyor } from "@/app/db/schema/files_from_surveyor"; +import type { FilesFromSurveyor } from "@/app/db/schema/files_from_surveyor"; import { uploadedFiles } from "@/app/db/surveyDB/schema/surveyDB"; import { type getUploadedFiles } from "@/app/db/surveyDB/schema/surveyDB"; @@ -13,6 +17,23 @@ async function getDocuments(uprn: number): Promise { return result; } +async function getSurveyorDocuments( + portfolioId: string, + propertyId: string +): Promise { + const files = await db + .select() + .from(filesFromSurveyor) + .where( + and( + eq(filesFromSurveyor.portfolioId, BigInt(portfolioId)), + eq(filesFromSurveyor.propertyId, BigInt(propertyId)) + ) + ); + + return files; +} + export default async function DocumentsPage(props: { params: Promise<{ slug: string; propertyId: string }>; }) { @@ -26,7 +47,8 @@ export default async function DocumentsPage(props: { const propertyMeta = await getPropertyMeta(propertyId); const uploadedFiles = await getDocuments(propertyMeta.uprn); - console.log("Uploaded files:", uploadedFiles); + // We also fetch surveyor documents, which is a temp solution + const surveyorDocuments = await getSurveyorDocuments(params.slug, propertyId); return ( <> @@ -40,6 +62,18 @@ export default async function DocumentsPage(props: { uploadedFilesData={uploadedFiles} />
+
+ +
+ Surveyor Uploaded Documents +
+
+ +
+
Coordination diff --git a/src/app/portfolio/[slug]/building-passport/[propertyId]/upload/page.tsx b/src/app/portfolio/[slug]/building-passport/[propertyId]/upload/page.tsx index 5d74ef0..dbb9ef2 100644 --- a/src/app/portfolio/[slug]/building-passport/[propertyId]/upload/page.tsx +++ b/src/app/portfolio/[slug]/building-passport/[propertyId]/upload/page.tsx @@ -26,7 +26,6 @@ const UploadPage: React.FC = () => { ); if (res.ok) { const data = await res.json(); - console.log(data, "hello"); setFiles(data.files); } };