nested tables

This commit is contained in:
Daniel Roth 2026-02-02 10:24:54 +00:00
parent 209729821e
commit 8184bbd0fb
2 changed files with 249 additions and 88 deletions

View file

@ -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>
);
}

View file

@ -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>
);
}