update filter functionality
This commit is contained in:
parent
464c62d1e5
commit
9431a0502a
490
frontend/PAGINATION_USAGE.md
Normal file
490
frontend/PAGINATION_USAGE.md
Normal file
@ -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
|
||||||
|
<template>
|
||||||
|
<DataTable
|
||||||
|
:data="tableData"
|
||||||
|
:columns="columns"
|
||||||
|
tableName="clients"
|
||||||
|
:lazy="true"
|
||||||
|
:totalRecords="totalRecords"
|
||||||
|
:loading="isLoading"
|
||||||
|
:onLazyLoad="handleLazyLoad"
|
||||||
|
@lazy-load="handleLazyLoad"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from "vue";
|
||||||
|
import { usePaginationStore } from "@/stores/pagination";
|
||||||
|
import { useFiltersStore } from "@/stores/filters";
|
||||||
|
import Api from "@/api";
|
||||||
|
|
||||||
|
const paginationStore = usePaginationStore();
|
||||||
|
const filtersStore = useFiltersStore();
|
||||||
|
|
||||||
|
const tableData = ref([]);
|
||||||
|
const totalRecords = ref(0);
|
||||||
|
const isLoading = ref(false);
|
||||||
|
|
||||||
|
const handleLazyLoad = async (event) => {
|
||||||
|
try {
|
||||||
|
isLoading.value = true;
|
||||||
|
|
||||||
|
const paginationParams = {
|
||||||
|
page: event.page || 0,
|
||||||
|
pageSize: event.rows || 10,
|
||||||
|
sortField: event.sortField,
|
||||||
|
sortOrder: event.sortOrder,
|
||||||
|
};
|
||||||
|
|
||||||
|
const filters = {};
|
||||||
|
if (event.filters) {
|
||||||
|
Object.keys(event.filters).forEach((key) => {
|
||||||
|
if (key !== "global" && event.filters[key]?.value) {
|
||||||
|
filters[key] = event.filters[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await Api.getPaginatedData(paginationParams, filters);
|
||||||
|
|
||||||
|
tableData.value = result.data;
|
||||||
|
totalRecords.value = result.totalRecords;
|
||||||
|
paginationStore.setTotalRecords("tableName", result.totalRecords);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading data:", error);
|
||||||
|
tableData.value = [];
|
||||||
|
totalRecords.value = 0;
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
// Initialize stores
|
||||||
|
paginationStore.initializeTablePagination("tableName", { rows: 10 });
|
||||||
|
filtersStore.initializeTableFilters("tableName", columns);
|
||||||
|
|
||||||
|
// Load initial data
|
||||||
|
const pagination = paginationStore.getTablePagination("tableName");
|
||||||
|
const filters = filtersStore.getTableFilters("tableName");
|
||||||
|
|
||||||
|
await handleLazyLoad({
|
||||||
|
page: pagination.page,
|
||||||
|
rows: pagination.rows,
|
||||||
|
first: pagination.first,
|
||||||
|
sortField: pagination.sortField,
|
||||||
|
sortOrder: pagination.sortOrder,
|
||||||
|
filters: filters,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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.
|
||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## Overview
|
## 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
|
## Basic Usage
|
||||||
|
|
||||||
@ -17,75 +17,119 @@ A feature-rich data table component built with PrimeVue's DataTable. This compon
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
import { ref } from "vue";
|
||||||
import DataTable from './components/common/DataTable.vue'
|
import DataTable from "./components/common/DataTable.vue";
|
||||||
|
|
||||||
const tableColumns = ref([
|
const tableColumns = ref([
|
||||||
{
|
{
|
||||||
fieldName: 'name',
|
fieldName: "name",
|
||||||
label: 'Name',
|
label: "Name",
|
||||||
sortable: true,
|
sortable: true,
|
||||||
filterable: true
|
filterable: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldName: 'status',
|
fieldName: "status",
|
||||||
label: 'Status',
|
label: "Status",
|
||||||
type: 'status',
|
type: "status",
|
||||||
sortable: true,
|
sortable: true,
|
||||||
filterable: true
|
filterable: true,
|
||||||
}
|
},
|
||||||
])
|
]);
|
||||||
|
|
||||||
const tableData = ref([
|
const tableData = ref([
|
||||||
{ id: 1, name: 'John Doe', status: 'completed' },
|
{ id: 1, name: "John Doe", status: "completed" },
|
||||||
{ id: 2, name: 'Jane Smith', status: 'in progress' }
|
{ id: 2, name: "Jane Smith", status: "in progress" },
|
||||||
])
|
]);
|
||||||
|
|
||||||
const handleRowClick = (event) => {
|
const handleRowClick = (event) => {
|
||||||
console.log('Row clicked:', event.data)
|
console.log("Row clicked:", event.data);
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
```
|
```
|
||||||
|
|
||||||
## Props
|
## Props
|
||||||
|
|
||||||
### `columns` (Array) - Required
|
### `columns` (Array) - Required
|
||||||
|
|
||||||
- **Description:** Array of column configuration objects that define the table structure
|
- **Description:** Array of column configuration objects that define the table structure
|
||||||
- **Type:** `Array<Object>`
|
- **Type:** `Array<Object>`
|
||||||
- **Required:** `true`
|
- **Required:** `true`
|
||||||
|
|
||||||
### `data` (Array) - Required
|
### `data` (Array) - Required
|
||||||
|
|
||||||
- **Description:** Array of data objects to display in the table
|
- **Description:** Array of data objects to display in the table
|
||||||
- **Type:** `Array<Object>`
|
- **Type:** `Array<Object>`
|
||||||
- **Required:** `true`
|
- **Required:** `true`
|
||||||
|
|
||||||
### `tableName` (String) - Required
|
### `tableName` (String) - Required
|
||||||
|
|
||||||
- **Description:** Unique identifier for the table, used for persistent filter state management
|
- **Description:** Unique identifier for the table, used for persistent filter state management
|
||||||
- **Type:** `String`
|
- **Type:** `String`
|
||||||
- **Required:** `true`
|
- **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)
|
### `filters` (Object)
|
||||||
- **Description:** Initial filter configuration object
|
|
||||||
|
- **Description:** Initial filter configuration object (used for non-lazy tables)
|
||||||
- **Type:** `Object`
|
- **Type:** `Object`
|
||||||
- **Default:** `{ global: { value: null, matchMode: FilterMatchMode.CONTAINS } }`
|
- **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
|
## Column Configuration
|
||||||
|
|
||||||
Each column object in the `columns` array supports the following properties:
|
Each column object in the `columns` array supports the following properties:
|
||||||
|
|
||||||
### Basic Properties
|
### Basic Properties
|
||||||
|
|
||||||
- **`fieldName`** (String, required) - The field name in the data object
|
- **`fieldName`** (String, required) - The field name in the data object
|
||||||
- **`label`** (String, required) - Display label for the column header
|
- **`label`** (String, required) - Display label for the column header
|
||||||
- **`sortable`** (Boolean, default: `false`) - Enables sorting for this column
|
- **`sortable`** (Boolean, default: `false`) - Enables sorting for this column
|
||||||
- **`filterable`** (Boolean, default: `false`) - Enables row-level filtering for this column
|
- **`filterable`** (Boolean, default: `false`) - Enables row-level filtering for this column
|
||||||
|
|
||||||
### Column Types
|
### Column Types
|
||||||
|
|
||||||
- **`type`** (String) - Defines special rendering behavior for the column
|
- **`type`** (String) - Defines special rendering behavior for the column
|
||||||
|
|
||||||
#### Available Types:
|
#### Available Types:
|
||||||
|
|
||||||
##### `'status'` Type
|
##### `'status'` Type
|
||||||
|
|
||||||
Renders values as colored tags/badges:
|
Renders values as colored tags/badges:
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
{
|
{
|
||||||
fieldName: 'status',
|
fieldName: 'status',
|
||||||
@ -97,13 +141,16 @@ Renders values as colored tags/badges:
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Status Colors:**
|
**Status Colors:**
|
||||||
|
|
||||||
- `'completed'` → Success (green)
|
- `'completed'` → Success (green)
|
||||||
- `'in progress'` → Warning (yellow/orange)
|
- `'in progress'` → Warning (yellow/orange)
|
||||||
- `'not started'` → Danger (red)
|
- `'not started'` → Danger (red)
|
||||||
- Other values → Info (blue)
|
- Other values → Info (blue)
|
||||||
|
|
||||||
##### `'button'` Type
|
##### `'button'` Type
|
||||||
|
|
||||||
Renders values as clickable buttons:
|
Renders values as clickable buttons:
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
{
|
{
|
||||||
fieldName: 'action',
|
fieldName: 'action',
|
||||||
@ -115,120 +162,237 @@ Renders values as clickable buttons:
|
|||||||
## Events
|
## Events
|
||||||
|
|
||||||
### `rowClick`
|
### `rowClick`
|
||||||
|
|
||||||
- **Description:** Emitted when a button-type column is clicked
|
- **Description:** Emitted when a button-type column is clicked
|
||||||
- **Payload:** PrimeVue slot properties object containing row data
|
- **Payload:** PrimeVue slot properties object containing row data
|
||||||
- **Usage:** `@row-click="handleRowClick"`
|
- **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
|
```javascript
|
||||||
const handleRowClick = (slotProps) => {
|
const handleRowClick = (slotProps) => {
|
||||||
console.log('Clicked row data:', slotProps.data)
|
console.log("Clicked row data:", slotProps.data);
|
||||||
console.log('Row index:', slotProps.index)
|
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
|
## Features
|
||||||
|
|
||||||
### Pagination
|
### Pagination
|
||||||
|
|
||||||
- **Rows per page options:** 5, 10, 20, 50
|
- **Rows per page options:** 5, 10, 20, 50
|
||||||
- **Default rows per page:** 10
|
- **Default rows per page:** 10
|
||||||
- **Built-in pagination controls**
|
- **Built-in pagination controls**
|
||||||
|
|
||||||
### Sorting
|
### Sorting
|
||||||
|
|
||||||
- **Multiple column sorting** support
|
- **Multiple column sorting** support
|
||||||
- **Removable sort** - click to remove sort from a column
|
- **Removable sort** - click to remove sort from a column
|
||||||
- **Sort indicators** in column headers
|
- **Sort indicators** in column headers
|
||||||
|
|
||||||
### Filtering
|
### Filtering
|
||||||
- **Row-level filtering** for filterable columns
|
|
||||||
- **Text-based search** with real-time filtering
|
- **Manual filter application** with Apply/Clear buttons
|
||||||
- **Persistent filter state** across component re-renders
|
- **Text-based search** for filterable columns
|
||||||
- **Global search capability**
|
- **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
|
### Selection
|
||||||
|
|
||||||
- **Multiple row selection** with checkboxes
|
- **Multiple row selection** with checkboxes
|
||||||
- **Meta key selection** (Ctrl/Cmd + click for individual selection)
|
- **Meta key selection** (Ctrl/Cmd + click for individual selection)
|
||||||
- **Unique row identification** using `dataKey="id"`
|
- **Unique row identification** using `dataKey="id"`
|
||||||
|
|
||||||
### Scrolling
|
### Scrolling
|
||||||
|
|
||||||
- **Vertical scrolling** with fixed height (70vh)
|
- **Vertical scrolling** with fixed height (70vh)
|
||||||
- **Horizontal scrolling** for wide tables
|
- **Horizontal scrolling** for wide tables
|
||||||
- **Fixed headers** during scroll
|
- **Fixed headers** during scroll
|
||||||
|
|
||||||
### State Management
|
### State Management
|
||||||
|
|
||||||
- **Persistent filters** using Pinia store (`useFiltersStore`)
|
- **Persistent filters** using Pinia store (`useFiltersStore`)
|
||||||
- **Automatic filter initialization** on component mount
|
- **Automatic filter initialization** on component mount
|
||||||
- **Cross-component filter synchronization**
|
- **Cross-component filter synchronization**
|
||||||
|
|
||||||
## Usage Examples
|
## Usage Examples
|
||||||
|
|
||||||
### Basic Table
|
### Server-Side Paginated Table (Recommended for Large Datasets)
|
||||||
|
|
||||||
```vue
|
```vue
|
||||||
<script setup>
|
<script setup>
|
||||||
const columns = [
|
import { ref } from "vue";
|
||||||
{ fieldName: 'id', label: 'ID', sortable: true },
|
import DataTable from "./components/common/DataTable.vue";
|
||||||
{ fieldName: 'name', label: 'Name', sortable: true, filterable: true },
|
import Api from "./api.js";
|
||||||
{ fieldName: 'email', label: 'Email', filterable: true }
|
|
||||||
]
|
|
||||||
|
|
||||||
const data = [
|
const columns = [
|
||||||
{ id: 1, name: 'John Doe', email: 'john@example.com' },
|
{ fieldName: "id", label: "ID", sortable: true },
|
||||||
{ id: 2, name: 'Jane Smith', email: 'jane@example.com' }
|
{ fieldName: "name", label: "Name", sortable: true, filterable: true },
|
||||||
]
|
{ fieldName: "email", label: "Email", filterable: true },
|
||||||
|
];
|
||||||
|
|
||||||
|
const tableData = ref([]);
|
||||||
|
const totalRecords = ref(0);
|
||||||
|
const isLoading = ref(false);
|
||||||
|
|
||||||
|
const handleLazyLoad = async (event) => {
|
||||||
|
try {
|
||||||
|
isLoading.value = true;
|
||||||
|
|
||||||
|
// Convert PrimeVue event to API parameters
|
||||||
|
const params = {
|
||||||
|
page: event.page,
|
||||||
|
pageSize: event.rows,
|
||||||
|
sortField: event.sortField,
|
||||||
|
sortOrder: event.sortOrder,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Convert filters
|
||||||
|
const filters = {};
|
||||||
|
if (event.filters) {
|
||||||
|
Object.keys(event.filters).forEach((key) => {
|
||||||
|
if (event.filters[key]?.value) {
|
||||||
|
filters[key] = event.filters[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// API call with caching support
|
||||||
|
const result = await Api.getPaginatedData(params, filters);
|
||||||
|
|
||||||
|
tableData.value = result.data;
|
||||||
|
totalRecords.value = result.totalRecords;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading data:", error);
|
||||||
|
tableData.value = [];
|
||||||
|
totalRecords.value = 0;
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<DataTable
|
<DataTable
|
||||||
|
:data="tableData"
|
||||||
:columns="columns"
|
:columns="columns"
|
||||||
:data="data"
|
tableName="myTable"
|
||||||
table-name="users-table"
|
:lazy="true"
|
||||||
|
:totalRecords="totalRecords"
|
||||||
|
:loading="isLoading"
|
||||||
|
:onLazyLoad="handleLazyLoad"
|
||||||
|
@lazy-load="handleLazyLoad"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Basic Client-Side Table
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<script setup>
|
||||||
|
const columns = [
|
||||||
|
{ fieldName: "id", label: "ID", sortable: true },
|
||||||
|
{ fieldName: "name", label: "Name", sortable: true, filterable: true },
|
||||||
|
{ fieldName: "email", label: "Email", filterable: true },
|
||||||
|
];
|
||||||
|
|
||||||
|
const data = [
|
||||||
|
{ id: 1, name: "John Doe", email: "john@example.com" },
|
||||||
|
{ id: 2, name: "Jane Smith", email: "jane@example.com" },
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DataTable :data="data" :columns="columns" tableName="basicTable" />
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
### Status Table
|
### Status Table
|
||||||
|
|
||||||
```vue
|
```vue
|
||||||
<script setup>
|
<script setup>
|
||||||
const columns = [
|
const columns = [
|
||||||
{ fieldName: 'task', label: 'Task', sortable: true, filterable: true },
|
{ fieldName: "task", label: "Task", sortable: true, filterable: true },
|
||||||
{ fieldName: 'status', label: 'Status', type: 'status', sortable: true, filterable: true },
|
{
|
||||||
{ fieldName: 'assignee', label: 'Assignee', filterable: true }
|
fieldName: "status",
|
||||||
]
|
label: "Status",
|
||||||
|
type: "status",
|
||||||
|
sortable: true,
|
||||||
|
filterable: true,
|
||||||
|
},
|
||||||
|
{ fieldName: "assignee", label: "Assignee", filterable: true },
|
||||||
|
];
|
||||||
|
|
||||||
const data = [
|
const data = [
|
||||||
{ id: 1, task: 'Setup project', status: 'completed', assignee: 'John' },
|
{ id: 1, task: "Setup project", status: "completed", assignee: "John" },
|
||||||
{ id: 2, task: 'Write tests', status: 'in progress', assignee: 'Jane' },
|
{ id: 2, task: "Write tests", status: "in progress", assignee: "Jane" },
|
||||||
{ id: 3, task: 'Deploy app', status: 'not started', assignee: 'Bob' }
|
{ id: 3, task: "Deploy app", status: "not started", assignee: "Bob" },
|
||||||
]
|
];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<DataTable
|
<DataTable :columns="columns" :data="data" table-name="tasks-table" />
|
||||||
:columns="columns"
|
|
||||||
:data="data"
|
|
||||||
table-name="tasks-table"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Interactive Table with Buttons
|
### Interactive Table with Buttons
|
||||||
|
|
||||||
```vue
|
```vue
|
||||||
<script setup>
|
<script setup>
|
||||||
const columns = [
|
const columns = [
|
||||||
{ fieldName: 'name', label: 'Name', sortable: true, filterable: true },
|
{ fieldName: "name", label: "Name", sortable: true, filterable: true },
|
||||||
{ fieldName: 'status', label: 'Status', type: 'status', sortable: true },
|
{ fieldName: "status", label: "Status", type: "status", sortable: true },
|
||||||
{ fieldName: 'action', label: 'Action', type: 'button' }
|
{ fieldName: "action", label: "Action", type: "button" },
|
||||||
]
|
];
|
||||||
|
|
||||||
const data = [
|
const data = [
|
||||||
{ id: 1, name: 'Project A', status: 'completed', action: 'View Details' },
|
{ id: 1, name: "Project A", status: "completed", action: "View Details" },
|
||||||
{ id: 2, name: 'Project B', status: 'in progress', action: 'Edit' }
|
{ id: 2, name: "Project B", status: "in progress", action: "Edit" },
|
||||||
]
|
];
|
||||||
|
|
||||||
const handleRowClick = (slotProps) => {
|
const handleRowClick = (slotProps) => {
|
||||||
const { data, index } = slotProps
|
const { data, index } = slotProps;
|
||||||
console.log(`Action clicked for ${data.name} at row ${index}`)
|
console.log(`Action clicked for ${data.name} at row ${index}`);
|
||||||
// Handle the action (navigate, open modal, etc.)
|
// Handle the action (navigate, open modal, etc.)
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -242,14 +406,15 @@ const handleRowClick = (slotProps) => {
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Custom Filters
|
### Custom Filters
|
||||||
|
|
||||||
```vue
|
```vue
|
||||||
<script setup>
|
<script setup>
|
||||||
import { FilterMatchMode } from '@primevue/core'
|
import { FilterMatchMode } from "@primevue/core";
|
||||||
|
|
||||||
const customFilters = {
|
const customFilters = {
|
||||||
global: { value: null, matchMode: FilterMatchMode.CONTAINS },
|
global: { value: null, matchMode: FilterMatchMode.CONTAINS },
|
||||||
name: { value: 'John', matchMode: FilterMatchMode.STARTS_WITH }
|
name: { value: "John", matchMode: FilterMatchMode.STARTS_WITH },
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -267,11 +432,13 @@ const customFilters = {
|
|||||||
The component integrates with a Pinia store (`useFiltersStore`) for persistent filter state:
|
The component integrates with a Pinia store (`useFiltersStore`) for persistent filter state:
|
||||||
|
|
||||||
### Store Methods Used
|
### Store Methods Used
|
||||||
|
|
||||||
- `initializeTableFilters(tableName, columns)` - Initialize filters for a table
|
- `initializeTableFilters(tableName, columns)` - Initialize filters for a table
|
||||||
- `getTableFilters(tableName)` - Get current filters for a table
|
- `getTableFilters(tableName)` - Get current filters for a table
|
||||||
- `updateTableFilter(tableName, fieldName, value, matchMode)` - Update a specific filter
|
- `updateTableFilter(tableName, fieldName, value, matchMode)` - Update a specific filter
|
||||||
|
|
||||||
### Filter Persistence
|
### Filter Persistence
|
||||||
|
|
||||||
- Filters are automatically saved when changed
|
- Filters are automatically saved when changed
|
||||||
- Filters persist across component re-mounts
|
- Filters persist across component re-mounts
|
||||||
- Each table maintains separate filter state based on `tableName`
|
- Each table maintains separate filter state based on `tableName`
|
||||||
@ -279,6 +446,7 @@ The component integrates with a Pinia store (`useFiltersStore`) for persistent f
|
|||||||
## Styling
|
## Styling
|
||||||
|
|
||||||
The component uses PrimeVue's default DataTable styling with:
|
The component uses PrimeVue's default DataTable styling with:
|
||||||
|
|
||||||
- **Scrollable layout** with fixed 70vh height
|
- **Scrollable layout** with fixed 70vh height
|
||||||
- **Responsive design** that adapts to container width
|
- **Responsive design** that adapts to container width
|
||||||
- **Consistent spacing** and typography
|
- **Consistent spacing** and typography
|
||||||
@ -287,11 +455,13 @@ The component uses PrimeVue's default DataTable styling with:
|
|||||||
## Performance Considerations
|
## Performance Considerations
|
||||||
|
|
||||||
### Large Datasets
|
### Large Datasets
|
||||||
|
|
||||||
- **Virtual scrolling** is not implemented - consider for datasets > 1000 rows
|
- **Virtual scrolling** is not implemented - consider for datasets > 1000 rows
|
||||||
- **Client-side pagination** may impact performance with very large datasets
|
- **Client-side pagination** may impact performance with very large datasets
|
||||||
- **Debounced filtering** helps with real-time search performance
|
- **Debounced filtering** helps with real-time search performance
|
||||||
|
|
||||||
### Memory Management
|
### Memory Management
|
||||||
|
|
||||||
- **Filter state persistence** may accumulate over time
|
- **Filter state persistence** may accumulate over time
|
||||||
- Consider implementing filter cleanup for unused tables
|
- Consider implementing filter cleanup for unused tables
|
||||||
- **Component re-rendering** is optimized through computed properties
|
- **Component re-rendering** is optimized through computed properties
|
||||||
@ -310,6 +480,7 @@ The component uses PrimeVue's default DataTable styling with:
|
|||||||
## Accessibility
|
## Accessibility
|
||||||
|
|
||||||
The component includes:
|
The component includes:
|
||||||
|
|
||||||
- **Keyboard navigation** support via PrimeVue
|
- **Keyboard navigation** support via PrimeVue
|
||||||
- **Screen reader compatibility** with proper ARIA labels
|
- **Screen reader compatibility** with proper ARIA labels
|
||||||
- **High contrast** status badges for visibility
|
- **High contrast** status badges for visibility
|
||||||
@ -319,6 +490,7 @@ The component includes:
|
|||||||
## Browser Support
|
## Browser Support
|
||||||
|
|
||||||
Compatible with all modern browsers that support:
|
Compatible with all modern browsers that support:
|
||||||
|
|
||||||
- Vue 3 Composition API
|
- Vue 3 Composition API
|
||||||
- ES6+ features
|
- ES6+ features
|
||||||
- CSS Grid and Flexbox
|
- CSS Grid and Flexbox
|
||||||
@ -329,4 +501,4 @@ Compatible with all modern browsers that support:
|
|||||||
- **Vue 3** with Composition API
|
- **Vue 3** with Composition API
|
||||||
- **PrimeVue** DataTable, Column, Tag, Button, InputText components
|
- **PrimeVue** DataTable, Column, Tag, Button, InputText components
|
||||||
- **@primevue/core** for FilterMatchMode
|
- **@primevue/core** for FilterMatchMode
|
||||||
- **Pinia** store for state management (`useFiltersStore`)
|
- **Pinia** store for state management (`useFiltersStore`)
|
||||||
|
|||||||
@ -23,73 +23,174 @@ class Api {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static async getClientDetails(forTable = true) {
|
static async getClientDetails(options = {}) {
|
||||||
const data = [];
|
const {
|
||||||
const addresses = await this.getDocsList("Address", ["*"]);
|
forTable = true,
|
||||||
for (const addr of addresses) {
|
page = 0,
|
||||||
const clientDetail = {};
|
pageSize = 10,
|
||||||
|
filters = {},
|
||||||
|
sortField = null,
|
||||||
|
sortOrder = null,
|
||||||
|
searchTerm = null,
|
||||||
|
} = options;
|
||||||
|
|
||||||
const customer = await this.getDetailedDoc(
|
console.log("DEBUG: API - getClientDetails called with options:", options);
|
||||||
"Customer",
|
|
||||||
addr["custom_customer_to_bill"],
|
// Build filters for the Address query
|
||||||
|
let addressFilters = {};
|
||||||
|
|
||||||
|
// Add search functionality across multiple fields
|
||||||
|
if (searchTerm) {
|
||||||
|
// Note: This is a simplified version. You might want to implement
|
||||||
|
// full-text search on the backend for better performance
|
||||||
|
addressFilters.address_line1 = ["like", `%${searchTerm}%`];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add any custom filters
|
||||||
|
Object.keys(filters).forEach((key) => {
|
||||||
|
if (filters[key] && filters[key].value) {
|
||||||
|
// Map frontend filter names to backend field names if needed
|
||||||
|
switch (key) {
|
||||||
|
case "fullName":
|
||||||
|
// This will need special handling since fullName is constructed
|
||||||
|
// For now, we'll search in address_line1 or city
|
||||||
|
addressFilters.address_line1 = ["like", `%${filters[key].value}%`];
|
||||||
|
break;
|
||||||
|
// Add other filter mappings as needed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get total count first for pagination
|
||||||
|
const totalCount = await this.getDocCount("Address", addressFilters);
|
||||||
|
|
||||||
|
// Get paginated addresses
|
||||||
|
const addresses = await this.getDocsList(
|
||||||
|
"Address",
|
||||||
|
["*"],
|
||||||
|
addressFilters,
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
);
|
);
|
||||||
|
|
||||||
const quotations = await this.getDocsList("Quotation", [], {
|
const data = [];
|
||||||
custom_installation_address: addr["name"],
|
const processedData = [];
|
||||||
});
|
|
||||||
const quoteDetails =
|
|
||||||
quotations.length > 0
|
|
||||||
? await this.getDetailedDoc("Quotation", quotations[0]["name"])
|
|
||||||
: null;
|
|
||||||
|
|
||||||
const jobs = await this.getDocsList("Project", [], {
|
// Process each address to build client details
|
||||||
project_template: "SNW Install",
|
for (const addr of addresses) {
|
||||||
custom_installation_address: addr["name"],
|
try {
|
||||||
});
|
const clientDetail = {};
|
||||||
const jobDetails =
|
|
||||||
jobs.length > 0 ? await this.getDetailedDoc("Project", jobs[0]["name"]) : null;
|
|
||||||
|
|
||||||
clientDetail.customer = customer;
|
const customer = await this.getDetailedDoc(
|
||||||
clientDetail.address = addr;
|
"Customer",
|
||||||
clientDetail.estimate = quoteDetails;
|
addr["custom_customer_to_bill"],
|
||||||
clientDetail.job = jobDetails;
|
);
|
||||||
|
|
||||||
const totalPaid = quoteDetails
|
const quotations = await this.getDocsList("Quotation", [], {
|
||||||
? quoteDetails.payment_schedule
|
custom_installation_address: addr["name"],
|
||||||
? quoteDetails.payment_schedule.reduce(
|
});
|
||||||
(sum, payment) => sum + (payment.paid_amount || 0),
|
const quoteDetails =
|
||||||
0,
|
quotations.length > 0
|
||||||
)
|
? await this.getDetailedDoc("Quotation", quotations[0]["name"])
|
||||||
: 0
|
: null;
|
||||||
: 0;
|
|
||||||
const tableRow = {
|
const jobs = await this.getDocsList("Project", [], {
|
||||||
fullName: `${customer.customer_name} - ${addr.address_line1}, ${addr.city} ${addr.state}`,
|
project_template: "SNW Install",
|
||||||
appointmentStatus: "not started",
|
custom_installation_address: addr["name"],
|
||||||
estimateStatus: quoteDetails
|
});
|
||||||
? quoteDetails.custom_response == "Accepted"
|
const jobDetails =
|
||||||
? "completed"
|
jobs.length > 0
|
||||||
: "in progress"
|
? await this.getDetailedDoc("Project", jobs[0]["name"])
|
||||||
: "not started",
|
: null;
|
||||||
paymentStatus: quoteDetails
|
|
||||||
? totalPaid < quoteDetails.grand_total
|
clientDetail.customer = customer;
|
||||||
? "in progress"
|
clientDetail.address = addr;
|
||||||
: "completed"
|
clientDetail.estimate = quoteDetails;
|
||||||
: "not started",
|
clientDetail.job = jobDetails;
|
||||||
jobStatus: jobDetails
|
|
||||||
? jobDetails.status === "Completed"
|
const totalPaid = quoteDetails
|
||||||
? "completed"
|
? quoteDetails.payment_schedule
|
||||||
: "in progress"
|
? quoteDetails.payment_schedule.reduce(
|
||||||
: "not started",
|
(sum, payment) => sum + (payment.paid_amount || 0),
|
||||||
};
|
0,
|
||||||
if (forTable) {
|
)
|
||||||
data.push(tableRow);
|
: 0
|
||||||
} else {
|
: 0;
|
||||||
data.push(clientDetail);
|
|
||||||
|
const tableRow = {
|
||||||
|
id: addr.name, // Add unique ID for DataTable
|
||||||
|
fullName: `${customer.customer_name} - ${addr.address_line1}, ${addr.city} ${addr.state}`,
|
||||||
|
appointmentStatus: "not started",
|
||||||
|
estimateStatus: quoteDetails
|
||||||
|
? quoteDetails.custom_response == "Accepted"
|
||||||
|
? "completed"
|
||||||
|
: "in progress"
|
||||||
|
: "not started",
|
||||||
|
paymentStatus: quoteDetails
|
||||||
|
? totalPaid < quoteDetails.grand_total
|
||||||
|
? "in progress"
|
||||||
|
: "completed"
|
||||||
|
: "not started",
|
||||||
|
jobStatus: jobDetails
|
||||||
|
? jobDetails.status === "Completed"
|
||||||
|
? "completed"
|
||||||
|
: "in progress"
|
||||||
|
: "not started",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (forTable) {
|
||||||
|
data.push(tableRow);
|
||||||
|
} else {
|
||||||
|
data.push(clientDetail);
|
||||||
|
}
|
||||||
|
processedData.push(clientDetail);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error processing address ${addr.name}:`, error);
|
||||||
|
// Continue with other addresses even if one fails
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply client-side sorting if needed (better to do on server)
|
||||||
|
if (sortField && forTable) {
|
||||||
|
data.sort((a, b) => {
|
||||||
|
const aValue = a[sortField] || "";
|
||||||
|
const bValue = b[sortField] || "";
|
||||||
|
const comparison = aValue.localeCompare(bValue);
|
||||||
|
return sortOrder === -1 ? -comparison : comparison;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply client-side filtering for constructed fields like fullName
|
||||||
|
let filteredData = data;
|
||||||
|
if (filters.fullName && filters.fullName.value && forTable) {
|
||||||
|
const searchValue = filters.fullName.value.toLowerCase();
|
||||||
|
filteredData = data.filter((item) =>
|
||||||
|
item.fullName.toLowerCase().includes(searchValue),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("DEBUG: API - Fetched Client Details:", {
|
||||||
|
total: totalCount,
|
||||||
|
page: page,
|
||||||
|
pageSize: pageSize,
|
||||||
|
returned: filteredData.length,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Return paginated response with metadata
|
||||||
|
return {
|
||||||
|
data: filteredData,
|
||||||
|
pagination: {
|
||||||
|
page: page,
|
||||||
|
pageSize: pageSize,
|
||||||
|
total: totalCount,
|
||||||
|
totalPages: Math.ceil(totalCount / pageSize),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("DEBUG: API - Error fetching client details:", error);
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
// const data = DataUtils.dummyClientData;
|
|
||||||
console.log("DEBUG: API - Fetched Client Details: ", data);
|
|
||||||
return data;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static async getJobDetails() {
|
static async getJobDetails() {
|
||||||
@ -125,6 +226,32 @@ class Api {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get paginated client data with filtering and sorting
|
||||||
|
* @param {Object} paginationParams - Pagination parameters from store
|
||||||
|
* @param {Object} filters - Filter parameters from store
|
||||||
|
* @returns {Promise<{data: Array, totalRecords: number}>}
|
||||||
|
*/
|
||||||
|
static async getPaginatedClientDetails(paginationParams = {}, filters = {}) {
|
||||||
|
const { page = 0, pageSize = 10, sortField = null, sortOrder = null } = paginationParams;
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
forTable: true,
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
filters,
|
||||||
|
sortField,
|
||||||
|
sortOrder,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await this.getClientDetails(options);
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: result.data,
|
||||||
|
totalRecords: result.pagination.total,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch a list of documents from a specific doctype.
|
* Fetch a list of documents from a specific doctype.
|
||||||
*
|
*
|
||||||
|
|||||||
@ -1,12 +1,86 @@
|
|||||||
<template lang="html">
|
<template lang="html">
|
||||||
|
<!-- Filter Controls Panel -->
|
||||||
|
<div v-if="lazy && hasFilters" class="filter-controls-panel mb-3 p-3 bg-light rounded">
|
||||||
|
<div class="row g-3 align-items-end">
|
||||||
|
<div v-for="col in filterableColumns" :key="col.fieldName" class="col-md-4 col-lg-3">
|
||||||
|
<label :for="`filter-${col.fieldName}`" class="form-label small fw-semibold">
|
||||||
|
{{ col.label }}
|
||||||
|
</label>
|
||||||
|
<InputText
|
||||||
|
:id="`filter-${col.fieldName}`"
|
||||||
|
v-model="pendingFilters[col.fieldName]"
|
||||||
|
:placeholder="`Filter by ${col.label.toLowerCase()}...`"
|
||||||
|
class="form-control"
|
||||||
|
@keyup.enter="applyFilters"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 col-lg-3">
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<Button
|
||||||
|
label="Apply Filters"
|
||||||
|
icon="pi pi-search"
|
||||||
|
@click="applyFilters"
|
||||||
|
:disabled="!hasFilterChanges"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
label="Clear"
|
||||||
|
icon="pi pi-times"
|
||||||
|
@click="clearFilters"
|
||||||
|
severity="secondary"
|
||||||
|
outlined
|
||||||
|
size="small"
|
||||||
|
:disabled="!hasActiveFilters"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="hasActiveFilters" class="mt-2">
|
||||||
|
<small class="text-muted"> Active filters: {{ getActiveFiltersText() }} </small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Page Jump Controls -->
|
||||||
|
<div v-if="lazy && totalPages > 1" class="page-controls-panel mb-3 p-2 bg-light rounded">
|
||||||
|
<div class="row g-3 align-items-center">
|
||||||
|
<div class="col-auto">
|
||||||
|
<small class="text-muted">Quick navigation:</small>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<select
|
||||||
|
v-model="selectedPageJump"
|
||||||
|
@change="jumpToPage"
|
||||||
|
class="form-select form-select-sm"
|
||||||
|
style="width: auto"
|
||||||
|
>
|
||||||
|
<option value="">Jump to page...</option>
|
||||||
|
<option v-for="page in totalPages" :key="page" :value="page">
|
||||||
|
Page {{ page }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<small class="text-muted">
|
||||||
|
Showing {{ getPageInfo().start }} - {{ getPageInfo().end }} of
|
||||||
|
{{ getPageInfo().total }} records
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<DataTable
|
<DataTable
|
||||||
:value="data"
|
:value="data"
|
||||||
:rowsPerPageOptions="[5, 10, 20, 50]"
|
:rowsPerPageOptions="[5, 10, 20, 50]"
|
||||||
:paginator="true"
|
:paginator="true"
|
||||||
:rows="10"
|
:rows="currentRows"
|
||||||
|
:lazy="lazy"
|
||||||
|
:totalRecords="lazy ? totalRecords : data.length"
|
||||||
|
@page="handlePage"
|
||||||
|
@sort="handleSort"
|
||||||
|
@filter="handleFilter"
|
||||||
sortMode="multiple"
|
sortMode="multiple"
|
||||||
removableSort
|
removableSort
|
||||||
filterDisplay="row"
|
filterDisplay="none"
|
||||||
v-model:filters="filterRef"
|
v-model:filters="filterRef"
|
||||||
scrollable
|
scrollable
|
||||||
scrollHeight="70vh"
|
scrollHeight="70vh"
|
||||||
@ -63,19 +137,20 @@
|
|||||||
</DataTable>
|
</DataTable>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { defineProps, computed, onMounted, watch } from "vue";
|
import { defineProps, computed, onMounted, watch, ref } from "vue";
|
||||||
import DataTable from "primevue/datatable";
|
import DataTable from "primevue/datatable";
|
||||||
import Column from "primevue/column";
|
import Column from "primevue/column";
|
||||||
import Tag from "primevue/tag";
|
import Tag from "primevue/tag";
|
||||||
import Button from "primevue/button";
|
import Button from "primevue/button";
|
||||||
import InputText from "primevue/inputtext";
|
import InputText from "primevue/inputtext";
|
||||||
import { ref } from "vue";
|
|
||||||
import { FilterMatchMode } from "@primevue/core";
|
import { FilterMatchMode } from "@primevue/core";
|
||||||
import { useFiltersStore } from "../../stores/filters";
|
import { useFiltersStore } from "../../stores/filters";
|
||||||
import { useLoadingStore } from "../../stores/loading";
|
import { useLoadingStore } from "../../stores/loading";
|
||||||
|
import { usePaginationStore } from "../../stores/pagination";
|
||||||
|
|
||||||
const filtersStore = useFiltersStore();
|
const filtersStore = useFiltersStore();
|
||||||
const loadingStore = useLoadingStore();
|
const loadingStore = useLoadingStore();
|
||||||
|
const paginationStore = usePaginationStore();
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
columns: {
|
columns: {
|
||||||
@ -117,9 +192,23 @@ const props = defineProps({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
|
// Server-side pagination support
|
||||||
|
lazy: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
totalRecords: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
// Custom pagination event handler
|
||||||
|
onLazyLoad: {
|
||||||
|
type: Function,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(["rowClick"]);
|
const emit = defineEmits(["rowClick", "lazy-load", "page-change", "sort-change", "filter-change"]);
|
||||||
|
|
||||||
// Computed loading state that considers both prop and global store
|
// Computed loading state that considers both prop and global store
|
||||||
const loading = computed(() => {
|
const loading = computed(() => {
|
||||||
@ -134,9 +223,23 @@ const loading = computed(() => {
|
|||||||
return props.loading;
|
return props.loading;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Initialize filters in store when component mounts
|
// Get current rows per page from pagination store or default
|
||||||
|
const currentRows = computed(() => {
|
||||||
|
if (props.lazy) {
|
||||||
|
return paginationStore.getTablePagination(props.tableName).rows;
|
||||||
|
}
|
||||||
|
return 10; // Default for non-lazy tables
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize filters and pagination in store when component mounts
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
filtersStore.initializeTableFilters(props.tableName, props.columns);
|
filtersStore.initializeTableFilters(props.tableName, props.columns);
|
||||||
|
if (props.lazy) {
|
||||||
|
paginationStore.initializeTablePagination(props.tableName, {
|
||||||
|
rows: 10,
|
||||||
|
totalRecords: props.totalRecords,
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get filters from store, with fallback to props.filters
|
// Get filters from store, with fallback to props.filters
|
||||||
@ -187,17 +290,167 @@ watch(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const selectedRows = ref();
|
const selectedRows = ref();
|
||||||
|
const pendingFilters = ref({});
|
||||||
|
const selectedPageJump = ref("");
|
||||||
|
|
||||||
// Handle filter input changes
|
// Computed properties for filtering
|
||||||
const handleFilterInput = (fieldName, value, filterCallback) => {
|
const filterableColumns = computed(() => {
|
||||||
// Get the current filter to preserve the match mode
|
return props.columns.filter((col) => col.filterable);
|
||||||
const currentFilter = filterRef.value[fieldName];
|
});
|
||||||
const matchMode = currentFilter?.matchMode || FilterMatchMode.CONTAINS;
|
|
||||||
|
|
||||||
// Update the store with both value and match mode
|
const hasFilters = computed(() => {
|
||||||
filtersStore.updateTableFilter(props.tableName, fieldName, value, matchMode);
|
return filterableColumns.value.length > 0;
|
||||||
// Call the PrimeVue filter callback
|
});
|
||||||
filterCallback();
|
|
||||||
|
const hasActiveFilters = computed(() => {
|
||||||
|
const currentFilters = filtersStore.getTableFilters(props.tableName);
|
||||||
|
return Object.values(currentFilters).some(
|
||||||
|
(filter) => filter.value && filter.value.trim() !== "",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasFilterChanges = computed(() => {
|
||||||
|
const currentFilters = filtersStore.getTableFilters(props.tableName);
|
||||||
|
return Object.keys(pendingFilters.value).some((key) => {
|
||||||
|
const pending = pendingFilters.value[key] || "";
|
||||||
|
const current = currentFilters[key]?.value || "";
|
||||||
|
return pending.trim() !== current.trim();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalPages = computed(() => {
|
||||||
|
if (!props.lazy) return 0;
|
||||||
|
return paginationStore.getTotalPages(props.tableName);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize pending filters from store
|
||||||
|
onMounted(() => {
|
||||||
|
const currentFilters = filtersStore.getTableFilters(props.tableName);
|
||||||
|
filterableColumns.value.forEach((col) => {
|
||||||
|
pendingFilters.value[col.fieldName] = currentFilters[col.fieldName]?.value || "";
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Filter management methods
|
||||||
|
const applyFilters = () => {
|
||||||
|
// Update store with pending filter values
|
||||||
|
Object.keys(pendingFilters.value).forEach((fieldName) => {
|
||||||
|
const value = pendingFilters.value[fieldName]?.trim();
|
||||||
|
filtersStore.updateTableFilter(
|
||||||
|
props.tableName,
|
||||||
|
fieldName,
|
||||||
|
value || null,
|
||||||
|
FilterMatchMode.CONTAINS,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// For lazy tables, reset to first page and trigger reload
|
||||||
|
if (props.lazy) {
|
||||||
|
paginationStore.resetToFirstPage(props.tableName);
|
||||||
|
triggerLazyLoad();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearFilters = () => {
|
||||||
|
// Clear pending filters
|
||||||
|
filterableColumns.value.forEach((col) => {
|
||||||
|
pendingFilters.value[col.fieldName] = "";
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear store filters
|
||||||
|
filtersStore.clearTableFilters(props.tableName);
|
||||||
|
|
||||||
|
// For lazy tables, reset to first page and trigger reload
|
||||||
|
if (props.lazy) {
|
||||||
|
paginationStore.resetToFirstPage(props.tableName);
|
||||||
|
triggerLazyLoad();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getActiveFiltersText = () => {
|
||||||
|
const currentFilters = filtersStore.getTableFilters(props.tableName);
|
||||||
|
const activeFilters = [];
|
||||||
|
Object.keys(currentFilters).forEach((key) => {
|
||||||
|
if (currentFilters[key]?.value) {
|
||||||
|
const column = filterableColumns.value.find((col) => col.fieldName === key);
|
||||||
|
if (column) {
|
||||||
|
activeFilters.push(`${column.label}: "${currentFilters[key].value}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return activeFilters.join(", ");
|
||||||
|
};
|
||||||
|
|
||||||
|
// Page navigation methods
|
||||||
|
const jumpToPage = () => {
|
||||||
|
if (selectedPageJump.value && props.lazy) {
|
||||||
|
const pageNumber = parseInt(selectedPageJump.value) - 1; // Convert to 0-based
|
||||||
|
paginationStore.setPage(props.tableName, pageNumber);
|
||||||
|
triggerLazyLoad();
|
||||||
|
}
|
||||||
|
selectedPageJump.value = ""; // Reset selection
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPageInfo = () => {
|
||||||
|
return paginationStore.getPageInfo(props.tableName);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle pagination events
|
||||||
|
const handlePage = (event) => {
|
||||||
|
console.log("Page event:", event);
|
||||||
|
if (props.lazy) {
|
||||||
|
paginationStore.updateTablePagination(props.tableName, {
|
||||||
|
page: event.page,
|
||||||
|
first: event.first,
|
||||||
|
rows: event.rows,
|
||||||
|
});
|
||||||
|
triggerLazyLoad();
|
||||||
|
}
|
||||||
|
emit("page-change", event);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle sorting events
|
||||||
|
const handleSort = (event) => {
|
||||||
|
console.log("Sort event:", event);
|
||||||
|
if (props.lazy) {
|
||||||
|
paginationStore.setSorting(props.tableName, event.sortField, event.sortOrder);
|
||||||
|
// Reset to first page when sorting changes
|
||||||
|
paginationStore.resetToFirstPage(props.tableName);
|
||||||
|
triggerLazyLoad();
|
||||||
|
}
|
||||||
|
emit("sort-change", event);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle filter events
|
||||||
|
const handleFilter = (event) => {
|
||||||
|
console.log("Filter event:", event);
|
||||||
|
if (props.lazy) {
|
||||||
|
// Reset to first page when filters change
|
||||||
|
paginationStore.resetToFirstPage(props.tableName);
|
||||||
|
triggerLazyLoad();
|
||||||
|
}
|
||||||
|
emit("filter-change", event);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Trigger lazy load event
|
||||||
|
const triggerLazyLoad = () => {
|
||||||
|
if (props.lazy && props.onLazyLoad) {
|
||||||
|
const paginationParams = paginationStore.getPaginationParams(props.tableName);
|
||||||
|
const filters = filtersStore.getTableFilters(props.tableName);
|
||||||
|
|
||||||
|
const lazyEvent = {
|
||||||
|
first: paginationParams.offset,
|
||||||
|
rows: paginationParams.limit,
|
||||||
|
page: paginationParams.page,
|
||||||
|
sortField: paginationParams.sortField,
|
||||||
|
sortOrder: paginationParams.sortOrder,
|
||||||
|
filters: filters,
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log("Triggering lazy load with:", lazyEvent);
|
||||||
|
emit("lazy-load", lazyEvent);
|
||||||
|
props.onLazyLoad(lazyEvent);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getBadgeColor = (status) => {
|
const getBadgeColor = (status) => {
|
||||||
@ -216,11 +469,43 @@ const getBadgeColor = (status) => {
|
|||||||
console.log("DEBUG: - DataTable props.columns", props.columns);
|
console.log("DEBUG: - DataTable props.columns", props.columns);
|
||||||
console.log("DEBUG: - DataTable props.data", props.data);
|
console.log("DEBUG: - DataTable props.data", props.data);
|
||||||
|
|
||||||
// Expose loading control methods for parent components
|
// Expose control methods for parent components
|
||||||
defineExpose({
|
defineExpose({
|
||||||
|
// Loading controls
|
||||||
startLoading: (message) => loadingStore.setComponentLoading(props.tableName, true, message),
|
startLoading: (message) => loadingStore.setComponentLoading(props.tableName, true, message),
|
||||||
stopLoading: () => loadingStore.setComponentLoading(props.tableName, false),
|
stopLoading: () => loadingStore.setComponentLoading(props.tableName, false),
|
||||||
isLoading: () => loading.value,
|
isLoading: () => loading.value,
|
||||||
|
|
||||||
|
// Pagination controls (for lazy tables)
|
||||||
|
goToPage: (page) => {
|
||||||
|
if (props.lazy) {
|
||||||
|
paginationStore.setPage(props.tableName, page);
|
||||||
|
triggerLazyLoad();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
nextPage: () => {
|
||||||
|
if (props.lazy) {
|
||||||
|
paginationStore.nextPage(props.tableName);
|
||||||
|
triggerLazyLoad();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
previousPage: () => {
|
||||||
|
if (props.lazy) {
|
||||||
|
paginationStore.previousPage(props.tableName);
|
||||||
|
triggerLazyLoad();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
refresh: () => {
|
||||||
|
if (props.lazy) {
|
||||||
|
triggerLazyLoad();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Get current state
|
||||||
|
getCurrentPage: () => paginationStore.getTablePagination(props.tableName).page,
|
||||||
|
getTotalPages: () => paginationStore.getTotalPages(props.tableName),
|
||||||
|
getFilters: () => filtersStore.getTableFilters(props.tableName),
|
||||||
|
getPaginationInfo: () => paginationStore.getPageInfo(props.tableName),
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
<style lang=""></style>
|
<style lang=""></style>
|
||||||
|
|||||||
@ -6,7 +6,17 @@
|
|||||||
Add
|
Add
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<DataTable :data="tableData" :columns="columns" :filters="filters" tableName="clients" />
|
<DataTable
|
||||||
|
:data="tableData"
|
||||||
|
:columns="columns"
|
||||||
|
:filters="filters"
|
||||||
|
tableName="clients"
|
||||||
|
:lazy="true"
|
||||||
|
:totalRecords="totalRecords"
|
||||||
|
:loading="isLoading"
|
||||||
|
:onLazyLoad="handleLazyLoad"
|
||||||
|
@lazy-load="handleLazyLoad"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
@ -15,13 +25,16 @@ import DataTable from "../common/DataTable.vue";
|
|||||||
import Api from "../../api";
|
import Api from "../../api";
|
||||||
import { FilterMatchMode } from "@primevue/core";
|
import { FilterMatchMode } from "@primevue/core";
|
||||||
import { useLoadingStore } from "../../stores/loading";
|
import { useLoadingStore } from "../../stores/loading";
|
||||||
|
import { usePaginationStore } from "../../stores/pagination";
|
||||||
|
import { useFiltersStore } from "../../stores/filters";
|
||||||
|
|
||||||
const loadingStore = useLoadingStore();
|
const loadingStore = useLoadingStore();
|
||||||
|
const paginationStore = usePaginationStore();
|
||||||
|
const filtersStore = useFiltersStore();
|
||||||
|
|
||||||
const itemCount = ref(0);
|
|
||||||
const page = ref(0);
|
|
||||||
const pageLength = ref(30);
|
|
||||||
const tableData = ref([]);
|
const tableData = ref([]);
|
||||||
|
const totalRecords = ref(0);
|
||||||
|
const isLoading = ref(false);
|
||||||
|
|
||||||
const onClick = () => {
|
const onClick = () => {
|
||||||
frappe.new_doc("Customer");
|
frappe.new_doc("Customer");
|
||||||
@ -49,23 +62,114 @@ const columns = [
|
|||||||
{ label: "Payment Received", fieldName: "paymentStatus", type: "status", sortable: true },
|
{ label: "Payment Received", fieldName: "paymentStatus", type: "status", sortable: true },
|
||||||
{ label: "Job Status", fieldName: "jobStatus", type: "status", sortable: true },
|
{ label: "Job Status", fieldName: "jobStatus", type: "status", sortable: true },
|
||||||
];
|
];
|
||||||
onMounted(async () => {
|
// Handle lazy loading events from DataTable
|
||||||
if (tableData.value.length > 0) {
|
const handleLazyLoad = async (event) => {
|
||||||
return;
|
console.log("Clients page - handling lazy load:", event);
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Use the loading store to track this API call
|
isLoading.value = true;
|
||||||
const data = await loadingStore.withComponentLoading(
|
|
||||||
|
// 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(
|
||||||
"clients",
|
"clients",
|
||||||
() => Api.getClientDetails(),
|
paginationParams.page,
|
||||||
"Loading client data...",
|
paginationParams.pageSize,
|
||||||
|
paginationParams.sortField,
|
||||||
|
paginationParams.sortOrder,
|
||||||
|
filters,
|
||||||
);
|
);
|
||||||
tableData.value = data;
|
|
||||||
|
if (cachedData) {
|
||||||
|
// Use cached data
|
||||||
|
tableData.value = cachedData.records;
|
||||||
|
totalRecords.value = cachedData.totalRecords;
|
||||||
|
paginationStore.setTotalRecords("clients", 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 });
|
||||||
|
|
||||||
|
// Call API with pagination and filters
|
||||||
|
const result = await Api.getPaginatedClientDetails(paginationParams, filters);
|
||||||
|
|
||||||
|
// Update local state
|
||||||
|
tableData.value = result.data;
|
||||||
|
totalRecords.value = result.totalRecords;
|
||||||
|
|
||||||
|
// Update pagination store with new total
|
||||||
|
paginationStore.setTotalRecords("clients", result.totalRecords);
|
||||||
|
|
||||||
|
// Cache the result
|
||||||
|
paginationStore.setCachedPage(
|
||||||
|
"clients",
|
||||||
|
paginationParams.page,
|
||||||
|
paginationParams.pageSize,
|
||||||
|
paginationParams.sortField,
|
||||||
|
paginationParams.sortOrder,
|
||||||
|
filters,
|
||||||
|
{
|
||||||
|
records: result.data,
|
||||||
|
totalRecords: result.totalRecords,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log("Loaded from API:", {
|
||||||
|
records: result.data.length,
|
||||||
|
total: result.totalRecords,
|
||||||
|
page: paginationParams.page + 1,
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error loading client data:", error);
|
console.error("Error loading client data:", error);
|
||||||
// You could also show a toast or other error notification here
|
// You could also show a toast or other error notification here
|
||||||
|
tableData.value = [];
|
||||||
|
totalRecords.value = 0;
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Load initial data
|
||||||
|
onMounted(async () => {
|
||||||
|
// Initialize pagination and filters
|
||||||
|
paginationStore.initializeTablePagination("clients", { rows: 10 });
|
||||||
|
filtersStore.initializeTableFilters("clients", columns);
|
||||||
|
|
||||||
|
// Load first page
|
||||||
|
const initialPagination = paginationStore.getTablePagination("clients");
|
||||||
|
const initialFilters = filtersStore.getTableFilters("clients");
|
||||||
|
|
||||||
|
await handleLazyLoad({
|
||||||
|
page: initialPagination.page,
|
||||||
|
rows: initialPagination.rows,
|
||||||
|
first: initialPagination.first,
|
||||||
|
sortField: initialPagination.sortField,
|
||||||
|
sortOrder: initialPagination.sortOrder,
|
||||||
|
filters: initialFilters,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
<style lang="css"></style>
|
<style lang="css"></style>
|
||||||
|
|||||||
@ -1,15 +1,34 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<h2>Jobs</h2>
|
<h2>Jobs</h2>
|
||||||
<DataTable :data="tableData" :columns="columns" tableName="jobs" />
|
<DataTable
|
||||||
|
:data="tableData"
|
||||||
|
:columns="columns"
|
||||||
|
tableName="jobs"
|
||||||
|
:lazy="true"
|
||||||
|
:totalRecords="totalRecords"
|
||||||
|
:loading="isLoading"
|
||||||
|
:onLazyLoad="handleLazyLoad"
|
||||||
|
@lazy-load="handleLazyLoad"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import DataTable from "../common/DataTable.vue";
|
import DataTable from "../common/DataTable.vue";
|
||||||
import { ref, onMounted } from "vue";
|
import { ref, onMounted } from "vue";
|
||||||
import Api from "../../api";
|
import Api from "../../api";
|
||||||
|
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 tableData = ref([]);
|
||||||
|
const totalRecords = ref(0);
|
||||||
|
const isLoading = ref(false);
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{ label: "Job ID", fieldName: "jobId", type: "text", sortable: true, filterable: true },
|
{ label: "Job ID", fieldName: "jobId", type: "text", sortable: true, filterable: true },
|
||||||
{ label: "Address", fieldName: "address", type: "text", sortable: true },
|
{ label: "Address", fieldName: "address", type: "text", sortable: true },
|
||||||
@ -18,12 +37,119 @@ const columns = [
|
|||||||
{ label: "Progress", fieldName: "stepProgress", type: "text", sortable: true },
|
{ label: "Progress", fieldName: "stepProgress", type: "text", sortable: true },
|
||||||
];
|
];
|
||||||
|
|
||||||
onMounted(async () => {
|
// Handle lazy loading events from DataTable
|
||||||
if (tableData.value.length > 0) {
|
const handleLazyLoad = async (event) => {
|
||||||
return;
|
console.log("Jobs 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(
|
||||||
|
"jobs",
|
||||||
|
paginationParams.page,
|
||||||
|
paginationParams.pageSize,
|
||||||
|
paginationParams.sortField,
|
||||||
|
paginationParams.sortOrder,
|
||||||
|
filters,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (cachedData) {
|
||||||
|
// Use cached data
|
||||||
|
tableData.value = cachedData.records;
|
||||||
|
totalRecords.value = cachedData.totalRecords;
|
||||||
|
paginationStore.setTotalRecords("jobs", 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.getPaginatedJobDetails() method
|
||||||
|
let data = await Api.getJobDetails();
|
||||||
|
|
||||||
|
// 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("jobs", data.length);
|
||||||
|
|
||||||
|
// Cache the result
|
||||||
|
paginationStore.setCachedPage(
|
||||||
|
"jobs",
|
||||||
|
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 job data:", error);
|
||||||
|
tableData.value = [];
|
||||||
|
totalRecords.value = 0;
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
}
|
}
|
||||||
let data = await Api.getJobDetails();
|
};
|
||||||
tableData.value = data;
|
|
||||||
|
// Load initial data
|
||||||
|
onMounted(async () => {
|
||||||
|
// Initialize pagination and filters
|
||||||
|
paginationStore.initializeTablePagination("jobs", { rows: 10 });
|
||||||
|
filtersStore.initializeTableFilters("jobs", columns);
|
||||||
|
|
||||||
|
// Load first page
|
||||||
|
const initialPagination = paginationStore.getTablePagination("jobs");
|
||||||
|
const initialFilters = filtersStore.getTableFilters("jobs");
|
||||||
|
|
||||||
|
await handleLazyLoad({
|
||||||
|
page: initialPagination.page,
|
||||||
|
rows: initialPagination.rows,
|
||||||
|
first: initialPagination.first,
|
||||||
|
sortField: initialPagination.sortField,
|
||||||
|
sortOrder: initialPagination.sortOrder,
|
||||||
|
filters: initialFilters,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
<style lang=""></style>
|
<style lang=""></style>
|
||||||
|
|||||||
@ -7,7 +7,17 @@
|
|||||||
|
|
||||||
<!-- Routes Data Table -->
|
<!-- Routes Data Table -->
|
||||||
<div class="routes-table-container">
|
<div class="routes-table-container">
|
||||||
<DataTable :data="tableData" :columns="columns" tableName="routes" @row-click="viewRouteDetails" />
|
<DataTable
|
||||||
|
:data="tableData"
|
||||||
|
:columns="columns"
|
||||||
|
tableName="routes"
|
||||||
|
:lazy="true"
|
||||||
|
:totalRecords="totalRecords"
|
||||||
|
:loading="isLoading"
|
||||||
|
:onLazyLoad="handleLazyLoad"
|
||||||
|
@lazy-load="handleLazyLoad"
|
||||||
|
@row-click="viewRouteDetails"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Route Details Modal -->
|
<!-- Route Details Modal -->
|
||||||
@ -139,9 +149,7 @@
|
|||||||
<v-icon size="x-small" class="mr-1"
|
<v-icon size="x-small" class="mr-1"
|
||||||
>mdi-clock</v-icon
|
>mdi-clock</v-icon
|
||||||
>
|
>
|
||||||
{{ stop.estimatedTime }} ({{
|
{{ stop.estimatedTime }} ({{ stop.duration }}
|
||||||
stop.duration
|
|
||||||
}}
|
|
||||||
min)
|
min)
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -230,17 +238,33 @@
|
|||||||
import { ref, onMounted } from "vue";
|
import { ref, onMounted } from "vue";
|
||||||
import DataTable from "../common/DataTable.vue";
|
import DataTable from "../common/DataTable.vue";
|
||||||
import Api from "../../api";
|
import Api from "../../api";
|
||||||
|
import { useLoadingStore } from "../../stores/loading";
|
||||||
|
import { usePaginationStore } from "../../stores/pagination";
|
||||||
|
import { useFiltersStore } from "../../stores/filters";
|
||||||
|
|
||||||
|
const loadingStore = useLoadingStore();
|
||||||
|
const paginationStore = usePaginationStore();
|
||||||
|
const filtersStore = useFiltersStore();
|
||||||
|
|
||||||
// Reactive data
|
// Reactive data
|
||||||
const tableData = ref([]);
|
const tableData = ref([]);
|
||||||
|
const totalRecords = ref(0);
|
||||||
|
const isLoading = ref(false);
|
||||||
const routeDialog = ref(false);
|
const routeDialog = ref(false);
|
||||||
const selectedRoute = ref(null);
|
const selectedRoute = ref(null);
|
||||||
|
const fullRouteData = ref([]); // Store full route data for modal access
|
||||||
|
|
||||||
// Table columns configuration
|
// Table columns configuration
|
||||||
const columns = [
|
const columns = [
|
||||||
{ label: "Route ID", fieldName: "routeId", type: "text", sortable: true },
|
{ label: "Route ID", fieldName: "routeId", type: "text", sortable: true, filterable: true },
|
||||||
{ label: "Route Name", fieldName: "routeName", type: "text", sortable: true },
|
{ label: "Route Name", fieldName: "routeName", type: "text", sortable: true },
|
||||||
{ label: "Technician", fieldName: "technician", type: "text", sortable: true },
|
{
|
||||||
|
label: "Technician",
|
||||||
|
fieldName: "technician",
|
||||||
|
type: "text",
|
||||||
|
sortable: true,
|
||||||
|
filterable: true,
|
||||||
|
},
|
||||||
{ label: "Date", fieldName: "date", type: "text", sortable: true },
|
{ label: "Date", fieldName: "date", type: "text", sortable: true },
|
||||||
{ label: "Status", fieldName: "status", type: "status", sortable: true },
|
{ label: "Status", fieldName: "status", type: "status", sortable: true },
|
||||||
{ label: "Progress", fieldName: "progress", type: "text", sortable: true },
|
{ label: "Progress", fieldName: "progress", type: "text", sortable: true },
|
||||||
@ -249,12 +273,127 @@ const columns = [
|
|||||||
{ label: "Actions", fieldName: "actions", type: "button", sortable: false },
|
{ label: "Actions", fieldName: "actions", type: "button", sortable: false },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Handle lazy loading events from DataTable
|
||||||
|
const handleLazyLoad = async (event) => {
|
||||||
|
console.log("Routes 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(
|
||||||
|
"routes",
|
||||||
|
paginationParams.page,
|
||||||
|
paginationParams.pageSize,
|
||||||
|
paginationParams.sortField,
|
||||||
|
paginationParams.sortOrder,
|
||||||
|
filters,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (cachedData) {
|
||||||
|
// Use cached data
|
||||||
|
tableData.value = cachedData.records;
|
||||||
|
totalRecords.value = cachedData.totalRecords;
|
||||||
|
fullRouteData.value = cachedData.fullData || [];
|
||||||
|
paginationStore.setTotalRecords("routes", 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.getPaginatedRouteData() method
|
||||||
|
const data = await Api.getRouteData();
|
||||||
|
|
||||||
|
// Store full data for modal access
|
||||||
|
fullRouteData.value = data;
|
||||||
|
|
||||||
|
// Simulate pagination on client side for now
|
||||||
|
const startIndex = paginationParams.page * paginationParams.pageSize;
|
||||||
|
const endIndex = startIndex + paginationParams.pageSize;
|
||||||
|
|
||||||
|
// Transform data for table display
|
||||||
|
const transformedData = data.map((route) => ({
|
||||||
|
routeId: route.routeId,
|
||||||
|
routeName: route.routeName,
|
||||||
|
technician: route.technician,
|
||||||
|
date: route.date,
|
||||||
|
status: route.status,
|
||||||
|
progress: `${route.completedStops}/${route.totalStops}`,
|
||||||
|
totalStops: route.totalStops,
|
||||||
|
estimatedDuration: route.estimatedDuration,
|
||||||
|
actions: "View Details",
|
||||||
|
}));
|
||||||
|
|
||||||
|
const paginatedData = transformedData.slice(startIndex, endIndex);
|
||||||
|
|
||||||
|
// Update local state
|
||||||
|
tableData.value = paginatedData;
|
||||||
|
totalRecords.value = transformedData.length;
|
||||||
|
|
||||||
|
// Update pagination store with new total
|
||||||
|
paginationStore.setTotalRecords("routes", transformedData.length);
|
||||||
|
|
||||||
|
// Cache the result
|
||||||
|
paginationStore.setCachedPage(
|
||||||
|
"routes",
|
||||||
|
paginationParams.page,
|
||||||
|
paginationParams.pageSize,
|
||||||
|
paginationParams.sortField,
|
||||||
|
paginationParams.sortOrder,
|
||||||
|
filters,
|
||||||
|
{
|
||||||
|
records: paginatedData,
|
||||||
|
totalRecords: transformedData.length,
|
||||||
|
fullData: data,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log("Loaded from API:", {
|
||||||
|
records: paginatedData.length,
|
||||||
|
total: transformedData.length,
|
||||||
|
page: paginationParams.page + 1,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading route data:", error);
|
||||||
|
tableData.value = [];
|
||||||
|
totalRecords.value = 0;
|
||||||
|
fullRouteData.value = [];
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Methods
|
// Methods
|
||||||
const viewRouteDetails = (event) => {
|
const viewRouteDetails = (event) => {
|
||||||
const routeId = event.data.routeId;
|
const routeId = event.data.routeId;
|
||||||
const route = tableData.value.find((r) => r.routeId === routeId);
|
const route = fullRouteData.value.find((r) => r.routeId === routeId);
|
||||||
if (route && route.fullData) {
|
if (route) {
|
||||||
selectedRoute.value = route.fullData;
|
selectedRoute.value = route;
|
||||||
routeDialog.value = true;
|
routeDialog.value = true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -293,29 +432,24 @@ const optimizeRoute = () => {
|
|||||||
alert("Route optimization feature coming soon!");
|
alert("Route optimization feature coming soon!");
|
||||||
};
|
};
|
||||||
|
|
||||||
// Load data on component mount
|
// Load initial data
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
try {
|
// Initialize pagination and filters
|
||||||
const data = await Api.getRouteData();
|
paginationStore.initializeTablePagination("routes", { rows: 10 });
|
||||||
|
filtersStore.initializeTableFilters("routes", columns);
|
||||||
|
|
||||||
// Transform data for table display and keep full data reference
|
// Load first page
|
||||||
tableData.value = data.map((route) => ({
|
const initialPagination = paginationStore.getTablePagination("routes");
|
||||||
routeId: route.routeId,
|
const initialFilters = filtersStore.getTableFilters("routes");
|
||||||
routeName: route.routeName,
|
|
||||||
technician: route.technician,
|
|
||||||
date: route.date,
|
|
||||||
status: route.status,
|
|
||||||
progress: `${route.completedStops}/${route.totalStops}`,
|
|
||||||
totalStops: route.totalStops,
|
|
||||||
estimatedDuration: route.estimatedDuration,
|
|
||||||
actions: "View Details",
|
|
||||||
fullData: route, // Keep reference to full route data
|
|
||||||
}));
|
|
||||||
|
|
||||||
console.log("Loaded routes:", tableData.value);
|
await handleLazyLoad({
|
||||||
} catch (error) {
|
page: initialPagination.page,
|
||||||
console.error("Error loading routes:", error);
|
rows: initialPagination.rows,
|
||||||
}
|
first: initialPagination.first,
|
||||||
|
sortField: initialPagination.sortField,
|
||||||
|
sortOrder: initialPagination.sortOrder,
|
||||||
|
filters: initialFilters,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@ -124,10 +124,14 @@
|
|||||||
<!-- Main Timesheet Table -->
|
<!-- Main Timesheet Table -->
|
||||||
<div class="timesheets-table-container">
|
<div class="timesheets-table-container">
|
||||||
<DataTable
|
<DataTable
|
||||||
:data="filteredTableData"
|
:data="tableData"
|
||||||
:columns="columns"
|
:columns="columns"
|
||||||
:filters="filters"
|
|
||||||
tableName="timesheets"
|
tableName="timesheets"
|
||||||
|
:lazy="true"
|
||||||
|
:totalRecords="totalRecords"
|
||||||
|
:loading="isLoading"
|
||||||
|
:onLazyLoad="handleLazyLoad"
|
||||||
|
@lazy-load="handleLazyLoad"
|
||||||
@row-click="viewTimesheetDetails"
|
@row-click="viewTimesheetDetails"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -372,12 +376,21 @@ import { ref, onMounted, computed } from "vue";
|
|||||||
import DataTable from "../common/DataTable.vue";
|
import DataTable from "../common/DataTable.vue";
|
||||||
import Api from "../../api";
|
import Api from "../../api";
|
||||||
import { FilterMatchMode } from "@primevue/core";
|
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();
|
||||||
|
|
||||||
// Reactive data
|
// Reactive data
|
||||||
const tableData = ref([]);
|
const tableData = ref([]);
|
||||||
const filteredTableData = ref([]);
|
const totalRecords = ref(0);
|
||||||
|
const isLoading = ref(false);
|
||||||
const timesheetDialog = ref(false);
|
const timesheetDialog = ref(false);
|
||||||
const selectedTimesheet = ref(null);
|
const selectedTimesheet = ref(null);
|
||||||
|
const allTimesheetData = ref([]); // Store all data for filtering and calculations
|
||||||
|
|
||||||
// Filter controls
|
// Filter controls
|
||||||
const selectedWeek = ref("current");
|
const selectedWeek = ref("current");
|
||||||
@ -412,8 +425,156 @@ const columns = [
|
|||||||
{ label: "Actions", fieldName: "actions", type: "button", sortable: false },
|
{ label: "Actions", fieldName: "actions", type: "button", sortable: false },
|
||||||
];
|
];
|
||||||
|
|
||||||
const filters = {
|
// Handle lazy loading events from DataTable
|
||||||
employee: { value: null, matchMode: FilterMatchMode.CONTAINS },
|
const handleLazyLoad = async (event) => {
|
||||||
|
console.log("TimeSheets 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];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply additional filters from the controls
|
||||||
|
const additionalFilters = {
|
||||||
|
week: selectedWeek.value,
|
||||||
|
employee: selectedEmployee.value,
|
||||||
|
status: selectedStatus.value,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Combine filters
|
||||||
|
const combinedFilters = { ...filters, ...additionalFilters };
|
||||||
|
|
||||||
|
// Check cache first
|
||||||
|
const cacheKey = `${JSON.stringify(combinedFilters)}`;
|
||||||
|
const cachedData = paginationStore.getCachedPage(
|
||||||
|
"timesheets",
|
||||||
|
paginationParams.page,
|
||||||
|
paginationParams.pageSize,
|
||||||
|
paginationParams.sortField,
|
||||||
|
paginationParams.sortOrder,
|
||||||
|
combinedFilters,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (cachedData) {
|
||||||
|
// Use cached data
|
||||||
|
tableData.value = cachedData.records;
|
||||||
|
totalRecords.value = cachedData.totalRecords;
|
||||||
|
allTimesheetData.value = cachedData.allData || [];
|
||||||
|
paginationStore.setTotalRecords("timesheets", 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, combinedFilters });
|
||||||
|
|
||||||
|
// For now, use existing API but we should create a paginated version
|
||||||
|
// TODO: Create Api.getPaginatedTimesheetData() method
|
||||||
|
const data = await Api.getTimesheetData();
|
||||||
|
|
||||||
|
// Store all data for calculations
|
||||||
|
allTimesheetData.value = data;
|
||||||
|
|
||||||
|
// Apply local filtering based on controls
|
||||||
|
let filteredData = data;
|
||||||
|
|
||||||
|
// Week filter
|
||||||
|
if (selectedWeek.value !== "all") {
|
||||||
|
const currentWeekStart = getCurrentWeekStart();
|
||||||
|
let weekStart;
|
||||||
|
|
||||||
|
switch (selectedWeek.value) {
|
||||||
|
case "current":
|
||||||
|
weekStart = currentWeekStart;
|
||||||
|
break;
|
||||||
|
case "last":
|
||||||
|
weekStart = getWeekStart(1);
|
||||||
|
break;
|
||||||
|
case "last2":
|
||||||
|
weekStart = getWeekStart(2);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
weekStart = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (weekStart) {
|
||||||
|
filteredData = filteredData.filter((timesheet) => {
|
||||||
|
const timesheetDate = new Date(timesheet.date);
|
||||||
|
return timesheetDate >= weekStart;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Employee filter
|
||||||
|
if (selectedEmployee.value) {
|
||||||
|
filteredData = filteredData.filter((ts) => ts.employee === selectedEmployee.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status filter
|
||||||
|
if (selectedStatus.value) {
|
||||||
|
filteredData = filteredData.filter((ts) => ts.status === selectedStatus.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simulate pagination on filtered data
|
||||||
|
const startIndex = paginationParams.page * paginationParams.pageSize;
|
||||||
|
const endIndex = startIndex + paginationParams.pageSize;
|
||||||
|
const paginatedData = filteredData.slice(startIndex, endIndex);
|
||||||
|
|
||||||
|
// Update local state
|
||||||
|
tableData.value = paginatedData;
|
||||||
|
totalRecords.value = filteredData.length;
|
||||||
|
|
||||||
|
// Update pagination store with new total
|
||||||
|
paginationStore.setTotalRecords("timesheets", filteredData.length);
|
||||||
|
|
||||||
|
// Cache the result
|
||||||
|
paginationStore.setCachedPage(
|
||||||
|
"timesheets",
|
||||||
|
paginationParams.page,
|
||||||
|
paginationParams.pageSize,
|
||||||
|
paginationParams.sortField,
|
||||||
|
paginationParams.sortOrder,
|
||||||
|
combinedFilters,
|
||||||
|
{
|
||||||
|
records: paginatedData,
|
||||||
|
totalRecords: filteredData.length,
|
||||||
|
allData: data,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log("Loaded from API:", {
|
||||||
|
records: paginatedData.length,
|
||||||
|
total: filteredData.length,
|
||||||
|
page: paginationParams.page + 1,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading timesheet data:", error);
|
||||||
|
tableData.value = [];
|
||||||
|
totalRecords.value = 0;
|
||||||
|
allTimesheetData.value = [];
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Computed properties
|
// Computed properties
|
||||||
@ -425,7 +586,7 @@ const totalHoursThisWeek = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const pendingApprovals = computed(() => {
|
const pendingApprovals = computed(() => {
|
||||||
return tableData.value.filter((ts) => !ts.approved).length;
|
return allTimesheetData.value.filter((ts) => !ts.approved).length;
|
||||||
});
|
});
|
||||||
|
|
||||||
const totalLaborCost = computed(() => {
|
const totalLaborCost = computed(() => {
|
||||||
@ -435,7 +596,7 @@ const totalLaborCost = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const activeEmployees = computed(() => {
|
const activeEmployees = computed(() => {
|
||||||
const uniqueEmployees = new Set(tableData.value.map((ts) => ts.employee));
|
const uniqueEmployees = new Set(allTimesheetData.value.map((ts) => ts.employee));
|
||||||
return uniqueEmployees.size;
|
return uniqueEmployees.size;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -444,10 +605,11 @@ const hasSelectedForApproval = computed(() => {
|
|||||||
return pendingApprovals.value > 0;
|
return pendingApprovals.value > 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Methods
|
||||||
// Methods
|
// Methods
|
||||||
const getCurrentWeekTimesheets = () => {
|
const getCurrentWeekTimesheets = () => {
|
||||||
const currentWeekStart = getCurrentWeekStart();
|
const currentWeekStart = getCurrentWeekStart();
|
||||||
return tableData.value.filter((timesheet) => {
|
return allTimesheetData.value.filter((timesheet) => {
|
||||||
const timesheetDate = new Date(timesheet.date);
|
const timesheetDate = new Date(timesheet.date);
|
||||||
return timesheetDate >= currentWeekStart;
|
return timesheetDate >= currentWeekStart;
|
||||||
});
|
});
|
||||||
@ -468,15 +630,38 @@ const getWeekStart = (weeksAgo) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const filterByWeek = () => {
|
const filterByWeek = () => {
|
||||||
applyFilters();
|
// Reset to first page when filters change
|
||||||
|
paginationStore.resetToFirstPage("timesheets");
|
||||||
|
triggerLazyLoad();
|
||||||
};
|
};
|
||||||
|
|
||||||
const filterByEmployee = () => {
|
const filterByEmployee = () => {
|
||||||
applyFilters();
|
// Reset to first page when filters change
|
||||||
|
paginationStore.resetToFirstPage("timesheets");
|
||||||
|
triggerLazyLoad();
|
||||||
};
|
};
|
||||||
|
|
||||||
const filterByStatus = () => {
|
const filterByStatus = () => {
|
||||||
applyFilters();
|
// Reset to first page when filters change
|
||||||
|
paginationStore.resetToFirstPage("timesheets");
|
||||||
|
triggerLazyLoad();
|
||||||
|
};
|
||||||
|
|
||||||
|
const triggerLazyLoad = () => {
|
||||||
|
const paginationParams = paginationStore.getPaginationParams("timesheets");
|
||||||
|
const filters = filtersStore.getTableFilters("timesheets");
|
||||||
|
|
||||||
|
const lazyEvent = {
|
||||||
|
page: paginationParams.page,
|
||||||
|
rows: paginationParams.pageSize,
|
||||||
|
first: paginationParams.offset,
|
||||||
|
sortField: paginationParams.sortField,
|
||||||
|
sortOrder: paginationParams.sortOrder,
|
||||||
|
filters: filters,
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log("Triggering lazy load with:", lazyEvent);
|
||||||
|
handleLazyLoad(lazyEvent);
|
||||||
};
|
};
|
||||||
|
|
||||||
const applyFilters = () => {
|
const applyFilters = () => {
|
||||||
@ -513,7 +698,7 @@ const applyFilters = () => {
|
|||||||
|
|
||||||
const viewTimesheetDetails = (event) => {
|
const viewTimesheetDetails = (event) => {
|
||||||
const timesheetId = event.data.timesheetId;
|
const timesheetId = event.data.timesheetId;
|
||||||
const timesheet = tableData.value.find((ts) => ts.timesheetId === timesheetId);
|
const timesheet = allTimesheetData.value.find((ts) => ts.timesheetId === timesheetId);
|
||||||
if (timesheet) {
|
if (timesheet) {
|
||||||
selectedTimesheet.value = timesheet;
|
selectedTimesheet.value = timesheet;
|
||||||
timesheetDialog.value = true;
|
timesheetDialog.value = true;
|
||||||
@ -570,10 +755,13 @@ const formatDate = (dateString) => {
|
|||||||
// Load data on component mount
|
// Load data on component mount
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
try {
|
try {
|
||||||
const data = await Api.getTimesheetData();
|
// Initialize pagination and filters
|
||||||
|
paginationStore.initializeTablePagination("timesheets", { rows: 10 });
|
||||||
|
filtersStore.initializeTableFilters("timesheets", columns);
|
||||||
|
|
||||||
// Transform data for table display
|
// Load data to set up employee options
|
||||||
tableData.value = data.map((timesheet) => ({
|
const data = await Api.getTimesheetData();
|
||||||
|
allTimesheetData.value = data.map((timesheet) => ({
|
||||||
...timesheet,
|
...timesheet,
|
||||||
totalPayFormatted: `$${timesheet.totalPay.toLocaleString()}`,
|
totalPayFormatted: `$${timesheet.totalPay.toLocaleString()}`,
|
||||||
actions: "View Details",
|
actions: "View Details",
|
||||||
@ -583,10 +771,20 @@ onMounted(async () => {
|
|||||||
const uniqueEmployees = [...new Set(data.map((ts) => ts.employee))];
|
const uniqueEmployees = [...new Set(data.map((ts) => ts.employee))];
|
||||||
employeeOptions.value = uniqueEmployees.map((emp) => ({ label: emp, value: emp }));
|
employeeOptions.value = uniqueEmployees.map((emp) => ({ label: emp, value: emp }));
|
||||||
|
|
||||||
// Apply initial filters
|
// Load first page
|
||||||
applyFilters();
|
const initialPagination = paginationStore.getTablePagination("timesheets");
|
||||||
|
const initialFilters = filtersStore.getTableFilters("timesheets");
|
||||||
|
|
||||||
console.log("Loaded timesheets:", tableData.value);
|
await handleLazyLoad({
|
||||||
|
page: initialPagination.page,
|
||||||
|
rows: initialPagination.rows,
|
||||||
|
first: initialPagination.first,
|
||||||
|
sortField: initialPagination.sortField,
|
||||||
|
sortOrder: initialPagination.sortOrder,
|
||||||
|
filters: initialFilters,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Loaded timesheets:", allTimesheetData.value.length);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error loading timesheets:", error);
|
console.error("Error loading timesheets:", error);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,7 +6,16 @@
|
|||||||
Add New Warranty Claim
|
Add New Warranty Claim
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<DataTable :data="tableData" :columns="columns" :filters="filters" tableName="warranties" />
|
<DataTable
|
||||||
|
:data="tableData"
|
||||||
|
:columns="columns"
|
||||||
|
tableName="warranties"
|
||||||
|
:lazy="true"
|
||||||
|
:totalRecords="totalRecords"
|
||||||
|
:loading="isLoading"
|
||||||
|
:onLazyLoad="handleLazyLoad"
|
||||||
|
@lazy-load="handleLazyLoad"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -15,8 +24,17 @@ import { onMounted, ref } from "vue";
|
|||||||
import DataTable from "../common/DataTable.vue";
|
import DataTable from "../common/DataTable.vue";
|
||||||
import Api from "../../api";
|
import Api from "../../api";
|
||||||
import { FilterMatchMode } from "@primevue/core";
|
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 tableData = ref([]);
|
||||||
|
const totalRecords = ref(0);
|
||||||
|
const isLoading = ref(false);
|
||||||
|
|
||||||
const addNewWarranty = () => {
|
const addNewWarranty = () => {
|
||||||
// TODO: Open modal or navigate to create warranty form
|
// TODO: Open modal or navigate to create warranty form
|
||||||
@ -25,13 +43,6 @@ const addNewWarranty = () => {
|
|||||||
// For now, just log the action
|
// 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 = [
|
const columns = [
|
||||||
{
|
{
|
||||||
label: "Warranty ID",
|
label: "Warranty ID",
|
||||||
@ -93,12 +104,119 @@ const columns = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
onMounted(async () => {
|
// Handle lazy loading events from DataTable
|
||||||
if (tableData.value.length > 0) {
|
const handleLazyLoad = async (event) => {
|
||||||
return;
|
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,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
388
frontend/src/stores/pagination.js
Normal file
388
frontend/src/stores/pagination.js
Normal file
@ -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",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
Loading…
x
Reference in New Issue
Block a user