ag grid
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { CurrencyPipe, DatePipe, registerLocaleData } from '@angular/common';
|
||||
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 { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
@@ -10,9 +10,20 @@ 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 { MatSelectModule } from '@angular/material/select';
|
||||
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
|
||||
import { AgGridAngular } from 'ag-grid-angular';
|
||||
import type {
|
||||
ColDef,
|
||||
GetRowIdParams,
|
||||
GridApi,
|
||||
GridReadyEvent,
|
||||
IDatasource,
|
||||
IGetRowsParams,
|
||||
} from 'ag-grid-community';
|
||||
import { Subject } from 'rxjs';
|
||||
import { debounceTime } from 'rxjs/operators';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { TeamPermissionsService } from '../../../core/team/team-permissions';
|
||||
import { PenaltyApi } from '../../../core/team/penalty-api';
|
||||
import { TeamStore } from '../../../core/team/team-store';
|
||||
@@ -22,21 +33,38 @@ import {
|
||||
CreatePlayerTransaction,
|
||||
CreateTeamWalletTransaction,
|
||||
TeamActivity,
|
||||
TransactionsJournalQuery,
|
||||
TransactionsSortField,
|
||||
} from '../../../models/transaction.model';
|
||||
import { ConfirmDialog, ConfirmDialogData } from '../../../shared/confirm-dialog/confirm-dialog';
|
||||
import { ContextHelp } from '../../../shared/context-help/context-help';
|
||||
import { TransactionAmount } from '../../../shared/transaction-amount/transaction-amount';
|
||||
import '../../../shared/ag-grid/ag-grid-modules';
|
||||
import { teamwalletGridTheme } from '../../../shared/ag-grid/ag-grid-theme';
|
||||
import { AmountCellRenderer } from '../../../shared/ag-grid/amount-cell-renderer';
|
||||
import {
|
||||
ReverseActionCellRenderer,
|
||||
ReverseActionCellRendererParams,
|
||||
} from '../../../shared/ag-grid/reverse-action-cell-renderer';
|
||||
import { splitAmounts } from './transaction-calculation';
|
||||
|
||||
registerLocaleData(localeDe);
|
||||
|
||||
const HIGH_AMOUNT_CONFIRM_THRESHOLD = 300;
|
||||
|
||||
const JOURNAL_TYPE_OPTIONS: { value: string; label: string }[] = [
|
||||
{ value: '', label: 'Alle Typen' },
|
||||
{ value: 'payment', label: 'Zahlung' },
|
||||
{ value: 'credit', label: 'Guthaben' },
|
||||
{ value: 'fine', label: 'Strafe' },
|
||||
{ value: 'levy', label: 'Umlage' },
|
||||
{ value: 'fee', label: 'Gebühr' },
|
||||
{ value: 'expense', label: 'Ausgabe' },
|
||||
];
|
||||
|
||||
@Component({
|
||||
selector: 'app-cashbox',
|
||||
imports: [
|
||||
CurrencyPipe,
|
||||
DatePipe,
|
||||
ReactiveFormsModule,
|
||||
MatButtonModule,
|
||||
MatCardModule,
|
||||
@@ -44,11 +72,10 @@ const HIGH_AMOUNT_CONFIRM_THRESHOLD = 300;
|
||||
MatFormFieldModule,
|
||||
MatIconModule,
|
||||
MatInputModule,
|
||||
MatProgressSpinnerModule,
|
||||
MatSelectModule,
|
||||
MatSnackBarModule,
|
||||
ContextHelp,
|
||||
TransactionAmount,
|
||||
AgGridAngular,
|
||||
],
|
||||
providers: [{ provide: LOCALE_ID, useValue: 'de-DE' }],
|
||||
templateUrl: './cashbox.html',
|
||||
@@ -69,9 +96,7 @@ export class Cashbox {
|
||||
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 = [
|
||||
{ id: 0, label: 'Zahlung' },
|
||||
@@ -85,6 +110,81 @@ export class Cashbox {
|
||||
{ id: 14, label: 'Ausgabe' },
|
||||
];
|
||||
|
||||
// --- Kassenjournal (AG Grid) ---
|
||||
protected readonly gridTheme = teamwalletGridTheme;
|
||||
protected readonly journalTypeOptions = JOURNAL_TYPE_OPTIONS;
|
||||
protected readonly journalTypeFilter = signal('');
|
||||
protected readonly journalSearch = signal('');
|
||||
protected readonly journalTotal = signal<number | null>(null);
|
||||
private readonly journalSearchInput$ = new Subject<string>();
|
||||
private gridApi?: GridApi<TeamActivity>;
|
||||
private journalTeamId: number | null = null;
|
||||
|
||||
protected readonly journalColumnDefs: ColDef<TeamActivity>[] = [
|
||||
{
|
||||
headerName: '',
|
||||
colId: 'icon',
|
||||
sortable: false,
|
||||
resizable: false,
|
||||
width: 56,
|
||||
cellRenderer: (params: { data?: TeamActivity }) => {
|
||||
const isTeam = !!params.data?.isTeamWalletTransaction;
|
||||
const span = document.createElement('span');
|
||||
span.className = 'material-icons cashbox-grid-icon' + (isTeam ? ' team' : '');
|
||||
span.textContent = isTeam ? 'account_balance' : 'person';
|
||||
return span;
|
||||
},
|
||||
},
|
||||
{
|
||||
headerName: 'Name',
|
||||
field: 'playerName',
|
||||
minWidth: 140,
|
||||
flex: 1,
|
||||
valueGetter: (params) => params.data?.playerName || 'Teamkasse',
|
||||
},
|
||||
{
|
||||
headerName: 'Typ',
|
||||
field: 'type',
|
||||
width: 130,
|
||||
valueFormatter: (params) => this.typeLabel(params.value),
|
||||
},
|
||||
{
|
||||
headerName: 'Datum',
|
||||
field: 'date',
|
||||
width: 120,
|
||||
valueFormatter: (params) =>
|
||||
params.value ? new Intl.DateTimeFormat('de-DE').format(new Date(params.value)) : '',
|
||||
},
|
||||
{
|
||||
headerName: 'Notiz',
|
||||
field: 'note',
|
||||
colId: 'note',
|
||||
minWidth: 160,
|
||||
flex: 2,
|
||||
},
|
||||
{
|
||||
headerName: 'Betrag',
|
||||
field: 'amount',
|
||||
width: 150,
|
||||
cellRenderer: AmountCellRenderer,
|
||||
},
|
||||
{
|
||||
headerName: '',
|
||||
colId: 'actions',
|
||||
width: 60,
|
||||
sortable: false,
|
||||
resizable: false,
|
||||
cellRenderer: ReverseActionCellRenderer,
|
||||
cellRendererParams: {
|
||||
canReverse: (activity: TeamActivity) => this.canReverse(activity),
|
||||
onReverse: (activity: TeamActivity) => this.reverseBooking(activity),
|
||||
} satisfies Partial<ReverseActionCellRendererParams>,
|
||||
},
|
||||
];
|
||||
|
||||
protected readonly journalGetRowId = (params: GetRowIdParams<TeamActivity>) =>
|
||||
`${params.data.isTeamWalletTransaction ? 'w' : 'p'}-${params.data.id}`;
|
||||
|
||||
protected readonly canBook = computed(() =>
|
||||
this.permissions.canDo(this.team(), 'transactionCreate'),
|
||||
);
|
||||
@@ -114,9 +214,12 @@ export class Cashbox {
|
||||
const teamId = this.team()?.id;
|
||||
if (teamId && teamId !== this.loadedTeamId) {
|
||||
this.loadedTeamId = teamId;
|
||||
this.loadActivities(teamId);
|
||||
this.loadPenalties(teamId);
|
||||
}
|
||||
if (teamId && teamId !== this.journalTeamId) {
|
||||
this.journalTeamId = teamId;
|
||||
this.refreshJournal();
|
||||
}
|
||||
});
|
||||
effect(() => {
|
||||
if (this.pendingPenaltyId === null) return;
|
||||
@@ -126,6 +229,77 @@ export class Cashbox {
|
||||
this.applyPenaltyPreset(penalty);
|
||||
void this.router.navigate([], { queryParams: {}, replaceUrl: true });
|
||||
});
|
||||
|
||||
this.journalSearchInput$
|
||||
.pipe(debounceTime(300), takeUntilDestroyed())
|
||||
.subscribe((value) => {
|
||||
this.journalSearch.set(value);
|
||||
this.refreshJournal();
|
||||
});
|
||||
}
|
||||
|
||||
private readonly narrowLayout =
|
||||
typeof window !== 'undefined' && typeof window.matchMedia === 'function'
|
||||
? window.matchMedia('(max-width: 650px)')
|
||||
: null;
|
||||
|
||||
protected onJournalGridReady(event: GridReadyEvent<TeamActivity>): void {
|
||||
this.gridApi = event.api;
|
||||
this.updateResponsiveColumns();
|
||||
this.narrowLayout?.addEventListener('change', () => this.updateResponsiveColumns());
|
||||
if (this.journalTeamId) this.setJournalDatasource(this.journalTeamId);
|
||||
}
|
||||
|
||||
private updateResponsiveColumns(): void {
|
||||
this.gridApi?.setColumnsVisible(['note'], !(this.narrowLayout?.matches ?? false));
|
||||
}
|
||||
|
||||
protected onJournalTypeChange(value: string): void {
|
||||
this.journalTypeFilter.set(value);
|
||||
this.refreshJournal();
|
||||
}
|
||||
|
||||
protected onJournalSearchInput(value: string): void {
|
||||
this.journalSearchInput$.next(value);
|
||||
}
|
||||
|
||||
private refreshJournal(): void {
|
||||
if (!this.journalTeamId) return;
|
||||
this.setJournalDatasource(this.journalTeamId);
|
||||
}
|
||||
|
||||
private setJournalDatasource(teamId: number): void {
|
||||
if (!this.gridApi) return;
|
||||
this.gridApi.setGridOption('datasource', this.buildJournalDatasource(teamId));
|
||||
}
|
||||
|
||||
private buildJournalDatasource(teamId: number): IDatasource {
|
||||
return {
|
||||
getRows: (params: IGetRowsParams) => {
|
||||
const limit = Math.max(1, params.endRow - params.startRow);
|
||||
const page = Math.floor(params.startRow / limit) + 1;
|
||||
const sortItem = params.sortModel[0];
|
||||
const query: TransactionsJournalQuery = {
|
||||
page,
|
||||
limit,
|
||||
sortBy: (sortItem?.colId as TransactionsSortField) ?? 'date',
|
||||
sortDir: (sortItem?.sort as 'asc' | 'desc') ?? 'desc',
|
||||
type: this.journalTypeFilter() || undefined,
|
||||
search: this.journalSearch().trim() || undefined,
|
||||
};
|
||||
|
||||
this.transactionsApi.loadTeamTransactionsJournal(teamId, query).subscribe({
|
||||
next: (result) => {
|
||||
this.journalTotal.set(result.total);
|
||||
params.successCallback(result.data, result.total);
|
||||
},
|
||||
error: () => {
|
||||
this.journalTotal.set(0);
|
||||
params.failCallback();
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
protected onPenaltySelect(penaltyId: number): void {
|
||||
@@ -268,25 +442,7 @@ export class Cashbox {
|
||||
this.saving.set(false);
|
||||
this.snackBar.open(message, undefined, { duration: 4000 });
|
||||
this.teamStore.refreshTeam();
|
||||
const teamId = this.team()?.id;
|
||||
if (teamId) this.loadActivities(teamId);
|
||||
}
|
||||
|
||||
private loadActivities(teamId: number): void {
|
||||
this.loading.set(true);
|
||||
this.transactionsApi.loadTeamTransactions(teamId).subscribe({
|
||||
next: (activities) => {
|
||||
this.activities.set(activities);
|
||||
this.loading.set(false);
|
||||
},
|
||||
error: () => {
|
||||
this.activities.set([]);
|
||||
this.loading.set(false);
|
||||
this.snackBar.open('Buchungen konnten nicht geladen werden.', undefined, {
|
||||
duration: 5000,
|
||||
});
|
||||
},
|
||||
});
|
||||
this.refreshJournal();
|
||||
}
|
||||
|
||||
private handleError(message: string): void {
|
||||
|
||||
Reference in New Issue
Block a user