mirror of
https://github.com/Hestia-Homes/assessment-model.git
synced 2026-06-30 12:55:02 +00:00
tidy up renewals chart, allow filtering out of
This commit is contained in:
parent
b91640d168
commit
3e102ae6ea
2 changed files with 65 additions and 278 deletions
|
|
@ -1,218 +1,3 @@
|
|||
// "use client";
|
||||
|
||||
// import React, { useRef, useState } from "react";
|
||||
// import {
|
||||
// Table,
|
||||
// TableHeader,
|
||||
// TableBody,
|
||||
// TableRow,
|
||||
// TableHead,
|
||||
// TableCell,
|
||||
// } from "@/app/shadcn_components/ui/table";
|
||||
// import { Element } from "../types/element";
|
||||
// import { ASPECT_TYPE_LABELS, ELEMENT_TYPE_LABELS } from "../constants";
|
||||
|
||||
|
||||
// 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 [dropdownPos, setDropdownPos] = useState<{ top: number; left: number }>({ top: 0, left: 0 });
|
||||
// const buttonRef = useRef<HTMLButtonElement | null>(null);
|
||||
|
||||
// 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);
|
||||
// };
|
||||
|
||||
// // Handle dropdown toggle and position
|
||||
// const handleDropdownToggle = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
// const rect = e.currentTarget.getBoundingClientRect();
|
||||
// const parentRect = (e.currentTarget.offsetParent as HTMLElement)?.getBoundingClientRect();
|
||||
// if (parentRect) {
|
||||
// setDropdownPos({
|
||||
// top: rect.bottom - parentRect.top,
|
||||
// left: rect.left - parentRect.left,
|
||||
// });
|
||||
// }
|
||||
// setShowDropdown(prev => !prev);
|
||||
// };
|
||||
|
||||
// return (
|
||||
// <div className="relative">
|
||||
// <div className="rounded-md border">
|
||||
// <Table>
|
||||
// <TableHeader>
|
||||
// <TableRow>
|
||||
// <TableHead>
|
||||
// Element
|
||||
// {/* Filter button stays in header */}
|
||||
// <button
|
||||
// ref={buttonRef}
|
||||
// onClick={handleDropdownToggle}
|
||||
// className="ml-2 btn btn-sm"
|
||||
// >
|
||||
// ▼
|
||||
// </button>
|
||||
// </TableHead>
|
||||
// <TableHead>No. Aspects</TableHead>
|
||||
// </TableRow>
|
||||
// </TableHeader>
|
||||
|
||||
// <TableBody>
|
||||
// {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}>
|
||||
// {/* Parent Row */}
|
||||
// <TableRow
|
||||
// className="bg-gray-100 font-bold cursor-pointer"
|
||||
// onClick={() => toggleParent(parentKey)}
|
||||
// >
|
||||
// <TableCell>
|
||||
// {`${ELEMENT_TYPE_LABELS[group.elementType] ?? group.elementType.replaceAll("_", " ")}${group.elementInstance > 1 ? ` (${group.elementInstance})` : ""}`}
|
||||
// </TableCell>
|
||||
// <TableCell>{totalAspects}</TableCell>
|
||||
// </TableRow>
|
||||
|
||||
// {/* Child Table */}
|
||||
// {isExpanded && (
|
||||
// <TableRow>
|
||||
// <TableCell colSpan={2} className="p-0">
|
||||
// <Table className="m-0 w-full border-t">
|
||||
// <TableHeader>
|
||||
// <TableRow>
|
||||
// <TableHead>Aspect Type</TableHead>
|
||||
// <TableHead>Value</TableHead>
|
||||
// <TableHead>Quantity</TableHead>
|
||||
// <TableHead>Install Date</TableHead>
|
||||
// <TableHead>Renewal Year</TableHead>
|
||||
// <TableHead>Comments</TableHead>
|
||||
// </TableRow>
|
||||
// </TableHeader>
|
||||
// <TableBody>
|
||||
// {group.elements.flatMap(el =>
|
||||
// el.aspects
|
||||
// .sort((a, b) => {
|
||||
// const labelA = ASPECT_TYPE_LABELS[a.aspectType] ?? a.aspectType;
|
||||
// const labelB = ASPECT_TYPE_LABELS[b.aspectType] ?? b.aspectType;
|
||||
// return labelA.localeCompare(labelB);
|
||||
// })
|
||||
// .map(aspect => (
|
||||
// <TableRow key={aspect.id}>
|
||||
// <TableCell>{ASPECT_TYPE_LABELS[aspect.aspectType] ?? aspect.aspectType}</TableCell>
|
||||
// <TableCell>{aspect.value ?? "-"}</TableCell>
|
||||
// <TableCell>{aspect.quantity ?? "-"}</TableCell>
|
||||
// <TableCell>{aspect.installDate ?? "-"}</TableCell>
|
||||
// <TableCell>{aspect.renewalYear ?? "-"}</TableCell>
|
||||
// <TableCell>{aspect.comments ?? "-"}</TableCell>
|
||||
// </TableRow>
|
||||
// ))
|
||||
// )}
|
||||
// </TableBody>
|
||||
// </Table>
|
||||
// </TableCell>
|
||||
// </TableRow>
|
||||
// )}
|
||||
// </React.Fragment>
|
||||
// );
|
||||
// })}
|
||||
// </TableBody>
|
||||
// </Table>
|
||||
// </div>
|
||||
|
||||
// {/* Floating Dropdown */}
|
||||
// {showDropdown && (
|
||||
// <div
|
||||
// className="absolute z-20 max-h-64 w-72 overflow-y-auto rounded border bg-white p-2 shadow"
|
||||
// style={{ top: dropdownPos.top, left: dropdownPos.left }}
|
||||
// >
|
||||
// <div className="mb-2 flex gap-2">
|
||||
// <button onClick={selectAll} className="btn btn-xs">
|
||||
// Select All
|
||||
// </button>
|
||||
// <button onClick={deselectAll} className="btn btn-xs">
|
||||
// Deselect All
|
||||
// </button>
|
||||
// </div>
|
||||
// {uniqueElementTypes
|
||||
// .sort((a, b) => {
|
||||
// const labelA = ELEMENT_TYPE_LABELS[a] ?? a.replaceAll("_", " ");
|
||||
// const labelB = ELEMENT_TYPE_LABELS[b] ?? b.replaceAll("_", " ");
|
||||
// return labelA.localeCompare(labelB);
|
||||
// })
|
||||
// .map(type => (
|
||||
// <label key={type} className="block cursor-pointer">
|
||||
// <input
|
||||
// type="checkbox"
|
||||
// checked={selectedTypes.has(type)}
|
||||
// onChange={() => toggleTypeFilter(type)}
|
||||
// className="mr-2"
|
||||
// />
|
||||
// {ELEMENT_TYPE_LABELS[type] ?? type.replaceAll("_", " ")}
|
||||
// </label>
|
||||
// ))}
|
||||
// <button
|
||||
// onClick={() => setShowDropdown(false)}
|
||||
// className="mt-2 btn btn-xs"
|
||||
// >
|
||||
// Close
|
||||
// </button>
|
||||
// </div>
|
||||
// )}
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
|
|
@ -238,18 +23,12 @@ type Props = {
|
|||
};
|
||||
|
||||
export default function ElementTable({ groupedElements }: Props) {
|
||||
// -----------------------
|
||||
// Hooks
|
||||
// -----------------------
|
||||
const [expandedRows, setExpandedRows] = useState<Set<string>>(new Set());
|
||||
const [filterText, setFilterText] = useState("");
|
||||
const [isMounted, setIsMounted] = useState(false);
|
||||
|
||||
useEffect(() => setIsMounted(true), []);
|
||||
|
||||
// -----------------------
|
||||
// Columns
|
||||
// -----------------------
|
||||
const columns: ColumnDef<ElementGroup, any>[] = [
|
||||
{
|
||||
accessorKey: "elementType",
|
||||
|
|
@ -270,9 +49,6 @@ export default function ElementTable({ groupedElements }: Props) {
|
|||
},
|
||||
];
|
||||
|
||||
// -----------------------
|
||||
// Table instance
|
||||
// -----------------------
|
||||
const table = useReactTable({
|
||||
data: groupedElements,
|
||||
columns,
|
||||
|
|
@ -298,9 +74,6 @@ export default function ElementTable({ groupedElements }: Props) {
|
|||
});
|
||||
};
|
||||
|
||||
// -----------------------
|
||||
// Prevent SSR hydration errors
|
||||
// -----------------------
|
||||
if (!isMounted) return null;
|
||||
|
||||
return (
|
||||
|
|
@ -348,7 +121,6 @@ export default function ElementTable({ groupedElements }: Props) {
|
|||
))}
|
||||
</TableRow>
|
||||
|
||||
{/* Child row / expanded table */}
|
||||
{isExpanded && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={row.getVisibleCells().length} className="p-0">
|
||||
|
|
@ -365,9 +137,6 @@ export default function ElementTable({ groupedElements }: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
// -----------------------
|
||||
// Child table (ShadCN table)
|
||||
// -----------------------
|
||||
function ChildTable({ elements }: { elements: Element[] }) {
|
||||
return (
|
||||
<Table className="m-0 w-full border-t">
|
||||
|
|
|
|||
|
|
@ -2,71 +2,89 @@
|
|||
|
||||
import React from "react";
|
||||
import { ScatterChart } from "@tremor/react";
|
||||
// import {
|
||||
// ScatterChart,
|
||||
// Scatter,
|
||||
// XAxis,
|
||||
// YAxis,
|
||||
// CartesianGrid,
|
||||
// Tooltip,
|
||||
// ResponsiveContainer,
|
||||
// ReferenceLine,
|
||||
// Label,
|
||||
// } from "recharts";
|
||||
import { Renewal } from "../viewModels/renewal";
|
||||
import { Tooltip } from "@/app/shadcn_components/ui/tooltip";
|
||||
import { ELEMENT_TYPE_LABELS } from "../constants";
|
||||
|
||||
type RenewalsTimelineProps = {
|
||||
renewals: Renewal[];
|
||||
};
|
||||
|
||||
export default function RenewalsTimeline({ renewals }: RenewalsTimelineProps) {
|
||||
const [showFuture, setShowFuture] = React.useState(false);
|
||||
|
||||
const today = new Date().getFullYear();
|
||||
|
||||
const renewalYears = renewals.map(r => r.renewalYear);
|
||||
const elementIds = renewals.map(r => parseInt(r.elementId));
|
||||
|
||||
const minXValue = Math.min(...renewalYears);
|
||||
const maxXValue = Math.max(...renewalYears);
|
||||
const minYValue = Math.min(...elementIds);
|
||||
const maxYValue = Math.max(...elementIds);
|
||||
|
||||
|
||||
const colouredData = renewals.map(r => ({
|
||||
...r,
|
||||
colourCategory:
|
||||
r.renewalYear < today
|
||||
? "past"
|
||||
: r.renewalYear <= today + 3
|
||||
? "soon"
|
||||
: "future",
|
||||
? "past"
|
||||
: r.renewalYear <= today + 3
|
||||
? "soon"
|
||||
: "future",
|
||||
label: ELEMENT_TYPE_LABELS[r.elementType] ?? r.elementType.replaceAll("_", " "),
|
||||
}));
|
||||
|
||||
const orderedData = colouredData.sort((a, b) => {
|
||||
|
||||
const filteredData = showFuture
|
||||
? colouredData
|
||||
: colouredData.filter(r => r.colourCategory !== "future");
|
||||
|
||||
const orderedData = filteredData.sort((a, b) => {
|
||||
const order: Record<'past' | 'soon' | 'future', number> = { past: 0, soon: 1, future: 2 };
|
||||
return order[a.colourCategory as 'past' | 'soon' | 'future'] - order[b.colourCategory as 'past' | 'soon' | 'future'];
|
||||
});
|
||||
|
||||
const filteredRenewalYears = filteredData.map(r => r.renewalYear);
|
||||
const filteredElementIds = filteredData.map(r => parseInt(r.elementId));
|
||||
|
||||
const todayPosition =
|
||||
((today - (minXValue - 1)) / ((maxXValue + 1) - (minXValue - 1))) * 100;
|
||||
const minXValue = Math.min(...filteredRenewalYears);
|
||||
const maxXValue = Math.max(...filteredRenewalYears);
|
||||
const minYValue = Math.min(...filteredElementIds);
|
||||
const maxYValue = Math.max(...filteredElementIds);
|
||||
|
||||
const customTooltip = ({ payload, active }: any) => {
|
||||
if (!active || !payload?.length) return null;
|
||||
|
||||
const data = payload[0].payload;
|
||||
|
||||
return (
|
||||
<div className="bg-white p-3 border rounded shadow-lg z-50 pointer-events-none">
|
||||
<div className="text-sm text-gray-500">{data.renewalYear}</div>
|
||||
<div className="text-base font-semibold">{data.label}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<ScatterChart
|
||||
className="h-80"
|
||||
data={orderedData}
|
||||
x="renewalYear"
|
||||
y="elementId"
|
||||
category="colorCategory"
|
||||
colors={['red', 'orange', 'green']}
|
||||
showLegend={false}
|
||||
xAxisLabel="Year"
|
||||
yAxisLabel="" // removes axis title
|
||||
showXAxis={true}
|
||||
showYAxis={false}
|
||||
minXValue={minXValue - 1}
|
||||
maxXValue={maxXValue + 1}
|
||||
minYValue={minYValue - 1}
|
||||
maxYValue={maxYValue + 1}
|
||||
showGridLines={false}
|
||||
/>
|
||||
<div>
|
||||
<label className="flex items-center gap-2 text-sm mb-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={showFuture}
|
||||
onChange={(e) => setShowFuture(e.target.checked)}
|
||||
/>
|
||||
Show renewals not due in the next 3 years
|
||||
</label>
|
||||
|
||||
<ScatterChart
|
||||
className="h-80 [&_.recharts-yAxis_.recharts-cartesian-axis-tick_text]:hidden"
|
||||
data={orderedData}
|
||||
x="renewalYear"
|
||||
y="elementId"
|
||||
category="colourCategory"
|
||||
colors={['red', 'orange', 'green']}
|
||||
showLegend={false}
|
||||
xAxisLabel="Year"
|
||||
yAxisLabel="" // removes axis title
|
||||
showXAxis={true}
|
||||
showYAxis={false}
|
||||
minXValue={minXValue - 1}
|
||||
maxXValue={maxXValue + 1}
|
||||
minYValue={minYValue - 1}
|
||||
maxYValue={maxYValue + 1}
|
||||
showGridLines={false}
|
||||
customTooltip={customTooltip}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Loading…
Add table
Reference in a new issue