refactor(FE-326): Remove custom header rows and simplify Table

This commit is contained in:
rstubryan
2025-12-06 09:47:38 +07:00
parent aad24c3c58
commit c9552dec2d
2 changed files with 53 additions and 293 deletions
+45 -163
View File
@@ -14,8 +14,6 @@ import {
SortingState, SortingState,
OnChangeFn, OnChangeFn,
Row, Row,
HeaderGroup,
Column,
} from '@tanstack/react-table'; } from '@tanstack/react-table';
import { rankItem } from '@tanstack/match-sorter-utils'; import { rankItem } from '@tanstack/match-sorter-utils';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
@@ -34,21 +32,6 @@ interface TableClassNames {
bodyRowClassName?: string; bodyRowClassName?: string;
bodyColumnClassName?: string; bodyColumnClassName?: string;
paginationClassName?: string; paginationClassName?: string;
customHeaderRowClassName?: string;
customHeaderCellClassName?: string;
}
export interface CustomHeaderRow {
id: string;
cells: Array<{
id: string;
content: ReactNode;
colSpan?: number;
rowSpan?: number;
className?: string;
field?: string;
}>;
className?: string;
} }
export interface TableProps<TData extends object> { export interface TableProps<TData extends object> {
@@ -69,13 +52,6 @@ export interface TableProps<TData extends object> {
rowSelection?: Record<string, boolean>; rowSelection?: Record<string, boolean>;
setRowSelection?: OnChangeFn<Record<string, boolean>>; setRowSelection?: OnChangeFn<Record<string, boolean>>;
enableRowSelection?: boolean | ((row: Row<TData>) => boolean); enableRowSelection?: boolean | ((row: Row<TData>) => boolean);
customHeaderRows?: CustomHeaderRow[];
renderCustomHeaders?: boolean;
onCustomHeaderCellRender?: <TData extends object>(
cell: ReactNode,
column: Column<TData, unknown>,
headerGroup: HeaderGroup<TData>
) => ReactNode;
renderFooter?: boolean; renderFooter?: boolean;
footerContent?: ReactNode; footerContent?: ReactNode;
} }
@@ -111,8 +87,6 @@ const Table = <TData extends object>({
bodyRowClassName: '', bodyRowClassName: '',
bodyColumnClassName: '', bodyColumnClassName: '',
paginationClassName: '', paginationClassName: '',
customHeaderRowClassName: '',
customHeaderCellClassName: '',
}, },
emptyContent = emptyContentDefaultValue, emptyContent = emptyContentDefaultValue,
sorting, sorting,
@@ -121,9 +95,6 @@ const Table = <TData extends object>({
rowSelection, rowSelection,
setRowSelection, setRowSelection,
enableRowSelection, enableRowSelection,
customHeaderRows = [],
renderCustomHeaders = false,
onCustomHeaderCellRender,
renderFooter = false, renderFooter = false,
footerContent, footerContent,
}: TableProps<TData>) => { }: TableProps<TData>) => {
@@ -228,143 +199,55 @@ const Table = <TData extends object>({
<div className={className.tableWrapperClassName}> <div className={className.tableWrapperClassName}>
<table className={className.tableClassName}> <table className={className.tableClassName}>
<thead className={className.tableHeaderClassName}> <thead className={className.tableHeaderClassName}>
{renderCustomHeaders &&
customHeaderRows.length > 0 &&
customHeaderRows.map((headerRow) => (
<tr
key={headerRow.id}
className={
headerRow.className || className.customHeaderRowClassName
}
>
{headerRow.cells.map((cell) => {
const column = table
.getAllColumns()
.find((col) => col.id === cell.field);
const canSort = column?.getCanSort();
const sortingState = column?.getIsSorted();
return (
<th
key={cell.id}
colSpan={cell.colSpan}
rowSpan={cell.rowSpan}
onClick={
canSort
? column?.getToggleSortingHandler()
: undefined
}
className={cn(
cell.className || className.customHeaderCellClassName,
canSort ? 'cursor-pointer select-none' : ''
)}
>
<div
className={cn(
'flex items-center gap-1',
cell.className?.includes('text-center')
? 'justify-center'
: ''
)}
>
{cell.content}
{canSort && (
<div className='flex items-center'>
<Icon
icon='lucide:arrow-up'
width={12}
height={12}
className={cn(
'transition-all ease-in-out duration-200',
sortingState === 'asc'
? 'text-black'
: 'text-black/30'
)}
/>
<Icon
icon='lucide:arrow-down'
width={12}
height={12}
className={cn(
'transition-all ease-in-out duration-200',
sortingState === 'desc'
? 'text-black'
: 'text-black/30'
)}
/>
</div>
)}
</div>
</th>
);
})}
</tr>
))}
{table.getHeaderGroups().map((headerGroup) => ( {table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id} className={className.headerRowClassName}> <tr key={headerGroup.id} className={className.headerRowClassName}>
{headerGroup.headers.map((header) => { {headerGroup.headers.map((header) => (
let cellContent = flexRender( <th
header.column.columnDef.header, key={header.id}
header.getContext() colSpan={header.colSpan}
); onClick={header.column.getToggleSortingHandler()}
className={cn(
if (onCustomHeaderCellRender) { header.column.getCanSort()
cellContent = onCustomHeaderCellRender( ? 'cursor-pointer select-none'
cellContent, : '',
header.column, className.headerColumnClassName
headerGroup )}
); >
} <div className='flex items-center gap-1'>
{flexRender(
return ( header.column.columnDef.header,
<th header.getContext()
key={header.id}
colSpan={header.colSpan}
rowSpan={header.rowSpan}
onClick={header.column.getToggleSortingHandler()}
className={cn(
header.column.getCanSort()
? 'cursor-pointer select-none'
: '',
className.headerColumnClassName
)} )}
>
<div className='flex items-center gap-1'>
{cellContent}
{header.column.getCanSort() && ( {header.column.getCanSort() && (
<div className='flex items-center'> <div className='flex items-center'>
<Icon <Icon
icon='lucide:arrow-up' icon='lucide:arrow-up'
width={12} width={12}
height={12} height={12}
className={cn( className={cn(
'transition-all ease-in-out duration-200', 'transition-all ease-in-out duration-200',
header.column.getIsSorted() === 'asc' header.column.getIsSorted() === 'asc'
? 'text-black' ? 'text-black'
: 'text-black/30' : 'text-black/30'
)} )}
/> />
<Icon <Icon
icon='lucide:arrow-down' icon='lucide:arrow-down'
width={12} width={12}
height={12} height={12}
className={cn( className={cn(
'transition-all ease-in-out duration-200', 'transition-all ease-in-out duration-200',
header.column.getIsSorted() === 'desc' header.column.getIsSorted() === 'desc'
? 'text-black' ? 'text-black'
: 'text-black/30' : 'text-black/30'
)} )}
/> />
</div> </div>
)} )}
</div> </div>
</th> </th>
); ))}
})}
</tr> </tr>
))} ))}
</thead> </thead>
@@ -383,7 +266,6 @@ const Table = <TData extends object>({
</tr> </tr>
))} ))}
</tbody> </tbody>
{renderFooter && footerContent} {renderFooter && footerContent}
</table> </table>
</div> </div>
@@ -2,7 +2,7 @@
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import { ColumnDef } from '@tanstack/react-table'; import { ColumnDef } from '@tanstack/react-table';
import Table, { CustomHeaderRow } from '@/components/Table'; import Table from '@/components/Table';
import Card from '@/components/Card'; import Card from '@/components/Card';
import Badge from '@/components/Badge'; import Badge from '@/components/Badge';
import { formatCurrency, formatNumber, formatDate } from '@/lib/helper'; import { formatCurrency, formatNumber, formatDate } from '@/lib/helper';
@@ -16,101 +16,6 @@ interface SalesReportTableProps {
initialValues?: BaseClosingSales; initialValues?: BaseClosingSales;
} }
interface HeaderCell {
id: string;
content: React.ReactNode;
colSpan?: number;
rowSpan?: number;
className: string;
field?: string;
}
const generateCustomHeaders = (template: {
groups: Array<{
label: string;
field?: string;
rowSpan?: number;
colSpan?: number;
subLabels?: string[];
}>;
}): CustomHeaderRow[] => {
const mainRow: Array<{
id: string;
content: React.ReactNode;
colSpan?: number;
rowSpan?: number;
className: string;
}> = [];
const subRow: Array<{
id: string;
content: React.ReactNode;
colSpan?: number;
rowSpan?: number;
className: string;
}> = [];
let subColumnIndex = 0;
template.groups.forEach((group) => {
if (group.subLabels) {
const mainCell: HeaderCell = {
id: `${group.field || 'group'}-${subColumnIndex}`,
content: group.label,
colSpan: group.colSpan,
className:
'px-4 py-3 text-xs font-semibold text-gray-700 text-center whitespace-nowrap border border-gray-200',
};
mainRow.push(mainCell);
group.subLabels.forEach((subLabel) => {
const subCell: HeaderCell = {
id: `sub-${subColumnIndex}`,
content: subLabel,
className:
'px-4 py-3 text-xs font-semibold text-gray-700 text-left whitespace-nowrap border border-gray-200',
};
if (group.label === 'Jumlah') {
subCell.field = subLabel === 'Kuantitas' ? 'quantity' : 'weight';
}
subRow.push(subCell);
subColumnIndex++;
});
} else {
const mainCell: HeaderCell = {
id: `${group.field}-header`,
content: group.label,
rowSpan: group.rowSpan,
className:
'px-4 py-3 text-xs font-semibold text-gray-700 text-left whitespace-nowrap border border-gray-200',
};
mainCell.field = group.field;
mainRow.push(mainCell);
}
});
const rows: CustomHeaderRow[] = [
{
id: 'main-header',
cells: mainRow,
className: 'bg-gray-50',
},
];
if (subRow.length > 0) {
rows.push({
id: 'sub-header',
cells: subRow,
className: 'bg-gray-50',
});
}
return rows;
};
const SalesReportTable = ({ const SalesReportTable = ({
type = 'detail', type = 'detail',
initialValues, initialValues,
@@ -310,7 +215,7 @@ const SalesReportTable = ({
}; };
return ( return (
<Badge variant='soft' size='xs' color={getStatusColor(status)}> <Badge variant='soft' size='sm' color={getStatusColor(status)}>
{status || '-'} {status || '-'}
</Badge> </Badge>
); );
@@ -320,33 +225,6 @@ const SalesReportTable = ({
[] []
); );
const headerTemplate = {
groups: [
{ label: 'Tanggal Realisasi', field: 'realization_date', rowSpan: 2 },
{ label: 'Umur', field: 'age', rowSpan: 2 },
{ label: 'No. DO', field: 'do_number', rowSpan: 2 },
{ label: 'Produk', field: 'product', rowSpan: 2 },
{ label: 'Customer', field: 'customer', rowSpan: 2 },
{
label: 'Jumlah',
colSpan: 2,
subLabels: ['Qty', 'Kg'],
},
{ label: 'AVG (Kg)', field: 'avg_weight', rowSpan: 2 },
{ label: 'Harga Mitra (Rp)', field: 'price_partner', rowSpan: 2 },
{ label: 'Total Mitra (Rp)', field: 'total_mitra', rowSpan: 2 },
{ label: 'Harga Act (Rp)', field: 'price_act', rowSpan: 2 },
{ label: 'Total Act (Rp)', field: 'total_act', rowSpan: 2 },
{ label: 'Kandang', field: 'kandang', rowSpan: 2 },
{ label: 'Status Pembayaran', field: 'payment_status', rowSpan: 2 },
],
};
const salesCustomHeaderRows = useMemo(
() => generateCustomHeaders(headerTemplate),
[]
);
return ( return (
<> <>
<section className='w-full'> <section className='w-full'>
@@ -361,8 +239,6 @@ const SalesReportTable = ({
<Table <Table
data={salesData} data={salesData}
columns={salesColumns} columns={salesColumns}
renderCustomHeaders={true}
customHeaderRows={salesCustomHeaderRows}
renderFooter={salesData.length > 0} renderFooter={salesData.length > 0}
footerContent={ footerContent={
<tfoot> <tfoot>
@@ -374,13 +250,13 @@ const SalesReportTable = ({
<td className='px-4 py-3 text-xs text-gray-900 whitespace-nowrap '></td> <td className='px-4 py-3 text-xs text-gray-900 whitespace-nowrap '></td>
<td className='px-4 py-3 text-xs text-gray-900 whitespace-nowrap '></td> <td className='px-4 py-3 text-xs text-gray-900 whitespace-nowrap '></td>
<td className='px-4 py-3 text-xs text-gray-900 whitespace-nowrap '></td> <td className='px-4 py-3 text-xs text-gray-900 whitespace-nowrap '></td>
<td className='px-4 py-3 text-xs text-gray-900 whitespace-nowrap text-right '> <td className='px-4 py-3 text-xs text-gray-900 whitespace-nowrap '>
{formatNumber(totals.totalQuantity)} {formatNumber(totals.totalQuantity)}
</td> </td>
<td className='px-4 py-3 text-xs text-gray-900 whitespace-nowrap text-right '> <td className='px-4 py-3 text-xs text-gray-900 whitespace-nowrap '>
{formatNumber(totals.totalWeight)} {formatNumber(totals.totalWeight)}
</td> </td>
<td className='px-4 py-3 text-xs text-gray-900 whitespace-nowrap text-right '> <td className='px-4 py-3 text-xs text-gray-900 whitespace-nowrap '>
{formatNumber(totals.avgWeight)} {formatNumber(totals.avgWeight)}
</td> </td>
<td className='px-4 py-3 text-xs text-gray-900 whitespace-nowrap text-right '> <td className='px-4 py-3 text-xs text-gray-900 whitespace-nowrap text-right '>
@@ -403,7 +279,9 @@ const SalesReportTable = ({
className={{ className={{
tableWrapperClassName: 'overflow-x-auto', tableWrapperClassName: 'overflow-x-auto',
tableClassName: 'w-full table-auto text-sm', tableClassName: 'w-full table-auto text-sm',
headerRowClassName: 'hidden', headerRowClassName: 'border-b border-b-gray-200',
headerColumnClassName:
'px-4 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end whitespace-nowrap',
bodyRowClassName: bodyRowClassName:
'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200', 'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200',
bodyColumnClassName: bodyColumnClassName: