- 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>
161 lines
6.0 KiB
Python
161 lines
6.0 KiB
Python
# 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"})
|