Merge branch 'fix/purchasing-filter' into 'development'

[FIX/FE] Purchase Filter

See merge request mbugroup/lti-web-client!363
This commit is contained in:
Rivaldi A N S
2026-04-02 03:00:15 +00:00
3 changed files with 250 additions and 9 deletions
@@ -0,0 +1,201 @@
'use client';
import { RefObject, useState, useEffect } from 'react';
import { useFormik } from 'formik';
import toast from 'react-hot-toast';
import { Icon } from '@iconify/react';
import Modal from '@/components/Modal';
import Button from '@/components/Button';
import DateInput from '@/components/input/DateInput';
import SelectInputCheckbox from '@/components/input/SelectInputCheckbox';
import { OptionType, useSelect } from '@/components/input/SelectInput';
import { PurchaseFilter } from '@/types/api/purchase/purchase';
import { ProductCategory } from '@/types/api/master-data/product-category';
import { ProductCategoryApi } from '@/services/api/master-data';
import { PURCHASE_ORDER_APPROVAL_LINE } from '@/config/approval-line';
interface PurchaseFilterModalProps {
ref: RefObject<HTMLDialogElement | null>;
onSubmit?: (values: PurchaseFilter) => void;
onReset?: () => void;
}
const PurchaseFilterModal = ({
ref,
onSubmit,
onReset,
}: PurchaseFilterModalProps) => {
const closeModalHandler = () => {
ref.current?.close();
};
// ===== DATE ERROR STATE =====
const [dateErrorShown, setDateErrorShown] = useState(false);
const [hasDateError, setHasDateError] = useState(false);
// ===== CLEANUP TOAST ON UNMOUNT =====
useEffect(() => {
return () => {
if (dateErrorShown) {
toast.dismiss();
}
};
}, [dateErrorShown]);
// ===== CLEANUP TOAST WHEN MODAL CLOSES =====
useEffect(() => {
const dialogElement = ref.current;
const handleModalClose = () => {
if (dateErrorShown) {
toast.dismiss();
setDateErrorShown(false);
}
};
dialogElement?.addEventListener('close', handleModalClose);
return () => {
dialogElement?.removeEventListener('close', handleModalClose);
};
}, [ref, dateErrorShown]);
const {
setInputValue: setProductCategoryInputValue,
options: productCategoryOptions,
isLoadingOptions: isLoadingProductCategoryOptions,
loadMore: loadMoreProductCategory,
} = useSelect<ProductCategory>(
ProductCategoryApi.basePath,
'id',
'name',
'search'
);
const formik = useFormik<{
poDate: string;
category: { label: string; value: number }[];
status: { label: string; value: string }[];
}>({
initialValues: {
poDate: '',
category: [],
status: [],
},
onSubmit: async (values) => {
const formattedValues = {
...values,
category: values.category.map((item) => String(item.value)),
status: values.status.map((item) => String(item.value)),
};
onSubmit?.(formattedValues);
closeModalHandler();
},
onReset: () => {
onReset?.();
closeModalHandler();
},
});
const productCategoryChangeHandler = (
val: OptionType | OptionType[] | null
) => {
formik.setFieldValue('category', val);
};
const statusChangeHandler = (val: OptionType | OptionType[] | null) => {
formik.setFieldValue('status', val);
};
return (
<Modal
ref={ref}
className={{
modalBox: 'p-0 rounded-xl',
}}
>
<form
onSubmit={formik.handleSubmit}
onReset={formik.handleReset}
className='w-full flex flex-col'
>
{/* Modal Header */}
<div className='p-4 flex items-center justify-between gap-2 border-b border-gray-300'>
<div className='flex items-center gap-2 text-primary'>
<Icon icon='heroicons:funnel' width={20} height={20} />
<h3 className='text-sm font-medium'>Filter Data</h3>
</div>
<Button
type='button'
variant='ghost'
color='none'
onClick={closeModalHandler}
className='p-0 text-base-content/50 hover:text-base-content'
>
<Icon icon='heroicons:x-mark' width={20} height={20} />
</Button>
</div>
{/* Modal Body */}
<div className='p-4 flex flex-col gap-1.5'>
<div className='flex flex-col'>
<DateInput
label='PO Date'
name='poDate'
placeholder='Pilih Tanggal'
value={formik.values.poDate}
onChange={formik.handleChange}
isNestedModal
/>
<SelectInputCheckbox
label='Kategori'
placeholder='Pilih Kategori'
value={formik.values.category}
onChange={productCategoryChangeHandler}
options={productCategoryOptions}
isLoading={isLoadingProductCategoryOptions}
onInputChange={setProductCategoryInputValue}
onMenuScrollToBottom={loadMoreProductCategory}
/>
<SelectInputCheckbox
label='Status'
placeholder='Status'
value={formik.values.status}
onChange={statusChangeHandler}
options={PURCHASE_ORDER_APPROVAL_LINE.map((item) => ({
label: item.step_name,
value: item.step_name,
}))}
/>
</div>
</div>
{/* Modal Footer */}
<div className='p-4 flex justify-between gap-4 border-t border-gray-300 bg-gray-100'>
<Button
type='reset'
variant='ghost'
color='none'
className='p-3 rounded-lg text-base-content/65'
>
Reset Filter
</Button>
<Button
type='submit'
className='p-3 rounded-lg w-fit sm:w-full max-w-40 text-base-100 text-sm'
>
Apply Filter
</Button>
</div>
</form>
</Modal>
);
};
export default PurchaseFilterModal;
@@ -14,6 +14,7 @@ import useSWRInfinite from 'swr/infinite';
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
import toast from 'react-hot-toast';
import Link from 'next/link';
import { Icon } from '@iconify/react';
import Table from '@/components/Table';
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
@@ -25,18 +26,19 @@ import PopoverContent from '@/components/popover/PopoverContent';
import RequirePermission from '@/components/helper/RequirePermission';
import StatusBadge from '@/components/helper/StatusBadge';
import PurchaseTableSkeleton from '@/components/pages/purchase/skeleton/PurchaseTableSkeleton';
import ButtonFilter from '@/components/helper/ButtonFilter';
import PurchaseFilterModal from '@/components/pages/purchase/PurchaseFilterModal';
import { cn, formatDate } from '@/lib/helper';
import { isResponseSuccess } from '@/lib/api-helper';
import { BaseApiResponse } from '@/types/api/api-general';
import { useTableFilter } from '@/services/hooks/useTableFilter';
import { Purchase } from '@/types/api/purchase/purchase';
import { Purchase, PurchaseFilter } from '@/types/api/purchase/purchase';
import { PurchaseApi } from '@/services/api/purchase';
import { ExpenseApi } from '@/services/api/expense';
import { Expense } from '@/types/api/expense';
import { Color } from '@/types/theme';
import Link from 'next/link';
// ===== STATUS BADGE UTILITIES =====
const statusTextMap: Record<string, string> = {
@@ -165,14 +167,21 @@ const PurchaseTable = () => {
} = useTableFilter({
initial: {
search: '',
po_date: '',
approval_status: '',
product_category_id: '',
},
paramMap: {
page: 'page',
pageSize: 'limit',
po_date: 'po_date',
approval_status: 'approval_status',
product_category_id: 'product_category_id',
},
});
// ===== MODAL HOOKS =====
const filterModal = useModal();
const deleteModal = useModal();
// ===== API DATA FETCHING =====
@@ -410,13 +419,17 @@ const PurchaseTable = () => {
[updateFilter, setSearchValue]
);
// const pageSizeChangeHandler = useCallback(
// (val: OptionType | OptionType[] | null) => {
// const newVal = val as OptionType;
// setPageSize(newVal.value as number);
// },
// [setPageSize]
// );
const filterSubmitHandler = (values: PurchaseFilter) => {
updateFilter('po_date', values.poDate);
updateFilter('product_category_id', values.category.join(','));
updateFilter('approval_status', values.status.join(','));
};
const filterResetHandler = () => {
updateFilter('po_date', '');
updateFilter('product_category_id', '');
updateFilter('approval_status', '');
};
return (
<>
@@ -455,6 +468,20 @@ const PurchaseTable = () => {
'placeholder:font-semibold placeholder:text-base-content/50',
}}
/>
<ButtonFilter
values={tableFilterState}
excludeFields={[
'page',
'pageSize',
'search',
'filter_by',
'sort_by',
]}
fieldGroups={[['startDate', 'endDate']]}
onClick={filterModal.openModal}
className='px-3 py-2.5'
/>
</div>
</div>
@@ -513,6 +540,13 @@ const PurchaseTable = () => {
</div>
{/* ===== MODAL COMPONENTS ===== */}
<PurchaseFilterModal
ref={filterModal.ref}
onSubmit={filterSubmitHandler}
onReset={filterResetHandler}
/>
<ConfirmationModal
ref={deleteModal.ref}
type='error'
+6
View File
@@ -144,3 +144,9 @@ export type DeletePurchaseRequestItemPayload = {
};
export type UpdatePurchaseRequestPayload = CreatePurchaseRequestPayload;
export type PurchaseFilter = {
poDate: string;
category: string[];
status: string[];
};