diff --git a/src/command/club/protocol/protocolAgendaCommand.ts b/src/command/club/protocol/protocolAgendaCommand.ts index 749390d..2889122 100644 --- a/src/command/club/protocol/protocolAgendaCommand.ts +++ b/src/command/club/protocol/protocolAgendaCommand.ts @@ -2,5 +2,6 @@ export interface SynchronizeProtocolAgendaCommand { id?: number; topic: string; context: string; + sort?: number; protocolId: number; } diff --git a/src/command/club/protocol/protocolAgendaCommandHandler.ts b/src/command/club/protocol/protocolAgendaCommandHandler.ts index 02fe4f2..96dbc08 100644 --- a/src/command/club/protocol/protocolAgendaCommandHandler.ts +++ b/src/command/club/protocol/protocolAgendaCommandHandler.ts @@ -2,6 +2,7 @@ import { dataSource } from "../../../data-source"; import { protocolAgenda } from "../../../entity/club/protocol/protocolAgenda"; import DatabaseActionException from "../../../exceptions/databaseActionException"; import InternalException from "../../../exceptions/internalException"; +import ProtocolAgendaService from "../../../service/club/protocol/protocolAgendaService"; import { SynchronizeProtocolAgendaCommand } from "./protocolAgendaCommand"; export default abstract class ProtocolAgendaCommandHandler { @@ -11,6 +12,7 @@ export default abstract class ProtocolAgendaCommandHandler { * @returns {Promise} */ static async create(protocolId: number): Promise { + let count = await ProtocolAgendaService.getInstanceCount(protocolId); return await dataSource .createQueryBuilder() .insert() @@ -18,6 +20,7 @@ export default abstract class ProtocolAgendaCommandHandler { .values({ topic: "", context: "", + sort: count, protocolId, }) .execute() @@ -40,7 +43,7 @@ export default abstract class ProtocolAgendaCommandHandler { .insert() .into(protocolAgenda) .values(syncProtocolAgenda) - .orUpdate(["topic", "context"], ["id"]) + .orUpdate(["topic", "context", "sort"], ["id"]) .execute() .then(() => {}) .catch((err) => { diff --git a/src/command/club/protocol/protocolDecisionCommand.ts b/src/command/club/protocol/protocolDecisionCommand.ts index 61eb6f0..aeb2f24 100644 --- a/src/command/club/protocol/protocolDecisionCommand.ts +++ b/src/command/club/protocol/protocolDecisionCommand.ts @@ -2,5 +2,6 @@ export interface SynchronizeProtocolDecisionCommand { id?: number; topic: string; context: string; + sort?: number; protocolId: number; } diff --git a/src/command/club/protocol/protocolDecisionCommandHandler.ts b/src/command/club/protocol/protocolDecisionCommandHandler.ts index 585abf1..9425d38 100644 --- a/src/command/club/protocol/protocolDecisionCommandHandler.ts +++ b/src/command/club/protocol/protocolDecisionCommandHandler.ts @@ -2,6 +2,7 @@ import { dataSource } from "../../../data-source"; import { protocolDecision } from "../../../entity/club/protocol/protocolDecision"; import DatabaseActionException from "../../../exceptions/databaseActionException"; import InternalException from "../../../exceptions/internalException"; +import ProtocolDecisionService from "../../../service/club/protocol/protocolDecisionService"; import { SynchronizeProtocolDecisionCommand } from "./protocolDecisionCommand"; export default abstract class ProtocolDecisionCommandHandler { @@ -11,6 +12,7 @@ export default abstract class ProtocolDecisionCommandHandler { * @returns {Promise} */ static async create(protocolId: number): Promise { + let count = await ProtocolDecisionService.getInstanceCount(protocolId); return await dataSource .createQueryBuilder() .insert() @@ -18,6 +20,7 @@ export default abstract class ProtocolDecisionCommandHandler { .values({ topic: "", context: "", + sort: count, protocolId, }) .execute() @@ -39,7 +42,7 @@ export default abstract class ProtocolDecisionCommandHandler { .insert() .into(protocolDecision) .values(syncProtocolDecisions) - .orUpdate(["topic", "context"], ["id"]) + .orUpdate(["topic", "context", "sort"], ["id"]) .execute() .then(() => {}) .catch((err) => { diff --git a/src/command/club/protocol/protocolVotingCommand.ts b/src/command/club/protocol/protocolVotingCommand.ts index a707b64..617a560 100644 --- a/src/command/club/protocol/protocolVotingCommand.ts +++ b/src/command/club/protocol/protocolVotingCommand.ts @@ -5,5 +5,6 @@ export interface SynchronizeProtocolVotingCommand { favour: number; abstain: number; against: number; + sort?: number; protocolId: number; } diff --git a/src/command/club/protocol/protocolVotingCommandHandler.ts b/src/command/club/protocol/protocolVotingCommandHandler.ts index 45a4642..741c0da 100644 --- a/src/command/club/protocol/protocolVotingCommandHandler.ts +++ b/src/command/club/protocol/protocolVotingCommandHandler.ts @@ -2,6 +2,7 @@ import { dataSource } from "../../../data-source"; import { protocolVoting } from "../../../entity/club/protocol/protocolVoting"; import DatabaseActionException from "../../../exceptions/databaseActionException"; import InternalException from "../../../exceptions/internalException"; +import ProtocolVotingService from "../../../service/club/protocol/protocolVotingService"; import { SynchronizeProtocolVotingCommand } from "./protocolVotingCommand"; export default abstract class ProtocolVotingCommandHandler { @@ -11,6 +12,7 @@ export default abstract class ProtocolVotingCommandHandler { * @returns {Promise} */ static async create(protocolId: number): Promise { + let count = await ProtocolVotingService.getInstanceCount(protocolId); return await dataSource .createQueryBuilder() .insert() @@ -18,6 +20,7 @@ export default abstract class ProtocolVotingCommandHandler { .values({ topic: "", context: "", + sort: count, protocolId, }) .execute() @@ -39,7 +42,7 @@ export default abstract class ProtocolVotingCommandHandler { .insert() .into(protocolVoting) .values(syncProtocolVotings) - .orUpdate(["topic", "context", "favour", "abstain", "against"], ["id"]) + .orUpdate(["topic", "context", "favour", "abstain", "against", "sort"], ["id"]) .execute() .then(() => {}) .catch((err) => { diff --git a/src/controller/admin/club/listprintController.ts b/src/controller/admin/club/listprintController.ts new file mode 100644 index 0000000..1159d9f --- /dev/null +++ b/src/controller/admin/club/listprintController.ts @@ -0,0 +1,60 @@ +import { Request, Response } from "express"; +import { PdfExport } from "../../../helpers/pdfExport"; +import DynamicQueryBuilder from "../../../helpers/dynamicQueryBuilder"; +import QueryStoreService from "../../../service/configuration/queryStoreService"; +import InternalException from "../../../exceptions/internalException"; + +/** + * @description print list by query and template + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function printListByQueryAndTemplate(req: Request, res: Response): Promise { + const title = req.body.title; + const queryStore = req.body.queryStore; + const headerId = req.body.headerId ?? null; + const bodyId = req.body.bodyId ?? null; + const footerId = req.body.footerId ?? null; + const headerHeight = req.body.headerHeight ?? null; + const footerHeight = req.body.footerHeight ?? null; + + let query = queryStore; + if (queryStore != "member" && queryStore != "memberByRunningMembership") { + query = (await QueryStoreService.getById(queryStore)).query; + } + + let data = await DynamicQueryBuilder.executeQuery({ + query: query.startsWith("{") ? JSON.parse(query) : query, + noLimit: true, + }); + + if (data.stats == "error") { + throw new InternalException("Failed executing Query"); + } + + let pdf = await PdfExport.renderFile({ + title: title, + template: "listprint", + saveToDisk: false, + data: { + today: new Date(), + list: data.rows, + }, + customTemplate: { + headerId, + footerId, + bodyId, + headerHeight, + footerHeight, + }, + }); + + 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); +} diff --git a/src/controller/admin/club/memberController.ts b/src/controller/admin/club/memberController.ts index 91c51f8..9ef0bf4 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 @@ -266,33 +305,6 @@ export async function getCommunicationByMemberAndRecord(req: Request, res: Respo res.json(CommunicationFactory.mapToSingle(communication)); } -/** - * @description create member printout list - * @param req {Request} Express req object - * @param res {Response} Express res object - * @returns {Promise<*>} - */ -export async function createMemberPrintoutList(req: Request, res: Response): Promise { - let members = await MemberService.getByRunningMembership(); - - let pdf = await PdfExport.renderFile({ - title: "Mitgliederliste", - template: "member.list", - saveToDisk: false, - data: { - member: members, - }, - }); - - 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 create member * @param req {Request} Express req object diff --git a/src/controller/admin/club/protocolController.ts b/src/controller/admin/club/protocolController.ts index 868715f..2a6f5fb 100644 --- a/src/controller/admin/club/protocolController.ts +++ b/src/controller/admin/club/protocolController.ts @@ -257,13 +257,13 @@ export async function createProtocolPrintoutById(req: Request, res: Response): P }), start: protocol.starttime, end: protocol.endtime, - agenda, - decisions, + agenda: agenda.sort((a, b) => a.sort - b.sort), + decisions: decisions.sort((a, b) => a.sort - b.sort), presence: presence.filter((p) => !p.absent).map((p) => p.member), absent: presence.filter((p) => p.absent).map((p) => ({ ...p.member, excused: p.excused })), excused_absent: presence.filter((p) => p.absent && p.excused).map((p) => p.member), unexcused_absent: presence.filter((p) => p.absent && !p.excused).map((p) => p.member), - votings, + votings: votings.sort((a, b) => a.sort - b.sort), }, }); @@ -320,6 +320,7 @@ export async function synchronizeProtocolAgendaById(req: Request, res: Response) id: a.id ?? null, topic: a.topic, context: a.context, + sort: a.sort, protocolId, }) ); @@ -343,6 +344,7 @@ export async function synchronizeProtocolDecisonsById(req: Request, res: Respons id: d.id ?? null, topic: d.topic, context: d.context, + sort: d.sort, protocolId, }) ); @@ -362,13 +364,14 @@ export async function synchronizeProtocolVotingsById(req: Request, res: Response let votings = req.body.votings as Array; let syncVoting: Array = votings.map( - (d: ProtocolVotingViewModel): SynchronizeProtocolVotingCommand => ({ - id: d.id ?? null, - topic: d.topic, - context: d.context, - favour: d.favour, - abstain: d.abstain, - against: d.abstain, + (v: ProtocolVotingViewModel): SynchronizeProtocolVotingCommand => ({ + id: v.id ?? null, + topic: v.topic, + context: v.context, + favour: v.favour, + abstain: v.abstain, + against: v.abstain, + sort: v.sort, protocolId, }) ); diff --git a/src/data-source.ts b/src/data-source.ts index 6babcb1..aa99610 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 { TemplatesAndProtocolSort1742549956787 } from "./migrations/1742549956787-templatesAndProtocolSort"; 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, TemplatesAndProtocolSort1742549956787], 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/demodata/member.list.data.ts b/src/demodata/member.list.data.ts deleted file mode 100644 index 7244fd9..0000000 --- a/src/demodata/member.list.data.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { member } from "../entity/club/member/member"; -import { protocolAgenda } from "../entity/club/protocol/protocolAgenda"; -import { protocolDecision } from "../entity/club/protocol/protocolDecision"; -import { protocolVoting } from "../entity/club/protocol/protocolVoting"; - -export const memberlistDemoData: { member: Array> } = { - member: [ - { - firstname: "Julian", - lastname: "Krauser", - }, - ] -} diff --git a/src/entity/club/protocol/protocolAgenda.ts b/src/entity/club/protocol/protocolAgenda.ts index 7ce7556..a64bf9a 100644 --- a/src/entity/club/protocol/protocolAgenda.ts +++ b/src/entity/club/protocol/protocolAgenda.ts @@ -12,6 +12,9 @@ export class protocolAgenda { @Column({ type: "text", default: "" }) context: string; + @Column({ type: "int", default: 0 }) + sort: number; + @Column() protocolId: number; diff --git a/src/entity/club/protocol/protocolDecision.ts b/src/entity/club/protocol/protocolDecision.ts index 4978d17..e2cc4c1 100644 --- a/src/entity/club/protocol/protocolDecision.ts +++ b/src/entity/club/protocol/protocolDecision.ts @@ -12,6 +12,9 @@ export class protocolDecision { @Column({ type: "text", default: "" }) context: string; + @Column({ type: "int", default: 0 }) + sort: number; + @Column() protocolId: number; diff --git a/src/entity/club/protocol/protocolVoting.ts b/src/entity/club/protocol/protocolVoting.ts index 8e8f7a1..438e8fc 100644 --- a/src/entity/club/protocol/protocolVoting.ts +++ b/src/entity/club/protocol/protocolVoting.ts @@ -21,6 +21,9 @@ export class protocolVoting { @Column({ type: "int", default: 0 }) against: number; + @Column({ type: "int", default: 0 }) + sort: number; + @Column() protocolId: number; diff --git a/src/factory/admin/club/protocol/protocolAgenda.ts b/src/factory/admin/club/protocol/protocolAgenda.ts index aa15b75..65d2334 100644 --- a/src/factory/admin/club/protocol/protocolAgenda.ts +++ b/src/factory/admin/club/protocol/protocolAgenda.ts @@ -12,6 +12,7 @@ export default abstract class ProtocolAgendaFactory { id: record.id, topic: record.topic, context: record.context, + sort: record.sort, protocolId: record.protocolId, }; } diff --git a/src/factory/admin/club/protocol/protocolDecision.ts b/src/factory/admin/club/protocol/protocolDecision.ts index 0fbfff5..608bc2a 100644 --- a/src/factory/admin/club/protocol/protocolDecision.ts +++ b/src/factory/admin/club/protocol/protocolDecision.ts @@ -12,6 +12,7 @@ export default abstract class ProtocolDecisionFactory { id: record.id, topic: record.topic, context: record.context, + sort: record.sort, protocolId: record.protocolId, }; } diff --git a/src/factory/admin/club/protocol/protocolVoting.ts b/src/factory/admin/club/protocol/protocolVoting.ts index 2f4fa4e..49f954c 100644 --- a/src/factory/admin/club/protocol/protocolVoting.ts +++ b/src/factory/admin/club/protocol/protocolVoting.ts @@ -15,6 +15,7 @@ export default abstract class ProtocolVotingFactory { favour: record.favour, abstain: record.abstain, against: record.against, + sort: record.sort, protocolId: record.protocolId, }; } diff --git a/src/handlebars.config.ts b/src/handlebars.config.ts new file mode 100644 index 0000000..2cbded1 --- /dev/null +++ b/src/handlebars.config.ts @@ -0,0 +1,32 @@ +import Handlebars from "handlebars"; + +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", + }); +}); + +Handlebars.registerHelper("json", function (context) { + return JSON.stringify(context); +}); diff --git a/src/helpers/backupHelper.ts b/src/helpers/backupHelper.ts index a60e963..7727527 100644 --- a/src/helpers/backupHelper.ts +++ b/src/helpers/backupHelper.ts @@ -300,8 +300,8 @@ export default abstract class BackupHelper { .leftJoin("protocol.printouts", "printouts") .leftJoin("protocol.votings", "votings") .select(["protocol.title", "protocol.date", "protocol.starttime", "protocol.endtime", "protocol.summary"]) - .addSelect(["agendas.topic", "agendas.context"]) - .addSelect(["decisions.topic", "decisions.context"]) + .addSelect(["agendas.topic", "agendas.context", "agendas.sort"]) + .addSelect(["decisions.topic", "decisions.context", "decisions.sort"]) .addSelect(["presences.absent", "presences.excused"]) .addSelect([ ...(collectIds ? ["member.id"] : []), @@ -312,7 +312,14 @@ export default abstract class BackupHelper { "member.internalId", ]) .addSelect(["printouts.title", "printouts.iteration", "printouts.filename", "printouts.createdAt"]) - .addSelect(["votings.topic", "votings.context", "votings.favour", "votings.abstain", "votings.against"]) + .addSelect([ + "votings.topic", + "votings.context", + "votings.favour", + "votings.abstain", + "votings.against", + "votings.sort", + ]) .getMany(); } private static async getNewsletter(collectIds: boolean): Promise> { diff --git a/src/helpers/calendarHelper.ts b/src/helpers/calendarHelper.ts index e645d5b..7418089 100644 --- a/src/helpers/calendarHelper.ts +++ b/src/helpers/calendarHelper.ts @@ -50,7 +50,7 @@ export abstract class CalendarHelper { alarms: [ { action: "display", - description: "Erinnerung", + summary: "Erinnerung", trigger: { minutes: 30, before: true, diff --git a/src/helpers/demoDataHelper.ts b/src/helpers/demoDataHelper.ts index 3187ca1..b577eb7 100644 --- a/src/helpers/demoDataHelper.ts +++ b/src/helpers/demoDataHelper.ts @@ -1,17 +1,17 @@ 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 { 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; case "newsletter": return newsletterDemoData; - case "member.list": - return memberlistDemoData; + case "member": + return memberDemoData; default: return {}; } diff --git a/src/helpers/dynamicQueryBuilder.ts b/src/helpers/dynamicQueryBuilder.ts index 1749016..446fc22 100644 --- a/src/helpers/dynamicQueryBuilder.ts +++ b/src/helpers/dynamicQueryBuilder.ts @@ -314,6 +314,13 @@ export default abstract class DynamicQueryBuilder { count: number; } > { + if (query == "member") { + query = memberQuery; + } + if (query == "memberByRunningMembership") { + query = memberByRunningMembershipQuery; + } + if (typeof query == "string") { const upperQuery = query.trim().toUpperCase(); if (!upperQuery.startsWith("SELECT") || /INSERT|UPDATE|DELETE|ALTER|DROP|CREATE|TRUNCATE/.test(upperQuery)) { @@ -330,7 +337,7 @@ export default abstract class DynamicQueryBuilder { return await dataSource .transaction(async (manager) => { - data = await manager.query(query); + data = await manager.query(query.toString()); throw new Error("AllwaysRollbackQuery"); }) @@ -382,3 +389,29 @@ export default abstract class DynamicQueryBuilder { } } } + +const memberQuery: DynamicQueryStructure = { + select: "*", + table: "member", + orderBy: [ + { column: "lastname", order: "ASC" }, + { column: "firstname", order: "ASC" }, + ], +}; + +const memberByRunningMembershipQuery: DynamicQueryStructure = { + select: "*", + table: "member", + join: [ + { + select: "*", + table: "membership", + where: [{ structureType: "condition", concat: "_", operation: "null", column: "end", value: "" }], + foreignColumn: "memberships", + }, + ], + orderBy: [ + { column: "lastname", order: "ASC" }, + { column: "firstname", order: "ASC" }, + ], +}; diff --git a/src/helpers/pdfExport.ts b/src/helpers/pdfExport.ts index afae7bf..e59ba54 100644 --- a/src/helpers/pdfExport.ts +++ b/src/helpers/pdfExport.ts @@ -12,6 +12,7 @@ export abstract class PdfExport { data = {}, saveToDisk = true, folder = "", + customTemplate = undefined, }: { template: `${PermissionModule}` | `${PermissionModule}.${string}`; title?: string; @@ -19,16 +20,35 @@ export abstract class PdfExport { data?: any; saveToDisk?: boolean; folder?: string; + customTemplate?: { + headerId?: number; + footerId?: number; + bodyId?: string | number; + headerHeight: number; + footerHeight: number; + }; }) { if (folder != "") FileSystemHelper.createFolder(folder); - const { header, footer, body, headerMargin, footerMargin } = await TemplateHelper.renderFileForModule({ - module: template, - headerData: data, - bodyData: data, - footerData: data, - title: title, - }); + let header: string, footer: string, body: string, headerMargin: number, footerMargin: number; + if (!customTemplate) { + ({ header, footer, body, headerMargin, footerMargin } = await TemplateHelper.renderFileForModule({ + module: template, + headerData: data, + bodyData: data, + footerData: data, + title: title, + })); + } else { + ({ header, footer, body, headerMargin, footerMargin } = await TemplateHelper.renderFileForCustom({ + module: template, + customTemplate, + headerData: data, + bodyData: data, + footerData: data, + title: title, + })); + } const browser = await puppeteer.launch({ headless: true, diff --git a/src/helpers/templateHelper.ts b/src/helpers/templateHelper.ts index a8a14b1..e75c87c 100644 --- a/src/helpers/templateHelper.ts +++ b/src/helpers/templateHelper.ts @@ -6,7 +6,13 @@ import { FileSystemHelper } from "./fileSystemHelper"; export abstract class TemplateHelper { static getTemplateFromFile(template: string) { - return FileSystemHelper.readTemplateFile(`/src/templates/${template}.template.html`); + let tmpFile; + try { + tmpFile = FileSystemHelper.readTemplateFile(`/src/templates/${template}.template.html`); + } catch (err) { + tmpFile = FileSystemHelper.readTemplateFile(`/src/templates/${template.split(".")[1]}.template.html`); + } + return tmpFile; } static async getTemplateFromStore(templateId: number): Promise { @@ -56,14 +62,14 @@ export abstract class TemplateHelper { } else { footer = this.getTemplateFromFile(module + ".footer"); } - footer = this.applyDataToTemplate(footer, footerData); + footer = this.applyDataToTemplate(footer, { title, ...footerData }); if (moduleTemplate.bodyId) { body = await this.getTemplateFromStore(moduleTemplate.bodyId); } else { body = this.getTemplateFromFile(module + ".body"); } - body = this.applyDataToTemplate(body, bodyData); + body = this.applyDataToTemplate(body, { title, ...bodyData }); return { header, @@ -73,4 +79,57 @@ export abstract class TemplateHelper { footerMargin: moduleTemplate.footerHeight, }; } + + static async renderFileForCustom({ + module, + title = "pdf-export FF Admin", + headerData = {}, + bodyData = {}, + footerData = {}, + customTemplate, + }: { + module: `${PermissionModule}` | `${PermissionModule}.${string}`; + title?: string; + headerData?: any; + bodyData?: any; + footerData?: any; + customTemplate: { + headerId?: number; + footerId?: number; + bodyId?: string | number; + headerHeight: number; + footerHeight: number; + }; + }): Promise<{ header: string; body: string; footer: string; headerMargin?: number; footerMargin?: number }> { + let header = `

${title}

`; + let footer = ""; + let body = ""; + + if (customTemplate.headerId) { + header = await this.getTemplateFromStore(customTemplate.headerId); + header = this.applyDataToTemplate(header, { title, ...headerData }); + } + + if (customTemplate.footerId) { + footer = await this.getTemplateFromStore(customTemplate.footerId); + } else { + footer = this.getTemplateFromFile(module + ".footer"); + } + footer = this.applyDataToTemplate(footer, { title, ...footerData }); + + if (customTemplate.bodyId && typeof customTemplate.bodyId == "number") { + body = await this.getTemplateFromStore(customTemplate.bodyId); + } else { + body = this.getTemplateFromFile((customTemplate.bodyId || module) + ".body"); + } + body = this.applyDataToTemplate(body, { title, ...bodyData }); + + return { + header, + footer, + body, + headerMargin: customTemplate.headerHeight, + footerMargin: customTemplate.footerHeight, + }; + } } diff --git a/src/index.ts b/src/index.ts index 3517a6b..a282863 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,5 @@ import "dotenv/config"; +import "./handlebars.config"; import express from "express"; import { BACKUP_AUTO_RESTORE, configCheck, SERVER_PORT } from "./env.defaults"; diff --git a/src/migrations/1742549956787-templatesAndProtocolSort.ts b/src/migrations/1742549956787-templatesAndProtocolSort.ts new file mode 100644 index 0000000..5036920 --- /dev/null +++ b/src/migrations/1742549956787-templatesAndProtocolSort.ts @@ -0,0 +1,55 @@ +import { MigrationInterface, QueryRunner, TableColumn } from "typeorm"; +import { templateUsage } from "../entity/configuration/templateUsage"; +import { getTypeByORM, getDefaultByORM } from "./ormHelper"; + +export class TemplatesAndProtocolSort1742549956787 implements MigrationInterface { + name = "TemplatesAndProtocolSort1742549956787"; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.manager + .createQueryBuilder() + .insert() + .into(templateUsage) + .values([{ scope: "member" }]) + .orUpdate(["headerId", "bodyId", "footerId", "headerHeight", "footerHeight"], ["scope"]) + .execute(); + + await queryRunner.manager + .createQueryBuilder() + .delete() + .from(templateUsage) + .where({ scope: "member.list" }) + .execute(); + + await queryRunner.addColumn( + "protocol_agenda", + new TableColumn({ name: "sort", ...getTypeByORM("int"), default: getDefaultByORM("number", 0) }) + ); + + await queryRunner.addColumn( + "protocol_decision", + new TableColumn({ name: "sort", ...getTypeByORM("int"), default: getDefaultByORM("number", 0) }) + ); + + await queryRunner.addColumn( + "protocol_voting", + new TableColumn({ name: "sort", ...getTypeByORM("int"), default: getDefaultByORM("number", 0) }) + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropColumn("protocol_agenda", "sort"); + await queryRunner.dropColumn("protocol_decision", "sort"); + await queryRunner.dropColumn("protocol_voting", "sort"); + + await queryRunner.manager + .createQueryBuilder() + .insert() + .into(templateUsage) + .values([{ scope: "member.list" }]) + .orUpdate(["headerId", "bodyId", "footerId", "headerHeight", "footerHeight"], ["scope"]) + .execute(); + + await queryRunner.manager.createQueryBuilder().delete().from(templateUsage).where({ scope: "member" }).execute(); + } +} diff --git a/src/routes/admin/club/listprint.ts b/src/routes/admin/club/listprint.ts new file mode 100644 index 0000000..08e24d5 --- /dev/null +++ b/src/routes/admin/club/listprint.ts @@ -0,0 +1,10 @@ +import express, { Request, Response } from "express"; +import { printListByQueryAndTemplate } from "../../../controller/admin/club/listprintController"; + +var router = express.Router({ mergeParams: true }); + +router.post("/", async (req: Request, res: Response) => { + await printListByQueryAndTemplate(req, res); +}); + +export default router; diff --git a/src/routes/admin/club/member.ts b/src/routes/admin/club/member.ts index 5f9d032..efd6e05 100644 --- a/src/routes/admin/club/member.ts +++ b/src/routes/admin/club/member.ts @@ -6,7 +6,6 @@ import { addMembershipToMember, addQualificationToMember, createMember, - createMemberPrintoutList, deleteAwardOfMember, deleteCommunicationOfMember, deleteExecutivePositionOfMember, @@ -21,6 +20,7 @@ import { getExecutivePositionByMemberAndRecord, getExecutivePositionsByMember, getMemberById, + getMemberPrintoutById, getMembersByIds, getMembershipByMemberAndRecord, getMembershipsByMember, @@ -55,8 +55,8 @@ router.get("/:id/statistics", async (req: Request, res: Response) => { await getMemberStatisticsById(req, res); }); -router.get("/print/namelist", async (req: Request, res: Response) => { - await createMemberPrintoutList(req, res); +router.get("/:id/print", async (req: Request, res: Response) => { + await getMemberPrintoutById(req, res); }); router.get("/:memberId/memberships", async (req: Request, res: Response) => { diff --git a/src/routes/admin/index.ts b/src/routes/admin/index.ts index 3dddb4f..c2bab0f 100644 --- a/src/routes/admin/index.ts +++ b/src/routes/admin/index.ts @@ -19,6 +19,7 @@ import protocol from "./club/protocol"; import calendar from "./club/calendar"; import queryBuilder from "./club/queryBuilder"; import newsletter from "./club/newsletter"; +import listprint from "./club/listprint"; import role from "./management/role"; import user from "./management/user"; @@ -84,7 +85,14 @@ router.use( ]), calendarType ); -router.use("/querystore", PermissionHelper.passCheckMiddleware("read", "configuration", "query_store"), queryStore); +router.use( + "/querystore", + PermissionHelper.passCheckSomeMiddleware([ + { requiredPermissions: "read", section: "configuration", module: "query_store" }, + { requiredPermissions: "read", section: "club", module: "listprint" }, + ]), + queryStore +); router.use("/template", PermissionHelper.passCheckMiddleware("read", "configuration", "template"), template); router.use( "/templateusage", @@ -132,6 +140,7 @@ router.use( ]), newsletter ); +router.use("/listprint", PermissionHelper.passCheckMiddleware("read", "club", "listprint"), listprint); router.use("/role", PermissionHelper.passCheckMiddleware("read", "management", "role"), role); router.use( diff --git a/src/service/club/protocol/protocolAgendaService.ts b/src/service/club/protocol/protocolAgendaService.ts index fb8ead8..7d54168 100644 --- a/src/service/club/protocol/protocolAgendaService.ts +++ b/src/service/club/protocol/protocolAgendaService.ts @@ -39,4 +39,22 @@ export default abstract class ProtocolAgendaService { throw new DatabaseActionException("SELECT", "protocolAgenda", err); }); } + + /** + * @description get count of exisiting protocolAgenda by protocolId + * @returns {Promise} + */ + static async getInstanceCount(protocolId: number): Promise { + return await dataSource + .getRepository(protocolAgenda) + .createQueryBuilder("protocolAgenda") + .where({ protocolId }) + .getCount() + .then((res) => { + return res; + }) + .catch((err) => { + throw new DatabaseActionException("COUNT", "protocolAgenda", err); + }); + } } diff --git a/src/service/club/protocol/protocolDecisionService.ts b/src/service/club/protocol/protocolDecisionService.ts index 56ac4c6..241dba2 100644 --- a/src/service/club/protocol/protocolDecisionService.ts +++ b/src/service/club/protocol/protocolDecisionService.ts @@ -39,4 +39,22 @@ export default abstract class ProtocolDecisionService { throw new DatabaseActionException("SELECT", "protocolDecision", err); }); } + + /** + * @description get count of exisiting protocolDecision by protocolId + * @returns {Promise} + */ + static async getInstanceCount(protocolId: number): Promise { + return await dataSource + .getRepository(protocolDecision) + .createQueryBuilder("protocolDecisions") + .where({ protocolId }) + .getCount() + .then((res) => { + return res; + }) + .catch((err) => { + throw new DatabaseActionException("COUNT", "protocolDecision", err); + }); + } } diff --git a/src/service/club/protocol/protocolVotingService.ts b/src/service/club/protocol/protocolVotingService.ts index 2000cce..220d29e 100644 --- a/src/service/club/protocol/protocolVotingService.ts +++ b/src/service/club/protocol/protocolVotingService.ts @@ -39,4 +39,22 @@ export default abstract class ProtocolVotingService { throw new DatabaseActionException("SELECT", "protocolVoting", err); }); } + + /** + * @description get count of exisiting protocolVoting by protocolId + * @returns {Promise} + */ + static async getInstanceCount(protocolId: number): Promise { + return await dataSource + .getRepository(protocolVoting) + .createQueryBuilder("protocolVotings") + .where({ protocolId }) + .getCount() + .then((res) => { + return res; + }) + .catch((err) => { + throw new DatabaseActionException("COUNT", "protocolVoting", err); + }); + } } diff --git a/src/templates/member.list.footer.template.html b/src/templates/footer.template.html similarity index 100% rename from src/templates/member.list.footer.template.html rename to src/templates/footer.template.html diff --git a/src/templates/listprint.body.template.html b/src/templates/listprint.body.template.html new file mode 100644 index 0000000..2be7e77 --- /dev/null +++ b/src/templates/listprint.body.template.html @@ -0,0 +1,48 @@ + + + + + Listen-Druck + + +

{{title}}

+

Ausdruck Stand {{longdate today}}

+
+ + + {{#each list}} + + + + {{/each}} +
{{json this}}
+ + + diff --git a/src/templates/member.list.body.template.html b/src/templates/listprint.member.body.template.html similarity index 78% rename from src/templates/member.list.body.template.html rename to src/templates/listprint.member.body.template.html index cc0996c..a4e1840 100644 --- a/src/templates/member.list.body.template.html +++ b/src/templates/listprint.member.body.template.html @@ -6,15 +6,16 @@

Mitgliederliste

+

Ausdruck Stand {{longdate today}}


- {{#each member}} + {{#each list}} - + + {{/each}}
+ {{this.lastname}} {{this.firstname}}{{#if this.nameaffix}} - {{this.nameaffix}}{{/if}} - -
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/protocol.footer.template.html b/src/templates/protocol.footer.template.html deleted file mode 100644 index 7a3190b..0000000 --- a/src/templates/protocol.footer.template.html +++ /dev/null @@ -1,3 +0,0 @@ -
- Seite von -
diff --git a/src/type/permissionTypes.ts b/src/type/permissionTypes.ts index a031c0e..e9ed138 100644 --- a/src/type/permissionTypes.ts +++ b/src/type/permissionTypes.ts @@ -6,6 +6,7 @@ export type PermissionModule = | "newsletter" | "newsletter_config" | "protocol" + | "listprint" | "qualification" | "award" | "executive_position" @@ -50,6 +51,7 @@ export const permissionModules: Array = [ "newsletter", "newsletter_config", "protocol", + "listprint", "qualification", "award", "executive_position", @@ -68,7 +70,7 @@ export const permissionModules: Array = [ ]; export const permissionTypes: Array = ["read", "create", "update", "delete"]; export const sectionsAndModules: SectionsAndModulesObject = { - club: ["member", "calendar", "newsletter", "protocol", "query"], + club: ["member", "calendar", "newsletter", "protocol", "query", "listprint"], configuration: [ "qualification", "award", diff --git a/src/viewmodel/admin/club/protocol/protocolAgenda.models.ts b/src/viewmodel/admin/club/protocol/protocolAgenda.models.ts index 3a59327..eed423d 100644 --- a/src/viewmodel/admin/club/protocol/protocolAgenda.models.ts +++ b/src/viewmodel/admin/club/protocol/protocolAgenda.models.ts @@ -2,5 +2,6 @@ export interface ProtocolAgendaViewModel { id: number; topic: string; context: string; + sort: number; protocolId: number; } diff --git a/src/viewmodel/admin/club/protocol/protocolDecision.models.ts b/src/viewmodel/admin/club/protocol/protocolDecision.models.ts index 4a7212c..7de33c4 100644 --- a/src/viewmodel/admin/club/protocol/protocolDecision.models.ts +++ b/src/viewmodel/admin/club/protocol/protocolDecision.models.ts @@ -2,5 +2,6 @@ export interface ProtocolDecisionViewModel { id: number; topic: string; context: string; + sort: number; protocolId: number; } diff --git a/src/viewmodel/admin/club/protocol/protocolVoting.models.ts b/src/viewmodel/admin/club/protocol/protocolVoting.models.ts index 686f423..a5bd723 100644 --- a/src/viewmodel/admin/club/protocol/protocolVoting.models.ts +++ b/src/viewmodel/admin/club/protocol/protocolVoting.models.ts @@ -5,5 +5,6 @@ export interface ProtocolVotingViewModel { favour: number; abstain: number; against: number; + sort: number; protocolId: number; }