feat: implement bulk action confirmation modal and related functionality

- Updated request methods for batch operations in constants.ts to use PUT instead of POST.
- Added new translation keys for bulk actions in English and Indonesian language files.
- Created BulkActionConfirmationModal component to handle bulk lifecycle actions with progress tracking and summary display.
- Introduced SummaryPanel component to show aggregated results of bulk actions.
- Enhanced BulkActionMenu to support custom bulk actions and improved action handling based on selected rows.
- Integrated bulk action handling in EnterpriseDataTable, allowing for batch processing of selected entities.
- Added new types for bulk action modal state and results in entity.ts.
- Exported BulkActionConfirmationModal from the module index for accessibility.
This commit is contained in:
Firman Ramdhani
2026-07-23 11:47:23 +07:00
parent 0243b6aa96
commit 4e13423409
11 changed files with 977 additions and 69 deletions
@@ -14,6 +14,7 @@ import {
import { Box } from '@mantine/core';
import { DataGrid, StatusBadge } from '../../../../components';
import type { PageActionProps } from '../../../../components';
import {
useEnterpriseModuleConfigContext,
@@ -22,11 +23,21 @@ import {
useEnterpriseModuleTranslationContext,
useEnterpriseModuleNavigationContext,
} from '../../hooks/use-module.context';
import { BaseEntity } from '@repo/core-api/data-services';
import type { BaseEntity } from '@repo/core-api/data-services';
import { notifications } from '@mantine/notifications';
import { ModuleActionType, ModuleAction, ActionModalState, ActionModalConfig } from '../../entities/entity';
import {
ModuleActionType,
ModuleAction,
ActionModalState,
ActionModalConfig,
BulkActionModalState,
BulkActionResult,
} from '../../entities/entity';
import { RowActionMenu } from './components/row-actions';
import { BulkActionMenu } from './components/bulk-actions';
import { ActionConfirmationModal, ACTION_TRANSLATION_MAP } from '../action-confirmation-modal';
import { BulkActionConfirmationModal } from '../bulk-action-confirmation';
import { EntityId } from '../../../../../../core-api/src/data-services/types';
export * from 'ag-grid-community';
export * from 'ag-grid-react';
@@ -89,6 +100,21 @@ export interface EnterpriseDataTableProps<E extends BaseEntity> extends Omit<AgG
cancelModalConfig?: ActionModalConfig;
rollbackModalConfig?: ActionModalConfig;
holdModalConfig?: ActionModalConfig;
// Bulk action props
/** Custom function to modify/extend the default bulk action buttons shown in the table header. */
customBulkActions?: (selectedRows: any[], defaultActions: PageActionProps[]) => PageActionProps[];
/** Maximum number of IDs per batch request during bulk operations. @default 20 */
batchSize?: number;
// Bulk action custom flow callbacks (mirrors single-action onClick* pattern)
onBulkClickDelete?: (data: E[]) => void;
onBulkClickActivate?: (data: E[]) => void;
onBulkClickDeactivate?: (data: E[]) => void;
onBulkClickConfirm?: (data: E[]) => void;
onBulkClickCancel?: (data: E[]) => void;
onBulkClickRollback?: (data: E[]) => void;
onBulkClickHold?: (data: E[]) => void;
}
// ---------------------------------------------------------------------------
@@ -129,6 +155,17 @@ export function EnterpriseDataTable<E extends BaseEntity>(props: EnterpriseDataT
rollbackModalConfig,
holdModalConfig,
// Bulk action props
customBulkActions,
batchSize,
onBulkClickDelete,
onBulkClickActivate,
onBulkClickDeactivate,
onBulkClickConfirm,
onBulkClickCancel,
onBulkClickRollback,
onBulkClickHold,
...restAgGridProps
} = props;
@@ -137,7 +174,7 @@ export function EnterpriseDataTable<E extends BaseEntity>(props: EnterpriseDataT
// ---------------------------------------------------------------------------
const { t } = useEnterpriseModuleTranslationContext();
const { dataServices } = useEnterpriseModuleDataServiceContext<E>();
const { setSelectedRows, metaData, setMetaData } = useEnterpriseModuleSelectionContext<E>();
const { selectedRows, setSelectedRows, metaData, setMetaData } = useEnterpriseModuleSelectionContext<E>();
const navigation = useEnterpriseModuleNavigationContext();
const { config } = useEnterpriseModuleConfigContext();
@@ -262,6 +299,138 @@ export function EnterpriseDataTable<E extends BaseEntity>(props: EnterpriseDataT
[dataServices, closeActionModal, modalConfigMap, t],
);
// ---------------------------------------------------------------------------
// Bulk Action Modal State & Handlers
// ---------------------------------------------------------------------------
const CLOSED_BULK_MODAL: BulkActionModalState<E> = useMemo(() => ({ opened: false, action: null, data: [] }), []);
const [bulkModalState, setBulkModalState] = useState<BulkActionModalState<E>>(CLOSED_BULK_MODAL);
const openBulkActionModal = useCallback(
(action: ModuleActionType, data: E[]) => {
const config = modalConfigMap[action as keyof typeof modalConfigMap];
setBulkModalState({ opened: true, action, data, config });
},
[modalConfigMap],
);
const closeBulkActionModal = useCallback(() => {
setBulkModalState(CLOSED_BULK_MODAL);
// Refresh grid data after bulk action completes
if (gridApiRef.current) {
gridApiRef.current.refreshServerSide({ purge: false });
}
// Clear selected rows
setSelectedRows([]);
if (gridApiRef.current) {
gridApiRef.current.deselectAll();
}
}, [CLOSED_BULK_MODAL, setSelectedRows]);
const handleBulkActionClick = useCallback(
(action: ModuleActionType, data: E[]) => {
switch (action) {
case ModuleAction.DELETE:
if (onBulkClickDelete) onBulkClickDelete(data);
else openBulkActionModal(action, data);
break;
case ModuleAction.ACTIVATE:
if (onBulkClickActivate) onBulkClickActivate(data);
else openBulkActionModal(action, data);
break;
case ModuleAction.DEACTIVATE:
if (onBulkClickDeactivate) onBulkClickDeactivate(data);
else openBulkActionModal(action, data);
break;
case ModuleAction.CONFIRM:
if (onBulkClickConfirm) onBulkClickConfirm(data);
else openBulkActionModal(action, data);
break;
case ModuleAction.CANCEL:
if (onBulkClickCancel) onBulkClickCancel(data);
else openBulkActionModal(action, data);
break;
case ModuleAction.ROLLBACK:
if (onBulkClickRollback) onBulkClickRollback(data);
else openBulkActionModal(action, data);
break;
case ModuleAction.HOLD:
if (onBulkClickHold) onBulkClickHold(data);
else openBulkActionModal(action, data);
break;
default:
openBulkActionModal(action, data);
break;
}
},
[
openBulkActionModal,
onBulkClickDelete,
onBulkClickActivate,
onBulkClickDeactivate,
onBulkClickConfirm,
onBulkClickCancel,
onBulkClickRollback,
onBulkClickHold,
],
);
/**
* Executes a single batch chunk of the bulk action.
* Called by BulkActionConfirmationModal per chunk.
*/
const executeBulkAction = useCallback(
async (action: ModuleActionType, ids: EntityId[], meta?: Record<string, unknown>): Promise<BulkActionResult> => {
try {
switch (action) {
case ModuleAction.DELETE:
await dataServices.batchDelete(ids, meta);
break;
case ModuleAction.ACTIVATE:
await dataServices.batchActivate(ids, meta);
break;
case ModuleAction.DEACTIVATE:
await dataServices.batchDeactivate(ids, meta);
break;
case ModuleAction.CONFIRM:
await dataServices.batchConfirmData(ids, meta);
break;
case ModuleAction.CANCEL:
await dataServices.batchCancelData(ids, meta);
break;
case ModuleAction.ROLLBACK:
await dataServices.batchRollbackData(ids, meta);
break;
case ModuleAction.HOLD:
await dataServices.batchHoldData(ids, meta);
break;
default:
console.warn(`[executeBulkAction] Unhandled action: ${action}`);
return { total_items: ids.length, total_success: 0, total_failed: ids.length };
}
return {
total_items: ids.length,
total_success: ids.length,
total_failed: 0,
};
} catch (error: any) {
return {
total_items: ids.length,
total_success: 0,
total_failed: ids.length,
messages: [error?.message || 'Unknown error'],
};
}
},
[dataServices],
);
// ---------------------------------------------------------------------------
// Action Handlers
// ---------------------------------------------------------------------------
const handleActionClick = useCallback(
(action: ModuleActionType | 'VIEW', data: E) => {
switch (action) {
@@ -521,6 +690,14 @@ export function EnterpriseDataTable<E extends BaseEntity>(props: EnterpriseDataT
// ---------------------------------------------------------------------------
return (
<Box>
{/* BULK ACTION TOOLBAR — shown when rows are selected */}
<BulkActionMenu
selectedRows={selectedRows as E[]}
onActionClick={handleBulkActionClick as (action: ModuleActionType, data: any[]) => void}
statusKey={statusKey}
customBulkActions={customBulkActions}
/>
{/* GRID CONTAINER */}
<Box
className="erp-data-grid-container"
@@ -552,13 +729,22 @@ export function EnterpriseDataTable<E extends BaseEntity>(props: EnterpriseDataT
/>
</Box>
{/* Action Confirmation Modal */}
{/* Single-Row Action Confirmation Modal */}
<ActionConfirmationModal<E>
modalState={actionModalState}
onClose={closeActionModal}
onExecute={executeAction}
t={t}
/>
{/* Bulk Action Confirmation Modal */}
<BulkActionConfirmationModal<E>
modalState={bulkModalState}
onClose={closeBulkActionModal}
onExecute={executeBulkAction}
t={t}
batchSize={batchSize}
/>
</Box>
);
}