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 +

+
+
+ + +
+

+ Übersicht {{ current_year }} +

+
+ +
+
+
+
+
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 %} +
+

+ Gesamtstatistik +

+
+ +
+
+
+
+
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 %} +
+
+ {{ group.name }} +
+ {% 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 %} +
+ + + + +
+
+
+
+
+
{{ current_year }}
+
+
+
+
+ +
+ + Wird geladen... +
+
+
+ + {% if circumstances_this_year %} +
+ {% for item in circumstances_this_year %} +
+
+
{{ item.name }}
+
{{ item.count }} ({{ item.percentage }}%)
+
+ {% endfor %} +
+ {% endif %} +
+
+
+ +
+
+
+
Alle Jahre
+
+
+
+
+ +
+ + 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 %} + +{% else %} +
+
+
+
+

+ Statistik Übersicht +

+
+
+ + +
+

+ Übersicht {{ current_year }} +

+
+ +
+ {% 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 %} +
+ + +
+

+ Gesamtübersicht (alle Jahre) +

+
+ +
+ {% 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 +

+
+
+ + +
+

+ Übersicht {{ current_year }} +

+
+ +
+
+
+
+
Patienten gesamt
+
{{ patients_this_year }}
+
+
+
+ + {% for group in year_groups %} +
+
+
+
{{ group.name }}
+
{{ group.count }}
+
+
+
+ {% endfor %} +
+ + + + +
+
+
+
+
+
{{ current_year }}
+
+
+
+
+ +
+ + Wird geladen... +
+
+
+ + {% if circumstances_this_year %} +
+ {% for item in circumstances_this_year %} +
+
+
{{ item.name }}
+
{{ item.count }} ({{ item.percentage }}%)
+
+ {% endfor %} +
+ {% endif %} +
+
+
+ +
+
+
+
Alle Jahre
+
+
+
+
+ +
+ + 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 %}