diff --git a/frontend/PAGINATION_USAGE.md b/frontend/PAGINATION_USAGE.md new file mode 100644 index 0000000..e35814f --- /dev/null +++ b/frontend/PAGINATION_USAGE.md @@ -0,0 +1,490 @@ +# Server-Side Pagination Implementation Guide + +## Overview + +This implementation provides server-side pagination with persistent state management for large datasets (5000+ records). It combines PrimeVue's lazy loading capabilities with Pinia stores for state persistence. + +## Architecture + +### Stores + +1. **`usePaginationStore`** - Manages pagination state (page, pageSize, totalRecords, sorting) +2. **`useFiltersStore`** - Manages filter state (existing, enhanced for pagination) +3. **`useLoadingStore`** - Manages loading states (existing, works with pagination) + +### Components + +1. **`DataTable`** - Enhanced with lazy loading support +2. **`Api`** - Updated with pagination and filtering parameters + +## Key Features + +✅ **Server-side pagination** - Only loads current page data +✅ **Persistent state** - Page and filter state survive navigation +✅ **Real-time filtering** - Filters reset to page 1 and re-query server +✅ **Sorting support** - Server-side sorting with state persistence +✅ **Loading states** - Integrated with existing loading system +✅ **Performance** - Handles 5000+ records efficiently + +## Usage + +### Basic Paginated DataTable + +```vue + + + +``` + +## API Implementation + +### Required API Method Structure + +```javascript +// In your API class +static async getPaginatedData(paginationParams = {}, filters = {}) { + const { + page = 0, + pageSize = 10, + sortField = null, + sortOrder = null + } = paginationParams; + + // Build database query with pagination + const offset = page * pageSize; + const limit = pageSize; + + // Apply filters to query + const whereClause = buildWhereClause(filters); + + // Apply sorting + const orderBy = sortField ? `${sortField} ${sortOrder === -1 ? 'DESC' : 'ASC'}` : ''; + + // Execute queries + const [data, totalCount] = await Promise.all([ + db.query(`SELECT * FROM table ${whereClause} ${orderBy} LIMIT ${limit} OFFSET ${offset}`), + db.query(`SELECT COUNT(*) FROM table ${whereClause}`) + ]); + + return { + data: data, + totalRecords: totalCount[0].count + }; +} +``` + +### Frappe Framework Implementation + +```javascript +static async getPaginatedClientDetails(paginationParams = {}, filters = {}) { + const { page = 0, pageSize = 10, sortField = null, sortOrder = null } = paginationParams; + + // Build Frappe filters + let frappeFilters = {}; + Object.keys(filters).forEach(key => { + if (filters[key] && filters[key].value) { + switch (key) { + case 'fullName': + frappeFilters.address_line1 = ['like', `%${filters[key].value}%`]; + break; + // Add other filter mappings + } + } + }); + + // Get total count and paginated data + const [totalCount, records] = await Promise.all([ + this.getDocCount("DocType", frappeFilters), + this.getDocsList("DocType", ["*"], frappeFilters, page, pageSize) + ]); + + // Process and return data + const processedData = records.map(record => ({ + id: record.name, + // ... other fields + })); + + return { + data: processedData, + totalRecords: totalCount + }; +} +``` + +## DataTable Props + +### New Props for Pagination + +```javascript +const props = defineProps({ + // Existing props... + + // Server-side pagination + lazy: { + type: Boolean, + default: false, // Set to true for server-side pagination + }, + totalRecords: { + type: Number, + default: 0, // Total records from server + }, + onLazyLoad: { + type: Function, + default: null, // Lazy load handler function + }, +}); +``` + +### Events + +- **`@lazy-load`** - Emitted when pagination/filtering/sorting changes +- **`@page-change`** - Emitted when page changes +- **`@sort-change`** - Emitted when sorting changes +- **`@filter-change`** - Emitted when filters change + +## Pagination Store Methods + +### Basic Usage + +```javascript +const paginationStore = usePaginationStore(); + +// Initialize pagination for a table +paginationStore.initializeTablePagination("clients", { + rows: 10, + totalRecords: 0, +}); + +// Update pagination after API response +paginationStore.setTotalRecords("clients", 1250); + +// Navigate pages +paginationStore.setPage("clients", 2); +paginationStore.nextPage("clients"); +paginationStore.previousPage("clients"); + +// Get pagination parameters for API calls +const params = paginationStore.getPaginationParams("clients"); +// Returns: { page: 2, pageSize: 10, offset: 20, limit: 10, sortField: null, sortOrder: null } + +// Get page information for display +const info = paginationStore.getPageInfo("clients"); +// Returns: { start: 21, end: 30, total: 1250 } +``` + +### Advanced Methods + +```javascript +// Handle PrimeVue lazy load events +const params = paginationStore.handleLazyLoad("clients", primeVueEvent); + +// Set sorting +paginationStore.setSorting("clients", "name", 1); // 1 for ASC, -1 for DESC + +// Change rows per page +paginationStore.setRowsPerPage("clients", 25); + +// Reset to first page (useful when filters change) +paginationStore.resetToFirstPage("clients"); + +// Get computed properties +const totalPages = paginationStore.getTotalPages("clients"); +const hasNext = paginationStore.hasNextPage("clients"); +const hasPrevious = paginationStore.hasPreviousPage("clients"); +``` + +## Filter Integration + +Filters work seamlessly with pagination: + +```javascript +// When a filter changes, pagination automatically resets to page 1 +const handleFilterChange = (fieldName, value) => { + // Update filter + filtersStore.updateTableFilter("clients", fieldName, value); + + // Pagination automatically resets to page 1 in DataTable component + // New API call is triggered with updated filters +}; +``` + +## State Persistence + +Both pagination and filter states persist across: + +- Component re-mounts +- Page navigation +- Browser refresh (if using localStorage) + +### Persistence Configuration + +```javascript +// In your store, you can add persistence +import { defineStore } from "pinia"; + +export const usePaginationStore = defineStore("pagination", { + // ... store definition + + persist: { + enabled: true, + strategies: [ + { + key: "pagination-state", + storage: localStorage, // or sessionStorage + paths: ["tablePagination"], + }, + ], + }, +}); +``` + +## Performance Considerations + +### Database Optimization + +1. **Indexes** - Ensure filtered and sorted columns are indexed +2. **Query Optimization** - Use efficient WHERE clauses +3. **Connection Pooling** - Handle concurrent requests efficiently + +### Frontend Optimization + +1. **Debounced Filtering** - Avoid excessive API calls +2. **Loading States** - Provide user feedback during requests +3. **Error Handling** - Gracefully handle API failures +4. **Memory Management** - Clear data when not needed + +### Recommended Page Sizes + +- **Small screens**: 5-10 records +- **Desktop**: 10-25 records +- **Large datasets**: 25-50 records +- **Avoid**: 100+ records per page + +## Error Handling + +```javascript +const handleLazyLoad = async (event) => { + try { + isLoading.value = true; + const result = await Api.getPaginatedData(params, filters); + + // Success handling + tableData.value = result.data; + totalRecords.value = result.totalRecords; + } catch (error) { + console.error("Pagination error:", error); + + // Reset to safe state + tableData.value = []; + totalRecords.value = 0; + + // Show user-friendly error + showErrorToast("Failed to load data. Please try again."); + + // Optionally retry with fallback parameters + if (event.page > 0) { + paginationStore.setPage(tableName, 0); + // Retry with page 0 + } + } finally { + isLoading.value = false; + } +}; +``` + +## Migration from Client-Side + +### Before (Client-side) + +```javascript +// Old approach - loads all data +onMounted(async () => { + const data = await Api.getAllClients(); // 5000+ records + tableData.value = data; +}); +``` + +### After (Server-side) + +```javascript +// New approach - loads only current page +onMounted(async () => { + paginationStore.initializeTablePagination("clients"); + + await handleLazyLoad({ + page: 0, + rows: 10, + // ... other params + }); +}); +``` + +## Testing + +### Unit Tests + +```javascript +import { usePaginationStore } from "@/stores/pagination"; + +describe("Pagination Store", () => { + it("should initialize pagination correctly", () => { + const store = usePaginationStore(); + store.initializeTablePagination("test", { rows: 20 }); + + const pagination = store.getTablePagination("test"); + expect(pagination.rows).toBe(20); + expect(pagination.page).toBe(0); + }); + + it("should handle page navigation", () => { + const store = usePaginationStore(); + store.setTotalRecords("test", 100); + store.setPage("test", 2); + + expect(store.getTablePagination("test").page).toBe(2); + expect(store.hasNextPage("test")).toBe(true); + }); +}); +``` + +### Integration Tests + +```javascript +// Test lazy loading with mock API +const mockLazyLoad = vi.fn().mockResolvedValue({ + data: [{ id: 1, name: "Test" }], + totalRecords: 50, +}); + +// Test component with mocked API +const wrapper = mount(DataTableComponent, { + props: { + lazy: true, + onLazyLoad: mockLazyLoad, + }, +}); + +// Verify API calls +expect(mockLazyLoad).toHaveBeenCalledWith({ + page: 0, + rows: 10, + // ... expected parameters +}); +``` + +## Troubleshooting + +### Common Issues + +1. **Infinite Loading** + - Check API endpoint returns correct totalRecords + - Verify pagination parameters are calculated correctly + +2. **Filters Not Working** + - Ensure filter parameters are passed to API correctly + - Check database query includes WHERE clauses + +3. **Page State Not Persisting** + - Verify store persistence is configured + - Check localStorage/sessionStorage permissions + +4. **Performance Issues** + - Add database indexes for filtered/sorted columns + - Optimize API query efficiency + - Consider reducing page size + +### Debug Information + +```javascript +// Add debug logging to lazy load handler +const handleLazyLoad = async (event) => { + console.log("Lazy Load Event:", { + page: event.page, + rows: event.rows, + sortField: event.sortField, + sortOrder: event.sortOrder, + filters: event.filters, + timestamp: new Date().toISOString(), + }); + + // ... rest of implementation +}; +``` + +This implementation provides a robust, performant solution for handling large datasets with persistent pagination and filtering state. diff --git a/frontend/documentation/components/DataTable.md b/frontend/documentation/components/DataTable.md index efbbc44..c30bacc 100644 --- a/frontend/documentation/components/DataTable.md +++ b/frontend/documentation/components/DataTable.md @@ -2,7 +2,7 @@ ## Overview -A feature-rich data table component built with PrimeVue's DataTable. This component provides advanced functionality including pagination, sorting, filtering, row selection, and customizable column types with persistent filter state management. +A feature-rich data table component built with PrimeVue's DataTable. This component provides advanced functionality including server-side pagination, sorting, manual filtering with apply buttons, row selection, page data caching, and customizable column types with persistent state management. ## Basic Usage @@ -17,75 +17,119 @@ A feature-rich data table component built with PrimeVue's DataTable. This compon ``` ## Props ### `columns` (Array) - Required + - **Description:** Array of column configuration objects that define the table structure - **Type:** `Array` - **Required:** `true` ### `data` (Array) - Required + - **Description:** Array of data objects to display in the table - **Type:** `Array` - **Required:** `true` ### `tableName` (String) - Required + - **Description:** Unique identifier for the table, used for persistent filter state management - **Type:** `String` - **Required:** `true` +### `totalRecords` (Number) + +- **Description:** Total number of records available on the server (for lazy loading) +- **Type:** `Number` +- **Default:** `0` + +### `onLazyLoad` (Function) + +- **Description:** Custom pagination event handler for server-side data loading +- **Type:** `Function` +- **Default:** `null` + ### `filters` (Object) -- **Description:** Initial filter configuration object + +- **Description:** Initial filter configuration object (used for non-lazy tables) - **Type:** `Object` - **Default:** `{ global: { value: null, matchMode: FilterMatchMode.CONTAINS } }` +## Server-Side Pagination & Lazy Loading + +When `lazy` is set to `true`, the DataTable operates in server-side mode with the following features: + +### Automatic Caching + +- **Page Data Caching:** Previously loaded pages are cached to prevent unnecessary API calls +- **Cache Duration:** 5 minutes default expiration time +- **Cache Size:** Maximum 50 pages per table with automatic cleanup +- **Smart Cache Keys:** Based on page, sorting, and filter combinations + +### Manual Filter Controls + +- **Apply Button:** Filters are applied manually via button click to prevent excessive API calls +- **Clear Button:** Quick reset of all active filters +- **Enter Key Support:** Apply filters by pressing Enter in any filter field +- **Visual Feedback:** Shows active filters and pending changes + +### Quick Page Navigation + +- **Page Dropdown:** Jump directly to any page number +- **Page Info Display:** Shows current record range and totals +- **Persistent State:** Page selection survives component re-mounts + ## Column Configuration Each column object in the `columns` array supports the following properties: ### Basic Properties + - **`fieldName`** (String, required) - The field name in the data object - **`label`** (String, required) - Display label for the column header - **`sortable`** (Boolean, default: `false`) - Enables sorting for this column - **`filterable`** (Boolean, default: `false`) - Enables row-level filtering for this column ### Column Types + - **`type`** (String) - Defines special rendering behavior for the column #### Available Types: ##### `'status'` Type + Renders values as colored tags/badges: + ```javascript { fieldName: 'status', @@ -97,13 +141,16 @@ Renders values as colored tags/badges: ``` **Status Colors:** + - `'completed'` → Success (green) -- `'in progress'` → Warning (yellow/orange) +- `'in progress'` → Warning (yellow/orange) - `'not started'` → Danger (red) - Other values → Info (blue) ##### `'button'` Type + Renders values as clickable buttons: + ```javascript { fieldName: 'action', @@ -115,120 +162,237 @@ Renders values as clickable buttons: ## Events ### `rowClick` + - **Description:** Emitted when a button-type column is clicked - **Payload:** PrimeVue slot properties object containing row data - **Usage:** `@row-click="handleRowClick"` +### `lazy-load` + +- **Description:** Emitted when lazy loading is triggered (pagination, sorting, filtering) +- **Payload:** Event object with page, sorting, and filter information +- **Usage:** `@lazy-load="handleLazyLoad"` + +### `page-change` + +- **Description:** Emitted when page changes +- **Payload:** PrimeVue page event object + +### `sort-change` + +- **Description:** Emitted when sorting changes +- **Payload:** PrimeVue sort event object + +### `filter-change` + +- **Description:** Emitted when filters are applied +- **Payload:** PrimeVue filter event object + ```javascript const handleRowClick = (slotProps) => { - console.log('Clicked row data:', slotProps.data) - console.log('Row index:', slotProps.index) -} + console.log("Clicked row data:", slotProps.data); + console.log("Row index:", slotProps.index); +}; + +const handleLazyLoad = async (event) => { + // event contains: page, rows, sortField, sortOrder, filters + console.log("Lazy load event:", event); + + // Load data from API based on event parameters + const result = await Api.getData({ + page: event.page, + pageSize: event.rows, + sortField: event.sortField, + sortOrder: event.sortOrder, + filters: event.filters, + }); + + // Update component data + tableData.value = result.data; + totalRecords.value = result.totalRecords; +}; ``` ## Features ### Pagination + - **Rows per page options:** 5, 10, 20, 50 - **Default rows per page:** 10 - **Built-in pagination controls** ### Sorting + - **Multiple column sorting** support - **Removable sort** - click to remove sort from a column - **Sort indicators** in column headers ### Filtering -- **Row-level filtering** for filterable columns -- **Text-based search** with real-time filtering -- **Persistent filter state** across component re-renders -- **Global search capability** + +- **Manual filter application** with Apply/Clear buttons +- **Text-based search** for filterable columns +- **Persistent filter state** across component re-renders and page navigation +- **Visual filter feedback** showing active filters and pending changes +- **Enter key support** for quick filter application ### Selection + - **Multiple row selection** with checkboxes - **Meta key selection** (Ctrl/Cmd + click for individual selection) - **Unique row identification** using `dataKey="id"` ### Scrolling + - **Vertical scrolling** with fixed height (70vh) - **Horizontal scrolling** for wide tables - **Fixed headers** during scroll ### State Management + - **Persistent filters** using Pinia store (`useFiltersStore`) - **Automatic filter initialization** on component mount - **Cross-component filter synchronization** ## Usage Examples -### Basic Table +### Server-Side Paginated Table (Recommended for Large Datasets) + ```vue ``` +### Basic Client-Side Table + +```vue + + + +``` + ### Status Table + ```vue ``` ### Interactive Table with Buttons + ```vue @@ -15,8 +24,17 @@ import { onMounted, ref } from "vue"; import DataTable from "../common/DataTable.vue"; import Api from "../../api"; import { FilterMatchMode } from "@primevue/core"; +import { useLoadingStore } from "../../stores/loading"; +import { usePaginationStore } from "../../stores/pagination"; +import { useFiltersStore } from "../../stores/filters"; + +const loadingStore = useLoadingStore(); +const paginationStore = usePaginationStore(); +const filtersStore = useFiltersStore(); const tableData = ref([]); +const totalRecords = ref(0); +const isLoading = ref(false); const addNewWarranty = () => { // TODO: Open modal or navigate to create warranty form @@ -25,13 +43,6 @@ const addNewWarranty = () => { // For now, just log the action }; -const filters = { - customer: { value: null, matchMode: FilterMatchMode.CONTAINS }, - warrantyId: { value: null, matchMode: FilterMatchMode.CONTAINS }, - address: { value: null, matchMode: FilterMatchMode.CONTAINS }, - assignedTechnician: { value: null, matchMode: FilterMatchMode.CONTAINS }, -}; - const columns = [ { label: "Warranty ID", @@ -93,12 +104,119 @@ const columns = [ }, ]; -onMounted(async () => { - if (tableData.value.length > 0) { - return; +// Handle lazy loading events from DataTable +const handleLazyLoad = async (event) => { + console.log("Warranties page - handling lazy load:", event); + + try { + isLoading.value = true; + + // Get pagination parameters + const paginationParams = { + page: event.page || 0, + pageSize: event.rows || 10, + sortField: event.sortField, + sortOrder: event.sortOrder, + }; + + // Get filters (convert PrimeVue format to API format) + const filters = {}; + if (event.filters) { + Object.keys(event.filters).forEach((key) => { + if (key !== "global" && event.filters[key] && event.filters[key].value) { + filters[key] = event.filters[key]; + } + }); + } + + // Check cache first + const cachedData = paginationStore.getCachedPage( + "warranties", + paginationParams.page, + paginationParams.pageSize, + paginationParams.sortField, + paginationParams.sortOrder, + filters, + ); + + if (cachedData) { + // Use cached data + tableData.value = cachedData.records; + totalRecords.value = cachedData.totalRecords; + paginationStore.setTotalRecords("warranties", cachedData.totalRecords); + + console.log("Loaded from cache:", { + records: cachedData.records.length, + total: cachedData.totalRecords, + page: paginationParams.page + 1, + }); + return; + } + + console.log("Making API call with:", { paginationParams, filters }); + + // For now, use existing API but we should create a paginated version + // TODO: Create Api.getPaginatedWarrantyData() method + let data = await Api.getWarrantyData(); + + // Simulate pagination on client side for now + const startIndex = paginationParams.page * paginationParams.pageSize; + const endIndex = startIndex + paginationParams.pageSize; + const paginatedData = data.slice(startIndex, endIndex); + + // Update local state + tableData.value = paginatedData; + totalRecords.value = data.length; + + // Update pagination store with new total + paginationStore.setTotalRecords("warranties", data.length); + + // Cache the result + paginationStore.setCachedPage( + "warranties", + paginationParams.page, + paginationParams.pageSize, + paginationParams.sortField, + paginationParams.sortOrder, + filters, + { + records: paginatedData, + totalRecords: data.length, + }, + ); + + console.log("Loaded from API:", { + records: paginatedData.length, + total: data.length, + page: paginationParams.page + 1, + }); + } catch (error) { + console.error("Error loading warranty data:", error); + tableData.value = []; + totalRecords.value = 0; + } finally { + isLoading.value = false; } - let data = await Api.getWarrantyData(); - tableData.value = data; +}; + +// Load initial data +onMounted(async () => { + // Initialize pagination and filters + paginationStore.initializeTablePagination("warranties", { rows: 10 }); + filtersStore.initializeTableFilters("warranties", columns); + + // Load first page + const initialPagination = paginationStore.getTablePagination("warranties"); + const initialFilters = filtersStore.getTableFilters("warranties"); + + await handleLazyLoad({ + page: initialPagination.page, + rows: initialPagination.rows, + first: initialPagination.first, + sortField: initialPagination.sortField, + sortOrder: initialPagination.sortOrder, + filters: initialFilters, + }); }); diff --git a/frontend/src/stores/pagination.js b/frontend/src/stores/pagination.js new file mode 100644 index 0000000..963d5c4 --- /dev/null +++ b/frontend/src/stores/pagination.js @@ -0,0 +1,388 @@ +import { defineStore } from "pinia"; + +export const usePaginationStore = defineStore("pagination", { + state: () => ({ + // Store pagination state by table/component name + tablePagination: { + clients: { + first: 0, // Starting index for current page + page: 0, // Current page number (0-based) + rows: 10, // Items per page + totalRecords: 0, // Total number of records available + sortField: null, // Current sort field + sortOrder: null, // Sort direction (1 for asc, -1 for desc) + }, + jobs: { + first: 0, + page: 0, + rows: 10, + totalRecords: 0, + sortField: null, + sortOrder: null, + }, + timesheets: { + first: 0, + page: 0, + rows: 10, + totalRecords: 0, + sortField: null, + sortOrder: null, + }, + warranties: { + first: 0, + page: 0, + rows: 10, + totalRecords: 0, + sortField: null, + sortOrder: null, + }, + routes: { + first: 0, + page: 0, + rows: 10, + totalRecords: 0, + sortField: null, + sortOrder: null, + }, + }, + + // Page data cache: tableName -> { pageKey -> { data, timestamp, filterHash } } + pageCache: {}, + + // Cache configuration + cacheTimeout: 5 * 60 * 1000, // 5 minutes in milliseconds + maxCacheSize: 50, // Maximum number of cached pages per table + }), + + getters: { + // Get pagination state for a specific table + getTablePagination: (state) => (tableName) => { + return ( + state.tablePagination[tableName] || { + first: 0, + page: 0, + rows: 10, + totalRecords: 0, + sortField: null, + sortOrder: null, + } + ); + }, + + // Calculate total pages for a table + getTotalPages: (state) => (tableName) => { + const pagination = state.tablePagination[tableName]; + if (!pagination || pagination.totalRecords === 0) return 0; + return Math.ceil(pagination.totalRecords / pagination.rows); + }, + + // Check if there's a next page + hasNextPage: (state) => (tableName) => { + const pagination = state.tablePagination[tableName]; + if (!pagination) return false; + return pagination.page + 1 < Math.ceil(pagination.totalRecords / pagination.rows); + }, + + // Check if there's a previous page + hasPreviousPage: (state) => (tableName) => { + const pagination = state.tablePagination[tableName]; + if (!pagination) return false; + return pagination.page > 0; + }, + + // Get current page info for display + getPageInfo: (state) => (tableName) => { + const pagination = state.tablePagination[tableName]; + if (!pagination) return { start: 0, end: 0, total: 0 }; + + const start = pagination.first + 1; + const end = Math.min(pagination.first + pagination.rows, pagination.totalRecords); + const total = pagination.totalRecords; + + return { start, end, total }; + }, + }, + + actions: { + // Initialize pagination for a table if it doesn't exist + initializeTablePagination(tableName, options = {}) { + if (!this.tablePagination[tableName]) { + this.tablePagination[tableName] = { + first: 0, + page: 0, + rows: options.rows || 10, + totalRecords: options.totalRecords || 0, + sortField: options.sortField || null, + sortOrder: options.sortOrder || null, + }; + } + }, + + // Update pagination state for a table + updateTablePagination(tableName, paginationData) { + if (!this.tablePagination[tableName]) { + this.initializeTablePagination(tableName); + } + + // Update provided fields + Object.keys(paginationData).forEach((key) => { + if (paginationData[key] !== undefined) { + this.tablePagination[tableName][key] = paginationData[key]; + } + }); + + // Ensure consistency between page and first + if (paginationData.page !== undefined) { + this.tablePagination[tableName].first = + paginationData.page * this.tablePagination[tableName].rows; + } else if (paginationData.first !== undefined) { + this.tablePagination[tableName].page = Math.floor( + paginationData.first / this.tablePagination[tableName].rows, + ); + } + }, + + // Set current page + setPage(tableName, page) { + this.updateTablePagination(tableName, { + page: page, + first: page * this.getTablePagination(tableName).rows, + }); + }, + + // Set rows per page + setRowsPerPage(tableName, rows) { + const currentPagination = this.getTablePagination(tableName); + const newPage = Math.floor(currentPagination.first / rows); + + this.updateTablePagination(tableName, { + rows: rows, + page: newPage, + first: newPage * rows, + }); + }, + + // Set total records (usually after API response) + setTotalRecords(tableName, totalRecords) { + this.updateTablePagination(tableName, { + totalRecords: totalRecords, + }); + + // Ensure current page is valid + const currentPagination = this.getTablePagination(tableName); + const maxPages = Math.ceil(totalRecords / currentPagination.rows); + if (currentPagination.page >= maxPages && maxPages > 0) { + this.setPage(tableName, maxPages - 1); + } + }, + + // Set sort information + setSorting(tableName, sortField, sortOrder) { + this.updateTablePagination(tableName, { + sortField: sortField, + sortOrder: sortOrder, + }); + }, + + // Go to next page + nextPage(tableName) { + const pagination = this.getTablePagination(tableName); + if (this.hasNextPage(tableName)) { + this.setPage(tableName, pagination.page + 1); + } + }, + + // Go to previous page + previousPage(tableName) { + const pagination = this.getTablePagination(tableName); + if (this.hasPreviousPage(tableName)) { + this.setPage(tableName, pagination.page - 1); + } + }, + + // Go to first page + firstPage(tableName) { + this.setPage(tableName, 0); + }, + + // Go to last page + lastPage(tableName) { + const totalPages = this.getTotalPages(tableName); + if (totalPages > 0) { + this.setPage(tableName, totalPages - 1); + } + }, + + // Reset pagination to first page (useful when filters change) + resetToFirstPage(tableName) { + this.updateTablePagination(tableName, { + page: 0, + first: 0, + }); + }, + + // Clear all pagination data for a table + clearTablePagination(tableName) { + if (this.tablePagination[tableName]) { + this.tablePagination[tableName] = { + first: 0, + page: 0, + rows: 10, + totalRecords: 0, + sortField: null, + sortOrder: null, + }; + } + }, + + // Get formatted pagination parameters for API calls + getPaginationParams(tableName) { + const pagination = this.getTablePagination(tableName); + return { + page: pagination.page, + pageSize: pagination.rows, + offset: pagination.first, + limit: pagination.rows, + sortField: pagination.sortField, + sortOrder: pagination.sortOrder, + }; + }, + + // Handle PrimeVue lazy pagination event + handleLazyLoad(tableName, event) { + console.log("Pagination lazy load event:", event); + + const page = Math.floor(event.first / event.rows); + + this.updateTablePagination(tableName, { + first: event.first, + page: page, + rows: event.rows, + sortField: event.sortField, + sortOrder: event.sortOrder, + }); + + return this.getPaginationParams(tableName); + }, + + // Cache management methods + generateCacheKey(page, pageSize, sortField, sortOrder, filters) { + const filterHash = this.hashFilters(filters); + return `${page}-${pageSize}-${sortField || "none"}-${sortOrder || 0}-${filterHash}`; + }, + + hashFilters(filters) { + if (!filters || Object.keys(filters).length === 0) return "no-filters"; + + const sortedKeys = Object.keys(filters).sort(); + const filterString = sortedKeys + .map((key) => { + const filter = filters[key]; + const value = filter?.value || ""; + const matchMode = filter?.matchMode || ""; + return `${key}:${value}:${matchMode}`; + }) + .join("|"); + + // Simple hash function + let hash = 0; + for (let i = 0; i < filterString.length; i++) { + const char = filterString.charCodeAt(i); + hash = (hash << 5) - hash + char; + hash = hash & hash; // Convert to 32bit integer + } + return Math.abs(hash).toString(36); + }, + + getCachedPage(tableName, page, pageSize, sortField, sortOrder, filters) { + if (!this.pageCache[tableName]) return null; + + const cacheKey = this.generateCacheKey(page, pageSize, sortField, sortOrder, filters); + const cachedEntry = this.pageCache[tableName][cacheKey]; + + if (!cachedEntry) return null; + + // Check if cache entry is still valid (not expired) + const now = Date.now(); + if (now - cachedEntry.timestamp > this.cacheTimeout) { + // Cache expired, remove it + delete this.pageCache[tableName][cacheKey]; + return null; + } + + console.log(`Cache HIT for ${tableName} page ${page + 1}`); + return cachedEntry.data; + }, + + setCachedPage(tableName, page, pageSize, sortField, sortOrder, filters, data) { + if (!this.pageCache[tableName]) { + this.pageCache[tableName] = {}; + } + + const cacheKey = this.generateCacheKey(page, pageSize, sortField, sortOrder, filters); + + // Clean up old cache entries if we're at the limit + const cacheKeys = Object.keys(this.pageCache[tableName]); + if (cacheKeys.length >= this.maxCacheSize) { + // Remove oldest entries (simple FIFO approach) + const sortedEntries = cacheKeys + .map((key) => ({ key, timestamp: this.pageCache[tableName][key].timestamp })) + .sort((a, b) => a.timestamp - b.timestamp); + + // Remove oldest 25% of entries + const toRemove = Math.floor(this.maxCacheSize * 0.25); + for (let i = 0; i < toRemove; i++) { + delete this.pageCache[tableName][sortedEntries[i].key]; + } + } + + this.pageCache[tableName][cacheKey] = { + data: JSON.parse(JSON.stringify(data)), // Deep clone to prevent mutations + timestamp: Date.now(), + }; + + console.log( + `Cache SET for ${tableName} page ${page + 1}, total cached pages: ${Object.keys(this.pageCache[tableName]).length}`, + ); + }, + + clearTableCache(tableName) { + if (this.pageCache[tableName]) { + delete this.pageCache[tableName]; + console.log(`Cache cleared for ${tableName}`); + } + }, + + clearExpiredCache() { + const now = Date.now(); + Object.keys(this.pageCache).forEach((tableName) => { + Object.keys(this.pageCache[tableName]).forEach((cacheKey) => { + if (now - this.pageCache[tableName][cacheKey].timestamp > this.cacheTimeout) { + delete this.pageCache[tableName][cacheKey]; + } + }); + + // If no cache entries left for this table, remove the table key + if (Object.keys(this.pageCache[tableName]).length === 0) { + delete this.pageCache[tableName]; + } + }); + }, + + getCacheStats(tableName) { + if (!this.pageCache[tableName]) { + return { totalPages: 0, totalSize: 0 }; + } + + const pages = Object.keys(this.pageCache[tableName]); + const totalSize = pages.reduce((size, key) => { + return size + JSON.stringify(this.pageCache[tableName][key]).length; + }, 0); + + return { + totalPages: pages.length, + totalSize: Math.round(totalSize / 1024) + " KB", + }; + }, + }, +});