Files
lti-web-client/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx
T
2026-04-25 12:15:42 +07:00

1041 lines
34 KiB
TypeScript

import Button from '@/components/Button';
import Card from '@/components/Card';
import Dropdown from '@/components/Dropdown';
import DateInput from '@/components/input/DateInput';
import { useSelect } from '@/components/input/SelectInput';
import Modal, { useModal } from '@/components/Modal';
import Table from '@/components/Table';
import { isResponseSuccess } from '@/lib/api-helper';
import { formatCurrency, formatDate, formatNumber } from '@/lib/helper';
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 {
LogisticPurchasePerSupplierReport,
LogisticPurchasePerSupplierSummary,
} from '@/types/api/report/logistic-stock';
import { generatePurchasesPerSupplierExcel } from '@/components/pages/report/logistic-stock/export/PurchasesPerSupplierExportXLSX';
import { generatePurchasesPerSupplierPDF } from '@/components/pages/report/logistic-stock/export/PurchasesPerSupplierExportPDF';
import { Icon } from '@iconify/react';
import { ColumnDef } from '@tanstack/react-table';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import toast from 'react-hot-toast';
import useSWR from 'swr';
import { useFormik } from 'formik';
import {
PurchasesPerSupplierFilterSchema,
PurchasesPerSupplierFilterType,
} from '@/components/pages/report/logistic-stock/filter/PurchasesPerSupplierFilter';
import SelectInputCheckbox from '@/components/input/SelectInputCheckbox';
import SelectInputRadio from '@/components/input/SelectInputRadio';
import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store';
import PurchasePerSupplierSkeleton from '@/components/pages/report/logistic-stock/skeleton/PurchasePerSupplierSkeleton';
import ButtonFilter from '@/components/helper/ButtonFilter';
interface PurchasesPerSupplierTabProps {
tabId: string;
}
interface FilterParams {
area_id?: string;
supplier_id?: string;
product_id?: string;
product_category_id?: string;
start_date?: string;
end_date?: string;
sort_by?: string;
filter_by?: string;
}
const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => {
// ===== 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] = useState(10);
// ===== SUBMISSION STATE =====
const [filterParams, setFilterParams] = useState<FilterParams>({});
const [dateErrorShown, setDateErrorShown] = useState(false);
const [hasDateError, setHasDateError] = useState(false);
const handleFilterModalOpenRef = useRef(() => {});
const filterModal = useModal();
// ===== OPTIONS =====
const {
options: areaOptions,
isLoadingOptions: isLoadingAreas,
setInputValue: setAreaInputValue,
loadMore: loadMoreArea,
} = useSelect(AreaApi.basePath, 'id', 'name', 'search');
const {
options: supplierOptions,
isLoadingOptions: isLoadingSuppliers,
setInputValue: setSupplierInputValue,
loadMore: loadMoreSupplier,
} = useSelect(SupplierApi.basePath, 'id', 'name', 'search', {
category: 'SAPRONAK',
});
const {
options: productOptions,
isLoadingOptions: isLoadingProducts,
setInputValue: setProductInputValue,
loadMore: loadMoreProduct,
} = useSelect(ProductApi.basePath, 'id', 'name', 'search');
const {
options: productCategoryOptions,
isLoadingOptions: isLoadingProductCategories,
setInputValue: setProductCategoryInputValue,
loadMore: loadMoreProductCategory,
} = 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' },
],
[]
);
// ===== FORMIK SETUP =====
const formik = useFormik<PurchasesPerSupplierFilterType>({
initialValues: {
start_date: null,
end_date: null,
area_ids: null,
supplier_ids: null,
product_ids: null,
product_category_ids: null,
filter_by: null,
sort_by: null,
},
validationSchema: PurchasesPerSupplierFilterSchema,
onSubmit: (values, { setSubmitting }) => {
setFilterParams({
start_date: values.start_date || undefined,
end_date: values.end_date || undefined,
area_id: values.area_ids || undefined,
supplier_id: values.supplier_ids || undefined,
product_id: values.product_ids || undefined,
product_category_id: values.product_category_ids || undefined,
filter_by: values.filter_by || undefined,
sort_by: values.sort_by || undefined,
});
filterModal.closeModal();
setCurrentPage(1);
setSubmitting(false);
},
onReset: () => {
setFilterParams({});
setCurrentPage(1);
setHasDateError(false);
if (dateErrorShown) {
toast.dismiss();
setDateErrorShown(false);
}
filterModal.closeModal();
},
});
handleFilterModalOpenRef.current = () => {
formik.setValues({
start_date: filterParams.start_date || null,
end_date: filterParams.end_date || null,
area_ids: filterParams.area_id || null,
supplier_ids: filterParams.supplier_id || null,
product_ids: filterParams.product_id || null,
product_category_ids: filterParams.product_category_id || null,
filter_by: filterParams.filter_by || null,
sort_by: filterParams.sort_by || null,
});
filterModal.openModal();
};
const { setFieldValue } = formik;
// ===== DATE CHANGE HANDLERS =====
const handleStartDateChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setFieldValue('start_date', value || null);
if (value && formik.values.end_date) {
const startDate = new Date(value);
const endDateObj = new Date(formik.values.end_date);
if (endDateObj < startDate) {
setHasDateError(true);
if (!dateErrorShown) {
toast.error('Tanggal akhir tidak boleh masa lampau', {
duration: Infinity,
});
setDateErrorShown(true);
}
} else {
setHasDateError(false);
if (dateErrorShown) {
toast.dismiss();
setDateErrorShown(false);
}
}
} else {
setHasDateError(false);
}
},
[setFieldValue, dateErrorShown, formik.values.end_date]
);
const handleEndDateChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setFieldValue('end_date', value || null);
if (value && formik.values.start_date) {
const startDateObj = new Date(formik.values.start_date);
const endDate = new Date(value);
if (endDate < startDateObj) {
setHasDateError(true);
if (!dateErrorShown) {
toast.error('Tanggal akhir tidak boleh masa lampau', {
duration: Infinity,
});
setDateErrorShown(true);
}
return;
}
}
setHasDateError(false);
if (dateErrorShown) {
toast.dismiss();
setDateErrorShown(false);
}
},
[setFieldValue, dateErrorShown, formik.values.start_date]
);
// ===== DERIVED VALUES =====
const areaIdsValue = useMemo(() => {
if (!formik.values.area_ids) return [];
const ids = formik.values.area_ids.split(',');
return areaOptions.filter((opt) => ids.includes(String(opt.value)));
}, [formik.values.area_ids, areaOptions]);
const supplierIdsValue = useMemo(() => {
if (!formik.values.supplier_ids) return [];
const ids = formik.values.supplier_ids.split(',');
return supplierOptions.filter((opt) => ids.includes(String(opt.value)));
}, [formik.values.supplier_ids, supplierOptions]);
const productIdsValue = useMemo(() => {
if (!formik.values.product_ids) return [];
const ids = formik.values.product_ids.split(',');
return productOptions.filter((opt) => ids.includes(String(opt.value)));
}, [formik.values.product_ids, productOptions]);
const productCategoryIdsValue = useMemo(() => {
if (!formik.values.product_category_ids) return [];
const ids = formik.values.product_category_ids.split(',');
return productCategoryOptions.filter((opt) =>
ids.includes(String(opt.value))
);
}, [formik.values.product_category_ids, productCategoryOptions]);
const filterByValue = useMemo(() => {
if (!formik.values.filter_by) return null;
return (
dataTypeOptions.find((opt) => opt.value === formik.values.filter_by) ||
null
);
}, [formik.values.filter_by, dataTypeOptions]);
const sortByValue = useMemo(() => {
if (!formik.values.sort_by) return null;
return (
sortByOptions.find((opt) => opt.value === formik.values.sort_by) || null
);
}, [formik.values.sort_by, sortByOptions]);
// ===== DATA FETCHING =====
const { data: purchasePerSupplier, isLoading } = useSWR(
() => {
const params = {
area_id: filterParams.area_id,
supplier_id: filterParams.supplier_id,
product_id: filterParams.product_id,
product_category_id: filterParams.product_category_id,
start_date: filterParams.start_date,
end_date: filterParams.end_date,
sort_by: filterParams.sort_by,
filter_by: filterParams.filter_by,
page: currentPage,
limit: pageSize,
};
return ['logistic-purchase-report', params];
},
([, params]) =>
LogisticApi.getLogisticPurchasePerSupplierReport(
params.area_id,
params.supplier_id,
params.product_id,
params.product_category_id,
params.filter_by === 'received_date' ? params.start_date : undefined,
params.filter_by === 'po_date' ? params.start_date : undefined,
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]
);
// ===== EXPORT DATA FETCHER =====
const logisticPurchasePerSupplierExport = useCallback(async (): Promise<
LogisticPurchasePerSupplierReport[] | null
> => {
const params = {
area_id: filterParams.area_id,
supplier_id: filterParams.supplier_id,
product_id: filterParams.product_id,
product_category_id: filterParams.product_category_id,
start_date: filterParams.start_date,
end_date: filterParams.end_date,
sort_by: filterParams.sort_by,
filter_by: filterParams.filter_by,
limit: 100,
page: 1,
};
const response = await LogisticApi.getLogisticPurchasePerSupplierReport(
params.area_id,
params.supplier_id,
params.product_id,
params.product_category_id,
params.filter_by === 'received_date' ? params.start_date : undefined,
params.filter_by === 'po_date' ? params.start_date : undefined,
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;
}, [filterParams]);
// ===== 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 = filterParams.area_id
? areaOptions
.filter((opt) =>
filterParams.area_id?.split(',').includes(String(opt.value))
)
.map((opt) => opt.label)
.join(', ') || 'Semua Area'
: 'Semua Area';
const supplierName = filterParams.supplier_id
? supplierOptions
.filter((opt) =>
filterParams.supplier_id?.split(',').includes(String(opt.value))
)
.map((opt) => opt.label)
.join(', ') || 'Semua Supplier'
: 'Semua Supplier';
const productName = filterParams.product_id
? productOptions
.filter((opt) =>
filterParams.product_id?.split(',').includes(String(opt.value))
)
.map((opt) => opt.label)
.join(', ') || 'Semua Produk'
: 'Semua Produk';
const productCategoryName = filterParams.product_category_id
? productCategoryOptions
.filter((opt) =>
filterParams.product_category_id
?.split(',')
.includes(String(opt.value))
)
.map((opt) => opt.label)
.join(', ') || 'Semua Kategori Produk'
: 'Semua Kategori Produk';
const exportParams = {
area_name: areaName,
supplier_name: supplierName,
product_name: productName,
product_category_name: productCategoryName,
filter_by: filterParams.filter_by,
start_date: filterParams.start_date,
end_date: filterParams.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,
filterParams,
areaOptions,
supplierOptions,
productOptions,
productCategoryOptions,
]);
// ===== TAB ACTIONS COMPONENT =====
const TabActions = useMemo(() => {
return function TabActionsComponent() {
const setTabActions = useTabActionsStore((state) => state.setTabActions);
const clearTabActions = useTabActionsStore(
(state) => state.clearTabActions
);
useEffect(() => {
setTabActions(
tabId,
<div className='flex flex-row gap-3'>
<ButtonFilter
values={filterParams}
fieldGroups={[['start_date', 'end_date']]}
onClick={() => handleFilterModalOpenRef.current()}
variant='outline'
className='px-3 py-2.5'
/>
<Dropdown
align='end'
direction='bottom'
className={{
content:
'mt-1 rounded-xl border border-base-content/5 shadow-sm overflow-hidden',
}}
trigger={
<Button
variant='outline'
color='none'
isLoading={isAnyExportLoading}
className='px-3 py-2.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft'
>
<div className='flex flex-row items-center gap-1.5'>
<Icon
icon='heroicons:cloud-arrow-down'
width={20}
height={20}
/>
<span>Export</span>
<div className='w-px self-stretch bg-base-content/10' />
<Icon
icon='heroicons:chevron-down'
width={14}
height={14}
/>
</div>
</Button>
}
>
<Button
variant='ghost'
color='none'
onClick={handleExportExcel}
isLoading={isExcelExportLoading}
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
>
<Icon icon='heroicons:table-cells' width={20} height={20} />
Export to Excel
</Button>
<Button
variant='ghost'
color='none'
onClick={handleExportPdf}
isLoading={isPdfExportLoading}
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
>
<Icon icon='heroicons:document' width={20} height={20} />
Export to PDF
</Button>
</Dropdown>
</div>
);
}, [setTabActions]);
useEffect(() => {
return () => {
clearTabActions(tabId);
};
}, [clearTabActions]);
return null;
};
}, [
tabId,
filterParams,
isAnyExportLoading,
handleExportExcel,
handleExportPdf,
isExcelExportLoading,
isPdfExportLoading,
]);
const TabActionsElement = useMemo(() => <TabActions />, [TabActions]);
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',
enableSorting: false,
cell: (props) => {
const value = props.row.original.receive_date;
return formatDate(value, 'DD MMM YYYY');
},
},
{
id: 'po_date',
header: 'Tanggal PO',
accessorKey: 'po_date',
enableSorting: false,
cell: (props) => {
const value = props.row.original.po_date;
return formatDate(value, 'DD MMM YYYY');
},
},
{
id: 'po_number',
header: 'No. Referensi',
accessorKey: 'po_number',
enableSorting: false,
cell: (props) => {
const value = props.row.original.po_number;
return value || '-';
},
},
{
id: 'product_name',
header: 'Nama Produk',
accessorKey: 'product.name',
enableSorting: false,
cell: (props) => {
const product = props.row.original.product;
return product?.name || '-';
},
},
{
id: 'destination_warehouse',
header: 'Tujuan',
accessorKey: 'warehouse.name',
enableSorting: false,
cell: (props) => {
const warehouse = props.row.original.warehouse;
return warehouse?.name || '-';
},
},
{
id: 'qty',
header: 'QTY',
accessorKey: 'qty',
enableSorting: false,
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',
enableSorting: false,
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',
enableSorting: false,
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',
enableSorting: false,
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',
enableSorting: false,
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',
enableSorting: false,
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',
enableSorting: false,
cell: (props) => {
const value = props.row.original.expedition;
return value || '-';
},
},
{
id: 'travel_number',
header: 'Surat Jalan',
accessorKey: 'delivery_number',
enableSorting: false,
cell: (props) => {
const value = props.row.original.delivery_number;
return value || '-';
},
},
];
return tableColumns;
};
return (
<>
{TabActionsElement}
<div className='w-full p-0 sm:p-3 flex flex-col gap-3'>
{isLoading && (
<PurchasePerSupplierSkeleton
columns={getTableColumns({} as LogisticPurchasePerSupplierSummary)}
icon={
<Icon
icon='heroicons:document-report'
className='text-white'
width={20}
height={20}
/>
}
title='Memuat Data Pembelian Per Supplier'
subtitle='Silakan tunggu sebentar...'
/>
)}
{!isLoading && data.length === 0 && (
<PurchasePerSupplierSkeleton
columns={getTableColumns({} as LogisticPurchasePerSupplierSummary)}
icon={
<Icon
icon='heroicons:chart-bar'
className='text-white'
width={20}
height={20}
/>
}
title='Data Not Yet Available'
subtitle='Please change your filters to get the data.'
/>
)}
{!isLoading &&
data.length > 0 &&
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-lg border-none',
body: 'p-0',
title:
'px-2 py-1.5 font-normal text-sm bg-primary text-white',
subtitle:
'px-2 pb-1.5 bg-primary text-white text-xs font-normal',
collapsible: 'rounded-lg',
}}
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 rounded-tr-none rounded-tl-none',
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>
);
})}
</div>
{/* Filter Modal */}
<Modal
ref={filterModal.ref}
className={{
modal: 'p-0',
modalBox: 'p-0 rounded-[0.875rem] xl:max-w-4/12 max-w-sm',
}}
>
{/* Modal Header */}
<div className='flex items-center justify-between gap-2 border-b border-base-content/10 p-4'>
<div className='flex items-center gap-2 text-primary'>
<Icon icon='heroicons:funnel' width={20} height={20} />
<h3 className='font-medium text-sm'>Filter Data</h3>
</div>
<Button
variant='link'
onClick={filterModal.closeModal}
className='text-base-content/50 hover:text-base-content transition-colors cursor-pointer'
>
<Icon icon='heroicons:x-mark' width={20} height={20} />
</Button>
</div>
<form onSubmit={formik.handleSubmit} onReset={formik.handleReset}>
<div className='p-4 flex flex-col gap-3'>
{/* Date Filter */}
<div>
<label className='block text-xs font-semibold text-base-content py-2'>
Tanggal
</label>
<div className='flex flex-row gap-1.5 items-center justify-between'>
<DateInput
name='start_date'
value={formik.values.start_date || ''}
errorMessage={formik.errors.start_date}
onChange={handleStartDateChange}
className={{ wrapper: 'w-full' }}
isNestedModal
isError={
formik.touched.start_date &&
Boolean(formik.errors.start_date)
}
/>
<hr className='w-full max-w-3 h-px border-base-content/10' />
<DateInput
name='end_date'
value={formik.values.end_date || ''}
errorMessage={formik.errors.end_date}
onChange={handleEndDateChange}
className={{ wrapper: 'w-full' }}
isNestedModal
isError={
(formik.touched.end_date &&
Boolean(formik.errors.end_date)) ||
hasDateError
}
/>
</div>
</div>
{/* Area Filter */}
<SelectInputCheckbox
label='Area'
placeholder='Pilih Area'
options={areaOptions}
value={areaIdsValue}
onChange={(val) => {
formik.setFieldValue(
'area_ids',
Array.isArray(val) && val.length > 0
? val.map((v) => String(v.value)).join(',')
: null
);
}}
isLoading={isLoadingAreas}
isClearable
className={{ wrapper: 'w-full' }}
onInputChange={setAreaInputValue}
onMenuScrollToBottom={loadMoreArea}
/>
{/* Supplier Filter */}
<SelectInputCheckbox
label='Supplier'
placeholder='Pilih Supplier'
options={supplierOptions}
value={supplierIdsValue}
onChange={(val) => {
formik.setFieldValue(
'supplier_ids',
Array.isArray(val) && val.length > 0
? val.map((v) => String(v.value)).join(',')
: null
);
}}
isLoading={isLoadingSuppliers}
isClearable
className={{ wrapper: 'w-full' }}
onInputChange={setSupplierInputValue}
onMenuScrollToBottom={loadMoreSupplier}
/>
{/* Product Filter */}
<SelectInputCheckbox
label='Produk'
placeholder='Pilih Produk'
options={productOptions}
value={productIdsValue}
onChange={(val) => {
formik.setFieldValue(
'product_ids',
Array.isArray(val) && val.length > 0
? val.map((v) => String(v.value)).join(',')
: null
);
}}
isLoading={isLoadingProducts}
isClearable
className={{ wrapper: 'w-full' }}
onInputChange={setProductInputValue}
onMenuScrollToBottom={loadMoreProduct}
/>
{/* Product Category Filter */}
<SelectInputCheckbox
label='Kategori Produk'
placeholder='Pilih Kategori Produk'
options={productCategoryOptions}
value={productCategoryIdsValue}
onChange={(val) => {
formik.setFieldValue(
'product_category_ids',
Array.isArray(val) && val.length > 0
? val.map((v) => String(v.value)).join(',')
: null
);
}}
isLoading={isLoadingProductCategories}
isClearable
className={{ wrapper: 'w-full' }}
onInputChange={setProductCategoryInputValue}
onMenuScrollToBottom={loadMoreProductCategory}
/>
{/* Filter By Type */}
<SelectInputRadio
label='Filter Berdasarkan'
placeholder='Pilih Filter Berdasarkan'
options={dataTypeOptions}
value={filterByValue}
onChange={(val) => {
if (!Array.isArray(val)) {
formik.setFieldValue(
'filter_by',
val?.value?.toString() || null
);
}
}}
className={{ wrapper: 'w-full' }}
isClearable={true}
/>
{/* Sort By */}
<SelectInputRadio
label='Urutkan Berdasarkan'
placeholder='Pilih Urutkan Berdasarkan'
options={sortByOptions}
value={sortByValue}
onChange={(val) => {
if (!Array.isArray(val)) {
formik.setFieldValue(
'sort_by',
val?.value?.toString() || null
);
}
}}
className={{ wrapper: 'w-full' }}
isClearable={true}
/>
</div>
{/* Modal Footer */}
<div className='flex justify-between items-center gap-4 p-4 border-t border-base-content/10 bg-gray-50'>
<Button
type='reset'
variant='soft'
className='rounded-lg text-base-content/65 bg-transparent border-none hover:bg-base-content/10 hover:text-base-content/65 transition-colors px-3 py-2'
>
Reset Filter
</Button>
<Button
type='submit'
className='min-w-40 text-sm rounded-lg py-3 text-white font-semibold'
disabled={hasDateError || !formik.isValid || formik.isSubmitting}
>
Apply Filter
</Button>
</div>
</form>
</Modal>
</>
);
};
export default PurchasesPerSupplierTab;