Some checks failed
CI / Unit & Component Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
CI / Unit & Component Tests (pull_request) Failing after 1m36s
CI / Backend Unit Tests (pull_request) Failing after 2m36s
CI / E2E Tests (pull_request) Failing after 1h49m0s
Blockers (14): - B1: fix senderName/receiverName to use $derived instead of $state + sync $effect - B2: migrate all korrespondenz components from messages-extra shim to paraglide m.* - B3: i18n CorrespondenzEmptyState (heading, subtext, search placeholder) - B4: add response.ok checks to admin layout server load - B5: add response.ok checks to korrespondenz page server load - B6: add page.server.spec.ts with 5 test suites for korrespondenz load function - B7: add axe-core accessibility checks to all e2e korrespondenz tests - B8: add Testcontainers JPQL tests for findSinglePersonCorrespondence (DISTINCT + sender) - B9: hide auth reset-token endpoint from OpenAPI spec; remove from generated api.ts - B11: replace amber hardcoded hex colors in SinglePersonHintBar with brand tokens - B12: replace clipboard emoji with Heroicons SVG in SinglePersonHintBar - B13: create DateInput component (German dd.mm.yyyy); use it in CorrespondenzFilterControls - B14: add Paraglide compile step to CI workflow before lint/test Suggestions (11): - S1: make CorrespondentSuggestionsDropdown a pure display component; lift fetch to PersonBar - S2: fix leftover messages-extra import in ConversationTimeline; use brand tokens for status dots - S3: add intent comment to EntityNav openFlyout behavior - S4: rename canManageGroups → canManagePermissions throughout admin - S6: remove domFlush helper from DateInput spec; use expect.poll instead - S7: replace test.skip with throw new Error in bilateral e2e tests - S8: add inverse aria-disabled test for filter strip - S9: remove sm:min-h-0 from sort button to preserve 44px touch target - S10: add title attributes to tablet trigger buttons in EntityNav - S11: delete messages-extra.ts shim entirely Also: fix admin pages revealing blank strip at bottom (-mb-6 on admin layout) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
141 lines
4.4 KiB
Svelte
141 lines
4.4 KiB
Svelte
<script lang="ts">
|
|
import { goto } from '$app/navigation';
|
|
import { untrack } from 'svelte';
|
|
import { SvelteURLSearchParams } from 'svelte/reactivity';
|
|
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();
|
|
|
|
// Filter values are local $state so swapPersons/toggleSort can mutate them before goto.
|
|
// They are initialised once from server data and never re-synced — navigation replaces
|
|
// the page component, so each load gets a fresh init.
|
|
let senderId = $state(untrack(() => data.filters.senderId));
|
|
let receiverId = $state(untrack(() => data.filters.receiverId));
|
|
let fromDate = $state(untrack(() => data.filters.from));
|
|
let toDate = $state(untrack(() => data.filters.to));
|
|
let sortDir = $state(untrack(() => data.filters.dir));
|
|
|
|
// Names are pure reads of server data — no local mutation needed.
|
|
const senderName = $derived(data.initialValues.senderName);
|
|
const receiverName = $derived(data.initialValues.receiverName);
|
|
|
|
// Side-effect only: persist the resolved sender to localStorage once the name is available.
|
|
$effect(() => {
|
|
if (data.filters.senderId && data.initialValues.senderName) {
|
|
persistRecentPerson(data.filters.senderId, data.initialValues.senderName);
|
|
}
|
|
});
|
|
|
|
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() {
|
|
const params = new SvelteURLSearchParams();
|
|
if (senderId) params.set('senderId', senderId);
|
|
if (receiverId) params.set('receiverId', receiverId);
|
|
if (fromDate) params.set('from', fromDate);
|
|
if (toDate) params.set('to', toDate);
|
|
params.set('dir', sortDir);
|
|
goto(`/korrespondenz?${params.toString()}`, { keepFocus: true });
|
|
}
|
|
|
|
function toggleSort() {
|
|
sortDir = sortDir === 'DESC' ? 'ASC' : 'DESC';
|
|
applyFilters();
|
|
}
|
|
|
|
function swapPersons() {
|
|
const tmp = senderId;
|
|
senderId = receiverId;
|
|
receiverId = tmp;
|
|
applyFilters();
|
|
}
|
|
|
|
function selectPerson(id: string) {
|
|
if (!id) {
|
|
document.querySelector<HTMLInputElement>('#senderId-search')?.focus();
|
|
return;
|
|
}
|
|
senderId = id;
|
|
receiverId = '';
|
|
applyFilters();
|
|
}
|
|
</script>
|
|
|
|
<!-- Strips — pulled up to negate main's py-6 top padding so they sit flush -->
|
|
<div class="-mt-6">
|
|
<!-- Strip: Row 1 — full width, no container -->
|
|
<CorrespondenzPersonBar
|
|
bind:senderId={senderId}
|
|
bind:receiverId={receiverId}
|
|
initialSenderName={data.initialValues.senderName}
|
|
initialReceiverName={data.initialValues.receiverName}
|
|
onapplyFilters={applyFilters}
|
|
onswapPersons={swapPersons}
|
|
/>
|
|
|
|
<!-- Strip: Row 2 — full width -->
|
|
<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}
|
|
</div>
|
|
|
|
<!-- Content area with padding -->
|
|
<div class="px-[18px] py-[14px]">
|
|
{#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-muted 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>
|
|
</div>
|
|
{:else}
|
|
<ConversationTimeline
|
|
documents={data.documents}
|
|
senderId={senderId}
|
|
receiverId={receiverId}
|
|
canWrite={data.canWrite}
|
|
senderName={senderName}
|
|
receiverName={receiverName}
|
|
/>
|
|
{/if}
|
|
</div>
|