newsletter CRUD & pdf

This commit is contained in:
Julian Krauser 2024-12-25 12:22:28 +01:00
parent e9b29f8acf
commit 01ce3fdd39
31 changed files with 1185 additions and 23 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 @@
export interface SynchronizeNewsletterDatesCommand {
newsletterId: number;
dates: Array<NewsletterDateCommand>;
}
export interface NewsletterDateCommand {
calendarId: number;
diffTitle?: string;
diffDescription?: string;
}

View file

@ -0,0 +1,95 @@
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.map((np) => np.calendarId).includes(r.calendarId)
);
let removeDates = currentDates.filter(
(r) => !syncNewsletterDates.dates.map((np) => np.calendarId).includes(r.calendarId)
);
let keptDates = currentDates.filter((r) =>
syncNewsletterDates.dates.map((np) => np.calendarId).includes(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,9 @@
export interface SynchronizeNewsletterRecipientsCommand {
newsletterId: number;
recipients: Array<NewsletterRecipientCommand>;
}
export interface NewsletterRecipientCommand {
memberId: number;
addedManually: boolean;
}

View file

@ -0,0 +1,94 @@
import { DeleteResult, EntityManager, InsertResult, UpdateResult } from "typeorm";
import { dataSource } from "../data-source";
import InternalException from "../exceptions/internalException";
import NewsletterRecipientsService from "../service/newsletterRecipientsService";
import { NewsletterRecipientCommand, 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);
return await dataSource.manager
.transaction(async (manager) => {
let newRecipients = syncNewsletterRecipients.recipients.filter(
(r) => !currentRecipients.map((np) => np.memberId).includes(r.memberId)
);
let removeRecipients = currentRecipients.filter(
(r) => !syncNewsletterRecipients.recipients.map((np) => np.memberId).includes(r.memberId)
);
let keptRecipients = currentRecipients.filter((r) =>
syncNewsletterRecipients.recipients.map((np) => np.memberId).includes(r.memberId)
);
if (newRecipients.length != 0) {
await this.syncPresenceAdd(manager, syncNewsletterRecipients.newsletterId, newRecipients);
}
if (removeRecipients.length != 0) {
await this.syncPresenceRemove(manager, syncNewsletterRecipients.newsletterId, removeRecipients);
}
for (const recipient of keptRecipients) {
await this.syncPresenceUpdate(manager, syncNewsletterRecipients.newsletterId, recipient);
}
})
.then(() => {})
.catch((err) => {
throw new InternalException("Failed syncing newsletter recipients", err);
});
}
private static async syncPresenceAdd(
manager: EntityManager,
newsletterId: number,
recipients: Array<NewsletterRecipientCommand>
): Promise<InsertResult> {
return await manager
.createQueryBuilder()
.insert()
.into(newsletterRecipients)
.values(
recipients.map((d) => ({
...d,
newsletterId: newsletterId,
}))
)
.execute();
}
private static async syncPresenceUpdate(
manager: EntityManager,
newsletterId: number,
recipient: NewsletterRecipientCommand
): Promise<UpdateResult> {
return await manager
.createQueryBuilder()
.update(newsletterRecipients)
.set({
addedManually: recipient.addedManually,
})
.where("memberId = :memberId", { memberId: recipient.memberId })
.andWhere("newsletterId = :newsletterId", { newsletterId })
.execute();
}
private static async syncPresenceRemove(
manager: EntityManager,
newsletterId: number,
recipients: Array<NewsletterRecipientCommand>
): Promise<DeleteResult> {
return await manager
.createQueryBuilder()
.delete()
.from(newsletterRecipients)
.where("memberId IN (:...ids)", { ids: recipients.map((d) => d.memberId) })
.andWhere("newsletterId = :newsletterId", { newsletterId })
.execute();
}
}

View file

@ -0,0 +1,229 @@
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 { NewsletterRecipientsViewModel } from "../../viewmodel/admin/newsletterRecipients.models";
/**
* @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 recipientss 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 recipientss = await NewsletterRecipientsService.getAll(newsletterId);
res.json(NewsletterRecipientsFactory.mapToBase(recipientss));
}
/**
* @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 folderPath = FileSystemHelper.formatPath("export", "newsletter", `${newsletter.id}_${newsletter.title}`);
let filesInFolder = FileSystemHelper.getFilesInDirectory(folderPath);
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("export", "newsletter", `${newsletter.id}_${newsletter.title}`, filename);
res.sendFile(process.cwd() + filepath, {
headers: {
"Content-Type": "application/pdf",
},
});
}
/**
* @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 create newsletter printout 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);
let newsletter = await NewsletterService.getById(newsletterId);
let dates = await NewsletterDatesService.getAll(newsletterId);
let recipients = await NewsletterRecipientsService.getAll(newsletterId);
// print newsletter pdf for every member having newsletter type configured to print or if all members get printout
// check if all users have mail or adress
// squash all files to single for printing
// use Helper for Newsletter printing and mail sending
res.sendStatus(204);
}
/**
* @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);
if (recipientsByQueryId) {
// TODO! set all recipients to query selection
}
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<NewsletterRecipientsViewModel>;
let syncRecipients: SynchronizeNewsletterRecipientsCommand = {
newsletterId,
recipients: recipients.map((r) => ({
memberId: r.memberId,
addedManually: r.addedManually,
})),
};
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
@ -240,6 +241,7 @@ export async function createProtocolPrintoutById(req: Request, res: Response): P
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.formatPath("protocol", filename),
protocolId,
};
await ProtocolPrintoutCommandHandler.create(printout);

View file

@ -55,6 +55,10 @@ 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";
const dataSource = new DataSource({
type: DB_TYPE as any,
@ -96,6 +100,9 @@ const dataSource = new DataSource({
query,
template,
templateUsage,
newsletter,
newsletterDates,
newsletterRecipients,
memberView,
memberExecutivePositionsView,
memberQualificationsView,
@ -120,6 +127,7 @@ const dataSource = new DataSource({
MemberDataViews1734520998539,
Template1734854680201,
TemplateUsage1734949173739,
Newsletter1735118780511,
],
migrationsRun: true,
migrationsTransactionMode: "each",

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

@ -0,0 +1,42 @@
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;
@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,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: "int" })
calendarId: number;
@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,29 @@
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;
@Column({ type: "boolean", default: false })
addedManually: boolean;
@ManyToOne(() => newsletter, (newsletter) => newsletter.recipients, {
nullable: false,
onDelete: "CASCADE",
onUpdate: "RESTRICT",
})
newsletter: newsletter;
@ManyToOne(() => member, {
nullable: false,
onDelete: "CASCADE",
onUpdate: "RESTRICT",
})
member: member;
}

View file

@ -0,0 +1,32 @@
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,
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,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,28 @@
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,
addedManually: record.addedManually,
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,31 @@
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
import { join } from "path";
import { readdirSync } from "fs";
export abstract class FileSystemHelper {
static createFolder(newFolder: string) {
const exportPath = join(process.cwd(), "export", newFolder);
if (!existsSync(exportPath)) {
mkdirSync(exportPath, { recursive: true });
}
}
static readFile(filePath: string) {
return readFileSync(join(process.cwd(), filePath), "utf8");
}
static writeFile(filePath: string, file: any) {
writeFileSync(filePath, file);
}
static formatPath(...args: string[]) {
return join(...args);
}
static getFilesInDirectory(directoryPath: string, filetype?: string): string[] {
const fullPath = join(process.cwd(), directoryPath);
return readdirSync(fullPath, { withFileTypes: true })
.filter((dirent) => !dirent.isDirectory() && (!filetype || dirent.name.endsWith(filetype)))
.map((dirent) => dirent.name);
}
}

View file

@ -1,6 +1,8 @@
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 async renderFile({
@ -10,6 +12,7 @@ export abstract class PdfExport {
data = {},
saveToDisk = true,
margins = { top: "15mm", bottom: "15mm" },
folder = "",
}: {
template: PermissionModule;
title?: string;
@ -17,7 +20,10 @@ export abstract class PdfExport {
data?: any;
saveToDisk?: boolean;
margins?: { top: string; bottom: string };
folder?: string;
}) {
if (folder != "") FileSystemHelper.createFolder(folder);
const { header, footer, body } = await TemplateHelper.renderFileForModule({
module: template,
bodyData: data,
@ -31,8 +37,10 @@ export abstract class PdfExport {
const page = await browser.newPage();
await page.setContent(body, { waitUntil: "domcontentloaded" });
const exportPath = FileSystemHelper.formatPath(process.cwd(), "export", folder, `${filename}.pdf`);
let pdf = await page.pdf({
...(saveToDisk ? { path: process.cwd() + `/export/${filename}.pdf` } : {}),
...(saveToDisk ? { path: exportPath } : {}),
format: "A4",
printBackground: false,
margin: {
@ -50,4 +58,25 @@ export abstract class PdfExport {
return pdf;
}
static async sqashToSingleFile(inputFolder: string, outputFile: string, outputFolder: string = "") {
if (outputFolder != "") FileSystemHelper.createFolder(outputFolder);
let pdfFilePaths = FileSystemHelper.getFilesInDirectory(inputFolder, ".pdf");
const mergedPdf = await PDFDocument.create();
for (const pdfPath of pdfFilePaths) {
const pdfBytes = FileSystemHelper.readFile(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();
const exportPath = FileSystemHelper.formatPath(process.cwd(), "export", outputFolder, `${outputFile}.pdf`);
FileSystemHelper.writeFile(exportPath, mergedPdfBytes);
}
}

View file

@ -1,12 +1,12 @@
import { readFileSync } from "fs";
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 readFileSync(`${process.cwd()}/src/templates/${template}.template.html`, "utf8");
return FileSystemHelper.readFile(`/src/templates/${template}.template.html`);
}
static async getTemplateFromStore(templateId: number): Promise<string> {

View file

@ -0,0 +1,129 @@
import { MigrationInterface, QueryRunner, Table, TableForeignKey } from "typeorm";
import { DB_TYPE } from "../env.defaults";
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 },
{ name: "addedManually", type: "tinyint", default: "0" },
],
}),
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",
})
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
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

@ -15,6 +15,7 @@ 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";
@ -48,6 +49,7 @@ router.use("/member", PermissionHelper.passCheckMiddleware("read", "club", "memb
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,85 @@
import express, { Request, Response } from "express";
import {
createNewsletter,
createNewsletterPrintoutById,
getAllNewsletters,
getNewsletterDatesById,
getNewsletterById,
getNewsletterRecipientsById,
getNewsletterPrintoutByIdAndPrint,
getNewsletterPrintoutsById,
synchronizeNewsletterDatesById,
synchronizeNewsletterById,
synchronizeNewsletterRecipientsById,
} 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("/:protocolId/dates", async (req: Request, res: Response) => {
await getNewsletterDatesById(req, res);
});
router.get("/:protocolId/recipients", async (req: Request, res: Response) => {
await getNewsletterRecipientsById(req, res);
});
router.get("/:protocolId/printouts", async (req: Request, res: Response) => {
await getNewsletterPrintoutsById(req, res);
});
router.get("/:protocolId/printout/:filename", async (req: Request, res: Response) => {
await getNewsletterPrintoutByIdAndPrint(req, res);
});
router.post(
"/",
PermissionHelper.passCheckMiddleware("create", "club", "protocol"),
async (req: Request, res: Response) => {
await createNewsletter(req, res);
}
);
router.post(
"/:protocolId/printout",
PermissionHelper.passCheckMiddleware("create", "club", "protocol"),
async (req: Request, res: Response) => {
await createNewsletterPrintoutById(req, res);
}
);
router.patch(
"/:id/synchronize",
PermissionHelper.passCheckMiddleware("update", "club", "protocol"),
async (req: Request, res: Response) => {
await synchronizeNewsletterById(req, res);
}
);
router.patch(
"/:protocolId/synchronize/dates",
PermissionHelper.passCheckMiddleware("update", "club", "protocol"),
async (req: Request, res: Response) => {
await synchronizeNewsletterDatesById(req, res);
}
);
router.patch(
"/:protocolId/synchronize/recipients",
PermissionHelper.passCheckMiddleware("update", "club", "protocol"),
async (req: Request, res: Response) => {
await synchronizeNewsletterRecipientsById(req, res);
}
);
// TODO: send mails | send mail preview | render preview before print job
export default router;

View file

@ -0,0 +1,26 @@
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("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("newsletterDates.newsletterId = :id", { id: newsletterId })
.getMany()
.then((res) => {
return res;
})
.catch((err) => {
throw new InternalException("newsletterRecipientss not found", err);
});
}
}

View file

@ -0,0 +1,44 @@
import { dataSource } from "../data-source";
import { newsletter } from "../entity/newsletter";
import { member } from "../entity/member";
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

@ -4,6 +4,7 @@ export type PermissionModule =
| "member"
| "calendar"
| "newsletter"
| "newsletter_config"
| "protocol"
| "qualification"
| "award"
@ -44,6 +45,7 @@ export const permissionModules: Array<PermissionModule> = [
"member",
"calendar",
"newsletter",
"newsletter_config",
"protocol",
"qualification",
"award",
@ -71,6 +73,7 @@ export const sectionsAndModules: SectionsAndModulesObject = {
"query_store",
"template",
"template_usage",
"newsletter_config",
],
user: ["user", "role"],
};

View file

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

View file

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

View file

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