diff --git a/README.md b/README.md
index b2d78a0..03fe0c9 100644
--- a/README.md
+++ b/README.md
@@ -13,7 +13,7 @@ Modern Django/Tailwind-baserad portal för att ta emot, granska och betala utlä
Nyckel-URLer (språkprefixed):
- Offentligt formulär `GET /sv/claims/new/` eller `/en/claims/new/`
- Bekräftelsesida `GET /sv/claims/submitted/`
-- Adminlista `GET /sv/claims/admin/`
+- Dashboard `GET /sv/claims/admin/`
- Mina utlägg `GET /sv/claims/mine/`
- Användarhantering `GET /sv/claims/users/`
- Export-placeholder `GET /sv/claims/export/`
@@ -26,7 +26,7 @@ Nyckel-URLer (språkprefixed):
- **Auto-prefill:** Inloggade användare får namn, e-post och senaste kontonummer förifyllt.
- **Valuta & projekt:** Varje rad har dold valutaväljare (SEK default) och projektreferens. Projekt listas från Django admin > Projekt.
- **Kvitton:** Filuppladdningar sparas med slumpat UUID-baserat namn under `receipts/` för säkerhet och unika namn.
-- **Adminlista:** Kortlayout med statuschippar, loggtimeline, kvittolänkar och inline-formulär för godkänn/avslag.
+- **Dashboard:** KPI-kort med totalsiffror, senaste aktivitet, statusfördelning och samma inline-flöde för beslut/utbetalningar.
- **Betalspårning:** När intern betalning är på får godkända claims en "Betala"-knapp. När ett claim markeras som betalt låses status/kommentar tills reset görs.
- **Mina utlägg:** Inloggade ser sina egna claims i samma Tailwind-layout med kvitto-länk och logg.
- **Användarhantering:** Tailwind-sida där personal kan skapa konton, tilldela `claims.view_claim`/`claims.change_claim`, markera staff och ta bort användare.
diff --git a/claims/templates/claims/admin_list.html b/claims/templates/claims/admin_list.html
deleted file mode 100644
index ca66d28..0000000
--- a/claims/templates/claims/admin_list.html
+++ /dev/null
@@ -1,188 +0,0 @@
-{% extends "claims/base.html" %}
-{% load i18n %}
-
-{% block title %}{% trans "Admin – Utlägg" %}{% endblock %}
-
-{% block content %}
-
-
-
- {% if claims %}
-
- {% for claim in claims %}
-
-
-
-
-
- {{ claim.amount }} {{ claim.currency }}
-
- {% if claim.project %}
-
- {{ claim.project }}
-
- {% endif %}
- {% trans "Skapad" %} {{ claim.created_at|date:"Y-m-d H:i" }}
-
-
-
{% trans "Person" %}
-
{{ claim.full_name }}
-
- {{ claim.email }} · {% trans "Konto" %}: {{ claim.account_number }}
- {% if claim.submitted_by %}
- {% trans "Inloggad användare" %}: {{ claim.submitted_by.get_username }}
- {% else %}
- {% trans "Inskickad av gäst" %}
- {% endif %}
-
-
-
-
-
- {{ claim.get_status_display }}
-
- {% if claim.decision_note %}
-
{% trans "Kommentar" %}: {{ claim.decision_note }}
- {% endif %}
- {% if payments_enabled and claim.status == 'approved' %}
- {% if claim.is_paid %}
-
- {% trans "Betald" %} {{ claim.paid_at|date:"Y-m-d H:i" }}
- {% if claim.paid_by %}{% trans "av" %} {{ claim.paid_by.get_username }}{% endif %}
-
- {% else %}
-
{% trans "Ej markerad som betald" %}
- {% endif %}
- {% endif %}
-
-
-
- {% if claim.status == 'approved' %}
-
-
-
-
{% trans "Sammanfattning" %}
-
{{ claim.full_name }}
-
{% trans "Belopp" %}: {{ claim.amount }} {{ claim.currency }} · {% trans "Konto" %}: {{ claim.account_number }}
-
- {% if payments_enabled %}
- {% if claim.is_paid %}
-
{% trans "Markerad som betald" %}
- {% else %}
-
- {% endif %}
- {% else %}
-
{% trans "Intern betalningshantering är av – markera betalning i ekonomisystemet." %}
- {% endif %}
-
-
- {% endif %}
-
-
-
-
{% trans "Beskrivning" %}
-
{{ claim.description }}
-
- {% if claim.receipt %}
-
-
-
-
- {% trans "Visa kvitto" %}
-
- {% else %}
-
{% trans "Inget kvitto bifogat" %}
- {% endif %}
-
-
-
-
-
- {% trans "Logg & tidslinje" %}
-
-
- {% for log in claim.logs.all %}
-
- {{ log.get_action_display }}
- {{ log.created_at|date:"Y-m-d H:i" }}
- {% if log.from_status %}
- {% trans "Status" %}: {{ log.get_from_status_display }} → {{ log.get_to_status_display }}
- {% endif %}
- {% if log.note %}
- "{{ log.note }}"
- {% endif %}
- {% if log.performed_by %}
- {% trans "Av" %} {{ log.performed_by.get_username }}
- {% endif %}
-
- {% empty %}
- {% trans "Ingen logg än." %}
- {% endfor %}
-
-
-
- {% if can_change %}
- {% if claim.is_paid %}
-
- {% trans "Utlägget är markerat som betalt. Ändringar av beslut/kommentar är låsta." %}
-
- {% else %}
-
- {% endif %}
- {% endif %}
-
-
-
- {% endfor %}
-
- {% else %}
-
-
{% trans "Inga utlägg ännu" %}
-
{% trans "När formuläret tas emot visas posterna automatiskt här." %}
-
- {% endif %}
-
-{% endblock %}
diff --git a/claims/templates/claims/base.html b/claims/templates/claims/base.html
index 651b62d..c5c3b50 100644
--- a/claims/templates/claims/base.html
+++ b/claims/templates/claims/base.html
@@ -40,7 +40,7 @@
-
{% trans "Utläggslista" %}
+
{% trans "Dashboard" %}
{% trans "Mina utlägg" %}
{% if perms.auth.view_user %}
{% trans "Användare" %}
diff --git a/claims/templates/claims/dashboard.html b/claims/templates/claims/dashboard.html
new file mode 100644
index 0000000..ef74129
--- /dev/null
+++ b/claims/templates/claims/dashboard.html
@@ -0,0 +1,295 @@
+{% extends "claims/base.html" %}
+{% load i18n %}
+
+{% block title %}{% trans "Admin – Dashboard" %}{% endblock %}
+
+{% block content %}
+
+
+
+
{% trans "Översikt" %}
+
{% trans "Dashboard för utlägg" %}
+
{% trans "Få koll på inflödet, beslutsläget och utbetalningar – och hantera ärenden direkt." %}
+
+
+ {% trans "Tips: använd filtren för att fokusera på specifika statusar eller projekt. Dashboarden uppdateras i realtid när data ändras." %}
+
+
+
+
+
+ {% trans "Totalt antal utlägg" %}
+ {{ summary.total_claims }}
+ {% trans "Alla statusar" %}
+
+
+ {% trans "Senaste 7 dagarna" %}
+ {{ summary.last_week_claims }}
+ {% trans "Nya inskick sedan en vecka" %}
+
+
+ {% trans "Pågående granskning" %}
+ {{ summary.pending_count }}
+ {% trans "Behöver beslut" %}
+
+
+ {% trans "Redo för utbetalning" %}
+ {{ summary.ready_to_pay }}
+ {% trans "Godkända men ej markerade som betalda" %}
+
+
+
+
+
+ {% trans "Belopp att besluta" %}
+ {{ summary.pending_amount|floatformat:2 }}
+ {% trans "Summa av väntande utlägg (alla valutor)" %}
+
+
+ {% trans "Godkända belopp" %}
+ {{ summary.approved_amount|floatformat:2 }}
+ {% trans "Summa för alla godkända utlägg" %}
+
+
+ {% trans "Utbetalda belopp" %}
+ {{ summary.paid_amount|floatformat:2 }}
+ {% trans "Markerade som betalda" %}
+
+
+
+
+
+
+
+
+
{% trans "Filtrera" %}
+
{% trans "Hantera utlägg" %}
+
{% trans "Välj status för att fokusera listan nedan." %}
+
+
+
+
+
+ {% if claims %}
+
+ {% for claim in claims %}
+
+
+
+
+
+ {{ claim.amount }} {{ claim.currency }}
+
+ {% if claim.project %}
+
+ {{ claim.project }}
+
+ {% endif %}
+ {% trans "Skapad" %} {{ claim.created_at|date:"Y-m-d H:i" }}
+
+
+
{% trans "Person" %}
+
{{ claim.full_name }}
+
+ {{ claim.email }} · {% trans "Konto" %}: {{ claim.account_number }}
+ {% if claim.submitted_by %}
+ {% trans "Inloggad användare" %}: {{ claim.submitted_by.get_username }}
+ {% else %}
+ {% trans "Inskickad av gäst" %}
+ {% endif %}
+
+
+
+
+
+ {{ claim.get_status_display }}
+
+ {% if claim.decision_note %}
+
{% trans "Kommentar" %}: {{ claim.decision_note }}
+ {% endif %}
+ {% if payments_enabled and claim.status == 'approved' %}
+ {% if claim.is_paid %}
+
+ {% trans "Betald" %} {{ claim.paid_at|date:"Y-m-d H:i" }}
+ {% if claim.paid_by %}{% trans "av" %} {{ claim.paid_by.get_username }}{% endif %}
+
+ {% else %}
+
{% trans "Ej markerad som betald" %}
+ {% endif %}
+ {% endif %}
+
+
+
+ {% if claim.status == 'approved' %}
+
+
+
+
{% trans "Sammanfattning" %}
+
{{ claim.full_name }}
+
{% trans "Belopp" %}: {{ claim.amount }} {{ claim.currency }} · {% trans "Konto" %}: {{ claim.account_number }}
+
+ {% if payments_enabled %}
+ {% if claim.is_paid %}
+
{% trans "Markerad som betald" %}
+ {% else %}
+
+ {% csrf_token %}
+
+
+
+ {% trans "Betala" %}
+
+
+ {% endif %}
+ {% else %}
+
{% trans "Intern betalningshantering är av – markera betalning i ekonomisystemet." %}
+ {% endif %}
+
+
+ {% endif %}
+
+
+
+
{% trans "Beskrivning" %}
+
{{ claim.description }}
+
+ {% if claim.receipt %}
+
+
+
+
+ {% trans "Visa kvitto" %}
+
+ {% else %}
+
{% trans "Inget kvitto bifogat" %}
+ {% endif %}
+
{% trans "Senast uppdaterad" %}: {{ claim.updated_at|date:"Y-m-d H:i" }}
+
+
+
+
+ {% trans "Logg" %}
+
+ {% for log in claim.logs.all %}
+
+ {{ log.get_action_display }}
+ {{ log.created_at|date:"Y-m-d H:i" }}
+ {% if log.from_status %}
+ {% trans "Status" %}: {{ log.get_from_status_display }} → {{ log.get_to_status_display }}
+ {% endif %}
+ {% if log.note %}
+ "{{ log.note }}"
+ {% endif %}
+ {% if log.performed_by %}
+ {% trans "Av" %} {{ log.performed_by.get_username }}
+ {% endif %}
+
+ {% empty %}
+ {% trans "Ingen logg än." %}
+ {% endfor %}
+
+
+
+ {% if can_change %}
+ {% if claim.is_paid %}
+
+ {% trans "Utlägget är markerat som betalt. Ändringar av beslut/kommentar är låsta." %}
+
+ {% else %}
+
+ {% csrf_token %}
+
+
+ {% trans "Åtgärd" %}
+
+ {% for value, label in decision_choices %}
+ {{ label }}
+ {% endfor %}
+
+
+ {% trans "Kommentar" %}
+ {{ claim.decision_note }}
+
+
+
+ {% trans "Uppdatera beslut" %}
+
+
+ {% endif %}
+ {% endif %}
+
+
+
+ {% endfor %}
+
+ {% else %}
+
+
{% trans "Inga utlägg ännu" %}
+
{% trans "När formuläret tas emot visas posterna automatiskt här." %}
+
+ {% endif %}
+
+
+
+
+
+
+
+
+
+
+
+
+
{% trans "Pending" %}
+ {{ summary.pending_count }}
+
+
+
{% trans "Approved" %}
+ {{ summary.approved_count }}
+
+
+
{% trans "Rejected" %}
+ {{ summary.rejected_count }}
+
+
+
+
+
+
+{% endblock %}
diff --git a/claims/tests.py b/claims/tests.py
index c492a05..f4d9cd4 100644
--- a/claims/tests.py
+++ b/claims/tests.py
@@ -1,7 +1,14 @@
+from datetime import timedelta
+
+from django.contrib.auth import get_user_model
+from django.contrib.auth.models import Permission
from django.core.exceptions import ValidationError
from django.core.files.uploadedfile import SimpleUploadedFile
from django.test import TestCase, override_settings
+from django.urls import reverse
+from django.utils import timezone
+from .models import Claim
from .validators import validate_receipt_file
from .views import SubmitClaimView
@@ -65,3 +72,44 @@ class ClaimFormsetLimitTests(TestCase):
formset = view.build_formset(data=data)
self.assertFalse(formset.is_valid())
self.assertTrue(formset.non_form_errors())
+
+
+class DashboardViewTests(TestCase):
+ def setUp(self):
+ User = get_user_model()
+ self.user = User.objects.create_user(username="admin", password="test123", email="admin@example.com")
+ view_perm = Permission.objects.get(codename="view_claim")
+ self.user.user_permissions.add(view_perm)
+ self.client.force_login(self.user)
+
+ def _create_claim(self, **kwargs):
+ defaults = {
+ "full_name": "Test User",
+ "email": "test@example.com",
+ "amount": 123,
+ "currency": Claim.Currency.SEK,
+ "description": "Taxi",
+ "account_number": "123-456",
+ }
+ defaults.update(kwargs)
+ claim = Claim.objects.create(**defaults)
+ return claim
+
+ def test_dashboard_summary_counts(self):
+ recent_pending = self._create_claim()
+ recent_approved = self._create_claim(status=Claim.Status.APPROVED)
+ paid_claim = self._create_claim(status=Claim.Status.APPROVED, amount=500)
+ paid_claim.paid_at = timezone.now()
+ paid_claim.save(update_fields=["paid_at"])
+
+ old_claim = self._create_claim(status=Claim.Status.REJECTED)
+ Claim.objects.filter(pk=old_claim.pk).update(created_at=timezone.now() - timedelta(days=10))
+
+ response = self.client.get(reverse("claims:admin-list"))
+ self.assertEqual(response.status_code, 200)
+ summary = response.context["summary"]
+ self.assertEqual(summary["total_claims"], 4)
+ self.assertEqual(summary["last_week_claims"], 3)
+ self.assertEqual(summary["pending_count"], 1)
+ self.assertEqual(summary["approved_count"], 2)
+ self.assertEqual(summary["ready_to_pay"], 1)
diff --git a/claims/urls.py b/claims/urls.py
index 4f06a76..7b96fb2 100644
--- a/claims/urls.py
+++ b/claims/urls.py
@@ -1,7 +1,7 @@
from django.urls import path
from .views import (
- ClaimAdminListView,
+ ClaimDashboardView,
ClaimExportMenuView,
MyClaimsView,
SubmitClaimView,
@@ -14,7 +14,7 @@ app_name = "claims"
urlpatterns = [
path("new/", SubmitClaimView.as_view(), name="submit"),
path("submitted/", SubmitClaimSuccessView.as_view(), name="submit-success"),
- path("admin/", ClaimAdminListView.as_view(), name="admin-list"),
+ path("admin/", ClaimDashboardView.as_view(), name="admin-list"),
path("export/", ClaimExportMenuView.as_view(), name="export"),
path("mine/", MyClaimsView.as_view(), name="my-claims"),
path("users/", UserManagementView.as_view(), name="user-manage"),
diff --git a/claims/views.py b/claims/views.py
index 0ecb745..b4eeabb 100644
--- a/claims/views.py
+++ b/claims/views.py
@@ -1,8 +1,12 @@
+from datetime import timedelta
+from decimal import Decimal
+
from django.conf import settings
from django.contrib import messages
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Permission
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
+from django.db.models import Sum
from django.forms import formset_factory
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
@@ -136,8 +140,8 @@ class SubmitClaimView(View):
)
-class ClaimAdminListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
- template_name = "claims/admin_list.html"
+class ClaimDashboardView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
+ template_name = "claims/dashboard.html"
context_object_name = "claims"
permission_required = "claims.view_claim"
@@ -159,6 +163,12 @@ class ClaimAdminListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
context["decision_choices"] = ClaimDecisionForm().fields["action"].choices
context["can_change"] = self.request.user.has_perm("claims.change_claim")
context["payments_enabled"] = getattr(settings, "CLAIMS_ENABLE_INTERNAL_PAYMENTS", False)
+ context["summary"] = self._build_summary()
+ context["recent_claims"] = (
+ Claim.objects.select_related("project")
+ .prefetch_related("logs__performed_by")
+ .order_by("-created_at")[:5]
+ )
return context
def post(self, request, *args, **kwargs):
@@ -232,6 +242,29 @@ class ClaimAdminListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
messages.success(request, _("%(claim)s markerades som betald.") % {"claim": claim})
return redirect(request.get_full_path())
+ def _build_summary(self):
+ now = timezone.now()
+ last_week = now - timedelta(days=7)
+ pending_qs = Claim.objects.filter(status=Claim.Status.PENDING)
+ approved_qs = Claim.objects.filter(status=Claim.Status.APPROVED)
+ rejected_qs = Claim.objects.filter(status=Claim.Status.REJECTED)
+ ready_to_pay_qs = approved_qs.filter(paid_at__isnull=True)
+
+ def _sum(qs):
+ return qs.aggregate(total=Sum("amount"))["total"] or Decimal("0")
+
+ return {
+ "total_claims": Claim.objects.count(),
+ "last_week_claims": Claim.objects.filter(created_at__gte=last_week).count(),
+ "pending_count": pending_qs.count(),
+ "approved_count": approved_qs.count(),
+ "rejected_count": rejected_qs.count(),
+ "ready_to_pay": ready_to_pay_qs.count(),
+ "pending_amount": _sum(pending_qs),
+ "approved_amount": _sum(approved_qs),
+ "paid_amount": _sum(approved_qs.filter(paid_at__isnull=False)),
+ }
+
class ClaimExportMenuView(LoginRequiredMixin, PermissionRequiredMixin, TemplateView):
template_name = "claims/export_placeholder.html"