feat(FE-438): Add bulk approve/reject/delete and FAB

This commit is contained in:
rstubryan
2025-12-28 14:32:14 +07:00
parent 8a6f78ef84
commit c0a818af7e
2 changed files with 197 additions and 126 deletions
@@ -3,7 +3,7 @@
import React, { useCallback, useState, useEffect, useMemo } from 'react';
import useSWR from 'swr';
import { Icon } from '@iconify/react';
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
import { ColumnDef, SortingState } from '@tanstack/react-table';
import { cn, formatDate } from '@/lib/helper';
import Button from '@/components/Button';
import UniformityChart from '@/components/pages/uniformity/UniformityChart';
@@ -15,9 +15,6 @@ import { isResponseSuccess } from '@/lib/api-helper';
import Table from '@/components/Table';
import Badge from '@/components/Badge';
import CheckboxInput from '@/components/input/CheckboxInput';
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
import { useModal } from '@/components/Modal';
import ConfirmationModal from '@/components/modal/ConfirmationModal';
import toast from 'react-hot-toast';
@@ -25,6 +22,7 @@ import Card from '@/components/Card';
import UniformityTableSkeleton from './skeleton/UniformityTableSkeleton';
import RequirePermission from '@/components/helper/RequirePermission';
import { useUniformityStore } from '@/stores/uniformity/uniformity.store';
import FloatingActionsButton from '@/components/FloatingActionsButton';
const statusColorMap: Record<string, string> = {
APPROVED: 'bg-[#00D39033]',
@@ -60,68 +58,6 @@ const isUniformityLocked = (uniformity: Uniformity): boolean => {
return uniformity.status === 'APPROVED' || uniformity.status === 'REJECTED';
};
const RowOptionsMenu = ({
type = 'dropdown',
props,
deleteClickHandler,
setSelectedUniformity,
openModal,
}: {
type: 'dropdown' | 'collapse';
props: CellContext<Uniformity, unknown>;
deleteClickHandler: () => void;
setSelectedUniformity: (uniformity: Uniformity) => void;
openModal: () => void;
}) => {
const handleDeleteClick = useCallback(() => {
setSelectedUniformity(props.row.original);
openModal();
}, [props.row.original, setSelectedUniformity, openModal]);
return (
<RowOptionsMenuWrapper type={type}>
<RequirePermission permissions='lti.production.uniformity.detail'>
<Button
href={`/uniformity/detail/?uniformityId=${props.row.original.id}`}
variant='ghost'
color='primary'
className='justify-start text-sm'
>
<Icon icon='mdi:eye-outline' width={16} height={16} />
Detail
</Button>
</RequirePermission>
<RequirePermission permissions='lti.production.uniformity.update'>
<Button
href={`/uniformity/edit/?uniformityId=${props.row.original.id}`}
variant='ghost'
color='warning'
className='justify-start text-sm'
>
<Icon icon='mdi:pencil-outline' width={16} height={16} />
Edit
</Button>
</RequirePermission>
<RequirePermission permissions='lti.production.uniformity.delete'>
<Button
onClick={handleDeleteClick}
variant='ghost'
color='error'
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
>
<Icon
icon='mdi:delete-outline'
width={16}
height={16}
className='justify-start text-sm'
/>
Delete
</Button>
</RequirePermission>
</RowOptionsMenuWrapper>
);
};
const UniformityTable = ({ refresh }: { refresh?: () => void }) => {
const isSuccess = useUniformityStore((s) => s.isSuccess);
const setIsSuccess = useUniformityStore((s) => s.setIsSuccess);
@@ -143,14 +79,41 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => {
const [sorting, setSorting] = useState<SortingState>([]);
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
const [selectedUniformity, setSelectedUniformity] = useState<
Uniformity | undefined
>(undefined);
const [selectedUniformity] = useState<Uniformity | undefined>(undefined);
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const [isBulkActionLoading, setIsBulkActionLoading] = useState(false);
const singleDeleteModal = useModal();
const bulkDeleteModal = useModal();
const successModal = useModal();
const {
data: uniformities,
isLoading,
mutate: refreshUniformities,
} = useSWR(
`${UniformityApi.basePath}${getTableFilterQueryString()}`,
UniformityApi.getAllFetcher
);
const selectedRowIds = useMemo(() => {
return Object.keys(rowSelection)
.filter((key) => rowSelection[key])
.map((key) => parseInt(key));
}, [rowSelection]);
const selectedUniformities = useMemo(() => {
if (!isResponseSuccess(uniformities) || !uniformities.data) return [];
return uniformities.data.filter((u) => selectedRowIds.includes(u.id));
}, [uniformities, selectedRowIds]);
const canApproveReject = useMemo(() => {
return (
selectedUniformities.length > 0 &&
selectedUniformities.every((u) => u.status === 'CREATED')
);
}, [selectedUniformities]);
useEffect(() => {
if (isSuccess) {
successModal.openModal();
@@ -162,15 +125,6 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => {
setIsSuccess(false);
};
const {
data: uniformities,
isLoading,
mutate: refreshUniformities,
} = useSWR(
`${UniformityApi.basePath}${getTableFilterQueryString()}`,
UniformityApi.getAllFetcher
);
const singleDeleteHandler = useCallback(async () => {
setIsDeleteLoading(true);
@@ -182,6 +136,72 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => {
setIsDeleteLoading(false);
}, [selectedUniformity?.id, refreshUniformities, singleDeleteModal]);
const handleBulkDelete = useCallback(() => {
bulkDeleteModal.openModal();
}, [bulkDeleteModal]);
const bulkDeleteHandler = useCallback(async () => {
setIsBulkActionLoading(true);
try {
await UniformityApi.bulkDelete(selectedRowIds);
setRowSelection({});
refreshUniformities();
bulkDeleteModal.closeModal();
toast.success(
`Successfully deleted ${selectedRowIds.length} Uniformity data!`
);
} catch {
toast.error('Failed to delete Uniformity data');
} finally {
setIsBulkActionLoading(false);
}
}, [selectedRowIds, refreshUniformities, bulkDeleteModal]);
const handleCloseFab = useCallback(() => {
setRowSelection({});
}, []);
const handleBulkApprove = useCallback(async () => {
setIsBulkActionLoading(true);
try {
await UniformityApi.approve(selectedRowIds);
setRowSelection({});
refreshUniformities();
toast.success(
`Successfully approved ${selectedRowIds.length} Uniformity data!`
);
} catch {
toast.error('Failed to approve Uniformity data');
} finally {
setIsBulkActionLoading(false);
}
}, [selectedRowIds, refreshUniformities]);
const handleBulkReject = useCallback(async () => {
setIsBulkActionLoading(true);
try {
await UniformityApi.reject(selectedRowIds);
setRowSelection({});
refreshUniformities();
toast.success(
`Successfully rejected ${selectedRowIds.length} Uniformity data!`
);
} catch (error) {
toast.error('Failed to reject Uniformity data');
} finally {
setIsBulkActionLoading(false);
}
}, [selectedRowIds, refreshUniformities]);
useEffect(() => {
if (isResponseSuccess(uniformities) && uniformities.data) {
const newSelection: Record<string, boolean> = {};
@@ -316,53 +336,6 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => {
return <span>{uniformity}%</span>;
},
},
{
id: 'actions',
header: 'Aksi',
cell: (props: CellContext<Uniformity, unknown>) => {
const currentPageSize =
props.table.getPaginationRowModel().rows.length;
const currentPageRows = props.table.getPaginationRowModel().flatRows;
const currentRowRelativeIndex =
currentPageRows.findIndex((r) => r.id === props.row.id) + 1;
const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2;
return (
<>
{currentPageSize > 2 && (
<RowDropdownOptions isLast2Rows={isLast2Rows}>
<RowOptionsMenu
type='dropdown'
props={props}
deleteClickHandler={() => {
setSelectedUniformity(props.row.original);
singleDeleteModal.openModal();
}}
setSelectedUniformity={setSelectedUniformity}
openModal={singleDeleteModal.openModal}
/>
</RowDropdownOptions>
)}
{currentPageSize <= 2 && (
<RowCollapseOptions>
<RowOptionsMenu
type='collapse'
props={props}
deleteClickHandler={() => {
setSelectedUniformity(props.row.original);
singleDeleteModal.openModal();
}}
setSelectedUniformity={setSelectedUniformity}
openModal={singleDeleteModal.openModal}
/>
</RowCollapseOptions>
)}
</>
);
},
},
],
[]
);
@@ -407,7 +380,7 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => {
<Card
variant='bordered'
className={{
wrapper: 'my-4 w-full',
wrapper: 'my-4 w-full relative',
}}
>
<Table<Uniformity>
@@ -459,6 +432,21 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => {
}}
/>
<ConfirmationModal
ref={bulkDeleteModal.ref}
type='error'
text={`Apakah anda yakin ingin menghapus ${selectedRowIds.length} data Uniformity yang dipilih?`}
secondaryButton={{
text: 'Tidak',
}}
primaryButton={{
text: 'Ya',
color: 'error',
isLoading: isBulkActionLoading,
onClick: bulkDeleteHandler,
}}
/>
<ConfirmationModal
ref={successModal.ref}
type='success'
@@ -524,6 +512,39 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => {
/>
</div>
</ConfirmationModal>
{/* Floating Actions Button */}
<FloatingActionsButton
actions={[
{
action: 'DELETE',
icon: 'mdi:delete-outline',
label: 'Delete',
onClick: handleBulkDelete,
permissions: 'lti.production.uniformity.delete',
},
]}
approvals={[
{
action: 'APPROVED',
icon: 'mdi:check-circle-outline',
label: 'Approve',
onClick: handleBulkApprove,
permissions: 'lti.production.uniformity.approve',
disabled: !canApproveReject,
},
{
action: 'REJECTED',
icon: 'mdi:close-circle-outline',
label: 'Reject',
onClick: handleBulkReject,
permissions: 'lti.production.uniformity.approve',
disabled: !canApproveReject,
},
]}
selectedRowIds={selectedRowIds}
onClose={handleCloseFab}
/>
</Card>
</>
);
+50
View File
@@ -61,6 +61,56 @@ export class UniformityApiService extends BaseApiService<
}
);
}
async approve(
idOrIds: number | number[],
notes?: string
): Promise<BaseApiResponse<Uniformity[]> | undefined> {
const approvable_ids = Array.isArray(idOrIds) ? idOrIds : [idOrIds];
return await this.customRequest<BaseApiResponse<Uniformity[]>>(
'approvals',
{
method: 'POST',
payload: {
action: 'APPROVED',
approvable_ids,
notes,
},
}
);
}
async reject(
idOrIds: number | number[],
notes: string = ''
): Promise<BaseApiResponse<Uniformity[]> | undefined> {
const approvable_ids = Array.isArray(idOrIds) ? idOrIds : [idOrIds];
return await this.customRequest<BaseApiResponse<Uniformity[]>>(
'approvals',
{
method: 'POST',
payload: {
action: 'REJECTED',
approvable_ids,
notes,
},
}
);
}
async bulkDelete(
ids: number[]
): Promise<BaseApiResponse<Uniformity[]> | undefined> {
return await this.customRequest<BaseApiResponse<Uniformity[]>>(
'bulk-delete',
{
method: 'POST',
payload: {
ids,
},
}
);
}
}
export const UniformityApi = new UniformityApiService(