From 2a367ce0c08c6f5eff181ca13f1d1875793a82c1 Mon Sep 17 00:00:00 2001 From: MacBook Air M1 Date: Fri, 19 Jun 2026 09:05:44 +0700 Subject: [PATCH] adjust filter po --- .../pages/purchase/PurchaseInlineFilter.tsx | 521 ++++++++++++++++++ .../pages/purchase/PurchaseTable.tsx | 61 +- 2 files changed, 530 insertions(+), 52 deletions(-) create mode 100644 src/components/pages/purchase/PurchaseInlineFilter.tsx diff --git a/src/components/pages/purchase/PurchaseInlineFilter.tsx b/src/components/pages/purchase/PurchaseInlineFilter.tsx new file mode 100644 index 00000000..6ad624cb --- /dev/null +++ b/src/components/pages/purchase/PurchaseInlineFilter.tsx @@ -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[] = [ + { 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 | undefined; + category: OptionType[]; + status: OptionType[]; + supplier: OptionType | null; + area: OptionType | null; + location: OptionType | null; + project_flock: OptionType | null; + project_flock_kandang: OptionType | null; + }; + searchValue?: string; + onSearchChange?: ChangeEventHandler; + 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( + 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(SupplierApi.basePath, 'id', 'name', 'search'); + + const { + setInputValue: setAreaInputValue, + options: areaOptions, + isLoadingOptions: isLoadingAreaOptions, + loadMore: loadMoreAreas, + } = useSelect(AreaApi.basePath, 'id', 'name', 'search'); + + const { + setInputValue: setLocationInputValue, + options: locationOptions, + isLoadingOptions: isLoadingLocationOptions, + loadMore: loadMoreLocations, + } = useSelect(LocationApi.basePath, 'id', 'name', 'search', { + area_id: selectedAreaId || '', + }); + + const { + setInputValue: setProjectFlockInputValue, + options: projectFlockOptions, + rawData: projectFlocksRawData, + isLoadingOptions: isLoadingProjectFlockOptions, + loadMore: loadMoreProjectFlocks, + } = useSelect( + ProjectFlockApi.basePath, + 'id', + 'flock_name', + 'search', + { + location_id: selectedLocationId || '', + } + ); + + const formik = useFormik<{ + poDate: string; + start_date: string; + end_date: string; + filterBy: OptionType | undefined; + category: { label: string; value: number }[]; + status: { label: string; value: string }[]; + supplier: OptionType | null; + area: OptionType | null; + location: OptionType | null; + project_flock: OptionType | null; + project_flock_kandang: OptionType | 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) => { + 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) => { + 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 ( +
+
+ {/* Row 1: Tanggal Awal, Tanggal Akhir, Filter Berdasarkan, PO Date */} + + + + + + formik.setFieldValue( + 'filterBy', + !Array.isArray(val) ? (val ?? undefined) : undefined + ) + } + isClearable + /> + + + + {/* Row 2: Area, Lokasi, Project Flock, Kandang */} + { + const nextValue = !Array.isArray(val) + ? (val as OptionType | 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 + /> + + { + const nextValue = !Array.isArray(val) + ? (val as OptionType | 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} + /> + + { + const nextValue = !Array.isArray(val) + ? (val as OptionType | 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} + /> + + + formik.setFieldValue( + 'project_flock_kandang', + !Array.isArray(val) ? (val as OptionType | null) : null + ) + } + options={projectFlockKandangOptions} + isClearable + isDisabled={!formik.values.project_flock} + /> + + {/* Row 3: Kategori, Status, Vendor, Search */} + + + ({ + label: item.step_name, + value: item.step_name, + }))} + /> + + + formik.setFieldValue( + 'supplier', + !Array.isArray(val) ? (val as OptionType | null) : null + ) + } + options={supplierOptions} + isLoading={isLoadingSupplierOptions} + onInputChange={setSupplierInputValue} + onMenuScrollToBottom={loadMoreSuppliers} + isClearable + /> + +
+ + + } + className={{ + inputWrapper: 'rounded-xl! shadow-button-soft', + input: + 'placeholder:font-semibold placeholder:text-base-content/50', + }} + /> +

+ Bisa cari PR/PO, vendor, produk, atau PO ekspedisi (biaya + pengiriman). +

+
+
+ +
+ + + +
+
+ ); +}; + +export default PurchaseInlineFilter; diff --git a/src/components/pages/purchase/PurchaseTable.tsx b/src/components/pages/purchase/PurchaseTable.tsx index 1e2da838..3476127a 100644 --- a/src/components/pages/purchase/PurchaseTable.tsx +++ b/src/components/pages/purchase/PurchaseTable.tsx @@ -13,7 +13,6 @@ 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'; import DateInput from '@/components/input/DateInput'; import Button from '@/components/Button'; import Modal, { useModal } from '@/components/Modal'; @@ -23,8 +22,7 @@ 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 PurchaseInlineFilter from '@/components/pages/purchase/PurchaseInlineFilter'; import Dropdown from '@/components/dropdown/Dropdown'; import { OptionType } from '@/components/input/SelectInput'; @@ -258,7 +256,6 @@ const PurchaseTable = () => { }; // ===== MODAL HOOKS ===== - const filterModal = useModal(); const deleteModal = useModal(); const exportProgressInputModal = useModal(); @@ -878,47 +875,6 @@ const PurchaseTable = () => {
- - } - 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', - }} - /> - - - {
+ + {/* Table Section */}
{isLoading ? ( @@ -1033,13 +997,6 @@ const PurchaseTable = () => { {/* ===== MODAL COMPONENTS ===== */} - -