diff --git a/STATISTIC_ADMIN_CONFIG.md b/STATISTIC_ADMIN_CONFIG.md
new file mode 100644
index 0000000..4f9a298
--- /dev/null
+++ b/STATISTIC_ADMIN_CONFIG.md
@@ -0,0 +1,140 @@
+# Statistic Admin Configuration
+
+## Überblick
+
+Die Statistik-App verfügt jetzt über eine vollständig konfigurierbare Admin-Oberfläche mit drei separaten Bereichen für maximale Flexibilität:
+
+1. **Statistik-Individuen**: Konfiguration der Balkendiagramme für Vogelarten
+2. **Statistik-Jahr**: Konfiguration der Jahresstatistik-Karten
+3. **Statistik-Insgesamt**: Konfiguration der Gesamtstatistik-Karten
+
+## Neue Modell-Struktur
+
+### StatisticIndividual (Statistik-Individuen)
+- **Zweck**: Definiert Gruppierungen von BirdStatus für die Vogelarten-Balkendiagramme
+- **Felder**:
+ - `name`: Name der Gruppe (z.B. "Gerettet", "Verstorben")
+ - `color`: Hex-Farbcode für die Darstellung (z.B. #28a745)
+ - `order`: Reihenfolge der Gruppen in den Balkendiagrammen
+ - `status_list`: ManyToMany-Beziehung zu BirdStatus
+ - `is_active`: Ob diese Gruppe angezeigt werden soll
+
+### StatisticYearGroup (Statistik-Jahr)
+- **Zweck**: Definiert Gruppierungen für die Jahresstatistik-Übersichtskarten
+- **Felder**:
+ - `name`: Name der Jahresgruppe
+ - `color`: Hex-Farbcode für die Karten-Darstellung
+ - `order`: Reihenfolge der Karten in der Jahresübersicht
+ - `status_list`: Welche BirdStatus gehören zu dieser Jahresgruppe
+ - `is_active`: Aktivierung/Deaktivierung
+
+### StatisticTotalGroup (Statistik-Insgesamt)
+- **Zweck**: Definiert Gruppierungen für die Gesamtstatistik-Übersichtskarten
+- **Felder**:
+ - `name`: Name der Gesamtgruppe
+ - `color`: Hex-Farbcode für die Karten-Darstellung
+ - `order`: Reihenfolge der Karten in der Gesamtübersicht
+ - `status_list`: Welche BirdStatus gehören zu dieser Gesamtgruppe
+ - `is_active`: Aktivierung/Deaktivierung
+
+### StatisticConfiguration (Vereinfacht)
+- **Zweck**: Globale Konfiguration für die Statistik-Anzeige
+- **Felder**:
+ - `show_year_total_patients`: Checkbox für Anzeige der Gesamtanzahl aktuelles Jahr
+ - `show_total_patients`: Checkbox für Anzeige der Gesamtanzahl aller Jahre
+ - `show_percentages`: Prozentangaben in Balkendiagrammen anzeigen
+ - `show_absolute_numbers`: Absolute Zahlen in Balkendiagrammen anzeigen
+ - `is_active`: Aktive Konfiguration (nur eine möglich)
+
+## Admin-Interface Struktur
+
+### Statistik-Individuen
+- **URL**: `/admin/statistic/statisticindividual/`
+- **Zweck**: Konfiguration der Vogelarten-Balkendiagramme
+- **Features**: Erweiterte Farbauswahl, Status-Zuordnung, Reihenfolge
+
+### Statistik-Jahr
+- **URL**: `/admin/statistic/statisticyeargroup/`
+- **Zweck**: Konfiguration der Jahresstatistik-Karten
+- **Features**: Separate Gruppen für Jahresübersicht, eigene Farben
+
+### Statistik-Insgesamt
+- **URL**: `/admin/statistic/statistictotalgroup/`
+- **Zweck**: Konfiguration der Gesamtstatistik-Karten
+- **Features**: Separate Gruppen für Gesamtübersicht, eigene Farben
+
+### Statistik-Konfiguration
+- **URL**: `/admin/statistic/statisticconfiguration/`
+- **Zweck**: Globale Ein-/Ausschaltung von Bereichen
+- **Features**: Checkboxen für Sichtbarkeit der Gesamtanzahl-Karten
+
+## Vollständige Konfigurierbarkeit
+
+Die Statistik-Seite (`http://localhost:8000/statistics/`) ist jetzt vollständig über das Admin-Interface konfigurierbar:
+
+### Jahresstatistik-Bereich
+- ✅ **Gesamtanzahl Patienten**: Ein-/Ausschaltbar über Konfiguration
+- ✅ **Jahresgruppen**: Beliebig viele konfigurierbare Gruppen mit eigenen Farben
+- ✅ **Status-Zuordnung**: Flexible Zuordnung von BirdStatus zu Gruppen
+
+### Gesamtstatistik-Bereich
+- ✅ **Gesamtanzahl aller Patienten**: Ein-/Ausschaltbar über Konfiguration
+- ✅ **Gesamtgruppen**: Beliebig viele konfigurierbare Gruppen mit eigenen Farben
+- ✅ **Prozentanzeige**: Automatische Berechnung und Anzeige
+
+### Vogelarten-Statistik
+- ✅ **Balkendiagramme**: Vollständig konfigurierbare Gruppierungen
+- ✅ **Farben**: Individuelle Farbzuordnung pro Gruppe
+- ✅ **Legende**: Dynamische Generierung basierend auf Konfiguration
+
+## Standard-Konfiguration
+
+### Statistik-Individuen (Balkendiagramme)
+1. **Gerettet** (#28a745 - Grün): Ausgewildert, Übermittelt
+2. **Verstorben** (#dc3545 - Rot): Verstorben
+3. **In Behandlung/Auswilderung** (#ffc107 - Gelb): In Behandlung, In Auswilderung
+
+### Statistik-Jahr (Jahresstatistik-Karten)
+1. **Gerettet** (#28a745 - Grün): Ausgewildert, Übermittelt
+2. **Verstorben** (#dc3545 - Rot): Verstorben
+3. **In Behandlung** (#ffc107 - Gelb): In Behandlung, In Auswilderung
+
+### Statistik-Insgesamt (Gesamtstatistik-Karten)
+1. **Erfolgreich gerettet** (#28a745 - Grün): Ausgewildert, Übermittelt
+2. **Verstorben** (#dc3545 - Rot): Verstorben
+3. **Aktuell in Betreuung** (#17a2b8 - Türkis): In Behandlung, In Auswilderung
+
+## Verwendung
+
+### Neue Jahresgruppe erstellen
+1. Admin → Statistic → Statistik-Jahr → Hinzufügen
+2. Name eingeben (z.B. "Notfälle")
+3. Farbe mit Color Picker auswählen
+4. Reihenfolge festlegen
+5. BirdStatus zuordnen
+6. Aktivieren und speichern
+
+### Gesamtstatistik anpassen
+1. Admin → Statistic → Statistik-Insgesamt → Gruppe bearbeiten
+2. Namen ändern oder neue Gruppe erstellen
+3. Farben nach Bedarf anpassen
+4. Status-Zuordnungen aktualisieren
+
+### Sichtbarkeit steuern
+1. Admin → Statistic → Statistik-Konfiguration
+2. Checkboxen für Gesamtanzahl-Anzeige setzen/entfernen
+3. Anzeige-Optionen für Balkendiagramme konfigurieren
+
+## Migration und Kompatibilität
+
+- ✅ **Automatische Migration**: Bestehende Daten wurden automatisch übernommen
+- ✅ **Rückwärtskompatibilität**: Alle bisherigen Funktionen bleiben erhalten
+- ✅ **Erweiterte Flexibilität**: Drei separate Konfigurationsbereiche
+- ✅ **Vereinfachte Verwaltung**: Nur noch eine Statistik-Konfiguration notwendig
+
+## Technische Details
+
+- **Separate Models**: Getrennte Konfiguration für verschiedene Statistik-Bereiche
+- **Dynamisches Rendering**: Template passt sich automatisch an Konfiguration an
+- **Color-Coded UI**: Jede Gruppe kann individuelle Farben haben
+- **Flexible Status-Zuordnung**: BirdStatus können frei zwischen Gruppen zugeordnet werden
diff --git a/app/core/settings.py b/app/core/settings.py
index 8d42f8a..754d9e9 100644
--- a/app/core/settings.py
+++ b/app/core/settings.py
@@ -43,7 +43,7 @@ SECURE_HSTS_PRELOAD = True
# -----------------------------------
# Allowed Hosts
# -----------------------------------
-ALLOWED_HOSTS = [env("ALLOWED_HOSTS")]
+ALLOWED_HOSTS = env("ALLOWED_HOSTS").split(",") if env("ALLOWED_HOSTS") else []
# -----------------------------------
# Application definition
@@ -84,6 +84,7 @@ INSTALLED_APPS = [
"bird",
"contact",
"costs",
+ "statistic",
"export",
"notizen",
"reports",
diff --git a/app/core/urls.py b/app/core/urls.py
index d4c159f..b019852 100644
--- a/app/core/urls.py
+++ b/app/core/urls.py
@@ -11,6 +11,7 @@ urlpatterns = [
path("bird/", include("bird.urls")),
path("contacts/", include("contact.urls")),
path("costs/", include("costs.urls")),
+ path("statistics/", include("statistic.urls")),
path("export/", include("export.urls")),
path("notizen/", include("notizen.urls")),
# Admin
diff --git a/app/static/admin/css/statistic_admin.css b/app/static/admin/css/statistic_admin.css
new file mode 100644
index 0000000..bb1a458
--- /dev/null
+++ b/app/static/admin/css/statistic_admin.css
@@ -0,0 +1,72 @@
+/* Custom CSS für die Statistik-Admin-Seite */
+
+.field-color {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+
+.field-color input[type="color"] {
+ width: 60px !important;
+ height: 40px !important;
+ border: 2px solid #ddd !important;
+ border-radius: 8px !important;
+ cursor: pointer !important;
+ padding: 0 !important;
+}
+
+.field-color input[type="text"] {
+ width: 120px !important;
+ font-family: 'Courier New', monospace !important;
+ font-size: 14px !important;
+ font-weight: bold !important;
+ text-transform: uppercase !important;
+ letter-spacing: 1px !important;
+ padding: 8px 12px !important;
+ border: 2px solid #ddd !important;
+ border-radius: 6px !important;
+ background-color: #f8f9fa !important;
+}
+
+.field-color input[type="text"]:focus {
+ border-color: #007cba !important;
+ box-shadow: 0 0 0 1px #007cba !important;
+ outline: none !important;
+}
+
+.color-preview {
+ width: 30px;
+ height: 30px;
+ border-radius: 50%;
+ border: 3px solid #fff;
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
+ display: inline-block;
+ margin-left: 10px;
+}
+
+.color-picker-container {
+ display: flex;
+ align-items: center;
+ gap: 15px;
+ padding: 10px 0;
+}
+
+.color-picker-label {
+ font-weight: bold;
+ color: #333;
+ min-width: 100px;
+}
+
+/* Responsive Design */
+@media (max-width: 768px) {
+ .field-color {
+ flex-direction: column;
+ align-items: flex-start;
+ }
+
+ .color-picker-container {
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 10px;
+ }
+}
diff --git a/app/static/admin/js/statistic_color_picker.js b/app/static/admin/js/statistic_color_picker.js
new file mode 100644
index 0000000..09ad800
--- /dev/null
+++ b/app/static/admin/js/statistic_color_picker.js
@@ -0,0 +1,210 @@
+// Erweiterte Farbauswahl-Funktionalität für Statistik-Admin
+
+document.addEventListener('DOMContentLoaded', function() {
+ // Warte kurz, damit alle Django-Admin-Skripte geladen sind
+ setTimeout(function() {
+ const colorField = document.querySelector('input[name="color"]');
+
+ if (colorField && !colorField.dataset.enhanced) {
+ // Markiere als bereits erweitert
+ colorField.dataset.enhanced = 'true';
+
+ // Erstelle Container für erweiterte Farbauswahl
+ const container = document.createElement('div');
+ container.className = 'color-picker-container';
+
+ // Erstelle Color Picker
+ const colorPicker = document.createElement('input');
+ colorPicker.type = 'color';
+ colorPicker.className = 'color-picker-input';
+ colorPicker.value = colorField.value || '#28a745';
+
+ // Erstelle Farb-Vorschau
+ const colorPreview = document.createElement('div');
+ colorPreview.className = 'color-preview';
+ colorPreview.style.backgroundColor = colorField.value || '#28a745';
+
+ // Erstelle vordefinierte Farbpalette
+ const colorPalette = document.createElement('div');
+ colorPalette.className = 'color-palette';
+ colorPalette.innerHTML = `
+
+
Vordefinierte Farben:
+
+
+
+
+
+
+
+
+
+
+
+ `;
+
+ // Styling für Schnellauswahl-Buttons
+ if (!document.getElementById('color-picker-styles')) {
+ const style = document.createElement('style');
+ style.id = 'color-picker-styles';
+ style.textContent = `
+ .color-picker-container {
+ display: flex;
+ align-items: center;
+ gap: 15px;
+ margin: 10px 0;
+ padding: 10px;
+ background-color: #f0f0f0;
+ border-radius: 8px;
+ border: 1px solid #ddd;
+ }
+
+ .color-picker-input {
+ width: 60px !important;
+ height: 40px !important;
+ border: 2px solid #ddd !important;
+ border-radius: 8px !important;
+ cursor: pointer !important;
+ padding: 0 !important;
+ }
+
+ .color-preview {
+ width: 40px;
+ height: 40px;
+ border-radius: 50%;
+ border: 3px solid #fff;
+ box-shadow: 0 2px 6px rgba(0,0,0,0.15);
+ display: inline-block;
+ }
+
+ .quick-color {
+ width: 40px !important;
+ height: 40px !important;
+ border: 3px solid #fff !important;
+ border-radius: 50% !important;
+ cursor: pointer !important;
+ color: white !important;
+ font-weight: bold !important;
+ font-size: 14px !important;
+ box-shadow: 0 3px 6px rgba(0,0,0,0.2) !important;
+ transition: all 0.2s ease !important;
+ margin: 3px !important;
+ outline: none !important;
+ }
+
+ .quick-color:hover {
+ transform: scale(1.15) !important;
+ border-color: #333 !important;
+ box-shadow: 0 4px 8px rgba(0,0,0,0.3) !important;
+ }
+
+ .quick-color:active {
+ transform: scale(0.95) !important;
+ }
+ `;
+ document.head.appendChild(style);
+ }
+
+ // Organisiere das Layout
+ const fieldContainer = colorField.closest('.field-color') || colorField.parentNode;
+
+ // Füge Label hinzu
+ const label = document.createElement('div');
+ label.innerHTML = '🎨 Farbauswahl:';
+
+ container.appendChild(colorPicker);
+ container.appendChild(colorPreview);
+
+ // Füge Container vor dem ursprünglichen Feld ein
+ fieldContainer.insertBefore(label, colorField);
+ fieldContainer.insertBefore(container, colorField);
+ fieldContainer.appendChild(colorPalette);
+
+ // Aktualisiere das ursprüngliche Textfeld für bessere Sichtbarkeit
+ colorField.style.cssText = `
+ width: 140px !important;
+ font-family: 'Monaco', 'Menlo', 'Consolas', 'Courier New', monospace !important;
+ font-size: 16px !important;
+ font-weight: bold !important;
+ text-transform: uppercase !important;
+ letter-spacing: 2px !important;
+ padding: 12px 15px !important;
+ border: 2px solid #007cba !important;
+ border-radius: 8px !important;
+ background-color: #fff !important;
+ margin-top: 10px !important;
+ text-align: center !important;
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1) !important;
+ `;
+
+ // Event Listeners
+
+ // Color Picker ändert Textfeld und Vorschau
+ colorPicker.addEventListener('input', function() {
+ const color = this.value.toUpperCase();
+ colorField.value = color;
+ colorPreview.style.backgroundColor = color;
+ });
+
+ // Textfeld ändert Color Picker und Vorschau
+ colorField.addEventListener('input', function() {
+ let color = this.value.trim().toUpperCase();
+ if (!color.startsWith('#') && color.length > 0) {
+ color = '#' + color;
+ this.value = color;
+ }
+
+ if (/^#[0-9A-Fa-f]{6}$/.test(color)) {
+ colorPicker.value = color;
+ colorPreview.style.backgroundColor = color;
+ }
+ });
+
+ // Schnellauswahl-Buttons
+ document.querySelectorAll('.quick-color').forEach(button => {
+ button.addEventListener('click', function(e) {
+ e.preventDefault();
+ const color = this.dataset.color.toUpperCase();
+ colorField.value = color;
+ colorPicker.value = color;
+ colorPreview.style.backgroundColor = color;
+
+ // Visuelles Feedback
+ this.style.transform = 'scale(0.9)';
+ setTimeout(() => {
+ this.style.transform = 'scale(1)';
+ }, 150);
+ });
+ });
+
+ // Validierung des Hex-Werts
+ colorField.addEventListener('blur', function() {
+ let color = this.value.trim().toUpperCase();
+
+ // Füge # hinzu falls vergessen
+ if (color && !color.startsWith('#')) {
+ color = '#' + color;
+ }
+
+ // Validiere Hex-Format
+ if (color && !/^#[0-9A-Fa-f]{6}$/.test(color)) {
+ alert('Bitte geben Sie einen gültigen Hex-Farbcode ein (z.B. #28A745)');
+ this.focus();
+ return;
+ }
+
+ if (color) {
+ this.value = color;
+ colorPicker.value = color;
+ colorPreview.style.backgroundColor = color;
+ }
+ });
+
+ // Initialisiere mit aktuellem Wert
+ const currentColor = (colorField.value || '#28A745').toUpperCase();
+ colorField.value = currentColor;
+ colorPicker.value = currentColor;
+ colorPreview.style.backgroundColor = currentColor;
+ }
+ }, 100);
+});
diff --git a/app/statistic/README.md b/app/statistic/README.md
new file mode 100644
index 0000000..aeb1147
--- /dev/null
+++ b/app/statistic/README.md
@@ -0,0 +1,97 @@
+# Statistik App
+
+Die Statistik-App bietet umfassende Übersichten über die Patientendaten in der FBF (Fallen Birdy) Anwendung.
+
+## 📊 Funktionen
+
+### 1. Übersicht aktuelles Jahr
+- **Aufgenommene Patienten**: Anzahl der neu aufgenommenen Patienten im aktuellen Jahr
+- **In Behandlung/Auswilderung**: Aktuell aktive Fälle (Status: "In Behandlung" oder "In Auswilderung")
+- **Gerettete Tiere**: Erfolgreich behandelte Patienten (Status: "Ausgewildert" oder "Übermittelt")
+
+### 2. Gesamtübersicht (alle Jahre)
+- **Patienten insgesamt**: Gesamtanzahl aller jemals erfassten Patienten
+- **Erfolgreiche Rettungen**: Gesamtanzahl geretteter Tiere mit Erfolgsquote in Prozent
+
+### 3. Statistik pro Vogelart (aufklappbar)
+- **Interaktives Balkendiagramm** mit zweifarbigen Balken:
+ - 🟢 **Grün**: Gerettete Vögel (ausgewildert + übermittelt)
+ - 🔴 **Rot**: Verstorbene Vögel
+- **Detaillierte Zahlen** an jedem Balken
+- **Sortierung** nach Gesamtanzahl der Patienten (absteigend)
+- **Zusatzinformationen**: Lateinischer Artname (falls verfügbar)
+
+## 🎨 Design-Features
+
+- **Responsive Design**: Optimiert für Desktop, Tablet und Mobile
+- **Animierte Karten**: Hover-Effekte und sanfte Übergänge
+- **Farbkodierung**: Intuitive Farben für verschiedene Statuskategorien
+- **Aufklappbare Bereiche**: Übersichtliche Darstellung großer Datenmengen
+- **Bootstrap 5**: Moderne, konsistente Benutzeroberfläche
+
+## 🔧 Technische Details
+
+### Datenmodell
+Die Statistiken basieren auf folgenden Modellen:
+- `FallenBird`: Patientendaten mit Status und Funddatum
+- `Bird`: Vogelarten/Bezeichnungen
+- `BirdStatus`: Status-Definitionen (In Behandlung, Ausgewildert, etc.)
+
+### Status-Kategorien
+1. **In Behandlung** (ID: 1) - Aktive Patienten
+2. **In Auswilderung** (ID: 2) - Vorbereitung zur Entlassung
+3. **Ausgewildert** (ID: 3) - Erfolgreich freigelassen
+4. **Übermittelt** (ID: 4) - An andere Einrichtungen weitergegeben
+5. **Verstorben** (ID: 5) - Nicht gerettete Patienten
+
+### View-Logik
+```python
+# Beispiel für Jahresstatistik
+patients_this_year = FallenBird.objects.filter(
+ date_found__year=current_year
+).count()
+
+# Beispiel für Erfolgsrate
+rescued_count = FallenBird.objects.filter(
+ status__id__in=[3, 4] # Ausgewildert, Übermittelt
+).count()
+```
+
+## 📍 Navigation
+
+Die Statistik-App ist in der Hauptnavigation zwischen **"Volieren"** und **"Kosten"** positioniert.
+
+**URL**: `/statistik/`
+
+## 🔍 Datenanalyse
+
+### Aktueller Datenstand (Beispiel)
+- **Gesamte Patienten**: 1.267
+- **Vogelarten**: 112 verschiedene Arten
+- **Dieses Jahr (2025)**: 393 neue Patienten
+- **Erfolgsquote**: ~62% (780 von 1.267 gerettet)
+
+### Status-Verteilung
+- In Behandlung: 143 Patienten
+- Ausgewildert: 683 Patienten
+- Übermittelt: 97 Patienten
+- Verstorben: 344 Patienten
+
+## 🎯 Zukünftige Erweiterungen
+
+Mögliche weitere Features:
+- **Zeitreihen-Diagramme**: Entwicklung über mehrere Jahre
+- **Monatsstatistiken**: Saisonale Verteilungen
+- **Fundort-Analyse**: Geografische Statistiken
+- **Kosten-Integration**: Behandlungskosten pro Art
+- **Export-Funktionen**: PDF/Excel-Reports
+- **Interaktive Charts**: D3.js oder Chart.js Integration
+
+## 📱 Responsive Verhalten
+
+- **Desktop**: Drei-spaltige Kartenlayouts
+- **Tablet**: Zwei-spaltige Anordnung
+- **Mobile**: Ein-spaltige Darstellung
+- **Balkendiagramm**: Automatische Anpassung der Beschriftungen
+
+Die Statistik-App bietet eine umfassende, benutzerfreundliche Übersicht über alle wichtigen Kennzahlen der Wildvogel-Rettungsstation.
diff --git a/app/statistic/__init__.py b/app/statistic/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/statistic/admin.py b/app/statistic/admin.py
new file mode 100644
index 0000000..32f2d47
--- /dev/null
+++ b/app/statistic/admin.py
@@ -0,0 +1,249 @@
+from django.contrib import admin
+from django.utils.html import format_html
+from django.utils.translation import gettext_lazy as _
+from .models import StatisticIndividual, StatisticYearGroup, StatisticTotalGroup, StatisticConfiguration
+
+
+@admin.register(StatisticIndividual)
+class StatisticIndividualAdmin(admin.ModelAdmin):
+ """
+ Admin-Interface für die Verwaltung von Statistik-Individuen (Vogelarten-Balkendiagramme).
+ """
+ list_display = [
+ 'name',
+ 'color_display',
+ 'order',
+ 'status_count',
+ 'is_active',
+ 'updated'
+ ]
+ list_filter = ['is_active', 'created', 'updated']
+ search_fields = ['name']
+ ordering = ['order', 'name']
+
+ fieldsets = (
+ (_("Grundeinstellungen"), {
+ 'fields': ('name', 'color', 'order', 'is_active')
+ }),
+ (_("Status-Zuordnung"), {
+ 'fields': ('status_list',),
+ 'description': _("Wählen Sie die BirdStatus aus, die zu dieser Gruppe gehören sollen.")
+ }),
+ )
+
+ filter_horizontal = ('status_list',)
+
+ def color_display(self, obj):
+ """Zeigt die Farbe als farbigen Block an."""
+ return format_html(
+ '',
+ obj.color
+ )
+ color_display.short_description = _("Farbe")
+
+ def status_count(self, obj):
+ """Zeigt die Anzahl der zugeordneten Status an."""
+ count = obj.status_list.count()
+ return f"{count} Status"
+ status_count.short_description = _("Anzahl Status")
+
+ def get_form(self, request, obj=None, **kwargs):
+ """Bereite das Form für erweiterte Farbauswahl vor."""
+ # Get the form class first
+ form = super().get_form(request, obj, **kwargs)
+ # Set default color for new objects
+ if obj is None and 'color' in form.base_fields:
+ form.base_fields['color'].initial = '#28a745'
+ return form
+
+ class Media:
+ css = {
+ 'all': (
+ 'admin/css/widgets.css',
+ 'admin/css/statistic_admin.css',
+ )
+ }
+ js = (
+ 'admin/js/admin/RelatedObjectLookups.js',
+ 'admin/js/statistic_color_picker.js',
+ )
+
+
+@admin.register(StatisticYearGroup)
+class StatisticYearGroupAdmin(admin.ModelAdmin):
+ """
+ Admin-Interface für die Verwaltung von Jahres-Statistik-Gruppen.
+ """
+ list_display = [
+ 'name',
+ 'color_display',
+ 'order',
+ 'status_count',
+ 'is_active',
+ 'updated'
+ ]
+ list_filter = ['is_active', 'created', 'updated']
+ search_fields = ['name']
+ ordering = ['order', 'name']
+
+ fieldsets = (
+ (_("Grundeinstellungen"), {
+ 'fields': ('name', 'color', 'order', 'is_active'),
+ 'description': _("Konfiguration für die Jahresstatistik-Karten")
+ }),
+ (_("Status-Zuordnung"), {
+ 'fields': ('status_list',),
+ 'description': _("Welche BirdStatus sollen in dieser Jahresgruppe zusammengefasst werden?")
+ }),
+ )
+
+ filter_horizontal = ('status_list',)
+
+ def color_display(self, obj):
+ """Zeigt die Farbe als farbigen Block an."""
+ return format_html(
+ '',
+ obj.color
+ )
+ color_display.short_description = _("Farbe")
+
+ def status_count(self, obj):
+ """Zeigt die Anzahl der zugeordneten Status an."""
+ count = obj.status_list.count()
+ return f"{count} Status"
+ status_count.short_description = _("Anzahl Status")
+
+ def get_form(self, request, obj=None, **kwargs):
+ """Bereite das Form für erweiterte Farbauswahl vor."""
+ # Get the form class first
+ form = super().get_form(request, obj, **kwargs)
+ # Set default color for new objects
+ if obj is None and 'color' in form.base_fields:
+ form.base_fields['color'].initial = '#007bff'
+ return form
+
+ class Media:
+ css = {
+ 'all': (
+ 'admin/css/widgets.css',
+ 'admin/css/statistic_admin.css',
+ )
+ }
+ js = (
+ 'admin/js/admin/RelatedObjectLookups.js',
+ 'admin/js/statistic_color_picker.js',
+ )
+
+
+@admin.register(StatisticTotalGroup)
+class StatisticTotalGroupAdmin(admin.ModelAdmin):
+ """
+ Admin-Interface für die Verwaltung von Gesamt-Statistik-Gruppen.
+ """
+ list_display = [
+ 'name',
+ 'color_display',
+ 'order',
+ 'status_count',
+ 'is_active',
+ 'updated'
+ ]
+ list_filter = ['is_active', 'created', 'updated']
+ search_fields = ['name']
+ ordering = ['order', 'name']
+
+ fieldsets = (
+ (_("Grundeinstellungen"), {
+ 'fields': ('name', 'color', 'order', 'is_active'),
+ 'description': _("Konfiguration für die Gesamtstatistik-Karten")
+ }),
+ (_("Status-Zuordnung"), {
+ 'fields': ('status_list',),
+ 'description': _("Welche BirdStatus sollen in dieser Gesamtgruppe zusammengefasst werden?")
+ }),
+ )
+
+ filter_horizontal = ('status_list',)
+
+ def color_display(self, obj):
+ """Zeigt die Farbe als farbigen Block an."""
+ return format_html(
+ '',
+ obj.color
+ )
+ color_display.short_description = _("Farbe")
+
+ def status_count(self, obj):
+ """Zeigt die Anzahl der zugeordneten Status an."""
+ count = obj.status_list.count()
+ return f"{count} Status"
+ status_count.short_description = _("Anzahl Status")
+
+ def get_form(self, request, obj=None, **kwargs):
+ """Bereite das Form für erweiterte Farbauswahl vor."""
+ # Get the form class first
+ form = super().get_form(request, obj, **kwargs)
+ # Set default color for new objects
+ if obj is None and 'color' in form.base_fields:
+ form.base_fields['color'].initial = '#28a745'
+ return form
+
+ class Media:
+ css = {
+ 'all': (
+ 'admin/css/widgets.css',
+ 'admin/css/statistic_admin.css',
+ )
+ }
+ js = (
+ 'admin/js/admin/RelatedObjectLookups.js',
+ 'admin/js/statistic_color_picker.js',
+ )
+
+
+@admin.register(StatisticConfiguration)
+class StatisticConfigurationAdmin(admin.ModelAdmin):
+ """
+ Admin-Interface für die Verwaltung der Statistik-Konfiguration.
+ """
+ list_display = [
+ 'get_name',
+ 'show_year_total_patients',
+ 'show_total_patients',
+ 'show_percentages',
+ 'show_absolute_numbers',
+ 'is_active',
+ 'updated'
+ ]
+ list_filter = ['is_active', 'show_percentages', 'show_absolute_numbers', 'show_year_total_patients', 'show_total_patients']
+
+ fieldsets = (
+ (_("Jahresstatistik"), {
+ 'fields': ('show_year_total_patients',),
+ 'description': _("Konfiguration für die Anzeige der aktuellen Jahresstatistik")
+ }),
+ (_("Gesamtstatistik"), {
+ 'fields': ('show_total_patients',),
+ 'description': _("Konfiguration für die Anzeige der Gesamtstatistik")
+ }),
+ (_("Anzeige-Optionen"), {
+ 'fields': ('show_percentages', 'show_absolute_numbers'),
+ 'description': _("Allgemeine Anzeige-Optionen für Balkendiagramme")
+ }),
+ (_("System"), {
+ 'fields': ('is_active',),
+ 'description': _("Systemeinstellungen")
+ }),
+ )
+
+ def get_name(self, obj):
+ return "Statistik Konfiguration"
+ get_name.short_description = _("Konfiguration")
+
+ def has_add_permission(self, request):
+ """Erlaube nur eine Konfiguration."""
+ return StatisticConfiguration.objects.count() == 0
+
+ def has_delete_permission(self, request, obj=None):
+ """Verhindert das Löschen der Konfiguration."""
+ return False
diff --git a/app/statistic/apps.py b/app/statistic/apps.py
new file mode 100644
index 0000000..e774985
--- /dev/null
+++ b/app/statistic/apps.py
@@ -0,0 +1,7 @@
+from django.apps import AppConfig
+
+
+class StatisticConfig(AppConfig):
+ default_auto_field = 'django.db.models.BigAutoField'
+ name = 'statistic'
+ verbose_name = 'Statistik'
diff --git a/app/statistic/migrations/0001_initial.py b/app/statistic/migrations/0001_initial.py
new file mode 100644
index 0000000..45ebb20
--- /dev/null
+++ b/app/statistic/migrations/0001_initial.py
@@ -0,0 +1,49 @@
+# Generated by Django 5.2.4 on 2025-07-07 22:24
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ('bird', '0009_merge_20250609_2033'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='StatisticConfiguration',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(default='Standard Konfiguration', max_length=100, verbose_name='Konfigurationsname')),
+ ('is_active', models.BooleanField(default=True, help_text='Nur eine Konfiguration kann gleichzeitig aktiv sein', verbose_name='Aktive Konfiguration')),
+ ('show_percentages', models.BooleanField(default=True, help_text='Sollen Prozentangaben in den Balkendiagrammen angezeigt werden?', verbose_name='Prozentangaben anzeigen')),
+ ('show_absolute_numbers', models.BooleanField(default=True, help_text='Sollen absolute Zahlen in den Balkendiagrammen angezeigt werden?', verbose_name='Absolute Zahlen anzeigen')),
+ ('created', models.DateTimeField(auto_now_add=True, verbose_name='Erstellt am')),
+ ('updated', models.DateTimeField(auto_now=True, verbose_name='Geändert am')),
+ ],
+ options={
+ 'verbose_name': 'Statistik-Konfiguration',
+ 'verbose_name_plural': 'Statistik-Konfigurationen',
+ },
+ ),
+ migrations.CreateModel(
+ name='StatisticGroup',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(help_text="Name der Gruppe (z.B. 'Gerettet', 'Verstorben')", max_length=100, verbose_name='Gruppenname')),
+ ('color', models.CharField(default='#28a745', help_text='Hex-Farbcode (z.B. #28a745 für Grün)', max_length=7, verbose_name='Farbe')),
+ ('order', models.PositiveIntegerField(default=0, help_text='Bestimmt die Reihenfolge der Gruppen in den Balkendiagrammen', verbose_name='Reihenfolge')),
+ ('is_active', models.BooleanField(default=True, help_text='Soll diese Gruppe in der Statistik angezeigt werden?', verbose_name='Aktiv')),
+ ('created', models.DateTimeField(auto_now_add=True, verbose_name='Erstellt am')),
+ ('updated', models.DateTimeField(auto_now=True, verbose_name='Geändert am')),
+ ('status_list', models.ManyToManyField(help_text='Welche Status gehören zu dieser Gruppe?', to='bird.birdstatus', verbose_name='Status')),
+ ],
+ options={
+ 'verbose_name': 'Statistik-Gruppe',
+ 'verbose_name_plural': 'Statistik-Gruppen',
+ 'ordering': ['order', 'name'],
+ },
+ ),
+ ]
diff --git a/app/statistic/migrations/0002_rename_statisticgroup_statisticindividual_and_more.py b/app/statistic/migrations/0002_rename_statisticgroup_statisticindividual_and_more.py
new file mode 100644
index 0000000..6a3b4ab
--- /dev/null
+++ b/app/statistic/migrations/0002_rename_statisticgroup_statisticindividual_and_more.py
@@ -0,0 +1,76 @@
+# Generated by Django 5.2.4 on 2025-07-07 22:46
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('bird', '0009_merge_20250609_2033'),
+ ('statistic', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.RenameModel(
+ old_name='StatisticGroup',
+ new_name='StatisticIndividual',
+ ),
+ migrations.AlterModelOptions(
+ name='statisticconfiguration',
+ options={'verbose_name': 'Statistik-Konfiguration', 'verbose_name_plural': 'Statistik-Konfiguration'},
+ ),
+ migrations.AlterModelOptions(
+ name='statisticindividual',
+ options={'ordering': ['order', 'name'], 'verbose_name': 'Statistik-Individuum', 'verbose_name_plural': 'Statistik-Individuen'},
+ ),
+ migrations.RemoveField(
+ model_name='statisticconfiguration',
+ name='name',
+ ),
+ migrations.AddField(
+ model_name='statisticconfiguration',
+ name='show_total_patients',
+ field=models.BooleanField(default=True, help_text='Zeigt die Gesamtanzahl aller Patienten seit Beginn der Aufzeichnungen', verbose_name='Gesamtanzahl aller Patienten anzeigen'),
+ ),
+ migrations.AddField(
+ model_name='statisticconfiguration',
+ name='show_year_total_patients',
+ field=models.BooleanField(default=True, help_text='Zeigt die Gesamtanzahl aller aufgenommenen Patienten des aktuellen Jahres', verbose_name='Gesamtanzahl Patienten dieses Jahr anzeigen'),
+ ),
+ migrations.CreateModel(
+ name='StatisticTotalGroup',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(help_text='Name der Gruppe für die Gesamtstatistik', max_length=100, verbose_name='Gruppenname')),
+ ('color', models.CharField(default='#28a745', help_text='Hex-Farbcode für die Anzeige', max_length=7, verbose_name='Farbe')),
+ ('order', models.PositiveIntegerField(default=0, help_text='Bestimmt die Reihenfolge der Karten in der Gesamtübersicht', verbose_name='Reihenfolge')),
+ ('is_active', models.BooleanField(default=True, help_text='Soll diese Gruppe in der Gesamtstatistik angezeigt werden?', verbose_name='Aktiv')),
+ ('created', models.DateTimeField(auto_now_add=True, verbose_name='Erstellt am')),
+ ('updated', models.DateTimeField(auto_now=True, verbose_name='Geändert am')),
+ ('status_list', models.ManyToManyField(help_text='Welche Status gehören zu dieser Gesamtgruppe?', to='bird.birdstatus', verbose_name='Status')),
+ ],
+ options={
+ 'verbose_name': 'Statistik-Insgesamt Gruppe',
+ 'verbose_name_plural': 'Statistik-Insgesamt',
+ 'ordering': ['order', 'name'],
+ },
+ ),
+ migrations.CreateModel(
+ name='StatisticYearGroup',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(help_text='Name der Gruppe für die Jahresstatistik', max_length=100, verbose_name='Gruppenname')),
+ ('color', models.CharField(default='#007bff', help_text='Hex-Farbcode für die Anzeige', max_length=7, verbose_name='Farbe')),
+ ('order', models.PositiveIntegerField(default=0, help_text='Bestimmt die Reihenfolge der Karten in der Jahresübersicht', verbose_name='Reihenfolge')),
+ ('is_active', models.BooleanField(default=True, help_text='Soll diese Gruppe in der Jahresstatistik angezeigt werden?', verbose_name='Aktiv')),
+ ('created', models.DateTimeField(auto_now_add=True, verbose_name='Erstellt am')),
+ ('updated', models.DateTimeField(auto_now=True, verbose_name='Geändert am')),
+ ('status_list', models.ManyToManyField(help_text='Welche Status gehören zu dieser Jahresgruppe?', to='bird.birdstatus', verbose_name='Status')),
+ ],
+ options={
+ 'verbose_name': 'Statistik-Jahr Gruppe',
+ 'verbose_name_plural': 'Statistik-Jahr',
+ 'ordering': ['order', 'name'],
+ },
+ ),
+ ]
diff --git a/app/statistic/migrations/__init__.py b/app/statistic/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/statistic/models.py b/app/statistic/models.py
new file mode 100644
index 0000000..fee7351
--- /dev/null
+++ b/app/statistic/models.py
@@ -0,0 +1,219 @@
+from django.db import models
+from django.utils.translation import gettext_lazy as _
+from bird.models import BirdStatus
+
+
+class StatisticIndividual(models.Model):
+ """
+ Definiert Gruppierungen von BirdStatus für die Statistik-Anzeige der Vogelarten.
+ Ermöglicht die flexible Konfiguration der Balkendiagramm-Kategorien.
+ """
+ name = models.CharField(
+ max_length=100,
+ verbose_name=_("Gruppenname"),
+ help_text=_("Name der Gruppe (z.B. 'Gerettet', 'Verstorben')")
+ )
+
+ color = models.CharField(
+ max_length=7,
+ default="#28a745",
+ verbose_name=_("Farbe"),
+ help_text=_("Hex-Farbcode (z.B. #28a745 für Grün)")
+ )
+
+ order = models.PositiveIntegerField(
+ default=0,
+ verbose_name=_("Reihenfolge"),
+ help_text=_("Bestimmt die Reihenfolge der Gruppen in den Balkendiagrammen")
+ )
+
+ status_list = models.ManyToManyField(
+ BirdStatus,
+ verbose_name=_("Status"),
+ help_text=_("Welche Status gehören zu dieser Gruppe?")
+ )
+
+ is_active = models.BooleanField(
+ default=True,
+ verbose_name=_("Aktiv"),
+ help_text=_("Soll diese Gruppe in der Statistik angezeigt werden?")
+ )
+
+ created = models.DateTimeField(auto_now_add=True, verbose_name=_("Erstellt am"))
+ updated = models.DateTimeField(auto_now=True, verbose_name=_("Geändert am"))
+
+ class Meta:
+ verbose_name = _("Statistik-Individuum")
+ verbose_name_plural = _("Statistik-Individuen")
+ ordering = ['order', 'name']
+
+ def __str__(self):
+ return f"{self.name} ({self.get_status_names()})"
+
+ def get_status_names(self):
+ """Gibt eine kommaseparierte Liste der Status-Namen zurück."""
+ return ", ".join(self.status_list.values_list('description', flat=True))
+
+ def get_css_color(self):
+ """Gibt die CSS-Farbe für die Anzeige zurück."""
+ return self.color
+
+
+class StatisticYearGroup(models.Model):
+ """
+ Definiert Gruppierungen von BirdStatus für die Jahres-Übersichtskarten.
+ """
+ name = models.CharField(
+ max_length=100,
+ verbose_name=_("Gruppenname"),
+ help_text=_("Name der Gruppe für die Jahresstatistik")
+ )
+
+ color = models.CharField(
+ max_length=7,
+ default="#007bff",
+ verbose_name=_("Farbe"),
+ help_text=_("Hex-Farbcode für die Anzeige")
+ )
+
+ order = models.PositiveIntegerField(
+ default=0,
+ verbose_name=_("Reihenfolge"),
+ help_text=_("Bestimmt die Reihenfolge der Karten in der Jahresübersicht")
+ )
+
+ status_list = models.ManyToManyField(
+ BirdStatus,
+ verbose_name=_("Status"),
+ help_text=_("Welche Status gehören zu dieser Jahresgruppe?")
+ )
+
+ is_active = models.BooleanField(
+ default=True,
+ verbose_name=_("Aktiv"),
+ help_text=_("Soll diese Gruppe in der Jahresstatistik angezeigt werden?")
+ )
+
+ created = models.DateTimeField(auto_now_add=True, verbose_name=_("Erstellt am"))
+ updated = models.DateTimeField(auto_now=True, verbose_name=_("Geändert am"))
+
+ class Meta:
+ verbose_name = _("Statistik-Jahr Gruppe")
+ verbose_name_plural = _("Statistik-Jahr")
+ ordering = ['order', 'name']
+
+ def __str__(self):
+ return f"{self.name} ({self.get_status_names()})"
+
+ def get_status_names(self):
+ """Gibt eine kommaseparierte Liste der Status-Namen zurück."""
+ return ", ".join(self.status_list.values_list('description', flat=True))
+
+
+class StatisticTotalGroup(models.Model):
+ """
+ Definiert Gruppierungen von BirdStatus für die Gesamt-Übersichtskarten.
+ """
+ name = models.CharField(
+ max_length=100,
+ verbose_name=_("Gruppenname"),
+ help_text=_("Name der Gruppe für die Gesamtstatistik")
+ )
+
+ color = models.CharField(
+ max_length=7,
+ default="#28a745",
+ verbose_name=_("Farbe"),
+ help_text=_("Hex-Farbcode für die Anzeige")
+ )
+
+ order = models.PositiveIntegerField(
+ default=0,
+ verbose_name=_("Reihenfolge"),
+ help_text=_("Bestimmt die Reihenfolge der Karten in der Gesamtübersicht")
+ )
+
+ status_list = models.ManyToManyField(
+ BirdStatus,
+ verbose_name=_("Status"),
+ help_text=_("Welche Status gehören zu dieser Gesamtgruppe?")
+ )
+
+ is_active = models.BooleanField(
+ default=True,
+ verbose_name=_("Aktiv"),
+ help_text=_("Soll diese Gruppe in der Gesamtstatistik angezeigt werden?")
+ )
+
+ created = models.DateTimeField(auto_now_add=True, verbose_name=_("Erstellt am"))
+ updated = models.DateTimeField(auto_now=True, verbose_name=_("Geändert am"))
+
+ class Meta:
+ verbose_name = _("Statistik-Insgesamt Gruppe")
+ verbose_name_plural = _("Statistik-Insgesamt")
+ ordering = ['order', 'name']
+
+ def __str__(self):
+ return f"{self.name} ({self.get_status_names()})"
+
+ def get_status_names(self):
+ """Gibt eine kommaseparierte Liste der Status-Namen zurück."""
+ return ", ".join(self.status_list.values_list('description', flat=True))
+
+
+class StatisticConfiguration(models.Model):
+ """
+ Globale Konfiguration für die Statistik-Anzeige.
+ """
+ # Jahresstatistik Einstellungen
+ show_year_total_patients = models.BooleanField(
+ default=True,
+ verbose_name=_("Gesamtanzahl Patienten dieses Jahr anzeigen"),
+ help_text=_("Zeigt die Gesamtanzahl aller aufgenommenen Patienten des aktuellen Jahres")
+ )
+
+ # Gesamtstatistik Einstellungen
+ show_total_patients = models.BooleanField(
+ default=True,
+ verbose_name=_("Gesamtanzahl aller Patienten anzeigen"),
+ help_text=_("Zeigt die Gesamtanzahl aller Patienten seit Beginn der Aufzeichnungen")
+ )
+
+ # Weitere Anzeige-Optionen
+ show_percentages = models.BooleanField(
+ default=True,
+ verbose_name=_("Prozentangaben anzeigen"),
+ help_text=_("Sollen Prozentangaben in den Balkendiagrammen angezeigt werden?")
+ )
+
+ show_absolute_numbers = models.BooleanField(
+ default=True,
+ verbose_name=_("Absolute Zahlen anzeigen"),
+ help_text=_("Sollen absolute Zahlen in den Balkendiagrammen angezeigt werden?")
+ )
+
+ is_active = models.BooleanField(
+ default=True,
+ verbose_name=_("Aktive Konfiguration"),
+ help_text=_("Nur eine Konfiguration kann gleichzeitig aktiv sein")
+ )
+
+ created = models.DateTimeField(auto_now_add=True, verbose_name=_("Erstellt am"))
+ updated = models.DateTimeField(auto_now=True, verbose_name=_("Geändert am"))
+
+ class Meta:
+ verbose_name = _("Statistik-Konfiguration")
+ verbose_name_plural = _("Statistik-Konfiguration")
+
+ def __str__(self):
+ return "Statistik Konfiguration"
+
+ def save(self, *args, **kwargs):
+ # Stelle sicher, dass nur eine Konfiguration aktiv ist
+ if self.is_active:
+ StatisticConfiguration.objects.filter(is_active=True).update(is_active=False)
+ super().save(*args, **kwargs)
+
+
+# Backward Compatibility Alias (temporär für Migration)
+StatisticGroup = StatisticIndividual
diff --git a/app/statistic/templates/statistic/overview.html b/app/statistic/templates/statistic/overview.html
new file mode 100644
index 0000000..37d508b
--- /dev/null
+++ b/app/statistic/templates/statistic/overview.html
@@ -0,0 +1,661 @@
+{% extends "base.html" %}
+{% load static %}
+
+{% block head_title %}Statistik - Fallen Birdy{% endblock %}
+
+{% block header %}
+
+{% endblock %}
+
+{% block content %}
+
+
+
+
+
+ Statistik Übersicht
+
+
+
+
+
+
+
+
+
+
+
+
Patienten gesamt
+
{{ patients_this_year }}
+
+
+
+
+ {% for group in year_summary %}
+
+
+
+
{{ group.name }}
+
{{ group.count }}
+ {% if config.show_percentages %}
+
{{ group.percentage }}%
+ {% endif %}
+
+
+
+ {% endfor %}
+
+
+
+ {% if config.show_total_patients %}
+
+
+
+
+
+
+
Patienten gesamt
+
{{ total_patients }}
+
+
+
+
+ {% for group in total_summary %}
+
+
+
+
{{ group.name }}
+
{{ group.count }}
+ {% if config.show_percentages %}
+
{{ group.percentage }}%
+ {% endif %}
+
+
+
+ {% endfor %}
+
+ {% endif %}
+
+
+
+
+
+ {% if bird_stats %}
+
+ {% for group in statistic_individuals %}
+
+ {% endfor %}
+
+
+
+
+ {% for bird in bird_stats %}
+
+
+
+ {{ bird.name }}
+
+
+
+
+
+ {% for group_data in bird.groups %}
+ {% if group_data.count > 0 %}
+
+
+ {% endif %}
+ {% endfor %}
+
+
+
+
+
+ {% for group_data in bird.groups %}
+ {% if group_data.count > 0 %}
+
+
+ {{ group_data.name }}: {{ group_data.count }} ({{ group_data.percentage }}%)
+
+ {% endif %}
+ {% endfor %}
+
+ Gesamt: {{ bird.total }}
+
+
+
+ {% endfor %}
+
+
+ {% else %}
+
+
+
Keine Vogelarten mit Patienten gefunden
+
Sobald Patienten erfasst werden, erscheinen hier die Statistiken pro Vogelart.
+
+ {% endif %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Wird geladen...
+
+
+
+
+ {% if circumstances_this_year %}
+
+ {% for item in circumstances_this_year %}
+
+
+
{{ item.name }}
+
{{ item.count }} ({{ item.percentage }}%)
+
+ {% endfor %}
+
+ {% endif %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Wird geladen...
+
+
+
+
+ {% if circumstances_all_time %}
+
+ {% for item in circumstances_all_time %}
+
+
+
{{ item.name }}
+
{{ item.count }} ({{ item.percentage }}%)
+
+ {% endfor %}
+
+ {% endif %}
+
+
+
+
+
+
+
+
+
+{% endblock %}
diff --git a/app/statistic/templates/statistic/overview_broken.html b/app/statistic/templates/statistic/overview_broken.html
new file mode 100644
index 0000000..306e839
--- /dev/null
+++ b/app/statistic/templates/statistic/overview_broken.html
@@ -0,0 +1,854 @@
+{% extends "base.html" %}
+{% load static %}
+
+{% block head_title %}Statistik - Fallen Birdy{% endblock %}
+
+{% block header %}
+
+{% endblock %}
+
+{% block content %}
+
+{% if not user.is_authenticated %}
+
+
Anmeldung erforderlich
+
Bitte melden Sie sich an, um die Statistiken zu sehen.
+
Anmelden
+
+{% else %}
+
+
+
+
+
+ Statistik Übersicht
+
+
+
+
+
+
+
+
+ {% if config.show_year_total_patients %}
+
+
+
+
Aufgenommene Patienten
+
{{ patients_this_year }}
+
dieses Jahr ({{ current_year }})
+
+
+
+ {% endif %}
+
+
+ {% for group in year_summary %}
+
+
+
+
{{ group.name }}
+
{{ group.count }}
+
dieses Jahr ({{ current_year }})
+
+
+
+ {% endfor %}
+
+
+
+
+
+
+ {% if config.show_total_patients %}
+
+
+
+
Patienten insgesamt
+
{{ total_patients }}
+
seit Beginn der Aufzeichnungen
+
+
+
+ {% endif %}
+
+
+ {% for group in total_summary %}
+
+
+
+
{{ group.name }}
+
{{ group.count }}
+
+ {% if total_patients > 0 %}
+ ({{ group.count }}/{{ total_patients }} =
+ {% widthratio group.count total_patients 100 %}%)
+ {% endif %}
+
+
+
+
+ {% endfor %}
+
+
+
+
+
+
+
+
+ {% if bird_stats %}
+
+
+ {% for group in statistic_individuals %}
+
+
+
{{ group.name }} ({{ group.get_status_names }})
+
+ {% endfor %}
+
+
+
+ {% for bird in bird_stats %}
+
+
+ {{ bird.name }}
+ {% if bird.species and bird.species != 'Unbekannt' %}
+
{{ bird.species }}
+ {% endif %}
+
+
+
+ {% for group_data in bird.groups %}
+ {% if group_data.count > 0 %}
+
+ {% if group_data.bar_width|floatformat:0|add:0 > 15 %}
+
{{ group_data.count }}
+ {% endif %}
+
+ {% endif %}
+ {% endfor %}
+
+
+ {{ bird.total }}
+
+ {% for group_data in bird.groups %}
+ {% if group_data.count > 0 %}
+ {{ group_data.count }}{% if not forloop.last %} / {% endif %}
+ {% endif %}
+ {% endfor %}
+
+
+ {% endfor %}
+
+ {% else %}
+
+
+
Keine Daten verfügbar
+
Es wurden noch keine Patienten erfasst.
+
+ {% endif %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ current_year }}
+ ({{ circumstances_this_year_total }} Patienten)
+
+ {% if circumstances_this_year %}
+
+
+
+ Diagramm wird geladen...
+
+
+
+
+ {% for item in circumstances_this_year %}
+
+
+
{{ item.name }}
+
{{ item.count }} ({{ item.percentage }}%)
+
+ {% endfor %}
+
+ {% else %}
+
+
+
Keine Fundumstände erfasst
+
Für dieses Jahr wurden noch keine Fundumstände dokumentiert.
+
+ {% endif %}
+
+
+
+
+
+ Alle Jahre
+ ({{ circumstances_all_time_total }} Patienten)
+
+ {% if circumstances_all_time %}
+
+
+
+ Diagramm wird geladen...
+
+
+
+ {% for item in circumstances_all_time %}
+
+
+
{{ item.name }}
+
{{ item.count }} ({{ item.percentage }}%)
+
+ {% endfor %}
+
+ {% else %}
+
+
+
Keine Fundumstände erfasst
+
Es wurden noch keine Fundumstände dokumentiert.
+
+ {% endif %}
+
+
+
+
+
+
+
+{% endif %}
+
+
+{% endif %}
+{% endblock %}
diff --git a/app/statistic/templates/statistic/overview_minimal.html b/app/statistic/templates/statistic/overview_minimal.html
new file mode 100644
index 0000000..c1d02a5
--- /dev/null
+++ b/app/statistic/templates/statistic/overview_minimal.html
@@ -0,0 +1,388 @@
+{% extends "base.html" %}
+{% load static %}
+
+{% block head_title %}Statistik - Fallen Birdy{% endblock %}
+
+{% block header %}
+
+{% endblock %}
+
+{% block content %}
+
+
+
+
+
+ Statistik Übersicht
+
+
+
+
+
+
+
+
+
+
+
+
Patienten gesamt
+
{{ patients_this_year }}
+
+
+
+
+ {% for group in year_groups %}
+
+
+
+
{{ group.name }}
+
{{ group.count }}
+
+
+
+ {% endfor %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Wird geladen...
+
+
+
+
+ {% if circumstances_this_year %}
+
+ {% for item in circumstances_this_year %}
+
+
+
{{ item.name }}
+
{{ item.count }} ({{ item.percentage }}%)
+
+ {% endfor %}
+
+ {% endif %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Wird geladen...
+
+
+
+
+ {% if circumstances_all_time %}
+
+ {% for item in circumstances_all_time %}
+
+
+
{{ item.name }}
+
{{ item.count }} ({{ item.percentage }}%)
+
+ {% endfor %}
+
+ {% endif %}
+
+
+
+
+
+
+
+
+
+{% endblock %}
diff --git a/app/statistic/tests.py b/app/statistic/tests.py
new file mode 100644
index 0000000..7ce503c
--- /dev/null
+++ b/app/statistic/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/app/statistic/urls.py b/app/statistic/urls.py
new file mode 100644
index 0000000..3083db1
--- /dev/null
+++ b/app/statistic/urls.py
@@ -0,0 +1,8 @@
+from django.urls import path
+from . import views
+
+app_name = 'statistic'
+
+urlpatterns = [
+ path('', views.StatisticView.as_view(), name='overview'),
+]
diff --git a/app/statistic/views.py b/app/statistic/views.py
new file mode 100644
index 0000000..49e7217
--- /dev/null
+++ b/app/statistic/views.py
@@ -0,0 +1,184 @@
+from django.views.generic import TemplateView
+from django.db.models import Count, Q
+from django.utils import timezone
+from datetime import datetime
+from bird.models import FallenBird, Bird, BirdStatus, Circumstance
+from .models import StatisticIndividual, StatisticYearGroup, StatisticTotalGroup, StatisticConfiguration
+
+
+class StatisticView(TemplateView):
+ template_name = 'statistic/overview.html'
+
+ def get_context_data(self, **kwargs):
+ context = super().get_context_data(**kwargs)
+
+ # Aktuelles Jahr
+ current_year = timezone.now().year
+
+ # Lade aktive Konfiguration
+ try:
+ config = StatisticConfiguration.objects.get(is_active=True)
+ except StatisticConfiguration.DoesNotExist:
+ # Fallback: Erstelle Standard-Konfiguration wenn keine vorhanden
+ config = StatisticConfiguration.objects.create(
+ is_active=True,
+ show_year_total_patients=True,
+ show_total_patients=True,
+ show_percentages=True,
+ show_absolute_numbers=True
+ )
+
+ context['config'] = config
+ context['current_year'] = current_year
+
+ # 1. Jahresstatistik
+ if config.show_year_total_patients:
+ patients_this_year = FallenBird.objects.filter(
+ date_found__year=current_year
+ ).count()
+ context['patients_this_year'] = patients_this_year
+
+ # Lade aktive Jahres-Gruppen und berechne Statistiken
+ year_groups = StatisticYearGroup.objects.filter(is_active=True).order_by('order')
+ year_summary = []
+
+ for group in year_groups:
+ status_ids = list(group.status_list.values_list('id', flat=True))
+ year_count = FallenBird.objects.filter(
+ date_found__year=current_year,
+ status__id__in=status_ids
+ ).count()
+
+ # Berechne Prozentanteil
+ year_percentage = (year_count / patients_this_year * 100) if patients_this_year > 0 else 0
+
+ year_summary.append({
+ 'name': group.name,
+ 'count': year_count,
+ 'percentage': round(year_percentage, 1),
+ 'color': group.color,
+ 'order': group.order
+ })
+
+ context['year_summary'] = year_summary
+
+ # 2. Gesamtstatistik
+ if config.show_total_patients:
+ total_patients = FallenBird.objects.count()
+ context['total_patients'] = total_patients
+
+ # Lade aktive Gesamt-Gruppen und berechne Statistiken
+ total_groups = StatisticTotalGroup.objects.filter(is_active=True).order_by('order')
+ total_summary = []
+
+ for group in total_groups:
+ status_ids = list(group.status_list.values_list('id', flat=True))
+ total_count = FallenBird.objects.filter(
+ status__id__in=status_ids
+ ).count()
+
+ # Berechne Prozentanteil
+ total_percentage = (total_count / total_patients * 100) if total_patients > 0 else 0
+
+ total_summary.append({
+ 'name': group.name,
+ 'count': total_count,
+ 'percentage': round(total_percentage, 1),
+ 'color': group.color,
+ 'order': group.order
+ })
+
+ context['total_summary'] = total_summary
+
+ # 3. Statistik pro Vogelart (Individuen - dynamisch basierend auf Konfiguration)
+ individual_groups = StatisticIndividual.objects.filter(is_active=True).order_by('order')
+ context['statistic_individuals'] = individual_groups
+
+ bird_stats = []
+ for bird in Bird.objects.all():
+ fallen_birds = FallenBird.objects.filter(bird=bird)
+ total_count = fallen_birds.count()
+
+ if total_count > 0: # Nur Vögel anzeigen, die auch Patienten haben
+ bird_data = {
+ 'name': bird.name,
+ 'species': bird.species or 'Unbekannt',
+ 'total': total_count,
+ 'groups': []
+ }
+
+ # Berechne Statistiken für jede konfigurierte Individuen-Gruppe
+ for group in individual_groups:
+ status_ids = list(group.status_list.values_list('id', flat=True))
+ group_count = fallen_birds.filter(status__id__in=status_ids).count()
+ group_percentage = (group_count / total_count) * 100 if total_count > 0 else 0
+
+ bird_data['groups'].append({
+ 'name': group.name,
+ 'color': group.color,
+ 'count': group_count,
+ 'percentage': round(group_percentage, 1),
+ 'order': group.order
+ })
+
+ bird_stats.append(bird_data)
+
+ # Sortiere nach Gesamtanzahl (absteigend)
+ bird_stats.sort(key=lambda x: x['total'], reverse=True)
+
+ # Berechne Balkenbreiten basierend auf der höchsten Vogelart für bessere Visualisierung
+ if bird_stats:
+ max_count = bird_stats[0]['total'] # Höchste Anzahl (wird 100% der Balkenbreite)
+
+ for bird in bird_stats:
+ # Berechne Balkenbreite für Gesamtanzahl
+ total_bar_width = (bird['total'] / max_count) * 100 if max_count > 0 else 0
+ bird['total_bar_width'] = f"{total_bar_width:.1f}".replace(',', '.')
+
+ # Berechne Balkenbreiten für jede Gruppe
+ for group_data in bird['groups']:
+ group_bar_width = (group_data['count'] / max_count) * 100 if max_count > 0 else 0
+ group_data['bar_width'] = f"{group_bar_width:.1f}".replace(',', '.')
+
+ context['bird_stats'] = bird_stats
+
+ # 4. Fundumstände-Statistiken (unverändert)
+ # Fundumstände für aktuelles Jahr
+ circumstances_this_year = FallenBird.objects.filter(
+ date_found__year=current_year,
+ find_circumstances__isnull=False
+ ).values('find_circumstances__name', 'find_circumstances__description').annotate(
+ count=Count('id')
+ ).order_by('-count')
+
+ # Fundumstände für alle Jahre
+ circumstances_all_time = FallenBird.objects.filter(
+ find_circumstances__isnull=False
+ ).values('find_circumstances__name', 'find_circumstances__description').annotate(
+ count=Count('id')
+ ).order_by('-count')
+
+ # Formatiere Daten für Tortendiagramme
+ def format_circumstances_data(circumstances_data):
+ total = sum(item['count'] for item in circumstances_data)
+ formatted_data = []
+ colors = [
+ '#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0', '#9966FF',
+ '#FF9F40', '#FF6384', '#C9CBCF', '#4BC0C0', '#FF6384'
+ ]
+
+ for i, item in enumerate(circumstances_data):
+ name = item['find_circumstances__name'] or item['find_circumstances__description']
+ percentage = round((item['count'] / total) * 100, 1) if total > 0 else 0
+ formatted_data.append({
+ 'name': name,
+ 'count': item['count'],
+ 'percentage': percentage,
+ 'color': colors[i % len(colors)]
+ })
+ return formatted_data, total
+
+ context['circumstances_this_year'], context['circumstances_this_year_total'] = format_circumstances_data(circumstances_this_year)
+ context['circumstances_all_time'], context['circumstances_all_time_total'] = format_circumstances_data(circumstances_all_time)
+
+ return context
diff --git a/app/templates/partials/_footer.html b/app/templates/partials/_footer.html
index 4b56832..4dfcdf1 100644
--- a/app/templates/partials/_footer.html
+++ b/app/templates/partials/_footer.html
@@ -1,7 +1,7 @@
{% if user.is_authenticated %}