From 785c40f2d11771660197d002b1878ef11be44fe1 Mon Sep 17 00:00:00 2001
From: Khalim Conn-Kowlessar
Date: Mon, 20 Apr 2026 15:55:45 +0000
Subject: [PATCH] adding measure level tracking for uploaded docs
---
.../api/upload/contractor-install/route.ts | 33 ++++
src/app/lib/hubspot/dealSync.ts | 23 ++-
src/app/lib/measureDocumentRequirements.ts | 81 +++++++++
.../live/ContractorUploadModal.tsx | 170 ++++++++++++++++--
.../your-projects/live/DocumentTable.tsx | 1 +
.../live/DocumentTableColumns.tsx | 25 ++-
.../(portfolio)/your-projects/live/page.tsx | 30 +++-
.../(portfolio)/your-projects/live/types.ts | 15 +-
8 files changed, 351 insertions(+), 27 deletions(-)
create mode 100644 src/app/lib/measureDocumentRequirements.ts
diff --git a/src/app/api/upload/contractor-install/route.ts b/src/app/api/upload/contractor-install/route.ts
index 0f9376b..6dc6b66 100644
--- a/src/app/api/upload/contractor-install/route.ts
+++ b/src/app/api/upload/contractor-install/route.ts
@@ -7,6 +7,26 @@ import { z } from "zod";
import { getServerSession } from "next-auth";
import { AuthOptions } from "@/app/api/auth/[...nextauth]/authOptions";
import { syncContractorDocUploadToHubSpot } from "@/app/lib/hubspot/dealSync";
+import { getRequiredDocs } from "@/app/lib/measureDocumentRequirements";
+
+function computeMeasureProgress(
+ proposedMeasures: string[],
+ dealDocs: { fileType: string | null; measureName: string | null }[],
+) {
+ const installDocs = dealDocs.filter((d) => d.fileType !== null && d.measureName !== null);
+ return proposedMeasures.map((measureName) => {
+ const required = getRequiredDocs(measureName);
+ const docsForMeasure = installDocs.filter((d) => d.measureName === measureName);
+ const uploadedTypeSet = new Set(docsForMeasure.map((d) => d.fileType));
+ const uploadedCount = required.filter((r) => uploadedTypeSet.has(r)).length;
+ return {
+ measureName,
+ uploadedCount,
+ requiredCount: required.length,
+ isComplete: uploadedCount === required.length,
+ };
+ });
+}
// POST — record a contractor install document in uploaded_files (fileType optional — can be classified later)
export async function POST(req: NextRequest) {
@@ -85,6 +105,8 @@ export async function PATCH(req: NextRequest) {
measureName: z.string().optional(),
}),
),
+ hubspotDealId: z.string().optional(),
+ proposedMeasures: z.array(z.string()).optional(),
});
let body: z.infer;
@@ -110,6 +132,17 @@ export async function PATCH(req: NextRequest) {
.where(eq(uploadedFiles.id, BigInt(update.id)));
}
+ // Sync per-measure progress to HubSpot after classification
+ if (body.hubspotDealId && body.proposedMeasures && body.proposedMeasures.length > 0) {
+ const dealDocs = await db
+ .select({ fileType: uploadedFiles.fileType, measureName: uploadedFiles.measureName })
+ .from(uploadedFiles)
+ .where(eq(uploadedFiles.hubsotDealId, body.hubspotDealId));
+
+ const measureProgress = computeMeasureProgress(body.proposedMeasures, dealDocs);
+ void syncContractorDocUploadToHubSpot({ hubspotDealId: body.hubspotDealId, measureProgress });
+ }
+
return NextResponse.json({ success: true });
} catch (err) {
console.error("PATCH /upload/contractor-install error:", err);
diff --git a/src/app/lib/hubspot/dealSync.ts b/src/app/lib/hubspot/dealSync.ts
index f16b181..67443ed 100644
--- a/src/app/lib/hubspot/dealSync.ts
+++ b/src/app/lib/hubspot/dealSync.ts
@@ -71,16 +71,37 @@ export async function syncRemovalRequestToHubSpot(params: {
}
}
+type MeasureUploadProgress = {
+ measureName: string;
+ uploadedCount: number;
+ requiredCount: number;
+ isComplete: boolean;
+};
+
export async function syncContractorDocUploadToHubSpot(params: {
hubspotDealId: string;
+ measureProgress?: MeasureUploadProgress[];
}): Promise {
+ let log: string;
+ if (params.measureProgress && params.measureProgress.length > 0) {
+ log = params.measureProgress
+ .map((m) => {
+ if (m.isComplete) return `${m.measureName}: Complete (${m.uploadedCount}/${m.requiredCount} docs)`;
+ if (m.uploadedCount > 0) return `${m.measureName}: In Progress (${m.uploadedCount}/${m.requiredCount} docs)`;
+ return `${m.measureName}: Not Started (0/${m.requiredCount} docs)`;
+ })
+ .join(" | ");
+ } else {
+ log = "Documents available - uploaded by contractor";
+ }
+
const maxAttempts = 3;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
const client = getHubSpotClient();
await client.crm.deals.basicApi.update(params.hubspotDealId, {
properties: {
- contractor_document_upload_log: "Documents available - uploaded by contractor",
+ contractor_document_upload_log: log,
},
});
return;
diff --git a/src/app/lib/measureDocumentRequirements.ts b/src/app/lib/measureDocumentRequirements.ts
new file mode 100644
index 0000000..e196faf
--- /dev/null
+++ b/src/app/lib/measureDocumentRequirements.ts
@@ -0,0 +1,81 @@
+/**
+ * Measure Document Requirements
+ * Maps HubSpot measure names to the required installer document types (fileType enum values).
+ * Used to compute per-measure upload completion and guide contractors in the upload modal.
+ */
+
+// Required for every measure
+const BASE_DOCS = [
+ "pre_photo",
+ "mid_photo",
+ "post_photo",
+ "pre_installation_building_inspection",
+ "claim_of_compliance",
+ "insurance_guarantee",
+ "workmanship_warranty",
+] as const;
+
+// MCS-accredited measures require MCS certification in addition to base docs
+const MCS_EXTRA = ["mcs_compliance_certificate"] as const;
+
+export const MEASURE_DOC_REQUIREMENTS: Record = {
+ ASHP: [...BASE_DOCS, ...MCS_EXTRA, "commissioning_records"],
+ "Solar PV": [...BASE_DOCS, ...MCS_EXTRA, "g98_notification"],
+ DMevs: [
+ ...BASE_DOCS,
+ "dmev_photos",
+ "anemometer_readings",
+ "commissioning_records",
+ "part_f_ventilation_document",
+ "door_undercut_photos",
+ "trickle_vent_photos",
+ "ventilation_assessment_checklist",
+ "minor_works_electrical_certificate",
+ ],
+ "Loft insulation": [...BASE_DOCS, "loft_hatch_photo"],
+ // All remaining measures require BASE_DOCS only:
+ // CWI, EWI, IWI, "Flat roof", RIR, UFI, HW, Windows, "Ext. doors",
+ // TRVs, "Heating controls", "New boiler", HHRSH, Battery, LEL,
+ // "Listed building", "Removal 2nd heating", Others
+};
+
+/**
+ * Returns the required document types for a given measure name.
+ * Falls back to BASE_DOCS for any measure not explicitly listed.
+ */
+export function getRequiredDocs(measureName: string): string[] {
+ return MEASURE_DOC_REQUIREMENTS[measureName] ?? [...BASE_DOCS];
+}
+
+/**
+ * Human-readable label for a fileType enum value.
+ * Matches the labels used in ContractorUploadModal FILE_TYPE_OPTIONS.
+ */
+export const FILE_TYPE_LABELS: Record = {
+ 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",
+ pre_installation_building_inspection: "PIBI / Tech Survey",
+ point_of_work_risk_assessment: "Point of Work Risk Assessment",
+ 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",
+ ventilation_assessment_checklist: "Ventilation Assessment Checklist",
+ anemometer_readings: "Anemometer Readings",
+ commissioning_records: "Commissioning Records",
+ part_f_ventilation_document: "Approved Document Part F",
+ handover_pack: "Handover Pack",
+ workmanship_warranty: "Workmanship Warranty",
+ insurance_guarantee: "Insurance Backed Guarantee (IBG)",
+ g98_notification: "G98 / G99 Notification",
+ installer_qualifications: "Installer Qualifications",
+ installer_feedback: "Installer Feedback",
+ contractor_other: "Other",
+};
diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/ContractorUploadModal.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/ContractorUploadModal.tsx
index 1fa07b7..4b86fa3 100644
--- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/ContractorUploadModal.tsx
+++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/ContractorUploadModal.tsx
@@ -21,7 +21,8 @@ import {
} from "@/app/shadcn_components/ui/select";
import { CheckCircle2, XCircle, Upload, Loader2, Clock, ChevronDown, ChevronRight, Info } from "lucide-react";
import { uploadFileToS3 } from "@/app/utils/s3";
-import type { ClassifiedDeal } from "./types";
+import type { ClassifiedDeal, DocStatusMap } from "./types";
+import { getRequiredDocs } from "@/app/lib/measureDocumentRequirements";
// ── Types ─────────────────────────────────────────────────────────────────
@@ -44,12 +45,13 @@ type FileEntry = {
measureName: string;
};
-type Phase = "loading" | "upload" | "classify";
+type Phase = "loading" | "measure-select" | "upload" | "classify";
type Props = {
deal: ClassifiedDeal;
portfolioId: string;
onClose: () => void;
+ docStatusMap?: DocStatusMap;
};
// ── Constants ─────────────────────────────────────────────────────────────
@@ -200,11 +202,13 @@ async function recordUpload(payload: {
async function saveClassifications(
updates: { id: string; fileType: string; measureName?: string }[],
+ hubspotDealId?: string,
+ proposedMeasures?: string[],
): Promise {
const res = await fetch("/api/upload/contractor-install", {
method: "PATCH",
headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ updates }),
+ body: JSON.stringify({ updates, hubspotDealId, proposedMeasures }),
});
if (!res.ok) throw new Error("Failed to save classifications");
}
@@ -257,8 +261,29 @@ function PasGuidancePanel() {
// ── DocType select ────────────────────────────────────────────────────────
-function DocTypeSelect({ value, onChange, showHint = false }: { value: string; onChange: (v: string) => void; showHint?: boolean }) {
+function DocTypeSelect({
+ value,
+ onChange,
+ showHint = false,
+ requiredDocs,
+ uploadedDocs,
+}: {
+ value: string;
+ onChange: (v: string) => void;
+ showHint?: boolean;
+ requiredDocs?: string[]; // file types required for the selected measure
+ uploadedDocs?: string[]; // file types already uploaded for the selected measure
+}) {
const selected = FILE_TYPE_OPTIONS.find((o) => o.value === value);
+ const requiredSet = new Set(requiredDocs ?? []);
+ const uploadedSet = new Set(uploadedDocs ?? []);
+
+ // If we have required docs, show them as a priority group first
+ const priorityItems = requiredDocs && requiredDocs.length > 0
+ ? requiredDocs
+ .map((t) => FILE_TYPE_OPTIONS.find((o) => o.value === t))
+ .filter((o): o is typeof FILE_TYPE_OPTIONS[number] => !!o)
+ : [];
return (
@@ -268,8 +293,32 @@ function DocTypeSelect({ value, onChange, showHint = false }: { value: string; o
Select type…
+
+ {/* Priority group: required docs for the selected measure */}
+ {priorityItems.length > 0 && (
+
+
+ Required for this measure
+
+ {priorityItems.map((o) => {
+ const alreadyUploaded = uploadedSet.has(o.value);
+ return (
+
+ {o.label}
+ {alreadyUploaded && (
+ ✓ uploaded
+ )}
+
+ );
+ })}
+
+ )}
+
+ {/* Remaining groups */}
{FILE_TYPE_GROUPS.map((group) => {
- const items = FILE_TYPE_OPTIONS.filter((o) => o.group === group);
+ const items = FILE_TYPE_OPTIONS.filter(
+ (o) => o.group === group && !requiredSet.has(o.value),
+ );
if (!items.length) return null;
return (
@@ -305,7 +354,7 @@ function StatusIcon({ status, isExisting, errorMsg }: { status: FileStatus; isEx
// ── Main component ─────────────────────────────────────────────────────────
-export default function ContractorUploadModal({ deal, portfolioId, onClose }: Props) {
+export default function ContractorUploadModal({ deal, portfolioId, onClose, docStatusMap }: Props) {
const measures = parseMeasures(deal.proposedMeasures);
const fileInputRef = useRef(null);
const [isDragOver, setIsDragOver] = useState(false);
@@ -314,6 +363,8 @@ export default function ContractorUploadModal({ deal, portfolioId, onClose }: Pr
const [isUploading, setIsUploading] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const [saveError, setSaveError] = useState(null);
+ // The measure selected in the measure-select phase (empty = "not measure-specific")
+ const [selectedMeasure, setSelectedMeasure] = useState("");
// ── Fetch existing unclassified files on mount ───────────────────────
@@ -322,7 +373,7 @@ export default function ContractorUploadModal({ deal, portfolioId, onClose }: Pr
const uprnParam = deal.uprn;
const propIdParam = deal.landlordPropertyId;
if (!uprnParam && !propIdParam) {
- setPhase("upload");
+ setPhase(measures.length > 0 ? "measure-select" : "upload");
return;
}
@@ -350,12 +401,14 @@ export default function ContractorUploadModal({ deal, portfolioId, onClose }: Pr
}));
setQueue(entries);
setPhase("classify");
+ } else if (measures.length > 0) {
+ setPhase("measure-select");
} else {
setPhase("upload");
}
} catch {
- // If fetch fails, just proceed to upload phase
- setPhase("upload");
+ // If fetch fails, just proceed to measure-select (or upload if no measures)
+ setPhase(measures.length > 0 ? "measure-select" : "upload");
}
}
@@ -373,7 +426,7 @@ export default function ContractorUploadModal({ deal, portfolioId, onClose }: Pr
displaySize: formatSize(f.size),
status: "queued",
docType: "",
- measureName: measures[0] ?? "",
+ measureName: selectedMeasure,
}));
setQueue((prev) => [...prev, ...newEntries]);
}
@@ -473,6 +526,8 @@ export default function ContractorUploadModal({ deal, portfolioId, onClose }: Pr
fileType: f.docType,
measureName: (f.measureName && f.measureName !== "__none__") ? f.measureName : undefined,
})),
+ deal.dealId,
+ measures.length > 0 ? measures : undefined,
);
onClose();
} catch {
@@ -496,14 +551,24 @@ export default function ContractorUploadModal({ deal, portfolioId, onClose }: Pr
{phase === "loading" ? "Loading…" :
+ phase === "measure-select" ? "Select Measure" :
phase === "upload" ? "Upload Documents" :
"Classify Documents"}
{phase === "loading" && "Checking for pending files…"}
+ {phase === "measure-select" && (
+ <>
+ Which measure are you uploading documents for?{" "}
+ {propertyLabel}
+ >
+ )}
{phase === "upload" && (
<>
- Upload install documents for {propertyLabel}.
+ {selectedMeasure
+ ? <>Uploading documents for {selectedMeasure} — {propertyLabel}.>
+ : <>Upload install documents for {propertyLabel}.>
+ }
{existingCount > 0 && ` ${existingCount} file${existingCount !== 1 ? "s" : ""} are pending classification.`}
>
)}
@@ -525,6 +590,56 @@ export default function ContractorUploadModal({ deal, portfolioId, onClose }: Pr
)}
+ {/* ── Measure Select ── */}
+ {phase === "measure-select" && (() => {
+ const uprn = deal.uprn ?? null;
+ const docStatus = uprn ? docStatusMap?.[uprn] : undefined;
+ const measureProgressMap = new Map(
+ (docStatus?.measureProgress ?? []).map((m) => [m.measureName, m]),
+ );
+ return (
+
+
+ Select the measure you are uploading documents for. This helps track completion against required documents.
+
+
+ {measures.map((measure) => {
+ const progress = measureProgressMap.get(measure);
+ const isComplete = progress?.isComplete ?? false;
+ const uploaded = progress?.uploadedCount ?? 0;
+ const required = progress?.requiredCount ?? getRequiredDocs(measure).length;
+ return (
+
+ );
+ })}
+
+
+
+ );
+ })()}
+
{/* ── Phase 1: Upload ── */}
{phase === "upload" && (
<>
@@ -592,7 +707,14 @@ export default function ContractorUploadModal({ deal, portfolioId, onClose }: Pr
)}
{/* ── Phase 2: Classify ── */}
- {phase === "classify" && (
+ {phase === "classify" && (() => {
+ // Per-entry: look up what's already uploaded for that entry's measure
+ const uprn = deal.uprn ?? null;
+ const docStatus = uprn ? docStatusMap?.[uprn] : undefined;
+ const measureProgressMap = new Map(
+ (docStatus?.measureProgress ?? []).map((m) => [m.measureName, m]),
+ );
+ return (
{/* PAS guidance */}
@@ -604,7 +726,11 @@ export default function ContractorUploadModal({ deal, portfolioId, onClose }: Pr
Measure
- {classifiableEntries.map((entry) => (
+ {classifiableEntries.map((entry) => {
+ const entryMeasure = entry.measureName && entry.measureName !== "__none__" ? entry.measureName : null;
+ const requiredDocs = entryMeasure ? getRequiredDocs(entryMeasure) : undefined;
+ const uploadedDocs = entryMeasure ? (measureProgressMap.get(entryMeasure)?.uploaded ?? []) : undefined;
+ return (
@@ -614,7 +740,13 @@ export default function ContractorUploadModal({ deal, portfolioId, onClose }: Pr
{entry.existingS3Key &&
Previously uploaded
}
- updateEntryField(entry.id, "docType", v)} showHint />
+ updateEntryField(entry.id, "docType", v)}
+ showHint
+ requiredDocs={requiredDocs}
+ uploadedDocs={uploadedDocs}
+ />
{measures.length > 0 ? (
)}
- )}
+ );
+ })()}
@@ -659,6 +793,10 @@ export default function ContractorUploadModal({ deal, portfolioId, onClose }: Pr
)}
+ {phase === "measure-select" && (
+
+ )}
+
{phase === "upload" && (
<>
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 01baed9..8265602 100644
--- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/DocumentTable.tsx
+++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/DocumentTable.tsx
@@ -302,6 +302,7 @@ export default function DocumentTable({ data, onOpenDrawer, docStatusMap, portfo
deal={uploadDeal}
portfolioId={portfolioId}
onClose={() => setUploadDeal(null)}
+ docStatusMap={docStatusMap}
/>
)}
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 d630130..2263d0d 100644
--- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/DocumentTableColumns.tsx
+++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/DocumentTableColumns.tsx
@@ -49,19 +49,38 @@ function RetroAssessmentBadge({ status }: { status: DocStatus | undefined }) {
function InstallDocsBadge({ status }: { status: DocStatus | undefined }) {
const installStatus = status?.installStatus ?? "none";
+ const measureProgress = status?.measureProgress ?? [];
+
+ // Build a tooltip showing per-measure doc counts (e.g. "ASHP: 5/9, CWI: 7/7")
+ const tooltip =
+ measureProgress.length > 0
+ ? measureProgress
+ .map((m) => `${m.measureName}: ${m.uploadedCount}/${m.requiredCount}`)
+ .join(" | ")
+ : undefined;
+
if (installStatus === "all") {
return (
-
+
All Measures
);
}
if (installStatus === "partial") {
+ const completedCount = measureProgress.filter((m) => m.isComplete).length;
+ const totalCount = measureProgress.length;
+ const label = totalCount > 0 ? `${completedCount} / ${totalCount} measures` : "Some Measures";
return (
-
+
- Some Measures
+ {label}
);
}
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 fb2cf17..ff958a0 100644
--- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/page.tsx
+++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/page.tsx
@@ -13,8 +13,9 @@ import { portfolioCapabilities, portfolioUsers } from "@/app/db/schema/portfolio
import { dealMeasureApprovals } from "@/app/db/schema/approvals";
import { propertyRemovalRequests } from "@/app/db/schema/removal_requests";
import { user as userTable } from "@/app/db/schema/users";
-import type { HubspotDeal, DocStatusMap, DocStatus, PortfolioCapabilityType, ApprovalsByDeal, RemovalStatusByDeal, EffectiveRemovalState } from "./types";
+import type { HubspotDeal, DocStatusMap, DocStatus, MeasureDocProgress, PortfolioCapabilityType, ApprovalsByDeal, RemovalStatusByDeal, EffectiveRemovalState } from "./types";
import { EXPECTED_RETROFIT_ASSESSMENT_DOC_TYPES, SURVEY_ALL_DOC_TYPES } from "./types";
+import { getRequiredDocs } from "@/app/lib/measureDocumentRequirements";
import type { InferSelectModel } from "drizzle-orm";
import { Card, CardContent } from "@/app/shadcn_components/ui/card";
import { Building2 } from "lucide-react";
@@ -271,15 +272,33 @@ export default async function LiveReportingPage(props: {
const surveyTypeSet = new Set(surveyDocs.map((d) => d.fileType));
const measures = measuresByUprn.get(uprn) ?? [];
+
+ // Compute per-measure document progress against the requirements matrix
+ const measureProgress: MeasureDocProgress[] = measures.map((measureName) => {
+ const required = getRequiredDocs(measureName);
+ const docsForMeasure = installDocs.filter((d) => d.measureName === measureName);
+ const uploadedTypeSet = new Set(docsForMeasure.map((d) => d.fileType));
+ const uploaded = required.filter((r) => uploadedTypeSet.has(r));
+ return {
+ measureName,
+ required,
+ uploaded,
+ isComplete: uploaded.length === required.length,
+ uploadedCount: uploaded.length,
+ requiredCount: required.length,
+ };
+ });
+
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";
+ installStatus = measureProgress.every((m) => m.isComplete)
+ ? "all"
+ : measureProgress.some((m) => m.uploadedCount > 0)
+ ? "partial"
+ : "none";
}
}
@@ -289,6 +308,7 @@ export default async function LiveReportingPage(props: {
isSurveyComplete: EXPECTED_RETROFIT_ASSESSMENT_DOC_TYPES.every((t) => surveyTypeSet.has(t)),
hasInstallDocs: installDocs.length > 0,
installStatus,
+ measureProgress,
};
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 987ea3c..c4a070b 100644
--- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/types.ts
+++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/types.ts
@@ -250,6 +250,16 @@ export const SURVEY_ALL_DOC_TYPES = new Set([
"ecmk_survey_xml",
]);
+// Per-measure document upload progress
+export type MeasureDocProgress = {
+ measureName: string;
+ required: string[]; // required fileType values for this measure
+ uploaded: string[]; // required fileType values that have been uploaded
+ isComplete: boolean;
+ uploadedCount: number;
+ requiredCount: number;
+};
+
export type DocStatus = {
// Retrofit assessment docs
presentSurveyTypes: string[];
@@ -258,10 +268,11 @@ export type DocStatus = {
// 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
+ // "all" = all required docs uploaded for every proposed measure
+ // "partial" = some (but not all) proposed measures have complete docs
// "hasDocs" = has install docs but no measures defined on the deal
// "none" = no install docs at all
+ measureProgress: MeasureDocProgress[]; // one entry per proposed measure
};
export type DocStatusMap = Record; // keyed by UPRN string