diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/docStatus.test.ts b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/docStatus.test.ts index 1ddd9c8b..45eed2f3 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/docStatus.test.ts +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/docStatus.test.ts @@ -216,6 +216,69 @@ describe("computeDocStatusMap", () => { expect(result["deal-1"].installStatus).toBe("hasDocs"); }); + it('is "none" when the only docs are coordination docs, which the drawer lists separately', () => { + // Arrange + const deal = makeDeal({ dealId: "deal-1", proposedMeasures: null }); + const coordinationDocs = [ + { fileType: "improvement_option_evaluation", measureName: null }, + ]; + const docs = new Map([["deal-1", coordinationDocs]]); + + // Act + const result = computeDocStatusMap([deal], docs, {}); + + // Assert + expect(result["deal-1"].installStatus).toBe("none"); + }); + + it('is "none" when the only docs are design docs, which the drawer lists separately', () => { + // Arrange + const deal = makeDeal({ dealId: "deal-1", proposedMeasures: null }); + const designDocs = [{ fileType: "retrofit_design_doc", measureName: null }]; + const docs = new Map([["deal-1", designDocs]]); + + // Act + const result = computeDocStatusMap([deal], docs, {}); + + // Assert + expect(result["deal-1"].installStatus).toBe("none"); + }); + + it("counts only genuine install docs when coordination and design docs are also present", () => { + // Arrange + const deal = makeDeal({ dealId: "deal-1", proposedMeasures: cwi }); + const mixedDocs = [ + { fileType: "improvement_option_evaluation", measureName: null }, + { fileType: "retrofit_design_doc", measureName: null }, + ...cwiRequiredDocs.map((ft) => ({ fileType: ft, measureName: cwi })), + ]; + const docs = new Map([["deal-1", mixedDocs]]); + + // Act + const result = computeDocStatusMap([deal], docs, {}); + + // Assert + expect(result["deal-1"].measureProgress).toEqual([ + expect.objectContaining({ measureName: cwi, uploadedCount: cwiRequiredDocs.length }), + ]); + }); + + it("reports no install docs when only coordination and design docs are present", () => { + // Arrange + const deal = makeDeal({ dealId: "deal-1", proposedMeasures: null }); + const nonInstallDocs = [ + { fileType: "medium_term_improvement_plan", measureName: null }, + { fileType: "retrofit_design_doc", measureName: null }, + ]; + const docs = new Map([["deal-1", nonInstallDocs]]); + + // Act + const result = computeDocStatusMap([deal], docs, {}); + + // Assert + expect(result["deal-1"].hasInstallDocs).toBe(false); + }); + 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 diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/docStatus.ts b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/docStatus.ts index 00818e46..4efae42b 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/docStatus.ts +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/docStatus.ts @@ -2,6 +2,7 @@ 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 { isInstallDocType } from "./propertyDocuments"; import { EXPECTED_RETROFIT_ASSESSMENT_DOC_TYPES, SURVEY_ALL_DOC_TYPES, @@ -122,12 +123,12 @@ export function computeDocStatusMap( for (const [dealId, docs] of docsByDealId) { const surveyDocs = docs.filter((d) => SURVEY_ALL_DOC_TYPES.has(d.fileType)); - // Design documents are their own category — they are neither survey nor - // install docs. Without this carve-out a Retrofit Design Document (absent - // from the survey set) would be mis-bucketed as an install document. - const installDocs = docs.filter( - (d) => !SURVEY_ALL_DOC_TYPES.has(d.fileType) && !DESIGN_DOC_TYPES.has(d.fileType), - ); + // Survey, coordination and design docs each get their own drawer section, + // so none count as install docs. `isInstallDocType` is the shared predicate + // that keeps this in sync with the drawer's own split — re-implementing it + // inline once dropped the coordination carve-out and mis-bucketed + // coordination-only docs as install documents. + const installDocs = docs.filter((d) => isInstallDocType(d.fileType)); const surveyTypeSet = new Set(surveyDocs.map((d) => d.fileType)); const measures = measuresByDealId.get(dealId) ?? []; diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/propertyDocuments.ts b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/propertyDocuments.ts index e9ebf415..9bab6454 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/propertyDocuments.ts +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/propertyDocuments.ts @@ -6,6 +6,19 @@ import { } from "./types"; import type { PropertyDocument, MeasureDocProgress } from "./types"; +// Survey, coordination and design docs each get their own section in the +// property drawer, so none of them count as install docs. Kept as a single +// predicate shared with computeDocStatusMap: when the two drifted apart, the +// Install Docs badge in the properties table read "Has Docs" for properties +// whose drawer showed no install documents at all. +export function isInstallDocType(docType: string): boolean { + return ( + !SURVEY_ALL_DOC_TYPES.has(docType) && + !COORDINATION_DOC_TYPES.has(docType) && + !DESIGN_DOC_TYPES.has(docType) + ); +} + export function splitDocumentsByType(docs: PropertyDocument[]): { docs: PropertyDocument[]; coordinationDocs: PropertyDocument[]; @@ -16,12 +29,7 @@ export function splitDocumentsByType(docs: PropertyDocument[]): { docs: docs.filter((d) => SURVEY_ALL_DOC_TYPES.has(d.docType)), coordinationDocs: docs.filter((d) => COORDINATION_DOC_TYPES.has(d.docType)), designDocs: docs.filter((d) => DESIGN_DOC_TYPES.has(d.docType)), - installDocs: docs.filter( - (d) => - !SURVEY_ALL_DOC_TYPES.has(d.docType) && - !COORDINATION_DOC_TYPES.has(d.docType) && - !DESIGN_DOC_TYPES.has(d.docType), - ), + installDocs: docs.filter((d) => isInstallDocType(d.docType)), }; }