From 5f827fb177b96f5acb84b3b70c035de79906c176 Mon Sep 17 00:00:00 2001 From: Julian Krauser Date: Sat, 28 Dec 2024 18:03:33 +0100 Subject: [PATCH] send or print newsletter preview --- src/controller/admin/newsletterController.ts | 175 +++++++++++++++++- .../admin/templateUsageController.ts | 6 +- src/controller/admin/userController.ts | 3 +- src/controller/inviteController.ts | 3 +- src/controller/resetController.ts | 3 +- src/demodata/newsletter.data.ts | 72 +++++++ src/helpers/demoDataHelper.ts | 3 + src/helpers/fileSystemHelper.ts | 3 + src/helpers/mailHelper.ts | 27 ++- src/helpers/pdfExport.ts | 2 + src/helpers/templateHelper.ts | 4 +- src/migrations/1735118780511-newsletter.ts | 16 ++ src/routes/admin/newsletter.ts | 21 ++- src/templates/newsletter.body.template.html | 41 ++++ src/templates/newsletter.footer.template.html | 4 + 15 files changed, 355 insertions(+), 28 deletions(-) create mode 100644 src/demodata/newsletter.data.ts create mode 100644 src/templates/newsletter.body.template.html create mode 100644 src/templates/newsletter.footer.template.html diff --git a/src/controller/admin/newsletterController.ts b/src/controller/admin/newsletterController.ts index 0e141f5..6a19524 100644 --- a/src/controller/admin/newsletterController.ts +++ b/src/controller/admin/newsletterController.ts @@ -13,7 +13,10 @@ import NewsletterDatesCommandHandler from "../../command/newsletterDatesCommandH import { SynchronizeNewsletterRecipientsCommand } from "../../command/newsletterRecipientsCommand"; import NewsletterRecipientsCommandHandler from "../../command/newsletterRecipientsCommandHandler"; import { NewsletterDatesViewModel } from "../../viewmodel/admin/newsletterDates.models"; -import { NewsletterRecipientsViewModel } from "../../viewmodel/admin/newsletterRecipients.models"; +import { PdfExport } from "../../helpers/pdfExport"; +import UserService from "../../service/userService"; +import { TemplateHelper } from "../../helpers/templateHelper"; +import MailHelper from "../../helpers/mailHelper"; /** * @description get all newsletters @@ -113,6 +116,84 @@ export async function getNewsletterPrintoutByIdAndPrint(req: Request, res: Respo }); } +/** + * @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 { + 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 = { + 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: { + firstname: recipient.firstname, + lastname: recipient.lastname, + 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 @@ -131,7 +212,7 @@ export async function createNewsletter(req: Request, res: Response): Promise} @@ -151,6 +232,96 @@ export async function createNewsletterPrintoutById(req: Request, res: Response): 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 { + 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 = { + 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: { + firstname: recipient.firstname, + lastname: recipient.lastname, + 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 { + let newsletterId = parseInt(req.params.newsletterId); + let newsletter = await NewsletterService.getById(newsletterId); + let dates = await NewsletterDatesService.getAll(newsletterId); + let recipients = await NewsletterRecipientsService.getAll(newsletterId); + + // attach ics files for date entries to mail + + res.sendStatus(204); +} + /** * @description synchronize newsletter by id * @param req {Request} Express req object diff --git a/src/controller/admin/templateUsageController.ts b/src/controller/admin/templateUsageController.ts index e121215..ac1e74a 100644 --- a/src/controller/admin/templateUsageController.ts +++ b/src/controller/admin/templateUsageController.ts @@ -40,7 +40,11 @@ export async function printTemplateUsageDemo(req: Request, res: Response): Promi const scope = req.params.scope as PermissionModule; let demoData = DemoDataHelper.getData(scope); - let pdf = await PdfExport.renderFile({ template: scope, saveToDisk: false, data: demoData }); + let pdf = await PdfExport.renderFile({ + template: scope, + saveToDisk: false, + data: demoData, + }); let pdfbuffer = Buffer.from(pdf); diff --git a/src/controller/admin/userController.ts b/src/controller/admin/userController.ts index 6596c0a..0177fec 100644 --- a/src/controller/admin/userController.ts +++ b/src/controller/admin/userController.ts @@ -146,8 +146,7 @@ export async function deleteUser(req: Request, res: Response): Promise { 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.` diff --git a/src/controller/inviteController.ts b/src/controller/inviteController.ts index 8fa2f34..bb08cd6 100644 --- a/src/controller/inviteController.ts +++ b/src/controller/inviteController.ts @@ -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}` diff --git a/src/controller/resetController.ts b/src/controller/resetController.ts index 32f2004..fe0554c 100644 --- a/src/controller/resetController.ts +++ b/src/controller/resetController.ts @@ -41,8 +41,7 @@ export async function startReset(req: Request, res: Response): Promise { 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}` diff --git a/src/demodata/newsletter.data.ts b/src/demodata/newsletter.data.ts new file mode 100644 index 0000000..41a81f4 --- /dev/null +++ b/src/demodata/newsletter.data.ts @@ -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; +} = { + title: "Beispiel Newsletter Daten", + description: "Zusammenfassung der Demodaten.", + newsletterTitle: "

Sehr geehrtes Feuerwehrmitglied

", + newsletterText: "

zu folgenden Terminen möchten wir recht herzlich zur Teilnahme einladen:

", + newsletterSignatur: "

Mit freundlichen Grüßen

...

", + dates: [ + { + title: "Termin 1", + content: "

Beschreibung eines Termins

", + 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", + }, +}; diff --git a/src/helpers/demoDataHelper.ts b/src/helpers/demoDataHelper.ts index 158f17b..e02e5b5 100644 --- a/src/helpers/demoDataHelper.ts +++ b/src/helpers/demoDataHelper.ts @@ -1,3 +1,4 @@ +import { newsletterDemoData } from "../demodata/newsletter.data"; import { protocolDemoData } from "../demodata/protocol.data"; import { PermissionModule } from "../type/permissionTypes"; @@ -6,6 +7,8 @@ export abstract class DemoDataHelper { switch (scope) { case "protocol": return protocolDemoData; + case "newsletter": + return newsletterDemoData; default: return {}; } diff --git a/src/helpers/fileSystemHelper.ts b/src/helpers/fileSystemHelper.ts index fc6423e..ad7f33f 100644 --- a/src/helpers/fileSystemHelper.ts +++ b/src/helpers/fileSystemHelper.ts @@ -24,6 +24,9 @@ export abstract class FileSystemHelper { static getFilesInDirectory(directoryPath: string, filetype?: string): string[] { const fullPath = join(process.cwd(), directoryPath); + if (!existsSync(fullPath)) { + return []; + } return readdirSync(fullPath, { withFileTypes: true }) .filter((dirent) => !dirent.isDirectory() && (!filetype || dirent.name.endsWith(filetype))) .map((dirent) => dirent.name); diff --git a/src/helpers/mailHelper.ts b/src/helpers/mailHelper.ts index b4c2dd9..5414673 100644 --- a/src/helpers/mailHelper.ts +++ b/src/helpers/mailHelper.ts @@ -1,20 +1,16 @@ import { Transporter, createTransport, TransportOptions } from "nodemailer"; import { CLUB_NAME, MAIL_HOST, MAIL_PASSWORD, MAIL_PORT, MAIL_SECURE, MAIL_USERNAME } from "../env.defaults"; -export default class MailHelper { - private readonly transporter: Transporter; - - constructor() { - this.transporter = createTransport({ - host: MAIL_HOST, - port: MAIL_PORT, - secure: (MAIL_SECURE as "true" | "false") == "true", - auth: { - user: MAIL_USERNAME, - pass: MAIL_PASSWORD, - }, - } as TransportOptions); - } +export default abstract class MailHelper { + private static readonly transporter: Transporter = createTransport({ + host: MAIL_HOST, + port: MAIL_PORT, + secure: (MAIL_SECURE as "true" | "false") == "true", + auth: { + user: MAIL_USERNAME, + pass: MAIL_PASSWORD, + }, + } as TransportOptions); /** * @description send mail @@ -23,7 +19,7 @@ export default class MailHelper { * @param {string} content * @returns {Prmose<*>} */ - async sendMail(target: string, subject: string, content: string): Promise { + static async sendMail(target: string, subject: string, content: string): Promise { return new Promise((resolve, reject) => { this.transporter .sendMail({ @@ -31,6 +27,7 @@ export default class MailHelper { to: target, subject, text: content, + html: content, }) .then((info) => resolve(info.messageId)) .catch((e) => reject(e)); diff --git a/src/helpers/pdfExport.ts b/src/helpers/pdfExport.ts index 519cc84..fa0875e 100644 --- a/src/helpers/pdfExport.ts +++ b/src/helpers/pdfExport.ts @@ -26,7 +26,9 @@ export abstract class PdfExport { const { header, footer, body } = await TemplateHelper.renderFileForModule({ module: template, + headerData: data, bodyData: data, + footerData: data, title: title, }); diff --git a/src/helpers/templateHelper.ts b/src/helpers/templateHelper.ts index c6cf289..3f49961 100644 --- a/src/helpers/templateHelper.ts +++ b/src/helpers/templateHelper.ts @@ -48,15 +48,15 @@ export abstract class TemplateHelper { if (moduleTemplates.headerId) { header = await this.getTemplateFromStore(moduleTemplates.headerId); - header = this.applyDataToTemplate(header, headerData); + header = this.applyDataToTemplate(header, { title, ...headerData }); } if (moduleTemplates.footerId) { footer = await this.getTemplateFromStore(moduleTemplates.footerId); - footer = this.applyDataToTemplate(footer, footerData); } else { footer = this.getTemplateFromFile(module + ".footer"); } + footer = this.applyDataToTemplate(footer, footerData); if (moduleTemplates.bodyId) { body = await this.getTemplateFromStore(moduleTemplates.bodyId); diff --git a/src/migrations/1735118780511-newsletter.ts b/src/migrations/1735118780511-newsletter.ts index e9f79ea..a6294d0 100644 --- a/src/migrations/1735118780511-newsletter.ts +++ b/src/migrations/1735118780511-newsletter.ts @@ -1,5 +1,6 @@ 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"; @@ -102,9 +103,24 @@ export class Newsletter1735118780511 implements MigrationInterface { onUpdate: "RESTRICT", }) ); + + await queryRunner.manager + .createQueryBuilder() + .insert() + .into(templateUsage) + .values({ scope: "newsletter" }) + .orIgnore() + .execute(); } public async down(queryRunner: QueryRunner): Promise { + 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"); diff --git a/src/routes/admin/newsletter.ts b/src/routes/admin/newsletter.ts index 93bde92..63d149c 100644 --- a/src/routes/admin/newsletter.ts +++ b/src/routes/admin/newsletter.ts @@ -11,6 +11,9 @@ import { synchronizeNewsletterDatesById, synchronizeNewsletterById, synchronizeNewsletterRecipientsById, + sendNewsletterById, + createNewsletterMailPreviewById, + createNewsletterPrintoutPreviewById, } from "../../controller/admin/newsletterController"; import PermissionHelper from "../../helpers/permissionHelper"; @@ -40,6 +43,10 @@ router.get("/:newsletterId/printout/:filename", async (req: Request, res: Respon await getNewsletterPrintoutByIdAndPrint(req, res); }); +router.get("/:newsletterId/printoutpreview", async (req: Request, res: Response) => { + await createNewsletterPrintoutPreviewById(req, res); +}); + router.post( "/", PermissionHelper.passCheckMiddleware("create", "club", "protocol"), @@ -56,6 +63,18 @@ router.post( } ); +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"), @@ -80,6 +99,4 @@ router.patch( } ); -// TODO: send mails | send mail preview | render preview before print job - export default router; diff --git a/src/templates/newsletter.body.template.html b/src/templates/newsletter.body.template.html new file mode 100644 index 0000000..941d89e --- /dev/null +++ b/src/templates/newsletter.body.template.html @@ -0,0 +1,41 @@ + + + + + Newsletter + + +

{{{newsletterTitle}}}

+

{{{newsletterText}}}

+
+ {{#each dates}} +
+

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

+ {{{this.content}}} +
+ {{/each}} +
+
+

{{{newsletterSignatur}}}

+ + + diff --git a/src/templates/newsletter.footer.template.html b/src/templates/newsletter.footer.template.html new file mode 100644 index 0000000..68f9f47 --- /dev/null +++ b/src/templates/newsletter.footer.template.html @@ -0,0 +1,4 @@ +
+ {{recipient.lastname}}, {{recipient.firstname}}, {{recipient.street}} {{recipient.streetNumber}} + {{recipient.streetNumberAdd}} +