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. 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..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", }; @@ -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 7adf28e..c5b314d 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 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 }