update statistics
This commit is contained in:
parent
ab11148521
commit
f8104b627b
8 changed files with 1756 additions and 0 deletions
97
app/statistik/README.md
Normal file
97
app/statistik/README.md
Normal 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
5
app/statistik/admin.py
Normal 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
7
app/statistik/apps.py
Normal 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'
|
311
app/statistik/templates/statistik/overview.html
Normal file
311
app/statistik/templates/statistik/overview.html
Normal 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
8
app/statistik/urls.py
Normal 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
85
app/statistik/views.py
Normal 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
|
Loading…
Add table
Add a link
Reference in a new issue