mirror of
https://github.com/Hestia-Homes/assessment-model.git
synced 2026-06-08 11:37:25 +00:00
Merge remote-tracking branch 'origin/main' into feature/frontend_landlord_overrides
This commit is contained in:
commit
4915c95875
11 changed files with 205 additions and 16 deletions
|
|
@ -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-<stage>`. The name of this IAM role is `s3_presign_role_<stage>` 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.
|
||||
|
|
|
|||
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"),
|
||||
|
|
|
|||
|
|
@ -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<Record<keyof ClassifiedDeal, string>> = {
|
||||
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<Record<keyof ClassifiedDeal, string>> = {
|
||||
dealname: "Address",
|
||||
landlordPropertyId: "Property Ref",
|
||||
dampMouldFlag: "Coordinator Flag",
|
||||
dampMouldFlag: "Damp & Mould",
|
||||
dampMouldAndRepairComments: "Comments",
|
||||
coordinator: "Coordinator",
|
||||
};
|
||||
|
|
@ -147,7 +149,7 @@ export default function DampMouldRiskPanel({
|
|||
</div>
|
||||
<div>
|
||||
<h3 className="text-base font-semibold text-gray-800">
|
||||
Awaab's Law — Damp & Mould Risk
|
||||
Awaab's Law — Damp, Mould & Other Condition Issues
|
||||
</h3>
|
||||
<p className="text-sm text-gray-500 mt-0.5">
|
||||
Comparison of flags raised at survey vs coordination stage
|
||||
|
|
@ -161,7 +163,7 @@ export default function DampMouldRiskPanel({
|
|||
<ShieldAlert className="h-4 w-4 text-emerald-600" />
|
||||
</div>
|
||||
<p className="text-sm font-medium text-emerald-700">
|
||||
No damp or mould flags recorded for this project.
|
||||
No condition issues recorded for this project.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
|
|
@ -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"}{" "}
|
||||
</span>
|
||||
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.
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<span className="inline-flex items-center gap-1 rounded-full bg-red-100 px-2 py-0.5 text-xs font-semibold text-red-700 border border-red-200">
|
||||
Damp & Mould
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
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 +186,12 @@ export default function DrillDownTable({
|
|||
if (key === "majorConditionIssuePhotosS3") {
|
||||
return <PhotoDownloadCell value={value} />;
|
||||
}
|
||||
if (key === "dampMouldAndRepairComments") {
|
||||
return <DampMouldCommentCell value={value} />;
|
||||
}
|
||||
if (key === "dampMouldFlag") {
|
||||
return <DampMouldBadgeCell 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