Files
stiftung-management-system/app/stiftung/views/dashboard.py
SysAdmin Agent bf47ba11c9 Phase 1: Sidebar-Navigation, Dashboard-Cockpit & HTMX-Integration
- New sidebar layout (6 sections: Dashboard, Personen, Land, Finanzen, Dokumente, System)
- Collapsible sidebar with localStorage persistence
- Top bar with user dropdown and breadcrumbs
- Dashboard cockpit with live KPI cards (Destinataere, Foerderungen, Zahlungen, Laendereien)
- Action items: overdue Nachweise, pending payments, upcoming events, new emails, expiring leases
- Quick actions panel and recent audit log
- HTMX (2.0.4) and Alpine.js (3.14.8) integration via CDN
- django-htmx middleware and CSRF token setup
- Fix IMAP_PORT empty string handling in settings

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 10:22:42 +00:00

161 lines
6.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# views/dashboard.py
# Vision 2026 Phase 1: Dashboard Cockpit
import csv
import io
import json
import os
import time
from datetime import datetime, timedelta, date
from decimal import Decimal
import qrcode
import qrcode.image.svg
import requests
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.core.paginator import Paginator
from django.db.models import (Avg, Count, DecimalField, F, IntegerField, Q,
Sum, Value)
from django.db.models.functions import Cast, Coalesce, NullIf, Replace
from django.http import HttpResponse, JsonResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.utils import timezone
from django.views.decorators.csrf import csrf_exempt
from django_otp.decorators import otp_required
from django_otp.plugins.otp_totp.models import TOTPDevice
from django_otp.plugins.otp_static.models import StaticDevice, StaticToken
from django_otp.util import random_hex
from rest_framework.decorators import api_view
from rest_framework.response import Response
from stiftung.models import (AppConfiguration, AuditLog, BackupJob, BankTransaction,
BriefVorlage, CSVImport, Destinataer,
DestinataerEmailEingang, DestinataerNotiz,
DestinataerUnterstuetzung,
DokumentLink, Foerderung, GeschichteBild, GeschichteSeite,
Land, LandAbrechnung, LandVerpachtung, Paechter, Person,
Rentmeister, StiftungsKalenderEintrag, StiftungsKonto,
UnterstuetzungWiederkehrend, Veranstaltung,
Veranstaltungsteilnehmer, Verwaltungskosten,
VierteljahresNachweis)
from stiftung.forms import (
DestinataerForm, DestinataerUnterstuetzungForm, DestinataerNotizForm,
FoerderungForm, GeschichteBildForm, GeschichteSeiteForm,
LandForm, LandVerpachtungForm, LandAbrechnungForm,
PaechterForm, DokumentLinkForm,
RentmeisterForm, StiftungsKontoForm, VerwaltungskostenForm,
BankTransactionForm, BankImportForm,
UnterstuetzungForm, UnterstuetzungWiederkehrendForm,
UnterstuetzungMarkAsPaidForm, VierteljahresNachweisForm,
UserCreationForm, UserUpdateForm, PasswordChangeForm, UserPermissionForm,
TwoFactorSetupForm, TwoFactorVerifyForm, TwoFactorDisableForm,
BackupTokenRegenerateForm, PersonForm,
VeranstaltungForm, VeranstaltungsteilnehmerForm,
)
@login_required
def home(request):
"""Vision 2026 Dashboard Cockpit"""
from stiftung.services.calendar_service import StiftungsKalenderService
today = timezone.now().date()
current_quarter = (today.month - 1) // 3 + 1
current_year = today.year
# ── Calendar events ──
calendar_service = StiftungsKalenderService()
end_date = today + timedelta(days=14)
all_events = calendar_service.get_all_events(today, end_date)
upcoming_events = [e for e in all_events if not getattr(e, 'overdue', False)]
overdue_events = [e for e in all_events if getattr(e, 'overdue', False)]
# ── Stats ──
destinataer_count = Destinataer.objects.count()
paechter_count = Paechter.objects.count()
land_count = Land.objects.count()
# Active Foerderungen (not rejected/cancelled, current or future year)
foerderung_active = Foerderung.objects.filter(
jahr__gte=current_year,
status__in=['beantragt', 'genehmigt'],
).count()
# ── Overdue Nachweise (current quarter) ──
overdue_nachweise = VierteljahresNachweis.objects.filter(
quartal=current_quarter,
jahr=current_year,
status__in=['offen', 'teilweise', 'nachbesserung'],
).select_related('destinataer').order_by('jahr', 'quartal')[:10]
# ── Pending payments (not yet paid out) ──
pending_payments = DestinataerUnterstuetzung.objects.filter(
status__in=['geplant', 'faellig', 'in_bearbeitung'],
).select_related('destinataer').order_by('faellig_am')[:10]
pending_payment_total = DestinataerUnterstuetzung.objects.filter(
status__in=['geplant', 'faellig', 'in_bearbeitung'],
).aggregate(total=Coalesce(Sum('betrag'), Decimal('0')))['total']
# ── New emails ──
new_emails = DestinataerEmailEingang.objects.filter(
status='neu',
).order_by('-eingangsdatum')[:5]
new_email_count = DestinataerEmailEingang.objects.filter(status='neu').count()
# ── Expiring leases (next 90 days) ──
lease_cutoff = today + timedelta(days=90)
expiring_leases = LandVerpachtung.objects.filter(
pachtende__lte=lease_cutoff,
pachtende__gte=today,
).select_related('paechter', 'land').order_by('pachtende')[:5]
# ── Recent audit log ──
recent_audit = AuditLog.objects.order_by('-timestamp')[:5]
context = {
"title": "Dashboard",
# Stats
"destinataer_count": destinataer_count,
"paechter_count": paechter_count,
"land_count": land_count,
"foerderung_active": foerderung_active,
# Calendar
"upcoming_events": upcoming_events[:5],
"overdue_events": overdue_events[:3],
"today": today,
# Action items
"overdue_nachweise": overdue_nachweise,
"pending_payments": pending_payments,
"pending_payment_total": pending_payment_total,
"new_emails": new_emails,
"new_email_count": new_email_count,
"expiring_leases": expiring_leases,
"recent_audit": recent_audit,
"current_quarter": current_quarter,
"current_year": current_year,
}
return render(request, "stiftung/home.html", context)
@api_view(["GET"])
def health_check(request):
"""Simple health check endpoint for deployment monitoring"""
return JsonResponse(
{
"status": "healthy",
"timestamp": timezone.now().isoformat(),
"service": "stiftung-web",
}
)
# CSV Import Views
@api_view(["GET"])
def health(_request):
return Response({"status": "ok"})