feat(invites): assign groups when creating an invite link (#566) #582

Merged
marcel merged 16 commits from feat/issue-566-invite-group-picker into main 2026-05-14 19:26:54 +02:00

16 Commits

Author SHA1 Message Date
Marcel
acd66b1551 fix(invites): resolve svelte-check warnings in UserGroupsSection and page.server.test
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 3m10s
CI / OCR Service Tests (pull_request) Successful in 16s
CI / Backend Unit Tests (pull_request) Successful in 4m24s
CI / fail2ban Regex (pull_request) Successful in 38s
CI / Compose Bucket Idempotency (pull_request) Successful in 55s
Use untrack() for intentional one-time prop seed in UserGroupsSection.
Add explicit LoadData type alias in page.server.test to avoid void|Record<string,any> union.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 17:03:14 +02:00
Marcel
688ece814a test(invites): verify groupIds are forwarded from request body in InviteController
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 16:54:04 +02:00
Marcel
85796d1776 feat(invites): show empty state when no groups exist in invite form
When groups load successfully but the list is empty, render a quiet
"Keine Gruppen vorhanden." message rather than a blank section that
leaves users uncertain whether groups failed to load.

Adds admin_new_invite_no_groups i18n key to de/en/es.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 16:50:13 +02:00
Marcel
fa8b266828 docs(invites): explain InviteTokenRepository injection in UserService
Spring Framework 7 prohibits constructor injection cycles. InviteService
already injects UserService, so UserService cannot inject InviteService
for the deleteGroup guard — repository injection is the correct workaround.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 16:48:24 +02:00
Marcel
d6947ce6d1 fix(invites): deduplicate groupIds before size check in createInvite
Client-submitted duplicate UUIDs were causing a false GROUP_NOT_FOUND:
size(deduplicated_db_result)==1 != size(submitted)==2. Deduplicate input
with HashSet before calling findGroupsByIds so the size comparison is
always against unique IDs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 16:47:36 +02:00
Marcel
9230e4a531 fix(invites): i18n legend + touch target in UserGroupsSection
- legend uses m.admin_new_invite_groups() instead of hardcoded "Gruppen"
  so screen readers announce the correct string in en/es locales
- label gets min-h-[44px] for WCAG 2.2 touch target compliance
- add test asserting fieldset accessible name comes from i18n key
- add test documenting empty-groups-no-error renders no checkboxes/banner

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 16:43:53 +02:00
Marcel
558b3ebe07 docs: align ErrorCode 4-step checklist in CLAUDE.md; note frontend sync in ARCHITECTURE.md
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 3m44s
CI / OCR Service Tests (pull_request) Successful in 17s
CI / Backend Unit Tests (pull_request) Successful in 4m53s
CI / fail2ban Regex (pull_request) Successful in 40s
CI / Compose Bucket Idempotency (pull_request) Successful in 56s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 16:16:32 +02:00
Marcel
385c3cd275 test(invites): add InviteTokenRepository integration tests for existsActiveWithGroupId + V66 group_id index
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 16:14:31 +02:00
Marcel
1943b7e9a1 fix(invites): validate groupIds existence in createInvite — throw GROUP_NOT_FOUND
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 16:12:02 +02:00
Marcel
7556ea0f69 feat(errors): add GROUP_NOT_FOUND error code + i18n keys
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 16:09:53 +02:00
Marcel
42d6a0700e test(invites): import real +page.server module via vi.mock env
Replace hand-copied load/action replicas with direct imports of the
real module. Mock $env/dynamic/private so the tests cover the actual
production code paths, not a duplicate that can drift.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 16:06:08 +02:00
Marcel
79698f8eb2 fix(invites): add role="alert" to groups-load-error banner
Screen readers now announce the amber warning when it appears after
the form expands, without requiring the user to navigate to it.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 16:00:19 +02:00
Marcel
1e31db57a9 fix(invites): make group checkboxes writable — $derived → $state
bind:group requires a writable $state variable; $derived is read-only
in Svelte 5, so every click was silently reset to unchecked, making
the group picker non-functional.

Also wraps checkboxes in <fieldset>/<legend> for WCAG 1.3.1 compliance.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 15:59:37 +02:00
Marcel
82e61291d4 feat(invites): group picker in new-invite form
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 3m11s
CI / OCR Service Tests (pull_request) Successful in 16s
CI / Backend Unit Tests (pull_request) Successful in 4m57s
CI / fail2ban Regex (pull_request) Successful in 38s
CI / Compose Bucket Idempotency (pull_request) Successful in 57s
- load() fetches /api/groups in parallel with /api/invites; returns
  sorted groups array and groupsLoadError for partial failures
- create action forwards groupIds[] to POST /api/invites so invited
  users are placed in the selected groups on registration
- +page.svelte: group checkboxes via UserGroupsSection inside the form;
  amber warning banner when groups could not be loaded
- page.svelte.test.ts: groups checkboxes + warning banner tests
- page.server.test.ts: parallel fetch, sorting, error fallback,
  groupIds in POST body

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 15:20:53 +02:00
Marcel
a1b319a535 feat(frontend): add GROUP_HAS_ACTIVE_INVITES error code + i18n keys
Adds the error code to the ErrorCode union and getErrorMessage() switch.
Adds admin_new_invite_groups, admin_invite_groups_load_error, and
error_group_has_active_invites to all three locale files (de/en/es).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 15:19:59 +02:00
Marcel
d7fcbfd4d9 feat(groups): prevent deletion of groups referenced by active invites
Adds GROUP_HAS_ACTIVE_INVITES error code and guards UserService.deleteGroup()
with a 409 conflict when any active (non-revoked, non-expired, non-exhausted)
invite token still holds the group UUID.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 14:57:06 +02:00