mirror of
https://github.com/Hestia-Homes/assessment-model.git
synced 2026-06-08 11:37:25 +00:00
Move the DocStatusMap computation and document fetch logic out of the
server component into a dedicated docStatus.ts module. computeDocStatusMap is now a pure function with full test coverage (8 tests). Also consolidates the double user table lookup in page.tsx into a single query.
This commit is contained in:
parent
166cea397b
commit
281f15c11e
3 changed files with 357 additions and 194 deletions
|
|
@ -0,0 +1,168 @@
|
|||
import { describe, it, expect } from "vitest";
|
||||
import { computeDocStatusMap } from "./docStatus";
|
||||
import { EXPECTED_RETROFIT_ASSESSMENT_DOC_TYPES } from "./types";
|
||||
import type { HubspotDeal, ApprovalsByDeal } from "./types";
|
||||
|
||||
function makeDeal(overrides: Partial<HubspotDeal> = {}): HubspotDeal {
|
||||
return {
|
||||
id: "1",
|
||||
dealId: "deal-1",
|
||||
dealname: "Test Property",
|
||||
dealstage: null,
|
||||
companyId: null,
|
||||
projectCode: null,
|
||||
landlordPropertyId: null,
|
||||
uprn: null,
|
||||
outcome: null,
|
||||
outcomeNotes: null,
|
||||
majorConditionIssueDescription: null,
|
||||
majorConditionIssuePhotos: null,
|
||||
majorConditionIssuePhotosS3: null,
|
||||
coordinationStatus: null,
|
||||
designStatus: null,
|
||||
pashubLink: null,
|
||||
sharepointLink: null,
|
||||
dampMouldFlag: null,
|
||||
dampMouldAndRepairComments: null,
|
||||
preSapScore: null,
|
||||
coordinator: null,
|
||||
ioeV1Date: null,
|
||||
ioeV2Date: null,
|
||||
ioeV3Date: null,
|
||||
proposedMeasures: null,
|
||||
approvedPackage: null,
|
||||
designer: null,
|
||||
designDate: null,
|
||||
actualMeasuresInstalled: null,
|
||||
installer: null,
|
||||
installerHandover: null,
|
||||
lodgementStatus: null,
|
||||
measuresLodgementDate: null,
|
||||
fullLodgementDate: null,
|
||||
confirmedSurveyDate: null,
|
||||
confirmedSurveyTime: null,
|
||||
surveyedDate: null,
|
||||
designType: null,
|
||||
eiScore: null,
|
||||
eiScorePotential: null,
|
||||
epcSapScore: null,
|
||||
epcSapScorePotential: null,
|
||||
surveyType: null,
|
||||
measuresForPibiOrdered: null,
|
||||
pibiOrderDate: null,
|
||||
pibiCompletedDate: null,
|
||||
propertyHaltedDate: null,
|
||||
propertyHaltedReason: null,
|
||||
technicalApprovedMeasuresForInstall: null,
|
||||
domnaSurveyType: null,
|
||||
domnaSurveyDate: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
const allSurveyDocs = EXPECTED_RETROFIT_ASSESSMENT_DOC_TYPES.map((t) => ({
|
||||
fileType: t,
|
||||
measureName: null,
|
||||
}));
|
||||
|
||||
describe("computeDocStatusMap", () => {
|
||||
it("marks survey as complete when all 9 mandatory doc types are present", () => {
|
||||
const deal = makeDeal({ dealId: "deal-1" });
|
||||
const docs = new Map([["deal-1", allSurveyDocs]]);
|
||||
|
||||
const result = computeDocStatusMap([deal], docs, {});
|
||||
|
||||
expect(result["deal-1"].isSurveyComplete).toBe(true);
|
||||
expect(result["deal-1"].hasSurveyDocs).toBe(true);
|
||||
});
|
||||
|
||||
it("marks survey as incomplete when only some mandatory doc types are present", () => {
|
||||
const deal = makeDeal({ dealId: "deal-1" });
|
||||
const partialSurveyDocs = [{ fileType: "photo_pack", measureName: null }];
|
||||
const docs = new Map([["deal-1", partialSurveyDocs]]);
|
||||
|
||||
const result = computeDocStatusMap([deal], docs, {});
|
||||
|
||||
expect(result["deal-1"].isSurveyComplete).toBe(false);
|
||||
expect(result["deal-1"].hasSurveyDocs).toBe(true);
|
||||
});
|
||||
|
||||
it("omits a deal from the map when it has no uploaded documents", () => {
|
||||
const deal = makeDeal({ dealId: "deal-1" });
|
||||
|
||||
const result = computeDocStatusMap([deal], new Map(), {});
|
||||
|
||||
expect(result["deal-1"]).toBeUndefined();
|
||||
});
|
||||
|
||||
it("uses approved measures in preference to proposed measures", () => {
|
||||
const deal = makeDeal({ dealId: "deal-1", proposedMeasures: "CWI" });
|
||||
const approvalsByDeal: ApprovalsByDeal = { "deal-1": ["ASHP"] };
|
||||
const docs = new Map([["deal-1", allSurveyDocs]]);
|
||||
|
||||
const result = computeDocStatusMap([deal], docs, approvalsByDeal);
|
||||
|
||||
const measureNames = result["deal-1"].measureProgress.map((m) => m.measureName);
|
||||
expect(measureNames).toEqual(["ASHP"]);
|
||||
expect(measureNames).not.toContain("CWI");
|
||||
});
|
||||
|
||||
describe("installStatus", () => {
|
||||
// CWI requires only BASE_DOCS — simple to satisfy in tests
|
||||
const cwi = "CWI";
|
||||
const cwiRequiredDocs = [
|
||||
"pre_photo",
|
||||
"mid_photo",
|
||||
"post_photo",
|
||||
"pre_installation_building_inspection",
|
||||
"claim_of_compliance",
|
||||
"insurance_guarantee",
|
||||
"workmanship_warranty",
|
||||
];
|
||||
|
||||
it('is "all" when every measure has all required install docs uploaded', () => {
|
||||
const deal = makeDeal({ dealId: "deal-1", proposedMeasures: cwi });
|
||||
const installDocs = cwiRequiredDocs.map((ft) => ({
|
||||
fileType: ft,
|
||||
measureName: cwi,
|
||||
}));
|
||||
const docs = new Map([["deal-1", installDocs]]);
|
||||
|
||||
const result = computeDocStatusMap([deal], docs, {});
|
||||
|
||||
expect(result["deal-1"].installStatus).toBe("all");
|
||||
});
|
||||
|
||||
it('is "partial" when some (but not all) measures have docs uploaded', () => {
|
||||
const deal = makeDeal({ dealId: "deal-1", proposedMeasures: cwi });
|
||||
const partialInstallDocs = [{ fileType: "pre_photo", measureName: cwi }];
|
||||
const docs = new Map([["deal-1", partialInstallDocs]]);
|
||||
|
||||
const result = computeDocStatusMap([deal], docs, {});
|
||||
|
||||
expect(result["deal-1"].installStatus).toBe("partial");
|
||||
});
|
||||
|
||||
it('is "hasDocs" when install docs exist but no measures are defined on the deal', () => {
|
||||
const deal = makeDeal({ dealId: "deal-1", proposedMeasures: null });
|
||||
const installDocs = [{ fileType: "pre_photo", measureName: null }];
|
||||
const docs = new Map([["deal-1", installDocs]]);
|
||||
|
||||
const result = computeDocStatusMap([deal], docs, {});
|
||||
|
||||
expect(result["deal-1"].installStatus).toBe("hasDocs");
|
||||
});
|
||||
|
||||
it('is "none" when measures are defined but no install docs have been uploaded', () => {
|
||||
const deal = makeDeal({ dealId: "deal-1", proposedMeasures: cwi });
|
||||
// Only survey docs — no install docs
|
||||
const docs = new Map([["deal-1", allSurveyDocs]]);
|
||||
|
||||
const result = computeDocStatusMap([deal], docs, {});
|
||||
|
||||
expect(result["deal-1"].installStatus).toBe("none");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,152 @@
|
|||
import { inArray } from "drizzle-orm";
|
||||
import { db } from "@/app/db/db";
|
||||
import { uploadedFiles } from "@/app/db/schema/uploaded_files";
|
||||
import { getRequiredDocs } from "@/app/lib/measureDocumentRequirements";
|
||||
import {
|
||||
EXPECTED_RETROFIT_ASSESSMENT_DOC_TYPES,
|
||||
SURVEY_ALL_DOC_TYPES,
|
||||
} from "./types";
|
||||
import type {
|
||||
HubspotDeal,
|
||||
DocStatus,
|
||||
DocStatusMap,
|
||||
MeasureDocProgress,
|
||||
ApprovalsByDeal,
|
||||
} from "./types";
|
||||
|
||||
type DocEntry = { fileType: string; measureName: string | null };
|
||||
|
||||
export async function fetchDocsByDealId(
|
||||
deals: HubspotDeal[],
|
||||
dealIds: string[],
|
||||
): Promise<Map<string, DocEntry[]>> {
|
||||
const docsByDealId = new Map<string, DocEntry[]>();
|
||||
|
||||
if (dealIds.length === 0) return docsByDealId;
|
||||
|
||||
const phase1Rows = await db
|
||||
.select({
|
||||
hubsotDealId: uploadedFiles.hubsotDealId,
|
||||
fileType: uploadedFiles.fileType,
|
||||
measureName: uploadedFiles.measureName,
|
||||
})
|
||||
.from(uploadedFiles)
|
||||
.where(inArray(uploadedFiles.hubsotDealId, dealIds));
|
||||
|
||||
for (const row of phase1Rows) {
|
||||
if (!row.hubsotDealId || row.fileType === null) continue;
|
||||
if (!docsByDealId.has(row.hubsotDealId)) docsByDealId.set(row.hubsotDealId, []);
|
||||
docsByDealId.get(row.hubsotDealId)!.push({ fileType: row.fileType, measureName: row.measureName });
|
||||
}
|
||||
|
||||
// Phase 2: UPRN fallback for deals that returned no results in phase 1
|
||||
const dealsWithoutDocs = deals.filter((d) => !docsByDealId.has(d.dealId));
|
||||
const fallbackUprns = dealsWithoutDocs
|
||||
.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);
|
||||
|
||||
if (fallbackUprns.length > 0) {
|
||||
const phase2Rows = await db
|
||||
.select({
|
||||
uprn: uploadedFiles.uprn,
|
||||
fileType: uploadedFiles.fileType,
|
||||
measureName: uploadedFiles.measureName,
|
||||
})
|
||||
.from(uploadedFiles)
|
||||
.where(inArray(uploadedFiles.uprn, fallbackUprns));
|
||||
|
||||
const uprnToDealId = new Map<string, string>(
|
||||
dealsWithoutDocs
|
||||
.filter((d) => d.uprn)
|
||||
.map((d) => {
|
||||
try { return [String(BigInt(d.uprn!)), d.dealId] as [string, string]; }
|
||||
catch { return null; }
|
||||
})
|
||||
.filter((e): e is [string, string] => e !== null),
|
||||
);
|
||||
|
||||
for (const row of phase2Rows) {
|
||||
if (row.uprn === null || row.fileType === null) continue;
|
||||
const dealId = uprnToDealId.get(String(row.uprn));
|
||||
if (!dealId) continue;
|
||||
if (!docsByDealId.has(dealId)) docsByDealId.set(dealId, []);
|
||||
docsByDealId.get(dealId)!.push({ fileType: row.fileType, measureName: row.measureName });
|
||||
}
|
||||
}
|
||||
|
||||
return docsByDealId;
|
||||
}
|
||||
|
||||
export function computeDocStatusMap(
|
||||
deals: HubspotDeal[],
|
||||
docsByDealId: Map<string, Array<{ fileType: string; measureName: string | null }>>,
|
||||
approvalsByDeal: ApprovalsByDeal,
|
||||
): DocStatusMap {
|
||||
const measuresByDealId = new Map<string, string[]>();
|
||||
for (const deal of deals) {
|
||||
const approved = approvalsByDeal[deal.dealId] ?? [];
|
||||
const measures =
|
||||
approved.length > 0
|
||||
? approved
|
||||
: (deal.proposedMeasures ?? "")
|
||||
.split(",")
|
||||
.map((m) => m.trim())
|
||||
.filter(Boolean);
|
||||
measuresByDealId.set(deal.dealId, measures);
|
||||
}
|
||||
|
||||
const docStatusMap: DocStatusMap = {};
|
||||
|
||||
for (const [dealId, docs] of docsByDealId) {
|
||||
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 = measuresByDealId.get(dealId) ?? [];
|
||||
|
||||
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 {
|
||||
installStatus = measureProgress.every((m) => m.isComplete)
|
||||
? "all"
|
||||
: measureProgress.some((m) => m.uploadedCount > 0)
|
||||
? "partial"
|
||||
: "none";
|
||||
}
|
||||
}
|
||||
|
||||
docStatusMap[dealId] = {
|
||||
presentSurveyTypes: Array.from(surveyTypeSet),
|
||||
hasSurveyDocs: surveyDocs.length > 0,
|
||||
isSurveyComplete: EXPECTED_RETROFIT_ASSESSMENT_DOC_TYPES.every((t) =>
|
||||
surveyTypeSet.has(t),
|
||||
),
|
||||
hasInstallDocs: installDocs.length > 0,
|
||||
installStatus,
|
||||
measureProgress,
|
||||
};
|
||||
}
|
||||
|
||||
return docStatusMap;
|
||||
}
|
||||
|
|
@ -4,11 +4,11 @@ import { redirect } from "next/navigation";
|
|||
import { eq, inArray, and, desc, sql } from "drizzle-orm";
|
||||
import LiveTracker from "./LiveTracker";
|
||||
import { computeLiveTrackerData } from "./transforms";
|
||||
import { fetchDocsByDealId, computeDocStatusMap } from "./docStatus";
|
||||
import { db } from "@/app/db/db";
|
||||
import { hubspotDealData } from "@/app/db/schema/crm/hubspot_deal_table";
|
||||
import { alias } from "drizzle-orm/pg-core";
|
||||
import { hubspotUsers } from "@/app/db/schema/crm/hubspot_user_table";
|
||||
import { uploadedFiles } from "@/app/db/schema/uploaded_files";
|
||||
import { portfolioOrganisation } from "@/app/db/schema/portfolio_organisation";
|
||||
import { organisation } from "@/app/db/schema/organisation";
|
||||
import {
|
||||
|
|
@ -21,20 +21,12 @@ import { propertyRemovalRequests } from "@/app/db/schema/removal_requests";
|
|||
import { user as userTable } from "@/app/db/schema/users";
|
||||
import type {
|
||||
HubspotDeal,
|
||||
DocStatusMap,
|
||||
DocStatus,
|
||||
MeasureDocProgress,
|
||||
PortfolioCapabilityType,
|
||||
ApprovalsByDeal,
|
||||
InstructedMeasuresByDeal,
|
||||
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";
|
||||
|
|
@ -193,57 +185,53 @@ export default async function LiveReportingPage(props: {
|
|||
const deals = rawDeals.map(mapDbRowToHubspotDeal);
|
||||
const trackerData = computeLiveTrackerData(deals);
|
||||
|
||||
// Fetch current user's portfolio capabilities (approver / contractor — can have both)
|
||||
let userCapability: PortfolioCapabilityType = [];
|
||||
const userEmail = user?.user?.email;
|
||||
|
||||
// Single user lookup shared by capability and role queries
|
||||
let userId: bigint | null = null;
|
||||
if (userEmail) {
|
||||
const userRow = await db
|
||||
.select({ id: userTable.id })
|
||||
.from(userTable)
|
||||
.where(eq(userTable.email, userEmail))
|
||||
.limit(1);
|
||||
userId = userRow[0]?.id ?? null;
|
||||
}
|
||||
|
||||
if (userRow[0]) {
|
||||
const capRows = await db
|
||||
.select({ capability: portfolioCapabilities.capability })
|
||||
.from(portfolioCapabilities)
|
||||
.where(
|
||||
and(
|
||||
eq(portfolioCapabilities.portfolioId, BigInt(portfolioId)),
|
||||
eq(portfolioCapabilities.userId, userRow[0].id),
|
||||
),
|
||||
);
|
||||
userCapability = capRows
|
||||
.map((r) => r.capability)
|
||||
.filter(
|
||||
(c): c is "approver" | "contractor" =>
|
||||
c === "approver" || c === "contractor",
|
||||
);
|
||||
}
|
||||
// Fetch current user's portfolio capabilities (approver / contractor — can have both)
|
||||
let userCapability: PortfolioCapabilityType = [];
|
||||
if (userId) {
|
||||
const capRows = await db
|
||||
.select({ capability: portfolioCapabilities.capability })
|
||||
.from(portfolioCapabilities)
|
||||
.where(
|
||||
and(
|
||||
eq(portfolioCapabilities.portfolioId, BigInt(portfolioId)),
|
||||
eq(portfolioCapabilities.userId, userId),
|
||||
),
|
||||
);
|
||||
userCapability = capRows
|
||||
.map((r) => r.capability)
|
||||
.filter(
|
||||
(c): c is "approver" | "contractor" =>
|
||||
c === "approver" || c === "contractor",
|
||||
);
|
||||
}
|
||||
|
||||
// Fetch current user's portfolio role (creator / admin / write / read)
|
||||
let userRole = "read";
|
||||
if (userEmail) {
|
||||
const userRow = await db
|
||||
.select({ id: userTable.id })
|
||||
.from(userTable)
|
||||
.where(eq(userTable.email, userEmail))
|
||||
if (userId) {
|
||||
const roleRow = await db
|
||||
.select({ role: portfolioUsers.role })
|
||||
.from(portfolioUsers)
|
||||
.where(
|
||||
and(
|
||||
eq(portfolioUsers.portfolioId, BigInt(portfolioId)),
|
||||
eq(portfolioUsers.userId, userId),
|
||||
),
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (userRow[0]) {
|
||||
const roleRow = await db
|
||||
.select({ role: portfolioUsers.role })
|
||||
.from(portfolioUsers)
|
||||
.where(
|
||||
and(
|
||||
eq(portfolioUsers.portfolioId, BigInt(portfolioId)),
|
||||
eq(portfolioUsers.userId, userRow[0].id),
|
||||
),
|
||||
)
|
||||
.limit(1);
|
||||
userRole = roleRow[0]?.role ?? "read";
|
||||
}
|
||||
userRole = roleRow[0]?.role ?? "read";
|
||||
}
|
||||
|
||||
// Fetch currently approved measures for all deals in scope
|
||||
|
|
@ -318,153 +306,8 @@ export default async function LiveReportingPage(props: {
|
|||
if (state !== "none") removalStatusByDeal[row.hubspotDealId] = state;
|
||||
}
|
||||
|
||||
// Fetch document status for all deals — two-phase strategy:
|
||||
// Phase 1: query by dealId (reliable even when UPRN is missing from hubspot_deal_data)
|
||||
// Phase 2: UPRN fallback only for deals that returned no results in phase 1
|
||||
const docsByDealId = new Map<
|
||||
string,
|
||||
Array<{ fileType: string; measureName: string | null }>
|
||||
>();
|
||||
|
||||
if (dealIds.length > 0) {
|
||||
const phase1Rows = await db
|
||||
.select({
|
||||
hubsotDealId: uploadedFiles.hubsotDealId,
|
||||
fileType: uploadedFiles.fileType,
|
||||
measureName: uploadedFiles.measureName,
|
||||
})
|
||||
.from(uploadedFiles)
|
||||
.where(inArray(uploadedFiles.hubsotDealId, dealIds));
|
||||
|
||||
for (const row of phase1Rows) {
|
||||
if (!row.hubsotDealId || row.fileType === null) continue;
|
||||
if (!docsByDealId.has(row.hubsotDealId))
|
||||
docsByDealId.set(row.hubsotDealId, []);
|
||||
docsByDealId
|
||||
.get(row.hubsotDealId)!
|
||||
.push({ fileType: row.fileType, measureName: row.measureName });
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 2: for deals with no docs from phase 1 that have a UPRN, try UPRN lookup
|
||||
const dealsWithoutDocs = deals.filter((d) => !docsByDealId.has(d.dealId));
|
||||
const fallbackUprns = dealsWithoutDocs
|
||||
.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);
|
||||
|
||||
if (fallbackUprns.length > 0) {
|
||||
const phase2Rows = await db
|
||||
.select({
|
||||
uprn: uploadedFiles.uprn,
|
||||
fileType: uploadedFiles.fileType,
|
||||
measureName: uploadedFiles.measureName,
|
||||
})
|
||||
.from(uploadedFiles)
|
||||
.where(inArray(uploadedFiles.uprn, fallbackUprns));
|
||||
|
||||
// Map phase 2 UPRN results back to dealId
|
||||
const uprnToDealId = new Map<string, string>(
|
||||
dealsWithoutDocs
|
||||
.filter((d) => d.uprn)
|
||||
.map((d) => {
|
||||
try {
|
||||
return [String(BigInt(d.uprn!)), d.dealId] as [string, string];
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter((e): e is [string, string] => e !== null),
|
||||
);
|
||||
|
||||
for (const row of phase2Rows) {
|
||||
if (row.uprn === null || row.fileType === null) continue;
|
||||
const dealId = uprnToDealId.get(String(row.uprn));
|
||||
if (!dealId) continue;
|
||||
if (!docsByDealId.has(dealId)) docsByDealId.set(dealId, []);
|
||||
docsByDealId
|
||||
.get(dealId)!
|
||||
.push({ fileType: row.fileType, measureName: row.measureName });
|
||||
}
|
||||
}
|
||||
|
||||
// Build measures lookup by dealId (approved measures, falling back to proposed)
|
||||
const measuresByDealId = new Map<string, string[]>();
|
||||
for (const deal of deals) {
|
||||
const approved = approvalsByDeal[deal.dealId] ?? [];
|
||||
const measures =
|
||||
approved.length > 0
|
||||
? approved
|
||||
: (deal.proposedMeasures ?? "")
|
||||
.split(",")
|
||||
.map((m: string) => m.trim())
|
||||
.filter(Boolean);
|
||||
measuresByDealId.set(deal.dealId, measures);
|
||||
}
|
||||
|
||||
// Build docStatusMap keyed by dealId
|
||||
const docStatusMap: DocStatusMap = {};
|
||||
|
||||
for (const [dealId, docs] of docsByDealId) {
|
||||
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 = measuresByDealId.get(dealId) ?? [];
|
||||
|
||||
// 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 {
|
||||
installStatus = measureProgress.every((m) => m.isComplete)
|
||||
? "all"
|
||||
: measureProgress.some((m) => m.uploadedCount > 0)
|
||||
? "partial"
|
||||
: "none";
|
||||
}
|
||||
}
|
||||
|
||||
docStatusMap[dealId] = {
|
||||
presentSurveyTypes: Array.from(surveyTypeSet),
|
||||
hasSurveyDocs: surveyDocs.length > 0,
|
||||
isSurveyComplete: EXPECTED_RETROFIT_ASSESSMENT_DOC_TYPES.every((t) =>
|
||||
surveyTypeSet.has(t),
|
||||
),
|
||||
hasInstallDocs: installDocs.length > 0,
|
||||
installStatus,
|
||||
measureProgress,
|
||||
};
|
||||
}
|
||||
const docsByDealId = await fetchDocsByDealId(deals, dealIds);
|
||||
const docStatusMap = computeDocStatusMap(deals, docsByDealId, approvalsByDeal);
|
||||
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto px-6 pb-10 space-y-4">
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue