Files
claims-system/claims/templates/claims/dashboard.html
2025-11-09 12:22:50 +01:00

404 lines
28 KiB
HTML
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.

{% extends "claims/base.html" %}
{% load i18n %}
{% block title %}{% trans "Admin Dashboard" %}{% endblock %}
{% block content %}
<section class="space-y-8 py-6">
<header class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
<div>
<p class="text-sm font-semibold uppercase tracking-wide text-gray-500">{% trans "Översikt" %}</p>
<h1 class="text-3xl font-semibold text-gray-900">{% trans "Dashboard för utlägg" %}</h1>
<p class="mt-2 text-sm text-gray-600">{% trans "Få koll på inflödet, beslutsläget och utbetalningar och hantera ärenden direkt." %}</p>
</div>
<div class="rounded-2xl border border-dashed border-gray-200 bg-white px-4 py-3 text-xs text-gray-600">
{% trans "Tips: använd filtren för att fokusera på specifika statusar eller projekt. Dashboarden uppdateras i realtid när data ändras." %}
</div>
</header>
<div class="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
<article class="rounded-3xl bg-white px-5 py-6 shadow-sm ring-1 ring-gray-100">
<p class="text-xs font-semibold uppercase tracking-wide text-gray-500">{% trans "Totalt antal utlägg" %}</p>
<p class="mt-3 text-4xl font-semibold text-gray-900">{{ summary.total_claims }}</p>
<p class="mt-1 text-xs text-gray-500">{% trans "Alla statusar" %}</p>
</article>
<article class="rounded-3xl bg-white px-5 py-6 shadow-sm ring-1 ring-gray-100">
<p class="text-xs font-semibold uppercase tracking-wide text-gray-500">{% trans "Senaste 7 dagarna" %}</p>
<p class="mt-3 text-4xl font-semibold text-gray-900">{{ summary.last_week_claims }}</p>
<p class="mt-1 text-xs text-gray-500">{% trans "Nya inskick sedan en vecka" %}</p>
</article>
<article class="rounded-3xl bg-white px-5 py-6 shadow-sm ring-1 ring-gray-100">
<p class="text-xs font-semibold uppercase tracking-wide text-gray-500">{% trans "Pågående granskning" %}</p>
<p class="mt-3 text-4xl font-semibold text-amber-600">{{ summary.pending_count }}</p>
<p class="mt-1 text-xs text-gray-500">{% trans "Behöver beslut" %}</p>
</article>
<article class="rounded-3xl bg-white px-5 py-6 shadow-sm ring-1 ring-gray-100">
<p class="text-xs font-semibold uppercase tracking-wide text-gray-500">{% trans "Redo för utbetalning" %}</p>
<p class="mt-3 text-4xl font-semibold text-emerald-600">{{ summary.ready_to_pay }}</p>
<p class="mt-1 text-xs text-gray-500">{% trans "Godkända men ej markerade som betalda" %}</p>
</article>
</div>
<div class="grid gap-4 md:grid-cols-3">
<article class="rounded-3xl bg-slate-900 px-5 py-6 text-white shadow-sm ring-1 ring-slate-800">
<p class="text-xs font-semibold uppercase tracking-wide text-slate-300">{% trans "Belopp att besluta" %}</p>
<p class="mt-3 text-3xl font-semibold">{{ summary.pending_amount|floatformat:2 }}</p>
<p class="mt-1 text-xs text-slate-400">{% trans "Summa av väntande utlägg (alla valutor)" %}</p>
</article>
<article class="rounded-3xl bg-white px-5 py-6 shadow-sm ring-1 ring-gray-100">
<p class="text-xs font-semibold uppercase tracking-wide text-gray-500">{% trans "Godkända belopp" %}</p>
<p class="mt-3 text-3xl font-semibold text-gray-900">{{ summary.approved_amount|floatformat:2 }}</p>
<p class="mt-1 text-xs text-gray-500">{% trans "Summa för alla godkända utlägg" %}</p>
</article>
<article class="rounded-3xl bg-white px-5 py-6 shadow-sm ring-1 ring-gray-100">
<p class="text-xs font-semibold uppercase tracking-wide text-gray-500">{% trans "Utbetalda belopp" %}</p>
<p class="mt-3 text-3xl font-semibold text-gray-900">{{ summary.paid_amount|floatformat:2 }}</p>
<p class="mt-1 text-xs text-gray-500">{% trans "Markerade som betalda" %}</p>
</article>
</div>
<div class="grid gap-6 lg:grid-cols-[minmax(0,2fr),minmax(0,1fr)]">
<div class="space-y-6">
<section class="rounded-3xl bg-white px-5 py-5 shadow-sm ring-1 ring-gray-100">
<div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div>
<p class="text-sm font-semibold uppercase tracking-wide text-gray-500">{% trans "Filtrera" %}</p>
<h2 class="text-xl font-semibold text-gray-900">{% trans "Hantera utlägg" %}</h2>
<p class="mt-1 text-sm text-gray-600">{% trans "Välj status för att fokusera listan nedan." %}</p>
</div>
<div class="flex flex-wrap gap-2" data-filter-controls>
<a href="?status=all"
data-filter-button
data-filter-value="all"
aria-pressed="{% if status_filter == 'all' %}true{% else %}false{% endif %}"
class="rounded-full px-4 py-2 text-sm font-semibold transition focus:outline-none focus-visible:ring-2 focus-visible:ring-brand-600 focus-visible:ring-offset-2 {% if status_filter == 'all' %}bg-brand-600 text-white hover:bg-brand-700{% else %}bg-slate-100 text-gray-700 hover:bg-slate-200{% endif %}">
{% trans "Alla" %}
</a>
{% for value, label in status_choices %}
<a href="?status={{ value }}"
data-filter-button
data-filter-value="{{ value }}"
aria-pressed="{% if status_filter == value %}true{% else %}false{% endif %}"
class="rounded-full px-4 py-2 text-sm font-semibold transition focus:outline-none focus-visible:ring-2 focus-visible:ring-brand-600 focus-visible:ring-offset-2 {% if status_filter == value %}bg-brand-600 text-white hover:bg-brand-700{% else %}bg-slate-100 text-gray-700 hover:bg-slate-200{% endif %}">
{{ label }}
</a>
{% endfor %}
</div>
</div>
</section>
<div class="space-y-6" data-claim-list>
{% for claim in claims %}
<article class="rounded-3xl bg-white shadow-sm ring-1 ring-gray-100 {% if status_filter != 'all' and claim.status != status_filter %}hidden{% endif %}"
data-claim-card
data-status="{{ claim.status }}">
<div class="flex flex-col gap-4 border-b border-gray-100 px-6 py-5 lg:flex-row lg:justify-between">
<div class="space-y-3">
<div class="flex flex-wrap items-center gap-3 text-sm text-gray-500">
<span class="rounded-full bg-slate-100 px-3 py-1 font-semibold text-gray-700">
{{ claim.amount }} {{ claim.currency }}
</span>
{% if claim.project %}
<span class="rounded-full bg-violet-50 px-3 py-1 font-semibold text-violet-700">
{{ claim.project }}
</span>
{% endif %}
<span class="text-xs text-gray-400">{% trans "Skapad" %} {{ claim.created_at|date:"Y-m-d H:i" }}</span>
</div>
<div class="space-y-1">
<p class="text-sm font-semibold uppercase tracking-wide text-gray-500">{% trans "Person" %}</p>
<h2 class="text-2xl font-semibold text-gray-900">{{ claim.full_name }}</h2>
<p class="text-sm text-gray-600">
{{ claim.email }} · {% trans "Konto" %}: <span class="font-mono text-gray-900">{{ claim.account_number }}</span><br>
{% if claim.submitted_by %}
<span class="text-xs uppercase tracking-wide text-green-600">{% trans "Inloggad användare" %}: {{ claim.submitted_by.get_username }}</span>
{% else %}
<span class="text-xs uppercase tracking-wide text-gray-500">{% trans "Inskickad av gäst" %}</span>
{% endif %}
</p>
</div>
</div>
<div class="flex flex-col items-start gap-2 text-sm lg:items-end">
<span class="rounded-full px-4 py-2 text-sm font-semibold {% if claim.status == 'approved' %}bg-green-50 text-green-700 border border-green-200{% elif claim.status == 'rejected' %}bg-rose-50 text-rose-700 border border-rose-200{% else %}bg-amber-50 text-amber-800 border border-amber-200{% endif %}">
{{ claim.get_status_display }}
</span>
{% if claim.decision_note %}
<p class="text-xs text-gray-500">{% trans "Kommentar" %}: {{ claim.decision_note }}</p>
{% endif %}
{% if payments_enabled and claim.status == 'approved' %}
{% if claim.is_paid %}
<span class="rounded-full bg-emerald-100 px-3 py-1 text-xs font-semibold text-emerald-800">
{% trans "Betald" %} {{ claim.paid_at|date:"Y-m-d H:i" }}
{% if claim.paid_by %}{% trans "av" %} {{ claim.paid_by.get_username }}{% endif %}
</span>
{% else %}
<span class="text-xs text-gray-500">{% trans "Ej markerad som betald" %}</span>
{% endif %}
{% endif %}
</div>
</div>
{% if claim.status == 'approved' %}
<div class="mx-6 mt-4 grid gap-4 rounded-3xl border border-green-100 bg-green-50 px-6 py-4 text-sm text-green-900 md:grid-cols-[2fr,1fr]">
<details class="space-y-3" {% if not claim.is_paid %}open{% endif %}>
<summary class="flex cursor-pointer items-center justify-between text-xs font-semibold uppercase tracking-wide text-green-600">
<span>{% trans "Utbetalningsdetaljer" %}</span>
<svg class="h-4 w-4 transition group-open:-rotate-180" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
</svg>
</summary>
<div class="rounded-2xl bg-white/80 p-4">
<dl class="grid gap-2 text-sm text-green-900 md:grid-cols-2">
<div>
<dt class="text-xs uppercase tracking-wide text-green-700">{% trans "Belopp" %}</dt>
<dd class="text-lg font-semibold">{{ claim.amount }} {{ claim.currency }}</dd>
</div>
<div>
<dt class="text-xs uppercase tracking-wide text-green-700">{% trans "Kontonummer" %}</dt>
<dd class="font-mono text-base">{{ claim.account_number }}</dd>
</div>
<div>
<dt class="text-xs uppercase tracking-wide text-green-700">{% trans "Referens (Claim ID)" %}</dt>
<dd class="font-semibold">#{{ claim.id }}</dd>
</div>
<div>
<dt class="text-xs uppercase tracking-wide text-green-700">{% trans "Skapad" %}</dt>
<dd>{{ claim.created_at|date:"Y-m-d H:i" }}</dd>
</div>
</dl>
<p class="mt-3 text-[11px] text-green-700">{% trans "Använd referensen och beloppet när du lägger upp betalningen hjälper att undvika dubbletter." %}</p>
</div>
</details>
<div class="flex flex-col items-start gap-3 md:items-end">
{% if payments_enabled %}
{% if not claim.is_paid %}
<form method="post" class="w-full max-w-xs" onsubmit="return confirm('{% trans "Är du säker att du har lagt upp betalningen? Markera endast som betald om beloppet skickas till banken." %}');">
{% csrf_token %}
<input type="hidden" name="action_type" value="payment">
<input type="hidden" name="payment_claim_id" value="{{ claim.id }}">
<button type="submit" class="flex w-full items-center justify-center gap-2 rounded-2xl bg-emerald-600 px-4 py-3 text-xs font-semibold uppercase tracking-wide text-white transition hover:bg-emerald-700">
{% trans "Markera som betald" %}
</button>
</form>
<p class="text-[11px] text-green-700">{% trans "Dubbelkolla belopp och kontonummer i panelen innan du bekräftar." %}</p>
{% endif %}
{% else %}
<p class="rounded-2xl bg-white/70 px-4 py-3 text-xs text-green-800">
{% trans "Intern betalningshantering är av markera betalning i ekonomisystemet och resetta status vid behov." %}
</p>
{% endif %}
</div>
</div>
{% endif %}
<div class="grid gap-6 px-6 py-6 lg:grid-cols-3">
<div class="lg:col-span-2">
<p class="text-sm font-semibold text-gray-500">{% trans "Beskrivning" %}</p>
<p class="mt-2 whitespace-pre-wrap text-gray-800">{{ claim.description }}</p>
<div class="mt-4 flex flex-wrap items-center gap-4 text-sm text-gray-600">
{% if claim.receipt %}
<a class="inline-flex items-center gap-2 text-brand-600 hover:text-brand-700" href="{{ claim.receipt.url }}" target="_blank" rel="noopener">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M15 12H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
{% trans "Visa kvitto" %}
</a>
{% else %}
<span class="text-xs text-gray-400">{% trans "Inget kvitto bifogat" %}</span>
{% endif %}
<span class="text-xs text-gray-400">{% trans "Senast uppdaterad" %}: {{ claim.updated_at|date:"Y-m-d H:i" }}</span>
</div>
</div>
<div>
<details class="rounded-2xl bg-slate-50 p-4 text-sm text-gray-700">
<summary class="cursor-pointer select-none text-sm font-semibold text-gray-800">{% trans "Logg" %}</summary>
<ul class="mt-3 space-y-2 text-sm text-gray-600">
{% for log in claim.logs.all %}
<li class="rounded-lg bg-white px-3 py-2 shadow-sm">
<p class="font-semibold text-gray-900">{{ log.get_action_display }}</p>
<p class="text-xs text-gray-500">{{ log.created_at|date:"Y-m-d H:i" }}</p>
{% if log.from_status %}
<p class="text-xs text-gray-500">{% trans "Status" %}: {{ log.get_from_status_display }} → {{ log.get_to_status_display }}</p>
{% endif %}
{% if log.note %}
<p class="mt-1 text-xs text-gray-600">"{{ log.note }}"</p>
{% endif %}
{% if log.performed_by %}
<p class="text-xs text-gray-400">{% trans "Av" %} {{ log.performed_by.get_username }}</p>
{% endif %}
</li>
{% empty %}
<li class="text-xs text-gray-400">{% trans "Ingen logg än." %}</li>
{% endfor %}
</ul>
</details>
{% if can_change %}
{% if claim.is_paid %}
<p class="mt-4 rounded-lg bg-slate-100 px-3 py-2 text-xs text-slate-600">
{% trans "Utlägget är markerat som betalt. Ändringar av beslut/kommentar är låsta." %}
</p>
{% else %}
<form method="post" class="mt-4 space-y-3">
{% csrf_token %}
<input type="hidden" name="claim_id" value="{{ claim.id }}">
<label class="block text-sm font-medium text-gray-700">{% trans "Åtgärd" %}</label>
<select name="action" class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm text-gray-900 focus:border-brand-600 focus:outline-none focus:ring-2 focus:ring-brand-600">
{% for value, label in decision_choices %}
<option value="{{ value }}">{{ label }}</option>
{% endfor %}
</select>
<label class="block text-sm font-medium text-gray-700">{% trans "Kommentar" %}</label>
<textarea name="decision_note" rows="3" class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm text-gray-900 focus:border-brand-600 focus:outline-none focus:ring-2 focus:ring-brand-600">{{ claim.decision_note }}</textarea>
<input type="hidden" name="action_type" value="decision">
<button type="submit" class="w-full rounded-full bg-brand-600 px-4 py-2 text-sm font-semibold text-white transition hover:bg-brand-700">
{% trans "Uppdatera beslut" %}
</button>
</form>
{% endif %}
{% endif %}
</div>
</div>
</article>
</article>
{% empty %}
<div class="rounded-3xl border border-dashed border-gray-200 bg-white px-6 py-10 text-center text-gray-500">
<p class="text-lg font-semibold text-gray-900">{% trans "Inga utlägg ännu" %}</p>
<p class="mt-2 text-sm">{% trans "När formuläret tas emot visas posterna automatiskt här." %}</p>
</div>
{% endfor %}
</div>
{% if has_any_claims %}
<div data-claim-empty class="{% if has_filtered_claims %}hidden{% endif %} rounded-3xl border border-dashed border-gray-200 bg-white px-6 py-10 text-center text-gray-500">
<p class="text-lg font-semibold text-gray-900">{% trans "Inga utlägg matchar filtret" %}</p>
<p class="mt-2 text-sm">{% trans "Välj en annan status för att se fler poster." %}</p>
</div>
{% endif %}
</div>
<aside class="space-y-6">
<article class="rounded-3xl bg-white px-5 py-6 shadow-sm ring-1 ring-gray-100">
<header>
<p class="text-xs font-semibold uppercase tracking-wide text-gray-500">{% trans "Senaste inskick" %}</p>
<h2 class="text-xl font-semibold text-gray-900">{% trans "Aktivitet" %}</h2>
</header>
<ul class="mt-4 space-y-3">
{% for recent in recent_claims %}
<li class="rounded-2xl border border-gray-100 bg-slate-50 px-4 py-3">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-semibold text-gray-900">{{ recent.full_name }}</p>
<p class="text-xs text-gray-500">{{ recent.created_at|date:"Y-m-d H:i" }}</p>
</div>
<span class="rounded-full px-3 py-1 text-xs font-semibold {% if recent.status == 'approved' %}bg-green-100 text-green-700{% elif recent.status == 'rejected' %}bg-rose-100 text-rose-700{% else %}bg-amber-100 text-amber-700{% endif %}">
{{ recent.get_status_display }}
</span>
</div>
<p class="mt-2 text-xs text-gray-600">{{ recent.description|default:"-" }}</p>
</li>
{% empty %}
<li class="rounded-2xl border border-dashed border-gray-200 px-4 py-6 text-center text-sm text-gray-500">
{% trans "Inga aktiviteter än." %}
</li>
{% endfor %}
</ul>
</article>
<article class="rounded-3xl bg-white px-5 py-6 shadow-sm ring-1 ring-gray-100">
<header>
<p class="text-xs font-semibold uppercase tracking-wide text-gray-500">{% trans "Statusfördelning" %}</p>
<h2 class="text-xl font-semibold text-gray-900">{% trans "Snabbstatistik" %}</h2>
</header>
<dl class="mt-4 space-y-3 text-sm text-gray-700">
<div class="flex items-center justify-between rounded-2xl bg-slate-50 px-4 py-3">
<dt class="font-semibold text-amber-700">{% trans "Pending" %}</dt>
<dd class="text-lg font-semibold text-amber-700">{{ summary.pending_count }}</dd>
</div>
<div class="flex items-center justify-between rounded-2xl bg-green-50 px-4 py-3">
<dt class="font-semibold text-green-700">{% trans "Approved" %}</dt>
<dd class="text-lg font-semibold text-green-700">{{ summary.approved_count }}</dd>
</div>
<div class="flex items-center justify-between rounded-2xl bg-rose-50 px-4 py-3">
<dt class="font-semibold text-rose-700">{% trans "Rejected" %}</dt>
<dd class="text-lg font-semibold text-rose-700">{{ summary.rejected_count }}</dd>
</div>
</dl>
</article>
</aside>
</div>
</section>
<script>
document.addEventListener("DOMContentLoaded", () => {
const filterButtons = Array.from(document.querySelectorAll("[data-filter-button]"));
const cards = Array.from(document.querySelectorAll("[data-claim-card]"));
const emptyState = document.querySelector("[data-claim-empty]");
if (!filterButtons.length || !cards.length) {
return;
}
const activeClasses = ["bg-brand-600", "text-white", "hover:bg-brand-700"];
const inactiveClasses = ["bg-slate-100", "text-gray-700", "hover:bg-slate-200"];
const setButtonState = (activeValue) => {
filterButtons.forEach((btn) => {
const value = btn.dataset.filterValue || "all";
const isActive = value === activeValue;
btn.setAttribute("aria-pressed", String(isActive));
const classList = btn.classList;
if (isActive) {
inactiveClasses.forEach((cls) => classList.remove(cls));
activeClasses.forEach((cls) => classList.add(cls));
} else {
activeClasses.forEach((cls) => classList.remove(cls));
inactiveClasses.forEach((cls) => classList.add(cls));
}
});
};
const applyFilter = (filterValue) => {
const value = filterValue || "all";
let visibleCount = 0;
cards.forEach((card) => {
const matches = value === "all" || card.dataset.status === value;
card.classList.toggle("hidden", !matches);
if (matches) {
visibleCount += 1;
}
});
if (emptyState) {
emptyState.classList.toggle("hidden", visibleCount > 0);
}
setButtonState(value);
try {
const url = new URL(window.location.href);
if (value === "all") {
url.searchParams.delete("status");
} else {
url.searchParams.set("status", value);
}
window.history.replaceState({}, "", url);
} catch (error) {
// ignore history errors
}
};
filterButtons.forEach((btn) => {
btn.addEventListener("click", (event) => {
event.preventDefault();
applyFilter(btn.dataset.filterValue || "all");
});
});
const initialFilter = new URLSearchParams(window.location.search).get("status") || "all";
applyFilter(initialFilter);
});
</script>
{% endblock %}