refactor(FE-170): streamline RecordingTable component by removing unused state and optimizing layout for better usability

This commit is contained in:
rstubryan
2025-11-03 10:40:13 +07:00
parent bcb4d4492d
commit d8599a850a
@@ -1,26 +1,22 @@
'use client';
import { useCallback, useMemo, useState } from 'react';
import { useCallback, useState } from 'react';
import useSWR from 'swr';
import { Icon } from '@iconify/react';
import { SortingState } from '@tanstack/react-table';
import { SortingState, CellContext } from '@tanstack/react-table';
import { cn } from '@/lib/helper';
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 DebouncedTextInput from '@/components/input/DebouncedTextInput';
import { ROWS_OPTIONS } from '@/config/constant';
import CheckboxInput from '@/components/input/CheckboxInput';
import { TableToolbar } from '@/components/table/TableToolbar';
import { TableRowSizeSelector } from '@/components/table/TableRowSizeSelector';
import Table from '@/components/Table';
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
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';
@@ -103,17 +99,12 @@ const RecordingTable = () => {
});
const [sorting, setSorting] = useState<SortingState>([]);
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
const [selectedRecording, setSelectedRecording] = useState<
Recording | undefined
>(undefined);
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const [isBulkApproveLoading, setIsBulkApproveLoading] = useState(false);
const [isBulkRejectLoading, setIsBulkRejectLoading] = useState(false);
const singleDeleteModal = useModal();
const bulkApproveModal = useModal();
const bulkRejectModal = useModal();
// State for dropdown search
const [locationSelectInputValue, setLocationSelectInputValue] = useState('');
@@ -205,62 +196,6 @@ const RecordingTable = () => {
[setPageSize, setPage]
);
const paginatedData = useMemo(() => {
if (!recordings || recordings.status !== 'success') return [];
return recordings.data;
}, [recordings]);
const selectedRowIds = Object.keys(rowSelection).map((item) =>
parseInt(item)
);
const bulkApproveHandler = async () => {
setIsBulkApproveLoading(true);
const approveResponse = await RecordingApi.approve(
selectedRowIds,
'Bulk Approved'
);
if (isResponseSuccess(approveResponse)) {
await refreshRecordings();
setRowSelection({});
bulkApproveModal.closeModal();
toast.success(
`Successfully approved ${selectedRowIds.length} recordings!`
);
}
if (isResponseError(approveResponse)) {
toast.error(approveResponse?.message as string);
bulkApproveModal.closeModal();
}
setIsBulkApproveLoading(false);
};
const bulkRejectHandler = async () => {
setIsBulkRejectLoading(true);
const rejectResponse = await RecordingApi.reject(
selectedRowIds,
'Bulk Rejected'
);
if (isResponseSuccess(rejectResponse)) {
refreshRecordings();
setRowSelection({});
bulkRejectModal.closeModal();
toast.success(
`Successfully rejected ${selectedRowIds.length} recordings!`
);
}
if (isResponseError(rejectResponse)) {
toast.error(rejectResponse?.message as string);
bulkRejectModal.closeModal();
}
setIsBulkRejectLoading(false);
};
const singleDeleteHandler = async () => {
setIsDeleteLoading(true);
@@ -273,315 +208,123 @@ const RecordingTable = () => {
};
return (
<div className='flex flex-col gap-4'>
<div className='w-full p-0 sm:p-4'>
<div className='flex flex-col gap-2 mb-4'>
<TableToolbar
addButton={{
href: 'recording/add',
label: 'Tambah',
}}
search={{
value: tableFilterState.search,
onChange: searchChangeHandler,
placeholder: 'Cari Recording',
}}
/>
<TableRowSizeSelector
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 */}
<div className={'flex justify-end items-center'}>
{selectedRowIds.length > 0 && (
<div className='flex gap-2 mb-4'>
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'>
<div className='w-full flex flex-row gap-2'>
<Button
type='button'
color='success'
onClick={() => bulkApproveModal.openModal()}
className='flex items-center gap-2'
href='recording/add'
variant='outline'
color='primary'
className='w-full sm:w-fit'
>
<Icon
icon='material-symbols:check-circle-outline'
width={20}
height={20}
/>
Approve ({selectedRowIds.length})
</Button>
<Button
type='button'
color='error'
onClick={() => bulkRejectModal.openModal()}
className='flex items-center gap-2'
>
<Icon
icon='material-symbols:cancel-outline'
width={20}
height={20}
/>
Reject ({selectedRowIds.length})
<Icon icon='ic:round-plus' width={24} height={24} />
Tambah
</Button>
</div>
)}
<ConfirmationModal
ref={bulkApproveModal.ref}
type='success'
text={`Apakah anda yakin ingin menyetujui ${selectedRowIds.length} data Recording yang dipilih?`}
secondaryButton={{
text: 'Tidak',
}}
primaryButton={{
text: 'Ya',
color: 'success',
isLoading: isBulkApproveLoading,
onClick: bulkApproveHandler,
}}
/>
<DebouncedTextInput
name='search'
placeholder='Cari Recording'
value={tableFilterState.search}
onChange={searchChangeHandler}
className={{ wrapper: 'sm:max-w-3xs' }}
/>
</div>
<ConfirmationModal
ref={bulkRejectModal.ref}
type='error'
text={`Apakah anda yakin ingin menolak ${selectedRowIds.length} data Recording yang dipilih?`}
secondaryButton={{
text: 'Tidak',
}}
primaryButton={{
text: 'Ya',
color: 'error',
isLoading: isBulkRejectLoading,
onClick: bulkRejectHandler,
}}
/>
<div className='grid grid-cols-12 justify-end gap-4'>
<SelectInput
label='Area'
placeholder='Pilih Area'
options={optionsArea}
isLoading={isLoadingAreas}
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);
}}
onInputChange={setAreaSelectInputValue}
isClearable
className={{
wrapper: 'col-span-12 sm:col-span-3',
}}
/>
<SelectInput
label='Lokasi'
placeholder='Pilih Lokasi'
options={optionsLocation}
isLoading={isLoadingLocations}
value={selectedLocation}
onChange={(selected) => {
const selectedValue = selected as OptionType | null;
setSelectedLocation(selectedValue);
setSelectedKandang(null);
updateFilter(
'locationFilter',
selectedValue ? selectedValue.value.toString() : ''
);
updateFilter('kandangFilter', '');
setPage(1);
}}
onInputChange={setLocationSelectInputValue}
isClearable
isDisabled={!selectedArea}
className={{
wrapper: 'col-span-12 sm:col-span-3',
}}
/>
<SelectInput
label='Kandang'
placeholder='Pilih Kandang'
options={optionsKandang}
isLoading={isLoadingKandang}
value={selectedKandang}
onChange={(selected) => {
const selectedValue = selected as OptionType | null;
setSelectedKandang(selectedValue);
updateFilter(
'kandangFilter',
selectedValue ? selectedValue.value.toString() : ''
);
setPage(1);
}}
onInputChange={setKandangSelectInputValue}
isClearable
isDisabled={!selectedLocation}
className={{
wrapper: 'col-span-12 sm:col-span-2',
}}
/>
<SelectInput
label='Baris'
options={ROWS_OPTIONS}
value={{
label: String(tableFilterState.pageSize),
value: tableFilterState.pageSize,
}}
onChange={pageSizeChangeHandler}
className={{
wrapper: 'col-span-6 sm:col-span-4 max-w-28 sm:justify-self-end',
}}
/>
</div>
</div>
<Table
data={paginatedData}
<Table<Recording>
data={isResponseSuccess(recordings) ? recordings?.data : []}
columns={[
{
id: 'select',
header: ({ table }) => (
<div className='w-full flex flex-row justify-center'>
<CheckboxInput
name='allRow'
checked={table.getIsAllRowsSelected()}
indeterminate={table.getIsSomeRowsSelected()}
onChange={table.getToggleAllRowsSelectedHandler()}
/>
</div>
),
cell: ({ row }) => (
<div>
<CheckboxInput
name='row'
checked={row.getIsSelected()}
disabled={!row.getCanSelect()}
indeterminate={row.getIsSomeSelected()}
onChange={row.getToggleSelectedHandler()}
/>
</div>
),
},
{
header: '#',
cell: (props) =>
@@ -609,38 +352,6 @@ const RecordingTable = () => {
cell: (props) =>
props.row.original.total_chick_qty?.toLocaleString() || '-',
},
{
header: 'BW',
cell: (props) =>
props.row.original.avg_daily_gain?.toFixed(2) || '-',
},
{
header: 'Pakan',
cell: (props) =>
props.row.original.cum_intake?.toLocaleString() || '-',
},
{
header: 'FCR',
cell: (props) => props.row.original.fcr_value?.toFixed(2) || '-',
},
{
accessorKey: 'total_depletion',
header: 'Total Deplesi',
cell: (props) => props.row.original.total_depletion_qty,
},
{
header: 'Deplesi (%)',
cell: (props) =>
props.row.original.daily_depletion_rate?.toFixed(2) || '-',
},
{
header: 'Populasi Akhir',
cell: (props) =>
(
props.row.original.total_chick_qty -
props.row.original.total_depletion_qty
)?.toLocaleString() || '-',
},
{
header: 'Tanggal Submit',
cell: (props) =>
@@ -690,23 +401,18 @@ const RecordingTable = () => {
},
]}
pageSize={tableFilterState.pageSize}
page={
recordings?.status === 'success'
? recordings.meta?.page
: tableFilterState.page
}
page={isResponseSuccess(recordings) ? recordings?.meta?.page : 0}
totalItems={
recordings?.status === 'success' ? recordings.meta?.total_results : 0
isResponseSuccess(recordings) ? recordings?.meta?.total_results : 0
}
onPageChange={setPage}
isLoading={isLoading}
sorting={sorting}
setSorting={setSorting}
rowSelection={rowSelection}
setRowSelection={setRowSelection}
className={{
containerClassName: cn({
'mb-20': paginatedData.length === 0,
'mb-20':
isResponseSuccess(recordings) && recordings?.data?.length === 0,
}),
tableWrapperClassName: 'overflow-x-auto min-h-full!',
tableClassName: 'font-inter w-full table-auto min-h-full!',