diff --git a/package-lock.json b/package-lock.json
index ef282c48..7bc83da5 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 30b73740..eae3d631 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 1a651107..3d2e97c5 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 ffebcd4c..e3667c2d 100644
--- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/DampMouldRiskPanel.tsx
+++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/DampMouldRiskPanel.tsx
@@ -106,6 +106,7 @@ export default function DampMouldRiskPanel({
const surveyColumns: (keyof ClassifiedDeal)[] = [
"dealname",
"landlordPropertyId",
+ "dampMouldFlag",
"majorConditionIssueDescription",
"majorConditionIssuePhotosS3",
];
@@ -113,6 +114,7 @@ export default function DampMouldRiskPanel({
const surveyLabels: Partial> = {
dealname: "Address",
landlordPropertyId: "Property Ref",
+ dampMouldFlag: "Damp & Mould",
majorConditionIssueDescription: "Surveyor Notes",
majorConditionIssuePhotosS3: "Photo Evidence",
};
@@ -128,7 +130,7 @@ export default function DampMouldRiskPanel({
const coordLabels: Partial> = {
dealname: "Address",
landlordPropertyId: "Property Ref",
- dampMouldFlag: "Coordinator Flag",
+ dampMouldFlag: "Damp & Mould",
dampMouldAndRepairComments: "Comments",
coordinator: "Coordinator",
};
@@ -147,7 +149,7 @@ export default function DampMouldRiskPanel({
- Awaab's Law — Damp & Mould Risk
+ Awaab's Law — Damp, Mould & Other Condition Issues
Comparison of flags raised at survey vs coordination stage
@@ -161,7 +163,7 @@ export default function DampMouldRiskPanel({
- No damp or mould flags recorded for this project.
+ No condition issues recorded for this project.
) : (
@@ -176,7 +178,7 @@ export default function DampMouldRiskPanel({
color="red"
onClick={() =>
onOpenTable(
- "Damp & Mould — Survey Stage Flags",
+ "Condition Issues — Survey Stage",
risk.surveyFlagDeals,
surveyColumns,
surveyLabels
@@ -192,7 +194,7 @@ export default function DampMouldRiskPanel({
color="red"
onClick={() =>
onOpenTable(
- "Damp & Mould — Coordination Stage Flags",
+ "Condition Issues — Coordination Stage",
risk.coordinatorFlagDeals,
coordColumns,
coordLabels
@@ -210,7 +212,7 @@ export default function DampMouldRiskPanel({
{risk.coordinatorFlagCount - risk.surveyFlagCount} additional{" "}
{risk.coordinatorFlagCount - risk.surveyFlagCount === 1 ? "property was" : "properties were"}{" "}
- flagged for damp & mould at the coordination stage that{" "}
+ flagged with condition issues at the coordination stage that{" "}
{risk.coordinatorFlagCount - risk.surveyFlagCount === 1 ? "was" : "were"} not
identified during the initial survey.
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 7adf28e6..c5b314d0 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,68 @@ 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 DampMouldBadgeCell({ value }: { value: unknown }) {
+ const isYes =
+ typeof value === "string" && value.trim().toLowerCase() === "yes";
+
+ if (!isYes) return null;
+
+ return (
+
+ Damp & Mould
+
+ );
+}
+
+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 +186,12 @@ export default function DrillDownTable({
if (key === "majorConditionIssuePhotosS3") {
return ;
}
+ if (key === "dampMouldAndRepairComments") {
+ return ;
+ }
+ if (key === "dampMouldFlag") {
+ 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 f4fa001c..0b2f2d78 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 bf34ff99..8f659192 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 f8df537b..9145af89 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 0223e76a..5d788391 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 00000000..3a991084
--- /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 }