feat(persons): clean filterable paginated directory with crash fix
Rewrite /persons: server-side filter chips (type, family-only, has-documents) that AND within the clean reader default (familyMember OR documentCount > 0), a writer-only show-all/Zu-prüfen toggle, and reused Pagination. Extract PersonCard (fixes the null-lastName render crash and never shows a "?" initial — provisional/UNKNOWN/"?" entries get a neutral placeholder avatar + a text+icon "unbestätigt" badge, WCAG 1.4.1) and PersonFilterBar (44px aria-pressed chips, role=switch toggle with the count in its accessible name). The loader applies the reader restriction unless review=1 and surfaces a cheap needsReviewCount. i18n keys added for de/en/es. Refs #667 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -2,8 +2,23 @@ import { error } from '@sveltejs/kit';
|
||||
import { createApiClient } from '$lib/shared/api.server';
|
||||
import { getErrorMessage } from '$lib/shared/errors';
|
||||
|
||||
const PAGE_SIZE = 50;
|
||||
|
||||
type PersonType = 'PERSON' | 'INSTITUTION' | 'GROUP';
|
||||
|
||||
function parseType(raw: string | null): PersonType | undefined {
|
||||
return raw === 'PERSON' || raw === 'INSTITUTION' || raw === 'GROUP' ? raw : undefined;
|
||||
}
|
||||
|
||||
export async function load({ url, fetch, locals }) {
|
||||
const q = url.searchParams.get('q') || '';
|
||||
const page = Math.max(0, Number.parseInt(url.searchParams.get('page') ?? '0', 10) || 0);
|
||||
const review =
|
||||
url.searchParams.get('review') === '1' || url.searchParams.get('review') === 'true';
|
||||
const type = parseType(url.searchParams.get('type'));
|
||||
const familyOnly = url.searchParams.get('familyOnly') === 'true';
|
||||
const hasDocuments = url.searchParams.get('hasDocuments') === 'true';
|
||||
|
||||
const api = createApiClient(fetch);
|
||||
|
||||
const canWrite =
|
||||
@@ -11,15 +26,32 @@ export async function load({ url, fetch, locals }) {
|
||||
g.permissions.includes('WRITE_ALL')
|
||||
) ?? false;
|
||||
|
||||
const [personsResult, statsResult] = await Promise.all([
|
||||
api.GET('/api/persons', { params: { query: { q: q || undefined } } }),
|
||||
api.GET('/api/stats', {})
|
||||
const filters = {
|
||||
q: q || undefined,
|
||||
type,
|
||||
familyOnly: familyOnly || undefined,
|
||||
hasDocuments: hasDocuments || undefined,
|
||||
review: review || undefined,
|
||||
page,
|
||||
size: PAGE_SIZE
|
||||
};
|
||||
|
||||
// The "Zu prüfen (N)" link count is the totalElements of a provisional-only query. A size=1
|
||||
// page keeps the extra request cheap — we only need the count, not the rows.
|
||||
const [personsResult, statsResult, reviewCountResult] = await Promise.all([
|
||||
api.GET('/api/persons', { params: { query: filters } }),
|
||||
api.GET('/api/stats', {}),
|
||||
canWrite
|
||||
? api.GET('/api/persons', { params: { query: { provisional: true, review: true, size: 1 } } })
|
||||
: Promise.resolve(null)
|
||||
]);
|
||||
|
||||
if (!personsResult.response.ok) {
|
||||
throw error(personsResult.response.status, getErrorMessage(undefined));
|
||||
}
|
||||
|
||||
const result = personsResult.data!;
|
||||
|
||||
const stats = statsResult.response.ok
|
||||
? {
|
||||
totalPersons: statsResult.data!.totalPersons ?? 0,
|
||||
@@ -27,5 +59,21 @@ export async function load({ url, fetch, locals }) {
|
||||
}
|
||||
: { totalPersons: 0, totalDocuments: 0 };
|
||||
|
||||
return { persons: personsResult.data!, stats, q, canWrite };
|
||||
const needsReviewCount =
|
||||
reviewCountResult && reviewCountResult.response.ok
|
||||
? (reviewCountResult.data!.totalElements ?? 0)
|
||||
: 0;
|
||||
|
||||
return {
|
||||
persons: result.items,
|
||||
totalElements: result.totalElements,
|
||||
totalPages: result.totalPages,
|
||||
pageNumber: result.pageNumber,
|
||||
pageSize: result.pageSize,
|
||||
filters: { type, familyOnly, hasDocuments, review },
|
||||
needsReviewCount,
|
||||
stats,
|
||||
q,
|
||||
canWrite
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user