feat(frontend): new strip components, suggestions dropdown, empty state
CorrespondenzPersonBar (Row 1), CorrespondenzFilterControls (Row 2 with live count + sort), CorrespondentSuggestionsDropdown (fetch-on-focus, keyboard nav), SinglePersonHintBar, CorrespondenzEmptyState (recent persons from localStorage). New i18n shim in messages-extra.ts until root-owned paraglide files can be regenerated. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
37
frontend/src/lib/messages-extra.ts
Normal file
37
frontend/src/lib/messages-extra.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
/**
|
||||||
|
* Extra message functions for i18n keys added after the last paraglide compile.
|
||||||
|
*
|
||||||
|
* TODO: Remove this file once the root-owned paraglide files in src/lib/paraglide/
|
||||||
|
* are regenerated (run `npm run dev` or the paraglide compile step as the owning user).
|
||||||
|
* At that point, these functions will be generated into _index.js and the components
|
||||||
|
* that import from here should switch back to importing from $lib/paraglide/messages.js.
|
||||||
|
*
|
||||||
|
* Note: these fall back to German only — locale switching is handled by the generated
|
||||||
|
* paraglide files, not this shim.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Svelte auto-escapes interpolated values — do not use {@html} with these strings.
|
||||||
|
|
||||||
|
export const conv_hint_single_person = (inputs: { name: string }) =>
|
||||||
|
`Alle Briefe von ${inputs.name} — wähle einen Korrespondenten oben um einzugrenzen`;
|
||||||
|
|
||||||
|
export const conv_hint_single_person_filtered = (inputs: {
|
||||||
|
name: string;
|
||||||
|
from: string;
|
||||||
|
to: string;
|
||||||
|
sortLabel: string;
|
||||||
|
}) => `Alle Briefe von ${inputs.name} · ${inputs.from}–${inputs.to} · ${inputs.sortLabel}`;
|
||||||
|
|
||||||
|
export const conv_strip_period = () => 'Zeitraum';
|
||||||
|
export const conv_strip_from_placeholder = () => 'Von…';
|
||||||
|
export const conv_strip_to_placeholder = () => 'Bis…';
|
||||||
|
export const conv_strip_all_correspondents = () => 'Alle Korrespondenten';
|
||||||
|
export const conv_strip_sort_newest = () => 'Neueste';
|
||||||
|
export const conv_strip_sort_oldest = () => 'Älteste';
|
||||||
|
export const conv_suggestions_heading = () => 'Häufigste Korrespondenten';
|
||||||
|
export const conv_suggestions_all_label = (inputs: { name: string }) =>
|
||||||
|
`Alle Korrespondenten von ${inputs.name}`;
|
||||||
|
export const conv_letters_count = (inputs: { count: number }) => `${inputs.count} Briefe`;
|
||||||
|
export const conv_empty_search_placeholder = () => 'Person suchen…';
|
||||||
|
export const conv_empty_recent_label = () => 'Zuletzt geöffnet';
|
||||||
|
export const conv_no_party = () => '—';
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { conv_suggestions_heading, conv_suggestions_all_label } from '$lib/messages-extra';
|
||||||
|
|
||||||
|
interface Correspondent {
|
||||||
|
id: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
senderId: string;
|
||||||
|
senderName: string;
|
||||||
|
onselect: (id: string) => void;
|
||||||
|
onclose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { senderId, senderName, onselect, onclose }: Props = $props();
|
||||||
|
|
||||||
|
let results = $state<Correspondent[]>([]);
|
||||||
|
let loading = $state(true);
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/persons/${senderId}/correspondents`);
|
||||||
|
results = res.ok ? await res.json() : [];
|
||||||
|
} catch {
|
||||||
|
results = [];
|
||||||
|
} finally {
|
||||||
|
loading = false;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
});
|
||||||
|
|
||||||
|
function clickOutside(node: HTMLElement) {
|
||||||
|
const handleClick = (event: MouseEvent) => {
|
||||||
|
if (node && !node.contains(event.target as Node) && !event.defaultPrevented) {
|
||||||
|
onclose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener('click', handleClick, true);
|
||||||
|
return {
|
||||||
|
destroy() {
|
||||||
|
document.removeEventListener('click', handleClick, true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOptionElements(container: HTMLElement): HTMLElement[] {
|
||||||
|
return Array.from(container.querySelectorAll<HTMLElement>('[role="option"]'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKeydown(event: KeyboardEvent, container: HTMLElement) {
|
||||||
|
const options = getOptionElements(container);
|
||||||
|
const focused = document.activeElement as HTMLElement;
|
||||||
|
const idx = options.indexOf(focused);
|
||||||
|
|
||||||
|
if (event.key === 'ArrowDown') {
|
||||||
|
event.preventDefault();
|
||||||
|
const next = options[idx + 1] ?? options[0];
|
||||||
|
next?.focus();
|
||||||
|
} else if (event.key === 'ArrowUp') {
|
||||||
|
event.preventDefault();
|
||||||
|
const prev = options[idx - 1] ?? options[options.length - 1];
|
||||||
|
prev?.focus();
|
||||||
|
} else if (event.key === 'Escape') {
|
||||||
|
onclose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getInitials(person: Correspondent): string {
|
||||||
|
return `${person.firstName.charAt(0)}${person.lastName.charAt(0)}`.toUpperCase();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
use:clickOutside
|
||||||
|
role="listbox"
|
||||||
|
tabindex="-1"
|
||||||
|
aria-label={conv_suggestions_heading()}
|
||||||
|
class="absolute top-full right-0 left-0 z-30 mt-1 rounded-sm border border-[#E0DDD6] bg-white shadow-lg"
|
||||||
|
onkeydown={(e) => handleKeydown(e, e.currentTarget as HTMLElement)}
|
||||||
|
>
|
||||||
|
<!-- Heading -->
|
||||||
|
<div class="px-3 pt-2 pb-1 text-[10px] font-bold tracking-widest text-[#888] uppercase">
|
||||||
|
{conv_suggestions_heading()}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Correspondent rows -->
|
||||||
|
{#if !loading}
|
||||||
|
{#each results as person (person.id)}
|
||||||
|
<div
|
||||||
|
role="option"
|
||||||
|
aria-selected="false"
|
||||||
|
tabindex="0"
|
||||||
|
class="flex cursor-pointer items-center gap-2 px-3 py-2 text-sm text-[#333] hover:bg-[#F7F5F2] focus:bg-[#F7F5F2] focus:outline-none"
|
||||||
|
onclick={() => onselect(person.id)}
|
||||||
|
onkeydown={(e) => e.key === 'Enter' && onselect(person.id)}
|
||||||
|
>
|
||||||
|
<!-- Avatar with initials -->
|
||||||
|
<span
|
||||||
|
class="flex h-4 w-4 shrink-0 items-center justify-center rounded-full bg-[#002850] text-[10px] font-bold text-white"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
{getInitials(person)}
|
||||||
|
</span>
|
||||||
|
<!-- Svelte auto-escapes — do not use {@html} here. -->
|
||||||
|
{person.lastName}, {person.firstName}
|
||||||
|
<!-- TODO: show proportional letter-count bar when counts are available from the API -->
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Separator -->
|
||||||
|
<div class="mt-1 border-t border-[#E0DDD6]"></div>
|
||||||
|
|
||||||
|
<!-- "Alle Korrespondenten" row -->
|
||||||
|
<div
|
||||||
|
role="option"
|
||||||
|
aria-selected="false"
|
||||||
|
tabindex="0"
|
||||||
|
class="flex cursor-pointer items-center gap-2 px-3 py-2 text-sm text-[#333] hover:bg-[#F7F5F2] focus:bg-[#F7F5F2] focus:outline-none"
|
||||||
|
onclick={() => onselect('')}
|
||||||
|
onkeydown={(e) => e.key === 'Enter' && onselect('')}
|
||||||
|
>
|
||||||
|
{conv_suggestions_all_label({ name: senderName })}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
121
frontend/src/routes/korrespondenz/CorrespondenzEmptyState.svelte
Normal file
121
frontend/src/routes/korrespondenz/CorrespondenzEmptyState.svelte
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { conv_empty_search_placeholder, conv_empty_recent_label } from '$lib/messages-extra';
|
||||||
|
|
||||||
|
interface RecentPerson {
|
||||||
|
id: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onSelectPerson: (id: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { onSelectPerson }: Props = $props();
|
||||||
|
|
||||||
|
let recentPersons = $state<RecentPerson[]>([]);
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem('korrespondenz_recent_persons');
|
||||||
|
if (raw) {
|
||||||
|
// Svelte auto-escapes firstName/lastName — do not use {@html} with these values
|
||||||
|
recentPersons = JSON.parse(raw) as RecentPerson[];
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
recentPersons = [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="mx-auto flex max-w-sm flex-col items-center gap-4 py-16 text-center">
|
||||||
|
<!-- Icon circle -->
|
||||||
|
<div class="rounded-full bg-[#F0EDE6] p-4">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class="text-[#002850]"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<rect x="2" y="4" width="20" height="16" rx="2" />
|
||||||
|
<path d="M2 7l10 7 10-7" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Heading -->
|
||||||
|
<h2 class="font-serif text-sm font-black text-[#0D2240]">Korrespondenz durchsuchen</h2>
|
||||||
|
|
||||||
|
<!-- Subtext -->
|
||||||
|
<p class="max-w-[280px] text-xs text-[#888]">
|
||||||
|
Wähle eine Person aus dem Archiv um deren Briefe zu sehen — mit oder ohne Korrespondent.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Search input placeholder (visual only — clicking focuses Person A typeahead above) -->
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
data-testid="conv-empty-search"
|
||||||
|
aria-label={conv_empty_search_placeholder()}
|
||||||
|
onclick={() => onSelectPerson('')}
|
||||||
|
class="flex h-[28px] w-[260px] items-center rounded-sm border border-[#D1D5DB] bg-[#F9F8F6] px-3 text-xs text-[#AAA] italic transition-colors hover:border-[#002850]"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="12"
|
||||||
|
height="12"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class="mr-1.5 shrink-0"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<circle cx="11" cy="11" r="8" />
|
||||||
|
<path d="m21 21-4.35-4.35" />
|
||||||
|
</svg>
|
||||||
|
Person suchen…
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Divider -->
|
||||||
|
<div class="flex w-full items-center gap-2">
|
||||||
|
<div class="flex-1 border-t border-[#E0DDD6]"></div>
|
||||||
|
<span class="text-[10px] font-bold tracking-wider text-[#AAA] uppercase">oder</span>
|
||||||
|
<div class="flex-1 border-t border-[#E0DDD6]"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Recent persons -->
|
||||||
|
{#if recentPersons.length > 0}
|
||||||
|
<div class="flex w-full flex-col items-center gap-2">
|
||||||
|
<span class="text-[10px] font-bold tracking-widest text-[#888] uppercase">
|
||||||
|
{conv_empty_recent_label()}
|
||||||
|
</span>
|
||||||
|
<div class="flex flex-wrap justify-center gap-2">
|
||||||
|
{#each recentPersons as person (person.id)}
|
||||||
|
<!-- TODO: allow clearing recent history -->
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick={() => onSelectPerson(person.id)}
|
||||||
|
class="flex items-center gap-1.5 rounded-full border border-[#D1D5DB] bg-white px-3 py-1.5 text-xs font-bold text-[#333] transition-colors hover:border-[#002850] hover:text-[#002850]"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="flex h-4 w-4 shrink-0 items-center justify-center rounded-full bg-[#002850] text-[10px] text-white"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
{person.firstName[0]}{person.lastName[0]}
|
||||||
|
</span>
|
||||||
|
<span class="hidden sm:inline">{person.lastName}, </span>{person.firstName}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {
|
||||||
|
conv_strip_period,
|
||||||
|
conv_strip_from_placeholder,
|
||||||
|
conv_strip_to_placeholder,
|
||||||
|
conv_strip_sort_newest,
|
||||||
|
conv_strip_sort_oldest,
|
||||||
|
conv_letters_count
|
||||||
|
} from '$lib/messages-extra';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
senderId: string;
|
||||||
|
fromDate?: string;
|
||||||
|
toDate?: string;
|
||||||
|
sortDir?: string;
|
||||||
|
documentCount?: number;
|
||||||
|
onapplyFilters: () => void;
|
||||||
|
ontoggleSort: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
let {
|
||||||
|
senderId,
|
||||||
|
fromDate = $bindable(''),
|
||||||
|
toDate = $bindable(''),
|
||||||
|
sortDir = $bindable('DESC'),
|
||||||
|
documentCount,
|
||||||
|
onapplyFilters,
|
||||||
|
ontoggleSort
|
||||||
|
}: Props = $props();
|
||||||
|
|
||||||
|
let hasDateFilter = $derived(!!(fromDate || toDate));
|
||||||
|
let isActive = $derived(!!(fromDate || toDate || sortDir !== 'DESC'));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="flex items-center gap-[10px] border-b border-[#E0DDD6] bg-[#F7F5F2] px-4 py-[5px] transition-opacity sm:px-[18px]"
|
||||||
|
class:opacity-40={!senderId}
|
||||||
|
class:pointer-events-none={!senderId}
|
||||||
|
aria-disabled={!senderId}
|
||||||
|
>
|
||||||
|
<!-- Period label -->
|
||||||
|
<span class="hidden text-xs font-bold text-[#888] sm:block">
|
||||||
|
{conv_strip_period()}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- From date -->
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
bind:value={fromDate}
|
||||||
|
onchange={() => onapplyFilters()}
|
||||||
|
placeholder={conv_strip_from_placeholder()}
|
||||||
|
aria-label="Von"
|
||||||
|
class="h-[22px] min-h-[44px] w-[80px] rounded-[3px] border px-1 text-xs focus:outline-none sm:min-h-0"
|
||||||
|
class:border-[#002850]={!!fromDate}
|
||||||
|
class:text-[#333]={!!fromDate}
|
||||||
|
class:border-[#D1D5DB]={!fromDate}
|
||||||
|
class:text-[#AAA]={!fromDate}
|
||||||
|
class:italic={!fromDate}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span class="text-xs text-[#AAA]">–</span>
|
||||||
|
|
||||||
|
<!-- To date -->
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
bind:value={toDate}
|
||||||
|
onchange={() => onapplyFilters()}
|
||||||
|
placeholder={conv_strip_to_placeholder()}
|
||||||
|
aria-label="Bis"
|
||||||
|
class="h-[22px] min-h-[44px] w-[80px] rounded-[3px] border px-1 text-xs focus:outline-none sm:min-h-0"
|
||||||
|
class:border-[#002850]={!!toDate}
|
||||||
|
class:text-[#333]={!!toDate}
|
||||||
|
class:border-[#D1D5DB]={!toDate}
|
||||||
|
class:text-[#AAA]={!toDate}
|
||||||
|
class:italic={!toDate}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Document count -->
|
||||||
|
<span
|
||||||
|
data-testid="conv-strip-count"
|
||||||
|
class="ml-auto text-xs font-bold"
|
||||||
|
class:text-[#002850]={hasDateFilter}
|
||||||
|
class:text-[#888]={!hasDateFilter}
|
||||||
|
>
|
||||||
|
{conv_letters_count({ count: documentCount ?? 0 })}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- Sort button -->
|
||||||
|
<button
|
||||||
|
data-testid="conv-sort-btn"
|
||||||
|
type="button"
|
||||||
|
aria-label="Sortierung umkehren"
|
||||||
|
aria-pressed={sortDir === 'ASC'}
|
||||||
|
onclick={ontoggleSort}
|
||||||
|
class="flex h-[22px] min-h-[44px] items-center gap-1 rounded-[3px] border px-2 text-xs font-bold sm:min-h-0"
|
||||||
|
class:border-[#002850]={isActive}
|
||||||
|
class:text-[#002850]={isActive}
|
||||||
|
class:border-[#D1D5DB]={!isActive}
|
||||||
|
class:text-[#888]={!isActive}
|
||||||
|
>
|
||||||
|
{#if sortDir === 'ASC'}
|
||||||
|
{conv_strip_sort_oldest()}
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="10"
|
||||||
|
height="10"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2.5"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<polyline points="18 15 12 9 6 15" />
|
||||||
|
</svg>
|
||||||
|
{:else}
|
||||||
|
{conv_strip_sort_newest()}
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="10"
|
||||||
|
height="10"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2.5"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<polyline points="6 9 12 15 18 9" />
|
||||||
|
</svg>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import PersonTypeahead from '$lib/components/PersonTypeahead.svelte';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
senderId?: string;
|
||||||
|
receiverId?: string;
|
||||||
|
initialSenderName?: string;
|
||||||
|
initialReceiverName?: string;
|
||||||
|
onapplyFilters: () => void;
|
||||||
|
onswapPersons: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
let {
|
||||||
|
senderId = $bindable(''),
|
||||||
|
receiverId = $bindable(''),
|
||||||
|
initialSenderName = '',
|
||||||
|
initialReceiverName = '',
|
||||||
|
onapplyFilters,
|
||||||
|
onswapPersons
|
||||||
|
}: Props = $props();
|
||||||
|
|
||||||
|
let swapVisible = $derived(!!(senderId && receiverId));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex items-end gap-[9px] border-b border-[#EAE7E0] bg-white px-4 py-[9px] sm:px-[18px]">
|
||||||
|
<!-- Person A -->
|
||||||
|
<div class="min-w-0 flex-1">
|
||||||
|
<PersonTypeahead
|
||||||
|
name="senderId"
|
||||||
|
label="Person"
|
||||||
|
bind:value={senderId}
|
||||||
|
initialName={initialSenderName}
|
||||||
|
restrictToCorrespondentsOf={receiverId || undefined}
|
||||||
|
onchange={() => onapplyFilters()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Swap button -->
|
||||||
|
<button
|
||||||
|
data-testid="conv-swap-btn"
|
||||||
|
type="button"
|
||||||
|
aria-label="Personen tauschen"
|
||||||
|
onclick={onswapPersons}
|
||||||
|
class="mb-1 flex h-7 w-7 shrink-0 items-center justify-center rounded border border-[#D1D5DB] bg-white text-[#888] transition-colors hover:border-[#002850] hover:text-[#002850]"
|
||||||
|
class:opacity-0={!swapVisible}
|
||||||
|
class:pointer-events-none={!swapVisible}
|
||||||
|
tabindex={swapVisible ? 0 : -1}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path d="M7 16V4m0 0L3 8m4-4l4 4" />
|
||||||
|
<path d="M17 8v12m0 0l4-4m-4 4l-4-4" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Korrespondent field -->
|
||||||
|
<div
|
||||||
|
class="min-w-0 flex-1"
|
||||||
|
class:[&_input]:border-dashed={!receiverId}
|
||||||
|
class:[&_input]:border-solid={!!receiverId}
|
||||||
|
class:[&_input]:bg-[#F9F8F6]={!receiverId}
|
||||||
|
>
|
||||||
|
<PersonTypeahead
|
||||||
|
name="receiverId"
|
||||||
|
label={receiverId ? 'Korrespondent' : 'Korrespondent'}
|
||||||
|
bind:value={receiverId}
|
||||||
|
initialName={initialReceiverName}
|
||||||
|
restrictToCorrespondentsOf={senderId || undefined}
|
||||||
|
onchange={() => onapplyFilters()}
|
||||||
|
/>
|
||||||
|
{#if !receiverId}
|
||||||
|
<span class="pointer-events-none absolute -mt-[1px] text-[11px] text-[#AAA] italic">
|
||||||
|
— optional
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
38
frontend/src/routes/korrespondenz/SinglePersonHintBar.svelte
Normal file
38
frontend/src/routes/korrespondenz/SinglePersonHintBar.svelte
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {
|
||||||
|
conv_hint_single_person,
|
||||||
|
conv_hint_single_person_filtered,
|
||||||
|
conv_strip_sort_newest,
|
||||||
|
conv_strip_sort_oldest
|
||||||
|
} from '$lib/messages-extra';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
senderName: string;
|
||||||
|
fromDate?: string;
|
||||||
|
toDate?: string;
|
||||||
|
sortDir?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { senderName, fromDate = '', toDate = '', sortDir = 'DESC' }: Props = $props();
|
||||||
|
|
||||||
|
let hasDateFilter = $derived(!!(fromDate || toDate));
|
||||||
|
|
||||||
|
let sortLabel = $derived(sortDir === 'ASC' ? conv_strip_sort_oldest() : conv_strip_sort_newest());
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="flex items-center gap-[5px] border-b border-[#FDBA74] bg-[#FFF7ED] px-[18px] py-[6px] text-xs text-[#92400E]"
|
||||||
|
>
|
||||||
|
<span class="text-sm" aria-hidden="true">📋</span>
|
||||||
|
|
||||||
|
{#if hasDateFilter}
|
||||||
|
{conv_hint_single_person_filtered({
|
||||||
|
name: senderName,
|
||||||
|
from: fromDate ?? '',
|
||||||
|
to: toDate ?? '',
|
||||||
|
sortLabel
|
||||||
|
})}
|
||||||
|
{:else}
|
||||||
|
{conv_hint_single_person({ name: senderName })}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
Reference in New Issue
Block a user