188 lines
5.3 KiB
Svelte
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>
|