Merge pull request 'minor v1.6.0' (#114) from develop into main

Reviewed-on: #114
This commit is contained in:
Julian Krauser 2025-06-06 07:35:59 +00:00
commit e4365135a1
53 changed files with 1335 additions and 74 deletions

View file

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

View file

@ -23,6 +23,7 @@ export default abstract class MemberCommandHandler {
nameaffix: createMember.nameaffix,
birthdate: createMember.birthdate,
internalId: createMember.internalId,
note: createMember.note,
})
.execute()
.then((result) => {
@ -49,6 +50,7 @@ export default abstract class MemberCommandHandler {
nameaffix: updateMember.nameaffix,
birthdate: updateMember.birthdate,
internalId: updateMember.internalId,
note: updateMember.note,
})
.where("id = :id", { id: updateMember.id })
.execute()

View file

@ -0,0 +1,23 @@
export interface CreateMemberEducationCommand {
start: Date;
end?: Date;
note?: string;
place?: string;
memberId: string;
educationId: number;
}
export interface UpdateMemberEducationCommand {
id: number;
start: Date;
end?: Date;
note?: string;
place?: string;
memberId: string;
educationId: number;
}
export interface DeleteMemberEducationCommand {
id: number;
memberId: string;
}

View file

@ -0,0 +1,82 @@
import { dataSource } from "../../../data-source";
import { memberEducations } from "../../../entity/club/member/memberEducations";
import DatabaseActionException from "../../../exceptions/databaseActionException";
import InternalException from "../../../exceptions/internalException";
import {
CreateMemberEducationCommand,
DeleteMemberEducationCommand,
UpdateMemberEducationCommand,
} from "./memberEducationCommand";
export default abstract class MemberEducationCommandHandler {
/**
* @description create memberEducation
* @param {CreateMemberEducationCommand} createMemberEducation
* @returns {Promise<number>}
*/
static async create(createMemberEducation: CreateMemberEducationCommand): Promise<number> {
return await dataSource
.createQueryBuilder()
.insert()
.into(memberEducations)
.values({
note: createMemberEducation.note,
place: createMemberEducation.place,
start: createMemberEducation.start,
end: createMemberEducation.end,
memberId: createMemberEducation.memberId,
educationId: createMemberEducation.educationId,
})
.execute()
.then((result) => {
return result.identifiers[0].id;
})
.catch((err) => {
throw new DatabaseActionException("CREATE", "memberEducation", err);
});
}
/**
* @description update memberEducation
* @param {UpdateMemberEducationCommand} updateMemberEducation
* @returns {Promise<void>}
*/
static async update(updateMemberEducation: UpdateMemberEducationCommand): Promise<void> {
return await dataSource
.createQueryBuilder()
.update(memberEducations)
.set({
note: updateMemberEducation.note,
start: updateMemberEducation.start,
end: updateMemberEducation.end,
place: updateMemberEducation.place,
educationId: updateMemberEducation.educationId,
})
.where("id = :id", { id: updateMemberEducation.id })
.andWhere("memberId = :memberId", { memberId: updateMemberEducation.memberId })
.execute()
.then(() => {})
.catch((err) => {
throw new DatabaseActionException("UPDATE", "memberEducation", err);
});
}
/**
* @description delete memberEducation
* @param {DeleteMemberEducationCommand} deleteMemberEducation
* @returns {Promise<void>}
*/
static async delete(deleteMemberEducation: DeleteMemberEducationCommand): Promise<void> {
return await dataSource
.createQueryBuilder()
.delete()
.from(memberEducations)
.where("id = :id", { id: deleteMemberEducation.id })
.andWhere("memberId = :memberId", { memberId: deleteMemberEducation.memberId })
.execute()
.then(() => {})
.catch((err) => {
throw new DatabaseActionException("DELETE", "memberEducation", err);
});
}
}

View file

@ -34,18 +34,32 @@ export default abstract class ProtocolAgendaCommandHandler {
/**
* @description sync protocolAgenda
* @param {number} protocolId
* @param {Array<SynchronizeProtocolAgendaCommand>} syncProtocolAgenda
* @returns {Promise<void>}
*/
static async sync(syncProtocolAgenda: Array<SynchronizeProtocolAgendaCommand>): Promise<void> {
static async sync(protocolId: number, syncProtocolAgenda: Array<SynchronizeProtocolAgendaCommand>): Promise<void> {
let currentAgenda = await ProtocolAgendaService.getAll(protocolId);
return await dataSource
.transaction(async (transactionalEntityManager) => {
let removed = currentAgenda.filter((ca) => !syncProtocolAgenda.some((spa) => spa.id == ca.id));
for (const agenda of syncProtocolAgenda) {
await transactionalEntityManager
.createQueryBuilder()
.update(protocolAgenda)
.set(agenda)
.where({ id: agenda.id })
.where({ id: agenda.id, protocolId })
.execute();
}
if (removed.length != 0) {
await transactionalEntityManager
.createQueryBuilder()
.delete()
.from(protocolAgenda)
.where("id IN (:...ids)", { ids: removed.map((m) => m.id) })
.andWhere({ protocolId })
.execute();
}
})

View file

@ -33,12 +33,19 @@ export default abstract class ProtocolDecisionCommandHandler {
}
/**
* @description sync protocolDecision
* @param {number} protocolId
* @param {Array<SynchronizeProtocolDecisionCommand>} syncProtocolDecisions
* @returns {Promise<void>}
*/
static async sync(syncProtocolDecisions: Array<SynchronizeProtocolDecisionCommand>): Promise<void> {
static async sync(
protocolId: number,
syncProtocolDecisions: Array<SynchronizeProtocolDecisionCommand>
): Promise<void> {
let currentDecision = await ProtocolDecisionService.getAll(protocolId);
return await dataSource
.transaction(async (transactionalEntityManager) => {
let removed = currentDecision.filter((ca) => !syncProtocolDecisions.some((spa) => spa.id == ca.id));
for (const decision of syncProtocolDecisions) {
await transactionalEntityManager
.createQueryBuilder()
@ -47,8 +54,17 @@ export default abstract class ProtocolDecisionCommandHandler {
.where({ id: decision.id })
.execute();
}
if (removed.length != 0) {
await transactionalEntityManager
.createQueryBuilder()
.delete()
.from(protocolDecision)
.where("id IN (:...ids)", { ids: removed.map((m) => m.id) })
.andWhere({ protocolId })
.execute();
}
})
.then(() => {})
.catch((err) => {
throw new DatabaseActionException("SYNC", "protocolDecision", err);
});

View file

@ -33,12 +33,16 @@ export default abstract class ProtocolVotingCommandHandler {
}
/**
* @description sync protocolVoting
* @param {number} protocolId
* @param {Array<SynchronizeProtocolVotingCommand>} syncProtocolVotings
* @returns {Promise<void>}
*/
static async sync(syncProtocolVotings: Array<SynchronizeProtocolVotingCommand>): Promise<void> {
static async sync(protocolId: number, syncProtocolVotings: Array<SynchronizeProtocolVotingCommand>): Promise<void> {
let currentVoting = await ProtocolVotingService.getAll(protocolId);
return await dataSource
.transaction(async (transactionalEntityManager) => {
let removed = currentVoting.filter((ca) => !syncProtocolVotings.some((spa) => spa.id == ca.id));
for (const voting of syncProtocolVotings) {
await transactionalEntityManager
.createQueryBuilder()
@ -47,8 +51,17 @@ export default abstract class ProtocolVotingCommandHandler {
.where({ id: voting.id })
.execute();
}
if (removed.length != 0) {
await transactionalEntityManager
.createQueryBuilder()
.delete()
.from(protocolVoting)
.where("id IN (:...ids)", { ids: removed.map((m) => m.id) })
.andWhere({ protocolId })
.execute();
}
})
.then(() => {})
.catch((err) => {
throw new DatabaseActionException("SYNC", "protocolVoting", err);
});

View file

@ -0,0 +1,14 @@
export interface CreateEducationCommand {
education: string;
description?: string;
}
export interface UpdateEducationCommand {
id: number;
education: string;
description?: string;
}
export interface DeleteEducationCommand {
id: number;
}

View file

@ -0,0 +1,68 @@
import { dataSource } from "../../../data-source";
import { education } from "../../../entity/configuration/education";
import DatabaseActionException from "../../../exceptions/databaseActionException";
import { CreateEducationCommand, DeleteEducationCommand, UpdateEducationCommand } from "./educationCommand";
export default abstract class EducationCommandHandler {
/**
* @description create education
* @param {CreateEducationCommand} createEducation
* @returns {Promise<number>}
*/
static async create(createEducation: CreateEducationCommand): Promise<number> {
return await dataSource
.createQueryBuilder()
.insert()
.into(education)
.values({
education: createEducation.education,
description: createEducation.description,
})
.execute()
.then((result) => {
return result.identifiers[0].id;
})
.catch((err) => {
throw new DatabaseActionException("CREATE", "education", err);
});
}
/**
* @description update education
* @param {UpdateEducationCommand} updateEducation
* @returns {Promise<void>}
*/
static async update(updateEducation: UpdateEducationCommand): Promise<void> {
return await dataSource
.createQueryBuilder()
.update(education)
.set({
education: updateEducation.education,
description: updateEducation.description,
})
.where("id = :id", { id: updateEducation.id })
.execute()
.then(() => {})
.catch((err) => {
throw new DatabaseActionException("UPDATE", "education", err);
});
}
/**
* @description delete education
* @param {DeleteEducationCommand} deleteEducation
* @returns {Promise<void>}
*/
static async delete(deleteEducation: DeleteEducationCommand): Promise<void> {
return await dataSource
.createQueryBuilder()
.delete()
.from(education)
.where("id = :id", { id: deleteEducation.id })
.execute()
.then(() => {})
.catch((err) => {
throw new DatabaseActionException("DELETE", "education", err);
});
}
}

View file

@ -49,6 +49,14 @@ import {
import CommunicationCommandHandler from "../../../command/club/member/communicationCommandHandler";
import { PdfExport } from "../../../helpers/pdfExport";
import { PermissionModule } from "../../../type/permissionTypes";
import MemberEducationFactory from "../../../factory/admin/club/member/memberEducation";
import MemberEducationService from "../../../service/club/member/memberEducationService";
import {
CreateMemberEducationCommand,
DeleteMemberEducationCommand,
UpdateMemberEducationCommand,
} from "../../../command/club/member/memberEducationCommand";
import MemberEducationCommandHandler from "../../../command/club/member/memberEducationCommandHandler";
/**
* @description get all members
@ -144,6 +152,7 @@ export async function getMemberPrintoutById(req: Request, res: Response): Promis
let qualifications = await MemberQualificationService.getAll(memberId);
let positions = await MemberExecutivePositionService.getAll(memberId);
let communications = await CommunicationService.getAll(memberId);
let educations = await MemberEducationService.getAll(memberId);
let pdf = await PdfExport.renderFile({
title: "Mitglieder-Ausdruck",
@ -157,6 +166,7 @@ export async function getMemberPrintoutById(req: Request, res: Response): Promis
qualifications,
positions,
communications,
educations,
},
});
@ -195,6 +205,19 @@ export async function getMembershipStatisticsById(req: Request, res: Response):
res.json(MembershipFactory.mapToBaseStatistics(member));
}
/**
* @description get member total statistics by id
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function getMembershipTotalStatisticsById(req: Request, res: Response): Promise<any> {
const memberId = req.params.memberId;
let member = await MembershipService.getTotalStatisticsById(memberId);
res.json(MembershipFactory.mapToSingleTotalStatistic(member));
}
/**
* @description get membership by member and record
* @param req {Request} Express req object
@ -263,6 +286,33 @@ export async function getQualificationByMemberAndRecord(req: Request, res: Respo
res.json(MemberQualificationFactory.mapToSingle(qualification));
}
/**
* @description get educations by member
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function getEducationsByMember(req: Request, res: Response): Promise<any> {
const memberId = req.params.memberId;
let educations = await MemberEducationService.getAll(memberId);
res.json(MemberEducationFactory.mapToBase(educations));
}
/**
* @description get education by member and record
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function getEducationByMemberAndRecord(req: Request, res: Response): Promise<any> {
const memberId = req.params.memberId;
const recordId = parseInt(req.params.id);
let education = await MemberEducationService.getById(memberId, recordId);
res.json(MemberEducationFactory.mapToSingle(education));
}
/**
* @description get executive positions by member
* @param req {Request} Express req object
@ -330,6 +380,7 @@ export async function createMember(req: Request, res: Response): Promise<any> {
const nameaffix = req.body.nameaffix;
const birthdate = req.body.birthdate;
const internalId = req.body.internalId || null;
const note = req.body.note || null;
let createMember: CreateMemberCommand = {
salutationId,
@ -338,6 +389,7 @@ export async function createMember(req: Request, res: Response): Promise<any> {
nameaffix,
birthdate,
internalId,
note,
};
let memberId = await MemberCommandHandler.create(createMember);
@ -413,6 +465,33 @@ export async function addQualificationToMember(req: Request, res: Response): Pro
res.sendStatus(204);
}
/**
* @description add education to member
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function addEducationToMember(req: Request, res: Response): Promise<any> {
const memberId = req.params.memberId;
const note = req.body.note;
const place = req.body.place;
const start = req.body.start;
const end = req.body.end || null;
const educationId = req.body.educationId;
let createMemberEducation: CreateMemberEducationCommand = {
note,
start,
end,
place,
memberId,
educationId,
};
await MemberEducationCommandHandler.create(createMemberEducation);
res.sendStatus(204);
}
/**
* @description add executive positions to member
* @param req {Request} Express req object
@ -491,6 +570,7 @@ export async function updateMemberById(req: Request, res: Response): Promise<any
const nameaffix = req.body.nameaffix;
const birthdate = req.body.birthdate;
const internalId = req.body.internalId || null;
const note = req.body.note || null;
let updateMember: UpdateMemberCommand = {
id: memberId,
@ -500,6 +580,7 @@ export async function updateMemberById(req: Request, res: Response): Promise<any
nameaffix,
birthdate,
internalId,
note,
};
await MemberCommandHandler.update(updateMember);
@ -589,6 +670,35 @@ export async function updateQualificationOfMember(req: Request, res: Response):
res.sendStatus(204);
}
/**
* @description update education of member
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function updateEducationOfMember(req: Request, res: Response): Promise<any> {
const memberId = req.params.memberId;
const recordId = parseInt(req.params.recordId);
const start = req.body.start;
const end = req.body.end || null;
const note = req.body.note;
const place = req.body.place;
const educationId = req.body.educationId;
let updateMemberEducation: UpdateMemberEducationCommand = {
id: recordId,
start,
end,
note,
place,
memberId,
educationId,
};
await MemberEducationCommandHandler.update(updateMemberEducation);
res.sendStatus(204);
}
/**
* @description update executive position of member
* @param req {Request} Express req object
@ -729,6 +839,25 @@ export async function deleteQualificationOfMember(req: Request, res: Response):
res.sendStatus(204);
}
/**
* @description delete education from member
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function deleteEducationsOfMember(req: Request, res: Response): Promise<any> {
const memberId = req.params.memberId;
const recordId = parseInt(req.params.recordId);
let deleteMemberEducation: DeleteMemberEducationCommand = {
id: recordId,
memberId,
};
await MemberEducationCommandHandler.delete(deleteMemberEducation);
res.sendStatus(204);
}
/**
* @description delete executive position from member
* @param req {Request} Express req object

View file

@ -249,12 +249,7 @@ export async function createProtocolPrintoutById(req: Request, res: Response): P
title: protocol.title,
summary: protocol.summary,
iteration: iteration + 1,
date: new Date(protocol.date).toLocaleDateString("de-DE", {
weekday: "long",
day: "2-digit",
month: "2-digit",
year: "numeric",
}),
date: protocol.date,
start: protocol.starttime,
end: protocol.endtime,
agenda: agenda.sort((a, b) => a.sort - b.sort),
@ -324,7 +319,7 @@ export async function synchronizeProtocolAgendaById(req: Request, res: Response)
protocolId,
})
);
await ProtocolAgendaCommandHandler.sync(syncAgenda);
await ProtocolAgendaCommandHandler.sync(protocolId, syncAgenda);
res.sendStatus(204);
}
@ -348,7 +343,7 @@ export async function synchronizeProtocolDecisonsById(req: Request, res: Respons
protocolId,
})
);
await ProtocolDecisionCommandHandler.sync(syncDecision);
await ProtocolDecisionCommandHandler.sync(protocolId, syncDecision);
res.sendStatus(204);
}
@ -375,7 +370,7 @@ export async function synchronizeProtocolVotingsById(req: Request, res: Response
protocolId,
})
);
await ProtocolVotingCommandHandler.sync(syncVoting);
await ProtocolVotingCommandHandler.sync(protocolId, syncVoting);
res.sendStatus(204);
}

View file

@ -0,0 +1,91 @@
import { Request, Response } from "express";
import EducationService from "../../../service/configuration/education";
import EducationFactory from "../../../factory/admin/configuration/education";
import {
CreateEducationCommand,
DeleteEducationCommand,
UpdateEducationCommand,
} from "../../../command/configuration/education/educationCommand";
import EducationCommandHandler from "../../../command/configuration/education/educationCommandHandler";
/**
* @description get all educations
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function getAllEducations(req: Request, res: Response): Promise<any> {
let educations = await EducationService.getAll();
res.json(EducationFactory.mapToBase(educations));
}
/**
* @description get education by id
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function getEducationById(req: Request, res: Response): Promise<any> {
const id = parseInt(req.params.id);
let education = await EducationService.getById(id);
res.json(EducationFactory.mapToSingle(education));
}
/**
* @description create new education
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function createEducation(req: Request, res: Response): Promise<any> {
const education = req.body.education;
const description = req.body.description;
let createEducation: CreateEducationCommand = {
education: education,
description: description,
};
await EducationCommandHandler.create(createEducation);
res.sendStatus(204);
}
/**
* @description update education
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function updateEducation(req: Request, res: Response): Promise<any> {
const id = parseInt(req.params.id);
const education = req.body.education;
const description = req.body.description;
let updateEducation: UpdateEducationCommand = {
id: id,
education: education,
description: description,
};
await EducationCommandHandler.update(updateEducation);
res.sendStatus(204);
}
/**
* @description delete education
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function deleteEducation(req: Request, res: Response): Promise<any> {
const id = parseInt(req.params.id);
let deleteEducation: DeleteEducationCommand = {
id: id,
};
await EducationCommandHandler.delete(deleteEducation);
res.sendStatus(204);
}

View file

@ -34,7 +34,7 @@ import { query } from "./entity/configuration/query";
import { memberView } from "./views/memberView";
import { memberExecutivePositionsView } from "./views/memberExecutivePositionView";
import { memberQualificationsView } from "./views/memberQualificationsView";
import { membershipView } from "./views/membershipsView";
import { membershipTotalView, membershipView } from "./views/membershipsView";
import { template } from "./entity/configuration/template";
import { templateUsage } from "./entity/configuration/templateUsage";
import { newsletter } from "./entity/club/newsletter/newsletter";
@ -56,6 +56,11 @@ import { SettingsFromEnv1745059495807 } from "./migrations/1745059495807-setting
import { MemberCreatedAt1746006549262 } from "./migrations/1746006549262-memberCreatedAt";
import { UserLoginRoutine1746252454922 } from "./migrations/1746252454922-UserLoginRoutine";
import { SettingsFromEnv_SET1745059495808 } from "./migrations/1745059495808-settingsFromEnv_set";
import { AddDateToNewsletter1748509435932 } from "./migrations/1748509435932-addDateToNewsletter";
import { AddMembershipTotalView1748607842929 } from "./migrations/1748607842929-addMembershipTotalView";
import { education } from "./entity/configuration/education";
import { memberEducations } from "./entity/club/member/memberEducations";
import { MemberExtendData1748953828644 } from "./migrations/1748953828644-memberExtendData";
configCheck();
@ -82,6 +87,7 @@ const dataSource = new DataSource({
communicationType,
executivePosition,
membershipStatus,
education,
qualification,
salutation,
member,
@ -89,6 +95,7 @@ const dataSource = new DataSource({
memberExecutivePositions,
memberQualifications,
membership,
memberEducations,
protocol,
protocolAgenda,
protocolDecision,
@ -108,6 +115,7 @@ const dataSource = new DataSource({
memberExecutivePositionsView,
memberQualificationsView,
membershipView,
membershipTotalView,
webapi,
webapiPermission,
setting,
@ -123,6 +131,9 @@ const dataSource = new DataSource({
SettingsFromEnv_SET1745059495808,
MemberCreatedAt1746006549262,
UserLoginRoutine1746252454922,
AddDateToNewsletter1748509435932,
AddMembershipTotalView1748607842929,
MemberExtendData1748953828644,
],
migrationsRun: true,
migrationsTransactionMode: "each",

View file

@ -7,7 +7,7 @@ export const protocolDemoData: {
title: string;
summary: string;
iteration: number;
date: string;
date: Date;
start: string;
end: string;
agenda: Array<Partial<protocolAgenda>>;
@ -19,12 +19,7 @@ export const protocolDemoData: {
title: "Beispiel Protokoll Daten",
summary: "Zusammenfassung der Demodaten.",
iteration: 1,
date: new Date().toLocaleDateString("de-DE", {
weekday: "long",
day: "2-digit",
month: "2-digit",
year: "numeric",
}),
date: new Date(),
start: "19:00:00",
end: "21:00:00",
agenda: [

View file

@ -16,6 +16,7 @@ import { memberExecutivePositions } from "./memberExecutivePositions";
import { communication } from "./communication";
import { salutation } from "../../configuration/salutation";
import { getTypeByORM } from "../../../migrations/ormHelper";
import { memberEducations } from "./memberEducations";
@Entity()
export class member {
@ -37,6 +38,9 @@ export class member {
@Column({ type: "varchar", length: 255, unique: true, nullable: true })
internalId?: string;
@Column({ type: "varchar", length: 255, nullable: true })
note?: string;
@Column()
salutationId: number;
@ -66,6 +70,9 @@ export class member {
@OneToMany(() => memberQualifications, (qualifications) => qualifications.member, { cascade: ["insert"] })
qualifications: memberQualifications[];
@OneToMany(() => memberEducations, (educations) => educations.member, { cascade: ["insert"] })
educations: memberEducations[];
firstMembershipEntry?: membership;
lastMembershipEntry?: membership;
preferredCommunication?: Array<communication>;

View file

@ -0,0 +1,43 @@
import { Column, ColumnType, Entity, ManyToOne, PrimaryColumn } from "typeorm";
import { member } from "./member";
import { education } from "../../configuration/education";
import { getTypeByORM } from "../../../migrations/ormHelper";
@Entity()
export class memberEducations {
@PrimaryColumn({ generated: "increment", type: "int" })
id: number;
@Column({ type: getTypeByORM("date").type as ColumnType })
start: Date;
@Column({ type: getTypeByORM("date").type as ColumnType, nullable: true })
end?: Date;
@Column({ type: "varchar", length: 255, nullable: true })
note?: string;
@Column({ type: "varchar", length: 255, nullable: true })
place?: string;
@Column()
memberId: string;
@Column()
educationId: number;
@ManyToOne(() => member, (member) => member.awards, {
nullable: false,
onDelete: "CASCADE",
onUpdate: "RESTRICT",
})
member: member;
@ManyToOne(() => education, (education) => education.members, {
nullable: false,
onDelete: "RESTRICT",
onUpdate: "RESTRICT",
cascade: ["insert"],
})
education: education;
}

View file

@ -1,4 +1,4 @@
import { Column, Entity, ManyToOne, OneToMany, PrimaryColumn } from "typeorm";
import { Column, CreateDateColumn, Entity, ManyToOne, OneToMany, PrimaryColumn } from "typeorm";
import { newsletterDates } from "./newsletterDates";
import { newsletterRecipients } from "./newsletterRecipients";
import { query } from "../../configuration/query";
@ -14,6 +14,9 @@ export class newsletter {
@Column({ type: "varchar", length: 255, default: "" })
description: string;
@CreateDateColumn()
createdAt: Date;
@Column({ type: "text", default: "" })
newsletterTitle: string;

View file

@ -0,0 +1,17 @@
import { Column, Entity, OneToMany, PrimaryColumn } from "typeorm";
import { memberEducations } from "../club/member/memberEducations";
@Entity()
export class education {
@PrimaryColumn({ generated: "increment", type: "int" })
id: number;
@Column({ type: "varchar", length: 255, unique: true })
education: string;
@Column({ type: "varchar", length: 255, nullable: true })
description?: string;
@OneToMany(() => memberEducations, (memberEducations) => memberEducations.education)
members: memberEducations[];
}

View file

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

View file

@ -0,0 +1,30 @@
import { memberEducations } from "../../../../entity/club/member/memberEducations";
import { MemberEducationViewModel } from "../../../../viewmodel/admin/club/member/memberEducation.models";
export default abstract class MemberEducationFactory {
/**
* @description map record to memberEducation
* @param {memberEducation} record
* @returns {MemberEducationViewModel}
*/
public static mapToSingle(record: memberEducations): MemberEducationViewModel {
return {
id: record.id,
start: record.start,
end: record.end,
note: record.note,
place: record.place,
education: record.education.education,
educationId: record.education.id,
};
}
/**
* @description map records to memberEducation
* @param {Array<memberEducation>} records
* @returns {Array<MemberEducationViewModel>}
*/
public static mapToBase(records: Array<memberEducations>): Array<MemberEducationViewModel> {
return records.map((r) => this.mapToSingle(r));
}
}

View file

@ -1,9 +1,10 @@
import { membership } from "../../../../entity/club/member/membership";
import {
MembershipStatisticsViewModel,
MembershipTotalStatisticsViewModel,
MembershipViewModel,
} from "../../../../viewmodel/admin/club/member/membership.models";
import { membershipView } from "../../../../views/membershipsView";
import { membershipTotalView, membershipView } from "../../../../views/membershipsView";
import DateMappingHelper from "./dateMappingHelper";
export default abstract class MembershipFactory {
@ -53,6 +54,25 @@ export default abstract class MembershipFactory {
};
}
/**
* @description map view record to MembershipTotalStatisticsViewModel
* @param {membershipTotalView} record
* @returns {MembershipTotalStatisticsViewModel}
*/
public static mapToSingleTotalStatistic(record: membershipTotalView): MembershipTotalStatisticsViewModel {
return {
durationInDays: record.durationInDays,
durationInYears: record.durationInYears,
exactDuration: DateMappingHelper.mapDate(record.exactDuration),
memberId: record.memberId,
memberSalutation: record.memberSalutation,
memberFirstname: record.memberFirstname,
memberLastname: record.memberLastname,
memberNameaffix: record.memberNameaffix,
memberBirthdate: record.memberBirthdate,
};
}
/**
* @description map records to MembershipStatisticsViewModel
* @param {Array<membershipView>} records

View file

@ -18,6 +18,7 @@ export default abstract class NewsletterFactory {
newsletterSignatur: record.newsletterSignatur,
isSent: record.isSent,
recipientsByQueryId: record?.recipientsByQuery ? record.recipientsByQuery.id : null,
createdAt: record.createdAt,
};
}

View file

@ -0,0 +1,26 @@
import { education } from "../../../entity/configuration/education";
import { EducationViewModel } from "../../../viewmodel/admin/configuration/education.models";
export default abstract class EducationFactory {
/**
* @description map record to education
* @param {education} record
* @returns {AwardViewModel}
*/
public static mapToSingle(record: education): EducationViewModel {
return {
id: record.id,
education: record.education,
description: record.description,
};
}
/**
* @description map records to education
* @param {Array<education>} records
* @returns {Array<AwardViewModel>}
*/
public static mapToBase(records: Array<education>): Array<EducationViewModel> {
return records.map((r) => this.mapToSingle(r));
}
}

View file

@ -8,6 +8,14 @@ Handlebars.registerHelper("date", function (aString) {
});
});
Handlebars.registerHelper("weekdayDayMonth", function (aString) {
return new Date(aString).toLocaleDateString("de-DE", {
weekday: "long",
day: "2-digit",
month: "long",
});
});
Handlebars.registerHelper("longdate", function (aString) {
return new Date(aString).toLocaleDateString("de-DE", {
weekday: "long",
@ -27,6 +35,27 @@ Handlebars.registerHelper("datetime", function (aString) {
});
});
Handlebars.registerHelper("longdatetime", function (aString) {
return new Date(aString).toLocaleDateString("de-DE", {
day: "2-digit",
month: "long",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
});
});
Handlebars.registerHelper("longdatetimeWithWeekday", function (aString) {
return new Date(aString).toLocaleDateString("de-DE", {
weekday: "long",
day: "2-digit",
month: "long",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
});
});
Handlebars.registerHelper("json", function (context) {
return JSON.stringify(context);
});

View file

@ -8,6 +8,7 @@ import DatabaseActionException from "../exceptions/databaseActionException";
import { availableTemplates } from "../type/templateTypes";
import SettingHelper from "./settingsHelper";
import { LoginRoutineEnum } from "../enums/loginRoutineEnum";
import { education } from "../entity/configuration/education";
export type BackupSection =
| "member"
@ -55,6 +56,7 @@ export default abstract class BackupHelper {
"member_executive_positions",
"membership",
"communication",
"member_educations",
],
memberBase: [
"award",
@ -63,6 +65,7 @@ export default abstract class BackupHelper {
"membership_status",
"communication_type",
"salutation",
"education",
],
protocol: [
"protocol",
@ -246,6 +249,8 @@ export default abstract class BackupHelper {
.leftJoin("positions.executivePosition", "executivePosition")
.leftJoin("member.qualifications", "qualifications")
.leftJoin("qualifications.qualification", "qualification")
.leftJoin("member.educations", "educations")
.leftJoin("educations.education", "education")
.select([
...(collectIds ? ["member.id"] : []),
"member.firstname",
@ -253,6 +258,7 @@ export default abstract class BackupHelper {
"member.nameaffix",
"member.birthdate",
"member.internalId",
"member.note",
])
.addSelect(["salutation.salutation"])
.addSelect([
@ -280,6 +286,14 @@ export default abstract class BackupHelper {
"qualification.qualification",
"qualification.description",
])
.addSelect([
"educations.start",
"educations.end",
"educations.place",
"educations.note",
"education.education",
"education.description",
])
.getMany();
}
private static async getMemberBase(): Promise<{ [key: string]: Array<any> }> {
@ -294,6 +308,7 @@ export default abstract class BackupHelper {
qualification: await dataSource
.getRepository("qualification")
.find({ select: { qualification: true, description: true } }),
education: await dataSource.getRepository("education").find({ select: { education: true, description: true } }),
};
}
private static async getProtocol(collectIds: boolean): Promise<Array<any>> {
@ -344,6 +359,7 @@ export default abstract class BackupHelper {
"newsletter.newsletterText",
"newsletter.newsletterSignatur",
"newsletter.isSent",
"newsletter.createdAt",
])
.addSelect(["dates.calendarId", "dates.diffTitle", "dates.diffDescription"])
.addSelect(["recipients.memberId"])
@ -534,6 +550,13 @@ export default abstract class BackupHelper {
.map((d) => ({ ...d, id: undefined })),
"qualification"
),
education: uniqBy(
data
.map((d) => (d.education ?? []).map((c: any) => c.education))
.flat()
.map((d) => ({ ...d, id: undefined })),
"education"
),
});
let salutation = await this.transactionManager.getRepository("salutation").find();
@ -541,6 +564,7 @@ export default abstract class BackupHelper {
let membership = await this.transactionManager.getRepository("membership_status").find();
let award = await this.transactionManager.getRepository("award").find();
let qualification = await this.transactionManager.getRepository("qualification").find();
let education = await this.transactionManager.getRepository("education").find();
let position = await this.transactionManager.getRepository("executive_position").find();
let dataWithMappedIds = data.map((d) => ({
...d,
@ -583,6 +607,13 @@ export default abstract class BackupHelper {
id: qualification.find((iq) => iq.qualification == q.qualification.qualification)?.id ?? undefined,
},
})),
educations: (d.educations ?? []).map((e: any) => ({
...e,
education: {
...e.education,
id: education.find((id) => id.education == e.education.education)?.id ?? undefined,
},
})),
}));
await this.transactionManager.getRepository("member").save(dataWithMappedIds);
}
@ -593,6 +624,7 @@ export default abstract class BackupHelper {
let award = await this.transactionManager.getRepository("award").find();
let qualification = await this.transactionManager.getRepository("qualification").find();
let position = await this.transactionManager.getRepository("executive_position").find();
let education = await this.transactionManager.getRepository("education").find();
await this.transactionManager
.createQueryBuilder()
@ -634,10 +666,19 @@ export default abstract class BackupHelper {
.insert()
.into("qualification")
.values(
(data?.["qualification"] ?? []).filter((d) => !qualification.map((q) => q.award).includes(d.qualification))
(data?.["qualification"] ?? []).filter(
(d) => !qualification.map((q) => q.qualification).includes(d.qualification)
)
)
.orIgnore()
.execute();
await this.transactionManager
.createQueryBuilder()
.insert()
.into("education")
.values((data?.["education"] ?? []).filter((d) => !education.map((q) => q.education).includes(d.education)))
.orIgnore()
.execute();
}
private static async setProtocol(data: Array<any>, collectedIds: boolean): Promise<void> {
let members = await this.transactionManager.getRepository("member").find();

View file

@ -12,6 +12,16 @@ export abstract class DemoDataHelper {
return newsletterDemoData;
case "member":
return memberDemoData;
case "listprint":
return {
today: new Date(),
list: [memberDemoData.memberships],
};
case "listprint.member":
return {
today: new Date(),
list: [memberDemoData.member],
};
default:
return {};
}

View file

@ -17,11 +17,13 @@ export default abstract class DynamicQueryBuilder {
"memberAwards",
"memberExecutivePositions",
"memberQualifications",
"memberEducations",
"membership",
"memberView",
"memberExecutivePositionsView",
"memberQualificationsView",
"membershipView",
"membershipTotalView",
];
public static getTableMeta(tableName: string): TableMeta {
@ -226,19 +228,19 @@ export default abstract class DynamicQueryBuilder {
query += ` IS NOT NULL`;
break;
case "contains":
query += ` LIKE :${parameterKey}`;
query += ` ILIKE :${parameterKey}`;
parameters[parameterKey] = `%${condition.value}%`;
break;
case "notContains":
query += ` NOT LIKE :${parameterKey}`;
query += ` NOT ILIKE :${parameterKey}`;
parameters[parameterKey] = `%${condition.value}%`;
break;
case "startsWith":
query += ` LIKE :${parameterKey}`;
query += ` ILIKE :${parameterKey}`;
parameters[parameterKey] = `${condition.value}%`;
break;
case "endsWith":
query += ` LIKE :${parameterKey}`;
query += ` ILIKE :${parameterKey}`;
parameters[parameterKey] = `%${condition.value}`;
break;
case "timespanEq":
@ -292,7 +294,12 @@ export default abstract class DynamicQueryBuilder {
results.forEach((res) => {
if (typeof value === "object" && value instanceof Date) {
res[newKey] = new Date(value).toISOString();
} else if (typeof value === "object" && !Array.isArray(value) && !(value instanceof Buffer)) {
} else if (
typeof value === "object" &&
!Array.isArray(value) &&
!(value instanceof Buffer) &&
value !== null
) {
let string = "";
for (const key of Object.keys(value)) {
string += `${value[key]} ${key} `;
@ -301,7 +308,7 @@ export default abstract class DynamicQueryBuilder {
// JSON.stringify(value).replace(/["\\{}]/g, "").replaceAll(",", ", ");
} else if (String(value) != "undefined") {
res[newKey] = String(value);
res[newKey] = value !== null ? String(value) : "";
}
});
}

View file

@ -66,33 +66,7 @@ export abstract class NewsletterHelper {
title: d.diffTitle || d.calendar.title,
content: d.diffDescription || d.calendar.content,
starttime: d.calendar.starttime,
formattedStarttime: new Date(d.calendar.starttime).toLocaleDateString("de-DE", {
weekday: "long",
day: "2-digit",
month: "long",
}),
formattedFullStarttime: new Date(d.calendar.starttime).toLocaleDateString("de-DE", {
weekday: "long",
day: "2-digit",
month: "long",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
}),
endtime: d.calendar.endtime,
formattedEndtime: new Date(d.calendar.endtime).toLocaleDateString("de-DE", {
weekday: "long",
day: "2-digit",
month: "long",
}),
formattedFullEndtime: new Date(d.calendar.endtime).toLocaleDateString("de-DE", {
weekday: "long",
day: "2-digit",
month: "long",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
}),
location: d.calendar.location,
}))
.sort((a, b) => a.starttime.getTime() - b.starttime.getTime()),

View file

@ -76,7 +76,7 @@ export default abstract class SettingHelper {
* @param value The value to set
*/
public static async setSetting<K extends SettingString>(key: K, value: SettingValueMapping[K]): Promise<void> {
if (value === undefined || value === null || value == "") {
if (value === undefined || value === null || value === "") {
if (key != "mail.password") this.resetSetting(key);
return;
}

View file

@ -6,7 +6,9 @@ export class NewsletterColumnType1744351418751 implements MigrationInterface {
name = "NewsletterColumnType1744351418751";
public async up(queryRunner: QueryRunner): Promise<void> {
let newsletters = await queryRunner.manager.getRepository("newsletter").find();
let newsletters = await queryRunner.manager
.getRepository("newsletter")
.find({ select: { id: true, newsletterTitle: true, newsletterSignatur: true } });
await queryRunner.dropColumn("newsletter", "newsletterTitle");
await queryRunner.dropColumn("newsletter", "newsletterSignatur");
@ -24,7 +26,9 @@ export class NewsletterColumnType1744351418751 implements MigrationInterface {
}
public async down(queryRunner: QueryRunner): Promise<void> {
let newsletters = await queryRunner.manager.getRepository("newsletter").find();
let newsletters = await queryRunner.manager
.getRepository("newsletter")
.find({ select: { id: true, newsletterTitle: true, newsletterSignatur: true } });
await queryRunner.dropColumn("newsletter", "newsletterTitle");
await queryRunner.dropColumn("newsletter", "newsletterSignatur");

View file

@ -0,0 +1,21 @@
import { MigrationInterface, QueryRunner, TableColumn } from "typeorm";
import { getTypeByORM, getDefaultByORM } from "./ormHelper";
export class AddDateToNewsletter1748509435932 implements MigrationInterface {
name = "AddDateToNewsletter1748509435932";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.addColumn(
"newsletter",
new TableColumn({
name: "createdAt",
...getTypeByORM("datetime", false, 6),
default: getDefaultByORM("currentTimestamp", 6),
})
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropColumn("newsletter", "createdAt");
}
}

View file

@ -0,0 +1,21 @@
import { MigrationInterface, QueryRunner } from "typeorm";
import { DB_TYPE } from "../env.defaults";
import {
membership_total_view_mysql,
membership_total_view_postgres,
membership_total_view_sqlite,
} from "./baseSchemaTables/member";
export class AddMembershipTotalView1748607842929 implements MigrationInterface {
name = "AddMembershipTotalView1748607842929";
public async up(queryRunner: QueryRunner): Promise<void> {
if (DB_TYPE == "postgres") await queryRunner.createView(membership_total_view_postgres, true);
else if (DB_TYPE == "mysql") await queryRunner.createView(membership_total_view_mysql, true);
else if (DB_TYPE == "sqlite") await queryRunner.createView(membership_total_view_sqlite, true);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropView("membership_total_view");
}
}

View file

@ -0,0 +1,34 @@
import { MigrationInterface, QueryRunner, TableColumn } from "typeorm";
import {
education_table,
member_educations_table,
member_view2_mysql,
member_view2_postgres,
member_view2_sqlite,
} from "./baseSchemaTables/member";
import { getDefaultByORM, getTypeByORM } from "./ormHelper";
import { DB_TYPE } from "../env.defaults";
export class MemberExtendData1748953828644 implements MigrationInterface {
name = "MemberExtendData1748953828644";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(education_table, true, true, true);
await queryRunner.createTable(member_educations_table, true, true, true);
await queryRunner.addColumn(
"member",
new TableColumn({ name: "note", ...getTypeByORM("varchar", true), default: getDefaultByORM("null") })
);
await queryRunner.dropView("member_view");
if (DB_TYPE == "postgres") await queryRunner.createView(member_view2_postgres, true);
else if (DB_TYPE == "mysql") await queryRunner.createView(member_view2_mysql, true);
else if (DB_TYPE == "sqlite") await queryRunner.createView(member_view2_sqlite, true);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable(member_educations_table, true, true, true);
await queryRunner.dropTable(education_table, true, true, true);
await queryRunner.dropColumn("member", "note");
}
}

View file

@ -51,6 +51,15 @@ export const qualification_table = new Table({
],
});
export const education_table = new Table({
name: "education",
columns: [
{ name: "id", ...getTypeByORM("int"), ...isIncrementPrimary },
{ name: "education", ...getTypeByORM("varchar"), isUnique: true },
{ name: "description", ...getTypeByORM("varchar"), isNullable: true },
],
});
/** member and relations */
export const member_table = new Table({
name: "member",
@ -222,6 +231,35 @@ export const member_communication_table = new Table({
],
});
export const member_educations_table = new Table({
name: "member_educations",
columns: [
{ name: "id", ...getTypeByORM("int"), ...isIncrementPrimary },
{ name: "start", ...getTypeByORM("date") },
{ name: "end", ...getTypeByORM("date", true), default: getDefaultByORM("null") },
{ name: "note", ...getTypeByORM("varchar"), isNullable: true },
{ name: "place", ...getTypeByORM("varchar"), isNullable: true },
{ name: "memberId", ...getTypeByORM("uuid") },
{ name: "educationId", ...getTypeByORM("int") },
],
foreignKeys: [
new TableForeignKey({
columnNames: ["memberId"],
referencedTableName: "member",
referencedColumnNames: ["id"],
onDelete: "CASCADE",
onUpdate: "RESTRICT",
}),
new TableForeignKey({
columnNames: ["educationId"],
referencedTableName: "education",
referencedColumnNames: ["id"],
onDelete: "RESTRICT",
onUpdate: "RESTRICT",
}),
],
});
/** views */
export const member_view_mysql = new View({
name: "member_view",
@ -293,6 +331,79 @@ export const member_view_sqlite = new View({
`,
});
export const member_view2_mysql = new View({
name: "member_view",
expression: `
SELECT
\`member\`.\`id\` AS \`id\`,
\`member\`.\`internalId\` AS \`internalId\`,
\`member\`.\`note\` AS \`note\`,
\`member\`.\`firstname\` AS \`firstname\`,
\`member\`.\`lastname\` AS \`lastname\`,
\`member\`.\`nameaffix\` AS \`nameaffix\`,
\`member\`.\`birthdate\` AS \`birthdate\`,
\`salutation\`.\`salutation\` AS \`salutation\`,
TIMESTAMPDIFF(YEAR, \`member\`.\`birthdate\`, CURDATE()) AS \`todayAge\`,
YEAR(CURDATE()) - YEAR(\`member\`.\`birthdate\`) AS \`ageThisYear\`,
CONCAT(
TIMESTAMPDIFF(YEAR, \`member\`.\`birthdate\`, CURDATE()), ' years ',
TIMESTAMPDIFF(MONTH, \`member\`.\`birthdate\`, CURDATE()) % 12, ' months ',
TIMESTAMPDIFF(DAY,
DATE_ADD(
\`member\`.\`birthdate\`,
INTERVAL TIMESTAMPDIFF(MONTH, \`member\`.\`birthdate\`, CURDATE()) MONTH
),
CURDATE()
), ' days'
) AS \`exactAge\`
FROM \`member\` \`member\`
LEFT JOIN \`salutation\` \`salutation\` ON \`salutation\`.\`id\`=\`member\`.\`salutationId\`
`,
});
export const member_view2_postgres = new View({
name: "member_view",
expression: `
SELECT
"member"."id" AS "id",
"member"."internalId" AS "internalId",
"member"."note" AS "note",
"member"."firstname" AS "firstname",
"member"."lastname" AS "lastname",
"member"."nameaffix" AS "nameaffix",
"member"."birthdate" AS "birthdate",
"salutation"."salutation" AS "salutation",
DATE_PART('year', AGE(CURRENT_DATE, member.birthdate)) AS "todayAge",
EXTRACT(YEAR FROM CURRENT_DATE) - EXTRACT(YEAR FROM member.birthdate) AS "ageThisYear",
AGE(CURRENT_DATE, member.birthdate) AS "exactAge"
FROM "member" "member"
LEFT JOIN "salutation" "salutation" ON "salutation"."id"="member"."salutationId"
`,
});
export const member_view2_sqlite = new View({
name: "member_view",
expression: `
SELECT
member.id AS id,
member.internalId AS internalId,
member.note AS note,
member.firstname AS firstname,
member.lastname AS lastname,
member.nameaffix AS nameaffix,
member.birthdate AS birthdate,
salutation.salutation AS salutation,
(strftime('%Y', 'now') - strftime('%Y', member.birthdate) - (strftime('%m-%d', 'now') < strftime('%m-%d', member.birthdate))) AS todayAge,
FLOOR(strftime('%Y', 'now') - strftime('%Y', member.birthdate)) AS ageThisYear,
(strftime('%Y', 'now') - strftime('%Y', member.birthdate)) || ' years ' ||
(strftime('%m', 'now') - strftime('%m', member.birthdate)) || ' months ' ||
(strftime('%d', 'now') - strftime('%d', member.birthdate)) || ' days'
AS exactAge
FROM member member
LEFT JOIN salutation salutation ON salutation.id=member.salutationId
`,
});
export const member_executive_positions_view_mysql = new View({
name: "member_executive_positions_view",
expression: `
@ -529,3 +640,76 @@ export const membership_view_sqlite = new View({
GROUP BY status.id, member.id, salutation.id
`,
});
export const membership_total_view_mysql = new View({
name: "membership_total_view",
expression: `
SELECT
\`member\`.\`id\` AS \`memberId\`,
\`member\`.\`firstname\` AS \`memberFirstname\`,
\`member\`.\`lastname\` AS \`memberLastname\`,
\`member\`.\`nameaffix\` AS \`memberNameaffix\`,
\`member\`.\`birthdate\` AS \`memberBirthdate\`,
\`salutation\`.\`salutation\` AS \`memberSalutation\`,
SUM(DATEDIFF(COALESCE(\`membership\`.\`end\`, CURDATE()), \`membership\`.\`start\`)) AS \`durationInDays\`,
SUM(TIMESTAMPDIFF(YEAR, \`membership\`.\`start\`, COALESCE(\`membership\`.\`end\`, CURDATE()))) AS \`durationInYears\`,
CONCAT(
SUM(FLOOR(TIMESTAMPDIFF(DAY, \`membership\`.\`start\`, COALESCE(\`membership\`.\`end\`, CURDATE())) / 365.25)),
' years ',
SUM(FLOOR(MOD(TIMESTAMPDIFF(MONTH, \`membership\`.\`start\`, COALESCE(\`membership\`.\`end\`, CURDATE())), 12))),
' months ',
SUM(FLOOR(MOD(TIMESTAMPDIFF(DAY, \`membership\`.\`start\`, COALESCE(\`membership\`.\`end\`, CURDATE())), 30))),
' days'
) AS \`exactDuration\`
FROM \`membership\` \`membership\`
LEFT JOIN \`membership_status\` \`status\` ON \`status\`.\`id\`=\`membership\`.\`statusId\`
LEFT JOIN \`member\` \`member\` ON \`member\`.\`id\`=\`membership\`.\`memberId\`
LEFT JOIN \`salutation\` \`salutation\` ON \`salutation\`.\`id\`=\`member\`.\`salutationId\`
GROUP BY \`member\`.\`id\`, \`salutation\`.\`id\`
`,
});
export const membership_total_view_postgres = new View({
name: "membership_total_view",
expression: `
SELECT
"member"."id" AS "memberId",
"member"."firstname" AS "memberFirstname",
"member"."lastname" AS "memberLastname",
"member"."nameaffix" AS "memberNameaffix",
"member"."birthdate" AS "memberBirthdate",
"salutation"."salutation" AS "memberSalutation",
SUM(COALESCE("membership"."end", CURRENT_DATE) - "membership"."start") AS "durationInDays",
SUM(EXTRACT(YEAR FROM AGE(COALESCE("membership"."end", CURRENT_DATE), "membership"."start"))) AS "durationInYears",
SUM(AGE(COALESCE("membership"."end", CURRENT_DATE), "membership"."start")) AS "exactDuration"
FROM "membership" "membership"
LEFT JOIN "membership_status" "status" ON "status"."id"="membership"."statusId"
LEFT JOIN "member" "member" ON "member"."id"="membership"."memberId"
LEFT JOIN "salutation" "salutation" ON "salutation"."id"="member"."salutationId"
GROUP BY "member"."id", "salutation"."id"
`,
});
export const membership_total_view_sqlite = new View({
name: "membership_total_view",
expression: `
SELECT
member.id AS memberId,
member.firstname AS memberFirstname,
member.lastname AS memberLastname,
member.nameaffix AS memberNameaffix,
member.birthdate AS memberBirthdate,
salutation.salutation AS memberSalutation,
SUM(JULIANDAY(COALESCE(membership.end, DATE('now'))) - JULIANDAY(membership.start)) AS durationInDays,
SUM(FLOOR((JULIANDAY(COALESCE(membership.end, DATE('now'))) - JULIANDAY(membership.start)) / 365.25)) AS durationInYears,
SUM((strftime('%Y', COALESCE(membership.end, DATE('now'))) - strftime('%Y', membership.start))) || ' years ' ||
SUM((strftime('%m', COALESCE(membership.end, DATE('now'))) - strftime('%m', membership.start))) || ' months ' ||
SUM((strftime('%d', COALESCE(membership.end, DATE('now'))) - strftime('%d', membership.start))) || ' days'
AS exactDuration
FROM membership membership
LEFT JOIN membership_status status ON status.id=membership.statusId
LEFT JOIN member member ON member.id=membership.memberId
LEFT JOIN salutation salutation ON salutation.id=member.salutationId
GROUP BY member.id, salutation.id
`,
});

View file

@ -2,12 +2,14 @@ import express, { Request, Response } from "express";
import {
addAwardToMember,
addCommunicationToMember,
addEducationToMember,
addExecutivePositionToMember,
addMembershipToMember,
addQualificationToMember,
createMember,
deleteAwardOfMember,
deleteCommunicationOfMember,
deleteEducationsOfMember,
deleteExecutivePositionOfMember,
deleteMemberById,
deleteMembershipOfMember,
@ -17,6 +19,8 @@ import {
getAwardsByMember,
getCommunicationByMemberAndRecord,
getCommunicationsByMember,
getEducationByMemberAndRecord,
getEducationsByMember,
getExecutivePositionByMemberAndRecord,
getExecutivePositionsByMember,
getMemberById,
@ -26,11 +30,13 @@ import {
getMembershipByMemberAndRecord,
getMembershipsByMember,
getMembershipStatisticsById,
getMembershipTotalStatisticsById,
getMemberStatisticsById,
getQualificationByMemberAndRecord,
getQualificationsByMember,
updateAwardOfMember,
updateCommunicationOfMember,
updateEducationOfMember,
updateExecutivePositionOfMember,
updateMemberById,
updateMembershipOfMember,
@ -72,6 +78,10 @@ router.get("/:memberId/memberships/statistics", async (req: Request, res: Respon
await getMembershipStatisticsById(req, res);
});
router.get("/:memberId/memberships/totalstatistics", async (req: Request, res: Response) => {
await getMembershipTotalStatisticsById(req, res);
});
router.get("/:memberId/membership/:id", async (req: Request, res: Response) => {
await getMembershipByMemberAndRecord(req, res);
});
@ -92,6 +102,14 @@ router.get("/:memberId/qualification/:id", async (req: Request, res: Response) =
await getQualificationByMemberAndRecord(req, res);
});
router.get("/:memberId/educations", async (req: Request, res: Response) => {
await getEducationsByMember(req, res);
});
router.get("/:memberId/education/:id", async (req: Request, res: Response) => {
await getEducationByMemberAndRecord(req, res);
});
router.get("/:memberId/positions", async (req: Request, res: Response) => {
await getExecutivePositionsByMember(req, res);
});
@ -140,6 +158,14 @@ router.post(
}
);
router.post(
"/:memberId/education",
PermissionHelper.passCheckMiddleware("create", "club", "member"),
async (req: Request, res: Response) => {
await addEducationToMember(req, res);
}
);
router.post(
"/:memberId/position",
PermissionHelper.passCheckMiddleware("create", "club", "member"),
@ -188,6 +214,14 @@ router.patch(
}
);
router.patch(
"/:memberId/education/:recordId",
PermissionHelper.passCheckMiddleware("update", "club", "member"),
async (req: Request, res: Response) => {
await updateEducationOfMember(req, res);
}
);
router.patch(
"/:memberId/position/:recordId",
PermissionHelper.passCheckMiddleware("update", "club", "member"),
@ -236,6 +270,14 @@ router.delete(
}
);
router.delete(
"/:memberId/education/:recordId",
PermissionHelper.passCheckMiddleware("delete", "club", "member"),
async (req: Request, res: Response) => {
await deleteEducationsOfMember(req, res);
}
);
router.delete(
"/:memberId/position/:recordId",
PermissionHelper.passCheckMiddleware("delete", "club", "member"),

View file

@ -0,0 +1,45 @@
import express, { Request, Response } from "express";
import {
createEducation,
deleteEducation,
getAllEducations,
getEducationById,
updateEducation,
} from "../../../controller/admin/configuration/educationController";
import PermissionHelper from "../../../helpers/permissionHelper";
var router = express.Router({ mergeParams: true });
router.get("/", async (req: Request, res: Response) => {
await getAllEducations(req, res);
});
router.get("/:id", async (req: Request, res: Response) => {
await getEducationById(req, res);
});
router.post(
"/",
PermissionHelper.passCheckMiddleware("create", "configuration", "education"),
async (req: Request, res: Response) => {
await createEducation(req, res);
}
);
router.patch(
"/:id",
PermissionHelper.passCheckMiddleware("update", "configuration", "education"),
async (req: Request, res: Response) => {
await updateEducation(req, res);
}
);
router.delete(
"/:id",
PermissionHelper.passCheckMiddleware("delete", "configuration", "education"),
async (req: Request, res: Response) => {
await deleteEducation(req, res);
}
);
export default router;

View file

@ -7,6 +7,7 @@ import communicationType from "./configuration/communicationType";
import executivePosition from "./configuration/executivePosition";
import membershipStatus from "./configuration/membershipStatus";
import qualification from "./configuration/qualification";
import education from "./configuration/education";
import salutation from "./configuration/salutation";
import calendarType from "./configuration/calendarType";
import queryStore from "./configuration/queryStore";
@ -70,6 +71,14 @@ router.use(
]),
qualification
);
router.use(
"/education",
PermissionHelper.passCheckSomeMiddleware([
{ requiredPermission: "read", section: "configuration", module: "education" },
{ requiredPermission: "read", section: "club", module: "member" },
]),
education
);
router.use(
"/salutation",
PermissionHelper.passCheckSomeMiddleware([

View file

@ -0,0 +1,49 @@
import { dataSource } from "../../../data-source";
import { memberEducations } from "../../../entity/club/member/memberEducations";
import DatabaseActionException from "../../../exceptions/databaseActionException";
import InternalException from "../../../exceptions/internalException";
export default abstract class MemberEducationService {
/**
* @description get all by member id
* @param {string} memberId
* @returns {Promise<Array<memberEducations>>}
*/
static async getAll(memberId: string): Promise<Array<memberEducations>> {
return await dataSource
.getRepository(memberEducations)
.createQueryBuilder("memberEducations")
.leftJoinAndSelect("memberEducations.education", "education")
.where("memberEducations.memberId = :memberId", { memberId: memberId })
.orderBy("education.education", "ASC")
.getMany()
.then((res) => {
return res;
})
.catch((err) => {
throw new DatabaseActionException("SELECT", "memberEducations", err);
});
}
/**
* @description get by memberId and recordId
* @param {string} memberId
* @param {number} recordId
* @returns {Promise<Array<member>>}
*/
static async getById(memberId: string, recordId: number): Promise<memberEducations> {
return await dataSource
.getRepository(memberEducations)
.createQueryBuilder("memberEducations")
.leftJoinAndSelect("memberEducations.education", "education")
.where("memberEducations.memberId = :memberId", { memberId: memberId })
.andWhere("memberEducations.id = :recordId", { recordId: recordId })
.getOneOrFail()
.then((res) => {
return res;
})
.catch((err) => {
throw new DatabaseActionException("SELECT", "memberEducations", err);
});
}
}

View file

@ -1,4 +1,4 @@
import { Brackets, Like, Not, SelectQueryBuilder } from "typeorm";
import { Brackets, ILike, Not, SelectQueryBuilder } from "typeorm";
import { dataSource } from "../../../data-source";
import { member } from "../../../entity/club/member/member";
import DatabaseActionException from "../../../exceptions/databaseActionException";
@ -30,10 +30,12 @@ export default abstract class MemberService {
if (searchBits.length < 2) {
query = query.where(
`member.firstname LIKE :searchQuery OR member.lastname LIKE :searchQuery OR member.internalId LIKE :searchQuery`,
{
searchQuery: `%${searchBits[0]}%`,
}
new Brackets((qb) =>
qb
.where({ firstname: ILike(`%${searchBits[0]}%`) })
.orWhere({ lastname: ILike(`%${searchBits[0]}%`) })
.orWhere({ internalId: ILike(`%${searchBits[0]}%`) })
)
);
} else {
searchBits
@ -42,12 +44,12 @@ export default abstract class MemberService {
query = query
.orWhere(
new Brackets((qb) =>
qb.where({ firstname: Like(`%${term[0]}%`) }).andWhere({ lastname: Like(`%${term[1]}%`) })
qb.where({ firstname: ILike(`%${term[0]}%`) }).andWhere({ lastname: ILike(`%${term[1]}%`) })
)
)
.orWhere(
new Brackets((qb) =>
qb.where({ firstname: Like(`%${term[1]}%`) }).andWhere({ lastname: Like(`%${term[0]}%`) })
qb.where({ firstname: ILike(`%${term[1]}%`) }).andWhere({ lastname: ILike(`%${term[0]}%`) })
)
);
});

View file

@ -2,7 +2,7 @@ import { dataSource } from "../../../data-source";
import { membership } from "../../../entity/club/member/membership";
import DatabaseActionException from "../../../exceptions/databaseActionException";
import InternalException from "../../../exceptions/internalException";
import { membershipView } from "../../../views/membershipsView";
import { membershipTotalView, membershipView } from "../../../views/membershipsView";
export default abstract class MembershipService {
/**
@ -66,4 +66,23 @@ export default abstract class MembershipService {
throw new DatabaseActionException("SELECT", "membershipView", err);
});
}
/**
* @description get membership total statistics by memberId
* @param {string} memberId
* @returns {Promise<Array<membershipTotalView>>}
*/
static async getTotalStatisticsById(memberId: string): Promise<membershipTotalView> {
return await dataSource
.getRepository(membershipTotalView)
.createQueryBuilder("membershipTotalView")
.where("membershipTotalView.memberId = :memberId", { memberId: memberId })
.getOneOrFail()
.then((res) => {
return res;
})
.catch((err) => {
throw new DatabaseActionException("SELECT", "membershipTotalView", err);
});
}
}

View file

@ -12,6 +12,7 @@ export default abstract class NewsletterService {
return await dataSource
.getRepository(newsletter)
.createQueryBuilder("newsletter")
.orderBy("createdAt", "DESC")
.offset(offset)
.limit(count)
.getManyAndCount()

View file

@ -0,0 +1,41 @@
import { dataSource } from "../../data-source";
import { education } from "../../entity/configuration/education";
import DatabaseActionException from "../../exceptions/databaseActionException";
export default abstract class EducationService {
/**
* @description get all educations
* @returns {Promise<Array<education>>}
*/
static async getAll(): Promise<Array<education>> {
return await dataSource
.getRepository(education)
.createQueryBuilder("education")
.orderBy("education", "ASC")
.getMany()
.then((res) => {
return res;
})
.catch((err) => {
throw new DatabaseActionException("SELECT", "education", err);
});
}
/**
* @description get education by id
* @returns {Promise<education>}
*/
static async getById(id: number): Promise<education> {
return await dataSource
.getRepository(education)
.createQueryBuilder("education")
.where("education.id = :id", { id: id })
.getOneOrFail()
.then((res) => {
return res;
})
.catch((err) => {
throw new DatabaseActionException("SELECT", "education", err);
});
}
}

View file

@ -58,7 +58,7 @@
<br />
{{/each}} {{/if}} {{#if qualifications.length}}
<br />
<h2>Qualifikationen</h2>
<h2>Qualifikationen / Funktionen</h2>
{{#each qualifications}}
<div>
<h3>{{this.qualification.qualification}}: {{date this.date}}</h3>
@ -69,6 +69,19 @@
{{/if}}
</div>
<br />
{{/each}} {{/if}} {{#if educations.length}}
<br />
<h2>Aus-/Fortbildungen</h2>
{{#each educations}}
<div>
<h3>{{this.education.education}}: {{date this.start}}{{#if this.end}} bis {{date this.end}}{{/if}}</h3>
{{#if this.place}}
<p>Ausbildungsort: {{this.place}}</p>
{{/if}}{{#if this.note}}
<p>Notiz: {{this.note}}</p>
{{/if}}
</div>
<br />
{{/each}} {{/if}}
</body>
<style>

View file

@ -10,7 +10,7 @@
<br />
{{#each dates}}
<div>
<h2><b>{{this.formattedStarttime}}: {{this.title}}</b></h2>
<h2><b>{{weekdayDayMonth this.starttime}}: {{this.title}}</b></h2>
<span>{{{this.content}}}</span>
</div>
<br />

View file

@ -6,7 +6,7 @@
</head>
<body>
<h1>{{title}}</h1>
<p>Am {{date}} von {{start}} Uhr bis {{end}} Uhr</p>
<p>Am {{longdate date}} von {{start}} Uhr bis {{end}} Uhr</p>
<p>Ausdruck Nr {{iteration}}</p>
<br />
<p><b>Zusammenfassung:</b></p>

View file

@ -13,6 +13,7 @@ export type PermissionModule =
| "communication_type"
| "membership_status"
| "salutation"
| "education"
| "calendar_type"
| "user"
| "role"
@ -70,6 +71,7 @@ export const permissionModules: Array<PermissionModule> = [
"communication_type",
"membership_status",
"salutation",
"education",
"calendar_type",
"user",
"role",
@ -91,6 +93,7 @@ export const sectionsAndModules: SectionsAndModulesObject = {
"communication_type",
"membership_status",
"salutation",
"education",
"calendar_type",
"query_store",
"template",

View file

@ -15,6 +15,7 @@ export interface MemberViewModel {
sendNewsletter?: CommunicationViewModel;
smsAlarming?: Array<CommunicationViewModel>;
preferredCommunication?: Array<CommunicationViewModel>;
note?: string;
}
export interface MemberStatisticsViewModel {

View file

@ -0,0 +1,26 @@
export interface MemberEducationViewModel {
id: number;
start: Date;
end?: Date;
place?: string;
note?: string;
education: string;
educationId: number;
}
export interface CreateMemberEducationViewModel {
start: Date;
end?: Date;
place?: string;
note?: string;
educationId: number;
}
export interface UpdateMemberEducationViewModel {
id: number;
start: Date;
end?: Date;
place?: string;
note?: string;
educationId: number;
}

View file

@ -20,3 +20,15 @@ export interface MembershipStatisticsViewModel {
memberNameaffix: string;
memberBirthdate: Date;
}
export interface MembershipTotalStatisticsViewModel {
durationInDays: number;
durationInYears: number;
exactDuration: string;
memberId: string;
memberSalutation: string;
memberFirstname: string;
memberLastname: string;
memberNameaffix: string;
memberBirthdate: Date;
}

View file

@ -7,4 +7,5 @@ export interface NewsletterViewModel {
newsletterSignatur: string;
isSent: boolean;
recipientsByQueryId?: string;
createdAt: Date;
}

View file

@ -0,0 +1,16 @@
export interface EducationViewModel {
id: number;
education: string;
description: string | null;
}
export interface CreateEducationViewModel {
education: string;
description: string | null;
}
export interface UpdateEducationViewModel {
id: number;
education: string;
description: string | null;
}

View file

@ -41,7 +41,8 @@ if (DB_TYPE == "postgres") {
.getRepository(member)
.createQueryBuilder("member")
.select("member.id", "id")
.select("member.internalId", "internalId")
.addSelect("member.internalId", "internalId")
.addSelect("member.note", "note")
.addSelect("member.firstname", "firstname")
.addSelect("member.lastname", "lastname")
.addSelect("member.nameaffix", "nameaffix")
@ -62,6 +63,9 @@ export class memberView {
@ViewColumn()
internalId: string;
@ViewColumn()
note: string;
@ViewColumn()
firstname: string;

View file

@ -89,3 +89,52 @@ export class membershipView {
@ViewColumn()
memberBirthdate: Date;
}
@ViewEntity({
expression: (datasource: DataSource) =>
datasource
.getRepository(membership)
.createQueryBuilder("membership")
.select("member.id", "memberId")
.addSelect("member.firstname", "memberFirstname")
.addSelect("member.lastname", "memberLastname")
.addSelect("member.nameaffix", "memberNameaffix")
.addSelect("member.birthdate", "memberBirthdate")
.addSelect("salutation.salutation", "memberSalutation")
.addSelect(durationInDays, "durationInDays")
.addSelect(durationInYears, "durationInYears")
.addSelect(exactDuration, "exactDuration")
.leftJoin("membership.status", "status")
.leftJoin("membership.member", "member")
.leftJoin("member.salutation", "salutation")
.groupBy("status.id")
.addGroupBy("member.id"),
})
export class membershipTotalView {
@ViewColumn()
durationInDays: number;
@ViewColumn()
durationInYears: number;
@ViewColumn()
exactDuration: string;
@ViewColumn()
memberId: string;
@ViewColumn()
memberSalutation: string;
@ViewColumn()
memberFirstname: string;
@ViewColumn()
memberLastname: string;
@ViewColumn()
memberNameaffix: string;
@ViewColumn()
memberBirthdate: Date;
}