Surface coordinator damp & mould commentary in the risk drill-down

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) <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-05-28 14:58:23 +00:00
parent 5c06e69102
commit 62e9c548f1
10 changed files with 181 additions and 8 deletions

56
package-lock.json generated
View file

@ -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",

View file

@ -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",

View file

@ -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"),

View file

@ -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<Record<keyof ClassifiedDeal, string>> = {
dealname: "Address",
landlordPropertyId: "Property Ref",
dampMouldFlag: "Coordinator Flag",
dampMouldAndRepairComments: "Comments",
coordinator: "Coordinator",
};

View file

@ -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 (
<span className="text-sm italic text-gray-500">
{NO_COMMENT_PLACEHOLDER}
</span>
);
}
const preview =
comment.length > COMMENT_PREVIEW_LIMIT
? comment.slice(0, COMMENT_PREVIEW_LIMIT).trimEnd() + "…"
: comment;
return (
<Popover>
<PopoverTrigger asChild>
<button
type="button"
className="group inline-flex items-center gap-1 text-left text-sm text-gray-800 hover:text-brandblue underline-offset-2 hover:underline focus:outline-none focus:underline"
>
<span>{preview}</span>
<ChevronDown className="h-3.5 w-3.5 shrink-0 text-gray-400 group-hover:text-brandblue" />
</button>
</PopoverTrigger>
<PopoverContent
align="start"
className="w-80 max-w-[90vw] whitespace-pre-wrap break-words text-sm text-gray-800"
>
{comment}
</PopoverContent>
</Popover>
);
}
interface DrillDownTableProps {
data: ClassifiedDeal[];
columns?: (keyof HubspotDeal)[];
@ -127,6 +173,9 @@ export default function DrillDownTable({
if (key === "majorConditionIssuePhotosS3") {
return <PhotoDownloadCell value={value} />;
}
if (key === "dampMouldAndRepairComments") {
return <DampMouldCommentCell value={value} />;
}
return (
<span className="text-sm text-gray-800">
{value != null ? String(value) : (

View file

@ -165,7 +165,7 @@ const mockDealRow = {
pashubLink: null,
sharepointLink: null,
dampmouldGrowth: null,
damnpMouldAndRepairComments: null,
dampMouldAndRepairComments: null,
preSap: null,
mtpCompletionDate: null,
mtpReModelCompletionDate: null,

View file

@ -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,

View file

@ -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" }),

View file

@ -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,

View file

@ -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<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
className
)}
{...props}
/>
</PopoverPrimitive.Portal>
))
PopoverContent.displayName = PopoverPrimitive.Content.displayName
export { Popover, PopoverTrigger, PopoverContent }