diff --git a/myteamwallet_frontend_modern/src/app/core/team/penalty-api.spec.ts b/myteamwallet_frontend_modern/src/app/core/team/penalty-api.spec.ts index 65dbbe1..6796306 100644 --- a/myteamwallet_frontend_modern/src/app/core/team/penalty-api.spec.ts +++ b/myteamwallet_frontend_modern/src/app/core/team/penalty-api.spec.ts @@ -33,4 +33,20 @@ describe('PenaltyApi', () => { expect(request.request.body).toEqual(penalty); request.flush({ id: 1, ...penalty }); }); + + it('updates a penalty catalog entry', () => { + const update = { description: 'Zu spät', amount: 7.5 }; + api.updatePenalty(8, update).subscribe(); + const request = httpMock.expectOne(`${environment.apiUrl}penalty/8`); + expect(request.request.method).toBe('PATCH'); + expect(request.request.body).toEqual(update); + request.flush({ id: 8, ...update }); + }); + + it('deletes a penalty catalog entry', () => { + api.deletePenalty(8).subscribe(); + const request = httpMock.expectOne(`${environment.apiUrl}penalty/8`); + expect(request.request.method).toBe('DELETE'); + request.flush(null); + }); }); diff --git a/myteamwallet_frontend_modern/src/app/core/team/penalty-api.ts b/myteamwallet_frontend_modern/src/app/core/team/penalty-api.ts index eadc2c3..dfe5873 100644 --- a/myteamwallet_frontend_modern/src/app/core/team/penalty-api.ts +++ b/myteamwallet_frontend_modern/src/app/core/team/penalty-api.ts @@ -2,7 +2,7 @@ import { HttpClient } from '@angular/common/http'; import { Injectable, inject } from '@angular/core'; import { Observable } from 'rxjs'; import { environment } from '../../../environments/environment'; -import { CreatePenalty, Penalty } from '../../models/penalty.model'; +import { CreatePenalty, Penalty, UpdatePenalty } from '../../models/penalty.model'; @Injectable({ providedIn: 'root' }) export class PenaltyApi { @@ -16,4 +16,12 @@ export class PenaltyApi { createPenalty(penalty: CreatePenalty): Observable { return this.http.post(this.baseUrl, penalty); } + + updatePenalty(penaltyId: number, penalty: UpdatePenalty): Observable { + return this.http.patch(`${this.baseUrl}/${penaltyId}`, penalty); + } + + deletePenalty(penaltyId: number): Observable { + return this.http.delete(`${this.baseUrl}/${penaltyId}`); + } } diff --git a/myteamwallet_frontend_modern/src/app/features/team/more/penalties/penalties.html b/myteamwallet_frontend_modern/src/app/features/team/more/penalties/penalties.html index 8f07b67..9bf4888 100644 --- a/myteamwallet_frontend_modern/src/app/features/team/more/penalties/penalties.html +++ b/myteamwallet_frontend_modern/src/app/features/team/more/penalties/penalties.html @@ -4,40 +4,148 @@

Strafenkatalog

Klare Regeln, transparent für das ganze Team.

+ @if (canManage()) { -
- Beschreibung - Betrag + + + + Beschreibung + + + + Betrag + + + -
+ +
} -Strafen durchsuchensearch + +@if (mutationError()) { + +} + + + Strafen durchsuchen + search + + + @if (loading()) { -
+
+ + Katalog wird geladen … +
+} @else if (loadError()) { + +} @else if (penalties().length === 0) { +
+ gavel + Noch keine Einträge + Der Strafenkatalog dieses Teams ist leer. +
} @else if (filteredPenalties().length === 0) { -
gavelKeine Einträge gefunden.
+
+ search_off + Keine passenden Einträge + Versuche einen anderen Suchbegriff. +
} @else {
@for (penalty of filteredPenalties(); track penalty.id) { - {{ penalty.description }}{{ penalty.amount | currency: 'EUR' }} + + @if (editingPenaltyId() === penalty.id) { +
+ + Beschreibung + + + + Betrag + + + +
+ + +
+
+ } @else { +
+ {{ penalty.description }} + {{ penalty.amount | currency: 'EUR' }} +
+ @if (canManage()) { +
+ + +
+ } + } +
}
} diff --git a/myteamwallet_frontend_modern/src/app/features/team/more/penalties/penalties.scss b/myteamwallet_frontend_modern/src/app/features/team/more/penalties/penalties.scss index 407924f..081a25b 100644 --- a/myteamwallet_frontend_modern/src/app/features/team/more/penalties/penalties.scss +++ b/myteamwallet_frontend_modern/src/app/features/team/more/penalties/penalties.scss @@ -4,18 +4,22 @@ max-width: 900px; margin: 0 auto; } + header { margin: 20px 0 26px; } + h1, p { margin-top: 0; } + h1 { font-size: clamp(2rem, 4vw, 3rem); line-height: clamp(2rem, 4vw, 3rem); margin-bottom: 8px; } + .eyebrow { color: var(--mat-sys-primary); font-size: 0.75rem; @@ -24,48 +28,118 @@ h1 { text-transform: uppercase; margin-bottom: 6px; } + .create-card { padding: 20px; border-radius: 18px; margin-bottom: 22px; } -form { + +.create-card form, +.edit-form { display: grid; - grid-template-columns: 1fr 160px auto; + grid-template-columns: minmax(0, 1fr) 160px auto; gap: 12px; align-items: start; } + +.create-card button, +.edit-actions button { + min-height: 48px; +} + +.create-card button mat-spinner, +.edit-actions button mat-spinner { + display: inline-block; + margin-right: 8px; +} + .search { width: 100%; } + +.error-message { + padding: 12px 16px; + border-radius: 12px; + color: var(--mat-sys-error); + background: var(--mat-sys-error-container); +} + .catalog { display: grid; gap: 10px; } -.catalog mat-card { - display: flex; - flex-direction: row; - justify-content: space-between; - gap: 16px; - padding: 18px; + +.penalty-card { + padding: 16px 18px; border-radius: 16px; } + +.penalty-content { + display: flex; + justify-content: space-between; + gap: 16px; + align-items: baseline; +} + +.penalty-actions { + display: flex; + justify-content: flex-end; + gap: 4px; + margin-top: 8px; +} + +.edit-form { + grid-template-columns: minmax(0, 1fr) 150px; +} + +.edit-actions { + grid-column: 1 / -1; + display: flex; + justify-content: flex-end; + gap: 8px; +} + .state { min-height: 180px; display: grid; place-content: center; justify-items: center; + text-align: center; gap: 10px; color: var(--mat-sys-on-surface-variant); } + @media (max-width: 700px) { :host { padding: 20px 16px; } - form { + + .create-card form, + .edit-form { grid-template-columns: 1fr; } - form button { - justify-self: stretch; + + .create-card form button, + .edit-actions, + .edit-actions button { + width: 100%; + } + + .edit-actions { + grid-column: auto; + flex-direction: column-reverse; + } + + .penalty-content { + align-items: flex-start; + } + + .penalty-actions { + justify-content: stretch; + } + + .penalty-actions button { + flex: 1; } } diff --git a/myteamwallet_frontend_modern/src/app/features/team/more/penalties/penalties.spec.ts b/myteamwallet_frontend_modern/src/app/features/team/more/penalties/penalties.spec.ts index 388a3ee..f541e42 100644 --- a/myteamwallet_frontend_modern/src/app/features/team/more/penalties/penalties.spec.ts +++ b/myteamwallet_frontend_modern/src/app/features/team/more/penalties/penalties.spec.ts @@ -1,54 +1,209 @@ +import { HttpErrorResponse } from '@angular/common/http'; import { signal } from '@angular/core'; -import { TestBed } from '@angular/core/testing'; -import { of } from 'rxjs'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MatDialog } from '@angular/material/dialog'; import { provideRouter } from '@angular/router'; +import { Subject, of, throwError } from 'rxjs'; import { AuthStore } from '../../../../core/auth/auth-store'; import { PenaltyApi } from '../../../../core/team/penalty-api'; import { TeamStore } from '../../../../core/team/team-store'; +import { Penalty } from '../../../../models/penalty.model'; import { Penalties } from './penalties'; -describe('Penalties', () => { - it('renders the catalog and lets a captain add an entry', async () => { - const createPenalty = vi.fn(() => of({ id: 2, description: 'Handy in der Kabine', amount: 3 })); - const loadPenalties = vi.fn(() => of([{ id: 1, description: 'Zu spät', amount: 5 }])); - const team = { - id: 5, - name: 'Team A', - alias: 'a', +const first: Penalty = { + id: 1, + description: 'Zu spät', + amount: 5, + createdAt: '2026-01-01T00:00:00.000Z', +}; +const team = { + id: 5, + name: 'Team A', + alias: 'a', + balance: 0, + players: [ + { + id: 1, + firstName: 'Alex', + lastName: 'Muster', balance: 0, - players: [ - { - id: 1, - firstName: 'Alex', - lastName: 'Muster', - balance: 0, - active: true, - teamRole: { id: 3 }, - user: { id: 42 }, - }, - ], - }; + active: true, + teamRole: { id: 3 }, + user: { id: 42 }, + }, + ], +}; + +describe('Penalties', () => { + let fixture: ComponentFixture; + let currentUser: ReturnType>; + let loadPenalties: ReturnType; + let createPenalty: ReturnType; + let updatePenalty: ReturnType; + let deletePenalty: ReturnType; + let dialogClosed: Subject; + let dialog: { open: ReturnType }; + + beforeEach(async () => { + currentUser = signal({ id: 42, role: { id: 2 } }); + loadPenalties = vi.fn(() => of([first])); + createPenalty = vi.fn(() => of({ id: 2, description: 'Handy', amount: 3 })); + updatePenalty = vi.fn(() => of({ ...first, description: 'Neu', amount: 6 })); + deletePenalty = vi.fn(() => of(undefined)); + dialogClosed = new Subject(); + dialog = { open: vi.fn(() => ({ afterClosed: () => dialogClosed.asObservable() })) }; + await TestBed.configureTestingModule({ imports: [Penalties], providers: [ provideRouter([]), { provide: TeamStore, useValue: { team: signal(team) } }, - { provide: AuthStore, useValue: { currentUser: signal({ id: 42, role: { id: 2 } }) } }, - { provide: PenaltyApi, useValue: { loadPenalties, createPenalty } }, + { provide: AuthStore, useValue: { currentUser } }, + { + provide: PenaltyApi, + useValue: { loadPenalties, createPenalty, updatePenalty, deletePenalty }, + }, + { provide: MatDialog, useValue: dialog }, ], }).compileComponents(); - const fixture = TestBed.createComponent(Penalties); + }); + + function create(): void { + fixture = TestBed.createComponent(Penalties); fixture.detectChanges(); - expect(fixture.nativeElement.textContent).toContain('Zu spät'); + } + + function text(): string { + return (fixture.nativeElement as HTMLElement).textContent ?? ''; + } + + function button(label: string): HTMLButtonElement { + const match = [...(fixture.nativeElement as HTMLElement).querySelectorAll('button')].find( + (element) => element.textContent?.includes(label), + ); + if (!match) throw new Error(`Missing button: ${label}`); + return match as HTMLButtonElement; + } + + it('shows the catalog without mutation controls to a reader', () => { + currentUser.set({ id: 7, role: { id: 2 } }); + create(); + + expect(text()).toContain('Zu spät'); + expect((fixture.nativeElement as HTMLElement).querySelector('.create-card')).toBeNull(); + expect(text()).not.toContain('Bearbeiten'); + expect(text()).not.toContain('Löschen'); + }); + + it('lets a captain create an entry and reloads authoritative data', () => { + loadPenalties + .mockReturnValueOnce(of([first])) + .mockReturnValueOnce(of([first, { id: 2, description: 'Handy', amount: 3 }])); + create(); + fixture.componentInstance['form'].setValue({ description: 'Handy', amount: 3 }); - fixture.componentInstance['form'].setValue({ description: 'Handy in der Kabine', amount: 3 }); fixture.componentInstance['createPenalty'](); + fixture.detectChanges(); - expect(createPenalty).toHaveBeenCalledWith({ - teamId: 5, - description: 'Handy in der Kabine', - amount: 3, - }); - expect(fixture.componentInstance['penalties']().length).toBe(2); + expect(createPenalty).toHaveBeenCalledWith({ teamId: 5, description: 'Handy', amount: 3 }); + expect(loadPenalties).toHaveBeenCalledTimes(2); + expect(text()).toContain('Handy'); + }); + + it('rejects blank descriptions and amounts with more than two decimals', () => { + create(); + + fixture.componentInstance['form'].setValue({ description: ' ', amount: 1.234 }); + + expect(fixture.componentInstance['form'].invalid).toBe(true); + fixture.componentInstance['createPenalty'](); + expect(createPenalty).not.toHaveBeenCalled(); + }); + + it('opens one inline editor, supports cancel, and saves pessimistically', () => { + const updateResult = new Subject(); + updatePenalty.mockReturnValue(updateResult); + loadPenalties + .mockReturnValueOnce(of([first])) + .mockReturnValueOnce(of([{ ...first, description: 'Neu', amount: 6 }])); + create(); + + button('Bearbeiten').click(); + fixture.detectChanges(); + expect((fixture.nativeElement as HTMLElement).querySelector('.edit-form')).not.toBeNull(); + fixture.componentInstance['cancelEdit'](); + fixture.detectChanges(); + expect((fixture.nativeElement as HTMLElement).querySelector('.edit-form')).toBeNull(); + + button('Bearbeiten').click(); + fixture.componentInstance['editForm'].setValue({ description: 'Neu', amount: 6 }); + fixture.componentInstance['savePenalty'](first); + fixture.detectChanges(); + expect(fixture.componentInstance['editingPenaltyId']()).toBe(first.id); + expect(fixture.componentInstance['penalties']()).toEqual([first]); + expect(loadPenalties).toHaveBeenCalledTimes(1); + + updateResult.next({ ...first, description: 'Neu', amount: 6 }); + fixture.detectChanges(); + expect(updatePenalty).toHaveBeenCalledWith(1, { description: 'Neu', amount: 6 }); + expect(loadPenalties).toHaveBeenCalledTimes(2); + expect(text()).toContain('Neu'); + }); + + it('confirms deletion and reloads only after server success', () => { + const deletion = new Subject(); + deletePenalty.mockReturnValue(deletion); + loadPenalties.mockReturnValueOnce(of([first])).mockReturnValueOnce(of([])); + create(); + + button('Löschen').click(); + expect(dialog.open).toHaveBeenCalled(); + dialogClosed.next(true); + expect(deletePenalty).toHaveBeenCalledWith(1); + expect(loadPenalties).toHaveBeenCalledTimes(1); + expect(text()).toContain('Zu spät'); + + deletion.next(); + fixture.detectChanges(); + expect(loadPenalties).toHaveBeenCalledTimes(2); + expect(text()).toContain('Noch keine Einträge'); + }); + + it('keeps the inline editor and explains a duplicate conflict', () => { + updatePenalty.mockReturnValue( + throwError( + () => + new HttpErrorResponse({ + status: 409, + error: { message: 'Ein Eintrag mit dieser Beschreibung existiert bereits.' }, + }), + ), + ); + create(); + button('Bearbeiten').click(); + fixture.componentInstance['editForm'].setValue({ description: 'Zu spät', amount: 6 }); + + fixture.componentInstance['savePenalty'](first); + fixture.detectChanges(); + + expect((fixture.nativeElement as HTMLElement).querySelector('.edit-form')).not.toBeNull(); + expect(text()).toContain('existiert bereits'); + }); + + it('shows a load error, retries, and distinguishes an empty search result', () => { + loadPenalties + .mockReturnValueOnce(throwError(() => new HttpErrorResponse({ status: 500 }))) + .mockReturnValueOnce(of([first])); + create(); + fixture.detectChanges(); + expect(text()).toContain('Katalog konnte nicht geladen werden'); + + button('Erneut versuchen').click(); + fixture.detectChanges(); + expect(text()).toContain('Zu spät'); + + fixture.componentInstance['search'].set('Nicht vorhanden'); + fixture.detectChanges(); + expect(text()).toContain('Keine passenden Einträge'); }); }); diff --git a/myteamwallet_frontend_modern/src/app/features/team/more/penalties/penalties.ts b/myteamwallet_frontend_modern/src/app/features/team/more/penalties/penalties.ts index 9481803..1f36944 100644 --- a/myteamwallet_frontend_modern/src/app/features/team/more/penalties/penalties.ts +++ b/myteamwallet_frontend_modern/src/app/features/team/more/penalties/penalties.ts @@ -1,18 +1,23 @@ import { CurrencyPipe, registerLocaleData } from '@angular/common'; import localeDe from '@angular/common/locales/de'; -import { Component, LOCALE_ID, computed, effect, inject, signal } from '@angular/core'; +import { HttpErrorResponse } from '@angular/common/http'; +import { Component, DestroyRef, LOCALE_ID, computed, effect, inject, signal } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'; -import { RouterLink } from '@angular/router'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; +import { MatDialog } from '@angular/material/dialog'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; +import { RouterLink } from '@angular/router'; +import { finalize, switchMap, take } from 'rxjs'; import { AuthStore } from '../../../../core/auth/auth-store'; import { PenaltyApi } from '../../../../core/team/penalty-api'; import { TeamStore } from '../../../../core/team/team-store'; import { Penalty } from '../../../../models/penalty.model'; +import { ConfirmDialog } from '../../../../shared/confirm-dialog/confirm-dialog'; registerLocaleData(localeDe); @@ -35,25 +40,53 @@ registerLocaleData(localeDe); }) export class Penalties { private readonly authStore = inject(AuthStore); + private readonly destroyRef = inject(DestroyRef); + private readonly dialog = inject(MatDialog); private readonly formBuilder = inject(FormBuilder); private readonly penaltyApi = inject(PenaltyApi); private readonly teamStore = inject(TeamStore); private loadedTeamId: number | null = null; + protected readonly team = this.teamStore.team; protected readonly penalties = signal([]); protected readonly loading = signal(false); protected readonly saving = signal(false); + protected readonly pendingPenaltyId = signal(null); + protected readonly editingPenaltyId = signal(null); + protected readonly loadError = signal(null); + protected readonly mutationError = signal(null); protected readonly search = signal(''); protected readonly form = this.formBuilder.nonNullable.group({ - description: ['', Validators.required], - amount: [0, [Validators.required, Validators.min(0.01), Validators.max(10000)]], + description: ['', [Validators.required, Validators.maxLength(120), Validators.pattern(/\S/)]], + amount: [ + 0, + [ + Validators.required, + Validators.min(0.01), + Validators.max(10000), + Validators.pattern(/^\d+(\.\d{1,2})?$/), + ], + ], + }); + protected readonly editForm = this.formBuilder.nonNullable.group({ + description: ['', [Validators.required, Validators.maxLength(120), Validators.pattern(/\S/)]], + amount: [ + 0, + [ + Validators.required, + Validators.min(0.01), + Validators.max(10000), + Validators.pattern(/^\d+(\.\d{1,2})?$/), + ], + ], }); protected readonly canManage = computed(() => { const user = this.authStore.currentUser(); if (user?.role?.id === 1) return true; return ( this.team()?.players?.some( - (player) => player.user?.id === user?.id && (player.teamRole?.id ?? 0) > 2, + (player) => + player.active && player.user?.id === user?.id && (player.teamRole?.id ?? 0) >= 3, ) ?? false ); }); @@ -78,24 +111,139 @@ export class Penalties { const team = this.team(); if (!this.canManage() || !team || this.form.invalid || this.saving()) return; this.saving.set(true); - this.penaltyApi.createPenalty({ teamId: team.id, ...this.form.getRawValue() }).subscribe({ - next: (penalty) => { - this.penalties.update((items) => [...items, penalty]); - this.form.reset({ description: '', amount: 0 }); - this.saving.set(false); - }, - error: () => this.saving.set(false), - }); + this.mutationError.set(null); + this.penaltyApi + .createPenalty({ teamId: team.id, ...this.form.getRawValue() }) + .pipe( + switchMap(() => this.penaltyApi.loadPenalties(team.id)), + finalize(() => this.saving.set(false)), + takeUntilDestroyed(this.destroyRef), + ) + .subscribe({ + next: (penalties) => { + this.penalties.set(penalties); + this.form.reset({ description: '', amount: 0 }); + }, + error: (error: HttpErrorResponse) => + this.mutationError.set(this.errorMessage(error, 'Eintrag konnte nicht angelegt werden.')), + }); + } + + protected startEdit(penalty: Penalty): void { + if (!this.canManage() || this.pendingPenaltyId() !== null) return; + this.editingPenaltyId.set(penalty.id); + this.editForm.setValue({ description: penalty.description, amount: penalty.amount }); + this.mutationError.set(null); + } + + protected cancelEdit(): void { + if (this.pendingPenaltyId() !== null) return; + this.editingPenaltyId.set(null); + this.mutationError.set(null); + } + + protected savePenalty(penalty: Penalty): void { + const teamId = this.team()?.id; + if ( + !this.canManage() || + !teamId || + this.editingPenaltyId() !== penalty.id || + this.editForm.invalid || + this.pendingPenaltyId() !== null + ) { + return; + } + this.pendingPenaltyId.set(penalty.id); + this.mutationError.set(null); + this.penaltyApi + .updatePenalty(penalty.id, this.editForm.getRawValue()) + .pipe( + switchMap(() => this.penaltyApi.loadPenalties(teamId)), + finalize(() => this.pendingPenaltyId.set(null)), + takeUntilDestroyed(this.destroyRef), + ) + .subscribe({ + next: (penalties) => { + this.penalties.set(penalties); + this.editingPenaltyId.set(null); + }, + error: (error: HttpErrorResponse) => + this.mutationError.set( + this.errorMessage(error, 'Eintrag konnte nicht gespeichert werden.'), + ), + }); + } + + protected confirmDelete(penalty: Penalty): void { + if (!this.canManage() || this.pendingPenaltyId() !== null) return; + this.dialog + .open(ConfirmDialog, { + data: { + title: 'Eintrag löschen?', + message: `„${penalty.description}“ wird endgültig aus dem Strafenkatalog gelöscht.`, + confirmLabel: 'Löschen', + }, + restoreFocus: true, + }) + .afterClosed() + .pipe(take(1), takeUntilDestroyed(this.destroyRef)) + .subscribe((confirmed) => { + if (confirmed) this.deletePenalty(penalty); + }); + } + + protected retryLoad(): void { + const teamId = this.team()?.id; + if (teamId) this.load(teamId); + } + + private deletePenalty(penalty: Penalty): void { + const teamId = this.team()?.id; + if (!teamId) return; + this.pendingPenaltyId.set(penalty.id); + this.mutationError.set(null); + this.penaltyApi + .deletePenalty(penalty.id) + .pipe( + switchMap(() => this.penaltyApi.loadPenalties(teamId)), + finalize(() => this.pendingPenaltyId.set(null)), + takeUntilDestroyed(this.destroyRef), + ) + .subscribe({ + next: (penalties) => { + this.penalties.set(penalties); + if (this.editingPenaltyId() === penalty.id) this.editingPenaltyId.set(null); + }, + error: (error: HttpErrorResponse) => + this.mutationError.set(this.errorMessage(error, 'Eintrag konnte nicht gelöscht werden.')), + }); } private load(teamId: number): void { this.loading.set(true); - this.penaltyApi.loadPenalties(teamId).subscribe({ - next: (penalties) => { - this.penalties.set(penalties); - this.loading.set(false); - }, - error: () => this.loading.set(false), - }); + this.loadError.set(null); + this.penaltyApi + .loadPenalties(teamId) + .pipe( + finalize(() => this.loading.set(false)), + takeUntilDestroyed(this.destroyRef), + ) + .subscribe({ + next: (penalties) => this.penalties.set(penalties), + error: () => { + this.penalties.set([]); + this.loadError.set('Katalog konnte nicht geladen werden.'); + }, + }); + } + + private errorMessage(error: HttpErrorResponse, fallback: string): string { + const detail = typeof error.error?.message === 'string' ? error.error.message : ''; + if (error.status === 403) return 'Keine Berechtigung für diese Änderung.'; + if (error.status === 404) + return 'Der Eintrag wurde nicht gefunden. Bitte lade den Katalog neu.'; + if (error.status === 409) return detail || 'Diese Beschreibung existiert bereits.'; + if (error.status === 422) return 'Bitte prüfe Beschreibung und Betrag.'; + return detail || fallback; } } diff --git a/myteamwallet_frontend_modern/src/app/models/penalty.model.ts b/myteamwallet_frontend_modern/src/app/models/penalty.model.ts index 01008b9..f6a1704 100644 --- a/myteamwallet_frontend_modern/src/app/models/penalty.model.ts +++ b/myteamwallet_frontend_modern/src/app/models/penalty.model.ts @@ -10,3 +10,8 @@ export interface CreatePenalty { description: string; amount: number; } + +export interface UpdatePenalty { + description: string; + amount: number; +}