Enhanced quarterly confirmation system with approval workflow and export improvements

Features added:
-  Fixed quarterly confirmation approval system with URL pattern
-  Added re-approval and status reset functionality for quarterly confirmations
-  Synchronized quarterly approval status with support payment system
-  Enhanced Destinataer export with missing fields (anrede, titel, mobil)
-  Added quarterly confirmation data and documents to export system
-  Fixed address field display issues in destinataer template
-  Added quarterly statistics dashboard to support payment lists
-  Implemented duplicate support payment prevention and cleanup
-  Added visual indicators for quarterly-linked support payments

Technical improvements:
- Enhanced create_quarterly_support_payment() with duplicate detection
- Added get_related_support_payment() method to VierteljahresNachweis model
- Improved quarterly confirmation workflow with proper status transitions
- Added computed address property to Destinataer model
- Fixed template field mismatches (anrede, titel, mobil vs strasse, plz, ort)
- Enhanced backup system with operation tracking and cancellation

Workflow enhancements:
- Quarterly confirmations now properly sync with support payments
- Single support payment per destinataer per quarter (no duplicates)
- Approval button works for both eingereicht and geprueft status
- Reset functionality allows workflow restart
- Export includes complete quarterly data with uploaded documents
This commit is contained in:
2025-09-28 19:09:08 +02:00
parent b00cf62d87
commit acac8695fd
73 changed files with 283380 additions and 206 deletions

View File

@@ -65,51 +65,79 @@
</div>
</div>
<!-- Stats and Chart -->
<!-- Charts Row -->
<div class="row mb-4">
<div class="col-lg-6 mb-3">
<div class="card shadow h-100">
<!-- Usage Chart -->
<div class="col-lg-4 mb-3">
<div class="card shadow">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-percentage me-2"></i>Flächennutzung (aktuelle Auswahl)
<i class="fas fa-percentage me-2"></i>Flächennutzung
</h6>
</div>
<div class="card-body">
<div class="row text-center">
<div class="row text-center mb-3">
<div class="col-4">
<div class="h4 mb-0">{{ stats.pct_wald|default:0 }}%</div>
<div class="text-muted">Wald</div>
<div class="h6 mb-0 text-success">{{ stats.pct_wald|default:0 }}%</div>
<div class="small text-muted">Wald</div>
</div>
<div class="col-4">
<div class="h4 mb-0">{{ stats.pct_acker|default:0 }}%</div>
<div class="text-muted">Acker</div>
<div class="h6 mb-0 text-info">{{ stats.pct_acker|default:0 }}%</div>
<div class="small text-muted">Acker</div>
</div>
<div class="col-4">
<div class="h4 mb-0">{{ stats.pct_gruenland|default:0 }}%</div>
<div class="text-muted">Grünland</div>
<div class="h6 mb-0 text-warning">{{ stats.pct_gruenland|default:0 }}%</div>
<div class="small text-muted">Grünland</div>
</div>
</div>
<div class="mt-3 small text-muted text-center">
Gesamt (Nutzungsarten): {{ stats.sum_total_use_qm|floatformat:0 }} qm
</div>
<div class="mt-3" style="height:200px">
<div style="height: 200px;">
<canvas id="usageChart"></canvas>
</div>
</div>
</div>
</div>
<div class="col-lg-6 mb-3">
<!-- Sizes Chart -->
<div class="col-lg-4 mb-3">
<div class="card shadow">
<div class="card-header py-3 d-flex justify-content-between align-items-center">
<div class="card-header py-3">
<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 (Top 30)
</h6>
</div>
<div class="card-body">
<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 class="text-center mb-3">
<div class="h6 mb-0">{{ stats.total_plots }}</div>
<div class="small text-muted">Grundstücke gesamt</div>
</div>
<div style="height: 200px;">
<canvas id="sizesChart"></canvas>
</div>
</div>
</div>
</div>
<!-- Verpachtung Chart -->
<div class="col-lg-4 mb-3">
<div class="card shadow">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-handshake me-2"></i>Verpachtungsstatus
</h6>
</div>
<div class="card-body">
<div class="row text-center mb-3">
<div class="col-6">
<div class="h6 mb-0 text-success">{{ stats.pct_verpachtet|default:0 }}%</div>
<div class="small text-muted">Verpachtet</div>
</div>
<div class="col-6">
<div class="h6 mb-0 text-secondary">{{ stats.pct_unveerpachtet|default:0 }}%</div>
<div class="small text-muted">Verfügbar</div>
</div>
</div>
<div style="height: 200px;">
<canvas id="verpachtungChart"></canvas>
</div>
</div>
</div>
@@ -379,8 +407,13 @@
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { position: 'bottom' },
legend: {
display: true,
position: 'bottom',
labels: { padding: 10, fontSize: 12 }
},
tooltip: {
callbacks: {
label: function(context) {
@@ -399,50 +432,107 @@
console.log('usageChart canvas not found');
}
// Simple CSS bar chart for sizes (no Chart.js)
const chartContainer = document.getElementById('sizesChart');
if (chartContainer) {
console.log('Found sizesChart container');
// Bar chart for sizes
const ctx = document.getElementById('sizesChart');
if (ctx) {
console.log('Found sizesChart canvas');
const labels = JSON.parse('{{ size_chart_labels_json|escapejs }}');
const dataVals = JSON.parse('{{ size_chart_values_json|escapejs }}');
console.log('Bar chart data:', {labels: labels.length, values: dataVals.length});
if (dataVals.length > 0) {
const maxValue = Math.max(...dataVals);
new Chart(ctx, {
type: 'bar',
data: {
labels: labels,
datasets: [{
label: 'Größe (qm)',
data: dataVals,
backgroundColor: 'rgba(0, 104, 55, 0.6)',
borderColor: '#006837',
borderWidth: 2
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
x: { ticks: { autoSkip: true, maxTicksLimit: 8 } },
y: { beginAtZero: true }
},
plugins: {
legend: { display: false },
tooltip: { callbacks: { label: function(ctx) { return ctx.parsed.y.toLocaleString() + ' qm'; } } }
}
}
});
console.log('Bar chart created successfully');
} else {
console.log('sizesChart canvas not found');
}
// Doughnut chart for verpachtung
const vctx = document.getElementById('verpachtungChart');
if (vctx) {
console.log('Found verpachtungChart canvas');
const verpachtet = {{ stats.verpachtete_plots|default:0 }};
const verfuegbar = {{ stats.unveerpachtete_plots|default:0 }};
console.log('Verpachtung data:', {verpachtet: verpachtet, verfuegbar: verfuegbar});
if (verpachtet > 0 || verfuegbar > 0) {
const verpachtungData = [];
const verpachtungLabels = [];
const verpachtungColors = [];
chartContainer.innerHTML = ''; // Clear container
if (verpachtet > 0) {
verpachtungData.push(verpachtet);
verpachtungLabels.push('Verpachtet');
verpachtungColors.push('rgba(40, 167, 69, 0.8)');
}
if (verfuegbar > 0) {
verpachtungData.push(verfuegbar);
verpachtungLabels.push('Verfügbar');
verpachtungColors.push('rgba(108, 117, 125, 0.8)');
}
// Create bars
dataVals.forEach((value, index) => {
const barHeight = (value / maxValue) * 120; // Max height 120px
const bar = document.createElement('div');
bar.style.cssText = `
background-color: rgba(0, 104, 55, 0.8);
border: 1px solid #006837;
width: ${Math.max(100 / dataVals.length - 1, 8)}%;
height: ${barHeight}px;
min-height: 2px;
display: flex;
align-items: end;
justify-content: center;
font-size: 10px;
color: white;
text-shadow: 1px 1px 1px rgba(0,0,0,0.5);
cursor: pointer;
`;
// Add tooltip on hover
bar.title = `${labels[index] || 'N/A'}: ${value.toLocaleString()} qm`;
chartContainer.appendChild(bar);
new Chart(vctx, {
type: 'doughnut',
data: {
labels: verpachtungLabels,
datasets: [{
data: verpachtungData,
backgroundColor: verpachtungColors,
borderWidth: 2,
borderColor: '#fff'
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: true,
position: 'bottom',
labels: { padding: 10, fontSize: 12 }
},
tooltip: {
callbacks: {
label: function(ctx) {
const percentage = ((ctx.parsed / (verpachtet + verfuegbar)) * 100).toFixed(1);
return ctx.label + ': ' + ctx.parsed + ' (' + percentage + '%)';
}
}
}
}
}
});
console.log('CSS bar chart created successfully');
console.log('Verpachtung chart created successfully');
} else {
chartContainer.innerHTML = '<div style="text-align: center; color: #666; padding: 20px;">Keine Daten verfügbar</div>';
console.log('No data for verpachtung chart');
}
} else {
console.log('sizesChart container not found');
console.log('verpachtungChart canvas not found');
}
} catch (e) {
console.error('Chart initialization error:', e);