- Neues Feld kette_id in ki_bewertungen für umsetzung_match - 3.757 bestehende Bewertungen migriert - API: Neueste Version + ältere Versionen getrennt im Response - Explorer: Umsetzungsgrad in Panel 2 (Kette), nicht mehr in Panel 3 (Vorlage) - 'Vorherige Bewertungen' Button aufklappbar mit Score + Begründung + Zeitstempel
689 lines
26 KiB
Svelte
689 lines
26 KiB
Svelte
<script lang="ts">
|
||
import { onMount } from 'svelte';
|
||
import { fetchKetten, fetchKette, fetchVorlage, reevalVorlage, fetchJobStatus, type KetteKurz, type KetteDetail, type VorlageDetail, type Paginated } from '$lib/api';
|
||
import { formatDate, typLabel } from '$lib/status';
|
||
import { filters, mergeFilterParams, filterVersion } from '$lib/filters.svelte';
|
||
import Ampel from '$lib/components/Ampel.svelte';
|
||
import StatusBadge from '$lib/components/StatusBadge.svelte';
|
||
|
||
// State
|
||
let ketten: KetteKurz[] = $state([]);
|
||
let kettenTotal = $state(0);
|
||
let selectedKette: KetteDetail | null = $state(null);
|
||
let selectedVorlage: VorlageDetail | null = $state(null);
|
||
let loading = $state(true);
|
||
let ketteLoading = $state(false);
|
||
let vorlageLoading = $state(false);
|
||
let error = $state('');
|
||
|
||
// Filters
|
||
let suche = $state('');
|
||
let strangFilter = $state('');
|
||
let statusFilter = $state('');
|
||
let currentPage = $state(1);
|
||
const PAGE_SIZE = 30;
|
||
|
||
// Active IDs
|
||
let activeKetteId = $state<number | null>(null);
|
||
let activeVorlageId = $state<number | null>(null);
|
||
|
||
// Mobile tab
|
||
let mobileTab = $state<'liste' | 'kette' | 'detail'>('liste');
|
||
let showVolltext = $state(false);
|
||
let showVersionen = $state(false);
|
||
let showUmsetzungVersionen = $state(false);
|
||
let showReeval = $state(false);
|
||
let reevalAnmerkung = $state('');
|
||
let reevalStatus = $state<'idle' | 'running' | 'done' | 'error'>('idle');
|
||
let reevalError = $state('');
|
||
|
||
async function triggerReeval() {
|
||
if (!selectedVorlage) return;
|
||
reevalStatus = 'running';
|
||
reevalError = '';
|
||
try {
|
||
const { job_id } = await reevalVorlage(selectedVorlage.id, reevalAnmerkung);
|
||
for (let i = 0; i < 60; i++) {
|
||
await new Promise(r => setTimeout(r, 3000));
|
||
const status = await fetchJobStatus(job_id);
|
||
if (status.status === 'done') {
|
||
reevalStatus = 'done';
|
||
selectedVorlage = await fetchVorlage(selectedVorlage!.id);
|
||
showReeval = false;
|
||
reevalAnmerkung = '';
|
||
return;
|
||
}
|
||
if (status.status === 'error') {
|
||
reevalStatus = 'error';
|
||
reevalError = status.error || 'Unbekannter Fehler';
|
||
return;
|
||
}
|
||
}
|
||
reevalStatus = 'error';
|
||
reevalError = 'Timeout nach 3 Minuten';
|
||
} catch (e) {
|
||
reevalStatus = 'error';
|
||
reevalError = e instanceof Error ? e.message : 'Fehler';
|
||
}
|
||
}
|
||
|
||
const STRANG_TABS = [
|
||
{ value: '', label: 'Alle' },
|
||
{ value: 'antrag', label: 'Anträge' },
|
||
{ value: 'anfrage', label: 'Anfragen' },
|
||
{ value: 'beschlussvorlage', label: 'Beschlussvorlagen' },
|
||
{ value: 'mitteilung', label: 'Mitteilungen' },
|
||
];
|
||
|
||
const STATUS_OPTIONS = [
|
||
{ value: '', label: 'Alle Status' },
|
||
{ value: 'in_beratung', label: '⏳ In Beratung' },
|
||
{ value: 'beschlossen', label: '🟡 Beschlossen' },
|
||
{ value: 'umgesetzt', label: '🟢 Umgesetzt' },
|
||
{ value: 'teilweise_umgesetzt', label: '🟡 Teilweise' },
|
||
{ value: 'versandet', label: '🔴 Versandet' },
|
||
{ value: 'abgelehnt', label: '🔴 Abgelehnt' },
|
||
{ value: 'beantwortet', label: '🟢 Beantwortet' },
|
||
{ value: 'angefragt', label: '⏳ Angefragt' },
|
||
];
|
||
|
||
const FARB_MAP: Record<string, string> = {
|
||
gruen: '#22c55e',
|
||
gelb: '#eab308',
|
||
rot: '#ef4444',
|
||
amber: '#f59e0b',
|
||
grau: '#d1d5db',
|
||
blau: '#3b82f6',
|
||
};
|
||
|
||
async function loadKetten() {
|
||
loading = true;
|
||
try {
|
||
let params: Record<string, string> = {
|
||
page: String(currentPage),
|
||
page_size: String(PAGE_SIZE),
|
||
};
|
||
if (suche) params.suche = suche;
|
||
if (strangFilter) params.typ = strangFilter;
|
||
if (statusFilter) params.status = statusFilter;
|
||
params = mergeFilterParams(params);
|
||
const data = await fetchKetten(params);
|
||
ketten = data.items;
|
||
kettenTotal = data.total;
|
||
} catch (e) {
|
||
error = e instanceof Error ? e.message : 'Fehler';
|
||
} finally {
|
||
loading = false;
|
||
}
|
||
}
|
||
|
||
async function selectKette(id: number) {
|
||
if (activeKetteId === id) return;
|
||
activeKetteId = id;
|
||
activeVorlageId = null;
|
||
selectedVorlage = null;
|
||
ketteLoading = true;
|
||
mobileTab = 'kette';
|
||
try {
|
||
selectedKette = await fetchKette(id);
|
||
// Auto-select the first glied (most recent = last position)
|
||
if (selectedKette.glieder.length > 0) {
|
||
const sorted = [...selectedKette.glieder].sort((a, b) => b.position - a.position);
|
||
selectVorlage(sorted[0].vorlage.id);
|
||
}
|
||
} catch (e) {
|
||
error = e instanceof Error ? e.message : 'Fehler';
|
||
} finally {
|
||
ketteLoading = false;
|
||
}
|
||
}
|
||
|
||
async function selectVorlage(id: number) {
|
||
if (activeVorlageId === id) return;
|
||
activeVorlageId = id;
|
||
vorlageLoading = true;
|
||
showVolltext = false;
|
||
mobileTab = 'detail';
|
||
try {
|
||
selectedVorlage = await fetchVorlage(id);
|
||
} catch (e) {
|
||
error = e instanceof Error ? e.message : 'Fehler';
|
||
} finally {
|
||
vorlageLoading = false;
|
||
}
|
||
}
|
||
|
||
function handleSearch(e: KeyboardEvent) {
|
||
if (e.key === 'Enter') {
|
||
currentPage = 1;
|
||
loadKetten();
|
||
}
|
||
}
|
||
|
||
function changeStrang(value: string) {
|
||
strangFilter = value;
|
||
currentPage = 1;
|
||
loadKetten();
|
||
}
|
||
|
||
function goPage(p: number) {
|
||
currentPage = p;
|
||
loadKetten();
|
||
}
|
||
|
||
onMount(() => {
|
||
loadKetten();
|
||
});
|
||
|
||
// Reload on global filter change
|
||
$effect(() => {
|
||
filterVersion();
|
||
currentPage = 1;
|
||
loadKetten();
|
||
});
|
||
|
||
// Sorted glieder for timeline (newest first)
|
||
let sortedGlieder = $derived(
|
||
selectedKette ? [...selectedKette.glieder].sort((a, b) => b.position - a.position) : []
|
||
);
|
||
|
||
let totalPages = $derived(Math.ceil(kettenTotal / PAGE_SIZE));
|
||
</script>
|
||
|
||
<svelte:head>
|
||
<title>Explorer - Antragstracker Hagen</title>
|
||
</svelte:head>
|
||
|
||
<!-- Mobile Tabs -->
|
||
<div class="lg:hidden flex border-b border-gray-200 mb-4 bg-white rounded-t-lg">
|
||
<button
|
||
onclick={() => mobileTab = 'liste'}
|
||
class="flex-1 py-3 text-sm font-medium text-center border-b-2 transition-colors
|
||
{mobileTab === 'liste' ? 'border-green-600 text-green-700' : 'border-transparent text-gray-500 hover:text-gray-700'}">
|
||
Liste
|
||
</button>
|
||
<button
|
||
onclick={() => mobileTab = 'kette'}
|
||
class="flex-1 py-3 text-sm font-medium text-center border-b-2 transition-colors
|
||
{mobileTab === 'kette' ? 'border-green-600 text-green-700' : 'border-transparent text-gray-500 hover:text-gray-700'}"
|
||
disabled={!selectedKette}>
|
||
Kette
|
||
</button>
|
||
<button
|
||
onclick={() => mobileTab = 'detail'}
|
||
class="flex-1 py-3 text-sm font-medium text-center border-b-2 transition-colors
|
||
{mobileTab === 'detail' ? 'border-green-600 text-green-700' : 'border-transparent text-gray-500 hover:text-gray-700'}"
|
||
disabled={!selectedVorlage}>
|
||
Detail
|
||
</button>
|
||
</div>
|
||
|
||
<!-- 3-Panel Layout -->
|
||
<div class="flex gap-0 lg:gap-0 h-[calc(100vh-12rem)] lg:h-[calc(100vh-11rem)]">
|
||
|
||
<!-- Panel 1: Ketten-Liste -->
|
||
<div class="w-full lg:w-[280px] lg:shrink-0 lg:border-r border-gray-200 flex flex-col bg-white rounded-l-lg lg:rounded-l-xl overflow-hidden
|
||
{mobileTab !== 'liste' ? 'hidden lg:flex' : 'flex'}">
|
||
|
||
<!-- Search & Filters -->
|
||
<div class="p-3 border-b border-gray-100 space-y-2">
|
||
<input
|
||
type="text"
|
||
bind:value={suche}
|
||
placeholder="Suche..."
|
||
onkeydown={handleSearch}
|
||
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-green-500 focus:border-green-500"
|
||
/>
|
||
<div class="flex flex-wrap gap-1">
|
||
{#each STRANG_TABS as tab}
|
||
<button
|
||
onclick={() => changeStrang(tab.value)}
|
||
class="px-2 py-1 rounded text-xs font-medium transition-all
|
||
{strangFilter === tab.value
|
||
? 'bg-green-600 text-white'
|
||
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'}">
|
||
{tab.label}
|
||
</button>
|
||
{/each}
|
||
</div>
|
||
<select
|
||
bind:value={statusFilter}
|
||
onchange={() => { currentPage = 1; loadKetten(); }}
|
||
class="w-full border border-gray-300 rounded-lg px-2 py-1.5 text-xs focus:ring-2 focus:ring-green-500 bg-white">
|
||
{#each STATUS_OPTIONS as opt}
|
||
<option value={opt.value}>{opt.label}</option>
|
||
{/each}
|
||
</select>
|
||
</div>
|
||
|
||
<!-- Count -->
|
||
<div class="px-3 py-1.5 text-xs text-gray-400 border-b border-gray-50">
|
||
{kettenTotal} Ketten
|
||
</div>
|
||
|
||
<!-- List -->
|
||
<div class="flex-1 overflow-y-auto">
|
||
{#if loading && ketten.length === 0}
|
||
<div class="flex justify-center py-10">
|
||
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-green-600"></div>
|
||
</div>
|
||
{:else if ketten.length === 0}
|
||
<div class="p-4 text-sm text-gray-500 text-center">Keine Ketten gefunden</div>
|
||
{:else}
|
||
{#each ketten as kette}
|
||
<button
|
||
onclick={() => selectKette(kette.id)}
|
||
class="w-full text-left px-3 py-2.5 border-b border-gray-50 hover:bg-gray-50 transition-colors
|
||
{activeKetteId === kette.id ? 'bg-green-50 border-l-2 border-l-green-600' : 'border-l-2 border-l-transparent'}">
|
||
<div class="flex items-center justify-between gap-2 mb-0.5">
|
||
<span class="font-mono text-xs font-medium text-green-700 truncate">
|
||
{kette.ursprung?.aktenzeichen || `#${kette.id}`}
|
||
</span>
|
||
{#if kette.ampel}
|
||
<span class="flex items-center gap-1 shrink-0">
|
||
<span
|
||
class="w-3 h-3 rounded-full"
|
||
style="background-color: {FARB_MAP[kette.ampel.farbe] || FARB_MAP.grau}"
|
||
></span>
|
||
<span class="text-[10px] text-gray-500">{kette.ampel.label}</span>
|
||
</span>
|
||
{/if}
|
||
</div>
|
||
<p class="text-xs text-gray-600 line-clamp-2 leading-snug">
|
||
{kette.thema || kette.ursprung?.betreff || '-'}
|
||
</p>
|
||
</button>
|
||
{/each}
|
||
|
||
<!-- Pagination -->
|
||
{#if totalPages > 1}
|
||
<div class="flex items-center justify-center gap-2 p-3 border-t border-gray-100">
|
||
<button
|
||
disabled={currentPage <= 1}
|
||
onclick={() => goPage(currentPage - 1)}
|
||
class="px-2 py-1 rounded text-xs border border-gray-300 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed">
|
||
‹
|
||
</button>
|
||
<span class="text-xs text-gray-500">{currentPage}/{totalPages}</span>
|
||
<button
|
||
disabled={currentPage >= totalPages}
|
||
onclick={() => goPage(currentPage + 1)}
|
||
class="px-2 py-1 rounded text-xs border border-gray-300 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed">
|
||
›
|
||
</button>
|
||
</div>
|
||
{/if}
|
||
{/if}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Panel 2: Kette Detail -->
|
||
<div class="w-full lg:w-[220px] lg:shrink-0 lg:border-r border-gray-200 flex flex-col bg-white overflow-hidden
|
||
{mobileTab !== 'kette' ? 'hidden lg:flex' : 'flex'}">
|
||
|
||
{#if ketteLoading}
|
||
<div class="flex justify-center py-10">
|
||
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-green-600"></div>
|
||
</div>
|
||
{:else if selectedKette}
|
||
<div class="flex-1 overflow-y-auto">
|
||
<!-- Ampel -->
|
||
<div class="p-4 border-b border-gray-100">
|
||
<div class="text-xs font-medium text-gray-500 uppercase mb-3">
|
||
{selectedKette.ampel?.strang_label || selectedKette.typ || 'Status'}
|
||
</div>
|
||
{#if selectedKette.ampel}
|
||
<Ampel ampel={selectedKette.ampel} vertical />
|
||
{:else}
|
||
<StatusBadge status={selectedKette.status} />
|
||
{/if}
|
||
</div>
|
||
|
||
<!-- Umsetzungsgrad -->
|
||
{#if selectedKette.umsetzung}
|
||
{@const u = selectedKette.umsetzung}
|
||
<div class="px-4 pb-3 border-b border-gray-100">
|
||
<div class="flex items-center gap-2 mb-1.5">
|
||
<div class="w-10 h-10 rounded-full flex items-center justify-center text-sm font-bold
|
||
{u.score >= 0.7 ? 'bg-green-200 text-green-800' : u.score >= 0.4 ? 'bg-amber-200 text-amber-800' : 'bg-red-200 text-red-800'}">
|
||
{Math.round((u.score || 0) * 100)}%
|
||
</div>
|
||
<div>
|
||
<div class="text-xs font-semibold {u.score >= 0.7 ? 'text-green-800' : u.score >= 0.4 ? 'text-amber-800' : 'text-red-800'}">
|
||
{u.bewertung || (u.score >= 0.7 ? 'Umgesetzt' : u.score >= 0.4 ? 'Teilweise' : 'Kaum umgesetzt')}
|
||
</div>
|
||
{#if u.kernpunkt_erfuellt !== null && u.kernpunkt_erfuellt !== undefined}
|
||
<div class="text-[10px] text-gray-500">
|
||
Kernpunkt: {u.kernpunkt_erfuellt ? '✅ erfüllt' : '❌ nicht erfüllt'}
|
||
</div>
|
||
{/if}
|
||
</div>
|
||
</div>
|
||
{#if u.begruendung}
|
||
<p class="text-[11px] text-gray-600 leading-snug">{u.begruendung}</p>
|
||
{/if}
|
||
{#if u.details}
|
||
<p class="text-[10px] text-gray-500 mt-1 leading-snug">{u.details}</p>
|
||
{/if}
|
||
{#if selectedKette.umsetzung_versionen?.length}
|
||
<button onclick={() => showUmsetzungVersionen = !showUmsetzungVersionen}
|
||
class="text-[10px] text-gray-400 hover:text-gray-600 mt-2 flex items-center gap-1">
|
||
<span>{showUmsetzungVersionen ? '▼' : '▶'}</span>
|
||
{selectedKette.umsetzung_versionen.length} vorherige Bewertung{selectedKette.umsetzung_versionen.length > 1 ? 'en' : ''}
|
||
</button>
|
||
{#if showUmsetzungVersionen}
|
||
<div class="mt-2 space-y-2">
|
||
{#each selectedKette.umsetzung_versionen as v}
|
||
<div class="rounded border border-gray-200 bg-gray-50 p-2">
|
||
<div class="flex items-center gap-2 mb-1">
|
||
<span class="text-[10px] font-bold {v.score >= 0.7 ? 'text-green-700' : v.score >= 0.4 ? 'text-amber-700' : 'text-red-700'}">
|
||
{Math.round((v.score || 0) * 100)}%
|
||
</span>
|
||
<span class="text-[10px] text-gray-400">{v.erstellt_at || ''}</span>
|
||
</div>
|
||
<p class="text-[10px] text-gray-500">{v.begruendung}</p>
|
||
</div>
|
||
{/each}
|
||
</div>
|
||
{/if}
|
||
{/if}
|
||
</div>
|
||
{/if}
|
||
|
||
<!-- Timeline -->
|
||
<div class="p-3">
|
||
<div class="text-xs font-medium text-gray-500 uppercase mb-3">Ketten-Glieder</div>
|
||
<div class="relative">
|
||
<!-- Vertical line -->
|
||
<div class="absolute left-3 top-2 bottom-2 w-0.5 bg-gray-200"></div>
|
||
|
||
{#each sortedGlieder as glied, i}
|
||
<button
|
||
onclick={() => selectVorlage(glied.vorlage.id)}
|
||
class="relative w-full text-left pl-8 pr-2 py-2 rounded-lg hover:bg-gray-50 transition-colors mb-1
|
||
{activeVorlageId === glied.vorlage.id ? 'bg-green-50' : ''}">
|
||
<!-- Dot -->
|
||
<div class="absolute left-1.5 top-3.5 w-3 h-3 rounded-full border-2 transition-colors
|
||
{activeVorlageId === glied.vorlage.id
|
||
? 'bg-green-600 border-green-600'
|
||
: 'bg-white border-gray-300'}">
|
||
</div>
|
||
<!-- Content -->
|
||
<div class="flex items-center gap-1.5 mb-0.5">
|
||
<span class="font-mono text-[11px] font-medium text-green-700 truncate">
|
||
{glied.vorlage.aktenzeichen || `#${glied.vorlage.id}`}
|
||
</span>
|
||
</div>
|
||
{#if glied.rolle}
|
||
<span class="inline-block text-[10px] px-1.5 py-0.5 rounded bg-gray-100 text-gray-600 mb-0.5">
|
||
{glied.rolle}
|
||
</span>
|
||
{/if}
|
||
{#if glied.vorlage.datum_eingang}
|
||
<div class="text-[10px] text-gray-400">
|
||
{formatDate(glied.vorlage.datum_eingang)}
|
||
</div>
|
||
{/if}
|
||
</button>
|
||
{/each}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{:else}
|
||
<div class="flex items-center justify-center h-full text-sm text-gray-400 p-4 text-center">
|
||
← Kette auswählen
|
||
</div>
|
||
{/if}
|
||
</div>
|
||
|
||
<!-- Panel 3: Vorlage Detail -->
|
||
<div class="w-full lg:flex-1 lg:min-w-0 flex flex-col bg-white rounded-r-lg lg:rounded-r-xl overflow-hidden
|
||
{mobileTab !== 'detail' ? 'hidden lg:flex' : 'flex'}">
|
||
|
||
{#if vorlageLoading}
|
||
<div class="flex justify-center py-10">
|
||
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-green-600"></div>
|
||
</div>
|
||
{:else if selectedVorlage}
|
||
<div class="flex-1 overflow-y-auto p-4 sm:p-6 space-y-5">
|
||
<!-- Header -->
|
||
<div>
|
||
<div class="flex flex-wrap items-center gap-2 mb-2">
|
||
{#if selectedVorlage.aktenzeichen}
|
||
<h2 class="text-xl font-bold text-gray-900 font-mono">{selectedVorlage.aktenzeichen}</h2>
|
||
{/if}
|
||
{#if selectedVorlage.typ}
|
||
<span class="text-xs px-2 py-0.5 rounded bg-gray-100 text-gray-600">{typLabel(selectedVorlage.typ)}</span>
|
||
{/if}
|
||
{#if selectedVorlage.ist_verwaltungsvorlage}
|
||
<span class="text-xs px-2 py-0.5 rounded bg-blue-100 text-blue-700">Verwaltungsvorlage</span>
|
||
{/if}
|
||
</div>
|
||
{#if selectedVorlage.betreff}
|
||
<p class="text-gray-700">{selectedVorlage.betreff}</p>
|
||
{/if}
|
||
<div class="flex flex-wrap items-center gap-3 mt-2 text-sm text-gray-500">
|
||
{#if selectedVorlage.datum_eingang}
|
||
<span>Eingegangen: <strong>{formatDate(selectedVorlage.datum_eingang)}</strong></span>
|
||
{/if}
|
||
{#if selectedVorlage.web_url}
|
||
<a href={selectedVorlage.web_url} target="_blank" rel="noopener" class="text-green-600 hover:underline">ALLRIS ↗</a>
|
||
{/if}
|
||
{#if selectedVorlage.pdf_url}
|
||
<a href={selectedVorlage.pdf_url} target="_blank" rel="noopener" class="text-green-600 hover:underline">PDF ↗</a>
|
||
{/if}
|
||
<a href="/vorlagen/{selectedVorlage.id}" class="text-green-600 hover:underline">Vollansicht →</a>
|
||
</div>
|
||
|
||
<!-- Antragsteller -->
|
||
{#if selectedVorlage.antragsteller?.length > 0}
|
||
<div class="mt-2 flex items-center gap-2">
|
||
<span class="text-xs text-gray-500">Antragsteller:</span>
|
||
{#each selectedVorlage.antragsteller as p}
|
||
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium"
|
||
style="background-color: {p.farbe || '#e5e7eb'}20; color: {p.farbe || '#4b5563'}; border: 1px solid {p.farbe || '#d1d5db'}">
|
||
{p.kuerzel}
|
||
</span>
|
||
{/each}
|
||
</div>
|
||
{/if}
|
||
</div>
|
||
|
||
<!-- KI-Zusammenfassung -->
|
||
{#if selectedVorlage.ki_zusammenfassung}
|
||
<div class="bg-gradient-to-r from-green-50 to-emerald-50 rounded-xl border border-green-200 p-5">
|
||
<h3 class="text-sm font-semibold text-green-800 mb-2 flex items-center gap-1.5">
|
||
<span>🤖</span> KI-Zusammenfassung
|
||
</h3>
|
||
<p class="text-sm text-gray-700 mb-3">{selectedVorlage.ki_zusammenfassung.zusammenfassung}</p>
|
||
|
||
{#if selectedVorlage.ki_zusammenfassung.kernforderung}
|
||
<div class="mb-2">
|
||
<span class="text-xs font-medium text-green-700 uppercase">Kernforderung:</span>
|
||
<p class="text-sm text-gray-800 font-medium">{selectedVorlage.ki_zusammenfassung.kernforderung}</p>
|
||
</div>
|
||
{/if}
|
||
|
||
{#if selectedVorlage.ki_zusammenfassung.begruendung}
|
||
<div class="mb-2">
|
||
<span class="text-xs font-medium text-green-700 uppercase">Begründung:</span>
|
||
<p class="text-xs text-gray-600">{selectedVorlage.ki_zusammenfassung.begruendung}</p>
|
||
</div>
|
||
{/if}
|
||
|
||
<div class="flex flex-wrap gap-1.5 mt-3">
|
||
{#if selectedVorlage.ki_zusammenfassung.thema}
|
||
<span class="text-xs px-2 py-0.5 rounded-full bg-green-100 text-green-800">📂 {selectedVorlage.ki_zusammenfassung.thema}</span>
|
||
{/if}
|
||
{#if selectedVorlage.ki_zusammenfassung.partei}
|
||
<span class="text-xs px-2 py-0.5 rounded-full bg-purple-100 text-purple-800">🏛️ {selectedVorlage.ki_zusammenfassung.partei}</span>
|
||
{/if}
|
||
{#each selectedVorlage.ki_zusammenfassung.betroffene_orte || [] as ort}
|
||
<span class="text-xs px-2 py-0.5 rounded-full bg-blue-100 text-blue-800">📍 {ort}</span>
|
||
{/each}
|
||
</div>
|
||
</div>
|
||
{/if}
|
||
|
||
<!-- Vorherige KI-Versionen -->
|
||
{#if selectedVorlage.ki_versionen?.length}
|
||
<div>
|
||
<button onclick={() => showVersionen = !showVersionen}
|
||
class="text-xs text-gray-500 hover:text-gray-700 flex items-center gap-1">
|
||
<span>{showVersionen ? '▼' : '▶'}</span>
|
||
{selectedVorlage.ki_versionen.length} vorherige Version{selectedVorlage.ki_versionen.length > 1 ? 'en' : ''}
|
||
</button>
|
||
{#if showVersionen}
|
||
<div class="mt-2 space-y-3">
|
||
{#each selectedVorlage.ki_versionen as v, i}
|
||
<div class="rounded-lg border border-gray-200 bg-gray-50 p-4">
|
||
<div class="flex items-center justify-between mb-2">
|
||
<span class="text-xs text-gray-400">Version {selectedVorlage.ki_versionen.length - i} · {v.erstellt_at || 'unbekannt'}</span>
|
||
<span class="text-[10px] px-1.5 py-0.5 rounded bg-gray-200 text-gray-500">{v.prompt_version || ''}</span>
|
||
</div>
|
||
<p class="text-sm text-gray-600">{v.zusammenfassung}</p>
|
||
{#if v.kernforderung}
|
||
<p class="text-xs text-gray-500 mt-1"><strong>Kernforderung:</strong> {v.kernforderung}</p>
|
||
{/if}
|
||
</div>
|
||
{/each}
|
||
</div>
|
||
{/if}
|
||
</div>
|
||
{/if}
|
||
|
||
<!-- Umsetzungsbewertung → jetzt in der Ketten-Ansicht (Panel 2) -->
|
||
|
||
<!-- Neu bewerten -->
|
||
<div class="rounded-xl border border-gray-200 p-4">
|
||
{#if !showReeval}
|
||
<button onclick={() => showReeval = true}
|
||
class="text-sm text-green-600 hover:text-green-800 font-medium flex items-center gap-1.5">
|
||
<span>🔄</span> Neu bewerten lassen
|
||
</button>
|
||
{:else}
|
||
<h3 class="text-sm font-semibold text-gray-900 mb-2">KI-Neubewertung anstoßen</h3>
|
||
<textarea bind:value={reevalAnmerkung} placeholder="Anmerkungen für die KI (optional)"
|
||
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm mb-3 h-20 resize-y focus:ring-2 focus:ring-green-500"
|
||
disabled={reevalStatus === 'running'}></textarea>
|
||
<div class="flex gap-2 items-center">
|
||
<button onclick={triggerReeval} disabled={reevalStatus === 'running'}
|
||
class="bg-green-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-green-700 disabled:opacity-50 transition-colors">
|
||
{#if reevalStatus === 'running'}
|
||
<span class="inline-flex items-center gap-2">
|
||
<span class="animate-spin h-4 w-4 border-2 border-white border-t-transparent rounded-full"></span>
|
||
KI bewertet…
|
||
</span>
|
||
{:else}
|
||
Bewertung starten
|
||
{/if}
|
||
</button>
|
||
{#if reevalStatus !== 'running'}
|
||
<button onclick={() => { showReeval = false; reevalStatus = 'idle'; }}
|
||
class="text-sm text-gray-500 hover:text-gray-700">Abbrechen</button>
|
||
{/if}
|
||
</div>
|
||
{#if reevalStatus === 'done'}
|
||
<p class="mt-2 text-sm text-green-700 font-medium">✅ Bewertung aktualisiert!</p>
|
||
{/if}
|
||
{#if reevalStatus === 'error'}
|
||
<p class="mt-2 text-sm text-red-600">❌ {reevalError}</p>
|
||
{/if}
|
||
{/if}
|
||
</div>
|
||
|
||
<!-- Volltext -->
|
||
{#if selectedVorlage.volltext_clean}
|
||
<div class="rounded-xl border border-gray-200 p-5">
|
||
<div class="flex items-center justify-between mb-2">
|
||
<h3 class="text-sm font-semibold text-gray-900">Volltext</h3>
|
||
<button onclick={() => showVolltext = !showVolltext} class="text-xs text-green-600 hover:underline">
|
||
{showVolltext ? 'Einklappen' : 'Aufklappen'}
|
||
</button>
|
||
</div>
|
||
{#if showVolltext}
|
||
<div class="prose prose-sm max-w-none text-gray-700 whitespace-pre-wrap text-xs">{selectedVorlage.volltext_clean}</div>
|
||
{:else}
|
||
<p class="text-xs text-gray-500 line-clamp-4">{selectedVorlage.volltext_clean}</p>
|
||
{/if}
|
||
</div>
|
||
{/if}
|
||
|
||
<!-- Beratungen -->
|
||
{#if selectedVorlage.beratungen?.length > 0}
|
||
<div class="rounded-xl border border-gray-200 p-5">
|
||
<h3 class="text-sm font-semibold text-gray-900 mb-3">Beratungsfolge</h3>
|
||
<div class="space-y-2">
|
||
{#each selectedVorlage.beratungen as b}
|
||
<div class="flex flex-col sm:flex-row sm:items-start sm:justify-between p-2.5 rounded-lg border border-gray-100 gap-1.5">
|
||
<div>
|
||
{#if b.gremium}
|
||
<span class="text-sm font-medium text-gray-900">{b.gremium.name}</span>
|
||
{/if}
|
||
{#if b.rolle}
|
||
<span class="text-xs ml-1.5 text-gray-500">({b.rolle})</span>
|
||
{/if}
|
||
{#if b.ergebnis}
|
||
<div class="mt-0.5">
|
||
<span class="text-xs px-2 py-0.5 rounded
|
||
{b.ergebnis.includes('angenommen') || b.ergebnis.includes('empfohlen') ? 'bg-green-100 text-green-700' :
|
||
b.ergebnis.includes('abgelehnt') ? 'bg-red-100 text-red-700' :
|
||
b.ergebnis.includes('vertagt') ? 'bg-amber-100 text-amber-700' :
|
||
'bg-gray-100 text-gray-700'}">
|
||
{b.ergebnis}
|
||
</span>
|
||
</div>
|
||
{/if}
|
||
</div>
|
||
<span class="text-xs text-gray-500 shrink-0">{formatDate(b.sitzung_datum)}</span>
|
||
</div>
|
||
{/each}
|
||
</div>
|
||
</div>
|
||
{/if}
|
||
|
||
<!-- Referenzen -->
|
||
{#if selectedVorlage.referenzen_ausgehend?.length > 0 || selectedVorlage.referenzen_eingehend?.length > 0}
|
||
<div class="rounded-xl border border-gray-200 p-5">
|
||
<h3 class="text-sm font-semibold text-gray-900 mb-3">Referenzen</h3>
|
||
{#if selectedVorlage.referenzen_ausgehend?.length > 0}
|
||
<div class="mb-3">
|
||
<span class="text-xs font-medium text-gray-500 uppercase">Verweist auf</span>
|
||
<div class="space-y-1 mt-1">
|
||
{#each selectedVorlage.referenzen_ausgehend as ref}
|
||
<a href="/vorlagen/{ref.vorlage_id}" class="block p-2 rounded border border-gray-100 hover:bg-gray-50 text-xs">
|
||
<span class="font-mono font-medium text-green-700">{ref.aktenzeichen || `#${ref.vorlage_id}`}</span>
|
||
{#if ref.betreff}
|
||
<span class="text-gray-600 ml-1 truncate">{ref.betreff}</span>
|
||
{/if}
|
||
</a>
|
||
{/each}
|
||
</div>
|
||
</div>
|
||
{/if}
|
||
{#if selectedVorlage.referenzen_eingehend?.length > 0}
|
||
<div>
|
||
<span class="text-xs font-medium text-gray-500 uppercase">Referenziert von</span>
|
||
<div class="space-y-1 mt-1">
|
||
{#each selectedVorlage.referenzen_eingehend as ref}
|
||
<a href="/vorlagen/{ref.vorlage_id}" class="block p-2 rounded border border-gray-100 hover:bg-gray-50 text-xs">
|
||
<span class="font-mono font-medium text-green-700">{ref.aktenzeichen || `#${ref.vorlage_id}`}</span>
|
||
{#if ref.betreff}
|
||
<span class="text-gray-600 ml-1 truncate">{ref.betreff}</span>
|
||
{/if}
|
||
</a>
|
||
{/each}
|
||
</div>
|
||
</div>
|
||
{/if}
|
||
</div>
|
||
{/if}
|
||
</div>
|
||
{:else}
|
||
<div class="flex items-center justify-center h-full text-sm text-gray-400 p-4 text-center">
|
||
← Vorlage auswählen
|
||
</div>
|
||
{/if}
|
||
</div>
|
||
</div>
|