antragstracker/frontend/src/routes/fraktionen/[kuerzel]/+page.svelte

181 lines
6.1 KiB
Svelte
Raw Normal View History

<script lang="ts">
import { page } from '$app/stores';
import { fetchFraktionDashboard, type FraktionDashboard } from '$lib/api';
import UmsetzungBadge from '$lib/components/UmsetzungBadge.svelte';
import { KATEGORIEN } from '$lib/umsetzung';
import { formatDate } from '$lib/status';
import { onMount } from 'svelte';
let data = $state<FraktionDashboard | null>(null);
let loading = $state(true);
let error = $state<string | null>(null);
let selectedJahr = $state<string>('');
let filterKategorie = $state<string>('');
let kuerzel = $derived($page.params.kuerzel);
async function loadData() {
loading = true;
error = null;
try {
data = await fetchFraktionDashboard(kuerzel, selectedJahr || undefined);
} catch (e) {
error = (e as Error).message;
}
loading = false;
}
onMount(loadData);
// Reload when filters change
$effect(() => {
if (kuerzel) loadData();
});
let filteredAntraege = $derived(
data?.antraege.filter(a => !filterKategorie || a.umsetzung_bewertung === filterKategorie) ?? []
);
// Calculate percentages for bar chart
let umsetzungPct = $derived(() => {
if (!data || data.bewertet === 0) return [];
return data.umsetzung.map(u => ({
...u,
pct: ((u.anzahl / data!.bewertet) * 100).toFixed(1),
}));
});
</script>
<svelte:head>
<title>{data?.partei?.name ?? kuerzel} — Antragstracker Hagen</title>
</svelte:head>
<div class="max-w-6xl mx-auto p-6">
{#if loading && !data}
<div class="text-gray-500">Laden...</div>
{:else if error}
<div class="text-red-600">Fehler: {error}</div>
{:else if data}
<!-- Header -->
<div class="flex items-center gap-4 mb-6">
<div class="w-3 h-12 rounded" style="background-color: {data.partei.farbe || '#6b7280'}"></div>
<div>
<h1 class="text-2xl font-bold">{data.partei.name}</h1>
<span class="text-sm text-gray-500">{data.partei.kuerzel}</span>
</div>
<a href="/fraktionen" class="ml-auto text-sm text-blue-600 hover:underline">← Alle Fraktionen</a>
</div>
<!-- KPIs -->
<div class="grid grid-cols-2 sm:grid-cols-4 gap-4 mb-8">
<div class="bg-white rounded-lg border p-4">
<div class="text-3xl font-bold">{data.total_antraege}</div>
<div class="text-sm text-gray-500">Anträge gesamt</div>
</div>
<div class="bg-white rounded-lg border p-4">
<div class="text-3xl font-bold">{data.bewertet}</div>
<div class="text-sm text-gray-500">Mit Umsetzungsbewertung</div>
</div>
{#each data.umsetzung.filter(u => u.bewertung === 'erfuellt') as u}
<div class="bg-green-50 rounded-lg border border-green-200 p-4">
<div class="text-3xl font-bold text-green-700">{u.anzahl}</div>
<div class="text-sm text-green-600">Erfüllt</div>
</div>
{/each}
{#each data.umsetzung.filter(u => u.bewertung === 'nebelkerze') as u}
<div class="bg-red-50 rounded-lg border border-red-200 p-4">
<div class="text-3xl font-bold text-red-700">{u.anzahl}</div>
<div class="text-sm text-red-600">Nebelkerzen</div>
</div>
{/each}
</div>
<!-- Umsetzungs-Übersicht (Horizontal Bar) -->
{#if data.bewertet > 0}
<div class="bg-white rounded-lg border p-6 mb-8">
<h2 class="font-bold mb-4">Umsetzungsquote</h2>
<div class="flex rounded-full overflow-hidden h-8 mb-4">
{#each data.umsetzung as u}
{@const info = KATEGORIEN[u.bewertung as keyof typeof KATEGORIEN]}
{@const pct = (u.anzahl / data.bewertet) * 100}
{#if info && pct > 0}
<div
class="flex items-center justify-center text-xs font-medium text-white transition-all"
style="width: {pct}%; background-color: {info.farbe};"
title="{info.label}: {u.anzahl} ({pct.toFixed(1)}%)"
>
{#if pct > 8}{info.label} {pct.toFixed(0)}%{/if}
</div>
{/if}
{/each}
</div>
<!-- Legend -->
<div class="flex flex-wrap gap-4 text-sm">
{#each data.umsetzung as u}
{@const info = KATEGORIEN[u.bewertung as keyof typeof KATEGORIEN]}
{#if info}
<button
class="flex items-center gap-1.5 hover:opacity-70 transition-opacity"
class:opacity-40={filterKategorie && filterKategorie !== u.bewertung}
onclick={() => filterKategorie = filterKategorie === u.bewertung ? '' : u.bewertung}
>
<span class="w-3 h-3 rounded-full inline-block" style="background-color: {info.farbe}"></span>
{info.label}: {u.anzahl}
</button>
{/if}
{/each}
</div>
</div>
{/if}
<!-- Filters -->
<div class="flex gap-4 mb-4">
<select bind:value={selectedJahr} onchange={loadData} class="border rounded px-3 py-1.5 text-sm">
<option value="">Alle Jahre</option>
{#each data.jahre as j}
<option value={j}>{j}</option>
{/each}
</select>
{#if filterKategorie}
<button onclick={() => filterKategorie = ''} class="text-sm text-blue-600 hover:underline">
Filter zurücksetzen
</button>
{/if}
<span class="text-sm text-gray-500 ml-auto">{filteredAntraege.length} Anträge</span>
</div>
<!-- Anträge-Liste -->
<div class="bg-white rounded-lg border overflow-hidden">
<table class="w-full text-sm">
<thead class="bg-gray-50 text-left">
<tr>
<th class="px-4 py-3 font-medium">Aktenzeichen</th>
<th class="px-4 py-3 font-medium">Betreff</th>
<th class="px-4 py-3 font-medium">Datum</th>
<th class="px-4 py-3 font-medium">Umsetzung</th>
</tr>
</thead>
<tbody class="divide-y">
{#each filteredAntraege as a}
<tr class="hover:bg-gray-50">
<td class="px-4 py-3">
<a href="/vorlagen/{a.id}" class="text-blue-600 hover:underline font-mono text-xs">
{a.aktenzeichen}
</a>
</td>
<td class="px-4 py-3 max-w-md truncate">{a.betreff}</td>
<td class="px-4 py-3 text-gray-500 whitespace-nowrap">{formatDate(a.datum_eingang)}</td>
<td class="px-4 py-3">
{#if a.umsetzung_bewertung}
<UmsetzungBadge kategorie={a.umsetzung_bewertung} score={a.umsetzung_score} />
{:else}
<span class="text-gray-400 text-xs"></span>
{/if}
</td>
</tr>
{/each}
</tbody>
</table>
</div>
{/if}
</div>