diff --git a/src/app/api/live-tracking/property-documents/route.ts b/src/app/api/live-tracking/property-documents/route.ts
new file mode 100644
index 0000000..9ebf596
--- /dev/null
+++ b/src/app/api/live-tracking/property-documents/route.ts
@@ -0,0 +1,55 @@
+import { NextResponse } from "next/server";
+import { eq, or } from "drizzle-orm";
+import { db } from "@/app/db/db";
+import { uploadedFiles } from "@/app/db/schema/uploaded_files";
+
+export async function GET(req: Request) {
+ const { searchParams } = new URL(req.url);
+ const uprnParam = searchParams.get("uprn");
+ const landlordPropertyIdParam = searchParams.get("landlordPropertyId");
+
+ if (!uprnParam && !landlordPropertyIdParam) {
+ return NextResponse.json(
+ { error: "uprn or landlordPropertyId is required" },
+ { status: 400 },
+ );
+ }
+
+ try {
+ const conditions = [];
+
+ if (uprnParam) {
+ const uprnBigInt = BigInt(uprnParam);
+ conditions.push(eq(uploadedFiles.uprn, uprnBigInt));
+ }
+
+ if (landlordPropertyIdParam) {
+ conditions.push(
+ eq(uploadedFiles.landlordPropertyId, landlordPropertyIdParam),
+ );
+ }
+
+ const rows = await db
+ .select()
+ .from(uploadedFiles)
+ .where(conditions.length === 1 ? conditions[0] : or(...conditions));
+
+ const documents = rows.map((row) => ({
+ id: String(row.id),
+ s3FileKey: row.s3FileKey,
+ s3FileBucket: row.s3FileBucket,
+ docType: row.fileType ?? "unknown",
+ s3UploadTimestamp: row.s3UploadTimestamp.toISOString(),
+ uprn: row.uprn !== null ? String(row.uprn) : null,
+ landlordPropertyId: row.landlordPropertyId,
+ }));
+
+ return NextResponse.json(documents);
+ } catch (error) {
+ console.error("Error fetching property documents:", error);
+ return NextResponse.json(
+ { error: "Failed to fetch documents" },
+ { status: 500 },
+ );
+ }
+}
diff --git a/src/app/db/db.ts b/src/app/db/db.ts
index 1d2d778..5219612 100644
--- a/src/app/db/db.ts
+++ b/src/app/db/db.ts
@@ -11,6 +11,7 @@ import * as FundingSchema from "@/app/db/schema/funding";
import * as Relations from "@/app/db/schema/relations";
import * as Users from "@/app/db/schema/users";
import * as CrmSchema from "@/app/db/schema/crm/hubspot_deal_table";
+import * as UploadedFilesSchema from "@/app/db/schema/uploaded_files";
export const pool = new Pool({
host: process.env.DB_HOST,
@@ -33,6 +34,7 @@ const schema = {
...FundingSchema,
...Users,
...CrmSchema,
+ ...UploadedFilesSchema,
};
export const db = drizzle(pool, {
diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/DrillDownTable.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/DrillDownTable.tsx
new file mode 100644
index 0000000..eaf3444
--- /dev/null
+++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/DrillDownTable.tsx
@@ -0,0 +1,302 @@
+"use client";
+
+import { useMemo, useState } from "react";
+import {
+ useReactTable,
+ getCoreRowModel,
+ getFilteredRowModel,
+ getSortedRowModel,
+ getPaginationRowModel,
+ flexRender,
+ type SortingState,
+ type PaginationState,
+ type ColumnDef,
+} from "@tanstack/react-table";
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/app/shadcn_components/ui/table";
+import { Input } from "@/app/shadcn_components/ui/input";
+import { Search, Download, ChevronLeft, ChevronRight } from "lucide-react";
+import type { ClassifiedDeal, HubspotDeal } from "./types";
+
+interface DrillDownTableProps {
+ data: ClassifiedDeal[];
+ columns?: (keyof HubspotDeal)[];
+ columnLabels?: Partial>;
+}
+
+function escapeCell(value: unknown): string {
+ if (value === null || value === undefined) return "";
+ const str =
+ value instanceof Date ? value.toLocaleDateString("en-GB") : String(value);
+ return str.includes(",") || str.includes('"') || str.includes("\n")
+ ? `"${str.replace(/"/g, '""')}"`
+ : str;
+}
+
+async function handlePhotoDownload(rawUrl: string) {
+ try {
+ const key = rawUrl.split(".amazonaws.com/")[1];
+ if (!key) return alert("Invalid S3 key");
+
+ const res = await fetch("/api/sign-s3-url", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ key }),
+ });
+
+ const data = await res.json();
+ if (data.url) {
+ window.open(data.url, "_blank");
+ } else {
+ alert("Failed to get signed URL");
+ }
+ } catch (err) {
+ console.error(err);
+ alert("Error downloading file");
+ }
+}
+
+function PhotoDownloadCell({ value }: { value: unknown }) {
+ let urls: string[] = [];
+
+ if (typeof value === "string") {
+ try {
+ const parsed = JSON.parse(value);
+ urls = Array.isArray(parsed) ? parsed : [value];
+ } catch {
+ urls = value.split(/[\s,]+/).filter((u) => u.startsWith("http"));
+ }
+ } else if (Array.isArray(value)) {
+ urls = value as string[];
+ }
+
+ if (urls.length === 0) return No photos;
+
+ return (
+
+ {urls.map((url, idx) => (
+
+ ))}
+
+ );
+}
+
+export default function DrillDownTable({
+ data,
+ columns: columnKeys,
+ columnLabels,
+}: DrillDownTableProps) {
+ const [globalFilter, setGlobalFilter] = useState("");
+ const [sorting, setSorting] = useState([]);
+ const [pagination, setPagination] = useState({
+ pageIndex: 0,
+ pageSize: 25,
+ });
+
+ const visibleKeys: (keyof HubspotDeal)[] = columnKeys?.length
+ ? columnKeys
+ : (Object.keys(data?.[0] || {}) as (keyof HubspotDeal)[]);
+
+ const columns = useMemo[]>(
+ () =>
+ visibleKeys.map((key) => ({
+ accessorKey: key as string,
+ id: key as string,
+ header: () => (
+
+ {columnLabels?.[key] ?? (key as string)}
+
+ ),
+ cell: ({ row }) => {
+ const value = row.original[key as keyof ClassifiedDeal];
+ if (key === "majorConditionIssuePhotosS3") {
+ return ;
+ }
+ return (
+
+ {value != null ? String(value) : (
+ —
+ )}
+
+ );
+ },
+ })),
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ [visibleKeys.join(","), columnLabels],
+ );
+
+ const table = useReactTable({
+ data,
+ columns,
+ state: { globalFilter, sorting, pagination },
+ onGlobalFilterChange: setGlobalFilter,
+ onSortingChange: setSorting,
+ onPaginationChange: setPagination,
+ getCoreRowModel: getCoreRowModel(),
+ getFilteredRowModel: getFilteredRowModel(),
+ getSortedRowModel: getSortedRowModel(),
+ getPaginationRowModel: getPaginationRowModel(),
+ globalFilterFn: "includesString",
+ });
+
+ const downloadCsv = () => {
+ const rows = table.getFilteredRowModel().rows;
+ const exportKeys = visibleKeys.filter((k) => k !== "majorConditionIssuePhotosS3");
+ const header = exportKeys
+ .map((k) => columnLabels?.[k] ?? (k as string))
+ .join(",");
+ const body = rows
+ .map((row) =>
+ exportKeys.map((k) => escapeCell(row.original[k as keyof ClassifiedDeal])).join(","),
+ )
+ .join("\n");
+ const blob = new Blob([header + "\n" + body], {
+ type: "text/csv;charset=utf-8;",
+ });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement("a");
+ a.href = url;
+ a.download = "data.csv";
+ a.click();
+ URL.revokeObjectURL(url);
+ };
+
+ const pageCount = table.getPageCount();
+ const currentPage = table.getState().pagination.pageIndex + 1;
+ const totalFiltered = table.getFilteredRowModel().rows.length;
+
+ return (
+
+ {/* Toolbar */}
+
+
+
+ {
+ setGlobalFilter(e.target.value);
+ setPagination((p) => ({ ...p, pageIndex: 0 }));
+ }}
+ placeholder="Search…"
+ className="pl-9 h-9 text-sm border-gray-200 focus:border-brandblue/40 focus:ring-brandblue/20"
+ />
+
+
+
+
+ {/* Row count */}
+
+ Showing{" "}
+ {totalFiltered}{" "}
+ {totalFiltered === 1 ? "row" : "rows"}
+
+
+ {/* Table */}
+
+
+
+
+ {table.getHeaderGroups().map((headerGroup) => (
+
+ {headerGroup.headers.map((header) => (
+
+ {header.isPlaceholder
+ ? null
+ : flexRender(
+ header.column.columnDef.header,
+ header.getContext(),
+ )}
+
+ ))}
+
+ ))}
+
+
+ {table.getRowModel().rows.length ? (
+ table.getRowModel().rows.map((row, i) => (
+
+ {row.getVisibleCells().map((cell) => (
+
+ {flexRender(
+ cell.column.columnDef.cell,
+ cell.getContext(),
+ )}
+
+ ))}
+
+ ))
+ ) : (
+
+
+ No results found.
+
+
+ )}
+
+
+
+
+
+ {/* Pagination */}
+ {pageCount > 1 && (
+
+
+ Page {currentPage} of {pageCount}
+
+
+
+
+
+
+ )}
+
+ );
+}
diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/LiveTracker.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/LiveTracker.tsx
index 4ecd754..5418d15 100644
--- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/LiveTracker.tsx
+++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/LiveTracker.tsx
@@ -20,12 +20,14 @@ import type {
TableModal,
ClassifiedDeal,
DocumentDrawerState,
+ DocStatusMap,
} from "./types";
export default function LiveTracker({
projects,
totalDeals,
majorConditionDeals,
+ docStatusMap,
}: LiveTrackerProps) {
// ── Tab state ────────────────────────────────────────────────────────
const [activeTab, setActiveTab] = useState<"analytics" | "properties">(
@@ -46,6 +48,7 @@ export default function LiveTracker({
const [drawerState, setDrawerState] = useState({
open: false,
uprn: null,
+ landlordPropertyId: null,
dealname: null,
});
@@ -68,8 +71,8 @@ export default function LiveTracker({
});
};
- const handleOpenDrawer = (uprn: string | null, dealname: string | null) => {
- setDrawerState({ open: true, uprn, dealname });
+ const handleOpenDrawer = (uprn: string | null, landlordPropertyId: string | null, dealname: string | null) => {
+ setDrawerState({ open: true, uprn, landlordPropertyId, dealname });
};
if (!totalDeals) {
@@ -156,6 +159,7 @@ export default function LiveTracker({
data={currentProject?.allDeals ?? []}
onOpenDrawer={handleOpenDrawer}
showDocuments={true}
+ docStatusMap={docStatusMap}
/>
@@ -253,9 +257,10 @@ export default function LiveTracker({
- setDrawerState({ open: false, uprn: null, dealname: null })
+ setDrawerState({ open: false, uprn: null, landlordPropertyId: null, dealname: null })
}
/>
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 c4464bc..0199ada 100644
--- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/PropertyDrawer.tsx
+++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/PropertyDrawer.tsx
@@ -6,8 +6,6 @@ import { motion, AnimatePresence } from "framer-motion";
import {
FileDown,
FileText,
- Code2,
- BarChart3,
Loader2,
FolderOpen,
X,
@@ -23,47 +21,22 @@ import {
} from "@/app/shadcn_components/ui/drawer";
import type { PropertyDocument } from "./types";
-// Human-readable labels for DB_REPORT_TYPES enum values
+// Human-readable labels for the main DB fileType enum values
const DOC_TYPE_LABELS: Record = {
- ECO_CONDITION_REPORT: "Condition Report (PAS 2035)",
- ENERGY_PERFORMANCE_REPORT_SUMMARY_INFORMATION: "EPC Summary Report",
- LIG_XML: "LIG XML",
- RDSAP_XML: "RdSAP XML",
- FULLSAP_XML: "Full SAP XML",
- DECENT_HOMES_RAW_DATA: "Decent Homes Raw Data",
- DECENT_HOMES_PROPERTY_META: "Decent Homes Property Meta",
- DECENT_HOMES_SUMMARY: "Decent Homes Summary",
+ photo_pack: "Photo Pack",
+ site_note: "Site Note",
+ rd_sap_site_note: "RdSAP Site Note",
+ pas_2023_ventilation: "PAS 2023 Ventilation",
+ pas_2023_condition: "PAS 2023 Condition Report",
+ pas_significance: "PAS Significance",
+ par_photo_pack: "PAR Photo Pack",
+ pas_2023_property: "PAS 2023 Property Report",
+ pas_2023_occupancy: "PAS 2023 Occupancy Report",
};
-// Icon + colour per doc category
-function docTypeStyle(docType: string): {
- icon: React.ReactNode;
- bg: string;
- text: string;
- border: string;
-} {
- if (docType.includes("XML")) {
- return {
- icon: ,
- bg: "bg-amber-50",
- text: "text-amber-700",
- border: "border-amber-200",
- };
- }
- if (docType.includes("DECENT_HOMES")) {
- return {
- icon: ,
- bg: "bg-violet-50",
- text: "text-violet-700",
- border: "border-violet-200",
- };
- }
- return {
- icon: ,
- bg: "bg-sky-50",
- text: "text-sky-700",
- border: "border-sky-200",
- };
+// All survey docs go under this group for now (extensible later)
+function getDocCategory(_docType: string): string {
+ return "Survey Documents";
}
function formatDate(iso: string): string {
@@ -83,29 +56,25 @@ function formatDate(iso: string): string {
// -----------------------------------------------------------------------
function DocumentRow({ doc }: { doc: PropertyDocument }) {
const [signing, setSigning] = useState(false);
- const style = docTypeStyle(doc.docType);
const label = DOC_TYPE_LABELS[doc.docType] ?? doc.docType;
async function handleDownload() {
setSigning(true);
try {
- // Extract S3 key from the full URI — same pattern as TableViewer.tsx
- const key = doc.s3FileUri.split(".amazonaws.com/")[1];
- if (!key) {
- window.open(doc.s3FileUri, "_blank");
- return;
- }
const res = await fetch("/api/sign-s3-url", {
method: "POST",
headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ key }),
+ body: JSON.stringify({ key: doc.s3FileKey }),
});
if (!res.ok) throw new Error("Failed to get signed URL");
const data = await res.json();
window.open(data.url, "_blank");
} catch {
- // Fallback: open raw URI
- window.open(doc.s3FileUri, "_blank");
+ // Fallback: construct raw S3 URL
+ window.open(
+ `https://${doc.s3FileBucket}.s3.amazonaws.com/${doc.s3FileKey}`,
+ "_blank",
+ );
} finally {
setSigning(false);
}
@@ -120,17 +89,15 @@ function DocumentRow({ doc }: { doc: PropertyDocument }) {
>
{/* Doc type badge */}
-
- {style.icon}
+
+
{label}
- {formatDate(doc.s3FileUploadTimestamp)}
+ {formatDate(doc.s3UploadTimestamp)}
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 7f9b0ce..07d4e53 100644
--- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/PropertyTableColumns.tsx
+++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/PropertyTableColumns.tsx
@@ -1,9 +1,9 @@
"use client";
import { ColumnDef } from "@tanstack/react-table";
-import { ArrowUpDown, FileDown } from "lucide-react";
+import { ArrowUpDown, FileDown, CheckCircle2, AlertCircle, FileX } from "lucide-react";
import { STAGE_COLORS } from "./types";
-import type { ClassifiedDeal, DisplayStage } from "./types";
+import type { ClassifiedDeal, DisplayStage, DocStatusMap } from "./types";
// -----------------------------------------------------------------------
// Stage badge — consistent pill rendering
@@ -42,10 +42,12 @@ function SortableHeader({
// -----------------------------------------------------------------------
// Column factory — takes onOpenDrawer so the Documents button can trigger it
// showDocuments controls whether the Docs action column is included
+// docStatusMap provides per-UPRN document status for status indicators
// -----------------------------------------------------------------------
export function createPropertyTableColumns(
- onOpenDrawer: (uprn: string | null, dealname: string | null) => void,
+ onOpenDrawer: (uprn: string | null, landlordPropertyId: string | null, dealname: string | null) => void,
showDocuments: boolean = false,
+ docStatusMap: DocStatusMap = {},
): ColumnDef[] {
const columns: ColumnDef[] = [
// ── Address ──────────────────────────────────────────────────────────
@@ -289,18 +291,42 @@ export function createPropertyTableColumns(
if (showDocuments) {
columns.push({
id: "documents",
- header: () => null,
- cell: ({ row }) => (
-
- onOpenDrawer(row.original.uprn, row.original.dealname)
- }
- className="inline-flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg text-xs font-medium border border-brandblue/20 text-brandblue bg-brandblue/5 hover:bg-brandblue/10 hover:border-brandblue/40 transition-all duration-150 whitespace-nowrap"
- >
-
- Docs
-
+ header: () => (
+ Docs
),
+ cell: ({ row }) => {
+ const uprn = row.original.uprn ?? "";
+ const status = uprn ? docStatusMap[uprn] : undefined;
+ const isComplete = status?.isComplete;
+ const hasDocs = status?.hasDocs;
+
+ let icon: React.ReactNode;
+ let className: string;
+
+ if (isComplete) {
+ 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 (hasDocs) {
+ 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";
+ } else {
+ icon = ;
+ className =
+ "inline-flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg text-xs font-medium border border-gray-200 text-gray-400 bg-gray-50 hover:bg-gray-100 hover:border-gray-300 transition-all duration-150 whitespace-nowrap";
+ }
+
+ return (
+ onOpenDrawer(row.original.uprn, row.original.landlordPropertyId, row.original.dealname)}
+ className={className}
+ >
+ {icon}
+ Docs
+
+ );
+ },
enableSorting: false,
enableHiding: false,
});
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 eca568f..87c9391 100644
--- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/page.tsx
+++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/page.tsx
@@ -1,12 +1,14 @@
import { getServerSession } from "next-auth";
import { AuthOptions } from "@/app/api/auth/[...nextauth]/authOptions";
import { redirect } from "next/navigation";
-import { eq } from "drizzle-orm";
+import { eq, inArray } from "drizzle-orm";
import LiveTracker from "./LiveTracker";
import { computeLiveTrackerData } from "./transforms";
import { db } from "@/app/db/db";
import { hubspotDealData } from "@/app/db/schema/crm/hubspot_deal_table";
-import type { HubspotDeal } from "./types";
+import { uploadedFiles } from "@/app/db/schema/uploaded_files";
+import type { HubspotDeal, DocStatusMap, DocStatus } from "./types";
+import { EXPECTED_SURVEY_DOC_TYPES } from "./types";
import type { InferSelectModel } from "drizzle-orm";
// ⚠️ ⚠️ ⚠️ HARDCODED COMPANY ID — temporary for testing only.
@@ -78,9 +80,43 @@ export default async function LiveReportingPage(props: {
console.log("Fetched deals from DB:", rawDeals.length);
- const trackerData = computeLiveTrackerData(
- rawDeals.map(mapDbRowToHubspotDeal),
- );
+ const deals = rawDeals.map(mapDbRowToHubspotDeal);
+ const trackerData = computeLiveTrackerData(deals);
+
+ // ── Fetch survey document status for all properties ─────────────────
+ const uprnList = deals
+ .map((d) => d.uprn)
+ .filter((u): u is string => !!u)
+ .map((u) => {
+ try { return BigInt(u); } catch { return null; }
+ })
+ .filter((u): u is bigint => u !== null);
+
+ let docStatusMap: DocStatusMap = {};
+
+ if (uprnList.length > 0) {
+ const docRows = await db
+ .select()
+ .from(uploadedFiles)
+ .where(inArray(uploadedFiles.uprn, uprnList));
+
+ const grouped: Record> = {};
+ 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);
+ }
+
+ for (const [uprn, types] of Object.entries(grouped)) {
+ const presentTypes = Array.from(types);
+ const status: DocStatus = {
+ presentTypes,
+ hasDocs: presentTypes.length > 0,
+ isComplete: EXPECTED_SURVEY_DOC_TYPES.every((t) => types.has(t)),
+ };
+ docStatusMap[uprn] = status;
+ }
+ }
return (
@@ -94,7 +130,7 @@ export default async function LiveReportingPage(props: {
-
+
);
}
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 f00495b..c51e7d8 100644
--- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/types.ts
+++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/types.ts
@@ -167,6 +167,7 @@ export type LiveTrackerProps = {
projects: ProjectData[];
totalDeals: number;
majorConditionDeals: ClassifiedDeal[]; // for Awaab's Law card
+ docStatusMap: DocStatusMap;
};
// -----------------------------------------------------------------------
@@ -186,16 +187,39 @@ export type TableModal = {
// -----------------------------------------------------------------------
export type PropertyDocument = {
id: string;
- s3FileUri: string;
- s3JsonUri: string | null;
- docType: string;
- s3FileUploadTimestamp: string; // ISO string
- uprn: string;
+ s3FileKey: string; // S3 object key — used directly for presigned URL
+ s3FileBucket: string; // S3 bucket name
+ docType: string; // fileType enum value
+ s3UploadTimestamp: string; // ISO string
+ uprn: string | null;
+ landlordPropertyId: string | null;
};
+// All survey document types expected for a complete survey
+export const EXPECTED_SURVEY_DOC_TYPES = [
+ "photo_pack",
+ "site_note",
+ "rd_sap_site_note",
+ "pas_2023_ventilation",
+ "pas_2023_condition",
+ "pas_significance",
+ "par_photo_pack",
+ "pas_2023_property",
+ "pas_2023_occupancy",
+] as const;
+
+export type DocStatus = {
+ presentTypes: string[];
+ hasDocs: boolean;
+ isComplete: boolean; // all EXPECTED_SURVEY_DOC_TYPES present
+};
+
+export type DocStatusMap = Record; // keyed by UPRN string
+
export type DocumentDrawerState = {
open: boolean;
uprn: string | null;
+ landlordPropertyId: string | null;
dealname: string | null;
};