refactor(FE-208,212): implement grouping of goods receipt items and enhance table rendering in PurchaseOrderDetail

This commit is contained in:
rstubryan
2025-11-19 10:50:53 +07:00
parent b520b4ee54
commit af5dfa9292
@@ -106,6 +106,49 @@ const PurchaseOrderDetail = ({
return purchaseOrderItems.filter((item) => item.received_date); return purchaseOrderItems.filter((item) => item.received_date);
}, [purchaseOrderItems]); }, [purchaseOrderItems]);
const groupedGoodsReceiptItems = useMemo(() => {
const uniqueProducts = Array.from(
new Map(
goodsReceiptItems.map((item) => [item.product?.id, item])
).values()
);
return uniqueProducts.map((item, index) => {
const productGroupItems = goodsReceiptItems.filter(
(groupItem) => groupItem.product?.id === item.product?.id
);
const totalQty = productGroupItems.reduce(
(sum, item) => sum + (item.total_qty || 0),
0
);
const receivedQty = productGroupItems.reduce(
(sum, item) => sum + (item.sub_qty || 0),
0
);
const unreceivedQty = totalQty - receivedQty;
const nominalReceived = productGroupItems.reduce(
(sum, item) => sum + (item.sub_qty || 0) * (item.price || 0),
0
);
const nominalUnreceived = productGroupItems.reduce(
(sum, item) => sum + unreceivedQty * (item.price || 0),
0
);
return {
productIndex: index + 1,
productName: item.product?.name || 'Unknown Product',
productGroupItems,
totalQty,
receivedQty,
unreceivedQty,
nominalReceived,
nominalUnreceived,
};
});
}, [goodsReceiptItems]);
const canUpdatePurchaseItems = useMemo(() => { const canUpdatePurchaseItems = useMemo(() => {
if (!initialValues?.approval) return false; if (!initialValues?.approval) return false;
@@ -337,84 +380,85 @@ const PurchaseOrderDetail = ({
const goodsReceiptColumns: ColumnDef<PurchaseItem>[] = [ const goodsReceiptColumns: ColumnDef<PurchaseItem>[] = [
{ {
header: 'Header Placeholder untuk tiap Produk Penerimaan Barang', header: 'Tanggal Penerimaan',
columns: [ accessorKey: 'received_date',
{ cell: (props) =>
header: 'No', props.row.original.received_date
cell: (props) => props.row.index + 1, ? formatDate(props.row.original.received_date, 'DD MMM YYYY')
}, : '-',
{ },
accessorKey: 'received_date', {
header: 'Tanggal Penerimaan', header: 'Gudang Tujuan',
cell: (props) => accessorKey: 'warehouse.name',
props.row.original.received_date cell: (props) => {
? formatDate(props.row.original.received_date, 'DD MMM YYYY') const warehouse = props.row.original.warehouse;
: '-', return warehouse?.name || '-';
}, },
{ },
accessorKey: 'warehouse.name', {
header: 'Gudang Tujuan', header: 'No. Surat Jalan',
cell: (props) => { accessorKey: 'travel_number',
const warehouse = props.row.original.warehouse; cell: (props) => props.row.original.travel_number || '-',
return warehouse?.name || '-'; },
}, {
}, header: 'Dokumen Surat Jalan',
{ accessorKey: 'travel_document_path',
accessorKey: 'travel_number', cell: (props) => {
header: 'No. Surat Jalan', const documentPath = props.row.original.travel_document_path;
cell: (props) => props.row.original.travel_number || '-', return documentPath ? (
}, <Button
{ color='primary'
accessorKey: 'travel_document_path', className='w-fit min-w-32 flex items-center justify-start gap-1 px-2 py-1 text-sm'
header: 'Dokumen Surat Jalan', href={documentPath}
cell: (props) => { target='_blank'
const documentPath = props.row.original.travel_document_path; rel='noopener noreferrer'
return documentPath ? ( >
<Button <Icon
color='primary' icon='material-symbols:file-open-outline'
className='w-fit min-w-32 flex items-center justify-start gap-1 px-2 py-1 text-sm' width={16}
href={documentPath} height={16}
target='_blank' />
rel='noopener noreferrer' Lihat Dokumen
> </Button>
<Icon ) : (
icon='material-symbols:file-open-outline' '-'
width={16} );
height={16} },
/> },
Lihat Dokumen {
</Button> header: 'No. Armada Pengangkut',
) : ( accessorKey: 'vehicle_number',
'-' cell: (props) => props.row.original.vehicle_number || '-',
); },
}, {
}, header: 'Jumlah Total',
{ accessorKey: 'total_qty',
accessorKey: 'vehicle_number', cell: (props) => formatNumber(props.getValue() as number),
header: 'No. Armada', },
cell: (props) => props.row.original.vehicle_number || '-', {
}, header: 'Jumlah Diterima',
{ accessorKey: 'sub_qty',
accessorKey: 'total_qty', cell: (props) => formatNumber(props.getValue() as number),
header: 'Jumlah Total', },
cell: (props) => formatNumber(props.getValue() as number), {
}, header: 'Jumlah Retur',
{ accessorKey: 'return_qty',
accessorKey: 'sub_qty', cell: (props) => formatNumber((props.getValue() as number) || 0),
header: 'Jumlah Diterima', },
cell: (props) => formatNumber(props.getValue() as number), {
}, header: 'Ekspedisi',
{ accessorKey: 'expedition_name',
accessorKey: 'transport_per_item', cell: (props) => '-',
header: 'Transport /Item', },
cell: (props) => formatCurrency(props.getValue() as number), {
}, header: 'Transport /Item',
{ accessorKey: 'transport_per_item',
accessorKey: 'transport_total', cell: (props) => formatCurrency(props.getValue() as number),
header: 'Transport Total', },
cell: (props) => formatCurrency(props.getValue() as number), {
}, header: 'Transport Total',
], accessorKey: 'transport_total',
cell: (props) => formatCurrency(props.getValue() as number),
}, },
]; ];
@@ -732,24 +776,47 @@ const PurchaseOrderDetail = ({
</div> </div>
<div className='rounded-lg border border-gray-200 overflow-hidden'> <div className='rounded-lg border border-gray-200 overflow-hidden'>
<div className='overflow-x-auto'> <div className='overflow-x-auto'>
<Table<PurchaseItem> {groupedGoodsReceiptItems.length > 0 ? (
data={goodsReceiptItems} <div className='space-y-8'>
columns={goodsReceiptColumns} {groupedGoodsReceiptItems.map((productData) => (
isLoading={false} <div
className={{ key={productData.productIndex}
containerClassName: 'm-0', className='border border-gray-200 rounded-lg overflow-hidden'
tableWrapperClassName: 'overflow-x-auto', >
tableClassName: 'w-full table-auto', {/* Product Header */}
headerRowClassName: 'bg-gray-50 border-b border-gray-200', <div className='font-semibold text-gray-900 bg-gray-100 px-6 py-4 text-lg'>
headerColumnClassName: {productData.productIndex}.{' '}
'px-4 py-3 text-sm font-semibold text-gray-700 text-left whitespace-nowrap', {productData.productName.toUpperCase()}
bodyRowClassName: </div>
'border-b border-gray-100 hover:bg-gray-50 transition-colors',
bodyColumnClassName: {/* Product Table */}
'px-4 py-3 text-sm text-gray-900 whitespace-nowrap', <Table<PurchaseItem>
paginationClassName: 'hidden', data={productData.productGroupItems}
}} columns={goodsReceiptColumns}
/> isLoading={false}
className={{
containerClassName: 'm-0',
tableWrapperClassName: 'overflow-x-auto',
tableClassName: 'w-full table-auto',
headerRowClassName:
'bg-gray-50 border-b border-gray-200',
headerColumnClassName:
'px-4 py-3 text-sm font-semibold text-gray-700 text-left whitespace-nowrap',
bodyRowClassName:
'border-b border-gray-100 hover:bg-gray-50 transition-colors',
bodyColumnClassName:
'px-4 py-3 text-sm text-gray-900 whitespace-nowrap',
paginationClassName: 'hidden',
}}
/>
</div>
))}
</div>
) : (
<div className='text-center py-8 text-gray-500'>
Tidak ada data penerimaan barang
</div>
)}
</div> </div>
</div> </div>
</div> </div>