feat(FE-137): integrate advanced filtering options in RecordingTable with dropdowns for area, location, and kandang

This commit is contained in:
rstubryan
2025-10-24 13:53:20 +07:00
parent 537fc617ff
commit d14fa2ed2b
@@ -9,6 +9,7 @@ import { useModal } from '@/components/Modal';
import Button from '@/components/Button'; import Button from '@/components/Button';
import ConfirmationModal from '@/components/modal/ConfirmationModal'; import ConfirmationModal from '@/components/modal/ConfirmationModal';
import { OptionType } from '@/components/input/SelectInput'; import { OptionType } from '@/components/input/SelectInput';
import SelectInput from '@/components/input/SelectInput';
import { ROWS_OPTIONS } from '@/config/constant'; import { ROWS_OPTIONS } from '@/config/constant';
import CheckboxInput from '@/components/input/CheckboxInput'; import CheckboxInput from '@/components/input/CheckboxInput';
import { TableToolbar } from '@/components/table/TableToolbar'; 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 Recording } from '@/types/api/production/recording';
import { type BaseApiResponse } from '@/types/api/api-general'; import { type BaseApiResponse } from '@/types/api/api-general';
import { RecordingApi } from '@/services/api/production'; 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 { isResponseSuccess, isResponseError } from '@/lib/api-helper';
import { useTableFilter } from '@/services/hooks/useTableFilter';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
const RowOptionsMenu = ({ const RowOptionsMenu = ({
@@ -80,9 +85,31 @@ const RowOptionsMenu = ({
}; };
const RecordingTable = () => { const RecordingTable = () => {
const [search, setSearch] = useState(''); const {
const [page, setPage] = useState(1); state: tableFilterState,
const [pageSize, setPageSize] = useState(10); 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<SortingState>([]); const [sorting, setSorting] = useState<SortingState>([]);
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({}); const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
const [selectedRecording, setSelectedRecording] = useState<Recording | undefined>(undefined); const [selectedRecording, setSelectedRecording] = useState<Recording | undefined>(undefined);
@@ -94,21 +121,81 @@ const RecordingTable = () => {
const bulkApproveModal = useModal(); const bulkApproveModal = useModal();
const bulkRejectModal = useModal(); const bulkRejectModal = useModal();
// State for dropdown search
const [locationSelectInputValue, setLocationSelectInputValue] = useState('');
const [areaSelectInputValue, setAreaSelectInputValue] = useState('');
const [kandangSelectInputValue, setKandangSelectInputValue] = useState('');
const [selectedArea, setSelectedArea] = useState<OptionType | null>(null);
const [selectedLocation, setSelectedLocation] = useState<OptionType | null>(null);
const [selectedKandang, setSelectedKandang] = useState<OptionType | null>(null);
const { const {
data: recordings, data: recordings,
isLoading, isLoading,
mutate: refreshRecordings, mutate: refreshRecordings,
} = useSWR( } = useSWR(
`${RecordingApi.basePath}?page=${page}&limit=${pageSize}`, `${RecordingApi.basePath}${getTableFilterQueryString()}`,
RecordingApi.getAllFetcher 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( const searchChangeHandler = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => { (e: React.ChangeEvent<HTMLInputElement>) => {
setSearch(e.target.value); updateFilter('search', e.target.value);
setPage(1); setPage(1);
}, },
[] [updateFilter, setPage]
); );
const pageSizeChangeHandler = useCallback( const pageSizeChangeHandler = useCallback(
@@ -117,22 +204,14 @@ const RecordingTable = () => {
setPageSize(newVal.value as number); setPageSize(newVal.value as number);
setPage(1); setPage(1);
}, },
[] [setPageSize, setPage]
); );
const paginatedData = useMemo(() => { const paginatedData = useMemo(() => {
if (!recordings || recordings.status !== 'success') return []; if (!recordings || recordings.status !== 'success') return [];
return recordings.data.filter( return recordings.data;
(recording: Recording) => { }, [recordings]);
// 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]);
const selectedRowIds = Object.keys(rowSelection).map((item) => parseInt(item)); const selectedRowIds = Object.keys(rowSelection).map((item) => parseInt(item));
@@ -210,16 +289,184 @@ const RecordingTable = () => {
label: 'Tambah Recording', label: 'Tambah Recording',
}} }}
search={{ search={{
value: search, value: tableFilterState.search,
onChange: searchChangeHandler, onChange: searchChangeHandler,
placeholder: 'Cari Recording', placeholder: 'Cari Recording',
}} }}
/> />
<TableRowSizeSelector <TableRowSizeSelector
value={pageSize} value={tableFilterState.pageSize}
onChange={pageSizeChangeHandler} onChange={pageSizeChangeHandler}
options={ROWS_OPTIONS} options={ROWS_OPTIONS}
/> />
{/* Filter Dropdowns - Desktop */}
<div className='hidden sm:grid sm:grid-cols-4 gap-4 mt-4'>
<SelectInput
label='Area'
placeholder='Pilih Area'
options={optionsArea}
value={selectedArea}
onChange={(selected) => {
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
/>
<SelectInput
label='Lokasi'
placeholder='Pilih Lokasi'
options={optionsLocation}
value={selectedLocation}
onChange={(selected) => {
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}
/>
<SelectInput
label='Kandang'
placeholder='Pilih Kandang'
options={optionsKandang}
value={selectedKandang}
onChange={(selected) => {
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}
/>
<SelectInput
label='Periode'
placeholder='Pilih Periode'
options={[
{ value: '1', label: 'Periode 1' },
{ value: '2', label: 'Periode 2' },
{ value: '3', label: 'Periode 3' },
]}
value={
tableFilterState.periodFilter
? { value: tableFilterState.periodFilter, label: `Periode ${tableFilterState.periodFilter}` }
: null
}
onChange={(selected) => {
const selectedValue = selected as OptionType | null;
updateFilter('periodFilter', selectedValue ? selectedValue.value.toString() : '');
setPage(1);
}}
className={{ wrapper: 'w-full' }}
isClearable
/>
</div>
{/* Filter Dropdowns - Mobile */}
<div className='sm:hidden flex flex-col gap-3 mt-4'>
<SelectInput
label='Area'
placeholder='Pilih Area'
options={optionsArea}
value={selectedArea}
onChange={(selected) => {
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
/>
<SelectInput
label='Lokasi'
placeholder='Pilih Lokasi'
options={optionsLocation}
value={selectedLocation}
onChange={(selected) => {
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}
/>
<SelectInput
label='Kandang'
placeholder='Pilih Kandang'
options={optionsKandang}
value={selectedKandang}
onChange={(selected) => {
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}
/>
<SelectInput
label='Periode'
placeholder='Pilih Periode'
options={[
{ value: '1', label: 'Periode 1' },
{ value: '2', label: 'Periode 2' },
{ value: '3', label: 'Periode 3' },
]}
value={
tableFilterState.periodFilter
? { value: tableFilterState.periodFilter, label: `Periode ${tableFilterState.periodFilter}` }
: null
}
onChange={(selected) => {
const selectedValue = selected as OptionType | null;
updateFilter('periodFilter', selectedValue ? selectedValue.value.toString() : '');
setPage(1);
}}
className={{ wrapper: 'w-full' }}
isClearable
/>
</div>
</div> </div>
{/* Bulk action buttons */} {/* Bulk action buttons */}
@@ -315,7 +562,7 @@ const RecordingTable = () => {
}, },
{ {
header: '#', header: '#',
cell: (props) => pageSize * (page - 1) + props.row.index + 1, cell: (props) => tableFilterState.pageSize * (tableFilterState.page - 1) + props.row.index + 1,
}, },
{ {
header: 'ID', header: 'ID',
@@ -427,8 +674,8 @@ const RecordingTable = () => {
}, },
}, },
]} ]}
pageSize={pageSize} pageSize={tableFilterState.pageSize}
page={recordings?.status === 'success' ? recordings.meta?.page : page} page={recordings?.status === 'success' ? recordings.meta?.page : tableFilterState.page}
totalItems={recordings?.status === 'success' ? recordings.meta?.total_results : 0} totalItems={recordings?.status === 'success' ? recordings.meta?.total_results : 0}
onPageChange={setPage} onPageChange={setPage}
isLoading={isLoading} isLoading={isLoading}