Refactors reports page search and filtering functionality

Moves search and filtering logic from inline script to a dedicated TypeScript module

Improves code organization by creating a ReportsController with:
- Enhanced search capabilities
- Advanced range query parsing
- Flexible filtering mechanisms

Removes inline JavaScript and integrates modular approach in the application
This commit is contained in:
Janus C. H. Knudsen 2026-01-21 21:49:10 +01:00
parent 405dabeb34
commit 2f92b0eb7b
3 changed files with 364 additions and 182 deletions

View file

@ -391,188 +391,6 @@
@section Scripts {
<script type="module">
import { createChart } from '/lib/swp-charting/dist/swp-charting.js';
import Fuse from '/lib/fuse/fuse.mjs';
// === SEARCH FUNCTIONALITY ===
const searchInput = document.getElementById('searchInput');
const tableRows = document.querySelectorAll('swp-card.sales-table swp-data-table-row');
// Build searchable data from table rows
const salesData = Array.from(tableRows).map((row, index) => {
const cells = row.querySelectorAll('swp-data-table-cell');
return {
index,
invoice: cells[0]?.textContent?.trim() || '',
date: cells[1]?.textContent?.trim() || '',
customer: cells[2]?.textContent?.trim() || '',
employee: cells[3]?.textContent?.trim() || '',
services: cells[4]?.textContent?.trim() || '',
amount: cells[5]?.textContent?.trim() || '',
payment: cells[6]?.textContent?.trim() || '',
status: cells[7]?.textContent?.trim() || ''
};
});
// Initialize Fuse.js
const fuse = new Fuse(salesData, {
keys: ['invoice', 'customer', 'employee', 'services', 'amount', 'payment', 'status'],
threshold: 0.3,
ignoreLocation: true
});
// Parse amount string "1.450 kr" -> 1450
function parseAmountFromText(text) {
const match = text.match(/-?([\d.]+)/);
if (!match) return null;
// Remove thousand separators (Danish format uses . for thousands)
return parseFloat(match[1].replace(/\./g, ''));
}
// Parse invoice number "#1847" -> 1847
function parseInvoiceNumber(text) {
const match = text.match(/#(\d+)/);
return match ? parseInt(match[1], 10) : null;
}
// Parse range operators from query (supports both amount and invoice ranges)
function parseRangeQuery(query) {
let textQuery = query;
let minAmount = null;
let maxAmount = null;
let minInvoice = null;
let maxInvoice = null;
// === INVOICE NUMBER RANGES (with # prefix) ===
// Match #1840-1845
let match = textQuery.match(/#(\d+)-(\d+)/);
if (match) {
minInvoice = parseInt(match[1], 10);
maxInvoice = parseInt(match[2], 10);
textQuery = textQuery.replace(match[0], '').trim();
}
// Match #>=
match = textQuery.match(/#>=\s*(\d+)/);
if (match) {
minInvoice = parseInt(match[1], 10);
textQuery = textQuery.replace(match[0], '').trim();
}
// Match #> (but not #>=)
match = textQuery.match(/#>\s*(\d+)/);
if (match) {
minInvoice = parseInt(match[1], 10) + 1;
textQuery = textQuery.replace(match[0], '').trim();
}
// Match #<=
match = textQuery.match(/#<=\s*(\d+)/);
if (match) {
maxInvoice = parseInt(match[1], 10);
textQuery = textQuery.replace(match[0], '').trim();
}
// Match #< (but not #<=)
match = textQuery.match(/#<\s*(\d+)/);
if (match) {
maxInvoice = parseInt(match[1], 10) - 1;
textQuery = textQuery.replace(match[0], '').trim();
}
// === AMOUNT RANGES (no prefix) ===
// Match range syntax: 400-1000
match = textQuery.match(/(\d+)-(\d+)/);
if (match) {
minAmount = parseFloat(match[1]);
maxAmount = parseFloat(match[2]);
textQuery = textQuery.replace(match[0], '').trim();
}
// Match >=
match = textQuery.match(/>=\s*(\d+(?:\.\d+)?)/);
if (match) {
minAmount = parseFloat(match[1]);
textQuery = textQuery.replace(match[0], '').trim();
}
// Match > (but not >=)
match = textQuery.match(/>\s*(\d+(?:\.\d+)?)/);
if (match) {
minAmount = parseFloat(match[1]) + 0.01;
textQuery = textQuery.replace(match[0], '').trim();
}
// Match <=
match = textQuery.match(/<=\s*(\d+(?:\.\d+)?)/);
if (match) {
maxAmount = parseFloat(match[1]);
textQuery = textQuery.replace(match[0], '').trim();
}
// Match < (but not <=)
match = textQuery.match(/<\s*(\d+(?:\.\d+)?)/);
if (match) {
maxAmount = parseFloat(match[1]) - 0.01;
textQuery = textQuery.replace(match[0], '').trim();
}
return { textQuery, minAmount, maxAmount, minInvoice, maxInvoice };
}
// Search handler with range support
searchInput.addEventListener('input', (e) => {
const query = e.target.value.trim();
if (!query) {
// Show all rows
tableRows.forEach(row => row.style.display = '');
return;
}
const { textQuery, minAmount, maxAmount, minInvoice, maxInvoice } = parseRangeQuery(query);
// Start with all indices
let matchedIndices = new Set(salesData.map((_, i) => i));
// Apply invoice number range filter
if (minInvoice !== null || maxInvoice !== null) {
matchedIndices = new Set(
salesData
.filter((item) => {
const invoiceNum = parseInvoiceNumber(item.invoice);
if (invoiceNum === null) return false;
if (minInvoice !== null && invoiceNum < minInvoice) return false;
if (maxInvoice !== null && invoiceNum > maxInvoice) return false;
return true;
})
.map(item => item.index)
);
}
// Apply amount range filter
if (minAmount !== null || maxAmount !== null) {
matchedIndices = new Set(
[...matchedIndices].filter(i => {
const amount = parseAmountFromText(salesData[i].amount);
if (amount === null) return false;
if (minAmount !== null && amount < minAmount) return false;
if (maxAmount !== null && amount > maxAmount) return false;
return true;
})
);
}
// Apply Fuse.js text search if there's remaining text
if (textQuery) {
const fuseResults = fuse.search(textQuery);
const textMatches = new Set(fuseResults.map(r => r.item.index));
matchedIndices = new Set([...matchedIndices].filter(i => textMatches.has(i)));
}
tableRows.forEach((row, index) => {
row.style.display = matchedIndices.has(index) ? '' : 'none';
});
});
// Revenue bar chart
createChart(document.getElementById('revenueChart'), {