diff --git a/src/controller/admin/club/memberController.ts b/src/controller/admin/club/memberController.ts index 91c51f8..a31e514 100644 --- a/src/controller/admin/club/memberController.ts +++ b/src/controller/admin/club/memberController.ts @@ -118,6 +118,45 @@ export async function getMemberStatisticsById(req: Request, res: Response): Prom res.json(MemberFactory.mapToMemberStatistic(member)); } +/** + * @description get member printout by id + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function getMemberPrintoutById(req: Request, res: Response): Promise { + const memberId = req.params.id; + let member = await MemberService.getById(memberId); + let memberships = await MembershipService.getAll(memberId); + let awards = await MemberAwardService.getAll(memberId); + let qualifications = await MemberQualificationService.getAll(memberId); + let positions = await MemberExecutivePositionService.getAll(memberId); + let communications = await CommunicationService.getAll(memberId); + + let pdf = await PdfExport.renderFile({ + title: "Mitglieder-Ausdruck", + template: "member", + saveToDisk: false, + data: { + today: new Date(), + member, + memberships, + awards, + qualifications, + positions, + communications, + }, + }); + + let pdfbuffer = Buffer.from(pdf); + + res.setHeader("Content-Type", "application/pdf"); + res.setHeader("Content-Length", pdfbuffer.byteLength); + res.setHeader("Content-Disposition", "inline; filename=preview.pdf"); + + res.send(pdfbuffer); +} + /** * @description get memberships by member * @param req {Request} Express req object diff --git a/src/data-source.ts b/src/data-source.ts index 6babcb1..87af7a0 100644 --- a/src/data-source.ts +++ b/src/data-source.ts @@ -47,6 +47,7 @@ import { salutation } from "./entity/configuration/salutation"; import { BackupAndResetDatabase1738166124200 } from "./migrations/1738166124200-BackupAndResetDatabase"; import { CreateSchema1738166167472 } from "./migrations/1738166167472-CreateSchema"; +import { MemberPrintoutTemplates1742207245862 } from "./migrations/1742207245862-memberPrintoutTemplates"; const dataSource = new DataSource({ type: DB_TYPE as any, @@ -100,7 +101,7 @@ const dataSource = new DataSource({ webapi, webapiPermission, ], - migrations: [BackupAndResetDatabase1738166124200, CreateSchema1738166167472], + migrations: [BackupAndResetDatabase1738166124200, CreateSchema1738166167472, MemberPrintoutTemplates1742207245862], migrationsRun: true, migrationsTransactionMode: "each", subscribers: [], diff --git a/src/demodata/member.data.ts b/src/demodata/member.data.ts new file mode 100644 index 0000000..150ae00 --- /dev/null +++ b/src/demodata/member.data.ts @@ -0,0 +1,138 @@ +import { communication } from "../entity/club/member/communication"; +import { member } from "../entity/club/member/member"; +import { memberAwards } from "../entity/club/member/memberAwards"; +import { memberExecutivePositions } from "../entity/club/member/memberExecutivePositions"; +import { memberQualifications } from "../entity/club/member/memberQualifications"; +import { membership } from "../entity/club/member/membership"; + +export const memberDemoData: { + member: Partial; + today: string; + memberships: Array>; + awards: Array>; + qualifications: Array>; + positions: Array>; + communications: Array>; +} = { + today: "Montag, 17.03.2025", + member: { + id: "2fe205f8-8ae8-4218-839f-af3456d3f39d", + firstname: "Julian", + lastname: "Krauser", + nameaffix: "", + //@ts-ignore + birthdate: "2003-09-20", + internalId: "1312", + salutationId: 47, + salutation: { id: 47, salutation: "Herr", members: [] }, + firstMembershipEntry: { + id: 8681, + //@ts-ignore + start: "2017-11-13", + end: null, + terminationReason: null, + memberId: "2fe205f8-8ae8-4218-839f-af3456d3f39d", + statusId: 34, + status: { id: 34, status: "aktiv", memberships: [] }, + //@ts-ignore + member: {}, + }, + lastMembershipEntry: { + id: 8681, + //@ts-ignore + start: "2017-11-13", + end: null, + terminationReason: null, + memberId: "2fe205f8-8ae8-4218-839f-af3456d3f39d", + statusId: 34, + status: { id: 34, status: "aktiv", memberships: [] }, + //@ts-ignore + member: {}, + }, + preferredCommunication: [ + { + id: 7031, + preferred: true, + isSMSAlarming: false, + isSendNewsletter: true, + mobile: "", + email: "julian.krauser@jk-effects.com", + postalCode: "", + city: "", + street: "", + streetNumber: 0, + streetNumberAddition: "", + memberId: "2fe205f8-8ae8-4218-839f-af3456d3f39d", + typeId: 46, + type: { id: 46, type: "Email", useColumns: ["email"], communications: [] }, + // @ts-ignore + member: {}, + }, + ], + smsAlarming: [], + sendNewsletter: { + id: 7031, + preferred: true, + isSMSAlarming: false, + isSendNewsletter: true, + mobile: "", + email: "julian.krauser@jk-effects.com ", + postalCode: "", + city: "", + street: "", + streetNumber: 0, + streetNumberAddition: "", + memberId: "2fe205f8-8ae8-4218-839f-af3456d3f39d", + typeId: 46, + type: { id: 46, type: "Email", useColumns: ["email"], communications: [] }, + // @ts-ignore + member: {}, + }, + }, + memberships: [ + { + id: 8681, + //@ts-ignore + start: "2017-11-13", + end: null, + terminationReason: null, + memberId: "2fe205f8-8ae8-4218-839f-af3456d3f39d", + statusId: 34, + status: { id: 34, status: "aktiv", memberships: [] }, + }, + ], + awards: [], + qualifications: [], + positions: [ + { + id: 696, + note: "", + //@ts-ignore + start: "2025-01-06", + end: null, + memberId: "2fe205f8-8ae8-4218-839f-af3456d3f39d", + executivePositionId: 192, + executivePosition: { id: 192, position: "Schriftführer", members: [] }, + }, + ], + communications: [ + { + id: 7031, + preferred: true, + isSMSAlarming: false, + isSendNewsletter: true, + mobile: "", + email: "julian.krauser@jk-effects.com", + postalCode: "", + city: "", + street: "", + streetNumber: 0, + streetNumberAddition: "", + memberId: "2fe205f8-8ae8-4218-839f-af3456d3f39d", + typeId: 46, + //@ts-ignore + member: {}, + type: { id: 46, type: "Email", useColumns: ["email"], communications: [] }, + }, + ], +}; diff --git a/src/helpers/demoDataHelper.ts b/src/helpers/demoDataHelper.ts index 3187ca1..45020a4 100644 --- a/src/helpers/demoDataHelper.ts +++ b/src/helpers/demoDataHelper.ts @@ -1,10 +1,11 @@ import { newsletterDemoData } from "../demodata/newsletter.data"; import { protocolDemoData } from "../demodata/protocol.data"; import { PermissionModule } from "../type/permissionTypes"; -import {memberlistDemoData} from "../demodata/member.list.data"; +import { memberlistDemoData } from "../demodata/member.list.data"; +import { memberDemoData } from "../demodata/member.data"; export abstract class DemoDataHelper { - static getData(scope: `${PermissionModule}`|`${PermissionModule}.${string}`) { + static getData(scope: `${PermissionModule}` | `${PermissionModule}.${string}`) { switch (scope) { case "protocol": return protocolDemoData; @@ -12,6 +13,8 @@ export abstract class DemoDataHelper { return newsletterDemoData; case "member.list": return memberlistDemoData; + case "member": + return memberDemoData; default: return {}; } diff --git a/src/helpers/templateHelper.ts b/src/helpers/templateHelper.ts index a8a14b1..af1c832 100644 --- a/src/helpers/templateHelper.ts +++ b/src/helpers/templateHelper.ts @@ -4,6 +4,33 @@ import TemplateUsageService from "../service/configuration/templateUsageService" import Handlebars, { template } from "handlebars"; import { FileSystemHelper } from "./fileSystemHelper"; +Handlebars.registerHelper("date", function (aString) { + return new Date(aString).toLocaleDateString("de-DE", { + day: "2-digit", + month: "2-digit", + year: "numeric", + }); +}); + +Handlebars.registerHelper("longdate", function (aString) { + return new Date(aString).toLocaleDateString("de-DE", { + weekday: "long", + day: "2-digit", + month: "2-digit", + year: "numeric", + }); +}); + +Handlebars.registerHelper("datetime", function (aString) { + return new Date(aString).toLocaleDateString("de-DE", { + day: "2-digit", + month: "2-digit", + year: "numeric", + hour: "2-digit", + minute: "2-digit", + }); +}); + export abstract class TemplateHelper { static getTemplateFromFile(template: string) { return FileSystemHelper.readTemplateFile(`/src/templates/${template}.template.html`); diff --git a/src/migrations/1742207245862-memberPrintoutTemplates.ts b/src/migrations/1742207245862-memberPrintoutTemplates.ts new file mode 100644 index 0000000..a799d78 --- /dev/null +++ b/src/migrations/1742207245862-memberPrintoutTemplates.ts @@ -0,0 +1,20 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; +import { templateUsage } from "../entity/configuration/templateUsage"; + +export class MemberPrintoutTemplates1742207245862 implements MigrationInterface { + name = "MemberPrintoutTemplates1742207245862"; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.manager + .createQueryBuilder() + .insert() + .into(templateUsage) + .values([{ scope: "member" }]) + .orUpdate(["headerId", "bodyId", "footerId", "headerHeight", "footerHeight"], ["scope"]) + .execute(); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.manager.createQueryBuilder().delete().from(templateUsage).where({ scope: "member" }).execute(); + } +} diff --git a/src/routes/admin/club/member.ts b/src/routes/admin/club/member.ts index 5f9d032..541487e 100644 --- a/src/routes/admin/club/member.ts +++ b/src/routes/admin/club/member.ts @@ -21,6 +21,7 @@ import { getExecutivePositionByMemberAndRecord, getExecutivePositionsByMember, getMemberById, + getMemberPrintoutById, getMembersByIds, getMembershipByMemberAndRecord, getMembershipsByMember, @@ -55,6 +56,10 @@ router.get("/:id/statistics", async (req: Request, res: Response) => { await getMemberStatisticsById(req, res); }); +router.get("/:id/print", async (req: Request, res: Response) => { + await getMemberPrintoutById(req, res); +}); + router.get("/print/namelist", async (req: Request, res: Response) => { await createMemberPrintoutList(req, res); }); diff --git a/src/templates/member.body.template.html b/src/templates/member.body.template.html new file mode 100644 index 0000000..67f8821 --- /dev/null +++ b/src/templates/member.body.template.html @@ -0,0 +1,102 @@ + + + + + Mitglied + + +

+ {{member.salutation.salutation}} {{member.lastname}}, {{member.firstname}}{{#if member.nameaffix}} - + {{member.nameaffix}}{{/if}} +

+

Mitglieds-Ausdruck Stand {{longdate today}}

+
+ +

geboren: {{date member.birthdate}}

+

eingetreten: {{date member.firstMembershipEntry.start}}

+ {{#if member.lastMembershipEntry?.end}} +

ausgetreten: {{date member.lastMembershipEntry.end}}

+ {{/if}} {{#if memberships.length}} +
+

Mitgliedschaften

+ {{#each memberships}} +
+

{{this.status.status}}: {{date this.start}} bis {{#if this.end}}{{date this.end}}{{else}}heute{{/if}}

+ {{#if this.terminationReason}} +

beendet, weil:{{this.terminationReason}}

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

Vereinsämter

+ {{#each positions}} +
+

+ {{this.executivePosition.position}}: {{date this.start}} bis {{#if this.end}}{{date + this.end}}{{else}}heute{{/if}} +

+ {{#if this.note}} +

Notiz: {{this.note}}

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

Auszeichnungen

+ {{#each awards}} +
+

{{this.award.award}}: {{date this.date}}

+ {{#if this.given}} +

wurde vergeben

+ {{else}} +

wurde verwehrt / zurückgewiesen

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

Notiz: {{this.note}}

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

Qualifikationen

+ {{#each qualifications}} +
+

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

+ {{#if this.terminationReason}} +

beendet, weil:{{this.terminationReason}}

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

Notiz: {{this.note}}

+ {{/if}} +
+
+ {{/each}} {{/if}} + + + diff --git a/src/templates/member.footer.template.html b/src/templates/member.footer.template.html new file mode 100644 index 0000000..7a3190b --- /dev/null +++ b/src/templates/member.footer.template.html @@ -0,0 +1,3 @@ +
+ Seite von +