mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-25 07:45:47 +00:00
feat(FE-361): Add logistic-stock report page and table footer
This commit is contained in:
@@ -0,0 +1,11 @@
|
|||||||
|
import SuspenseHelper from '@/components/helper/SuspenseHelper';
|
||||||
|
|
||||||
|
const Layout = ({
|
||||||
|
children,
|
||||||
|
}: Readonly<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
}>) => {
|
||||||
|
return <SuspenseHelper>{children}</SuspenseHelper>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Layout;
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import Tabs from '@/components/Tabs';
|
||||||
|
import PurchasesPerSupplierTab from '@/components/pages/report/logistic-stock/PurchasesPerSupplierTab';
|
||||||
|
|
||||||
|
const LogisticStock = () => {
|
||||||
|
const tabs = [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
label: 'Rekapitulasi Pembelian Per Supplier',
|
||||||
|
content: <PurchasesPerSupplierTab />,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className='w-full p-4'>
|
||||||
|
<Tabs tabs={tabs} variant='boxed' />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LogisticStock;
|
||||||
@@ -31,6 +31,9 @@ interface TableClassNames {
|
|||||||
tableBodyClassName?: string;
|
tableBodyClassName?: string;
|
||||||
bodyRowClassName?: string;
|
bodyRowClassName?: string;
|
||||||
bodyColumnClassName?: string;
|
bodyColumnClassName?: string;
|
||||||
|
tableFooterClassName?: string;
|
||||||
|
footerRowClassName?: string;
|
||||||
|
footerColumnClassName?: string;
|
||||||
paginationClassName?: string;
|
paginationClassName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,6 +55,9 @@ export interface TableProps<TData extends object> {
|
|||||||
rowSelection?: Record<string, boolean>;
|
rowSelection?: Record<string, boolean>;
|
||||||
setRowSelection?: OnChangeFn<Record<string, boolean>>;
|
setRowSelection?: OnChangeFn<Record<string, boolean>>;
|
||||||
enableRowSelection?: boolean | ((row: Row<TData>) => boolean);
|
enableRowSelection?: boolean | ((row: Row<TData>) => boolean);
|
||||||
|
renderFooter?: boolean;
|
||||||
|
footerContent?: ReactNode;
|
||||||
|
footerData?: TData[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const DUMMY_SKELETON_DATA = [{}, {}, {}, {}, {}];
|
const DUMMY_SKELETON_DATA = [{}, {}, {}, {}, {}];
|
||||||
@@ -84,6 +90,9 @@ const Table = <TData extends object>({
|
|||||||
tableBodyClassName: '',
|
tableBodyClassName: '',
|
||||||
bodyRowClassName: '',
|
bodyRowClassName: '',
|
||||||
bodyColumnClassName: '',
|
bodyColumnClassName: '',
|
||||||
|
tableFooterClassName: '',
|
||||||
|
footerRowClassName: '',
|
||||||
|
footerColumnClassName: '',
|
||||||
paginationClassName: '',
|
paginationClassName: '',
|
||||||
},
|
},
|
||||||
emptyContent = emptyContentDefaultValue,
|
emptyContent = emptyContentDefaultValue,
|
||||||
@@ -93,6 +102,9 @@ const Table = <TData extends object>({
|
|||||||
rowSelection,
|
rowSelection,
|
||||||
setRowSelection,
|
setRowSelection,
|
||||||
enableRowSelection,
|
enableRowSelection,
|
||||||
|
renderFooter = false,
|
||||||
|
footerContent,
|
||||||
|
footerData = [],
|
||||||
}: TableProps<TData>) => {
|
}: TableProps<TData>) => {
|
||||||
const isServerSideTable =
|
const isServerSideTable =
|
||||||
totalItems !== undefined &&
|
totalItems !== undefined &&
|
||||||
@@ -160,6 +172,14 @@ const Table = <TData extends object>({
|
|||||||
const table = useReactTable(tableOptions);
|
const table = useReactTable(tableOptions);
|
||||||
const { setPageSize } = table;
|
const { setPageSize } = table;
|
||||||
|
|
||||||
|
const footerTableOptions: TableOptions<TData> = {
|
||||||
|
columns,
|
||||||
|
data: footerData,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const footerTable = useReactTable(footerTableOptions);
|
||||||
|
|
||||||
const prevPageClickHandler = () => {
|
const prevPageClickHandler = () => {
|
||||||
table.previousPage();
|
table.previousPage();
|
||||||
|
|
||||||
@@ -262,6 +282,26 @@ const Table = <TData extends object>({
|
|||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
<tfoot className={cn(className.tableFooterClassName)}>
|
||||||
|
{renderFooter &&
|
||||||
|
(footerData && footerData.length > 0
|
||||||
|
? footerTable.getRowModel().rows.map((row) => (
|
||||||
|
<tr key={row.id} className={className.footerRowClassName}>
|
||||||
|
{row.getVisibleCells().map((cell) => (
|
||||||
|
<td
|
||||||
|
key={cell.id}
|
||||||
|
className={className.footerColumnClassName}
|
||||||
|
>
|
||||||
|
{flexRender(
|
||||||
|
cell.column.columnDef.cell,
|
||||||
|
cell.getContext()
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
))
|
||||||
|
: footerContent)}
|
||||||
|
</tfoot>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,521 @@
|
|||||||
|
import { useState, useMemo } from 'react';
|
||||||
|
import Card from '@/components/Card';
|
||||||
|
import SelectInput, { useSelect } from '@/components/input/SelectInput';
|
||||||
|
import { AreaApi } from '@/services/api/master-data';
|
||||||
|
import { SupplierApi } from '@/services/api/master-data';
|
||||||
|
import { ProductApi } from '@/services/api/master-data';
|
||||||
|
import Table from '@/components/Table';
|
||||||
|
import { ColumnDef } from '@tanstack/react-table';
|
||||||
|
import { formatCurrency, formatDate } from '@/lib/helper';
|
||||||
|
|
||||||
|
const PurchasesPerSupplierTab = () => {
|
||||||
|
const [selectedArea, setSelectedArea] = useState<number | null>(null);
|
||||||
|
const [selectedSupplier, setSelectedSupplier] = useState<number | null>(null);
|
||||||
|
const [selectedProduct, setSelectedProduct] = useState<number | null>(null);
|
||||||
|
|
||||||
|
const { options: areaOptions, isLoadingOptions: isLoadingAreas } = useSelect(
|
||||||
|
AreaApi.basePath,
|
||||||
|
'id',
|
||||||
|
'name',
|
||||||
|
'search'
|
||||||
|
);
|
||||||
|
|
||||||
|
const { options: supplierOptions, isLoadingOptions: isLoadingSuppliers } =
|
||||||
|
useSelect(SupplierApi.basePath, 'id', 'name', 'search');
|
||||||
|
|
||||||
|
const { options: productOptions, isLoadingOptions: isLoadingProducts } =
|
||||||
|
useSelect(ProductApi.basePath, 'id', 'name', 'search');
|
||||||
|
|
||||||
|
const data = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
supplier: 'PT. RAJAWALI MITRA PAKANINDO',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
received_date: '2025-09-26',
|
||||||
|
po_date: '2025-09-30',
|
||||||
|
po_number: 'PO-MBU-00670',
|
||||||
|
product_name: 'KAPORIT BESAR (TCCA) 200 GR @1 KG',
|
||||||
|
destination_warehouse: 'GUDANG CIMARAGAS 1',
|
||||||
|
qty: 5,
|
||||||
|
price: 45000,
|
||||||
|
purchase_amount: 225000,
|
||||||
|
transport: 0,
|
||||||
|
value_transport: 0,
|
||||||
|
total: 225000,
|
||||||
|
expedition_vendor_name: 'PT. RAJAWALI MITRA PAKANINDO',
|
||||||
|
travel_number: '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
received_date: '2025-09-30',
|
||||||
|
po_date: '2025-09-30',
|
||||||
|
po_number: 'PO-MBU-00670',
|
||||||
|
product_name: 'KAPORIT BESAR (TCCA) 200 GR @1 KG',
|
||||||
|
destination_warehouse: 'GUDANG CIMARAGAS 2',
|
||||||
|
qty: 5,
|
||||||
|
price: 45000,
|
||||||
|
purchase_amount: 225000,
|
||||||
|
transport: 0,
|
||||||
|
value_transport: 0,
|
||||||
|
total: 225000,
|
||||||
|
expedition_vendor_name: 'PT. RAJAWALI MITRA PAKANINDO',
|
||||||
|
travel_number: '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
received_date: '2025-09-30',
|
||||||
|
po_date: '2025-09-30',
|
||||||
|
po_number: 'PO-MBU-00670',
|
||||||
|
product_name: 'KAPORIT BESAR (TCCA) 200 GR @1 KG',
|
||||||
|
destination_warehouse: 'GUDANG CIMARAGAS 3',
|
||||||
|
qty: 5,
|
||||||
|
price: 45000,
|
||||||
|
purchase_amount: 225000,
|
||||||
|
transport: 0,
|
||||||
|
value_transport: 0,
|
||||||
|
total: 225000,
|
||||||
|
expedition_vendor_name: 'PT. RAJAWALI MITRA PAKANINDO',
|
||||||
|
travel_number: '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
received_date: '2025-09-30',
|
||||||
|
po_date: '2025-09-30',
|
||||||
|
po_number: 'PO-MBU-00670',
|
||||||
|
product_name: 'KAPORIT BESAR (TCCA) 200 GR @1 KG',
|
||||||
|
destination_warehouse: 'GUDANG CIMARAGAS 4',
|
||||||
|
qty: 5,
|
||||||
|
price: 45000,
|
||||||
|
purchase_amount: 225000,
|
||||||
|
transport: 0,
|
||||||
|
value_transport: 0,
|
||||||
|
total: 225000,
|
||||||
|
expedition_vendor_name: 'PT. RAJAWALI MITRA PAKANINDO',
|
||||||
|
travel_number: '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
received_date: '2025-09-04',
|
||||||
|
po_date: '2025-09-30',
|
||||||
|
po_number: 'PO-MBU-00606',
|
||||||
|
product_name: 'DESINFEKTAN C 100 @20L',
|
||||||
|
destination_warehouse: 'GUDANG MANDALAWANGI 1',
|
||||||
|
qty: 1,
|
||||||
|
price: 800000,
|
||||||
|
purchase_amount: 800000,
|
||||||
|
transport: 0,
|
||||||
|
value_transport: 0,
|
||||||
|
total: 800000,
|
||||||
|
expedition_vendor_name: 'PT. RAJAWALI MITRA PAKANINDO',
|
||||||
|
travel_number: '-',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
supplier: 'Supplier B',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
received_date: '2024-01-18',
|
||||||
|
po_date: '2024-01-13',
|
||||||
|
po_number: 'PO-2024-004',
|
||||||
|
product_name: 'Produk D',
|
||||||
|
destination_warehouse: 'Gudang Pusat',
|
||||||
|
qty: 200,
|
||||||
|
price: 25000,
|
||||||
|
purchase_amount: 5000000,
|
||||||
|
transport: 200000,
|
||||||
|
value_transport: 200000,
|
||||||
|
total: 5200000,
|
||||||
|
expedition_vendor_name: 'Ekspedisi GHI',
|
||||||
|
travel_number: 'SJ-004',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
supplier: 'Supplier C',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: 7,
|
||||||
|
received_date: '2024-01-20',
|
||||||
|
po_date: '2024-01-15',
|
||||||
|
po_number: 'PO-2024-006',
|
||||||
|
product_name: 'Produk F',
|
||||||
|
destination_warehouse: 'Gudang Cabang',
|
||||||
|
qty: 80,
|
||||||
|
price: 55000,
|
||||||
|
purchase_amount: 4400000,
|
||||||
|
transport: 80000,
|
||||||
|
value_transport: 80000,
|
||||||
|
total: 4480000,
|
||||||
|
expedition_vendor_name: 'Ekspedisi MNO',
|
||||||
|
travel_number: 'SJ-006',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
// TODO START
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const tableColumns: ColumnDef<any>[] = [
|
||||||
|
{
|
||||||
|
header: 'No',
|
||||||
|
accessorKey: 'no',
|
||||||
|
cell: (props) => {
|
||||||
|
const isFooter = '_isFooter' in props.row.original;
|
||||||
|
if (isFooter) {
|
||||||
|
return (
|
||||||
|
<div className='font-semibold text-gray-900'>
|
||||||
|
{props.row.original.no}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return props.row.index + 1;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Tanggal Terima',
|
||||||
|
accessorKey: 'received_date',
|
||||||
|
cell: (props) => {
|
||||||
|
const isFooter = '_isFooter' in props.row.original;
|
||||||
|
if (isFooter) {
|
||||||
|
return (
|
||||||
|
<div className='font-semibold text-gray-900'>
|
||||||
|
{props.row.original.received_date}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return formatDate(props.row.original.received_date, 'DD MMM YYYY');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Tanggal PO',
|
||||||
|
accessorKey: 'po_date',
|
||||||
|
cell: (props) => {
|
||||||
|
const isFooter = '_isFooter' in props.row.original;
|
||||||
|
if (isFooter) return null;
|
||||||
|
return formatDate(props.row.original.po_date, 'DD MMM YYYY');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'No. Referensi',
|
||||||
|
accessorKey: 'po_number',
|
||||||
|
cell: (props) => {
|
||||||
|
const isFooter = '_isFooter' in props.row.original;
|
||||||
|
if (isFooter) return null;
|
||||||
|
return props.row.original.po_number;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Nama Produk',
|
||||||
|
accessorKey: 'product_name',
|
||||||
|
cell: (props) => {
|
||||||
|
const isFooter = '_isFooter' in props.row.original;
|
||||||
|
if (isFooter) return null;
|
||||||
|
return props.row.original.product_name;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Tujuan',
|
||||||
|
accessorKey: 'destination_warehouse',
|
||||||
|
cell: (props) => {
|
||||||
|
const isFooter = '_isFooter' in props.row.original;
|
||||||
|
if (isFooter) return null;
|
||||||
|
return props.row.original.destination_warehouse;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'QTY',
|
||||||
|
accessorKey: 'qty',
|
||||||
|
cell: (props) => {
|
||||||
|
const value = props.getValue() as number;
|
||||||
|
const isFooter = '_isFooter' in props.row.original;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
isFooter ? 'text-right font-semibold text-gray-900' : 'text-right'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{value.toLocaleString()}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Harga Beli (Rp)',
|
||||||
|
accessorKey: 'price',
|
||||||
|
cell: (props) => {
|
||||||
|
const value = props.getValue() as number;
|
||||||
|
const isFooter = '_isFooter' in props.row.original;
|
||||||
|
if (isFooter) {
|
||||||
|
return (
|
||||||
|
<div className='text-right font-semibold text-gray-900'>
|
||||||
|
{formatCurrency(value)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className='text-right'>
|
||||||
|
{formatCurrency(props.row.original.price)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Value Harga Beli (Rp)',
|
||||||
|
accessorKey: 'purchase_amount',
|
||||||
|
cell: (props) => {
|
||||||
|
const value = props.getValue() as number;
|
||||||
|
const isFooter = '_isFooter' in props.row.original;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
isFooter ? 'text-right font-semibold text-gray-900' : 'text-right'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{formatCurrency(value)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Transport (Rp)',
|
||||||
|
accessorKey: 'transport',
|
||||||
|
cell: (props) => {
|
||||||
|
const value = props.getValue() as number;
|
||||||
|
const isFooter = '_isFooter' in props.row.original;
|
||||||
|
if (isFooter) {
|
||||||
|
return (
|
||||||
|
<div className='text-right font-semibold text-gray-900'>
|
||||||
|
{formatCurrency(value)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className='text-right'>
|
||||||
|
{formatCurrency(props.row.original.transport)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Value Transport (Rp)',
|
||||||
|
accessorKey: 'value_transport',
|
||||||
|
cell: (props) => {
|
||||||
|
const value = props.getValue() as number;
|
||||||
|
const isFooter = '_isFooter' in props.row.original;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
isFooter ? 'text-right font-semibold text-gray-900' : 'text-right'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{formatCurrency(value)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Jumlah (Rp)',
|
||||||
|
accessorKey: 'total',
|
||||||
|
cell: (props) => {
|
||||||
|
const value = props.getValue() as number;
|
||||||
|
const isFooter = '_isFooter' in props.row.original;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
isFooter ? 'text-right font-semibold text-gray-900' : 'text-right'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{formatCurrency(value)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Ekspedisi',
|
||||||
|
accessorKey: 'expedition_vendor_name',
|
||||||
|
cell: (props) => {
|
||||||
|
const isFooter = '_isFooter' in props.row.original;
|
||||||
|
if (isFooter) return null;
|
||||||
|
return props.row.original.expedition_vendor_name;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Surat Jalan',
|
||||||
|
accessorKey: 'travel_number',
|
||||||
|
cell: (props) => {
|
||||||
|
const isFooter = '_isFooter' in props.row.original;
|
||||||
|
if (isFooter) return null;
|
||||||
|
return props.row.original.travel_number;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Card
|
||||||
|
subtitle='Laporan > Rekapitulasi Pembelian Per Supplier'
|
||||||
|
className={{ wrapper: 'w-full' }}
|
||||||
|
>
|
||||||
|
<div className='grid grid-cols-1 md:grid-cols-3 gap-4'>
|
||||||
|
{/* TODO START */}
|
||||||
|
<SelectInput
|
||||||
|
label='Area'
|
||||||
|
placeholder='Pilih Area'
|
||||||
|
options={areaOptions}
|
||||||
|
value={
|
||||||
|
areaOptions.find((option) => option.value === selectedArea) ||
|
||||||
|
null
|
||||||
|
}
|
||||||
|
// @ts-expect-error TS2345
|
||||||
|
onChange={(val) => setSelectedArea(val?.value || null)}
|
||||||
|
isLoading={isLoadingAreas}
|
||||||
|
isClearable
|
||||||
|
/>
|
||||||
|
<SelectInput
|
||||||
|
label='Supplier'
|
||||||
|
placeholder='Pilih Supplier'
|
||||||
|
options={supplierOptions}
|
||||||
|
value={
|
||||||
|
supplierOptions.find(
|
||||||
|
(option) => option.value === selectedSupplier
|
||||||
|
) || null
|
||||||
|
}
|
||||||
|
// @ts-expect-error TS2345
|
||||||
|
onChange={(val) => setSelectedSupplier(val?.value || null)}
|
||||||
|
isLoading={isLoadingSuppliers}
|
||||||
|
isClearable
|
||||||
|
/>
|
||||||
|
<SelectInput
|
||||||
|
label='Produk'
|
||||||
|
placeholder='Pilih Produk'
|
||||||
|
options={productOptions}
|
||||||
|
value={
|
||||||
|
productOptions.find(
|
||||||
|
(option) => option.value === selectedProduct
|
||||||
|
) || null
|
||||||
|
}
|
||||||
|
// @ts-expect-error TS2345
|
||||||
|
onChange={(val) => setSelectedProduct(val?.value || null)}
|
||||||
|
isLoading={isLoadingProducts}
|
||||||
|
isClearable
|
||||||
|
/>
|
||||||
|
{/* TODO END */}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{data.length === 0 ? (
|
||||||
|
<div className='mt-6 text-center text-gray-500'>
|
||||||
|
Tidak ada data untuk ditampilkan.
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
data.map((supplier) => {
|
||||||
|
const totalQty = supplier.items.reduce(
|
||||||
|
(sum, item) => sum + (item.qty || 0),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
const totalPrice = supplier.items.reduce(
|
||||||
|
(sum, item) => sum + (item.price || 0),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
const totalPurchaseAmount = supplier.items.reduce(
|
||||||
|
(sum, item) => sum + (item.purchase_amount || 0),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
const totalTransport = supplier.items.reduce(
|
||||||
|
(sum, item) => sum + (item.transport || 0),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
const totalValueTransport = supplier.items.reduce(
|
||||||
|
(sum, item) => sum + (item.value_transport || 0),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
const totalJumlah = supplier.items.reduce(
|
||||||
|
(sum, item) => sum + (item.total || 0),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
const totals = {
|
||||||
|
totalQty,
|
||||||
|
totalPrice,
|
||||||
|
totalPurchaseAmount,
|
||||||
|
totalTransport,
|
||||||
|
totalValueTransport,
|
||||||
|
totalJumlah,
|
||||||
|
};
|
||||||
|
|
||||||
|
const footerData =
|
||||||
|
supplier.items.length > 0
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
id: -999,
|
||||||
|
no: 'Total',
|
||||||
|
received_date: '',
|
||||||
|
po_date: null,
|
||||||
|
po_number: null,
|
||||||
|
product_name: null,
|
||||||
|
destination_warehouse: null,
|
||||||
|
qty: totals.totalQty,
|
||||||
|
price: totals.totalPrice,
|
||||||
|
purchase_amount: totals.totalPurchaseAmount,
|
||||||
|
transport: totals.totalTransport,
|
||||||
|
value_transport: totals.totalValueTransport,
|
||||||
|
total: totals.totalJumlah,
|
||||||
|
expedition_vendor_name: null,
|
||||||
|
travel_number: null,
|
||||||
|
_isFooter: true,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const totalPurchase = totals.totalJumlah;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
key={supplier.id}
|
||||||
|
title={supplier.supplier}
|
||||||
|
subtitle={`Total Pembelian: ${formatCurrency(totalPurchase)}`}
|
||||||
|
className={{ wrapper: 'mt-6 w-full' }}
|
||||||
|
collapsible={true}
|
||||||
|
>
|
||||||
|
<Table
|
||||||
|
data={supplier.items}
|
||||||
|
columns={tableColumns}
|
||||||
|
pageSize={10}
|
||||||
|
footerData={footerData}
|
||||||
|
renderFooter={supplier.items.length > 0}
|
||||||
|
className={{
|
||||||
|
tableWrapperClassName: 'overflow-x-auto mt-4',
|
||||||
|
tableClassName: 'w-full table-auto text-sm',
|
||||||
|
headerRowClassName: 'border-b border-b-gray-200 bg-gray-50',
|
||||||
|
headerColumnClassName:
|
||||||
|
'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200',
|
||||||
|
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',
|
||||||
|
bodyColumnClassName:
|
||||||
|
'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',
|
||||||
|
paginationClassName: 'hidden',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PurchasesPerSupplierTab;
|
||||||
Reference in New Issue
Block a user