mirror of
https://github.com/Hestia-Homes/assessment-model.git
synced 2026-06-30 12:55:02 +00:00
nested tables
This commit is contained in:
parent
209729821e
commit
8184bbd0fb
2 changed files with 249 additions and 88 deletions
|
|
@ -0,0 +1,208 @@
|
|||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
|
||||
export type AspectCondition = {
|
||||
id: string;
|
||||
elementId: string;
|
||||
aspectType: string;
|
||||
aspectInstance: number;
|
||||
value: string | null;
|
||||
quantity: number | null;
|
||||
installDate: string | null;
|
||||
renewalYear: number | null;
|
||||
comments: string | null;
|
||||
};
|
||||
|
||||
export type Element = {
|
||||
id: string;
|
||||
surveyId: string;
|
||||
elementType: string;
|
||||
elementInstance: number;
|
||||
aspects: AspectCondition[];
|
||||
};
|
||||
|
||||
export type ElementGroup = {
|
||||
elementType: string;
|
||||
elementInstance: number;
|
||||
elements: Element[];
|
||||
};
|
||||
|
||||
const thStyle: React.CSSProperties = {
|
||||
borderBottom: "1px solid #ccc",
|
||||
textAlign: "left",
|
||||
padding: "4px",
|
||||
};
|
||||
|
||||
const tdStyle: React.CSSProperties = {
|
||||
borderBottom: "1px solid #eee",
|
||||
padding: "4px",
|
||||
};
|
||||
|
||||
type Props = {
|
||||
groupedElements: ElementGroup[];
|
||||
};
|
||||
|
||||
export default function ElementTable({ groupedElements }: Props) {
|
||||
const [selectedTypes, setSelectedTypes] = useState<Set<string>>(new Set());
|
||||
const [expandedParents, setExpandedParents] = useState<Set<string>>(new Set());
|
||||
const [showDropdown, setShowDropdown] = useState(false);
|
||||
|
||||
const uniqueElementTypes = Array.from(
|
||||
new Set(groupedElements.map(g => g.elementType))
|
||||
).sort((a, b) => a.localeCompare(b));
|
||||
|
||||
const toggleTypeFilter = (type: string) => {
|
||||
const newSet = new Set(selectedTypes);
|
||||
if (newSet.has(type)) newSet.delete(type);
|
||||
else newSet.add(type);
|
||||
setSelectedTypes(newSet);
|
||||
};
|
||||
|
||||
const selectAll = () => setSelectedTypes(new Set(uniqueElementTypes));
|
||||
|
||||
const deselectAll = () => setSelectedTypes(new Set());
|
||||
|
||||
const filteredGroups =
|
||||
selectedTypes.size === 0
|
||||
? groupedElements
|
||||
: groupedElements.filter(g => selectedTypes.has(g.elementType));
|
||||
|
||||
const sortedGroups = [...filteredGroups].sort((a, b) =>
|
||||
a.elementType.localeCompare(b.elementType)
|
||||
);
|
||||
|
||||
const toggleParent = (key: string) => {
|
||||
const newSet = new Set(expandedParents);
|
||||
if (newSet.has(key)) newSet.delete(key);
|
||||
else newSet.add(key);
|
||||
setExpandedParents(newSet);
|
||||
};
|
||||
|
||||
return (
|
||||
<table style={{ width: "100%", borderCollapse: "collapse" }}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={thStyle}>
|
||||
Element Type
|
||||
<div style={{ position: "relative", display: "inline-block", marginLeft: "0.5rem" }}>
|
||||
<button
|
||||
onClick={() => setShowDropdown(!showDropdown)}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
▼
|
||||
</button>
|
||||
{showDropdown && (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "100%",
|
||||
left: 0,
|
||||
backgroundColor: "#fff",
|
||||
border: "1px solid #ccc",
|
||||
padding: "0.5rem",
|
||||
zIndex: 10,
|
||||
maxHeight: "250px",
|
||||
overflowY: "auto",
|
||||
minWidth: "300px",
|
||||
}}
|
||||
>
|
||||
<div style={{ marginBottom: "0.5rem" }}>
|
||||
<button onClick={selectAll} style={{ marginRight: "0.5rem" }}>
|
||||
Select All
|
||||
</button>
|
||||
<button onClick={deselectAll}>Deselect All</button>
|
||||
</div>
|
||||
{uniqueElementTypes.map(type => (
|
||||
<label key={type} style={{ display: "block", cursor: "pointer" }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedTypes.has(type)}
|
||||
onChange={() => toggleTypeFilter(type)}
|
||||
/>{" "}
|
||||
{type.replaceAll("_", " ")}
|
||||
</label>
|
||||
))}
|
||||
<button
|
||||
style={{ marginTop: "0.5rem" }}
|
||||
onClick={() => setShowDropdown(false)}
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</th>
|
||||
<th style={thStyle}>Instance</th>
|
||||
<th style={thStyle}># Aspects</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{sortedGroups.map(group => {
|
||||
const parentKey = `${group.elementType}__${group.elementInstance}`;
|
||||
const isExpanded = expandedParents.has(parentKey);
|
||||
const totalAspects = group.elements.reduce((acc, el) => acc + el.aspects.length, 0);
|
||||
|
||||
return (
|
||||
<React.Fragment key={parentKey}>
|
||||
<tr
|
||||
onClick={() => toggleParent(parentKey)}
|
||||
style={{
|
||||
backgroundColor: "#f7f7f7",
|
||||
cursor: "pointer",
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
>
|
||||
<td style={tdStyle}>{group.elementType.replaceAll("_", " ")}</td>
|
||||
<td style={tdStyle}>{group.elementInstance}</td>
|
||||
<td style={tdStyle}>{totalAspects}</td>
|
||||
</tr>
|
||||
|
||||
{isExpanded && (
|
||||
<tr>
|
||||
<td colSpan={3} style={{ padding: 0 }}>
|
||||
<table
|
||||
style={{
|
||||
width: "100%",
|
||||
borderCollapse: "collapse",
|
||||
margin: "0",
|
||||
}}
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={thStyle}>Aspect Type</th>
|
||||
<th style={thStyle}>Value</th>
|
||||
<th style={thStyle}>Quantity</th>
|
||||
<th style={thStyle}>Install Date</th>
|
||||
<th style={thStyle}>Renewal Year</th>
|
||||
<th style={thStyle}>Comments</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{group.elements
|
||||
.sort((a, b) => a.elementType.localeCompare(b.elementType))
|
||||
.flatMap(el =>
|
||||
el.aspects.map(aspect => (
|
||||
<tr key={aspect.id}>
|
||||
<td style={tdStyle}>{aspect.aspectType}</td>
|
||||
<td style={tdStyle}>{aspect.value ?? "-"}</td>
|
||||
<td style={tdStyle}>{aspect.quantity ?? "-"}</td>
|
||||
<td style={tdStyle}>{aspect.installDate ?? "-"}</td>
|
||||
<td style={tdStyle}>{aspect.renewalYear ?? "-"}</td>
|
||||
<td style={tdStyle}>{aspect.comments ?? "-"}</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import { getPropertyMeta } from "../utils";
|
||||
import { db } from "@/app/db/db";
|
||||
import ElementTable, { ElementGroup, Element } from "./components/ElementTable";
|
||||
|
||||
type ConditionPageProps = {
|
||||
params: {
|
||||
|
|
@ -8,16 +9,21 @@ type ConditionPageProps = {
|
|||
};
|
||||
};
|
||||
|
||||
// --- Helper to serialize BigInts ---
|
||||
function serializeBigInt(obj: any): any {
|
||||
type Survey = {
|
||||
id: string;
|
||||
uprn: string;
|
||||
date: string;
|
||||
source: string;
|
||||
elements: Element[];
|
||||
};
|
||||
|
||||
function serializeBigInt(obj: unknown): Survey[] {
|
||||
return JSON.parse(
|
||||
JSON.stringify(obj, (_key, value) =>
|
||||
typeof value === "bigint" ? value.toString() : value
|
||||
)
|
||||
JSON.stringify(obj, (_key, value) => (typeof value === "bigint" ? value.toString() : value))
|
||||
);
|
||||
}
|
||||
|
||||
async function getConditionsByUprn(uprn: number) {
|
||||
async function getConditionsByUprn(uprn: number): Promise<Survey[]> {
|
||||
const surveys = await db.query.propertyConditionSurvey.findMany({
|
||||
where: (survey, { eq }) => eq(survey.uprn, uprn),
|
||||
with: {
|
||||
|
|
@ -29,99 +35,46 @@ async function getConditionsByUprn(uprn: number) {
|
|||
},
|
||||
});
|
||||
|
||||
return surveys;
|
||||
return serializeBigInt(surveys);
|
||||
}
|
||||
|
||||
export default async function Condition({ params }: ConditionPageProps) {
|
||||
const { slug, propertyId } = params;
|
||||
|
||||
const { propertyId } = params;
|
||||
const propertyMeta = await getPropertyMeta(propertyId);
|
||||
const uprn = propertyMeta.uprn;
|
||||
const uprn = Number(propertyMeta.uprn);
|
||||
|
||||
const conditions = await getConditionsByUprn(Number(uprn));
|
||||
const safeConditions = serializeBigInt(conditions);
|
||||
const surveys = await getConditionsByUprn(uprn);
|
||||
if (!surveys || surveys.length === 0) {
|
||||
return <div>No survey data found for this property.</div>;
|
||||
}
|
||||
|
||||
// Flatten elements + aspects for table rendering
|
||||
const tableRows: {
|
||||
surveyId: string;
|
||||
surveyDate: string;
|
||||
surveySource: string;
|
||||
elementType: string;
|
||||
elementInstance: number;
|
||||
aspectType: string;
|
||||
aspectValue: string | null;
|
||||
aspectQuantity: number | null;
|
||||
aspectInstallDate: string | null;
|
||||
aspectRenewalYear: number | null;
|
||||
aspectComments: string | null;
|
||||
}[] = [];
|
||||
const survey = surveys[0]; // assuming one survey per property
|
||||
|
||||
safeConditions.forEach((survey: any) => {
|
||||
survey.elements.forEach((el: any) => {
|
||||
el.aspects.forEach((asp: any) => {
|
||||
tableRows.push({
|
||||
surveyId: survey.id,
|
||||
surveyDate: survey.date,
|
||||
surveySource: survey.source,
|
||||
elementType: el.elementType,
|
||||
elementInstance: el.elementInstance,
|
||||
aspectType: asp.aspectType,
|
||||
aspectValue: asp.value,
|
||||
aspectQuantity: asp.quantity,
|
||||
aspectInstallDate: asp.installDate,
|
||||
aspectRenewalYear: asp.renewalYear,
|
||||
aspectComments: asp.comments,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
const groupedElements: ElementGroup[] = Object.values(
|
||||
survey.elements.reduce((acc: Record<string, ElementGroup>, el: Element) => {
|
||||
const key = `${el.elementType}__${el.elementInstance}`;
|
||||
if (!acc[key]) acc[key] = {
|
||||
elementType: el.elementType,
|
||||
elementInstance: el.elementInstance,
|
||||
elements: [],
|
||||
};
|
||||
acc[key].elements.push(el);
|
||||
return acc;
|
||||
}, {})
|
||||
);
|
||||
|
||||
return (
|
||||
<div style={{ padding: "1rem" }}>
|
||||
<h1>
|
||||
Property Condition for {propertyMeta.address || uprn} (UPRN: {uprn})
|
||||
</h1>
|
||||
<h1>Property Condition</h1>
|
||||
<p>
|
||||
<strong>UPRN:</strong> {uprn} <br />
|
||||
<strong>Survey Date:</strong> {survey.date} <br />
|
||||
<strong>Source:</strong> {survey.source}
|
||||
</p>
|
||||
|
||||
<table
|
||||
style={{
|
||||
width: "100%",
|
||||
borderCollapse: "collapse",
|
||||
marginTop: "1rem",
|
||||
}}
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ border: "1px solid #ccc", padding: "4px" }}>Survey ID</th>
|
||||
<th style={{ border: "1px solid #ccc", padding: "4px" }}>Date</th>
|
||||
<th style={{ border: "1px solid #ccc", padding: "4px" }}>Source</th>
|
||||
<th style={{ border: "1px solid #ccc", padding: "4px" }}>Element Type</th>
|
||||
<th style={{ border: "1px solid #ccc", padding: "4px" }}>Element Instance</th>
|
||||
<th style={{ border: "1px solid #ccc", padding: "4px" }}>Aspect Type</th>
|
||||
<th style={{ border: "1px solid #ccc", padding: "4px" }}>Value</th>
|
||||
<th style={{ border: "1px solid #ccc", padding: "4px" }}>Quantity</th>
|
||||
<th style={{ border: "1px solid #ccc", padding: "4px" }}>Install Date</th>
|
||||
<th style={{ border: "1px solid #ccc", padding: "4px" }}>Renewal Year</th>
|
||||
<th style={{ border: "1px solid #ccc", padding: "4px" }}>Comments</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{tableRows.map((row, i) => (
|
||||
<tr key={i}>
|
||||
<td style={{ border: "1px solid #ccc", padding: "4px" }}>{row.surveyId}</td>
|
||||
<td style={{ border: "1px solid #ccc", padding: "4px" }}>{row.surveyDate}</td>
|
||||
<td style={{ border: "1px solid #ccc", padding: "4px" }}>{row.surveySource}</td>
|
||||
<td style={{ border: "1px solid #ccc", padding: "4px" }}>{row.elementType}</td>
|
||||
<td style={{ border: "1px solid #ccc", padding: "4px" }}>{row.elementInstance}</td>
|
||||
<td style={{ border: "1px solid #ccc", padding: "4px" }}>{row.aspectType}</td>
|
||||
<td style={{ border: "1px solid #ccc", padding: "4px" }}>{row.aspectValue}</td>
|
||||
<td style={{ border: "1px solid #ccc", padding: "4px" }}>{row.aspectQuantity}</td>
|
||||
<td style={{ border: "1px solid #ccc", padding: "4px" }}>{row.aspectInstallDate || "-"}</td>
|
||||
<td style={{ border: "1px solid #ccc", padding: "4px" }}>{row.aspectRenewalYear || "-"}</td>
|
||||
<td style={{ border: "1px solid #ccc", padding: "4px" }}>{row.aspectComments || "-"}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
{/* Client-side interactive table */}
|
||||
<ElementTable groupedElements={groupedElements} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue