custom_ui/frontend/src/stores/pagination.js

397 lines
11 KiB
JavaScript

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,
},
estimates: {
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",
};
},
},
});