adjust filter po

This commit is contained in:
MacBook Air M1
2026-06-19 09:05:44 +07:00
parent cca9c5175f
commit 2a367ce0c0
2 changed files with 530 additions and 52 deletions
@@ -0,0 +1,521 @@
'use client';
import {
ChangeEventHandler,
useState,
useEffect,
useMemo,
useCallback,
} from 'react';
import { useFormik } from 'formik';
import toast from 'react-hot-toast';
import { Icon } from '@iconify/react';
import Button from '@/components/Button';
import DateInput from '@/components/input/DateInput';
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
import SelectInputCheckbox from '@/components/input/SelectInputCheckbox';
import SelectInput from '@/components/input/SelectInput';
import SelectInputRadio from '@/components/input/SelectInputRadio';
import { OptionType, useSelect } from '@/components/input/SelectInput';
import { PurchaseFilter } from '@/types/api/purchase/purchase';
import { AreaApi, LocationApi, SupplierApi } from '@/services/api/master-data';
import { ProductCategory } from '@/types/api/master-data/product-category';
import { ProductCategoryApi } from '@/services/api/master-data';
import { Area } from '@/types/api/master-data/area';
import { Location } from '@/types/api/master-data/location';
import { Supplier } from '@/types/api/master-data/supplier';
import { PURCHASE_ORDER_APPROVAL_LINE } from '@/config/approval-line';
import { ProjectFlockApi } from '@/services/api/production';
import { ProjectFlock } from '@/types/api/production/project-flock';
import { isResponseSuccess } from '@/lib/api-helper';
const filterByOptions: OptionType<string>[] = [
{ value: 'po_date', label: 'Tanggal PO' },
{ value: 'received_date', label: 'Tanggal Terima' },
{ value: 'due_date', label: 'Tanggal Jatuh Tempo' },
{ value: 'created_at', label: 'Tanggal Dibuat' },
];
interface PurchaseInlineFilterProps {
initialValues?: {
poDate: string;
start_date: string;
end_date: string;
filterBy: OptionType<string> | undefined;
category: OptionType<number>[];
status: OptionType<string>[];
supplier: OptionType<number> | null;
area: OptionType<number> | null;
location: OptionType<number> | null;
project_flock: OptionType<number> | null;
project_flock_kandang: OptionType<number> | null;
};
searchValue?: string;
onSearchChange?: ChangeEventHandler<HTMLInputElement>;
onSubmit?: (values: PurchaseFilter) => void;
onReset?: () => void;
}
const PurchaseInlineFilter = ({
initialValues,
searchValue,
onSearchChange,
onSubmit,
onReset,
}: PurchaseInlineFilterProps) => {
const [hasDateError, setHasDateError] = useState(false);
const [dateErrorShown, setDateErrorShown] = useState(false);
useEffect(() => {
return () => {
if (dateErrorShown) {
toast.dismiss();
}
};
}, [dateErrorShown]);
const {
setInputValue: setProductCategoryInputValue,
options: productCategoryOptions,
isLoadingOptions: isLoadingProductCategoryOptions,
loadMore: loadMoreProductCategory,
} = useSelect<ProductCategory>(
ProductCategoryApi.basePath,
'id',
'name',
'search'
);
const [selectedAreaId, setSelectedAreaId] = useState(
initialValues?.area?.value ? String(initialValues.area.value) : ''
);
const [selectedLocationId, setSelectedLocationId] = useState(
initialValues?.location?.value ? String(initialValues.location.value) : ''
);
const {
setInputValue: setSupplierInputValue,
options: supplierOptions,
isLoadingOptions: isLoadingSupplierOptions,
loadMore: loadMoreSuppliers,
} = useSelect<Supplier>(SupplierApi.basePath, 'id', 'name', 'search');
const {
setInputValue: setAreaInputValue,
options: areaOptions,
isLoadingOptions: isLoadingAreaOptions,
loadMore: loadMoreAreas,
} = useSelect<Area>(AreaApi.basePath, 'id', 'name', 'search');
const {
setInputValue: setLocationInputValue,
options: locationOptions,
isLoadingOptions: isLoadingLocationOptions,
loadMore: loadMoreLocations,
} = useSelect<Location>(LocationApi.basePath, 'id', 'name', 'search', {
area_id: selectedAreaId || '',
});
const {
setInputValue: setProjectFlockInputValue,
options: projectFlockOptions,
rawData: projectFlocksRawData,
isLoadingOptions: isLoadingProjectFlockOptions,
loadMore: loadMoreProjectFlocks,
} = useSelect<ProjectFlock>(
ProjectFlockApi.basePath,
'id',
'flock_name',
'search',
{
location_id: selectedLocationId || '',
}
);
const formik = useFormik<{
poDate: string;
start_date: string;
end_date: string;
filterBy: OptionType<string> | undefined;
category: { label: string; value: number }[];
status: { label: string; value: string }[];
supplier: OptionType<number> | null;
area: OptionType<number> | null;
location: OptionType<number> | null;
project_flock: OptionType<number> | null;
project_flock_kandang: OptionType<number> | null;
}>({
initialValues: initialValues || {
poDate: '',
start_date: '',
end_date: '',
filterBy: undefined,
category: [],
status: [],
supplier: null,
area: null,
location: null,
project_flock: null,
project_flock_kandang: null,
},
onSubmit: async (values) => {
const formattedValues = {
...values,
category: values.category.map((item) => String(item.value)),
category_labels: values.category,
status: values.status.map((item) => String(item.value)),
supplier_id: values.supplier?.value,
supplier_label: values.supplier?.label,
area_id: values.area?.value,
area_label: values.area?.label,
location_id: values.location?.value,
location_label: values.location?.label,
project_flock_id: values.project_flock?.value,
project_flock_label: values.project_flock?.label,
project_flock_kandang_id: values.project_flock_kandang?.value,
project_flock_kandang_label: values.project_flock_kandang?.label,
};
onSubmit?.(formattedValues);
},
});
const { resetForm, submitForm } = formik;
useEffect(() => {
setSelectedAreaId(
initialValues?.area?.value ? String(initialValues.area.value) : ''
);
setSelectedLocationId(
initialValues?.location?.value ? String(initialValues.location.value) : ''
);
}, [initialValues?.area, initialValues?.location]);
const projectFlockKandangOptions = useMemo(() => {
if (
!formik.values.project_flock ||
!projectFlocksRawData ||
!isResponseSuccess(projectFlocksRawData)
) {
return [];
}
const selectedProjectFlock = projectFlocksRawData.data.find(
(item) => item.id === formik.values.project_flock?.value
);
return (
selectedProjectFlock?.kandangs?.map((item) => ({
value: item.project_flock_kandang_id,
label: item.name,
})) || []
);
}, [formik.values.project_flock, projectFlocksRawData]);
const productCategoryChangeHandler = (
val: OptionType | OptionType[] | null
) => {
formik.setFieldValue('category', val);
};
const statusChangeHandler = (val: OptionType | OptionType[] | null) => {
formik.setFieldValue('status', val);
};
const formikResetHandler = useCallback(() => {
setHasDateError(false);
if (dateErrorShown) {
toast.dismiss();
setDateErrorShown(false);
}
resetForm({
values: {
poDate: '',
start_date: '',
end_date: '',
filterBy: undefined,
category: [],
status: [],
supplier: null,
area: null,
location: null,
project_flock: null,
project_flock_kandang: null,
},
});
setSelectedAreaId('');
setSelectedLocationId('');
onReset?.();
}, [resetForm, onReset, dateErrorShown]);
const handleStartDateChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
formik.setFieldValue('start_date', value);
if (value && formik.values.end_date) {
if (new Date(formik.values.end_date) < new Date(value)) {
setHasDateError(true);
if (!dateErrorShown) {
toast.error('Tanggal akhir tidak boleh sebelum tanggal mulai', {
duration: Infinity,
});
setDateErrorShown(true);
}
} else {
setHasDateError(false);
if (dateErrorShown) {
toast.dismiss();
setDateErrorShown(false);
}
}
} else {
setHasDateError(false);
}
};
const handleEndDateChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
formik.setFieldValue('end_date', value);
if (value && formik.values.start_date) {
if (new Date(value) < new Date(formik.values.start_date)) {
setHasDateError(true);
if (!dateErrorShown) {
toast.error('Tanggal akhir tidak boleh sebelum tanggal mulai', {
duration: Infinity,
});
setDateErrorShown(true);
}
return;
}
}
setHasDateError(false);
if (dateErrorShown) {
toast.dismiss();
setDateErrorShown(false);
}
};
const formikSubmitHandler = useCallback(async () => {
await submitForm();
}, [submitForm]);
return (
<form
onSubmit={formik.handleSubmit}
onReset={formikResetHandler}
className='w-full p-4 border-b border-base-content/10'
>
<div className='grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-x-4 gap-y-1'>
{/* Row 1: Tanggal Awal, Tanggal Akhir, Filter Berdasarkan, PO Date */}
<DateInput
label='Tanggal Awal'
name='start_date'
placeholder='Pilih tanggal'
value={formik.values.start_date}
onChange={handleStartDateChange}
/>
<DateInput
label='Tanggal Akhir'
name='end_date'
placeholder='Pilih tanggal'
value={formik.values.end_date}
onChange={handleEndDateChange}
isError={hasDateError}
/>
<SelectInputRadio
label='Filter Berdasarkan'
placeholder='Pilih Filter Berdasarkan'
options={filterByOptions}
value={formik.values.filterBy ?? null}
onChange={(val) =>
formik.setFieldValue(
'filterBy',
!Array.isArray(val) ? (val ?? undefined) : undefined
)
}
isClearable
/>
<DateInput
label='PO Date'
name='poDate'
placeholder='Pilih Tanggal'
value={formik.values.poDate}
onChange={formik.handleChange}
/>
{/* Row 2: Area, Lokasi, Project Flock, Kandang */}
<SelectInput
label='Area'
placeholder='Pilih Area'
value={formik.values.area}
onChange={(val) => {
const nextValue = !Array.isArray(val)
? (val as OptionType<number> | null)
: null;
formik.setFieldValue('area', nextValue);
formik.setFieldValue('location', null);
formik.setFieldValue('project_flock', null);
formik.setFieldValue('project_flock_kandang', null);
setSelectedAreaId(nextValue?.value ? String(nextValue.value) : '');
setSelectedLocationId('');
}}
options={areaOptions}
isLoading={isLoadingAreaOptions}
onInputChange={setAreaInputValue}
onMenuScrollToBottom={loadMoreAreas}
isClearable
/>
<SelectInput
label='Lokasi'
placeholder='Pilih Lokasi'
value={formik.values.location}
onChange={(val) => {
const nextValue = !Array.isArray(val)
? (val as OptionType<number> | null)
: null;
formik.setFieldValue('location', nextValue);
formik.setFieldValue('project_flock', null);
formik.setFieldValue('project_flock_kandang', null);
setSelectedLocationId(
nextValue?.value ? String(nextValue.value) : ''
);
}}
options={locationOptions}
isLoading={isLoadingLocationOptions}
onInputChange={setLocationInputValue}
onMenuScrollToBottom={loadMoreLocations}
isClearable
isDisabled={!formik.values.area}
/>
<SelectInput
label='Project Flock'
placeholder='Pilih Project Flock'
value={formik.values.project_flock}
onChange={(val) => {
const nextValue = !Array.isArray(val)
? (val as OptionType<number> | null)
: null;
formik.setFieldValue('project_flock', nextValue);
formik.setFieldValue('project_flock_kandang', null);
}}
options={projectFlockOptions}
isLoading={isLoadingProjectFlockOptions}
onInputChange={setProjectFlockInputValue}
onMenuScrollToBottom={loadMoreProjectFlocks}
isClearable
isDisabled={!formik.values.location}
/>
<SelectInput
label='Kandang'
placeholder='Pilih Kandang'
value={formik.values.project_flock_kandang}
onChange={(val) =>
formik.setFieldValue(
'project_flock_kandang',
!Array.isArray(val) ? (val as OptionType<number> | null) : null
)
}
options={projectFlockKandangOptions}
isClearable
isDisabled={!formik.values.project_flock}
/>
{/* Row 3: Kategori, Status, Vendor, Search */}
<SelectInputCheckbox
label='Kategori Produk'
placeholder='Pilih Kategori'
value={formik.values.category}
onChange={productCategoryChangeHandler}
options={productCategoryOptions}
isLoading={isLoadingProductCategoryOptions}
onInputChange={setProductCategoryInputValue}
onMenuScrollToBottom={loadMoreProductCategory}
/>
<SelectInputCheckbox
label='Status'
placeholder='Pilih Status'
value={formik.values.status}
onChange={statusChangeHandler}
options={PURCHASE_ORDER_APPROVAL_LINE.map((item) => ({
label: item.step_name,
value: item.step_name,
}))}
/>
<SelectInput
label='Vendor'
placeholder='Pilih Vendor'
value={formik.values.supplier}
onChange={(val) =>
formik.setFieldValue(
'supplier',
!Array.isArray(val) ? (val as OptionType<number> | null) : null
)
}
options={supplierOptions}
isLoading={isLoadingSupplierOptions}
onInputChange={setSupplierInputValue}
onMenuScrollToBottom={loadMoreSuppliers}
isClearable
/>
<div className='flex flex-col'>
<label className='block text-xs font-semibold text-base-content py-2'>
Search
</label>
<DebouncedTextInput
name='search'
placeholder='Cari...'
value={searchValue ?? ''}
onChange={onSearchChange}
startAdornment={
<Icon icon='heroicons:magnifying-glass' width={20} height={20} />
}
className={{
inputWrapper: 'rounded-xl! shadow-button-soft',
input:
'placeholder:font-semibold placeholder:text-base-content/50',
}}
/>
<p className='text-xs text-base-content/50 mt-1'>
Bisa cari PR/PO, vendor, produk, atau PO ekspedisi (biaya
pengiriman).
</p>
</div>
</div>
<div className='flex justify-end gap-3 mt-4'>
<Button
type='reset'
variant='outline'
color='none'
className='px-4 py-2.5 rounded-xl text-sm text-base-content/65'
>
Reset
</Button>
<Button
type='button'
onClick={formikSubmitHandler}
disabled={hasDateError}
className='px-4 py-2.5 rounded-xl text-sm text-base-100'
>
Cari
</Button>
</div>
</form>
);
};
export default PurchaseInlineFilter;
@@ -13,7 +13,6 @@ import toast from 'react-hot-toast';
import Link from 'next/link'; import Link from 'next/link';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
import Table from '@/components/Table'; import Table from '@/components/Table';
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
import DateInput from '@/components/input/DateInput'; import DateInput from '@/components/input/DateInput';
import Button from '@/components/Button'; import Button from '@/components/Button';
import Modal, { useModal } from '@/components/Modal'; import Modal, { useModal } from '@/components/Modal';
@@ -23,8 +22,7 @@ import PopoverContent from '@/components/popover/PopoverContent';
import RequirePermission from '@/components/helper/RequirePermission'; import RequirePermission from '@/components/helper/RequirePermission';
import StatusBadge from '@/components/helper/StatusBadge'; import StatusBadge from '@/components/helper/StatusBadge';
import PurchaseTableSkeleton from '@/components/pages/purchase/skeleton/PurchaseTableSkeleton'; import PurchaseTableSkeleton from '@/components/pages/purchase/skeleton/PurchaseTableSkeleton';
import ButtonFilter from '@/components/helper/ButtonFilter'; import PurchaseInlineFilter from '@/components/pages/purchase/PurchaseInlineFilter';
import PurchaseFilterModal from '@/components/pages/purchase/PurchaseFilterModal';
import Dropdown from '@/components/dropdown/Dropdown'; import Dropdown from '@/components/dropdown/Dropdown';
import { OptionType } from '@/components/input/SelectInput'; import { OptionType } from '@/components/input/SelectInput';
@@ -258,7 +256,6 @@ const PurchaseTable = () => {
}; };
// ===== MODAL HOOKS ===== // ===== MODAL HOOKS =====
const filterModal = useModal();
const deleteModal = useModal(); const deleteModal = useModal();
const exportProgressInputModal = useModal(); const exportProgressInputModal = useModal();
@@ -878,47 +875,6 @@ const PurchaseTable = () => {
</div> </div>
<div className='flex flex-1 flex-row justify-start sm:justify-end items-center gap-3 flex-wrap'> <div className='flex flex-1 flex-row justify-start sm:justify-end items-center gap-3 flex-wrap'>
<DebouncedTextInput
name='search'
placeholder='Search'
value={tableFilterState.search}
onChange={searchChangeHandler}
startAdornment={
<Icon
icon='heroicons:magnifying-glass'
width={20}
height={20}
/>
}
className={{
wrapper: 'w-full min-w-24 max-w-3xs',
inputWrapper: 'rounded-xl! shadow-button-soft',
input:
'placeholder:font-semibold placeholder:text-base-content/50',
}}
/>
<ButtonFilter
values={tableFilterState}
excludeFields={[
'page',
'pageSize',
'search',
'filter_by',
'sort_by',
'order_by',
'product_category_name',
'supplier_name',
'area_name',
'location_name',
'project_flock_name',
'project_flock_kandang_name',
]}
fieldGroups={[['start_date', 'end_date']]}
onClick={filterModal.openModal}
className='px-3 py-2.5'
/>
<Dropdown <Dropdown
align='end' align='end'
direction='bottom' direction='bottom'
@@ -976,6 +932,14 @@ const PurchaseTable = () => {
</div> </div>
</div> </div>
<PurchaseInlineFilter
initialValues={purchaseFilterInitialValues}
searchValue={tableFilterState.search}
onSearchChange={searchChangeHandler}
onSubmit={filterSubmitHandler}
onReset={filterResetHandler}
/>
{/* Table Section */} {/* Table Section */}
<div className='flex flex-col mb-4'> <div className='flex flex-col mb-4'>
{isLoading ? ( {isLoading ? (
@@ -1033,13 +997,6 @@ const PurchaseTable = () => {
{/* ===== MODAL COMPONENTS ===== */} {/* ===== MODAL COMPONENTS ===== */}
<PurchaseFilterModal
ref={filterModal.ref}
initialValues={purchaseFilterInitialValues}
onSubmit={filterSubmitHandler}
onReset={filterResetHandler}
/>
<ConfirmationModal <ConfirmationModal
ref={deleteModal.ref} ref={deleteModal.ref}
type='error' type='error'