From 4232038a14018027feb76cbd5eefb83182ce47b3 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Thu, 28 May 2026 13:52:01 +0000 Subject: [PATCH 1/3] deploy? --- README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/README.md b/README.md index 6937a3e..9d69b5c 100644 --- a/README.md +++ b/README.md @@ -129,7 +129,3 @@ In our terraform stack, we have a module called `s3_presignable_bucket` which co We will generate a pre-signed url and then make a post request to that endpoint to store that data to s3. Part of that process is the creation of an AWS IAM role which contains the permission set to access the bucket, `rerofit-plan-inputs-`. The name of this IAM role is `s3_presign_role_` and for our NextJS application, as it's hosted outside of AWS (for the moment), we need to generate a set of access credentials to give the application access to this bucket. The access key and secret key are automatically generated and stored in AWS secrets manager under `dev/presign_frontend/access_key` and `dev/presign_frontend/secret_key` and need to be set in the environment for the pre-sign api to store csv data to aws. - -Quick wins: - -- [] Frequently asked questions page. From 62e9c548f15eb62eb7f38933a1115e1a8993bc41 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Thu, 28 May 2026 14:58:23 +0000 Subject: [PATCH 2/3] 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 } From 9e3df59ea04eb01e23400b4d47cb9c413139cbaa Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Thu, 28 May 2026 15:33:08 +0000 Subject: [PATCH 3/3] Broaden the Awaab's Law panel to cover other condition issues Coordinators record non-damp/mould observations (e.g. wasp nests) in the same comments field, but the section was framed entirely around damp and mould. Reframe the panel copy and table titles around "condition issues", keep "Damp, Mould" up front so the Awaab's Law urgency still leads, and mark the damp/mould rows specifically with a red badge column so they don't blend into the broader list. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../your-projects/live/DampMouldRiskPanel.tsx | 14 +++++++++----- .../your-projects/live/DrillDownTable.tsx | 16 ++++++++++++++++ 2 files changed, 25 insertions(+), 5 deletions(-) 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 816de4b..e3667c2 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", }; @@ -120,6 +122,7 @@ export default function DampMouldRiskPanel({ const coordColumns: (keyof ClassifiedDeal)[] = [ "dealname", "landlordPropertyId", + "dampMouldFlag", "dampMouldAndRepairComments", "coordinator", ]; @@ -127,6 +130,7 @@ export default function DampMouldRiskPanel({ const coordLabels: Partial> = { dealname: "Address", landlordPropertyId: "Property Ref", + dampMouldFlag: "Damp & Mould", dampMouldAndRepairComments: "Comments", coordinator: "Coordinator", }; @@ -145,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 @@ -159,7 +163,7 @@ export default function DampMouldRiskPanel({

- No damp or mould flags recorded for this project. + No condition issues recorded for this project.

) : ( @@ -174,7 +178,7 @@ export default function DampMouldRiskPanel({ color="red" onClick={() => onOpenTable( - "Damp & Mould — Survey Stage Flags", + "Condition Issues — Survey Stage", risk.surveyFlagDeals, surveyColumns, surveyLabels @@ -190,7 +194,7 @@ export default function DampMouldRiskPanel({ color="red" onClick={() => onOpenTable( - "Damp & Mould — Coordination Stage Flags", + "Condition Issues — Coordination Stage", risk.coordinatorFlagDeals, coordColumns, coordLabels @@ -208,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 25dc5f8..c5b314d 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/DrillDownTable.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/DrillDownTable.tsx @@ -34,6 +34,19 @@ 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() : ""; @@ -176,6 +189,9 @@ export default function DrillDownTable({ if (key === "dampMouldAndRepairComments") { return ; } + if (key === "dampMouldFlag") { + return ; + } return ( {value != null ? String(value) : (