finalize migration (yay)

This commit is contained in:
tw-blue
2025-12-10 22:11:30 +01:00
parent cd6cfec7f3
commit c7d53c88bc
40 changed files with 966 additions and 499 deletions
@@ -14,9 +14,13 @@ import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
import * as api from '../../generated-api/api';
import { DatePipePipe } from '../../pipes/date-pipe.pipe.js';
import { DatePipePipe, dateToLocal } from '../../pipes/date-pipe.pipe.js';
import { EditMemberDialogComponent } from '../edit-member-dialog/edit-member-dialog.component.js';
type DeletionForm = {
kuendigungzum: FormControl<Date | null>;
};
@Component({
selector: 'app-delete-member-dialog',
standalone: true,
@@ -35,23 +39,27 @@ import { EditMemberDialogComponent } from '../edit-member-dialog/edit-member-dia
styleUrl: './delete-member-dialog.component.scss',
})
export class DeleteMemberDialogComponent {
form: FormGroup;
member: api.Member;
form: FormGroup<DeletionForm>;
member: api.MemberDto;
constructor(
@Inject(MAT_DIALOG_DATA) public data: { member: api.Member },
@Inject(MAT_DIALOG_DATA) public data: { member: api.MemberDto },
private dialogRef: MatDialogRef<EditMemberDialogComponent>,
) {
this.member = data.member;
this.form = new FormGroup({
kuendigungzum: new FormControl('', [Validators.required]),
this.form = new FormGroup<DeletionForm>({
kuendigungzum: new FormControl(null, [Validators.required]),
});
}
async submit() {
await api.scheduleDeletion(
this.member.id!,
this.form.controls['kuendigungzum'].value,
);
this.dialogRef.close();
const date = this.form.controls.kuendigungzum.value;
if (date) {
await api.scheduleDeletion(
this.member.id!,
dateToLocal(date).toISOString(),
);
this.dialogRef.close();
}
}
}
@@ -30,7 +30,7 @@ import { MemberDtoHelper } from '../../util/member.js';
export class EditMemberDialogComponent {
form: FormGroup;
constructor(
@Inject(MAT_DIALOG_DATA) public data: { member: api.Member },
@Inject(MAT_DIALOG_DATA) public data: { member: api.MemberDto },
private memberDtoHelper: MemberDtoHelper,
private dialogRef: MatDialogRef<EditMemberDialogComponent>,
) {
@@ -38,7 +38,7 @@ export class EditMemberDialogComponent {
}
submit() {
const member: api.Member = this.memberDtoHelper.memberFromForm(
const member: api.MemberDto = this.memberDtoHelper.memberFromForm(
this.form,
);
api.updateMember(member);
@@ -1,39 +1,43 @@
<h1>Neues Mitglied anlegen</h1>
<form [formGroup]="memberForm" (ngSubmit)="onSubmit()">
<mat-form-field>
<mat-label>Vorname</mat-label>
<input matInput formControlName="vorname" />
</mat-form-field>
<mat-form-field>
<mat-label>Nachname</mat-label>
<input matInput formControlName="nachname" />
</mat-form-field>
<mat-form-field>
<mat-label>Geschlecht</mat-label>
<input matInput formControlName="geschlecht" />
</mat-form-field>
<mat-form-field>
<mat-label>Geburtsdatum</mat-label>
<input matInput [matDatepicker]="picker" formControlName="geburtsdatum" />
<mat-hint>DD.MM.YYYY</mat-hint>
<mat-datepicker-toggle matIconSuffix [for]="picker"></mat-datepicker-toggle>
<mat-datepicker #picker></mat-datepicker>
</mat-form-field>
<mat-form-field>
<mat-label>Geburtsort</mat-label>
<input matInput formControlName="geburtsort" />
</mat-form-field>
<mat-checkbox formControlName="uvertrag"
>Unterrichtsvertrag Abgegeben?</mat-checkbox
>
<mat-checkbox formControlName="uvertrag">Verein Beigetreten?</mat-checkbox>
<mat-form-field>
<mat-label>Passnummer (falls vorhanden)</mat-label>
<input matInput formControlName="passnummer" />
</mat-form-field>
<mat-form-field>
<mat-label>Kontakt</mat-label>
<input matInput formControlName="kontakt" />
</mat-form-field>
<button mat-flat-button color="primary" type="submit">Hinzufügen</button>
<mat-form-field>
<mat-label>Vorname</mat-label>
<input matInput formControlName="vorname" />
</mat-form-field>
<mat-form-field>
<mat-label>Nachname</mat-label>
<input matInput formControlName="nachname" />
</mat-form-field>
<mat-form-field>
<mat-label>Geschlecht</mat-label>
<input matInput formControlName="geschlecht" />
</mat-form-field>
<mat-form-field>
<mat-label>Geburtsdatum</mat-label>
<input
matInput
[matDatepicker]="picker"
formControlName="geburtsdatum" />
<mat-hint>DD.MM.YYYY</mat-hint>
<mat-datepicker-toggle
matIconSuffix
[for]="picker"></mat-datepicker-toggle>
<mat-datepicker #picker></mat-datepicker>
</mat-form-field>
<mat-form-field>
<mat-label>Geburtsort</mat-label>
<input matInput formControlName="geburtsort" />
</mat-form-field>
<mat-checkbox [formControl]="memberForm.controls.verein"
>Verein Beigetreten?</mat-checkbox
>
<mat-form-field>
<mat-label>Passnummer (falls vorhanden)</mat-label>
<input matInput formControlName="passnummer" />
</mat-form-field>
<mat-form-field>
<mat-label>Kontakt</mat-label>
<input matInput formControlName="kontakt" />
</mat-form-field>
<button mat-flat-button color="primary" type="submit">Hinzufügen</button>
</form>
@@ -34,7 +34,7 @@ export class NewMemberComponent {
);
onSubmit() {
const newMember: api.PickMemberExcludeKeyofMemberIdOrLoeschenam =
const newMember: api.MemberCreateArgs =
this.memberDtoHelper.newMemberFromForm(this.memberForm);
api.createMember(newMember)
@@ -1,125 +1,142 @@
<h1>Mitglieder & Sportlerübersicht</h1>
<button mat-raised-button color="accent" (click)="newMember()">
Neues Mitglied
Neues Mitglied
</button>
<div class="filter-container mat-elevation-z8">
<div class="filter-header">
<mat-form-field>
<input
matInput
(keyup)="applyFilter($event)"
[value]="filter$ | async"
placeholder="Filter"
/>
</mat-form-field>
</div>
</div>
<ng-container *ngIf="dataSource$ | async as dataSource">
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
<ng-container matColumnDef="edit">
<th mat-header-cell *matHeaderCellDef>Bearb.</th>
<td mat-cell *matCellDef="let element">
<button
mat-mini-fab-button
(click)="editMember(element.id)"
aria-label="Edit Member"
<div class="filter-header">
<mat-form-field>
<label>Filter:</label>
<input
matInput
[formControl]="filterControl"
placeholder="Filter" />
</mat-form-field>
<div class="status-filters">
<mat-radio-group [formControl]="statusControl">
<mat-radio-button [value]="MemberStatusFilter.ACTIVE"
>Aktiv</mat-radio-button
>
<mat-radio-button [value]="MemberStatusFilter.INACTIVE"
>Inaktiv</mat-radio-button
>
<mat-radio-button [value]="MemberStatusFilter.ALL"
>Alle</mat-radio-button
>
</mat-radio-group>
<mat-form-field
matTooltip="Zeigt nur Mitglieder, die zum angegebenen Datum den ausgewählten status hatten"
matTooltipPosition="above">
<label>Zum Datum:</label>
<input
matInput
[matDatepicker]="picker"
[formControl]="statusDateControl"
[disabled]="statusControl.value===MemberStatusFilter.ALL"
placeholder="Aktiv bis" />
<mat-hint>DD.MM.YYYY</mat-hint>
<mat-datepicker-toggle
matIconSuffix
[disabled]="statusControl.value===MemberStatusFilter.ALL"
[for]="picker"></mat-datepicker-toggle>
<mat-datepicker #picker></mat-datepicker>
</mat-form-field>
</div>
<mat-checkbox [formControl]="onlyWithID"
>Nur mit Passnummer</mat-checkbox
>
<mat-icon fontIcon="edit"></mat-icon>
</button>
</td>
</div>
</div>
<!--TODO Sorting doesnt work anymore-->
<table mat-table matSort [dataSource]="dataSource" class="mat-elevation-z8">
<ng-container matColumnDef="edit">
<th mat-header-cell *matHeaderCellDef>Bearb.</th>
<td mat-cell *matCellDef="let element">
<button
mat-mini-fab-button
(click)="editMember(element.id)"
aria-label="Edit Member">
<mat-icon fontIcon="edit"></mat-icon>
</button>
</td>
</ng-container>
<ng-container matColumnDef="delete">
<th mat-header-cell *matHeaderCellDef>Entf.</th>
<td mat-cell *matCellDef="let element">
<button
mat-mini-fab-button
(click)="deleteMember(element.id)"
aria-label="Delete Member"
>
<mat-icon fontIcon="delete"></mat-icon>
</button>
</td>
<th mat-header-cell *matHeaderCellDef>Entf.</th>
<td mat-cell *matCellDef="let element">
<button
mat-mini-fab-button
(click)="deleteMember(element.id)"
aria-label="Delete Member">
<mat-icon fontIcon="delete"></mat-icon>
</button>
</td>
</ng-container>
<ng-container matColumnDef="vorname">
<th mat-header-cell *matHeaderCellDef>Vorname</th>
<td mat-cell *matCellDef="let element">
<span [innerHTML]="element.vorname | stylizeNames"></span>
</td>
<th mat-header-cell mat-sort-header *matHeaderCellDef>Vorname</th>
<td mat-cell *matCellDef="let element">
<span [innerHTML]="element.vorname | stylizeNames"></span>
</td>
</ng-container>
<ng-container matColumnDef="nachname">
<th mat-header-cell *matHeaderCellDef>Nachname</th>
<td mat-cell *matCellDef="let element">
<span [innerHTML]="element.nachname | stylizeNames"></span>
</td>
<th mat-header-cell mat-sort-header *matHeaderCellDef>Nachname</th>
<td mat-cell *matCellDef="let element">
<span [innerHTML]="element.nachname | stylizeNames"></span>
</td>
</ng-container>
<ng-container matColumnDef="geschlecht">
<th mat-header-cell *matHeaderCellDef>Geschlecht</th>
<td mat-cell *matCellDef="let element">{{ element.geschlecht }}</td>
<th mat-header-cell mat-sort-header *matHeaderCellDef>Geschlecht</th>
<td mat-cell *matCellDef="let element">{{ element.geschlecht }}</td>
</ng-container>
<ng-container matColumnDef="passnummer">
<th mat-header-cell *matHeaderCellDef>Passnummer</th>
<td mat-cell *matCellDef="let element">
{{ element.passnummer | nobreakHyphen }}
</td>
<th mat-header-cell mat-sort-header *matHeaderCellDef>Passnummer</th>
<td mat-cell *matCellDef="let element">
{{ element.passnummer | nobreakHyphen }}
</td>
</ng-container>
<ng-container matColumnDef="geburtsdatum">
<th mat-header-cell *matHeaderCellDef>Geburtsdatum</th>
<td mat-cell *matCellDef="let element">
{{ element.geburtsdatum | datePipe }}
</td>
<th mat-header-cell mat-sort-header *matHeaderCellDef>Geburtsdatum</th>
<td mat-cell *matCellDef="let element">
{{ element.geburtsdatum | datePipe }}
</td>
</ng-container>
<ng-container matColumnDef="geburtsort">
<th mat-header-cell *matHeaderCellDef>Geburtsort</th>
<td mat-cell *matCellDef="let element">{{ element.geburtsort }}</td>
</ng-container>
<ng-container matColumnDef="uvertrag">
<th mat-header-cell *matHeaderCellDef>Unterrichtsvertrag</th>
<td mat-cell *matCellDef="let element">
<mat-icon
*ngIf="element.uvertrag"
class="positive"
aria-label="Unterrichtsvertrag Check"
fontIcon="check"
></mat-icon>
<mat-icon
*ngIf="!element.uvertrag"
class="negative"
aria-label="Unterrichtsvertrag Cross"
fontIcon="close"
></mat-icon>
</td>
<th mat-header-cell mat-sort-header *matHeaderCellDef>Geburtsort</th>
<td mat-cell *matCellDef="let element">{{ element.geburtsort }}</td>
</ng-container>
<ng-container matColumnDef="verein">
<th mat-header-cell *matHeaderCellDef>Vereinsmitgliedschaft</th>
<td mat-cell *matCellDef="let element">
<mat-icon
*ngIf="element.verein"
class="positive"
aria-label="Verein Check"
fontIcon="check"
></mat-icon>
<mat-icon
*ngIf="!element.verein"
class="negative"
aria-label="Verein Cross"
fontIcon="close"
></mat-icon>
</td>
<th mat-header-cell mat-sort-header *matHeaderCellDef>
Vereinsmitgliedschaft
</th>
<td mat-cell *matCellDef="let element">
@if(element.verein){
<mat-icon
class="positive"
aria-label="Verein Check"
fontIcon="check"></mat-icon>
}@else {
<mat-icon
class="negative"
aria-label="Verein Cross"
fontIcon="close"></mat-icon>
}
</td>
</ng-container>
<ng-container matColumnDef="kontakt">
<th mat-header-cell *matHeaderCellDef>Kontakt</th>
<td mat-cell *matCellDef="let element">{{ element.kontakt }}</td>
<th mat-header-cell *matHeaderCellDef>Kontakt</th>
<td mat-cell *matCellDef="let element">{{ element.kontakt }}</td>
</ng-container>
<ng-container matColumnDef="kuendigungzum">
<th mat-header-cell *matHeaderCellDef>K&uuml;ndigung zum</th>
<td mat-cell *matCellDef="let element">
{{ (element.kuendigungzum | datePipe) ?? "-" }}
</td>
<th mat-header-cell mat-sort-header *matHeaderCellDef>
K&uuml;ndigung zum
</th>
<td mat-cell *matCellDef="let element">
{{ (element.kuendigungzum | datePipe) ?? "-" }}
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table>
</ng-container>
</table>
@@ -0,0 +1,21 @@
mat-radio-group {
display: flex;
flex-direction: column;
}
.filter-header {
display: flex;
flex-direction: row;
justify-content: space-evenly;
}
mat-form-field {
height: 100%;
}
.status-filters {
display: flex;
flex-direction: row;
justify-content: center;
gap: 1rem;
}
@@ -1,13 +1,18 @@
import { AsyncPipe, NgIf } from '@angular/common';
import { Component } from '@angular/core';
import { AfterViewInit, Component, viewChild } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatCheckbox } from '@angular/material/checkbox';
import { MatDatepickerModule } from '@angular/material/datepicker';
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 { MatRadioModule } from '@angular/material/radio';
import { MatSort, MatSortModule } from '@angular/material/sort';
import { MatTableDataSource, MatTableModule } from '@angular/material/table';
import { MatTooltipModule } from '@angular/material/tooltip';
import { RouterModule } from '@angular/router';
import { BehaviorSubject, firstValueFrom, from, Observable } from 'rxjs';
import { combineLatest, startWith } from 'rxjs';
import * as api from '../../generated-api/api';
import { DatePipePipe } from '../../pipes/date-pipe.pipe.js';
import { NobreakHyphenPipe } from '../../pipes/nobreak-hyphen.pipe.js';
@@ -16,6 +21,12 @@ import { DeleteMemberDialogComponent } from '../delete-member-dialog/delete-memb
import { EditMemberDialogComponent } from '../edit-member-dialog/edit-member-dialog.component.js';
import { NewMemberComponent } from '../new-member/new-member.component';
enum MemberStatusFilter {
ACTIVE = 'ACTIVE',
INACTIVE = 'INACTIVE',
ALL = 'ALL',
}
@Component({
selector: 'app-view-members',
imports: [
@@ -23,60 +34,33 @@ import { NewMemberComponent } from '../new-member/new-member.component';
MatFormFieldModule,
MatInputModule,
MatIconModule,
NgIf,
StylizeNamesPipe,
NobreakHyphenPipe,
RouterModule,
MatButtonModule,
AsyncPipe,
DatePipePipe,
ReactiveFormsModule,
MatDatepickerModule,
MatTooltipModule,
MatCheckbox,
MatRadioModule,
MatSortModule,
],
templateUrl: './view-members.html',
styleUrl: './view-members.scss',
})
export class ViewMembers {
async deleteMember(memberId: any) {
const dataSource = await firstValueFrom(this.dataSource$);
export class ViewMembers implements AfterViewInit {
protected MemberStatusFilter = MemberStatusFilter;
protected readonly filterControl = new FormControl<string>('');
protected readonly statusControl = new FormControl<MemberStatusFilter>(
MemberStatusFilter.ACTIVE,
);
protected readonly statusDateControl = new FormControl<Date | null>(null);
protected readonly onlyWithID = new FormControl<boolean>(false);
protected dataSource = new MatTableDataSource<api.MemberDto>();
private readonly datePipe = new DatePipePipe();
const dialogRef = this.dialog.open(DeleteMemberDialogComponent, {
data: {
member: dataSource.data.find(member => member.id === memberId),
},
hasBackdrop: true,
width: '90%',
height: '90%',
});
dialogRef.afterClosed().subscribe(async result => {
this.dataSource$ = this.updateData();
});
}
newMember() {
const dialogRef = this.dialog.open(NewMemberComponent, {});
dialogRef.afterClosed().subscribe(() => {
this.dataSource$ = this.updateData();
});
}
async editMember(memberId: any) {
const dataSource = await firstValueFrom(this.dataSource$);
const dialogRef = this.dialog.open(EditMemberDialogComponent, {
data: {
member: dataSource.data.find(member => member.id === memberId),
},
hasBackdrop: true,
width: '90%',
height: '90%',
});
dialogRef.afterClosed().subscribe(async result => {
this.dataSource$ = this.updateData();
});
}
dataSource$: Observable<MatTableDataSource<api.Member>> =
new BehaviorSubject(new MatTableDataSource([] as api.Member[]));
filter$: BehaviorSubject<string> = new BehaviorSubject('');
displayedColumns: string[] = [
protected readonly displayedColumns: string[] = [
'edit',
'delete',
'vorname',
@@ -85,31 +69,141 @@ export class ViewMembers {
'passnummer',
'geburtsdatum',
'geburtsort',
'uvertrag',
'verein',
'kontakt',
'kuendigungzum',
];
applyFilter(event: KeyboardEvent) {
let filterValue = (event.target as HTMLInputElement).value ?? '';
filterValue = filterValue.trim();
filterValue = filterValue.toLowerCase();
this.filter$.next(filterValue);
protected deleteMember(memberId: any) {
const dialogRef = this.dialog.open(DeleteMemberDialogComponent, {
data: {
member: this.dataSource.data.find(
member => member.id === memberId,
),
},
hasBackdrop: true,
width: '90%',
height: '90%',
});
dialogRef.afterClosed().subscribe(async result => {
this.updateData();
});
}
constructor(private dialog: MatDialog) {
this.dataSource$ = this.updateData();
protected newMember() {
const dialogRef = this.dialog.open(NewMemberComponent, {});
dialogRef.afterClosed().subscribe(() => {
this.updateData();
});
}
protected editMember(memberId: any) {
const dialogRef = this.dialog.open(EditMemberDialogComponent, {
data: {
member: this.dataSource.data.find(
member => member.id === memberId,
),
},
hasBackdrop: true,
width: '90%',
height: '90%',
});
dialogRef.afterClosed().subscribe(async result => {
//TODO do upsert instead
this.updateData();
});
}
updateData() {
return from(
api.getMembers().then(members => {
let dataSource = new MatTableDataSource<api.Member>(members);
this.filter$.subscribe(filter => {
dataSource.filter = filter;
});
return dataSource;
}),
);
public constructor(private dialog: MatDialog) {
const originalFilterPredicate = this.dataSource.filterPredicate;
this.dataSource.filterPredicate = (
data: api.MemberDto,
encodedFilter: string,
) => {
const parts = encodedFilter.split(';');
const filter = parts[0];
const status = parts[1] as MemberStatusFilter;
const statusDateStr = parts[2];
const onlyWithID = parts[3] === 'true';
const currentDate =
statusDateStr && statusDateStr != ''
? new Date(statusDateStr)
: null;
const cancellationDate = data.kuendigungzum
? new Date(data.kuendigungzum)
: null;
if (
currentDate &&
cancellationDate &&
((status === MemberStatusFilter.ACTIVE &&
cancellationDate <= currentDate) ||
(status === MemberStatusFilter.INACTIVE &&
cancellationDate > currentDate))
) {
return false;
}
if (status === MemberStatusFilter.INACTIVE && !cancellationDate) {
return false;
}
if (
onlyWithID &&
(!data.passnummer || data.passnummer.trim() === '')
) {
return false;
}
return originalFilterPredicate(
{
...data,
geburtsdatum: this.datePipe.transform(
data.geburtsdatum ?? undefined,
),
kuendigungzum: this.datePipe.transform(
data.kuendigungzum ?? undefined,
),
letztePruefung: this.datePipe.transform(
data.letztePruefung ?? undefined,
),
},
filter,
);
};
combineLatest([
this.filterControl.valueChanges.pipe(
startWith(this.filterControl.value),
),
this.statusControl.valueChanges.pipe(
startWith(this.statusControl.value),
),
this.statusDateControl.valueChanges.pipe(
startWith(this.statusDateControl.value),
),
this.onlyWithID.valueChanges.pipe(startWith(this.onlyWithID.value)),
])
// .pipe(takeUntilDestroyed())
.subscribe(combined => {
const filter = combined[0];
const status = combined[1];
const date = combined[2];
const onlyWithID = combined[3];
this.dataSource.filter = `${filter?.replaceAll(';', '') ?? ''};${status};${date?.toISOString()};${onlyWithID}`;
console.log('filter', this.dataSource.filter);
});
this.updateData();
}
protected readonly sort = viewChild.required(MatSort);
public ngAfterViewInit(): void {
this.dataSource.sort = this.sort();
}
private async updateData(): Promise<void> {
const members = await api.getMembers();
console.log('Members loaded');
console.dir(members);
this.dataSource.data = members;
}
}