diff --git a/package-lock.json b/package-lock.json index d359826..4946c5b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 621f201..555db79 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/command/newsletterCommand.ts b/src/command/newsletterCommand.ts new file mode 100644 index 0000000..354ae62 --- /dev/null +++ b/src/command/newsletterCommand.ts @@ -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; +} diff --git a/src/command/newsletterCommandHandler.ts b/src/command/newsletterCommandHandler.ts new file mode 100644 index 0000000..6667a74 --- /dev/null +++ b/src/command/newsletterCommandHandler.ts @@ -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} + */ + static async create(createNewsletter: CreateNewsletterCommand): Promise { + 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} + */ + static async sync(syncNewsletter: SynchronizeNewsletterCommand): Promise { + 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} + */ + static async send(syncNewsletter: SendNewsletterCommand): Promise { + 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); + }); + } +} diff --git a/src/command/newsletterDatesCommand.ts b/src/command/newsletterDatesCommand.ts new file mode 100644 index 0000000..70d789f --- /dev/null +++ b/src/command/newsletterDatesCommand.ts @@ -0,0 +1,10 @@ +export interface SynchronizeNewsletterDatesCommand { + newsletterId: number; + dates: Array; +} + +export interface NewsletterDateCommand { + calendarId: number; + diffTitle?: string; + diffDescription?: string; +} diff --git a/src/command/newsletterDatesCommandHandler.ts b/src/command/newsletterDatesCommandHandler.ts new file mode 100644 index 0000000..f1ec22b --- /dev/null +++ b/src/command/newsletterDatesCommandHandler.ts @@ -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} + */ + static async sync(syncNewsletterDates: SynchronizeNewsletterDatesCommand): Promise { + 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 + ): Promise { + 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 { + 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 + ): Promise { + return await manager + .createQueryBuilder() + .delete() + .from(newsletterDates) + .where("calendarId IN (:...ids)", { ids: dates.map((d) => d.calendarId) }) + .andWhere("newsletterId = :newsletterId", { newsletterId }) + .execute(); + } +} diff --git a/src/command/newsletterRecipientsCommand.ts b/src/command/newsletterRecipientsCommand.ts new file mode 100644 index 0000000..fdd8928 --- /dev/null +++ b/src/command/newsletterRecipientsCommand.ts @@ -0,0 +1,9 @@ +export interface SynchronizeNewsletterRecipientsCommand { + newsletterId: number; + recipients: Array; +} + +export interface NewsletterRecipientCommand { + memberId: number; + addedManually: boolean; +} diff --git a/src/command/newsletterRecipientsCommandHandler.ts b/src/command/newsletterRecipientsCommandHandler.ts new file mode 100644 index 0000000..0421c41 --- /dev/null +++ b/src/command/newsletterRecipientsCommandHandler.ts @@ -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} + */ + static async sync(syncNewsletterRecipients: SynchronizeNewsletterRecipientsCommand): Promise { + 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 + ): Promise { + 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 { + 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 + ): Promise { + return await manager + .createQueryBuilder() + .delete() + .from(newsletterRecipients) + .where("memberId IN (:...ids)", { ids: recipients.map((d) => d.memberId) }) + .andWhere("newsletterId = :newsletterId", { newsletterId }) + .execute(); + } +} diff --git a/src/controller/admin/newsletterController.ts b/src/controller/admin/newsletterController.ts new file mode 100644 index 0000000..1d4efb0 --- /dev/null +++ b/src/controller/admin/newsletterController.ts @@ -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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + let newsletterId = parseInt(req.params.newsletterId); + let dates = req.body.dates as Array; + + 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 { + let newsletterId = parseInt(req.params.newsletterId); + let recipients = req.body.recipients as Array; + + let syncRecipients: SynchronizeNewsletterRecipientsCommand = { + newsletterId, + recipients: recipients.map((r) => ({ + memberId: r.memberId, + addedManually: r.addedManually, + })), + }; + await NewsletterRecipientsCommandHandler.sync(syncRecipients); + + res.sendStatus(204); +} diff --git a/src/controller/admin/protocolController.ts b/src/controller/admin/protocolController.ts index ef5ca3d..345a42b 100644 --- a/src/controller/admin/protocolController.ts +++ b/src/controller/admin/protocolController.ts @@ -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); diff --git a/src/data-source.ts b/src/data-source.ts index d9567fd..7b97e03 100644 --- a/src/data-source.ts +++ b/src/data-source.ts @@ -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", diff --git a/src/entity/newsletter.ts b/src/entity/newsletter.ts new file mode 100644 index 0000000..0694d88 --- /dev/null +++ b/src/entity/newsletter.ts @@ -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; +} diff --git a/src/entity/newsletterDates.ts b/src/entity/newsletterDates.ts new file mode 100644 index 0000000..5ef80ba --- /dev/null +++ b/src/entity/newsletterDates.ts @@ -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; +} diff --git a/src/entity/newsletterRecipients.ts b/src/entity/newsletterRecipients.ts new file mode 100644 index 0000000..1f62a5a --- /dev/null +++ b/src/entity/newsletterRecipients.ts @@ -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; +} diff --git a/src/factory/admin/newsletter.ts b/src/factory/admin/newsletter.ts new file mode 100644 index 0000000..3dca651 --- /dev/null +++ b/src/factory/admin/newsletter.ts @@ -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} records + * @returns {Array} + */ + public static mapToBase(records: Array): Array { + return records.map((r) => this.mapToSingle(r)); + } +} diff --git a/src/factory/admin/newsletterDates.ts b/src/factory/admin/newsletterDates.ts new file mode 100644 index 0000000..232e7ac --- /dev/null +++ b/src/factory/admin/newsletterDates.ts @@ -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} records + * @returns {Array} + */ + public static mapToBase(records: Array): Array { + return records.map((r) => this.mapToSingle(r)); + } +} diff --git a/src/factory/admin/newsletterRecipients.ts b/src/factory/admin/newsletterRecipients.ts new file mode 100644 index 0000000..f9373ef --- /dev/null +++ b/src/factory/admin/newsletterRecipients.ts @@ -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} records + * @returns {Array} + */ + public static mapToBase(records: Array): Array { + return records.map((r) => this.mapToSingle(r)); + } +} diff --git a/src/helpers/fileSystemHelper.ts b/src/helpers/fileSystemHelper.ts new file mode 100644 index 0000000..fc6423e --- /dev/null +++ b/src/helpers/fileSystemHelper.ts @@ -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); + } +} diff --git a/src/helpers/pdfExport.ts b/src/helpers/pdfExport.ts index 827f3db..519cc84 100644 --- a/src/helpers/pdfExport.ts +++ b/src/helpers/pdfExport.ts @@ -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); + } } diff --git a/src/helpers/templateHelper.ts b/src/helpers/templateHelper.ts index 04a0874..c6cf289 100644 --- a/src/helpers/templateHelper.ts +++ b/src/helpers/templateHelper.ts @@ -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 { diff --git a/src/migrations/1735118780511-newsletter.ts b/src/migrations/1735118780511-newsletter.ts new file mode 100644 index 0000000..6308425 --- /dev/null +++ b/src/migrations/1735118780511-newsletter.ts @@ -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 { + 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 { + 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"); + } +} diff --git a/src/routes/admin/index.ts b/src/routes/admin/index.ts index f1c6982..4da8c12 100644 --- a/src/routes/admin/index.ts +++ b/src/routes/admin/index.ts @@ -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); diff --git a/src/routes/admin/newsletter.ts b/src/routes/admin/newsletter.ts new file mode 100644 index 0000000..c721810 --- /dev/null +++ b/src/routes/admin/newsletter.ts @@ -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; diff --git a/src/service/newsletterDatesService.ts b/src/service/newsletterDatesService.ts new file mode 100644 index 0000000..5152de0 --- /dev/null +++ b/src/service/newsletterDatesService.ts @@ -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>} + */ + static async getAll(newsletterId: number): Promise> { + 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); + }); + } +} diff --git a/src/service/newsletterRecipientsService.ts b/src/service/newsletterRecipientsService.ts new file mode 100644 index 0000000..923ad0d --- /dev/null +++ b/src/service/newsletterRecipientsService.ts @@ -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>} + */ + static async getAll(newsletterId: number): Promise> { + 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); + }); + } +} diff --git a/src/service/newsletterService.ts b/src/service/newsletterService.ts new file mode 100644 index 0000000..cd05747 --- /dev/null +++ b/src/service/newsletterService.ts @@ -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, number]>} + */ + static async getAll(offset: number = 0, count: number = 25): Promise<[Array, 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} + */ + static async getById(id: number): Promise { + 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); + }); + } +} diff --git a/src/service/protocolPrecenseService.ts b/src/service/protocolPrecenseService.ts index c8840c3..2fc9c94 100644 --- a/src/service/protocolPrecenseService.ts +++ b/src/service/protocolPrecenseService.ts @@ -21,23 +21,4 @@ export default abstract class ProtocolPresenceService { throw new InternalException("protocolPresence not found", err); }); } - - /** - * @description get protocolDecision by id - * @returns {Promise} - */ - static async getById(id: number): Promise { - 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); - }); - } } diff --git a/src/type/permissionTypes.ts b/src/type/permissionTypes.ts index 34e2be9..95ea543 100644 --- a/src/type/permissionTypes.ts +++ b/src/type/permissionTypes.ts @@ -4,6 +4,7 @@ export type PermissionModule = | "member" | "calendar" | "newsletter" + | "newsletter_config" | "protocol" | "qualification" | "award" @@ -44,6 +45,7 @@ export const permissionModules: Array = [ "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"], }; diff --git a/src/viewmodel/admin/newsletter.models.ts b/src/viewmodel/admin/newsletter.models.ts new file mode 100644 index 0000000..b68814f --- /dev/null +++ b/src/viewmodel/admin/newsletter.models.ts @@ -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; +} diff --git a/src/viewmodel/admin/newsletterDates.models.ts b/src/viewmodel/admin/newsletterDates.models.ts new file mode 100644 index 0000000..a1bf0cc --- /dev/null +++ b/src/viewmodel/admin/newsletterDates.models.ts @@ -0,0 +1,9 @@ +import { CalendarViewModel } from "./calendar.models"; + +export interface NewsletterDatesViewModel { + newsletterId: number; + calendarId: number; + diffTitle: string | null; + diffDescription: string | null; + calendar: CalendarViewModel; +} diff --git a/src/viewmodel/admin/newsletterRecipients.models.ts b/src/viewmodel/admin/newsletterRecipients.models.ts new file mode 100644 index 0000000..55160f9 --- /dev/null +++ b/src/viewmodel/admin/newsletterRecipients.models.ts @@ -0,0 +1,8 @@ +import { MemberViewModel } from "./member.models"; + +export interface NewsletterRecipientsViewModel { + newsletterId: number; + memberId: number; + addedManually: boolean; + member: MemberViewModel; +}