#6-messages #24

Merged
jkeffects merged 16 commits from #6-messages into main 2024-12-31 13:25:12 +00:00
15 changed files with 355 additions and 28 deletions
Showing only changes of commit 5f827fb177 - Show all commits

View file

@ -13,7 +13,10 @@ import NewsletterDatesCommandHandler from "../../command/newsletterDatesCommandH
import { SynchronizeNewsletterRecipientsCommand } from "../../command/newsletterRecipientsCommand"; import { SynchronizeNewsletterRecipientsCommand } from "../../command/newsletterRecipientsCommand";
import NewsletterRecipientsCommandHandler from "../../command/newsletterRecipientsCommandHandler"; import NewsletterRecipientsCommandHandler from "../../command/newsletterRecipientsCommandHandler";
import { NewsletterDatesViewModel } from "../../viewmodel/admin/newsletterDates.models"; 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 * @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<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 = {
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 * @description create newsletter
* @param req {Request} Express req object * @param req {Request} Express req object
@ -131,7 +212,7 @@ export async function createNewsletter(req: Request, res: Response): Promise<any
} }
/** /**
* @description create newsletter printout by id * @description create newsletter printouts for each member by id
* @param req {Request} Express req object * @param req {Request} Express req object
* @param res {Response} Express res object * @param res {Response} Express res object
* @returns {Promise<*>} * @returns {Promise<*>}
@ -151,6 +232,96 @@ export async function createNewsletterPrintoutById(req: Request, res: Response):
res.sendStatus(204); 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 = {
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<any> {
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 * @description synchronize newsletter by id
* @param req {Request} Express req object * @param req {Request} Express req object

View file

@ -40,7 +40,11 @@ export async function printTemplateUsageDemo(req: Request, res: Response): Promi
const scope = req.params.scope as PermissionModule; const scope = req.params.scope as PermissionModule;
let demoData = DemoDataHelper.getData(scope); 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); let pdfbuffer = Buffer.from(pdf);

View file

@ -146,8 +146,7 @@ export async function deleteUser(req: Request, res: Response): Promise<any> {
try { try {
// sendmail // sendmail
let mailhelper = new MailHelper(); await MailHelper.sendMail(
await mailhelper.sendMail(
user.mail, user.mail,
`Email Bestätigung für Mitglieder Admin-Portal von ${CLUB_NAME}`, `Email Bestätigung für Mitglieder Admin-Portal von ${CLUB_NAME}`,
`Ihr Nutzerkonto des Adminportals wurde erfolgreich gelöscht.` `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); let token = await InviteCommandHandler.create(createInvite);
// sendmail // sendmail
let mailhelper = new MailHelper(); await MailHelper.sendMail(
await mailhelper.sendMail(
mail, mail,
`Email Bestätigung für Mitglieder Admin-Portal von ${CLUB_NAME}`, `Email Bestätigung für Mitglieder Admin-Portal von ${CLUB_NAME}`,
`Öffne folgenden Link: ${origin}/${isInvite ? "invite" : "setup"}/verify?mail=${mail}&token=${token}` `Öffne folgenden Link: ${origin}/${isInvite ? "invite" : "setup"}/verify?mail=${mail}&token=${token}`

View file

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

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

@ -1,3 +1,4 @@
import { newsletterDemoData } from "../demodata/newsletter.data";
import { protocolDemoData } from "../demodata/protocol.data"; import { protocolDemoData } from "../demodata/protocol.data";
import { PermissionModule } from "../type/permissionTypes"; import { PermissionModule } from "../type/permissionTypes";
@ -6,6 +7,8 @@ export abstract class DemoDataHelper {
switch (scope) { switch (scope) {
case "protocol": case "protocol":
return protocolDemoData; return protocolDemoData;
case "newsletter":
return newsletterDemoData;
default: default:
return {}; return {};
} }

View file

@ -24,6 +24,9 @@ export abstract class FileSystemHelper {
static getFilesInDirectory(directoryPath: string, filetype?: string): string[] { static getFilesInDirectory(directoryPath: string, filetype?: string): string[] {
const fullPath = join(process.cwd(), directoryPath); const fullPath = join(process.cwd(), directoryPath);
if (!existsSync(fullPath)) {
return [];
}
return readdirSync(fullPath, { withFileTypes: true }) return readdirSync(fullPath, { withFileTypes: true })
.filter((dirent) => !dirent.isDirectory() && (!filetype || dirent.name.endsWith(filetype))) .filter((dirent) => !dirent.isDirectory() && (!filetype || dirent.name.endsWith(filetype)))
.map((dirent) => dirent.name); .map((dirent) => dirent.name);

View file

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

View file

@ -26,7 +26,9 @@ export abstract class PdfExport {
const { header, footer, body } = await TemplateHelper.renderFileForModule({ const { header, footer, body } = await TemplateHelper.renderFileForModule({
module: template, module: template,
headerData: data,
bodyData: data, bodyData: data,
footerData: data,
title: title, title: title,
}); });

View file

@ -48,15 +48,15 @@ export abstract class TemplateHelper {
if (moduleTemplates.headerId) { if (moduleTemplates.headerId) {
header = await this.getTemplateFromStore(moduleTemplates.headerId); header = await this.getTemplateFromStore(moduleTemplates.headerId);
header = this.applyDataToTemplate(header, headerData); header = this.applyDataToTemplate(header, { title, ...headerData });
} }
if (moduleTemplates.footerId) { if (moduleTemplates.footerId) {
footer = await this.getTemplateFromStore(moduleTemplates.footerId); footer = await this.getTemplateFromStore(moduleTemplates.footerId);
footer = this.applyDataToTemplate(footer, footerData);
} else { } else {
footer = this.getTemplateFromFile(module + ".footer"); footer = this.getTemplateFromFile(module + ".footer");
} }
footer = this.applyDataToTemplate(footer, footerData);
if (moduleTemplates.bodyId) { if (moduleTemplates.bodyId) {
body = await this.getTemplateFromStore(moduleTemplates.bodyId); body = await this.getTemplateFromStore(moduleTemplates.bodyId);

View file

@ -1,5 +1,6 @@
import { MigrationInterface, QueryRunner, Table, TableForeignKey } from "typeorm"; import { MigrationInterface, QueryRunner, Table, TableForeignKey } from "typeorm";
import { DB_TYPE } from "../env.defaults"; import { DB_TYPE } from "../env.defaults";
import { templateUsage } from "../entity/templateUsage";
export class Newsletter1735118780511 implements MigrationInterface { export class Newsletter1735118780511 implements MigrationInterface {
name = "Newsletter1735118780511"; name = "Newsletter1735118780511";
@ -102,9 +103,24 @@ export class Newsletter1735118780511 implements MigrationInterface {
onUpdate: "RESTRICT", onUpdate: "RESTRICT",
}) })
); );
await queryRunner.manager
.createQueryBuilder()
.insert()
.into(templateUsage)
.values({ scope: "newsletter" })
.orIgnore()
.execute();
} }
public async down(queryRunner: QueryRunner): Promise<void> { 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 tableN = await queryRunner.getTable("newsletter");
const tableNR = await queryRunner.getTable("newsletter_recipients"); const tableNR = await queryRunner.getTable("newsletter_recipients");
const tableND = await queryRunner.getTable("newsletter_dates"); const tableND = await queryRunner.getTable("newsletter_dates");

View file

@ -11,6 +11,9 @@ import {
synchronizeNewsletterDatesById, synchronizeNewsletterDatesById,
synchronizeNewsletterById, synchronizeNewsletterById,
synchronizeNewsletterRecipientsById, synchronizeNewsletterRecipientsById,
sendNewsletterById,
createNewsletterMailPreviewById,
createNewsletterPrintoutPreviewById,
} from "../../controller/admin/newsletterController"; } from "../../controller/admin/newsletterController";
import PermissionHelper from "../../helpers/permissionHelper"; import PermissionHelper from "../../helpers/permissionHelper";
@ -40,6 +43,10 @@ router.get("/:newsletterId/printout/:filename", async (req: Request, res: Respon
await getNewsletterPrintoutByIdAndPrint(req, res); await getNewsletterPrintoutByIdAndPrint(req, res);
}); });
router.get("/:newsletterId/printoutpreview", async (req: Request, res: Response) => {
await createNewsletterPrintoutPreviewById(req, res);
});
router.post( router.post(
"/", "/",
PermissionHelper.passCheckMiddleware("create", "club", "protocol"), 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( router.patch(
"/:id/synchronize", "/:id/synchronize",
PermissionHelper.passCheckMiddleware("update", "club", "protocol"), 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; export default router;

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}}, {{recipient.street}} {{recipient.streetNumber}}
{{recipient.streetNumberAdd}}
</div>