refactor(FE): Refactor PurchasesPerSupplierTab to use Formik for filters

This commit is contained in:
rstubryan
2026-02-11 16:44:10 +07:00
parent 14d0dc590f
commit 52d58d0921
2 changed files with 439 additions and 405 deletions
@@ -0,0 +1,89 @@
import { OptionType } from '@/components/input/SelectInput';
import * as yup from 'yup';
export type PurchasesPerSupplierFilterType = {
start_date: string | null | undefined;
end_date: string | null | undefined;
area_ids: OptionType[] | null | undefined;
supplier_ids: OptionType[] | null | undefined;
product_ids: OptionType[] | null | undefined;
product_category_ids: OptionType[] | null | undefined;
filter_by: OptionType | null | undefined;
sort_by: OptionType | null | undefined;
};
export const PurchasesPerSupplierFilterSchema: yup.ObjectSchema<PurchasesPerSupplierFilterType> =
yup.object({
start_date: yup.string().optional().nullable(),
end_date: yup
.string()
.optional()
.nullable()
.test(
'is-greater-than-start',
'Tanggal akhir tidak boleh masa lampau',
function (value) {
const { start_date } = this.parent;
if (!start_date || !value) return true;
return new Date(value) >= new Date(start_date);
}
),
area_ids: yup
.array()
.of(
yup.object({
value: yup.mixed<string | number>().required(),
label: yup.string().required(),
})
)
.optional()
.nullable(),
supplier_ids: yup
.array()
.of(
yup.object({
value: yup.mixed<string | number>().required(),
label: yup.string().required(),
})
)
.optional()
.nullable(),
product_ids: yup
.array()
.of(
yup.object({
value: yup.mixed<string | number>().required(),
label: yup.string().required(),
})
)
.optional()
.nullable(),
product_category_ids: yup
.array()
.of(
yup.object({
value: yup.mixed<string | number>().required(),
label: yup.string().required(),
})
)
.optional()
.nullable(),
filter_by: yup
.object({
value: yup.mixed<string | number>().required(),
label: yup.string().required(),
})
.optional()
.nullable(),
sort_by: yup
.object({
value: yup.mixed<string | number>().required(),
label: yup.string().required(),
})
.optional()
.nullable(),
});
export type PurchasesPerSupplierFilterValues = yup.InferType<
typeof PurchasesPerSupplierFilterSchema
>;
@@ -1,40 +1,55 @@
import { useState, useMemo, useCallback, useEffect } from 'react'; import Button from '@/components/Button';
import useSWR from 'swr';
import Card from '@/components/Card'; import Card from '@/components/Card';
import { useSelect, OptionType } from '@/components/input/SelectInput'; import Dropdown from '@/components/Dropdown';
import SelectInputCheckbox from '@/components/input/SelectInputCheckbox';
import SelectInputRadio from '@/components/input/SelectInputRadio';
import DateInput from '@/components/input/DateInput'; import DateInput from '@/components/input/DateInput';
import { OptionType, useSelect } from '@/components/input/SelectInput';
import Menu from '@/components/menu/Menu';
import MenuItem from '@/components/menu/MenuItem';
import Modal, { useModal } from '@/components/Modal';
import Table from '@/components/Table';
import { isResponseSuccess } from '@/lib/api-helper';
import { cn, formatCurrency, formatDate, formatNumber } from '@/lib/helper';
import { AreaApi } from '@/services/api/master-data'; import { AreaApi } from '@/services/api/master-data';
import { SupplierApi } from '@/services/api/master-data'; import { SupplierApi } from '@/services/api/master-data';
import { ProductApi } from '@/services/api/master-data'; import { ProductApi } from '@/services/api/master-data';
import { ProductCategoryApi } from '@/services/api/master-data'; import { ProductCategoryApi } from '@/services/api/master-data';
import { LogisticApi } from '@/services/api/report/logistic-stock'; import { LogisticApi } from '@/services/api/report/logistic-stock';
import Table from '@/components/Table';
import { ColumnDef } from '@tanstack/react-table';
import { formatCurrency, formatDate, formatNumber, cn } from '@/lib/helper';
import { import {
LogisticPurchasePerSupplierReport, LogisticPurchasePerSupplierReport,
LogisticPurchasePerSupplierSummary, LogisticPurchasePerSupplierSummary,
} from '@/types/api/report/logistic-stock'; } from '@/types/api/report/logistic-stock';
import { isResponseSuccess } from '@/lib/api-helper';
import Button from '@/components/Button';
import Dropdown from '@/components/Dropdown';
import MenuItem from '@/components/menu/MenuItem';
import Menu from '@/components/menu/Menu';
import Modal from '@/components/Modal';
import { useModal } from '@/components/Modal';
import { generatePurchasesPerSupplierPDF } from '@/components/pages/report/logistic-stock/export/PurchasesPerSupplierExportPDF';
import { generatePurchasesPerSupplierExcel } from '@/components/pages/report/logistic-stock/export/PurchasesPerSupplierExportXLSX'; import { generatePurchasesPerSupplierExcel } from '@/components/pages/report/logistic-stock/export/PurchasesPerSupplierExportXLSX';
import PurchasePerSupplierSkeleton from '@/components/pages/report/logistic-stock/skeleton/PurchasePerSupplierSkeleton'; import { generatePurchasesPerSupplierPDF } from '@/components/pages/report/logistic-stock/export/PurchasesPerSupplierExportPDF';
import toast from 'react-hot-toast';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
import { ColumnDef } from '@tanstack/react-table';
import { useCallback, useEffect, useMemo, 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 { useLogisticStockTabStore } from '@/stores/logistic-stock-tab/logistic-stock-tab.store'; import { useLogisticStockTabStore } from '@/stores/logistic-stock-tab/logistic-stock-tab.store';
import PurchasePerSupplierSkeleton from '@/components/pages/report/logistic-stock/skeleton/PurchasePerSupplierSkeleton';
interface PurchasesPerSupplierTabProps { interface PurchasesPerSupplierTabProps {
tabId: string; tabId: string;
} }
interface FilterParams {
area_ids?: string;
supplier_ids?: string;
product_ids?: string;
product_category_ids?: string;
start_date?: string;
end_date?: string;
sort_by?: string;
filter_by?: string;
}
const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => {
// ===== STATE MANAGEMENT ===== // ===== STATE MANAGEMENT =====
const [isPdfExportLoading, setIsPdfExportLoading] = useState(false); const [isPdfExportLoading, setIsPdfExportLoading] = useState(false);
@@ -46,11 +61,14 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => {
const [pageSize] = useState(10); const [pageSize] = useState(10);
// ===== SUBMISSION STATE ===== // ===== SUBMISSION STATE =====
const [filterParams, setFilterParams] = useState<FilterParams>({});
const [isSubmitted, setIsSubmitted] = useState(false); const [isSubmitted, setIsSubmitted] = useState(false);
const [dateErrorShown, setDateErrorShown] = useState(false);
const [hasDateError, setHasDateError] = useState(false);
const filterModal = useModal(); const filterModal = useModal();
// ===== OPTIONS (Declare before filter state) ===== // ===== OPTIONS =====
const { options: areaOptions, isLoadingOptions: isLoadingAreas } = useSelect( const { options: areaOptions, isLoadingOptions: isLoadingAreas } = useSelect(
AreaApi.basePath, AreaApi.basePath,
'id', 'id',
@@ -87,127 +105,66 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => {
[] []
); );
// ===== APPLIED FILTER STATE (Yang sudah di-apply) ===== const handleFilterModalOpen = () => {
const [appliedFilterArea, setAppliedFilterArea] = useState<
typeof areaOptions
>([]);
const [appliedFilterSupplier, setAppliedFilterSupplier] = useState<
typeof supplierOptions
>([]);
const [appliedFilterProduct, setAppliedFilterProduct] = useState<
typeof productOptions
>([]);
const [appliedFilterProductCategory, setAppliedFilterProductCategory] =
useState<typeof productCategoryOptions>([]);
const [appliedFilterByType, setAppliedFilterByType] = useState<
(typeof dataTypeOptions)[0] | null
>(null);
const [appliedFilterSortBy, setAppliedFilterSortBy] = useState<
(typeof sortByOptions)[0] | null
>(null);
const [appliedFilterStartDate, setAppliedFilterStartDate] = useState('');
const [appliedFilterEndDate, setAppliedFilterEndDate] = useState('');
const [dateErrorShown, setDateErrorShown] = useState(false);
const [hasDateError, setHasDateError] = useState(false);
// ===== PENDING FILTER STATE (Yang ada di modal, belum di-apply) =====
const [filterArea, setFilterArea] = useState<typeof areaOptions>([]);
const [filterSupplier, setFilterSupplier] = useState<typeof supplierOptions>(
[]
);
const [filterProduct, setFilterProduct] = useState<typeof productOptions>([]);
const [filterProductCategory, setFilterProductCategory] = useState<
typeof productCategoryOptions
>([]);
const [filterByType, setFilterByType] = useState<
(typeof dataTypeOptions)[0] | null
>(null);
const [filterSortBy, setFilterSortBy] = useState<
(typeof sortByOptions)[0] | null
>(null);
const [filterStartDate, setFilterStartDate] = useState('');
const [filterEndDate, setFilterEndDate] = useState('');
// ===== FILTER HANDLERS =====
const handleFilterModalOpen = useCallback(() => {
setFilterArea(appliedFilterArea);
setFilterSupplier(appliedFilterSupplier);
setFilterProduct(appliedFilterProduct);
setFilterProductCategory(appliedFilterProductCategory);
setFilterByType(appliedFilterByType);
setFilterSortBy(appliedFilterSortBy);
setFilterStartDate(appliedFilterStartDate);
setFilterEndDate(appliedFilterEndDate);
filterModal.openModal(); filterModal.openModal();
}, [ };
filterModal,
appliedFilterArea,
appliedFilterSupplier,
appliedFilterProduct,
appliedFilterProductCategory,
appliedFilterByType,
appliedFilterSortBy,
appliedFilterStartDate,
appliedFilterEndDate,
]);
const handleResetFilters = useCallback(() => { // ===== FORMIK SETUP =====
setIsSubmitted(false); const formik = useFormik<PurchasesPerSupplierFilterType>({
setFilterArea([]); initialValues: {
setFilterSupplier([]); start_date: null,
setFilterProduct([]); end_date: null,
setFilterProductCategory([]); area_ids: null,
setFilterByType(null); supplier_ids: null,
setFilterSortBy(null); product_ids: null,
setFilterStartDate(''); product_category_ids: null,
setFilterEndDate(''); filter_by: null,
setAppliedFilterArea([]); sort_by: null,
setAppliedFilterSupplier([]); },
setAppliedFilterProduct([]); validationSchema: PurchasesPerSupplierFilterSchema,
setAppliedFilterProductCategory([]); onSubmit: (values) => {
setAppliedFilterByType(null); setFilterParams({
setAppliedFilterSortBy(null); start_date: values.start_date?.toString() || undefined,
setAppliedFilterStartDate(''); end_date: values.end_date?.toString() || undefined,
setAppliedFilterEndDate(''); area_ids:
setHasDateError(false); values.area_ids?.map((v) => String(v.value)).join(',') || undefined,
if (dateErrorShown) { supplier_ids:
toast.dismiss(); values.supplier_ids?.map((v) => String(v.value)).join(',') ||
setDateErrorShown(false); undefined,
} product_ids:
}, [dateErrorShown]); values.product_ids?.map((v) => String(v.value)).join(',') ||
undefined,
const handleApplyFilters = useCallback(() => { product_category_ids:
setAppliedFilterArea(filterArea); values.product_category_ids?.map((v) => String(v.value)).join(',') ||
setAppliedFilterSupplier(filterSupplier); undefined,
setAppliedFilterProduct(filterProduct); filter_by: values.filter_by?.value?.toString() || undefined,
setAppliedFilterProductCategory(filterProductCategory); sort_by: values.sort_by?.value?.toString() || undefined,
setAppliedFilterByType(filterByType); });
setAppliedFilterSortBy(filterSortBy); filterModal.closeModal();
setAppliedFilterStartDate(filterStartDate); setIsSubmitted(true);
setAppliedFilterEndDate(filterEndDate); setCurrentPage(1);
setIsSubmitted(true); },
setCurrentPage(1); onReset: () => {
filterModal.closeModal(); setFilterParams({});
}, [ setIsSubmitted(false);
filterModal, setCurrentPage(1);
filterArea, setHasDateError(false);
filterSupplier, if (dateErrorShown) {
filterProduct, toast.dismiss();
filterProductCategory, setDateErrorShown(false);
filterByType, }
filterSortBy, },
filterStartDate, });
filterEndDate,
]);
// ===== DATE CHANGE HANDLERS =====
const handleStartDateChange = useCallback( const handleStartDateChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => { (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value; const value = e.target.value;
setFilterStartDate(value); formik.setFieldValue('start_date', value || null);
if (value && filterEndDate) { if (value && formik.values.end_date) {
const startDate = new Date(value); const startDate = new Date(value);
const endDateObj = new Date(filterEndDate); const endDateObj = new Date(formik.values.end_date);
if (endDateObj < startDate) { if (endDateObj < startDate) {
setHasDateError(true); setHasDateError(true);
@@ -228,16 +185,16 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => {
setHasDateError(false); setHasDateError(false);
} }
}, },
[filterEndDate, dateErrorShown] [formik, dateErrorShown]
); );
const handleEndDateChange = useCallback( const handleEndDateChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => { (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value; const value = e.target.value;
setFilterEndDate(value); formik.setFieldValue('end_date', value || null);
if (value && filterStartDate) { if (value && formik.values.start_date) {
const startDateObj = new Date(filterStartDate); const startDateObj = new Date(formik.values.start_date);
const endDate = new Date(value); const endDate = new Date(value);
if (endDate < startDateObj) { if (endDate < startDateObj) {
@@ -258,59 +215,43 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => {
setDateErrorShown(false); setDateErrorShown(false);
} }
}, },
[filterStartDate, dateErrorShown] [formik, dateErrorShown]
); );
// ===== ACTIVE FILTERS COUNT ===== // ===== ACTIVE FILTERS COUNT =====
const activeFiltersCount = useMemo(() => { const activeFiltersCount = useMemo(() => {
let count = 0; let count = 0;
// Date filter (start_date + end_date = 1 filter) if (filterParams.start_date || filterParams.end_date) {
if (appliedFilterStartDate || appliedFilterEndDate) {
count += 1; count += 1;
} }
// Area filter if (filterParams.area_ids) {
if (appliedFilterArea.length > 0) {
count += 1; count += 1;
} }
// Supplier filter if (filterParams.supplier_ids) {
if (appliedFilterSupplier.length > 0) {
count += 1; count += 1;
} }
// Product filter if (filterParams.product_ids) {
if (appliedFilterProduct.length > 0) {
count += 1; count += 1;
} }
// Product category filter if (filterParams.product_category_ids) {
if (appliedFilterProductCategory.length > 0) {
count += 1; count += 1;
} }
// Filter by type filter if (filterParams.filter_by) {
if (appliedFilterByType) {
count += 1; count += 1;
} }
// Sort by filter if (filterParams.sort_by) {
if (appliedFilterSortBy) {
count += 1; count += 1;
} }
return count; return count;
}, [ }, [filterParams]);
appliedFilterStartDate,
appliedFilterEndDate,
appliedFilterArea,
appliedFilterSupplier,
appliedFilterProduct,
appliedFilterProductCategory,
appliedFilterByType,
appliedFilterSortBy,
]);
const hasFilters = activeFiltersCount > 0; const hasFilters = activeFiltersCount > 0;
@@ -319,39 +260,14 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => {
isSubmitted isSubmitted
? () => { ? () => {
const params = { const params = {
area_id: area_ids: filterParams.area_ids,
appliedFilterArea.length > 0 supplier_ids: filterParams.supplier_ids,
? appliedFilterArea.map((v) => String(v.value)).join(',') product_ids: filterParams.product_ids,
: undefined, product_category_ids: filterParams.product_category_ids,
supplier_id: start_date: filterParams.start_date,
appliedFilterSupplier.length > 0 end_date: filterParams.end_date,
? appliedFilterSupplier.map((v) => String(v.value)).join(',') sort_by: filterParams.sort_by,
: undefined, filter_by: filterParams.filter_by,
product_id:
appliedFilterProduct.length > 0
? appliedFilterProduct.map((v) => String(v.value)).join(',')
: undefined,
product_category_id:
appliedFilterProductCategory.length > 0
? appliedFilterProductCategory
.map((v) => String(v.value))
.join(',')
: undefined,
received_date:
appliedFilterByType?.value === 'received_date'
? appliedFilterStartDate || undefined
: undefined,
po_date:
appliedFilterByType?.value === 'po_date'
? appliedFilterStartDate || undefined
: undefined,
start_date: appliedFilterStartDate || undefined,
end_date: appliedFilterEndDate || undefined,
sort_by:
(appliedFilterSortBy?.value as 'ASC' | 'DESC') || undefined,
filter_by:
(appliedFilterByType?.value as 'received_date' | 'po_date') ||
undefined,
page: currentPage, page: currentPage,
limit: pageSize, limit: pageSize,
}; };
@@ -361,12 +277,12 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => {
: null, : null,
([, params]) => ([, params]) =>
LogisticApi.getLogisticPurchasePerSupplierReport( LogisticApi.getLogisticPurchasePerSupplierReport(
params.area_id, params.area_ids,
params.supplier_id, params.supplier_ids,
params.product_id, params.product_ids,
params.product_category_id, params.product_category_ids,
params.received_date, params.filter_by === 'received_date' ? params.start_date : undefined,
params.po_date, params.filter_by === 'po_date' ? params.start_date : undefined,
params.start_date, params.start_date,
params.end_date, params.end_date,
params.sort_by, params.sort_by,
@@ -395,47 +311,25 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => {
LogisticPurchasePerSupplierReport[] | null LogisticPurchasePerSupplierReport[] | null
> => { > => {
const params = { const params = {
area_id: area_ids: filterParams.area_ids,
appliedFilterArea.length > 0 supplier_ids: filterParams.supplier_ids,
? appliedFilterArea.map((v) => String(v.value)).join(',') product_ids: filterParams.product_ids,
: undefined, product_category_ids: filterParams.product_category_ids,
supplier_id: start_date: filterParams.start_date,
appliedFilterSupplier.length > 0 end_date: filterParams.end_date,
? appliedFilterSupplier.map((v) => String(v.value)).join(',') sort_by: filterParams.sort_by,
: undefined, filter_by: filterParams.filter_by,
product_id:
appliedFilterProduct.length > 0
? appliedFilterProduct.map((v) => String(v.value)).join(',')
: undefined,
product_category_id:
appliedFilterProductCategory.length > 0
? appliedFilterProductCategory.map((v) => String(v.value)).join(',')
: undefined,
received_date:
appliedFilterByType?.value === 'received_date'
? appliedFilterStartDate || undefined
: undefined,
po_date:
appliedFilterByType?.value === 'po_date'
? appliedFilterStartDate || undefined
: undefined,
start_date: appliedFilterStartDate || undefined,
end_date: appliedFilterEndDate || undefined,
sort_by: (appliedFilterSortBy?.value as 'ASC' | 'DESC') || undefined,
filter_by:
(appliedFilterByType?.value as 'received_date' | 'po_date') ||
undefined,
limit: 100, limit: 100,
page: 1, page: 1,
}; };
const response = await LogisticApi.getLogisticPurchasePerSupplierReport( const response = await LogisticApi.getLogisticPurchasePerSupplierReport(
params.area_id, params.area_ids,
params.supplier_id, params.supplier_ids,
params.product_id, params.product_ids,
params.product_category_id, params.product_category_ids,
params.received_date, params.filter_by === 'received_date' ? params.start_date : undefined,
params.po_date, params.filter_by === 'po_date' ? params.start_date : undefined,
params.start_date, params.start_date,
params.end_date, params.end_date,
params.sort_by, params.sort_by,
@@ -447,16 +341,7 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => {
return isResponseSuccess(response) return isResponseSuccess(response)
? (response.data as unknown as LogisticPurchasePerSupplierReport[]) ? (response.data as unknown as LogisticPurchasePerSupplierReport[])
: null; : null;
}, [ }, [filterParams]);
appliedFilterArea,
appliedFilterSupplier,
appliedFilterProduct,
appliedFilterProductCategory,
appliedFilterStartDate,
appliedFilterEndDate,
appliedFilterByType,
appliedFilterSortBy,
]);
// ===== EXPORT HANDLERS ===== // ===== EXPORT HANDLERS =====
const handleExportExcel = useCallback(async () => { const handleExportExcel = useCallback(async () => {
@@ -496,39 +381,52 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => {
return; return;
} }
const areaName = const areaName = filterParams.area_ids
appliedFilterArea.length > 0 ? areaOptions
? appliedFilterArea.map((c) => c.label).join(', ') || 'Semua Area' .filter((opt) =>
: 'Semua Area'; filterParams.area_ids?.split(',').includes(String(opt.value))
)
.map((opt) => opt.label)
.join(', ') || 'Semua Area'
: 'Semua Area';
const supplierName = const supplierName = filterParams.supplier_ids
appliedFilterSupplier.length > 0 ? supplierOptions
? appliedFilterSupplier.map((c) => c.label).join(', ') || .filter((opt) =>
'Semua Supplier' filterParams.supplier_ids?.split(',').includes(String(opt.value))
: 'Semua Supplier'; )
.map((opt) => opt.label)
.join(', ') || 'Semua Supplier'
: 'Semua Supplier';
const productName = const productName = filterParams.product_ids
appliedFilterProduct.length > 0 ? productOptions
? appliedFilterProduct.map((c) => c.label).join(', ') || .filter((opt) =>
'Semua Produk' filterParams.product_ids?.split(',').includes(String(opt.value))
: 'Semua Produk'; )
.map((opt) => opt.label)
.join(', ') || 'Semua Produk'
: 'Semua Produk';
const productCategoryName = const productCategoryName = filterParams.product_category_ids
appliedFilterProductCategory.length > 0 ? productCategoryOptions
? appliedFilterProductCategory.map((c) => c.label).join(', ') || .filter((opt) =>
'Semua Kategori Produk' filterParams.product_category_ids
: 'Semua Kategori Produk'; ?.split(',')
.includes(String(opt.value))
)
.map((opt) => opt.label)
.join(', ') || 'Semua Kategori Produk'
: 'Semua Kategori Produk';
const exportParams = { const exportParams = {
area_name: areaName, area_name: areaName,
supplier_name: supplierName, supplier_name: supplierName,
product_name: productName, product_name: productName,
product_category_name: productCategoryName, product_category_name: productCategoryName,
filter_by: filter_by: filterParams.filter_by,
(appliedFilterByType?.value as 'received_date' | 'po_date') || start_date: filterParams.start_date,
undefined, end_date: filterParams.end_date,
start_date: appliedFilterStartDate || undefined,
end_date: appliedFilterEndDate || undefined,
}; };
await generatePurchasesPerSupplierPDF({ await generatePurchasesPerSupplierPDF({
@@ -543,13 +441,11 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => {
} }
}, [ }, [
logisticPurchasePerSupplierExport, logisticPurchasePerSupplierExport,
appliedFilterArea, filterParams,
appliedFilterSupplier, areaOptions,
appliedFilterProduct, supplierOptions,
appliedFilterProductCategory, productOptions,
appliedFilterStartDate, productCategoryOptions,
appliedFilterEndDate,
appliedFilterByType,
]); ]);
// ===== REGISTER TAB ACTIONS TO STORE ===== // ===== REGISTER TAB ACTIONS TO STORE =====
@@ -624,6 +520,7 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => {
</Dropdown> </Dropdown>
</div> </div>
); );
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ }, [
tabId, tabId,
hasFilters, hasFilters,
@@ -633,6 +530,7 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => {
setTabActions, setTabActions,
]); ]);
// Cleanup on unmount
useEffect(() => { useEffect(() => {
return () => { return () => {
clearTabActions(tabId); clearTabActions(tabId);
@@ -945,137 +843,184 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => {
<Icon icon='heroicons:x-mark' width={20} height={20} /> <Icon icon='heroicons:x-mark' width={20} height={20} />
</Button> </Button>
</div> </div>
<div className='p-4 flex flex-col gap-3'> <form onSubmit={formik.handleSubmit} onReset={formik.handleReset}>
{/* Date Filter */} <div className='p-4 flex flex-col gap-3'>
<div> {/* Date Filter */}
<label className='block text-xs font-semibold text-base-content py-2'> <div>
Tanggal <label className='block text-xs font-semibold text-base-content py-2'>
</label> Tanggal
<div className='flex flex-row gap-1.5 items-center justify-between'> </label>
<DateInput <div className='flex flex-row gap-1.5 items-center justify-between'>
name='start_date' <DateInput
value={filterStartDate} name='start_date'
onChange={handleStartDateChange} value={formik.values.start_date || ''}
className={{ wrapper: 'w-full' }} onChange={handleStartDateChange}
isNestedModal className={{ wrapper: 'w-full' }}
/> isNestedModal
<hr className='w-full max-w-3 h-px border-base-content/10' /> />
<DateInput <hr className='w-full max-w-3 h-px border-base-content/10' />
name='end_date' <DateInput
value={filterEndDate} name='end_date'
onChange={handleEndDateChange} value={formik.values.end_date || ''}
className={{ wrapper: 'w-full' }} onChange={handleEndDateChange}
isNestedModal className={{ wrapper: 'w-full' }}
/> isNestedModal
isError={hasDateError}
/>
</div>
</div> </div>
{/* Area Filter */}
<SelectInputCheckbox
label='Area'
placeholder='Pilih Area'
options={areaOptions}
value={
(formik.values.area_ids as
| { value: number; label: string }
| { value: number; label: string }[]
| null
| undefined) || []
}
onChange={(val) => {
formik.setFieldValue(
'area_ids',
Array.isArray(val) ? val : val ? [val] : null
);
}}
isLoading={isLoadingAreas}
isClearable
className={{ wrapper: 'w-full' }}
/>
{/* Supplier Filter */}
<SelectInputCheckbox
label='Supplier'
placeholder='Pilih Supplier'
options={supplierOptions}
value={
(formik.values.supplier_ids as
| { value: number; label: string }
| { value: number; label: string }[]
| null
| undefined) || []
}
onChange={(val) => {
formik.setFieldValue(
'supplier_ids',
Array.isArray(val) ? val : val ? [val] : null
);
}}
isLoading={isLoadingSuppliers}
isClearable
className={{ wrapper: 'w-full' }}
/>
{/* Product Filter */}
<SelectInputCheckbox
label='Produk'
placeholder='Pilih Produk'
options={productOptions}
value={
(formik.values.product_ids as
| { value: number; label: string }
| { value: number; label: string }[]
| null
| undefined) || []
}
onChange={(val) => {
formik.setFieldValue(
'product_ids',
Array.isArray(val) ? val : val ? [val] : null
);
}}
isLoading={isLoadingProducts}
isClearable
className={{ wrapper: 'w-full' }}
/>
{/* Product Category Filter */}
<SelectInputCheckbox
label='Kategori Produk'
placeholder='Pilih Kategori Produk'
options={productCategoryOptions}
value={
(formik.values.product_category_ids as
| { value: number; label: string }
| { value: number; label: string }[]
| null
| undefined) || []
}
onChange={(val) => {
formik.setFieldValue(
'product_category_ids',
Array.isArray(val) ? val : val ? [val] : null
);
}}
isLoading={isLoadingProductCategories}
isClearable
className={{ wrapper: 'w-full' }}
/>
{/* Filter By Type */}
<SelectInputRadio
label='Filter Berdasarkan'
placeholder='Pilih Filter Berdasarkan'
options={dataTypeOptions}
value={
(formik.values.filter_by as
| { value: string; label: string }
| null
| undefined) || null
}
onChange={(val) => {
if (!Array.isArray(val)) {
formik.setFieldValue('filter_by', val);
}
}}
className={{ wrapper: 'w-full' }}
isClearable={true}
/>
{/* Sort By */}
<SelectInputRadio
label='Urutkan Berdasarkan'
placeholder='Pilih Urutkan Berdasarkan'
options={sortByOptions}
value={
(formik.values.sort_by as
| { value: string; label: string }
| null
| undefined) || null
}
onChange={(val) => {
if (!Array.isArray(val)) {
formik.setFieldValue('sort_by', val);
}
}}
className={{ wrapper: 'w-full' }}
isClearable={true}
/>
</div> </div>
{/* Area Filter */} {/* Modal Footer */}
<SelectInputCheckbox <div className='flex justify-between items-center gap-4 p-4 border-t border-base-content/10 bg-gray-50'>
label='Area' <Button
placeholder='Pilih Area' type='reset'
options={areaOptions} variant='soft'
value={filterArea} 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'
onChange={(val) => { >
setFilterArea(Array.isArray(val) ? val : val ? [val] : []); Reset Filter
}} </Button>
isLoading={isLoadingAreas} <Button
isClearable type='submit'
className={{ wrapper: 'w-full' }} className='min-w-40 text-sm rounded-lg py-3 text-white font-semibold'
/> disabled={hasDateError || !formik.isValid || formik.isSubmitting}
>
{/* Supplier Filter */} Apply Filter
<SelectInputCheckbox </Button>
label='Supplier' </div>
placeholder='Pilih Supplier' </form>
options={supplierOptions}
value={filterSupplier}
onChange={(val) => {
setFilterSupplier(Array.isArray(val) ? val : val ? [val] : []);
}}
isLoading={isLoadingSuppliers}
isClearable
className={{ wrapper: 'w-full' }}
/>
{/* Product Filter */}
<SelectInputCheckbox
label='Produk'
placeholder='Pilih Produk'
options={productOptions}
value={filterProduct}
onChange={(val) => {
setFilterProduct(Array.isArray(val) ? val : val ? [val] : []);
}}
isLoading={isLoadingProducts}
isClearable
className={{ wrapper: 'w-full' }}
/>
{/* Product Category Filter */}
<SelectInputCheckbox
label='Kategori Produk'
placeholder='Pilih Kategori Produk'
options={productCategoryOptions}
value={filterProductCategory}
onChange={(val) => {
setFilterProductCategory(
Array.isArray(val) ? val : val ? [val] : []
);
}}
isLoading={isLoadingProductCategories}
isClearable
className={{ wrapper: 'w-full' }}
/>
{/* Filter By Type */}
<SelectInputRadio
label='Filter Berdasarkan'
placeholder='Pilih Filter Berdasarkan'
options={dataTypeOptions}
value={filterByType}
onChange={(val) => {
if (!Array.isArray(val)) {
setFilterByType(val);
}
}}
className={{ wrapper: 'w-full' }}
isClearable={true}
/>
{/* Sort By */}
<SelectInputRadio
label='Urutkan Berdasarkan'
placeholder='Pilih Urutkan Berdasarkan'
options={sortByOptions}
value={filterSortBy}
onChange={(val) => {
if (!Array.isArray(val)) {
setFilterSortBy(val);
}
}}
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
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'
onClick={handleResetFilters}
>
Reset Filter
</Button>
<Button
className='min-w-40 text-sm rounded-lg py-3 text-white font-semibold'
onClick={handleApplyFilters}
disabled={hasDateError}
>
Apply Filter
</Button>
</div>
</Modal> </Modal>
</> </>
); );