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:
@@ -1,13 +1,18 @@
|
|||||||
import type { components } from '$lib/generated/api';
|
import type { components } from '$lib/generated/api';
|
||||||
import { createApiClient } from '$lib/api.server';
|
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 senderId = url.searchParams.get('senderId') || '';
|
||||||
const receiverId = url.searchParams.get('receiverId') || '';
|
const receiverId = url.searchParams.get('receiverId') || '';
|
||||||
const from = url.searchParams.get('from') || '';
|
const from = url.searchParams.get('from') || '';
|
||||||
const to = url.searchParams.get('to') || '';
|
const to = url.searchParams.get('to') || '';
|
||||||
const dir = url.searchParams.get('dir') || 'DESC';
|
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);
|
const api = createApiClient(fetch);
|
||||||
|
|
||||||
let documents: components['schemas']['Document'][] = [];
|
let documents: components['schemas']['Document'][] = [];
|
||||||
@@ -56,6 +61,7 @@ export async function load({ url, fetch }) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
documents,
|
documents,
|
||||||
|
canWrite,
|
||||||
initialValues: { senderName, receiverName },
|
initialValues: { senderName, receiverName },
|
||||||
filters: { senderId, receiverId, from, to, dir }
|
filters: { senderId, receiverId, from, to, dir }
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,9 +2,12 @@
|
|||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { untrack } from 'svelte';
|
import { untrack } from 'svelte';
|
||||||
import { SvelteURLSearchParams } from 'svelte/reactivity';
|
import { SvelteURLSearchParams } from 'svelte/reactivity';
|
||||||
import { m } from '$lib/paraglide/messages.js';
|
import CorrespondenzPersonBar from './CorrespondenzPersonBar.svelte';
|
||||||
import ConversationFilterBar from './ConversationFilterBar.svelte';
|
import CorrespondenzFilterControls from './CorrespondenzFilterControls.svelte';
|
||||||
|
import SinglePersonHintBar from './SinglePersonHintBar.svelte';
|
||||||
import ConversationTimeline from './ConversationTimeline.svelte';
|
import ConversationTimeline from './ConversationTimeline.svelte';
|
||||||
|
import CorrespondenzEmptyState from './CorrespondenzEmptyState.svelte';
|
||||||
|
import { m } from '$lib/paraglide/messages.js';
|
||||||
|
|
||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
|
|
||||||
@@ -14,6 +17,10 @@ let fromDate = $state(untrack(() => data.filters.from));
|
|||||||
let toDate = $state(untrack(() => data.filters.to));
|
let toDate = $state(untrack(() => data.filters.to));
|
||||||
let sortDir = $state(untrack(() => data.filters.dir));
|
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
|
// Sync with server data after navigation
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
senderId = data.filters.senderId;
|
senderId = data.filters.senderId;
|
||||||
@@ -21,9 +28,32 @@ $effect(() => {
|
|||||||
fromDate = data.filters.from;
|
fromDate = data.filters.from;
|
||||||
toDate = data.filters.to;
|
toDate = data.filters.to;
|
||||||
sortDir = data.filters.dir;
|
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() {
|
function applyFilters() {
|
||||||
|
// Persist to recent persons when a person is selected
|
||||||
|
if (senderId && senderName) persistRecentPerson(senderId, senderName);
|
||||||
|
|
||||||
const params = new SvelteURLSearchParams();
|
const params = new SvelteURLSearchParams();
|
||||||
if (senderId) params.set('senderId', senderId);
|
if (senderId) params.set('senderId', senderId);
|
||||||
if (receiverId) params.set('receiverId', receiverId);
|
if (receiverId) params.set('receiverId', receiverId);
|
||||||
@@ -44,54 +74,63 @@ function swapPersons() {
|
|||||||
receiverId = tmp;
|
receiverId = tmp;
|
||||||
applyFilters();
|
applyFilters();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function selectPerson(id: string) {
|
||||||
|
senderId = id;
|
||||||
|
receiverId = '';
|
||||||
|
applyFilters();
|
||||||
|
}
|
||||||
</script>
|
</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 -->
|
<!-- Page Header -->
|
||||||
<div class="mb-8 border-b border-ink/10 pb-4">
|
<div class="mb-6 border-b border-[#E0DDD6] pb-4">
|
||||||
<h1 class="font-serif text-3xl font-medium text-ink">{m.conv_heading()}</h1>
|
<h1 class="font-serif text-2xl font-semibold text-[#002850]">{m.conv_heading()}</h1>
|
||||||
<p class="mt-2 font-sans text-sm text-ink-2">
|
<p class="mt-1 font-sans text-sm text-[#666]">
|
||||||
{m.conv_subtitle()}
|
{m.conv_subtitle()}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ConversationFilterBar
|
<!-- Filter strip: Row 1 — persons -->
|
||||||
|
<CorrespondenzPersonBar
|
||||||
bind:senderId={senderId}
|
bind:senderId={senderId}
|
||||||
bind:receiverId={receiverId}
|
bind:receiverId={receiverId}
|
||||||
bind:fromDate={fromDate}
|
|
||||||
bind:toDate={toDate}
|
|
||||||
bind:sortDir={sortDir}
|
|
||||||
initialSenderName={data.initialValues.senderName}
|
initialSenderName={data.initialValues.senderName}
|
||||||
initialReceiverName={data.initialValues.receiverName}
|
initialReceiverName={data.initialValues.receiverName}
|
||||||
onapplyFilters={applyFilters}
|
onapplyFilters={applyFilters}
|
||||||
ontoggleSort={toggleSort}
|
|
||||||
onswapPersons={swapPersons}
|
onswapPersons={swapPersons}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- RESULTS LIST SECTION -->
|
<!-- Filter strip: Row 2 — date/sort/count -->
|
||||||
{#if !senderId || !receiverId}
|
<CorrespondenzFilterControls
|
||||||
<div
|
senderId={senderId}
|
||||||
class="flex flex-col items-center justify-center rounded-sm border border-dashed border-line bg-surface py-24 text-center"
|
bind:fromDate={fromDate}
|
||||||
>
|
bind:toDate={toDate}
|
||||||
<div class="mb-4 rounded-full bg-muted p-4 text-ink">
|
bind:sortDir={sortDir}
|
||||||
<svg class="h-8 w-8" fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
documentCount={data.documents.length}
|
||||||
><path
|
onapplyFilters={applyFilters}
|
||||||
stroke-linecap="round"
|
ontoggleSort={toggleSort}
|
||||||
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"
|
<!-- Single-person hint bar -->
|
||||||
/></svg
|
{#if isSinglePerson}
|
||||||
>
|
<SinglePersonHintBar
|
||||||
</div>
|
senderName={senderName}
|
||||||
<p class="font-serif text-lg text-ink">{m.conv_empty_heading()}</p>
|
fromDate={fromDate || undefined}
|
||||||
<p class="mt-1 font-sans text-sm text-ink-2">{m.conv_empty_text()}</p>
|
toDate={toDate || undefined}
|
||||||
</div>
|
sortDir={sortDir}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Results -->
|
||||||
|
{#if !senderId}
|
||||||
|
<CorrespondenzEmptyState onSelectPerson={selectPerson} />
|
||||||
{:else if data.documents.length === 0}
|
{:else if data.documents.length === 0}
|
||||||
<div
|
<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="font-serif text-[#333]">{m.conv_no_results_heading()}</p>
|
||||||
<p class="mt-2 text-sm text-ink-3">{m.conv_no_results_text()}</p>
|
<p class="mt-2 text-sm text-[#888]">{m.conv_no_results_text()}</p>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<ConversationTimeline
|
<ConversationTimeline
|
||||||
@@ -99,6 +138,8 @@ function swapPersons() {
|
|||||||
senderId={senderId}
|
senderId={senderId}
|
||||||
receiverId={receiverId}
|
receiverId={receiverId}
|
||||||
canWrite={data.canWrite}
|
canWrite={data.canWrite}
|
||||||
|
senderName={senderName}
|
||||||
|
receiverName={receiverName}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user