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
+11 -6
View File
@@ -8,13 +8,9 @@
"type": "node",
"request": "launch",
"name": "Launch Program",
"skipFiles": [
"<node_internals>/**"
],
"skipFiles": ["<node_internals>/**"],
"program": "${workspaceFolder}\\dist\\index.js",
"outFiles": [
"${workspaceFolder}/**/*.js"
],
"outFiles": ["${workspaceFolder}/**/*.js"],
"env": {
"TKD_DB_USER": "webapi",
"TKD_DB_PASSWORD": "devpassword",
@@ -22,6 +18,15 @@
"TKD_COOKIE_SECRET": "devsecret"
},
"runtimeExecutable": "C:\\Users\\Toni\\AppData\\Roaming\\fnm\\aliases\\default\\node"
},
{
"name": "Attach to Gandalf",
"type": "node",
"request": "attach",
"address": "192.168.2.250",
"port": 9229,
"localRoot": "${workspaceFolder}",
"remoteRoot": "/var/www/api"
}
]
}
+8 -5
View File
@@ -3,14 +3,15 @@
"version": "1.0.0",
"scripts": {
"start": "node dist/index.js",
"build": "tsc -p ."
"build": "tsc -p .",
"dev": "pnpm -w run dev"
},
"type": "module",
"packageManager": "pnpm@10.24.0+sha512.01ff8ae71b4419903b65c60fb2dc9d34cf8bb6e06d03bde112ef38f7a34d6904c424ba66bea5cdcf12890230bf39f9580473140ed9c946fef328b6e5238a345a",
"private": true,
"devDependencies": {
"@types/cors": "^2.8.19",
"@types/connect-pg-simple": "^7.0.3",
"@types/cors": "^2.8.19",
"@types/express": "^5.0.5",
"@types/express-session": "^1.18.2",
"@types/node": "^24.9.2",
@@ -20,17 +21,19 @@
"typescript": "~5.9.3"
},
"dependencies": {
"cors": "^2.8.5",
"rxjs": "~7.8.2",
"@keycloak/keycloak-admin-client": "^26.4.4",
"@prisma/adapter-pg": "^7.0.1",
"@prisma/client": "^7.0.1",
"@tsoa/runtime": "^6.6.0",
"connect-pg-simple": "^10.0.0",
"cors": "^2.8.5",
"dotenv": "^17.2.3",
"express": "^5.1.0",
"express-session": "^1.18.2",
"helmet": "^8.1.0",
"keycloak-connect": "^26.1.1",
"pg": "^8.16.3"
"pg": "^8.16.3",
"rxjs": "~7.8.2",
"tsoa": "^6.6.0"
}
}
@@ -0,0 +1,21 @@
/*
Warnings:
- You are about to drop the column `uvertrag` on the `Member` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "Member" DROP COLUMN "uvertrag",
ADD COLUMN "marker" BOOLEAN;
-- CreateTable
CREATE TABLE "session" (
"sid" VARCHAR NOT NULL,
"sess" JSON NOT NULL,
"expire" TIMESTAMP(6) NOT NULL,
CONSTRAINT "session_pkey" PRIMARY KEY ("sid")
);
-- CreateIndex
CREATE INDEX "IDX_session_expire" ON "session"("expire");
+1 -1
View File
@@ -15,12 +15,12 @@ model Member {
geburtsort String? @db.VarChar
geschlecht String? @db.VarChar
passnummer String? @db.VarChar
uvertrag Boolean?
verein Boolean?
graduierung String? @db.VarChar
letztePruefung DateTime? @db.Date
kontakt String? @db.VarChar
kuendigungzum DateTime? @db.Date
marker Boolean?
}
model session {
-5
View File
@@ -1,5 +0,0 @@
#!/bin/bash
echo "Starting development environment..."
pnpm -v
@@ -6,6 +6,7 @@ import {
Path,
Post,
Put,
Query,
Request,
Response,
Route,
@@ -14,7 +15,7 @@ import {
} from "@tsoa/runtime";
import express from "express";
import { Claim } from "../dtos/claim.js";
import type { Member, MemberCreateArgs } from "../dtos/member.js";
import type { MemberCreateArgs, MemberDto } from "../dtos/member.js";
import { inject } from "../infrastructure/di/index.js";
import { ErrorStates } from "../infrastructure/errorStates.js";
import { KC_SECURITY_NAME } from "../services/keycloak/user/index.js";
@@ -28,7 +29,7 @@ export class MembersContoller extends Controller {
@Get()
public async getMembers(
@Request() request: express.Request,
): Promise<Member[]> {
): Promise<MemberDto[]> {
return await this.membersService.getMembers(request);
}
@@ -39,7 +40,7 @@ export class MembersContoller extends Controller {
public async getMember(
@Path() memberId: string,
@Request() request: express.Request,
): Promise<Member> {
): Promise<MemberDto> {
try {
const foundMember = await this.membersService.getMemberById(
memberId,
@@ -64,7 +65,7 @@ export class MembersContoller extends Controller {
public async createMember(
@Body() createArgs: MemberCreateArgs,
@Request() request: express.Request,
): Promise<Member> {
): Promise<MemberDto> {
return await this.membersService.createMember(createArgs, request);
}
@@ -74,7 +75,7 @@ export class MembersContoller extends Controller {
@Response(404, "Member not found")
@Put()
public async updateMember(
@Body() member: Member,
@Body() member: MemberDto,
@Request() request: express.Request,
): Promise<void> {
try {
@@ -97,7 +98,7 @@ export class MembersContoller extends Controller {
@Delete("{memberId}")
public async scheduleDeletion(
@Path() memberId: string,
@Body() cancelDate: Date,
@Query() cancelDate: Date,
@Request() request: express.Request,
) {
await this.membersService.scheduleDeletion(
+48 -13
View File
@@ -1,27 +1,39 @@
import { type MemberModel as DbMember } from "../generated/prisma/models.js";
export interface Member {
export interface MemberDto {
id: string | null;
vorname: string | null;
nachname: string | null;
geburtsdatum: Date | null;
geburtsdatum: string | null;
geburtsort: string | null;
geschlecht: string | null;
passnummer: string | null;
uvertrag: boolean | null;
verein: boolean | null;
graduierung: string | null;
letztePruefung: Date | null;
letztePruefung: string | null;
kontakt: string | null;
kuendigungzum: Date | null;
loeschenam: Date | null;
kuendigungzum: string | null;
marker: boolean | null;
}
export type MemberCreateArgs = Omit<Member, "id" | "loeschenam">;
export class MemberCreateArgs implements Omit<MemberDto, "id"> {
vorname!: string | null;
nachname!: string | null;
geburtsdatum!: string | null;
geburtsort!: string | null;
geschlecht!: string | null;
passnummer!: string | null;
verein!: boolean | null;
graduierung!: string | null;
letztePruefung!: string | null;
kontakt!: string | null;
kuendigungzum!: string | null;
marker!: boolean | null;
}
export function mapDbMemberToMember(dbMember: DbMember): Member {
export function mapDbMemberToMember(dbMember: DbMember): MemberDto {
return {
geburtsdatum: dbMember.geburtsdatum,
geburtsdatum: dbMember.geburtsdatum?.toDateString() ?? null,
id: dbMember.id,
nachname: dbMember.nachname,
geburtsort: dbMember.geburtsort,
@@ -29,11 +41,34 @@ export function mapDbMemberToMember(dbMember: DbMember): Member {
graduierung: dbMember.graduierung,
vorname: dbMember.vorname,
kontakt: dbMember.kontakt,
kuendigungzum: dbMember.kuendigungzum,
letztePruefung: dbMember.letztePruefung,
kuendigungzum: dbMember.kuendigungzum?.toDateString() ?? null,
letztePruefung: dbMember.letztePruefung?.toDateString() ?? null,
passnummer: dbMember.passnummer,
uvertrag: dbMember.uvertrag,
verein: dbMember.verein,
loeschenam: new Date(), //TODO
marker: dbMember.marker,
};
}
export function mapMemberDtoToDbMember(member: MemberDto): DbMember {
return {
id: member.id!,
vorname: member.vorname,
nachname: member.nachname,
geburtsdatum: member.geburtsdatum
? new Date(member.geburtsdatum)
: null,
geburtsort: member.geburtsort,
geschlecht: member.geschlecht,
passnummer: member.passnummer,
verein: member.verein,
graduierung: member.graduierung,
letztePruefung: member.letztePruefung
? new Date(member.letztePruefung)
: null,
kontakt: member.kontakt,
kuendigungzum: member.kuendigungzum
? new Date(member.kuendigungzum)
: null,
marker: member.marker,
};
}
+94 -48
View File
@@ -1,67 +1,113 @@
import constants from "constants";
import cors from "cors";
import express, {
type NextFunction,
type Request,
type Response,
} from "express";
import fs from "fs";
import https from "https";
import { RegisterRoutes } from "./generated/routes.js";
import { inject } from "./infrastructure/di/injector.js";
import { sessionHandler } from "./infrastructure/sessionHandler.js";
import { EnvironmentService } from "./services/environmentService.js";
import { keycloak } from "./services/keycloak/user/index.js";
import { KeycloakUser } from "./services/keycloak/user/keycloak-user.js";
// import helmet from "helmet";
// TODO import http2 from 'http2';
const app = express();
async function run() {
const app = express();
const keycloakUser = inject(KeycloakUser);
app.use(sessionHandler);
app.use(keycloak.middleware());
//TODO app.use(helmet.hsts()); this and other helmets
app.use(
cors({
origin: [
"https://taekwondo-chemnitz.toni714.de",
"https://tcc-1-ev-intern.web.app",
"http://localhost:4200",
"https://localhost:4200",
"http://localhost:8080",
],
methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
//TODO reevaluate
}),
);
app.use(sessionHandler);
app.use(await keycloakUser.middleware());
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
app.use(
cors({
origin: [
"https://taekwondo-chemnitz.toni714.de",
"https://tcc-1-ev-intern.web.app",
"http://localhost:4200",
"https://localhost:4200",
"http://localhost:3000",
"http://localhost:8080",
],
//TODO reevaluate
}),
);
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
// const dbService = inject(DatabaseService);
// app.use(async (req, res, next) => {
// await dbService.createTransaction(async prisma => {
// (req as any).transaction = prisma;
// return new Promise<void>((resolve, reject) => {
// res.on("finish", resolve);
// res.on("close", resolve);
// res.on("error", reject);
// next();
// });
// });
// });
// const dbService = inject(DatabaseService);
// app.use(async (req, res, next) => {
// await dbService.createTransaction(async prisma => {
// (req as any).transaction = prisma;
// return new Promise<void>((resolve, reject) => {
// res.on("finish", resolve);
// res.on("close", resolve);
// res.on("error", reject);
// next();
// });
// });
// });
RegisterRoutes(app);
RegisterRoutes(app);
const environment = inject(EnvironmentService);
// wants to be last
app.use((err: unknown, _req: Request, res: Response, _next: NextFunction) => {
res.status(500);
res.send(JSON.stringify(err));
throw err;
});
const environment = inject(EnvironmentService);
// wants to be last
app.use(
(err: unknown, _req: Request, res: Response, _next: NextFunction) => {
console.dir(err);
const ex = JSON.stringify(err);
res.header("Content-Length", `${ex.length}`);
res.status(500).send(ex);
throw err;
},
);
if (environment.isDev()) {
//TODO create self-signed for localhost
const port = 3000;
app.listen(port, () => {
console.log(`Server is running at http://localhost:${port}`);
});
} else {
//TODO start with HTTPS in production
if (environment.isDev()) {
//TODO create self-signed for localhost
const port = 3000;
app.listen(port, () => {
console.log(`Server is running at http://localhost:${port}`);
});
} else {
//TODO start with HTTPS in production
const port = 5443;
const SSL_CERT_PATH = process.env["SSL_CERT_PATH"];
const SSL_KEY_PATH = process.env["SSL_KEY_PATH"];
const SSL_KEY_PASSWORD = process.env["SSL_KEY_PASSWORD"];
if (!SSL_CERT_PATH || !SSL_KEY_PATH || !SSL_KEY_PASSWORD) {
throw new Error("Missing SSL configuration");
}
const sslOptions: https.ServerOptions = {
cert: fs.readFileSync(SSL_CERT_PATH),
key: fs.readFileSync(SSL_KEY_PATH),
passphrase: SSL_KEY_PASSWORD,
minVersion: "TLSv1.3",
maxVersion: "TLSv1.3",
ecdhCurve: "X25519:prime256v1:secp384r1",
honorCipherOrder: false,
secureOptions:
constants.SSL_OP_NO_TLSv1 |
constants.SSL_OP_NO_TLSv1_1 |
constants.SSL_OP_NO_TLSv1_2 |
constants.SSL_OP_NO_SSLv2 |
constants.SSL_OP_NO_SSLv3,
};
https.createServer(sslOptions, app).listen(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");
}
console.log("done");
run();
+18 -27
View File
@@ -1,39 +1,30 @@
import { type Request, type RequestHandler } from "express";
import { type Request, type Response } from "express";
import { KC_SECURITY_NAME, keycloak } from "../services/keycloak/user/index.js";
import { inject } from "../infrastructure/di/index.js";
import {
KC_SECURITY_NAME,
KeycloakUser,
} from "../services/keycloak/user/index.js";
function promisifyMiddleware(
request: Request,
middleware: RequestHandler,
response?: Response,
): Promise<any> {
return new Promise((resolve, reject) => {
middleware(request, response || ({} as any), (next_result: any) => {
console.dir(next_result);
next_result instanceof Error
? reject(next_result)
: resolve(next_result);
});
});
}
const keycloak = inject(KeycloakUser);
async function doKeycloakAuth(
request: Request,
roles?: string[],
response?: Response,
): Promise<any> {
if (roles && roles.length > 0) {
const grant = await keycloak.getGrant(request, response ?? ({} as any));
const allowed = roles.every(role =>
grant.access_token?.hasRealmRole(role),
);
if (allowed) {
return Promise.resolve();
} else {
return Promise.reject(httpError(403, "Forbidden"));
}
if (!roles || roles.length < 1) {
roles = ["default-roles-taekwondo"];
}
const allowed = await keycloak.checkRoles(
roles,
request,
response ?? ({} as any),
);
if (allowed) {
return Promise.resolve();
} else {
return await promisifyMiddleware(request, keycloak.protect(), response);
return Promise.reject(httpError(403, "Forbidden"));
}
}
+3 -3
View File
@@ -44,9 +44,9 @@ export class DatabaseService {
private readonly prismaClient: ExtendedClient;
public constructor() {
const dbUser = process.env["TKD_DB_USER"];
const dbPassword = process.env["TKD_DB_PASSWORD"];
const dbHost = process.env["TKD_DB_HOST"];
const dbUser = process.env["TKD_DB_USER"]!;
const dbPassword = process.env["TKD_DB_PASSWORD"]!;
const dbHost = process.env["TKD_DB_HOST"]!;
const connectionString = `postgresql://${dbUser}:${dbPassword}@${dbHost}:5432/taekwondo?schema=public`;
@@ -1,23 +1,43 @@
import { Injectable } from "../../../infrastructure/di/injectable-decorator.js";
import kcClientConfig from "./nodejs-api.json" with { type: "json" };
import KeycloakAdminClient from "@keycloak/keycloak-admin-client";
import type RoleRepresentation from "@keycloak/keycloak-admin-client/lib/defs/roleRepresentation.js";
import { Claim } from "../../../dtos/claim.js";
import { type User } from "../../../dtos/user.js";
import { inject } from "../../../infrastructure/di/index.js";
import { EnvironmentService } from "../../environmentService.js";
@Injectable()
export class KeycloakAdmin {
private readonly kcAdminClient: Promise<KeycloakAdminClient>;
private readonly environment = inject(EnvironmentService);
private readonly config = this.getConfig();
private async getConfig() {
if (this.environment.isDev()) {
const config = await import("./nodejs-api.dev.json", {
with: { type: "json" },
});
return config.default;
} else {
const config = await import("./nodejs-api.prod.json", {
with: { type: "json" },
});
return config.default;
}
}
constructor() {
this.kcAdminClient = new Promise(async resolve => {
const config = await this.config;
const client = new KeycloakAdminClient({
baseUrl: kcClientConfig.baseUrl,
baseUrl: config["auth-server-url"],
realmName: "taekwondo",
});
await client.auth({
...kcClientConfig,
clientSecret: config.credentials.secret,
clientId: config.resource,
grantType: "client_credentials",
});
resolve(client);
@@ -0,0 +1,10 @@
{
"realm": "taekwondo",
"auth-server-url": "http://localhost:8080/",
"ssl-required": "external",
"resource": "nodejs-api",
"credentials": {
"secret": "devsecret"
},
"confidential-port": 0
}
@@ -1,60 +0,0 @@
{
"clientId": "nodejs-api",
"name": "NodejsApi",
"description": "",
"rootUrl": "http://localhost:3000",
"adminUrl": "http://localhost:3000",
"baseUrl": "http://localhost:3000",
"surrogateAuthRequired": false,
"enabled": true,
"alwaysDisplayInConsole": false,
"clientAuthenticatorType": "client-secret",
"secret": "devsecret",
"redirectUris": ["http://localhost:3000/*"],
"webOrigins": ["http://localhost:3000"],
"notBefore": 0,
"bearerOnly": false,
"consentRequired": false,
"standardFlowEnabled": false,
"implicitFlowEnabled": false,
"directAccessGrantsEnabled": false,
"serviceAccountsEnabled": true,
"publicClient": false,
"frontchannelLogout": true,
"protocol": "openid-connect",
"attributes": {
"realm_client": "false",
"oidc.ciba.grant.enabled": "false",
"client.secret.creation.time": "1762463686",
"backchannel.logout.session.required": "true",
"standard.token.exchange.enabled": "true",
"oauth2.device.authorization.grant.enabled": "false",
"pkce.code.challenge.method": "S256",
"backchannel.logout.revoke.offline.tokens": "false",
"dpop.bound.access.tokens": "false"
},
"authenticationFlowBindingOverrides": {},
"fullScopeAllowed": true,
"nodeReRegistrationTimeout": -1,
"defaultClientScopes": [
"service_account",
"web-origins",
"acr",
"roles",
"profile",
"basic",
"email"
],
"optionalClientScopes": [
"address",
"phone",
"organization",
"offline_access",
"microprofile-jwt"
],
"access": {
"view": true,
"configure": true,
"manage": true
}
}
@@ -1,11 +1,52 @@
import { type Request, type RequestHandler, type Response } from "express";
import KeycloakConnect from "keycloak-connect";
import { inject } from "../../../infrastructure/di/index.js";
import { Injectable } from "../../../infrastructure/di/injectable-decorator.js";
import { sessionStore } from "../../../infrastructure/sessionHandler.js";
import keycloakConfig from "./keycloak.json" with { type: "json" };
import { EnvironmentService } from "../../environmentService.js";
//TODO transform into service and load config based on environment (see keycloak-admin)
export const KC_SECURITY_NAME = "bearerAuth";
export const keycloak = new KeycloakConnect(
{ store: sessionStore },
keycloakConfig,
);
export const kcUserConfig = keycloakConfig;
@Injectable()
export class KeycloakUser {
private readonly environment = inject(EnvironmentService);
private readonly keycloakConnect: Promise<KeycloakConnect.Keycloak>;
private async getConfig() {
if (this.environment.isDev()) {
const config = await import("./keycloak.dev.json", {
with: { type: "json" },
});
return config.default;
} else {
const config = await import("./keycloak.prod.json", {
with: { type: "json" },
});
return config.default;
}
}
public constructor() {
this.keycloakConnect = new Promise(async resolve => {
const config = await this.getConfig();
const client = new KeycloakConnect({ store: sessionStore }, config);
resolve(client);
});
}
public async checkRoles(
roles: string[],
request: Request,
response: Response,
) {
const client = await this.keycloakConnect;
const grant = await client.getGrant(request, response ?? ({} as any));
return roles.every(role => grant.access_token?.hasRealmRole(role));
}
public async middleware(): Promise<RequestHandler[]> {
const client = await this.keycloakConnect;
return client.middleware();
}
}
+11 -10
View File
@@ -1,7 +1,7 @@
import express from "express";
import {
type Member,
type MemberCreateArgs,
type MemberDto,
mapDbMemberToMember,
} from "../dtos/member.js";
import { inject, Injectable } from "../infrastructure/di/index.js";
@@ -12,7 +12,7 @@ import { DatabaseService } from "./db/prisma.js";
export class MembersService {
private readonly database = inject(DatabaseService);
public async getMembers(request: express.Request): Promise<Member[]> {
public async getMembers(request: express.Request): Promise<MemberDto[]> {
const dbMembers = await this.database.doRequest(
async prisma => await prisma.member.findMany(),
request,
@@ -23,7 +23,7 @@ export class MembersService {
public async getMemberById(
memberId: string,
request: express.Request,
): Promise<Member> {
): Promise<MemberDto> {
const member = await this.database.doRequest(
async prisma =>
await prisma.member.findFirst({ where: { id: memberId } }),
@@ -42,11 +42,11 @@ export class MembersService {
public async createMember(
createArgs: MemberCreateArgs,
request: express.Request,
): Promise<Member> {
const x: MemberCreateArgs = {
...createArgs,
geburtsdatum: createArgs.geburtsdatum ?? null,
};
): Promise<MemberDto> {
// const x: MemberCreateArgs = {
// ...createArgs,
// geburtsdatum: createArgs.geburtsdatum ?? null,
// };
const newMember = await this.database.doRequest(
async prisma => await prisma.member.create({ data: createArgs }),
request,
@@ -55,15 +55,16 @@ export class MembersService {
}
public async updateMember(
member: Member,
member: MemberDto,
request: express.Request,
): Promise<Member> {
): Promise<MemberDto> {
const id = member.id;
if (!id) {
throw new Error("Member id is required for update", {
cause: ErrorStates.BAD_REQUEST,
});
}
const modifiedMember = await this.database.doRequest(
async prisma =>
await prisma.member.update({
+2 -1
View File
@@ -56,5 +56,6 @@
"alwaysStrict": true,
"forceConsistentCasingInFileNames": true
},
"files": ["./src/index.ts"]
"files": ["./src/index.ts"],
"include": ["src"]
}
+4 -3
View File
@@ -7,7 +7,8 @@
"build": "ng build",
"watch": "ng build --watch --configuration development",
"test": "ng test",
"generate": "oazapfts --optimistic --useEnumType ./src/app/generated-api/swagger.json ./src/app/generated-api/api.ts"
"generate": "oazapfts --optimistic --useEnumType ./src/app/generated-api/swagger.json ./src/app/generated-api/api.ts",
"dev": "pnpm -w run dev"
},
"prettier": {
"printWidth": 100,
@@ -29,7 +30,7 @@
"@angular/core": "^20.3.15",
"@angular/forms": "^20.3.15",
"@angular/material": "20.2.11",
"@angular/material-date-fns-adapter": "^21.0.2",
"@angular/material-date-fns-adapter": "^20.2.14",
"@angular/platform-browser": "^20.3.15",
"@angular/router": "^20.3.15",
"@date-fns/tz": "^1.4.1",
@@ -57,4 +58,4 @@
"oazapfts": "^6.3.1",
"typescript": "~5.9.3"
}
}
}
+9 -2
View File
@@ -1,6 +1,8 @@
import {
ApplicationConfig,
isDevMode,
LOCALE_ID,
provideAppInitializer,
provideBrowserGlobalErrorListeners,
provideZoneChangeDetection,
} from '@angular/core';
@@ -33,9 +35,11 @@ import {
import { MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field';
import { de } from 'date-fns/locale';
import { routes } from './app.routes';
import { initializeApi } from './util/api';
export const appConfig: ApplicationConfig = {
providers: [
provideAppInitializer(initializeApi),
provideRouter(
routes,
withPreloading(PreloadAllModules),
@@ -45,7 +49,7 @@ export const appConfig: ApplicationConfig = {
provideZoneChangeDetection({ eventCoalescing: true }),
provideKeycloak({
config: {
url: true
url: isDevMode()
? 'http://localhost:8080/'
: 'https://keycloak.toni714.de:8443/',
realm: 'taekwondo',
@@ -55,6 +59,9 @@ export const appConfig: ApplicationConfig = {
onLoad: 'check-sso',
silentCheckSsoRedirectUri:
window.location.origin + '/silent-check-sso.html',
pkceMethod: 'S256',
flow: 'standard',
locale: 'de',
},
features: [withAutoRefreshToken({ sessionTimeout: 600000 })],
providers: [AutoRefreshTokenService, UserActivityService],
@@ -63,7 +70,7 @@ export const appConfig: ApplicationConfig = {
provide: INCLUDE_BEARER_TOKEN_INTERCEPTOR_CONFIG,
useValue: [
createInterceptorCondition<IncludeBearerTokenCondition>({
urlPattern: true
urlPattern: isDevMode()
? /^http:\/\/localhost:(3000|8080)\/.*$/
: /^https:\/\/tkd-api\.toni714\.de\/.*$/,
}),
@@ -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;
}
}
@@ -1,20 +1,28 @@
import { Inject, Pipe, PipeTransform } from '@angular/core';
import { inject, Pipe, PipeTransform } from '@angular/core';
import { MAT_DATE_LOCALE } from '@angular/material/core';
import * as tz from '@date-fns/tz';
import { format, Locale } from 'date-fns';
@Pipe({
name: 'datePipe',
standalone: true
name: 'datePipe',
standalone: true,
})
export class DatePipePipe implements PipeTransform {
constructor(@Inject(MAT_DATE_LOCALE) private locale: Locale){}
private readonly locale = inject<Locale>(MAT_DATE_LOCALE);
transform(value?: string): string {
if(!value) return "-";
const date=new tz.TZDate(value!, "UTC");
const timeZone=Intl.DateTimeFormat().resolvedOptions().timeZone;
const zonedDate=date.withTimeZone(timeZone);
return format(zonedDate, 'P', { locale: this.locale });
}
public transform(value?: string): string {
if (!value) return '-';
const zonedDate = dateToUTC(value);
return format(zonedDate, 'P', { locale: this.locale });
}
}
export function dateToLocal(date: Date): Date {
return new Date(date.getTime() - date.getTimezoneOffset() * 60000);
}
export function dateToUTC(value: string): Date {
const date = new tz.TZDate(value!, 'UTC');
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
return date.withTimeZone(timeZone);
}
@@ -116,6 +116,7 @@ export class Authentication {
return await this.keycloak.login({
redirectUri: redirectUri,
locale: 'de-DE',
//TODO loginHint:''
});
}
+10
View File
@@ -0,0 +1,10 @@
import { isDevMode } from '@angular/core';
import * as api from '../generated-api/api';
export function initializeApi() {
if (isDevMode()) {
// override api url in dev mode
api.defaults.baseUrl = 'http://localhost:3000';
}
}
+31 -24
View File
@@ -2,10 +2,8 @@ import { Inject, Injectable } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
import { MAT_DATE_LOCALE } from '@angular/material/core';
import { Locale, parse } from 'date-fns';
import {
Member,
PickMemberExcludeKeyofMemberIdOrLoeschenam,
} from '../generated-api/api';
import { MemberCreateArgs, MemberDto } from '../generated-api/api';
import { dateToLocal, dateToUTC } from '../pipes/date-pipe.pipe';
interface MemberForm {
id: FormControl<string | null | undefined>;
@@ -15,12 +13,12 @@ interface MemberForm {
geburtsdatum: FormControl<Date | null>;
geburtsort: FormControl<string | null | undefined>;
passnummer: FormControl<string | null | undefined>;
uvertrag: FormControl<boolean | null | undefined>;
verein: FormControl<boolean | null | undefined>;
kontakt: FormControl<string | null | undefined>;
graduierung: FormControl<string | null | undefined>;
kuendigungzum: FormControl<string | null | undefined>;
letztePruefung: FormControl<string | null | undefined>;
kuendigungzum: FormControl<Date | null | undefined>;
letztePruefung: FormControl<Date | null | undefined>;
marker: FormControl<boolean | null | undefined>;
}
@Injectable({
@@ -28,7 +26,7 @@ interface MemberForm {
})
export class MemberDtoHelper {
constructor(@Inject(MAT_DATE_LOCALE) private locale: Locale) {}
public newMemberForm(member?: Member): FormGroup<MemberForm> {
public newMemberForm(member?: MemberDto): FormGroup<MemberForm> {
const geburtsdatum = member?.geburtsdatum;
let enDate: Date | null = null;
if (geburtsdatum) {
@@ -38,62 +36,71 @@ export class MemberDtoHelper {
});
}
return new FormGroup({
return new FormGroup<MemberForm>({
id: new FormControl(member?.id),
vorname: new FormControl(member?.vorname),
nachname: new FormControl(member?.nachname),
geschlecht: new FormControl(member?.geschlecht),
geburtsdatum: new FormControl(enDate),
geburtsdatum: new FormControl(
member?.geburtsdatum ? dateToUTC(member?.geburtsdatum) : null,
),
geburtsort: new FormControl(member?.geburtsort),
passnummer: new FormControl(member?.passnummer),
uvertrag: new FormControl(member?.uvertrag),
verein: new FormControl(member?.verein),
kontakt: new FormControl(member?.kontakt),
graduierung: new FormControl(member?.graduierung),
kuendigungzum: new FormControl(member?.kuendigungzum),
letztePruefung: new FormControl(member?.letztePruefung),
kuendigungzum: new FormControl(
member?.kuendigungzum ? dateToUTC(member?.kuendigungzum) : null,
),
letztePruefung: new FormControl(
member?.letztePruefung
? dateToUTC(member?.letztePruefung)
: null,
),
marker: new FormControl(false),
});
}
memberFromForm(form: FormGroup<MemberForm>): Member {
memberFromForm(form: FormGroup<MemberForm>): MemberDto {
return {
id: form.controls.id.value ?? null,
vorname: form.controls.vorname.value ?? null,
nachname: form.controls.nachname.value ?? null,
geschlecht: form.controls.geschlecht.value ?? null,
geburtsdatum: form.controls.geburtsdatum.value
? JSON.stringify(form.controls.geburtsdatum.value)
? dateToLocal(form.controls.geburtsdatum.value).toISOString()
: null,
geburtsort: form.controls.geburtsort.value ?? null,
passnummer: form.controls.passnummer.value ?? null,
uvertrag: form.controls.uvertrag.value ?? null,
verein: form.controls.verein.value ?? null,
kontakt: form.controls.kontakt.value ?? null,
graduierung: form.controls.graduierung?.value ?? null,
kuendigungzum: form.controls.kuendigungzum.value ?? null, //TODO not for new member
letztePruefung: form.controls.letztePruefung?.value ?? null, //TODO not for new member
loeschenam: null, //TODO? not client settable
kuendigungzum: form.controls.kuendigungzum.value
? dateToLocal(form.controls.kuendigungzum.value).toISOString()
: null,
letztePruefung: form.controls.letztePruefung.value
? dateToLocal(form.controls.letztePruefung.value).toISOString()
: null,
marker: form.controls.marker.value ?? false,
};
}
newMemberFromForm(
form: FormGroup<MemberForm>,
): PickMemberExcludeKeyofMemberIdOrLoeschenam {
newMemberFromForm(form: FormGroup<MemberForm>): MemberCreateArgs {
return {
vorname: form.controls.vorname.value ?? null,
nachname: form.controls.nachname.value ?? null,
geschlecht: form.controls.geschlecht.value ?? null,
geburtsdatum: form.controls.geburtsdatum.value
? JSON.stringify(form.controls.geburtsdatum.value)
? dateToLocal(form.controls.geburtsdatum.value).toISOString()
: null,
geburtsort: form.controls.geburtsort.value ?? null,
passnummer: form.controls.passnummer.value ?? null,
uvertrag: form.controls.uvertrag.value ?? null,
verein: form.controls.verein.value ?? null,
kontakt: form.controls.kontakt.value ?? null,
graduierung: form.controls.graduierung?.value ?? null,
kuendigungzum: null, //TODO not for new member
letztePruefung: null, //TODO not for new member
marker: form.controls.marker.value ?? false,
};
}
}