Intermediate: Merge pull request '#25-cleanup-&-enhancements' (#26) from #25-cleanup-&-enhancements into main

Reviewed-on: Ehrenamt/member-administration-server#26
This commit is contained in:
Julian Krauser 2025-01-02 17:37:18 +00:00
commit 9afb205da5
31 changed files with 188 additions and 50 deletions

View file

@ -17,3 +17,4 @@ MAIL_PORT = mail_portnumber
MAIL_SECURE (true|false) // true for port 465, fals for other ports
CLUB_NAME = clubname
CLUB_WEBSITE = https://my-club-website-url

View file

@ -37,6 +37,7 @@ services:
- MAIL_PORT=<port>
- MAIL_SECURE=<boolean>
- CLUB_NAME=<tobemodified>
- CLUB_WEBSITE=<tobemodified>
volumes:
- <volume|local path>:/app/export
networks:

View file

@ -1,7 +1,7 @@
import { dataSource } from "../data-source";
import { award } from "../entity/award";
import { member } from "../entity/member";
import { memberAwards } from "../entity/memberAwards";
import { user } from "../entity/user";
import InternalException from "../exceptions/internalException";
import { CreateMemberAwardCommand, DeleteMemberAwardCommand, UpdateMemberAwardCommand } from "./memberAwardCommand";
@ -21,8 +21,8 @@ export default abstract class MemberAwardCommandHandler {
note: createMemberAward.note,
date: createMemberAward.date,
member: await dataSource
.getRepository(user)
.createQueryBuilder("user")
.getRepository(member)
.createQueryBuilder("member")
.where("id = :id", { id: createMemberAward.memberId })
.getOneOrFail(),
award: await dataSource

View file

@ -6,6 +6,7 @@ export interface CreateMemberCommand {
lastname: string;
nameaffix: string;
birthdate: Date;
internalId?: string;
}
export interface UpdateMemberCommand {
@ -15,6 +16,7 @@ export interface UpdateMemberCommand {
lastname: string;
nameaffix: string;
birthdate: Date;
internalId?: string;
}
export interface UpdateMemberNewsletterCommand {

View file

@ -27,6 +27,7 @@ export default abstract class MemberCommandHandler {
lastname: createMember.lastname,
nameaffix: createMember.nameaffix,
birthdate: createMember.birthdate,
internalId: createMember.internalId,
})
.execute()
.then((result) => {
@ -53,6 +54,7 @@ export default abstract class MemberCommandHandler {
lastname: updateMember.lastname,
nameaffix: updateMember.nameaffix,
birthdate: updateMember.birthdate,
internalId: updateMember.internalId,
})
.where("id = :id", { id: updateMember.id })
.execute()

View file

@ -1,7 +1,7 @@
import { dataSource } from "../data-source";
import { executivePosition } from "../entity/executivePosition";
import { member } from "../entity/member";
import { memberExecutivePositions } from "../entity/memberExecutivePositions";
import { user } from "../entity/user";
import InternalException from "../exceptions/internalException";
import {
CreateMemberExecutivePositionCommand,
@ -25,8 +25,8 @@ export default abstract class MemberExecutivePositionCommandHandler {
start: createMemberExecutivePosition.start,
end: createMemberExecutivePosition.end,
member: await dataSource
.getRepository(user)
.createQueryBuilder("user")
.getRepository(member)
.createQueryBuilder("member")
.where("id = :id", { id: createMemberExecutivePosition.memberId })
.getOneOrFail(),
executivePosition: await dataSource

View file

@ -1,13 +1,13 @@
import { dataSource } from "../data-source";
import { qualification } from "../entity/qualification";
import { memberQualifications } from "../entity/memberQualifications";
import { user } from "../entity/user";
import InternalException from "../exceptions/internalException";
import {
CreateMemberQualificationCommand,
DeleteMemberQualificationCommand,
UpdateMemberQualificationCommand,
} from "./memberQualificationCommand";
import { member } from "../entity/member";
export default abstract class MemberQualificationCommandHandler {
/**
@ -24,8 +24,8 @@ export default abstract class MemberQualificationCommandHandler {
note: createMemberQualification.note,
start: createMemberQualification.start,
member: await dataSource
.getRepository(user)
.createQueryBuilder("user")
.getRepository(member)
.createQueryBuilder("member")
.where("id = :id", { id: createMemberQualification.memberId })
.getOneOrFail(),
qualification: await dataSource

View file

@ -1,5 +1,4 @@
export interface CreateMembershipCommand {
internalId?: string;
start: Date;
memberId: number;
statusId: number;
@ -7,7 +6,6 @@ export interface CreateMembershipCommand {
export interface UpdateMembershipCommand {
id: number;
internalId?: string;
start: Date;
end?: Date;
terminationReason?: string;

View file

@ -1,18 +1,18 @@
import { dataSource } from "../data-source";
import { member } from "../entity/member";
import { membership } from "../entity/membership";
import { membershipStatus } from "../entity/membershipStatus";
import { user } from "../entity/user";
import InternalException from "../exceptions/internalException";
import { CreateMembershipCommand, DeleteMembershipCommand, UpdateMembershipCommand } from "./membershipCommand";
export default abstract class MembershipCommandHandler {
/**
* @description create membership
* @param CreateMembershipCommand
* @param {CreateMembershipCommand} createMembership
* @returns {Promise<number>}
*/
static async create(createMembership: CreateMembershipCommand): Promise<number> {
let insertid = -1;
let insertId = -1;
return await dataSource
.transaction(async (manager) => {
await manager
@ -20,11 +20,10 @@ export default abstract class MembershipCommandHandler {
.insert()
.into(membership)
.values({
internalId: createMembership.internalId,
start: createMembership.start,
member: await dataSource
.getRepository(user)
.createQueryBuilder("user")
.getRepository(member)
.createQueryBuilder("member")
.where("id = :id", { id: createMembership.memberId })
.getOneOrFail(),
status: await dataSource
@ -35,7 +34,7 @@ export default abstract class MembershipCommandHandler {
})
.execute()
.then((result) => {
insertid = result.identifiers[0].id;
insertId = result.identifiers[0].id;
});
await manager
@ -43,14 +42,15 @@ export default abstract class MembershipCommandHandler {
.update(membership)
.set({
end: createMembership.start,
terminationReason: "beendet durch neuen Eintrag.",
})
.where("end IS NULL")
.andWhere("memberId = :memberId", { memberId: createMembership.memberId })
.andWhere("id <> :id", { id: insertid })
.andWhere("id <> :id", { id: insertId })
.execute();
})
.then(() => {
return insertid;
return insertId;
})
.catch((err) => {
throw new InternalException("Failed creating membership", err);
@ -59,7 +59,7 @@ export default abstract class MembershipCommandHandler {
/**
* @description update membership
* @param UpdateMembershipCommand
* @param {UpdateMembershipCommand} updateMembership
* @returns {Promise<void>}
*/
static async update(updateMembership: UpdateMembershipCommand): Promise<void> {
@ -67,7 +67,6 @@ export default abstract class MembershipCommandHandler {
.createQueryBuilder()
.update(membership)
.set({
internalId: updateMembership.internalId,
start: updateMembership.start,
end: updateMembership.end,
terminationReason: updateMembership.terminationReason,
@ -88,16 +87,16 @@ export default abstract class MembershipCommandHandler {
/**
* @description delete membership
* @param DeleteMembershipCommand
* @param {DeleteMembershipCommand} deleteMembership
* @returns {Promise<void>}
*/
static async delete(deletMembership: DeleteMembershipCommand): Promise<void> {
static async delete(deleteMembership: DeleteMembershipCommand): Promise<void> {
return await dataSource
.createQueryBuilder()
.delete()
.from(membership)
.where("id = :id", { id: deletMembership.id })
.andWhere("memberId = :memberId", { memberId: deletMembership.memberId })
.where("id = :id", { id: deleteMembership.id })
.andWhere("memberId = :memberId", { memberId: deleteMembership.memberId })
.execute()
.then(() => {})
.catch((err) => {

View file

@ -3,4 +3,6 @@ export interface UpdateTemplateUsageCommand {
headerId: number | null;
bodyId: number | null;
footerId: number | null;
headerHeight: number | null;
footerHeight: number | null;
}

View file

@ -17,6 +17,8 @@ export default abstract class TemplateUsageCommandHandler {
headerId: updateTemplateUsage.headerId,
bodyId: updateTemplateUsage.bodyId,
footerId: updateTemplateUsage.footerId,
headerHeight: updateTemplateUsage.headerHeight,
footerHeight: updateTemplateUsage.footerHeight,
})
.where("scope = :scope", { scope: updateTemplateUsage.scope })
.execute()

View file

@ -228,6 +228,7 @@ export async function createMember(req: Request, res: Response): Promise<any> {
const lastname = req.body.lastname;
const nameaffix = req.body.nameaffix;
const birthdate = req.body.birthdate;
const internalId = req.body.internalId;
let createMember: CreateMemberCommand = {
salutation,
@ -235,6 +236,7 @@ export async function createMember(req: Request, res: Response): Promise<any> {
lastname,
nameaffix,
birthdate,
internalId,
};
let memberId = await MemberCommandHandler.create(createMember);
@ -249,12 +251,10 @@ export async function createMember(req: Request, res: Response): Promise<any> {
*/
export async function addMembershipToMember(req: Request, res: Response): Promise<any> {
const memberId = parseInt(req.params.memberId);
const internalId = req.body.internalId;
const start = req.body.start;
const statusId = req.body.statusId;
let createMembership: CreateMembershipCommand = {
internalId,
start,
memberId,
statusId,
@ -394,6 +394,7 @@ export async function updateMemberById(req: Request, res: Response): Promise<any
const lastname = req.body.lastname;
const nameaffix = req.body.nameaffix;
const birthdate = req.body.birthdate;
const internalId = req.body.internalId;
let updateMember: UpdateMemberCommand = {
id: memberId,
@ -402,6 +403,7 @@ export async function updateMemberById(req: Request, res: Response): Promise<any
lastname,
nameaffix,
birthdate,
internalId,
};
await MemberCommandHandler.update(updateMember);
@ -417,7 +419,6 @@ export async function updateMemberById(req: Request, res: Response): Promise<any
export async function updateMembershipOfMember(req: Request, res: Response): Promise<any> {
const memberId = parseInt(req.params.memberId);
const recordId = parseInt(req.params.recordId);
const internalId = req.body.internalId;
const start = req.body.start;
const end = req.body.end || null;
const terminationReason = req.body.terminationReason;
@ -425,7 +426,6 @@ export async function updateMembershipOfMember(req: Request, res: Response): Pro
let updateMembership: UpdateMembershipCommand = {
id: recordId,
internalId,
start,
end,
terminationReason,

View file

@ -78,12 +78,16 @@ export async function updateTemplateUsage(req: Request, res: Response): Promise<
const headerId = req.body.headerId ?? null;
const bodyId = req.body.bodyId ?? null;
const footerId = req.body.footerId ?? null;
const headerHeight = req.body.headerHeight ?? null;
const footerHeight = req.body.footerHeight ?? null;
let updateTemplateUsage: UpdateTemplateUsageCommand = {
scope: scope,
headerId: headerId,
bodyId: bodyId,
footerId: footerId,
headerHeight: headerHeight,
footerHeight: footerHeight,
};
await TemplateUsageCommandHandler.update(updateTemplateUsage);

View file

@ -61,6 +61,8 @@ import { newsletterRecipients } from "./entity/newsletterRecipients";
import { Newsletter1735118780511 } from "./migrations/1735118780511-newsletter";
import { newsletterConfig } from "./entity/newsletterConfig";
import { NewsletterConfig1735207446910 } from "./migrations/1735207446910-newsletterConfig";
import { TemplateMargins1735733514043 } from "./migrations/1735733514043-templateMargins";
import { InternalId1735822722235 } from "./migrations/1735822722235-internalId";
const dataSource = new DataSource({
type: DB_TYPE as any,
@ -132,6 +134,8 @@ const dataSource = new DataSource({
TemplateUsage1734949173739,
Newsletter1735118780511,
NewsletterConfig1735207446910,
TemplateMargins1735733514043,
InternalId1735822722235,
],
migrationsRun: true,
migrationsTransactionMode: "each",

View file

@ -39,6 +39,9 @@ export class member {
@Column({ type: "date" })
birthdate: Date;
@Column({ type: "varchar", length: 255, unique: true, nullable: true })
internalId?: string;
@OneToMany(() => communication, (communications) => communications.member)
communications: communication[];

View file

@ -16,6 +16,12 @@ export class memberAwards {
@Column({ type: "date" })
date: Date;
@Column()
memberId: number;
@Column()
awardId: number;
@ManyToOne(() => member, (member) => member.awards, {
nullable: false,
onDelete: "CASCADE",

View file

@ -16,6 +16,12 @@ export class memberExecutivePositions {
@Column({ type: "date", nullable: true })
end?: Date;
@Column()
memberId: number;
@Column()
executivePositionId: number;
@ManyToOne(() => member, (member) => member.awards, {
nullable: false,
onDelete: "CASCADE",

View file

@ -19,6 +19,12 @@ export class memberQualifications {
@Column({ type: "varchar", length: 255, nullable: true })
terminationReason?: string;
@Column()
memberId: number;
@Column()
qualificationId: number;
@ManyToOne(() => member, (member) => member.awards, {
nullable: false,
onDelete: "CASCADE",

View file

@ -7,6 +7,9 @@ export class membership {
@PrimaryColumn({ generated: "increment", type: "int" })
id: number;
/**
* @deprecated
*/
@Column({ type: "varchar", length: 255, unique: true, nullable: true })
internalId?: string;
@ -19,6 +22,12 @@ export class membership {
@Column({ type: "varchar", length: 255, nullable: true })
terminationReason?: string;
@Column()
memberId: number;
@Column()
statusId: number;
@ManyToOne(() => member, (member) => member.memberships, {
nullable: false,
onDelete: "CASCADE",

View file

@ -7,15 +7,21 @@ export class templateUsage {
@PrimaryColumn({ type: "varchar", length: 255 })
scope: PermissionModule;
@Column({ type: "number", nullable: true })
@Column({ type: "int", nullable: true })
headerId: number | null;
@Column({ type: "number", nullable: true })
@Column({ type: "int", nullable: true })
bodyId: number | null;
@Column({ type: "number", nullable: true })
@Column({ type: "int", nullable: true })
footerId: number | null;
@Column({ type: "int", nullable: true })
headerHeight: number | null;
@Column({ type: "int", nullable: true })
footerHeight: number | null;
@ManyToOne(() => template, {
nullable: true,
onDelete: "RESTRICT",

View file

@ -17,6 +17,7 @@ export default abstract class MemberFactory {
lastname: record?.lastname,
nameaffix: record?.nameaffix,
birthdate: record?.birthdate,
internalId: record.internalId,
firstMembershipEntry: record?.firstMembershipEntry
? MembershipFactory.mapToSingle(record.firstMembershipEntry)
: null,

View file

@ -10,7 +10,6 @@ export default abstract class MembershipFactory {
public static mapToSingle(record: membership): MembershipViewModel {
return {
id: record.id,
internalId: record.internalId,
start: record.start,
end: record.end,
terminationReason: record.terminationReason,

View file

@ -13,6 +13,8 @@ export default abstract class TemplateUsageFactory {
header: record.header ? { id: record.header.id, template: record.header.template } : null,
body: record.body ? { id: record.body.id, template: record.body.template } : null,
footer: record.footer ? { id: record.footer.id, template: record.footer.template } : null,
headerHeight: record.headerHeight,
footerHeight: record.footerHeight,
};
}

View file

@ -11,7 +11,6 @@ export abstract class PdfExport {
filename = null,
data = {},
saveToDisk = true,
margins = { top: "15mm", bottom: "15mm" },
folder = "",
}: {
template: PermissionModule;
@ -19,12 +18,11 @@ export abstract class PdfExport {
filename?: string;
data?: any;
saveToDisk?: boolean;
margins?: { top: string; bottom: string };
folder?: string;
}) {
if (folder != "") FileSystemHelper.createFolder(folder);
const { header, footer, body } = await TemplateHelper.renderFileForModule({
const { header, footer, body, headerMargin, footerMargin } = await TemplateHelper.renderFileForModule({
module: template,
headerData: data,
bodyData: data,
@ -46,8 +44,8 @@ export abstract class PdfExport {
format: "A4",
printBackground: false,
margin: {
top: margins.top,
bottom: margins.bottom,
top: (headerMargin ?? 15) + "mm",
bottom: (footerMargin ?? 15) + "mm",
left: "10mm",
right: "10mm",
},

View file

@ -1,7 +1,7 @@
import TemplateService from "../service/templateService";
import { PermissionModule } from "../type/permissionTypes";
import TemplateUsageService from "../service/templateUsageService";
import Handlebars from "handlebars";
import Handlebars, { template } from "handlebars";
import { FileSystemHelper } from "./fileSystemHelper";
export abstract class TemplateHelper {
@ -39,27 +39,27 @@ export abstract class TemplateHelper {
headerData?: any;
bodyData?: any;
footerData?: any;
}): Promise<{ header: string; body: string; footer: string; margins?: { top: string; bottom: string } }> {
const moduleTemplates = await TemplateUsageService.getByScope(module);
}): Promise<{ header: string; body: string; footer: string; headerMargin?: number; footerMargin?: number }> {
const moduleTemplate = await TemplateUsageService.getByScope(module);
let header = `<h1 style="font-size:10px; text-align:center; width:100%;">${title}</h1>`;
let footer = "";
let body = "";
if (moduleTemplates.headerId) {
header = await this.getTemplateFromStore(moduleTemplates.headerId);
if (moduleTemplate.headerId) {
header = await this.getTemplateFromStore(moduleTemplate.headerId);
header = this.applyDataToTemplate(header, { title, ...headerData });
}
if (moduleTemplates.footerId) {
footer = await this.getTemplateFromStore(moduleTemplates.footerId);
if (moduleTemplate.footerId) {
footer = await this.getTemplateFromStore(moduleTemplate.footerId);
} else {
footer = this.getTemplateFromFile(module + ".footer");
}
footer = this.applyDataToTemplate(footer, footerData);
if (moduleTemplates.bodyId) {
body = await this.getTemplateFromStore(moduleTemplates.bodyId);
if (moduleTemplate.bodyId) {
body = await this.getTemplateFromStore(moduleTemplate.bodyId);
} else {
body = this.getTemplateFromFile(module + ".body");
}
@ -69,6 +69,8 @@ export abstract class TemplateHelper {
header,
footer,
body,
headerMargin: moduleTemplate.headerHeight,
footerMargin: moduleTemplate.footerHeight,
};
}
}

View file

@ -0,0 +1,35 @@
import { MigrationInterface, QueryRunner, TableColumn } from "typeorm";
import { DB_TYPE } from "../env.defaults";
export class TemplateMargins1735733514043 implements MigrationInterface {
name = "TemplateMargins1735733514043";
public async up(queryRunner: QueryRunner): Promise<void> {
const variableType_int = DB_TYPE == "mysql" ? "int" : "integer";
await queryRunner.addColumn(
"template_usage",
new TableColumn({
name: "headerHeight",
type: variableType_int,
default: null,
isNullable: true,
})
);
await queryRunner.addColumn(
"template_usage",
new TableColumn({
name: "footerHeight",
type: variableType_int,
default: null,
isNullable: true,
})
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropColumn("template_usage", "footerHeight");
await queryRunner.dropColumn("template_usage", "headerHeight");
}
}

View file

@ -0,0 +1,47 @@
import { MigrationInterface, QueryRunner, TableColumn } from "typeorm";
import { membership } from "../entity/membership";
import { member } from "../entity/member";
export class InternalId1735822722235 implements MigrationInterface {
name = "InternalId1735822722235";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.addColumn(
"member",
new TableColumn({
name: "internalId",
type: "varchar",
length: "255",
default: null,
isNullable: true,
isUnique: true,
})
);
let memberships = await queryRunner.manager.getRepository(membership).find();
console.log(memberships);
let internalIds = memberships.reduce<{ [key: number]: Array<string> }>((acc, cur) => {
let memberId = cur.memberId;
let setIds = acc[memberId] ?? [];
if (cur?.internalId) {
setIds.push(cur.internalId);
}
acc[memberId] = setIds;
return acc;
}, {});
console.log(internalIds);
for (const [id, value] of Object.entries(internalIds)) {
const ids = value.filter((v) => v != null).join(", ");
if (ids) {
let m = await queryRunner.manager.getRepository(member).findOneByOrFail({ id: parseInt(id) });
m.internalId = ids;
await queryRunner.manager.getRepository(member).save(m);
}
}
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropColumn("member", "internalId");
}
}

View file

@ -14,6 +14,7 @@ export default abstract class MembershipService {
.createQueryBuilder("membership")
.leftJoinAndSelect("membership.status", "membershipStatus")
.where("membership.memberId = :memberId", { memberId: memberId })
.orderBy("membership.start", "DESC")
.getMany()
.then((res) => {
return res;

View file

@ -9,6 +9,7 @@ export interface MemberViewModel {
lastname: string;
nameaffix: string;
birthdate: Date;
internalId?: string;
firstMembershipEntry?: MembershipViewModel;
lastMembershipEntry?: MembershipViewModel;
sendNewsletter?: CommunicationViewModel;

View file

@ -1,6 +1,5 @@
export interface MembershipViewModel {
id: number;
internalId?: string;
start: Date;
end?: Date;
terminationReason?: string;

View file

@ -5,4 +5,6 @@ export interface TemplateUsageViewModel {
header: { id: number; template: string } | null;
body: { id: number; template: string } | null;
footer: { id: number; template: string } | null;
headerHeight: number | null;
footerHeight: number | null;
}