diff --git a/src/components/Table.tsx b/src/components/Table.tsx index 970c5bc1..9feb33e2 100644 --- a/src/components/Table.tsx +++ b/src/components/Table.tsx @@ -14,6 +14,7 @@ import { SortingState, OnChangeFn, Row, + HeaderContext, } from '@tanstack/react-table'; import { rankItem } from '@tanstack/match-sorter-utils'; import { Icon } from '@iconify/react'; @@ -31,6 +32,9 @@ interface TableClassNames { tableBodyClassName?: string; bodyRowClassName?: string; bodyColumnClassName?: string; + tableFooterClassName?: string; + footerRowClassName?: string; + footerColumnClassName?: string; paginationClassName?: string; } @@ -53,6 +57,7 @@ export interface TableProps { rowSelection?: Record; setRowSelection?: OnChangeFn>; enableRowSelection?: boolean | ((row: Row) => boolean); + renderFooter?: boolean; withCheckbox?: boolean; rowOptions?: number[]; } @@ -67,18 +72,22 @@ const emptyContentDefaultValue = ( ); -const TABLE_DEFAULT_STYLING = { +export const TABLE_DEFAULT_STYLING = { containerClassName: 'w-full mb-20', tableWrapperClassName: 'overflow-x-auto border border-solid border-base-content/10 rounded-lg', tableClassName: 'font-inter w-full table-auto text-sm font-medium', tableHeaderClassName: '', headerRowClassName: '', - headerColumnClassName: 'px-4 py-3 text-base-content/50', + headerColumnClassName: + 'px-4 py-3 border-base-content/10 text-base-content/50', tableBodyClassName: '', - bodyRowClassName: 'border-t border-t-base-content/10', + bodyRowClassName: 'border-t border-base-content/10', bodyColumnClassName: 'px-4 py-3 text-base-content', paginationClassName: '', + tableFooterClassName: 'font-semibold border-base-content/10', + footerRowClassName: 'bg-base-200 border-t-2 border-base-content/10', + footerColumnClassName: 'p-4 text-base-content whitespace-nowrap', }; const Table = ({ @@ -100,6 +109,7 @@ const Table = ({ rowSelection, setRowSelection, enableRowSelection, + renderFooter = false, withCheckbox = false, rowOptions = [10, 20, 50, 100], }: TableProps) => { @@ -214,58 +224,82 @@ const Table = ({ key={headerGroup.id} className={tableClassNames.headerRowClassName} > - {headerGroup.headers.map((header) => ( - -
- {flexRender( - header.column.columnDef.header, - header.getContext() + {headerGroup.headers.map((header) => { + const columnRelativeDepth = + header.depth - header.column.depth; + if ( + !header.isPlaceholder && + columnRelativeDepth > 1 && + header.id === header.column.id + ) { + return null; + } + let rowSpan = 1; + if (header.isPlaceholder) { + const leafs = header.getLeafHeaders(); + rowSpan = leafs[leafs.length - 1].depth - header.depth; + } + return ( + 1, + }, + tableClassNames.headerColumnClassName )} + > +
1, + })} + > + {flexRender( + header.column.columnDef.header, + header.getContext() + )} - {header.column.getCanSort() && ( -
- - -
- )} -
- - ))} + {header.column.getCanSort() && ( +
+ + +
+ )} +
+ + ); + })} ))} @@ -290,6 +324,28 @@ const Table = ({ ))} + + {renderFooter && ( + + {table.getAllLeafColumns().map((column) => ( + + {column.columnDef.footer && + flexRender(column.columnDef.footer, { + column, + header: column.columnDef, + table, + } as HeaderContext)} + + ))} + + )} + diff --git a/src/components/pages/closing/sale/SalesReportTable.tsx b/src/components/pages/closing/sale/SalesReportTable.tsx index f0810f15..e509eb7d 100644 --- a/src/components/pages/closing/sale/SalesReportTable.tsx +++ b/src/components/pages/closing/sale/SalesReportTable.tsx @@ -6,7 +6,7 @@ import Table from '@/components/Table'; import Card from '@/components/Card'; import Badge from '@/components/Badge'; import { formatCurrency, formatNumber, formatDate } from '@/lib/helper'; -import { BaseClosingSales, BaseSales } from '@/types/api/closing/closing'; +import { BaseClosingSales, BaseSales } from '@/types/api/closing'; import { Product } from '@/types/api/master-data/product'; import { Customer } from '@/types/api/master-data/customer'; import { Kandang } from '@/types/api/master-data/kandang'; @@ -16,10 +16,6 @@ interface SalesReportTableProps { initialValues?: BaseClosingSales; } -interface FooterSalesRow extends BaseSales { - _isFooter: true; -} - const SalesReportTable = ({ type = 'detail', initialValues, @@ -72,29 +68,6 @@ const SalesReportTable = ({ }; }, [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[] = useMemo( () => [ { @@ -102,43 +75,30 @@ const SalesReportTable = ({ accessorKey: 'realization_date', header: 'Tanggal Realisasi', cell: (props) => { - const isFooter = '_isFooter' in props.row.original; - if (isFooter) { - return ( -
- {props.row.original.realization_date} -
- ); - } const date = props.row.original.realization_date; return date ? formatDate(date, 'DD MMM YYYY') : '-'; }, + footer: () => ( +
Total Penjualan
+ ), }, { id: 'age', accessorKey: 'age', header: 'Umur', - cell: (props) => { - const isFooter = '_isFooter' in props.row.original; - return isFooter ? null : props.getValue() || '-'; - }, + cell: (props) => props.getValue() || '-', }, { id: 'do_number', accessorKey: 'do_number', header: 'No. DO', - cell: (props) => { - const isFooter = '_isFooter' in props.row.original; - return isFooter ? null : props.getValue() || '-'; - }, + cell: (props) => props.getValue() || '-', }, { id: 'product', accessorKey: 'product', header: 'Produk', cell: (props) => { - const isFooter = '_isFooter' in props.row.original; - if (isFooter) return null; const product = props.getValue() as Product; return product?.name || '-'; }, @@ -148,47 +108,43 @@ const SalesReportTable = ({ accessorKey: 'customer', header: 'Customer', cell: (props) => { - const isFooter = '_isFooter' in props.row.original; - if (isFooter) return null; const customer = props.getValue() as Customer; return customer?.name || '-'; }, }, { - id: 'qty', - accessorKey: 'qty', - header: 'Kuantitas', - cell: (props) => { - const value = props.getValue() as number; - const isFooter = '_isFooter' in props.row.original; - return ( -
- {formatNumber(value)} -
- ); - }, - }, - { - id: 'weight', - accessorKey: 'weight', - header: 'Kg', - cell: (props) => { - const value = props.getValue() as number; - const isFooter = '_isFooter' in props.row.original; - return ( -
- {formatNumber(value)} -
- ); - }, + id: 'jumlah', + header: 'Jumlah', + columns: [ + { + id: 'qty', + accessorKey: 'qty', + header: 'Kuantitas', + cell: (props) => { + const value = props.getValue() as number; + return
{formatNumber(value)}
; + }, + footer: () => ( +
+ {formatNumber(totals.totalQuantity)} +
+ ), + }, + { + id: 'weight', + accessorKey: 'weight', + header: 'Kg', + cell: (props) => { + const value = props.getValue() as number; + return
{formatNumber(value)}
; + }, + footer: () => ( +
+ {formatNumber(totals.totalWeight)} +
+ ), + }, + ], }, { id: 'avg_weight', @@ -196,17 +152,13 @@ const SalesReportTable = ({ header: 'AVG (Kg)', cell: (props) => { const value = props.getValue() as number; - const isFooter = '_isFooter' in props.row.original; - return ( -
- {formatNumber(value)} -
- ); + return
{formatNumber(value)}
; }, + footer: () => ( +
+ {formatNumber(totals.avgWeight)} +
+ ), }, { id: 'price_partner', @@ -214,19 +166,13 @@ const SalesReportTable = ({ header: 'Harga Mitra (Rp)', cell: (props) => { const value = props.getValue() as number; - const isFooter = '_isFooter' in props.row.original; - return ( -
- {formatCurrency(value)} -
- ); + return
{formatCurrency(value)}
; }, + footer: () => ( +
+ {formatCurrency(totals.avgPricePartner)} +
+ ), }, { id: 'total_mitra', @@ -234,19 +180,13 @@ const SalesReportTable = ({ header: 'Total Mitra (Rp)', cell: (props) => { const value = props.getValue() as number; - const isFooter = '_isFooter' in props.row.original; - return ( -
- {formatCurrency(value)} -
- ); + return
{formatCurrency(value)}
; }, + footer: () => ( +
+ {formatCurrency(totals.totalPartner)} +
+ ), }, { id: 'price_act', @@ -254,18 +194,7 @@ const SalesReportTable = ({ header: 'Harga Act (Rp)', cell: (props) => { const value = props.getValue() as number; - const isFooter = '_isFooter' in props.row.original; - return ( -
- {formatCurrency(value)} -
- ); + return
{formatCurrency(value)}
; }, }, { @@ -274,18 +203,7 @@ const SalesReportTable = ({ header: 'Total Act (Rp)', cell: (props) => { const value = props.getValue() as number; - const isFooter = '_isFooter' in props.row.original; - return ( -
- {formatCurrency(value)} -
- ); + return
{formatCurrency(value)}
; }, }, { @@ -293,8 +211,6 @@ const SalesReportTable = ({ accessorKey: 'kandang', header: 'Kandang', cell: (props) => { - const isFooter = '_isFooter' in props.row.original; - if (isFooter) return null; const kandang = props.getValue() as Kandang; return kandang?.name || '-'; }, @@ -304,9 +220,6 @@ const SalesReportTable = ({ accessorKey: 'payment_status', header: 'Status Pembayaran', cell: (props) => { - const isFooter = '_isFooter' in props.row.original; - if (isFooter) return null; - const status = props.getValue() as string; const getStatusColor = (status: string) => { if (!status) return 'neutral'; @@ -345,16 +258,14 @@ const SalesReportTable = ({ 0} className={{ tableWrapperClassName: 'overflow-x-auto', tableClassName: 'w-full table-auto text-sm', - 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', + 'px-4 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end whitespace-nowrap border-l border-l-gray-200 border-r border-r-gray-200 border-t border-t-gray-200 border-gray-200 border-b-0', 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-gray-200 first:border-t first:border-t-gray-200 border-l border-l-gray-200 border-r border-r-gray-200', bodyColumnClassName: 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', tableFooterClassName: diff --git a/src/types/api/closing.d.ts b/src/types/api/closing.d.ts index 3f7ba816..95b2f57f 100644 --- a/src/types/api/closing.d.ts +++ b/src/types/api/closing.d.ts @@ -1,9 +1,34 @@ import { Area } from '@/types/api/master-data/area'; import { Fcr } from '@/types/api/master-data/fcr'; import { Flock } from '@/types/api/master-data/flock'; -import { Kandang } from '@/types/api/master-data/kandang'; import { Location } from '@/types/api/master-data/location'; -import { BaseApproval, BaseMetadata } from '@/types/api/api-general'; +import { Kandang } from '@/types/api/master-data/kandang'; +import { Product } from '@type/api/master-data/product'; +import { Customer } from '@type/api/master-data/customer'; +import { BaseMetadata } from '@/types/api/api-general'; + +export type BaseSales = { + id: number; + realization_date: string; + age: number; + do_number: string; + product: Product; + customer: Customer; + qty: number; + weight: number; + avg_weight: number; + price: number; + total_price: number; + kandang: Kandang; + payment_status: string; +}; + +export type BaseClosingSales = { + project_type: string; + flock_id: number; + period: number; + sales: BaseSales[]; +}; export type BaseClosing = { id: number; @@ -53,3 +78,4 @@ export type ClosingIncomingSapronak = { }; export type ClosingOutgoingSapronak = ClosingIncomingSapronak; +export type ClosingSales = BaseMetadata & BaseClosingSales;