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 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}
|
||||||
|
|||||||
Reference in New Issue
Block a user