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} /> ); }