Add IBAN and Verwendungszweck columns to support list and fix chart growing bug

- Enhanced 'Alle Unterstützungen' view with IBAN and Verwendungszweck columns for better payment tracking
- Updated export functions to handle both legacy 'selected_fields' and new 'fields' parameters
- Added IBAN and Verwendungszweck to default export field selections
- Improved destinataer list UI by adding Status column and removing obsolete study proof field
- Fixed infinite growing animation bug in 'Größen der Grundstücke (Top 30)' chart by replacing Chart.js with CSS-based implementation
- Removed Bootstrap h-100 class conflicts that caused chart resize loops
This commit is contained in:
2025-09-24 22:13:27 +02:00
parent d3ed13dda0
commit b00cf62d87
4 changed files with 106 additions and 53 deletions

View File

@@ -4474,12 +4474,23 @@ def export_unterstuetzungen_csv(request, queryset, selected_ids=None):
if selected_ids: if selected_ids:
queryset = queryset.filter(id__in=selected_ids) queryset = queryset.filter(id__in=selected_ids)
# Get selected fields from request (default to all if none specified) # Get selected fields from request (handle both 'fields' and 'selected_fields' parameter names)
selected_fields_param = ( selected_fields_param = ""
request.POST.get("selected_fields", "") if request.method == "POST":
if request.method == "POST" # Try 'fields' first (new format), then 'selected_fields' (legacy)
else request.GET.get("selected_fields", "") fields_list = request.POST.getlist("fields")
) if fields_list:
selected_fields_param = ",".join(fields_list)
else:
selected_fields_param = request.POST.get("selected_fields", "")
else:
# Try 'fields' first (new format), then 'selected_fields' (legacy)
fields_list = request.GET.getlist("fields")
if fields_list:
selected_fields_param = ",".join(fields_list)
else:
selected_fields_param = request.GET.get("selected_fields", "")
selected_fields = selected_fields_param.split(",") if selected_fields_param else [] selected_fields = selected_fields_param.split(",") if selected_fields_param else []
if not selected_fields: if not selected_fields:
@@ -4488,8 +4499,9 @@ def export_unterstuetzungen_csv(request, queryset, selected_ids=None):
"destinataer_name", "destinataer_name",
"betrag", "betrag",
"faellig_am", "faellig_am",
"status",
"empfaenger_iban", "empfaenger_iban",
"verwendungszweck",
"status",
"empfaenger_name", "empfaenger_name",
"beschreibung", "beschreibung",
] ]
@@ -4671,12 +4683,23 @@ def export_unterstuetzungen_pdf(request, queryset, selected_ids=None):
if selected_ids: if selected_ids:
queryset = queryset.filter(id__in=selected_ids) queryset = queryset.filter(id__in=selected_ids)
# Get selected fields from request (default to key fields if none specified) # Get selected fields from request (handle both 'fields' and 'selected_fields' parameter names)
selected_fields_param = ( selected_fields_param = ""
request.POST.get("selected_fields", "") if request.method == "POST":
if request.method == "POST" # Try 'fields' first (new format), then 'selected_fields' (legacy)
else request.GET.get("selected_fields", "") fields_list = request.POST.getlist("fields")
) if fields_list:
selected_fields_param = ",".join(fields_list)
else:
selected_fields_param = request.POST.get("selected_fields", "")
else:
# Try 'fields' first (new format), then 'selected_fields' (legacy)
fields_list = request.GET.getlist("fields")
if fields_list:
selected_fields_param = ",".join(fields_list)
else:
selected_fields_param = request.GET.get("selected_fields", "")
selected_fields = selected_fields_param.split(",") if selected_fields_param else [] selected_fields = selected_fields_param.split(",") if selected_fields_param else []
if not selected_fields: if not selected_fields:
@@ -4685,6 +4708,8 @@ def export_unterstuetzungen_pdf(request, queryset, selected_ids=None):
"destinataer_name", "destinataer_name",
"betrag", "betrag",
"faellig_am", "faellig_am",
"empfaenger_iban",
"verwendungszweck",
"status", "status",
"beschreibung", "beschreibung",
"ausgezahlt_am", "ausgezahlt_am",

View File

@@ -100,7 +100,7 @@
<a href="?sort=vierteljaehrlicher_betrag&dir={% if sort == 'vierteljaehrlicher_betrag' and dir != 'desc' %}desc{% else %}asc{% endif %}{% if search_query %}&search={{ search_query }}{% endif %}{% if familienzweig_filter %}&familienzweig={{ familienzweig_filter }}{% endif %}{% if berufsgruppe_filter %}&berufsgruppe={{ berufsgruppe_filter }}{% endif %}{% if aktiv_filter %}&aktiv={{ aktiv_filter }}{% endif %}">Vierteljährlicher Betrag</a> <a href="?sort=vierteljaehrlicher_betrag&dir={% if sort == 'vierteljaehrlicher_betrag' and dir != 'desc' %}desc{% else %}asc{% endif %}{% if search_query %}&search={{ search_query }}{% endif %}{% if familienzweig_filter %}&familienzweig={{ familienzweig_filter }}{% endif %}{% if berufsgruppe_filter %}&berufsgruppe={{ berufsgruppe_filter }}{% endif %}{% if aktiv_filter %}&aktiv={{ aktiv_filter }}{% endif %}">Vierteljährlicher Betrag</a>
</th> </th>
<th> <th>
<a href="?sort=letzter_studiennachweis&dir={% if sort == 'letzter_studiennachweis' and dir != 'desc' %}desc{% else %}asc{% endif %}{% if search_query %}&search={{ search_query }}{% endif %}{% if familienzweig_filter %}&familienzweig={{ familienzweig_filter }}{% endif %}{% if berufsgruppe_filter %}&berufsgruppe={{ berufsgruppe_filter }}{% endif %}{% if aktiv_filter %}&aktiv={{ aktiv_filter }}{% endif %}">Letzter Studiennachweis</a> <a href="?sort=aktiv&dir={% if sort == 'aktiv' and dir != 'desc' %}desc{% else %}asc{% endif %}{% if search_query %}&search={{ search_query }}{% endif %}{% if familienzweig_filter %}&familienzweig={{ familienzweig_filter }}{% endif %}{% if berufsgruppe_filter %}&berufsgruppe={{ berufsgruppe_filter }}{% endif %}{% if aktiv_filter %}&aktiv={{ aktiv_filter }}{% endif %}">Status</a>
</th> </th>
<th> <th>
<a href="?sort=unterstuetzung_bestaetigt&dir={% if sort == 'unterstuetzung_bestaetigt' and dir != 'desc' %}desc{% else %}asc{% endif %}{% if search_query %}&search={{ search_query }}{% endif %}{% if familienzweig_filter %}&familienzweig={{ familienzweig_filter }}{% endif %}{% if berufsgruppe_filter %}&berufsgruppe={{ berufsgruppe_filter }}{% endif %}{% if aktiv_filter %}&aktiv={{ aktiv_filter }}{% endif %}">Unterstützung bestätigt</a> <a href="?sort=unterstuetzung_bestaetigt&dir={% if sort == 'unterstuetzung_bestaetigt' and dir != 'desc' %}desc{% else %}asc{% endif %}{% if search_query %}&search={{ search_query }}{% endif %}{% if familienzweig_filter %}&familienzweig={{ familienzweig_filter }}{% endif %}{% if berufsgruppe_filter %}&berufsgruppe={{ berufsgruppe_filter }}{% endif %}{% if aktiv_filter %}&aktiv={{ aktiv_filter }}{% endif %}">Unterstützung bestätigt</a>
@@ -135,10 +135,10 @@
{% endif %} {% endif %}
</td> </td>
<td> <td>
{% if destinataer.letzter_studiennachweis %} {% if destinataer.aktiv %}
<span class="badge bg-info">{{ destinataer.letzter_studiennachweis|date:"d.m.Y" }}</span> <span class="badge bg-success"><i class="fas fa-check-circle me-1"></i>Aktiv</span>
{% else %} {% else %}
<span class="text-muted">-</span> <span class="badge bg-secondary"><i class="fas fa-pause-circle me-1"></i>Inaktiv</span>
{% endif %} {% endif %}
</td> </td>
<td> <td>

View File

@@ -99,14 +99,18 @@
</div> </div>
</div> </div>
<div class="col-lg-6 mb-3"> <div class="col-lg-6 mb-3">
<div class="card shadow h-100"> <div class="card shadow">
<div class="card-header py-3 d-flex justify-content-between align-items-center"> <div class="card-header py-3 d-flex justify-content-between align-items-center">
<h6 class="m-0 font-weight-bold text-primary"> <h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-chart-bar me-2"></i>Größen der Grundstücke (Top 30) <i class="fas fa-chart-bar me-2"></i>Größen der Grundstücke (Top 30)
</h6> </h6>
</div> </div>
<div class="card-body"> <div class="card-body">
<canvas id="sizesChart" height="140"></canvas> <div style="height: 140px; overflow: hidden;">
<div id="sizesChart" style="display: flex; align-items: end; height: 100%; gap: 2px; padding: 5px;">
<!-- Simple CSS bar chart will be populated by JavaScript -->
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -395,42 +399,50 @@
console.log('usageChart canvas not found'); console.log('usageChart canvas not found');
} }
// Bar chart for sizes // Simple CSS bar chart for sizes (no Chart.js)
const ctx = document.getElementById('sizesChart'); const chartContainer = document.getElementById('sizesChart');
if (ctx) { if (chartContainer) {
console.log('Found sizesChart canvas'); console.log('Found sizesChart container');
const labels = JSON.parse('{{ size_chart_labels_json|escapejs }}'); const labels = JSON.parse('{{ size_chart_labels_json|escapejs }}');
const dataVals = JSON.parse('{{ size_chart_values_json|escapejs }}'); const dataVals = JSON.parse('{{ size_chart_values_json|escapejs }}');
console.log('Bar chart data:', {labels: labels.length, values: dataVals.length}); console.log('Bar chart data:', {labels: labels.length, values: dataVals.length});
new Chart(ctx, { if (dataVals.length > 0) {
type: 'bar', const maxValue = Math.max(...dataVals);
data: {
labels: labels, chartContainer.innerHTML = ''; // Clear container
datasets: [{
label: 'Größe (qm)', // Create bars
data: dataVals, dataVals.forEach((value, index) => {
backgroundColor: 'rgba(0, 104, 55, 0.6)', const barHeight = (value / maxValue) * 120; // Max height 120px
borderColor: '#006837', const bar = document.createElement('div');
borderWidth: 2 bar.style.cssText = `
}] background-color: rgba(0, 104, 55, 0.8);
}, border: 1px solid #006837;
options: { width: ${Math.max(100 / dataVals.length - 1, 8)}%;
responsive: true, height: ${barHeight}px;
maintainAspectRatio: false, min-height: 2px;
scales: { display: flex;
x: { ticks: { autoSkip: true, maxTicksLimit: 10 } }, align-items: end;
y: { beginAtZero: true } justify-content: center;
}, font-size: 10px;
plugins: { color: white;
legend: { display: false }, text-shadow: 1px 1px 1px rgba(0,0,0,0.5);
tooltip: { callbacks: { label: function(ctx) { return ctx.parsed.y.toLocaleString() + ' qm'; } } } cursor: pointer;
} `;
}
}); // Add tooltip on hover
console.log('Bar chart created successfully'); bar.title = `${labels[index] || 'N/A'}: ${value.toLocaleString()} qm`;
chartContainer.appendChild(bar);
});
console.log('CSS bar chart created successfully');
} else {
chartContainer.innerHTML = '<div style="text-align: center; color: #666; padding: 20px;">Keine Daten verfügbar</div>';
}
} else { } else {
console.log('sizesChart canvas not found'); console.log('sizesChart container not found');
} }
} catch (e) { } catch (e) {
console.error('Chart initialization error:', e); console.error('Chart initialization error:', e);

View File

@@ -117,7 +117,7 @@
<label class="form-check-label" for="field_empfaenger_name">Empfänger Name</label> <label class="form-check-label" for="field_empfaenger_name">Empfänger Name</label>
</div> </div>
<div class="form-check form-check-sm"> <div class="form-check form-check-sm">
<input class="form-check-input field-checkbox" type="checkbox" name="fields" value="verwendungszweck" id="field_verwendungszweck"> <input class="form-check-input field-checkbox" type="checkbox" name="fields" value="verwendungszweck" id="field_verwendungszweck" checked>
<label class="form-check-label" for="field_verwendungszweck">Verwendungszweck</label> <label class="form-check-label" for="field_verwendungszweck">Verwendungszweck</label>
</div> </div>
</div> </div>
@@ -275,6 +275,8 @@
<th>Destinatär</th> <th>Destinatär</th>
<th>Betrag</th> <th>Betrag</th>
<th>Fällig am</th> <th>Fällig am</th>
<th>IBAN</th>
<th>Verwendungszweck</th>
<th>Konto</th> <th>Konto</th>
<th>Status</th> <th>Status</th>
<th>Beschreibung</th> <th>Beschreibung</th>
@@ -296,6 +298,20 @@
</td> </td>
<td>€{{ unterstuetzung.betrag|floatformat:2 }}</td> <td>€{{ unterstuetzung.betrag|floatformat:2 }}</td>
<td>{{ unterstuetzung.faellig_am|date:"d.m.Y" }}</td> <td>{{ unterstuetzung.faellig_am|date:"d.m.Y" }}</td>
<td>
{% if unterstuetzung.empfaenger_iban %}
<code class="text-dark">{{ unterstuetzung.empfaenger_iban }}</code>
{% else %}
<span class="text-muted">-</span>
{% endif %}
</td>
<td>
{% if unterstuetzung.verwendungszweck %}
{{ unterstuetzung.verwendungszweck|truncatechars:30 }}
{% else %}
<span class="text-muted">-</span>
{% endif %}
</td>
<td> <td>
<span class="badge bg-secondary">{{ unterstuetzung.konto }}</span> <span class="badge bg-secondary">{{ unterstuetzung.konto }}</span>
</td> </td>
@@ -353,8 +369,8 @@ function selectDefaultFields() {
// Check default fields // Check default fields
const defaultFields = [ const defaultFields = [
'destinataer_name', 'betrag', 'faellig_am', 'status', 'destinataer_name', 'betrag', 'faellig_am', 'empfaenger_iban', 'verwendungszweck',
'empfaenger_iban', 'empfaenger_name', 'beschreibung' 'status', 'empfaenger_name', 'beschreibung'
]; ];
defaultFields.forEach(fieldName => { defaultFields.forEach(fieldName => {