From d14fa2ed2b11f756e3241f68744bf3b2638c4208 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 24 Oct 2025 13:53:20 +0700 Subject: [PATCH] feat(FE-137): integrate advanced filtering options in RecordingTable with dropdowns for area, location, and kandang --- .../production/recording/RecordingTable.tsx | 291 ++++++++++++++++-- 1 file changed, 269 insertions(+), 22 deletions(-) diff --git a/src/components/pages/production/recording/RecordingTable.tsx b/src/components/pages/production/recording/RecordingTable.tsx index 15c891bb..f7cc451c 100644 --- a/src/components/pages/production/recording/RecordingTable.tsx +++ b/src/components/pages/production/recording/RecordingTable.tsx @@ -9,6 +9,7 @@ import { useModal } from '@/components/Modal'; import Button from '@/components/Button'; import ConfirmationModal from '@/components/modal/ConfirmationModal'; import { OptionType } from '@/components/input/SelectInput'; +import SelectInput from '@/components/input/SelectInput'; import { ROWS_OPTIONS } from '@/config/constant'; import CheckboxInput from '@/components/input/CheckboxInput'; import { TableToolbar } from '@/components/table/TableToolbar'; @@ -20,7 +21,11 @@ import { type CellContext } from '@tanstack/react-table'; import { type Recording } from '@/types/api/production/recording'; import { type BaseApiResponse } from '@/types/api/api-general'; import { RecordingApi } from '@/services/api/production'; +import { AreaApi } from '@/services/api/master-data'; +import { LocationApi } from '@/services/api/master-data'; +import { KandangApi } from '@/services/api/master-data'; import { isResponseSuccess, isResponseError } from '@/lib/api-helper'; +import { useTableFilter } from '@/services/hooks/useTableFilter'; import toast from 'react-hot-toast'; const RowOptionsMenu = ({ @@ -80,9 +85,31 @@ const RowOptionsMenu = ({ }; const RecordingTable = () => { - const [search, setSearch] = useState(''); - const [page, setPage] = useState(1); - const [pageSize, setPageSize] = useState(10); + const { + state: tableFilterState, + updateFilter, + setPage, + setPageSize, + toQueryString: getTableFilterQueryString, + } = useTableFilter({ + initial: { + search: '', + areaFilter: '', + locationFilter: '', + kandangFilter: '', + periodFilter: '', + }, + paramMap: { + page: 'page', + pageSize: 'limit', + search: 'search', + areaFilter: 'area_id', + locationFilter: 'location_id', + kandangFilter: 'kandang_id', + periodFilter: 'period', + }, + }); + const [sorting, setSorting] = useState([]); const [rowSelection, setRowSelection] = useState>({}); const [selectedRecording, setSelectedRecording] = useState(undefined); @@ -94,21 +121,81 @@ const RecordingTable = () => { const bulkApproveModal = useModal(); const bulkRejectModal = useModal(); + // State for dropdown search + const [locationSelectInputValue, setLocationSelectInputValue] = useState(''); + const [areaSelectInputValue, setAreaSelectInputValue] = useState(''); + const [kandangSelectInputValue, setKandangSelectInputValue] = useState(''); + + const [selectedArea, setSelectedArea] = useState(null); + const [selectedLocation, setSelectedLocation] = useState(null); + const [selectedKandang, setSelectedKandang] = useState(null); + const { data: recordings, isLoading, mutate: refreshRecordings, } = useSWR( - `${RecordingApi.basePath}?page=${page}&limit=${pageSize}`, + `${RecordingApi.basePath}${getTableFilterQueryString()}`, RecordingApi.getAllFetcher ); + // Fetch data for dropdowns + const areaUrl = `${AreaApi.basePath}?${new URLSearchParams({ + search: areaSelectInputValue, + limit: '100', + }).toString()}`; + const { + data: areas, + isLoading: isLoadingAreas, + } = useSWR(areaUrl, AreaApi.getAllFetcher); + + const locationUrl = `${LocationApi.basePath}?${new URLSearchParams({ + search: locationSelectInputValue, + area_id: selectedArea != null ? selectedArea.value.toString() : '', + limit: '100', + }).toString()}`; + const { + data: locations, + isLoading: isLoadingLocations, + } = useSWR(locationUrl, LocationApi.getAllFetcher); + + const kandangUrl = `${KandangApi.basePath}?${new URLSearchParams({ + search: kandangSelectInputValue, + location_id: + selectedLocation != null ? selectedLocation.value.toString() : '', + limit: '100', + }).toString()}`; + const { + data: kandangs, + isLoading: isLoadingKandang, + } = useSWR(kandangUrl, KandangApi.getAllFetcher); + + // Data to Options Mapping + const optionsArea = isResponseSuccess(areas) + ? areas?.data.map((area) => ({ + value: area.id, + label: area.name, + })) + : []; + const optionsLocation = isResponseSuccess(locations) + ? locations?.data.map((location) => ({ + value: location.id, + label: location.name, + })) + : []; + const optionsKandang = isResponseSuccess(kandangs) + ? kandangs?.data.map((kandang) => ({ + value: kandang.id, + label: kandang.name, + })) + : []; + const searchChangeHandler = useCallback( (e: React.ChangeEvent) => { - setSearch(e.target.value); + updateFilter('search', e.target.value); setPage(1); }, - [] + [updateFilter, setPage] ); const pageSizeChangeHandler = useCallback( @@ -117,22 +204,14 @@ const RecordingTable = () => { setPageSize(newVal.value as number); setPage(1); }, - [] + [setPageSize, setPage] ); const paginatedData = useMemo(() => { if (!recordings || recordings.status !== 'success') return []; - return recordings.data.filter( - (recording: Recording) => { - // For now, we don't have project_flock relation data in the API response - // So we'll filter by basic recording data - return recording.project_flock_kandang_id.toString().includes(search.toLowerCase()) || - recording.record_date.includes(search.toLowerCase()) || - recording.created_user.name.toLowerCase().includes(search.toLowerCase()); - } - ); - }, [recordings, search]); + return recordings.data; + }, [recordings]); const selectedRowIds = Object.keys(rowSelection).map((item) => parseInt(item)); @@ -210,16 +289,184 @@ const RecordingTable = () => { label: 'Tambah Recording', }} search={{ - value: search, + value: tableFilterState.search, onChange: searchChangeHandler, placeholder: 'Cari Recording', }} /> + + {/* Filter Dropdowns - Desktop */} +
+ { + const selectedValue = selected as OptionType | null; + setSelectedArea(selectedValue); + setSelectedLocation(null); + setSelectedKandang(null); + updateFilter('areaFilter', selectedValue ? selectedValue.value.toString() : ''); + updateFilter('locationFilter', ''); + updateFilter('kandangFilter', ''); + setPage(1); + }} + className={{ wrapper: 'w-full' }} + onInputChange={(value) => setAreaSelectInputValue(value)} + isLoading={isLoadingAreas} + isClearable + /> + + { + const selectedValue = selected as OptionType | null; + setSelectedLocation(selectedValue); + setSelectedKandang(null); + updateFilter('locationFilter', selectedValue ? selectedValue.value.toString() : ''); + updateFilter('kandangFilter', ''); + setPage(1); + }} + className={{ wrapper: 'w-full' }} + onInputChange={(value) => setLocationSelectInputValue(value)} + isLoading={isLoadingLocations} + isClearable + isDisabled={!selectedArea} + /> + + { + const selectedValue = selected as OptionType | null; + setSelectedKandang(selectedValue); + updateFilter('kandangFilter', selectedValue ? selectedValue.value.toString() : ''); + setPage(1); + }} + className={{ wrapper: 'w-full' }} + onInputChange={(value) => setKandangSelectInputValue(value)} + isLoading={isLoadingKandang} + isClearable + isDisabled={!selectedLocation} + /> + + { + const selectedValue = selected as OptionType | null; + updateFilter('periodFilter', selectedValue ? selectedValue.value.toString() : ''); + setPage(1); + }} + className={{ wrapper: 'w-full' }} + isClearable + /> +
+ + {/* Filter Dropdowns - Mobile */} +
+ { + const selectedValue = selected as OptionType | null; + setSelectedArea(selectedValue); + setSelectedLocation(null); + setSelectedKandang(null); + updateFilter('areaFilter', selectedValue ? selectedValue.value.toString() : ''); + updateFilter('locationFilter', ''); + updateFilter('kandangFilter', ''); + setPage(1); + }} + className={{ wrapper: 'w-full' }} + onInputChange={(value) => setAreaSelectInputValue(value)} + isLoading={isLoadingAreas} + isClearable + /> + + { + const selectedValue = selected as OptionType | null; + setSelectedLocation(selectedValue); + setSelectedKandang(null); + updateFilter('locationFilter', selectedValue ? selectedValue.value.toString() : ''); + updateFilter('kandangFilter', ''); + setPage(1); + }} + className={{ wrapper: 'w-full' }} + onInputChange={(value) => setLocationSelectInputValue(value)} + isLoading={isLoadingLocations} + isClearable + isDisabled={!selectedArea} + /> + + { + const selectedValue = selected as OptionType | null; + setSelectedKandang(selectedValue); + updateFilter('kandangFilter', selectedValue ? selectedValue.value.toString() : ''); + setPage(1); + }} + className={{ wrapper: 'w-full' }} + onInputChange={(value) => setKandangSelectInputValue(value)} + isLoading={isLoadingKandang} + isClearable + isDisabled={!selectedLocation} + /> + + { + const selectedValue = selected as OptionType | null; + updateFilter('periodFilter', selectedValue ? selectedValue.value.toString() : ''); + setPage(1); + }} + className={{ wrapper: 'w-full' }} + isClearable + /> +
{/* Bulk action buttons */} @@ -315,7 +562,7 @@ const RecordingTable = () => { }, { header: '#', - cell: (props) => pageSize * (page - 1) + props.row.index + 1, + cell: (props) => tableFilterState.pageSize * (tableFilterState.page - 1) + props.row.index + 1, }, { header: 'ID', @@ -427,8 +674,8 @@ const RecordingTable = () => { }, }, ]} - pageSize={pageSize} - page={recordings?.status === 'success' ? recordings.meta?.page : page} + pageSize={tableFilterState.pageSize} + page={recordings?.status === 'success' ? recordings.meta?.page : tableFilterState.page} totalItems={recordings?.status === 'success' ? recordings.meta?.total_results : 0} onPageChange={setPage} isLoading={isLoading}