feat(korrespondenz): wire up +page.svelte orchestrator with new components

Compose CorrespondenzPersonBar, CorrespondenzFilterControls, SinglePersonHintBar,
CorrespondenzEmptyState, and updated ConversationTimeline. Add localStorage
recent-persons persistence on applyFilters, single-person mode gate, and
canWrite derived from user groups in load function.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-03-30 12:59:33 +02:00
parent 3addc72693
commit 4f5f8255a1
2 changed files with 80 additions and 33 deletions

View File

@@ -1,13 +1,18 @@
import type { components } from '$lib/generated/api';
import { createApiClient } from '$lib/api.server';
export async function load({ url, fetch }) {
export async function load({ url, fetch, locals }) {
const senderId = url.searchParams.get('senderId') || '';
const receiverId = url.searchParams.get('receiverId') || '';
const from = url.searchParams.get('from') || '';
const to = url.searchParams.get('to') || '';
const dir = url.searchParams.get('dir') || 'DESC';
const canWrite =
(locals.user as { groups?: { permissions: string[] }[] } | undefined)?.groups?.some((g) =>
g.permissions.includes('WRITE_ALL')
) ?? false;
const api = createApiClient(fetch);
let documents: components['schemas']['Document'][] = [];
@@ -56,6 +61,7 @@ export async function load({ url, fetch }) {
return {
documents,
canWrite,
initialValues: { senderName, receiverName },
filters: { senderId, receiverId, from, to, dir }
};

View File

@@ -2,9 +2,12 @@
import { goto } from '$app/navigation';
import { untrack } from 'svelte';
import { SvelteURLSearchParams } from 'svelte/reactivity';
import { m } from '$lib/paraglide/messages.js';
import ConversationFilterBar from './ConversationFilterBar.svelte';
import CorrespondenzPersonBar from './CorrespondenzPersonBar.svelte';
import CorrespondenzFilterControls from './CorrespondenzFilterControls.svelte';
import SinglePersonHintBar from './SinglePersonHintBar.svelte';
import ConversationTimeline from './ConversationTimeline.svelte';
import CorrespondenzEmptyState from './CorrespondenzEmptyState.svelte';
import { m } from '$lib/paraglide/messages.js';
let { data } = $props();
@@ -14,6 +17,10 @@ let fromDate = $state(untrack(() => data.filters.from));
let toDate = $state(untrack(() => data.filters.to));
let sortDir = $state(untrack(() => data.filters.dir));
// Derived name states — kept as reactive copies so ConversationTimeline always has current names
let senderName = $state(untrack(() => data.initialValues.senderName));
let receiverName = $state(untrack(() => data.initialValues.receiverName));
// Sync with server data after navigation
$effect(() => {
senderId = data.filters.senderId;
@@ -21,9 +28,32 @@ $effect(() => {
fromDate = data.filters.from;
toDate = data.filters.to;
sortDir = data.filters.dir;
senderName = data.initialValues.senderName;
receiverName = data.initialValues.receiverName;
});
const isSinglePerson = $derived(!!senderId && !receiverId);
const RECENT_STORAGE_KEY = 'korrespondenz_recent_persons';
const MAX_RECENT = 5;
function persistRecentPerson(id: string, name: string) {
if (!id) return;
try {
const raw = localStorage.getItem(RECENT_STORAGE_KEY);
const existing: { id: string; name: string }[] = raw ? JSON.parse(raw) : [];
const filtered = existing.filter((p) => p.id !== id);
const updated = [{ id, name }, ...filtered].slice(0, MAX_RECENT);
localStorage.setItem(RECENT_STORAGE_KEY, JSON.stringify(updated));
} catch {
// localStorage unavailable — silently ignore
}
}
function applyFilters() {
// Persist to recent persons when a person is selected
if (senderId && senderName) persistRecentPerson(senderId, senderName);
const params = new SvelteURLSearchParams();
if (senderId) params.set('senderId', senderId);
if (receiverId) params.set('receiverId', receiverId);
@@ -44,54 +74,63 @@ function swapPersons() {
receiverId = tmp;
applyFilters();
}
function selectPerson(id: string) {
senderId = id;
receiverId = '';
applyFilters();
}
</script>
<div class="mx-auto max-w-5xl px-4 py-10">
<div class="mx-auto max-w-3xl px-4 py-8">
<!-- Page Header -->
<div class="mb-8 border-b border-ink/10 pb-4">
<h1 class="font-serif text-3xl font-medium text-ink">{m.conv_heading()}</h1>
<p class="mt-2 font-sans text-sm text-ink-2">
<div class="mb-6 border-b border-[#E0DDD6] pb-4">
<h1 class="font-serif text-2xl font-semibold text-[#002850]">{m.conv_heading()}</h1>
<p class="mt-1 font-sans text-sm text-[#666]">
{m.conv_subtitle()}
</p>
</div>
<ConversationFilterBar
<!-- Filter strip: Row 1 — persons -->
<CorrespondenzPersonBar
bind:senderId={senderId}
bind:receiverId={receiverId}
bind:fromDate={fromDate}
bind:toDate={toDate}
bind:sortDir={sortDir}
initialSenderName={data.initialValues.senderName}
initialReceiverName={data.initialValues.receiverName}
onapplyFilters={applyFilters}
ontoggleSort={toggleSort}
onswapPersons={swapPersons}
/>
<!-- RESULTS LIST SECTION -->
{#if !senderId || !receiverId}
<div
class="flex flex-col items-center justify-center rounded-sm border border-dashed border-line bg-surface py-24 text-center"
>
<div class="mb-4 rounded-full bg-muted p-4 text-ink">
<svg class="h-8 w-8" fill="none" stroke="currentColor" viewBox="0 0 24 24"
><path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"
/></svg
>
</div>
<p class="font-serif text-lg text-ink">{m.conv_empty_heading()}</p>
<p class="mt-1 font-sans text-sm text-ink-2">{m.conv_empty_text()}</p>
</div>
<!-- Filter strip: Row 2 — date/sort/count -->
<CorrespondenzFilterControls
senderId={senderId}
bind:fromDate={fromDate}
bind:toDate={toDate}
bind:sortDir={sortDir}
documentCount={data.documents.length}
onapplyFilters={applyFilters}
ontoggleSort={toggleSort}
/>
<!-- Single-person hint bar -->
{#if isSinglePerson}
<SinglePersonHintBar
senderName={senderName}
fromDate={fromDate || undefined}
toDate={toDate || undefined}
sortDir={sortDir}
/>
{/if}
<!-- Results -->
{#if !senderId}
<CorrespondenzEmptyState onSelectPerson={selectPerson} />
{:else if data.documents.length === 0}
<div
class="flex flex-col items-center justify-center rounded-sm border border-line bg-surface py-24 text-center shadow-sm"
class="flex flex-col items-center justify-center rounded-sm border border-[#E0DDD6] bg-[#F7F5F2] py-24 text-center shadow-sm"
>
<p class="font-serif text-ink">{m.conv_no_results_heading()}</p>
<p class="mt-2 text-sm text-ink-3">{m.conv_no_results_text()}</p>
<p class="font-serif text-[#333]">{m.conv_no_results_heading()}</p>
<p class="mt-2 text-sm text-[#888]">{m.conv_no_results_text()}</p>
</div>
{:else}
<ConversationTimeline
@@ -99,6 +138,8 @@ function swapPersons() {
senderId={senderId}
receiverId={receiverId}
canWrite={data.canWrite}
senderName={senderName}
receiverName={receiverName}
/>
{/if}
</div>