diff --git a/app/core/settings.py b/app/core/settings.py index dedda34..831bb04 100644 --- a/app/core/settings.py +++ b/app/core/settings.py @@ -35,6 +35,7 @@ INSTALLED_APPS = [ "django.contrib.humanize", "rest_framework", "rest_framework.authtoken", + "django_htmx", "django_otp", "django_otp.plugins.otp_totp", "django_otp.plugins.otp_static", @@ -48,6 +49,7 @@ MIDDLEWARE = [ "django.middleware.security.SecurityMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.common.CommonMiddleware", + "django_htmx.middleware.HtmxMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", "django_otp.middleware.OTPMiddleware", @@ -131,7 +133,7 @@ CELERY_BEAT_SCHEDULE = { # IMAP-Konfiguration für E-Mail-Eingang (Destinatäre) # Pflichtfelder: IMAP_HOST, IMAP_USER, IMAP_PASSWORD IMAP_HOST = os.getenv("IMAP_HOST", "") -IMAP_PORT = int(os.getenv("IMAP_PORT", "993")) +IMAP_PORT = int(os.getenv("IMAP_PORT") or "993") IMAP_USER = os.getenv("IMAP_USER", "paperless@vhtv-stiftung.de") IMAP_PASSWORD = os.getenv("IMAP_PASSWORD", "") IMAP_FOLDER = os.getenv("IMAP_FOLDER", "INBOX") diff --git a/app/requirements.txt b/app/requirements.txt index df51b84..7b44ac5 100644 --- a/app/requirements.txt +++ b/app/requirements.txt @@ -11,4 +11,5 @@ gunicorn==22.0.0 python-dateutil==2.9.0 markdown==3.6 django-otp==1.2.4 +django-htmx==1.19.0 qrcode[pil]==7.4.2 diff --git a/app/stiftung/views/dashboard.py b/app/stiftung/views/dashboard.py index aa50cf9..93b20d2 100644 --- a/app/stiftung/views/dashboard.py +++ b/app/stiftung/views/dashboard.py @@ -1,5 +1,5 @@ # views/dashboard.py -# Phase 0: Vision 2026 – Code-Refactoring +# Vision 2026 – Phase 1: Dashboard Cockpit import csv import io @@ -59,38 +59,86 @@ from stiftung.forms import ( @login_required def home(request): - """Home page for the Stiftungsverwaltung application""" + """Vision 2026 Dashboard Cockpit""" from stiftung.services.calendar_service import StiftungsKalenderService - - # Get upcoming events for the calendar widget - calendar_service = StiftungsKalenderService() - - # Get all events for the next 14 days - from datetime import timedelta + 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) - - # Filter for upcoming and overdue 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)] - - # Get current month events for mini calendar - from calendar import monthrange - _, last_day = monthrange(today.year, today.month) - month_start = today.replace(day=1) - month_end = today.replace(day=last_day) - current_month_events = calendar_service.get_all_events(month_start, month_end) - + + # ── 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": "Stiftungsverwaltung", - "description": "Foundation Management System", - "upcoming_events": upcoming_events[:5], # Show only 5 upcoming events - "overdue_events": overdue_events[:3], # Show only 3 overdue events - "current_month_events": current_month_events, + "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) @@ -106,12 +154,7 @@ def health_check(request): ) -## Removed duplicate paperless_ping referencing non-existent PAPERLESS_URL - - # CSV Import Views @api_view(["GET"]) def health(_request): return Response({"status": "ok"}) - - diff --git a/app/templates/base.html b/app/templates/base.html index 4e7ebc4..d7ebb8b 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -3,14 +3,14 @@
-