From 62e9c548f15eb62eb7f38933a1115e1a8993bc41 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Thu, 28 May 2026 14:58:23 +0000 Subject: [PATCH] Surface coordinator damp & mould commentary in the risk drill-down MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The risk drill-down's coordinator-stage table showed "Yes" for every row, which carried no useful signal. It also missed properties where the coordinator wrote a comment without setting the growth flag. Include rows where dampmould_growth is "yes" (case-insensitive) OR the comment is populated, and render the comment in the cell — truncated with a popover for the full text, or a "no note from coordinator" placeholder when the row is here only because the flag was ticked. Also drop the typo in the schema property name (damnp -> damp); SQL column unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) --- package-lock.json | 56 +++++++++++++++++++ package.json | 1 + src/app/db/schema/crm/hubspot_deal_table.ts | 2 +- .../your-projects/live/DampMouldRiskPanel.tsx | 2 - .../your-projects/live/DrillDownTable.tsx | 51 ++++++++++++++++- .../your-projects/live/[dealId]/page.test.ts | 2 +- .../your-projects/live/dealQuery.ts | 2 +- .../your-projects/live/transforms.test.ts | 32 +++++++++++ .../your-projects/live/transforms.ts | 10 +++- src/app/shadcn_components/ui/popover.tsx | 31 ++++++++++ 10 files changed, 181 insertions(+), 8 deletions(-) create mode 100644 src/app/shadcn_components/ui/popover.tsx diff --git a/package-lock.json b/package-lock.json index ef282c4..7bc83da 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "@radix-ui/react-hover-card": "^1.0.6", "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-navigation-menu": "^1.1.3", + "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-scroll-area": "^1.2.10", "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-separator": "^1.0.3", @@ -3812,6 +3813,61 @@ } } }, + "node_modules/@radix-ui/react-popover": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz", + "integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-popper": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", diff --git a/package.json b/package.json index 30b7374..eae3d63 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "@radix-ui/react-hover-card": "^1.0.6", "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-navigation-menu": "^1.1.3", + "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-scroll-area": "^1.2.10", "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-separator": "^1.0.3", diff --git a/src/app/db/schema/crm/hubspot_deal_table.ts b/src/app/db/schema/crm/hubspot_deal_table.ts index 1a65110..3d2e97c 100644 --- a/src/app/db/schema/crm/hubspot_deal_table.ts +++ b/src/app/db/schema/crm/hubspot_deal_table.ts @@ -47,7 +47,7 @@ export const hubspotDealData = pgTable("hubspot_deal_data", { expectedCommencementDate: timestamp("expected_commencement_date", { precision: 6, withTimezone: true }), coordination_comments: text("coordination_comments"), surveyor: text("surveyor"), - damnpMouldAndRepairComments: text("damp_mould_and_repairs_comments"), + dampMouldAndRepairComments: text("damp_mould_and_repairs_comments"), batch: text("batch"), batchDescription: text("batch_description"), blockReference: text("block_reference"), diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/DampMouldRiskPanel.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/DampMouldRiskPanel.tsx index ffebcd4..816de4b 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/DampMouldRiskPanel.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/DampMouldRiskPanel.tsx @@ -120,7 +120,6 @@ export default function DampMouldRiskPanel({ const coordColumns: (keyof ClassifiedDeal)[] = [ "dealname", "landlordPropertyId", - "dampMouldFlag", "dampMouldAndRepairComments", "coordinator", ]; @@ -128,7 +127,6 @@ export default function DampMouldRiskPanel({ const coordLabels: Partial> = { dealname: "Address", landlordPropertyId: "Property Ref", - dampMouldFlag: "Coordinator Flag", dampMouldAndRepairComments: "Comments", coordinator: "Coordinator", }; diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/DrillDownTable.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/DrillDownTable.tsx index 7adf28e..25dc5f8 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/DrillDownTable.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/DrillDownTable.tsx @@ -22,9 +22,55 @@ import { TableRow, } from "@/app/shadcn_components/ui/table"; import { Input } from "@/app/shadcn_components/ui/input"; -import { Search, Download, ChevronLeft, ChevronRight } from "lucide-react"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/app/shadcn_components/ui/popover"; +import { Search, Download, ChevronLeft, ChevronRight, ChevronDown } from "lucide-react"; import type { ClassifiedDeal, HubspotDeal } from "./types"; +const NO_COMMENT_PLACEHOLDER = + "Damp & mould discovered — no note from coordinator"; +const COMMENT_PREVIEW_LIMIT = 60; + +function DampMouldCommentCell({ value }: { value: unknown }) { + const comment = typeof value === "string" ? value.trim() : ""; + + if (!comment) { + return ( + + {NO_COMMENT_PLACEHOLDER} + + ); + } + + const preview = + comment.length > COMMENT_PREVIEW_LIMIT + ? comment.slice(0, COMMENT_PREVIEW_LIMIT).trimEnd() + "…" + : comment; + + return ( + + + + + + {comment} + + + ); +} + interface DrillDownTableProps { data: ClassifiedDeal[]; columns?: (keyof HubspotDeal)[]; @@ -127,6 +173,9 @@ export default function DrillDownTable({ if (key === "majorConditionIssuePhotosS3") { return ; } + if (key === "dampMouldAndRepairComments") { + return ; + } return ( {value != null ? String(value) : ( diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/[dealId]/page.test.ts b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/[dealId]/page.test.ts index f4fa001..0b2f2d7 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/[dealId]/page.test.ts +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/[dealId]/page.test.ts @@ -165,7 +165,7 @@ const mockDealRow = { pashubLink: null, sharepointLink: null, dampmouldGrowth: null, - damnpMouldAndRepairComments: null, + dampMouldAndRepairComments: null, preSap: null, mtpCompletionDate: null, mtpReModelCompletionDate: null, diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/dealQuery.ts b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/dealQuery.ts index bf34ff9..8f65919 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/dealQuery.ts +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/dealQuery.ts @@ -34,7 +34,7 @@ export function mapDbRowToHubspotDeal(row: DealRow): HubspotDeal { pashubLink: d.pashubLink, sharepointLink: d.sharepointLink, dampMouldFlag: d.dampmouldGrowth, - dampMouldAndRepairComments: d.damnpMouldAndRepairComments, + dampMouldAndRepairComments: d.dampMouldAndRepairComments, preSapScore: d.preSap, coordinator: row.coordinator, ioeV1Date: d.mtpCompletionDate, diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/transforms.test.ts b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/transforms.test.ts index f8df537..9145af8 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/transforms.test.ts +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/transforms.test.ts @@ -305,6 +305,38 @@ describe("computeDampMouldRisk", () => { expect(result.coordinatorFlagCount).toBe(2); }); + it("ignores a growth flag of 'No' when there is no comment", () => { + const deals = [ + makeClassified({ dampMouldFlag: "No", dampMouldAndRepairComments: null }), + makeClassified({ dampMouldFlag: "no", dampMouldAndRepairComments: " " }), + ]; + const result = computeDampMouldRisk(deals); + expect(result.coordinatorFlagCount).toBe(0); + }); + + it("treats a 'yes' growth flag case-insensitively, ignoring whitespace", () => { + const deals = [ + makeClassified({ dampMouldFlag: "yes" }), + makeClassified({ dampMouldFlag: " Yes " }), + makeClassified({ dampMouldFlag: "YES" }), + ]; + const result = computeDampMouldRisk(deals); + expect(result.coordinatorFlagCount).toBe(3); + }); + + it("counts a deal with a comment but no growth flag as coordinator-flagged", () => { + const deals = [ + makeClassified({ + dampMouldFlag: null, + dampMouldAndRepairComments: "Mould in NE bedroom corner", + }), + makeClassified({ dampMouldFlag: null, dampMouldAndRepairComments: null }), + ]; + const result = computeDampMouldRisk(deals); + expect(result.coordinatorFlagCount).toBe(1); + expect(result.coordinatorFlagDeals).toHaveLength(1); + }); + it("counts deals flagged at both stages independently", () => { const deals = [ makeClassified({ majorConditionIssuePhotosS3: "s3://x", dampMouldFlag: "Yes" }), diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/transforms.ts b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/transforms.ts index 0223e76..5d78839 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/transforms.ts +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/transforms.ts @@ -145,10 +145,16 @@ export function classifyDeals(deals: HubspotDeal[]): ClassifiedDeal[] { // ----------------------------------------------------------------------- // Compute damp & mould risk — survey vs coordination stage comparison // ----------------------------------------------------------------------- +function isCoordinatorFlagged(d: ClassifiedDeal): boolean { + const growthIsYes = d.dampMouldFlag?.trim().toLowerCase() === "yes"; + const hasComment = !!d.dampMouldAndRepairComments?.trim(); + return growthIsYes || hasComment; +} + export function computeDampMouldRisk(deals: ClassifiedDeal[]): DampMouldRiskData { const surveyFlagDeals = deals.filter((d) => !!d.majorConditionIssuePhotosS3); - const coordinatorFlagDeals = deals.filter((d) => !!d.dampMouldFlag); - const bothFlaggedCount = surveyFlagDeals.filter((d) => !!d.dampMouldFlag).length; + const coordinatorFlagDeals = deals.filter(isCoordinatorFlagged); + const bothFlaggedCount = surveyFlagDeals.filter(isCoordinatorFlagged).length; return { surveyFlagCount: surveyFlagDeals.length, diff --git a/src/app/shadcn_components/ui/popover.tsx b/src/app/shadcn_components/ui/popover.tsx new file mode 100644 index 0000000..3a99108 --- /dev/null +++ b/src/app/shadcn_components/ui/popover.tsx @@ -0,0 +1,31 @@ +"use client" + +import * as React from "react" +import * as PopoverPrimitive from "@radix-ui/react-popover" + +import { cn } from "@/lib/utils" + +const Popover = PopoverPrimitive.Root + +const PopoverTrigger = PopoverPrimitive.Trigger + +const PopoverContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( + + + +)) +PopoverContent.displayName = PopoverPrimitive.Content.displayName + +export { Popover, PopoverTrigger, PopoverContent }