feat(frontend): Rechnungsempfaenger-Badge + Toggle in Haushaltsansicht #251

Closed
opened 2026-07-06 16:28:09 +02:00 by niboer · 0 comments
Owner

Abhaengigkeit

Dieser Issue setzt #216 (Backend) voraus — die Endpunkte GET und PUT /api/v1/groups/{groupId}/invoice-recipients muessen vorhanden sein.

Ziel

In der Haushaltsdetailseite (/app/groups/{groupId}) soll angezeigt werden, wer aktuell Rechnungsempfaenger ist. Mitglieder sollen den Empfaenger per ActionMenu umschalten koennen.

Backend-API (bereits umgesetzt in #216)

  • GET /api/v1/groups/{groupId}/invoice-recipients{ personIds: string[] }
  • PUT /api/v1/groups/{groupId}/invoice-recipients → Body: { personIds: string[] }
  • Leeres Array = Fallback auf group.createdBy

Das Backend unterstuetzt mehrere Empfaenger (Array). Das Frontend limitiert auf maximal eine Person.

Umsetzung

1. Orval-Types regenerieren

cd frontend && npx orval --config orval.config.ts

Erzeugt Types fuer InvoiceRecipients / SetInvoiceRecipients aus openapi.yaml.

2. +page.server.ts erweitern

Load-Funktion — 4. paralleler API-Fetch:

  • GET /api/v1/groups/{groupId}/invoice-recipients
  • Response in invoiceRecipientIds: string[] parsen
  • Im Return-Objekt an die Page uebergeben

Neue Form-Action ?/toggleInvoiceRecipient:

  • Holt personId aus FormData
  • Holt aktuelle Liste per findMany (oder via mitgeliefertem State)
  • Ist personId bereits enthalten → PUT { personIds: [] } (entfernen)
  • Ist personId nicht enthalten → PUT { personIds: [personId] } (setzen, ersetzt alle anderen)
  • Antwort: immer nur 0 oder 1 Eintrag in personIds

3. +page.svelte erweitern

a) Badge im name-Slot (neben Vor-/Nachname):

<a href="/app/persons/{m.personId}" class="font-medium hover:text-primary"
   data-testid="link-member-name">
  {personName(m)}
</a>
{#if invoiceRecipientIds.includes(m.personId)}
  <Badge variant="outline" class="ml-2 text-xs">
    {t('groups.invoiceRecipient')}
  </Badge>
{/if}

b) ActionMenu-Eintrag pro Mitglied (zwischen Bearbeiten und Entfernen):

<ActionMenu items={[
  { label: 'Bearbeiten', onClick: () => { ... } },
  invoiceRecipientIds.includes(m.personId)
    ? { label: t('groups.removeRecipient'), type: 'submit',
        action: '?/toggleInvoiceRecipient',
        fields: [{ name: 'personId', value: m.personId }] }
    : { label: t('groups.setRecipient'), type: 'submit',
        action: '?/toggleInvoiceRecipient',
        fields: [{ name: 'personId', value: m.personId }] },
  { label: 'Entfernen', ... },
] as ActionMenuItem[]} />

4. i18n

frontend/src/lib/i18n/de/groups.json:

"invoiceRecipient": "Rechnung",
"setRecipient": "Rechnungsempfaenger setzen",
"removeRecipient": "Rechnungsempfaenger entfernen"

frontend/src/lib/i18n/en/groups.json:

"invoiceRecipient": "Invoice",
"setRecipient": "Set as invoice recipient",
"removeRecipient": "Remove as invoice recipient"

5. Tests

Frontend-Unit-Tests:

  • Badge wird bei Rechnungsempfaenger angezeigt
  • Badge wird bei Nicht-Empfaenger nicht angezeigt
  • Toggle setzt/entfernt den Empfaenger korrekt

E2E-Tests (in bestehendem groups.spec.ts, test-2-independent):

  • Max Mustermann loggt ein, navigiert zu seiner Gruppe
  • Setzt Petra Musterfrau als Rechnungsempfaenger via ActionMenu
  • Verifiziert Badge "Rechnung" bei Petra, kein Badge bei Max
  • Loggt als Petra Musterfrau ein (neue Session)
  • Entfernt sich selbst als Rechnungsempfaenger via ActionMenu
  • Verifiziert Badge verschwunden

Dateien im Ueberblick

Datei Aenderung
frontend/src/lib/api/generated.ts Orval-Regeneration
frontend/src/routes/app/groups/[groupId]/+page.server.ts invoiceRecipientIds Fetch + toggle action
frontend/src/routes/app/groups/[groupId]/+page.svelte Badge + ActionMenu-Toggle
frontend/src/lib/i18n/de/groups.json +3 Keys
frontend/src/lib/i18n/en/groups.json +3 Keys
frontend/src/tests/groups.test.ts + Badge + Toggle Tests
e2e/specs/test-2-independent/groups.spec.ts + 2 Subtests
e2e/.test-coverage.json unveraendert (Route bereits covered)

Notizen

  • Der Badge verwendet variant="outline" (dezent, grau umrandet) — Farbe kann spaeter via Design-Token angepasst werden.
  • Die Toggle-Logik stellt sicher, dass immer nur 0 oder 1 Eintrag in personIds ist — konsistent mit der Issue-Vorgabe "zunaechst nur einer".
  • Der Backend-Fallback (leeres Array → group.createdBy) ist transparent; das Frontend muss keinen Fallback-Text rendern.
## Abhaengigkeit Dieser Issue setzt **#216 (Backend)** voraus — die Endpunkte `GET` und `PUT /api/v1/groups/{groupId}/invoice-recipients` muessen vorhanden sein. ## Ziel In der Haushaltsdetailseite (`/app/groups/{groupId}`) soll angezeigt werden, wer aktuell Rechnungsempfaenger ist. Mitglieder sollen den Empfaenger per ActionMenu umschalten koennen. ## Backend-API (bereits umgesetzt in #216) - `GET /api/v1/groups/{groupId}/invoice-recipients` → `{ personIds: string[] }` - `PUT /api/v1/groups/{groupId}/invoice-recipients` → Body: `{ personIds: string[] }` - Leeres Array = Fallback auf `group.createdBy` > Das Backend unterstuetzt mehrere Empfaenger (Array). Das Frontend limitiert auf **maximal eine Person**. ## Umsetzung ### 1. Orval-Types regenerieren ```bash cd frontend && npx orval --config orval.config.ts ``` Erzeugt Types fuer `InvoiceRecipients` / `SetInvoiceRecipients` aus `openapi.yaml`. ### 2. +page.server.ts erweitern **Load-Funktion** — 4. paralleler API-Fetch: - `GET /api/v1/groups/{groupId}/invoice-recipients` - Response in `invoiceRecipientIds: string[]` parsen - Im Return-Objekt an die Page uebergeben **Neue Form-Action `?/toggleInvoiceRecipient`:** - Holt `personId` aus FormData - Holt aktuelle Liste per `findMany` (oder via mitgeliefertem State) - Ist personId bereits enthalten → `PUT { personIds: [] }` (entfernen) - Ist personId nicht enthalten → `PUT { personIds: [personId] }` (setzen, ersetzt alle anderen) - Antwort: immer nur 0 oder 1 Eintrag in personIds ### 3. +page.svelte erweitern **a) Badge im name-Slot (neben Vor-/Nachname):** ```svelte <a href="/app/persons/{m.personId}" class="font-medium hover:text-primary" data-testid="link-member-name"> {personName(m)} </a> {#if invoiceRecipientIds.includes(m.personId)} <Badge variant="outline" class="ml-2 text-xs"> {t('groups.invoiceRecipient')} </Badge> {/if} ``` **b) ActionMenu-Eintrag pro Mitglied (zwischen Bearbeiten und Entfernen):** ```svelte <ActionMenu items={[ { label: 'Bearbeiten', onClick: () => { ... } }, invoiceRecipientIds.includes(m.personId) ? { label: t('groups.removeRecipient'), type: 'submit', action: '?/toggleInvoiceRecipient', fields: [{ name: 'personId', value: m.personId }] } : { label: t('groups.setRecipient'), type: 'submit', action: '?/toggleInvoiceRecipient', fields: [{ name: 'personId', value: m.personId }] }, { label: 'Entfernen', ... }, ] as ActionMenuItem[]} /> ``` ### 4. i18n **`frontend/src/lib/i18n/de/groups.json`:** ```json "invoiceRecipient": "Rechnung", "setRecipient": "Rechnungsempfaenger setzen", "removeRecipient": "Rechnungsempfaenger entfernen" ``` **`frontend/src/lib/i18n/en/groups.json`:** ```json "invoiceRecipient": "Invoice", "setRecipient": "Set as invoice recipient", "removeRecipient": "Remove as invoice recipient" ``` ### 5. Tests **Frontend-Unit-Tests:** - Badge wird bei Rechnungsempfaenger angezeigt - Badge wird bei Nicht-Empfaenger nicht angezeigt - Toggle setzt/entfernt den Empfaenger korrekt **E2E-Tests (in bestehendem `groups.spec.ts`, test-2-independent):** - Max Mustermann loggt ein, navigiert zu seiner Gruppe - Setzt Petra Musterfrau als Rechnungsempfaenger via ActionMenu - Verifiziert Badge "Rechnung" bei Petra, kein Badge bei Max - Loggt als Petra Musterfrau ein (neue Session) - Entfernt sich selbst als Rechnungsempfaenger via ActionMenu - Verifiziert Badge verschwunden ## Dateien im Ueberblick | Datei | Aenderung | |---|---| | `frontend/src/lib/api/generated.ts` | Orval-Regeneration | | `frontend/src/routes/app/groups/[groupId]/+page.server.ts` | invoiceRecipientIds Fetch + toggle action | | `frontend/src/routes/app/groups/[groupId]/+page.svelte` | Badge + ActionMenu-Toggle | | `frontend/src/lib/i18n/de/groups.json` | +3 Keys | | `frontend/src/lib/i18n/en/groups.json` | +3 Keys | | `frontend/src/tests/groups.test.ts` | + Badge + Toggle Tests | | `e2e/specs/test-2-independent/groups.spec.ts` | + 2 Subtests | | `e2e/.test-coverage.json` | unveraendert (Route bereits `covered`) | ## Notizen - Der Badge verwendet `variant="outline"` (dezent, grau umrandet) — Farbe kann spaeter via Design-Token angepasst werden. - Die Toggle-Logik stellt sicher, dass immer nur 0 oder 1 Eintrag in `personIds` ist — konsistent mit der Issue-Vorgabe "zunaechst nur einer". - Der Backend-Fallback (leeres Array → `group.createdBy`) ist transparent; das Frontend muss keinen Fallback-Text rendern.
Sign in to join this conversation.
No description provided.