mirror of
https://github.com/Hestia-Homes/assessment-model.git
synced 2026-06-30 12:55:02 +00:00
switch timeline to use tremor scatter chart
This commit is contained in:
parent
244053bea8
commit
b91640d168
2 changed files with 427 additions and 286 deletions
|
|
@ -1,17 +1,231 @@
|
|||
// "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, { useRef, useState } from "react";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableRow,
|
||||
TableHead,
|
||||
TableCell,
|
||||
} from "@/app/shadcn_components/ui/table";
|
||||
useReactTable,
|
||||
getCoreRowModel,
|
||||
getFilteredRowModel,
|
||||
ColumnDef,
|
||||
flexRender,
|
||||
} from "@tanstack/react-table";
|
||||
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";
|
||||
|
||||
import { ELEMENT_TYPE_LABELS, ASPECT_TYPE_LABELS } from "../constants";
|
||||
|
||||
export type ElementGroup = {
|
||||
elementType: string;
|
||||
|
|
@ -19,196 +233,168 @@ export type ElementGroup = {
|
|||
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);
|
||||
// -----------------------
|
||||
// Hooks
|
||||
// -----------------------
|
||||
const [expandedRows, setExpandedRows] = useState<Set<string>>(new Set());
|
||||
const [filterText, setFilterText] = useState("");
|
||||
const [isMounted, setIsMounted] = useState(false);
|
||||
|
||||
const uniqueElementTypes = Array.from(new Set(groupedElements.map(g => g.elementType))).sort((a, b) =>
|
||||
a.localeCompare(b)
|
||||
);
|
||||
useEffect(() => setIsMounted(true), []);
|
||||
|
||||
const toggleTypeFilter = (type: string) => {
|
||||
const newSet = new Set(selectedTypes);
|
||||
if (newSet.has(type)) newSet.delete(type);
|
||||
else newSet.add(type);
|
||||
setSelectedTypes(newSet);
|
||||
// -----------------------
|
||||
// Columns
|
||||
// -----------------------
|
||||
const columns: ColumnDef<ElementGroup, any>[] = [
|
||||
{
|
||||
accessorKey: "elementType",
|
||||
header: "Element",
|
||||
cell: info => {
|
||||
const value = info.getValue() as string;
|
||||
const label = ELEMENT_TYPE_LABELS[value] ?? value.replaceAll("_", " ");
|
||||
const instance = info.row.original.elementInstance;
|
||||
return instance > 1 ? `${label} (${instance})` : label;
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorFn: row => row.elements.reduce((acc, el) => acc + el.aspects.length, 0),
|
||||
id: "noAspects",
|
||||
header: "No. Aspects",
|
||||
cell: info => info.getValue(),
|
||||
enableColumnFilter: false, // disable column filtering
|
||||
},
|
||||
];
|
||||
|
||||
// -----------------------
|
||||
// Table instance
|
||||
// -----------------------
|
||||
const table = useReactTable({
|
||||
data: groupedElements,
|
||||
columns,
|
||||
state: { globalFilter: filterText },
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
getRowId: row => `${row.elementType}__${row.elementInstance}`,
|
||||
globalFilterFn: (row, _columnId, filterValue) => {
|
||||
// filter only elementType
|
||||
return row.original.elementType
|
||||
.toLowerCase()
|
||||
.includes((filterValue as string).toLowerCase());
|
||||
},
|
||||
onGlobalFilterChange: setFilterText,
|
||||
});
|
||||
|
||||
const toggleRow = (rowId: string) => {
|
||||
setExpandedRows(prev => {
|
||||
const newSet = new Set(prev);
|
||||
if (newSet.has(rowId)) newSet.delete(rowId);
|
||||
else newSet.add(rowId);
|
||||
return 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);
|
||||
};
|
||||
// -----------------------
|
||||
// Prevent SSR hydration errors
|
||||
// -----------------------
|
||||
if (!isMounted) return null;
|
||||
|
||||
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>
|
||||
{/* Filter input above the table */}
|
||||
<div className="mb-2">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Filter elements..."
|
||||
value={filterText}
|
||||
onChange={e => setFilterText(e.target.value)}
|
||||
className="border rounded p-2 max-w-sm"
|
||||
/>
|
||||
</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>
|
||||
)}
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map(headerGroup => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map(header => (
|
||||
<TableHead key={header.id}>
|
||||
{flexRender(header.column.columnDef.header, header.getContext())}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
|
||||
<TableBody>
|
||||
{table.getRowModel().rows.map(row => {
|
||||
const rowId = row.id;
|
||||
const isExpanded = expandedRows.has(rowId);
|
||||
|
||||
return (
|
||||
<React.Fragment key={rowId}>
|
||||
{/* Parent row */}
|
||||
<TableRow
|
||||
className="bg-gray-100 font-bold cursor-pointer"
|
||||
onClick={() => toggleRow(rowId)}
|
||||
>
|
||||
{row.getVisibleCells().map(cell => (
|
||||
<TableCell key={cell.id}>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
|
||||
{/* Child row / expanded table */}
|
||||
{isExpanded && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={row.getVisibleCells().length} className="p-0">
|
||||
<ChildTable elements={row.original.elements} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------
|
||||
// Child table (ShadCN table)
|
||||
// -----------------------
|
||||
function ChildTable({ elements }: { elements: Element[] }) {
|
||||
return (
|
||||
<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>
|
||||
{elements.flatMap(el =>
|
||||
el.aspects.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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,20 @@
|
|||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import {
|
||||
ScatterChart,
|
||||
Scatter,
|
||||
XAxis,
|
||||
YAxis,
|
||||
CartesianGrid,
|
||||
Tooltip,
|
||||
ResponsiveContainer,
|
||||
ReferenceLine,
|
||||
Label,
|
||||
} from "recharts";
|
||||
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";
|
||||
|
||||
type RenewalsTimelineProps = {
|
||||
renewals: Renewal[];
|
||||
|
|
@ -21,97 +23,50 @@ type RenewalsTimelineProps = {
|
|||
export default function RenewalsTimeline({ renewals }: RenewalsTimelineProps) {
|
||||
const today = new Date().getFullYear();
|
||||
|
||||
const elementTypes = Array.from(
|
||||
new Set(renewals.map(r => (r.elementType ?? "").trim()))
|
||||
);
|
||||
const elementTypeToY = Object.fromEntries(elementTypes.map((type, i) => [type, i]));
|
||||
const renewalYears = renewals.map(r => r.renewalYear);
|
||||
const elementIds = renewals.map(r => parseInt(r.elementId));
|
||||
|
||||
const data = renewals.map(r => {
|
||||
let color = "#10b981";
|
||||
if (r.renewalYear < today) color = "#ef4444";
|
||||
else if (r.renewalYear - today <= 3) color = "#f59e0b";
|
||||
const minXValue = Math.min(...renewalYears);
|
||||
const maxXValue = Math.max(...renewalYears);
|
||||
const minYValue = Math.min(...elementIds);
|
||||
const maxYValue = Math.max(...elementIds);
|
||||
|
||||
return {
|
||||
id: r.id,
|
||||
elementType: r.elementType,
|
||||
x: r.renewalYear,
|
||||
y: elementTypeToY[r.elementType ?? ""],
|
||||
comments: r.comments ?? "",
|
||||
color,
|
||||
};
|
||||
const colouredData = renewals.map(r => ({
|
||||
...r,
|
||||
colourCategory:
|
||||
r.renewalYear < today
|
||||
? "past"
|
||||
: r.renewalYear <= today + 3
|
||||
? "soon"
|
||||
: "future",
|
||||
}));
|
||||
|
||||
const orderedData = colouredData.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'];
|
||||
});
|
||||
|
||||
// 3️⃣ Click handler
|
||||
const handleClick = (payload: any) => {
|
||||
if (!payload) return;
|
||||
alert(`Element Type: ${payload.elementType}\nYear: ${payload.x}\nComments: ${payload.comments}`);
|
||||
};
|
||||
const todayPosition =
|
||||
((today - (minXValue - 1)) / ((maxXValue + 1) - (minXValue - 1))) * 100;
|
||||
|
||||
return (
|
||||
<div style={{ width: "100%", height: 400 }}>
|
||||
<ResponsiveContainer>
|
||||
<ScatterChart
|
||||
margin={{ top: 20, right: 40, bottom: 20, left: 200 }}
|
||||
>
|
||||
<CartesianGrid />
|
||||
<XAxis
|
||||
type="number"
|
||||
dataKey="x"
|
||||
domain={["dataMin - 1", "dataMax + 1"]}
|
||||
allowDecimals={false}
|
||||
tickFormatter={tick => tick.toString()}
|
||||
/>
|
||||
<YAxis
|
||||
type="number"
|
||||
dataKey="y"
|
||||
width={200}
|
||||
ticks={elementTypes.map((_, i) => i)}
|
||||
tickFormatter={index => elementTypes[index]}
|
||||
interval={0} // force all element labels to render
|
||||
/>
|
||||
<Tooltip
|
||||
cursor={{ strokeDasharray: "3 3" }}
|
||||
content={({ active, payload }) => {
|
||||
if (active && payload && payload.length) {
|
||||
const data = payload[0].payload;
|
||||
return (
|
||||
<div style={{
|
||||
background: "white",
|
||||
padding: "8px",
|
||||
border: "1px solid #ccc",
|
||||
borderRadius: "4px",
|
||||
fontSize: "0.875rem"
|
||||
}}>
|
||||
<div><strong>{data.elementType}</strong></div>
|
||||
<div>Renewal year: {data.x}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}}
|
||||
/>
|
||||
<ReferenceLine x={today} stroke="#000" strokeDasharray="3 3">
|
||||
<Label value="Today" position="top" />
|
||||
</ReferenceLine>
|
||||
|
||||
<Scatter
|
||||
data={data}
|
||||
shape={(props: any) => {
|
||||
const { cx, cy, payload } = props;
|
||||
return (
|
||||
<circle
|
||||
cx={cx}
|
||||
cy={cy}
|
||||
r={6}
|
||||
fill={payload.color}
|
||||
onClick={() => handleClick(payload)}
|
||||
style={{ cursor: "pointer" }}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</ScatterChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
<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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
Loading…
Add table
Reference in a new issue