mirror of
https://github.com/Hestia-Homes/assessment-model.git
synced 2026-06-30 12:55:02 +00:00
Added the summary dashboard
This commit is contained in:
parent
65ce0ecb2a
commit
66a43a19b8
2 changed files with 339 additions and 261 deletions
|
|
@ -9,16 +9,20 @@ import {
|
|||
DrawerHeader,
|
||||
DrawerTitle,
|
||||
} from "@/app/shadcn_components/ui/drawer";
|
||||
import { Badge } from "@/app/shadcn_components/ui/badge";
|
||||
|
||||
// Types
|
||||
interface PropertySummary {
|
||||
id: bigint;
|
||||
uprn: bigint;
|
||||
address: string;
|
||||
postcode: string;
|
||||
has_condition_data: boolean;
|
||||
passes_decent_homes: boolean;
|
||||
category1: Record<string, boolean>;
|
||||
category2: Record<string, boolean>;
|
||||
decent_homes: "pass" | "fail" | "no_data";
|
||||
criterion_a: "pass" | "fail" | "no_data";
|
||||
criterion_b: "pass" | "fail" | "no_data";
|
||||
criterion_c: "pass" | "fail" | "no_data";
|
||||
criterion_d: "pass" | "fail" | "no_data";
|
||||
replacements?: { sub_variable: string; expiry_date: string | null }[];
|
||||
}
|
||||
|
||||
interface Props {
|
||||
|
|
@ -26,49 +30,170 @@ interface Props {
|
|||
portfolioId: string;
|
||||
}
|
||||
|
||||
type ReasonKey =
|
||||
| "damp_passes"
|
||||
| "structure_passes"
|
||||
| "heating_passes"
|
||||
| "sap_passes"
|
||||
| "thermal_comfort_passes"
|
||||
| "defects_pass"
|
||||
| "windows_pass"
|
||||
| "kitchen_pass"
|
||||
| "bathroom_pass"
|
||||
| "space_pass";
|
||||
// --- Urgency Badge for notifications ---
|
||||
function UrgencyBadge({ label }: { label: string }) {
|
||||
const colorMap: Record<string, string> = {
|
||||
Overdue: "bg-red-600",
|
||||
"0–6 months": "bg-orange-500",
|
||||
"6–12 months": "bg-yellow-500",
|
||||
">12 months": "bg-green-600",
|
||||
};
|
||||
return (
|
||||
<Badge className={`${colorMap[label]} text-white text-xs px-2 py-1`}>
|
||||
{label}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
const reasonLabelMap: Record<ReasonKey, string> = {
|
||||
damp_passes: "Damp",
|
||||
structure_passes: "Structural Issues",
|
||||
heating_passes: "Heating",
|
||||
sap_passes: "SAP Rating",
|
||||
thermal_comfort_passes: "Thermal Comfort",
|
||||
defects_pass: "General Disrepair",
|
||||
windows_pass: "Windows",
|
||||
kitchen_pass: "Kitchen",
|
||||
bathroom_pass: "Bathroom",
|
||||
space_pass: "Space Standard",
|
||||
};
|
||||
function NotificationsPanel({
|
||||
data,
|
||||
portfolioId,
|
||||
}: {
|
||||
data: PropertySummary[];
|
||||
portfolioId: string;
|
||||
}) {
|
||||
const today = new Date();
|
||||
const groups: Record<
|
||||
string,
|
||||
{
|
||||
property: PropertySummary;
|
||||
sub_variable: string;
|
||||
expiry: Date;
|
||||
remaining: string;
|
||||
}[]
|
||||
> = {
|
||||
Overdue: [],
|
||||
"0–6 months": [],
|
||||
"6–12 months": [],
|
||||
">12 months": [],
|
||||
};
|
||||
|
||||
data.forEach((p) => {
|
||||
p.replacements?.forEach((r) => {
|
||||
if (!r.expiry_date) return;
|
||||
const expiry = new Date(r.expiry_date);
|
||||
const diffMs = expiry.getTime() - today.getTime();
|
||||
const diffMonths = diffMs / (1000 * 60 * 60 * 24 * 30);
|
||||
|
||||
const years = Math.floor(Math.abs(diffMonths) / 12);
|
||||
const months = Math.floor(Math.abs(diffMonths) % 12);
|
||||
|
||||
let remaining = "";
|
||||
if (diffMs < 0) {
|
||||
remaining = `Expired ${years > 0 ? `${years}y ` : ""}${months}m ago`;
|
||||
} else {
|
||||
remaining = `${years > 0 ? `${years}y ` : ""}${months}m remaining`;
|
||||
}
|
||||
|
||||
if (diffMs < 0) {
|
||||
groups.Overdue.push({
|
||||
property: p,
|
||||
sub_variable: r.sub_variable,
|
||||
expiry,
|
||||
remaining,
|
||||
});
|
||||
} else if (diffMonths <= 6) {
|
||||
groups["0–6 months"].push({
|
||||
property: p,
|
||||
sub_variable: r.sub_variable,
|
||||
expiry,
|
||||
remaining,
|
||||
});
|
||||
} else if (diffMonths <= 12) {
|
||||
groups["6–12 months"].push({
|
||||
property: p,
|
||||
sub_variable: r.sub_variable,
|
||||
expiry,
|
||||
remaining,
|
||||
});
|
||||
} else {
|
||||
groups[">12 months"].push({
|
||||
property: p,
|
||||
sub_variable: r.sub_variable,
|
||||
expiry,
|
||||
remaining,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const groupOrder: (keyof typeof groups)[] = [
|
||||
"Overdue",
|
||||
"0–6 months",
|
||||
"6–12 months",
|
||||
">12 months",
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="mt-10">
|
||||
<h2 className="text-lg font-semibold mb-4 text-brandbrown">
|
||||
Upcoming Replacements
|
||||
</h2>
|
||||
<Card className="max-h-[600px] overflow-y-auto">
|
||||
<CardContent className="p-4 space-y-2">
|
||||
{groupOrder.map((urgency) =>
|
||||
groups[urgency].length > 0 ? (
|
||||
<div key={urgency}>
|
||||
<div className="flex items-center space-x-2 mb-3">
|
||||
<UrgencyBadge label={urgency} />
|
||||
<span className="text-gray-700 font-medium">
|
||||
{groups[urgency].length} item
|
||||
{groups[urgency].length > 1 ? "s" : ""}
|
||||
</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-1">
|
||||
{groups[urgency].map((item, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="p-4 bg-gray-50 rounded-md border hover:bg-gray-100 transition cursor-pointer flex flex-col justify-between"
|
||||
onClick={() =>
|
||||
window.open(
|
||||
`/portfolio/${portfolioId}/building-passport/${item.property.id}/decent-homes`,
|
||||
"_blank"
|
||||
)
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<div className="font-medium text-gray-800">
|
||||
{item.sub_variable}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">
|
||||
{item.property.address}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 flex justify-between text-sm text-gray-600">
|
||||
<span>{item.remaining}</span>
|
||||
<span>
|
||||
Expiry Date: {item.expiry.toLocaleDateString("en-GB")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : null
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// --- Dashboard ---
|
||||
const DecentHomesDashboard: React.FC<Props> = ({ data, portfolioId }) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [selectedGroup, setSelectedGroup] = useState<PropertySummary[]>([]);
|
||||
const [selectedTitle, setSelectedTitle] = useState("");
|
||||
|
||||
const total = data.length;
|
||||
const passed = data.filter((p) => p.passes_decent_homes);
|
||||
const failedC1 = data.filter(
|
||||
(p) =>
|
||||
p.has_condition_data &&
|
||||
Object.values(p.category1).some((v) => v === false)
|
||||
);
|
||||
const failedC2 = data.filter(
|
||||
(p) =>
|
||||
p.has_condition_data &&
|
||||
Object.values(p.category2).some((v) => v === false)
|
||||
);
|
||||
const missing = data.filter((p) => !p.has_condition_data);
|
||||
const passes = data.filter((p) => p.decent_homes === "pass");
|
||||
const noData = data.filter((p) => p.decent_homes === "no_data");
|
||||
const critAFails = data.filter((p) => p.criterion_a === "fail");
|
||||
const critBFails = data.filter((p) => p.criterion_b === "fail");
|
||||
const critCFails = data.filter((p) => p.criterion_c === "fail");
|
||||
const critDFails = data.filter((p) => p.criterion_d === "fail");
|
||||
|
||||
const overallPassRate = ((passes.length / total) * 100).toFixed(1);
|
||||
|
||||
const openDrawer = (group: PropertySummary[], title: string) => {
|
||||
setSelectedGroup(group);
|
||||
|
|
@ -76,149 +201,158 @@ const DecentHomesDashboard: React.FC<Props> = ({ data, portfolioId }) => {
|
|||
setOpen(true);
|
||||
};
|
||||
|
||||
const failedReasons = new Map<string, number>();
|
||||
|
||||
data.forEach((p) => {
|
||||
if (!p.has_condition_data || p.passes_decent_homes) return;
|
||||
|
||||
const reasons = Object.entries({ ...p.category1, ...p.category2 })
|
||||
.filter(([_, v]) => v === false)
|
||||
.map(([key]) => key);
|
||||
|
||||
reasons.forEach((r) => {
|
||||
failedReasons.set(r, (failedReasons.get(r) ?? 0) + 1);
|
||||
});
|
||||
});
|
||||
|
||||
const sortedReasons = Array.from(failedReasons.entries()).sort(
|
||||
(a, b) => b[1] - a[1]
|
||||
);
|
||||
const top3Reasons = sortedReasons.slice(0, 3);
|
||||
const dataCompleteness = (
|
||||
(data.filter((p) => p.has_condition_data).length / total) *
|
||||
100
|
||||
).toFixed(1);
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-6">
|
||||
<h1 className="text-2xl font-semibold mb-6">Decent Homes Summary</h1>
|
||||
<div className="sm:col-span-2 rounded-xl bg-brandblue p-[3px] mb-6">
|
||||
<div className="rounded-lg bg-brandbrown p-[3px]">
|
||||
<div className="rounded-md bg-brandblue">
|
||||
<CardContent className="relative p-8 flex flex-col items-center justify-center text-center space-y-3">
|
||||
<div className="text-sm uppercase tracking-wider font-semibold text-brandbrown">
|
||||
Overall Pass Rate
|
||||
</div>
|
||||
<div className="text-6xl font-extrabold text-white drop-shadow">
|
||||
{((passed.length / total) * 100).toFixed(1)}%
|
||||
</div>
|
||||
<div className="text-md font-medium text-white/80">
|
||||
<span className="text-brandbrown font-semibold">
|
||||
{passed.length}
|
||||
</span>{" "}
|
||||
out of{" "}
|
||||
<span className="text-brandbrown font-semibold">{total}</span>{" "}
|
||||
homes meet the Decent Homes Standard
|
||||
</div>
|
||||
<h1 className="text-2xl font-semibold mb-6">
|
||||
Decent Homes Portfolio Summary
|
||||
</h1>
|
||||
|
||||
<p className="absolute bottom-4 right-4 text-white/50 italic">
|
||||
Data completeness: {dataCompleteness}%
|
||||
</p>
|
||||
</CardContent>
|
||||
{/* 8 compact cards in a 2x4 grid */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
{/* Overall Pass Rate */}
|
||||
<div className="rounded-xl bg-brandblue p-[3px]">
|
||||
<div className="rounded-lg bg-brandbrown p-[3px]">
|
||||
<div className="rounded-md bg-brandblue h-full">
|
||||
<CardContent className="p-4 text-center flex flex-col items-center justify-center space-y-2 h-full">
|
||||
<div className="text-sm uppercase tracking-wider font-semibold text-brandbrown">
|
||||
Overall Pass Rate
|
||||
</div>
|
||||
<div className="text-3xl font-extrabold text-white drop-shadow">
|
||||
{overallPassRate}%
|
||||
</div>
|
||||
<div className="text-sm font-medium text-white/80">
|
||||
<span className="text-brandbrown font-semibold">
|
||||
{passes.length}
|
||||
</span>{" "}
|
||||
out of{" "}
|
||||
<span className="text-brandbrown font-semibold">{total}</span>{" "}
|
||||
properties
|
||||
</div>
|
||||
</CardContent>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
{/* Criterion A */}
|
||||
<Card
|
||||
onClick={() =>
|
||||
openDrawer(failedC1, "Properties with Category 1 Risks")
|
||||
}
|
||||
className="hover:shadow-lg cursor-pointer border border-brandblue/30 bg-brandblue/5 transition duration-150 ease-in-out hover:bg-brandblue/10"
|
||||
onClick={() => openDrawer(critAFails, "Criterion A Fails")}
|
||||
className="cursor-pointer transition hover:bg-hoverblue hover:text-white"
|
||||
>
|
||||
<CardContent className="p-6 relative bg-brandblue hover:bg-hoverblue">
|
||||
<div className="text-brandbrown font-semibold mb-2">
|
||||
Category 1 Risks
|
||||
<CardContent className="p-4 text-center">
|
||||
<div className="text-brandbrown font-semibold">
|
||||
Criterion A Fails
|
||||
</div>
|
||||
<div className="text-5xl font-bold text-white text-center">
|
||||
{failedC1.length}
|
||||
<div className="text-3xl font-bold text-brandbrown">
|
||||
{critAFails.length}
|
||||
</div>
|
||||
<div className="text-sm text-brandbrown/80">
|
||||
properties affected
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Criterion B */}
|
||||
<Card
|
||||
onClick={() =>
|
||||
openDrawer(failedC2, "Properties with Category 2 Risks")
|
||||
}
|
||||
className="hover:shadow-lg cursor-pointer border border-brandblue/30 bg-brandblue/5 transition duration-150 ease-in-out hover:bg-brandblue/10"
|
||||
onClick={() => openDrawer(critBFails, "Criterion B Fails")}
|
||||
className="cursor-pointer transition hover:bg-hoverblue hover:text-white"
|
||||
>
|
||||
<CardContent className="p-6 relative bg-brandblue hover:bg-hoverblue">
|
||||
<div className="text-brandbrown font-semibold mb-2">
|
||||
Category 2 Risks
|
||||
<CardContent className="p-4 text-center">
|
||||
<div className="text-brandbrown font-semibold">
|
||||
Criterion B Fails
|
||||
</div>
|
||||
<div className="text-5xl font-bold text-white text-center">
|
||||
{failedC2.length}
|
||||
<div className="text-3xl font-bold text-brandbrown">
|
||||
{critBFails.length}
|
||||
</div>
|
||||
<div className="text-sm text-brandbrown/80">
|
||||
properties affected
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Criterion C */}
|
||||
<Card
|
||||
onClick={() =>
|
||||
openDrawer(missing, "Properties Missing Condition Data")
|
||||
}
|
||||
className="hover:shadow-lg cursor-pointer border border-brandblue/30 bg-brandblue/5 transition duration-150 ease-in-out hover:bg-brandblue/10"
|
||||
onClick={() => openDrawer(critCFails, "Criterion C Fails")}
|
||||
className="cursor-pointer transition hover:bg-hoverblue hover:text-white"
|
||||
>
|
||||
<CardContent className="p-6 relative bg-brandblue hover:bg-hoverblue">
|
||||
<div className="text-brandbrown font-semibold mb-2">
|
||||
No Condition Data
|
||||
<CardContent className="p-4 text-center">
|
||||
<div className="text-brandbrown font-semibold">
|
||||
Criterion C Fails
|
||||
</div>
|
||||
<div className="text-5xl font-bold text-white text-center">
|
||||
{missing.length}
|
||||
<div className="text-3xl font-bold text-brandbrown">
|
||||
{critCFails.length}
|
||||
</div>
|
||||
<div className="text-sm text-brandbrown/80">
|
||||
properties affected
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Criterion D */}
|
||||
<Card
|
||||
onClick={() => openDrawer(passed, "Properties Passing Decent Homes")}
|
||||
className="hover:shadow-lg cursor-pointer border border-brandblue/30 bg-brandblue/5 transition duration-150 ease-in-out"
|
||||
onClick={() => openDrawer(critDFails, "Criterion D Fails")}
|
||||
className="cursor-pointer transition hover:bg-hoverblue hover:text-white"
|
||||
>
|
||||
<CardContent className="p-6 relative bg-brandblue hover:bg-hoverblue">
|
||||
<div className="text-brandbrown font-semibold mb-2">
|
||||
Passes Decent Homes
|
||||
<CardContent className="p-4 text-center">
|
||||
<div className="text-brandbrown font-semibold">
|
||||
Criterion D Fails
|
||||
</div>
|
||||
<div className="text-5xl font-bold text-white text-center">
|
||||
{passed.length}
|
||||
<div className="text-3xl font-bold text-brandbrown">
|
||||
{critDFails.length}
|
||||
</div>
|
||||
<div className="text-sm text-brandbrown/80">
|
||||
properties affected
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Survey information missing */}
|
||||
<Card
|
||||
onClick={() => openDrawer(noData, "Survey information missing")}
|
||||
className="cursor-pointer transition hover:bg-hoverblue hover:text-white"
|
||||
>
|
||||
<CardContent className="p-4 text-center">
|
||||
<div className="text-brandbrown font-semibold">
|
||||
Survey information missing
|
||||
</div>
|
||||
<div className="text-3xl font-bold text-brandbrown">
|
||||
{noData.length}
|
||||
</div>
|
||||
<div className="text-sm text-brandbrown/80">
|
||||
properties affected
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Passes */}
|
||||
<Card
|
||||
onClick={() => openDrawer(passes, "Passes Decent Homes")}
|
||||
className="cursor-pointer transition hover:bg-hoverblue hover:text-white"
|
||||
>
|
||||
<CardContent className="p-4 text-center">
|
||||
<div className="text-brandbrown font-semibold">Passes</div>
|
||||
<div className="text-3xl font-bold text-brandbrown">
|
||||
{passes.length}
|
||||
</div>
|
||||
<div className="text-sm text-brandbrown/80">properties</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Total Properties */}
|
||||
<Card className="transition">
|
||||
<CardContent className="p-4 text-center">
|
||||
<div className="text-brandbrown font-semibold">
|
||||
Total Properties
|
||||
</div>
|
||||
<div className="text-3xl font-bold text-brandbrown">{total}</div>
|
||||
<div className="text-sm text-brandbrown/80">portfolio size</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="mt-8">
|
||||
<h2 className="text-lg font-semibold mb-4 text-brandbrown">
|
||||
Top Reasons for Failing
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
|
||||
{top3Reasons.map(([reason, count]) => (
|
||||
<div
|
||||
key={reason}
|
||||
className="bg-brandblue/10 border border-brandblue/20 rounded-lg p-6 shadow-sm flex flex-col justify-between"
|
||||
>
|
||||
<div>
|
||||
<div className="text-red-500 text-2xl mb-2">🚫</div>
|
||||
<h3 className="text-md font-semibold text-brandbrown mb-1">
|
||||
{reasonLabelMap[reason as ReasonKey] ??
|
||||
reason.replace(/_/g, " ")}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Failing in <span className="font-medium">{count}</span>{" "}
|
||||
properties
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{/* Notifications */}
|
||||
<NotificationsPanel data={data} portfolioId={portfolioId} />
|
||||
|
||||
{/* Drawer */}
|
||||
<Drawer open={open} onOpenChange={setOpen}>
|
||||
<DrawerContent className="p-4 w-full max-w-md ml-auto border-l h-full overflow-y-auto">
|
||||
<DrawerHeader>
|
||||
|
|
@ -233,30 +367,13 @@ const DecentHomesDashboard: React.FC<Props> = ({ data, portfolioId }) => {
|
|||
<div className="text-sm text-muted-foreground mb-2">
|
||||
UPRN: {String(p.uprn)}
|
||||
</div>
|
||||
{p.has_condition_data ? (
|
||||
<>
|
||||
{Object.entries({ ...p.category1, ...p.category2 })
|
||||
.filter(([_, value]) => value === false)
|
||||
.map(([key]) => (
|
||||
<div key={key} className="text-red-500 text-sm">
|
||||
Issue:{" "}
|
||||
{reasonLabelMap[key as ReasonKey] ??
|
||||
key.replace(/_/g, " ")}
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<div className="text-orange-500 text-sm">
|
||||
No condition data available
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="mt-3 bg-brandblue text-white hover:bg-hoverblue hover:text-gray-100"
|
||||
onClick={() =>
|
||||
window.open(
|
||||
`/portfolio/${portfolioId}/building-passport/${p.id}/assessment`,
|
||||
`/portfolio/${portfolioId}/building-passport/${p.id}/decent-homes`,
|
||||
"_blank"
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,21 +43,32 @@ async function getPropertiesWithUprn(
|
|||
|
||||
async function getDocumentsForUprns(
|
||||
uprns: (string | number)[]
|
||||
): Promise<Map<string, typeof uploadedFiles.$inferSelect>> {
|
||||
): Promise<Map<string, Map<string, typeof uploadedFiles.$inferSelect>>> {
|
||||
const rows = await surveyDB.query.uploadedFiles.findMany({
|
||||
where: and(
|
||||
inArray(uploadedFiles.uprn, uprns.map(String)),
|
||||
eq(uploadedFiles.docType, "ECO_CONDITION_REPORT")
|
||||
inArray(uploadedFiles.docType, [
|
||||
"DECENT_HOMES_SUMMARY",
|
||||
"DECENT_HOMES_PROPERTY_META",
|
||||
])
|
||||
),
|
||||
orderBy: (uploadedFiles, { desc }) => [
|
||||
desc(uploadedFiles.s3JsonUploadTimestamp),
|
||||
],
|
||||
});
|
||||
|
||||
const latestByUprn = new Map<string, (typeof rows)[number]>();
|
||||
// Map<UPRN, Map<docType, row>>
|
||||
const latestByUprn = new Map<string, Map<string, (typeof rows)[number]>>();
|
||||
|
||||
for (const row of rows) {
|
||||
if (!latestByUprn.has(row.uprn)) {
|
||||
latestByUprn.set(row.uprn, row);
|
||||
latestByUprn.set(row.uprn, new Map());
|
||||
}
|
||||
const docMap = latestByUprn.get(row.uprn)!;
|
||||
|
||||
// if this docType isn’t already set, use it (rows are ordered newest first)
|
||||
if (!docMap.has(row.docType)) {
|
||||
docMap.set(row.docType, row);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -80,107 +91,57 @@ export default async function DecentHomesPage({
|
|||
|
||||
const summaryResults = await Promise.all(
|
||||
properties.map(async (property) => {
|
||||
const result: any = {
|
||||
const propertyDocs = documentsMap.get(String(property.uprn));
|
||||
const propertyDecentHomes = propertyDocs?.get("DECENT_HOMES_SUMMARY");
|
||||
const propertyDecentHomesMeta = propertyDocs?.get(
|
||||
"DECENT_HOMES_PROPERTY_META"
|
||||
);
|
||||
|
||||
if (
|
||||
!propertyDecentHomes?.s3JsonUri ||
|
||||
!propertyDecentHomesMeta?.s3JsonUri
|
||||
) {
|
||||
return {
|
||||
id: property.id,
|
||||
uprn: property.uprn,
|
||||
address: property.address,
|
||||
postcode: property.postcode,
|
||||
decent_homes: "no_data",
|
||||
criterion_a: "no_data",
|
||||
criterion_b: "no_data",
|
||||
criterion_c: "no_data",
|
||||
criterion_d: "no_data",
|
||||
replacements: [],
|
||||
};
|
||||
}
|
||||
|
||||
const decentHomes = await getEnergyAssessmentFromS3(
|
||||
propertyDecentHomes.s3JsonUri
|
||||
);
|
||||
const decentHomesMeta = await getEnergyAssessmentFromS3(
|
||||
propertyDecentHomesMeta.s3JsonUri
|
||||
);
|
||||
|
||||
return {
|
||||
id: property.id,
|
||||
uprn: property.uprn,
|
||||
address: property.address,
|
||||
postcode: property.postcode,
|
||||
has_condition_data: false,
|
||||
passes_decent_homes: false,
|
||||
category1: {},
|
||||
category2: {},
|
||||
decent_homes: decentHomes.decent_homes,
|
||||
criterion_a: decentHomes.criterion_a,
|
||||
criterion_b: decentHomes.criterion_b,
|
||||
criterion_c: decentHomes.criterion_c,
|
||||
criterion_d: decentHomes.criterion_d,
|
||||
replacements: decentHomesMeta.map((m: any) => ({
|
||||
sub_variable: m.sub_variable,
|
||||
expiry_date: m.expiry_date ?? null,
|
||||
})),
|
||||
};
|
||||
|
||||
const propertyMeta = await getPropertyMeta(property.id);
|
||||
|
||||
const conditionReportMeta = documentsMap.get(String(propertyMeta.uprn));
|
||||
|
||||
if (!conditionReportMeta || !conditionReportMeta.s3JsonUri) {
|
||||
return result;
|
||||
}
|
||||
|
||||
result.has_condition_data = true;
|
||||
|
||||
const conditionReport = await getEnergyAssessmentFromS3(
|
||||
conditionReportMeta.s3JsonUri
|
||||
);
|
||||
const conditionData = await getConditionReport(String(property.id));
|
||||
|
||||
if (!conditionReport || !conditionData) return result;
|
||||
|
||||
const allRoomData = getAllRoomData(conditionReport.rooms);
|
||||
const roomsWithDamp = getRoomsWithDamp(allRoomData);
|
||||
const roomsWithDefects = getRoomsWithDefects(allRoomData);
|
||||
const roomsWithBadWindows = getRoomsWithBadWindows(allRoomData);
|
||||
const windowsOk = areAllWindowsOk(allRoomData);
|
||||
const elevationsWithIssues = getElevationsWithIssues(conditionReport);
|
||||
|
||||
const {
|
||||
isSufficient: enoughSpace,
|
||||
occupantsToUse,
|
||||
numberOfBedrooms,
|
||||
areaPerPerson,
|
||||
} = hasSufficientSpace(
|
||||
conditionReport.occupant_info,
|
||||
conditionData.totalFloorArea ?? 0,
|
||||
allRoomData
|
||||
);
|
||||
|
||||
const sapOk = meetsSapThreshold(propertyMeta.currentSapPoints ?? 0);
|
||||
const efficientHeating = hasEfficientHeatingSystem(
|
||||
conditionData.heatingRating
|
||||
);
|
||||
const insulationOk = isInsulationAdequate(
|
||||
conditionData.roofRating,
|
||||
conditionData.wallsRating
|
||||
);
|
||||
const thermalComfortOk = efficientHeating && insulationOk;
|
||||
|
||||
const kitchenOk =
|
||||
conditionReport.rooms.kitchen?.room_info
|
||||
?.overall_condition_of_the_room === "Good";
|
||||
const bathroomsOk =
|
||||
Array.isArray(conditionReport.rooms.bathrooms) &&
|
||||
conditionReport.rooms.bathrooms.length > 0 &&
|
||||
conditionReport.rooms.bathrooms.every(
|
||||
(b: any) => b?.room_info?.overall_condition_of_the_room === "Good"
|
||||
);
|
||||
|
||||
const hasDefects = roomsWithDefects.length > 0;
|
||||
|
||||
result.category1 = {
|
||||
damp_passes: roomsWithDamp.length === 0,
|
||||
structure_passes: elevationsWithIssues.length === 0,
|
||||
heating_passes: efficientHeating,
|
||||
sap_passes: sapOk,
|
||||
thermal_comfort_passes: thermalComfortOk,
|
||||
};
|
||||
|
||||
result.category2 = {
|
||||
defects_pass: !hasDefects,
|
||||
windows_pass: windowsOk,
|
||||
kitchen_pass: kitchenOk,
|
||||
bathroom_pass: bathroomsOk,
|
||||
space_pass: enoughSpace,
|
||||
};
|
||||
|
||||
// Overall result
|
||||
result.passes_decent_homes =
|
||||
result.category1.damp_passes &&
|
||||
result.category1.structure_passes &&
|
||||
result.category1.heating_passes &&
|
||||
result.category1.sap_passes &&
|
||||
result.category1.thermal_comfort_passes &&
|
||||
result.category2.defects_pass &&
|
||||
result.category2.windows_pass &&
|
||||
result.category2.kitchen_pass &&
|
||||
result.category2.bathroom_pass &&
|
||||
result.category2.space_pass;
|
||||
|
||||
return result;
|
||||
})
|
||||
);
|
||||
|
||||
console.log("summaryResults", summaryResults);
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4">
|
||||
<DecentHomesDashboard data={summaryResults} portfolioId={portfolioId} />
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue