Pure formatting (line wrap) so the file passes prettier --check; no behaviour
change.
Refs #667
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add countByFilter parity coverage for the query (LIKE) path so the shared
FILTER_WHERE slice and count can't drift, and an integration test proving
deletePerson detaches a person referenced as both sender and receiver before
delete — the documents survive (sender nulled, receiver link removed) with no
FK orphan.
Refs #667
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The legacy sort=documentCount path wrapped its result with paged(top, 0,
safeSize, top.size()), so totalElements/pageSize looked like a paged slice of
a larger set when in fact the top-N query returns the complete result. Add a
dedicated PersonSearchResult.topN factory that reports reality — totalElements
= returned count, pageSize = that count, totalPages = 1 (0 when empty) — and
pin both the populated and empty semantics with controller tests.
Refs #667
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The locals.user.groups.some(...WRITE_ALL) derivation was copy-pasted across
the persons directory, persons review and the two document loaders touched by
this PR. Extract a single tested hasWriteAll(locals) helper in
$lib/shared/server and reuse it, removing the ad-hoc casts.
Refs #667
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The role="switch" toggle set a fixed aria-label of "Zu prüfen (N)" while its
visible text flips to "Alle anzeigen" when active — a visible-text /
accessible-name mismatch (WCAG 2.5.3 Label in Name). Drop the aria-label so
the visible text is the accessible name; aria-checked carries the state.
Refs #667
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The triage rename form reused persons_filter_type_person ("Person") and
persons_section_details ("Angaben zur Person") as the first/last-name field
labels, so a screen reader announced the wrong name for each input. Add
dedicated persons_field_first_name / persons_field_last_name keys (de/en/es)
and use them.
Refs #667
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Align PersonCard's "unbestätigt" badge with the authoritative provisional
flag so the badge, the "Zu prüfen (N)" count and the /persons/review triage
list can never disagree. Empty/"?" name handling is now a separate
crash-safety concern: it still routes to the neutral placeholder glyph
(never a "?" initial) but no longer implies a badge on its own.
Refs #667
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add /persons/review to the CLAUDE.md route tables and reflect the paged,
filtered directory plus the confirm/delete endpoints in the frontend
people-stories and backend persons C4 diagrams.
Closes#667
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
GET /api/persons now returns PersonSearchResult { items, … } instead of a bare
list. Update every caller: the dashboard top-persons path reads .items; the
unused full-list fetches in documents/new and documents/[id]/edit are dropped
(both pages use the self-fetching PersonTypeahead); the raw-fetch consumers
(PersonTypeahead, PersonMultiSelect, PersonMentionEditor) read body.items and
pass review=true so search still spans the whole directory. Specs updated to
the new envelope shape.
Refs #667
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
New WRITE-gated triage route lists provisional persons (one PersonReviewRow
each) with Merge (reuses POST /merge), Umbenennen (PUT), Bestätigen
(PATCH /confirm) and Löschen (DELETE behind the focus-trapped, Escape-dismissible
ConfirmDialog service). Actions run as form actions via use:enhance so they work
without JS and stay server-side permission-guarded; the loader is READ_ALL.
Refs #667
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>
Hand-edited frontend/src/lib/generated/api.ts to match the backend:
GET /api/persons now returns PersonSearchResult with the new filter/page/size
query params; adds PATCH /api/persons/{id}/confirm and DELETE /api/persons/{id}.
Generated offline (no dev backend running) — CI should re-run
`npm run generate:api` against the live spec to confirm parity.
Refs #667
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
GET /api/persons now returns PersonSearchResult with server-side filter params
(type, familyOnly, hasDocuments, provisional) and page/size bounds (@Min/@Max
-> 400). review=true drops the clean reader default. The legacy
sort=documentCount top-N path is folded into the paged contract. Add
PATCH /{id}/confirm and DELETE /{id}, both WRITE_ALL-guarded. Remove the now
unreachable PersonService.findAll(String).
BREAKING-CHANGE: GET /api/persons response shape changes from a bare list to
PersonSearchResult { items, totalElements, pageNumber, pageSize, totalPages }.
Refs #667
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
PersonService.search maps a PersonFilter to the paired slice/count repository
queries and returns a PersonSearchResult with a server-side total. confirmPerson
clears the provisional flag (the state transition behind PATCH /confirm).
deletePerson detaches sender/receiver document references before the hard delete
so it cannot orphan an FK.
Refs #667
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add PersonSearchResult (mirrors DocumentSearchResult shape) and PersonFilter
records, plus paired findByFilter/countByFilter native queries sharing one
WHERE clause so the rendered page and totalElements can never drift. Filters
(type, familyOnly, hasDocuments, provisional, readerDefault, q) each disable
via a null/false param. Tested against real Postgres via Testcontainers.
Refs #667
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>