# 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.