feat: Add enhanced table features with sorting, pagination, and column visibility

Implement comprehensive table enhancements across all data tables in the
application:

- Add sortable columns with visual indicators (up/down arrows)
- Implement client-side pagination with configurable page size (10/25/50/100)
- Add column visibility toggles with dropdown menu
- Enable sticky headers that stick to top on scroll
- Save user preferences (page size, visible columns) in localStorage
- Support numeric, date, and text sorting with proper formatting

Features:
- Visual sort indicators show active column and direction
- Pagination controls with page size selector and navigation buttons
- Column visibility button integrated into existing toolbars
- Sticky headers with shadow effect when active
- Responsive design with mobile-friendly pagination
- Dark mode support throughout

Technical implementation:
- Created data-tables-enhanced.js module with DataTableEnhanced class
- Created data-tables-enhanced.css for all styling
- Auto-initializes tables with data-table-enhanced attribute or table-zebra class
- Properly integrates with existing Finance category table toolbars
- Handles checkbox columns and special table structures

Updated templates:
- tasks/list.html, projects/list.html, clients/list.html
- invoices/list.html, expenses/list.html, payments/list.html
- mileage/list.html, per_diem/list.html, main/search.html

All tables now provide consistent, enhanced user experience with improved
data navigation and viewing options.
This commit is contained in:
Dries Peeters
2025-11-05 08:17:38 +01:00
parent b4b8bafb9a
commit dc010c8da1
12 changed files with 882 additions and 9 deletions

View File

@@ -0,0 +1,260 @@
/**
* Data Tables Enhanced Styles
* Sticky headers, sort indicators, pagination, and column visibility
*/
/* Sticky Header Styles */
.table-scroll-container {
position: relative;
max-height: 70vh;
overflow-y: auto;
overflow-x: auto;
}
table thead.sticky-header {
position: sticky;
top: 0;
z-index: 10;
background: white;
transition: box-shadow 0.2s ease;
}
.dark table thead.sticky-header {
background: #1f2937;
}
table thead.sticky-header.sticky-active {
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.dark table thead.sticky-header.sticky-active {
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
/* Sortable Column Styles */
table th.sortable-column {
position: relative;
user-select: none;
transition: background-color 0.2s ease;
}
table th.sortable-column:hover {
background-color: rgba(0, 0, 0, 0.05);
}
.dark table th.sortable-column:hover {
background-color: rgba(255, 255, 255, 0.05);
}
table th.sortable-column:focus {
outline: 2px solid #3b82f6;
outline-offset: -2px;
}
/* Sort Indicator */
.sort-indicator {
display: inline-block;
margin-left: 0.5rem;
color: #9ca3af;
font-size: 0.75rem;
transition: color 0.2s ease;
}
.dark .sort-indicator {
color: #6b7280;
}
.sort-indicator.active {
color: #3b82f6;
}
.dark .sort-indicator.active {
color: #60a5fa;
}
table th.sortable-column:hover .sort-indicator {
color: #3b82f6;
}
.dark table th.sortable-column:hover .sort-indicator {
color: #60a5fa;
}
/* Column Visibility Dropdown */
.column-visibility-dropdown {
max-height: 20rem;
overflow-y: auto;
}
.column-visibility-dropdown label {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem;
cursor: pointer;
transition: background-color 0.2s ease;
}
.column-visibility-dropdown label:hover {
background-color: #f3f4f6;
}
.dark .column-visibility-dropdown label:hover {
background-color: #374151;
}
/* Pagination Styles */
.table-pagination-container {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 1rem;
padding-top: 1rem;
border-top: 1px solid #e5e7eb;
}
.dark .table-pagination-container {
border-top-color: #4b5563;
}
.pagination-btn {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 2.5rem;
height: 2.5rem;
border: 1px solid #d1d5db;
background: white;
color: #374151;
font-size: 0.875rem;
cursor: pointer;
transition: all 0.2s ease;
border-radius: 0.375rem;
}
.dark .pagination-btn {
border-color: #4b5563;
background: #1f2937;
color: #e5e7eb;
}
.pagination-btn:hover:not(:disabled) {
background: #f3f4f6;
border-color: #9ca3af;
}
.dark .pagination-btn:hover:not(:disabled) {
background: #374151;
border-color: #6b7280;
}
.pagination-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.pagination-btn.active {
background: #3b82f6;
color: white;
border-color: #3b82f6;
}
.dark .pagination-btn.active {
background: #2563eb;
border-color: #2563eb;
}
/* Page Size Selector */
.table-page-size-select {
padding: 0.25rem 0.5rem;
font-size: 0.875rem;
border: 1px solid #d1d5db;
border-radius: 0.375rem;
background: white;
color: #374151;
cursor: pointer;
transition: border-color 0.2s ease;
}
.dark .table-page-size-select {
border-color: #4b5563;
background: #1f2937;
color: #e5e7eb;
}
.table-page-size-select:hover {
border-color: #9ca3af;
}
.dark .table-page-size-select:hover {
border-color: #6b7280;
}
.table-page-size-select:focus {
outline: 2px solid #3b82f6;
outline-offset: 2px;
}
/* Table Toolbar */
.table-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1rem;
gap: 1rem;
flex-wrap: wrap;
}
.table-toolbar-left,
.table-toolbar-right {
display: flex;
align-items: center;
gap: 0.5rem;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.table-pagination-container {
flex-direction: column;
gap: 1rem;
align-items: stretch;
}
.table-pagination-container > div:first-child {
width: 100%;
}
.table-pagination-container > div:last-child {
width: 100%;
justify-content: space-between;
}
.pagination-btn {
min-width: 2rem;
height: 2rem;
font-size: 0.75rem;
}
}
/* Accessibility improvements */
table th.sortable-column[aria-label] {
cursor: pointer;
}
table th.sortable-column:focus-visible {
outline: 2px solid #3b82f6;
outline-offset: -2px;
border-radius: 0.25rem;
}
/* Smooth transitions for column visibility */
table th,
table td {
transition: opacity 0.2s ease, visibility 0.2s ease;
}
table th[style*="display: none"],
table td[style*="display: none"] {
display: none !important;
}

View File

@@ -0,0 +1,611 @@
/**
* Data Tables Enhanced
* Adds sortable columns, pagination, column visibility, and sticky headers to all tables
*/
(function() {
'use strict';
class DataTableEnhanced {
constructor(table, options = {}) {
this.table = table;
this.tableId = table.id || `table-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
if (!table.id) table.id = this.tableId;
this.options = {
sortable: options.sortable !== false,
pagination: options.pagination !== false,
pageSize: options.pageSize || 10,
pageSizeOptions: options.pageSizeOptions || [10, 25, 50, 100],
columnVisibility: options.columnVisibility !== false,
stickyHeader: options.stickyHeader !== false,
storageKey: options.storageKey || `table-${this.tableId}`,
...options
};
this.currentPage = 1;
this.pageSize = this.options.pageSize;
this.sortColumn = null;
this.sortDirection = 'asc';
this.visibleColumns = new Set();
this.originalData = [];
this.init();
}
init() {
// Extract data from table
this.extractData();
// Load saved preferences
this.loadPreferences();
// Initialize features
if (this.options.sortable) this.initSorting();
if (this.options.columnVisibility) this.initColumnVisibility();
if (this.options.stickyHeader) this.initStickyHeader();
if (this.options.pagination) this.initPagination();
// Apply initial state
this.render();
}
extractData() {
const tbody = this.table.querySelector('tbody');
if (!tbody) return;
const rows = Array.from(tbody.querySelectorAll('tr'));
this.originalData = rows.map(row => ({
element: row,
cells: Array.from(row.querySelectorAll('td')),
values: Array.from(row.querySelectorAll('td')).map(cell => {
// Get sortable value (check for data-sort attribute)
const sortValue = cell.getAttribute('data-sort');
if (sortValue !== null) return sortValue;
// Otherwise use text content
return cell.textContent.trim();
})
}));
}
loadPreferences() {
try {
const saved = localStorage.getItem(this.options.storageKey);
if (saved) {
const prefs = JSON.parse(saved);
if (prefs.pageSize) this.pageSize = prefs.pageSize;
if (prefs.visibleColumns) {
this.visibleColumns = new Set(prefs.visibleColumns);
}
}
} catch (e) {
console.warn('Failed to load table preferences', e);
}
}
savePreferences() {
try {
const prefs = {
pageSize: this.pageSize,
visibleColumns: Array.from(this.visibleColumns)
};
localStorage.setItem(this.options.storageKey, JSON.stringify(prefs));
} catch (e) {
console.warn('Failed to save table preferences', e);
}
}
initSorting() {
const thead = this.table.querySelector('thead');
if (!thead) return;
const headers = Array.from(thead.querySelectorAll('th'));
headers.forEach((header, index) => {
// Check if column is sortable (has data-sortable attribute or class)
const isSortable = header.hasAttribute('data-sortable') ||
header.classList.contains('sortable') ||
!header.classList.contains('no-sort');
if (!isSortable) return;
// Skip checkbox columns
if (header.querySelector('input[type="checkbox"]')) return;
header.classList.add('sortable-column');
header.style.cursor = 'pointer';
header.setAttribute('role', 'button');
header.setAttribute('tabindex', '0');
header.setAttribute('aria-label', `Sort by ${header.textContent.trim()}`);
// Add sort indicator container
const indicator = document.createElement('span');
indicator.className = 'sort-indicator';
indicator.innerHTML = '<i class="fas fa-sort"></i>';
header.appendChild(indicator);
// Click handler
header.addEventListener('click', (e) => {
e.preventDefault();
this.sort(index);
});
// Keyboard handler
header.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
this.sort(index);
}
});
});
}
sort(columnIndex) {
// Toggle sort direction if same column
if (this.sortColumn === columnIndex) {
this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc';
} else {
this.sortColumn = columnIndex;
this.sortDirection = 'asc';
}
// Update visual indicators
this.updateSortIndicators();
// Sort data
this.originalData.sort((a, b) => {
const aVal = a.values[columnIndex] || '';
const bVal = b.values[columnIndex] || '';
// Try numeric sort
const aNum = parseFloat(aVal.replace(/[^0-9.-]/g, ''));
const bNum = parseFloat(bVal.replace(/[^0-9.-]/g, ''));
let comparison = 0;
if (!isNaN(aNum) && !isNaN(bNum)) {
comparison = aNum - bNum;
} else {
// Date sorting (try to parse as date)
const aDate = new Date(aVal);
const bDate = new Date(bVal);
if (!isNaN(aDate.getTime()) && !isNaN(bDate.getTime())) {
comparison = aDate - bDate;
} else {
// String sort
comparison = aVal.localeCompare(bVal, undefined, {
numeric: true,
sensitivity: 'base'
});
}
}
return this.sortDirection === 'asc' ? comparison : -comparison;
});
// Reset to first page
this.currentPage = 1;
this.render();
}
updateSortIndicators() {
const headers = Array.from(this.table.querySelectorAll('thead th'));
headers.forEach((header, index) => {
const indicator = header.querySelector('.sort-indicator');
if (!indicator) return;
if (index === this.sortColumn) {
indicator.innerHTML = this.sortDirection === 'asc'
? '<i class="fas fa-sort-up"></i>'
: '<i class="fas fa-sort-down"></i>';
indicator.classList.add('active');
} else {
indicator.innerHTML = '<i class="fas fa-sort"></i>';
indicator.classList.remove('active');
}
});
}
initColumnVisibility() {
// Create column visibility toggle button
const tableContainer = this.table.closest('.bg-card-light, .bg-card-dark, .card, .table-container') ||
this.table.parentElement;
// Look for existing toolbar (Finance tables have a flex toolbar with buttons before the table)
const tableWrapper = this.table.closest('.overflow-x-auto, .table-responsive') || this.table;
let existingToolbarRight = null;
// Check if there's an existing toolbar div before the table wrapper
if (tableWrapper.parentElement) {
let sibling = tableWrapper.previousElementSibling;
while (sibling) {
// Look for the flex toolbar div that contains buttons
if (sibling.classList.contains('flex') &&
sibling.classList.contains('justify-between') &&
sibling.classList.contains('items-center')) {
// Find the right side container (has flex items-center gap-2)
existingToolbarRight = sibling.querySelector('div.flex.items-center.gap-2');
if (!existingToolbarRight) {
// If no right container found, use the sibling itself
existingToolbarRight = sibling;
}
break;
}
sibling = sibling.previousElementSibling;
}
}
// Check for table-toolbar class as fallback
let toolbar = existingToolbarRight || tableContainer.querySelector('.table-toolbar');
if (!toolbar) {
// Create new toolbar
toolbar = document.createElement('div');
toolbar.className = 'table-toolbar';
if (tableWrapper.parentElement) {
tableWrapper.parentElement.insertBefore(toolbar, tableWrapper);
} else {
tableContainer.insertBefore(toolbar, this.table);
}
}
// Add column visibility button if not exists
if (!tableContainer.querySelector('.column-visibility-btn')) {
const btn = document.createElement('button');
btn.className = 'column-visibility-btn px-3 py-1.5 text-sm bg-background-light dark:bg-background-dark rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors inline-flex items-center gap-2';
btn.innerHTML = '<i class="fas fa-columns"></i> <span>Columns</span>';
btn.setAttribute('aria-label', 'Toggle column visibility');
const dropdown = document.createElement('div');
dropdown.className = 'column-visibility-dropdown hidden absolute right-0 mt-2 w-56 bg-card-light dark:bg-card-dark border border-border-light dark:border-border-dark rounded-md shadow-lg z-50 p-2';
btn.addEventListener('click', (e) => {
e.stopPropagation();
this.toggleColumnVisibilityDropdown(dropdown);
});
// Position dropdown
const btnContainer = document.createElement('div');
btnContainer.className = 'relative inline-block';
btnContainer.appendChild(btn);
btnContainer.appendChild(dropdown);
// If existing toolbar (Finance tables), append to the right side container
if (existingToolbarRight) {
// Insert at the beginning of the button group (before Export and Bulk Actions)
existingToolbarRight.insertBefore(btnContainer, existingToolbarRight.firstChild);
} else {
// New toolbar - create proper structure
const toolbarRight = toolbar.querySelector('.table-toolbar-right') || toolbar;
if (!toolbar.querySelector('.table-toolbar-right')) {
toolbar.style.display = 'flex';
toolbar.style.justifyContent = 'space-between';
toolbar.style.alignItems = 'center';
toolbar.style.marginBottom = '1rem';
}
toolbarRight.appendChild(btnContainer);
}
// Close on outside click
document.addEventListener('click', (e) => {
if (!btnContainer.contains(e.target)) {
dropdown.classList.add('hidden');
}
});
// Populate dropdown
this.populateColumnVisibilityDropdown(dropdown);
}
}
populateColumnVisibilityDropdown(dropdown) {
const headers = Array.from(this.table.querySelectorAll('thead th'));
// Create mapping of visible headers to their original indices
const columnMap = [];
headers.forEach((header, index) => {
if (!header.querySelector('input[type="checkbox"]')) {
columnMap.push({ header, index });
}
});
dropdown.innerHTML = columnMap.map(({ header, index }) => {
const isVisible = !this.visibleColumns.has(index) || this.visibleColumns.size === 0;
const headerText = header.textContent.trim().replace(/\s+/g, ' ');
return `
<label class="flex items-center gap-2 px-2 py-1.5 hover:bg-gray-100 dark:hover:bg-gray-700 rounded cursor-pointer">
<input type="checkbox"
class="h-4 w-4 rounded border-gray-300 text-primary focus:ring-primary"
data-column="${index}"
${isVisible ? 'checked' : ''}>
<span class="text-sm">${headerText}</span>
</label>
`;
}).join('');
// Bind checkbox events
dropdown.querySelectorAll('input[type="checkbox"]').forEach(checkbox => {
checkbox.addEventListener('change', (e) => {
const columnIndex = parseInt(e.target.getAttribute('data-column'));
this.toggleColumn(columnIndex, e.target.checked);
});
});
}
toggleColumnVisibilityDropdown(dropdown) {
dropdown.classList.toggle('hidden');
if (!dropdown.classList.contains('hidden')) {
this.populateColumnVisibilityDropdown(dropdown);
}
}
toggleColumn(columnIndex, show) {
const headers = Array.from(this.table.querySelectorAll('thead th'));
const rows = Array.from(this.table.querySelectorAll('tbody tr'));
if (show) {
this.visibleColumns.delete(columnIndex);
} else {
this.visibleColumns.add(columnIndex);
}
headers[columnIndex].style.display = show ? '' : 'none';
rows.forEach(row => {
const cells = Array.from(row.querySelectorAll('td'));
if (cells[columnIndex]) {
cells[columnIndex].style.display = show ? '' : 'none';
}
});
this.savePreferences();
}
initStickyHeader() {
const thead = this.table.querySelector('thead');
if (!thead) return;
// Add sticky header class
thead.classList.add('sticky-header');
// Add scroll listener to table container
const tableWrapper = this.table.closest('.table-responsive, .bg-card-light, .bg-card-dark') ||
this.table.parentElement;
// Create wrapper if needed
if (!tableWrapper.classList.contains('table-scroll-container')) {
const scrollContainer = document.createElement('div');
scrollContainer.className = 'table-scroll-container';
scrollContainer.style.maxHeight = '70vh';
scrollContainer.style.overflowY = 'auto';
this.table.parentNode.insertBefore(scrollContainer, this.table);
scrollContainer.appendChild(this.table);
}
// Update sticky header on scroll
const scrollContainer = this.table.closest('.table-scroll-container') || tableWrapper;
scrollContainer.addEventListener('scroll', () => {
this.updateStickyHeader(scrollContainer);
});
}
updateStickyHeader(container) {
const thead = this.table.querySelector('thead');
if (!thead) return;
if (container.scrollTop > 0) {
thead.classList.add('sticky-active');
} else {
thead.classList.remove('sticky-active');
}
}
initPagination() {
// Create pagination controls
const tableContainer = this.table.closest('.bg-card-light, .bg-card-dark, .card') ||
this.table.parentElement;
let paginationContainer = tableContainer.querySelector('.table-pagination-container');
if (!paginationContainer) {
paginationContainer = document.createElement('div');
paginationContainer.className = 'table-pagination-container flex items-center justify-between mt-4';
// Insert after table
const tableWrapper = this.table.closest('.overflow-x-auto, .table-responsive') || this.table.parentElement;
if (tableWrapper && tableWrapper.nextSibling) {
tableWrapper.parentNode.insertBefore(paginationContainer, tableWrapper.nextSibling);
} else {
this.table.parentNode.appendChild(paginationContainer);
}
}
this.paginationContainer = paginationContainer;
this.renderPagination();
}
renderPagination() {
if (!this.paginationContainer) return;
const totalItems = this.originalData.length;
const totalPages = Math.ceil(totalItems / this.pageSize);
const start = (this.currentPage - 1) * this.pageSize + 1;
const end = Math.min(this.currentPage * this.pageSize, totalItems);
// Page size selector
const pageSizeSelect = `
<div class="flex items-center gap-2">
<label class="text-sm text-text-muted-light dark:text-text-muted-dark">Show:</label>
<select class="table-page-size-select px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded bg-background-light dark:bg-background-dark">
${this.options.pageSizeOptions.map(size =>
`<option value="${size}" ${size === this.pageSize ? 'selected' : ''}>${size}</option>`
).join('')}
</select>
</div>
`;
// Pagination info
const paginationInfo = `
<div class="text-sm text-text-muted-light dark:text-text-muted-dark">
Showing ${start} to ${end} of ${totalItems} entries
</div>
`;
// Pagination buttons
const paginationButtons = this.renderPaginationButtons(totalPages);
this.paginationContainer.innerHTML = `
${pageSizeSelect}
<div class="flex items-center gap-2">
${paginationInfo}
${paginationButtons}
</div>
`;
// Bind page size change
const pageSizeSelectEl = this.paginationContainer.querySelector('.table-page-size-select');
if (pageSizeSelectEl) {
pageSizeSelectEl.addEventListener('change', (e) => {
this.pageSize = parseInt(e.target.value);
this.currentPage = 1;
this.savePreferences();
this.render();
});
}
// Bind pagination button clicks
this.paginationContainer.querySelectorAll('.pagination-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
e.preventDefault();
const page = parseInt(e.target.getAttribute('data-page'));
if (!isNaN(page) && page >= 1 && page <= totalPages) {
this.goToPage(page);
}
});
});
}
renderPaginationButtons(totalPages) {
if (totalPages <= 1) return '';
let buttons = '';
// Previous button
buttons = `
<button class="pagination-btn px-3 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded bg-background-light dark:bg-background-dark hover:bg-gray-100 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed"
data-page="${this.currentPage - 1}"
${this.currentPage === 1 ? 'disabled' : ''}>
<i class="fas fa-chevron-left"></i>
</button>
`;
// Page numbers
const maxVisible = 5;
let startPage = Math.max(1, this.currentPage - Math.floor(maxVisible / 2));
let endPage = Math.min(totalPages, startPage + maxVisible - 1);
if (endPage - startPage < maxVisible - 1) {
startPage = Math.max(1, endPage - maxVisible + 1);
}
if (startPage > 1) {
buttons += `<button class="pagination-btn px-3 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded bg-background-light dark:bg-background-dark hover:bg-gray-100 dark:hover:bg-gray-700" data-page="1">1</button>`;
if (startPage > 2) {
buttons += `<span class="px-2 text-text-muted-light dark:text-text-muted-dark">...</span>`;
}
}
for (let i = startPage; i <= endPage; i++) {
buttons += `
<button class="pagination-btn px-3 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded ${
i === this.currentPage
? 'bg-primary text-white'
: 'bg-background-light dark:bg-background-dark hover:bg-gray-100 dark:hover:bg-gray-700'
}"
data-page="${i}">
${i}
</button>
`;
}
if (endPage < totalPages) {
if (endPage < totalPages - 1) {
buttons += `<span class="px-2 text-text-muted-light dark:text-text-muted-dark">...</span>`;
}
buttons += `<button class="pagination-btn px-3 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded bg-background-light dark:bg-background-dark hover:bg-gray-100 dark:hover:bg-gray-700" data-page="${totalPages}">${totalPages}</button>`;
}
// Next button
buttons += `
<button class="pagination-btn px-3 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded bg-background-light dark:bg-background-dark hover:bg-gray-100 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed"
data-page="${this.currentPage + 1}"
${this.currentPage === totalPages ? 'disabled' : ''}>
<i class="fas fa-chevron-right"></i>
</button>
`;
return buttons;
}
goToPage(page) {
const totalPages = Math.ceil(this.originalData.length / this.pageSize);
if (page < 1 || page > totalPages) return;
this.currentPage = page;
this.render();
// Scroll to top of table
this.table.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
render() {
const tbody = this.table.querySelector('tbody');
if (!tbody) return;
// Hide all rows
this.originalData.forEach(row => {
row.element.style.display = 'none';
});
// Calculate pagination
const start = (this.currentPage - 1) * this.pageSize;
const end = start + this.pageSize;
const visibleRows = this.originalData.slice(start, end);
// Show visible rows
visibleRows.forEach(row => {
row.element.style.display = '';
});
// Update pagination UI
if (this.options.pagination) {
this.renderPagination();
}
}
}
// Auto-initialize tables with data-table-enhanced attribute
document.addEventListener('DOMContentLoaded', () => {
// Find all tables with data-table-enhanced attribute or class
const tables = document.querySelectorAll('table[data-table-enhanced], table.table-enhanced, table.table-zebra');
tables.forEach((table, index) => {
// Skip if already initialized
if (table.dataset.enhancedTableInitialized) return;
table.dataset.enhancedTableInitialized = 'true';
// Get options from data attributes
const options = {
sortable: table.dataset.sortable !== 'false',
pagination: table.dataset.pagination !== 'false',
pageSize: parseInt(table.dataset.pageSize) || 25,
columnVisibility: table.dataset.columnVisibility !== 'false',
stickyHeader: table.dataset.stickyHeader !== 'false',
storageKey: table.dataset.storageKey || `table-${table.id || index}`
};
new DataTableEnhanced(table, options);
});
});
// Export for manual initialization
window.DataTableEnhanced = DataTableEnhanced;
})();

View File

@@ -28,6 +28,7 @@
<link rel="stylesheet" href="{{ url_for('static', filename='form-bridge.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='form-validation.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='enhanced-ui.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='data-tables-enhanced.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='toast-notifications.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='keyboard-shortcuts.css') }}">
<style>
@@ -1073,6 +1074,7 @@
document.addEventListener('keydown', function(e){ if (e.key === 'Escape') closeAllMenus(); });
</script>
<script src="{{ url_for('static', filename='data-tables-enhanced.js') }}"></script>
{% block scripts_extra %}{% endblock %}
</body>
</html>

View File

@@ -66,7 +66,7 @@
{% endif %}
</div>
</div>
<table class="table table-zebra w-full text-left">
<table class="table table-zebra w-full text-left" data-table-enhanced data-page-size="25" data-sticky-header="true" data-column-visibility="true">
<thead class="border-b border-border-light dark:border-border-dark">
<tr>
{% if current_user.is_admin %}

View File

@@ -255,7 +255,7 @@
</div>
</div>
<div class="overflow-x-auto p-6 pt-4">
<table class="table table-zebra w-full text-left">
<table class="table table-zebra w-full text-left" data-table-enhanced data-page-size="25" data-sticky-header="true" data-column-visibility="true">
<thead class="border-b border-border-light dark:border-border-dark">
<tr>
{% if current_user.is_admin %}

View File

@@ -82,7 +82,7 @@
{% endif %}
</div>
</div>
<table class="table table-zebra w-full text-left">
<table class="table table-zebra w-full text-left" data-table-enhanced data-page-size="25" data-sticky-header="true" data-column-visibility="true">
<thead class="border-b border-border-light dark:border-border-dark">
<tr>
{% if current_user.is_admin %}

View File

@@ -27,7 +27,7 @@
<div class="card-body">
{% if entries and entries.items %}
<div class="table-responsive">
<table class="table table-hover">
<table class="table table-hover table-zebra" data-table-enhanced data-page-size="25" data-sticky-header="true" data-column-visibility="true">
<thead>
<tr>
<th>{{ _('Project') }}</th>

View File

@@ -147,7 +147,7 @@
</div>
</div>
<div class="overflow-x-auto p-6 pt-4">
<table class="table table-zebra w-full text-left">
<table class="table table-zebra w-full text-left" data-table-enhanced data-page-size="25" data-sticky-header="true" data-column-visibility="true">
<thead class="border-b border-border-light dark:border-border-dark">
<tr>
{% if current_user.is_admin %}

View File

@@ -112,7 +112,7 @@
</div>
</div>
<div class="overflow-x-auto p-6 pt-4">
<table class="table table-zebra w-full text-left">
<table class="table table-zebra w-full text-left" data-table-enhanced data-page-size="25" data-sticky-header="true" data-column-visibility="true">
<thead class="border-b border-border-light dark:border-border-dark">
<tr>
{% if current_user.is_admin %}

View File

@@ -179,7 +179,7 @@
</div>
</div>
<div class="overflow-x-auto p-6 pt-4">
<table class="table table-zebra w-full text-left">
<table class="table table-zebra w-full text-left" data-table-enhanced data-page-size="25" data-sticky-header="true" data-column-visibility="true">
<thead class="border-b border-border-light dark:border-border-dark">
<tr>
{% if current_user.is_admin %}

View File

@@ -87,7 +87,7 @@
{% endif %}
</div>
</div>
<table class="table table-zebra w-full text-left">
<table class="table table-zebra w-full text-left" data-table-enhanced data-page-size="25" data-sticky-header="true" data-column-visibility="true">
<thead class="border-b border-border-light dark:border-border-dark">
<tr>
{% if current_user.is_admin %}

View File

@@ -113,7 +113,7 @@
</div>
</div>
</div>
<table class="table table-zebra w-full text-left">
<table class="table table-zebra w-full text-left" data-table-enhanced data-page-size="25" data-sticky-header="true" data-column-visibility="true">
<thead class="border-b border-border-light dark:border-border-dark">
<tr>
<th class="p-4 w-12">