tinyqc/public/js/tables.js
mahdahar ff90e0eb29 Initial commit: Add CodeIgniter 4 QC application with full MVC structure
- CodeIgniter 4 framework setup with SQL Server database config
- Models: Control, Test, Dept, Result, Daily/ Monthly entry models
- Controllers: Dashboard, Control, Test, Dept, Entry, Report, API endpoints
- Views: CRUD pages with modal dialogs, dashboard, reports
- Database: Migrations for control test and daily/monthly result tables
- Legacy v1 PHP application preserved in /v1 directory
- Documentation: AGENTS.md, VIEWS_RULES.md for development guidelines
2026-01-14 16:49:27 +07:00

187 lines
7.7 KiB
JavaScript

window.TableEnhancer = {
init(selector, options = {}) {
const defaults = {
perPage: 10,
searchable: true,
sortable: true,
persistKey: null
};
const config = { ...defaults, ...options };
document.querySelectorAll(selector).forEach(table => this.enhance(table, config));
},
enhance(table, config) {
const container = table.closest('.table-container') || table.parentElement;
if (!container.querySelector('.table-search')) {
this.addControls(container, table, config);
}
this.setupSearch(container, table, config);
this.setupSort(container, table, config);
this.setupPagination(container, table, config);
},
addControls(container, table, config) {
const controls = document.createElement('div');
controls.className = 'table-controls flex justify-between items-center mb-4 p-4 bg-gray-50 rounded-t-lg';
controls.innerHTML = `
${config.searchable ? `
<div class="relative">
<input type="text" class="table-search pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500 w-64" placeholder="Search...">
<svg class="absolute left-3 top-2.5 h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
</svg>
</div>
` : ''}
<div class="table-info text-sm text-gray-500"></div>
`;
container.insertBefore(controls, table);
const pagination = document.createElement('div');
pagination.className = 'table-pagination flex justify-center items-center space-x-2 mt-4';
container.appendChild(pagination);
},
setupSearch(container, table, config) {
const searchInput = container.querySelector('.table-search');
if (!searchInput) return;
const key = config.persistKey ? `table_search_${config.persistKey}` : null;
if (key && localStorage.getItem(key)) {
searchInput.value = localStorage.getItem(key);
}
searchInput.addEventListener('input', (e) => {
if (key) localStorage.setItem(key, e.target.value);
this.filterTable(table, e.target.value);
this.updatePagination(container, table, 1, config);
});
},
setupSort(container, table, config) {
if (!config.sortable) return;
const headers = table.querySelectorAll('th');
headers.forEach((th, index) => {
if (th.textContent.trim()) {
th.classList.add('cursor-pointer', 'hover:bg-gray-100');
th.dataset.sortCol = index;
th.innerHTML += `
<svg class="inline h-4 w-4 ml-1 text-gray-400 sort-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16V4m0 0L3 8m4-4l4 4m6 0v12m0 0l4-4m-4 4l-4-4"/>
</svg>
`;
}
});
container.querySelectorAll('th[data-sort-col]').forEach(th => {
th.addEventListener('click', () => {
const col = th.dataset.sortCol;
const currentSort = th.dataset.sort || 'none';
const newSort = currentSort === 'asc' ? 'desc' : 'asc';
this.sortTable(table, col, newSort);
th.dataset.sort = newSort;
th.querySelector('.sort-icon').innerHTML = newSort === 'asc'
? '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 15l7-7 7 7"/>'
: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>';
});
});
},
setupPagination(container, table, config) {
this.updatePagination(container, table, 1, config);
},
filterTable(table, query) {
const rows = table.querySelectorAll('tbody tr');
const lowerQuery = query.toLowerCase();
let visibleCount = 0;
rows.forEach(row => {
const text = row.textContent.toLowerCase();
const match = text.includes(lowerQuery);
row.style.display = match ? '' : 'none';
if (match) visibleCount++;
});
const infoEl = container.querySelector('.table-info');
if (infoEl) {
infoEl.textContent = `${visibleCount} row${visibleCount !== 1 ? 's' : ''} found`;
}
},
sortTable(table, col, direction) {
const tbody = table.querySelector('tbody');
const rows = Array.from(tbody.querySelectorAll('tr'));
rows.sort((a, b) => {
const aVal = a.querySelectorAll('td')[col]?.textContent.trim() || '';
const bVal = b.querySelectorAll('td')[col]?.textContent.trim() || '';
const aNum = parseFloat(aVal);
const bNum = parseFloat(bVal);
if (!isNaN(aNum) && !isNaN(bNum)) {
return direction === 'asc' ? aNum - bNum : bNum - aNum;
}
return direction === 'asc'
? aVal.localeCompare(bVal)
: bVal.localeCompare(aVal);
});
rows.forEach(row => tbody.appendChild(row));
},
updatePagination(container, table, page, config) {
const rows = Array.from(table.querySelectorAll('tbody tr:not([style*=\"display: none\"])'));
const totalPages = Math.ceil(rows.length / config.perPage);
const pagination = container.querySelector('.table-pagination');
if (!pagination) return;
const start = (page - 1) * config.perPage;
const end = start + config.perPage;
rows.forEach((row, index) => {
row.style.display = index >= start && index < end ? '' : 'none';
});
if (totalPages <= 1) {
pagination.innerHTML = '';
return;
}
let html = `
<button class="px-3 py-1 border rounded hover:bg-gray-100" ${page === 1 ? 'disabled' : ''} onclick="TableEnhancer.goToPage('${container.querySelector('table').id || ''}', ${page - 1})">Prev</button>
`;
for (let i = 1; i <= totalPages; i++) {
if (i === 1 || i === totalPages || (i >= page - 1 && i <= page + 1)) {
html += `<button class="px-3 py-1 border rounded ${i === page ? 'bg-primary-600 text-white' : 'hover:bg-gray-100'}" onclick="TableEnhancer.goToPage('${container.querySelector('table').id || ''}', ${i})">${i}</button>`;
} else if (i === page - 2 || i === page + 2) {
html += `<span class="px-2">...</span>`;
}
}
html += `
<button class="px-3 py-1 border rounded hover:bg-gray-100" ${page === totalPages ? 'disabled' : ''} onclick="TableEnhancer.goToPage('${container.querySelector('table').id || ''}', ${page + 1})">Next</button>
`;
pagination.innerHTML = html;
},
goToPage(tableId, page) {
const table = tableId ? document.getElementById(tableId) : document.querySelector('table');
if (!table) return;
const container = table.closest('.table-container') || table.parentElement;
const config = { perPage: 10 };
this.updatePagination(container, table, page, config);
},
goToPageBySelector(selector, page) {
const container = document.querySelector(selector);
if (!container) return;
const table = container.querySelector('table');
if (!table) return;
const config = { perPage: 10 };
this.updatePagination(container, table, page, config);
}
};