update statistics
This commit is contained in:
parent
ab11148521
commit
f8104b627b
8 changed files with 1756 additions and 0 deletions
388
app/statistic/templates/statistic/overview_clean.html
Normal file
388
app/statistic/templates/statistic/overview_clean.html
Normal file
|
@ -0,0 +1,388 @@
|
||||||
|
{% 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: static;
|
||||||
|
min-height: 400px;
|
||||||
|
width: 100%;
|
||||||
|
margin: 20px 0;
|
||||||
|
overflow: visible;
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 15px 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin: 30px 0 20px 0;
|
||||||
|
position: static;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsible {
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
position: static;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsible:hover {
|
||||||
|
background: linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsible::after {
|
||||||
|
content: '▼';
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsible.collapsed::after {
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pie-chart {
|
||||||
|
width: 200px;
|
||||||
|
height: 200px;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin: 0 auto;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border: 2px solid #dee2e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pie-legend {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pie-legend-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pie-legend-color {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border-radius: 3px;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pie-legend-text {
|
||||||
|
flex-grow: 1;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pie-legend-value {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #495057;
|
||||||
|
}
|
||||||
|
|
||||||
|
#birdStatsSection, #circumstancesSection {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
|
||||||
|
#birdStatsSection .card, #circumstancesSection .card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="main-container">
|
||||||
|
<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-md-6 col-lg-3">
|
||||||
|
<div class="card stats-card h-100">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<h5 class="card-title text-primary">Patienten gesamt</h5>
|
||||||
|
<div class="stats-number text-primary">{{ patients_this_year }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% for group in year_groups %}
|
||||||
|
<div class="col-md-6 col-lg-3">
|
||||||
|
<div class="card stats-card h-100">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<h5 class="card-title" style="color: {{ group.color }};">{{ group.name }}</h5>
|
||||||
|
<div class="stats-number" style="color: {{ group.color }};">{{ group.count }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 2. Fundumstände (Lazy loaded pie charts) -->
|
||||||
|
<div class="section-header collapsible collapsed" data-bs-toggle="collapse" data-bs-target="#circumstancesSection" aria-expanded="false">
|
||||||
|
<h2 class="mb-0">
|
||||||
|
<i class="fas fa-map-marker-alt"></i> Fundumstände
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="collapse" id="circumstancesSection">
|
||||||
|
<div class="row g-4 mb-4">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">{{ current_year }}</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="chart-container">
|
||||||
|
<div class="pie-chart" id="pieChartThisYear">
|
||||||
|
<!-- Lazy loading placeholder -->
|
||||||
|
<div style="display: flex; align-items: center; justify-content: center; height: 100%; color: #6c757d;">
|
||||||
|
<i class="fas fa-chart-pie fa-2x"></i>
|
||||||
|
<span style="margin-left: 10px;">Wird geladen...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if circumstances_this_year %}
|
||||||
|
<div class="pie-legend">
|
||||||
|
{% for item in circumstances_this_year %}
|
||||||
|
<div class="pie-legend-item">
|
||||||
|
<div class="pie-legend-color" style="background-color: {{ item.color }};"></div>
|
||||||
|
<div class="pie-legend-text">{{ item.name }}</div>
|
||||||
|
<div class="pie-legend-value">{{ item.count }} ({{ item.percentage }}%)</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">Alle Jahre</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="chart-container">
|
||||||
|
<div class="pie-chart" id="pieChartAllTime">
|
||||||
|
<!-- Lazy loading placeholder -->
|
||||||
|
<div style="display: flex; align-items: center; justify-content: center; height: 100%; color: #6c757d;">
|
||||||
|
<i class="fas fa-chart-pie fa-2x"></i>
|
||||||
|
<span style="margin-left: 10px;">Wird geladen...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if circumstances_all_time %}
|
||||||
|
<div class="pie-legend">
|
||||||
|
{% for item in circumstances_all_time %}
|
||||||
|
<div class="pie-legend-item">
|
||||||
|
<div class="pie-legend-color" style="background-color: {{ item.color }};"></div>
|
||||||
|
<div class="pie-legend-text">{{ item.name }}</div>
|
||||||
|
<div class="pie-legend-value">{{ item.count }} ({{ item.percentage }}%)</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Initialize Bootstrap collapse functionality
|
||||||
|
const collapsibles = document.querySelectorAll('.collapsible');
|
||||||
|
|
||||||
|
collapsibles.forEach(function(collapsible) {
|
||||||
|
collapsible.addEventListener('click', function() {
|
||||||
|
this.classList.toggle('collapsed');
|
||||||
|
});
|
||||||
|
|
||||||
|
const targetId = collapsible.getAttribute('data-bs-target');
|
||||||
|
if (targetId) {
|
||||||
|
const targetElement = document.querySelector(targetId);
|
||||||
|
if (targetElement) {
|
||||||
|
targetElement.addEventListener('shown.bs.collapse', function() {
|
||||||
|
collapsible.classList.remove('collapsed');
|
||||||
|
});
|
||||||
|
|
||||||
|
targetElement.addEventListener('hidden.bs.collapse', function() {
|
||||||
|
collapsible.classList.add('collapsed');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize collapsed state
|
||||||
|
const circumstancesSection = document.getElementById('circumstancesSection');
|
||||||
|
const circumstancesHeader = document.querySelector('[data-bs-target="#circumstancesSection"]');
|
||||||
|
if (circumstancesSection && circumstancesHeader) {
|
||||||
|
circumstancesHeader.classList.add('collapsed');
|
||||||
|
}
|
||||||
|
|
||||||
|
// SVG Pie Chart Creator
|
||||||
|
function createPieChart(elementId, data) {
|
||||||
|
const element = document.getElementById(elementId);
|
||||||
|
if (!element) {
|
||||||
|
console.error('Pie chart element not found:', elementId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data || data.length === 0) {
|
||||||
|
element.innerHTML = '<div style="display: flex; align-items: center; justify-content: center; height: 100%; color: #6c757d;">Keine Daten</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Creating SVG pie chart for:', elementId, data);
|
||||||
|
|
||||||
|
const validData = data.filter(item => item.percentage > 0).sort((a, b) => b.percentage - a.percentage);
|
||||||
|
|
||||||
|
if (validData.length === 0) {
|
||||||
|
element.innerHTML = '<div style="display: flex; align-items: center; justify-content: center; height: 100%; color: #6c757d;">Keine Daten</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const size = 200;
|
||||||
|
const radius = 80;
|
||||||
|
const centerX = size / 2;
|
||||||
|
const centerY = size / 2;
|
||||||
|
|
||||||
|
let svg = `<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" style="transform: rotate(-90deg);">`;
|
||||||
|
|
||||||
|
let currentAngle = 0;
|
||||||
|
|
||||||
|
validData.forEach((item, index) => {
|
||||||
|
const angle = (item.percentage / 100) * 360;
|
||||||
|
const startAngle = currentAngle;
|
||||||
|
const endAngle = currentAngle + angle;
|
||||||
|
|
||||||
|
const startRad = (startAngle * Math.PI) / 180;
|
||||||
|
const endRad = (endAngle * Math.PI) / 180;
|
||||||
|
|
||||||
|
const x1 = centerX + radius * Math.cos(startRad);
|
||||||
|
const y1 = centerY + radius * Math.sin(startRad);
|
||||||
|
const x2 = centerX + radius * Math.cos(endRad);
|
||||||
|
const y2 = centerY + radius * Math.sin(endRad);
|
||||||
|
|
||||||
|
const largeArcFlag = angle > 180 ? 1 : 0;
|
||||||
|
|
||||||
|
const pathData = [
|
||||||
|
`M ${centerX} ${centerY}`,
|
||||||
|
`L ${x1} ${y1}`,
|
||||||
|
`A ${radius} ${radius} 0 ${largeArcFlag} 1 ${x2} ${y2}`,
|
||||||
|
'Z'
|
||||||
|
].join(' ');
|
||||||
|
|
||||||
|
svg += `<path d="${pathData}" fill="${item.color}" stroke="white" stroke-width="2">`;
|
||||||
|
svg += `<title>${item.name}: ${item.percentage}%</title>`;
|
||||||
|
svg += `</path>`;
|
||||||
|
|
||||||
|
currentAngle = endAngle;
|
||||||
|
});
|
||||||
|
|
||||||
|
svg += '</svg>';
|
||||||
|
|
||||||
|
element.style.position = 'relative';
|
||||||
|
element.innerHTML = svg;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lazy loading function
|
||||||
|
function initializePieCharts() {
|
||||||
|
console.log('Initializing pie charts...');
|
||||||
|
|
||||||
|
// Generate pie chart data for this year
|
||||||
|
{% if circumstances_this_year %}
|
||||||
|
const thisYearData = [
|
||||||
|
{% for item in circumstances_this_year %}
|
||||||
|
{
|
||||||
|
name: '{{ item.name|escapejs }}',
|
||||||
|
percentage: {{ item.percentage }},
|
||||||
|
color: '{{ item.color }}'
|
||||||
|
}{% if not forloop.last %},{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
];
|
||||||
|
console.log('This year data:', thisYearData);
|
||||||
|
createPieChart('pieChartThisYear', thisYearData);
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
// Generate pie chart data for all time
|
||||||
|
{% if circumstances_all_time %}
|
||||||
|
const allTimeData = [
|
||||||
|
{% for item in circumstances_all_time %}
|
||||||
|
{
|
||||||
|
name: '{{ item.name|escapejs }}',
|
||||||
|
percentage: {{ item.percentage }},
|
||||||
|
color: '{{ item.color }}'
|
||||||
|
}{% if not forloop.last %},{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
];
|
||||||
|
console.log('All time data:', allTimeData);
|
||||||
|
createPieChart('pieChartAllTime', allTimeData);
|
||||||
|
{% endif %}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LAZY LOADING: Initialize pie charts only when circumstances section is expanded
|
||||||
|
if (circumstancesSection) {
|
||||||
|
let chartsInitialized = false;
|
||||||
|
|
||||||
|
circumstancesSection.addEventListener('shown.bs.collapse', function() {
|
||||||
|
console.log('Circumstances section expanded - lazy loading charts');
|
||||||
|
if (!chartsInitialized) {
|
||||||
|
setTimeout(initializePieCharts, 100);
|
||||||
|
chartsInitialized = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.error('Circumstances section not found');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
855
app/statistic/templates/statistic/overview_full.html
Normal file
855
app/statistic/templates/statistic/overview_full.html
Normal file
|
@ -0,0 +1,855 @@
|
||||||
|
{% 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: static; /* Changed from relative */
|
||||||
|
min-height: 400px; /* Changed from fixed height to min-height */
|
||||||
|
width: 100%;
|
||||||
|
margin: 20px 0;
|
||||||
|
/* Ensure proper flow */
|
||||||
|
overflow: visible;
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bird-bar {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
align-items: center;
|
||||||
|
/* Ensure proper clearing */
|
||||||
|
clear: both;
|
||||||
|
overflow: hidden; /* Contain floated elements */
|
||||||
|
}
|
||||||
|
|
||||||
|
.bird-name {
|
||||||
|
width: 200px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: right;
|
||||||
|
padding-right: 15px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
flex-shrink: 0; /* Prevent shrinking */
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar-container {
|
||||||
|
flex-grow: 1;
|
||||||
|
position: relative;
|
||||||
|
height: 30px;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-radius: 15px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
display: flex; /* Changed to flex instead of block */
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Remove the clearfix as we're using flexbox now */
|
||||||
|
.bar-container::after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar-rescued {
|
||||||
|
background: linear-gradient(90deg, #28a745, #20c997);
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
min-width: 2px; /* Mindestbreite für Sichtbarkeit */
|
||||||
|
border-radius: 15px 0 0 15px; /* Runde nur die linke Seite */
|
||||||
|
flex: none; /* Use flex instead of float */
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar-deceased {
|
||||||
|
background: linear-gradient(90deg, #dc3545, #e74c3c);
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
min-width: 2px; /* Mindestbreite für Sichtbarkeit */
|
||||||
|
flex: none; /* Use flex instead of float */
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar-in-treatment {
|
||||||
|
background: linear-gradient(90deg, #ffc107, #ffb300);
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
min-width: 2px; /* Mindestbreite für Sichtbarkeit */
|
||||||
|
flex: none; /* Use flex instead of float */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Generic bar segment class for dynamic segments */
|
||||||
|
.bar-segment {
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
min-width: 2px;
|
||||||
|
flex: none; /* Use flex instead of float */
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
/* Ensure proper stacking and flow */
|
||||||
|
position: static;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsible {
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
position: static; /* Changed from relative to static */
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
/* Ensure no overlap issues */
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsible:hover {
|
||||||
|
background: linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsible::after {
|
||||||
|
content: '▼';
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsible.collapsed::after {
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure collapse sections don't overlap */
|
||||||
|
.collapse {
|
||||||
|
position: static;
|
||||||
|
z-index: auto;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fix for proper section spacing and flow */
|
||||||
|
.collapse .card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
position: static;
|
||||||
|
z-index: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-container {
|
||||||
|
min-height: calc(100vh - 200px);
|
||||||
|
padding-bottom: 100px;
|
||||||
|
/* Ensure proper layout flow */
|
||||||
|
overflow: visible;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure proper section spacing without overlap */
|
||||||
|
.section-header + .collapse,
|
||||||
|
.section-header + .row,
|
||||||
|
.section-header + .card {
|
||||||
|
clear: both;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Bootstrap collapse override to ensure proper flow */
|
||||||
|
.collapse:not(.show) {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapse.show {
|
||||||
|
display: block !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Prevent any absolute positioning issues */
|
||||||
|
.card, .card-body {
|
||||||
|
position: static;
|
||||||
|
z-index: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pie Chart Styles */
|
||||||
|
.pie-chart {
|
||||||
|
position: relative;
|
||||||
|
width: 300px;
|
||||||
|
height: 300px;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: #f8f9fa; /* Default background */
|
||||||
|
border: 2px solid #dee2e6;
|
||||||
|
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pie-legend {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pie-legend-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pie-legend-color {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-right: 10px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pie-legend-text {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pie-legend-value {
|
||||||
|
font-weight: bold;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fallback for pie charts if conic-gradient fails */
|
||||||
|
.pie-chart-fallback {
|
||||||
|
display: none;
|
||||||
|
max-width: 300px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pie-chart-fallback .bar-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pie-chart-fallback .bar-color {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-right: 10px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pie-chart-fallback .bar-info {
|
||||||
|
flex-grow: 1;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pie-chart-fallback .bar-progress {
|
||||||
|
flex-grow: 1;
|
||||||
|
height: 8px;
|
||||||
|
background: #e9ecef;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin: 0 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pie-chart-fallback .bar-fill {
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Specific fixes for the bird statistics and circumstances sections */
|
||||||
|
#birdStatsSection, #circumstancesSection {
|
||||||
|
margin-bottom: 30px; /* Ensure space after each section */
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
|
||||||
|
#birdStatsSection .card, #circumstancesSection .card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fix any potential z-index issues */
|
||||||
|
.section-header.collapsible {
|
||||||
|
z-index: 1;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapse.show {
|
||||||
|
z-index: auto;
|
||||||
|
position: static;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure footer doesn't overlap */
|
||||||
|
footer {
|
||||||
|
clear: both;
|
||||||
|
margin-top: 50px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<!-- Check if user is authenticated - if not, show login prompt -->
|
||||||
|
{% if not user.is_authenticated %}
|
||||||
|
<div class="alert alert-warning text-center" role="alert">
|
||||||
|
<h4>Anmeldung erforderlich</h4>
|
||||||
|
<p>Bitte melden Sie sich an, um die Statistiken zu sehen.</p>
|
||||||
|
<a href="/admin/login/?next={{ request.get_full_path }}" class="btn btn-primary">Anmelden</a>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="main-container">
|
||||||
|
<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">
|
||||||
|
{% if config.show_year_total_patients %}
|
||||||
|
<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>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Dynamische Karten basierend auf konfigurierten Jahres-Gruppen -->
|
||||||
|
{% for group in year_summary %}
|
||||||
|
<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" style="color: {{ group.color }};">{{ group.name }}</h5>
|
||||||
|
<div class="stats-number" style="color: {{ group.color }};">{{ group.count }}</div>
|
||||||
|
<p class="card-text text-muted">dieses Jahr ({{ current_year }})</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</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">
|
||||||
|
{% if config.show_total_patients %}
|
||||||
|
<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-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>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Dynamische Gesamtstatistik basierend auf konfigurierten Gesamt-Gruppen -->
|
||||||
|
{% for group in total_summary %}
|
||||||
|
<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" style="color: {{ group.color }};">{{ group.name }}</h5>
|
||||||
|
<div class="stats-number" style="color: {{ group.color }};">{{ group.count }}</div>
|
||||||
|
<p class="card-text text-muted">
|
||||||
|
{% if total_patients > 0 %}
|
||||||
|
({{ group.count }}/{{ total_patients }} =
|
||||||
|
{% widthratio group.count total_patients 100 %}%)
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</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 %}
|
||||||
|
<!-- Dynamische Legende basierend auf konfigurierten Individuen-Gruppen -->
|
||||||
|
<div class="legend">
|
||||||
|
{% for group in statistic_individuals %}
|
||||||
|
<div class="legend-item">
|
||||||
|
<div class="legend-color" style="background: {{ group.color }};"></div>
|
||||||
|
<span>{{ group.name }} ({{ group.get_status_names }})</span>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</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">
|
||||||
|
<!-- Dynamische Balken basierend auf konfigurierten Gruppen -->
|
||||||
|
{% for group_data in bird.groups %}
|
||||||
|
{% if group_data.count > 0 %}
|
||||||
|
<div class="bar-segment" style="
|
||||||
|
width: {{ group_data.bar_width }}%;
|
||||||
|
background: {{ group_data.color }};
|
||||||
|
">
|
||||||
|
{% if group_data.bar_width|floatformat:0|add:0 > 15 %}
|
||||||
|
<div class="bar-text">{{ group_data.count }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<div class="bar-numbers">
|
||||||
|
<strong>{{ bird.total }}</strong>
|
||||||
|
<br>
|
||||||
|
{% for group_data in bird.groups %}
|
||||||
|
{% if group_data.count > 0 %}
|
||||||
|
<small style="color: {{ group_data.color }};">{{ group_data.count }}</small>{% if not forloop.last %} / {% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</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>
|
||||||
|
|
||||||
|
<!-- 4. Fundumstände -->
|
||||||
|
<div class="section-header collapsible" data-bs-toggle="collapse" data-bs-target="#circumstancesSection" aria-expanded="false">
|
||||||
|
<h2 class="mb-0">
|
||||||
|
<i class="fas fa-search-location"></i> Fundumstände
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="collapse" id="circumstancesSection">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<!-- Fundumstände aktuelles Jahr -->
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<h4 class="text-center mb-4">
|
||||||
|
<i class="fas fa-calendar-alt"></i> {{ current_year }}
|
||||||
|
<small class="text-muted">({{ circumstances_this_year_total }} Patienten)</small>
|
||||||
|
</h4>
|
||||||
|
{% if circumstances_this_year %}
|
||||||
|
<div class="pie-chart" id="pieChartThisYear">
|
||||||
|
<div style="display: flex; align-items: center; justify-content: center; height: 100%; color: #6c757d; text-align: center; flex-direction: column;">
|
||||||
|
<i class="fas fa-chart-pie fa-3x mb-2" style="opacity: 0.3;"></i>
|
||||||
|
<small>Diagramm wird geladen...</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Debug: Data available with {{ circumstances_this_year|length }} items -->
|
||||||
|
<div class="pie-legend">
|
||||||
|
{% for item in circumstances_this_year %}
|
||||||
|
<div class="pie-legend-item">
|
||||||
|
<div class="pie-legend-color" style="background-color: {{ item.color }};"></div>
|
||||||
|
<div class="pie-legend-text">{{ item.name }}</div>
|
||||||
|
<div class="pie-legend-value">{{ item.count }} ({{ item.percentage }}%)</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="text-center text-muted py-5">
|
||||||
|
<i class="fas fa-info-circle fa-2x mb-3"></i>
|
||||||
|
<h6>Keine Fundumstände erfasst</h6>
|
||||||
|
<p>Für dieses Jahr wurden noch keine Fundumstände dokumentiert.</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Fundumstände alle Jahre -->
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<h4 class="text-center mb-4">
|
||||||
|
<i class="fas fa-globe"></i> Alle Jahre
|
||||||
|
<small class="text-muted">({{ circumstances_all_time_total }} Patienten)</small>
|
||||||
|
</h4>
|
||||||
|
{% if circumstances_all_time %}
|
||||||
|
<div class="pie-chart" id="pieChartAllTime">
|
||||||
|
<div style="display: flex; align-items: center; justify-content: center; height: 100%; color: #6c757d; text-align: center; flex-direction: column;">
|
||||||
|
<i class="fas fa-chart-pie fa-3x mb-2" style="opacity: 0.3;"></i>
|
||||||
|
<small>Diagramm wird geladen...</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="pie-legend">
|
||||||
|
{% for item in circumstances_all_time %}
|
||||||
|
<div class="pie-legend-item">
|
||||||
|
<div class="pie-legend-color" style="background-color: {{ item.color }};"></div>
|
||||||
|
<div class="pie-legend-text">{{ item.name }}</div>
|
||||||
|
<div class="pie-legend-value">{{ item.count }} ({{ item.percentage }}%)</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="text-center text-muted py-5">
|
||||||
|
<i class="fas fa-info-circle fa-2x mb-3"></i>
|
||||||
|
<h6>Keine Fundumstände erfasst</h6>
|
||||||
|
<p>Es wurden noch keine Fundumstände dokumentiert.</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div> <!-- Close main-container -->
|
||||||
|
{% endif %} <!-- Close authentication check -->
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Initialize Bootstrap collapse functionality properly
|
||||||
|
const collapsibles = document.querySelectorAll('.collapsible');
|
||||||
|
|
||||||
|
collapsibles.forEach(function(collapsible) {
|
||||||
|
// Handle the visual state change
|
||||||
|
collapsible.addEventListener('click', function() {
|
||||||
|
this.classList.toggle('collapsed');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen to Bootstrap collapse events to ensure proper layout
|
||||||
|
const targetId = collapsible.getAttribute('data-bs-target');
|
||||||
|
if (targetId) {
|
||||||
|
const targetElement = document.querySelector(targetId);
|
||||||
|
if (targetElement) {
|
||||||
|
// When collapse is shown, remove collapsed class
|
||||||
|
targetElement.addEventListener('shown.bs.collapse', function() {
|
||||||
|
collapsible.classList.remove('collapsed');
|
||||||
|
});
|
||||||
|
|
||||||
|
// When collapse is hidden, add collapsed class
|
||||||
|
targetElement.addEventListener('hidden.bs.collapse', function() {
|
||||||
|
collapsible.classList.add('collapsed');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Force layout recalculation on collapse events
|
||||||
|
targetElement.addEventListener('show.bs.collapse', function() {
|
||||||
|
// Ensure no fixed positioning interferes
|
||||||
|
setTimeout(() => {
|
||||||
|
window.dispatchEvent(new Event('resize'));
|
||||||
|
}, 50);
|
||||||
|
});
|
||||||
|
|
||||||
|
targetElement.addEventListener('hide.bs.collapse', function() {
|
||||||
|
// Ensure layout reflow
|
||||||
|
setTimeout(() => {
|
||||||
|
window.dispatchEvent(new Event('resize'));
|
||||||
|
}, 350); // After Bootstrap animation completes
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initially set collapsed state
|
||||||
|
const birdStatsSection = document.getElementById('birdStatsSection');
|
||||||
|
const birdStatsHeader = document.querySelector('[data-bs-target="#birdStatsSection"]');
|
||||||
|
if (birdStatsSection && birdStatsHeader) {
|
||||||
|
birdStatsHeader.classList.add('collapsed');
|
||||||
|
}
|
||||||
|
|
||||||
|
const circumstancesSection = document.getElementById('circumstancesSection');
|
||||||
|
const circumstancesHeader = document.querySelector('[data-bs-target="#circumstancesSection"]');
|
||||||
|
if (circumstancesSection && circumstancesHeader) {
|
||||||
|
circumstancesHeader.classList.add('collapsed');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create pie charts using SVG (more reliable than CSS)
|
||||||
|
function createPieChart(elementId, data) {
|
||||||
|
const element = document.getElementById(elementId);
|
||||||
|
if (!element) {
|
||||||
|
console.error('Pie chart element not found:', elementId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data || data.length === 0) {
|
||||||
|
console.warn('No data for pie chart:', elementId);
|
||||||
|
element.innerHTML = '<div style="display: flex; align-items: center; justify-content: center; height: 100%; color: #6c757d;">Keine Daten</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Creating SVG pie chart for:', elementId, data);
|
||||||
|
|
||||||
|
// Filter valid data
|
||||||
|
const validData = data.filter(item => item.percentage > 0).sort((a, b) => b.percentage - a.percentage);
|
||||||
|
|
||||||
|
if (validData.length === 0) {
|
||||||
|
element.innerHTML = '<div style="display: flex; align-items: center; justify-content: center; height: 100%; color: #6c757d;">Keine Daten</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create SVG
|
||||||
|
const size = 200;
|
||||||
|
const radius = 80;
|
||||||
|
const centerX = size / 2;
|
||||||
|
const centerY = size / 2;
|
||||||
|
|
||||||
|
let svg = `<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" style="transform: rotate(-90deg);">`;
|
||||||
|
|
||||||
|
let currentAngle = 0;
|
||||||
|
|
||||||
|
validData.forEach((item, index) => {
|
||||||
|
const angle = (item.percentage / 100) * 360;
|
||||||
|
const startAngle = currentAngle;
|
||||||
|
const endAngle = currentAngle + angle;
|
||||||
|
|
||||||
|
// Convert to radians
|
||||||
|
const startRad = (startAngle * Math.PI) / 180;
|
||||||
|
const endRad = (endAngle * Math.PI) / 180;
|
||||||
|
|
||||||
|
// Calculate path
|
||||||
|
const x1 = centerX + radius * Math.cos(startRad);
|
||||||
|
const y1 = centerY + radius * Math.sin(startRad);
|
||||||
|
const x2 = centerX + radius * Math.cos(endRad);
|
||||||
|
const y2 = centerY + radius * Math.sin(endRad);
|
||||||
|
|
||||||
|
const largeArcFlag = angle > 180 ? 1 : 0;
|
||||||
|
|
||||||
|
const pathData = [
|
||||||
|
`M ${centerX} ${centerY}`,
|
||||||
|
`L ${x1} ${y1}`,
|
||||||
|
`A ${radius} ${radius} 0 ${largeArcFlag} 1 ${x2} ${y2}`,
|
||||||
|
'Z'
|
||||||
|
].join(' ');
|
||||||
|
|
||||||
|
svg += `<path d="${pathData}" fill="${item.color}" stroke="white" stroke-width="2">`;
|
||||||
|
svg += `<title>${item.name}: ${item.percentage}%</title>`;
|
||||||
|
svg += `</path>`;
|
||||||
|
|
||||||
|
currentAngle = endAngle;
|
||||||
|
});
|
||||||
|
|
||||||
|
svg += '</svg>';
|
||||||
|
|
||||||
|
// Add percentage labels
|
||||||
|
let labelsHtml = '<div style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none;">';
|
||||||
|
currentAngle = 0;
|
||||||
|
|
||||||
|
validData.forEach((item, index) => {
|
||||||
|
if (item.percentage >= 8) { // Only show labels for significant segments
|
||||||
|
const angle = (item.percentage / 100) * 360;
|
||||||
|
const midAngle = currentAngle + (angle / 2);
|
||||||
|
const midRad = (midAngle * Math.PI) / 180;
|
||||||
|
|
||||||
|
const labelRadius = radius * 0.7;
|
||||||
|
const labelX = 50 + (labelRadius / radius) * 40 * Math.cos(midRad);
|
||||||
|
const labelY = 50 + (labelRadius / radius) * 40 * Math.sin(midRad);
|
||||||
|
|
||||||
|
labelsHtml += `<div style="
|
||||||
|
position: absolute;
|
||||||
|
left: ${labelX}%;
|
||||||
|
top: ${labelY}%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
color: white;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 12px;
|
||||||
|
text-shadow: 1px 1px 2px rgba(0,0,0,0.7);
|
||||||
|
pointer-events: none;
|
||||||
|
">${Math.round(item.percentage)}%</div>`;
|
||||||
|
}
|
||||||
|
currentAngle += (item.percentage / 100) * 360;
|
||||||
|
});
|
||||||
|
|
||||||
|
labelsHtml += '</div>';
|
||||||
|
|
||||||
|
element.style.position = 'relative';
|
||||||
|
element.innerHTML = svg + labelsHtml;
|
||||||
|
}
|
||||||
|
// Function to initialize pie charts when elements are visible (LAZY LOADING)
|
||||||
|
function initializePieCharts() {
|
||||||
|
console.log('Lazy loading pie charts - starting initialization...');
|
||||||
|
|
||||||
|
// Clear existing content and show loading state
|
||||||
|
const thisYearElement = document.getElementById('pieChartThisYear');
|
||||||
|
const allTimeElement = document.getElementById('pieChartAllTime');
|
||||||
|
|
||||||
|
if (thisYearElement) {
|
||||||
|
thisYearElement.innerHTML = '<div style="display: flex; align-items: center; justify-content: center; height: 100%; color: #007bff;"><i class="fas fa-spinner fa-spin fa-2x"></i></div>';
|
||||||
|
}
|
||||||
|
if (allTimeElement) {
|
||||||
|
allTimeElement.innerHTML = '<div style="display: flex; align-items: center; justify-content: center; height: 100%; color: #007bff;"><i class="fas fa-spinner fa-spin fa-2x"></i></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Small delay to show loading state, then generate charts
|
||||||
|
setTimeout(() => {
|
||||||
|
// Generate pie chart data for this year
|
||||||
|
{% if circumstances_this_year %}
|
||||||
|
const thisYearData = [
|
||||||
|
{% for item in circumstances_this_year %}
|
||||||
|
{
|
||||||
|
name: '{{ item.name|escapejs }}',
|
||||||
|
percentage: {{ item.percentage }},
|
||||||
|
color: '{{ item.color }}'
|
||||||
|
}{% if not forloop.last %},{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
];
|
||||||
|
console.log('This year data loaded:', thisYearData);
|
||||||
|
createPieChart('pieChartThisYear', thisYearData);
|
||||||
|
{% else %}
|
||||||
|
if (thisYearElement) {
|
||||||
|
thisYearElement.innerHTML = '<div style="display: flex; align-items: center; justify-content: center; height: 100%; color: #6c757d;">Keine Daten für dieses Jahr</div>';
|
||||||
|
}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
// Generate pie chart data for all time
|
||||||
|
{% if circumstances_all_time %}
|
||||||
|
const allTimeData = [
|
||||||
|
{% for item in circumstances_all_time %}
|
||||||
|
{
|
||||||
|
name: '{{ item.name|escapejs }}',
|
||||||
|
percentage: {{ item.percentage }},
|
||||||
|
color: '{{ item.color }}'
|
||||||
|
}{% if not forloop.last %},{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
];
|
||||||
|
console.log('All time data loaded:', allTimeData);
|
||||||
|
createPieChart('pieChartAllTime', allTimeData);
|
||||||
|
{% else %}
|
||||||
|
if (allTimeElement) {
|
||||||
|
allTimeElement.innerHTML = '<div style="display: flex; align-items: center; justify-content: center; height: 100%; color: #6c757d;">Keine Daten verfügbar</div>';
|
||||||
|
}
|
||||||
|
{% endif %}
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set initial placeholder content for pie charts (before lazy loading)
|
||||||
|
function setPlaceholderContent() {
|
||||||
|
const thisYearElement = document.getElementById('pieChartThisYear');
|
||||||
|
const allTimeElement = document.getElementById('pieChartAllTime');
|
||||||
|
|
||||||
|
const placeholderHtml = '<div style="display: flex; align-items: center; justify-content: center; height: 100%; color: #6c757d;"><i class="fas fa-chart-pie fa-2x"></i><span style="margin-left: 10px;">Klicken Sie auf "Fundumstände" um die Diagramme zu laden</span></div>';
|
||||||
|
|
||||||
|
if (thisYearElement) {
|
||||||
|
thisYearElement.innerHTML = placeholderHtml;
|
||||||
|
}
|
||||||
|
if (allTimeElement) {
|
||||||
|
allTimeElement.innerHTML = placeholderHtml;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize placeholders first
|
||||||
|
setPlaceholderContent();
|
||||||
|
|
||||||
|
// Initialize pie charts ONLY when the circumstances section becomes visible (LAZY LOADING)
|
||||||
|
const circumstancesSection = document.getElementById('circumstancesSection');
|
||||||
|
if (circumstancesSection) {
|
||||||
|
let chartsInitialized = false;
|
||||||
|
|
||||||
|
// Listen for when the section is shown
|
||||||
|
circumstancesSection.addEventListener('shown.bs.collapse', function() {
|
||||||
|
console.log('Circumstances section expanded - initializing charts');
|
||||||
|
if (!chartsInitialized) {
|
||||||
|
initializePieCharts();
|
||||||
|
chartsInitialized = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if section is already visible on page load
|
||||||
|
if (circumstancesSection.classList.contains('show')) {
|
||||||
|
console.log('Circumstances section already visible - initializing charts');
|
||||||
|
setTimeout(() => {
|
||||||
|
initializePieCharts();
|
||||||
|
chartsInitialized = true;
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error('Circumstances section not found');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</div> <!-- Close main-container -->
|
||||||
|
{% endif %} <!-- Close authentication check -->
|
||||||
|
{% endblock %}
|
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