update statistics

This commit is contained in:
NABU Jena 2025-07-08 07:43:16 +02:00
parent ab11148521
commit f8104b627b
8 changed files with 1756 additions and 0 deletions

97
app/statistik/README.md Normal file
View file

@ -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.

5
app/statistik/admin.py Normal file
View file

@ -0,0 +1,5 @@
from django.contrib import admin
# Da die Statistik-App keine eigenen Modelle hat,
# ist keine Admin-Registrierung erforderlich.
# Die Statistik ist über die normale Web-Oberfläche unter /statistik/ zugänglich.

7
app/statistik/apps.py Normal file
View file

@ -0,0 +1,7 @@
from django.apps import AppConfig
class StatistikConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'statistik'
verbose_name = 'Statistik'

View file

@ -0,0 +1,311 @@
{% extends "base.html" %}
{% load static %}
{% block head_title %}Statistik - Fallen Birdy{% endblock %}
{% block header %}
<style>
.stats-card {
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
border-radius: 15px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.stats-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 15px rgba(0, 0, 0, 0.2);
}
.stats-number {
font-size: 2.5rem;
font-weight: bold;
color: #2c3e50;
}
.chart-container {
position: relative;
height: 400px;
width: 100%;
margin: 20px 0;
}
.bird-bar {
display: flex;
margin-bottom: 15px;
align-items: center;
}
.bird-name {
width: 200px;
font-weight: bold;
text-align: right;
padding-right: 15px;
font-size: 0.9rem;
}
.bar-container {
flex-grow: 1;
position: relative;
height: 30px;
background-color: #f8f9fa;
border-radius: 15px;
overflow: hidden;
border: 1px solid #dee2e6;
}
.bar-rescued {
background: linear-gradient(90deg, #28a745, #20c997);
height: 100%;
float: left;
position: relative;
}
.bar-deceased {
background: linear-gradient(90deg, #dc3545, #e74c3c);
height: 100%;
float: left;
position: relative;
}
.bar-text {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: white;
font-weight: bold;
font-size: 0.8rem;
text-shadow: 1px 1px 1px rgba(0,0,0,0.5);
}
.bar-numbers {
width: 120px;
text-align: center;
font-size: 0.9rem;
padding-left: 10px;
}
.legend {
display: flex;
justify-content: center;
gap: 30px;
margin: 20px 0;
}
.legend-item {
display: flex;
align-items: center;
gap: 8px;
}
.legend-color {
width: 20px;
height: 20px;
border-radius: 4px;
}
.legend-rescued {
background: linear-gradient(90deg, #28a745, #20c997);
}
.legend-deceased {
background: linear-gradient(90deg, #dc3545, #e74c3c);
}
.section-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 15px 20px;
border-radius: 10px;
margin: 30px 0 20px 0;
}
.collapsible {
cursor: pointer;
user-select: none;
}
.collapsible:hover {
background: linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%);
}
.collapsible::after {
content: ' ▼';
float: right;
transition: transform 0.3s ease;
}
.collapsible.collapsed::after {
transform: rotate(-90deg);
}
</style>
{% endblock %}
{% block content %}
<div class="container-fluid py-4">
<div class="row">
<div class="col-12">
<h1 class="text-center mb-4">
<i class="fas fa-chart-bar"></i> Statistik Übersicht
</h1>
</div>
</div>
<!-- 1. Übersicht aktuelles Jahr -->
<div class="section-header">
<h2 class="mb-0">
<i class="fas fa-calendar-alt"></i> Übersicht {{ current_year }}
</h2>
</div>
<div class="row g-4 mb-4">
<div class="col-lg-4 col-md-6">
<div class="card stats-card h-100">
<div class="card-body text-center">
<h5 class="card-title text-primary">Aufgenommene Patienten</h5>
<div class="stats-number text-primary">{{ patients_this_year }}</div>
<p class="card-text text-muted">dieses Jahr ({{ current_year }})</p>
</div>
</div>
</div>
<div class="col-lg-4 col-md-6">
<div class="card stats-card h-100">
<div class="card-body text-center">
<h5 class="card-title text-warning">In Behandlung / Auswilderung</h5>
<div class="stats-number text-warning">{{ in_treatment_or_release }}</div>
<p class="card-text text-muted">aktuell aktive Fälle</p>
</div>
</div>
</div>
<div class="col-lg-4 col-md-6">
<div class="card stats-card h-100">
<div class="card-body text-center">
<h5 class="card-title text-success">Gerettete Tiere</h5>
<div class="stats-number text-success">{{ rescued_this_year }}</div>
<p class="card-text text-muted">ausgewildert & übermittelt</p>
</div>
</div>
</div>
</div>
<!-- 2. Übersicht alle Jahre -->
<div class="section-header">
<h2 class="mb-0">
<i class="fas fa-globe"></i> Gesamtübersicht (alle Jahre)
</h2>
</div>
<div class="row g-4 mb-4">
<div class="col-lg-6 col-md-6">
<div class="card stats-card h-100">
<div class="card-body text-center">
<h5 class="card-title text-info">Patienten insgesamt</h5>
<div class="stats-number text-info">{{ total_patients }}</div>
<p class="card-text text-muted">seit Beginn der Aufzeichnungen</p>
</div>
</div>
</div>
<div class="col-lg-6 col-md-6">
<div class="card stats-card h-100">
<div class="card-body text-center">
<h5 class="card-title text-success">Erfolgreiche Rettungen</h5>
<div class="stats-number text-success">{{ total_rescued }}</div>
<p class="card-text text-muted">
{% if total_patients > 0 %}
({{ total_rescued|floatformat:0 }}/{{ total_patients }} =
{% widthratio total_rescued total_patients 100 %}%)
{% endif %}
</p>
</div>
</div>
</div>
</div>
<!-- 3. Statistik pro Vogelart -->
<div class="section-header collapsible" data-bs-toggle="collapse" data-bs-target="#birdStatsSection" aria-expanded="false">
<h2 class="mb-0">
<i class="fas fa-dove"></i> Statistik pro Vogelart
</h2>
</div>
<div class="collapse" id="birdStatsSection">
<div class="card">
<div class="card-body">
{% if bird_stats %}
<div class="legend">
<div class="legend-item">
<div class="legend-color legend-rescued"></div>
<span>Gerettet (ausgewildert + übermittelt)</span>
</div>
<div class="legend-item">
<div class="legend-color legend-deceased"></div>
<span>Verstorben</span>
</div>
</div>
<div class="chart-container">
{% for bird in bird_stats %}
<div class="bird-bar">
<div class="bird-name" title="{{ bird.species }}">
{{ bird.name }}
{% if bird.species and bird.species != 'Unbekannt' %}
<br><small class="text-muted">{{ bird.species }}</small>
{% endif %}
</div>
<div class="bar-container">
{% if bird.rescued > 0 %}
<div class="bar-rescued" style="width: {{ bird.rescued_percentage }}%;">
{% if bird.rescued_percentage > 15 %}
<div class="bar-text">{{ bird.rescued }}</div>
{% endif %}
</div>
{% endif %}
{% if bird.deceased > 0 %}
<div class="bar-deceased" style="width: {{ bird.deceased_percentage }}%;">
{% if bird.deceased_percentage > 15 %}
<div class="bar-text">{{ bird.deceased }}</div>
{% endif %}
</div>
{% endif %}
</div>
<div class="bar-numbers">
<strong>{{ bird.total }}</strong>
<br>
<small class="text-success">{{ bird.rescued }}</small> /
<small class="text-danger">{{ bird.deceased }}</small>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="text-center text-muted py-5">
<i class="fas fa-info-circle fa-3x mb-3"></i>
<h5>Keine Daten verfügbar</h5>
<p>Es wurden noch keine Patienten erfasst.</p>
</div>
{% endif %}
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Collapsible functionality
const collapsibles = document.querySelectorAll('.collapsible');
collapsibles.forEach(function(collapsible) {
collapsible.addEventListener('click', function() {
this.classList.toggle('collapsed');
});
});
// Initially set collapsed state
const birdStatsSection = document.getElementById('birdStatsSection');
const birdStatsHeader = document.querySelector('[data-bs-target="#birdStatsSection"]');
if (birdStatsSection && birdStatsHeader) {
birdStatsHeader.classList.add('collapsed');
}
});
</script>
{% endblock %}

8
app/statistik/urls.py Normal file
View file

@ -0,0 +1,8 @@
from django.urls import path
from . import views
app_name = 'statistik'
urlpatterns = [
path('', views.StatistikView.as_view(), name='overview'),
]

85
app/statistik/views.py Normal file
View file

@ -0,0 +1,85 @@
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
class StatistikView(TemplateView):
template_name = 'statistik/overview.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# Aktuelles Jahr
current_year = timezone.now().year
# 1. Übersicht über das aktuelle Jahr
context['current_year'] = current_year
# Patienten dieses Jahr aufgenommen
patients_this_year = FallenBird.objects.filter(
date_found__year=current_year
).count()
context['patients_this_year'] = patients_this_year
# Aktuell in Behandlung oder Auswilderung
in_treatment_or_release = FallenBird.objects.filter(
date_found__year=current_year,
status__id__in=[1, 2] # In Behandlung, In Auswilderung
).count()
context['in_treatment_or_release'] = in_treatment_or_release
# Ausgewildert + Übermittelt dieses Jahr
rescued_this_year = FallenBird.objects.filter(
date_found__year=current_year,
status__id__in=[3, 4] # Ausgewildert, Übermittelt
).count()
context['rescued_this_year'] = rescued_this_year
# 2. Übersicht über alle Jahre
total_patients = FallenBird.objects.count()
context['total_patients'] = total_patients
total_rescued = FallenBird.objects.filter(
status__id__in=[3, 4] # Ausgewildert, Übermittelt
).count()
context['total_rescued'] = total_rescued
# 3. Statistik pro Vogelart
bird_stats = []
for bird in Bird.objects.all():
fallen_birds = FallenBird.objects.filter(bird=bird)
total_count = fallen_birds.count()
rescued_count = fallen_birds.filter(status__id__in=[3, 4]).count()
deceased_count = fallen_birds.filter(status__id=5).count()
if total_count > 0: # Nur Vögel anzeigen, die auch Patienten haben
bird_stats.append({
'name': bird.name,
'species': bird.species or 'Unbekannt',
'total': total_count,
'rescued': rescued_count,
'deceased': deceased_count,
'rescued_percentage': round((rescued_count / total_count) * 100, 1) if total_count > 0 else 0,
'deceased_percentage': round((deceased_count / total_count) * 100, 1) if total_count > 0 else 0
})
# Sortiere nach Gesamtanzahl (absteigend)
bird_stats.sort(key=lambda x: x['total'], reverse=True)
context['bird_stats'] = bird_stats
# Status-Namen für das Template
try:
context['status_names'] = {
1: BirdStatus.objects.get(id=1).description,
2: BirdStatus.objects.get(id=2).description,
3: BirdStatus.objects.get(id=3).description,
4: BirdStatus.objects.get(id=4).description,
5: BirdStatus.objects.get(id=5).description,
}
except BirdStatus.DoesNotExist:
context['status_names'] = {}
return context