Dashboard: - Neuer Endpoint GET /api/stats/dashboard mit allen Kennzahlen - Klickbare Kacheln: Vorlagen nach Typ, Ketten nach Status → navigieren zu Filterlisten - Umsetzungsquote als horizontaler Balken mit klickbaren Segmenten Abstimmungen: - Stimmverhalten-Tabelle klickbar: Fraktion oder Ja/Nein/Enthaltung → filtert - Neuer Endpoint GET /api/abstimmungen/details (?fraktion=&stimme=) mit Pagination - Neuer Endpoint GET /api/abstimmungen/vergleich (?f1=&f2=) für Koalitionsmatrix-Drill-Down - Koalitionsmatrix-Zellen klickbar → zeigt Abstimmungsvergleich beider Fraktionen Fraktions-Normalisierung: - fraktionen_mapping.py: 40+ DB-Varianten → kanonische Namen - 'Bündnis 90 / Die Grünen' / 'Bündnis 90/Die Grünen' / 'Grüne' → 'Grüne' - 'Die Linke' / 'Die Linke.' / 'Linke' → 'Linke' - BfHo-Varianten, Hagen Aktiv, Einzelvertreter etc. normalisiert - Mapping in allen Abstimmungs-Endpoints aktiv - ist_ratsfraktion Flag in Fraktionen-Response Closes #14
302 lines
11 KiB
Svelte
302 lines
11 KiB
Svelte
<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">2004–2026</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}
|