mirror of
https://github.com/Hestia-Homes/assessment-model.git
synced 2026-06-08 11:37:25 +00:00
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:
parent
5c06e69102
commit
62e9c548f1
10 changed files with 181 additions and 8 deletions
56
package-lock.json
generated
56
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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) : (
|
||||
|
|
|
|||
|
|
@ -165,7 +165,7 @@ const mockDealRow = {
|
|||
pashubLink: null,
|
||||
sharepointLink: null,
|
||||
dampmouldGrowth: null,
|
||||
damnpMouldAndRepairComments: null,
|
||||
dampMouldAndRepairComments: null,
|
||||
preSap: null,
|
||||
mtpCompletionDate: null,
|
||||
mtpReModelCompletionDate: null,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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" }),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
31
src/app/shadcn_components/ui/popover.tsx
Normal file
31
src/app/shadcn_components/ui/popover.tsx
Normal 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 }
|
||||
Loading…
Add table
Reference in a new issue