mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-24 23:35:45 +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 React, { useCallback, useState, useEffect, useMemo } from 'react';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { Icon } from '@iconify/react';
|
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 { cn, formatDate } from '@/lib/helper';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
import UniformityChart from '@/components/pages/uniformity/UniformityChart';
|
import UniformityChart from '@/components/pages/uniformity/UniformityChart';
|
||||||
@@ -15,9 +15,6 @@ import { isResponseSuccess } from '@/lib/api-helper';
|
|||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
import Badge from '@/components/Badge';
|
import Badge from '@/components/Badge';
|
||||||
import CheckboxInput from '@/components/input/CheckboxInput';
|
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 { useModal } from '@/components/Modal';
|
||||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
@@ -25,6 +22,7 @@ import Card from '@/components/Card';
|
|||||||
import UniformityTableSkeleton from './skeleton/UniformityTableSkeleton';
|
import UniformityTableSkeleton from './skeleton/UniformityTableSkeleton';
|
||||||
import RequirePermission from '@/components/helper/RequirePermission';
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
import { useUniformityStore } from '@/stores/uniformity/uniformity.store';
|
import { useUniformityStore } from '@/stores/uniformity/uniformity.store';
|
||||||
|
import FloatingActionsButton from '@/components/FloatingActionsButton';
|
||||||
|
|
||||||
const statusColorMap: Record<string, string> = {
|
const statusColorMap: Record<string, string> = {
|
||||||
APPROVED: 'bg-[#00D39033]',
|
APPROVED: 'bg-[#00D39033]',
|
||||||
@@ -60,68 +58,6 @@ const isUniformityLocked = (uniformity: Uniformity): boolean => {
|
|||||||
return uniformity.status === 'APPROVED' || uniformity.status === 'REJECTED';
|
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 UniformityTable = ({ refresh }: { refresh?: () => void }) => {
|
||||||
const isSuccess = useUniformityStore((s) => s.isSuccess);
|
const isSuccess = useUniformityStore((s) => s.isSuccess);
|
||||||
const setIsSuccess = useUniformityStore((s) => s.setIsSuccess);
|
const setIsSuccess = useUniformityStore((s) => s.setIsSuccess);
|
||||||
@@ -143,14 +79,41 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
|
|
||||||
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 [selectedUniformity, setSelectedUniformity] = useState<
|
const [selectedUniformity] = useState<Uniformity | undefined>(undefined);
|
||||||
Uniformity | undefined
|
|
||||||
>(undefined);
|
|
||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
|
const [isBulkActionLoading, setIsBulkActionLoading] = useState(false);
|
||||||
|
|
||||||
const singleDeleteModal = useModal();
|
const singleDeleteModal = useModal();
|
||||||
|
const bulkDeleteModal = useModal();
|
||||||
const successModal = 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(() => {
|
useEffect(() => {
|
||||||
if (isSuccess) {
|
if (isSuccess) {
|
||||||
successModal.openModal();
|
successModal.openModal();
|
||||||
@@ -162,15 +125,6 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
setIsSuccess(false);
|
setIsSuccess(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const {
|
|
||||||
data: uniformities,
|
|
||||||
isLoading,
|
|
||||||
mutate: refreshUniformities,
|
|
||||||
} = useSWR(
|
|
||||||
`${UniformityApi.basePath}${getTableFilterQueryString()}`,
|
|
||||||
UniformityApi.getAllFetcher
|
|
||||||
);
|
|
||||||
|
|
||||||
const singleDeleteHandler = useCallback(async () => {
|
const singleDeleteHandler = useCallback(async () => {
|
||||||
setIsDeleteLoading(true);
|
setIsDeleteLoading(true);
|
||||||
|
|
||||||
@@ -182,6 +136,72 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
setIsDeleteLoading(false);
|
setIsDeleteLoading(false);
|
||||||
}, [selectedUniformity?.id, refreshUniformities, singleDeleteModal]);
|
}, [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(() => {
|
useEffect(() => {
|
||||||
if (isResponseSuccess(uniformities) && uniformities.data) {
|
if (isResponseSuccess(uniformities) && uniformities.data) {
|
||||||
const newSelection: Record<string, boolean> = {};
|
const newSelection: Record<string, boolean> = {};
|
||||||
@@ -316,53 +336,6 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
return <span>{uniformity}%</span>;
|
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
|
<Card
|
||||||
variant='bordered'
|
variant='bordered'
|
||||||
className={{
|
className={{
|
||||||
wrapper: 'my-4 w-full',
|
wrapper: 'my-4 w-full relative',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Table<Uniformity>
|
<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
|
<ConfirmationModal
|
||||||
ref={successModal.ref}
|
ref={successModal.ref}
|
||||||
type='success'
|
type='success'
|
||||||
@@ -524,6 +512,39 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</ConfirmationModal>
|
</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>
|
</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(
|
export const UniformityApi = new UniformityApiService(
|
||||||
|
|||||||
Reference in New Issue
Block a user