Merge pull request '#6-messages' (#24) from #6-messages into main

Reviewed-on: Ehrenamt/member-administration-server#24
This commit is contained in:
Julian Krauser 2024-12-31 13:25:11 +00:00
commit dc049c7e85
79 changed files with 3359 additions and 129 deletions

43
package-lock.json generated
View file

@ -20,6 +20,7 @@
"mysql": "^2.18.1",
"node-schedule": "^2.1.1",
"nodemailer": "^6.9.14",
"pdf-lib": "^1.17.1",
"puppeteer": "^23.11.1",
"qrcode": "^1.5.4",
"reflect-metadata": "^0.2.2",
@ -177,6 +178,24 @@
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/@pdf-lib/standard-fonts": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@pdf-lib/standard-fonts/-/standard-fonts-1.0.0.tgz",
"integrity": "sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA==",
"license": "MIT",
"dependencies": {
"pako": "^1.0.6"
}
},
"node_modules/@pdf-lib/upng": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@pdf-lib/upng/-/upng-1.0.1.tgz",
"integrity": "sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ==",
"license": "MIT",
"dependencies": {
"pako": "^1.0.10"
}
},
"node_modules/@pkgjs/parseargs": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
@ -2666,6 +2685,12 @@
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz",
"integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw=="
},
"node_modules/pako": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
"license": "(MIT AND Zlib)"
},
"node_modules/parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@ -2766,6 +2791,24 @@
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.2.0.tgz",
"integrity": "sha512-jczvQbCUS7XmS7o+y1aEO9OBVFeZBQ1MDSEqmO7xSoPgOPoowY/SxLpZ6Vh97/8qHZOteiCKb7gkG9gA2ZUxJA=="
},
"node_modules/pdf-lib": {
"version": "1.17.1",
"resolved": "https://registry.npmjs.org/pdf-lib/-/pdf-lib-1.17.1.tgz",
"integrity": "sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw==",
"license": "MIT",
"dependencies": {
"@pdf-lib/standard-fonts": "^1.0.0",
"@pdf-lib/upng": "^1.0.1",
"pako": "^1.0.11",
"tslib": "^1.11.1"
}
},
"node_modules/pdf-lib/node_modules/tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
"license": "0BSD"
},
"node_modules/pend": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",

View file

@ -35,6 +35,7 @@
"mysql": "^2.18.1",
"node-schedule": "^2.1.1",
"nodemailer": "^6.9.14",
"pdf-lib": "^1.17.1",
"puppeteer": "^23.11.1",
"qrcode": "^1.5.4",
"reflect-metadata": "^0.2.2",

View file

@ -0,0 +1,18 @@
export interface CreateNewsletterCommand {
title: string;
}
export interface SynchronizeNewsletterCommand {
id: number;
title: string;
description: string;
newsletterTitle: string;
newsletterText: string;
newsletterSignatur: string;
recipientsByQueryId?: number;
}
export interface SendNewsletterCommand {
id: number;
isSent: boolean;
}

View file

@ -0,0 +1,73 @@
import { dataSource } from "../data-source";
import { newsletter } from "../entity/newsletter";
import InternalException from "../exceptions/internalException";
import { CreateNewsletterCommand, SendNewsletterCommand, SynchronizeNewsletterCommand } from "./newsletterCommand";
export default abstract class NewsletterCommandHandler {
/**
* @description create newsletter
* @param CreateNewsletterCommand
* @returns {Promise<number>}
*/
static async create(createNewsletter: CreateNewsletterCommand): Promise<number> {
return await dataSource
.createQueryBuilder()
.insert()
.into(newsletter)
.values({
title: createNewsletter.title,
})
.execute()
.then((result) => {
return result.identifiers[0].id;
})
.catch((err) => {
throw new InternalException("Failed creating newsletter", err);
});
}
/**
* @description sync newsletter
* @param SynchronizeNewsletterCommand
* @returns {Promise<void>}
*/
static async sync(syncNewsletter: SynchronizeNewsletterCommand): Promise<void> {
return await dataSource
.createQueryBuilder()
.update(newsletter)
.set({
title: syncNewsletter.title,
description: syncNewsletter.description,
newsletterTitle: syncNewsletter.newsletterTitle,
newsletterText: syncNewsletter.newsletterText,
newsletterSignatur: syncNewsletter.newsletterSignatur,
recipientsByQueryId: syncNewsletter.recipientsByQueryId,
})
.where("id = :id", { id: syncNewsletter.id })
.execute()
.then(() => {})
.catch((err) => {
throw new InternalException("Failed synching newsletter", err);
});
}
/**
* @description send newsletter
* @param SendNewsletterCommand
* @returns {Promise<void>}
*/
static async send(syncNewsletter: SendNewsletterCommand): Promise<void> {
return await dataSource
.createQueryBuilder()
.update(newsletter)
.set({
isSent: syncNewsletter.isSent,
})
.where("id = :id", { id: syncNewsletter.id })
.execute()
.then(() => {})
.catch((err) => {
throw new InternalException("Failed setting newsletter send state", err);
});
}
}

View file

@ -0,0 +1,10 @@
import { NewsletterConfigType } from "../enums/newsletterConfigType";
export interface SetNewsletterConfigCommand {
comTypeId: number;
config: NewsletterConfigType;
}
export interface DeleteNewsletterConfigCommand {
comTypeId: number;
}

View file

@ -0,0 +1,47 @@
import { dataSource } from "../data-source";
import { newsletterConfig } from "../entity/newsletterConfig";
import InternalException from "../exceptions/internalException";
import { DeleteNewsletterConfigCommand, SetNewsletterConfigCommand } from "./newsletterConfigCommand";
export default abstract class NewsletterConfigCommandHandler {
/**
* @description set newsletterConfig
* @param SetNewsletterConfigCommand
* @returns {Promise<number>}
*/
static async set(setNewsletterConfig: SetNewsletterConfigCommand): Promise<number> {
return await dataSource
.createQueryBuilder()
.insert()
.into(newsletterConfig)
.values({
comTypeId: setNewsletterConfig.comTypeId,
config: setNewsletterConfig.config,
})
.orUpdate(["config"], "comTypeId")
.execute()
.then((result) => {
return result.identifiers[0].id;
})
.catch((err) => {
throw new InternalException("Failed setting newsletterConfig", err);
});
}
/**
* @description delete newsletterConfig
* @param number
* @returns {Promise<void>}
*/
static async delete(deleteNewsletterConfig: DeleteNewsletterConfigCommand): Promise<void> {
return await dataSource
.createQueryBuilder()
.delete()
.from(newsletterConfig)
.where("comTypeId = :comTypeId", { comTypeId: deleteNewsletterConfig.comTypeId })
.execute()
.then(() => {})
.catch((err) => {
throw new InternalException("Failed setting newsletterConfig", err);
});
}
}

View file

@ -0,0 +1,10 @@
export interface SynchronizeNewsletterDatesCommand {
newsletterId: number;
dates: Array<NewsletterDateCommand>;
}
export interface NewsletterDateCommand {
calendarId: string;
diffTitle?: string;
diffDescription?: string;
}

View file

@ -0,0 +1,97 @@
import { DeleteResult, EntityManager, InsertResult, UpdateResult } from "typeorm";
import { dataSource } from "../data-source";
import InternalException from "../exceptions/internalException";
import NewsletterDatesService from "../service/newsletterDatesService";
import { NewsletterDateCommand, SynchronizeNewsletterDatesCommand } from "./newsletterDatesCommand";
import { newsletterDates } from "../entity/newsletterDates";
export default abstract class NewsletterDatesCommandHandler {
/**
* @description sync newsletter dates
* @param {SynchronizeNewsletterDatesCommand} syncNewsletterDates
* @returns {Promise<void>}
*/
static async sync(syncNewsletterDates: SynchronizeNewsletterDatesCommand): Promise<void> {
let currentDates = await NewsletterDatesService.getAll(syncNewsletterDates.newsletterId);
return await dataSource.manager
.transaction(async (manager) => {
let newDates = syncNewsletterDates.dates.filter(
(r) => !currentDates.some((cd) => cd.calendarId == r.calendarId)
);
let removeDates = currentDates.filter(
(r) => !syncNewsletterDates.dates.some((cd) => cd.calendarId == r.calendarId)
);
let keptDates = syncNewsletterDates.dates.filter(
(r) =>
currentDates.some((cd) => cd.calendarId == r.calendarId) &&
!removeDates.some((cd) => cd.calendarId == r.calendarId)
);
if (newDates.length != 0) {
await this.syncPresenceAdd(manager, syncNewsletterDates.newsletterId, newDates);
}
if (removeDates.length != 0) {
await this.syncPresenceRemove(manager, syncNewsletterDates.newsletterId, removeDates);
}
for (const date of keptDates) {
await this.syncPresenceUpdate(manager, syncNewsletterDates.newsletterId, date);
}
})
.then(() => {})
.catch((err) => {
throw new InternalException("Failed syncing newsletter dates", err);
});
}
private static async syncPresenceAdd(
manager: EntityManager,
newsletterId: number,
dates: Array<NewsletterDateCommand>
): Promise<InsertResult> {
return await manager
.createQueryBuilder()
.insert()
.into(newsletterDates)
.values(
dates.map((d) => ({
...d,
newsletterId: newsletterId,
}))
)
.execute();
}
private static async syncPresenceUpdate(
manager: EntityManager,
newsletterId: number,
date: NewsletterDateCommand
): Promise<UpdateResult> {
return await manager
.createQueryBuilder()
.update(newsletterDates)
.set({
diffTitle: date.diffTitle,
diffDescription: date.diffDescription,
})
.where("calendarId = :calendarId", { calendarId: date.calendarId })
.andWhere("newsletterId = :newsletterId", { newsletterId })
.execute();
}
private static async syncPresenceRemove(
manager: EntityManager,
newsletterId: number,
dates: Array<NewsletterDateCommand>
): Promise<DeleteResult> {
return await manager
.createQueryBuilder()
.delete()
.from(newsletterDates)
.where("calendarId IN (:...ids)", { ids: dates.map((d) => d.calendarId) })
.andWhere("newsletterId = :newsletterId", { newsletterId })
.execute();
}
}

View file

@ -0,0 +1,4 @@
export interface SynchronizeNewsletterRecipientsCommand {
newsletterId: number;
recipients: Array<number>;
}

View file

@ -0,0 +1,73 @@
import { DeleteResult, EntityManager, InsertResult, UpdateResult } from "typeorm";
import { dataSource } from "../data-source";
import InternalException from "../exceptions/internalException";
import NewsletterRecipientsService from "../service/newsletterRecipientsService";
import { SynchronizeNewsletterRecipientsCommand } from "./newsletterRecipientsCommand";
import { newsletterRecipients } from "../entity/newsletterRecipients";
export default abstract class NewsletterRecipientsCommandHandler {
/**
* @description sync newsletterRecipients
* @param {SynchronizeNewsletterRecipientsCommand} syncNewsletterRecipients
* @returns {Promise<void>}
*/
static async sync(syncNewsletterRecipients: SynchronizeNewsletterRecipientsCommand): Promise<void> {
let currentRecipients = (await NewsletterRecipientsService.getAll(syncNewsletterRecipients.newsletterId)).map(
(r) => r.memberId
);
return await dataSource.manager
.transaction(async (manager) => {
let newRecipients = syncNewsletterRecipients.recipients.filter(
(r) => !currentRecipients.map((np) => np).includes(r)
);
let removeRecipients = currentRecipients.filter(
(r) => !syncNewsletterRecipients.recipients.map((np) => np).includes(r)
);
if (newRecipients.length != 0) {
await this.syncPresenceAdd(manager, syncNewsletterRecipients.newsletterId, newRecipients);
}
if (removeRecipients.length != 0) {
await this.syncPresenceRemove(manager, syncNewsletterRecipients.newsletterId, removeRecipients);
}
})
.then(() => {})
.catch((err) => {
throw new InternalException("Failed syncing newsletter recipients", err);
});
}
private static async syncPresenceAdd(
manager: EntityManager,
newsletterId: number,
recipients: Array<number>
): Promise<InsertResult> {
return await manager
.createQueryBuilder()
.insert()
.into(newsletterRecipients)
.values(
recipients.map((r) => ({
memberId: r,
newsletterId: newsletterId,
}))
)
.execute();
}
private static async syncPresenceRemove(
manager: EntityManager,
newsletterId: number,
recipients: Array<number>
): Promise<DeleteResult> {
return await manager
.createQueryBuilder()
.delete()
.from(newsletterRecipients)
.where("memberId IN (:...ids)", { ids: recipients })
.andWhere("newsletterId = :newsletterId", { newsletterId })
.execute();
}
}

View file

@ -0,0 +1,16 @@
export interface CreateTemplateCommand {
template: string;
description: string | null;
}
export interface UpdateTemplateCommand {
id: number;
template: string;
description: string | null;
design: object;
html: string;
}
export interface DeleteTemplateCommand {
id: number;
}

View file

@ -0,0 +1,70 @@
import { dataSource } from "../data-source";
import { template } from "../entity/template";
import InternalException from "../exceptions/internalException";
import { CreateTemplateCommand, DeleteTemplateCommand, UpdateTemplateCommand } from "./templateCommand";
export default abstract class TemplateCommandHandler {
/**
* @description create template
* @param CreateTemplateCommand
* @returns {Promise<number>}
*/
static async create(createTemplate: CreateTemplateCommand): Promise<number> {
return await dataSource
.createQueryBuilder()
.insert()
.into(template)
.values({
template: createTemplate.template,
description: createTemplate.description,
})
.execute()
.then((result) => {
return result.identifiers[0].id;
})
.catch((err) => {
throw new InternalException("Failed creating template", err);
});
}
/**
* @description update template
* @param UpdateTemplateCommand
* @returns {Promise<void>}
*/
static async update(updateTemplate: UpdateTemplateCommand): Promise<void> {
return await dataSource
.createQueryBuilder()
.update(template)
.set({
template: updateTemplate.template,
description: updateTemplate.description,
design: updateTemplate.design,
html: updateTemplate.html,
})
.where("id = :id", { id: updateTemplate.id })
.execute()
.then(() => {})
.catch((err) => {
throw new InternalException("Failed updating template", err);
});
}
/**
* @description delete template
* @param DeleteTemplateCommand
* @returns {Promise<void>}
*/
static async delete(deletTemplate: DeleteTemplateCommand): Promise<void> {
return await dataSource
.createQueryBuilder()
.delete()
.from(template)
.where("id = :id", { id: deletTemplate.id })
.execute()
.then(() => {})
.catch((err) => {
throw new InternalException("Failed deleting template", err);
});
}
}

View file

@ -0,0 +1,6 @@
export interface UpdateTemplateUsageCommand {
scope: string;
headerId: number | null;
bodyId: number | null;
footerId: number | null;
}

View file

@ -0,0 +1,28 @@
import { dataSource } from "../data-source";
import { templateUsage } from "../entity/templateUsage";
import InternalException from "../exceptions/internalException";
import { UpdateTemplateUsageCommand } from "./templateUsageCommand";
export default abstract class TemplateUsageCommandHandler {
/**
* @description update templateUsage
* @param UpdateTemplateUsageCommand
* @returns {Promise<void>}
*/
static async update(updateTemplateUsage: UpdateTemplateUsageCommand): Promise<void> {
return await dataSource
.createQueryBuilder()
.update(templateUsage)
.set({
headerId: updateTemplateUsage.headerId,
bodyId: updateTemplateUsage.bodyId,
footerId: updateTemplateUsage.footerId,
})
.where("scope = :scope", { scope: updateTemplateUsage.scope })
.execute()
.then(() => {})
.catch((err) => {
throw new InternalException("Failed updating templateUsage", err);
});
}
}

View file

@ -0,0 +1,66 @@
import { Request, Response } from "express";
import NewsletterConfigService from "../../service/newsletterConfigService";
import NewsletterConfigFactory from "../../factory/admin/newsletterConfig";
import NewsletterConfigCommandHandler from "../../command/newsletterConfigCommandHandler";
import { DeleteNewsletterConfigCommand, SetNewsletterConfigCommand } from "../../command/newsletterConfigCommand";
/**
* @description get all newsletterConfigs
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function getAllNewsletterConfigs(req: Request, res: Response): Promise<any> {
let newsletterConfigs = await NewsletterConfigService.getAll();
res.json(NewsletterConfigFactory.mapToBase(newsletterConfigs));
}
/**
* @description get newsletterConfig by id
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function getNewsletterConfigById(req: Request, res: Response): Promise<any> {
let comId = parseInt(req.params.comId);
let newsletterConfig = await NewsletterConfigService.getByComId(comId);
res.json(NewsletterConfigFactory.mapToSingle(newsletterConfig));
}
/**
* @description set newsletterConfig
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function setNewsletterConfig(req: Request, res: Response): Promise<any> {
let comTypeId = req.body.comTypeId;
let config = req.body.config;
let createNewsletterConfig: SetNewsletterConfigCommand = {
comTypeId,
config,
};
let id = await NewsletterConfigCommandHandler.set(createNewsletterConfig);
res.send(id);
}
/**
* @description delete award
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function deleteNewsletterConfig(req: Request, res: Response): Promise<any> {
const comTypeId = parseInt(req.params.comTypeId);
let deleteNewsletterConfig: DeleteNewsletterConfigCommand = {
comTypeId: comTypeId,
};
await NewsletterConfigCommandHandler.delete(deleteNewsletterConfig);
res.sendStatus(204);
}

View file

@ -0,0 +1,387 @@
import { Request, Response } from "express";
import NewsletterService from "../../service/newsletterService";
import NewsletterFactory from "../../factory/admin/newsletter";
import NewsletterDatesService from "../../service/newsletterDatesService";
import NewsletterDatesFactory from "../../factory/admin/newsletterDates";
import NewsletterRecipientsService from "../../service/newsletterRecipientsService";
import NewsletterRecipientsFactory from "../../factory/admin/newsletterRecipients";
import { FileSystemHelper } from "../../helpers/fileSystemHelper";
import { CreateNewsletterCommand, SynchronizeNewsletterCommand } from "../../command/newsletterCommand";
import NewsletterCommandHandler from "../../command/newsletterCommandHandler";
import { SynchronizeNewsletterDatesCommand } from "../../command/newsletterDatesCommand";
import NewsletterDatesCommandHandler from "../../command/newsletterDatesCommandHandler";
import { SynchronizeNewsletterRecipientsCommand } from "../../command/newsletterRecipientsCommand";
import NewsletterRecipientsCommandHandler from "../../command/newsletterRecipientsCommandHandler";
import { NewsletterDatesViewModel } from "../../viewmodel/admin/newsletterDates.models";
import { PdfExport } from "../../helpers/pdfExport";
import UserService from "../../service/userService";
import { TemplateHelper } from "../../helpers/templateHelper";
import MailHelper from "../../helpers/mailHelper";
import { NewsletterEventType, NewsletterHelper } from "../../helpers/newsletterHelper";
import { Salutation } from "../../enums/salutation";
/**
* @description get all newsletters
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function getAllNewsletters(req: Request, res: Response): Promise<any> {
let offset = parseInt((req.query.offset as string) ?? "0");
let count = parseInt((req.query.count as string) ?? "25");
let [newsletters, total] = await NewsletterService.getAll(offset, count);
res.json({
newsletters: NewsletterFactory.mapToBase(newsletters),
total: total,
offset: offset,
count: count,
});
}
/**
* @description get newsletter by id
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function getNewsletterById(req: Request, res: Response): Promise<any> {
let id = parseInt(req.params.id);
let newsletter = await NewsletterService.getById(id);
res.json(NewsletterFactory.mapToSingle(newsletter));
}
/**
* @description get newsletter dates by id
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function getNewsletterDatesById(req: Request, res: Response): Promise<any> {
let newsletterId = parseInt(req.params.newsletterId);
let dates = await NewsletterDatesService.getAll(newsletterId);
res.json(NewsletterDatesFactory.mapToBase(dates));
}
/**
* @description get newsletter recipients by id
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function getNewsletterRecipientsById(req: Request, res: Response): Promise<any> {
let newsletterId = parseInt(req.params.newsletterId);
let recipients = await NewsletterRecipientsService.getAll(newsletterId);
res.json(NewsletterRecipientsFactory.mapToBase(recipients));
}
/**
* @description get newsletter printouts by id
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function getNewsletterPrintoutsById(req: Request, res: Response): Promise<any> {
let newsletterId = parseInt(req.params.newsletterId);
let newsletter = await NewsletterService.getById(newsletterId);
let filesInFolder = FileSystemHelper.getFilesInDirectory(
`newsletter/${newsletter.id}_${newsletter.title.replace(" ", "")}`
);
res.json(filesInFolder);
}
/**
* @description get newsletter printout by id and print
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function getNewsletterPrintoutByIdAndPrint(req: Request, res: Response): Promise<any> {
let newsletterId = parseInt(req.params.newsletterId);
let filename = req.params.filename;
let newsletter = await NewsletterService.getById(newsletterId);
let filepath = FileSystemHelper.formatPath(
"newsletter",
`${newsletter.id}_${newsletter.title.replace(" ", "")}`,
filename
);
res.sendFile(filepath, {
headers: {
"Content-Type": "application/pdf",
},
});
}
/**
* @description create newsletter printout preview by id
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function createNewsletterPrintoutPreviewById(req: Request, res: Response): Promise<any> {
let newsletterId = parseInt(req.params.newsletterId);
let newsletter = await NewsletterService.getById(newsletterId);
let dates = await NewsletterDatesService.getAll(newsletterId);
let recipient = await UserService.getById(parseInt(req.userId));
let data = NewsletterHelper.buildData(newsletter, dates);
data.recipient = {
firstname: recipient.firstname,
lastname: recipient.lastname,
salutation: Salutation.none,
nameaffix: "",
street: "Straße",
streetNumber: "Hausnummer",
streetNumberAdd: "Adresszusatz",
};
let pdf = await PdfExport.renderFile({
title: "Probedruck Newsletter",
template: "newsletter",
saveToDisk: false,
data: data,
});
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 newsletter
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function createNewsletter(req: Request, res: Response): Promise<any> {
let title = req.body.title;
let createNewsletter: CreateNewsletterCommand = {
title,
};
let id = await NewsletterCommandHandler.create(createNewsletter);
res.send(id);
}
/**
* @description get newsletter printout progress by id
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function getNewsletterPrintoutProgressById(req: Request, res: Response): Promise<any> {
let newsletterId = parseInt(req.params.newsletterId);
res.setHeader("Content-Type", "text/event-stream");
res.setHeader("Cache-Control", "no-cache");
res.setHeader("Connection", "keep-alive");
res.flushHeaders();
const progressHandler = (data: NewsletterEventType) => {
if (data.newsletterId == newsletterId && data.kind == "pdf") {
res.write(JSON.stringify(data));
}
};
const completeHandler = (data: NewsletterEventType) => {
if (data.newsletterId == newsletterId && data.kind == "pdf") {
res.write(JSON.stringify(data));
res.end();
}
};
NewsletterHelper.jobStatus.on("progress", progressHandler);
NewsletterHelper.jobStatus.on("complete", completeHandler);
req.on("close", () => {
NewsletterHelper.jobStatus.off("progress", progressHandler);
NewsletterHelper.jobStatus.off("complete", completeHandler);
});
}
/**
* @description create newsletter printouts for each member by id
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function createNewsletterPrintoutById(req: Request, res: Response): Promise<any> {
let newsletterId = parseInt(req.params.newsletterId);
await NewsletterHelper.printPdfs(newsletterId);
res.sendStatus(204);
}
/**
* @description create newsletter mail preview by id
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function createNewsletterMailPreviewById(req: Request, res: Response): Promise<any> {
let newsletterId = parseInt(req.params.newsletterId);
let newsletter = await NewsletterService.getById(newsletterId);
let dates = await NewsletterDatesService.getAll(newsletterId);
let recipient = await UserService.getById(parseInt(req.userId));
let data = NewsletterHelper.buildData(newsletter, dates);
data.recipient = {
firstname: recipient.firstname,
lastname: recipient.lastname,
salutation: Salutation.none,
nameaffix: "",
street: "Straße",
streetNumber: "Hausnummer",
streetNumberAdd: "Adresszusatz",
};
const { body } = await TemplateHelper.renderFileForModule({
module: "newsletter",
bodyData: data,
title: "Probeversand Newsletter",
});
await MailHelper.sendMail(recipient.mail, "Probeversand Newsletter", body);
res.sendStatus(204);
}
/**
* @description send newsletter mail and create printouts by id
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function sendNewsletterById(req: Request, res: Response): Promise<any> {
let newsletterId = parseInt(req.params.newsletterId);
await NewsletterHelper.sendMails(newsletterId);
res.sendStatus(204);
}
/**
* @description get newsletter sending progress by id
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function getNewsletterSendingProgressById(req: Request, res: Response): Promise<any> {
let newsletterId = parseInt(req.params.newsletterId);
res.setHeader("Content-Type", "text/event-stream");
res.setHeader("Cache-Control", "no-cache");
res.setHeader("Connection", "keep-alive");
res.flushHeaders();
const progressHandler = (data: NewsletterEventType) => {
if (data.newsletterId == newsletterId && data.kind == "mail") {
res.write(JSON.stringify(data));
}
};
const completeHandler = (data: NewsletterEventType) => {
if (data.newsletterId == newsletterId && data.kind == "mail") {
res.write(JSON.stringify(data));
res.end();
}
};
NewsletterHelper.jobStatus.on("progress", progressHandler);
NewsletterHelper.jobStatus.on("complete", completeHandler);
req.on("close", () => {
NewsletterHelper.jobStatus.off("progress", progressHandler);
NewsletterHelper.jobStatus.off("complete", completeHandler);
});
}
/**
* @description synchronize newsletter by id
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function synchronizeNewsletterById(req: Request, res: Response): Promise<any> {
let id = parseInt(req.params.id);
let title = req.body.title;
let description = req.body.description;
let newsletterTitle = req.body.newsletterTitle;
let newsletterText = req.body.newsletterText;
let newsletterSignatur = req.body.newsletterSignatur;
let recipientsByQueryId = req.body.recipientsByQueryId ?? null;
let syncNewsletter: SynchronizeNewsletterCommand = {
id,
title,
description,
newsletterTitle,
newsletterText,
newsletterSignatur,
recipientsByQueryId,
};
await NewsletterCommandHandler.sync(syncNewsletter);
res.sendStatus(204);
}
/**
* @description synchronize newsletter dates by id
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function synchronizeNewsletterDatesById(req: Request, res: Response): Promise<any> {
let newsletterId = parseInt(req.params.newsletterId);
let dates = req.body.dates as Array<NewsletterDatesViewModel>;
let syncDates: SynchronizeNewsletterDatesCommand = {
newsletterId,
dates: dates.map((d) => ({
calendarId: d.calendarId,
diffTitle: d.diffTitle,
diffDescription: d.diffDescription,
})),
};
await NewsletterDatesCommandHandler.sync(syncDates);
res.sendStatus(204);
}
/**
* @description synchronize newsletter recipients by id
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function synchronizeNewsletterRecipientsById(req: Request, res: Response): Promise<any> {
let newsletterId = parseInt(req.params.newsletterId);
let recipients = req.body.recipients as Array<number>;
let syncRecipients: SynchronizeNewsletterRecipientsCommand = {
newsletterId,
recipients: recipients,
};
await NewsletterRecipientsCommandHandler.sync(syncRecipients);
res.sendStatus(204);
}

View file

@ -27,6 +27,7 @@ import ProtocolPrintoutService from "../../service/protocolPrintoutService";
import ProtocolPrintoutFactory from "../../factory/admin/protocolPrintout";
import { CreateProtocolPrintoutCommand } from "../../command/protocolPrintoutCommand";
import ProtocolPrintoutCommandHandler from "../../command/protocolPrintoutCommandHandler";
import { FileSystemHelper } from "../../helpers/fileSystemHelper";
/**
* @description get all protocols
@ -237,9 +238,10 @@ export async function createProtocolPrintoutById(req: Request, res: Response): P
)}`;
await PdfExport.renderFile({
template: "protocol.template.html",
template: "protocol",
title,
filename,
folder: "protocol",
data: {
title: protocol.title,
summary: protocol.summary,
@ -262,7 +264,7 @@ export async function createProtocolPrintoutById(req: Request, res: Response): P
let printout: CreateProtocolPrintoutCommand = {
title,
iteration: iteration + 1,
filename,
filename: FileSystemHelper.normalizePath("protocol", filename),
protocolId,
};
await ProtocolPrintoutCommandHandler.create(printout);

View file

@ -91,7 +91,7 @@ export async function executeQuery(req: Request, res: Response): Promise<any> {
res.json({
stats: "success",
rows: rows,
rows: DynamicQueryBuilder.flattenQueryResult(rows),
total: total,
offset: offset,
count: count,

View file

@ -0,0 +1,120 @@
import { Request, Response } from "express";
import TemplateService from "../../service/templateService";
import TemplateFactory from "../../factory/admin/template";
import { CreateTemplateCommand, DeleteTemplateCommand, UpdateTemplateCommand } from "../../command/templateCommand";
import TemplateCommandHandler from "../../command/templateCommandHandler";
/**
* @description get all templates
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function getAllTemplates(req: Request, res: Response): Promise<any> {
let templates = await TemplateService.getAll();
res.json(TemplateFactory.mapToBase(templates));
}
/**
* @description get template by id
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function getTemplateById(req: Request, res: Response): Promise<any> {
const id = parseInt(req.params.id);
let template = await TemplateService.getById(id);
res.json(TemplateFactory.mapToSingle(template));
}
/**
* @description create new template
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function createTemplate(req: Request, res: Response): Promise<any> {
const template = req.body.template;
const description = req.body.description;
let createTemplate: CreateTemplateCommand = {
template: template,
description: description,
};
let id = await TemplateCommandHandler.create(createTemplate);
res.status(200).send(id);
}
/**
* @description clone template
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function cloneTemplate(req: Request, res: Response): Promise<any> {
const cloneId = req.body.cloneId;
const { template, description, design, html } = await TemplateService.getById(cloneId);
let createTemplate: CreateTemplateCommand = {
template: "",
description: "",
};
let id = await TemplateCommandHandler.create(createTemplate);
let updateTemplate: UpdateTemplateCommand = {
id: id,
template: template + " - Kopie",
description: description,
design: design,
html: html,
};
await TemplateCommandHandler.update(updateTemplate);
res.status(200).send(id);
}
/**
* @description update template
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function updateTemplate(req: Request, res: Response): Promise<any> {
const id = parseInt(req.params.id);
const template = req.body.template;
const description = req.body.description;
const design = req.body.design;
const html = req.body.html;
let updateTemplate: UpdateTemplateCommand = {
id: id,
template: template,
description: description,
design: design,
html: html,
};
await TemplateCommandHandler.update(updateTemplate);
res.sendStatus(204);
}
/**
* @description delete template
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function deleteTemplate(req: Request, res: Response): Promise<any> {
const id = parseInt(req.params.id);
let deleteTemplate: DeleteTemplateCommand = {
id: id,
};
await TemplateCommandHandler.delete(deleteTemplate);
res.sendStatus(204);
}

View file

@ -0,0 +1,91 @@
import { Request, Response } from "express";
import TemplateUsageService from "../../service/templateUsageService";
import TemplateUsageFactory from "../../factory/admin/templateUsage";
import { UpdateTemplateUsageCommand } from "../../command/templateUsageCommand";
import TemplateUsageCommandHandler from "../../command/templateUsageCommandHandler";
import PermissionHelper from "../../helpers/permissionHelper";
import ForbiddenRequestException from "../../exceptions/forbiddenRequestException";
import { PermissionModule } from "../../type/permissionTypes";
import { PdfExport } from "../../helpers/pdfExport";
import { DemoDataHelper } from "../../helpers/demoDataHelper";
/**
* @description get all templateUsages
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function getAllTemplateUsages(req: Request, res: Response): Promise<any> {
let templateUsages = await TemplateUsageService.getAll();
if (!req.isOwner) {
templateUsages = templateUsages.filter((tu) => {
return (
PermissionHelper.can(req.permissions, "update", "settings", tu.scope) ||
PermissionHelper.can(req.permissions, "update", "club", tu.scope)
);
});
}
res.json(TemplateUsageFactory.mapToBase(templateUsages));
}
/**
* @description print demo of templateUsage
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function printTemplateUsageDemo(req: Request, res: Response): Promise<any> {
const scope = req.params.scope as PermissionModule;
let demoData = DemoDataHelper.getData(scope);
let pdf = await PdfExport.renderFile({
template: scope,
saveToDisk: false,
data: demoData,
});
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 update templateUsage
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function updateTemplateUsage(req: Request, res: Response): Promise<any> {
const scope = req.params.scope;
let allowedSettings = PermissionHelper.can(
req.permissions,
"update",
"settings",
req.params.scope as PermissionModule
);
let allowedClub = PermissionHelper.can(req.permissions, "update", "club", req.params.scope as PermissionModule);
if (!(req.isOwner || allowedSettings || allowedClub)) {
throw new ForbiddenRequestException(`missing permission for editing scope ${req.params.scope}`);
}
const headerId = req.body.headerId ?? null;
const bodyId = req.body.bodyId ?? null;
const footerId = req.body.footerId ?? null;
let updateTemplateUsage: UpdateTemplateUsageCommand = {
scope: scope,
headerId: headerId,
bodyId: bodyId,
footerId: footerId,
};
await TemplateUsageCommandHandler.update(updateTemplateUsage);
res.sendStatus(204);
}

View file

@ -146,8 +146,7 @@ export async function deleteUser(req: Request, res: Response): Promise<any> {
try {
// sendmail
let mailhelper = new MailHelper();
await mailhelper.sendMail(
await MailHelper.sendMail(
user.mail,
`Email Bestätigung für Mitglieder Admin-Portal von ${CLUB_NAME}`,
`Ihr Nutzerkonto des Adminportals wurde erfolgreich gelöscht.`

View file

@ -71,8 +71,7 @@ export async function inviteUser(req: Request, res: Response, isInvite: boolean
let token = await InviteCommandHandler.create(createInvite);
// sendmail
let mailhelper = new MailHelper();
await mailhelper.sendMail(
await MailHelper.sendMail(
mail,
`Email Bestätigung für Mitglieder Admin-Portal von ${CLUB_NAME}`,
`Öffne folgenden Link: ${origin}/${isInvite ? "invite" : "setup"}/verify?mail=${mail}&token=${token}`

View file

@ -6,6 +6,7 @@ import { createEvents } from "ics";
import moment from "moment";
import InternalException from "../exceptions/internalException";
import CalendarFactory from "../factory/admin/calendar";
import { CalendarHelper } from "../helpers/calendarHelper";
/**
* @description get all calendar items by types or nscdr
@ -16,6 +17,7 @@ import CalendarFactory from "../factory/admin/calendar";
*/
export async function getCalendarItemsByTypes(req: Request, res: Response): Promise<any> {
let types = Array.isArray(req.query.types) ? req.query.types : [req.query.types];
let nscdr = req.query.nscdr == "true";
let output = (req.query.output as "ics" | "json") ?? "ics";
if (output != "ics" && output != "json") {
@ -33,7 +35,10 @@ export async function getCalendarItemsByTypes(req: Request, res: Response): Prom
ti.passphrase == "" ||
ti.passphrase == (types as Array<string>).find((t) => t.includes(ti.type)).split(":")[1]
);
items = await CalendarService.getByTypes(typeIds.map((t) => t.id));
items = await CalendarService.getByTypes(
typeIds.map((t) => t.id),
nscdr
);
} else {
items = await CalendarService.getByTypeNSCDR();
}
@ -41,59 +46,8 @@ export async function getCalendarItemsByTypes(req: Request, res: Response): Prom
if (output == "json") {
res.json(CalendarFactory.mapToBase(items));
} else {
let events = createEvents(
items.map((i) => ({
calName: process.env.CLUB_NAME,
uid: i.id,
sequence: 1,
...(i.allDay
? {
start: moment(i.starttime)
.format("YYYY-M-D")
.split("-")
.map((a) => parseInt(a)) as [number, number, number],
end: moment(i.endtime)
.format("YYYY-M-D")
.split("-")
.map((a) => parseInt(a)) as [number, number, number],
}
: {
start: moment(i.starttime)
.format("YYYY-M-D-H-m")
.split("-")
.map((a) => parseInt(a)) as [number, number, number, number, number],
end: moment(i.endtime)
.format("YYYY-M-D-H-m")
.split("-")
.map((a) => parseInt(a)) as [number, number, number, number, number],
}),
title: i.title,
description: i.content,
location: i.location,
categories: [i.type.type],
created: moment(i.createdAt)
.format("YYYY-M-D-H-m")
.split("-")
.map((a) => parseInt(a)) as [number, number, number, number, number],
lastModified: moment(i.updatedAt)
.format("YYYY-M-D-H-m")
.split("-")
.map((a) => parseInt(a)) as [number, number, number, number, number],
transp: "OPAQUE" as "OPAQUE",
url: "https://www.ff-merching.de",
alarms: [
{
action: "display",
description: "Erinnerung",
trigger: {
minutes: 30,
before: true,
},
},
],
}))
);
let { error, value } = CalendarHelper.buildICS(items);
res.type("ics").send(events.value);
res.type("ics").send(value);
}
}

View file

@ -41,8 +41,7 @@ export async function startReset(req: Request, res: Response): Promise<any> {
let token = await ResetCommandHandler.create(createReset);
// sendmail
let mailhelper = new MailHelper();
await mailhelper.sendMail(
await MailHelper.sendMail(
mail,
`Email Bestätigung für Mitglieder Admin-Portal von ${CLUB_NAME}`,
`Öffne folgenden Link: ${origin}/reset/reset?mail=${mail}&token=${token}`

View file

@ -51,6 +51,16 @@ import { memberExecutivePositionsView } from "./views/memberExecutivePositionVie
import { memberQualificationsView } from "./views/memberQualificationsView";
import { membershipView } from "./views/membershipsView";
import { MemberDataViews1734520998539 } from "./migrations/1734520998539-memberDataViews";
import { template } from "./entity/template";
import { Template1734854680201 } from "./migrations/1734854680201-template";
import { templateUsage } from "./entity/templateUsage";
import { TemplateUsage1734949173739 } from "./migrations/1734949173739-templateUsage";
import { newsletter } from "./entity/newsletter";
import { newsletterDates } from "./entity/newsletterDates";
import { newsletterRecipients } from "./entity/newsletterRecipients";
import { Newsletter1735118780511 } from "./migrations/1735118780511-newsletter";
import { newsletterConfig } from "./entity/newsletterConfig";
import { NewsletterConfig1735207446910 } from "./migrations/1735207446910-newsletterConfig";
const dataSource = new DataSource({
type: DB_TYPE as any,
@ -90,6 +100,12 @@ const dataSource = new DataSource({
calendar,
calendarType,
query,
template,
templateUsage,
newsletter,
newsletterDates,
newsletterRecipients,
newsletterConfig,
memberView,
memberExecutivePositionsView,
memberQualificationsView,
@ -112,6 +128,10 @@ const dataSource = new DataSource({
SecuringCalendarType1733249553766,
QueryStore1734187754677,
MemberDataViews1734520998539,
Template1734854680201,
TemplateUsage1734949173739,
Newsletter1735118780511,
NewsletterConfig1735207446910,
],
migrationsRun: true,
migrationsTransactionMode: "each",

View file

@ -0,0 +1,72 @@
import { calendar } from "../entity/calendar";
import { member } from "../entity/member";
import { Salutation } from "../enums/salutation";
export const newsletterDemoData: {
title: string;
description: string;
newsletterTitle: string;
newsletterText: string;
newsletterSignatur: string;
dates: Array<
Partial<
calendar & {
formattedStarttime: string;
formattedFullStarttime: string;
formattedEndtime: string;
formattedFullEndtime: string;
}
>
>;
recipient: Partial<member & { street: string; streetNumber: string; streetNumberAdd: string }>;
} = {
title: "Beispiel Newsletter Daten",
description: "Zusammenfassung der Demodaten.",
newsletterTitle: "<h1>Sehr geehrtes Feuerwehrmitglied</h1>",
newsletterText: "<p>zu folgenden Terminen möchten wir recht herzlich zur Teilnahme einladen:</p>",
newsletterSignatur: "<p>Mit freundlichen Grüßen</p><p>...</p>",
dates: [
{
title: "Termin 1",
content: "<p>Beschreibung eines Termins</p>",
starttime: new Date(),
formattedStarttime: new Date().toLocaleDateString("de-DE", {
weekday: "long",
day: "2-digit",
month: "long",
}),
formattedFullStarttime: new Date().toLocaleDateString("de-DE", {
weekday: "long",
day: "2-digit",
month: "long",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
}),
endtime: new Date(),
formattedEndtime: new Date().toLocaleDateString("de-DE", {
weekday: "long",
day: "2-digit",
month: "long",
}),
formattedFullEndtime: new Date().toLocaleDateString("de-DE", {
weekday: "long",
day: "2-digit",
month: "long",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
}),
location: "Feuerwehrhaus",
},
],
recipient: {
firstname: "Julian",
lastname: "Krauser",
salutation: Salutation.sir,
nameaffix: "",
street: "Straße",
streetNumber: "Hausnummer",
streetNumberAdd: "Adresszusatz",
},
};

View file

@ -0,0 +1,56 @@
import { member } from "../entity/member";
import { protocolAgenda } from "../entity/protocolAgenda";
import { protocolDecision } from "../entity/protocolDecision";
import { protocolVoting } from "../entity/protocolVoting";
export const protocolDemoData: {
title: string;
summary: string;
iteration: number;
date: string;
start: string;
end: string;
agenda: Array<Partial<protocolAgenda>>;
decisions: Array<Partial<protocolDecision>>;
presence: Array<Partial<member>>;
votings: Array<Partial<protocolVoting>>;
} = {
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",
}),
start: "19:00:00",
end: "21:00:00",
agenda: [
{
topic: "Protokoll-TOP",
context: "Inhalt des Punktes",
},
],
decisions: [
{
topic: "Entscheidung yz",
context: "Inhalt der Entscheidung",
},
],
presence: [
{
firstname: "Julian",
lastname: "Krauser",
},
],
votings: [
{
topic: "Abstimmung xy",
context: "Inhalt der Abstimmung",
favour: 1,
abstain: 2,
against: 3,
},
],
};

View file

@ -40,7 +40,7 @@ export class member {
birthdate: Date;
@OneToMany(() => communication, (communications) => communications.member)
communications: communication;
communications: communication[];
@OneToOne(() => communication, {
nullable: true,

45
src/entity/newsletter.ts Normal file
View file

@ -0,0 +1,45 @@
import { Column, Entity, ManyToOne, OneToMany, PrimaryColumn } from "typeorm";
import { newsletterDates } from "./newsletterDates";
import { member } from "./member";
import { newsletterRecipients } from "./newsletterRecipients";
import { query } from "./query";
@Entity()
export class newsletter {
@PrimaryColumn({ generated: "increment", type: "int" })
id: number;
@Column({ type: "varchar", length: 255 })
title: string;
@Column({ type: "varchar", length: 255, default: "" })
description: string;
@Column({ type: "varchar", length: 255, default: "" })
newsletterTitle: string;
@Column({ type: "text", default: "" })
newsletterText: string;
@Column({ type: "varchar", length: 255, default: "" })
newsletterSignatur: string;
@Column({ type: "boolean", default: false })
isSent: boolean;
@Column({ type: "int", nullable: true })
recipientsByQueryId?: number;
@OneToMany(() => newsletterDates, (dates) => dates.newsletter)
dates: newsletterDates[];
@OneToMany(() => newsletterRecipients, (recipient) => recipient.newsletter)
recipients: newsletterRecipients[];
@ManyToOne(() => query, {
nullable: true,
onDelete: "CASCADE",
onUpdate: "RESTRICT",
})
recipientsByQuery?: query;
}

View file

@ -0,0 +1,30 @@
import { Column, Entity, ManyToOne, PrimaryColumn } from "typeorm";
import { NewsletterConfigType } from "../enums/newsletterConfigType";
import { communicationType } from "./communicationType";
@Entity()
export class newsletterConfig {
@PrimaryColumn({ type: "int" })
comTypeId: number;
@Column({
type: "varchar",
length: "255",
transformer: {
to(value: NewsletterConfigType) {
return value.toString();
},
from(value: string) {
return NewsletterConfigType[value as keyof typeof NewsletterConfigType];
},
},
})
config: NewsletterConfigType;
@ManyToOne(() => communicationType, {
nullable: false,
onDelete: "CASCADE",
onUpdate: "RESTRICT",
})
comType: communicationType;
}

View file

@ -0,0 +1,32 @@
import { Column, Entity, ManyToOne, PrimaryColumn } from "typeorm";
import { newsletter } from "./newsletter";
import { calendar } from "./calendar";
@Entity()
export class newsletterDates {
@PrimaryColumn({ type: "int" })
newsletterId: number;
@PrimaryColumn({ type: "varchar" })
calendarId: string;
@Column({ type: "varchar", length: 255, nullable: true })
diffTitle: string | null;
@Column({ type: "text", nullable: true })
diffDescription: string | null;
@ManyToOne(() => newsletter, (newsletter) => newsletter.dates, {
nullable: false,
onDelete: "CASCADE",
onUpdate: "RESTRICT",
})
newsletter: newsletter;
@ManyToOne(() => calendar, {
nullable: false,
onDelete: "RESTRICT",
onUpdate: "RESTRICT",
})
calendar: calendar;
}

View file

@ -0,0 +1,26 @@
import { Column, Entity, ManyToOne, PrimaryColumn } from "typeorm";
import { newsletter } from "./newsletter";
import { member } from "./member";
@Entity()
export class newsletterRecipients {
@PrimaryColumn({ type: "int" })
newsletterId: number;
@PrimaryColumn({ type: "int" })
memberId: number;
@ManyToOne(() => newsletter, (newsletter) => newsletter.recipients, {
nullable: false,
onDelete: "CASCADE",
onUpdate: "RESTRICT",
})
newsletter: newsletter;
@ManyToOne(() => member, {
nullable: false,
onDelete: "CASCADE",
onUpdate: "RESTRICT",
})
member: member;
}

30
src/entity/template.ts Normal file
View file

@ -0,0 +1,30 @@
import { Column, Entity, PrimaryColumn } from "typeorm";
@Entity()
export class template {
@PrimaryColumn({ generated: "increment", type: "int" })
id: number;
@Column({ type: "varchar", length: 255 })
template: string;
@Column({ type: "varchar", length: 255, nullable: true })
description?: string;
@Column({
type: "text",
default: "{}",
transformer: {
to(value: object) {
return JSON.stringify(value);
},
from(value: string) {
return JSON.parse(value);
},
},
})
design: object;
@Column({ type: "text", default: "" })
html: string;
}

View file

@ -0,0 +1,39 @@
import { Column, Entity, ManyToOne, PrimaryColumn } from "typeorm";
import { template } from "./template";
import { PermissionModule } from "../type/permissionTypes";
@Entity()
export class templateUsage {
@PrimaryColumn({ type: "varchar", length: 255 })
scope: PermissionModule;
@Column({ type: "number", nullable: true })
headerId: number | null;
@Column({ type: "number", nullable: true })
bodyId: number | null;
@Column({ type: "number", nullable: true })
footerId: number | null;
@ManyToOne(() => template, {
nullable: true,
onDelete: "RESTRICT",
onUpdate: "RESTRICT",
})
header: template | null;
@ManyToOne(() => template, {
nullable: true,
onDelete: "RESTRICT",
onUpdate: "RESTRICT",
})
body: template | null;
@ManyToOne(() => template, {
nullable: true,
onDelete: "RESTRICT",
onUpdate: "RESTRICT",
})
footer: template | null;
}

View file

@ -0,0 +1,4 @@
export enum NewsletterConfigType {
pdf = "pdf",
mail = "mail",
}

View file

@ -20,6 +20,7 @@ export const MAIL_PORT = Number(process.env.MAIL_PORT ?? "587");
export const MAIL_SECURE = process.env.MAIL_SECURE ?? "false";
export const CLUB_NAME = process.env.CLUB_NAME ?? "";
export const CLUB_WEBSITE = process.env.CLUB_WEBSITE ?? "";
export function configCheck() {
if (DB_TYPE != "mysql" && DB_TYPE != "sqlite") throw new Error("set valid value to DB_TYPE (mysql|sqlite)");
@ -39,6 +40,13 @@ export function configCheck() {
if (MAIL_HOST == "" || typeof MAIL_HOST != "string") throw new Error("set valid value to MAIL_HOST");
if (typeof MAIL_PORT != "number") throw new Error("set valid numeric value to MAIL_PORT");
if (MAIL_SECURE != "true" && MAIL_SECURE != "false") throw new Error("set 'true' or 'false' to MAIL_SECURE");
console.log(CLUB_WEBSITE);
if (
CLUB_WEBSITE != "" &&
!/^(http(s):\/\/.)[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)$/.test(CLUB_WEBSITE)
)
throw new Error("CLUB_WEBSITE is not valid url");
}
function checkMS(input: string, origin: string) {

View file

@ -0,0 +1,33 @@
import { newsletter } from "../../entity/newsletter";
import { NewsletterViewModel } from "../../viewmodel/admin/newsletter.models";
import QueryStoreFactory from "./queryStore";
export default abstract class NewsletterFactory {
/**
* @description map record to newsletter
* @param {newsletter} record
* @returns {NewsletterViewModel}
*/
public static mapToSingle(record: newsletter): NewsletterViewModel {
return {
id: record.id,
title: record.title,
description: record.description,
newsletterTitle: record.newsletterTitle,
newsletterText: record.newsletterText,
newsletterSignatur: record.newsletterSignatur,
isSent: record.isSent,
recipientsByQueryId: record?.recipientsByQuery ? record.recipientsByQuery.id : null,
recipientsByQuery: record?.recipientsByQuery ? QueryStoreFactory.mapToSingle(record.recipientsByQuery) : null,
};
}
/**
* @description map records to newsletter
* @param {Array<newsletter>} records
* @returns {Array<NewsletterViewModel>}
*/
public static mapToBase(records: Array<newsletter>): Array<NewsletterViewModel> {
return records.map((r) => this.mapToSingle(r));
}
}

View file

@ -0,0 +1,27 @@
import { newsletterConfig } from "../../entity/newsletterConfig";
import { NewsletterConfigViewModel } from "../../viewmodel/admin/newsletterConfig.models";
import CommunicationTypeFactory from "./communicationType";
export default abstract class NewsletterConfigFactory {
/**
* @description map record to newsletterConfig
* @param {newsletterConfig} record
* @returns {NewsletterConfigViewModel}
*/
public static mapToSingle(record: newsletterConfig): NewsletterConfigViewModel {
return {
comTypeId: record.comTypeId,
config: record.config,
comType: record?.comType ? CommunicationTypeFactory.mapToSingle(record.comType) : null,
};
}
/**
* @description map records to newsletterConfig
* @param {Array<newsletterConfig>} records
* @returns {Array<NewsletterConfigViewModel>}
*/
public static mapToBase(records: Array<newsletterConfig>): Array<NewsletterConfigViewModel> {
return records.map((r) => this.mapToSingle(r));
}
}

View file

@ -0,0 +1,30 @@
import { newsletterDates } from "../../entity/newsletterDates";
import { NewsletterDatesViewModel } from "../../viewmodel/admin/newsletterDates.models";
import CalendarFactory from "./calendar";
import MemberFactory from "./member";
export default abstract class NewsletterDatesFactory {
/**
* @description map record to newsletterDates
* @param {newsletterDates} record
* @returns {NewsletterDatesViewModel}
*/
public static mapToSingle(record: newsletterDates): NewsletterDatesViewModel {
return {
newsletterId: record.newsletterId,
calendarId: record.calendarId,
diffTitle: record.diffTitle,
diffDescription: record.diffDescription,
calendar: CalendarFactory.mapToSingle(record.calendar),
};
}
/**
* @description map records to newsletterDates
* @param {Array<newsletterDates>} records
* @returns {Array<NewsletterDatesViewModel>}
*/
public static mapToBase(records: Array<newsletterDates>): Array<NewsletterDatesViewModel> {
return records.map((r) => this.mapToSingle(r));
}
}

View file

@ -0,0 +1,27 @@
import { newsletterRecipients } from "../../entity/newsletterRecipients";
import { NewsletterRecipientsViewModel } from "../../viewmodel/admin/newsletterRecipients.models";
import MemberFactory from "./member";
export default abstract class NewsletterRecipientsFactory {
/**
* @description map record to newsletterRecipients
* @param {newsletterRecipients} record
* @returns {NewsletterRecipientsViewModel}
*/
public static mapToSingle(record: newsletterRecipients): NewsletterRecipientsViewModel {
return {
newsletterId: record.newsletterId,
memberId: record.memberId,
member: MemberFactory.mapToSingle(record.member),
};
}
/**
* @description map records to newsletterRecipients
* @param {Array<newsletterRecipients>} records
* @returns {Array<NewsletterRecipientsViewModel>}
*/
public static mapToBase(records: Array<newsletterRecipients>): Array<NewsletterRecipientsViewModel> {
return records.map((r) => this.mapToSingle(r));
}
}

View file

@ -0,0 +1,28 @@
import { template } from "../../entity/template";
import { TemplateViewModel } from "../../viewmodel/admin/template.models";
export default abstract class TemplateFactory {
/**
* @description map record to template
* @param {template} record
* @returns {TemplateViewModel}
*/
public static mapToSingle(record: template): TemplateViewModel {
return {
id: record.id,
template: record.template,
description: record.description,
design: record.design,
html: record.html,
};
}
/**
* @description map records to template
* @param {Array<template>} records
* @returns {Array<TemplateViewModel>}
*/
public static mapToBase(records: Array<template>): Array<TemplateViewModel> {
return records.map((r) => this.mapToSingle(r));
}
}

View file

@ -0,0 +1,27 @@
import { templateUsage } from "../../entity/templateUsage";
import { TemplateUsageViewModel } from "../../viewmodel/admin/templateUsage.models";
export default abstract class TemplateUsageFactory {
/**
* @description map record to templateUsage
* @param {templateUsage} record
* @returns {TemplateUsageViewModel}
*/
public static mapToSingle(record: templateUsage): TemplateUsageViewModel {
return {
scope: record.scope,
header: record.header ? { id: record.header.id, template: record.header.template } : null,
body: record.body ? { id: record.body.id, template: record.body.template } : null,
footer: record.footer ? { id: record.footer.id, template: record.footer.template } : null,
};
}
/**
* @description map records to templateUsage
* @param {Array<templateUsage>} records
* @returns {Array<TemplateUsageViewModel>}
*/
public static mapToBase(records: Array<templateUsage>): Array<TemplateUsageViewModel> {
return records.map((r) => this.mapToSingle(r));
}
}

View file

@ -0,0 +1,62 @@
import { createEvents } from "ics";
import { calendar } from "../entity/calendar";
import moment from "moment";
import { CLUB_NAME, CLUB_WEBSITE, MAIL_USERNAME } from "../env.defaults";
export abstract class CalendarHelper {
public static buildICS(entries: Array<calendar>): { error?: Error; value?: string } {
return createEvents(
entries.map((i) => ({
calName: process.env.CLUB_NAME,
uid: i.id,
sequence: i.sequence,
...(i.allDay
? {
start: moment(i.starttime)
.format("YYYY-M-D")
.split("-")
.map((a) => parseInt(a)) as [number, number, number],
end: moment(i.endtime)
.format("YYYY-M-D")
.split("-")
.map((a) => parseInt(a)) as [number, number, number],
}
: {
start: moment(i.starttime)
.format("YYYY-M-D-H-m")
.split("-")
.map((a) => parseInt(a)) as [number, number, number, number, number],
end: moment(i.endtime)
.format("YYYY-M-D-H-m")
.split("-")
.map((a) => parseInt(a)) as [number, number, number, number, number],
}),
title: i.title,
description: i.content,
location: i.location,
categories: [i.type.type],
organizer: { name: CLUB_NAME, email: MAIL_USERNAME },
created: moment(i.createdAt)
.format("YYYY-M-D-H-m")
.split("-")
.map((a) => parseInt(a)) as [number, number, number, number, number],
lastModified: moment(i.updatedAt)
.format("YYYY-M-D-H-m")
.split("-")
.map((a) => parseInt(a)) as [number, number, number, number, number],
transp: "OPAQUE" as "OPAQUE",
...(CLUB_WEBSITE != "" ? { url: CLUB_WEBSITE } : {}),
alarms: [
{
action: "display",
description: "Erinnerung",
trigger: {
minutes: 30,
before: true,
},
},
],
}))
);
}
}

View file

@ -0,0 +1,16 @@
import { newsletterDemoData } from "../demodata/newsletter.data";
import { protocolDemoData } from "../demodata/protocol.data";
import { PermissionModule } from "../type/permissionTypes";
export abstract class DemoDataHelper {
static getData(scope: PermissionModule) {
switch (scope) {
case "protocol":
return protocolDemoData;
case "newsletter":
return newsletterDemoData;
default:
return {};
}
}
}

View file

@ -1,6 +1,6 @@
import { Brackets, DataSource, NotBrackets, ObjectLiteral, SelectQueryBuilder, WhereExpressionBuilder } from "typeorm";
import { dataSource } from "../data-source";
import { ConditionStructure, DynamicQueryStructure } from "../type/dynamicQueries";
import { ConditionStructure, DynamicQueryStructure, FieldType, QueryResult } from "../type/dynamicQueries";
import { TableMeta } from "../type/tableMeta";
export default abstract class DynamicQueryBuilder {
@ -229,4 +229,141 @@ export default abstract class DynamicQueryBuilder {
return { query, parameters };
}
public static flattenQueryResult(result: Array<QueryResult>): Array<{ [key: string]: FieldType }> {
function flatten(row: QueryResult, prefix: string = ""): Array<{ [key: string]: FieldType }> {
let results: Array<{ [key: string]: FieldType }> = [{}];
for (const key in row) {
const value = row[key];
const newKey = prefix ? `${prefix}_${key}` : key;
if (Array.isArray(value) && value.every((item) => typeof item === "object" && item !== null)) {
const arrayResults: Array<{ [key: string]: FieldType }> = [];
value.forEach((item) => {
const flattenedItems = flatten(item, newKey);
arrayResults.push(...flattenedItems);
});
const tempResults: Array<{ [key: string]: FieldType }> = [];
results.forEach((res) => {
arrayResults.forEach((arrRes) => {
tempResults.push({ ...res, ...arrRes });
});
});
results = tempResults;
} else if (value && typeof value === "object" && !Array.isArray(value)) {
const objResults = flatten(value as QueryResult, newKey);
const tempResults: Array<{ [key: string]: FieldType }> = [];
results.forEach((res) => {
objResults.forEach((objRes) => {
tempResults.push({ ...res, ...objRes });
});
});
results = tempResults;
} else {
results.forEach((res) => {
res[newKey] = String(value);
});
}
}
return results;
}
const flattenedResults: Array<{ [key: string]: FieldType }> = [];
result.forEach((item) => {
const flattenedItems = flatten(item);
flattenedResults.push(...flattenedItems);
});
return flattenedResults;
}
public static async executeQuery(
query: string | DynamicQueryStructure,
offset: number,
count: number
): Promise<
| {
stats: "error";
sql: string;
code: string;
msg: string;
}
| {
stats: "success";
rows: Array<{ [key: string]: FieldType }>;
total: number;
offset: number;
count: number;
}
> {
if (typeof query == "string") {
const upperQuery = query.trim().toUpperCase();
if (!upperQuery.startsWith("SELECT") || /INSERT|UPDATE|DELETE|ALTER|DROP|CREATE|TRUNCATE/.test(upperQuery)) {
return {
stats: "error",
sql: query,
code: "UNALLOWED",
msg: "Not allowed to change rows",
};
}
try {
let data: Array<any> = [];
return await dataSource
.transaction(async (manager) => {
data = await manager.query(query);
throw new Error("AllwaysRollbackQuery");
})
.catch((error) => {
if (error.message === "AllwaysRollbackQuery") {
return {
stats: "success",
rows: data,
total: data.length,
offset: offset,
count: count,
};
} else {
return {
stats: "error",
sql: error.sql,
code: error.code,
msg: error.sqlMessage,
};
}
});
} catch (error) {
return {
stats: "error",
sql: error.sql,
code: error.code,
msg: error.sqlMessage,
};
}
} else {
try {
let [rows, total] = await this.buildQuery(query, offset, count).getManyAndCount();
return {
stats: "success",
rows: this.flattenQueryResult(rows),
total: total,
offset: offset,
count: count,
};
} catch (error) {
return {
stats: "error",
sql: error.sql,
code: error.code,
msg: error.sqlMessage,
};
}
}
}
}

View file

@ -0,0 +1,61 @@
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "fs";
import { join } from "path";
import { readdirSync } from "fs";
export abstract class FileSystemHelper {
static createFolder(...args: string[]) {
const exportPath = this.formatPath(...args);
if (!existsSync(exportPath)) {
mkdirSync(exportPath, { recursive: true });
}
}
static readFile(...filePath: string[]) {
return readFileSync(this.formatPath(...filePath), "utf8");
}
static readFileasBase64(...filePath: string[]) {
return readFileSync(this.formatPath(...filePath), "base64");
}
static readTemplateFile(filePath: string) {
return readFileSync(process.cwd() + filePath, "utf8");
}
static writeFile(filePath: string, filename: string, file: any) {
this.createFolder(filePath);
let path = this.formatPath(filePath, filename);
writeFileSync(path, file);
}
static formatPath(...args: string[]) {
return join(process.cwd(), "export", ...args);
}
static normalizePath(...args: string[]) {
return join(...args);
}
static getFilesInDirectory(directoryPath: string, filetype?: string): string[] {
const fullPath = this.formatPath(directoryPath);
if (!existsSync(fullPath)) {
return [];
}
return readdirSync(fullPath, { withFileTypes: true })
.filter((dirent) => !dirent.isDirectory() && (!filetype || dirent.name.endsWith(filetype)))
.map((dirent) => dirent.name);
}
static clearDirectoryByFiletype(directoryPath: string, filetype: string) {
const fullPath = this.formatPath(directoryPath);
if (!existsSync(fullPath)) {
return;
}
readdirSync(fullPath, { withFileTypes: true })
.filter((dirent) => !dirent.isDirectory() && dirent.name.endsWith(filetype))
.forEach((dirent) => {
const filePath = join(fullPath, dirent.name);
unlinkSync(filePath);
});
}
}

View file

@ -1,11 +1,9 @@
import { Transporter, createTransport, TransportOptions } from "nodemailer";
import { CLUB_NAME, MAIL_HOST, MAIL_PASSWORD, MAIL_PORT, MAIL_SECURE, MAIL_USERNAME } from "../env.defaults";
import { Attachment } from "nodemailer/lib/mailer";
export default class MailHelper {
private readonly transporter: Transporter;
constructor() {
this.transporter = createTransport({
export default abstract class MailHelper {
private static readonly transporter: Transporter = createTransport({
host: MAIL_HOST,
port: MAIL_PORT,
secure: (MAIL_SECURE as "true" | "false") == "true",
@ -14,7 +12,6 @@ export default class MailHelper {
pass: MAIL_PASSWORD,
},
} as TransportOptions);
}
/**
* @description send mail
@ -23,7 +20,12 @@ export default class MailHelper {
* @param {string} content
* @returns {Prmose<*>}
*/
async sendMail(target: string, subject: string, content: string): Promise<any> {
static async sendMail(
target: string,
subject: string,
content: string,
attach: Array<Attachment> = []
): Promise<any> {
return new Promise((resolve, reject) => {
this.transporter
.sendMail({
@ -31,6 +33,8 @@ export default class MailHelper {
to: target,
subject,
text: content,
html: content,
attachments: attach,
})
.then((info) => resolve(info.messageId))
.catch((e) => reject(e));

View file

@ -0,0 +1,341 @@
import Mail from "nodemailer/lib/mailer";
import { member } from "../entity/member";
import { newsletter } from "../entity/newsletter";
import { newsletterDates } from "../entity/newsletterDates";
import { newsletterRecipients } from "../entity/newsletterRecipients";
import MemberService from "../service/memberService";
import NewsletterDatesService from "../service/newsletterDatesService";
import NewsletterRecipientsService from "../service/newsletterRecipientsService";
import NewsletterService from "../service/newsletterService";
import { CalendarHelper } from "./calendarHelper";
import DynamicQueryBuilder from "./dynamicQueryBuilder";
import { FileSystemHelper } from "./fileSystemHelper";
import MailHelper from "./mailHelper";
import { CLUB_NAME } from "../env.defaults";
import { TemplateHelper } from "./templateHelper";
import { PdfExport } from "./pdfExport";
import NewsletterConfigService from "../service/newsletterConfigService";
import { NewsletterConfigType } from "../enums/newsletterConfigType";
import InternalException from "../exceptions/internalException";
import EventEmitter from "events";
export interface NewsletterEventType {
kind: "pdf" | "mail";
newsletterId: number;
total: number;
iteration: number;
msg: string;
}
export abstract class NewsletterHelper {
public static jobStatus = new EventEmitter();
private static formatJobEmit(
event: "progress" | "complete",
kind: "pdf" | "mail",
newsletterId: number,
total: number,
iteration: number,
msg: string
) {
this.jobStatus.emit<NewsletterEventType>(event, { kind, newsletterId, total, iteration, msg, date: new Date() });
}
public static buildData(
newsletter: newsletter,
dates: Array<newsletterDates>,
recipient?: member,
showAdress: boolean = false
) {
return {
title: newsletter.title,
description: newsletter.description,
newsletterTitle: newsletter.newsletterTitle,
newsletterText: newsletter.newsletterText,
newsletterSignatur: newsletter.newsletterSignatur,
dates: dates.map((d) => ({
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,
})),
...(recipient
? {
recipient: {
firstname: recipient.firstname,
lastname: recipient.lastname,
salutation: recipient.salutation,
nameaffix: recipient.nameaffix,
...(showAdress
? {
street: recipient.sendNewsletter.street ?? "",
streetNumber: recipient.sendNewsletter.streetNumber ?? "",
streetNumberAdd: recipient.sendNewsletter.streetNumberAddition ?? "",
}
: {}),
},
}
: {}),
};
}
public static async transformRecipientsToMembers(
newsletter: newsletter,
recipients: Array<newsletterRecipients>
): Promise<Array<member>> {
let useQuery = newsletter.recipientsByQuery?.query;
let queryMemberIds: Array<number> = [];
if (useQuery) {
let result = await DynamicQueryBuilder.executeQuery(
useQuery.startsWith("{") ? JSON.parse(useQuery) : useQuery,
0,
1000
);
if (result.stats == "success") {
let keys = Object.keys(result.rows?.[0] ?? {});
let memberKey = keys.find((k) => k.includes("member_id"));
queryMemberIds = result.rows.map((t) => parseInt((t[memberKey] ?? t.id) as string));
}
}
for (let recipient of recipients) {
if (!queryMemberIds.includes(recipient.memberId)) {
queryMemberIds.push(recipient.memberId);
}
}
console.log(queryMemberIds);
let members = await MemberService.getAll(0, 1000);
return members[0].filter((m) => queryMemberIds.includes(m.id));
}
public static getICSFilePath(newsletter: newsletter) {
return FileSystemHelper.formatPath(
"newsletter",
`${newsletter.id}_${newsletter.title.replace(" ", "")}`,
`events.ics`
);
}
public static saveIcsToFile(newsletter: newsletter, ics: string) {
FileSystemHelper.writeFile(`newsletter/${newsletter.id}_${newsletter.title.replace(" ", "")}`, "events.ics", ics);
}
public static async sendMails(newsletterId: number) {
let newsletter = await NewsletterService.getById(newsletterId);
let dates = await NewsletterDatesService.getAll(newsletterId);
let recipients = await NewsletterRecipientsService.getAll(newsletterId);
let config = await NewsletterConfigService.getAll();
const { value, error } = CalendarHelper.buildICS(dates.map((r) => r.calendar));
if (error) throw new InternalException("Failed Building ICS form Mail", error);
this.saveIcsToFile(newsletter, value);
let allowedForMail = config.filter((c) => c.config == NewsletterConfigType.mail).map((c) => c.comTypeId);
const members = await this.transformRecipientsToMembers(newsletter, recipients);
const mailRecipients = members.filter(
(m) =>
m.sendNewsletter != null &&
m.sendNewsletter?.email != null &&
allowedForMail.includes(m.sendNewsletter?.type?.id)
);
this.formatJobEmit("progress", "mail", newsletterId, mailRecipients.length, 0, "starting sending");
for (const [index, rec] of mailRecipients.entries()) {
this.formatJobEmit(
"progress",
"mail",
newsletterId,
mailRecipients.length,
index,
`start sending to ${rec.sendNewsletter.email}`
);
let data = this.buildData(newsletter, dates, rec);
const { body } = await TemplateHelper.renderFileForModule({
module: "newsletter",
bodyData: data,
title: `Newsletter von ${CLUB_NAME}`,
});
await MailHelper.sendMail(rec.sendNewsletter.email, `Newsletter von ${CLUB_NAME}`, body, [
{ filename: "events.ics", path: this.getICSFilePath(newsletter) },
])
.then(() => {
this.formatJobEmit(
"progress",
"mail",
newsletterId,
mailRecipients.length,
index,
`successfully sent to ${rec.sendNewsletter.email}`
);
})
.catch((err) => {
this.formatJobEmit(
"progress",
"mail",
newsletterId,
mailRecipients.length,
index,
`failed to send to ${rec.sendNewsletter.email}`
);
console.log("mail send", err);
});
}
this.formatJobEmit(
"complete",
"mail",
newsletterId,
mailRecipients.length,
mailRecipients.length,
`completed sending process`
);
}
public static async printPdfs(newsletterId: number) {
let newsletter = await NewsletterService.getById(newsletterId);
let dates = await NewsletterDatesService.getAll(newsletterId);
let recipients = await NewsletterRecipientsService.getAll(newsletterId);
let config = await NewsletterConfigService.getAll();
FileSystemHelper.clearDirectoryByFiletype(
`newsletter/${newsletter.id}_${newsletter.title.replace(" ", "")}`,
".pdf"
);
const { value, error } = CalendarHelper.buildICS(dates.map((r) => r.calendar));
if (error) throw new InternalException("Failed Building ICS form Pdf", error);
this.saveIcsToFile(newsletter, value);
let notAllowedForPdf = config.filter((c) => c.config == NewsletterConfigType.mail).map((c) => c.comTypeId);
let printWithAdress = config.filter((c) => c.config == NewsletterConfigType.pdf).map((c) => c.comTypeId);
const members = await this.transformRecipientsToMembers(newsletter, recipients);
const pdfRecipients = members.filter(
(m) => !notAllowedForPdf.includes(m.sendNewsletter?.type?.id) || m.sendNewsletter == null
);
this.formatJobEmit("progress", "pdf", newsletterId, pdfRecipients.length, 0, "starting sending");
for (const [index, rec] of pdfRecipients.entries()) {
this.formatJobEmit(
"progress",
"pdf",
newsletterId,
pdfRecipients.length,
index,
`start print for ${rec.lastname}, ${rec.firstname}`
);
let data = this.buildData(newsletter, dates, rec, printWithAdress.includes(rec.sendNewsletter?.type?.id));
await PdfExport.renderFile({
template: "newsletter",
title: `Newsletter von ${CLUB_NAME}`,
filename: `${rec.lastname}_${rec.firstname}_${rec.id}`,
folder: `newsletter/${newsletter.id}_${newsletter.title.replace(" ", "")}`,
data: data,
})
.then(() => {
this.formatJobEmit(
"progress",
"pdf",
newsletterId,
pdfRecipients.length,
index,
`successfully printed for ${rec.lastname}, ${rec.firstname}`
);
})
.catch((err) => {
this.formatJobEmit(
"progress",
"pdf",
newsletterId,
pdfRecipients.length,
index,
`failed print for ${rec.lastname}, ${rec.firstname}`
);
console.log("pdf print", err);
});
}
this.formatJobEmit(
"progress",
"pdf",
newsletterId,
pdfRecipients.length,
pdfRecipients.length,
"starting pdf combine"
);
await PdfExport.sqashToSingleFile(
`newsletter/${newsletter.id}_${newsletter.title.replace(" ", "")}`,
"allPdfsTogether",
`newsletter/${newsletter.id}_${newsletter.title.replace(" ", "")}`
)
.then(() => {
this.formatJobEmit(
"progress",
"pdf",
newsletterId,
pdfRecipients.length,
pdfRecipients.length,
"sucessfully combined pdf"
);
})
.catch((err) => {
this.formatJobEmit(
"progress",
"pdf",
newsletterId,
pdfRecipients.length,
pdfRecipients.length,
"failed combining pdf"
);
console.log("pdf squash", err);
});
this.formatJobEmit(
"complete",
"pdf",
newsletterId,
pdfRecipients.length,
pdfRecipients.length,
`completed printing process`
);
}
}

View file

@ -1,53 +1,84 @@
import { readFileSync } from "fs";
import Handlebars from "handlebars";
import puppeteer from "puppeteer";
import { TemplateHelper } from "./templateHelper";
import { PermissionModule } from "../type/permissionTypes";
import { FileSystemHelper } from "./fileSystemHelper";
import { PDFDocument } from "pdf-lib";
export abstract class PdfExport {
static getTemplate(template: string) {
return readFileSync(process.cwd() + "/src/templates/" + template, "utf8");
}
static async renderFile({
template,
title = "pdf-export Mitgliederverwaltung",
filename,
data,
filename = null,
data = {},
saveToDisk = true,
margins = { top: "15mm", bottom: "15mm" },
folder = "",
}: {
template: string;
title: string;
filename: string;
data: any;
template: PermissionModule;
title?: string;
filename?: string;
data?: any;
saveToDisk?: boolean;
margins?: { top: string; bottom: string };
folder?: string;
}) {
const templateHtml = this.getTemplate(template);
const templateCompiled = Handlebars.compile(templateHtml);
const html = templateCompiled(data);
if (folder != "") FileSystemHelper.createFolder(folder);
const { header, footer, body } = await TemplateHelper.renderFileForModule({
module: template,
headerData: data,
bodyData: data,
footerData: data,
title: title,
});
const browser = await puppeteer.launch({
headless: true,
args: ["--no-sandbox", "--disable-gpu", "--disable-setuid-sandbox"],
});
const page = await browser.newPage();
await page.setContent(html, { waitUntil: "domcontentloaded" });
await page.setContent(body, { waitUntil: "domcontentloaded" });
await page.pdf({
path: process.cwd() + `/export/${filename}.pdf`, // Name der PDF-Datei
const exportPath = FileSystemHelper.formatPath(folder, `${filename}.pdf`);
let pdf = await page.pdf({
...(saveToDisk ? { path: exportPath } : {}),
format: "A4",
printBackground: false,
margin: {
top: "15mm",
bottom: "15mm",
top: margins.top,
bottom: margins.bottom,
left: "10mm",
right: "10mm",
},
displayHeaderFooter: true,
headerTemplate: `<h1 style="font-size:10px; text-align:center; width:100%;">${title}</h1>`,
footerTemplate: `
<div style="font-size:10px; text-align:center; width:100%; color:#888;">
Seite <span class="pageNumber"></span> von <span class="totalPages"></span>
</div>
`,
headerTemplate: header,
footerTemplate: footer,
});
await browser.close();
return pdf;
}
static async sqashToSingleFile(inputFolder: string, outputFile: string, outputFolder: string = "") {
if (outputFolder != "") FileSystemHelper.createFolder(outputFolder);
let pdfFilePaths = FileSystemHelper.getFilesInDirectory(inputFolder, ".pdf");
if (pdfFilePaths.length == 0) return;
const mergedPdf = await PDFDocument.create();
for (const pdfPath of pdfFilePaths) {
const pdfBytes = FileSystemHelper.readFileasBase64(inputFolder, pdfPath);
const pdf = await PDFDocument.load(pdfBytes);
const copiedPages = await mergedPdf.copyPages(pdf, pdf.getPageIndices());
copiedPages.forEach((page) => mergedPdf.addPage(page));
}
const mergedPdfBytes = await mergedPdf.save();
FileSystemHelper.writeFile(outputFolder, `${outputFile}.pdf`, mergedPdfBytes);
}
}

View file

@ -0,0 +1,74 @@
import TemplateService from "../service/templateService";
import { PermissionModule } from "../type/permissionTypes";
import TemplateUsageService from "../service/templateUsageService";
import Handlebars from "handlebars";
import { FileSystemHelper } from "./fileSystemHelper";
export abstract class TemplateHelper {
static getTemplateFromFile(template: string) {
return FileSystemHelper.readTemplateFile(`/src/templates/${template}.template.html`);
}
static async getTemplateFromStore(templateId: number): Promise<string> {
return (await TemplateService.getById(templateId)).html;
}
static applyDataToTemplate(template: string, data: any): string {
const normalizedTemplate = this.normalizeTemplate(template);
const templateCompiled = Handlebars.compile(normalizedTemplate);
return templateCompiled(data);
}
static normalizeTemplate(template: string): string {
template = template.replace(/<listend>.*?<\/listend>/g, "{{/each}}");
template = template.replace(/<liststart\b[^>]*>(WDH Start: )?/g, "{{#each ");
template = template.replace(/<\/liststart>/g, "}}");
return template;
}
static async renderFileForModule({
module,
title = "pdf-export Mitgliederverwaltung",
headerData = {},
bodyData = {},
footerData = {},
}: {
module: PermissionModule;
title?: string;
headerData?: any;
bodyData?: any;
footerData?: any;
}): Promise<{ header: string; body: string; footer: string; margins?: { top: string; bottom: string } }> {
const moduleTemplates = await TemplateUsageService.getByScope(module);
let header = `<h1 style="font-size:10px; text-align:center; width:100%;">${title}</h1>`;
let footer = "";
let body = "";
if (moduleTemplates.headerId) {
header = await this.getTemplateFromStore(moduleTemplates.headerId);
header = this.applyDataToTemplate(header, { title, ...headerData });
}
if (moduleTemplates.footerId) {
footer = await this.getTemplateFromStore(moduleTemplates.footerId);
} else {
footer = this.getTemplateFromFile(module + ".footer");
}
footer = this.applyDataToTemplate(footer, footerData);
if (moduleTemplates.bodyId) {
body = await this.getTemplateFromStore(moduleTemplates.bodyId);
} else {
body = this.getTemplateFromFile(module + ".body");
}
body = this.applyDataToTemplate(body, bodyData);
return {
header,
footer,
body,
};
}
}

View file

@ -0,0 +1,28 @@
import { MigrationInterface, QueryRunner, Table } from "typeorm";
import { DB_TYPE } from "../env.defaults";
export class Template1734854680201 implements MigrationInterface {
name = "Template1734854680201";
public async up(queryRunner: QueryRunner): Promise<void> {
const variableType_int = DB_TYPE == "mysql" ? "int" : "integer";
await queryRunner.createTable(
new Table({
name: "template",
columns: [
{ name: "id", type: variableType_int, isPrimary: true, isGenerated: true, generationStrategy: "increment" },
{ name: "template", type: "varchar", length: "255", isNullable: false },
{ name: "description", type: "varchar", length: "255", isNullable: true },
{ name: "design", type: "text", isNullable: false, default: "'{}'" },
{ name: "html", type: "text", isNullable: false, default: "''" },
],
}),
true
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable("template");
}
}

View file

@ -0,0 +1,75 @@
import { MigrationInterface, QueryRunner, Table, TableForeignKey } from "typeorm";
import { DB_TYPE } from "../env.defaults";
import { templateUsage } from "../entity/templateUsage";
export class TemplateUsage1734949173739 implements MigrationInterface {
name = "TemplateUsage1734949173739";
public async up(queryRunner: QueryRunner): Promise<void> {
const variableType_int = DB_TYPE == "mysql" ? "int" : "integer";
await queryRunner.createTable(
new Table({
name: "template_usage",
columns: [
{ name: "scope", type: "varchar", length: "255", isPrimary: true },
{ name: "headerId", type: variableType_int, isNullable: true },
{ name: "bodyId", type: variableType_int, isNullable: true },
{ name: "footerId", type: variableType_int, isNullable: true },
],
}),
true
);
await queryRunner.manager
.createQueryBuilder()
.insert()
.into(templateUsage)
.values({ scope: "protocol" })
.orIgnore()
.execute();
await queryRunner.createForeignKey(
"template_usage",
new TableForeignKey({
columnNames: ["headerId"],
referencedColumnNames: ["id"],
referencedTableName: "template",
onDelete: "RESTRICT",
onUpdate: "RESTRICT",
})
);
await queryRunner.createForeignKey(
"template_usage",
new TableForeignKey({
columnNames: ["bodyId"],
referencedColumnNames: ["id"],
referencedTableName: "template",
onDelete: "RESTRICT",
onUpdate: "RESTRICT",
})
);
await queryRunner.createForeignKey(
"template_usage",
new TableForeignKey({
columnNames: ["footerId"],
referencedColumnNames: ["id"],
referencedTableName: "template",
onDelete: "RESTRICT",
onUpdate: "RESTRICT",
})
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
const template_usage = await queryRunner.getTable("template_usage");
let foreignKey = template_usage.foreignKeys.find((fk) => fk.columnNames.indexOf("headerId") !== -1);
await queryRunner.dropForeignKey("template_usage", foreignKey);
foreignKey = template_usage.foreignKeys.find((fk) => fk.columnNames.indexOf("bodyId") !== -1);
await queryRunner.dropForeignKey("template_usage", foreignKey);
foreignKey = template_usage.foreignKeys.find((fk) => fk.columnNames.indexOf("footerId") !== -1);
await queryRunner.dropForeignKey("template_usage", foreignKey);
await queryRunner.dropTable("template_usage");
}
}

View file

@ -0,0 +1,144 @@
import { MigrationInterface, QueryRunner, Table, TableForeignKey } from "typeorm";
import { DB_TYPE } from "../env.defaults";
import { templateUsage } from "../entity/templateUsage";
export class Newsletter1735118780511 implements MigrationInterface {
name = "Newsletter1735118780511";
public async up(queryRunner: QueryRunner): Promise<void> {
const variableType_int = DB_TYPE == "mysql" ? "int" : "integer";
await queryRunner.createTable(
new Table({
name: "newsletter_dates",
columns: [
{ name: "newsletterId", type: variableType_int, isPrimary: true },
{ name: "calendarId", type: "varchar", length: "255", isPrimary: true },
{ name: "diffTitle", type: "varchar", length: "255", isNullable: true },
{ name: "diffDescription", type: "text", isNullable: true },
],
}),
true
);
await queryRunner.createTable(
new Table({
name: "newsletter_recipients",
columns: [
{ name: "newsletterId", type: variableType_int, isPrimary: true },
{ name: "memberId", type: variableType_int, isPrimary: true },
],
}),
true
);
await queryRunner.createTable(
new Table({
name: "newsletter",
columns: [
{ name: "id", type: variableType_int, isPrimary: true, isGenerated: true, generationStrategy: "increment" },
{ name: "title", type: "varchar", length: "255" },
{ name: "description", type: "varchar", length: "255", default: "''" },
{ name: "newsletterTitle", type: "varchar", length: "255", default: "''" },
{ name: "newsletterText", type: "text", default: "''" },
{ name: "newsletterSignatur", type: "varchar", length: "255", default: "''" },
{ name: "isSent", type: "tinyint", default: "0" },
{ name: "recipientsByQueryId", type: variableType_int, isNullable: true },
],
}),
true
);
await queryRunner.createForeignKey(
"newsletter_dates",
new TableForeignKey({
columnNames: ["newsletterId"],
referencedColumnNames: ["id"],
referencedTableName: "newsletter",
onDelete: "CASCADE",
onUpdate: "RESTRICT",
})
);
await queryRunner.createForeignKey(
"newsletter_dates",
new TableForeignKey({
columnNames: ["calendarId"],
referencedColumnNames: ["id"],
referencedTableName: "calendar",
onDelete: "RESTRICT",
onUpdate: "RESTRICT",
})
);
await queryRunner.createForeignKey(
"newsletter_recipients",
new TableForeignKey({
columnNames: ["newsletterId"],
referencedColumnNames: ["id"],
referencedTableName: "newsletter",
onDelete: "CASCADE",
onUpdate: "RESTRICT",
})
);
await queryRunner.createForeignKey(
"newsletter_recipients",
new TableForeignKey({
columnNames: ["memberId"],
referencedColumnNames: ["id"],
referencedTableName: "member",
onDelete: "CASCADE",
onUpdate: "RESTRICT",
})
);
await queryRunner.createForeignKey(
"newsletter",
new TableForeignKey({
columnNames: ["recipientsByQueryId"],
referencedColumnNames: ["id"],
referencedTableName: "query",
onDelete: "CASCADE",
onUpdate: "RESTRICT",
})
);
await queryRunner.manager
.createQueryBuilder()
.insert()
.into(templateUsage)
.values({ scope: "newsletter" })
.orIgnore()
.execute();
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.manager
.createQueryBuilder()
.delete()
.from(templateUsage)
.where({ scope: "newsletter" })
.execute();
const tableN = await queryRunner.getTable("newsletter");
const tableNR = await queryRunner.getTable("newsletter_recipients");
const tableND = await queryRunner.getTable("newsletter_dates");
const foreignKeyN = tableN.foreignKeys.find((fk) => fk.columnNames.indexOf("recipientsByQueryId") !== -1);
const foreignKeyNR = tableNR.foreignKeys.find((fk) => fk.columnNames.indexOf("newsletterId") !== -1);
const foreignKeyNR2 = tableNR.foreignKeys.find((fk) => fk.columnNames.indexOf("memberId") !== -1);
const foreignKeyND1 = tableND.foreignKeys.find((fk) => fk.columnNames.indexOf("newsletterId") !== -1);
const foreignKeyND2 = tableND.foreignKeys.find((fk) => fk.columnNames.indexOf("calendarId") !== -1);
await queryRunner.dropForeignKey("newsletter", foreignKeyN);
await queryRunner.dropForeignKey("newsletter_recipients", foreignKeyNR);
await queryRunner.dropForeignKey("newsletter_recipients", foreignKeyNR2);
await queryRunner.dropForeignKey("newsletter_dates", foreignKeyND1);
await queryRunner.dropForeignKey("newsletter_dates", foreignKeyND2);
await queryRunner.dropTable("newsletter");
await queryRunner.dropTable("newsletter_recipients");
await queryRunner.dropTable("newsletter_dates");
}
}

View file

@ -0,0 +1,39 @@
import { MigrationInterface, QueryRunner, Table, TableForeignKey } from "typeorm";
import { DB_TYPE } from "../env.defaults";
export class NewsletterConfig1735207446910 implements MigrationInterface {
name = "NewsletterConfig1735207446910";
public async up(queryRunner: QueryRunner): Promise<void> {
const variableType_int = DB_TYPE == "mysql" ? "int" : "integer";
await queryRunner.createTable(
new Table({
name: "newsletter_config",
columns: [
{ name: "comTypeId", type: variableType_int, isPrimary: true, isNullable: false },
{ name: "config", type: "varchar", length: "255", isNullable: false },
],
}),
true
);
await queryRunner.createForeignKey(
"newsletter_config",
new TableForeignKey({
columnNames: ["comTypeId"],
referencedColumnNames: ["id"],
referencedTableName: "communication_type",
onDelete: "CASCADE",
onUpdate: "RESTRICT",
})
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
const table = await queryRunner.getTable("newsletter_config");
const foreignKey = table.foreignKeys.find((fk) => fk.columnNames.indexOf("comTypeId") !== -1);
await queryRunner.dropForeignKey("newsletter_config", foreignKey);
await queryRunner.dropTable("newsletter_config");
}
}

View file

@ -8,11 +8,15 @@ import membershipStatus from "./membershipStatus";
import qualification from "./qualification";
import calendarType from "./calendarType";
import queryStore from "./queryStore";
import template from "./template";
import templateUsage from "./templateUsage";
import newsletterConfig from "./newsletterConfig";
import member from "./member";
import protocol from "./protocol";
import calendar from "./calendar";
import queryBuilder from "./queryBuilder";
import newsletter from "./newsletter";
import role from "./role";
import user from "./user";
@ -39,11 +43,19 @@ router.use(
router.use("/qualification", PermissionHelper.passCheckMiddleware("read", "settings", "qualification"), qualification);
router.use("/calendartype", PermissionHelper.passCheckMiddleware("read", "settings", "calendar_type"), calendarType);
router.use("/querystore", PermissionHelper.passCheckMiddleware("read", "settings", "query_store"), queryStore);
router.use("/template", PermissionHelper.passCheckMiddleware("read", "settings", "template"), template);
router.use("/templateusage", PermissionHelper.passCheckMiddleware("read", "settings", "template_usage"), templateUsage);
router.use(
"/newsletterconfig",
PermissionHelper.passCheckMiddleware("read", "settings", "newsletter_config"),
newsletterConfig
);
router.use("/member", PermissionHelper.passCheckMiddleware("read", "club", "member"), member);
router.use("/protocol", PermissionHelper.passCheckMiddleware("read", "club", "protocol"), protocol);
router.use("/calendar", PermissionHelper.passCheckMiddleware("read", "club", "calendar"), calendar);
router.use("/querybuilder", PermissionHelper.passCheckMiddleware("read", "club", "query"), queryBuilder);
router.use("/newsletter", PermissionHelper.passCheckMiddleware("read", "club", "newsletter"), newsletter);
router.use("/role", PermissionHelper.passCheckMiddleware("read", "user", "role"), role);
router.use("/user", PermissionHelper.passCheckMiddleware("read", "user", "user"), user);

View file

@ -0,0 +1,112 @@
import express, { Request, Response } from "express";
import {
createNewsletter,
createNewsletterPrintoutById,
getAllNewsletters,
getNewsletterDatesById,
getNewsletterById,
getNewsletterRecipientsById,
getNewsletterPrintoutByIdAndPrint,
getNewsletterPrintoutsById,
synchronizeNewsletterDatesById,
synchronizeNewsletterById,
synchronizeNewsletterRecipientsById,
sendNewsletterById,
createNewsletterMailPreviewById,
createNewsletterPrintoutPreviewById,
getNewsletterPrintoutProgressById,
getNewsletterSendingProgressById,
} from "../../controller/admin/newsletterController";
import PermissionHelper from "../../helpers/permissionHelper";
var router = express.Router({ mergeParams: true });
router.get("/", async (req: Request, res: Response) => {
await getAllNewsletters(req, res);
});
router.get("/:id", async (req: Request, res: Response) => {
await getNewsletterById(req, res);
});
router.get("/:newsletterId/dates", async (req: Request, res: Response) => {
await getNewsletterDatesById(req, res);
});
router.get("/:newsletterId/recipients", async (req: Request, res: Response) => {
await getNewsletterRecipientsById(req, res);
});
router.get("/:newsletterId/printouts", async (req: Request, res: Response) => {
await getNewsletterPrintoutsById(req, res);
});
router.get("/:newsletterId/printout/:filename", async (req: Request, res: Response) => {
await getNewsletterPrintoutByIdAndPrint(req, res);
});
router.get("/:newsletterId/printoutpreview", async (req: Request, res: Response) => {
await createNewsletterPrintoutPreviewById(req, res);
});
router.get("/:newsletterId/printoutprogress", async (req: Request, res: Response) => {
await getNewsletterPrintoutProgressById(req, res);
});
router.get("/:newsletterId/sendprogress", async (req: Request, res: Response) => {
await getNewsletterSendingProgressById(req, res);
});
router.post(
"/",
PermissionHelper.passCheckMiddleware("create", "club", "protocol"),
async (req: Request, res: Response) => {
await createNewsletter(req, res);
}
);
router.post(
"/:newsletterId/printout",
PermissionHelper.passCheckMiddleware("create", "club", "protocol"),
async (req: Request, res: Response) => {
await createNewsletterPrintoutById(req, res);
}
);
router.post("/:newsletterId/mailpreview", async (req: Request, res: Response) => {
await createNewsletterMailPreviewById(req, res);
});
router.post(
"/:newsletterId/send",
PermissionHelper.passCheckMiddleware("create", "club", "protocol"),
async (req: Request, res: Response) => {
await sendNewsletterById(req, res);
}
);
router.patch(
"/:id/synchronize",
PermissionHelper.passCheckMiddleware("update", "club", "protocol"),
async (req: Request, res: Response) => {
await synchronizeNewsletterById(req, res);
}
);
router.patch(
"/:newsletterId/synchronize/dates",
PermissionHelper.passCheckMiddleware("update", "club", "protocol"),
async (req: Request, res: Response) => {
await synchronizeNewsletterDatesById(req, res);
}
);
router.patch(
"/:newsletterId/synchronize/recipients",
PermissionHelper.passCheckMiddleware("update", "club", "protocol"),
async (req: Request, res: Response) => {
await synchronizeNewsletterRecipientsById(req, res);
}
);
export default router;

View file

@ -0,0 +1,36 @@
import express, { Request, Response } from "express";
import {
deleteNewsletterConfig,
getAllNewsletterConfigs,
getNewsletterConfigById,
setNewsletterConfig,
} from "../../controller/admin/newsletterConfigController";
import PermissionHelper from "../../helpers/permissionHelper";
var router = express.Router({ mergeParams: true });
router.get("/", async (req: Request, res: Response) => {
await getAllNewsletterConfigs(req, res);
});
router.get("/:id", async (req: Request, res: Response) => {
await getNewsletterConfigById(req, res);
});
router.put(
"/",
PermissionHelper.passCheckMiddleware("create", "settings", "newsletter_config"),
async (req: Request, res: Response) => {
await setNewsletterConfig(req, res);
}
);
router.delete(
"/:comTypeId",
PermissionHelper.passCheckMiddleware("create", "settings", "newsletter_config"),
async (req: Request, res: Response) => {
await deleteNewsletterConfig(req, res);
}
);
export default router;

View file

@ -0,0 +1,54 @@
import express, { Request, Response } from "express";
import {
cloneTemplate,
createTemplate,
deleteTemplate,
getAllTemplates,
getTemplateById,
updateTemplate,
} from "../../controller/admin/templateController";
import PermissionHelper from "../../helpers/permissionHelper";
var router = express.Router({ mergeParams: true });
router.get("/", async (req: Request, res: Response) => {
await getAllTemplates(req, res);
});
router.get("/:id", async (req: Request, res: Response) => {
await getTemplateById(req, res);
});
router.post(
"/",
PermissionHelper.passCheckMiddleware("create", "settings", "template"),
async (req: Request, res: Response) => {
await createTemplate(req, res);
}
);
router.post(
"/clone",
PermissionHelper.passCheckMiddleware("create", "settings", "template"),
async (req: Request, res: Response) => {
await cloneTemplate(req, res);
}
);
router.patch(
"/:id",
PermissionHelper.passCheckMiddleware("update", "settings", "template"),
async (req: Request, res: Response) => {
await updateTemplate(req, res);
}
);
router.delete(
"/:id",
PermissionHelper.passCheckMiddleware("delete", "settings", "template"),
async (req: Request, res: Response) => {
await deleteTemplate(req, res);
}
);
export default router;

View file

@ -0,0 +1,29 @@
import express, { Request, Response } from "express";
import PermissionHelper from "../../helpers/permissionHelper";
import {
getAllTemplateUsages,
printTemplateUsageDemo,
updateTemplateUsage,
} from "../../controller/admin/templateUsageController";
import { PermissionModule } from "../../type/permissionTypes";
import ForbiddenRequestException from "../../exceptions/forbiddenRequestException";
var router = express.Router({ mergeParams: true });
router.get("/", async (req: Request, res: Response) => {
await getAllTemplateUsages(req, res);
});
router.get("/:scope", async (req: Request, res: Response) => {
await printTemplateUsageDemo(req, res);
});
router.patch(
"/:scope",
PermissionHelper.passCheckMiddleware("update", "settings", "template_usage"),
async (req: Request, res: Response) => {
await updateTemplateUsage(req, res);
}
);
export default router;

View file

@ -44,12 +44,18 @@ export default abstract class CalendarService {
* @description get calendar by types
* @returns {Promise<Array<calendar>>}
*/
static async getByTypes(types: Array<number>): Promise<Array<calendar>> {
return await dataSource
static async getByTypes(types: Array<number>, addNscdr: boolean = false): Promise<Array<calendar>> {
const query = dataSource
.getRepository(calendar)
.createQueryBuilder("calendar")
.leftJoinAndSelect("calendar.type", "type")
.where("type.id IN (:...types)", { types: types })
.where("type.id IN (:...types)", { types: types });
if (addNscdr) {
query.orWhere("type.nscdr = :nscdr", { nscdr: true });
}
return await query
.getMany()
.then((res) => {
return res;

View file

@ -0,0 +1,43 @@
import { dataSource } from "../data-source";
import { newsletterConfig } from "../entity/newsletterConfig";
import { member } from "../entity/member";
import InternalException from "../exceptions/internalException";
export default abstract class NewsletterConfigService {
/**
* @description get all newsletterConfigs
* @returns {Promise<Array<newsletterConfig>>}
*/
static async getAll(): Promise<Array<newsletterConfig>> {
return await dataSource
.getRepository(newsletterConfig)
.createQueryBuilder("newsletterConfig")
.leftJoinAndSelect("newsletterConfig.comType", "comType")
.getMany()
.then((res) => {
return res;
})
.catch((err) => {
throw new InternalException("newsletterConfigs not found", err);
});
}
/**
* @description get newsletterConfig by id
* @returns {Promise<newsletterConfig>}
*/
static async getByComId(comId: number): Promise<newsletterConfig> {
return await dataSource
.getRepository(newsletterConfig)
.createQueryBuilder("newsletterConfig")
.leftJoinAndSelect("newsletterConfig.comType", "comType")
.where("newsletterConfig.comTypId = :comTypId", { icomTypId: comId })
.getOneOrFail()
.then((res) => {
return res;
})
.catch((err) => {
throw new InternalException("newsletterConfig not found by cmId", err);
});
}
}

View file

@ -0,0 +1,27 @@
import { dataSource } from "../data-source";
import { newsletterDates } from "../entity/newsletterDates";
import { member } from "../entity/member";
import InternalException from "../exceptions/internalException";
export default abstract class NewsletterDatesService {
/**
* @description get all newsletterDates
* @returns {Promise<Array<newsletterDates>>}
*/
static async getAll(newsletterId: number): Promise<Array<newsletterDates>> {
return await dataSource
.getRepository(newsletterDates)
.createQueryBuilder("newsletterDates")
.leftJoinAndSelect("newsletterDates.calendar", "calendar")
.leftJoinAndSelect("calendar.type", "type")
.leftJoinAndSelect("newsletterDates.newsletter", "newsletter")
.where("newsletterDates.newsletterId = :id", { id: newsletterId })
.getMany()
.then((res) => {
return res;
})
.catch((err) => {
throw new InternalException("newsletterDatess not found", err);
});
}
}

View file

@ -0,0 +1,28 @@
import { dataSource } from "../data-source";
import { newsletterRecipients } from "../entity/newsletterRecipients";
import { member } from "../entity/member";
import InternalException from "../exceptions/internalException";
export default abstract class NewsletterRecipientsService {
/**
* @description get all newsletterRecipients
* @returns {Promise<Array<newsletterRecipients>>}
*/
static async getAll(newsletterId: number): Promise<Array<newsletterRecipients>> {
return await dataSource
.getRepository(newsletterRecipients)
.createQueryBuilder("newsletterRecipients")
.leftJoinAndSelect("newsletterRecipients.member", "member")
.leftJoinAndSelect("member.sendNewsletter", "sendNewsletter")
.leftJoinAndSelect("sendNewsletter.type", "communicationtype")
.leftJoinAndSelect("newsletterRecipients.newsletter", "newsletter")
.where("newsletterRecipients.newsletterId = :id", { id: newsletterId })
.getMany()
.then((res) => {
return res;
})
.catch((err) => {
throw new InternalException("newsletterRecipients not found", err);
});
}
}

View file

@ -0,0 +1,43 @@
import { dataSource } from "../data-source";
import { newsletter } from "../entity/newsletter";
import InternalException from "../exceptions/internalException";
export default abstract class NewsletterService {
/**
* @description get all newsletters
* @returns {Promise<[Array<newsletter>, number]>}
*/
static async getAll(offset: number = 0, count: number = 25): Promise<[Array<newsletter>, number]> {
return await dataSource
.getRepository(newsletter)
.createQueryBuilder("newsletter")
.offset(offset)
.limit(count)
.getManyAndCount()
.then((res) => {
return res;
})
.catch((err) => {
throw new InternalException("newsletters not found", err);
});
}
/**
* @description get newsletter by id
* @returns {Promise<newsletter>}
*/
static async getById(id: number): Promise<newsletter> {
return await dataSource
.getRepository(newsletter)
.createQueryBuilder("newsletter")
.leftJoinAndSelect("newsletter.recipientsByQuery", "query")
.where("newsletter.id = :id", { id: id })
.getOneOrFail()
.then((res) => {
return res;
})
.catch((err) => {
throw new InternalException("newsletter not found by id", err);
});
}
}

View file

@ -21,23 +21,4 @@ export default abstract class ProtocolPresenceService {
throw new InternalException("protocolPresence not found", err);
});
}
/**
* @description get protocolDecision by id
* @returns {Promise<protocolPresence>}
*/
static async getById(id: number): Promise<protocolPresence> {
return await dataSource
.getRepository(protocolPresence)
.createQueryBuilder("protocolPresence")
.leftJoinAndSelect("protocolPresence.member", "member")
.where("protocolPresence.id = :id", { id: id })
.getOneOrFail()
.then((res) => {
return res;
})
.catch((err) => {
throw new InternalException("protocolDecision not found by id", err);
});
}
}

View file

@ -0,0 +1,41 @@
import { dataSource } from "../data-source";
import { template } from "../entity/template";
import { member } from "../entity/member";
import InternalException from "../exceptions/internalException";
export default abstract class TemplateService {
/**
* @description get all templates
* @returns {Promise<Array<template>>}
*/
static async getAll(): Promise<Array<template>> {
return await dataSource
.getRepository(template)
.createQueryBuilder("template")
.getMany()
.then((res) => {
return res;
})
.catch((err) => {
throw new InternalException("templates not found", err);
});
}
/**
* @description get template by id
* @returns {Promise<template>}
*/
static async getById(id: number): Promise<template> {
return await dataSource
.getRepository(template)
.createQueryBuilder("template")
.where("template.id = :id", { id: id })
.getOneOrFail()
.then((res) => {
return res;
})
.catch((err) => {
throw new InternalException("template not found by id", err);
});
}
}

View file

@ -0,0 +1,46 @@
import { dataSource } from "../data-source";
import { templateUsage } from "../entity/templateUsage";
import InternalException from "../exceptions/internalException";
export default abstract class TemplateUsageService {
/**
* @description get all templateUsages
* @returns {Promise<Array<templateUsage>>}
*/
static async getAll(): Promise<Array<templateUsage>> {
return await dataSource
.getRepository(templateUsage)
.createQueryBuilder("templateUsage")
.leftJoinAndSelect("templateUsage.header", "headerTemplate")
.leftJoinAndSelect("templateUsage.body", "bodyTemplate")
.leftJoinAndSelect("templateUsage.footer", "footerTemplate")
.getMany()
.then((res) => {
return res;
})
.catch((err) => {
throw new InternalException("templates not found", err);
});
}
/**
* @description get template by scope
* @returns {Promise<templateUsage>}
*/
static async getByScope(scope: string): Promise<templateUsage | null> {
return await dataSource
.getRepository(templateUsage)
.createQueryBuilder("templateUsage")
.leftJoinAndSelect("templateUsage.header", "headerTemplate")
.leftJoinAndSelect("templateUsage.body", "bodyTemplate")
.leftJoinAndSelect("templateUsage.footer", "footerTemplate")
.where("templateUsage.scope = :scope", { scope: scope })
.getOneOrFail()
.then((res) => {
return res;
})
.catch((err): null => {
return null;
});
}
}

View file

@ -0,0 +1,41 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Newsletter</title>
</head>
<body>
<h1>{{{newsletterTitle}}}</h1>
<p>{{{newsletterText}}}</p>
<br />
{{#each dates}}
<div>
<h2><b>{{this.formattedStarttime}}: {{this.title}}</b></h2>
<span>{{{this.content}}}</span>
</div>
{{/each}}
<br />
<br />
<p>{{{newsletterSignatur}}}</p>
</body>
<style>
h2,
h3,
p,
span,
ul,
li {
padding: 0;
margin: 0;
}
h1,
h2 {
color: #990b00;
}
h2 {
margin-bottom: 5px;
}
</style>
</html>

View file

@ -0,0 +1,4 @@
<div style="font-size: 10pt; width: 100%; margin: 0 20px; padding-top: 5px; color: #888; border-top: 0.5px solid black">
{{recipient.lastname}}, {{recipient.firstname}}{{#if recipient.street}},{{/if}} {{recipient.street}}
{{recipient.streetNumber}} {{recipient.streetNumberAdd}}
</div>

View file

@ -0,0 +1,3 @@
<div style="font-size: 10px; text-align: center; width: 100%; color: #888">
Seite <span class="pageNumber"></span> von <span class="totalPages"></span>
</div>

View file

@ -54,6 +54,10 @@ export type OrderByStructure = {
export type OrderByType = "ASC" | "DESC";
export type QueryResult = {
[key: string]: FieldType | QueryResult | Array<QueryResult>;
};
export const exampleQuery: DynamicQueryStructure = {
select: ["firstname", "lastname"],
table: "member",

View file

@ -4,6 +4,7 @@ export type PermissionModule =
| "member"
| "calendar"
| "newsletter"
| "newsletter_config"
| "protocol"
| "qualification"
| "award"
@ -14,7 +15,9 @@ export type PermissionModule =
| "user"
| "role"
| "query"
| "query_store";
| "query_store"
| "template"
| "template_usage";
export type PermissionType = "read" | "create" | "update" | "delete";
@ -42,6 +45,7 @@ export const permissionModules: Array<PermissionModule> = [
"member",
"calendar",
"newsletter",
"newsletter_config",
"protocol",
"qualification",
"award",
@ -53,6 +57,8 @@ export const permissionModules: Array<PermissionModule> = [
"role",
"query",
"query_store",
"template",
"template_usage",
];
export const permissionTypes: Array<PermissionType> = ["read", "create", "update", "delete"];
export const sectionsAndModules: SectionsAndModulesObject = {
@ -65,6 +71,9 @@ export const sectionsAndModules: SectionsAndModulesObject = {
"membership_status",
"calendar_type",
"query_store",
"template",
"template_usage",
"newsletter_config",
],
user: ["user", "role"],
};

View file

@ -0,0 +1,13 @@
import { QueryStoreViewModel } from "./queryStore.models";
export interface NewsletterViewModel {
id: number;
title: string;
description: string;
newsletterTitle: string;
newsletterText: string;
newsletterSignatur: string;
isSent: boolean;
recipientsByQueryId?: number;
recipientsByQuery?: QueryStoreViewModel;
}

View file

@ -0,0 +1,8 @@
import { NewsletterConfigType } from "../../enums/newsletterConfigType";
import { CommunicationTypeViewModel } from "./communicationType.models";
export interface NewsletterConfigViewModel {
comTypeId: number;
config: NewsletterConfigType;
comType: CommunicationTypeViewModel;
}

View file

@ -0,0 +1,9 @@
import { CalendarViewModel } from "./calendar.models";
export interface NewsletterDatesViewModel {
newsletterId: number;
calendarId: string;
diffTitle: string | null;
diffDescription: string | null;
calendar: CalendarViewModel;
}

View file

@ -0,0 +1,7 @@
import { MemberViewModel } from "./member.models";
export interface NewsletterRecipientsViewModel {
newsletterId: number;
memberId: number;
member: MemberViewModel;
}

View file

@ -0,0 +1,7 @@
export interface TemplateViewModel {
id: number;
template: string;
description: string | null;
design: object;
html: string;
}

View file

@ -0,0 +1,8 @@
import { PermissionModule } from "../../type/permissionTypes";
export interface TemplateUsageViewModel {
scope: PermissionModule;
header: { id: number; template: string } | null;
body: { id: number; template: string } | null;
footer: { id: number; template: string } | null;
}