diff --git a/packages/core-i18n/src/languages/en/common.json b/packages/core-i18n/src/languages/en/common.json index 98e6a83..6bd07fa 100644 --- a/packages/core-i18n/src/languages/en/common.json +++ b/packages/core-i18n/src/languages/en/common.json @@ -58,7 +58,9 @@ "back": "Back", "reload": "Reload", "filter": "Filter", - "setting": "Setting" + "setting": "Setting", + "detail": "Detail", + "view": "View" }, "confirmDialog": { "delete": { diff --git a/packages/core-i18n/src/languages/id/common.json b/packages/core-i18n/src/languages/id/common.json index a390063..28cabce 100644 --- a/packages/core-i18n/src/languages/id/common.json +++ b/packages/core-i18n/src/languages/id/common.json @@ -58,7 +58,9 @@ "back": "Kembali", "reload": "Muat Ulang", "filter": "Filter", - "setting": "Pengaturan" + "setting": "Pengaturan", + "detail": "Detail", + "view": "Lihat" }, "confirmDialog": { "delete": { diff --git a/packages/ui/src/components/actions-tools/page-actions.tsx b/packages/ui/src/components/actions-tools/page-actions.tsx index 26b38a8..8226b0d 100644 --- a/packages/ui/src/components/actions-tools/page-actions.tsx +++ b/packages/ui/src/components/actions-tools/page-actions.tsx @@ -40,10 +40,12 @@ export const PageActions = memo(function PageActions({ actions = [], customButto } const isPremiumGlow = action.intent === 'primary' && action.variant === 'filled'; + const showLabel = action.showLabel !== false; + const tooltipContent = action.tooltipLabel || (!showLabel ? action.label : undefined); // 1. Button with Dropdown (Menu.Target) if (action.children && action.children.length > 0) { - const ButtonWithDropdown = ( + const ButtonWithDropdown = showLabel ? ( + ) : ( + + {action.icon} + ); return ( - {/* Shortcuts on the main button remain hidden in the Tooltip */} - {action.tooltipLabel ? ( - + {tooltipContent ? ( + {ButtonWithDropdown} ) : ( @@ -92,7 +103,7 @@ export const PageActions = memo(function PageActions({ actions = [], customButto } // 2. Regular Button (Standalone) - const StandaloneButton = ( + const StandaloneButton = showLabel ? ( + ) : ( + action.onClick?.(action.key || '')} + size="lg" + style={isPremiumGlow ? defaultButtonStyle(true).style : undefined} + > + {action.icon} + ); - return action.tooltipLabel ? ( - + return tooltipContent ? ( + {StandaloneButton} ) : ( diff --git a/packages/ui/src/components/actions-tools/row-actions.tsx b/packages/ui/src/components/actions-tools/row-actions.tsx index 35e85c4..62a3c3f 100644 --- a/packages/ui/src/components/actions-tools/row-actions.tsx +++ b/packages/ui/src/components/actions-tools/row-actions.tsx @@ -8,6 +8,7 @@ export interface RowActionsProps { /** Array of configured row-level actions. */ actions: RowActionProps[]; showLabels?: boolean; + responsiveView?: boolean; } /** @@ -16,34 +17,54 @@ export interface RowActionsProps { * * @performance Wrapped in React.memo to guarantee zero overhead inside large lists/grids. */ -export const RowActions = memo(function RowActions({ actions = [], showLabels = false }: RowActionsProps) { +export const RowActions = memo(function RowActions({ + actions = [], + showLabels = false, + responsiveView = true, +}: RowActionsProps) { /** - * Helper function to render a standalone icon button. - * Wraps the icon in a Tooltip if the configuration provides one. + * Helper function to render a standalone item. */ - const renderIcon = (action: RowActionProps, fallbackKey: string) => { + const renderItem = (action: RowActionProps, fallbackKey: string) => { const actionKey = action.key || fallbackKey; + const isButton = showLabels; + + const handleClick = (e: React.MouseEvent) => { + e.stopPropagation(); + action.onClick?.(action.key || ''); + }; + + if (isButton) { + return ( + + ); + } const iconBtn = ( - + {action.icon} + ); return action.tooltip ? ( - + {iconBtn} ) : ( @@ -51,104 +72,119 @@ export const RowActions = memo(function RowActions({ actions = [], showLabels = ); }; + const renderFlatActions = () => { + return actions.map((action, index) => { + if (action.type === 'divider') { + return ; + } + + if (action.children && action.children.length > 0) { + return ( + + {renderItem(action, `action-${index}`)} + + {action.children.map((child, childIndex) => { + if (child.type === 'divider') { + return ; + } + return ( + { + e.stopPropagation(); + child.onClick?.(child.key || ''); + }} + > + {child.label} + + ); + })} + + + ); + } + + return renderItem(action, `action-${index}`); + }); + }; + return ( {/* --- DESKTOP VIEW (hidden on mobile devices) --- */} - - {actions.map((action, index) => { - if (action.type === 'divider') { - return ; - } - - // Render Dropdown Menu for actions with children - if (action.children && action.children.length > 0) { - return ( - - {renderIcon(action, `action-${index}`)} - - {action.children.map((child, childIndex) => { - if (child.type === 'divider') { - return ; - } - return ( - child.onClick?.(child.key || '')} - > - {child.label} - - ); - })} - - - ); - } - - return renderIcon(action, `action-${index}`); - })} + + {renderFlatActions()} {/* --- MOBILE VIEW (hidden on desktop devices) --- */} - - - - - - - - - {actions.map((action, index) => { - if (action.type === 'divider') { - return ; - } - if (action.children && action.children.length > 0) { + {responsiveView && ( + + + + + + + + + {actions.map((action, index) => { + if (action.type === 'divider') { + return ; + } + + if (action.children && action.children.length > 0) { + return ( + + {action.label} + {action.children.map((child, childIndex) => { + if (child.type === 'divider') { + return ; + } + return ( + { + e.stopPropagation(); + child.onClick?.(child.key || ''); + }} + style={{ paddingLeft: '1.5rem' }} + mt="sm" + mb="sm" + > + {child.label} + + ); + })} + + ); + } + return ( - - {action.label} - {action.children.map((child, childIndex) => { - if (child.type === 'divider') { - return ; - } - return ( - child.onClick?.(child.key || '')} - style={{ paddingLeft: '1.5rem' }} // Indent nested items - mt="sm" - mb="sm" - > - {child.label} - - ); - })} - + { + e.stopPropagation(); + action.onClick?.(action.key || ''); + }} + mt="sm" + mb="sm" + > + {action.label} + ); - } - - return ( - action.onClick?.(action.key || '')} - mt="sm" - mb="sm" - > - {action.label} - - ); - })} - - - + })} + + + + )} ); }); diff --git a/packages/ui/src/components/actions-tools/types.ts b/packages/ui/src/components/actions-tools/types.ts index f465efc..1aa78e5 100644 --- a/packages/ui/src/components/actions-tools/types.ts +++ b/packages/ui/src/components/actions-tools/types.ts @@ -44,6 +44,8 @@ export interface PageActionProps extends BaseAction { /** Human-readable keyboard tooltip label (e.g., '⇧⌘N'). Shown in tooltip. */ tooltipLabel?: string; + /** Whether to show the text label for the main action button. Defaults to true. If false, renders as ActionIcon. */ + showLabel?: boolean; } /** diff --git a/packages/ui/src/components/status-badge/status-badge.tsx b/packages/ui/src/components/status-badge/status-badge.tsx index 3b3c2cc..bfc314f 100644 --- a/packages/ui/src/components/status-badge/status-badge.tsx +++ b/packages/ui/src/components/status-badge/status-badge.tsx @@ -207,7 +207,7 @@ export const DEFAULT_STATUS_MAP: Record = { [STATUS_DATA.PRESENT]: { color: '#E2B43E', leftSection: getIcon(Users) }, }; -export function StatusBadge({ status, label, getCustomConfig, color, leftSection, ...rest }: StatusBadgeProps) { +export function StatusBadge({ status, label, getCustomConfig, color, ...rest }: StatusBadgeProps) { if (!status) return null; const normalizedStatus = status?.toLowerCase() || ''; @@ -222,7 +222,6 @@ export function StatusBadge({ status, label, getCustomConfig, color, leftSection style={{ textTransform: 'capitalize' }} variant={customConfig.variant ? customConfig.variant : 'light'} color={color || customConfig.color || defaultConfig.color} - leftSection={leftSection || customConfig.leftSection || defaultConfig.leftSection} {...rest} > {label || (status ? status.replace(/-/g, ' ') : 'Unknown')} diff --git a/packages/ui/src/foundations/enterprise-module/components/data-table/components/row-actions.tsx b/packages/ui/src/foundations/enterprise-module/components/data-table/components/row-actions.tsx index b45ae8f..0731048 100644 --- a/packages/ui/src/foundations/enterprise-module/components/data-table/components/row-actions.tsx +++ b/packages/ui/src/foundations/enterprise-module/components/data-table/components/row-actions.tsx @@ -1,77 +1,151 @@ import { useMemo } from 'react'; -import { Menu, ActionIcon } from '@mantine/core'; -import { MoreVertical, Eye, Trash2, CheckCircle, XCircle } from 'lucide-react'; +import { Eye, Trash2, CheckCircle, XCircle, Edit2, Copy, PauseCircle, RotateCcw, X, Check } from 'lucide-react'; import { ModuleAction, ModuleActionType } from '../../../entities/entity'; -import { useEnterpriseModuleTranslationContext } from '../../../hooks/use-module.context'; +import { + useEnterpriseModuleTranslationContext, + useEnterpriseModuleConfigContext, +} from '../../../hooks/use-module.context'; +import { RowActionProps, RowActions } from '../../../../../components'; export interface RowActionMenuProps { data: any; - rowIndex: number; - onActionClick: (action: ModuleActionType, data: any) => void; + onActionClick: (action: ModuleActionType | 'VIEW', data: any) => void; statusKey?: string; - customActions?: (data: any, defaultActions: any[]) => any[]; + customActions?: (data: any, defaultActions: RowActionProps[]) => RowActionProps[]; } export function RowActionMenu({ data, onActionClick, statusKey = 'status', customActions }: RowActionMenuProps) { const { t } = useEnterpriseModuleTranslationContext(); + const { privileges, config } = useEnterpriseModuleConfigContext(); + const { moduleType } = config; + const status = data?.[statusKey]?.toLowerCase(); - const defaultActions = useMemo(() => { - const actions: any[] = []; + const defaultActions = useMemo(() => { + const actions: RowActionProps[] = []; + + const { + ALLOW_EDIT, + ALLOW_DELETE, + ALLOW_CREATE, + ALLOW_ACTIVATE, + ALLOW_DEACTIVATE, + ALLOW_CONFIRM, + ALLOW_CANCEL, + ALLOW_ROLLBACK, + ALLOW_HOLD, + } = privileges; + + const isTransaction = moduleType === 'TRANSACTION'; + const isMasterData = moduleType === 'MASTER_DATA'; + + const isDataActive = status === 'active'; + const isDataInActive = status === 'inactive' || status === 'draft'; // View Details actions.push({ key: 'VIEW', label: t('common:actions.detail'), + tooltip: t('common:actions.detail'), icon: , onClick: () => onActionClick('VIEW' as any, data), }); - // Active/Inactive toggle - if (status === 'active') { + if (ALLOW_EDIT) { actions.push({ - key: ModuleAction.DEACTIVATE, - label: t('common:actions.deactivate'), - icon: , - onClick: () => onActionClick(ModuleAction.DEACTIVATE, data), - }); - } else if (status === 'inactive') { - actions.push({ - key: ModuleAction.ACTIVATE, - label: t('common:actions.activate'), - icon: , - onClick: () => onActionClick(ModuleAction.ACTIVATE, data), + key: ModuleAction.EDIT, + label: t('common:actions.edit'), + tooltip: t('common:actions.edit'), + icon: , + onClick: () => onActionClick(ModuleAction.EDIT, data), }); } - // Delete - actions.push({ - key: ModuleAction.DELETE, - label: t('common:actions.delete'), - icon: , - color: 'red', - onClick: () => onActionClick(ModuleAction.DELETE, data), - }); + if (ALLOW_CREATE) { + actions.push({ + key: ModuleAction.DUPLICATE, + label: t('common:actions.duplicate'), + tooltip: t('common:actions.duplicate'), + icon: , + onClick: () => onActionClick(ModuleAction.DUPLICATE, data), + }); + } + + if (isMasterData) { + if (ALLOW_DEACTIVATE && isDataActive) { + actions.push({ + key: ModuleAction.DEACTIVATE, + label: t('common:actions.deactivate'), + tooltip: t('common:actions.deactivate'), + icon: , + onClick: () => onActionClick(ModuleAction.DEACTIVATE, data), + }); + } + if (ALLOW_ACTIVATE && isDataInActive) { + actions.push({ + key: ModuleAction.ACTIVATE, + label: t('common:actions.activate'), + tooltip: t('common:actions.activate'), + icon: , + onClick: () => onActionClick(ModuleAction.ACTIVATE, data), + }); + } + } + + if (isTransaction) { + if (ALLOW_HOLD) { + actions.push({ + key: ModuleAction.HOLD, + label: t('common:actions.hold'), + tooltip: t('common:actions.hold'), + icon: , + onClick: () => onActionClick(ModuleAction.HOLD, data), + }); + } + if (ALLOW_ROLLBACK) { + actions.push({ + key: ModuleAction.ROLLBACK, + label: t('common:actions.rollback'), + tooltip: t('common:actions.rollback'), + icon: , + onClick: () => onActionClick(ModuleAction.ROLLBACK, data), + }); + } + if (ALLOW_CANCEL) { + actions.push({ + key: ModuleAction.CANCEL, + label: t('common:actions.cancel'), + tooltip: t('common:actions.cancel'), + icon: , + onClick: () => onActionClick(ModuleAction.CANCEL, data), + }); + } + if (ALLOW_CONFIRM) { + actions.push({ + key: ModuleAction.CONFIRM, + label: t('common:actions.confirm'), + tooltip: t('common:actions.confirm'), + icon: , + onClick: () => onActionClick(ModuleAction.CONFIRM, data), + }); + } + } + + if (ALLOW_DELETE) { + actions.push({ + key: ModuleAction.DELETE, + label: t('common:actions.delete'), + tooltip: t('common:actions.delete'), + icon: , + intent: 'destructive', + onClick: () => onActionClick(ModuleAction.DELETE, data), + }); + } return actions; - }, [status, t, onActionClick, data]); + }, [status, t, onActionClick, data, privileges, moduleType]); const finalActions = customActions ? customActions(data, defaultActions) : defaultActions; - return ( - - - - - - - - {finalActions.map((action, idx) => ( - - {action.label} - - ))} - - - ); + return ; } diff --git a/packages/ui/src/foundations/enterprise-module/components/data-table/index.tsx b/packages/ui/src/foundations/enterprise-module/components/data-table/index.tsx index 51bccbf..d4a5cdd 100644 --- a/packages/ui/src/foundations/enterprise-module/components/data-table/index.tsx +++ b/packages/ui/src/foundations/enterprise-module/components/data-table/index.tsx @@ -1,4 +1,4 @@ -import { useMemo, useCallback, useRef } from 'react'; +import { useMemo, useCallback, useRef, useState } from 'react'; import { AgGridReactProps } from 'ag-grid-react'; import { ColDef, @@ -16,12 +16,17 @@ import { Box } from '@mantine/core'; import { DataGrid, StatusBadge } from '../../../../components'; import { + useEnterpriseModuleConfigContext, useEnterpriseModuleDataServiceContext, useEnterpriseModuleSelectionContext, useEnterpriseModuleTranslationContext, + useEnterpriseModuleNavigationContext, } from '../../hooks/use-module.context'; import { BaseEntity } from '@repo/core-api/data-services'; import { notifications } from '@mantine/notifications'; +import { ModuleActionType, ModuleAction, ActionModalState, ActionModalConfig } from '../../entities/entity'; +import { RowActionMenu } from './components/row-actions'; +import { ActionConfirmationModal, ACTION_TRANSLATION_MAP } from '../action-confirmation-modal'; export * from 'ag-grid-community'; export * from 'ag-grid-react'; @@ -60,6 +65,30 @@ export interface EnterpriseDataTableProps extends Omit[]) => ColDef[]; + + // Action related props + statusKey?: string; + customRowActions?: (data: E, defaultActions: any[]) => any[]; + onClickView?: (data: E) => void; + onClickEdit?: (data: E) => void; + onClickDuplicate?: (data: E) => void; + onClickDelete?: (data: E) => void; + onClickActivate?: (data: E) => void; + onClickDeactivate?: (data: E) => void; + onClickConfirm?: (data: E) => void; + onClickCancel?: (data: E) => void; + onClickRollback?: (data: E) => void; + onClickHold?: (data: E) => void; + + deleteModalConfig?: ActionModalConfig; + activateModalConfig?: ActionModalConfig; + deactivateModalConfig?: ActionModalConfig; + confirmModalConfig?: ActionModalConfig; + cancelModalConfig?: ActionModalConfig; + rollbackModalConfig?: ActionModalConfig; + holdModalConfig?: ActionModalConfig; } // --------------------------------------------------------------------------- @@ -77,6 +106,29 @@ export function EnterpriseDataTable(props: EnterpriseDataT loadingMessage = 'Loading data...', noRowsMessage = 'No records found', showStatusbar, + customPrefixColumn, + + // new action props + statusKey = 'status', + customRowActions, + onClickView, + onClickEdit, + onClickDuplicate, + onClickDelete, + onClickActivate, + onClickDeactivate, + onClickConfirm, + onClickCancel, + onClickRollback, + onClickHold, + deleteModalConfig, + activateModalConfig, + deactivateModalConfig, + confirmModalConfig, + cancelModalConfig, + rollbackModalConfig, + holdModalConfig, + ...restAgGridProps } = props; @@ -86,6 +138,11 @@ export function EnterpriseDataTable(props: EnterpriseDataT const { t } = useEnterpriseModuleTranslationContext(); const { dataServices } = useEnterpriseModuleDataServiceContext(); const { setSelectedRows, metaData, setMetaData } = useEnterpriseModuleSelectionContext(); + const navigation = useEnterpriseModuleNavigationContext(); + + const { config } = useEnterpriseModuleConfigContext(); + const { moduleType } = config; + const isTransaction = moduleType === 'TRANSACTION'; // --------------------------------------------------------------------------- // Local UI State @@ -94,12 +151,186 @@ export function EnterpriseDataTable(props: EnterpriseDataT // Reference to the AG Grid API for programmatic interaction const gridApiRef = useRef | null>(null); + // --------------------------------------------------------------------------- + // Action Handlers & Modal State + // --------------------------------------------------------------------------- + const CLOSED_MODAL: ActionModalState = useMemo(() => ({ opened: false, action: null, data: null }), []); + const [actionModalState, setActionModalState] = useState>(CLOSED_MODAL); + + 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, + ], + ); + + const openActionModal = useCallback( + (action: ModuleActionType, data: E) => { + const config = modalConfigMap[action as keyof typeof modalConfigMap]; + setActionModalState({ opened: true, action, data, config }); + }, + [modalConfigMap], + ); + + const closeActionModal = useCallback(() => { + setActionModalState(CLOSED_MODAL); + }, [CLOSED_MODAL]); + + const executeAction = useCallback( + async (action: ModuleActionType, data: E, meta?: Record) => { + 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'), + message: customSuccessMessage || defaultSuccessMessage, + color: 'teal', + }); + + closeActionModal(); + if (gridApiRef.current) { + gridApiRef.current.refreshServerSide({ purge: false }); + } + } 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'), + message: customErrorMessage || defaultErrorMessage, + color: 'red', + }); + throw error; + } + }, + [dataServices, closeActionModal, modalConfigMap, t], + ); + + const handleActionClick = useCallback( + (action: ModuleActionType | 'VIEW', data: E) => { + switch (action) { + case 'VIEW': + if (onClickView) onClickView(data); + else navigation.navigateToDetail(data.id as string); + break; + case ModuleAction.EDIT: + if (onClickEdit) onClickEdit(data); + else navigation.navigateToEdit(data.id as string); + break; + case ModuleAction.DUPLICATE: + if (onClickDuplicate) onClickDuplicate(data); + else navigation.navigateToDuplicate(data.id as string); + break; + case ModuleAction.DELETE: + if (onClickDelete) onClickDelete(data); + else openActionModal(ModuleAction.DELETE, data); + break; + case ModuleAction.ACTIVATE: + if (onClickActivate) onClickActivate(data); + else openActionModal(ModuleAction.ACTIVATE, data); + break; + case ModuleAction.DEACTIVATE: + if (onClickDeactivate) onClickDeactivate(data); + else openActionModal(ModuleAction.DEACTIVATE, data); + break; + case ModuleAction.CONFIRM: + if (onClickConfirm) onClickConfirm(data); + else openActionModal(ModuleAction.CONFIRM, data); + break; + case ModuleAction.CANCEL: + if (onClickCancel) onClickCancel(data); + else openActionModal(ModuleAction.CANCEL, data); + break; + case ModuleAction.ROLLBACK: + if (onClickRollback) onClickRollback(data); + else openActionModal(ModuleAction.ROLLBACK, data); + break; + case ModuleAction.HOLD: + if (onClickHold) onClickHold(data); + else openActionModal(ModuleAction.HOLD, data); + break; + default: + console.warn(`[ActionHandler] Unhandled action key: ${action}`); + break; + } + }, + [ + navigation, + onClickView, + onClickEdit, + onClickDuplicate, + onClickDelete, + onClickActivate, + onClickDeactivate, + onClickConfirm, + onClickCancel, + onClickRollback, + onClickHold, + openActionModal, + ], + ); + // --------------------------------------------------------------------------- // Derived State & Configuration // --------------------------------------------------------------------------- // Determine the number of rows per page based on metadata, defaulting to 10 const perPage = useMemo(() => { - console.log({ metaData }); return metaData?.limit ?? 10; }, [metaData]); @@ -108,23 +339,91 @@ export function EnterpriseDataTable(props: EnterpriseDataT // Ensure column definitions are referentially stable const finalColumnDefs = useMemo[]>(() => { - const masterDetailColumn: ColDef = { maxWidth: 50, sortable: false, cellRenderer: 'agGroupCellRenderer' }; - const statusColumn: ColDef = { - maxWidth: 130, - field: 'status' as any, - headerName: t('common:fields.status'), - cellRenderer: ({ value }: any) => , + // Dedicated Checkbox Column (Pinned to the far left) + const selectionColumn: ColDef = { + colId: 'selection_column', + maxWidth: 40, + pinned: 'left', + sortable: false, + filter: false, + suppressHeaderMenuButton: true, + checkboxSelection: true, // Manually enable checkbox selection specifically for this column + suppressMovable: true, }; - const prefixColumn: ColDef[] = [props.masterDetail ? masterDetailColumn : (null as any), statusColumn].filter( - Boolean, - ); + // Define the Master-Detail collapse/expand column + const masterDetailColumn: ColDef = { + colId: 'master_detail_column', + pinned: 'left', // Pin to the right so it remains visible during horizontal scrolling + maxWidth: 50, - return [...prefixColumn, ...columnDefs]; - }, [columnDefs, props.masterDetail]); + sortable: false, // Disable sorting + filter: false, // Disable filtering + suppressHeaderMenuButton: true, // Suppress menu to keep the header clean + suppressMovable: true, + + cellRenderer: 'agGroupCellRenderer', + }; + + // Define the Action column (for Edit, Delete, View, etc.) + const actionColumn: ColDef = { + colId: 'action_column', + pinned: 'left', // Pin to the right so it remains visible during horizontal scrolling, + width: 180, + minWidth: 100, + sortable: false, // Disable sorting + filter: false, // Disable filtering + suppressHeaderMenuButton: true, // Suppress menu to keep the header clean + suppressSizeToFit: true, // Prevent this column from stretching if you call api.sizeColumnsToFit() + suppressMovable: true, + + headerName: t('common:fields.action'), + cellRenderer: (params: any) => { + if (!params.data) return null; + return ( + + ); + }, + }; + + // Define the default Status column + const statusColumn: ColDef = { + colId: 'status', + maxWidth: 130, + + suppressHeaderMenuButton: true, // Suppress menu to keep the header clean + + field: 'status' as any, + headerName: t('common:fields.status'), + cellRenderer: ({ value }: any) => , + }; + + const prefixColumn: ColDef[] = [ + selectionColumn, + props.masterDetail ? masterDetailColumn : (null as any), + actionColumn, + statusColumn, + ].filter(Boolean); + + return [...(customPrefixColumn ? customPrefixColumn(prefixColumn) : prefixColumn), ...columnDefs]; + }, [ + columnDefs, + props.masterDetail, + isTransaction, + customPrefixColumn, + t, + statusKey, + customRowActions, + handleActionClick, + ]); // Default configuration applied to all columns in the grid - const defaultColDef = useMemo(() => ({ flex: 1, minWidth: 100, sortable: true, resizable: true }), []); + const defaultColDef = useMemo(() => ({ flex: 1, minWidth: 40, sortable: true, resizable: true }), []); // --------------------------------------------------------------------------- // Data Source (Server-Side Row Model) @@ -240,7 +539,7 @@ export function EnterpriseDataTable(props: EnterpriseDataT columnDefs={finalColumnDefs} defaultColDef={defaultColDef} animateRows={true} - rowSelection={{ mode: 'multiRow', checkboxes: true, copySelectedRows: false, headerCheckbox: false }} + rowSelection={{ mode: 'multiRow', checkboxes: false, copySelectedRows: false, headerCheckbox: false }} enableCellTextSelection={true} onSelectionChanged={handleSelectionChanged} onGridReady={onGridReady} @@ -252,6 +551,14 @@ export function EnterpriseDataTable(props: EnterpriseDataT {...restAgGridProps} /> + + {/* Action Confirmation Modal */} + + modalState={actionModalState} + onClose={closeActionModal} + onExecute={executeAction} + t={t} + /> ); } diff --git a/packages/ui/src/foundations/enterprise-module/components/module-page-header/index.tsx b/packages/ui/src/foundations/enterprise-module/components/module-page-header/index.tsx index 80d55f0..ad16a1c 100644 --- a/packages/ui/src/foundations/enterprise-module/components/module-page-header/index.tsx +++ b/packages/ui/src/foundations/enterprise-module/components/module-page-header/index.tsx @@ -238,7 +238,6 @@ export function ModulePageHeader(_props: ModulePageHeaderProps) { // Expanded / Full mode // ------------------------------------------------------------------------- - // Helper variable agar kode lebih bersih const hasBreadcrumbs = breadcrumbs && breadcrumbs.length > 0; return ( diff --git a/packages/ui/src/foundations/enterprise-module/providers/detail-page.provider.tsx b/packages/ui/src/foundations/enterprise-module/providers/detail-page.provider.tsx index e217240..86e91be 100644 --- a/packages/ui/src/foundations/enterprise-module/providers/detail-page.provider.tsx +++ b/packages/ui/src/foundations/enterprise-module/providers/detail-page.provider.tsx @@ -274,7 +274,7 @@ export function EnterpriseDetailPageProvider( // --------------------------------------------------------------------------- const handleActionClick = useCallback( async (key: string) => { - // Guard utama: Untuk aksi selain CREATE, pastikan dataId dan detailData sudah ada + // Main guard: For actions other than CREATE, make sure dataId and detailData exist. const hasValidData = Boolean(dataId && detailData); const currentData = detailData as E; @@ -356,7 +356,6 @@ export function EnterpriseDetailPageProvider( } }, [ - // Semua dependensi wajib dimasukkan agar terhindar dari bug Stale Closure navigation, privileges, dataId,