mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 21:41:57 +00:00
feat(FE-438): Add bulk approve/reject/delete and FAB
This commit is contained in:
@@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user