From 8184bbd0fbc8ee4b952dd99660a4cee686a07055 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Mon, 2 Feb 2026 10:24:54 +0000 Subject: [PATCH] nested tables --- .../condition/components/ElementTable.tsx | 208 ++++++++++++++++++ .../[propertyId]/condition/page.tsx | 129 ++++------- 2 files changed, 249 insertions(+), 88 deletions(-) create mode 100644 src/app/portfolio/[slug]/building-passport/[propertyId]/condition/components/ElementTable.tsx diff --git a/src/app/portfolio/[slug]/building-passport/[propertyId]/condition/components/ElementTable.tsx b/src/app/portfolio/[slug]/building-passport/[propertyId]/condition/components/ElementTable.tsx new file mode 100644 index 00000000..fd5c3cf5 --- /dev/null +++ b/src/app/portfolio/[slug]/building-passport/[propertyId]/condition/components/ElementTable.tsx @@ -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>(new Set()); + const [expandedParents, setExpandedParents] = useState>(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 ( + + + + + + + + + + {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 ( + + toggleParent(parentKey)} + style={{ + backgroundColor: "#f7f7f7", + cursor: "pointer", + fontWeight: "bold", + }} + > + + + + + + {isExpanded && ( + + + + )} + + ); + })} + +
+ Element Type +
+ + {showDropdown && ( +
+
+ + +
+ {uniqueElementTypes.map(type => ( + + ))} + +
+ )} +
+ +
Instance# Aspects
{group.elementType.replaceAll("_", " ")}{group.elementInstance}{totalAspects}
+ + + + + + + + + + + + + {group.elements + .sort((a, b) => a.elementType.localeCompare(b.elementType)) + .flatMap(el => + el.aspects.map(aspect => ( + + + + + + + + + )) + )} + +
Aspect TypeValueQuantityInstall DateRenewal YearComments
{aspect.aspectType}{aspect.value ?? "-"}{aspect.quantity ?? "-"}{aspect.installDate ?? "-"}{aspect.renewalYear ?? "-"}{aspect.comments ?? "-"}
+
+ ); +} diff --git a/src/app/portfolio/[slug]/building-passport/[propertyId]/condition/page.tsx b/src/app/portfolio/[slug]/building-passport/[propertyId]/condition/page.tsx index 01d6900f..72fdeb8c 100644 --- a/src/app/portfolio/[slug]/building-passport/[propertyId]/condition/page.tsx +++ b/src/app/portfolio/[slug]/building-passport/[propertyId]/condition/page.tsx @@ -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 { 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
No survey data found for this property.
; + } - // 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, 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 (
-

- Property Condition for {propertyMeta.address || uprn} (UPRN: {uprn}) -

+

Property Condition

+

+ UPRN: {uprn}
+ Survey Date: {survey.date}
+ Source: {survey.source} +

- - - - - - - - - - - - - - - - - - {tableRows.map((row, i) => ( - - - - - - - - - - - - - - ))} - -
Survey IDDateSourceElement TypeElement InstanceAspect TypeValueQuantityInstall DateRenewal YearComments
{row.surveyId}{row.surveyDate}{row.surveySource}{row.elementType}{row.elementInstance}{row.aspectType}{row.aspectValue}{row.aspectQuantity}{row.aspectInstallDate || "-"}{row.aspectRenewalYear || "-"}{row.aspectComments || "-"}
+ {/* Client-side interactive table */} +
); } +