feat(members): implement /members page — Kachel-Ansicht (E2, issue #48) #58

Merged
marcel merged 10 commits from feat/issue-48-members-kachel into master 2026-04-10 20:34:24 +02:00
Owner

Summary

  • Backend: GET /v1/households/mine/invites, DELETE /v1/households/mine/members/{userId}, PATCH /v1/households/mine/members/{userId} endpoints; invite shareUrl base URL now configurable via app.base-url / APP_BASE_URL env var
  • Frontend: full /members Kachel grid — member cards with avatar (role colour), role badge, join date, Du-badge, kebab menu with inline role control and remove dialog; invite card + invite panel; optimistic updates with toast rollback
  • Spec alignment: white card bg, 1px border + shadow, avatar green-dark/blue by role, role badge colours, seit DD.MM.YYYY join date, kebab with icons/divider, Mitglied einladen invite card with hover states, Einladelink teilen panel with mono link box + yellow expiry pill + text-link regenerate

Test plan

  • Backend: ./mvnw test — 329 tests green
  • Frontend: npm run test — 771 tests green
  • Open /members as planer — grid shows all members, own card has green border + Du badge, other cards have kebab
  • Click → dropdown shows Rolle ändern + Entfernen
  • Click Rolle ändern → segmented control appears inline, Abbrechen reverts without API call
  • Click Entfernen → confirmation dialog; confirm removes member from grid
  • Click invite card → panel expands with share link; Kopieren copies to clipboard; Neuen Link generieren invalidates old code
  • As Mitglied — no kebab buttons, no invite card
  • Set APP_BASE_URL=https://yourdomain.com in docker env → invite shareUrl uses that domain

Closes #48

## Summary - **Backend**: `GET /v1/households/mine/invites`, `DELETE /v1/households/mine/members/{userId}`, `PATCH /v1/households/mine/members/{userId}` endpoints; invite `shareUrl` base URL now configurable via `app.base-url` / `APP_BASE_URL` env var - **Frontend**: full `/members` Kachel grid — member cards with avatar (role colour), role badge, join date, Du-badge, kebab menu with inline role control and remove dialog; invite card + invite panel; optimistic updates with toast rollback - **Spec alignment**: white card bg, 1px border + shadow, avatar `green-dark`/`blue` by role, role badge colours, `seit DD.MM.YYYY` join date, `⋯` kebab with icons/divider, `Mitglied einladen` invite card with hover states, `Einladelink teilen` panel with mono link box + yellow expiry pill + text-link regenerate ## Test plan - [ ] Backend: `./mvnw test` — 329 tests green - [ ] Frontend: `npm run test` — 771 tests green - [ ] Open `/members` as planer — grid shows all members, own card has green border + Du badge, other cards have `⋯` kebab - [ ] Click `⋯` → dropdown shows Rolle ändern + Entfernen - [ ] Click Rolle ändern → segmented control appears inline, Abbrechen reverts without API call - [ ] Click Entfernen → confirmation dialog; confirm removes member from grid - [ ] Click invite card → panel expands with share link; Kopieren copies to clipboard; Neuen Link generieren invalidates old code - [ ] As Mitglied — no kebab buttons, no invite card - [ ] Set `APP_BASE_URL=https://yourdomain.com` in docker env → invite shareUrl uses that domain Closes #48
marcel added 8 commits 2026-04-10 20:17:12 +02:00
- Add V006 migration: invalidated_at column + partial unique index on household_invite
- Add findByHouseholdIdAndInvalidatedAtIsNull, findByHouseholdIdAndUserId, countByHouseholdIdAndRole
- Add ChangeRoleRequest DTO
- HouseholdService: getActiveInvite, createInvite (regenerate), removeMember, changeMemberRole
- HouseholdController: GET /v1/households/mine/invites, DELETE/PATCH /v1/households/mine/members/{userId}

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backend:
- Rename V006 migration to V026 (avoid conflict with existing V006)
- Migration adds invalidated_at + partial unique index on household_invite

Frontend:
- Toast.svelte — new system component (message + dismiss)
- SegmentedControl.svelte — new system component (options, value, onchange)
- members/+page.server.ts — loads members + active invite
- members/[userId]/+server.ts — DELETE/PATCH proxy
- members/invites/+server.ts — POST (regenerate) proxy
- MemberCard.svelte — tile with avatar, kebab, inline role edit
- RemoveDialog.svelte — confirmation dialog (desktop modal + BottomSheet mobile)
- InviteCard.svelte + InvitePanel.svelte — invite management UI
- MemberGrid.svelte — responsive 4/2-col grid with sorted members
- members/+page.svelte — page composing all components with optimistic updates

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- MemberCard: white bg, 1px border + shadow-card, centered column layout,
  avatar color by role (green-dark/blue), role badge with role-specific colors,
  join date "seit DD.MM.YYYY", Du-badge below join date, ⋯ kebab with icons
  and divider, inline role-control with Abbrechen, blue editing border #B5D4F4
- InviteCard: white bg, 1.5px dashed border, min-height 180px, plus circle,
  label "Mitglied einladen", full hover state (green border/bg/icon/label)
- InvitePanel: white bg, title "Einladelink teilen", description, mono link
  box, yellow expiry pill when ≤ 24h, text-link "Neuen Link generieren"
- RemoveDialog: white bg, padding 28px 32px, "?" in title, updated body text
- +page.server.ts: expose householdName from locals.haushalt
- +page.svelte: subtitle "{n} Mitglieder · {householdName}"
- Tests: add join date format test, Abbrechen test, InvitePanel title test

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When activeInvite is null and the user clicks the invite card, POST to
/members/invites first to generate a code, then toggle the panel open.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
POST /members/invites was returning the full ApiResponseInviteResponse
wrapper. The client set activeInvite directly from the response body,
so shareUrl/inviteCode/expiresAt were missing (nested under .data).
Fixed to return data?.data — the inner InviteResponse — matching the
shape that InvitePanel and page.server.ts already expect.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces hardcoded \"https://yourapp.com\" with a Spring property.

- application.yml: app.base-url defaults to http://localhost:5173
- application-docker.yml: reads APP_BASE_URL env var, same default
- HouseholdService: injects @Value("${app.base-url}") and uses it in
  toInviteResponse() to build shareUrl
- HouseholdServiceTest: sets field via ReflectionTestUtils in @BeforeEach;
  adds test asserting shareUrl starts with configured base URL

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
marcel added 2 commits 2026-04-10 20:33:37 +02:00
removeMember now checks the planner count before deleting a planner
member. Throws ConflictException("Cannot remove the last planner")
when only one planner remains, matching the spec requirement in S4.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- handleInviteClick: show toast and bail early when POST /invites fails
- handleRegenerate: show toast when regeneration POST fails
- handleRoleChange: add Content-Type: application/json header on PATCH

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Author
Owner

Review concerns addressed

All four concerns identified during the self-review have been resolved:

9d3be84feat(members): guard removeMember against removing the last planner

  • Added a last-planner check in HouseholdService.removeMember() using the existing countByHouseholdIdAndRole repository method — same pattern as changeMemberRole. Throws ConflictException when the target is the sole remaining planner.
  • Covered by new backend test: removeMemberShouldThrow409WhenRemovingLastPlanner

60d84c0feat(members): add error toasts for invite failures and Content-Type header on role PATCH

  • handleInviteClick now shows a toast and returns early when the invite POST fails (previously silently dropped the error)
  • handleRegenerate now shows a toast when the regenerate POST fails (previously silently ignored errors)
  • handleRoleChange now sends Content-Type: application/json on the PATCH request (previously missing, causing the Spring backend to reject the body)
  • Covered by new frontend tests in MemberCard.test.ts and InvitePanel.test.ts

All tests green: 330 backend · 772 frontend

## Review concerns addressed All four concerns identified during the self-review have been resolved: **9d3be84** — `feat(members): guard removeMember against removing the last planner` - Added a last-planner check in `HouseholdService.removeMember()` using the existing `countByHouseholdIdAndRole` repository method — same pattern as `changeMemberRole`. Throws `ConflictException` when the target is the sole remaining planner. - Covered by new backend test: `removeMemberShouldThrow409WhenRemovingLastPlanner` **60d84c0** — `feat(members): add error toasts for invite failures and Content-Type header on role PATCH` - `handleInviteClick` now shows a toast and returns early when the invite POST fails (previously silently dropped the error) - `handleRegenerate` now shows a toast when the regenerate POST fails (previously silently ignored errors) - `handleRoleChange` now sends `Content-Type: application/json` on the PATCH request (previously missing, causing the Spring backend to reject the body) - Covered by new frontend tests in `MemberCard.test.ts` and `InvitePanel.test.ts` All tests green: 330 backend ✅ · 772 frontend ✅
marcel merged commit b577b7a0f8 into master 2026-04-10 20:34:24 +02:00
marcel deleted branch feat/issue-48-members-kachel 2026-04-10 20:34:25 +02:00
Sign in to join this conversation.