feat(FE-284): Refactor table component support for nesting header

This commit is contained in:
randy-ar
2025-12-09 17:57:46 +07:00
parent 3569955e7f
commit f9dfe7b27f
5 changed files with 1446 additions and 351 deletions
@@ -5,7 +5,6 @@ import Card from '@/components/Card';
import Table from '@/components/Table';
import { cn, formatCurrency, formatNumber } from '@/lib/helper';
import {
ClosingSapronakCalculation,
RowSapronakCalculation,
TotalSapronakCalculation,
} from '@/types/api/closing';
@@ -20,10 +19,6 @@ interface ClosingSapronakCalculationTableProps {
projectFlockId: number;
}
interface FooterSapronakCalculationRow extends RowSapronakCalculation {
_isFooter: true;
}
const ClosingSapronakCalculationTable = ({
type,
projectFlockId,
@@ -33,176 +28,124 @@ const ClosingSapronakCalculationTable = ({
() => ClosingApi.getPerhitunganSapronak(projectFlockId)
);
const columns: ColumnDef<RowSapronakCalculation>[] = useMemo(
() => [
{
header: 'Tanggal',
accessorKey: 'tanggal',
cell: (props) => {
const isFooter = '_isFooter' in props.row.original;
if (isFooter) return null;
const value = props.getValue() as string;
return value || '-';
},
},
{
header: 'No. Referensi',
accessorKey: 'no_referensi',
cell: (props) => {
const isFooter = '_isFooter' in props.row.original;
const value = props.getValue() as string;
if (isFooter) {
return (
<div className='font-semibold text-gray-900 col-span-2'>
{value}
</div>
);
}
return value || '-';
},
},
{
header: 'QTY Masuk',
accessorKey: 'qty_masuk',
cell: (props) => {
const value = props.getValue() as number;
const isFooter = '_isFooter' in props.row.original;
return (
<div className={isFooter ? 'font-semibold text-gray-900' : ''}>
{formatNumber(value)}
</div>
);
},
},
{
header: 'QTY Keluar',
accessorKey: 'qty_keluar',
cell: (props) => {
const value = props.getValue() as number;
const isFooter = '_isFooter' in props.row.original;
return (
<div className={isFooter ? 'font-semibold text-gray-900' : ''}>
{formatNumber(value)}
</div>
);
},
},
{
header: 'QTY Pakai',
accessorKey: 'qty_pakai',
cell: (props) => {
const value = props.getValue() as number;
const isFooter = '_isFooter' in props.row.original;
return (
<div className={isFooter ? 'font-semibold text-gray-900' : ''}>
{formatNumber(value)}
</div>
);
},
},
{
header: 'Uraian',
accessorKey: 'uraian',
cell: (props) => {
const isFooter = '_isFooter' in props.row.original;
if (isFooter) return null;
const value = props.getValue() as string;
return value || '-';
},
},
{
header: 'Kategori Produk',
accessorKey: 'kategori_produk',
cell: (props) => {
const isFooter = '_isFooter' in props.row.original;
if (isFooter) return null;
const value = props.getValue() as string;
return value || '-';
},
},
{
header: 'Harga Beli/Qty (Rp)',
accessorKey: 'harga_beli_per_qty',
cell: (props) => {
const value = props.getValue() as number;
const isFooter = '_isFooter' in props.row.original;
return (
<div className={isFooter ? 'font-semibold text-gray-900' : ''}>
{formatCurrency(value)}
</div>
);
},
},
{
header: 'Total Harga (Rp)',
accessorKey: 'total_harga',
cell: (props) => {
const value = props.getValue() as number;
const isFooter = '_isFooter' in props.row.original;
return (
<div className={isFooter ? 'font-semibold text-gray-900' : ''}>
{formatCurrency(value)}
</div>
);
},
},
{
header: 'Keterangan',
accessorKey: 'keterangan',
cell: (props) => {
const isFooter = '_isFooter' in props.row.original;
if (isFooter) return null;
const value = props.getValue() as string;
return value || '-';
},
},
],
[]
);
const createFooterRow = (
// Helper function to create columns with footer support
const createColumns = (
total?: TotalSapronakCalculation
): FooterSapronakCalculationRow[] => {
if (!total) return [];
return [
{
id: -999,
tanggal: '',
no_referensi: total.label,
qty_masuk: total.qty_masuk,
qty_keluar: total.qty_keluar,
qty_pakai: total.qty_pakai,
uraian: '',
kategori_produk: '',
harga_beli_per_qty: total.harga_beli_per_qty,
total_harga: total.total_harga,
keterangan: '',
_isFooter: true,
},
];
};
): ColumnDef<RowSapronakCalculation>[] => [
{
header: 'Tanggal',
accessorKey: 'tanggal',
cell: (props) => (props.getValue() as string) || '-',
footer: 'Total',
},
{
header: 'No. Referensi',
accessorKey: 'no_referensi',
cell: (props) => (props.getValue() as string) || '-',
footer: '',
},
{
header: 'QTY Masuk',
accessorKey: 'qty_masuk',
cell: (props) => formatNumber(props.getValue() as number),
footer: total
? () => (
<div className='font-semibold text-gray-900'>
{formatNumber(total.qty_masuk)}
</div>
)
: '',
},
{
header: 'QTY Keluar',
accessorKey: 'qty_keluar',
cell: (props) => formatNumber(props.getValue() as number),
footer: total
? () => (
<div className='font-semibold text-gray-900'>
{formatNumber(total.qty_keluar)}
</div>
)
: '',
},
{
header: 'QTY Pakai',
accessorKey: 'qty_pakai',
cell: (props) => formatNumber(props.getValue() as number),
footer: total
? () => (
<div className='font-semibold text-gray-900'>
{formatNumber(total.qty_pakai)}
</div>
)
: '',
},
{
header: 'Uraian',
accessorKey: 'uraian',
cell: (props) => (props.getValue() as string) || '-',
footer: '',
},
{
header: 'Kategori Produk',
accessorKey: 'kategori_produk',
cell: (props) => (props.getValue() as string) || '-',
footer: '',
},
{
header: 'Harga Beli/Qty (Rp)',
accessorKey: 'harga_beli_per_qty',
cell: (props) => formatCurrency(props.getValue() as number),
footer: total
? () => (
<div className='font-semibold text-gray-900'>
{formatCurrency(total.harga_beli_per_qty)}
</div>
)
: '',
},
{
header: 'Total Harga (Rp)',
accessorKey: 'total_harga',
cell: (props) => formatCurrency(props.getValue() as number),
footer: total
? () => (
<div className='font-semibold text-gray-900'>
{formatCurrency(total.total_harga)}
</div>
)
: '',
},
{
header: 'Keterangan',
accessorKey: 'keterangan',
cell: (props) => (props.getValue() as string) || '-',
footer: '',
},
];
const docBroilerFooter = useMemo(
// Memoize columns untuk setiap kategori
const docBroilerColumns = useMemo(
() =>
isResponseSuccess(sapronakCalculation)
? createFooterRow(sapronakCalculation.data?.doc_broiler.total)
: [],
? createColumns(sapronakCalculation.data?.doc_broiler.total)
: createColumns(),
[sapronakCalculation]
);
const ovkFooter = useMemo(
const ovkColumns = useMemo(
() =>
isResponseSuccess(sapronakCalculation)
? createFooterRow(sapronakCalculation.data?.ovk.total)
: [],
? createColumns(sapronakCalculation.data?.ovk.total)
: createColumns(),
[sapronakCalculation]
);
const pakanFooter = useMemo(
const pakanColumns = useMemo(
() =>
isResponseSuccess(sapronakCalculation)
? createFooterRow(sapronakCalculation.data?.pakan.total)
: [],
? createColumns(sapronakCalculation.data?.pakan.total)
: createColumns(),
[sapronakCalculation]
);
@@ -212,39 +155,20 @@ const ClosingSapronakCalculationTable = ({
<>
<Card
title='DOC Broiler'
variant='bordered'
collapsible
defaultCollapsed={false}
className={{
wrapper: 'w-full',
body: 'p-4 shadow',
}}
>
<Table<RowSapronakCalculation>
data={sapronakCalculation.data?.doc_broiler.rows ?? []}
columns={columns}
footerData={docBroilerFooter}
renderFooter={
(sapronakCalculation.data?.doc_broiler.rows.length ?? 0) > 0 &&
!!sapronakCalculation.data?.doc_broiler.total
}
columns={docBroilerColumns}
className={{
containerClassName: cn({
'mb-20':
sapronakCalculation.data?.doc_broiler.rows.length === 0,
}),
tableWrapperClassName: 'overflow-x-auto min-h-full!',
tableClassName: 'font-inter w-full table-auto min-h-full!',
headerRowClassName: 'border-b border-b-gray-200',
headerColumnClassName:
'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end',
bodyRowClassName: 'border-b border-b-gray-200',
bodyColumnClassName:
'px-6 py-3 last:flex last:flex-row last:justify-end',
tableFooterClassName:
'bg-gray-100 font-semibold border border-gray-200',
footerRowClassName: 'border-t-2 border-gray-300',
footerColumnClassName: 'px-6 py-3 text-xs text-gray-900',
containerClassName: 'my-4',
}}
renderFooter
/>
</Card>
@@ -259,29 +183,11 @@ const ClosingSapronakCalculationTable = ({
>
<Table<RowSapronakCalculation>
data={sapronakCalculation.data?.ovk.rows ?? []}
columns={columns}
footerData={ovkFooter}
renderFooter={
(sapronakCalculation.data?.ovk.rows.length ?? 0) > 0 &&
!!sapronakCalculation.data?.ovk.total
}
columns={ovkColumns}
className={{
containerClassName: cn({
'mb-20': sapronakCalculation.data?.ovk.rows.length === 0,
}),
tableWrapperClassName: 'overflow-x-auto min-h-full!',
tableClassName: 'font-inter w-full table-auto min-h-full!',
headerRowClassName: 'border-b border-b-gray-200',
headerColumnClassName:
'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end',
bodyRowClassName: 'border-b border-b-gray-200',
bodyColumnClassName:
'px-6 py-3 last:flex last:flex-row last:justify-end',
tableFooterClassName:
'bg-gray-100 font-semibold border border-gray-200',
footerRowClassName: 'border-t-2 border-gray-300',
footerColumnClassName: 'px-6 py-3 text-xs text-gray-900',
containerClassName: 'my-4',
}}
renderFooter
/>
</Card>
@@ -296,29 +202,11 @@ const ClosingSapronakCalculationTable = ({
>
<Table<RowSapronakCalculation>
data={sapronakCalculation.data?.pakan.rows ?? []}
columns={columns}
footerData={pakanFooter}
renderFooter={
(sapronakCalculation.data?.pakan.rows.length ?? 0) > 0 &&
!!sapronakCalculation.data?.pakan.total
}
columns={pakanColumns}
className={{
containerClassName: cn({
'mb-20': sapronakCalculation.data?.pakan.rows.length === 0,
}),
tableWrapperClassName: 'overflow-x-auto min-h-full!',
tableClassName: 'font-inter w-full table-auto min-h-full!',
headerRowClassName: 'border-b border-b-gray-200',
headerColumnClassName:
'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end',
bodyRowClassName: 'border-b border-b-gray-200',
bodyColumnClassName:
'px-6 py-3 last:flex last:flex-row last:justify-end',
tableFooterClassName:
'bg-gray-100 font-semibold border border-gray-200',
footerRowClassName: 'border-t-2 border-gray-300',
footerColumnClassName: 'px-6 py-3 text-xs text-gray-900',
containerClassName: 'my-4',
}}
renderFooter
/>
</Card>
</>