mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-22 06:15:47 +00:00
feat(FE-284): Refactor table component support for nesting header
This commit is contained in:
@@ -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>
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user