feat(FE-326): Add totals footer row to sales report table

This commit is contained in:
rstubryan
2025-12-06 10:26:26 +07:00
parent e407410c4a
commit b3f7b8a3c5
@@ -16,6 +16,10 @@ interface SalesReportTableProps {
initialValues?: BaseClosingSales; initialValues?: BaseClosingSales;
} }
interface FooterSalesRow extends BaseSales {
_isFooter: true;
}
const SalesReportTable = ({ const SalesReportTable = ({
type = 'detail', type = 'detail',
initialValues, initialValues,
@@ -68,6 +72,29 @@ const SalesReportTable = ({
}; };
}, [salesData]); }, [salesData]);
const footerData = useMemo((): FooterSalesRow[] => {
if (salesData.length === 0) return [];
const footerRow: FooterSalesRow = {
id: -999,
realization_date: 'Total Penjualan',
age: 0,
do_number: '',
product: {} as Product,
customer: {} as Customer,
qty: totals.totalQuantity,
weight: totals.totalWeight,
avg_weight: totals.avgWeight,
price: totals.avgPricePartner,
total_price: totals.totalPartner,
kandang: {} as Kandang,
payment_status: '',
_isFooter: true,
};
return [footerRow];
}, [salesData, totals]);
const salesColumns: ColumnDef<BaseSales>[] = useMemo( const salesColumns: ColumnDef<BaseSales>[] = useMemo(
() => [ () => [
{ {
@@ -75,6 +102,14 @@ const SalesReportTable = ({
accessorKey: 'realization_date', accessorKey: 'realization_date',
header: 'Tanggal Realisasi', header: 'Tanggal Realisasi',
cell: (props) => { cell: (props) => {
const isFooter = '_isFooter' in props.row.original;
if (isFooter) {
return (
<div className='font-semibold text-gray-900 col-span-5'>
{props.row.original.realization_date}
</div>
);
}
const date = props.row.original.realization_date; const date = props.row.original.realization_date;
return date ? formatDate(date, 'DD MMM YYYY') : '-'; return date ? formatDate(date, 'DD MMM YYYY') : '-';
}, },
@@ -83,19 +118,27 @@ const SalesReportTable = ({
id: 'age', id: 'age',
accessorKey: 'age', accessorKey: 'age',
header: 'Umur', header: 'Umur',
cell: (props) => props.getValue() || '-', cell: (props) => {
const isFooter = '_isFooter' in props.row.original;
return isFooter ? null : props.getValue() || '-';
},
}, },
{ {
id: 'do_number', id: 'do_number',
accessorKey: 'do_number', accessorKey: 'do_number',
header: 'No. DO', header: 'No. DO',
cell: (props) => props.getValue() || '-', cell: (props) => {
const isFooter = '_isFooter' in props.row.original;
return isFooter ? null : props.getValue() || '-';
},
}, },
{ {
id: 'product', id: 'product',
accessorKey: 'product', accessorKey: 'product',
header: 'Produk', header: 'Produk',
cell: (props) => { cell: (props) => {
const isFooter = '_isFooter' in props.row.original;
if (isFooter) return null;
const product = props.getValue() as Product; const product = props.getValue() as Product;
return product?.name || '-'; return product?.name || '-';
}, },
@@ -105,6 +148,8 @@ const SalesReportTable = ({
accessorKey: 'customer', accessorKey: 'customer',
header: 'Customer', header: 'Customer',
cell: (props) => { cell: (props) => {
const isFooter = '_isFooter' in props.row.original;
if (isFooter) return null;
const customer = props.getValue() as Customer; const customer = props.getValue() as Customer;
return customer?.name || '-'; return customer?.name || '-';
}, },
@@ -115,9 +160,13 @@ const SalesReportTable = ({
header: 'Kuantitas', header: 'Kuantitas',
cell: (props) => { cell: (props) => {
const value = props.getValue() as number; const value = props.getValue() as number;
const isSummary = props.row.id === 'summary'; const isFooter = '_isFooter' in props.row.original;
return ( return (
<div className={isSummary ? 'text-right' : ''}> <div
className={
isFooter ? 'text-left font-semibold text-gray-900' : 'text-left'
}
>
{formatNumber(value)} {formatNumber(value)}
</div> </div>
); );
@@ -129,9 +178,13 @@ const SalesReportTable = ({
header: 'Kg', header: 'Kg',
cell: (props) => { cell: (props) => {
const value = props.getValue() as number; const value = props.getValue() as number;
const isSummary = props.row.id === 'summary'; const isFooter = '_isFooter' in props.row.original;
return ( return (
<div className={isSummary ? 'text-right' : ''}> <div
className={
isFooter ? 'text-left font-semibold text-gray-900' : 'text-left'
}
>
{formatNumber(value)} {formatNumber(value)}
</div> </div>
); );
@@ -143,9 +196,13 @@ const SalesReportTable = ({
header: 'AVG (Kg)', header: 'AVG (Kg)',
cell: (props) => { cell: (props) => {
const value = props.getValue() as number; const value = props.getValue() as number;
const isSummary = props.row.id === 'summary'; const isFooter = '_isFooter' in props.row.original;
return ( return (
<div className={isSummary ? 'text-right' : ''}> <div
className={
isFooter ? 'text-left font-semibold text-gray-900' : 'text-left'
}
>
{formatNumber(value)} {formatNumber(value)}
</div> </div>
); );
@@ -157,7 +214,18 @@ const SalesReportTable = ({
header: 'Harga Mitra (Rp)', header: 'Harga Mitra (Rp)',
cell: (props) => { cell: (props) => {
const value = props.getValue() as number; const value = props.getValue() as number;
return <div className='text-right'>{formatCurrency(value)}</div>; const isFooter = '_isFooter' in props.row.original;
return (
<div
className={
isFooter
? 'text-right font-semibold text-gray-900'
: 'text-right'
}
>
{formatCurrency(value)}
</div>
);
}, },
}, },
{ {
@@ -166,7 +234,18 @@ const SalesReportTable = ({
header: 'Total Mitra (Rp)', header: 'Total Mitra (Rp)',
cell: (props) => { cell: (props) => {
const value = props.getValue() as number; const value = props.getValue() as number;
return <div className='text-right'>{formatCurrency(value)}</div>; const isFooter = '_isFooter' in props.row.original;
return (
<div
className={
isFooter
? 'text-right font-semibold text-gray-900'
: 'text-right'
}
>
{formatCurrency(value)}
</div>
);
}, },
}, },
{ {
@@ -175,7 +254,18 @@ const SalesReportTable = ({
header: 'Harga Act (Rp)', header: 'Harga Act (Rp)',
cell: (props) => { cell: (props) => {
const value = props.getValue() as number; const value = props.getValue() as number;
return <div className='text-right'>{formatCurrency(value)}</div>; const isFooter = '_isFooter' in props.row.original;
return (
<div
className={
isFooter
? 'text-right font-semibold text-gray-900'
: 'text-right'
}
>
{formatCurrency(value)}
</div>
);
}, },
}, },
{ {
@@ -184,7 +274,18 @@ const SalesReportTable = ({
header: 'Total Act (Rp)', header: 'Total Act (Rp)',
cell: (props) => { cell: (props) => {
const value = props.getValue() as number; const value = props.getValue() as number;
return <div className='text-right'>{formatCurrency(value)}</div>; const isFooter = '_isFooter' in props.row.original;
return (
<div
className={
isFooter
? 'text-right font-semibold text-gray-900'
: 'text-right'
}
>
{formatCurrency(value)}
</div>
);
}, },
}, },
{ {
@@ -192,6 +293,8 @@ const SalesReportTable = ({
accessorKey: 'kandang', accessorKey: 'kandang',
header: 'Kandang', header: 'Kandang',
cell: (props) => { cell: (props) => {
const isFooter = '_isFooter' in props.row.original;
if (isFooter) return null;
const kandang = props.getValue() as Kandang; const kandang = props.getValue() as Kandang;
return kandang?.name || '-'; return kandang?.name || '-';
}, },
@@ -201,6 +304,9 @@ const SalesReportTable = ({
accessorKey: 'payment_status', accessorKey: 'payment_status',
header: 'Status Pembayaran', header: 'Status Pembayaran',
cell: (props) => { cell: (props) => {
const isFooter = '_isFooter' in props.row.original;
if (isFooter) return null;
const status = props.getValue() as string; const status = props.getValue() as string;
const getStatusColor = (status: string) => { const getStatusColor = (status: string) => {
if (!status) return 'neutral'; if (!status) return 'neutral';
@@ -239,43 +345,8 @@ const SalesReportTable = ({
<Table <Table
data={salesData} data={salesData}
columns={salesColumns} columns={salesColumns}
footerData={footerData}
renderFooter={salesData.length > 0} renderFooter={salesData.length > 0}
footerContent={
<tfoot>
<tr className='bg-gray-100 font-semibold border border-gray-200'>
<td className='px-4 py-3 text-xs text-gray-900 whitespace-nowrap '>
Total Penjualan
</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 '>
{formatNumber(totals.totalQuantity)}
</td>
<td className='px-4 py-3 text-xs text-gray-900 whitespace-nowrap '>
{formatNumber(totals.totalWeight)}
</td>
<td className='px-4 py-3 text-xs text-gray-900 whitespace-nowrap '>
{formatNumber(totals.avgWeight)}
</td>
<td className='px-4 py-3 text-xs text-gray-900 whitespace-nowrap text-right '>
{formatCurrency(totals.avgPricePartner)}
</td>
<td className='px-4 py-3 text-xs text-gray-900 whitespace-nowrap text-right '>
{formatCurrency(totals.totalPartner)}
</td>
<td className='px-4 py-3 text-xs text-gray-900 whitespace-nowrap text-right '>
{formatCurrency(totals.avgPricePartner)}
</td>
<td className='px-4 py-3 text-xs text-gray-900 whitespace-nowrap text-right '>
{formatCurrency(totals.totalPartner)}
</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>
</tr>
</tfoot>
}
className={{ className={{
tableWrapperClassName: 'overflow-x-auto', tableWrapperClassName: 'overflow-x-auto',
tableClassName: 'w-full table-auto text-sm', tableClassName: 'w-full table-auto text-sm',
@@ -286,6 +357,11 @@ const SalesReportTable = ({
'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:
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
tableFooterClassName:
'bg-gray-100 font-semibold border border-gray-200',
footerRowClassName: 'border-t-2 border-gray-300',
footerColumnClassName:
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
}} }}
/> />
</Card> </Card>