From 496a60f2df39d50701a5bd31bb7bacd0146cb5ac Mon Sep 17 00:00:00 2001 From: Julian Krauser Date: Wed, 21 May 2025 10:33:05 +0200 Subject: [PATCH 01/11] enhance: enable deletion of protocol content --- .../protocol/protocolAgendaCommandHandler.ts | 18 +++++++++++++++-- .../protocolDecisionCommandHandler.ts | 20 +++++++++++++++++-- .../protocol/protocolVotingCommandHandler.ts | 17 ++++++++++++++-- .../admin/club/protocolController.ts | 6 +++--- 4 files changed, 52 insertions(+), 9 deletions(-) diff --git a/src/command/club/protocol/protocolAgendaCommandHandler.ts b/src/command/club/protocol/protocolAgendaCommandHandler.ts index 73ce349..190f288 100644 --- a/src/command/club/protocol/protocolAgendaCommandHandler.ts +++ b/src/command/club/protocol/protocolAgendaCommandHandler.ts @@ -34,18 +34,32 @@ export default abstract class ProtocolAgendaCommandHandler { /** * @description sync protocolAgenda + * @param {number} protocolId * @param {Array} syncProtocolAgenda * @returns {Promise} */ - static async sync(syncProtocolAgenda: Array): Promise { + static async sync(protocolId: number, syncProtocolAgenda: Array): Promise { + 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(); } }) diff --git a/src/command/club/protocol/protocolDecisionCommandHandler.ts b/src/command/club/protocol/protocolDecisionCommandHandler.ts index c466188..6e8b51c 100644 --- a/src/command/club/protocol/protocolDecisionCommandHandler.ts +++ b/src/command/club/protocol/protocolDecisionCommandHandler.ts @@ -33,12 +33,19 @@ export default abstract class ProtocolDecisionCommandHandler { } /** * @description sync protocolDecision + * @param {number} protocolId * @param {Array} syncProtocolDecisions * @returns {Promise} */ - static async sync(syncProtocolDecisions: Array): Promise { + static async sync( + protocolId: number, + syncProtocolDecisions: Array + ): Promise { + 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); }); diff --git a/src/command/club/protocol/protocolVotingCommandHandler.ts b/src/command/club/protocol/protocolVotingCommandHandler.ts index 7a27cb6..a788ae5 100644 --- a/src/command/club/protocol/protocolVotingCommandHandler.ts +++ b/src/command/club/protocol/protocolVotingCommandHandler.ts @@ -33,12 +33,16 @@ export default abstract class ProtocolVotingCommandHandler { } /** * @description sync protocolVoting + * @param {number} protocolId * @param {Array} syncProtocolVotings * @returns {Promise} */ - static async sync(syncProtocolVotings: Array): Promise { + static async sync(protocolId: number, syncProtocolVotings: Array): Promise { + 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); }); diff --git a/src/controller/admin/club/protocolController.ts b/src/controller/admin/club/protocolController.ts index 98396c8..02a1bab 100644 --- a/src/controller/admin/club/protocolController.ts +++ b/src/controller/admin/club/protocolController.ts @@ -324,7 +324,7 @@ export async function synchronizeProtocolAgendaById(req: Request, res: Response) protocolId, }) ); - await ProtocolAgendaCommandHandler.sync(syncAgenda); + await ProtocolAgendaCommandHandler.sync(protocolId, syncAgenda); res.sendStatus(204); } @@ -348,7 +348,7 @@ export async function synchronizeProtocolDecisonsById(req: Request, res: Respons protocolId, }) ); - await ProtocolDecisionCommandHandler.sync(syncDecision); + await ProtocolDecisionCommandHandler.sync(protocolId, syncDecision); res.sendStatus(204); } @@ -375,7 +375,7 @@ export async function synchronizeProtocolVotingsById(req: Request, res: Response protocolId, }) ); - await ProtocolVotingCommandHandler.sync(syncVoting); + await ProtocolVotingCommandHandler.sync(protocolId, syncVoting); res.sendStatus(204); } From 5187a2dba6b5168112d98c24d987acc8663776dd Mon Sep 17 00:00:00 2001 From: Julian Krauser Date: Wed, 21 May 2025 10:56:09 +0200 Subject: [PATCH 02/11] change: handlebar abstracts for date --- .../admin/club/protocolController.ts | 7 +---- src/demodata/protocol.data.ts | 9 ++---- src/handlebars.config.ts | 29 +++++++++++++++++++ src/helpers/newsletterHelper.ts | 26 ----------------- src/templates/newsletter.body.template.html | 2 +- src/templates/protocol.body.template.html | 2 +- 6 files changed, 34 insertions(+), 41 deletions(-) diff --git a/src/controller/admin/club/protocolController.ts b/src/controller/admin/club/protocolController.ts index 02a1bab..4edfd88 100644 --- a/src/controller/admin/club/protocolController.ts +++ b/src/controller/admin/club/protocolController.ts @@ -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), diff --git a/src/demodata/protocol.data.ts b/src/demodata/protocol.data.ts index 8d18b2a..437d280 100644 --- a/src/demodata/protocol.data.ts +++ b/src/demodata/protocol.data.ts @@ -7,7 +7,7 @@ export const protocolDemoData: { title: string; summary: string; iteration: number; - date: string; + date: Date; start: string; end: string; agenda: Array>; @@ -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: [ diff --git a/src/handlebars.config.ts b/src/handlebars.config.ts index 2cbded1..19b32ad 100644 --- a/src/handlebars.config.ts +++ b/src/handlebars.config.ts @@ -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); }); diff --git a/src/helpers/newsletterHelper.ts b/src/helpers/newsletterHelper.ts index c09da94..26f9342 100644 --- a/src/helpers/newsletterHelper.ts +++ b/src/helpers/newsletterHelper.ts @@ -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()), diff --git a/src/templates/newsletter.body.template.html b/src/templates/newsletter.body.template.html index 01433c0..bae1064 100644 --- a/src/templates/newsletter.body.template.html +++ b/src/templates/newsletter.body.template.html @@ -10,7 +10,7 @@
{{#each dates}}
-

{{this.formattedStarttime}}: {{this.title}}

+

{{weekdayDayMonth this.starttime}}: {{this.title}}

{{{this.content}}}

diff --git a/src/templates/protocol.body.template.html b/src/templates/protocol.body.template.html index 5a263ff..6b6bbdb 100644 --- a/src/templates/protocol.body.template.html +++ b/src/templates/protocol.body.template.html @@ -6,7 +6,7 @@

{{title}}

-

Am {{date}} von {{start}} Uhr bis {{end}} Uhr

+

Am {{longdate date}} von {{start}} Uhr bis {{end}} Uhr

Ausdruck Nr {{iteration}}


Zusammenfassung:

From 2357497d3ac1bf1ef6eac415e7a311ac8cc090ca Mon Sep 17 00:00:00 2001 From: Julian Krauser Date: Thu, 29 May 2025 11:12:33 +0200 Subject: [PATCH 03/11] enhance: sort newsletter by creation date --- src/data-source.ts | 2 ++ src/entity/club/newsletter/newsletter.ts | 5 ++++- .../1748509435932-addDateToNewsletter.ts | 21 +++++++++++++++++++ .../club/newsletter/newsletterService.ts | 1 + 4 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 src/migrations/1748509435932-addDateToNewsletter.ts diff --git a/src/data-source.ts b/src/data-source.ts index 63de0f8..bb96819 100644 --- a/src/data-source.ts +++ b/src/data-source.ts @@ -56,6 +56,7 @@ 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"; configCheck(); @@ -123,6 +124,7 @@ const dataSource = new DataSource({ SettingsFromEnv_SET1745059495808, MemberCreatedAt1746006549262, UserLoginRoutine1746252454922, + AddDateToNewsletter1748509435932, ], migrationsRun: true, migrationsTransactionMode: "each", diff --git a/src/entity/club/newsletter/newsletter.ts b/src/entity/club/newsletter/newsletter.ts index 24258b4..ead7052 100644 --- a/src/entity/club/newsletter/newsletter.ts +++ b/src/entity/club/newsletter/newsletter.ts @@ -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; diff --git a/src/migrations/1748509435932-addDateToNewsletter.ts b/src/migrations/1748509435932-addDateToNewsletter.ts new file mode 100644 index 0000000..5f4b8fa --- /dev/null +++ b/src/migrations/1748509435932-addDateToNewsletter.ts @@ -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 { + await queryRunner.addColumn( + "newsletter", + new TableColumn({ + name: "createdAt", + ...getTypeByORM("datetime", false, 6), + default: getDefaultByORM("currentTimestamp", 6), + }) + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropColumn("newsletter", "createdAt"); + } +} diff --git a/src/service/club/newsletter/newsletterService.ts b/src/service/club/newsletter/newsletterService.ts index 27489d0..b11bfd7 100644 --- a/src/service/club/newsletter/newsletterService.ts +++ b/src/service/club/newsletter/newsletterService.ts @@ -12,6 +12,7 @@ export default abstract class NewsletterService { return await dataSource .getRepository(newsletter) .createQueryBuilder("newsletter") + .orderBy("createdAt", "DESC") .offset(offset) .limit(count) .getManyAndCount() From f6a0a61ed8d60c395b0dda5f97f7a9516940c7b0 Mon Sep 17 00:00:00 2001 From: Julian Krauser Date: Thu, 29 May 2025 11:21:08 +0200 Subject: [PATCH 04/11] change: provide createdAt Date for newsletter --- src/factory/admin/club/newsletter/newsletter.ts | 1 + src/viewmodel/admin/club/newsletter/newsletter.models.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/src/factory/admin/club/newsletter/newsletter.ts b/src/factory/admin/club/newsletter/newsletter.ts index 6cc9685..f47b6eb 100644 --- a/src/factory/admin/club/newsletter/newsletter.ts +++ b/src/factory/admin/club/newsletter/newsletter.ts @@ -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, }; } diff --git a/src/viewmodel/admin/club/newsletter/newsletter.models.ts b/src/viewmodel/admin/club/newsletter/newsletter.models.ts index 3a1c47f..09c7d40 100644 --- a/src/viewmodel/admin/club/newsletter/newsletter.models.ts +++ b/src/viewmodel/admin/club/newsletter/newsletter.models.ts @@ -7,4 +7,5 @@ export interface NewsletterViewModel { newsletterSignatur: string; isSent: boolean; recipientsByQueryId?: string; + createdAt: Date; } From 573f92d098faa1be28c85d2d09228eb5cca4a915 Mon Sep 17 00:00:00 2001 From: Julian Krauser Date: Fri, 30 May 2025 15:13:38 +0200 Subject: [PATCH 05/11] enhance: add membership total view --- src/controller/admin/club/memberController.ts | 13 ++++ src/data-source.ts | 5 +- src/factory/admin/club/member/membership.ts | 22 +++++- src/helpers/dynamicQueryBuilder.ts | 1 + .../1748607842929-addMembershipTotalView.ts | 21 ++++++ src/migrations/baseSchemaTables/member.ts | 73 +++++++++++++++++++ src/routes/admin/club/member.ts | 5 ++ src/service/club/member/membershipService.ts | 21 +++++- .../admin/club/member/membership.models.ts | 12 +++ src/views/membershipsView.ts | 49 +++++++++++++ 10 files changed, 219 insertions(+), 3 deletions(-) create mode 100644 src/migrations/1748607842929-addMembershipTotalView.ts diff --git a/src/controller/admin/club/memberController.ts b/src/controller/admin/club/memberController.ts index c0b2cd7..bbdf16a 100644 --- a/src/controller/admin/club/memberController.ts +++ b/src/controller/admin/club/memberController.ts @@ -195,6 +195,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 { + 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 diff --git a/src/data-source.ts b/src/data-source.ts index bb96819..f436bf4 100644 --- a/src/data-source.ts +++ b/src/data-source.ts @@ -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"; @@ -57,6 +57,7 @@ import { MemberCreatedAt1746006549262 } from "./migrations/1746006549262-memberC 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"; configCheck(); @@ -109,6 +110,7 @@ const dataSource = new DataSource({ memberExecutivePositionsView, memberQualificationsView, membershipView, + membershipTotalView, webapi, webapiPermission, setting, @@ -125,6 +127,7 @@ const dataSource = new DataSource({ MemberCreatedAt1746006549262, UserLoginRoutine1746252454922, AddDateToNewsletter1748509435932, + AddMembershipTotalView1748607842929, ], migrationsRun: true, migrationsTransactionMode: "each", diff --git a/src/factory/admin/club/member/membership.ts b/src/factory/admin/club/member/membership.ts index b71d56c..a57063e 100644 --- a/src/factory/admin/club/member/membership.ts +++ b/src/factory/admin/club/member/membership.ts @@ -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} records diff --git a/src/helpers/dynamicQueryBuilder.ts b/src/helpers/dynamicQueryBuilder.ts index 3c6d4e6..ef9e97e 100644 --- a/src/helpers/dynamicQueryBuilder.ts +++ b/src/helpers/dynamicQueryBuilder.ts @@ -22,6 +22,7 @@ export default abstract class DynamicQueryBuilder { "memberExecutivePositionsView", "memberQualificationsView", "membershipView", + "membershipTotalView", ]; public static getTableMeta(tableName: string): TableMeta { diff --git a/src/migrations/1748607842929-addMembershipTotalView.ts b/src/migrations/1748607842929-addMembershipTotalView.ts new file mode 100644 index 0000000..97cfea0 --- /dev/null +++ b/src/migrations/1748607842929-addMembershipTotalView.ts @@ -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 { + 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 { + await queryRunner.dropView("membership_total_view"); + } +} diff --git a/src/migrations/baseSchemaTables/member.ts b/src/migrations/baseSchemaTables/member.ts index c5f304b..8ef428c 100644 --- a/src/migrations/baseSchemaTables/member.ts +++ b/src/migrations/baseSchemaTables/member.ts @@ -529,3 +529,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 + `, +}); diff --git a/src/routes/admin/club/member.ts b/src/routes/admin/club/member.ts index 804b595..a2b653a 100644 --- a/src/routes/admin/club/member.ts +++ b/src/routes/admin/club/member.ts @@ -26,6 +26,7 @@ import { getMembershipByMemberAndRecord, getMembershipsByMember, getMembershipStatisticsById, + getMembershipTotalStatisticsById, getMemberStatisticsById, getQualificationByMemberAndRecord, getQualificationsByMember, @@ -72,6 +73,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); }); diff --git a/src/service/club/member/membershipService.ts b/src/service/club/member/membershipService.ts index f5e7a07..d423b13 100644 --- a/src/service/club/member/membershipService.ts +++ b/src/service/club/member/membershipService.ts @@ -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>} + */ + static async getTotalStatisticsById(memberId: string): Promise { + 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); + }); + } } diff --git a/src/viewmodel/admin/club/member/membership.models.ts b/src/viewmodel/admin/club/member/membership.models.ts index ace02ea..a7a2256 100644 --- a/src/viewmodel/admin/club/member/membership.models.ts +++ b/src/viewmodel/admin/club/member/membership.models.ts @@ -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; +} diff --git a/src/views/membershipsView.ts b/src/views/membershipsView.ts index fc38c23..7807f8e 100644 --- a/src/views/membershipsView.ts +++ b/src/views/membershipsView.ts @@ -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; +} From 5368a96d0fef2f10403368f7daff55b810524dad Mon Sep 17 00:00:00 2001 From: Julian Krauser Date: Sat, 31 May 2025 07:32:25 +0200 Subject: [PATCH 06/11] fix: save false value to database --- src/helpers/settingsHelper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/helpers/settingsHelper.ts b/src/helpers/settingsHelper.ts index aea7584..d4c21d8 100644 --- a/src/helpers/settingsHelper.ts +++ b/src/helpers/settingsHelper.ts @@ -76,7 +76,7 @@ export default abstract class SettingHelper { * @param value The value to set */ public static async setSetting(key: K, value: SettingValueMapping[K]): Promise { - if (value === undefined || value === null || value == "") { + if (value === undefined || value === null || value === "") { if (key != "mail.password") this.resetSetting(key); return; } From fded8a663ab401bac1f85c2da6267b3df7de4d23 Mon Sep 17 00:00:00 2001 From: Julian Krauser Date: Tue, 3 Jun 2025 15:20:46 +0200 Subject: [PATCH 07/11] education controller, service, command --- src/command/club/member/memberCommand.ts | 2 + .../club/member/memberCommandHandler.ts | 2 + .../club/member/memberEducationCommand.ts | 23 ++++ .../member/memberEducationCommandHandler.ts | 82 +++++++++++++ .../education/educationCommand.ts | 14 +++ .../education/educationCommandHandler.ts | 68 +++++++++++ src/controller/admin/club/memberController.ts | 114 ++++++++++++++++++ .../configuration/educationController.ts | 91 ++++++++++++++ src/data-source.ts | 6 + src/entity/club/member/member.ts | 7 ++ src/entity/club/member/memberEducations.ts | 43 +++++++ src/entity/configuration/education.ts | 17 +++ src/factory/admin/club/member/member.ts | 3 +- .../admin/club/member/memberEducation.ts | 30 +++++ src/factory/admin/configuration/education.ts | 26 ++++ src/helpers/backupHelper.ts | 43 ++++++- src/helpers/dynamicQueryBuilder.ts | 10 +- .../1748953828644-memberExtendData.ts | 34 ++++++ src/migrations/baseSchemaTables/member.ts | 41 +++++++ src/routes/admin/club/member.ts | 37 ++++++ src/routes/admin/configuration/education.ts | 45 +++++++ src/routes/admin/index.ts | 9 ++ .../club/member/memberEducationService.ts | 49 ++++++++ src/service/configuration/education.ts | 41 +++++++ src/type/permissionTypes.ts | 3 + .../admin/club/member/member.models.ts | 1 + .../club/member/memberEducation.models.ts | 26 ++++ .../admin/configuration/education.models.ts | 16 +++ src/views/memberView.ts | 6 +- 29 files changed, 884 insertions(+), 5 deletions(-) create mode 100644 src/command/club/member/memberEducationCommand.ts create mode 100644 src/command/club/member/memberEducationCommandHandler.ts create mode 100644 src/command/configuration/education/educationCommand.ts create mode 100644 src/command/configuration/education/educationCommandHandler.ts create mode 100644 src/controller/admin/configuration/educationController.ts create mode 100644 src/entity/club/member/memberEducations.ts create mode 100644 src/entity/configuration/education.ts create mode 100644 src/factory/admin/club/member/memberEducation.ts create mode 100644 src/factory/admin/configuration/education.ts create mode 100644 src/migrations/1748953828644-memberExtendData.ts create mode 100644 src/routes/admin/configuration/education.ts create mode 100644 src/service/club/member/memberEducationService.ts create mode 100644 src/service/configuration/education.ts create mode 100644 src/viewmodel/admin/club/member/memberEducation.models.ts create mode 100644 src/viewmodel/admin/configuration/education.models.ts diff --git a/src/command/club/member/memberCommand.ts b/src/command/club/member/memberCommand.ts index f5c03d1..6d2767e 100644 --- a/src/command/club/member/memberCommand.ts +++ b/src/command/club/member/memberCommand.ts @@ -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 { diff --git a/src/command/club/member/memberCommandHandler.ts b/src/command/club/member/memberCommandHandler.ts index 78f1da0..128605d 100644 --- a/src/command/club/member/memberCommandHandler.ts +++ b/src/command/club/member/memberCommandHandler.ts @@ -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() diff --git a/src/command/club/member/memberEducationCommand.ts b/src/command/club/member/memberEducationCommand.ts new file mode 100644 index 0000000..be28106 --- /dev/null +++ b/src/command/club/member/memberEducationCommand.ts @@ -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; +} diff --git a/src/command/club/member/memberEducationCommandHandler.ts b/src/command/club/member/memberEducationCommandHandler.ts new file mode 100644 index 0000000..10f0311 --- /dev/null +++ b/src/command/club/member/memberEducationCommandHandler.ts @@ -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} + */ + static async create(createMemberEducation: CreateMemberEducationCommand): Promise { + 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} + */ + static async update(updateMemberEducation: UpdateMemberEducationCommand): Promise { + 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} + */ + static async delete(deleteMemberEducation: DeleteMemberEducationCommand): Promise { + 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); + }); + } +} diff --git a/src/command/configuration/education/educationCommand.ts b/src/command/configuration/education/educationCommand.ts new file mode 100644 index 0000000..c0b10ad --- /dev/null +++ b/src/command/configuration/education/educationCommand.ts @@ -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; +} diff --git a/src/command/configuration/education/educationCommandHandler.ts b/src/command/configuration/education/educationCommandHandler.ts new file mode 100644 index 0000000..0c1eee9 --- /dev/null +++ b/src/command/configuration/education/educationCommandHandler.ts @@ -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} + */ + static async create(createEducation: CreateEducationCommand): Promise { + 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} + */ + static async update(updateEducation: UpdateEducationCommand): Promise { + 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} + */ + static async delete(deleteEducation: DeleteEducationCommand): Promise { + return await dataSource + .createQueryBuilder() + .delete() + .from(education) + .where("id = :id", { id: deleteEducation.id }) + .execute() + .then(() => {}) + .catch((err) => { + throw new DatabaseActionException("DELETE", "education", err); + }); + } +} diff --git a/src/controller/admin/club/memberController.ts b/src/controller/admin/club/memberController.ts index bbdf16a..4c128c5 100644 --- a/src/controller/admin/club/memberController.ts +++ b/src/controller/admin/club/memberController.ts @@ -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 @@ -276,6 +284,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 { + 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 { + 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 @@ -343,6 +378,7 @@ export async function createMember(req: Request, res: Response): Promise { 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, @@ -351,6 +387,7 @@ export async function createMember(req: Request, res: Response): Promise { nameaffix, birthdate, internalId, + note, }; let memberId = await MemberCommandHandler.create(createMember); @@ -426,6 +463,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 { + 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 @@ -504,6 +568,7 @@ export async function updateMemberById(req: Request, res: Response): Promise} + */ +export async function updateEducationOfMember(req: Request, res: Response): Promise { + 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 @@ -742,6 +837,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 { + 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 diff --git a/src/controller/admin/configuration/educationController.ts b/src/controller/admin/configuration/educationController.ts new file mode 100644 index 0000000..b8f2e70 --- /dev/null +++ b/src/controller/admin/configuration/educationController.ts @@ -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 { + 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 { + 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 { + 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 { + 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 { + const id = parseInt(req.params.id); + + let deleteEducation: DeleteEducationCommand = { + id: id, + }; + await EducationCommandHandler.delete(deleteEducation); + + res.sendStatus(204); +} diff --git a/src/data-source.ts b/src/data-source.ts index f436bf4..7f8a9e0 100644 --- a/src/data-source.ts +++ b/src/data-source.ts @@ -58,6 +58,9 @@ import { UserLoginRoutine1746252454922 } from "./migrations/1746252454922-UserLo 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(); @@ -84,6 +87,7 @@ const dataSource = new DataSource({ communicationType, executivePosition, membershipStatus, + education, qualification, salutation, member, @@ -91,6 +95,7 @@ const dataSource = new DataSource({ memberExecutivePositions, memberQualifications, membership, + memberEducations, protocol, protocolAgenda, protocolDecision, @@ -128,6 +133,7 @@ const dataSource = new DataSource({ UserLoginRoutine1746252454922, AddDateToNewsletter1748509435932, AddMembershipTotalView1748607842929, + MemberExtendData1748953828644, ], migrationsRun: true, migrationsTransactionMode: "each", diff --git a/src/entity/club/member/member.ts b/src/entity/club/member/member.ts index e724155..5f349c3 100644 --- a/src/entity/club/member/member.ts +++ b/src/entity/club/member/member.ts @@ -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; diff --git a/src/entity/club/member/memberEducations.ts b/src/entity/club/member/memberEducations.ts new file mode 100644 index 0000000..614d404 --- /dev/null +++ b/src/entity/club/member/memberEducations.ts @@ -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; +} diff --git a/src/entity/configuration/education.ts b/src/entity/configuration/education.ts new file mode 100644 index 0000000..e380570 --- /dev/null +++ b/src/entity/configuration/education.ts @@ -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[]; +} diff --git a/src/factory/admin/club/member/member.ts b/src/factory/admin/club/member/member.ts index 246c0ae..9dd157f 100644 --- a/src/factory/admin/club/member/member.ts +++ b/src/factory/admin/club/member/member.ts @@ -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, diff --git a/src/factory/admin/club/member/memberEducation.ts b/src/factory/admin/club/member/memberEducation.ts new file mode 100644 index 0000000..407100f --- /dev/null +++ b/src/factory/admin/club/member/memberEducation.ts @@ -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} records + * @returns {Array} + */ + public static mapToBase(records: Array): Array { + return records.map((r) => this.mapToSingle(r)); + } +} diff --git a/src/factory/admin/configuration/education.ts b/src/factory/admin/configuration/education.ts new file mode 100644 index 0000000..c130a6e --- /dev/null +++ b/src/factory/admin/configuration/education.ts @@ -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} records + * @returns {Array} + */ + public static mapToBase(records: Array): Array { + return records.map((r) => this.mapToSingle(r)); + } +} diff --git a/src/helpers/backupHelper.ts b/src/helpers/backupHelper.ts index 2f737ad..42632df 100644 --- a/src/helpers/backupHelper.ts +++ b/src/helpers/backupHelper.ts @@ -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 }> { @@ -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> { @@ -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, collectedIds: boolean): Promise { let members = await this.transactionManager.getRepository("member").find(); diff --git a/src/helpers/dynamicQueryBuilder.ts b/src/helpers/dynamicQueryBuilder.ts index ef9e97e..dd5461d 100644 --- a/src/helpers/dynamicQueryBuilder.ts +++ b/src/helpers/dynamicQueryBuilder.ts @@ -17,6 +17,7 @@ export default abstract class DynamicQueryBuilder { "memberAwards", "memberExecutivePositions", "memberQualifications", + "memberEducations", "membership", "memberView", "memberExecutivePositionsView", @@ -293,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} `; @@ -302,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) : ""; } }); } diff --git a/src/migrations/1748953828644-memberExtendData.ts b/src/migrations/1748953828644-memberExtendData.ts new file mode 100644 index 0000000..4e53466 --- /dev/null +++ b/src/migrations/1748953828644-memberExtendData.ts @@ -0,0 +1,34 @@ +import { MigrationInterface, QueryRunner, TableColumn } from "typeorm"; +import { + education_table, + member_educations_table, + member_view_mysql, + member_view_postgres, + member_view_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 { + 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_view_postgres, true); + else if (DB_TYPE == "mysql") await queryRunner.createView(member_view_mysql, true); + else if (DB_TYPE == "sqlite") await queryRunner.createView(member_view_sqlite, true); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropTable(member_educations_table, true, true, true); + await queryRunner.dropTable(education_table, true, true, true); + await queryRunner.dropColumn("member", "note"); + } +} diff --git a/src/migrations/baseSchemaTables/member.ts b/src/migrations/baseSchemaTables/member.ts index 8ef428c..4eacb7e 100644 --- a/src/migrations/baseSchemaTables/member.ts +++ b/src/migrations/baseSchemaTables/member.ts @@ -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", @@ -229,6 +267,7 @@ export const member_view_mysql = new View({ 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\`, @@ -258,6 +297,7 @@ export const member_view_postgres = new View({ 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", @@ -277,6 +317,7 @@ export const member_view_sqlite = new View({ 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, diff --git a/src/routes/admin/club/member.ts b/src/routes/admin/club/member.ts index a2b653a..4ceb5f5 100644 --- a/src/routes/admin/club/member.ts +++ b/src/routes/admin/club/member.ts @@ -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, @@ -32,6 +36,7 @@ import { getQualificationsByMember, updateAwardOfMember, updateCommunicationOfMember, + updateEducationOfMember, updateExecutivePositionOfMember, updateMemberById, updateMembershipOfMember, @@ -97,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); }); @@ -145,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"), @@ -193,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"), @@ -241,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"), diff --git a/src/routes/admin/configuration/education.ts b/src/routes/admin/configuration/education.ts new file mode 100644 index 0000000..8a07628 --- /dev/null +++ b/src/routes/admin/configuration/education.ts @@ -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; diff --git a/src/routes/admin/index.ts b/src/routes/admin/index.ts index 4e74012..b306bfc 100644 --- a/src/routes/admin/index.ts +++ b/src/routes/admin/index.ts @@ -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([ diff --git a/src/service/club/member/memberEducationService.ts b/src/service/club/member/memberEducationService.ts new file mode 100644 index 0000000..5b98032 --- /dev/null +++ b/src/service/club/member/memberEducationService.ts @@ -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>} + */ + static async getAll(memberId: string): Promise> { + 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>} + */ + static async getById(memberId: string, recordId: number): Promise { + 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); + }); + } +} diff --git a/src/service/configuration/education.ts b/src/service/configuration/education.ts new file mode 100644 index 0000000..b621db3 --- /dev/null +++ b/src/service/configuration/education.ts @@ -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>} + */ + static async getAll(): Promise> { + 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} + */ + static async getById(id: number): Promise { + 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); + }); + } +} diff --git a/src/type/permissionTypes.ts b/src/type/permissionTypes.ts index eff7fd1..013615c 100644 --- a/src/type/permissionTypes.ts +++ b/src/type/permissionTypes.ts @@ -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 = [ "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", diff --git a/src/viewmodel/admin/club/member/member.models.ts b/src/viewmodel/admin/club/member/member.models.ts index 2a0f37b..3745d66 100644 --- a/src/viewmodel/admin/club/member/member.models.ts +++ b/src/viewmodel/admin/club/member/member.models.ts @@ -15,6 +15,7 @@ export interface MemberViewModel { sendNewsletter?: CommunicationViewModel; smsAlarming?: Array; preferredCommunication?: Array; + note?: string; } export interface MemberStatisticsViewModel { diff --git a/src/viewmodel/admin/club/member/memberEducation.models.ts b/src/viewmodel/admin/club/member/memberEducation.models.ts new file mode 100644 index 0000000..4013bce --- /dev/null +++ b/src/viewmodel/admin/club/member/memberEducation.models.ts @@ -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; +} diff --git a/src/viewmodel/admin/configuration/education.models.ts b/src/viewmodel/admin/configuration/education.models.ts new file mode 100644 index 0000000..9ccab1f --- /dev/null +++ b/src/viewmodel/admin/configuration/education.models.ts @@ -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; +} diff --git a/src/views/memberView.ts b/src/views/memberView.ts index 194bade..260e0ff 100644 --- a/src/views/memberView.ts +++ b/src/views/memberView.ts @@ -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; From 38bfa3dd759c0f19e2f0b5eb7b826fd9cf5e89b2 Mon Sep 17 00:00:00 2001 From: Julian Krauser Date: Tue, 3 Jun 2025 15:32:41 +0200 Subject: [PATCH 08/11] member print --- src/controller/admin/club/memberController.ts | 2 ++ src/templates/member.body.template.html | 15 ++++++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/controller/admin/club/memberController.ts b/src/controller/admin/club/memberController.ts index 4c128c5..6301cec 100644 --- a/src/controller/admin/club/memberController.ts +++ b/src/controller/admin/club/memberController.ts @@ -152,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", @@ -165,6 +166,7 @@ export async function getMemberPrintoutById(req: Request, res: Response): Promis qualifications, positions, communications, + educations, }, }); diff --git a/src/templates/member.body.template.html b/src/templates/member.body.template.html index 67f8821..6fd781c 100644 --- a/src/templates/member.body.template.html +++ b/src/templates/member.body.template.html @@ -58,7 +58,7 @@
{{/each}} {{/if}} {{#if qualifications.length}}
-

Qualifikationen

+

Qualifikationen / Funktionen

{{#each qualifications}}

{{this.qualification.qualification}}: {{date this.date}}

@@ -69,6 +69,19 @@ {{/if}}

+ {{/each}} {{/if}} {{#if educations.length}} +
+

Aus-/Fortbildungen

+ {{#each educations}} +
+

{{this.education.education}}: {{date this.start}}{{#if this.end}} bis {{date this.end}}{{/if}}

+ {{#if this.place}} +

Ausbildungsort: {{this.place}}

+ {{/if}}{{#if this.note}} +

Notiz: {{this.note}}

+ {{/if}} +
+
{{/each}} {{/if}}