Files
familienarchiv/frontend/src/routes/briefwechsel/CorrespondenzPersonBar.svelte
2026-05-05 14:26:21 +02:00

188 lines
5.3 KiB
Svelte

<script lang="ts">
import PersonTypeahead from '$lib/person/PersonTypeahead.svelte';
import CorrespondentSuggestionsDropdown from './CorrespondentSuggestionsDropdown.svelte';
import { m } from '$lib/paraglide/messages.js';
interface Props {
senderId?: string;
receiverId?: string;
initialSenderName?: string;
initialReceiverName?: string;
sortDir?: string;
showAdvanced?: boolean;
documentCount?: number;
onapplyFilters: () => void;
onswapPersons: () => void;
ontoggleSort: () => void;
ontoggleAdvanced: () => void;
}
let {
senderId = $bindable(''),
receiverId = $bindable(''),
initialSenderName = '',
initialReceiverName = '',
sortDir = 'DESC',
showAdvanced = false,
documentCount = 0,
onapplyFilters,
onswapPersons,
ontoggleSort,
ontoggleAdvanced
}: Props = $props();
interface Correspondent {
id: string;
firstName: string;
lastName: string;
}
let swapVisible = $derived(!!(senderId && receiverId));
let showSuggestions = $state(false);
let correspondents = $state<Correspondent[]>([]);
let loadingCorrespondents = $state(false);
async function handleCorrespondentFocused() {
if (!senderId) return;
showSuggestions = true;
loadingCorrespondents = true;
try {
const res = await fetch(`/api/persons/${senderId}/correspondents`);
correspondents = res.ok ? await res.json() : [];
} catch {
correspondents = [];
} finally {
loadingCorrespondents = false;
}
}
function handleSuggestionSelect(id: string) {
receiverId = id;
showSuggestions = false;
onapplyFilters();
}
</script>
<!-- Row 1: Person inputs -->
<div data-testid="conv-person-bar" class="flex items-end gap-4">
<!-- Person A -->
<div
class="min-w-0 flex-1 [&_input]:border-line [&_input]:py-2.5 [&_label]:mb-2 [&_label]:text-xs [&_label]:font-bold [&_label]:tracking-widest [&_label]:text-ink-2 [&_label]:uppercase"
>
<PersonTypeahead
name="senderId"
label="Person"
bind:value={senderId}
initialName={initialSenderName}
restrictToCorrespondentsOf={receiverId || undefined}
onchange={(id) => { if (id) onapplyFilters(); }}
/>
</div>
<!-- Swap button -->
<button
data-testid="conv-swap-btn"
type="button"
aria-label="Personen tauschen"
onclick={onswapPersons}
class="mb-[3px] flex items-center justify-center rounded border border-line bg-muted px-3 py-2.5 text-ink-3 transition-colors hover:border-primary hover:text-primary"
class:opacity-0={!swapVisible}
class:pointer-events-none={!swapVisible}
tabindex={swapVisible ? 0 : -1}
>
<div class="-my-1 flex flex-col items-center">
<img
src="/degruyter-icons/Simple/Medium-24px/SVG/Action/Long-Arrow/Long-Arrow-Right-MD.svg"
alt=""
aria-hidden="true"
class="h-3.5 w-3.5 opacity-60"
/>
<img
src="/degruyter-icons/Simple/Medium-24px/SVG/Action/Long-Arrow/Long-Arrow-Left-MD.svg"
alt=""
aria-hidden="true"
class="h-3.5 w-3.5 opacity-60"
/>
</div>
</button>
<!-- Korrespondent field -->
<div
class="relative min-w-0 flex-1 [&_input]:border-line [&_input]:py-2.5 [&_label]:mb-2 [&_label]:text-xs [&_label]:font-bold [&_label]:tracking-widest [&_label]:text-ink-2 [&_label]:uppercase"
>
<PersonTypeahead
name="receiverId"
label={receiverId ? 'Korrespondent' : 'Korrespondent — optional'}
bind:value={receiverId}
initialName={initialReceiverName}
placeholder="Alle Korrespondenten"
restrictToCorrespondentsOf={senderId || undefined}
onchange={() => {
showSuggestions = false;
onapplyFilters();
}}
onfocused={handleCorrespondentFocused}
/>
{#if showSuggestions && senderId && !receiverId}
<CorrespondentSuggestionsDropdown
correspondents={correspondents}
loading={loadingCorrespondents}
senderName=""
onselect={handleSuggestionSelect}
onclose={() => (showSuggestions = false)}
/>
{/if}
</div>
</div>
<!-- Row 2: Sort + Filter toggle + Count (mirrors document search bar pattern) -->
<div class="mt-4 flex items-center gap-4">
<!-- Sort button -->
<button
data-testid="conv-sort-btn"
type="button"
aria-label="Sortierung umkehren"
aria-pressed={sortDir === 'ASC'}
onclick={ontoggleSort}
class="flex items-center gap-2 border border-line bg-muted px-4 py-2.5 text-sm font-bold tracking-wide text-ink-2 uppercase transition hover:bg-muted hover:text-ink"
>
{#if sortDir === 'ASC'}
{m.conv_strip_sort_oldest()}
<img
src="/degruyter-icons/Simple/Medium-24px/SVG/Action/Long-Arrow/Long-Arrow-Up-MD.svg"
alt=""
aria-hidden="true"
class="h-5 w-5 opacity-60"
/>
{:else}
{m.conv_strip_sort_newest()}
<img
src="/degruyter-icons/Simple/Medium-24px/SVG/Action/Long-Arrow/Long-Arrow-Down-MD.svg"
alt=""
aria-hidden="true"
class="h-5 w-5 opacity-60"
/>
{/if}
</button>
<!-- Filter toggle button -->
<button
onclick={ontoggleAdvanced}
class="flex items-center gap-2 border border-line bg-muted px-4 py-2.5 text-sm font-bold tracking-wide text-ink-2 uppercase transition hover:bg-muted hover:text-ink"
>
<img
src="/degruyter-icons/Simple/Small-16px/SVG/Action/Chevron/Chevron-Down-SM.svg"
alt=""
aria-hidden="true"
class="h-4 w-4 transform transition-transform duration-200 {showAdvanced ? 'rotate-180' : ''}"
/>
{m.docs_btn_filter()}
</button>
<!-- Document count -->
<span data-testid="conv-strip-count" class="ml-auto text-sm font-bold text-ink-3">
{m.conv_letters_count({ count: documentCount })}
</span>
</div>