diff --git a/packages/core-api/src/data-services/index.ts b/packages/core-api/src/data-services/index.ts
index ff7ca20..d58722b 100644
--- a/packages/core-api/src/data-services/index.ts
+++ b/packages/core-api/src/data-services/index.ts
@@ -19,4 +19,4 @@ export type {
DataServicesConfig,
} from './types';
-export type { IDataTransformer } from './base-data.transformer';
+export type { IDataTransformer, StandardPaginationMeta } from './base-data.transformer';
diff --git a/packages/ui/src/components/ag-grid/ag-grid-provider.tsx b/packages/ui/src/components/ag-grid/ag-grid-provider.tsx
index 85bf64c..c59ca70 100644
--- a/packages/ui/src/components/ag-grid/ag-grid-provider.tsx
+++ b/packages/ui/src/components/ag-grid/ag-grid-provider.tsx
@@ -1,8 +1,10 @@
import React, { createContext, useContext, useEffect, useRef } from 'react';
import { useMantineColorScheme } from '@mantine/core';
-import type { Theme } from 'ag-grid-community';
+import { AllCommunityModule, type Theme } from 'ag-grid-community';
import { initAgGrid, type AgGridInitOptions } from './ag-grid-setup';
import { agGridMantineTheme } from './ag-grid-theme';
+import { AgGridProvider as BaseAgGridProvider } from 'ag-grid-react';
+import { AllEnterpriseModule } from 'ag-grid-enterprise';
/* ─── Context ──────────────────────────────────────────────────────── */
@@ -68,10 +70,12 @@ export function AgGridProvider({ licenseKey, bypassLicense, children }: AgGridPr
const agThemeMode = colorScheme === 'dark' ? 'dark' : 'light';
return (
-
-
- {children}
-
-
+
+
+
+ {children}
+
+
+
);
}
diff --git a/packages/ui/src/components/ag-grid/ag-grid-theme.ts b/packages/ui/src/components/ag-grid/ag-grid-theme.ts
index 7fe386c..a873e68 100644
--- a/packages/ui/src/components/ag-grid/ag-grid-theme.ts
+++ b/packages/ui/src/components/ag-grid/ag-grid-theme.ts
@@ -56,7 +56,7 @@ export const agGridMantineTheme = themeQuartz
selectedRowBackgroundColor: 'var(--mantine-primary-color-light)',
/* ── Radius ─────────────────────────────────────────────────── */
- borderRadius: 'var(--mantine-radius-xs)',
+ borderRadius: 'var(--mantine-radius-sm)',
wrapperBorderRadius: 'var(--mantine-radius-sm)',
/* ── Spacing ────────────────────────────────────────────────── */
@@ -88,8 +88,7 @@ export const agGridMantineTheme = themeQuartz
/* ── Row Styling ──────────────────────────────────────────── */
oddRowBackgroundColor: 'var(--mantine-color-dark-6)',
rowHoverColor: 'color-mix(in srgb, var(--mantine-primary-color-filled) 12%, transparent)',
- selectedRowBackgroundColor:
- 'color-mix(in srgb, var(--mantine-primary-color-filled) 18%, transparent)',
+ selectedRowBackgroundColor: 'color-mix(in srgb, var(--mantine-primary-color-filled) 18%, transparent)',
},
'dark',
);
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
new file mode 100644
index 0000000..a50b27f
--- /dev/null
+++ b/packages/ui/src/foundations/enterprise-module/components/data-table/index.tsx
@@ -0,0 +1,234 @@
+import { useMemo, useCallback, useRef } from 'react';
+import { AgGridReactProps } from 'ag-grid-react';
+import {
+ ColDef,
+ GridReadyEvent,
+ GridApi,
+ IServerSideDatasource,
+ IServerSideGetRowsParams,
+ SelectionChangedEvent,
+ GetContextMenuItemsParams,
+ MenuItemDef,
+ DefaultMenuItem,
+ StatusBar,
+} from 'ag-grid-community';
+import { Box } from '@mantine/core';
+
+import { DataGrid } from '../../../../components';
+
+import {
+ useEnterpriseModuleDataServiceContext,
+ useEnterpriseModuleSelectionContext,
+ useEnterpriseModuleTranslationContext,
+} from '../../hooks/use-module.context';
+import { BaseEntity } from '@repo/core-api/data-services';
+import { notifications } from '@mantine/notifications';
+
+export * from 'ag-grid-community';
+export * from 'ag-grid-react';
+
+// ---------------------------------------------------------------------------
+// Types & Interfaces
+// ---------------------------------------------------------------------------
+
+export type PaginationMode = 'pagination' | 'infinite-scroll';
+
+export interface EnterpriseDataTableProps extends Omit, 'rowData'> {
+ columnDefs: ColDef[];
+ gridHeight?: number | string;
+
+ /**
+ * Controls the data navigation strategy:
+ * - `'pagination'` – Manual page navigation with Previous/Next buttons (default).
+ * - `'infinite-scroll'` – Rows are lazily loaded as the user scrolls, leveraging
+ * AG Grid's server-side infinite row model.
+ *
+ * Both modes use the same server-side datasource under the hood.
+ * @default 'pagination'
+ */
+ paginationMode?: PaginationMode;
+
+ /**
+ * Custom message displayed in the loading overlay while data is being fetched.
+ * @default 'Loading data...'
+ */
+ loadingMessage?: string;
+
+ /**
+ * Custom message displayed when the datasource returns zero rows.
+ * @default 'No records found'
+ */
+ noRowsMessage?: string;
+
+ showStatusbar?: boolean;
+}
+
+// ---------------------------------------------------------------------------
+// Main Component
+// ---------------------------------------------------------------------------
+
+export function EnterpriseDataTable(props: EnterpriseDataTableProps) {
+ // ---------------------------------------------------------------------------
+ // Component Props Destructuring
+ // ---------------------------------------------------------------------------
+ const {
+ columnDefs,
+ gridHeight = 600,
+ paginationMode = 'pagination',
+ loadingMessage = 'Loading data...',
+ noRowsMessage = 'No records found',
+ showStatusbar,
+ ...restAgGridProps
+ } = props;
+
+ // ---------------------------------------------------------------------------
+ // Context & Hooks
+ // ---------------------------------------------------------------------------
+ const { t } = useEnterpriseModuleTranslationContext();
+ const { dataServices } = useEnterpriseModuleDataServiceContext();
+ const { setSelectedRows, setFilterData, metaData, setMetaData } = useEnterpriseModuleSelectionContext();
+
+ // ---------------------------------------------------------------------------
+ // Local UI State
+ // ---------------------------------------------------------------------------
+
+ // Reference to the AG Grid API for programmatic interaction
+ const gridApiRef = useRef | null>(null);
+
+ // ---------------------------------------------------------------------------
+ // Derived State & Configuration
+ // ---------------------------------------------------------------------------
+ // Determine the number of rows per page based on metadata, defaulting to 10
+ const perPage = useMemo(() => {
+ return metaData?.limit ?? 10;
+ }, [metaData]);
+
+ // Boolean flag to check if the current mode is pagination
+ const isPaginated = paginationMode === 'pagination';
+
+ // Ensure column definitions are referentially stable
+ const finalColumnDefs = useMemo[]>(() => {
+ return columnDefs;
+ }, [columnDefs]);
+
+ // Default configuration applied to all columns in the grid
+ const defaultColDef = useMemo(() => ({ flex: 1, minWidth: 100, sortable: true, resizable: true }), []);
+
+ // ---------------------------------------------------------------------------
+ // Data Source (Server-Side Row Model)
+ // ---------------------------------------------------------------------------
+ // Configures the server-side datasource to handle data fetching, pagination, and sorting
+ const datasource: IServerSideDatasource = useMemo(
+ () => ({
+ getRows: async (params: IServerSideGetRowsParams) => {
+ try {
+ const request = params.request;
+
+ // Calculate the current page based on the start row and per-page limit
+ const page = Math.floor((request.startRow ?? 0) / perPage) + 1;
+
+ // Extract sorting information from the request
+ const sortModel = request.sortModel[0];
+ const orderBy = sortModel?.colId;
+ const orderType = sortModel?.sort?.toUpperCase();
+
+ // Prepare the request parameters for the API call
+ const requestParams = { page, limit: perPage, order_by: orderBy, order_type: orderType };
+ const response = await dataServices.getMany({ params: requestParams });
+
+ if (!response.data?.data) throw new Error('Invalid response');
+
+ const rowData = response.data.data;
+ const meta = response.data.meta;
+ const rowCount = meta?.total || 0;
+
+ // Update the global metadata state
+ setMetaData(meta);
+
+ // Pass the retrieved data back to AG Grid
+ params.success({ rowData, rowCount });
+ } catch (error: any) {
+ // Display an error notification if the request fails
+ notifications.show({ title: t('common:notifications.errorTitle'), message: error?.message, color: 'red' });
+ params.fail();
+ }
+ },
+ }),
+ [dataServices, perPage, setMetaData, t],
+ );
+
+ // ---------------------------------------------------------------------------
+ // Event Handlers
+ // ---------------------------------------------------------------------------
+ // Triggered when the grid is initialized and ready
+ const onGridReady = useCallback(
+ (params: GridReadyEvent) => {
+ gridApiRef.current = params.api;
+ setSelectedRows([]);
+
+ if (params.api) {
+ // Attach the server-side datasource to the grid API
+ params.api.setGridOption('serverSideDatasource', datasource);
+ }
+ },
+ [datasource, setSelectedRows],
+ );
+
+ // Triggered whenever the row selection in the grid changes
+ const handleSelectionChanged = useCallback(
+ (event: SelectionChangedEvent) => {
+ const selectedData = event.api.getSelectedRows();
+
+ // Forward full rows to the context for backward compatibility with consumers
+ setSelectedRows(selectedData);
+ },
+ [setSelectedRows],
+ );
+
+ // Configures the context menu items available when right-clicking a cell
+ const getContextMenuItems = useCallback(
+ (
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ params: GetContextMenuItemsParams,
+ ): (DefaultMenuItem | MenuItemDef)[] | Promise<(DefaultMenuItem | MenuItemDef)[]> => {
+ return ['copy', 'copyWithHeaders'];
+ },
+ [],
+ );
+
+ const statusBar = useMemo(() => {
+ if (!showStatusbar) return undefined;
+ return { statusPanels: [{ statusPanel: 'agSelectedRowCountComponent', align: 'left' }] };
+ }, [showStatusbar]);
+
+ // ---------------------------------------------------------------------------
+ // Render
+ // ---------------------------------------------------------------------------
+ return (
+
+ {/* GRID CONTAINER */}
+
+
+ rowModelType="serverSide"
+ getRowId={(v) => v.data.id as string}
+ cacheBlockSize={perPage}
+ pagination={isPaginated}
+ paginationPageSize={isPaginated ? perPage : undefined}
+ paginationPageSizeSelector={isPaginated ? [10, 20, 50] : undefined}
+ columnDefs={finalColumnDefs}
+ defaultColDef={defaultColDef}
+ animateRows={true}
+ rowSelection={{ mode: 'multiRow', checkboxes: true, copySelectedRows: false, headerCheckbox: false }}
+ enableCellTextSelection={true}
+ onSelectionChanged={handleSelectionChanged}
+ onGridReady={onGridReady}
+ getContextMenuItems={getContextMenuItems}
+ statusBar={statusBar}
+ overlayLoadingTemplate={`${loadingMessage}`}
+ overlayNoRowsTemplate={`${noRowsMessage}`}
+ {...restAgGridProps}
+ />
+
+
+ );
+}
diff --git a/packages/ui/src/foundations/enterprise-module/entities/entity.ts b/packages/ui/src/foundations/enterprise-module/entities/entity.ts
index 4c6ee6d..35f658b 100644
--- a/packages/ui/src/foundations/enterprise-module/entities/entity.ts
+++ b/packages/ui/src/foundations/enterprise-module/entities/entity.ts
@@ -1,7 +1,7 @@
import { ReactNode } from 'react';
import type { UseFormReturn } from 'react-hook-form';
import type { ZodType } from 'zod';
-import type { BaseEntity, BaseRemoteDataServices } from '@repo/core-api/data-services';
+import type { BaseEntity, BaseRemoteDataServices, StandardPaginationMeta } from '@repo/core-api/data-services';
import type { ModulePageHeaderProps } from '../components/module-page-header';
import { PageActionsProps } from '../../../components';
@@ -132,7 +132,7 @@ export interface DataServiceSlice<
export interface SelectionSlice<
E extends BaseEntity = BaseEntity,
TFilter = Record,
- TMeta = Record,
+ TMeta = StandardPaginationMeta,
> {
selectedRows: E[];
setSelectedRows: (rows: E[]) => void;
@@ -148,7 +148,7 @@ export interface SelectionSlice<
export interface EnterpriseModuleState<
E extends BaseEntity = BaseEntity,
TFilter = Record,
- TMeta = Record,
+ TMeta = StandardPaginationMeta,
> {
metaData: TMeta | null;
setMetaData: (data: TMeta | null) => void;
diff --git a/packages/ui/src/foundations/enterprise-module/index.ts b/packages/ui/src/foundations/enterprise-module/index.ts
index fb8ba94..24bd8b3 100644
--- a/packages/ui/src/foundations/enterprise-module/index.ts
+++ b/packages/ui/src/foundations/enterprise-module/index.ts
@@ -9,3 +9,4 @@ export * from './providers/index-page.provider';
export * from './providers/detail-page.provider';
export * from './components/module-page-header';
export * from './components/action-confirmation-modal';
+export * from './components/data-table';