antragstracker/frontend/src/routes/+page.svelte

302 lines
11 KiB
Svelte
Raw Normal View History

<script lang="ts">
import '../app.css';
import { goto } from '$app/navigation';
interface Vorlage {
id: number;
aktenzeichen: string;
typ: string;
betreff: string;
datum_eingang: string;
ist_verwaltungsvorlage: boolean;
}
interface DashboardStats {
vorlagen_total: number;
ketten_total: number;
vorlagen_nach_typ: { typ: string; anzahl: number }[];
ketten_nach_status: { status: string; anzahl: number }[];
abstimmungen_total: number;
umsetzungsquote: {
umgesetzt: number;
teilweise: number;
versandet: number;
beschlossen: number;
abgelehnt: number;
total_bewertet: number;
};
}
let stats = $state<DashboardStats | null>(null);
let antraege = $state<Vorlage[]>([]);
let loading = $state(true);
let error = $state('');
const API_BASE = typeof window !== 'undefined'
? (window.location.port === '5173'
? `http://${window.location.hostname}:8099/api`
: '/api')
: '/api';
const statusColors: Record<string, string> = {
umgesetzt: 'bg-green-100 text-green-800 border-green-200',
teilweise_umgesetzt: 'bg-amber-100 text-amber-800 border-amber-200',
versandet: 'bg-gray-100 text-gray-700 border-gray-200',
beschlossen: 'bg-blue-100 text-blue-800 border-blue-200',
abgelehnt: 'bg-red-100 text-red-800 border-red-200',
in_beratung: 'bg-purple-100 text-purple-800 border-purple-200',
angefragt: 'bg-cyan-100 text-cyan-800 border-cyan-200',
beantwortet: 'bg-teal-100 text-teal-800 border-teal-200',
verwiesen: 'bg-indigo-100 text-indigo-800 border-indigo-200',
offen: 'bg-yellow-100 text-yellow-800 border-yellow-200',
};
const statusLabels: Record<string, string> = {
umgesetzt: 'Umgesetzt',
teilweise_umgesetzt: 'Teilw. umgesetzt',
versandet: 'Versandet',
beschlossen: 'Beschlossen',
abgelehnt: 'Abgelehnt',
in_beratung: 'In Beratung',
angefragt: 'Angefragt',
beantwortet: 'Beantwortet',
verwiesen: 'Verwiesen',
offen: 'Offen',
};
const typLabels: Record<string, string> = {
antrag: 'Anträge',
anfrage: 'Anfragen',
bericht: 'Berichte',
beschlussvorlage: 'Beschlussvorlagen',
mitteilungsvorlage: 'Mitteilungen',
stellungnahme: 'Stellungnahmen',
sonstig: 'Sonstige',
};
const typIcons: Record<string, string> = {
antrag: '📋',
anfrage: '❓',
bericht: '📄',
beschlussvorlage: '📑',
mitteilungsvorlage: '📨',
stellungnahme: '💬',
sonstig: '📁',
};
// Umsetzungsquote bar colors
const umsetzungBarColors: Record<string, string> = {
umgesetzt: 'bg-green-500',
teilweise: 'bg-amber-400',
beschlossen: 'bg-blue-400',
versandet: 'bg-gray-400',
abgelehnt: 'bg-red-400',
};
async function loadData() {
try {
const [dashRes, antraegeRes] = await Promise.all([
fetch(`${API_BASE}/stats/dashboard`),
fetch(`${API_BASE}/vorlagen?typ=antrag&page_size=10`),
]);
if (dashRes.ok) {
stats = await dashRes.json();
} else {
error = `Dashboard-Stats fehler: ${dashRes.status}`;
}
if (antraegeRes.ok) {
const data = await antraegeRes.json();
antraege = data.items;
}
} catch (e) {
error = `Fehler: ${e}`;
} finally {
loading = false;
}
}
loadData();
</script>
<svelte:head>
<title>Antragstracker Hagen</title>
</svelte:head>
<div class="mb-6">
<h1 class="text-2xl font-bold text-gray-900">🏛️ Dashboard</h1>
<p class="text-gray-500 text-sm mt-1">Kommunale Anträge & Anfragen auf einen Blick</p>
</div>
{#if error}
<div class="bg-red-50 text-red-700 p-4 rounded-lg mb-6">{error}</div>
{/if}
{#if loading}
<div class="flex justify-center py-20">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-green-600"></div>
</div>
{:else if stats}
<!-- Hauptzahlen -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
<button onclick={() => goto('/vorlagen')}
class="bg-white rounded-xl shadow-sm border border-gray-200 p-5 text-center cursor-pointer hover:shadow-md transition-shadow">
<div class="text-3xl font-bold text-green-600">{stats.vorlagen_total.toLocaleString()}</div>
<div class="text-gray-500 text-sm mt-1">Vorlagen</div>
</button>
<button onclick={() => goto('/ketten')}
class="bg-white rounded-xl shadow-sm border border-gray-200 p-5 text-center cursor-pointer hover:shadow-md transition-shadow">
<div class="text-3xl font-bold text-blue-600">{stats.ketten_total.toLocaleString()}</div>
<div class="text-gray-500 text-sm mt-1">Ketten</div>
</button>
<button onclick={() => goto('/abstimmungen')}
class="bg-white rounded-xl shadow-sm border border-gray-200 p-5 text-center cursor-pointer hover:shadow-md transition-shadow">
<div class="text-3xl font-bold text-purple-600">{stats.abstimmungen_total.toLocaleString()}</div>
<div class="text-gray-500 text-sm mt-1">Abstimmungen</div>
</button>
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-5 text-center">
<div class="text-3xl font-bold text-orange-600">20042026</div>
<div class="text-gray-500 text-sm mt-1">Zeitraum</div>
</div>
</div>
<!-- Vorlagen nach Typ -->
<section class="bg-white rounded-xl shadow-sm border border-gray-200 p-5 mb-8">
<h2 class="text-lg font-semibold text-gray-900 mb-4">📊 Vorlagen nach Typ</h2>
<div class="grid grid-cols-2 md:grid-cols-4 gap-3">
{#each stats.vorlagen_nach_typ as vt}
<button onclick={() => goto(`/vorlagen?typ=${vt.typ}`)}
class="border border-gray-200 rounded-lg p-4 text-left cursor-pointer hover:shadow-md hover:border-green-300 transition-all group">
<div class="text-2xl mb-1">{typIcons[vt.typ] || '📁'}</div>
<div class="text-xl font-bold text-gray-900 group-hover:text-green-700">{vt.anzahl.toLocaleString()}</div>
<div class="text-sm text-gray-500">{typLabels[vt.typ] || vt.typ}</div>
</button>
{/each}
</div>
</section>
<!-- Ketten nach Status -->
<section class="bg-white rounded-xl shadow-sm border border-gray-200 p-5 mb-8">
<h2 class="text-lg font-semibold text-gray-900 mb-4">🔗 Ketten nach Status</h2>
<div class="grid grid-cols-2 md:grid-cols-4 gap-3">
{#each stats.ketten_nach_status as ks}
<button onclick={() => goto(`/ketten?status=${ks.status}`)}
class="border rounded-lg p-4 text-left cursor-pointer hover:shadow-md transition-all {statusColors[ks.status] || 'bg-gray-50 text-gray-700 border-gray-200'}">
<div class="text-xl font-bold">{ks.anzahl.toLocaleString()}</div>
<div class="text-sm opacity-80">{statusLabels[ks.status] || ks.status}</div>
</button>
{/each}
</div>
</section>
<!-- Umsetzungsquote -->
{#if stats.umsetzungsquote.total_bewertet > 0}
{@const uq = stats.umsetzungsquote}
<section class="bg-white rounded-xl shadow-sm border border-gray-200 p-5 mb-8">
<h2 class="text-lg font-semibold text-gray-900 mb-1">📈 Umsetzungsquote</h2>
<p class="text-sm text-gray-500 mb-4">{uq.total_bewertet} Ketten mit Endergebnis</p>
<!-- Bar -->
<div class="flex rounded-full overflow-hidden h-8 mb-4">
{#if uq.umgesetzt > 0}
<button onclick={() => goto('/ketten?status=umgesetzt')}
class="{umsetzungBarColors.umgesetzt} hover:brightness-110 transition-all cursor-pointer flex items-center justify-center text-white text-xs font-medium"
style="width: {uq.umgesetzt / uq.total_bewertet * 100}%"
title="Umgesetzt: {uq.umgesetzt}">
{#if uq.umgesetzt / uq.total_bewertet > 0.05}{uq.umgesetzt}{/if}
</button>
{/if}
{#if uq.teilweise > 0}
<button onclick={() => goto('/ketten?status=teilweise_umgesetzt')}
class="{umsetzungBarColors.teilweise} hover:brightness-110 transition-all cursor-pointer flex items-center justify-center text-white text-xs font-medium"
style="width: {uq.teilweise / uq.total_bewertet * 100}%"
title="Teilweise umgesetzt: {uq.teilweise}">
{#if uq.teilweise / uq.total_bewertet > 0.05}{uq.teilweise}{/if}
</button>
{/if}
{#if uq.beschlossen > 0}
<button onclick={() => goto('/ketten?status=beschlossen')}
class="{umsetzungBarColors.beschlossen} hover:brightness-110 transition-all cursor-pointer flex items-center justify-center text-white text-xs font-medium"
style="width: {uq.beschlossen / uq.total_bewertet * 100}%"
title="Beschlossen: {uq.beschlossen}">
{#if uq.beschlossen / uq.total_bewertet > 0.05}{uq.beschlossen}{/if}
</button>
{/if}
{#if uq.versandet > 0}
<button onclick={() => goto('/ketten?status=versandet')}
class="{umsetzungBarColors.versandet} hover:brightness-110 transition-all cursor-pointer flex items-center justify-center text-white text-xs font-medium"
style="width: {uq.versandet / uq.total_bewertet * 100}%"
title="Versandet: {uq.versandet}">
{#if uq.versandet / uq.total_bewertet > 0.05}{uq.versandet}{/if}
</button>
{/if}
{#if uq.abgelehnt > 0}
<button onclick={() => goto('/ketten?status=abgelehnt')}
class="{umsetzungBarColors.abgelehnt} hover:brightness-110 transition-all cursor-pointer flex items-center justify-center text-white text-xs font-medium"
style="width: {uq.abgelehnt / uq.total_bewertet * 100}%"
title="Abgelehnt: {uq.abgelehnt}">
{#if uq.abgelehnt / uq.total_bewertet > 0.05}{uq.abgelehnt}{/if}
</button>
{/if}
</div>
<!-- Legend -->
<div class="flex flex-wrap gap-4 text-sm">
<span class="flex items-center gap-1.5">
<span class="w-3 h-3 rounded-full bg-green-500"></span>
Umgesetzt ({uq.umgesetzt})
</span>
<span class="flex items-center gap-1.5">
<span class="w-3 h-3 rounded-full bg-amber-400"></span>
Teilweise ({uq.teilweise})
</span>
<span class="flex items-center gap-1.5">
<span class="w-3 h-3 rounded-full bg-blue-400"></span>
Beschlossen ({uq.beschlossen})
</span>
<span class="flex items-center gap-1.5">
<span class="w-3 h-3 rounded-full bg-gray-400"></span>
Versandet ({uq.versandet})
</span>
<span class="flex items-center gap-1.5">
<span class="w-3 h-3 rounded-full bg-red-400"></span>
Abgelehnt ({uq.abgelehnt})
</span>
</div>
</section>
{/if}
<!-- Aktuelle Anträge -->
<section class="bg-white rounded-xl shadow-sm border border-gray-200">
<div class="px-5 py-4 border-b border-gray-200 flex items-center justify-between">
<h2 class="text-lg font-semibold text-gray-900">📋 Aktuelle Anträge</h2>
<a href="/vorlagen?typ=antrag" class="text-sm text-green-600 hover:text-green-800 font-medium">Alle →</a>
</div>
{#if antraege.length === 0}
<div class="p-6 text-center text-gray-500">Keine Anträge gefunden</div>
{:else}
<ul class="divide-y divide-gray-100">
{#each antraege as antrag}
<li>
<a href="/vorlagen/{antrag.id}" class="block px-5 py-4 hover:bg-gray-50 transition-colors">
<div class="flex justify-between items-start">
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2 flex-wrap">
<span class="font-mono text-sm text-green-700 bg-green-50 px-2 py-0.5 rounded">
{antrag.aktenzeichen}
</span>
<span class="text-xs text-gray-400">{antrag.datum_eingang}</span>
</div>
<p class="mt-1 text-gray-700 text-sm line-clamp-2">{antrag.betreff}</p>
</div>
</div>
</a>
</li>
{/each}
</ul>
{/if}
</section>
{/if}