/* ESPILON C2 — Reusable sortable/filterable table mixin for Alpine.js */ function dataTable(config = {}) { return { rows: [], sortCol: config.defaultSort || null, sortDir: config.defaultDir || 'asc', filter: '', loading: true, page: 0, pageSize: config.pageSize || 100, get filteredRows() { let r = this.rows; if (this.filter) { const q = this.filter.toLowerCase(); r = r.filter(row => { for (const v of Object.values(row)) { if (v != null && String(v).toLowerCase().includes(q)) return true; } return false; }); } if (this.sortCol) { r = [...r].sort((a, b) => { let va = a[this.sortCol], vb = b[this.sortCol]; if (va == null) va = ''; if (vb == null) vb = ''; if (typeof va === 'number' && typeof vb === 'number') { return this.sortDir === 'asc' ? va - vb : vb - va; } va = String(va).toLowerCase(); vb = String(vb).toLowerCase(); const cmp = va < vb ? -1 : va > vb ? 1 : 0; return this.sortDir === 'asc' ? cmp : -cmp; }); } return r; }, get pagedRows() { const start = this.page * this.pageSize; return this.filteredRows.slice(start, start + this.pageSize); }, get totalPages() { return Math.ceil(this.filteredRows.length / this.pageSize); }, toggleSort(col) { if (this.sortCol === col) { this.sortDir = this.sortDir === 'asc' ? 'desc' : 'asc'; } else { this.sortCol = col; this.sortDir = 'asc'; } }, sortClass(col) { if (this.sortCol !== col) return ''; return this.sortDir === 'asc' ? 'sort-asc' : 'sort-desc'; }, async refresh(url, extract) { this.loading = true; try { const res = await fetch(url); const data = await res.json(); this.rows = extract ? extract(data) : (config.extract ? config.extract(data) : data); } catch (e) { console.error('dataTable refresh error:', e); } this.loading = false; } }; }