feat(cashbox): book a catalog penalty directly as a transaction

Adds a catalog picker to the member-booking form in the cashbox (prefills
amount/note/type, stays editable) and a "Buchen" button on each penalty
catalog entry that jumps to the cashbox with that entry preselected via a
penaltyId query param. No backend changes — reuses the existing POST
/transactions flow, the catalog only supplies starting values.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Bastian Wagner
2026-08-01 18:50:58 +02:00
parent 9664187049
commit 5dae4362b2
6 changed files with 154 additions and 7 deletions

View File

@@ -2,6 +2,7 @@ import { CurrencyPipe, DatePipe, registerLocaleData } from '@angular/common';
import localeDe from '@angular/common/locales/de';
import { Component, LOCALE_ID, computed, effect, inject, signal } from '@angular/core';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatCheckboxModule } from '@angular/material/checkbox';
@@ -13,8 +14,10 @@ import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSelectModule } from '@angular/material/select';
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
import { AuthStore } from '../../../core/auth/auth-store';
import { PenaltyApi } from '../../../core/team/penalty-api';
import { TeamStore } from '../../../core/team/team-store';
import { TransactionsApi } from '../../../core/team/transactions-api';
import { Penalty } from '../../../models/penalty.model';
import {
CreatePlayerTransaction,
CreateTeamWalletTransaction,
@@ -52,13 +55,20 @@ export class Cashbox {
private readonly authStore = inject(AuthStore);
private readonly dialog = inject(MatDialog);
private readonly formBuilder = inject(FormBuilder);
private readonly penaltyApi = inject(PenaltyApi);
private readonly route = inject(ActivatedRoute);
private readonly router = inject(Router);
private readonly snackBar = inject(MatSnackBar);
private readonly teamStore = inject(TeamStore);
private readonly transactionsApi = inject(TransactionsApi);
private loadedTeamId: number | null = null;
private pendingPenaltyId: number | null = Number(
this.route.snapshot.queryParamMap.get('penaltyId'),
) || null;
protected readonly team = this.teamStore.team;
protected readonly activities = signal<TeamActivity[]>([]);
protected readonly penalties = signal<Penalty[]>([]);
protected readonly loading = signal(false);
protected readonly saving = signal(false);
protected readonly playerTransactionTypes = [
@@ -109,8 +119,33 @@ export class Cashbox {
if (teamId && teamId !== this.loadedTeamId) {
this.loadedTeamId = teamId;
this.loadActivities(teamId);
this.loadPenalties(teamId);
}
});
effect(() => {
if (this.pendingPenaltyId === null) return;
const penalty = this.penalties().find((p) => p.id === this.pendingPenaltyId);
if (!penalty) return;
this.pendingPenaltyId = null;
this.applyPenaltyPreset(penalty);
void this.router.navigate([], { queryParams: {}, replaceUrl: true });
});
}
protected onPenaltySelect(penaltyId: number): void {
const penalty = this.penalties().find((p) => p.id === penaltyId);
if (penalty) this.applyPenaltyPreset(penalty);
}
private applyPenaltyPreset(penalty: Penalty): void {
this.playerForm.patchValue({ amount: penalty.amount, note: penalty.description, type: 11 });
}
private loadPenalties(teamId: number): void {
this.penaltyApi.loadPenalties(teamId).subscribe({
next: (penalties) => this.penalties.set(penalties),
error: () => this.penalties.set([]),
});
}
protected submitPlayerBooking(): void {