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,7 +2,9 @@ import { signal } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { of } from 'rxjs';
import { MatDialog } from '@angular/material/dialog';
import { ActivatedRoute, convertToParamMap } from '@angular/router';
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 { Cashbox } from './cashbox';
@@ -54,11 +56,17 @@ describe('Cashbox', () => {
},
];
async function setup(roleId = 2, confirm = true) {
const penalties = [
{ id: 101, description: 'Zu spät zum Training', amount: 5 },
{ id: 102, description: 'Handy vergessen', amount: 2.5 },
];
async function setup(roleId = 2, confirm = true, penaltyIdParam: string | null = null) {
const createPlayerTransactions = vi.fn(() => of([]));
const createTeamWalletTransaction = vi.fn(() => of(activities[0]));
const reverseTransaction = vi.fn(() => of(activities[0]));
const loadTeamTransactions = vi.fn(() => of(activities));
const loadPenalties = vi.fn(() => of(penalties));
const refreshTeam = vi.fn();
const dialog = {
open: vi.fn(() => ({ afterClosed: () => of(confirm) })),
@@ -92,6 +100,17 @@ describe('Cashbox', () => {
reverseTransaction,
},
},
{ provide: PenaltyApi, useValue: { loadPenalties } },
{
provide: ActivatedRoute,
useValue: {
snapshot: {
queryParamMap: convertToParamMap(
penaltyIdParam ? { penaltyId: penaltyIdParam } : {},
),
},
},
},
{ provide: MatDialog, useValue: dialog },
],
}).compileComponents();
@@ -166,4 +185,33 @@ describe('Cashbox', () => {
expect(fixture.nativeElement.querySelector('[data-testid="player-booking"]')).toBeNull();
expect(fixture.nativeElement.querySelector('[data-testid="reverse-booking"]')).toBeNull();
});
it('prefills amount, note and type from a manually selected catalog entry, and stays editable', async () => {
const { component } = await setup();
component['onPenaltySelect'](102);
expect(component['playerForm'].getRawValue()).toEqual(
expect.objectContaining({ amount: 2.5, note: 'Handy vergessen', type: 11 }),
);
component['playerForm'].patchValue({ amount: 7 });
expect(component['playerForm'].getRawValue().amount).toBe(7);
});
it('prefills the booking form automatically from a penaltyId query param', async () => {
const { component } = await setup(2, true, '101');
expect(component['playerForm'].getRawValue()).toEqual(
expect.objectContaining({ amount: 5, note: 'Zu spät zum Training', type: 11 }),
);
});
it('ignores an unknown penaltyId query param without error', async () => {
const { component } = await setup(2, true, '999');
expect(component['playerForm'].getRawValue()).toEqual(
expect.objectContaining({ amount: 0, note: '', type: 11 }),
);
});
});