mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
feat(FE-137): integrate advanced filtering options in RecordingTable with dropdowns for area, location, and kandang
This commit is contained in:
@@ -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<SortingState>([]);
|
||||
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
|
||||
const [selectedRecording, setSelectedRecording] = useState<Recording | undefined>(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<OptionType | null>(null);
|
||||
const [selectedLocation, setSelectedLocation] = useState<OptionType | null>(null);
|
||||
const [selectedKandang, setSelectedKandang] = useState<OptionType | null>(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<HTMLInputElement>) => {
|
||||
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',
|
||||
}}
|
||||
/>
|
||||
<TableRowSizeSelector
|
||||
value={pageSize}
|
||||
value={tableFilterState.pageSize}
|
||||
onChange={pageSizeChangeHandler}
|
||||
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>
|
||||
|
||||
{/* 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}
|
||||
|
||||
Reference in New Issue
Block a user