feat: Responsive Layout für Mobile (#12)
- Navigation: Hamburger-Menü mit Slide-Down auf Mobile - Vorlagen + Ketten: Tabelle → Card-Layout auf Mobile (<md) - Filter: vertikal gestackt auf kleinen Screens - Suchfeld + Typeahead: volle Breite auf Mobile - Vorlagen-Detail: Header + Sidebar responsive - Fraktionen-Detail: Tabelle → Cards auf Mobile - Abstimmungen: Stimmverhalten-Cards + scrollbare Koalitionsmatrix - Touch-Targets überall ≥44px - Keine horizontalen Scrollbars Closes #12
This commit is contained in:
parent
401cd3acb0
commit
31b1e1bd7e
@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import '../app.css';
|
||||
let { children } = $props();
|
||||
let menuOpen = $state(false);
|
||||
</script>
|
||||
|
||||
<div class="min-h-screen bg-gray-50">
|
||||
@ -8,11 +9,11 @@
|
||||
<nav class="bg-white border-b border-gray-200 shadow-sm">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between h-16">
|
||||
<div class="flex items-center space-x-8">
|
||||
<a href="/" class="text-xl font-bold text-gray-900">
|
||||
<div class="flex items-center">
|
||||
<a href="/" class="text-xl font-bold text-gray-900 shrink-0">
|
||||
Antragstracker <span class="text-green-600">Hagen</span>
|
||||
</a>
|
||||
<div class="hidden sm:flex space-x-4">
|
||||
<div class="hidden sm:flex sm:ml-8 space-x-4">
|
||||
<a href="/" class="text-gray-600 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium">Dashboard</a>
|
||||
<a href="/ketten" class="text-gray-600 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium">Ketten</a>
|
||||
<a href="/vorlagen" class="text-gray-600 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium">Vorlagen</a>
|
||||
@ -21,12 +22,45 @@
|
||||
<a href="/fraktionen" class="text-gray-600 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium">Fraktionen</a>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Hamburger Button -->
|
||||
<div class="flex items-center sm:hidden">
|
||||
<button
|
||||
onclick={() => menuOpen = !menuOpen}
|
||||
class="inline-flex items-center justify-center p-3 rounded-md text-gray-500 hover:text-gray-900 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-green-500"
|
||||
aria-expanded={menuOpen}
|
||||
aria-label="Hauptmenü"
|
||||
>
|
||||
{#if menuOpen}
|
||||
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
{:else}
|
||||
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Menu -->
|
||||
{#if menuOpen}
|
||||
<div class="sm:hidden border-t border-gray-200 bg-white">
|
||||
<div class="px-2 pt-2 pb-3 space-y-1">
|
||||
<a href="/" onclick={() => menuOpen = false} class="block text-gray-600 hover:text-gray-900 hover:bg-gray-50 px-3 py-3 rounded-md text-base font-medium">Dashboard</a>
|
||||
<a href="/ketten" onclick={() => menuOpen = false} class="block text-gray-600 hover:text-gray-900 hover:bg-gray-50 px-3 py-3 rounded-md text-base font-medium">Ketten</a>
|
||||
<a href="/vorlagen" onclick={() => menuOpen = false} class="block text-gray-600 hover:text-gray-900 hover:bg-gray-50 px-3 py-3 rounded-md text-base font-medium">Vorlagen</a>
|
||||
<a href="/abstimmungen" onclick={() => menuOpen = false} class="block text-gray-600 hover:text-gray-900 hover:bg-gray-50 px-3 py-3 rounded-md text-base font-medium">Abstimmungen</a>
|
||||
<a href="/karte" onclick={() => menuOpen = false} class="block text-gray-600 hover:text-gray-900 hover:bg-gray-50 px-3 py-3 rounded-md text-base font-medium">Karte</a>
|
||||
<a href="/fraktionen" onclick={() => menuOpen = false} class="block text-gray-600 hover:text-gray-900 hover:bg-gray-50 px-3 py-3 rounded-md text-base font-medium">Fraktionen</a>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</nav>
|
||||
|
||||
<!-- Content -->
|
||||
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6 sm:py-8">
|
||||
{@render children()}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@ -80,7 +80,7 @@
|
||||
<!-- Header -->
|
||||
<header class="bg-green-700 text-white py-6 shadow-lg">
|
||||
<div class="max-w-6xl mx-auto px-4">
|
||||
<h1 class="text-3xl font-bold">🏛️ Antragstracker Hagen</h1>
|
||||
<h1 class="text-2xl sm:text-3xl font-bold">🏛️ Antragstracker Hagen</h1>
|
||||
<p class="text-green-100 mt-1">Kommunale Anträge & Anfragen nachverfolgen</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@ -80,10 +80,11 @@
|
||||
</div>
|
||||
{:else}
|
||||
<!-- Fraktionen Übersicht -->
|
||||
<section class="bg-white rounded-xl shadow-sm border border-gray-200 p-6 mb-8">
|
||||
<section class="bg-white rounded-xl shadow-sm border border-gray-200 p-4 sm:p-6 mb-8">
|
||||
<h2 class="text-lg font-semibold text-gray-900 mb-4">📊 Stimmverhalten nach Fraktion</h2>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<!-- Desktop Table -->
|
||||
<div class="hidden md:block overflow-x-auto">
|
||||
<table class="w-full">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
@ -116,16 +117,49 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Cards -->
|
||||
<div class="md:hidden space-y-3">
|
||||
{#each fraktionen as f}
|
||||
<div class="border border-gray-200 rounded-lg p-4">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<span class="font-medium text-gray-900">{f.fraktion}</span>
|
||||
<span class="text-sm font-medium text-gray-600">{f.ja_quote}%</span>
|
||||
</div>
|
||||
<div class="bg-gray-200 rounded-full h-2.5 mb-3">
|
||||
<div class="bg-green-500 h-2.5 rounded-full" style="width: {f.ja_quote}%"></div>
|
||||
</div>
|
||||
<div class="grid grid-cols-4 gap-2 text-center text-xs">
|
||||
<div>
|
||||
<div class="font-medium text-green-600">{f.ja}</div>
|
||||
<div class="text-gray-500">Ja</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-medium text-red-600">{f.nein}</div>
|
||||
<div class="text-gray-500">Nein</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-medium text-yellow-600">{f.enthaltung}</div>
|
||||
<div class="text-gray-500">Enth.</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-medium text-gray-600">{f.gesamt}</div>
|
||||
<div class="text-gray-500">Gesamt</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Koalitionsmatrix -->
|
||||
<section class="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
|
||||
<section class="bg-white rounded-xl shadow-sm border border-gray-200 p-4 sm:p-6">
|
||||
<h2 class="text-lg font-semibold text-gray-900 mb-4">🤝 Koalitionsmatrix</h2>
|
||||
<p class="text-sm text-gray-500 mb-4">Wie oft stimmen Fraktionen gleich ab? (nur Ja/Nein-Stimmen)</p>
|
||||
|
||||
{#if koalitionsmatrix.length > 0}
|
||||
{@const allFraktionen = koalitionsmatrix.map(r => r.fraktion).sort()}
|
||||
<div class="overflow-x-auto">
|
||||
<div class="overflow-x-auto -mx-4 sm:mx-0 px-4 sm:px-0">
|
||||
<table class="text-xs">
|
||||
<thead>
|
||||
<tr>
|
||||
@ -170,7 +204,7 @@
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex items-center gap-4 text-xs text-gray-500">
|
||||
<div class="mt-4 flex flex-wrap items-center gap-3 sm:gap-4 text-xs text-gray-500">
|
||||
<span>Legende:</span>
|
||||
<span class="flex items-center gap-1"><span class="w-4 h-4 bg-green-500 rounded"></span> 90-100%</span>
|
||||
<span class="flex items-center gap-1"><span class="w-4 h-4 bg-green-400 rounded"></span> 70-90%</span>
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
<title>Fraktionen — Antragstracker Hagen</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="max-w-4xl mx-auto p-6">
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<h1 class="text-2xl font-bold mb-6">Fraktionen</h1>
|
||||
|
||||
{#if loading}
|
||||
|
||||
@ -49,7 +49,7 @@
|
||||
<title>{data?.partei?.name ?? kuerzel} — Antragstracker Hagen</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="max-w-6xl mx-auto p-6">
|
||||
<div class="max-w-6xl mx-auto">
|
||||
{#if loading && !data}
|
||||
<div class="text-gray-500">Laden...</div>
|
||||
{:else if error}
|
||||
@ -57,41 +57,41 @@
|
||||
{: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>
|
||||
<div class="w-3 h-12 rounded shrink-0" style="background-color: {data.partei.farbe || '#6b7280'}"></div>
|
||||
<div class="min-w-0">
|
||||
<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>
|
||||
<a href="/fraktionen" class="ml-auto text-sm text-blue-600 hover:underline shrink-0">← Alle Fraktionen</a>
|
||||
</div>
|
||||
|
||||
<!-- KPIs -->
|
||||
<div class="grid grid-cols-2 sm:grid-cols-4 gap-4 mb-8">
|
||||
<div class="grid grid-cols-2 sm:grid-cols-4 gap-3 sm: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 class="text-2xl sm:text-3xl font-bold">{data.total_antraege}</div>
|
||||
<div class="text-xs sm: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 class="text-2xl sm:text-3xl font-bold">{data.bewertet}</div>
|
||||
<div class="text-xs sm:text-sm text-gray-500">Mit Bewertung</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 class="text-2xl sm:text-3xl font-bold text-green-700">{u.anzahl}</div>
|
||||
<div class="text-xs sm: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 class="text-2xl sm:text-3xl font-bold text-red-700">{u.anzahl}</div>
|
||||
<div class="text-xs sm: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">
|
||||
<div class="bg-white rounded-lg border p-4 sm: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}
|
||||
@ -109,16 +109,16 @@
|
||||
{/each}
|
||||
</div>
|
||||
<!-- Legend -->
|
||||
<div class="flex flex-wrap gap-4 text-sm">
|
||||
<div class="flex flex-wrap gap-3 sm: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="flex items-center gap-1.5 hover:opacity-70 transition-opacity p-1"
|
||||
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>
|
||||
<span class="w-3 h-3 rounded-full inline-block shrink-0" style="background-color: {info.farbe}"></span>
|
||||
{info.label}: {u.anzahl}
|
||||
</button>
|
||||
{/if}
|
||||
@ -128,23 +128,23 @@
|
||||
{/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">
|
||||
<div class="flex flex-wrap gap-3 sm:gap-4 mb-4 items-center">
|
||||
<select bind:value={selectedJahr} onchange={loadData} class="border rounded px-3 py-2 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">
|
||||
<button onclick={() => filterKategorie = ''} class="text-sm text-blue-600 hover:underline p-1">
|
||||
Filter zurücksetzen
|
||||
</button>
|
||||
{/if}
|
||||
<span class="text-sm text-gray-500 ml-auto">{filteredAntraege.length} Anträge</span>
|
||||
<span class="text-sm text-gray-500 sm:ml-auto">{filteredAntraege.length} Anträge</span>
|
||||
</div>
|
||||
|
||||
<!-- Anträge-Liste -->
|
||||
<div class="bg-white rounded-lg border overflow-hidden">
|
||||
<!-- Desktop Table -->
|
||||
<div class="hidden md:block bg-white rounded-lg border overflow-hidden">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-gray-50 text-left">
|
||||
<tr>
|
||||
@ -176,5 +176,21 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Cards -->
|
||||
<div class="md:hidden space-y-3">
|
||||
{#each filteredAntraege as a}
|
||||
<a href="/vorlagen/{a.id}" class="block bg-white rounded-lg border p-4 hover:shadow-md transition-shadow">
|
||||
<div class="flex items-start justify-between gap-2 mb-2">
|
||||
<span class="font-mono text-xs text-blue-600">{a.aktenzeichen}</span>
|
||||
<span class="text-xs text-gray-500 shrink-0">{formatDate(a.datum_eingang)}</span>
|
||||
</div>
|
||||
<div class="text-sm text-gray-700 line-clamp-2 mb-2">{a.betreff}</div>
|
||||
{#if a.umsetzung_bewertung}
|
||||
<UmsetzungBadge kategorie={a.umsetzung_bewertung} score={a.umsetzung_score} />
|
||||
{/if}
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@ -81,44 +81,46 @@
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-4 mb-6">
|
||||
<div class="flex flex-wrap gap-3 items-end">
|
||||
<div>
|
||||
<div class="flex flex-col sm:flex-row flex-wrap gap-3 sm:items-end">
|
||||
<div class="w-full sm:w-auto">
|
||||
<label for="suche" class="block text-xs font-medium text-gray-500 mb-1">Suche</label>
|
||||
<input id="suche" type="text" bind:value={filterSuche} placeholder="Thema suchen..."
|
||||
class="border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-green-500 focus:border-green-500"
|
||||
class="border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-green-500 focus:border-green-500 w-full sm:w-auto"
|
||||
onkeydown={(e) => { if (e.key === 'Enter') applyFilters(); }} />
|
||||
</div>
|
||||
<div>
|
||||
<label for="status" class="block text-xs font-medium text-gray-500 mb-1">Status</label>
|
||||
<select id="status" bind:value={filterStatus} onchange={applyFilters}
|
||||
class="border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-green-500">
|
||||
<option value="">Alle</option>
|
||||
<option value="eingereicht">Eingereicht</option>
|
||||
<option value="in_beratung">In Beratung</option>
|
||||
<option value="vertagt">Vertagt</option>
|
||||
<option value="beschlossen">Beschlossen</option>
|
||||
<option value="umgesetzt">Umgesetzt</option>
|
||||
<option value="abgelehnt">Abgelehnt</option>
|
||||
<option value="versandet">Versandet</option>
|
||||
<option value="angefragt">Angefragt</option>
|
||||
<option value="beantwortet">Beantwortet</option>
|
||||
<option value="offen">Offen</option>
|
||||
<option value="abgewiegelt">Abgewiegelt</option>
|
||||
</select>
|
||||
<div class="flex gap-3 w-full sm:w-auto">
|
||||
<div class="flex-1 sm:flex-none">
|
||||
<label for="status" class="block text-xs font-medium text-gray-500 mb-1">Status</label>
|
||||
<select id="status" bind:value={filterStatus} onchange={applyFilters}
|
||||
class="border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-green-500 w-full">
|
||||
<option value="">Alle</option>
|
||||
<option value="eingereicht">Eingereicht</option>
|
||||
<option value="in_beratung">In Beratung</option>
|
||||
<option value="vertagt">Vertagt</option>
|
||||
<option value="beschlossen">Beschlossen</option>
|
||||
<option value="umgesetzt">Umgesetzt</option>
|
||||
<option value="abgelehnt">Abgelehnt</option>
|
||||
<option value="versandet">Versandet</option>
|
||||
<option value="angefragt">Angefragt</option>
|
||||
<option value="beantwortet">Beantwortet</option>
|
||||
<option value="offen">Offen</option>
|
||||
<option value="abgewiegelt">Abgewiegelt</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex-1 sm:flex-none">
|
||||
<label for="typ" class="block text-xs font-medium text-gray-500 mb-1">Typ</label>
|
||||
<select id="typ" bind:value={filterTyp} onchange={applyFilters}
|
||||
class="border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-green-500 w-full">
|
||||
<option value="">Alle</option>
|
||||
<option value="antrag">Antrag</option>
|
||||
<option value="anfrage">Anfrage</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="typ" class="block text-xs font-medium text-gray-500 mb-1">Typ</label>
|
||||
<select id="typ" bind:value={filterTyp} onchange={applyFilters}
|
||||
class="border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-green-500">
|
||||
<option value="">Alle</option>
|
||||
<option value="antrag">Antrag</option>
|
||||
<option value="anfrage">Anfrage</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<div class="w-full sm:w-auto">
|
||||
<label for="partei" class="block text-xs font-medium text-gray-500 mb-1">Partei</label>
|
||||
<select id="partei" bind:value={filterPartei} onchange={applyFilters}
|
||||
class="border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-green-500">
|
||||
class="border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-green-500 w-full sm:w-auto">
|
||||
<option value="">Alle</option>
|
||||
{#each parteien as p}
|
||||
<option value={p.kuerzel}>{p.kuerzel} ({p.anzahl})</option>
|
||||
@ -126,7 +128,7 @@
|
||||
</select>
|
||||
</div>
|
||||
<button onclick={applyFilters}
|
||||
class="bg-green-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-green-700 transition-colors">
|
||||
class="bg-green-600 text-white px-4 py-3 sm:py-2 rounded-lg text-sm font-medium hover:bg-green-700 transition-colors w-full sm:w-auto">
|
||||
Filtern
|
||||
</button>
|
||||
</div>
|
||||
@ -141,7 +143,8 @@
|
||||
{:else if data}
|
||||
<div class="text-sm text-gray-500 mb-3">{data.total} Ketten gefunden</div>
|
||||
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
|
||||
<!-- Desktop Table -->
|
||||
<div class="hidden md:block bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
|
||||
<table class="w-full">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
@ -172,17 +175,41 @@
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Cards -->
|
||||
<div class="md:hidden space-y-3">
|
||||
{#each data.items as kette}
|
||||
<a href="/ketten/{kette.id}" class="block bg-white rounded-xl shadow-sm border border-gray-200 p-4 hover:shadow-md transition-shadow">
|
||||
<div class="flex items-start justify-between gap-2 mb-2">
|
||||
<span class="font-mono text-sm font-medium text-green-700">
|
||||
{kette.ursprung?.aktenzeichen || `#${kette.id}`}
|
||||
</span>
|
||||
<StatusBadge status={kette.status} />
|
||||
</div>
|
||||
<div class="text-sm text-gray-700 line-clamp-2 mb-3">{kette.thema || '-'}</div>
|
||||
<div class="flex items-center justify-between text-xs text-gray-500">
|
||||
<div class="flex items-center gap-3">
|
||||
{#if kette.typ}
|
||||
<span class="capitalize">{kette.typ}</span>
|
||||
{/if}
|
||||
<span>{kette.glieder_count} Glieder</span>
|
||||
</div>
|
||||
<span>{formatDate(kette.letzte_aktivitaet)}</span>
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
{#if data.total > data.page_size}
|
||||
{@const totalPages = Math.ceil(data.total / data.page_size)}
|
||||
<div class="flex justify-center mt-6 space-x-2">
|
||||
<button disabled={currentPage <= 1} onclick={() => goPage(currentPage - 1)}
|
||||
class="px-3 py-2 rounded-lg text-sm border border-gray-300 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed">
|
||||
class="px-3 py-3 sm:py-2 rounded-lg text-sm border border-gray-300 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed">
|
||||
« Zurück
|
||||
</button>
|
||||
<span class="px-3 py-2 text-sm text-gray-600">Seite {currentPage} von {totalPages}</span>
|
||||
<span class="px-3 py-3 sm:py-2 text-sm text-gray-600">Seite {currentPage} von {totalPages}</span>
|
||||
<button disabled={currentPage >= totalPages} onclick={() => goPage(currentPage + 1)}
|
||||
class="px-3 py-2 rounded-lg text-sm border border-gray-300 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed">
|
||||
class="px-3 py-3 sm:py-2 rounded-lg text-sm border border-gray-300 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed">
|
||||
Weiter »
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@ -68,10 +68,10 @@
|
||||
<a href="/ketten" class="text-sm text-gray-500 hover:text-gray-700 mb-4 inline-block">← Zurück zur Liste</a>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6 mb-6">
|
||||
<div class="flex flex-wrap items-start justify-between gap-4">
|
||||
<div>
|
||||
<div class="flex items-center space-x-3 mb-2">
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-4 sm:p-6 mb-6">
|
||||
<div class="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-4">
|
||||
<div class="min-w-0">
|
||||
<div class="flex flex-wrap items-center gap-2 sm:gap-3 mb-2">
|
||||
{#if kette.ursprung?.aktenzeichen}
|
||||
<h1 class="text-2xl font-bold text-gray-900 font-mono">{kette.ursprung.aktenzeichen}</h1>
|
||||
{/if}
|
||||
@ -84,7 +84,7 @@
|
||||
<p class="text-gray-700">{kette.thema}</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="text-right text-sm text-gray-500 space-y-1">
|
||||
<div class="text-left sm:text-right text-sm text-gray-500 space-y-1 shrink-0">
|
||||
{#if kette.status_seit}
|
||||
<div>Status seit: <strong>{formatDate(kette.status_seit)}</strong></div>
|
||||
{/if}
|
||||
@ -174,8 +174,8 @@
|
||||
<h2 class="text-lg font-semibold text-gray-900 mb-4">Verknüpfte Vorlagen</h2>
|
||||
<div class="space-y-2">
|
||||
{#each kette.graph.nodes.filter(n => n.extern) as ext}
|
||||
<a href="/vorlagen/{ext.id}" class="flex items-center justify-between p-3 rounded-lg border border-gray-100 hover:bg-gray-50 transition-colors">
|
||||
<div class="flex items-center space-x-3">
|
||||
<a href="/vorlagen/{ext.id}" class="flex flex-col sm:flex-row sm:items-center sm:justify-between p-3 rounded-lg border border-gray-100 hover:bg-gray-50 transition-colors gap-2">
|
||||
<div class="flex flex-wrap items-center gap-2 sm:gap-3 min-w-0">
|
||||
{#if ext.aktenzeichen}
|
||||
<span class="font-mono text-sm font-medium text-green-700">{ext.aktenzeichen}</span>
|
||||
{/if}
|
||||
@ -183,7 +183,7 @@
|
||||
<span class="text-xs px-2 py-0.5 rounded bg-gray-100 text-gray-600 capitalize">{ext.typ}</span>
|
||||
{/if}
|
||||
{#if ext.betreff}
|
||||
<span class="text-sm text-gray-600 truncate max-w-md">{ext.betreff}</span>
|
||||
<span class="text-sm text-gray-600 truncate">{ext.betreff}</span>
|
||||
{/if}
|
||||
</div>
|
||||
<span class="text-xs text-gray-500">{formatDate(ext.datum_eingang)}</span>
|
||||
|
||||
@ -137,17 +137,17 @@
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-4 mb-6">
|
||||
<div class="flex flex-wrap gap-3 items-end">
|
||||
<div class="relative">
|
||||
<div class="flex flex-col sm:flex-row flex-wrap gap-3 sm:items-end">
|
||||
<div class="relative w-full sm:w-auto">
|
||||
<label for="suche" class="block text-xs font-medium text-gray-500 mb-1">Suche</label>
|
||||
<input id="suche" type="text" bind:value={filterSuche} placeholder="Volltextsuche..."
|
||||
class="border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-green-500 focus:border-green-500 w-64"
|
||||
class="border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-green-500 focus:border-green-500 w-full sm:w-64"
|
||||
oninput={onSucheInput}
|
||||
onkeydown={onSucheKeydown}
|
||||
onfocusout={() => { setTimeout(() => showSuggestions = false, 200); }}
|
||||
autocomplete="off" />
|
||||
{#if showSuggestions}
|
||||
<div class="absolute z-50 top-full left-0 mt-1 w-96 bg-white rounded-lg shadow-lg border border-gray-200 max-h-80 overflow-y-auto">
|
||||
<div class="absolute z-50 top-full left-0 mt-1 w-full sm:w-96 bg-white rounded-lg shadow-lg border border-gray-200 max-h-80 overflow-y-auto">
|
||||
{#each suggestions as s, i}
|
||||
<button
|
||||
class="w-full text-left px-3 py-2 hover:bg-green-50 border-b border-gray-100 last:border-0 transition-colors {i === selectedIdx ? 'bg-green-50' : ''}"
|
||||
@ -165,29 +165,31 @@
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div>
|
||||
<label for="typ" class="block text-xs font-medium text-gray-500 mb-1">Typ</label>
|
||||
<select id="typ" bind:value={filterTyp} onchange={applyFilters}
|
||||
class="border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-green-500">
|
||||
<option value="">Alle</option>
|
||||
<option value="antrag">Antrag</option>
|
||||
<option value="anfrage">Anfrage</option>
|
||||
<option value="stellungnahme">Stellungnahme</option>
|
||||
<option value="bericht">Bericht</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="partei" class="block text-xs font-medium text-gray-500 mb-1">Partei</label>
|
||||
<select id="partei" bind:value={filterPartei} onchange={applyFilters}
|
||||
class="border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-green-500">
|
||||
<option value="">Alle</option>
|
||||
{#each parteien as p}
|
||||
<option value={p.kuerzel}>{p.kuerzel} ({p.anzahl})</option>
|
||||
{/each}
|
||||
</select>
|
||||
<div class="flex gap-3 w-full sm:w-auto">
|
||||
<div class="flex-1 sm:flex-none">
|
||||
<label for="typ" class="block text-xs font-medium text-gray-500 mb-1">Typ</label>
|
||||
<select id="typ" bind:value={filterTyp} onchange={applyFilters}
|
||||
class="border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-green-500 w-full">
|
||||
<option value="">Alle</option>
|
||||
<option value="antrag">Antrag</option>
|
||||
<option value="anfrage">Anfrage</option>
|
||||
<option value="stellungnahme">Stellungnahme</option>
|
||||
<option value="bericht">Bericht</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex-1 sm:flex-none">
|
||||
<label for="partei" class="block text-xs font-medium text-gray-500 mb-1">Partei</label>
|
||||
<select id="partei" bind:value={filterPartei} onchange={applyFilters}
|
||||
class="border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-green-500 w-full">
|
||||
<option value="">Alle</option>
|
||||
{#each parteien as p}
|
||||
<option value={p.kuerzel}>{p.kuerzel} ({p.anzahl})</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<button onclick={applyFilters}
|
||||
class="bg-green-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-green-700 transition-colors">
|
||||
class="bg-green-600 text-white px-4 py-3 sm:py-2 rounded-lg text-sm font-medium hover:bg-green-700 transition-colors w-full sm:w-auto">
|
||||
Filtern
|
||||
</button>
|
||||
</div>
|
||||
@ -202,7 +204,8 @@
|
||||
{:else if data}
|
||||
<div class="text-sm text-gray-500 mb-3">{data.total} Vorlagen gefunden</div>
|
||||
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
|
||||
<!-- Desktop Table -->
|
||||
<div class="hidden md:block bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
|
||||
<table class="w-full">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
@ -261,17 +264,60 @@
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Cards -->
|
||||
<div class="md:hidden space-y-3">
|
||||
{#each data.items as v}
|
||||
<a href="/vorlagen/{v.id}" class="block bg-white rounded-xl shadow-sm border border-gray-200 p-4 hover:shadow-md transition-shadow">
|
||||
<div class="flex items-start justify-between gap-2 mb-2">
|
||||
<span class="font-mono text-sm font-medium text-green-700">
|
||||
{#if filterSuche}
|
||||
{@html highlight(v.aktenzeichen || `#${v.id}`, filterSuche)}
|
||||
{:else}
|
||||
{v.aktenzeichen || `#${v.id}`}
|
||||
{/if}
|
||||
</span>
|
||||
<span class="text-xs text-gray-500 shrink-0">{formatDate(v.datum_eingang)}</span>
|
||||
</div>
|
||||
<div class="text-sm text-gray-700 line-clamp-2 mb-2">
|
||||
{#if filterSuche}
|
||||
{@html highlight(v.betreff, filterSuche)}
|
||||
{:else}
|
||||
{v.betreff || '-'}
|
||||
{/if}
|
||||
</div>
|
||||
{#if v.snippet}
|
||||
<div class="text-xs text-gray-500 line-clamp-2 mb-2">{@html v.snippet}</div>
|
||||
{/if}
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex flex-wrap gap-1">
|
||||
{#if v.antragsteller?.length}
|
||||
{#each v.antragsteller as a}
|
||||
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium"
|
||||
style="background-color: {a.farbe || '#6b7280'}20; color: {a.farbe || '#6b7280'}; border: 1px solid {a.farbe || '#6b7280'}40;">
|
||||
{a.kuerzel}
|
||||
</span>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
{#if v.typ}
|
||||
<span class="text-xs text-gray-500 capitalize">{v.typ}</span>
|
||||
{/if}
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
{#if data.total > data.page_size}
|
||||
{@const totalPages = Math.ceil(data.total / data.page_size)}
|
||||
<div class="flex justify-center mt-6 space-x-2">
|
||||
<button disabled={currentPage <= 1} onclick={() => goPage(currentPage - 1)}
|
||||
class="px-3 py-2 rounded-lg text-sm border border-gray-300 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed">
|
||||
class="px-3 py-3 sm:py-2 rounded-lg text-sm border border-gray-300 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed">
|
||||
« Zurück
|
||||
</button>
|
||||
<span class="px-3 py-2 text-sm text-gray-600">Seite {currentPage} von {totalPages}</span>
|
||||
<span class="px-3 py-3 sm:py-2 text-sm text-gray-600">Seite {currentPage} von {totalPages}</span>
|
||||
<button disabled={currentPage >= totalPages} onclick={() => goPage(currentPage + 1)}
|
||||
class="px-3 py-2 rounded-lg text-sm border border-gray-300 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed">
|
||||
class="px-3 py-3 sm:py-2 rounded-lg text-sm border border-gray-300 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed">
|
||||
Weiter »
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@ -75,9 +75,9 @@
|
||||
|
||||
<!-- Header -->
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6 mb-6">
|
||||
<div class="flex flex-wrap items-start justify-between gap-4">
|
||||
<div>
|
||||
<div class="flex items-center space-x-3 mb-2">
|
||||
<div class="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-4">
|
||||
<div class="min-w-0">
|
||||
<div class="flex flex-wrap items-center gap-2 sm:gap-3 mb-2">
|
||||
{#if vorlage.aktenzeichen}
|
||||
<h1 class="text-2xl font-bold text-gray-900 font-mono">{vorlage.aktenzeichen}</h1>
|
||||
{/if}
|
||||
@ -95,7 +95,7 @@
|
||||
<p class="text-sm text-gray-500 mt-1">Thema: {vorlage.thema_kurz}</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="text-right text-sm text-gray-500 space-y-1">
|
||||
<div class="text-left sm:text-right text-sm text-gray-500 space-y-1 shrink-0">
|
||||
{#if vorlage.datum_eingang}
|
||||
<div>Eingegangen: <strong>{formatDate(vorlage.datum_eingang)}</strong></div>
|
||||
{/if}
|
||||
@ -119,7 +119,7 @@
|
||||
{/if}
|
||||
|
||||
<!-- External links -->
|
||||
<div class="mt-4 flex space-x-4">
|
||||
<div class="mt-4 flex flex-wrap gap-4">
|
||||
{#if vorlage.web_url}
|
||||
<a href={vorlage.web_url} target="_blank" rel="noopener"
|
||||
class="text-sm text-green-600 hover:underline">ALLRIS ↗</a>
|
||||
@ -297,7 +297,7 @@
|
||||
<h2 class="text-lg font-semibold text-gray-900 mb-4">Beratungsfolge</h2>
|
||||
<div class="space-y-3">
|
||||
{#each vorlage.beratungen as b}
|
||||
<div class="flex items-start justify-between p-3 rounded-lg border border-gray-100">
|
||||
<div class="flex flex-col sm:flex-row sm:items-start sm:justify-between p-3 rounded-lg border border-gray-100 gap-2">
|
||||
<div>
|
||||
{#if b.gremium}
|
||||
<span class="text-sm font-medium text-gray-900">{b.gremium.name}</span>
|
||||
@ -320,7 +320,7 @@
|
||||
<p class="text-xs text-gray-500 mt-1">{b.ergebnis_text}</p>
|
||||
{/if}
|
||||
</div>
|
||||
<span class="text-xs text-gray-500 flex-shrink-0 ml-4">{formatDate(b.sitzung_datum)}</span>
|
||||
<span class="text-xs text-gray-500 flex-shrink-0 sm:ml-4">{formatDate(b.sitzung_datum)}</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user