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/newsletterConfigCommand.ts b/src/command/newsletterConfigCommand.ts new file mode 100644 index 0000000..011bc11 --- /dev/null +++ b/src/command/newsletterConfigCommand.ts @@ -0,0 +1,10 @@ +import { NewsletterConfigType } from "../enums/newsletterConfigType"; + +export interface SetNewsletterConfigCommand { + comTypeId: number; + config: NewsletterConfigType; +} + +export interface DeleteNewsletterConfigCommand { + comTypeId: number; +} diff --git a/src/command/newsletterConfigCommandHandler.ts b/src/command/newsletterConfigCommandHandler.ts new file mode 100644 index 0000000..6f6b948 --- /dev/null +++ b/src/command/newsletterConfigCommandHandler.ts @@ -0,0 +1,47 @@ +import { dataSource } from "../data-source"; +import { newsletterConfig } from "../entity/newsletterConfig"; +import InternalException from "../exceptions/internalException"; +import { DeleteNewsletterConfigCommand, SetNewsletterConfigCommand } from "./newsletterConfigCommand"; + +export default abstract class NewsletterConfigCommandHandler { + /** + * @description set newsletterConfig + * @param SetNewsletterConfigCommand + * @returns {Promise} + */ + static async set(setNewsletterConfig: SetNewsletterConfigCommand): Promise { + return await dataSource + .createQueryBuilder() + .insert() + .into(newsletterConfig) + .values({ + comTypeId: setNewsletterConfig.comTypeId, + config: setNewsletterConfig.config, + }) + .orUpdate(["config"], "comTypeId") + .execute() + .then((result) => { + return result.identifiers[0].id; + }) + .catch((err) => { + throw new InternalException("Failed setting newsletterConfig", err); + }); + } + /** + * @description delete newsletterConfig + * @param number + * @returns {Promise} + */ + static async delete(deleteNewsletterConfig: DeleteNewsletterConfigCommand): Promise { + return await dataSource + .createQueryBuilder() + .delete() + .from(newsletterConfig) + .where("comTypeId = :comTypeId", { comTypeId: deleteNewsletterConfig.comTypeId }) + .execute() + .then(() => {}) + .catch((err) => { + throw new InternalException("Failed setting newsletterConfig", err); + }); + } +} diff --git a/src/command/newsletterDatesCommand.ts b/src/command/newsletterDatesCommand.ts new file mode 100644 index 0000000..5b92ba6 --- /dev/null +++ b/src/command/newsletterDatesCommand.ts @@ -0,0 +1,10 @@ +export interface SynchronizeNewsletterDatesCommand { + newsletterId: number; + dates: Array; +} + +export interface NewsletterDateCommand { + calendarId: string; + diffTitle?: string; + diffDescription?: string; +} diff --git a/src/command/newsletterDatesCommandHandler.ts b/src/command/newsletterDatesCommandHandler.ts new file mode 100644 index 0000000..99ee1ad --- /dev/null +++ b/src/command/newsletterDatesCommandHandler.ts @@ -0,0 +1,97 @@ +import { DeleteResult, EntityManager, InsertResult, UpdateResult } from "typeorm"; +import { dataSource } from "../data-source"; +import InternalException from "../exceptions/internalException"; +import NewsletterDatesService from "../service/newsletterDatesService"; +import { NewsletterDateCommand, SynchronizeNewsletterDatesCommand } from "./newsletterDatesCommand"; +import { newsletterDates } from "../entity/newsletterDates"; + +export default abstract class NewsletterDatesCommandHandler { + /** + * @description sync newsletter dates + * @param {SynchronizeNewsletterDatesCommand} syncNewsletterDates + * @returns {Promise} + */ + 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.some((cd) => cd.calendarId == r.calendarId) + ); + let removeDates = currentDates.filter( + (r) => !syncNewsletterDates.dates.some((cd) => cd.calendarId == r.calendarId) + ); + let keptDates = syncNewsletterDates.dates.filter( + (r) => + currentDates.some((cd) => cd.calendarId == r.calendarId) && + !removeDates.some((cd) => cd.calendarId == r.calendarId) + ); + + if (newDates.length != 0) { + await this.syncPresenceAdd(manager, syncNewsletterDates.newsletterId, newDates); + } + + if (removeDates.length != 0) { + await this.syncPresenceRemove(manager, syncNewsletterDates.newsletterId, removeDates); + } + + for (const date of keptDates) { + await this.syncPresenceUpdate(manager, syncNewsletterDates.newsletterId, date); + } + }) + .then(() => {}) + .catch((err) => { + throw new InternalException("Failed syncing newsletter dates", err); + }); + } + + private static async syncPresenceAdd( + manager: EntityManager, + newsletterId: number, + dates: Array + ): 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..ec69c35 --- /dev/null +++ b/src/command/newsletterRecipientsCommand.ts @@ -0,0 +1,4 @@ +export interface SynchronizeNewsletterRecipientsCommand { + newsletterId: number; + recipients: Array; +} diff --git a/src/command/newsletterRecipientsCommandHandler.ts b/src/command/newsletterRecipientsCommandHandler.ts new file mode 100644 index 0000000..b321c71 --- /dev/null +++ b/src/command/newsletterRecipientsCommandHandler.ts @@ -0,0 +1,73 @@ +import { DeleteResult, EntityManager, InsertResult, UpdateResult } from "typeorm"; +import { dataSource } from "../data-source"; +import InternalException from "../exceptions/internalException"; +import NewsletterRecipientsService from "../service/newsletterRecipientsService"; +import { SynchronizeNewsletterRecipientsCommand } from "./newsletterRecipientsCommand"; +import { newsletterRecipients } from "../entity/newsletterRecipients"; + +export default abstract class NewsletterRecipientsCommandHandler { + /** + * @description sync newsletterRecipients + * @param {SynchronizeNewsletterRecipientsCommand} syncNewsletterRecipients + * @returns {Promise} + */ + static async sync(syncNewsletterRecipients: SynchronizeNewsletterRecipientsCommand): Promise { + let currentRecipients = (await NewsletterRecipientsService.getAll(syncNewsletterRecipients.newsletterId)).map( + (r) => r.memberId + ); + + return await dataSource.manager + .transaction(async (manager) => { + let newRecipients = syncNewsletterRecipients.recipients.filter( + (r) => !currentRecipients.map((np) => np).includes(r) + ); + let removeRecipients = currentRecipients.filter( + (r) => !syncNewsletterRecipients.recipients.map((np) => np).includes(r) + ); + + if (newRecipients.length != 0) { + await this.syncPresenceAdd(manager, syncNewsletterRecipients.newsletterId, newRecipients); + } + + if (removeRecipients.length != 0) { + await this.syncPresenceRemove(manager, syncNewsletterRecipients.newsletterId, removeRecipients); + } + }) + .then(() => {}) + .catch((err) => { + throw new InternalException("Failed syncing newsletter recipients", err); + }); + } + + private static async syncPresenceAdd( + manager: EntityManager, + newsletterId: number, + recipients: Array + ): Promise { + return await manager + .createQueryBuilder() + .insert() + .into(newsletterRecipients) + .values( + recipients.map((r) => ({ + memberId: r, + newsletterId: newsletterId, + })) + ) + .execute(); + } + + private static async syncPresenceRemove( + manager: EntityManager, + newsletterId: number, + recipients: Array + ): Promise { + return await manager + .createQueryBuilder() + .delete() + .from(newsletterRecipients) + .where("memberId IN (:...ids)", { ids: recipients }) + .andWhere("newsletterId = :newsletterId", { newsletterId }) + .execute(); + } +} diff --git a/src/command/templateCommand.ts b/src/command/templateCommand.ts new file mode 100644 index 0000000..55a5f44 --- /dev/null +++ b/src/command/templateCommand.ts @@ -0,0 +1,16 @@ +export interface CreateTemplateCommand { + template: string; + description: string | null; +} + +export interface UpdateTemplateCommand { + id: number; + template: string; + description: string | null; + design: object; + html: string; +} + +export interface DeleteTemplateCommand { + id: number; +} diff --git a/src/command/templateCommandHandler.ts b/src/command/templateCommandHandler.ts new file mode 100644 index 0000000..1f9b4ce --- /dev/null +++ b/src/command/templateCommandHandler.ts @@ -0,0 +1,70 @@ +import { dataSource } from "../data-source"; +import { template } from "../entity/template"; +import InternalException from "../exceptions/internalException"; +import { CreateTemplateCommand, DeleteTemplateCommand, UpdateTemplateCommand } from "./templateCommand"; + +export default abstract class TemplateCommandHandler { + /** + * @description create template + * @param CreateTemplateCommand + * @returns {Promise} + */ + static async create(createTemplate: CreateTemplateCommand): Promise { + return await dataSource + .createQueryBuilder() + .insert() + .into(template) + .values({ + template: createTemplate.template, + description: createTemplate.description, + }) + .execute() + .then((result) => { + return result.identifiers[0].id; + }) + .catch((err) => { + throw new InternalException("Failed creating template", err); + }); + } + + /** + * @description update template + * @param UpdateTemplateCommand + * @returns {Promise} + */ + static async update(updateTemplate: UpdateTemplateCommand): Promise { + return await dataSource + .createQueryBuilder() + .update(template) + .set({ + template: updateTemplate.template, + description: updateTemplate.description, + design: updateTemplate.design, + html: updateTemplate.html, + }) + .where("id = :id", { id: updateTemplate.id }) + .execute() + .then(() => {}) + .catch((err) => { + throw new InternalException("Failed updating template", err); + }); + } + + /** + * @description delete template + * @param DeleteTemplateCommand + * @returns {Promise} + */ + static async delete(deletTemplate: DeleteTemplateCommand): Promise { + return await dataSource + .createQueryBuilder() + .delete() + .from(template) + .where("id = :id", { id: deletTemplate.id }) + .execute() + .then(() => {}) + .catch((err) => { + throw new InternalException("Failed deleting template", err); + }); + } +} diff --git a/src/command/templateUsageCommand.ts b/src/command/templateUsageCommand.ts new file mode 100644 index 0000000..b9218a9 --- /dev/null +++ b/src/command/templateUsageCommand.ts @@ -0,0 +1,6 @@ +export interface UpdateTemplateUsageCommand { + scope: string; + headerId: number | null; + bodyId: number | null; + footerId: number | null; +} diff --git a/src/command/templateUsageCommandHandler.ts b/src/command/templateUsageCommandHandler.ts new file mode 100644 index 0000000..54626ba --- /dev/null +++ b/src/command/templateUsageCommandHandler.ts @@ -0,0 +1,28 @@ +import { dataSource } from "../data-source"; +import { templateUsage } from "../entity/templateUsage"; +import InternalException from "../exceptions/internalException"; +import { UpdateTemplateUsageCommand } from "./templateUsageCommand"; + +export default abstract class TemplateUsageCommandHandler { + /** + * @description update templateUsage + * @param UpdateTemplateUsageCommand + * @returns {Promise} + */ + static async update(updateTemplateUsage: UpdateTemplateUsageCommand): Promise { + return await dataSource + .createQueryBuilder() + .update(templateUsage) + .set({ + headerId: updateTemplateUsage.headerId, + bodyId: updateTemplateUsage.bodyId, + footerId: updateTemplateUsage.footerId, + }) + .where("scope = :scope", { scope: updateTemplateUsage.scope }) + .execute() + .then(() => {}) + .catch((err) => { + throw new InternalException("Failed updating templateUsage", err); + }); + } +} diff --git a/src/controller/admin/newsletterConfigController.ts b/src/controller/admin/newsletterConfigController.ts new file mode 100644 index 0000000..3abe4b5 --- /dev/null +++ b/src/controller/admin/newsletterConfigController.ts @@ -0,0 +1,66 @@ +import { Request, Response } from "express"; +import NewsletterConfigService from "../../service/newsletterConfigService"; +import NewsletterConfigFactory from "../../factory/admin/newsletterConfig"; +import NewsletterConfigCommandHandler from "../../command/newsletterConfigCommandHandler"; +import { DeleteNewsletterConfigCommand, SetNewsletterConfigCommand } from "../../command/newsletterConfigCommand"; + +/** + * @description get all newsletterConfigs + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function getAllNewsletterConfigs(req: Request, res: Response): Promise { + let newsletterConfigs = await NewsletterConfigService.getAll(); + + res.json(NewsletterConfigFactory.mapToBase(newsletterConfigs)); +} + +/** + * @description get newsletterConfig by id + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function getNewsletterConfigById(req: Request, res: Response): Promise { + let comId = parseInt(req.params.comId); + let newsletterConfig = await NewsletterConfigService.getByComId(comId); + + res.json(NewsletterConfigFactory.mapToSingle(newsletterConfig)); +} + +/** + * @description set newsletterConfig + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function setNewsletterConfig(req: Request, res: Response): Promise { + let comTypeId = req.body.comTypeId; + let config = req.body.config; + + let createNewsletterConfig: SetNewsletterConfigCommand = { + comTypeId, + config, + }; + let id = await NewsletterConfigCommandHandler.set(createNewsletterConfig); + + res.send(id); +} + +/** + * @description delete award + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function deleteNewsletterConfig(req: Request, res: Response): Promise { + const comTypeId = parseInt(req.params.comTypeId); + + let deleteNewsletterConfig: DeleteNewsletterConfigCommand = { + comTypeId: comTypeId, + }; + await NewsletterConfigCommandHandler.delete(deleteNewsletterConfig); + + res.sendStatus(204); +} diff --git a/src/controller/admin/newsletterController.ts b/src/controller/admin/newsletterController.ts new file mode 100644 index 0000000..87e62ca --- /dev/null +++ b/src/controller/admin/newsletterController.ts @@ -0,0 +1,387 @@ +import { Request, Response } from "express"; +import NewsletterService from "../../service/newsletterService"; +import NewsletterFactory from "../../factory/admin/newsletter"; +import NewsletterDatesService from "../../service/newsletterDatesService"; +import NewsletterDatesFactory from "../../factory/admin/newsletterDates"; +import NewsletterRecipientsService from "../../service/newsletterRecipientsService"; +import NewsletterRecipientsFactory from "../../factory/admin/newsletterRecipients"; +import { FileSystemHelper } from "../../helpers/fileSystemHelper"; +import { CreateNewsletterCommand, SynchronizeNewsletterCommand } from "../../command/newsletterCommand"; +import NewsletterCommandHandler from "../../command/newsletterCommandHandler"; +import { SynchronizeNewsletterDatesCommand } from "../../command/newsletterDatesCommand"; +import NewsletterDatesCommandHandler from "../../command/newsletterDatesCommandHandler"; +import { SynchronizeNewsletterRecipientsCommand } from "../../command/newsletterRecipientsCommand"; +import NewsletterRecipientsCommandHandler from "../../command/newsletterRecipientsCommandHandler"; +import { NewsletterDatesViewModel } from "../../viewmodel/admin/newsletterDates.models"; +import { PdfExport } from "../../helpers/pdfExport"; +import UserService from "../../service/userService"; +import { TemplateHelper } from "../../helpers/templateHelper"; +import MailHelper from "../../helpers/mailHelper"; +import { NewsletterEventType, NewsletterHelper } from "../../helpers/newsletterHelper"; +import { Salutation } from "../../enums/salutation"; + +/** + * @description get all newsletters + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function getAllNewsletters(req: Request, res: Response): Promise { + 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 recipients by id + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function getNewsletterRecipientsById(req: Request, res: Response): Promise { + let newsletterId = parseInt(req.params.newsletterId); + + let recipients = await NewsletterRecipientsService.getAll(newsletterId); + + res.json(NewsletterRecipientsFactory.mapToBase(recipients)); +} + +/** + * @description get newsletter printouts by id + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function getNewsletterPrintoutsById(req: Request, res: Response): Promise { + let newsletterId = parseInt(req.params.newsletterId); + + let newsletter = await NewsletterService.getById(newsletterId); + + let filesInFolder = FileSystemHelper.getFilesInDirectory( + `newsletter/${newsletter.id}_${newsletter.title.replace(" ", "")}` + ); + + res.json(filesInFolder); +} + +/** + * @description get newsletter printout by id and print + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function getNewsletterPrintoutByIdAndPrint(req: Request, res: Response): Promise { + let newsletterId = parseInt(req.params.newsletterId); + let filename = req.params.filename; + + let newsletter = await NewsletterService.getById(newsletterId); + + let filepath = FileSystemHelper.formatPath( + "newsletter", + `${newsletter.id}_${newsletter.title.replace(" ", "")}`, + filename + ); + + res.sendFile(filepath, { + headers: { + "Content-Type": "application/pdf", + }, + }); +} + +/** + * @description create newsletter printout preview by id + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function createNewsletterPrintoutPreviewById(req: Request, res: Response): Promise { + let newsletterId = parseInt(req.params.newsletterId); + let newsletter = await NewsletterService.getById(newsletterId); + let dates = await NewsletterDatesService.getAll(newsletterId); + let recipient = await UserService.getById(parseInt(req.userId)); + + let data = NewsletterHelper.buildData(newsletter, dates); + data.recipient = { + firstname: recipient.firstname, + lastname: recipient.lastname, + salutation: Salutation.none, + nameaffix: "", + street: "Straße", + streetNumber: "Hausnummer", + streetNumberAdd: "Adresszusatz", + }; + + let pdf = await PdfExport.renderFile({ + title: "Probedruck Newsletter", + template: "newsletter", + saveToDisk: false, + data: data, + }); + + let pdfbuffer = Buffer.from(pdf); + + res.setHeader("Content-Type", "application/pdf"); + res.setHeader("Content-Length", pdfbuffer.byteLength); + res.setHeader("Content-Disposition", "inline; filename=preview.pdf"); + + res.send(pdfbuffer); +} + +/** + * @description create newsletter + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function createNewsletter(req: Request, res: Response): Promise { + let title = req.body.title; + + let createNewsletter: CreateNewsletterCommand = { + title, + }; + let id = await NewsletterCommandHandler.create(createNewsletter); + + res.send(id); +} + +/** + * @description get newsletter printout progress by id + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function getNewsletterPrintoutProgressById(req: Request, res: Response): Promise { + let newsletterId = parseInt(req.params.newsletterId); + + res.setHeader("Content-Type", "text/event-stream"); + res.setHeader("Cache-Control", "no-cache"); + res.setHeader("Connection", "keep-alive"); + + res.flushHeaders(); + + const progressHandler = (data: NewsletterEventType) => { + if (data.newsletterId == newsletterId && data.kind == "pdf") { + res.write(JSON.stringify(data)); + } + }; + + const completeHandler = (data: NewsletterEventType) => { + if (data.newsletterId == newsletterId && data.kind == "pdf") { + res.write(JSON.stringify(data)); + res.end(); + } + }; + + NewsletterHelper.jobStatus.on("progress", progressHandler); + NewsletterHelper.jobStatus.on("complete", completeHandler); + + req.on("close", () => { + NewsletterHelper.jobStatus.off("progress", progressHandler); + NewsletterHelper.jobStatus.off("complete", completeHandler); + }); +} + +/** + * @description create newsletter printouts for each member by id + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function createNewsletterPrintoutById(req: Request, res: Response): Promise { + let newsletterId = parseInt(req.params.newsletterId); + + await NewsletterHelper.printPdfs(newsletterId); + + res.sendStatus(204); +} + +/** + * @description create newsletter mail preview by id + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function createNewsletterMailPreviewById(req: Request, res: Response): Promise { + let newsletterId = parseInt(req.params.newsletterId); + let newsletter = await NewsletterService.getById(newsletterId); + let dates = await NewsletterDatesService.getAll(newsletterId); + let recipient = await UserService.getById(parseInt(req.userId)); + + let data = NewsletterHelper.buildData(newsletter, dates); + data.recipient = { + firstname: recipient.firstname, + lastname: recipient.lastname, + salutation: Salutation.none, + nameaffix: "", + street: "Straße", + streetNumber: "Hausnummer", + streetNumberAdd: "Adresszusatz", + }; + + const { body } = await TemplateHelper.renderFileForModule({ + module: "newsletter", + bodyData: data, + title: "Probeversand Newsletter", + }); + + await MailHelper.sendMail(recipient.mail, "Probeversand Newsletter", body); + + res.sendStatus(204); +} + +/** + * @description send newsletter mail and create printouts by id + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function sendNewsletterById(req: Request, res: Response): Promise { + let newsletterId = parseInt(req.params.newsletterId); + + await NewsletterHelper.sendMails(newsletterId); + + res.sendStatus(204); +} + +/** + * @description get newsletter sending progress by id + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function getNewsletterSendingProgressById(req: Request, res: Response): Promise { + let newsletterId = parseInt(req.params.newsletterId); + + res.setHeader("Content-Type", "text/event-stream"); + res.setHeader("Cache-Control", "no-cache"); + res.setHeader("Connection", "keep-alive"); + + res.flushHeaders(); + + const progressHandler = (data: NewsletterEventType) => { + if (data.newsletterId == newsletterId && data.kind == "mail") { + res.write(JSON.stringify(data)); + } + }; + + const completeHandler = (data: NewsletterEventType) => { + if (data.newsletterId == newsletterId && data.kind == "mail") { + res.write(JSON.stringify(data)); + res.end(); + } + }; + + NewsletterHelper.jobStatus.on("progress", progressHandler); + NewsletterHelper.jobStatus.on("complete", completeHandler); + + req.on("close", () => { + NewsletterHelper.jobStatus.off("progress", progressHandler); + NewsletterHelper.jobStatus.off("complete", completeHandler); + }); +} + +/** + * @description synchronize newsletter by id + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function synchronizeNewsletterById(req: Request, res: Response): Promise { + let id = parseInt(req.params.id); + let title = req.body.title; + let description = req.body.description; + let newsletterTitle = req.body.newsletterTitle; + let newsletterText = req.body.newsletterText; + let newsletterSignatur = req.body.newsletterSignatur; + let recipientsByQueryId = req.body.recipientsByQueryId ?? null; + + let syncNewsletter: SynchronizeNewsletterCommand = { + id, + title, + description, + newsletterTitle, + newsletterText, + newsletterSignatur, + recipientsByQueryId, + }; + await NewsletterCommandHandler.sync(syncNewsletter); + + res.sendStatus(204); +} + +/** + * @description synchronize newsletter dates by id + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function synchronizeNewsletterDatesById(req: Request, res: Response): Promise { + 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, + }; + await NewsletterRecipientsCommandHandler.sync(syncRecipients); + + res.sendStatus(204); +} diff --git a/src/controller/admin/protocolController.ts b/src/controller/admin/protocolController.ts index 12a5b22..a82608f 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 @@ -237,9 +238,10 @@ export async function createProtocolPrintoutById(req: Request, res: Response): P )}`; await PdfExport.renderFile({ - template: "protocol.template.html", + template: "protocol", title, filename, + folder: "protocol", data: { title: protocol.title, summary: protocol.summary, @@ -262,7 +264,7 @@ export async function createProtocolPrintoutById(req: Request, res: Response): P let printout: CreateProtocolPrintoutCommand = { title, iteration: iteration + 1, - filename, + filename: FileSystemHelper.normalizePath("protocol", filename), protocolId, }; await ProtocolPrintoutCommandHandler.create(printout); diff --git a/src/controller/admin/queryBuilderController.ts b/src/controller/admin/queryBuilderController.ts index be4a68e..e6bb519 100644 --- a/src/controller/admin/queryBuilderController.ts +++ b/src/controller/admin/queryBuilderController.ts @@ -91,7 +91,7 @@ export async function executeQuery(req: Request, res: Response): Promise { res.json({ stats: "success", - rows: rows, + rows: DynamicQueryBuilder.flattenQueryResult(rows), total: total, offset: offset, count: count, diff --git a/src/controller/admin/templateController.ts b/src/controller/admin/templateController.ts new file mode 100644 index 0000000..9f867f9 --- /dev/null +++ b/src/controller/admin/templateController.ts @@ -0,0 +1,120 @@ +import { Request, Response } from "express"; +import TemplateService from "../../service/templateService"; +import TemplateFactory from "../../factory/admin/template"; +import { CreateTemplateCommand, DeleteTemplateCommand, UpdateTemplateCommand } from "../../command/templateCommand"; +import TemplateCommandHandler from "../../command/templateCommandHandler"; + +/** + * @description get all templates + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function getAllTemplates(req: Request, res: Response): Promise { + let templates = await TemplateService.getAll(); + + res.json(TemplateFactory.mapToBase(templates)); +} + +/** + * @description get template by id + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function getTemplateById(req: Request, res: Response): Promise { + const id = parseInt(req.params.id); + let template = await TemplateService.getById(id); + + res.json(TemplateFactory.mapToSingle(template)); +} + +/** + * @description create new template + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function createTemplate(req: Request, res: Response): Promise { + const template = req.body.template; + const description = req.body.description; + + let createTemplate: CreateTemplateCommand = { + template: template, + description: description, + }; + let id = await TemplateCommandHandler.create(createTemplate); + + res.status(200).send(id); +} + +/** + * @description clone template + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function cloneTemplate(req: Request, res: Response): Promise { + const cloneId = req.body.cloneId; + + const { template, description, design, html } = await TemplateService.getById(cloneId); + + let createTemplate: CreateTemplateCommand = { + template: "", + description: "", + }; + let id = await TemplateCommandHandler.create(createTemplate); + + let updateTemplate: UpdateTemplateCommand = { + id: id, + template: template + " - Kopie", + description: description, + design: design, + html: html, + }; + await TemplateCommandHandler.update(updateTemplate); + + res.status(200).send(id); +} + +/** + * @description update template + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function updateTemplate(req: Request, res: Response): Promise { + const id = parseInt(req.params.id); + const template = req.body.template; + const description = req.body.description; + const design = req.body.design; + const html = req.body.html; + + let updateTemplate: UpdateTemplateCommand = { + id: id, + template: template, + description: description, + design: design, + html: html, + }; + await TemplateCommandHandler.update(updateTemplate); + + res.sendStatus(204); +} + +/** + * @description delete template + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function deleteTemplate(req: Request, res: Response): Promise { + const id = parseInt(req.params.id); + + let deleteTemplate: DeleteTemplateCommand = { + id: id, + }; + await TemplateCommandHandler.delete(deleteTemplate); + + res.sendStatus(204); +} diff --git a/src/controller/admin/templateUsageController.ts b/src/controller/admin/templateUsageController.ts new file mode 100644 index 0000000..ac1e74a --- /dev/null +++ b/src/controller/admin/templateUsageController.ts @@ -0,0 +1,91 @@ +import { Request, Response } from "express"; +import TemplateUsageService from "../../service/templateUsageService"; +import TemplateUsageFactory from "../../factory/admin/templateUsage"; +import { UpdateTemplateUsageCommand } from "../../command/templateUsageCommand"; +import TemplateUsageCommandHandler from "../../command/templateUsageCommandHandler"; +import PermissionHelper from "../../helpers/permissionHelper"; +import ForbiddenRequestException from "../../exceptions/forbiddenRequestException"; +import { PermissionModule } from "../../type/permissionTypes"; +import { PdfExport } from "../../helpers/pdfExport"; +import { DemoDataHelper } from "../../helpers/demoDataHelper"; + +/** + * @description get all templateUsages + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function getAllTemplateUsages(req: Request, res: Response): Promise { + let templateUsages = await TemplateUsageService.getAll(); + + if (!req.isOwner) { + templateUsages = templateUsages.filter((tu) => { + return ( + PermissionHelper.can(req.permissions, "update", "settings", tu.scope) || + PermissionHelper.can(req.permissions, "update", "club", tu.scope) + ); + }); + } + + res.json(TemplateUsageFactory.mapToBase(templateUsages)); +} + +/** + * @description print demo of templateUsage + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function printTemplateUsageDemo(req: Request, res: Response): Promise { + const scope = req.params.scope as PermissionModule; + + let demoData = DemoDataHelper.getData(scope); + let pdf = await PdfExport.renderFile({ + template: scope, + saveToDisk: false, + data: demoData, + }); + + let pdfbuffer = Buffer.from(pdf); + + res.setHeader("Content-Type", "application/pdf"); + res.setHeader("Content-Length", pdfbuffer.byteLength); + res.setHeader("Content-Disposition", "inline; filename=preview.pdf"); + + res.send(pdfbuffer); +} + +/** + * @description update templateUsage + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function updateTemplateUsage(req: Request, res: Response): Promise { + const scope = req.params.scope; + let allowedSettings = PermissionHelper.can( + req.permissions, + "update", + "settings", + req.params.scope as PermissionModule + ); + let allowedClub = PermissionHelper.can(req.permissions, "update", "club", req.params.scope as PermissionModule); + + if (!(req.isOwner || allowedSettings || allowedClub)) { + throw new ForbiddenRequestException(`missing permission for editing scope ${req.params.scope}`); + } + + const headerId = req.body.headerId ?? null; + const bodyId = req.body.bodyId ?? null; + const footerId = req.body.footerId ?? null; + + let updateTemplateUsage: UpdateTemplateUsageCommand = { + scope: scope, + headerId: headerId, + bodyId: bodyId, + footerId: footerId, + }; + await TemplateUsageCommandHandler.update(updateTemplateUsage); + + res.sendStatus(204); +} diff --git a/src/controller/admin/userController.ts b/src/controller/admin/userController.ts index 6596c0a..0177fec 100644 --- a/src/controller/admin/userController.ts +++ b/src/controller/admin/userController.ts @@ -146,8 +146,7 @@ export async function deleteUser(req: Request, res: Response): Promise { try { // sendmail - let mailhelper = new MailHelper(); - await mailhelper.sendMail( + await MailHelper.sendMail( user.mail, `Email Bestätigung für Mitglieder Admin-Portal von ${CLUB_NAME}`, `Ihr Nutzerkonto des Adminportals wurde erfolgreich gelöscht.` diff --git a/src/controller/inviteController.ts b/src/controller/inviteController.ts index 8fa2f34..bb08cd6 100644 --- a/src/controller/inviteController.ts +++ b/src/controller/inviteController.ts @@ -71,8 +71,7 @@ export async function inviteUser(req: Request, res: Response, isInvite: boolean let token = await InviteCommandHandler.create(createInvite); // sendmail - let mailhelper = new MailHelper(); - await mailhelper.sendMail( + await MailHelper.sendMail( mail, `Email Bestätigung für Mitglieder Admin-Portal von ${CLUB_NAME}`, `Öffne folgenden Link: ${origin}/${isInvite ? "invite" : "setup"}/verify?mail=${mail}&token=${token}` diff --git a/src/controller/publicController.ts b/src/controller/publicController.ts index 8c627fc..d933522 100644 --- a/src/controller/publicController.ts +++ b/src/controller/publicController.ts @@ -6,6 +6,7 @@ import { createEvents } from "ics"; import moment from "moment"; import InternalException from "../exceptions/internalException"; import CalendarFactory from "../factory/admin/calendar"; +import { CalendarHelper } from "../helpers/calendarHelper"; /** * @description get all calendar items by types or nscdr @@ -16,6 +17,7 @@ import CalendarFactory from "../factory/admin/calendar"; */ export async function getCalendarItemsByTypes(req: Request, res: Response): Promise { let types = Array.isArray(req.query.types) ? req.query.types : [req.query.types]; + let nscdr = req.query.nscdr == "true"; let output = (req.query.output as "ics" | "json") ?? "ics"; if (output != "ics" && output != "json") { @@ -33,7 +35,10 @@ export async function getCalendarItemsByTypes(req: Request, res: Response): Prom ti.passphrase == "" || ti.passphrase == (types as Array).find((t) => t.includes(ti.type)).split(":")[1] ); - items = await CalendarService.getByTypes(typeIds.map((t) => t.id)); + items = await CalendarService.getByTypes( + typeIds.map((t) => t.id), + nscdr + ); } else { items = await CalendarService.getByTypeNSCDR(); } @@ -41,59 +46,8 @@ export async function getCalendarItemsByTypes(req: Request, res: Response): Prom if (output == "json") { res.json(CalendarFactory.mapToBase(items)); } else { - let events = createEvents( - items.map((i) => ({ - calName: process.env.CLUB_NAME, - uid: i.id, - sequence: 1, - ...(i.allDay - ? { - start: moment(i.starttime) - .format("YYYY-M-D") - .split("-") - .map((a) => parseInt(a)) as [number, number, number], - end: moment(i.endtime) - .format("YYYY-M-D") - .split("-") - .map((a) => parseInt(a)) as [number, number, number], - } - : { - start: moment(i.starttime) - .format("YYYY-M-D-H-m") - .split("-") - .map((a) => parseInt(a)) as [number, number, number, number, number], - end: moment(i.endtime) - .format("YYYY-M-D-H-m") - .split("-") - .map((a) => parseInt(a)) as [number, number, number, number, number], - }), - title: i.title, - description: i.content, - location: i.location, - categories: [i.type.type], - created: moment(i.createdAt) - .format("YYYY-M-D-H-m") - .split("-") - .map((a) => parseInt(a)) as [number, number, number, number, number], - lastModified: moment(i.updatedAt) - .format("YYYY-M-D-H-m") - .split("-") - .map((a) => parseInt(a)) as [number, number, number, number, number], - transp: "OPAQUE" as "OPAQUE", - url: "https://www.ff-merching.de", - alarms: [ - { - action: "display", - description: "Erinnerung", - trigger: { - minutes: 30, - before: true, - }, - }, - ], - })) - ); + let { error, value } = CalendarHelper.buildICS(items); - res.type("ics").send(events.value); + res.type("ics").send(value); } } diff --git a/src/controller/resetController.ts b/src/controller/resetController.ts index 32f2004..fe0554c 100644 --- a/src/controller/resetController.ts +++ b/src/controller/resetController.ts @@ -41,8 +41,7 @@ export async function startReset(req: Request, res: Response): Promise { let token = await ResetCommandHandler.create(createReset); // sendmail - let mailhelper = new MailHelper(); - await mailhelper.sendMail( + await MailHelper.sendMail( mail, `Email Bestätigung für Mitglieder Admin-Portal von ${CLUB_NAME}`, `Öffne folgenden Link: ${origin}/reset/reset?mail=${mail}&token=${token}` diff --git a/src/data-source.ts b/src/data-source.ts index 685d05c..7e8321f 100644 --- a/src/data-source.ts +++ b/src/data-source.ts @@ -51,6 +51,16 @@ import { memberExecutivePositionsView } from "./views/memberExecutivePositionVie import { memberQualificationsView } from "./views/memberQualificationsView"; import { membershipView } from "./views/membershipsView"; import { MemberDataViews1734520998539 } from "./migrations/1734520998539-memberDataViews"; +import { template } from "./entity/template"; +import { Template1734854680201 } from "./migrations/1734854680201-template"; +import { templateUsage } from "./entity/templateUsage"; +import { TemplateUsage1734949173739 } from "./migrations/1734949173739-templateUsage"; +import { newsletter } from "./entity/newsletter"; +import { newsletterDates } from "./entity/newsletterDates"; +import { newsletterRecipients } from "./entity/newsletterRecipients"; +import { Newsletter1735118780511 } from "./migrations/1735118780511-newsletter"; +import { newsletterConfig } from "./entity/newsletterConfig"; +import { NewsletterConfig1735207446910 } from "./migrations/1735207446910-newsletterConfig"; const dataSource = new DataSource({ type: DB_TYPE as any, @@ -90,6 +100,12 @@ const dataSource = new DataSource({ calendar, calendarType, query, + template, + templateUsage, + newsletter, + newsletterDates, + newsletterRecipients, + newsletterConfig, memberView, memberExecutivePositionsView, memberQualificationsView, @@ -112,6 +128,10 @@ const dataSource = new DataSource({ SecuringCalendarType1733249553766, QueryStore1734187754677, MemberDataViews1734520998539, + Template1734854680201, + TemplateUsage1734949173739, + Newsletter1735118780511, + NewsletterConfig1735207446910, ], migrationsRun: true, migrationsTransactionMode: "each", diff --git a/src/demodata/newsletter.data.ts b/src/demodata/newsletter.data.ts new file mode 100644 index 0000000..41a81f4 --- /dev/null +++ b/src/demodata/newsletter.data.ts @@ -0,0 +1,72 @@ +import { calendar } from "../entity/calendar"; +import { member } from "../entity/member"; +import { Salutation } from "../enums/salutation"; + +export const newsletterDemoData: { + title: string; + description: string; + newsletterTitle: string; + newsletterText: string; + newsletterSignatur: string; + dates: Array< + Partial< + calendar & { + formattedStarttime: string; + formattedFullStarttime: string; + formattedEndtime: string; + formattedFullEndtime: string; + } + > + >; + recipient: Partial; +} = { + title: "Beispiel Newsletter Daten", + description: "Zusammenfassung der Demodaten.", + newsletterTitle: "

Sehr geehrtes Feuerwehrmitglied

", + newsletterText: "

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

", + newsletterSignatur: "

Mit freundlichen Grüßen

...

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

Beschreibung eines Termins

", + starttime: new Date(), + formattedStarttime: new Date().toLocaleDateString("de-DE", { + weekday: "long", + day: "2-digit", + month: "long", + }), + formattedFullStarttime: new Date().toLocaleDateString("de-DE", { + weekday: "long", + day: "2-digit", + month: "long", + year: "numeric", + hour: "2-digit", + minute: "2-digit", + }), + endtime: new Date(), + formattedEndtime: new Date().toLocaleDateString("de-DE", { + weekday: "long", + day: "2-digit", + month: "long", + }), + formattedFullEndtime: new Date().toLocaleDateString("de-DE", { + weekday: "long", + day: "2-digit", + month: "long", + year: "numeric", + hour: "2-digit", + minute: "2-digit", + }), + location: "Feuerwehrhaus", + }, + ], + recipient: { + firstname: "Julian", + lastname: "Krauser", + salutation: Salutation.sir, + nameaffix: "", + street: "Straße", + streetNumber: "Hausnummer", + streetNumberAdd: "Adresszusatz", + }, +}; diff --git a/src/demodata/protocol.data.ts b/src/demodata/protocol.data.ts new file mode 100644 index 0000000..1feeace --- /dev/null +++ b/src/demodata/protocol.data.ts @@ -0,0 +1,56 @@ +import { member } from "../entity/member"; +import { protocolAgenda } from "../entity/protocolAgenda"; +import { protocolDecision } from "../entity/protocolDecision"; +import { protocolVoting } from "../entity/protocolVoting"; + +export const protocolDemoData: { + title: string; + summary: string; + iteration: number; + date: string; + start: string; + end: string; + agenda: Array>; + decisions: Array>; + presence: Array>; + votings: Array>; +} = { + title: "Beispiel Protokoll Daten", + summary: "Zusammenfassung der Demodaten.", + iteration: 1, + date: new Date().toLocaleDateString("de-DE", { + weekday: "long", + day: "2-digit", + month: "2-digit", + year: "numeric", + }), + start: "19:00:00", + end: "21:00:00", + agenda: [ + { + topic: "Protokoll-TOP", + context: "Inhalt des Punktes", + }, + ], + decisions: [ + { + topic: "Entscheidung yz", + context: "Inhalt der Entscheidung", + }, + ], + presence: [ + { + firstname: "Julian", + lastname: "Krauser", + }, + ], + votings: [ + { + topic: "Abstimmung xy", + context: "Inhalt der Abstimmung", + favour: 1, + abstain: 2, + against: 3, + }, + ], +}; diff --git a/src/entity/member.ts b/src/entity/member.ts index 7bb0d78..de7f612 100644 --- a/src/entity/member.ts +++ b/src/entity/member.ts @@ -40,7 +40,7 @@ export class member { birthdate: Date; @OneToMany(() => communication, (communications) => communications.member) - communications: communication; + communications: communication[]; @OneToOne(() => communication, { nullable: true, diff --git a/src/entity/newsletter.ts b/src/entity/newsletter.ts new file mode 100644 index 0000000..9393e53 --- /dev/null +++ b/src/entity/newsletter.ts @@ -0,0 +1,45 @@ +import { Column, Entity, ManyToOne, OneToMany, PrimaryColumn } from "typeorm"; +import { newsletterDates } from "./newsletterDates"; +import { member } from "./member"; +import { newsletterRecipients } from "./newsletterRecipients"; +import { query } from "./query"; + +@Entity() +export class newsletter { + @PrimaryColumn({ generated: "increment", type: "int" }) + id: number; + + @Column({ type: "varchar", length: 255 }) + title: string; + + @Column({ type: "varchar", length: 255, default: "" }) + description: string; + + @Column({ type: "varchar", length: 255, default: "" }) + newsletterTitle: string; + + @Column({ type: "text", default: "" }) + newsletterText: string; + + @Column({ type: "varchar", length: 255, default: "" }) + newsletterSignatur: string; + + @Column({ type: "boolean", default: false }) + isSent: boolean; + + @Column({ type: "int", nullable: true }) + recipientsByQueryId?: number; + + @OneToMany(() => newsletterDates, (dates) => dates.newsletter) + dates: newsletterDates[]; + + @OneToMany(() => newsletterRecipients, (recipient) => recipient.newsletter) + recipients: newsletterRecipients[]; + + @ManyToOne(() => query, { + nullable: true, + onDelete: "CASCADE", + onUpdate: "RESTRICT", + }) + recipientsByQuery?: query; +} diff --git a/src/entity/newsletterConfig.ts b/src/entity/newsletterConfig.ts new file mode 100644 index 0000000..6ad74ab --- /dev/null +++ b/src/entity/newsletterConfig.ts @@ -0,0 +1,30 @@ +import { Column, Entity, ManyToOne, PrimaryColumn } from "typeorm"; +import { NewsletterConfigType } from "../enums/newsletterConfigType"; +import { communicationType } from "./communicationType"; + +@Entity() +export class newsletterConfig { + @PrimaryColumn({ type: "int" }) + comTypeId: number; + + @Column({ + type: "varchar", + length: "255", + transformer: { + to(value: NewsletterConfigType) { + return value.toString(); + }, + from(value: string) { + return NewsletterConfigType[value as keyof typeof NewsletterConfigType]; + }, + }, + }) + config: NewsletterConfigType; + + @ManyToOne(() => communicationType, { + nullable: false, + onDelete: "CASCADE", + onUpdate: "RESTRICT", + }) + comType: communicationType; +} diff --git a/src/entity/newsletterDates.ts b/src/entity/newsletterDates.ts new file mode 100644 index 0000000..aefb8ee --- /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: "varchar" }) + calendarId: string; + + @Column({ type: "varchar", length: 255, nullable: true }) + diffTitle: string | null; + + @Column({ type: "text", nullable: true }) + diffDescription: string | null; + + @ManyToOne(() => newsletter, (newsletter) => newsletter.dates, { + nullable: false, + onDelete: "CASCADE", + onUpdate: "RESTRICT", + }) + newsletter: newsletter; + + @ManyToOne(() => calendar, { + nullable: false, + onDelete: "RESTRICT", + onUpdate: "RESTRICT", + }) + calendar: calendar; +} diff --git a/src/entity/newsletterRecipients.ts b/src/entity/newsletterRecipients.ts new file mode 100644 index 0000000..b57bce4 --- /dev/null +++ b/src/entity/newsletterRecipients.ts @@ -0,0 +1,26 @@ +import { Column, Entity, ManyToOne, PrimaryColumn } from "typeorm"; +import { newsletter } from "./newsletter"; +import { member } from "./member"; + +@Entity() +export class newsletterRecipients { + @PrimaryColumn({ type: "int" }) + newsletterId: number; + + @PrimaryColumn({ type: "int" }) + memberId: number; + + @ManyToOne(() => newsletter, (newsletter) => newsletter.recipients, { + nullable: false, + onDelete: "CASCADE", + onUpdate: "RESTRICT", + }) + newsletter: newsletter; + + @ManyToOne(() => member, { + nullable: false, + onDelete: "CASCADE", + onUpdate: "RESTRICT", + }) + member: member; +} diff --git a/src/entity/template.ts b/src/entity/template.ts new file mode 100644 index 0000000..6a25724 --- /dev/null +++ b/src/entity/template.ts @@ -0,0 +1,30 @@ +import { Column, Entity, PrimaryColumn } from "typeorm"; + +@Entity() +export class template { + @PrimaryColumn({ generated: "increment", type: "int" }) + id: number; + + @Column({ type: "varchar", length: 255 }) + template: string; + + @Column({ type: "varchar", length: 255, nullable: true }) + description?: string; + + @Column({ + type: "text", + default: "{}", + transformer: { + to(value: object) { + return JSON.stringify(value); + }, + from(value: string) { + return JSON.parse(value); + }, + }, + }) + design: object; + + @Column({ type: "text", default: "" }) + html: string; +} diff --git a/src/entity/templateUsage.ts b/src/entity/templateUsage.ts new file mode 100644 index 0000000..8aefd11 --- /dev/null +++ b/src/entity/templateUsage.ts @@ -0,0 +1,39 @@ +import { Column, Entity, ManyToOne, PrimaryColumn } from "typeorm"; +import { template } from "./template"; +import { PermissionModule } from "../type/permissionTypes"; + +@Entity() +export class templateUsage { + @PrimaryColumn({ type: "varchar", length: 255 }) + scope: PermissionModule; + + @Column({ type: "number", nullable: true }) + headerId: number | null; + + @Column({ type: "number", nullable: true }) + bodyId: number | null; + + @Column({ type: "number", nullable: true }) + footerId: number | null; + + @ManyToOne(() => template, { + nullable: true, + onDelete: "RESTRICT", + onUpdate: "RESTRICT", + }) + header: template | null; + + @ManyToOne(() => template, { + nullable: true, + onDelete: "RESTRICT", + onUpdate: "RESTRICT", + }) + body: template | null; + + @ManyToOne(() => template, { + nullable: true, + onDelete: "RESTRICT", + onUpdate: "RESTRICT", + }) + footer: template | null; +} diff --git a/src/enums/newsletterConfigType.ts b/src/enums/newsletterConfigType.ts new file mode 100644 index 0000000..4703494 --- /dev/null +++ b/src/enums/newsletterConfigType.ts @@ -0,0 +1,4 @@ +export enum NewsletterConfigType { + pdf = "pdf", + mail = "mail", +} diff --git a/src/env.defaults.ts b/src/env.defaults.ts index 2ad0925..21c9430 100644 --- a/src/env.defaults.ts +++ b/src/env.defaults.ts @@ -20,6 +20,7 @@ export const MAIL_PORT = Number(process.env.MAIL_PORT ?? "587"); export const MAIL_SECURE = process.env.MAIL_SECURE ?? "false"; export const CLUB_NAME = process.env.CLUB_NAME ?? ""; +export const CLUB_WEBSITE = process.env.CLUB_WEBSITE ?? ""; export function configCheck() { if (DB_TYPE != "mysql" && DB_TYPE != "sqlite") throw new Error("set valid value to DB_TYPE (mysql|sqlite)"); @@ -39,6 +40,13 @@ export function configCheck() { if (MAIL_HOST == "" || typeof MAIL_HOST != "string") throw new Error("set valid value to MAIL_HOST"); if (typeof MAIL_PORT != "number") throw new Error("set valid numeric value to MAIL_PORT"); if (MAIL_SECURE != "true" && MAIL_SECURE != "false") throw new Error("set 'true' or 'false' to MAIL_SECURE"); + + console.log(CLUB_WEBSITE); + if ( + CLUB_WEBSITE != "" && + !/^(http(s):\/\/.)[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)$/.test(CLUB_WEBSITE) + ) + throw new Error("CLUB_WEBSITE is not valid url"); } function checkMS(input: string, origin: string) { diff --git a/src/factory/admin/newsletter.ts b/src/factory/admin/newsletter.ts new file mode 100644 index 0000000..d734564 --- /dev/null +++ b/src/factory/admin/newsletter.ts @@ -0,0 +1,33 @@ +import { newsletter } from "../../entity/newsletter"; +import { NewsletterViewModel } from "../../viewmodel/admin/newsletter.models"; +import QueryStoreFactory from "./queryStore"; + +export default abstract class NewsletterFactory { + /** + * @description map record to newsletter + * @param {newsletter} record + * @returns {NewsletterViewModel} + */ + public static mapToSingle(record: newsletter): NewsletterViewModel { + return { + id: record.id, + title: record.title, + description: record.description, + newsletterTitle: record.newsletterTitle, + newsletterText: record.newsletterText, + newsletterSignatur: record.newsletterSignatur, + isSent: record.isSent, + recipientsByQueryId: record?.recipientsByQuery ? record.recipientsByQuery.id : null, + recipientsByQuery: record?.recipientsByQuery ? QueryStoreFactory.mapToSingle(record.recipientsByQuery) : null, + }; + } + + /** + * @description map records to newsletter + * @param {Array} records + * @returns {Array} + */ + public static mapToBase(records: Array): Array { + return records.map((r) => this.mapToSingle(r)); + } +} diff --git a/src/factory/admin/newsletterConfig.ts b/src/factory/admin/newsletterConfig.ts new file mode 100644 index 0000000..854bd83 --- /dev/null +++ b/src/factory/admin/newsletterConfig.ts @@ -0,0 +1,27 @@ +import { newsletterConfig } from "../../entity/newsletterConfig"; +import { NewsletterConfigViewModel } from "../../viewmodel/admin/newsletterConfig.models"; +import CommunicationTypeFactory from "./communicationType"; + +export default abstract class NewsletterConfigFactory { + /** + * @description map record to newsletterConfig + * @param {newsletterConfig} record + * @returns {NewsletterConfigViewModel} + */ + public static mapToSingle(record: newsletterConfig): NewsletterConfigViewModel { + return { + comTypeId: record.comTypeId, + config: record.config, + comType: record?.comType ? CommunicationTypeFactory.mapToSingle(record.comType) : null, + }; + } + + /** + * @description map records to newsletterConfig + * @param {Array} 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..9a1e332 --- /dev/null +++ b/src/factory/admin/newsletterRecipients.ts @@ -0,0 +1,27 @@ +import { newsletterRecipients } from "../../entity/newsletterRecipients"; +import { NewsletterRecipientsViewModel } from "../../viewmodel/admin/newsletterRecipients.models"; +import MemberFactory from "./member"; + +export default abstract class NewsletterRecipientsFactory { + /** + * @description map record to newsletterRecipients + * @param {newsletterRecipients} record + * @returns {NewsletterRecipientsViewModel} + */ + public static mapToSingle(record: newsletterRecipients): NewsletterRecipientsViewModel { + return { + newsletterId: record.newsletterId, + memberId: record.memberId, + member: MemberFactory.mapToSingle(record.member), + }; + } + + /** + * @description map records to newsletterRecipients + * @param {Array} records + * @returns {Array} + */ + public static mapToBase(records: Array): Array { + return records.map((r) => this.mapToSingle(r)); + } +} diff --git a/src/factory/admin/template.ts b/src/factory/admin/template.ts new file mode 100644 index 0000000..7da05e0 --- /dev/null +++ b/src/factory/admin/template.ts @@ -0,0 +1,28 @@ +import { template } from "../../entity/template"; +import { TemplateViewModel } from "../../viewmodel/admin/template.models"; + +export default abstract class TemplateFactory { + /** + * @description map record to template + * @param {template} record + * @returns {TemplateViewModel} + */ + public static mapToSingle(record: template): TemplateViewModel { + return { + id: record.id, + template: record.template, + description: record.description, + design: record.design, + html: record.html, + }; + } + + /** + * @description map records to template + * @param {Array