mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-25 07:45:47 +00:00
869 lines
28 KiB
TypeScript
869 lines
28 KiB
TypeScript
import { useState, useMemo, useCallback } from 'react';
|
|
import { ChangeEventHandler } from 'react';
|
|
import useSWR from 'swr';
|
|
import Card from '@/components/Card';
|
|
import SelectInput, {
|
|
useSelect,
|
|
OptionType,
|
|
} from '@/components/input/SelectInput';
|
|
import DateInput from '@/components/input/DateInput';
|
|
import { AreaApi } from '@/services/api/master-data';
|
|
import { SupplierApi } from '@/services/api/master-data';
|
|
import { ProductApi } from '@/services/api/master-data';
|
|
import { ProductCategoryApi } from '@/services/api/master-data';
|
|
import { LogisticApi } from '@/services/api/report/logistic-stock';
|
|
import Table from '@/components/Table';
|
|
import { ColumnDef } from '@tanstack/react-table';
|
|
import { formatCurrency, formatDate, formatNumber } from '@/lib/helper';
|
|
import {
|
|
LogisticPurchasePerSupplierReport,
|
|
LogisticPurchasePerSupplierSummary,
|
|
} from '@/types/api/report/logistic-stock';
|
|
import { isResponseSuccess } from '@/lib/api-helper';
|
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
|
import Pagination from '@/components/Pagination';
|
|
import Button from '@/components/Button';
|
|
import Dropdown from '@/components/Dropdown';
|
|
import MenuItem from '@/components/menu/MenuItem';
|
|
import Menu from '@/components/menu/Menu';
|
|
import { generatePurchasesPerSupplierPDF } from '@/components/pages/report/logistic-stock/export/PurchasesPerSupplierExportPDF';
|
|
import { generatePurchasesPerSupplierExcel } from '@/components/pages/report/logistic-stock/export/PurchasesPerSupplierExportXLSX';
|
|
import toast from 'react-hot-toast';
|
|
import { Icon } from '@iconify/react';
|
|
|
|
const PurchasesPerSupplierTab = () => {
|
|
// ===== STATE MANAGEMENT =====
|
|
const [isPdfExportLoading, setIsPdfExportLoading] = useState(false);
|
|
const [isExcelExportLoading, setIsExcelExportLoading] = useState(false);
|
|
const isAnyExportLoading = isPdfExportLoading || isExcelExportLoading;
|
|
|
|
// ===== PAGINATION STATE =====
|
|
const [currentPage, setCurrentPage] = useState(1);
|
|
const [pageSize, setPageSize] = useState(10);
|
|
|
|
// ===== SUBMISSION STATE =====
|
|
const [isSubmitted, setIsSubmitted] = useState(false);
|
|
|
|
// ===== TABLE FILTER STATE =====
|
|
const { state: tableFilterState, updateFilter } = useTableFilter({
|
|
initial: {
|
|
area_id: [] as string[],
|
|
supplier_id: [] as string[],
|
|
product_id: [] as string[],
|
|
product_category_id: [] as string[],
|
|
received_date: '',
|
|
po_date: '',
|
|
start_date: '',
|
|
end_date: '',
|
|
sort_by: '',
|
|
filter_by: 'received_date',
|
|
},
|
|
paramMap: {
|
|
page: 'page',
|
|
pageSize: 'limit',
|
|
},
|
|
});
|
|
|
|
const { options: areaOptions, isLoadingOptions: isLoadingAreas } = useSelect(
|
|
AreaApi.basePath,
|
|
'id',
|
|
'name',
|
|
'search'
|
|
);
|
|
|
|
const { options: supplierOptions, isLoadingOptions: isLoadingSuppliers } =
|
|
useSelect(SupplierApi.basePath, 'id', 'name', 'search', {
|
|
category: 'SAPRONAK',
|
|
});
|
|
|
|
const { options: productOptions, isLoadingOptions: isLoadingProducts } =
|
|
useSelect(ProductApi.basePath, 'id', 'name', 'search');
|
|
|
|
const {
|
|
options: productCategoryOptions,
|
|
isLoadingOptions: isLoadingProductCategories,
|
|
} = useSelect(ProductCategoryApi.basePath, 'id', 'name', 'search');
|
|
|
|
const dataTypeOptions = useMemo(
|
|
() => [
|
|
{ value: 'received_date', label: 'Tanggal Terima' },
|
|
{ value: 'po_date', label: 'Tanggal PO' },
|
|
],
|
|
[]
|
|
);
|
|
|
|
const sortByOptions = useMemo(
|
|
() => [
|
|
{ value: 'ASC', label: 'Ascending' },
|
|
{ value: 'DESC', label: 'Descending' },
|
|
],
|
|
[]
|
|
);
|
|
|
|
const areaChangeHandler = useCallback(
|
|
(val: OptionType | OptionType[] | null) => {
|
|
const arr = Array.isArray(val) ? val : val ? [val] : [];
|
|
updateFilter(
|
|
'area_id',
|
|
arr.map((v) => String((v as OptionType).value))
|
|
);
|
|
setIsSubmitted(false);
|
|
},
|
|
[updateFilter]
|
|
);
|
|
|
|
const supplierChangeHandler = useCallback(
|
|
(val: OptionType | OptionType[] | null) => {
|
|
const arr = Array.isArray(val) ? val : val ? [val] : [];
|
|
updateFilter(
|
|
'supplier_id',
|
|
arr.map((v) => String((v as OptionType).value))
|
|
);
|
|
setIsSubmitted(false);
|
|
},
|
|
[updateFilter]
|
|
);
|
|
|
|
const productChangeHandler = useCallback(
|
|
(val: OptionType | OptionType[] | null) => {
|
|
const arr = Array.isArray(val) ? val : val ? [val] : [];
|
|
updateFilter(
|
|
'product_id',
|
|
arr.map((v) => String((v as OptionType).value))
|
|
);
|
|
setIsSubmitted(false);
|
|
},
|
|
[updateFilter]
|
|
);
|
|
|
|
const productCategoryChangeHandler = useCallback(
|
|
(val: OptionType | OptionType[] | null) => {
|
|
const arr = Array.isArray(val) ? val : val ? [val] : [];
|
|
updateFilter(
|
|
'product_category_id',
|
|
arr.map((v) => String((v as OptionType).value))
|
|
);
|
|
setIsSubmitted(false);
|
|
},
|
|
[updateFilter]
|
|
);
|
|
|
|
const dataTypeChangeHandler = useCallback(
|
|
(val: OptionType | OptionType[] | null) => {
|
|
const newVal = val as OptionType;
|
|
const filterValue =
|
|
(newVal?.value as 'received_date' | 'po_date') || 'received_date';
|
|
updateFilter('filter_by', filterValue);
|
|
updateFilter('received_date', '');
|
|
updateFilter('po_date', '');
|
|
setIsSubmitted(false);
|
|
},
|
|
[updateFilter]
|
|
);
|
|
|
|
const sortByHandler = useCallback(
|
|
(val: OptionType | OptionType[] | null) => {
|
|
const newVal = val as OptionType;
|
|
const sortValue = (newVal?.value as 'ASC' | 'DESC') || 'ASC';
|
|
updateFilter('sort_by', sortValue);
|
|
setIsSubmitted(false);
|
|
},
|
|
[updateFilter]
|
|
);
|
|
|
|
const startDateChangeHandler = useCallback<
|
|
ChangeEventHandler<HTMLInputElement>
|
|
>(
|
|
(e) => {
|
|
const val = e.target.value;
|
|
updateFilter('start_date', val || '');
|
|
setIsSubmitted(false);
|
|
},
|
|
[updateFilter]
|
|
);
|
|
|
|
const endDateChangeHandler = useCallback<
|
|
ChangeEventHandler<HTMLInputElement>
|
|
>(
|
|
(e) => {
|
|
const val = e.target.value;
|
|
updateFilter('end_date', val || '');
|
|
setIsSubmitted(false);
|
|
},
|
|
[updateFilter]
|
|
);
|
|
|
|
const resetFilters = useCallback(() => {
|
|
updateFilter('area_id', []);
|
|
updateFilter('supplier_id', []);
|
|
updateFilter('product_id', []);
|
|
updateFilter('product_category_id', []);
|
|
updateFilter('received_date', '');
|
|
updateFilter('po_date', '');
|
|
updateFilter('start_date', '');
|
|
updateFilter('end_date', '');
|
|
updateFilter('sort_by', '');
|
|
updateFilter('filter_by', 'received_date');
|
|
setIsSubmitted(false);
|
|
}, [updateFilter]);
|
|
|
|
const handleSubmit = useCallback(() => {
|
|
setIsSubmitted(true);
|
|
setCurrentPage(1);
|
|
}, []);
|
|
|
|
// ===== DATA FETCHING =====
|
|
const { data: purchasePerSupplier, isLoading } = useSWR(
|
|
isSubmitted
|
|
? () => {
|
|
const params = {
|
|
area_id:
|
|
tableFilterState.area_id.length > 0
|
|
? tableFilterState.area_id.join(',')
|
|
: undefined,
|
|
supplier_id:
|
|
tableFilterState.supplier_id.length > 0
|
|
? tableFilterState.supplier_id.join(',')
|
|
: undefined,
|
|
product_id:
|
|
tableFilterState.product_id.length > 0
|
|
? tableFilterState.product_id.join(',')
|
|
: undefined,
|
|
product_category_id:
|
|
tableFilterState.product_category_id.length > 0
|
|
? tableFilterState.product_category_id.join(',')
|
|
: undefined,
|
|
received_date:
|
|
tableFilterState.filter_by === 'received_date'
|
|
? tableFilterState.start_date || undefined
|
|
: undefined,
|
|
po_date:
|
|
tableFilterState.filter_by === 'po_date'
|
|
? tableFilterState.start_date || undefined
|
|
: undefined,
|
|
start_date: tableFilterState.start_date || undefined,
|
|
end_date: tableFilterState.end_date || undefined,
|
|
sort_by: tableFilterState.sort_by || undefined,
|
|
filter_by: tableFilterState.filter_by || undefined,
|
|
page: currentPage,
|
|
limit: pageSize,
|
|
};
|
|
|
|
return ['logistic-purchase-report', params];
|
|
}
|
|
: null,
|
|
([, params]) =>
|
|
LogisticApi.getLogisticPurchasePerSupplierReport(
|
|
params.area_id,
|
|
params.supplier_id,
|
|
params.product_id,
|
|
params.product_category_id,
|
|
params.received_date,
|
|
params.po_date,
|
|
params.start_date,
|
|
params.end_date,
|
|
params.sort_by,
|
|
params.filter_by,
|
|
params.page,
|
|
params.limit
|
|
)
|
|
);
|
|
|
|
const data: LogisticPurchasePerSupplierReport[] = useMemo(
|
|
() =>
|
|
isResponseSuccess(purchasePerSupplier)
|
|
? (purchasePerSupplier?.data as unknown as LogisticPurchasePerSupplierReport[]) ||
|
|
[]
|
|
: [],
|
|
[purchasePerSupplier]
|
|
);
|
|
|
|
const meta =
|
|
isResponseSuccess(purchasePerSupplier) && purchasePerSupplier?.meta
|
|
? purchasePerSupplier.meta
|
|
: null;
|
|
|
|
// ===== EXPORT DATA FETCHER =====
|
|
const logisticPurchasePerSupplierExport = useCallback(async (): Promise<
|
|
LogisticPurchasePerSupplierReport[] | null
|
|
> => {
|
|
const params = {
|
|
area_id:
|
|
tableFilterState.area_id.length > 0
|
|
? tableFilterState.area_id.join(',')
|
|
: undefined,
|
|
supplier_id:
|
|
tableFilterState.supplier_id.length > 0
|
|
? tableFilterState.supplier_id.join(',')
|
|
: undefined,
|
|
product_id:
|
|
tableFilterState.product_id.length > 0
|
|
? tableFilterState.product_id.join(',')
|
|
: undefined,
|
|
product_category_id:
|
|
tableFilterState.product_category_id.length > 0
|
|
? tableFilterState.product_category_id.join(',')
|
|
: undefined,
|
|
received_date:
|
|
tableFilterState.filter_by === 'received_date'
|
|
? tableFilterState.start_date || undefined
|
|
: undefined,
|
|
po_date:
|
|
tableFilterState.filter_by === 'po_date'
|
|
? tableFilterState.start_date || undefined
|
|
: undefined,
|
|
start_date: tableFilterState.start_date || undefined,
|
|
end_date: tableFilterState.end_date || undefined,
|
|
sort_by: tableFilterState.sort_by || undefined,
|
|
filter_by: tableFilterState.filter_by || undefined,
|
|
limit: 100,
|
|
page: 1,
|
|
};
|
|
|
|
const response = await LogisticApi.getLogisticPurchasePerSupplierReport(
|
|
params.area_id,
|
|
params.supplier_id,
|
|
params.product_id,
|
|
params.product_category_id,
|
|
params.received_date,
|
|
params.po_date,
|
|
params.start_date,
|
|
params.end_date,
|
|
params.sort_by,
|
|
params.filter_by,
|
|
params.page,
|
|
params.limit
|
|
);
|
|
|
|
return isResponseSuccess(response)
|
|
? (response.data as unknown as LogisticPurchasePerSupplierReport[])
|
|
: null;
|
|
}, [tableFilterState]);
|
|
|
|
// ===== EXPORT HANDLERS =====
|
|
const handleExportExcel = useCallback(async () => {
|
|
setIsExcelExportLoading(true);
|
|
try {
|
|
const allDataForExport = await logisticPurchasePerSupplierExport();
|
|
|
|
if (
|
|
!allDataForExport ||
|
|
!Array.isArray(allDataForExport) ||
|
|
allDataForExport.length === 0
|
|
) {
|
|
toast.error('Tidak ada data untuk diekspor.');
|
|
return;
|
|
}
|
|
|
|
await generatePurchasesPerSupplierExcel({ data: allDataForExport });
|
|
toast.success('Excel berhasil dibuat dan diunduh.');
|
|
} catch {
|
|
toast.error('Gagal membuat Excel. Silakan coba lagi.');
|
|
} finally {
|
|
setIsExcelExportLoading(false);
|
|
}
|
|
}, [logisticPurchasePerSupplierExport]);
|
|
|
|
const handleExportPdf = useCallback(async () => {
|
|
setIsPdfExportLoading(true);
|
|
try {
|
|
const allDataForExport = await logisticPurchasePerSupplierExport();
|
|
|
|
if (
|
|
!allDataForExport ||
|
|
!Array.isArray(allDataForExport) ||
|
|
allDataForExport.length === 0
|
|
) {
|
|
toast.error('Tidak ada data untuk diekspor.');
|
|
return;
|
|
}
|
|
|
|
const areaName =
|
|
tableFilterState.area_id.length > 0
|
|
? tableFilterState.area_id
|
|
.map(
|
|
(id) =>
|
|
areaOptions.find((opt) => opt.value === Number(id))?.label
|
|
)
|
|
.filter(Boolean)
|
|
.join(', ') || 'Semua Area'
|
|
: 'Semua Area';
|
|
|
|
const supplierName =
|
|
tableFilterState.supplier_id.length > 0
|
|
? tableFilterState.supplier_id
|
|
.map(
|
|
(id) =>
|
|
supplierOptions.find((opt) => opt.value === Number(id))?.label
|
|
)
|
|
.filter(Boolean)
|
|
.join(', ') || 'Semua Supplier'
|
|
: 'Semua Supplier';
|
|
|
|
const productName =
|
|
tableFilterState.product_id.length > 0
|
|
? tableFilterState.product_id
|
|
.map(
|
|
(id) =>
|
|
productOptions.find((opt) => opt.value === Number(id))?.label
|
|
)
|
|
.filter(Boolean)
|
|
.join(', ') || 'Semua Produk'
|
|
: 'Semua Produk';
|
|
|
|
const productCategoryName =
|
|
tableFilterState.product_category_id.length > 0
|
|
? tableFilterState.product_category_id
|
|
.map(
|
|
(id) =>
|
|
productCategoryOptions.find((opt) => opt.value === Number(id))
|
|
?.label
|
|
)
|
|
.filter(Boolean)
|
|
.join(', ') || 'Semua Kategori Produk'
|
|
: 'Semua Kategori Produk';
|
|
|
|
const exportParams = {
|
|
area_name: areaName,
|
|
supplier_name: supplierName,
|
|
product_name: productName,
|
|
product_category_name: productCategoryName,
|
|
filter_by: tableFilterState.filter_by || 'received_date',
|
|
start_date: tableFilterState.start_date || '',
|
|
end_date: tableFilterState.end_date || '',
|
|
};
|
|
|
|
await generatePurchasesPerSupplierPDF({
|
|
data: allDataForExport,
|
|
params: exportParams,
|
|
});
|
|
toast.success('PDF berhasil dibuat dan diunduh.');
|
|
} catch {
|
|
toast.error('Gagal membuat PDF. Silakan coba lagi.');
|
|
} finally {
|
|
setIsPdfExportLoading(false);
|
|
}
|
|
}, [
|
|
logisticPurchasePerSupplierExport,
|
|
tableFilterState,
|
|
areaOptions,
|
|
supplierOptions,
|
|
productOptions,
|
|
productCategoryOptions,
|
|
]);
|
|
|
|
// ===== PAGINATION HANDLERS =====
|
|
const handlePageChange = (page: number) => {
|
|
setCurrentPage(page);
|
|
};
|
|
|
|
const handleRowChange = (pageSize: number) => {
|
|
setPageSize(pageSize);
|
|
};
|
|
|
|
const handleNextPage = () => {
|
|
if (meta && currentPage < meta.total_pages) {
|
|
setCurrentPage(currentPage + 1);
|
|
}
|
|
};
|
|
|
|
const handlePrevPage = () => {
|
|
if (currentPage > 1) {
|
|
setCurrentPage(currentPage - 1);
|
|
}
|
|
};
|
|
|
|
const getTableColumns = (
|
|
summary: LogisticPurchasePerSupplierSummary
|
|
): ColumnDef<LogisticPurchasePerSupplierReport['rows'][0]>[] => {
|
|
const tableColumns: ColumnDef<
|
|
LogisticPurchasePerSupplierReport['rows'][0]
|
|
>[] = [
|
|
{
|
|
id: 'no',
|
|
header: 'No',
|
|
cell: (props) => props.row.index + 1,
|
|
footer: () => <div className='font-semibold text-gray-900'>Total</div>,
|
|
},
|
|
|
|
{
|
|
id: 'received_date',
|
|
header: 'Tanggal Terima',
|
|
accessorKey: 'receive_date',
|
|
cell: (props) => {
|
|
const value = props.row.original.receive_date;
|
|
return formatDate(value, 'DD MMM YYYY');
|
|
},
|
|
},
|
|
{
|
|
id: 'po_date',
|
|
header: 'Tanggal PO',
|
|
accessorKey: 'po_date',
|
|
cell: (props) => {
|
|
const value = props.row.original.po_date;
|
|
return formatDate(value, 'DD MMM YYYY');
|
|
},
|
|
},
|
|
{
|
|
id: 'po_number',
|
|
header: 'No. Referensi',
|
|
accessorKey: 'po_number',
|
|
cell: (props) => {
|
|
const value = props.row.original.po_number;
|
|
return value || '-';
|
|
},
|
|
},
|
|
{
|
|
id: 'product_name',
|
|
header: 'Nama Produk',
|
|
accessorKey: 'product.name',
|
|
cell: (props) => {
|
|
const product = props.row.original.product;
|
|
return product?.name || '-';
|
|
},
|
|
},
|
|
{
|
|
id: 'destination_warehouse',
|
|
header: 'Tujuan',
|
|
accessorKey: 'warehouse.name',
|
|
cell: (props) => {
|
|
const warehouse = props.row.original.warehouse;
|
|
return warehouse?.name || '-';
|
|
},
|
|
},
|
|
{
|
|
id: 'qty',
|
|
header: 'QTY',
|
|
accessorKey: 'qty',
|
|
cell: (props) => {
|
|
const value = props.row.original.qty;
|
|
return <div className='text-right'>{formatNumber(value)}</div>;
|
|
},
|
|
footer: () => (
|
|
<div className='text-right font-semibold text-gray-900'>
|
|
{formatNumber(summary.total_qty) || '-'}
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
id: 'price',
|
|
header: 'Harga Beli (Rp)',
|
|
accessorKey: 'unit_price',
|
|
cell: (props) => {
|
|
const value = props.row.original.unit_price;
|
|
return <div className='text-right'>{formatCurrency(value)}</div>;
|
|
},
|
|
footer: () => (
|
|
<div className='text-right font-semibold text-gray-900'>
|
|
{formatCurrency(summary.total_unit_price) || '-'}
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
id: 'purchase_amount',
|
|
header: 'Value Harga Beli (Rp)',
|
|
accessorKey: 'purchase_value',
|
|
cell: (props) => {
|
|
const value = props.row.original.purchase_value;
|
|
return <div className='text-right'>{formatCurrency(value)}</div>;
|
|
},
|
|
footer: () => (
|
|
<div className='text-right font-semibold text-gray-900'>
|
|
{formatCurrency(summary.total_purchase_value) || '-'}
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
id: 'transport',
|
|
header: 'Transport (Rp)',
|
|
accessorKey: 'transport_unit_price',
|
|
cell: (props) => {
|
|
const value = props.row.original.transport_unit_price;
|
|
return <div className='text-right'>{formatCurrency(value)}</div>;
|
|
},
|
|
footer: () => (
|
|
<div className='text-right font-semibold text-gray-900'>
|
|
{formatCurrency(summary.total_transport_unit_price) || '-'}
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
id: 'value_transport',
|
|
header: 'Value Transport (Rp)',
|
|
accessorKey: 'transport_value',
|
|
cell: (props) => {
|
|
const value = props.row.original.transport_value;
|
|
return <div className='text-right'>{formatCurrency(value)}</div>;
|
|
},
|
|
footer: () => (
|
|
<div className='text-right font-semibold text-gray-900'>
|
|
{formatCurrency(summary.total_transport_value) || '-'}
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
id: 'total',
|
|
header: 'Jumlah (Rp)',
|
|
accessorKey: 'total_amount',
|
|
cell: (props) => {
|
|
const value = props.row.original.total_amount;
|
|
return <div className='text-right'>{formatCurrency(value)}</div>;
|
|
},
|
|
footer: () => (
|
|
<div className='text-right font-semibold text-gray-900'>
|
|
{formatCurrency(summary.total_amount) || '-'}
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
id: 'expedition_vendor_name',
|
|
header: 'Ekspedisi',
|
|
accessorKey: 'expedition',
|
|
cell: (props) => {
|
|
const value = props.row.original.expedition;
|
|
return value || '-';
|
|
},
|
|
},
|
|
{
|
|
id: 'travel_number',
|
|
header: 'Surat Jalan',
|
|
accessorKey: 'delivery_number',
|
|
cell: (props) => {
|
|
const value = props.row.original.delivery_number;
|
|
return value || '-';
|
|
},
|
|
},
|
|
];
|
|
return tableColumns;
|
|
};
|
|
|
|
return (
|
|
<div className='w-full p-0 sm:p-4'>
|
|
<Card
|
|
subtitle='Laporan > Rekapitulasi Pembelian Per Supplier'
|
|
className={{ wrapper: 'w-full', body: 'p-1!' }}
|
|
>
|
|
<div className='grid md:grid-cols-3 grid-cols-1 gap-4'>
|
|
<SelectInput
|
|
label='Area'
|
|
placeholder='Pilih Area'
|
|
isMulti
|
|
options={areaOptions}
|
|
value={areaOptions.filter((opt) =>
|
|
(tableFilterState.area_id || [])
|
|
.map(String)
|
|
.includes(String(opt.value))
|
|
)}
|
|
onChange={areaChangeHandler}
|
|
isLoading={isLoadingAreas}
|
|
isClearable
|
|
/>
|
|
<SelectInput
|
|
label='Supplier'
|
|
placeholder='Pilih Supplier'
|
|
isMulti
|
|
options={supplierOptions}
|
|
value={supplierOptions.filter((opt) =>
|
|
(tableFilterState.supplier_id || [])
|
|
.map(String)
|
|
.includes(String(opt.value))
|
|
)}
|
|
onChange={supplierChangeHandler}
|
|
isLoading={isLoadingSuppliers}
|
|
isClearable
|
|
/>
|
|
<SelectInput
|
|
label='Produk'
|
|
placeholder='Pilih Produk'
|
|
isMulti
|
|
options={productOptions}
|
|
value={productOptions.filter((opt) =>
|
|
(tableFilterState.product_id || [])
|
|
.map(String)
|
|
.includes(String(opt.value))
|
|
)}
|
|
onChange={productChangeHandler}
|
|
isLoading={isLoadingProducts}
|
|
isClearable
|
|
/>
|
|
</div>
|
|
<div className='grid md:grid-cols-3 grid-cols-1 gap-4'>
|
|
<SelectInput
|
|
label='Kategori Produk'
|
|
placeholder='Pilih Kategori Produk'
|
|
isMulti
|
|
options={productCategoryOptions}
|
|
value={productCategoryOptions.filter((opt) =>
|
|
(tableFilterState.product_category_id || [])
|
|
.map(String)
|
|
.includes(String(opt.value))
|
|
)}
|
|
onChange={productCategoryChangeHandler}
|
|
isLoading={isLoadingProductCategories}
|
|
isClearable
|
|
/>
|
|
<div className='md:flex md:flex-row grid grid-cols-1 gap-4'>
|
|
<SelectInput
|
|
label='Filter Berdasarkan'
|
|
placeholder='Pilih Filter Berdasarkan'
|
|
options={dataTypeOptions}
|
|
value={
|
|
dataTypeOptions?.find(
|
|
(option) => option.value === tableFilterState.filter_by
|
|
) || null
|
|
}
|
|
onChange={dataTypeChangeHandler}
|
|
isLoading={false}
|
|
isClearable={false}
|
|
/>
|
|
<SelectInput
|
|
label='Urutkan Berdasarkan'
|
|
placeholder='Urutkan Berdasarkan'
|
|
options={sortByOptions}
|
|
value={
|
|
sortByOptions?.find(
|
|
(option) => option.value === tableFilterState.sort_by
|
|
) || null
|
|
}
|
|
onChange={sortByHandler}
|
|
isLoading={false}
|
|
isClearable={false}
|
|
/>
|
|
</div>
|
|
<div className='md:flex md:flex-row grid grid-cols-1 gap-4'>
|
|
<DateInput
|
|
label='Tanggal Awal'
|
|
name='start_date'
|
|
placeholder='Pilih Tanggal Awal'
|
|
value={tableFilterState.start_date}
|
|
onChange={startDateChangeHandler}
|
|
/>
|
|
<DateInput
|
|
label='Tanggal Akhir'
|
|
name='end_date'
|
|
placeholder='Pilih Tanggal Akhir'
|
|
value={tableFilterState.end_date}
|
|
onChange={endDateChangeHandler}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className='mt-4 flex justify-end gap-2 [&_button]:px-4'>
|
|
<Button color='primary' onClick={handleSubmit}>
|
|
<Icon icon='heroicons:magnifying-glass' width={20} height={20} />
|
|
Cari
|
|
</Button>
|
|
<Button color='warning' onClick={resetFilters}>
|
|
<Icon icon='heroicons-outline:refresh' width={20} height={20} />
|
|
Reset
|
|
</Button>
|
|
<Dropdown
|
|
trigger={
|
|
<Button color='success' isLoading={isAnyExportLoading}>
|
|
Export
|
|
<Icon
|
|
icon='heroicons-outline:download'
|
|
width={20}
|
|
height={20}
|
|
/>
|
|
</Button>
|
|
}
|
|
align='end'
|
|
>
|
|
<Menu className='w-32'>
|
|
<MenuItem title='Excel' onClick={handleExportExcel} />
|
|
<MenuItem title='PDF' onClick={handleExportPdf} />
|
|
</Menu>
|
|
</Dropdown>
|
|
</div>
|
|
|
|
{!isSubmitted ? (
|
|
<div className='mt-6 text-center text-gray-500'>
|
|
Silakan pilih filter dan klik tombol Submit untuk menampilkan data.
|
|
</div>
|
|
) : isLoading ? (
|
|
<div className='w-full flex flex-row justify-center items-center p-4'>
|
|
<span className='loading loading-spinner loading-xl' />
|
|
</div>
|
|
) : data.length === 0 ? (
|
|
<div className='mt-6 text-center text-gray-500'>
|
|
Tidak ada data yang dapat ditampilkan...
|
|
</div>
|
|
) : (
|
|
data.map((supplierReport) => {
|
|
const summary = supplierReport.summary || {
|
|
total_qty: 0,
|
|
total_unit_price: 0,
|
|
total_purchase_value: 0,
|
|
total_transport_unit_price: 0,
|
|
total_transport_value: 0,
|
|
total_amount: 0,
|
|
};
|
|
|
|
const totalPurchase = summary.total_amount;
|
|
const tableColumns = getTableColumns(summary);
|
|
|
|
return (
|
|
<Card
|
|
key={supplierReport.supplier.id}
|
|
title={supplierReport.supplier.name}
|
|
subtitle={`Total Pembelian: ${formatCurrency(totalPurchase)}`}
|
|
className={{
|
|
wrapper: 'w-full rounded-2xl',
|
|
body: 'p-0',
|
|
title:
|
|
'py-1.5 px-3 bg-primary text-white text-lg font-normal',
|
|
subtitle:
|
|
'px-3 pb-1 bg-primary text-white text-sm font-normal',
|
|
}}
|
|
variant='bordered'
|
|
collapsible={true}
|
|
>
|
|
<Table
|
|
data={supplierReport.rows}
|
|
columns={tableColumns}
|
|
pageSize={supplierReport.rows.length}
|
|
renderFooter={supplierReport.rows.length > 0}
|
|
className={{
|
|
containerClassName: 'w-full mb-0!',
|
|
tableWrapperClassName: 'overflow-x-auto',
|
|
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>
|
|
{meta && data.length > 0 && (
|
|
<div className='mt-6'>
|
|
<Pagination
|
|
currentPage={meta.page}
|
|
totalItems={meta.total_results}
|
|
onPageChange={handlePageChange}
|
|
onRowChange={handleRowChange}
|
|
onNextPage={handleNextPage}
|
|
onPrevPage={handlePrevPage}
|
|
rowOptions={[10, 25, 50, 100]}
|
|
itemsPerPage={meta.limit}
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default PurchasesPerSupplierTab;
|