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:
Dotty Dotter 2026-04-01 14:21:06 +02:00
parent 401cd3acb0
commit 31b1e1bd7e
9 changed files with 271 additions and 114 deletions

View File

@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import '../app.css'; import '../app.css';
let { children } = $props(); let { children } = $props();
let menuOpen = $state(false);
</script> </script>
<div class="min-h-screen bg-gray-50"> <div class="min-h-screen bg-gray-50">
@ -8,11 +9,11 @@
<nav class="bg-white border-b border-gray-200 shadow-sm"> <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="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between h-16"> <div class="flex justify-between h-16">
<div class="flex items-center space-x-8"> <div class="flex items-center">
<a href="/" class="text-xl font-bold text-gray-900"> <a href="/" class="text-xl font-bold text-gray-900 shrink-0">
Antragstracker <span class="text-green-600">Hagen</span> Antragstracker <span class="text-green-600">Hagen</span>
</a> </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="/" 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="/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> <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> <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>
</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> </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> </nav>
<!-- Content --> <!-- 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()} {@render children()}
</main> </main>
</div> </div>

View File

@ -80,7 +80,7 @@
<!-- Header --> <!-- Header -->
<header class="bg-green-700 text-white py-6 shadow-lg"> <header class="bg-green-700 text-white py-6 shadow-lg">
<div class="max-w-6xl mx-auto px-4"> <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> <p class="text-green-100 mt-1">Kommunale Anträge & Anfragen nachverfolgen</p>
</div> </div>
</header> </header>

View File

@ -80,10 +80,11 @@
</div> </div>
{:else} {:else}
<!-- Fraktionen Übersicht --> <!-- 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> <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"> <table class="w-full">
<thead class="bg-gray-50"> <thead class="bg-gray-50">
<tr> <tr>
@ -116,16 +117,49 @@
</tbody> </tbody>
</table> </table>
</div> </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> </section>
<!-- Koalitionsmatrix --> <!-- 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> <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> <p class="text-sm text-gray-500 mb-4">Wie oft stimmen Fraktionen gleich ab? (nur Ja/Nein-Stimmen)</p>
{#if koalitionsmatrix.length > 0} {#if koalitionsmatrix.length > 0}
{@const allFraktionen = koalitionsmatrix.map(r => r.fraktion).sort()} {@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"> <table class="text-xs">
<thead> <thead>
<tr> <tr>
@ -170,7 +204,7 @@
</table> </table>
</div> </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>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-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> <span class="flex items-center gap-1"><span class="w-4 h-4 bg-green-400 rounded"></span> 70-90%</span>

View File

@ -15,7 +15,7 @@
<title>Fraktionen — Antragstracker Hagen</title> <title>Fraktionen — Antragstracker Hagen</title>
</svelte:head> </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> <h1 class="text-2xl font-bold mb-6">Fraktionen</h1>
{#if loading} {#if loading}

View File

@ -49,7 +49,7 @@
<title>{data?.partei?.name ?? kuerzel} — Antragstracker Hagen</title> <title>{data?.partei?.name ?? kuerzel} — Antragstracker Hagen</title>
</svelte:head> </svelte:head>
<div class="max-w-6xl mx-auto p-6"> <div class="max-w-6xl mx-auto">
{#if loading && !data} {#if loading && !data}
<div class="text-gray-500">Laden...</div> <div class="text-gray-500">Laden...</div>
{:else if error} {:else if error}
@ -57,41 +57,41 @@
{:else if data} {:else if data}
<!-- Header --> <!-- Header -->
<div class="flex items-center gap-4 mb-6"> <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 class="w-3 h-12 rounded shrink-0" style="background-color: {data.partei.farbe || '#6b7280'}"></div>
<div> <div class="min-w-0">
<h1 class="text-2xl font-bold">{data.partei.name}</h1> <h1 class="text-2xl font-bold">{data.partei.name}</h1>
<span class="text-sm text-gray-500">{data.partei.kuerzel}</span> <span class="text-sm text-gray-500">{data.partei.kuerzel}</span>
</div> </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> </div>
<!-- KPIs --> <!-- 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="bg-white rounded-lg border p-4">
<div class="text-3xl font-bold">{data.total_antraege}</div> <div class="text-2xl sm:text-3xl font-bold">{data.total_antraege}</div>
<div class="text-sm text-gray-500">Anträge gesamt</div> <div class="text-xs sm:text-sm text-gray-500">Anträge gesamt</div>
</div> </div>
<div class="bg-white rounded-lg border p-4"> <div class="bg-white rounded-lg border p-4">
<div class="text-3xl font-bold">{data.bewertet}</div> <div class="text-2xl sm:text-3xl font-bold">{data.bewertet}</div>
<div class="text-sm text-gray-500">Mit Umsetzungsbewertung</div> <div class="text-xs sm:text-sm text-gray-500">Mit Bewertung</div>
</div> </div>
{#each data.umsetzung.filter(u => u.bewertung === 'erfuellt') as u} {#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="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-2xl sm:text-3xl font-bold text-green-700">{u.anzahl}</div>
<div class="text-sm text-green-600">Erfüllt</div> <div class="text-xs sm:text-sm text-green-600">Erfüllt</div>
</div> </div>
{/each} {/each}
{#each data.umsetzung.filter(u => u.bewertung === 'nebelkerze') as u} {#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="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-2xl sm:text-3xl font-bold text-red-700">{u.anzahl}</div>
<div class="text-sm text-red-600">Nebelkerzen</div> <div class="text-xs sm:text-sm text-red-600">Nebelkerzen</div>
</div> </div>
{/each} {/each}
</div> </div>
<!-- Umsetzungs-Übersicht (Horizontal Bar) --> <!-- Umsetzungs-Übersicht (Horizontal Bar) -->
{#if data.bewertet > 0} {#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> <h2 class="font-bold mb-4">Umsetzungsquote</h2>
<div class="flex rounded-full overflow-hidden h-8 mb-4"> <div class="flex rounded-full overflow-hidden h-8 mb-4">
{#each data.umsetzung as u} {#each data.umsetzung as u}
@ -109,16 +109,16 @@
{/each} {/each}
</div> </div>
<!-- Legend --> <!-- 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} {#each data.umsetzung as u}
{@const info = KATEGORIEN[u.bewertung as keyof typeof KATEGORIEN]} {@const info = KATEGORIEN[u.bewertung as keyof typeof KATEGORIEN]}
{#if info} {#if info}
<button <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} class:opacity-40={filterKategorie && filterKategorie !== u.bewertung}
onclick={() => filterKategorie = filterKategorie === u.bewertung ? '' : 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} {info.label}: {u.anzahl}
</button> </button>
{/if} {/if}
@ -128,23 +128,23 @@
{/if} {/if}
<!-- Filters --> <!-- Filters -->
<div class="flex gap-4 mb-4"> <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-1.5 text-sm"> <select bind:value={selectedJahr} onchange={loadData} class="border rounded px-3 py-2 text-sm">
<option value="">Alle Jahre</option> <option value="">Alle Jahre</option>
{#each data.jahre as j} {#each data.jahre as j}
<option value={j}>{j}</option> <option value={j}>{j}</option>
{/each} {/each}
</select> </select>
{#if filterKategorie} {#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 Filter zurücksetzen
</button> </button>
{/if} {/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> </div>
<!-- Anträge-Liste --> <!-- Desktop Table -->
<div class="bg-white rounded-lg border overflow-hidden"> <div class="hidden md:block bg-white rounded-lg border overflow-hidden">
<table class="w-full text-sm"> <table class="w-full text-sm">
<thead class="bg-gray-50 text-left"> <thead class="bg-gray-50 text-left">
<tr> <tr>
@ -176,5 +176,21 @@
</tbody> </tbody>
</table> </table>
</div> </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} {/if}
</div> </div>

View File

@ -81,17 +81,18 @@
<!-- Filters --> <!-- Filters -->
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-4 mb-6"> <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="flex flex-col sm:flex-row flex-wrap gap-3 sm:items-end">
<div> <div class="w-full sm:w-auto">
<label for="suche" class="block text-xs font-medium text-gray-500 mb-1">Suche</label> <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..." <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(); }} /> onkeydown={(e) => { if (e.key === 'Enter') applyFilters(); }} />
</div> </div>
<div> <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> <label for="status" class="block text-xs font-medium text-gray-500 mb-1">Status</label>
<select id="status" bind:value={filterStatus} onchange={applyFilters} <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"> 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="">Alle</option>
<option value="eingereicht">Eingereicht</option> <option value="eingereicht">Eingereicht</option>
<option value="in_beratung">In Beratung</option> <option value="in_beratung">In Beratung</option>
@ -106,19 +107,20 @@
<option value="abgewiegelt">Abgewiegelt</option> <option value="abgewiegelt">Abgewiegelt</option>
</select> </select>
</div> </div>
<div> <div class="flex-1 sm:flex-none">
<label for="typ" class="block text-xs font-medium text-gray-500 mb-1">Typ</label> <label for="typ" class="block text-xs font-medium text-gray-500 mb-1">Typ</label>
<select id="typ" bind:value={filterTyp} onchange={applyFilters} <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"> 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="">Alle</option>
<option value="antrag">Antrag</option> <option value="antrag">Antrag</option>
<option value="anfrage">Anfrage</option> <option value="anfrage">Anfrage</option>
</select> </select>
</div> </div>
<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> <label for="partei" class="block text-xs font-medium text-gray-500 mb-1">Partei</label>
<select id="partei" bind:value={filterPartei} onchange={applyFilters} <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> <option value="">Alle</option>
{#each parteien as p} {#each parteien as p}
<option value={p.kuerzel}>{p.kuerzel} ({p.anzahl})</option> <option value={p.kuerzel}>{p.kuerzel} ({p.anzahl})</option>
@ -126,7 +128,7 @@
</select> </select>
</div> </div>
<button onclick={applyFilters} <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 Filtern
</button> </button>
</div> </div>
@ -141,7 +143,8 @@
{:else if data} {:else if data}
<div class="text-sm text-gray-500 mb-3">{data.total} Ketten gefunden</div> <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"> <table class="w-full">
<thead class="bg-gray-50"> <thead class="bg-gray-50">
<tr> <tr>
@ -172,17 +175,41 @@
</table> </table>
</div> </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 --> <!-- Pagination -->
{#if data.total > data.page_size} {#if data.total > data.page_size}
{@const totalPages = Math.ceil(data.total / data.page_size)} {@const totalPages = Math.ceil(data.total / data.page_size)}
<div class="flex justify-center mt-6 space-x-2"> <div class="flex justify-center mt-6 space-x-2">
<button disabled={currentPage <= 1} onclick={() => goPage(currentPage - 1)} <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">
&laquo; Zurück &laquo; Zurück
</button> </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)} <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 &raquo; Weiter &raquo;
</button> </button>
</div> </div>

View File

@ -68,10 +68,10 @@
<a href="/ketten" class="text-sm text-gray-500 hover:text-gray-700 mb-4 inline-block">&larr; Zurück zur Liste</a> <a href="/ketten" class="text-sm text-gray-500 hover:text-gray-700 mb-4 inline-block">&larr; Zurück zur Liste</a>
<!-- Header --> <!-- Header -->
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6 mb-6"> <div class="bg-white rounded-xl shadow-sm border border-gray-200 p-4 sm:p-6 mb-6">
<div class="flex flex-wrap items-start justify-between gap-4"> <div class="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-4">
<div> <div class="min-w-0">
<div class="flex items-center space-x-3 mb-2"> <div class="flex flex-wrap items-center gap-2 sm:gap-3 mb-2">
{#if kette.ursprung?.aktenzeichen} {#if kette.ursprung?.aktenzeichen}
<h1 class="text-2xl font-bold text-gray-900 font-mono">{kette.ursprung.aktenzeichen}</h1> <h1 class="text-2xl font-bold text-gray-900 font-mono">{kette.ursprung.aktenzeichen}</h1>
{/if} {/if}
@ -84,7 +84,7 @@
<p class="text-gray-700">{kette.thema}</p> <p class="text-gray-700">{kette.thema}</p>
{/if} {/if}
</div> </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} {#if kette.status_seit}
<div>Status seit: <strong>{formatDate(kette.status_seit)}</strong></div> <div>Status seit: <strong>{formatDate(kette.status_seit)}</strong></div>
{/if} {/if}
@ -174,8 +174,8 @@
<h2 class="text-lg font-semibold text-gray-900 mb-4">Verknüpfte Vorlagen</h2> <h2 class="text-lg font-semibold text-gray-900 mb-4">Verknüpfte Vorlagen</h2>
<div class="space-y-2"> <div class="space-y-2">
{#each kette.graph.nodes.filter(n => n.extern) as ext} {#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"> <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 items-center space-x-3"> <div class="flex flex-wrap items-center gap-2 sm:gap-3 min-w-0">
{#if ext.aktenzeichen} {#if ext.aktenzeichen}
<span class="font-mono text-sm font-medium text-green-700">{ext.aktenzeichen}</span> <span class="font-mono text-sm font-medium text-green-700">{ext.aktenzeichen}</span>
{/if} {/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> <span class="text-xs px-2 py-0.5 rounded bg-gray-100 text-gray-600 capitalize">{ext.typ}</span>
{/if} {/if}
{#if ext.betreff} {#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} {/if}
</div> </div>
<span class="text-xs text-gray-500">{formatDate(ext.datum_eingang)}</span> <span class="text-xs text-gray-500">{formatDate(ext.datum_eingang)}</span>

View File

@ -137,17 +137,17 @@
<!-- Filters --> <!-- Filters -->
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-4 mb-6"> <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="flex flex-col sm:flex-row flex-wrap gap-3 sm:items-end">
<div class="relative"> <div class="relative w-full sm:w-auto">
<label for="suche" class="block text-xs font-medium text-gray-500 mb-1">Suche</label> <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..." <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} oninput={onSucheInput}
onkeydown={onSucheKeydown} onkeydown={onSucheKeydown}
onfocusout={() => { setTimeout(() => showSuggestions = false, 200); }} onfocusout={() => { setTimeout(() => showSuggestions = false, 200); }}
autocomplete="off" /> autocomplete="off" />
{#if showSuggestions} {#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} {#each suggestions as s, i}
<button <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' : ''}" 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,10 +165,11 @@
</div> </div>
{/if} {/if}
</div> </div>
<div> <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> <label for="typ" class="block text-xs font-medium text-gray-500 mb-1">Typ</label>
<select id="typ" bind:value={filterTyp} onchange={applyFilters} <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"> 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="">Alle</option>
<option value="antrag">Antrag</option> <option value="antrag">Antrag</option>
<option value="anfrage">Anfrage</option> <option value="anfrage">Anfrage</option>
@ -176,18 +177,19 @@
<option value="bericht">Bericht</option> <option value="bericht">Bericht</option>
</select> </select>
</div> </div>
<div> <div class="flex-1 sm:flex-none">
<label for="partei" class="block text-xs font-medium text-gray-500 mb-1">Partei</label> <label for="partei" class="block text-xs font-medium text-gray-500 mb-1">Partei</label>
<select id="partei" bind:value={filterPartei} onchange={applyFilters} <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">
<option value="">Alle</option> <option value="">Alle</option>
{#each parteien as p} {#each parteien as p}
<option value={p.kuerzel}>{p.kuerzel} ({p.anzahl})</option> <option value={p.kuerzel}>{p.kuerzel} ({p.anzahl})</option>
{/each} {/each}
</select> </select>
</div> </div>
</div>
<button onclick={applyFilters} <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 Filtern
</button> </button>
</div> </div>
@ -202,7 +204,8 @@
{:else if data} {:else if data}
<div class="text-sm text-gray-500 mb-3">{data.total} Vorlagen gefunden</div> <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"> <table class="w-full">
<thead class="bg-gray-50"> <thead class="bg-gray-50">
<tr> <tr>
@ -261,17 +264,60 @@
</table> </table>
</div> </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 --> <!-- Pagination -->
{#if data.total > data.page_size} {#if data.total > data.page_size}
{@const totalPages = Math.ceil(data.total / data.page_size)} {@const totalPages = Math.ceil(data.total / data.page_size)}
<div class="flex justify-center mt-6 space-x-2"> <div class="flex justify-center mt-6 space-x-2">
<button disabled={currentPage <= 1} onclick={() => goPage(currentPage - 1)} <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">
&laquo; Zurück &laquo; Zurück
</button> </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)} <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 &raquo; Weiter &raquo;
</button> </button>
</div> </div>

View File

@ -75,9 +75,9 @@
<!-- Header --> <!-- Header -->
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6 mb-6"> <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 class="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-4">
<div> <div class="min-w-0">
<div class="flex items-center space-x-3 mb-2"> <div class="flex flex-wrap items-center gap-2 sm:gap-3 mb-2">
{#if vorlage.aktenzeichen} {#if vorlage.aktenzeichen}
<h1 class="text-2xl font-bold text-gray-900 font-mono">{vorlage.aktenzeichen}</h1> <h1 class="text-2xl font-bold text-gray-900 font-mono">{vorlage.aktenzeichen}</h1>
{/if} {/if}
@ -95,7 +95,7 @@
<p class="text-sm text-gray-500 mt-1">Thema: {vorlage.thema_kurz}</p> <p class="text-sm text-gray-500 mt-1">Thema: {vorlage.thema_kurz}</p>
{/if} {/if}
</div> </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} {#if vorlage.datum_eingang}
<div>Eingegangen: <strong>{formatDate(vorlage.datum_eingang)}</strong></div> <div>Eingegangen: <strong>{formatDate(vorlage.datum_eingang)}</strong></div>
{/if} {/if}
@ -119,7 +119,7 @@
{/if} {/if}
<!-- External links --> <!-- External links -->
<div class="mt-4 flex space-x-4"> <div class="mt-4 flex flex-wrap gap-4">
{#if vorlage.web_url} {#if vorlage.web_url}
<a href={vorlage.web_url} target="_blank" rel="noopener" <a href={vorlage.web_url} target="_blank" rel="noopener"
class="text-sm text-green-600 hover:underline">ALLRIS &nearr;</a> class="text-sm text-green-600 hover:underline">ALLRIS &nearr;</a>
@ -297,7 +297,7 @@
<h2 class="text-lg font-semibold text-gray-900 mb-4">Beratungsfolge</h2> <h2 class="text-lg font-semibold text-gray-900 mb-4">Beratungsfolge</h2>
<div class="space-y-3"> <div class="space-y-3">
{#each vorlage.beratungen as b} {#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> <div>
{#if b.gremium} {#if b.gremium}
<span class="text-sm font-medium text-gray-900">{b.gremium.name}</span> <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> <p class="text-xs text-gray-500 mt-1">{b.ergebnis_text}</p>
{/if} {/if}
</div> </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> </div>
{/each} {/each}
</div> </div>