visual + updates

This commit is contained in:
toni
2026-03-08 11:09:52 +01:00
parent a9ff8517ff
commit 10f4750755
22 changed files with 2009 additions and 2822 deletions
+1 -1
View File
@@ -12,7 +12,7 @@
"packageManager": "pnpm@10.24.0+sha512.01ff8ae71b4419903b65c60fb2dc9d34cf8bb6e06d03bde112ef38f7a34d6904c424ba66bea5cdcf12890230bf39f9580473140ed9c946fef328b6e5238a345a", "packageManager": "pnpm@10.24.0+sha512.01ff8ae71b4419903b65c60fb2dc9d34cf8bb6e06d03bde112ef38f7a34d6904c424ba66bea5cdcf12890230bf39f9580473140ed9c946fef328b6e5238a345a",
"type": "module", "type": "module",
"devDependencies": { "devDependencies": {
"nodemon": "^3.1.11", "nodemon": "^3.1.14",
"tsoa": "^6.6.0" "tsoa": "^6.6.0"
} }
} }
+3
View File
@@ -5,3 +5,6 @@ generated/
.env .env
keycloak/realm-export keycloak/realm-export
src/cert.pem
src/key.pem
+13 -13
View File
@@ -12,27 +12,27 @@
"devDependencies": { "devDependencies": {
"@types/connect-pg-simple": "^7.0.3", "@types/connect-pg-simple": "^7.0.3",
"@types/cors": "^2.8.19", "@types/cors": "^2.8.19",
"@types/express": "^5.0.5", "@types/express": "^5.0.6",
"@types/express-session": "^1.18.2", "@types/express-session": "^1.18.2",
"@types/node": "^24.9.2", "@types/node": "^25.3.5",
"@types/pg": "^8.15.6", "@types/pg": "^8.18.0",
"prisma": "^7.0.1", "prisma": "^7.4.2",
"tsx": "^4.20.6", "tsx": "^4.21.0",
"typescript": "~5.9.3" "typescript": "~5.9.3"
}, },
"dependencies": { "dependencies": {
"@keycloak/keycloak-admin-client": "^26.4.4", "@keycloak/keycloak-admin-client": "^26.5.5",
"@prisma/adapter-pg": "^7.0.1", "@prisma/adapter-pg": "^7.4.2",
"@prisma/client": "^7.0.1", "@prisma/client": "^7.4.2",
"@tsoa/runtime": "^6.6.0", "@tsoa/runtime": "^6.6.0",
"connect-pg-simple": "^10.0.0", "connect-pg-simple": "^10.0.0",
"cors": "^2.8.5", "cors": "^2.8.6",
"dotenv": "^17.2.3", "dotenv": "^17.3.1",
"express": "^5.1.0", "express": "^5.2.1",
"express-session": "^1.18.2", "express-session": "^1.19.0",
"helmet": "^8.1.0", "helmet": "^8.1.0",
"keycloak-connect": "^26.1.1", "keycloak-connect": "^26.1.1",
"pg": "^8.16.3", "pg": "^8.20.0",
"rxjs": "~7.8.2", "rxjs": "~7.8.2",
"tsoa": "^6.6.0" "tsoa": "^6.6.0"
} }
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Member" ADD COLUMN "creationDate" DATE;
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Member" ALTER COLUMN "creationDate" SET DEFAULT CURRENT_TIMESTAMP;
+1
View File
@@ -21,6 +21,7 @@ model Member {
kontakt String? @db.VarChar kontakt String? @db.VarChar
kuendigungzum DateTime? @db.Date kuendigungzum DateTime? @db.Date
marker Boolean? marker Boolean?
creationDate DateTime? @db.Date @default(now())
} }
model session { model session {
+6 -1
View File
@@ -14,11 +14,12 @@ export interface MemberDto {
kontakt: string; kontakt: string;
kuendigungzum: string; kuendigungzum: string;
marker: boolean; marker: boolean;
creationDate: string;
} }
export class MemberCreateArgs implements Omit< export class MemberCreateArgs implements Omit<
MemberDto, MemberDto,
"id" | "kuendigungzum" | "letztePruefung" "id" | "kuendigungzum" | "letztePruefung" | "creationDate"
> { > {
vorname!: string; vorname!: string;
nachname!: string; nachname!: string;
@@ -47,6 +48,7 @@ export function mapDbMemberToMember(dbMember: DbMember): MemberDto {
passnummer: dbMember.passnummer ?? "", passnummer: dbMember.passnummer ?? "",
verein: dbMember.verein ?? false, verein: dbMember.verein ?? false,
marker: dbMember.marker ?? false, marker: dbMember.marker ?? false,
creationDate: dbMember.creationDate?.toDateString() ?? "",
}; };
} }
@@ -71,5 +73,8 @@ export function mapMemberDtoToDbMember(member: MemberDto): DbMember {
? new Date(member.kuendigungzum) ? new Date(member.kuendigungzum)
: null, : null,
marker: member.marker, marker: member.marker,
creationDate: member.creationDate
? new Date(member.creationDate)
: null,
}; };
} }
+29 -14
View File
@@ -66,25 +66,40 @@ async function run() {
}, },
); );
if (environment.isDev()) { const isDev = environment.isDev();
//TODO create self-signed for localhost
const port = 3000; let cert: https.ServerOptions["cert"];
app.listen(port, () => { let key: https.ServerOptions["key"];
let SSL_KEY_PASSWORD: string | undefined;
let port: number;
if (isDev) {
// cert = fs.readFileSync("src/cert.pem");
// key = fs.readFileSync("src/key.pem");
port = 3000;
app.listen(port, error => {
if (error) {
console.error("Startup crashed:", error);
}
console.log(`Server is running at http://localhost:${port}`); console.log(`Server is running at http://localhost:${port}`);
}); });
} else { } else {
//TODO start with HTTPS in production //TODO start with HTTPS in production
const port = 5443; port = 5443;
const SSL_CERT_PATH = process.env["SSL_CERT_PATH"]; const SSL_CERT_PATH = process.env["SSL_CERT_PATH"];
const SSL_KEY_PATH = process.env["SSL_KEY_PATH"]; const SSL_KEY_PATH = process.env["SSL_KEY_PATH"];
const SSL_KEY_PASSWORD = process.env["SSL_KEY_PASSWORD"]; SSL_KEY_PASSWORD = process.env["SSL_KEY_PASSWORD"];
if (!SSL_CERT_PATH || !SSL_KEY_PATH || !SSL_KEY_PASSWORD) { if (!SSL_CERT_PATH || !SSL_KEY_PATH || !SSL_KEY_PASSWORD) {
throw new Error("Missing SSL configuration"); throw new Error("Missing SSL configuration");
} }
cert = fs.readFileSync(SSL_CERT_PATH);
key = fs.readFileSync(SSL_KEY_PATH);
const sslOptions: https.ServerOptions = { const sslOptions: https.ServerOptions = {
cert: fs.readFileSync(SSL_CERT_PATH), cert: cert,
key: fs.readFileSync(SSL_KEY_PATH), key: key,
passphrase: SSL_KEY_PASSWORD, passphrase: SSL_KEY_PASSWORD,
minVersion: "TLSv1.3", minVersion: "TLSv1.3",
maxVersion: "TLSv1.3", maxVersion: "TLSv1.3",
@@ -100,14 +115,14 @@ async function run() {
https.createServer(sslOptions, app).listen(port, () => { https.createServer(sslOptions, app).listen(port, () => {
console.log(`Server is running on port ${port}`); console.log(`Server is running on port ${port}`);
}); });
// const h2SslOptions={};
// http2.createServer(h2SslOptions, app).listen(port, ()=>{
// console.log(`HTTP/2 server is running on port ${port}`);
// })
} }
console.log("done"); // const h2SslOptions={};
// http2.createServer(h2SslOptions, app).listen(port, ()=>{
// console.log(`HTTP/2 server is running on port ${port}`);
// })
console.log("setup done");
} }
run(); run();
@@ -82,6 +82,9 @@ export class MembersService {
kuendigungzum: member.kuendigungzum kuendigungzum: member.kuendigungzum
? member.kuendigungzum ? member.kuendigungzum
: null, : null,
creationDate: member.creationDate
? member.creationDate
: null,
}, },
where: { id: id }, where: { id: id },
}), }),
+22 -22
View File
@@ -25,37 +25,37 @@
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@angular/cdk": "21.0.5", "@angular/cdk": "^21.2.1",
"@angular/common": "^21.0.6", "@angular/common": "^21.2.1",
"@angular/compiler": "^21.0.6", "@angular/compiler": "^21.2.1",
"@angular/core": "^21.0.6", "@angular/core": "^21.2.1",
"@angular/forms": "^21.0.6", "@angular/forms": "^21.2.1",
"@angular/material": "21.0.5", "@angular/material": "^21.2.1",
"@angular/material-date-fns-adapter": "^21.0.5", "@angular/material-date-fns-adapter": "^21.2.1",
"@angular/platform-browser": "^21.0.6", "@angular/platform-browser": "^21.2.1",
"@angular/router": "^21.0.6", "@angular/router": "^21.2.1",
"@angular/service-worker": "^21.0.6", "@angular/service-worker": "^21.2.1",
"@date-fns/tz": "^1.4.1", "@date-fns/tz": "^1.4.1",
"@date-fns/utc": "^2.1.1", "@date-fns/utc": "^2.1.1",
"@oazapfts/runtime": "^1.0.4", "@oazapfts/runtime": "^1.2.0",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"keycloak-angular": "^20.0.0", "keycloak-angular": "^21.0.0",
"keycloak-js": "^26.2.1", "keycloak-js": "^26.2.3",
"rxjs": "~7.8.2", "rxjs": "^7.8.2",
"tslib": "^2.8.1" "tslib": "^2.8.1"
}, },
"devDependencies": { "devDependencies": {
"@angular/build": "^21.0.4", "@angular/build": "^21.2.1",
"@angular/cli": "^21.0.4", "@angular/cli": "^21.2.1",
"@angular/compiler-cli": "^21.0.6", "@angular/compiler-cli": "^21.2.1",
"@types/jasmine": "~5.1.13", "@types/jasmine": "~6.0.0",
"jasmine-core": "~5.9.0", "jasmine-core": "~6.1.0",
"karma": "~6.4.4", "karma": "~6.4.4",
"karma-chrome-launcher": "~3.2.0", "karma-chrome-launcher": "~3.2.0",
"karma-coverage": "~2.2.1", "karma-coverage": "~2.2.1",
"karma-jasmine": "~5.1.0", "karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0", "karma-jasmine-html-reporter": "~2.2.0",
"oazapfts": "^6.3.1", "oazapfts": "^7.4.2",
"typescript": "~5.9.3" "typescript": "~5.9.3"
} }
} }
@@ -1,10 +1,13 @@
<form (submit)="submit(); $event.preventDefault()"> <form [formRoot]="form">
<p>Name: {{ member.vorname }} {{ member.nachname }}</p> <p>Name: {{ member.vorname }} {{ member.nachname }}</p>
<p>Passnummer: {{ member.passnummer }}</p> <p>Passnummer: {{ member.passnummer }}</p>
<p>Geburtsdatum: {{ member.geburtsdatum | datePipe }}</p> <p>Geburtsdatum: {{ member.geburtsdatum | datePipe }}</p>
<mat-form-field> <mat-form-field>
<mat-label>K&uuml;ndigun zum</mat-label> <mat-label>K&uuml;ndigun zum</mat-label>
<input matInput [matDatepicker]="picker" [field]="form.kuendigungzum" /> <input
matInput
[matDatepicker]="picker"
[formField]="form.kuendigungzum" />
<mat-hint>DD.MM.YYYY</mat-hint> <mat-hint>DD.MM.YYYY</mat-hint>
<mat-datepicker-toggle <mat-datepicker-toggle
matIconSuffix matIconSuffix
@@ -4,7 +4,12 @@ import {
inject, inject,
signal, signal,
} from '@angular/core'; } from '@angular/core';
import { Field, form } from '@angular/forms/signals'; import {
form,
FormField,
FormRoot,
TreeValidationResult,
} from '@angular/forms/signals';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatOptionModule } from '@angular/material/core'; import { MatOptionModule } from '@angular/material/core';
@@ -33,7 +38,8 @@ import { EditMemberDialogComponent } from '../edit-member-dialog/edit-member-dia
MatOptionModule, MatOptionModule,
MatDatepickerModule, MatDatepickerModule,
DatePipePipe, DatePipePipe,
Field, FormField,
FormRoot,
], ],
templateUrl: './delete-member-dialog.component.html', templateUrl: './delete-member-dialog.component.html',
styleUrl: './delete-member-dialog.component.scss', styleUrl: './delete-member-dialog.component.scss',
@@ -49,16 +55,24 @@ export class DeleteMemberDialogComponent {
? dateToUTC(this.data.member.kuendigungzum) ? dateToUTC(this.data.member.kuendigungzum)
: null, : null,
}); });
protected readonly form = form(this.deletionModel); protected readonly form = form(this.deletionModel, {
submission: {
action: async () => {
return await this.submit();
},
},
});
protected readonly member = this.data.member; protected readonly member = this.data.member;
protected submit() { protected async submit(): Promise<TreeValidationResult> {
const date = this.deletionModel().kuendigungzum; const date = this.deletionModel().kuendigungzum;
api.scheduleDeletion( await api
this.member.id, .scheduleDeletion(
date ? dateToLocal(date).toISOString() : '', this.member.id,
).then(() => { date ? dateToLocal(date).toISOString() : '',
this.dialogRef.close(); )
}); .then(() => {
this.dialogRef.close();
});
} }
} }
@@ -1,23 +1,32 @@
<form (submit)="submit(); $event.preventDefault()"> <form [formRoot]="form">
<mat-form-field> <mat-form-field>
<mat-label>Vorname</mat-label> <mat-label>Vorname</mat-label>
<input matInput placeholder="Vorname" [field]="form.vorname" /> <input matInput placeholder="Vorname" [formField]="form.vorname" />
</mat-form-field> </mat-form-field>
<mat-form-field> <mat-form-field>
<mat-label>Nachname</mat-label> <mat-label>Nachname</mat-label>
<input matInput placeholder="Nachname" [field]="form.nachname" /> <input matInput placeholder="Nachname" [formField]="form.nachname" />
</mat-form-field> </mat-form-field>
<mat-form-field> <mat-form-field>
<mat-label>Geschlecht</mat-label> <mat-label>Geschlecht</mat-label>
<input matInput placeholder="Geschlecht" [field]="form.geschlecht" /> <input
matInput
placeholder="Geschlecht"
[formField]="form.geschlecht" />
</mat-form-field> </mat-form-field>
<mat-form-field> <mat-form-field>
<mat-label>Passnummer</mat-label> <mat-label>Passnummer</mat-label>
<input matInput placeholder="Passnummer" [field]="form.passnummer" /> <input
matInput
placeholder="Passnummer"
[formField]="form.passnummer" />
</mat-form-field> </mat-form-field>
<mat-form-field> <mat-form-field>
<mat-label>Geburtsdatum</mat-label> <mat-label>Geburtsdatum</mat-label>
<input matInput [matDatepicker]="picker" [field]="form.geburtsdatum" /> <input
matInput
[matDatepicker]="picker"
[formField]="form.geburtsdatum" />
<mat-hint>DD.MM.YYYY</mat-hint> <mat-hint>DD.MM.YYYY</mat-hint>
<mat-datepicker-toggle <mat-datepicker-toggle
matIconSuffix matIconSuffix
@@ -26,11 +35,14 @@
</mat-form-field> </mat-form-field>
<mat-form-field> <mat-form-field>
<mat-label>Geburtsort</mat-label> <mat-label>Geburtsort</mat-label>
<input matInput placeholder="Geburtsort" [field]="form.geburtsort" /> <input
matInput
placeholder="Geburtsort"
[formField]="form.geburtsort" />
</mat-form-field> </mat-form-field>
<mat-form-field> <mat-form-field>
<mat-label>Vereinsmitglied?</mat-label> <mat-label>Vereinsmitglied?</mat-label>
<mat-select matInput [field]="form.verein"> <mat-select matInput [formField]="form.verein">
<mat-option [value]="true">Ja</mat-option> <mat-option [value]="true">Ja</mat-option>
<mat-option [value]="false">Nein</mat-option> <mat-option [value]="false">Nein</mat-option>
</mat-select> </mat-select>
@@ -4,7 +4,12 @@ import {
inject, inject,
signal, signal,
} from '@angular/core'; } from '@angular/core';
import { Field, form } from '@angular/forms/signals'; import {
form,
FormField,
FormRoot,
TreeValidationResult,
} from '@angular/forms/signals';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatOptionModule } from '@angular/material/core'; import { MatOptionModule } from '@angular/material/core';
@@ -27,7 +32,8 @@ import { MemberDtoHelper, MemberEditForm } from '../../util/member.js';
MatSelectModule, MatSelectModule,
MatOptionModule, MatOptionModule,
MatDatepickerModule, MatDatepickerModule,
Field, FormField,
FormRoot,
], ],
templateUrl: './edit-member-dialog.component.html', templateUrl: './edit-member-dialog.component.html',
styleUrl: './edit-member-dialog.component.scss', styleUrl: './edit-member-dialog.component.scss',
@@ -43,13 +49,19 @@ export class EditMemberDialogComponent {
private readonly memberModel = signal<MemberEditForm>( private readonly memberModel = signal<MemberEditForm>(
this.memberDtoHelper.editMemberForm(this.data.member), this.memberDtoHelper.editMemberForm(this.data.member),
); );
protected readonly form = form(this.memberModel); protected readonly form = form(this.memberModel, {
submission: {
action: async () => {
return await this.submit();
},
},
});
protected submit() { protected async submit(): Promise<TreeValidationResult> {
const member: api.MemberDto = this.memberDtoHelper.memberFromSignal( const member: api.MemberDto = this.memberDtoHelper.memberFromSignal(
this.memberModel(), this.memberModel(),
); );
api.updateMember(member).then(() => { await api.updateMember(member).then(() => {
this.dialogRef.close(); this.dialogRef.close();
}); });
} }
@@ -1,23 +1,23 @@
<h1>Neues Mitglied anlegen</h1> <h1>Neues Mitglied anlegen</h1>
<form (submit)="onSubmit(); $event.preventDefault()"> <form [formRoot]="memberForm">
<mat-form-field> <mat-form-field>
<mat-label>Vorname</mat-label> <mat-label>Vorname</mat-label>
<input matInput [field]="memberForm.vorname" /> <input matInput [formField]="memberForm.vorname" />
</mat-form-field> </mat-form-field>
<mat-form-field> <mat-form-field>
<mat-label>Nachname</mat-label> <mat-label>Nachname</mat-label>
<input matInput [field]="memberForm.nachname" /> <input matInput [formField]="memberForm.nachname" />
</mat-form-field> </mat-form-field>
<mat-form-field> <mat-form-field>
<mat-label>Geschlecht</mat-label> <mat-label>Geschlecht</mat-label>
<input matInput [field]="memberForm.geschlecht" /> <input matInput [formField]="memberForm.geschlecht" />
</mat-form-field> </mat-form-field>
<mat-form-field> <mat-form-field>
<mat-label>Geburtsdatum</mat-label> <mat-label>Geburtsdatum</mat-label>
<input <input
matInput matInput
[matDatepicker]="picker" [matDatepicker]="picker"
[field]="memberForm.geburtsdatum" /> [formField]="memberForm.geburtsdatum" />
<mat-hint>DD.MM.YYYY</mat-hint> <mat-hint>DD.MM.YYYY</mat-hint>
<mat-datepicker-toggle <mat-datepicker-toggle
matIconSuffix matIconSuffix
@@ -26,16 +26,18 @@
</mat-form-field> </mat-form-field>
<mat-form-field> <mat-form-field>
<mat-label>Geburtsort</mat-label> <mat-label>Geburtsort</mat-label>
<input matInput [field]="memberForm.geburtsort" /> <input matInput [formField]="memberForm.geburtsort" />
</mat-form-field> </mat-form-field>
<mat-checkbox [field]="memberForm.verein">Verein Beigetreten?</mat-checkbox> <mat-checkbox [formField]="memberForm.verein"
>Verein Beigetreten?</mat-checkbox
>
<mat-form-field> <mat-form-field>
<mat-label>Passnummer (falls vorhanden)</mat-label> <mat-label>Passnummer (falls vorhanden)</mat-label>
<input matInput [field]="memberForm.passnummer" /> <input matInput [formField]="memberForm.passnummer" />
</mat-form-field> </mat-form-field>
<mat-form-field> <mat-form-field>
<mat-label>Kontakt</mat-label> <mat-label>Kontakt</mat-label>
<input matInput [field]="memberForm.kontakt" /> <input matInput [formField]="memberForm.kontakt" />
</mat-form-field> </mat-form-field>
<button mat-flat-button color="primary" type="submit">Hinzufügen</button> <button mat-flat-button color="primary" type="submit">Hinzufügen</button>
</form> </form>
@@ -4,7 +4,12 @@ import {
inject, inject,
signal, signal,
} from '@angular/core'; } from '@angular/core';
import { Field, form } from '@angular/forms/signals'; import {
form,
FormField,
FormRoot,
TreeValidationResult,
} from '@angular/forms/signals';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatDatepickerModule } from '@angular/material/datepicker';
@@ -24,7 +29,8 @@ import { MemberCreateForm, MemberDtoHelper } from '../../util/member.js';
MatInputModule, MatInputModule,
MatCheckboxModule, MatCheckboxModule,
MatButtonModule, MatButtonModule,
Field, FormField,
FormRoot,
], ],
templateUrl: './new-member.component.html', templateUrl: './new-member.component.html',
styleUrl: './new-member.component.scss', styleUrl: './new-member.component.scss',
@@ -39,18 +45,21 @@ export class NewMemberComponent {
private readonly memberModel = signal<MemberCreateForm>( private readonly memberModel = signal<MemberCreateForm>(
this.memberDtoHelper.newMemberForm(), this.memberDtoHelper.newMemberForm(),
); );
protected readonly memberForm = form(this.memberModel); protected readonly memberForm = form(this.memberModel, {
submission: {
protected onSubmit() { action: async () => {
if (!this.memberForm().valid()) { return await this.submit();
return; },
} },
});
protected async submit(): Promise<TreeValidationResult> {
const newMember = this.memberDtoHelper.newMemberFromSignal( const newMember = this.memberDtoHelper.newMemberFromSignal(
this.memberModel(), this.memberModel(),
); );
api.createMember(newMember) await api
.createMember(newMember)
.then(() => { .then(() => {
console.log('Successfully added Member'); console.log('Successfully added Member');
this.dialogRef.close(); this.dialogRef.close();
@@ -20,16 +20,16 @@
<label>Filter:</label> <label>Filter:</label>
<input <input
matInput matInput
[field]="filterForm.filterString" [formField]="filterForm.filterString"
placeholder="Filter" /> placeholder="Filter" />
</mat-form-field> </mat-form-field>
<div class="status-filters"> <div class="status-filters">
<mat-radio-group [field]="filterForm.status"> <mat-radio-group [formField]="filterForm.status">
<mat-radio-button [value]="MemberStatusFilter.ACTIVE" <mat-radio-button [value]="MemberStatusFilter.ACTIVE"
>Aktiv</mat-radio-button >Aktiv zum</mat-radio-button
> >
<mat-radio-button [value]="MemberStatusFilter.INACTIVE" <mat-radio-button [value]="MemberStatusFilter.INACTIVE"
>Inaktiv</mat-radio-button >Gekündigt seit</mat-radio-button
> >
<mat-radio-button [value]="MemberStatusFilter.ALL" <mat-radio-button [value]="MemberStatusFilter.ALL"
>Alle</mat-radio-button >Alle</mat-radio-button
@@ -42,7 +42,7 @@
<input <input
matInput matInput
[matDatepicker]="picker" [matDatepicker]="picker"
[field]="filterForm.statusDate" [formField]="filterForm.statusDate"
placeholder="Aktiv bis" /> placeholder="Aktiv bis" />
<mat-hint>DD.MM.YYYY</mat-hint> <mat-hint>DD.MM.YYYY</mat-hint>
<mat-datepicker-toggle <mat-datepicker-toggle
@@ -52,7 +52,7 @@
<mat-datepicker #picker></mat-datepicker> <mat-datepicker #picker></mat-datepicker>
</mat-form-field> </mat-form-field>
</div> </div>
<mat-checkbox [field]="filterForm.onlyWithID" <mat-checkbox [formField]="filterForm.onlyWithID"
>Nur mit Passnummer</mat-checkbox >Nur mit Passnummer</mat-checkbox
> >
</div> </div>
@@ -110,7 +110,7 @@
</ng-container> </ng-container>
<ng-container matColumnDef="geburtsdatum"> <ng-container matColumnDef="geburtsdatum">
<th mat-header-cell mat-sort-header *matHeaderCellDef> <th mat-header-cell mat-sort-header *matHeaderCellDef>
Geburtsdatum Geburtsdatum Geboren am
</th> </th>
<td mat-cell *matCellDef="let element"> <td mat-cell *matCellDef="let element">
{{ element.geburtsdatum | datePipe }} {{ element.geburtsdatum | datePipe }}
@@ -123,9 +123,7 @@
<td mat-cell *matCellDef="let element">{{ element.geburtsort }}</td> <td mat-cell *matCellDef="let element">{{ element.geburtsort }}</td>
</ng-container> </ng-container>
<ng-container matColumnDef="verein"> <ng-container matColumnDef="verein">
<th mat-header-cell mat-sort-header *matHeaderCellDef> <th mat-header-cell mat-sort-header *matHeaderCellDef>Verein?</th>
Vereinsmitgliedschaft
</th>
<td mat-cell *matCellDef="let element"> <td mat-cell *matCellDef="let element">
@if(element.verein){ @if(element.verein){
<mat-icon <mat-icon
@@ -149,12 +147,18 @@
<ng-container matColumnDef="kuendigungzum"> <ng-container matColumnDef="kuendigungzum">
<th mat-header-cell mat-sort-header *matHeaderCellDef> <th mat-header-cell mat-sort-header *matHeaderCellDef>
K&uuml;ndigung zum K&uuml;ndigung
</th> </th>
<td mat-cell *matCellDef="let element"> <td mat-cell *matCellDef="let element">
{{ (element.kuendigungzum | datePipe) ?? "-" }} {{ (element.kuendigungzum | datePipe) ?? "-" }}
</td> </td>
</ng-container> </ng-container>
<ng-container matColumnDef="creationDate">
<th mat-header-cell mat-sort-header *matHeaderCellDef>Beitritt</th>
<td mat-cell *matCellDef="let element">
{{ element.creationDate | datePipe }}
</td>
</ng-container>
<ng-container matColumnDef="delete"> <ng-container matColumnDef="delete">
<th mat-header-cell *matHeaderCellDef>Entf.</th> <th mat-header-cell *matHeaderCellDef>Entf.</th>
@@ -187,7 +191,8 @@
<tr <tr
mat-row mat-row
*matRowDef="let row; columns: displayedColumns" *matRowDef="let row; columns: displayedColumns"
class="table-row"></tr> class="table-row"
[class.expired]="isInPast(row.kuendigungzum)"></tr>
<tr mat-footer-row *matFooterRowDef="['totalCount']; sticky: true"></tr> <tr mat-footer-row *matFooterRowDef="['totalCount']; sticky: true"></tr>
</table> </table>
</div> </div>
@@ -49,3 +49,16 @@ td {
.table-row:nth-child(odd) { .table-row:nth-child(odd) {
background-color: var(--mat-sys-surface); background-color: var(--mat-sys-surface);
} }
.table-row.expired {
//background-color: lightsalmon;
filter: sepia(50%);
}
.positive {
color: green;
}
.negative {
color: red;
}
@@ -9,7 +9,7 @@ import {
signal, signal,
viewChild, viewChild,
} from '@angular/core'; } from '@angular/core';
import { Field, form } from '@angular/forms/signals'; import { form, FormField } from '@angular/forms/signals';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatCheckbox } from '@angular/material/checkbox'; import { MatCheckbox } from '@angular/material/checkbox';
import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatDatepickerModule } from '@angular/material/datepicker';
@@ -30,6 +30,44 @@ import { DeleteMemberDialogComponent } from '../delete-member-dialog/delete-memb
import { EditMemberDialogComponent } from '../edit-member-dialog/edit-member-dialog.component.js'; import { EditMemberDialogComponent } from '../edit-member-dialog/edit-member-dialog.component.js';
import { NewMemberComponent } from '../new-member/new-member.component'; import { NewMemberComponent } from '../new-member/new-member.component';
class MemberFilters {
public filterString: string;
public status: MemberStatusFilter;
public statusDate: Date | null;
public onlyWithID: boolean;
public constructor(data: {
filterString: string;
status: MemberStatusFilter;
statusDate: Date | null;
onlyWithID: boolean;
}) {
this.filterString = data.filterString;
this.status = data.status;
this.statusDate = data.statusDate;
this.onlyWithID = data.onlyWithID;
}
public encode(): string {
return `${this.filterString?.replaceAll(';', '') ?? ''};${this.status};${this.statusDate?.toISOString()};${this.onlyWithID}`;
}
public static decode(encoded: string): MemberFilters {
const parts = encoded.split(';');
const statusDateStr = parts[2];
return new MemberFilters({
filterString: parts[0],
status: parts[1] as MemberStatusFilter,
statusDate:
statusDateStr && statusDateStr != ''
? new Date(statusDateStr)
: null,
onlyWithID: parts[3] === 'true',
});
}
}
enum MemberStatusFilter { enum MemberStatusFilter {
ACTIVE = 'ACTIVE', ACTIVE = 'ACTIVE',
INACTIVE = 'INACTIVE', INACTIVE = 'INACTIVE',
@@ -53,7 +91,7 @@ enum MemberStatusFilter {
MatCheckbox, MatCheckbox,
MatRadioModule, MatRadioModule,
MatSortModule, MatSortModule,
Field, FormField,
], ],
templateUrl: './view-members.html', templateUrl: './view-members.html',
styleUrl: './view-members.scss', styleUrl: './view-members.scss',
@@ -66,7 +104,7 @@ export class ViewMembers implements AfterViewInit {
private readonly filterModel = signal({ private readonly filterModel = signal({
filterString: '', filterString: '',
status: MemberStatusFilter.ACTIVE, status: MemberStatusFilter.ACTIVE,
statusDate: null as Date | null, statusDate: new Date() as Date | null,
onlyWithID: false, onlyWithID: false,
}); });
protected readonly filterForm = form(this.filterModel); protected readonly filterForm = form(this.filterModel);
@@ -89,6 +127,7 @@ export class ViewMembers implements AfterViewInit {
'geburtsort', 'geburtsort',
'verein', 'verein',
'kontakt', 'kontakt',
'creationDate',
'kuendigungzum', 'kuendigungzum',
'delete', 'delete',
]; ];
@@ -139,35 +178,41 @@ export class ViewMembers implements AfterViewInit {
data: api.MemberDto, data: api.MemberDto,
encodedFilter: string, encodedFilter: string,
) => { ) => {
const parts = encodedFilter.split(';'); const filters = MemberFilters.decode(encodedFilter);
const filter = parts[0];
const status = parts[1] as MemberStatusFilter;
const statusDateStr = parts[2];
const onlyWithID = parts[3] === 'true';
const currentDate = if (filters.statusDate) {
statusDateStr && statusDateStr != '' const cancellationDate = data.kuendigungzum
? new Date(statusDateStr) ? new Date(data.kuendigungzum)
: null; : null;
const cancellationDate = data.kuendigungzum
? new Date(data.kuendigungzum) switch (filters.status) {
: null; case MemberStatusFilter.ACTIVE:
if ( const creationDate = data.creationDate
currentDate && ? new Date(data.creationDate)
cancellationDate && : null;
((status === MemberStatusFilter.ACTIVE && if (
cancellationDate <= currentDate) || (cancellationDate &&
(status === MemberStatusFilter.INACTIVE && cancellationDate <= filters.statusDate) ||
cancellationDate > currentDate)) (creationDate && creationDate > filters.statusDate)
) { ) {
return false; return false;
} }
if (status === MemberStatusFilter.INACTIVE && !cancellationDate) { break;
return false; case MemberStatusFilter.INACTIVE:
if (
!cancellationDate ||
cancellationDate < filters.statusDate
) {
return false;
}
break;
case MemberStatusFilter.ALL:
break;
}
} }
if ( if (
onlyWithID && filters.onlyWithID &&
(!data.passnummer || data.passnummer.trim() === '') (!data.passnummer || data.passnummer.trim() === '')
) { ) {
return false; return false;
@@ -186,14 +231,23 @@ export class ViewMembers implements AfterViewInit {
letztePruefung: this.datePipe.transform( letztePruefung: this.datePipe.transform(
data.letztePruefung ?? undefined, data.letztePruefung ?? undefined,
), ),
creationDate: this.datePipe.transform(
data.creationDate ?? undefined,
),
}, },
filter, filters.filterString,
); );
}; };
effect(() => { effect(() => {
const filters = this.filterModel(); const filtersSignal = this.filterModel();
this.dataSource.filter = `${filters.filterString?.replaceAll(';', '') ?? ''};${filters.status};${filters.statusDate?.toISOString()};${filters.onlyWithID}`; const filters = new MemberFilters({
filterString: filtersSignal.filterString,
status: filtersSignal.status,
statusDate: filtersSignal.statusDate,
onlyWithID: filtersSignal.onlyWithID,
});
this.dataSource.filter = filters.encode();
}); });
this.updateData(); this.updateData();
@@ -234,7 +288,7 @@ export class ViewMembers implements AfterViewInit {
headerRow.appendChild(clubnrHeader); headerRow.appendChild(clubnrHeader);
const birthdayHeader = document.createElement('th'); const birthdayHeader = document.createElement('th');
birthdayHeader.innerText = 'Geburtsdatum'; birthdayHeader.innerText = 'Geboren am';
headerRow.appendChild(birthdayHeader); headerRow.appendChild(birthdayHeader);
const geburtsortHeader = document.createElement('th'); const geburtsortHeader = document.createElement('th');
@@ -242,7 +296,7 @@ export class ViewMembers implements AfterViewInit {
headerRow.appendChild(geburtsortHeader); headerRow.appendChild(geburtsortHeader);
const cancelDateHeader = document.createElement('th'); const cancelDateHeader = document.createElement('th');
cancelDateHeader.innerText = 'Kündigungsdatum'; cancelDateHeader.innerText = 'Kündigung';
headerRow.appendChild(cancelDateHeader); headerRow.appendChild(cancelDateHeader);
table.appendChild(headerRow); table.appendChild(headerRow);
@@ -323,4 +377,11 @@ export class ViewMembers implements AfterViewInit {
document.body.appendChild(printWindow); document.body.appendChild(printWindow);
printWindow.contentWindow?.print(); printWindow.contentWindow?.print();
} }
protected isInPast(date: string): boolean {
if (!date) return false;
const today = new Date();
const cancellationDate = new Date(date);
return cancellationDate <= today;
}
} }
@@ -116,7 +116,7 @@ export class Authentication {
return await this.keycloak.login({ return await this.keycloak.login({
redirectUri: redirectUri, redirectUri: redirectUri,
locale: 'de-DE', locale: 'de-DE',
//TODO loginHint:'' //TODO loginHint:''
}); });
} }
+1 -1
View File
@@ -23,7 +23,7 @@ interface MemberForm {
export interface MemberCreateForm extends Omit< export interface MemberCreateForm extends Omit<
MemberCreateArgs, MemberCreateArgs,
'geburtsdatum' 'geburtsdatum' | 'creationDate'
> { > {
geburtsdatum: Date | null; geburtsdatum: Date | null;
} }
+1707 -2682
View File
File diff suppressed because it is too large Load Diff