feat: Add 1-year filter + search to subscription matrix

v1.3.135:
- Added date filter to e-conomic API (only fetch invoices from last year)
- Implemented product search in billing matrix
- Shows/hides search field based on product count
- Real-time filtering with clear button
This commit is contained in:
Christian 2026-01-27 07:08:09 +01:00
parent 36e0f8b0f7
commit 180933948f
3 changed files with 87 additions and 4 deletions

View File

@ -1 +1 @@
1.3.134 1.3.135

View File

@ -507,6 +507,19 @@
</button> </button>
</div> </div>
<!-- Search field -->
<div class="mb-3" id="matrixSearchContainer" style="display: none;">
<div class="input-group">
<span class="input-group-text">
<i class="bi bi-search"></i>
</span>
<input type="text" class="form-control" id="matrixSearchInput" placeholder="Søg efter produkt..." onkeyup="filterMatrixProducts()">
<button class="btn btn-outline-secondary" type="button" onclick="clearMatrixSearch()">
<i class="bi bi-x"></i>
</button>
</div>
</div>
<div id="billingMatrixContainer" style="display: none;"> <div id="billingMatrixContainer" style="display: none;">
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-sm table-hover mb-0" id="billingMatrixTable"> <table class="table table-sm table-hover mb-0" id="billingMatrixTable">
@ -2304,6 +2317,18 @@ function renderBillingMatrix(matrix) {
</tr>`; </tr>`;
}).join(''); }).join('');
// Populate table body
const tableBody = document.getElementById('matrixBodyRows');
tableBody.innerHTML = matrixHtml;
// Show search container if there are products
const searchContainer = document.getElementById('matrixSearchContainer');
if (matrix.products.length > 0) {
searchContainer.style.display = 'block';
} else {
searchContainer.style.display = 'none';
}
// Show matrix, hide loading // Show matrix, hide loading
document.getElementById('billingMatrixContainer').style.display = 'block'; document.getElementById('billingMatrixContainer').style.display = 'block';
document.getElementById('billingMatrixLoading').style.display = 'none'; document.getElementById('billingMatrixLoading').style.display = 'none';
@ -2329,6 +2354,55 @@ function formatDKK(amount) {
return amount.toLocaleString('da-DK', { style: 'currency', currency: 'DKK', minimumFractionDigits: 0 }); return amount.toLocaleString('da-DK', { style: 'currency', currency: 'DKK', minimumFractionDigits: 0 });
} }
/**
* Filter products in billing matrix based on search input
*/
function filterMatrixProducts() {
const searchInput = document.getElementById('matrixSearchInput');
const searchTerm = searchInput.value.toLowerCase();
const tableBody = document.getElementById('matrixBodyRows');
const rows = tableBody.getElementsByTagName('tr');
let visibleCount = 0;
for (let i = 0; i < rows.length; i++) {
const row = rows[i];
const productName = row.cells[0].textContent.toLowerCase();
if (productName.includes(searchTerm)) {
row.style.display = '';
visibleCount++;
} else {
row.style.display = 'none';
}
}
// Show message if no results
if (visibleCount === 0 && searchTerm.length > 0) {
if (!document.getElementById('matrixNoResults')) {
const noResultsRow = document.createElement('tr');
noResultsRow.id = 'matrixNoResults';
noResultsRow.innerHTML = '<td colspan="100" class="text-center text-muted py-3"><i class="bi bi-search me-2"></i>Ingen produkter matcher søgningen</td>';
tableBody.appendChild(noResultsRow);
}
} else {
const noResultsRow = document.getElementById('matrixNoResults');
if (noResultsRow) {
noResultsRow.remove();
}
}
}
/**
* Clear matrix search filter
*/
function clearMatrixSearch() {
const searchInput = document.getElementById('matrixSearchInput');
searchInput.value = '';
filterMatrixProducts();
searchInput.focus();
}
// Auto-load matrix when subscriptions tab is shown // Auto-load matrix when subscriptions tab is shown
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const subscriptionsTab = document.querySelector('a[href="#subscriptions"]'); const subscriptionsTab = document.querySelector('a[href="#subscriptions"]');

View File

@ -9,6 +9,7 @@ Send invoices and supplier invoices (kassekladde) to e-conomic accounting system
import logging import logging
import aiohttp import aiohttp
import json import json
from datetime import datetime, timedelta
from typing import Dict, Optional, List from typing import Dict, Optional, List
from app.core.config import settings from app.core.config import settings
@ -439,16 +440,20 @@ class EconomicService:
async def get_customer_invoices(self, customer_number: int, include_lines: bool = True) -> List[Dict]: async def get_customer_invoices(self, customer_number: int, include_lines: bool = True) -> List[Dict]:
""" """
Get customer invoices (sales invoices) from e-conomic Get customer invoices (sales invoices) from e-conomic (last 1 year only)
Args: Args:
customer_number: e-conomic customer number customer_number: e-conomic customer number
include_lines: Whether to include invoice lines (adds more API calls but gets full data) include_lines: Whether to include invoice lines (adds more API calls but gets full data)
Returns: Returns:
List of invoice records with lines List of invoice records with lines from the last year
""" """
try: try:
# Calculate date 1 year ago
one_year_ago = (datetime.now() - timedelta(days=365)).strftime('%Y-%m-%d')
logger.info(f"📅 Fetching invoices from {one_year_ago} onwards (1 year filter)")
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
# Try multiple endpoints to find invoices # Try multiple endpoints to find invoices
# Include drafts, paid, unpaid, booked, sent, archived, etc. # Include drafts, paid, unpaid, booked, sent, archived, etc.
@ -475,7 +480,11 @@ class EconomicService:
while True: while True:
async with session.get( async with session.get(
endpoint, endpoint,
params={"pagesize": 1000, "skippages": page}, params={
"pagesize": 1000,
"skippages": page,
"filter": f"date$gte:{one_year_ago}"
},
headers=self._get_headers() headers=self._get_headers()
) as response: ) as response:
logger.info(f"🔍 [API] Response status from {endpoint} (page {page}): {response.status}") logger.info(f"🔍 [API] Response status from {endpoint} (page {page}): {response.status}")