mirror of
https://github.com/Hestia-Homes/assessment-model.git
synced 2026-07-27 22:45:03 +00:00
Merge pull request #431 from Hestia-Homes/fix/major-condition-issue-flag
fix(live): flag major condition issues by the issue, not a retired stage / S3 URL
This commit is contained in:
commit
b8a0743ddc
6 changed files with 54 additions and 31 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -52,3 +52,6 @@ docs/wip/**
|
|||
.chip-preview.*
|
||||
.shot.cjs
|
||||
scratch-*.cjs
|
||||
|
||||
# Sandcastle agent worktrees (local scratch — never commit)
|
||||
.sandcastle/
|
||||
|
|
|
|||
|
|
@ -311,7 +311,6 @@ interface AnalyticsViewProps {
|
|||
columnLabels?: Partial<Record<keyof ClassifiedDeal, string>>,
|
||||
breakdown?: Record<string, ClassifiedDeal[]>,
|
||||
) => void;
|
||||
majorConditionDeals: ClassifiedDeal[];
|
||||
totalDeals: number;
|
||||
availableGroups: GroupNode[];
|
||||
groupFilter: string[];
|
||||
|
|
@ -325,7 +324,6 @@ export default function AnalyticsView({
|
|||
currentProjectCode,
|
||||
onProjectChange,
|
||||
onOpenTable,
|
||||
majorConditionDeals,
|
||||
totalDeals,
|
||||
availableGroups,
|
||||
groupFilter,
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -6,6 +6,7 @@ import {
|
|||
computeFunnelStages,
|
||||
computeProjectProgress,
|
||||
computeLiveTrackerData,
|
||||
hasMajorConditionIssue,
|
||||
} from "./transforms";
|
||||
import type { HubspotDeal, ClassifiedDeal } from "./types";
|
||||
|
||||
|
|
@ -408,15 +409,28 @@ describe("classifyDeals", () => {
|
|||
// -----------------------------------------------------------------------
|
||||
|
||||
describe("computeDampMouldRisk", () => {
|
||||
it("counts survey flags from majorConditionIssuePhotosS3", () => {
|
||||
it("counts survey flags from a flagged major condition issue", () => {
|
||||
const deals = [
|
||||
makeClassified({ majorConditionIssuePhotosS3: "s3://bucket/file.jpg" }),
|
||||
makeClassified({ majorConditionIssuePhotosS3: null }),
|
||||
makeClassified({ majorConditionIssueDescription: "Damp in bathroom" }),
|
||||
makeClassified({ majorConditionIssueDescription: null }),
|
||||
];
|
||||
const result = computeDampMouldRisk(deals);
|
||||
expect(result.surveyFlagCount).toBe(1);
|
||||
});
|
||||
|
||||
it("counts a survey flag even when no S3 evidence URL was mirrored", () => {
|
||||
// Regression: a flagged issue whose photos live in the raw HubSpot field
|
||||
// (not the S3 mirror) must still count — keying on the S3 URL missed these.
|
||||
const deals = [
|
||||
makeClassified({
|
||||
majorConditionIssueDescription: "Damp in bathroom",
|
||||
majorConditionIssuePhotos: "https://hubspot/uploaded/photo.jpeg",
|
||||
majorConditionIssuePhotosS3: null,
|
||||
}),
|
||||
];
|
||||
expect(computeDampMouldRisk(deals).surveyFlagCount).toBe(1);
|
||||
});
|
||||
|
||||
it("counts coordinator flags from dampMouldFlag", () => {
|
||||
const deals = [
|
||||
makeClassified({ dampMouldFlag: "Yes" }),
|
||||
|
|
@ -461,9 +475,9 @@ describe("computeDampMouldRisk", () => {
|
|||
|
||||
it("counts deals flagged at both stages independently", () => {
|
||||
const deals = [
|
||||
makeClassified({ majorConditionIssuePhotosS3: "s3://x", dampMouldFlag: "Yes" }),
|
||||
makeClassified({ majorConditionIssuePhotosS3: "s3://y", dampMouldFlag: null }),
|
||||
makeClassified({ majorConditionIssuePhotosS3: null, dampMouldFlag: "Yes" }),
|
||||
makeClassified({ majorConditionIssueDescription: "Damp", dampMouldFlag: "Yes" }),
|
||||
makeClassified({ majorConditionIssueDescription: "Mould", dampMouldFlag: null }),
|
||||
makeClassified({ majorConditionIssueDescription: null, dampMouldFlag: "Yes" }),
|
||||
];
|
||||
const result = computeDampMouldRisk(deals);
|
||||
expect(result.surveyFlagCount).toBe(2);
|
||||
|
|
@ -643,16 +657,6 @@ describe("computeLiveTrackerData", () => {
|
|||
expect(computeLiveTrackerData(deals).totalDeals).toBe(3);
|
||||
});
|
||||
|
||||
it("filters majorConditionDeals by the major condition stage ID", () => {
|
||||
const deals = [
|
||||
makeDeal({ dealstage: "3061261536" }), // MAJOR_CONDITION_STAGE_ID
|
||||
makeDeal({ dealstage: "1617223910" }),
|
||||
];
|
||||
const result = computeLiveTrackerData(deals);
|
||||
expect(result.majorConditionDeals).toHaveLength(1);
|
||||
expect(result.majorConditionDeals[0].dealstage).toBe("3061261536");
|
||||
});
|
||||
|
||||
it("groups deals without a projectCode under Unknown Project", () => {
|
||||
const deals = [makeDeal({ projectCode: null })];
|
||||
const result = computeLiveTrackerData(deals);
|
||||
|
|
@ -663,6 +667,19 @@ describe("computeLiveTrackerData", () => {
|
|||
const result = computeLiveTrackerData([]);
|
||||
expect(result.projects).toHaveLength(0);
|
||||
expect(result.totalDeals).toBe(0);
|
||||
expect(result.majorConditionDeals).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("hasMajorConditionIssue", () => {
|
||||
it("is true when a major condition issue is described", () => {
|
||||
expect(
|
||||
hasMajorConditionIssue(makeDeal({ majorConditionIssueDescription: "Damp in bathroom" })),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("is false when the description is null, empty, or whitespace only", () => {
|
||||
expect(hasMajorConditionIssue(makeDeal({ majorConditionIssueDescription: null }))).toBe(false);
|
||||
expect(hasMajorConditionIssue(makeDeal({ majorConditionIssueDescription: "" }))).toBe(false);
|
||||
expect(hasMajorConditionIssue(makeDeal({ majorConditionIssueDescription: " " }))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ import type {
|
|||
|
||||
import {
|
||||
STAGE_ORDER,
|
||||
MAJOR_CONDITION_STAGE_ID,
|
||||
SUCCESSFUL_SURVEY_OUTCOMES,
|
||||
} from "./types";
|
||||
|
||||
|
|
@ -170,7 +169,10 @@ function isCoordinatorFlagged(d: ClassifiedDeal): boolean {
|
|||
}
|
||||
|
||||
export function computeDampMouldRisk(deals: ClassifiedDeal[]): DampMouldRiskData {
|
||||
const surveyFlagDeals = deals.filter((d) => !!d.majorConditionIssuePhotosS3);
|
||||
// Survey-stage flag = the assessor recorded a major condition issue. Keyed on
|
||||
// the issue itself, not the S3 evidence-photo URL — that mirror is often empty
|
||||
// (photos land in the raw HubSpot field), which silently dropped flagged homes.
|
||||
const surveyFlagDeals = deals.filter(hasMajorConditionIssue);
|
||||
const coordinatorFlagDeals = deals.filter(isCoordinatorFlagged);
|
||||
const bothFlaggedCount = surveyFlagDeals.filter(isCoordinatorFlagged).length;
|
||||
|
||||
|
|
@ -261,6 +263,18 @@ export function computeProjectProgress(
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a deal has a major condition issue flagged (Awaab's Law). The signal
|
||||
* is the assessor's recorded description — there is no yes/no HubSpot field; a
|
||||
* description is only filled in when an issue exists. Whitespace-only counts as
|
||||
* empty.
|
||||
*/
|
||||
export function hasMajorConditionIssue(deal: {
|
||||
majorConditionIssueDescription: string | null;
|
||||
}): boolean {
|
||||
return !!deal.majorConditionIssueDescription?.trim();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Top-level function called by page.tsx
|
||||
// Orchestrates all transformations: classify, group by project, compute stats
|
||||
|
|
@ -271,11 +285,6 @@ export function computeLiveTrackerData(
|
|||
// Classify all deals (add displayStage field)
|
||||
const classified = classifyDeals(rawDeals);
|
||||
|
||||
// Filter for major condition deals (Awaab's Law)
|
||||
const majorConditionDeals = classified.filter(
|
||||
(d) => d.dealstage === MAJOR_CONDITION_STAGE_ID
|
||||
);
|
||||
|
||||
// Group deals by projectCode
|
||||
const grouped: Record<string, ClassifiedDeal[]> = {};
|
||||
for (const deal of classified) {
|
||||
|
|
@ -304,6 +313,5 @@ export function computeLiveTrackerData(
|
|||
return {
|
||||
projects,
|
||||
totalDeals: classified.length,
|
||||
majorConditionDeals,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -127,7 +127,7 @@ export type StageProgressItem = {
|
|||
// Damp & mould risk comparison (survey-stage vs coordination-stage flags)
|
||||
// -----------------------------------------------------------------------
|
||||
export type DampMouldRiskData = {
|
||||
surveyFlagCount: number; // majorConditionIssuePhotosS3 not null
|
||||
surveyFlagCount: number; // major condition issue flagged (description present)
|
||||
coordinatorFlagCount: number; // dampMouldFlag not null/non-empty
|
||||
bothFlaggedCount: number; // flagged at both stages (highest risk)
|
||||
totalDeals: number;
|
||||
|
|
@ -204,7 +204,6 @@ export type InstructedMeasuresByDeal = Record<string, string[]>;
|
|||
export type LiveTrackerProps = {
|
||||
projects: ProjectData[];
|
||||
totalDeals: number;
|
||||
majorConditionDeals: ClassifiedDeal[]; // for Awaab's Law card
|
||||
docStatusMap: DocStatusMap;
|
||||
userCapability: PortfolioCapabilityType;
|
||||
approvalsByDeal: ApprovalsByDeal;
|
||||
|
|
@ -336,8 +335,6 @@ export const SUCCESSFUL_SURVEY_OUTCOMES: ReadonlySet<string> = new Set([
|
|||
"EPC Completed",
|
||||
]);
|
||||
|
||||
export const MAJOR_CONDITION_STAGE_ID = "3061261536" as const;
|
||||
|
||||
// Order of stages for grouping/display (queries excluded from this list)
|
||||
export const STAGE_ORDER: DisplayStage[] = [
|
||||
"Scope & Planning",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue