feat: add StatusBadge component and integrate action confirmation modal
- Introduced StatusBadge component for displaying various status indicators with customizable colors and icons. - Updated ModulePageHeader to conditionally render badges based on props. - Implemented ActionConfirmationModal for handling lifecycle actions (delete, activate, etc.) with support for forms and custom body rendering. - Enhanced EnterpriseDetailPageProvider to manage action confirmation modal state and execution of actions. - Added new modal configurations for various actions in EnterpriseDetailPageConfig. - Updated theme provider to include ModalsProvider and Notifications for better user feedback.
This commit is contained in:
@@ -4,9 +4,11 @@ import { BaseEntity } from '@repo/core-api/data-services';
|
||||
import { Check, CheckCircle, Copy, Edit2, PauseCircle, Plus, RotateCcw, Trash2, X, XCircle } from 'lucide-react';
|
||||
|
||||
import { DetailPageContext } from '../hooks/use-detail-page.context';
|
||||
import { EnterpriseDetailPageConfig, ModuleAction, ModuleActionType } from '../entities/entity';
|
||||
import { CorePageContainer, PageActionProps } from '../../../components';
|
||||
import { EnterpriseDetailPageConfig, ModuleAction, ModuleActionType, ActionModalState } from '../entities/entity';
|
||||
import { CorePageContainer, PageActionProps, StatusBadge } from '../../../components';
|
||||
import { ModulePageHeader, ModulePageHeaderProps } from '../components/module-page-header';
|
||||
import { ActionConfirmationModal, ACTION_TRANSLATION_MAP } from '../components/action-confirmation-modal';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
import {
|
||||
useEnterpriseModuleConfigContext,
|
||||
useEnterpriseModuleDataServiceContext,
|
||||
@@ -22,6 +24,14 @@ export function EnterpriseDetailPageProvider<E extends BaseEntity = BaseEntity>(
|
||||
pageHeaderProps,
|
||||
px,
|
||||
py,
|
||||
|
||||
showHighlightData = true,
|
||||
showHighlightDataOnBreadcrumbs = true,
|
||||
highlightDataKey = 'code',
|
||||
statusKey = 'status',
|
||||
getCustomStatusConfig,
|
||||
onDetailLoaded,
|
||||
|
||||
customPageActions,
|
||||
onClickCreate,
|
||||
onClickDuplicate,
|
||||
@@ -34,9 +44,14 @@ export function EnterpriseDetailPageProvider<E extends BaseEntity = BaseEntity>(
|
||||
onClickRollback,
|
||||
onClickHold,
|
||||
|
||||
showHighlightData = true,
|
||||
showHighlightDataOnBreadcrumbs = true,
|
||||
highlightDataKey = 'code',
|
||||
// Modal configurations per action
|
||||
deleteModalConfig,
|
||||
activateModalConfig,
|
||||
deactivateModalConfig,
|
||||
confirmModalConfig,
|
||||
cancelModalConfig,
|
||||
rollbackModalConfig,
|
||||
holdModalConfig,
|
||||
} = props;
|
||||
|
||||
const { t } = useEnterpriseModuleTranslationContext();
|
||||
@@ -51,7 +66,7 @@ export function EnterpriseDetailPageProvider<E extends BaseEntity = BaseEntity>(
|
||||
|
||||
const { moduleKey, moduleType } = config;
|
||||
|
||||
const [detailData, setDetailData] = useState<E | any>({ id: 1, code: 'ABC-001' });
|
||||
const [detailData, setDetailData] = useState<E | any>();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const loadData = useCallback(async () => {
|
||||
@@ -60,9 +75,16 @@ export function EnterpriseDetailPageProvider<E extends BaseEntity = BaseEntity>(
|
||||
try {
|
||||
const response = await dataServices.getOne(dataId);
|
||||
if (response && response.data) {
|
||||
setDetailData(response.data as E);
|
||||
const data = response.data;
|
||||
setDetailData(data as E);
|
||||
if (onDetailLoaded) onDetailLoaded(data as E);
|
||||
}
|
||||
} catch (error) {
|
||||
// FIXME => remove this example later;
|
||||
|
||||
const data = { id: 1, code: 'ABCD-0001', status: 'open' } as any;
|
||||
setDetailData(data as E);
|
||||
if (onDetailLoaded) onDetailLoaded(data as E);
|
||||
console.error('Failed to load detail data', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
@@ -74,41 +96,178 @@ export function EnterpriseDetailPageProvider<E extends BaseEntity = BaseEntity>(
|
||||
}, [loadData]);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 1. Stub Handlers (Sudah diperbaiki typonya & lengkap)
|
||||
// 1. Action Confirmation Modal State & Handlers
|
||||
// ---------------------------------------------------------------------------
|
||||
async function handleDelete(data: E): Promise<void> {
|
||||
// implementation later
|
||||
console.log({ data });
|
||||
|
||||
/** Default (closed) modal state — stable reference to avoid re-creating on every render. */
|
||||
const CLOSED_MODAL: ActionModalState<E> = useMemo(() => ({ opened: false, action: null, data: null }), []);
|
||||
|
||||
const [actionModalState, setActionModalState] = useState<ActionModalState<E>>(CLOSED_MODAL);
|
||||
|
||||
/** Map action type → modal config provided by the implementer. */
|
||||
const modalConfigMap = useMemo(
|
||||
() => ({
|
||||
[ModuleAction.DELETE]: deleteModalConfig,
|
||||
[ModuleAction.ACTIVATE]: activateModalConfig,
|
||||
[ModuleAction.DEACTIVATE]: deactivateModalConfig,
|
||||
[ModuleAction.CONFIRM]: confirmModalConfig,
|
||||
[ModuleAction.CANCEL]: cancelModalConfig,
|
||||
[ModuleAction.ROLLBACK]: rollbackModalConfig,
|
||||
[ModuleAction.HOLD]: holdModalConfig,
|
||||
}),
|
||||
[
|
||||
deleteModalConfig,
|
||||
activateModalConfig,
|
||||
deactivateModalConfig,
|
||||
confirmModalConfig,
|
||||
cancelModalConfig,
|
||||
rollbackModalConfig,
|
||||
holdModalConfig,
|
||||
],
|
||||
);
|
||||
|
||||
/** Open the confirmation modal for the given action. */
|
||||
const openActionModal = useCallback(
|
||||
(action: ModuleActionType, data: E) => {
|
||||
const config = modalConfigMap[action as keyof typeof modalConfigMap];
|
||||
setActionModalState({ opened: true, action, data, config });
|
||||
},
|
||||
[modalConfigMap],
|
||||
);
|
||||
|
||||
/** Close the confirmation modal and reset state. */
|
||||
const closeActionModal = useCallback(() => {
|
||||
setActionModalState(CLOSED_MODAL);
|
||||
}, [CLOSED_MODAL]);
|
||||
|
||||
/**
|
||||
* Execute a lifecycle action against the data service.
|
||||
*
|
||||
* Called by ActionConfirmationModal after the user confirms.
|
||||
* Dispatches to the correct `dataServices.*` method based on action type.
|
||||
* Passes optional `meta` (form data from the modal body) wrapped in the request payload.
|
||||
*
|
||||
* Post-action behavior:
|
||||
* - DELETE → navigates back to index (record no longer exists)
|
||||
* - All others → reloads detail data (record still exists, status changed)
|
||||
*/
|
||||
const executeAction = useCallback(
|
||||
async (action: ModuleActionType, data: E, meta?: Record<string, unknown>) => {
|
||||
const id = data.id;
|
||||
if (!id) return;
|
||||
|
||||
const currentConfig = modalConfigMap[action as keyof typeof modalConfigMap];
|
||||
|
||||
try {
|
||||
switch (action) {
|
||||
case ModuleAction.DELETE:
|
||||
await dataServices.delete(id, meta);
|
||||
break;
|
||||
|
||||
case ModuleAction.ACTIVATE:
|
||||
await dataServices.activate(id, meta);
|
||||
break;
|
||||
|
||||
case ModuleAction.DEACTIVATE:
|
||||
await dataServices.deactivate(id, meta);
|
||||
break;
|
||||
|
||||
case ModuleAction.CONFIRM:
|
||||
await dataServices.confirmData(id, meta);
|
||||
break;
|
||||
|
||||
case ModuleAction.CANCEL:
|
||||
await dataServices.cancelData(id, meta);
|
||||
break;
|
||||
|
||||
case ModuleAction.ROLLBACK:
|
||||
await dataServices.rollbackData(id, meta);
|
||||
break;
|
||||
|
||||
case ModuleAction.HOLD:
|
||||
await dataServices.holdData(id, meta);
|
||||
break;
|
||||
|
||||
default:
|
||||
console.warn(`[executeAction] Unhandled action: ${action}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const actionKey = ACTION_TRANSLATION_MAP[action]?.confirmKey || `common:actions.${action.toLowerCase()}`;
|
||||
const defaultSuccessMessage = t('common:notifications.actionSuccess', { action: t(actionKey) });
|
||||
|
||||
const customSuccessMessage =
|
||||
typeof currentConfig?.successMessage === 'function'
|
||||
? currentConfig.successMessage(data)
|
||||
: currentConfig?.successMessage;
|
||||
|
||||
notifications.show({
|
||||
title: t('common:notifications.successTitle', { defaultValue: 'Success' }),
|
||||
message: customSuccessMessage || defaultSuccessMessage,
|
||||
color: 'teal',
|
||||
});
|
||||
|
||||
if (action === ModuleAction.DELETE) {
|
||||
closeActionModal();
|
||||
navigation.navigateToIndex();
|
||||
} else {
|
||||
closeActionModal();
|
||||
await loadData();
|
||||
}
|
||||
} catch (error: any) {
|
||||
const actionKey = ACTION_TRANSLATION_MAP[action]?.confirmKey || `common:actions.${action.toLowerCase()}`;
|
||||
const defaultErrorMessage = t('common:notifications.actionFailed', {
|
||||
action: t(actionKey),
|
||||
message: error?.message || 'Unknown error',
|
||||
});
|
||||
|
||||
const customErrorMessage =
|
||||
typeof currentConfig?.errorMessage === 'function'
|
||||
? currentConfig.errorMessage(error)
|
||||
: currentConfig?.errorMessage;
|
||||
|
||||
notifications.show({
|
||||
title: t('common:notifications.errorTitle', { defaultValue: 'Error' }),
|
||||
message: customErrorMessage || defaultErrorMessage,
|
||||
color: 'red',
|
||||
});
|
||||
|
||||
// Rethrow the error so the modal knows the submission failed
|
||||
// and re-enables the buttons for the user to try again or cancel
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[dataServices, closeActionModal, navigation, loadData, modalConfigMap, t],
|
||||
);
|
||||
|
||||
// --- Handler functions that open the confirmation modal ---
|
||||
|
||||
function handleDelete(data: E): void {
|
||||
openActionModal(ModuleAction.DELETE, data);
|
||||
}
|
||||
|
||||
async function handleActivate(data: E): Promise<void> {
|
||||
// implementation later
|
||||
console.log({ data });
|
||||
function handleActivate(data: E): void {
|
||||
openActionModal(ModuleAction.ACTIVATE, data);
|
||||
}
|
||||
|
||||
async function handleDeactivate(data: E): Promise<void> {
|
||||
// implementation later
|
||||
console.log({ data });
|
||||
function handleDeactivate(data: E): void {
|
||||
openActionModal(ModuleAction.DEACTIVATE, data);
|
||||
}
|
||||
|
||||
async function handleConfirm(data: E): Promise<void> {
|
||||
// implementation later
|
||||
console.log({ data });
|
||||
function handleConfirm(data: E): void {
|
||||
openActionModal(ModuleAction.CONFIRM, data);
|
||||
}
|
||||
|
||||
async function handleCancel(data: E): Promise<void> {
|
||||
// implementation later
|
||||
console.log({ data });
|
||||
function handleCancel(data: E): void {
|
||||
openActionModal(ModuleAction.CANCEL, data);
|
||||
}
|
||||
|
||||
async function handleRollback(data: E): Promise<void> {
|
||||
// implementation later
|
||||
console.log({ data });
|
||||
function handleRollback(data: E): void {
|
||||
openActionModal(ModuleAction.ROLLBACK, data);
|
||||
}
|
||||
|
||||
async function handleHold(data: E): Promise<void> {
|
||||
// implementation later
|
||||
console.log({ data });
|
||||
function handleHold(data: E): void {
|
||||
openActionModal(ModuleAction.HOLD, data);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -164,28 +323,28 @@ export function EnterpriseDetailPageProvider<E extends BaseEntity = BaseEntity>(
|
||||
else await handleDeactivate(currentData);
|
||||
break;
|
||||
|
||||
// --- CONFIRM (Tambahan Baru) ---
|
||||
// --- CONFIRM ---
|
||||
case ModuleAction.CONFIRM:
|
||||
if (!privileges.ALLOW_CONFIRM || !hasValidData) return;
|
||||
if (onClickConfirm) onClickConfirm(currentData);
|
||||
else await handleConfirm(currentData);
|
||||
break;
|
||||
|
||||
// --- CANCEL (Tambahan Baru) ---
|
||||
// --- CANCEL ---
|
||||
case ModuleAction.CANCEL:
|
||||
if (!privileges.ALLOW_CANCEL || !hasValidData) return;
|
||||
if (onClickCancel) onClickCancel(currentData);
|
||||
else await handleCancel(currentData);
|
||||
break;
|
||||
|
||||
// --- ROLLBACK (Tambahan Baru) ---
|
||||
// --- ROLLBACK ---
|
||||
case ModuleAction.ROLLBACK:
|
||||
if (!privileges.ALLOW_ROLLBACK || !hasValidData) return;
|
||||
if (onClickRollback) onClickRollback(currentData);
|
||||
else await handleRollback(currentData);
|
||||
break;
|
||||
|
||||
// --- HOLD (Tambahan Baru) ---
|
||||
// --- HOLD ---
|
||||
case ModuleAction.HOLD:
|
||||
if (!privileges.ALLOW_HOLD || !hasValidData) return;
|
||||
if (onClickHold) onClickHold(currentData);
|
||||
@@ -397,10 +556,10 @@ export function EnterpriseDetailPageProvider<E extends BaseEntity = BaseEntity>(
|
||||
|
||||
function makeTitle(showHighlight: boolean, key: string, pageProvide: ModulePageHeaderProps | any, detailData: any) {
|
||||
const staticTitle = pageProvide?.title;
|
||||
if (!showHighlight) {
|
||||
if (!showHighlight || !detailData) {
|
||||
return { flatTitle: staticTitle, title: staticTitle };
|
||||
} else {
|
||||
const highlightData = detailData[key];
|
||||
const highlightData = detailData && detailData[key];
|
||||
const flatTitle = `${staticTitle} | ${highlightData}`;
|
||||
|
||||
return {
|
||||
@@ -413,7 +572,7 @@ export function EnterpriseDetailPageProvider<E extends BaseEntity = BaseEntity>(
|
||||
style={{
|
||||
fontWeight: 400,
|
||||
marginLeft: '8px',
|
||||
color: 'var(--mantine-color-dimmed)',
|
||||
// color: 'var(--mantine-color-dimmed)',
|
||||
}}
|
||||
>
|
||||
| {highlightData}
|
||||
@@ -432,10 +591,10 @@ export function EnterpriseDetailPageProvider<E extends BaseEntity = BaseEntity>(
|
||||
detailData: any,
|
||||
) {
|
||||
const staticBreadcrumbs = pageProvide.breadcrumbs ?? [];
|
||||
if (!showHighlight || staticBreadcrumbs.length === 0) {
|
||||
if (!showHighlight || staticBreadcrumbs.length === 0 || !detailData) {
|
||||
return pageProvide.breadcrumbs;
|
||||
} else {
|
||||
const highlightData = detailData[key];
|
||||
const highlightData = detailData && detailData[key];
|
||||
const breadcrumbs = [
|
||||
...staticBreadcrumbs,
|
||||
{
|
||||
@@ -478,11 +637,27 @@ export function EnterpriseDetailPageProvider<E extends BaseEntity = BaseEntity>(
|
||||
miniTitleProps={{
|
||||
fz: { base: 'md', sm: 'lg' },
|
||||
}}
|
||||
badges={
|
||||
detailData ? (
|
||||
<StatusBadge
|
||||
status={detailData[statusKey as keyof typeof detailData] as string}
|
||||
getCustomConfig={getCustomStatusConfig}
|
||||
/>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</CorePageContainer>
|
||||
|
||||
{/* Action Confirmation Modal */}
|
||||
<ActionConfirmationModal<E>
|
||||
modalState={actionModalState}
|
||||
onClose={closeActionModal}
|
||||
onExecute={executeAction}
|
||||
t={t}
|
||||
/>
|
||||
</DetailPageContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user