/** * Admin Data Table Module * * Provides interactive functionality for admin data tables: * - AJAX loading and pagination * - Sorting by columns * - Search/filtering * - Auto-refresh */ export class AdminDataTable { constructor(element) { this.element = element; this.resource = element.dataset.resource; this.apiEndpoint = element.dataset.apiEndpoint; this.sortable = element.dataset.sortable === 'true'; this.searchable = element.dataset.searchable === 'true'; this.paginated = element.dataset.paginated === 'true'; this.perPage = parseInt(element.dataset.perPage) || 25; this.currentPage = 1; this.currentSort = null; this.currentSearch = ''; } async init() { this.tbody = this.element.querySelector('tbody'); this.thead = this.element.querySelector('thead'); if (!this.tbody) { console.warn('AdminDataTable: No tbody found'); return; } // Always setup event listeners for sorting, even if data is already loaded this.setupEventListeners(); // Only load data via AJAX if API endpoint is configured and table is empty if (this.apiEndpoint && this.tbody.children.length === 0) { await this.loadData(); } } setupEventListeners() { // Sortable columns - only for columns with data-column attribute // Check both table-level sortable flag and individual column sortable attribute if (this.thead) { this.thead.querySelectorAll('th[data-column]').forEach((th) => { const columnKey = th.dataset.column; if (columnKey) { // Only make sortable if table-level sortable is enabled OR column has data-column if (this.sortable || th.hasAttribute('data-column')) { th.style.cursor = 'pointer'; th.classList.add('sortable'); th.addEventListener('click', () => { // If API endpoint is available, use AJAX sorting if (this.apiEndpoint) { this.sortByColumn(columnKey, th); } else { // Otherwise, do client-side sorting this.sortByColumnClientSide(columnKey, th); } }); } } }); } // Search input if (this.searchable) { const searchInput = this.getSearchInput(); if (searchInput) { searchInput.addEventListener('input', this.debounce(() => { this.currentSearch = searchInput.value; this.currentPage = 1; this.loadData(); }, 300)); } } // Pagination links (if exist) this.setupPaginationListeners(); } setupPaginationListeners() { const paginationContainer = this.getPaginationContainer(); if (!paginationContainer) return; paginationContainer.addEventListener('click', (e) => { const pageLink = e.target.closest('[data-page]'); if (pageLink) { e.preventDefault(); const page = parseInt(pageLink.dataset.page); if (page && page !== this.currentPage) { this.currentPage = page; this.loadData(); } } }); } async sortByColumn(columnKey, th) { // Toggle sort direction const currentDir = th.dataset.sortDir; const newDir = currentDir === 'asc' ? 'desc' : 'asc'; // Update sort state this.currentSort = { column: columnKey, direction: newDir }; // Update UI this.thead.querySelectorAll('th').forEach(header => { header.removeAttribute('data-sort-dir'); header.classList.remove('sort-asc', 'sort-desc'); }); th.dataset.sortDir = newDir; th.classList.add(`sort-${newDir}`); // Reload data if API endpoint is available if (this.apiEndpoint) { await this.loadData(); } } sortByColumnClientSide(columnKey, th) { // Client-side sorting for tables without API endpoint const currentDir = th.dataset.sortDir; const newDir = currentDir === 'asc' ? 'desc' : 'asc'; // Update UI this.thead.querySelectorAll('th').forEach(header => { header.removeAttribute('data-sort-dir'); header.classList.remove('sort-asc', 'sort-desc'); }); th.dataset.sortDir = newDir; th.classList.add(`sort-${newDir}`); // Get all rows const rows = Array.from(this.tbody.querySelectorAll('tr')); const columnIndex = Array.from(this.thead.querySelectorAll('th')).indexOf(th); // Sort rows rows.sort((a, b) => { const aValue = a.cells[columnIndex]?.textContent?.trim() || ''; const bValue = b.cells[columnIndex]?.textContent?.trim() || ''; // Try numeric comparison first const aNum = parseFloat(aValue); const bNum = parseFloat(bValue); if (!isNaN(aNum) && !isNaN(bNum)) { return newDir === 'asc' ? aNum - bNum : bNum - aNum; } // String comparison const comparison = aValue.localeCompare(bValue); return newDir === 'asc' ? comparison : -comparison; }); // Re-append sorted rows rows.forEach(row => this.tbody.appendChild(row)); } async loadData() { const params = new URLSearchParams({ page: this.currentPage.toString(), per_page: this.perPage.toString(), }); if (this.currentSearch) { params.set('search', this.currentSearch); } if (this.currentSort) { params.set('sort_by', this.currentSort.column); params.set('sort_dir', this.currentSort.direction); } const url = `${this.apiEndpoint}?${params}`; try { this.setLoadingState(true); const response = await fetch(url, { headers: { 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' } }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const result = await response.json(); if (result.success) { this.renderRows(result.data); if (this.paginated && result.pagination) { this.renderPagination(result.pagination); } } else { this.showError(result.error || 'Failed to load data'); } } catch (error) { console.error('AdminDataTable: Failed to load data', error); console.error('AdminDataTable: URL was', url); // Fallback to client-side sorting if API fails if (this.currentSort && this.tbody.children.length > 0) { console.warn('AdminDataTable: API failed, falling back to client-side sorting'); const th = this.thead.querySelector(`th[data-column="${this.currentSort.column}"]`); if (th) { this.sortByColumnClientSide(this.currentSort.column, th); } else { this.showError('Failed to load data. Please refresh the page.'); } } else { this.showError('Failed to load data'); } } finally { this.setLoadingState(false); } } renderRows(data) { if (!data || data.length === 0) { this.tbody.innerHTML = this.getEmptyRow(); return; } // Get column keys from header const columnKeys = this.getColumnKeys(); this.tbody.innerHTML = data.map(item => { const cells = columnKeys.map(key => { const value = item[key] !== undefined ? item[key] : ''; return `