Merge pull request #274 from Hestia-Homes/feature/rc-rd-files-ui
Some checks failed
Test Suite / unit-tests (push) Has been cancelled

Coordination and Design files UI
This commit is contained in:
Daniel Roth 2026-05-21 11:53:01 +01:00 committed by GitHub
commit 8b79f9a23e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 91 additions and 32 deletions

View file

@ -16,7 +16,7 @@ import {
} from "lucide-react";
import type { PropertyDocument, DocStatus } from "./types";
import { EXPECTED_RETROFIT_ASSESSMENT_DOC_TYPES } from "./types";
import { splitDocumentsByType, getMissingRetrofitTypes, getUnassignedInstallDocs } from "./propertyDocuments";
import { splitDocumentsByType, getMissingSurveyDocTypes, getUnassignedInstallDocs } from "./propertyDocuments";
import ContractorUploadModal from "./ContractorUploadModal";
import type { ClassifiedDeal, PortfolioCapabilityType } from "./types";
@ -59,6 +59,9 @@ export const DOC_TYPE_LABELS: Record<string, string> = {
installer_qualifications: "Installer Qualifications",
installer_feedback: "Installer Feedback",
contractor_other: "Other",
improvement_option_evaluation: "Improvement Option Evaluation",
medium_term_improvement_plan: "Medium Term Improvement Plan",
retrofit_design_doc: "Retrofit Design Document"
};
function formatDocDate(iso: string): string {
@ -172,8 +175,8 @@ export default function PropertyDocumentsContent({
return next;
});
const { retrofitDocs, installDocs } = splitDocumentsByType(documents);
const missingRetrofitTypes = getMissingRetrofitTypes(retrofitDocs);
const { docs: surveyDocs, coordinationDocs, designDocs, installDocs } = splitDocumentsByType(documents);
const missingSurveyDocTypes = getMissingSurveyDocTypes(surveyDocs);
const hasDocuments = documents.length > 0;
const isContractor = userCapability?.includes("contractor") ?? false;
@ -232,9 +235,9 @@ export default function PropertyDocumentsContent({
</div>
<div className="space-y-1.5">
<h3 className="text-xs font-semibold uppercase tracking-wide text-amber-500 px-0.5">
Missing Documents ({missingRetrofitTypes.length})
Missing Documents ({missingSurveyDocTypes.length})
</h3>
{missingRetrofitTypes.map((t) => (
{missingSurveyDocTypes.map((t) => (
<div
key={t}
className="flex items-center gap-2.5 p-3 rounded-lg border border-dashed border-amber-200 bg-amber-50/40"
@ -254,25 +257,25 @@ export default function PropertyDocumentsContent({
{!isFetching && !isError && hasDocuments && (
<>
{/* Retrofit Assessment */}
<motion.div key="retrofit" initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="space-y-2">
<motion.div key="survey" initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="space-y-2">
<h3 className="text-xs font-semibold uppercase tracking-wide text-gray-400 px-0.5">
Retrofit Assessment Documents
</h3>
{retrofitDocs.length > 0 ? (
{surveyDocs.length > 0 ? (
<div className="space-y-1.5">
{retrofitDocs.map((doc) => (
{surveyDocs.map((doc) => (
<DocumentRow key={doc.id} doc={doc} />
))}
</div>
) : (
<p className="text-xs text-gray-400 px-0.5">None uploaded yet.</p>
)}
{missingRetrofitTypes.length > 0 && (
{missingSurveyDocTypes.length > 0 && (
<div className="space-y-1.5 pt-1">
<h4 className="text-xs font-semibold uppercase tracking-wide text-amber-500 px-0.5">
Missing ({missingRetrofitTypes.length})
Missing ({missingSurveyDocTypes.length})
</h4>
{missingRetrofitTypes.map((t) => (
{missingSurveyDocTypes.map((t) => (
<div
key={t}
className="flex items-center gap-2.5 p-3 rounded-lg border border-dashed border-amber-200 bg-amber-50/40"
@ -287,6 +290,40 @@ export default function PropertyDocumentsContent({
)}
</motion.div>
{/* Coordination Documents */}
<motion.div key="coordination" initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="space-y-3">
<h3 className="text-xs font-semibold uppercase tracking-wide text-gray-400 px-0.5 flex items-center gap-1.5">
Coordination Documents
</h3>
{coordinationDocs.length > 0 ? (
<div className="space-y-1.5">
{coordinationDocs.map((doc) => (
<DocumentRow key={doc.id} doc={doc} />
))}
</div>
) : (
<p className="text-xs text-gray-400 px-0.5">None uploaded yet.</p>
)}
</motion.div>
{/* Design Documents */}
<motion.div key="design" initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="space-y-3">
<h3 className="text-xs font-semibold uppercase tracking-wide text-gray-400 px-0.5 flex items-center gap-1.5">
Design Documents
</h3>
{designDocs.length > 0 ? (
<div className="space-y-1.5">
{designDocs.map((doc) => (
<DocumentRow key={doc.id} doc={doc} />
))}
</div>
) : (
<p className="text-xs text-gray-400 px-0.5">None uploaded yet.</p>
)}
</motion.div>
{/* Install Documents */}
<motion.div key="install" initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="space-y-3">
<h3 className="text-xs font-semibold uppercase tracking-wide text-gray-400 px-0.5 flex items-center gap-1.5">

View file

@ -93,7 +93,7 @@ export function computeDocStatusMap(
approved.length > 0
? approved
: (deal.proposedMeasures ?? "")
.split(",")
.split(/[,;]/)
.map((m) => m.trim())
.filter(Boolean);
measuresByDealId.set(deal.dealId, measures);

View file

@ -1,7 +1,7 @@
import { describe, it, expect } from "vitest";
import {
splitDocumentsByType,
getMissingRetrofitTypes,
getMissingSurveyDocTypes,
getUnassignedInstallDocs,
} from "./propertyDocuments";
import type { PropertyDocument, MeasureDocProgress } from "./types";
@ -35,21 +35,21 @@ function makeMeasureProgress(overrides: Partial<MeasureDocProgress> = {}): Measu
describe("splitDocumentsByType", () => {
it("puts survey doc types in retrofitDocs", () => {
const doc = makeDoc({ docType: "photo_pack" });
const { retrofitDocs, installDocs } = splitDocumentsByType([doc]);
const { docs: retrofitDocs, installDocs } = splitDocumentsByType([doc]);
expect(retrofitDocs).toHaveLength(1);
expect(installDocs).toHaveLength(0);
});
it("puts install doc types in installDocs", () => {
const doc = makeDoc({ docType: "pre_photo" });
const { retrofitDocs, installDocs } = splitDocumentsByType([doc]);
const { docs: retrofitDocs, installDocs } = splitDocumentsByType([doc]);
expect(retrofitDocs).toHaveLength(0);
expect(installDocs).toHaveLength(1);
});
it("includes optional ecmk types in retrofitDocs", () => {
const doc = makeDoc({ docType: "ecmk_site_note" });
const { retrofitDocs } = splitDocumentsByType([doc]);
const { docs: retrofitDocs } = splitDocumentsByType([doc]);
expect(retrofitDocs).toHaveLength(1);
});
@ -60,13 +60,13 @@ describe("splitDocumentsByType", () => {
makeDoc({ id: "3", docType: "site_note" }),
makeDoc({ id: "4", docType: "post_photo" }),
];
const { retrofitDocs, installDocs } = splitDocumentsByType(docs);
const { docs: retrofitDocs, installDocs } = splitDocumentsByType(docs);
expect(retrofitDocs).toHaveLength(2);
expect(installDocs).toHaveLength(2);
});
it("returns empty arrays for empty input", () => {
const { retrofitDocs, installDocs } = splitDocumentsByType([]);
const { docs: retrofitDocs, installDocs } = splitDocumentsByType([]);
expect(retrofitDocs).toHaveLength(0);
expect(installDocs).toHaveLength(0);
});
@ -74,15 +74,15 @@ describe("splitDocumentsByType", () => {
describe("getMissingRetrofitTypes", () => {
it("returns all mandatory types when no docs uploaded", () => {
const missing = getMissingRetrofitTypes([]);
expect(missing).toHaveLength(9);
const missing = getMissingSurveyDocTypes([]);
expect(missing).toHaveLength(8);
});
it("excludes types that have been uploaded", () => {
const uploaded = [makeDoc({ docType: "photo_pack" })];
const missing = getMissingRetrofitTypes(uploaded);
const missing = getMissingSurveyDocTypes(uploaded);
expect(missing).not.toContain("photo_pack");
expect(missing).toHaveLength(8);
expect(missing).toHaveLength(7);
});
it("returns empty array when all mandatory types uploaded", () => {
@ -91,14 +91,14 @@ describe("getMissingRetrofitTypes", () => {
"pas_2023_condition", "pas_significance", "par_photo_pack",
"pas_2023_property", "pas_2023_occupancy",
].map((docType, i) => makeDoc({ id: String(i), docType }));
expect(getMissingRetrofitTypes(uploaded)).toHaveLength(0);
expect(getMissingSurveyDocTypes(uploaded)).toHaveLength(0);
});
it("does not count ecmk types as mandatory", () => {
const uploaded = [makeDoc({ docType: "ecmk_site_note" })];
const missing = getMissingRetrofitTypes(uploaded);
const missing = getMissingSurveyDocTypes(uploaded);
expect(missing).not.toContain("ecmk_site_note");
expect(missing).toHaveLength(9);
expect(missing).toHaveLength(8);
});
});

View file

@ -1,21 +1,32 @@
import {
SURVEY_ALL_DOC_TYPES,
EXPECTED_RETROFIT_ASSESSMENT_DOC_TYPES,
COORDINATION_DOC_TYPES,
DESIGN_DOC_TYPES,
} from "./types";
import type { PropertyDocument, MeasureDocProgress } from "./types";
export function splitDocumentsByType(docs: PropertyDocument[]): {
retrofitDocs: PropertyDocument[];
docs: PropertyDocument[];
coordinationDocs: PropertyDocument[];
designDocs: PropertyDocument[];
installDocs: PropertyDocument[];
} {
return {
retrofitDocs: docs.filter((d) => SURVEY_ALL_DOC_TYPES.has(d.docType)),
installDocs: docs.filter((d) => !SURVEY_ALL_DOC_TYPES.has(d.docType)),
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),
),
};
}
export function getMissingRetrofitTypes(retrofitDocs: PropertyDocument[]): string[] {
const present = new Set(retrofitDocs.map((d) => d.docType));
export function getMissingSurveyDocTypes(surveyDocs: PropertyDocument[]): string[] {
const present = new Set(surveyDocs.map((d) => d.docType));
return EXPECTED_RETROFIT_ASSESSMENT_DOC_TYPES.filter((t) => !present.has(t));
}

View file

@ -239,7 +239,6 @@ export const EXPECTED_RETROFIT_ASSESSMENT_DOC_TYPES = [
"rd_sap_site_note",
"pas_2023_ventilation",
"pas_2023_condition",
"pas_significance",
"par_photo_pack",
"pas_2023_property",
"pas_2023_occupancy",
@ -251,6 +250,18 @@ export const SURVEY_ALL_DOC_TYPES = new Set<string>([
"ecmk_site_note",
"ecmk_rd_sap_site_note",
"ecmk_survey_xml",
"pas_significance",
]);
// Coordination doc types
export const COORDINATION_DOC_TYPES = new Set<string>([
"improvement_option_evaluation",
"medium_term_improvement_plan"
]);
// Design doc types
export const DESIGN_DOC_TYPES = new Set<string>([
"retrofit_design_doc"
]);
// Per-measure document upload progress
@ -267,7 +278,7 @@ export type DocStatus = {
// Retrofit assessment docs
presentSurveyTypes: string[];
hasSurveyDocs: boolean;
isSurveyComplete: boolean; // all 9 EXPECTED_RETROFIT_ASSESSMENT_DOC_TYPES present (ecmk not counted)
isSurveyComplete: boolean; // all 8 EXPECTED_RETROFIT_ASSESSMENT_DOC_TYPES present (ecmk not counted)
// Install docs
hasInstallDocs: boolean;
installStatus: "none" | "partial" | "hasDocs" | "all";