From f04fb8f0265fd15dae9adcc330906a997d246cb1 Mon Sep 17 00:00:00 2001
From: Khalim Conn-Kowlessar
Date: Fri, 17 Apr 2026 18:49:10 +0000
Subject: [PATCH] updated install documents ui
---
.../live-tracking/property-documents/route.ts | 2 +
.../your-projects/live/DocumentTable.tsx | 101 +++++++---
.../live/DocumentTableColumns.tsx | 77 ++++++--
.../your-projects/live/PropertyDrawer.tsx | 182 +++++++++++-------
.../your-projects/live/PropertyTable.tsx | 6 +-
.../live/PropertyTableColumns.tsx | 4 +-
.../(portfolio)/your-projects/live/page.tsx | 53 ++++-
.../(portfolio)/your-projects/live/types.ts | 27 ++-
8 files changed, 327 insertions(+), 125 deletions(-)
diff --git a/src/app/api/live-tracking/property-documents/route.ts b/src/app/api/live-tracking/property-documents/route.ts
index 0e73dc6..270d826 100644
--- a/src/app/api/live-tracking/property-documents/route.ts
+++ b/src/app/api/live-tracking/property-documents/route.ts
@@ -32,6 +32,7 @@ export async function GET(req: Request) {
source: uploadedFiles.source,
uprn: uploadedFiles.uprn,
landlordPropertyId: uploadedFiles.landlordPropertyId,
+ measureName: uploadedFiles.measureName,
})
.from(uploadedFiles)
.where(condition);
@@ -45,6 +46,7 @@ export async function GET(req: Request) {
s3UploadTimestamp: row.s3UploadTimestamp.toISOString(),
uprn: row.uprn !== null ? String(row.uprn) : null,
landlordPropertyId: row.landlordPropertyId,
+ measureName: row.measureName ?? null,
}));
return NextResponse.json(documents);
diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/DocumentTable.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/DocumentTable.tsx
index 195b534..01baed9 100644
--- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/DocumentTable.tsx
+++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/DocumentTable.tsx
@@ -31,7 +31,8 @@ import { createDocumentTableColumns } from "./DocumentTableColumns";
import ContractorUploadModal from "./ContractorUploadModal";
import type { ClassifiedDeal, DocStatusMap, PortfolioCapabilityType } from "./types";
-type SurveyStatusFilter = "all" | "none" | "partial" | "complete";
+type RetroAssessmentFilter = "all" | "none" | "partial" | "complete";
+type InstallStatusFilter = "all" | "none" | "hasDocs" | "partial" | "complete";
interface DocumentTableProps {
data: ClassifiedDeal[];
@@ -54,7 +55,8 @@ function escapeCell(value: unknown): string {
export default function DocumentTable({ data, onOpenDrawer, docStatusMap, portfolioId, userCapability }: DocumentTableProps) {
const [globalFilter, setGlobalFilter] = useState("");
- const [surveyStatusFilter, setSurveyStatusFilter] = useState("all");
+ const [retroAssessmentFilter, setRetroAssessmentFilter] = useState("all");
+ const [installStatusFilter, setInstallStatusFilter] = useState("all");
const [sorting, setSorting] = useState([]);
const [pagination, setPagination] = useState({
pageIndex: 0,
@@ -63,15 +65,26 @@ export default function DocumentTable({ data, onOpenDrawer, docStatusMap, portfo
const [uploadDeal, setUploadDeal] = useState(null);
const filteredData = useMemo(() => {
- if (surveyStatusFilter === "all") return data;
return data.filter((d) => {
const status = d.uprn ? docStatusMap[d.uprn] : undefined;
- if (surveyStatusFilter === "none") return !status || !status.hasDocs;
- if (surveyStatusFilter === "partial") return !!status?.hasDocs && !status.isComplete;
- if (surveyStatusFilter === "complete") return !!status?.isComplete;
+
+ if (retroAssessmentFilter !== "all") {
+ if (retroAssessmentFilter === "none" && !(!status || !status.hasSurveyDocs)) return false;
+ if (retroAssessmentFilter === "partial" && !(status?.hasSurveyDocs && !status.isSurveyComplete)) return false;
+ if (retroAssessmentFilter === "complete" && !status?.isSurveyComplete) return false;
+ }
+
+ if (installStatusFilter !== "all") {
+ const s = status?.installStatus ?? "none";
+ if (installStatusFilter === "none" && s !== "none") return false;
+ if (installStatusFilter === "hasDocs" && s !== "hasDocs") return false;
+ if (installStatusFilter === "partial" && s !== "partial") return false;
+ if (installStatusFilter === "complete" && s !== "all") return false;
+ }
+
return true;
});
- }, [data, surveyStatusFilter, docStatusMap]);
+ }, [data, retroAssessmentFilter, installStatusFilter, docStatusMap]);
const columns = useMemo(
() => createDocumentTableColumns(
@@ -98,19 +111,27 @@ export default function DocumentTable({ data, onOpenDrawer, docStatusMap, portfo
const downloadCsv = () => {
const rows = table.getFilteredRowModel().rows;
- const header = "Address,Landlord ID,Survey Status";
+ const header = "Address,Landlord ID,Retrofit Assessment Status,Install Docs Status";
const body = rows
.map((row) => {
const status = row.original.uprn ? docStatusMap[row.original.uprn] : undefined;
- const surveyStatus = status?.isComplete
+ const retroStatus = status?.isSurveyComplete
? "Complete"
- : status?.hasDocs
+ : status?.hasSurveyDocs
? "Partial"
: "No Docs";
+ const installStatusMap: Record = {
+ all: "All Measures",
+ partial: "Some Measures",
+ hasDocs: "Has Docs",
+ none: "No Docs",
+ };
+ const installStatus = installStatusMap[status?.installStatus ?? "none"];
return [
escapeCell(row.original.dealname),
escapeCell(row.original.landlordPropertyId),
- surveyStatus,
+ retroStatus,
+ installStatus,
].join(",");
})
.join("\n");
@@ -127,11 +148,19 @@ export default function DocumentTable({ data, onOpenDrawer, docStatusMap, portfo
const currentPage = table.getState().pagination.pageIndex + 1;
const totalFiltered = table.getFilteredRowModel().rows.length;
- const surveyStatusLabel: Record = {
- all: "All statuses",
- none: "No Survey Docs",
- partial: "Partial Survey Docs",
- complete: "Complete Survey Docs",
+ const retroAssessmentLabel: Record = {
+ all: "All retrofit statuses",
+ none: "No Retrofit Docs",
+ partial: "Partial Retrofit Docs",
+ complete: "Complete Retrofit Docs",
+ };
+
+ const installStatusLabel: Record = {
+ all: "All install statuses",
+ none: "No Install Docs",
+ hasDocs: "Has Install Docs",
+ partial: "Some Measures",
+ complete: "All Measures",
};
return (
@@ -152,22 +181,42 @@ export default function DocumentTable({ data, onOpenDrawer, docStatusMap, portfo
/>
- {/* Survey status filter */}
+ {/* Retrofit assessment filter */}
{
- setSurveyStatusFilter(v as SurveyStatusFilter);
+ setRetroAssessmentFilter(v as RetroAssessmentFilter);
setPagination((p) => ({ ...p, pageIndex: 0 }));
}}
>
-
- {surveyStatusLabel[surveyStatusFilter]}
+
+ {retroAssessmentLabel[retroAssessmentFilter]}
- All statuses
- No Survey Docs
- Partial Survey Docs
- Complete Survey Docs
+ All retrofit statuses
+ No Retrofit Docs
+ Partial Retrofit Docs
+ Complete Retrofit Docs
+
+
+
+ {/* Install docs filter */}
+ {
+ setInstallStatusFilter(v as InstallStatusFilter);
+ setPagination((p) => ({ ...p, pageIndex: 0 }));
+ }}
+ >
+
+ {installStatusLabel[installStatusFilter]}
+
+
+ All install statuses
+ No Install Docs
+ Has Install Docs
+ Some Measures
+ All Measures
@@ -192,7 +241,7 @@ export default function DocumentTable({ data, onOpenDrawer, docStatusMap, portfo
{" "}
of{" "}
{totalFiltered} {" "}
- {surveyStatusFilter !== "all" ? `(${surveyStatusLabel[surveyStatusFilter].toLowerCase()}) ` : ""}
+ {(retroAssessmentFilter !== "all" || installStatusFilter !== "all") ? "(filtered) " : ""}
propert{totalFiltered === 1 ? "y" : "ies"}
diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/DocumentTableColumns.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/DocumentTableColumns.tsx
index 1b762c9..d630130 100644
--- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/DocumentTableColumns.tsx
+++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/DocumentTableColumns.tsx
@@ -1,7 +1,7 @@
"use client";
import { ColumnDef } from "@tanstack/react-table";
-import { ArrowUpDown, CheckCircle2, AlertCircle, FileX, Upload } from "lucide-react";
+import { ArrowUpDown, CheckCircle2, AlertCircle, FileX, Upload, Package } from "lucide-react";
import type { ClassifiedDeal, DocStatusMap, DocStatus } from "./types";
function SortableHeader({
@@ -22,8 +22,8 @@ function SortableHeader({
);
}
-function SurveyStatusBadge({ status }: { status: DocStatus | undefined }) {
- if (status?.isComplete) {
+function RetroAssessmentBadge({ status }: { status: DocStatus | undefined }) {
+ if (status?.isSurveyComplete) {
return (
@@ -31,7 +31,7 @@ function SurveyStatusBadge({ status }: { status: DocStatus | undefined }) {
);
}
- if (status?.hasDocs) {
+ if (status?.hasSurveyDocs) {
return (
@@ -47,6 +47,40 @@ function SurveyStatusBadge({ status }: { status: DocStatus | undefined }) {
);
}
+function InstallDocsBadge({ status }: { status: DocStatus | undefined }) {
+ const installStatus = status?.installStatus ?? "none";
+ if (installStatus === "all") {
+ return (
+
+
+ All Measures
+
+ );
+ }
+ if (installStatus === "partial") {
+ return (
+
+
+ Some Measures
+
+ );
+ }
+ if (installStatus === "hasDocs") {
+ return (
+
+
+ Has Docs
+
+ );
+ }
+ return (
+
+
+ No Docs
+
+ );
+}
+
export function createDocumentTableColumns(
onOpenDrawer: (uprn: string | null, landlordPropertyId: string | null, dealname: string | null) => void,
docStatusMap: DocStatusMap = {},
@@ -81,19 +115,38 @@ export function createDocumentTableColumns(
enableHiding: false,
},
- // ── Survey Status ─────────────────────────────────────────────────────
+ // ── Retrofit Assessment Docs Status ───────────────────────────────────
{
- id: "surveyStatus",
+ id: "retroAssessmentStatus",
accessorFn: (row) => {
const status = row.uprn ? docStatusMap[row.uprn] : undefined;
- if (status?.isComplete) return 2;
- if (status?.hasDocs) return 1;
+ if (status?.isSurveyComplete) return 2;
+ if (status?.hasSurveyDocs) return 1;
return 0;
},
- header: ({ column }) => ,
+ header: ({ column }) => ,
cell: ({ row }) => {
const status = row.original.uprn ? docStatusMap[row.original.uprn] : undefined;
- return ;
+ return ;
+ },
+ enableHiding: false,
+ },
+
+ // ── Install Docs Status ───────────────────────────────────────────────
+ {
+ id: "installDocs",
+ accessorFn: (row) => {
+ const status = row.uprn ? docStatusMap[row.uprn] : undefined;
+ const s = status?.installStatus ?? "none";
+ if (s === "all") return 3;
+ if (s === "partial") return 2;
+ if (s === "hasDocs") return 1;
+ return 0;
+ },
+ header: ({ column }) => ,
+ cell: ({ row }) => {
+ const status = row.original.uprn ? docStatusMap[row.original.uprn] : undefined;
+ return ;
},
enableHiding: false,
},
@@ -111,11 +164,11 @@ export function createDocumentTableColumns(
let icon: React.ReactNode;
let className: string;
- if (status?.isComplete) {
+ if (status?.isSurveyComplete) {
icon = ;
className =
"inline-flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg text-xs font-medium border border-emerald-200 text-emerald-700 bg-emerald-50 hover:bg-emerald-100 hover:border-emerald-300 transition-all duration-150 whitespace-nowrap";
- } else if (status?.hasDocs) {
+ } else if (status?.hasSurveyDocs) {
icon = ;
className =
"inline-flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg text-xs font-medium border border-amber-200 text-amber-700 bg-amber-50 hover:bg-amber-100 hover:border-amber-300 transition-all duration-150 whitespace-nowrap";
diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/PropertyDrawer.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/PropertyDrawer.tsx
index 6dc83ef..63ef131 100644
--- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/PropertyDrawer.tsx
+++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/PropertyDrawer.tsx
@@ -11,6 +11,7 @@ import {
FolderOpen,
X,
ExternalLink,
+ HardHat,
} from "lucide-react";
import {
Drawer,
@@ -21,10 +22,11 @@ import {
DrawerDescription,
} from "@/app/shadcn_components/ui/drawer";
import type { PropertyDocument } from "./types";
-import { EXPECTED_SURVEY_DOC_TYPES } from "./types";
+import { EXPECTED_RETROFIT_ASSESSMENT_DOC_TYPES, SURVEY_ALL_DOC_TYPES } from "./types";
-// Human-readable labels for the main DB fileType enum values
+// Human-readable labels for all DB fileType enum values
const DOC_TYPE_LABELS: Record = {
+ // Survey / retrofit assessment docs
photo_pack: "Photo Pack",
site_note: "Site Note",
rd_sap_site_note: "RdSAP Site Note",
@@ -34,13 +36,43 @@ const DOC_TYPE_LABELS: Record = {
par_photo_pack: "PAR Photo Pack",
pas_2023_property: "PAS 2023 Property Report",
pas_2023_occupancy: "PAS 2023 Occupancy Report",
+ ecmk_site_note: "ECMK Site Note",
+ ecmk_rd_sap_site_note: "ECMK RdSAP Site Note",
+ ecmk_survey_xml: "ECMK Survey XML",
+ // Install docs — photos
+ pre_photo: "Pre-Install Photos",
+ mid_photo: "Mid-Install Photos",
+ post_photo: "Post-Install Photos",
+ loft_hatch_photo: "Loft Hatch & Draft Excluder Photos",
+ dmev_photos: "DMEV Photos (Wetrooms)",
+ door_undercut_photos: "Door Undercut Photos",
+ trickle_vent_photos: "Trickle Vent Photos",
+ // Install docs — pre-installation
+ pre_installation_building_inspection: "PIBI / Tech Survey",
+ point_of_work_risk_assessment: "Point of Work Risk Assessment",
+ // Install docs — compliance & lodgement
+ claim_of_compliance: "DOCC 2030 (Claim of Compliance)",
+ mcs_compliance_certificate: "MCS Compliance Certificate",
+ certificate_of_conformity: "Certificate of Conformity",
+ minor_works_electrical_certificate: "Minor Works Electrical Certificate",
+ trustmark_licence_numbers: "TrustMark Licence Numbers",
+ operative_competency: "Operative Competency",
+ // Install docs — ventilation
+ ventilation_assessment_checklist: "Ventilation Assessment Checklist",
+ anemometer_readings: "Anemometer Readings",
+ commissioning_records: "Commissioning Records",
+ part_f_ventilation_document: "Approved Document Part F",
+ // Install docs — handover & warranties
+ handover_pack: "Handover Pack",
+ insurance_guarantee: "Insurance Backed Guarantee (IBG)",
+ workmanship_warranty: "Workmanship Warranty",
+ g98_notification: "G98 / G99 Notification",
+ // Install docs — qualifications & other
+ installer_qualifications: "Installer Qualifications",
+ installer_feedback: "Installer Feedback",
+ contractor_other: "Other",
};
-// All survey docs go under this group for now (extensible later)
-function getDocCategory(_docType: string): string {
- return "Survey Documents";
-}
-
function formatDate(iso: string): string {
try {
return new Date(iso).toLocaleDateString("en-GB", {
@@ -56,7 +88,7 @@ function formatDate(iso: string): string {
// -----------------------------------------------------------------------
// Individual document row
// -----------------------------------------------------------------------
-function DocumentRow({ doc }: { doc: PropertyDocument }) {
+function DocumentRow({ doc, showMeasure }: { doc: PropertyDocument; showMeasure?: boolean }) {
const label = DOC_TYPE_LABELS[doc.docType] ?? doc.docType;
const { mutate: download, isPending: signing } = useMutation({
@@ -90,7 +122,10 @@ function DocumentRow({ doc }: { doc: PropertyDocument }) {
{label}
- {formatDate(doc.s3UploadTimestamp)}
+ {showMeasure && doc.measureName
+ ? <>{doc.measureName} · {formatDate(doc.s3UploadTimestamp)}>
+ : formatDate(doc.s3UploadTimestamp)
+ }
@@ -161,20 +196,16 @@ export default function PropertyDrawer({
}
const documents = open ? (fetchedDocuments as PropertyDocument[]) : lastDocumentsRef.current;
- // Group docs by category for display
- const grouped = documents.reduce<
- Record
- >((acc, doc) => {
- const category = getDocCategory(doc.docType);
- (acc[category] ??= []).push(doc);
- return acc;
- }, {});
+ // Split documents into the two sections
+ const retrofitDocs = documents.filter((d) => SURVEY_ALL_DOC_TYPES.has(d.docType));
+ const installDocs = documents.filter((d) => !SURVEY_ALL_DOC_TYPES.has(d.docType));
const hasDocuments = documents.length > 0;
- const presentTypes = new Set(documents.map((d) => d.docType));
- const missingTypes = EXPECTED_SURVEY_DOC_TYPES.filter(
- (t) => !presentTypes.has(t),
+ // Missing mandatory retrofit assessment docs (ecmk types are optional — not shown as missing)
+ const presentRetrofitTypes = new Set(retrofitDocs.map((d) => d.docType));
+ const missingRetrofitTypes = EXPECTED_RETROFIT_ASSESSMENT_DOC_TYPES.filter(
+ (t) => !presentRetrofitTypes.has(t),
);
return (
@@ -220,7 +251,7 @@ export default function PropertyDrawer({
{/* Body */}
-
+
{/* Loading state */}
{isFetching && (
@@ -248,7 +279,7 @@ export default function PropertyDrawer({
)}
- {/* Empty state — shows all missing doc types */}
+ {/* Empty state */}
{!isFetching && !isError && !hasDocuments && (
@@ -259,15 +290,14 @@ export default function PropertyDrawer({
No documents available
- All {EXPECTED_SURVEY_DOC_TYPES.length} survey documents are
- outstanding.
+ All {EXPECTED_RETROFIT_ASSESSMENT_DOC_TYPES.length} retrofit assessment documents are outstanding.
- Missing Documents ({missingTypes.length})
+ Missing Documents ({missingRetrofitTypes.length})
- {missingTypes.map((t) => (
+ {missingRetrofitTypes.map((t) => (
)}
- {/* Document groups */}
- {!isFetching &&
- !isError &&
- hasDocuments &&
- Object.entries(grouped).map(([category, docs]) => (
+ {!isFetching && !isError && hasDocuments && (
+ <>
+ {/* ── Retrofit Assessment Documents ── */}
- {category}
+ Retrofit Assessment Documents
-
- {docs.map((doc) => (
-
- ))}
-
-
- ))}
-
-
- {/* Missing documents section — shown when some but not all docs are present */}
- {!isFetching &&
- !isError &&
- hasDocuments &&
- missingTypes.length > 0 && (
-
-
- Missing Documents ({missingTypes.length})
-
-
- {missingTypes.map((t) => (
-
-
-
- {DOC_TYPE_LABELS[t] ?? t}
-
+ {retrofitDocs.length > 0 ? (
+
+ {retrofitDocs.map((doc) => (
+
+ ))}
- ))}
-
-
+ ) : (
+
None uploaded yet.
+ )}
+
+ {/* Missing mandatory retrofit assessment docs */}
+ {missingRetrofitTypes.length > 0 && (
+
+
+ Missing ({missingRetrofitTypes.length})
+
+ {missingRetrofitTypes.map((t) => (
+
+
+
+ {DOC_TYPE_LABELS[t] ?? t}
+
+
+ ))}
+
+ )}
+
+
+ {/* ── Install Documents ── */}
+
+
+
+ Install Documents
+
+ {installDocs.length > 0 ? (
+
+ {installDocs.map((doc) => (
+
+ ))}
+
+ ) : (
+ No install documents uploaded yet.
+ )}
+
+ >
)}
+
{/* Footer */}
diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/PropertyTable.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/PropertyTable.tsx
index ac29be0..db9c867 100644
--- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/PropertyTable.tsx
+++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/PropertyTable.tsx
@@ -126,9 +126,9 @@ export default function PropertyTable({ data, onOpenDrawer, onOpenDetail, showDo
if (docFilter !== "all") {
result = result.filter((d) => {
const status = d.uprn ? docStatusMap[d.uprn] : undefined;
- if (docFilter === "none") return !status || !status.hasDocs;
- if (docFilter === "has_docs") return !!status?.hasDocs;
- if (docFilter === "incomplete") return !!status?.hasDocs && !status.isComplete;
+ if (docFilter === "none") return !status || !status.hasSurveyDocs;
+ if (docFilter === "has_docs") return !!status?.hasSurveyDocs;
+ if (docFilter === "incomplete") return !!status?.hasSurveyDocs && !status.isSurveyComplete;
return true;
});
}
diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/PropertyTableColumns.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/PropertyTableColumns.tsx
index 9562418..03804ee 100644
--- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/PropertyTableColumns.tsx
+++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/PropertyTableColumns.tsx
@@ -285,8 +285,8 @@ export function createPropertyTableColumns(
cell: ({ row }) => {
const uprn = row.original.uprn ?? "";
const status = uprn ? docStatusMap[uprn] : undefined;
- const isComplete = status?.isComplete;
- const hasDocs = status?.hasDocs;
+ const isComplete = status?.isSurveyComplete;
+ const hasDocs = status?.hasSurveyDocs;
let icon: React.ReactNode;
let className: string;
diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/page.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/page.tsx
index 0cbc869..db5a3ae 100644
--- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/page.tsx
+++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/page.tsx
@@ -13,7 +13,7 @@ import { portfolioCapabilities } from "@/app/db/schema/portfolio";
import { dealMeasureApprovals } from "@/app/db/schema/approvals";
import { user as userTable } from "@/app/db/schema/users";
import type { HubspotDeal, DocStatusMap, DocStatus, PortfolioCapabilityType, ApprovalsByDeal } from "./types";
-import { EXPECTED_SURVEY_DOC_TYPES } from "./types";
+import { EXPECTED_RETROFIT_ASSESSMENT_DOC_TYPES, SURVEY_ALL_DOC_TYPES } from "./types";
import type { InferSelectModel } from "drizzle-orm";
import { Card, CardContent } from "@/app/shadcn_components/ui/card";
import { Building2 } from "lucide-react";
@@ -184,23 +184,58 @@ export default async function LiveReportingPage(props: {
if (uprnList.length > 0) {
const docRows = await db
- .select()
+ .select({
+ uprn: uploadedFiles.uprn,
+ fileType: uploadedFiles.fileType,
+ measureName: uploadedFiles.measureName,
+ })
.from(uploadedFiles)
.where(inArray(uploadedFiles.uprn, uprnList));
- const grouped: Record> = {};
+ // Group docs by UPRN
+ const docsByUprn = new Map>();
for (const row of docRows) {
if (row.uprn === null || row.fileType === null) continue;
const key = String(row.uprn);
- (grouped[key] ??= new Set()).add(row.fileType);
+ if (!docsByUprn.has(key)) docsByUprn.set(key, []);
+ docsByUprn.get(key)!.push({ fileType: row.fileType, measureName: row.measureName });
}
- for (const [uprn, types] of Object.entries(grouped)) {
- const presentTypes = Array.from(types);
+ // Build measures lookup from deals (uprn → proposed measure names)
+ const measuresByUprn = new Map();
+ for (const deal of deals) {
+ if (deal.uprn) {
+ const key = String(deal.uprn);
+ const measures = (deal.proposedMeasures ?? "")
+ .split(",").map((m: string) => m.trim()).filter(Boolean);
+ measuresByUprn.set(key, measures);
+ }
+ }
+
+ for (const [uprn, docs] of docsByUprn) {
+ const surveyDocs = docs.filter((d) => SURVEY_ALL_DOC_TYPES.has(d.fileType));
+ const installDocs = docs.filter((d) => !SURVEY_ALL_DOC_TYPES.has(d.fileType));
+ const surveyTypeSet = new Set(surveyDocs.map((d) => d.fileType));
+
+ const measures = measuresByUprn.get(uprn) ?? [];
+ let installStatus: DocStatus["installStatus"] = "none";
+ if (installDocs.length > 0) {
+ if (measures.length === 0) {
+ installStatus = "hasDocs";
+ } else {
+ const measuresWithDocs = new Set(
+ installDocs.map((d) => d.measureName).filter(Boolean),
+ );
+ installStatus = measures.every((m) => measuresWithDocs.has(m)) ? "all" : "partial";
+ }
+ }
+
const status: DocStatus = {
- presentTypes,
- hasDocs: presentTypes.length > 0,
- isComplete: EXPECTED_SURVEY_DOC_TYPES.every((t) => types.has(t)),
+ presentSurveyTypes: Array.from(surveyTypeSet),
+ hasSurveyDocs: surveyDocs.length > 0,
+ isSurveyComplete: EXPECTED_RETROFIT_ASSESSMENT_DOC_TYPES.every((t) => surveyTypeSet.has(t)),
+ hasInstallDocs: installDocs.length > 0,
+ installStatus,
};
docStatusMap[uprn] = status;
}
diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/types.ts b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/types.ts
index a244f68..749a97d 100644
--- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/types.ts
+++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/types.ts
@@ -205,10 +205,11 @@ export type PropertyDocument = {
s3UploadTimestamp: string; // ISO string
uprn: string | null;
landlordPropertyId: string | null;
+ measureName: string | null; // set for install docs
};
-// All survey document types expected for a complete survey
-export const EXPECTED_SURVEY_DOC_TYPES = [
+// Mandatory retrofit assessment doc types (used for completeness check — ecmk types are optional)
+export const EXPECTED_RETROFIT_ASSESSMENT_DOC_TYPES = [
"photo_pack",
"site_note",
"rd_sap_site_note",
@@ -220,10 +221,26 @@ export const EXPECTED_SURVEY_DOC_TYPES = [
"pas_2023_occupancy",
] as const;
+// All survey-adjacent types (including optional ecmk docs) — used for display categorisation
+export const SURVEY_ALL_DOC_TYPES = new Set([
+ ...EXPECTED_RETROFIT_ASSESSMENT_DOC_TYPES,
+ "ecmk_site_note",
+ "ecmk_rd_sap_site_note",
+ "ecmk_survey_xml",
+]);
+
export type DocStatus = {
- presentTypes: string[];
- hasDocs: boolean;
- isComplete: boolean; // all EXPECTED_SURVEY_DOC_TYPES present
+ // Retrofit assessment docs
+ presentSurveyTypes: string[];
+ hasSurveyDocs: boolean;
+ isSurveyComplete: boolean; // all 9 EXPECTED_RETROFIT_ASSESSMENT_DOC_TYPES present (ecmk not counted)
+ // Install docs
+ hasInstallDocs: boolean;
+ installStatus: "none" | "partial" | "hasDocs" | "all";
+ // "all" = install docs exist for every proposed measure
+ // "partial" = some (but not all) proposed measures have docs
+ // "hasDocs" = has install docs but no measures defined on the deal
+ // "none" = no install docs at all
};
export type DocStatusMap = Record; // keyed by UPRN string