From 591d687550bba829cb3f5ade03b0c39f5017bb55 Mon Sep 17 00:00:00 2001 From: david-loe <56305409+david-loe@users.noreply.github.com> Date: Mon, 11 Nov 2024 15:44:44 +0100 Subject: [PATCH 01/14] add email without button --- backend/authStrategies/magiclogin.ts | 2 +- backend/mail/mail.ts | 32 ++++++++++++++-------------- backend/retentionpolicy.ts | 2 +- backend/templates/mail.ejs | 2 ++ 4 files changed, 20 insertions(+), 18 deletions(-) diff --git a/backend/authStrategies/magiclogin.ts b/backend/authStrategies/magiclogin.ts index 94c00e4d..1dbf335f 100644 --- a/backend/authStrategies/magiclogin.ts +++ b/backend/authStrategies/magiclogin.ts @@ -21,7 +21,7 @@ export default new MagicLoginStrategy.default({ i18n.t('mail.magiclogin.subject', { lng: user.settings.language }), i18n.t('mail.magiclogin.paragraph', { lng: user.settings.language }), { text: i18n.t('mail.magiclogin.buttonText', { lng: user.settings.language }), link: href }, - '', + undefined, false ) } else { diff --git a/backend/mail/mail.ts b/backend/mail/mail.ts index 87a9b66d..c0192b9c 100644 --- a/backend/mail/mail.ts +++ b/backend/mail/mail.ts @@ -30,20 +30,23 @@ export async function sendMail( recipients: IUser[], subject: string, paragraph: string, - button: { text: string; link: string }, - lastParagraph: string, + button?: { text: string; link: string }, + lastParagraph?: string, authenticateLink = true ) { for (let i = 0; i < recipients.length; i++) { const language = recipients[i].settings.language - const recipientButton = { ...button } - if (authenticateLink && recipients[i].fk.magiclogin && recipientButton.link.startsWith(process.env.VITE_FRONTEND_URL)) { - recipientButton.link = await genAuthenticatedLink({ - destination: recipients[i].fk.magiclogin!, - redirect: recipientButton.link.substring(process.env.VITE_FRONTEND_URL.length) - }) + let recipientButton: { text: string; link: string } | undefined = undefined + if (button) { + recipientButton = { ...button } + if (authenticateLink && recipients[i].fk.magiclogin && recipientButton.link.startsWith(process.env.VITE_FRONTEND_URL)) { + recipientButton.link = await genAuthenticatedLink({ + destination: recipients[i].fk.magiclogin!, + redirect: recipientButton.link.substring(process.env.VITE_FRONTEND_URL.length) + }) + } } - _sendMail(recipients[i], subject, paragraph, recipientButton, lastParagraph, language) + _sendMail(recipients[i], subject, paragraph, language, recipientButton, lastParagraph) } } @@ -51,9 +54,9 @@ async function _sendMail( recipient: IUser, subject: string, paragraph: string, - button: { text: string; link: string }, - lastParagraph: string, - language: Locale + language: Locale, + button?: { text: string; link: string }, + lastParagraph?: string ) { const mailClient = await getClient() const salutation = i18n.t('mail.hiX', { lng: language, X: recipient.name.givenName }) @@ -77,10 +80,7 @@ async function _sendMail( '\n\n' + paragraph + '\n\n' + - button.text + - ': ' + - button.link + - '\n\n' + + (button ? button.text + ': ' + button.link + '\n\n' : '') + lastParagraph + '\n\n' + regards + diff --git a/backend/retentionpolicy.ts b/backend/retentionpolicy.ts index 0306cd09..34ec9e0f 100644 --- a/backend/retentionpolicy.ts +++ b/backend/retentionpolicy.ts @@ -134,7 +134,7 @@ async function sendNotificationMails(report: ITravel | IExpenseReport | IHealthC const subject = i18n.t(`mail.${reportType}.${report.state}DeletedSoon.subject`, interpolation) const paragraph = i18n.t(`mail.${reportType}.${report.state}DeletedSoon.paragraph`, interpolation) - await sendMail(recipients, subject, paragraph, button, '') + await sendMail(recipients, subject, paragraph, button) } } } diff --git a/backend/templates/mail.ejs b/backend/templates/mail.ejs index 06ddc8c6..ba9186f3 100644 --- a/backend/templates/mail.ejs +++ b/backend/templates/mail.ejs @@ -350,6 +350,7 @@

<%= salutation %>

<%= paragraph %>

+ <% if (button) { %> @@ -365,6 +366,7 @@ + <% } %> <% if (lastParagraph) { %>

<%= lastParagraph %>

<% } %> From 059f9eb73ba9c056a2448c889bc26885f29fc638 Mon Sep 17 00:00:00 2001 From: david-loe <56305409+david-loe@users.noreply.github.com> Date: Mon, 11 Nov 2024 15:45:09 +0100 Subject: [PATCH 02/14] add send report via email settings --- backend/models/connectionSettings.ts | 1 + backend/models/organisation.ts | 5 +++-- common/locales/de.json | 2 ++ common/locales/en.json | 2 ++ common/types.ts | 3 +++ 5 files changed, 11 insertions(+), 2 deletions(-) diff --git a/backend/models/connectionSettings.ts b/backend/models/connectionSettings.ts index 98ce2477..58a80998 100644 --- a/backend/models/connectionSettings.ts +++ b/backend/models/connectionSettings.ts @@ -7,6 +7,7 @@ function requiredIf(ifPath: string) { } export const connectionSettingsSchema = new Schema({ + sendPDFReportsToOrganisationEmail: { type: Boolean, default: false, required: true }, smtp: { type: { host: { type: String, trim: true, required: true, label: 'Host', rules: requiredIf('smtp.user') }, diff --git a/backend/models/organisation.ts b/backend/models/organisation.ts index 9b9c2d6b..f8c3f493 100644 --- a/backend/models/organisation.ts +++ b/backend/models/organisation.ts @@ -1,9 +1,10 @@ -import { Document, Query, Schema, model } from 'mongoose' -import { Organisation } from '../../common/types.js' +import { Document, model, Query, Schema } from 'mongoose' +import { emailRegex, Organisation } from '../../common/types.js' export const organisationSchema = new Schema({ name: { type: String, trim: true, required: true }, subfolderPath: { type: String, trim: true, default: '' }, + reportEmail: { type: String, validate: emailRegex }, bankDetails: { type: String }, companyNumber: { type: String, trim: true }, logo: { type: Schema.Types.ObjectId, ref: 'DocumentFile' }, diff --git a/common/locales/de.json b/common/locales/de.json index 481c06ac..3f1aec93 100755 --- a/common/locales/de.json +++ b/common/locales/de.json @@ -97,6 +97,8 @@ "vehicleRegistration": "Lade hier den Fahrzeugschein deines Autos hoch." }, "labels": { + "reportEmail": "Email Adresse für Berichte", + "sendPDFReportsToOrganisationEmail": "PDF Berichte an Organisations-Email senden", "access": "Zugriffsrechte", "accessIcons": "Icons für Zugriffsrechte", "add": "Hinzufügen", diff --git a/common/locales/en.json b/common/locales/en.json index db5222fd..c804b7e0 100755 --- a/common/locales/en.json +++ b/common/locales/en.json @@ -97,6 +97,8 @@ "vehicleRegistration": "Upload the vehicle registration of your car here." }, "labels": { + "reportEmail": "Email Address for Reports", + "sendPDFReportsToOrganisationEmail": "Send PDF reports to organisation email", "access": "Access", "accessIcons": "Icons for access rights", "add": "Add", diff --git a/common/types.ts b/common/types.ts index 4576fc23..57ed47c8 100644 --- a/common/types.ts +++ b/common/types.ts @@ -74,11 +74,13 @@ export interface microsoftSettings { } export interface ConnectionSettings { + sendPDFReportsToOrganisationEmail: boolean auth: { microsoft?: microsoftSettings | null ldapauth?: ldapauthSettings | null } smtp?: smtpSettings | null + _id: _id } @@ -211,6 +213,7 @@ export interface ProjectWithUsers extends Project, ProjectUsers {} export interface Organisation extends OrganisationSimple { subfolderPath: string + reportEmail?: string | null bankDetails?: string | null companyNumber?: string | null logo?: DocumentFile | null From 7149b3003a27d362ced8dedc410f125065a4872a Mon Sep 17 00:00:00 2001 From: david-loe <56305409+david-loe@users.noreply.github.com> Date: Mon, 11 Nov 2024 16:00:39 +0100 Subject: [PATCH 03/14] use fs/promises --- backend/controller/uploadController.ts | 2 +- backend/mail/mail.ts | 4 ++-- backend/pdf/advance.ts | 4 ++-- backend/pdf/expenseReport.ts | 4 ++-- backend/pdf/healthCareCost.ts | 4 ++-- backend/pdf/travel.ts | 4 ++-- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/backend/controller/uploadController.ts b/backend/controller/uploadController.ts index c95e2a88..5d0488ab 100644 --- a/backend/controller/uploadController.ts +++ b/backend/controller/uploadController.ts @@ -1,6 +1,6 @@ import ejs from 'ejs' import { Request as ExRequest, Response as ExResponse, NextFunction } from 'express' -import fs from 'node:fs/promises' +import fs from 'fs/promises' import { Body, Consumes, Controller, Get, Middlewares, Post, Produces, Query, Request, Route, SuccessResponse } from 'tsoa' import { _id } from '../../common/types.js' import { getSettings } from '../db.js' diff --git a/backend/mail/mail.ts b/backend/mail/mail.ts index c0192b9c..f8602597 100644 --- a/backend/mail/mail.ts +++ b/backend/mail/mail.ts @@ -1,5 +1,5 @@ import ejs from 'ejs' -import fs from 'fs' +import fs from 'fs/promises' import nodemailer from 'nodemailer' import { ExpenseReportSimple, @@ -66,7 +66,7 @@ async function _sendMail( url: process.env.VITE_FRONTEND_URL } - const template = fs.readFileSync('./templates/mail.ejs', { encoding: 'utf-8' }) + const template = await fs.readFile('./templates/mail.ejs', { encoding: 'utf-8' }) const renderedHTML = ejs.render(template, { salutation, paragraph, diff --git a/backend/pdf/advance.ts b/backend/pdf/advance.ts index 42b6c56c..4d47d034 100644 --- a/backend/pdf/advance.ts +++ b/backend/pdf/advance.ts @@ -1,4 +1,4 @@ -import fs from 'fs' +import fs from 'fs/promises' import pdf_fontkit from 'pdf-fontkit' import pdf_lib from 'pdf-lib' import { Locale, Money, TravelSimple } from '../../common/types.js' @@ -9,7 +9,7 @@ export async function generateAdvanceReport(travel: TravelSimple, language: Loca formatter.setLocale(language) const pdfDoc = await pdf_lib.PDFDocument.create() pdfDoc.registerFontkit(pdf_fontkit) - const fontBytes = fs.readFileSync('../common/fonts/NotoSans-Regular.ttf') + const fontBytes = await fs.readFile('../common/fonts/NotoSans-Regular.ttf') const font = await pdfDoc.embedFont(fontBytes, { subset: true }) const edge = 36 const fontSize = 11 diff --git a/backend/pdf/expenseReport.ts b/backend/pdf/expenseReport.ts index 683fdf84..5e85a766 100644 --- a/backend/pdf/expenseReport.ts +++ b/backend/pdf/expenseReport.ts @@ -1,4 +1,4 @@ -import fs from 'fs' +import fs from 'fs/promises' import pdf_fontkit from 'pdf-fontkit' import pdf_lib from 'pdf-lib' import { addUp } from '../../common/scripts.js' @@ -21,7 +21,7 @@ export async function generateExpenseReportReport(expenseReport: ExpenseReport, formatter.setLocale(language) const pdfDoc = await pdf_lib.PDFDocument.create() pdfDoc.registerFontkit(pdf_fontkit) - const fontBytes = fs.readFileSync('../common/fonts/NotoSans-Regular.ttf') + const fontBytes = await fs.readFile('../common/fonts/NotoSans-Regular.ttf') const font = await pdfDoc.embedFont(fontBytes, { subset: true }) const edge = 36 const fontSize = 11 diff --git a/backend/pdf/healthCareCost.ts b/backend/pdf/healthCareCost.ts index 2a437996..05b04d3b 100644 --- a/backend/pdf/healthCareCost.ts +++ b/backend/pdf/healthCareCost.ts @@ -1,4 +1,4 @@ -import fs from 'fs' +import fs from 'fs/promises' import pdf_fontkit from 'pdf-fontkit' import pdf_lib from 'pdf-lib' import { addUp } from '../../common/scripts.js' @@ -21,7 +21,7 @@ export async function generateHealthCareCostReport(healthCareCost: HealthCareCos formatter.setLocale(language) const pdfDoc = await pdf_lib.PDFDocument.create() pdfDoc.registerFontkit(pdf_fontkit) - const fontBytes = fs.readFileSync('../common/fonts/NotoSans-Regular.ttf') + const fontBytes = await fs.readFile('../common/fonts/NotoSans-Regular.ttf') const font = await pdfDoc.embedFont(fontBytes, { subset: true }) const edge = 36 const fontSize = 11 diff --git a/backend/pdf/travel.ts b/backend/pdf/travel.ts index 8472c97b..a9b619f9 100644 --- a/backend/pdf/travel.ts +++ b/backend/pdf/travel.ts @@ -1,4 +1,4 @@ -import fs from 'fs' +import fs from 'fs/promises' import pdf_fontkit from 'pdf-fontkit' import pdf_lib from 'pdf-lib' import { addUp } from '../../common/scripts.js' @@ -39,7 +39,7 @@ export async function generateTravelReport(travel: Travel, language: Locale) { formatter.setLocale(language) const pdfDoc = await pdf_lib.PDFDocument.create() pdfDoc.registerFontkit(pdf_fontkit) - const fontBytes = fs.readFileSync('../common/fonts/NotoSans-Regular.ttf') + const fontBytes = await fs.readFile('../common/fonts/NotoSans-Regular.ttf') const font = await pdfDoc.embedFont(fontBytes, { subset: true }) const edge = 36 const fontSize = 11 From 750df77bd8d4f0230dcd15d922376f998de255bb Mon Sep 17 00:00:00 2001 From: david-loe <56305409+david-loe@users.noreply.github.com> Date: Mon, 11 Nov 2024 16:09:51 +0100 Subject: [PATCH 04/14] make sendMail truly awaitable --- backend/mail/mail.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/backend/mail/mail.ts b/backend/mail/mail.ts index f8602597..b1f06019 100644 --- a/backend/mail/mail.ts +++ b/backend/mail/mail.ts @@ -34,6 +34,7 @@ export async function sendMail( lastParagraph?: string, authenticateLink = true ) { + const mailPromises = [] for (let i = 0; i < recipients.length; i++) { const language = recipients[i].settings.language let recipientButton: { text: string; link: string } | undefined = undefined @@ -46,8 +47,9 @@ export async function sendMail( }) } } - _sendMail(recipients[i], subject, paragraph, language, recipientButton, lastParagraph) + mailPromises.push(_sendMail(recipients[i], subject, paragraph, language, recipientButton, lastParagraph)) } + return await Promise.allSettled(mailPromises) } async function _sendMail( @@ -89,7 +91,7 @@ async function _sendMail( ': ' + app.url - mailClient.sendMail({ + return await mailClient.sendMail({ from: '"' + app.name + '" <' + mailClient.options.from + '>', // sender address to: recipient.email, // list of receivers subject: subject, // Subject line From a336fa2534ab9b5d79ff428909e759d88d96a605 Mon Sep 17 00:00:00 2001 From: david-loe <56305409+david-loe@users.noreply.github.com> Date: Mon, 11 Nov 2024 17:33:30 +0100 Subject: [PATCH 05/14] send via mail --- backend/controller/expenseReportController.ts | 3 +- .../controller/healthCareCostController.ts | 4 +- backend/controller/travelController.ts | 14 ++--- backend/mail/mail.ts | 2 +- backend/models/connectionSettings.ts | 13 ++++- backend/models/displaySettings.ts | 3 +- backend/pdf/helper.ts | 57 +++++++++++++++++++ common/locales/de.json | 1 + common/locales/en.json | 1 + common/types.ts | 5 +- 10 files changed, 87 insertions(+), 16 deletions(-) diff --git a/backend/controller/expenseReportController.ts b/backend/controller/expenseReportController.ts index 62ab1b5e..072cca11 100644 --- a/backend/controller/expenseReportController.ts +++ b/backend/controller/expenseReportController.ts @@ -8,7 +8,7 @@ import { sendNotificationMail } from '../mail/mail.js' import ExpenseReport, { ExpenseReportDoc } from '../models/expenseReport.js' import User from '../models/user.js' import { generateExpenseReportReport } from '../pdf/expenseReport.js' -import { writeToDiskFilePath } from '../pdf/helper.js' +import { sendViaMail, writeToDiskFilePath } from '../pdf/helper.js' import { Controller, GetterQuery, SetterBody } from './controller.js' import { AuthorizationError, NotFoundError } from './error.js' import { IdDocument, MoneyPost } from './types.js' @@ -273,6 +273,7 @@ export class ExpenseReportExamineController extends Controller { const cb = async (expenseReport: IExpenseReport) => { sendNotificationMail(expenseReport) + sendViaMail(expenseReport) if (process.env.BACKEND_SAVE_REPORTS_ON_DISK.toLowerCase() === 'true') { await writeToDisk( await writeToDiskFilePath(expenseReport), diff --git a/backend/controller/healthCareCostController.ts b/backend/controller/healthCareCostController.ts index 468f349d..65be9d04 100644 --- a/backend/controller/healthCareCostController.ts +++ b/backend/controller/healthCareCostController.ts @@ -16,7 +16,7 @@ import HealthCareCost, { HealthCareCostDoc } from '../models/healthCareCost.js' import Organisation from '../models/organisation.js' import User from '../models/user.js' import { generateHealthCareCostReport } from '../pdf/healthCareCost.js' -import { writeToDiskFilePath } from '../pdf/helper.js' +import { sendViaMail, writeToDiskFilePath } from '../pdf/helper.js' import { Controller, GetterQuery, SetterBody } from './controller.js' import { AuthorizationError, NotAllowedError } from './error.js' import { IdDocument, MoneyPlusPost } from './types.js' @@ -251,6 +251,7 @@ export class HealthCareCostExamineController extends Controller { const cb = async (healthCareCost: IHealthCareCost) => { sendNotificationMail(healthCareCost) + sendViaMail(healthCareCost) if (process.env.BACKEND_SAVE_REPORTS_ON_DISK.toLowerCase() === 'true') { await writeToDisk( await writeToDiskFilePath(healthCareCost), @@ -371,6 +372,7 @@ export class HealthCareCostConfirmController extends Controller { const cb = async (healthCareCost: IHealthCareCost) => { sendNotificationMail(healthCareCost) + sendViaMail(healthCareCost) if (process.env.BACKEND_SAVE_REPORTS_ON_DISK.toLowerCase() === 'true') { await writeToDisk( await writeToDiskFilePath(healthCareCost), diff --git a/backend/controller/travelController.ts b/backend/controller/travelController.ts index e20a8b00..8cc7e02b 100644 --- a/backend/controller/travelController.ts +++ b/backend/controller/travelController.ts @@ -8,7 +8,7 @@ import { sendNotificationMail } from '../mail/mail.js' import Travel, { TravelDoc } from '../models/travel.js' import User from '../models/user.js' import { generateAdvanceReport } from '../pdf/advance.js' -import { writeToDiskFilePath } from '../pdf/helper.js' +import { sendViaMail, writeToDiskFilePath } from '../pdf/helper.js' import { generateTravelReport } from '../pdf/travel.js' import { Controller, GetterQuery, SetterBody } from './controller.js' import { AuthorizationError, NotAllowedError } from './error.js' @@ -278,12 +278,11 @@ export class TravelApproveController extends Controller { } const cb = async (travel: ITravel) => { sendNotificationMail(travel) - if ( - travel.advance.amount !== null && - travel.advance.amount > 0 && - process.env.BACKEND_SAVE_REPORTS_ON_DISK.toLowerCase() === 'true' - ) { - await writeToDisk(await writeToDiskFilePath(travel), await generateAdvanceReport(travel, i18n.language as Locale)) + if (travel.advance.amount !== null && travel.advance.amount > 0) { + sendViaMail(travel) + if (process.env.BACKEND_SAVE_REPORTS_ON_DISK.toLowerCase() === 'true') { + await writeToDisk(await writeToDiskFilePath(travel), await generateAdvanceReport(travel, i18n.language as Locale)) + } } } @@ -448,6 +447,7 @@ export class TravelExamineController extends Controller { const cb = async (travel: ITravel) => { sendNotificationMail(travel) + sendViaMail(travel) if (process.env.BACKEND_SAVE_REPORTS_ON_DISK.toLowerCase() === 'true') { await writeToDisk(await writeToDiskFilePath(travel), await generateTravelReport(travel, i18n.language as Locale)) } diff --git a/backend/mail/mail.ts b/backend/mail/mail.ts index b1f06019..c060fe7b 100644 --- a/backend/mail/mail.ts +++ b/backend/mail/mail.ts @@ -17,7 +17,7 @@ import i18n, { formatter } from '../i18n.js' import User from '../models/user.js' import { mapSmtpConfig } from '../settingsValidator.js' -async function getClient() { +export async function getClient() { const connectionSettings = await getConnectionSettings() if (connectionSettings.smtp?.host) { return nodemailer.createTransport(mapSmtpConfig(connectionSettings.smtp)) diff --git a/backend/models/connectionSettings.ts b/backend/models/connectionSettings.ts index 58a80998..54ffc040 100644 --- a/backend/models/connectionSettings.ts +++ b/backend/models/connectionSettings.ts @@ -1,13 +1,20 @@ import { HydratedDocument, model, Schema } from 'mongoose' -import { ConnectionSettings, emailRegex } from '../../common/types.js' +import { ConnectionSettings, emailRegex, locales } from '../../common/types.js' import { verifyLdapauthConfig, verifySmtpConfig } from '../settingsValidator.js' function requiredIf(ifPath: string) { - return [{ required: [ifPath, 'not_in', [null, '']] }, { nullable: [ifPath, 'in', [null, '']] }] + return [{ required: [ifPath, 'not_in', [null, '', false]] }, { nullable: [ifPath, 'in', [null, '', false]] }] } export const connectionSettingsSchema = new Schema({ - sendPDFReportsToOrganisationEmail: { type: Boolean, default: false, required: true }, + PDFReportsViaEmail: { + type: { + sendPDFReportsToOrganisationEmail: { type: Boolean, default: false, required: true }, + locale: { type: String, enum: locales, required: true } + }, + required: true, + label: 'PDF via Email' + }, smtp: { type: { host: { type: String, trim: true, required: true, label: 'Host', rules: requiredIf('smtp.user') }, diff --git a/backend/models/displaySettings.ts b/backend/models/displaySettings.ts index f1b09b81..5dc3a414 100644 --- a/backend/models/displaySettings.ts +++ b/backend/models/displaySettings.ts @@ -30,8 +30,7 @@ export const displaySettingsSchema = new Schema( fallback: { type: String, enum: locales, required: true }, overwrite: { type: overwrites, required: true, description: 'description.keyFullIdentifier' } }, - required: true, - label: 'Sprache' + required: true } }, { minimize: false, toObject: { minimize: false }, toJSON: { minimize: false } } diff --git a/backend/pdf/helper.ts b/backend/pdf/helper.ts index d0e2a153..e5e2c69b 100644 --- a/backend/pdf/helper.ts +++ b/backend/pdf/helper.ts @@ -14,9 +14,15 @@ import { reportIsHealthCareCost, reportIsTravel } from '../../common/types.js' +import { getConnectionSettings } from '../db.js' import i18n, { formatter } from '../i18n.js' +import { getClient } from '../mail/mail.js' import DocumentFile from '../models/documentFile.js' import Organisation from '../models/organisation.js' +import { generateAdvanceReport } from './advance.js' +import { generateExpenseReportReport } from './expenseReport.js' +import { generateHealthCareCostReport } from './healthCareCost.js' +import { generateTravelReport } from './travel.js' export async function writeToDiskFilePath(report: Travel | ExpenseReport | HealthCareCost): Promise { let path = '/reports/' @@ -43,6 +49,57 @@ export async function writeToDiskFilePath(report: Travel | ExpenseReport | Healt return path } +export async function sendViaMail(report: Travel | ExpenseReport | HealthCareCost) { + const connectionSettings = await getConnectionSettings() + if (connectionSettings.PDFReportsViaEmail.sendPDFReportsToOrganisationEmail) { + const org = await Organisation.findOne({ _id: report.project.organisation._id }) + if (org?.reportEmail) { + const mailClient = await getClient() + const lng = connectionSettings.PDFReportsViaEmail.locale + let subject = '🧾 ' + let pdf: Uint8Array + if (reportIsTravel(report)) { + if (report.state == 'refunded') { + subject = subject + i18n.t('labels.travel', { lng }) + pdf = await generateTravelReport(report, lng) + } else { + subject = subject + i18n.t('labels.advance', { lng }) + pdf = await generateAdvanceReport(report, lng) + } + } else if (reportIsHealthCareCost(report)) { + subject = subject + i18n.t('labels.healthCareCost', { lng }) + pdf = await generateHealthCareCostReport(report, lng) + } else { + subject = subject + i18n.t('labels.expenseReport', { lng }) + pdf = await generateExpenseReportReport(report, lng) + } + const appName = i18n.t('headlines.title', { lng }) + ' ' + i18n.t('headlines.emoji', { lng }) + formatter.setLocale(i18n.language as Locale) + const totalSum = formatter.money(addUp(report).total) + + const text = + `${i18n.t('labels.project', { lng })}: ${report.project.identifier}\n` + + `${i18n.t('labels.name', { lng })}: ${report.name}\n` + + `${i18n.t('labels.owner', { lng })}: ${report.owner.name.givenName} ${report.owner.name.familyName}\n` + + `${i18n.t('labels.total', { lng })}: ${totalSum}\n` + + return await mailClient.sendMail({ + from: '"' + appName + '" <' + mailClient.options.from + '>', // sender address + to: org?.reportEmail, // list of receivers + subject: subject, // Subject line + text, + attachments: [ + { + content: Buffer.from(pdf), + contentType: 'application/pdf', + filename: 'report.pdf' + } + ] + }) + } + } +} + export interface Options { font: pdf_lib.PDFFont fontSize: number diff --git a/common/locales/de.json b/common/locales/de.json index 3f1aec93..57a4ddaf 100755 --- a/common/locales/de.json +++ b/common/locales/de.json @@ -97,6 +97,7 @@ "vehicleRegistration": "Lade hier den Fahrzeugschein deines Autos hoch." }, "labels": { + "locale": "Sprache", "reportEmail": "Email Adresse für Berichte", "sendPDFReportsToOrganisationEmail": "PDF Berichte an Organisations-Email senden", "access": "Zugriffsrechte", diff --git a/common/locales/en.json b/common/locales/en.json index c804b7e0..11f91d9d 100755 --- a/common/locales/en.json +++ b/common/locales/en.json @@ -97,6 +97,7 @@ "vehicleRegistration": "Upload the vehicle registration of your car here." }, "labels": { + "locale": "Language", "reportEmail": "Email Address for Reports", "sendPDFReportsToOrganisationEmail": "Send PDF reports to organisation email", "access": "Access", diff --git a/common/types.ts b/common/types.ts index 57ed47c8..17d49611 100644 --- a/common/types.ts +++ b/common/types.ts @@ -74,7 +74,10 @@ export interface microsoftSettings { } export interface ConnectionSettings { - sendPDFReportsToOrganisationEmail: boolean + PDFReportsViaEmail: { + sendPDFReportsToOrganisationEmail: boolean + locale: Locale + } auth: { microsoft?: microsoftSettings | null ldapauth?: ldapauthSettings | null From a2bd9e68605d15b41f97800d638f8de9440641ce Mon Sep 17 00:00:00 2001 From: david-loe <56305409+david-loe@users.noreply.github.com> Date: Wed, 20 Nov 2024 14:09:42 +0100 Subject: [PATCH 06/14] add migration --- backend/data/connectionSettings.development.json | 4 ++++ backend/db.ts | 6 +++--- backend/migrations.ts | 10 ++++++++++ 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/backend/data/connectionSettings.development.json b/backend/data/connectionSettings.development.json index 4427a543..2fc98f45 100644 --- a/backend/data/connectionSettings.development.json +++ b/backend/data/connectionSettings.development.json @@ -22,5 +22,9 @@ "user": "username", "password": "password", "senderAddress": "info@abrechnung.com" + }, + "PDFReportsViaEmail": { + "sendPDFReportsToOrganisationEmail": false, + "locale": "de" } } \ No newline at end of file diff --git a/backend/db.ts b/backend/db.ts index 2c42e60e..5059daa9 100644 --- a/backend/db.ts +++ b/backend/db.ts @@ -56,12 +56,12 @@ export async function initDB() { } if (process.env.NODE_ENV === 'development') { - await initer(ConnectionSettings, 'connectionSettings', [connectionSettingsDevelopment], true) + await initer(ConnectionSettings, 'connectionSettings', [connectionSettingsDevelopment as Partial], true) } else { - const emtpyConnectionSettings: Partial = { + const emptyConnectionSettings: Partial = { auth: {} } - await initer(ConnectionSettings, 'connectionSettings', [emtpyConnectionSettings]) + await initer(ConnectionSettings, 'connectionSettings', [emptyConnectionSettings]) } await initer(DisplaySettings, 'displaySettings', [displaySettings as Partial]) diff --git a/backend/migrations.ts b/backend/migrations.ts index 2312e298..e4b42f95 100644 --- a/backend/migrations.ts +++ b/backend/migrations.ts @@ -124,6 +124,16 @@ export async function checkForMigrations() { } await mongoose.connection.collection('displaysettings').updateOne({}, { $set: displaySettingsFromEnv }) } + if (semver.lte(migrateFrom, '1.4.1')) { + console.log('Apply migration from v1.4.1: Add PDFReportsViaEmail Settings') + const displaySettings = await mongoose.connection.collection('displaysettings').findOne({}) + await mongoose.connection + .collection('connectionsettings') + .updateOne( + {}, + { $set: { PDFReportsViaEmail: { sendPDFReportsToOrganisationEmail: false, locale: displaySettings?.locale?.default || 'de' } } } + ) + } if (settings) { settings.migrateFrom = undefined await settings.save() From db7e9b13fb9f614c7179304896920dbd88d99400 Mon Sep 17 00:00:00 2001 From: david-loe <56305409+david-loe@users.noreply.github.com> Date: Wed, 20 Nov 2024 17:08:16 +0100 Subject: [PATCH 07/14] ctrl + s => save settings --- .../elements/ConnectionSettingsForm.vue | 12 +++++++++++ .../settings/elements/DisplaySettingsForm.vue | 21 ++++++++++++++++++- .../settings/elements/SettingsForm.vue | 12 +++++++++++ 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/settings/elements/ConnectionSettingsForm.vue b/frontend/src/components/settings/elements/ConnectionSettingsForm.vue index 58d7c036..557cc457 100644 --- a/frontend/src/components/settings/elements/ConnectionSettingsForm.vue +++ b/frontend/src/components/settings/elements/ConnectionSettingsForm.vue @@ -30,9 +30,21 @@ export default defineComponent({ this.connectionSettings = result.ok ;(this.$refs.form$ as any).load(this.connectionSettings) } + }, + ctrlS(event: KeyboardEvent) { + if (event.ctrlKey && event.key === 's') { + event.preventDefault() + if (!event.repeat && this.$refs.form$) { + this.postConnectionSettings((this.$refs.form$ as any).data) + } + } } }, + beforeDestroy() { + document.removeEventListener('keydown', this.ctrlS) + }, async mounted() { + document.addEventListener('keydown', this.ctrlS) await this.$root.load() this.schema = Object.assign({}, (await this.$root.getter('admin/connectionSettings/form')).ok?.data, { buttons: { diff --git a/frontend/src/components/settings/elements/DisplaySettingsForm.vue b/frontend/src/components/settings/elements/DisplaySettingsForm.vue index 95451842..30eb7b31 100644 --- a/frontend/src/components/settings/elements/DisplaySettingsForm.vue +++ b/frontend/src/components/settings/elements/DisplaySettingsForm.vue @@ -21,15 +21,34 @@ export default defineComponent({ this.displaySettings = result.ok ;(this.$refs.form$ as any).load(this.displaySettings) } + }, + ctrlS(event: KeyboardEvent) { + if (event.ctrlKey && event.key === 's') { + event.preventDefault() + if (!event.repeat && this.$refs.form$) { + this.postDisplaySettings((this.$refs.form$ as any).data) + } + } } }, + beforeDestroy() { + document.removeEventListener('keydown', this.ctrlS) + }, async mounted() { + document.addEventListener('keydown', this.ctrlS) await this.$root.load() this.schema = Object.assign({}, (await this.$root.getter('admin/displaySettings/form')).ok?.data, { buttons: { type: 'group', schema: { - submit: { type: 'button', submits: true, buttonLabel: this.$t('labels.save'), full: true, columns: { container: 6 } } + submit: { + type: 'button', + submits: true, + buttonLabel: this.$t('labels.save'), + full: true, + columns: { container: 6 }, + id: 'submit-button' + } } }, _id: { type: 'hidden', meta: true } diff --git a/frontend/src/components/settings/elements/SettingsForm.vue b/frontend/src/components/settings/elements/SettingsForm.vue index 50146004..1a0688f6 100644 --- a/frontend/src/components/settings/elements/SettingsForm.vue +++ b/frontend/src/components/settings/elements/SettingsForm.vue @@ -20,9 +20,21 @@ export default defineComponent({ this.$root.settings = result.ok ;(this.$refs.form$ as any).load(this.$root.settings) } + }, + ctrlS(event: KeyboardEvent) { + if (event.ctrlKey && event.key === 's') { + event.preventDefault() + if (!event.repeat && this.$refs.form$) { + this.postSettings((this.$refs.form$ as any).data) + } + } } }, + beforeDestroy() { + document.removeEventListener('keydown', this.ctrlS) + }, async mounted() { + document.addEventListener('keydown', this.ctrlS) await this.$root.load() this.schema = Object.assign({}, (await this.$root.getter('admin/settings/form')).ok?.data, { buttons: { From 3de1e832ff713902dffaf84f386520e1d2b634f3 Mon Sep 17 00:00:00 2001 From: david-loe <56305409+david-loe@users.noreply.github.com> Date: Wed, 20 Nov 2024 17:13:30 +0100 Subject: [PATCH 08/14] organize organisations --- backend/models/organisation.ts | 4 ++-- .../src/components/settings/elements/OrganisationList.vue | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/models/organisation.ts b/backend/models/organisation.ts index f8c3f493..e144200d 100644 --- a/backend/models/organisation.ts +++ b/backend/models/organisation.ts @@ -3,12 +3,12 @@ import { emailRegex, Organisation } from '../../common/types.js' export const organisationSchema = new Schema({ name: { type: String, trim: true, required: true }, - subfolderPath: { type: String, trim: true, default: '' }, reportEmail: { type: String, validate: emailRegex }, + website: { type: String }, bankDetails: { type: String }, companyNumber: { type: String, trim: true }, logo: { type: Schema.Types.ObjectId, ref: 'DocumentFile' }, - website: { type: String } + subfolderPath: { type: String, trim: true, default: '' } }) function populate(doc: Document) { diff --git a/frontend/src/components/settings/elements/OrganisationList.vue b/frontend/src/components/settings/elements/OrganisationList.vue index 6b281b90..15a6201a 100644 --- a/frontend/src/components/settings/elements/OrganisationList.vue +++ b/frontend/src/components/settings/elements/OrganisationList.vue @@ -15,7 +15,7 @@ ]" :headers="[ { text: $t('labels.name'), value: 'name' }, - { text: $t('labels.subfolderPath'), value: 'subfolderPath' }, + { text: $t('labels.reportEmail'), value: 'reportEmail' }, { text: $t('labels.website'), value: 'website' }, { value: 'buttons' } ]"> From 56bd3d30f150ecf2f6edc968ab9318a3bbcb2a52 Mon Sep 17 00:00:00 2001 From: david-loe <56305409+david-loe@users.noreply.github.com> Date: Wed, 20 Nov 2024 17:38:15 +0100 Subject: [PATCH 09/14] adding default locale --- backend/db.ts | 5 +---- backend/models/connectionSettings.ts | 8 +++++--- backend/models/displaySettings.ts | 6 +++--- common/types.ts | 1 + frontend/src/formatter.ts | 3 ++- frontend/src/i18n.ts | 16 +++++++++------- frontend/src/vueform.config.ts | 3 ++- frontend/vue.config.ts | 11 ----------- 8 files changed, 23 insertions(+), 30 deletions(-) diff --git a/backend/db.ts b/backend/db.ts index 5059daa9..df91d729 100644 --- a/backend/db.ts +++ b/backend/db.ts @@ -58,10 +58,7 @@ export async function initDB() { if (process.env.NODE_ENV === 'development') { await initer(ConnectionSettings, 'connectionSettings', [connectionSettingsDevelopment as Partial], true) } else { - const emptyConnectionSettings: Partial = { - auth: {} - } - await initer(ConnectionSettings, 'connectionSettings', [emptyConnectionSettings]) + await initer(ConnectionSettings, 'connectionSettings', [{} as Partial]) } await initer(DisplaySettings, 'displaySettings', [displaySettings as Partial]) diff --git a/backend/models/connectionSettings.ts b/backend/models/connectionSettings.ts index 54ffc040..6d27458f 100644 --- a/backend/models/connectionSettings.ts +++ b/backend/models/connectionSettings.ts @@ -1,5 +1,5 @@ import { HydratedDocument, model, Schema } from 'mongoose' -import { ConnectionSettings, emailRegex, locales } from '../../common/types.js' +import { ConnectionSettings, defaultLocale, emailRegex, locales } from '../../common/types.js' import { verifyLdapauthConfig, verifySmtpConfig } from '../settingsValidator.js' function requiredIf(ifPath: string) { @@ -10,9 +10,10 @@ export const connectionSettingsSchema = new Schema({ PDFReportsViaEmail: { type: { sendPDFReportsToOrganisationEmail: { type: Boolean, default: false, required: true }, - locale: { type: String, enum: locales, required: true } + locale: { type: String, enum: locales, required: true, default: defaultLocale } }, required: true, + default: () => ({}), label: 'PDF via Email' }, smtp: { @@ -101,7 +102,8 @@ export const connectionSettingsSchema = new Schema({ label: 'LDAP' } }, - required: true + required: true, + default: () => ({}) } }) diff --git a/backend/models/displaySettings.ts b/backend/models/displaySettings.ts index 5dc3a414..b591a6f2 100644 --- a/backend/models/displaySettings.ts +++ b/backend/models/displaySettings.ts @@ -1,5 +1,5 @@ import { model, Schema } from 'mongoose' -import { DisplaySettings, Locale, locales } from '../../common/types.js' +import { defaultLocale, DisplaySettings, Locale, locales } from '../../common/types.js' const overwrites = {} as { [key in Locale]: { type: typeof Schema.Types.Mixed; required: true; default: () => object } } for (const locale of locales) { @@ -26,8 +26,8 @@ export const displaySettingsSchema = new Schema( }, locale: { type: { - default: { type: String, enum: locales, required: true }, - fallback: { type: String, enum: locales, required: true }, + default: { type: String, enum: locales, required: true, default: defaultLocale }, + fallback: { type: String, enum: locales, required: true, default: defaultLocale }, overwrite: { type: overwrites, required: true, description: 'description.keyFullIdentifier' } }, required: true diff --git a/common/types.ts b/common/types.ts index 17d49611..43903b75 100644 --- a/common/types.ts +++ b/common/types.ts @@ -541,3 +541,4 @@ export const baseCurrency: Currency = { subunit: 'Cent', symbol: '€' } +export const defaultLocale: Locale = 'de' diff --git a/frontend/src/formatter.ts b/frontend/src/formatter.ts index 6045c3eb..22db7a36 100644 --- a/frontend/src/formatter.ts +++ b/frontend/src/formatter.ts @@ -1,9 +1,10 @@ import { App, Plugin } from 'vue' import Formatter from '../../common/formatter' +import { defaultLocale } from '../../common/types' const FormatterPlugin: Plugin = { install(app: App, options: any) { - app.config.globalProperties.$formatter = new Formatter('de') + app.config.globalProperties.$formatter = new Formatter(defaultLocale) } } diff --git a/frontend/src/i18n.ts b/frontend/src/i18n.ts index f2379085..f2f2294b 100644 --- a/frontend/src/i18n.ts +++ b/frontend/src/i18n.ts @@ -1,15 +1,17 @@ import { createI18n } from 'vue-i18n' -import { Locale, locales } from '../../common/types.js' +import de from '../../common/locales/de.json' with { type: 'json' } +import en from '../../common/locales/en.json' with { type: 'json' } +import { defaultLocale, Locale } from '../../common/types.js' -const emptyMessages = {} as { [key in Locale]: {} } -for (const locale of locales) { - emptyMessages[locale] = {} +const defaultMessages = { + de, + en } export default createI18n({ legacy: false, - locale: 'de', - fallbackLocale: 'de', - messages: emptyMessages, + locale: defaultLocale, + fallbackLocale: defaultLocale, + messages: defaultMessages, globalInjection: true }) diff --git a/frontend/src/vueform.config.ts b/frontend/src/vueform.config.ts index 7352662e..6a0620f7 100644 --- a/frontend/src/vueform.config.ts +++ b/frontend/src/vueform.config.ts @@ -3,6 +3,7 @@ import vueform from '@vueform/vueform/dist/vueform' import de from '@vueform/vueform/locales/de' import en from '@vueform/vueform/locales/en' +import { defaultLocale } from '../../common/types' import CountryElement from './components/elements/vueform/CountryElement.vue' import CurrencyElement from './components/elements/vueform/CurrencyElement.vue' import DocumentfileElement from './components/elements/vueform/DocumentfileElement.vue' @@ -38,7 +39,7 @@ export default defineConfig({ MixedElement ], locales: { de, en }, - locale: 'de', + locale: defaultLocale, env: import.meta.env.MODE, displayErrors: false, displayMessages: false, diff --git a/frontend/vue.config.ts b/frontend/vue.config.ts index c1a2fe1b..8ff0ff82 100644 --- a/frontend/vue.config.ts +++ b/frontend/vue.config.ts @@ -4,16 +4,5 @@ export default { client: { webSocketURL: 'auto://0.0.0.0:0/ws' } - }, - pluginOptions: { - i18n: { - locale: 'en', - fallbackLocale: 'en', - localeDir: 'locales', - enableLegacy: false, - runtimeOnly: false, - compositionOnly: false, - fullInstall: true - } } } From 7da4905c43eff5452146afec689305caa1e6f429 Mon Sep 17 00:00:00 2001 From: david-loe <56305409+david-loe@users.noreply.github.com> Date: Wed, 20 Nov 2024 17:42:03 +0100 Subject: [PATCH 10/14] deprecate BACKEND_SAVE_REPORTS_ON_DISK --- .env.example | 1 + deploy-compose.yml | 5 ----- docker-compose.yml | 4 ---- 3 files changed, 1 insertion(+), 9 deletions(-) diff --git a/.env.example b/.env.example index ed60181b..40b54492 100644 --- a/.env.example +++ b/.env.example @@ -35,5 +35,6 @@ RATE_LIMIT= # TRUE | number | string (https://expressjs.com/en/guide/behind-proxies.html) use BACKEND_URL/ip to validate TRUST_PROXY= +# ⚠️Deprecated⚠️ Use send via mail under connection settings # If set to 'TRUE', all reports will be saved to `/reports` in the backend container. Uncomment the corresponding backend volume in `docker-compose.yml` to get reports on host maschine BACKEND_SAVE_REPORTS_ON_DISK=FALSE diff --git a/deploy-compose.yml b/deploy-compose.yml index cb9bf30f..c02e66fa 100644 --- a/deploy-compose.yml +++ b/deploy-compose.yml @@ -4,11 +4,6 @@ services: restart: always depends_on: - db - # volumes: - # - ./reports/travel:/reports/travel #BACKEND_SAVE_REPORTS_ON_DISK - # - ./reports/expenseReport:/reports/expenseReport #BACKEND_SAVE_REPORTS_ON_DISK - # - ./reports/advance:/reports/advance #BACKEND_SAVE_REPORTS_ON_DISK - # - ./reports/healthCareCost:/reports/healthCareCost #BACKEND_SAVE_REPORTS_ON_DISK ports: - ${BACKEND_PORT}:${BACKEND_PORT} env_file: diff --git a/docker-compose.yml b/docker-compose.yml index 1279ebba..913f258c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,10 +9,6 @@ services: volumes: - ./backend:/app - ./common:/common - # - ./reports/travel:/reports/travel #BACKEND_SAVE_REPORTS_ON_DISK - # - ./reports/expenseReport:/reports/expenseReport #BACKEND_SAVE_REPORTS_ON_DISK - # - ./reports/advance:/reports/advance #BACKEND_SAVE_REPORTS_ON_DISK - # - ./reports/healthCareCost:/reports/healthCareCost #BACKEND_SAVE_REPORTS_ON_DISK ports: - ${BACKEND_PORT}:${BACKEND_PORT} - ${BACKEND_DEBUG_PORT}:${BACKEND_DEBUG_PORT} From aed64db16f7e36c8ae50ef807a883cf3184ba7fa Mon Sep 17 00:00:00 2001 From: david-loe <56305409+david-loe@users.noreply.github.com> Date: Thu, 21 Nov 2024 09:38:23 +0100 Subject: [PATCH 11/14] add caching for templates --- backend/controller/uploadController.ts | 4 ++-- backend/mail/mail.ts | 4 ++-- backend/templates/cache.ts | 18 ++++++++++++++++++ 3 files changed, 22 insertions(+), 4 deletions(-) create mode 100644 backend/templates/cache.ts diff --git a/backend/controller/uploadController.ts b/backend/controller/uploadController.ts index 5d0488ab..7bff1ba0 100644 --- a/backend/controller/uploadController.ts +++ b/backend/controller/uploadController.ts @@ -1,6 +1,5 @@ import ejs from 'ejs' import { Request as ExRequest, Response as ExResponse, NextFunction } from 'express' -import fs from 'fs/promises' import { Body, Consumes, Controller, Get, Middlewares, Post, Produces, Query, Request, Route, SuccessResponse } from 'tsoa' import { _id } from '../../common/types.js' import { getSettings } from '../db.js' @@ -8,6 +7,7 @@ import { documentFileHandler, fileHandler } from '../helper.js' import i18n from '../i18n.js' import Token from '../models/token.js' import User from '../models/user.js' +import { getUploadTemplate } from '../templates/cache.js' import { AuthorizationError, NotFoundError } from './error.js' import { File } from './types.js' @@ -33,7 +33,7 @@ export class UploadController extends Controller { ): Promise { const settings = await getSettings() const user = await User.findOne({ _id: userId }).lean() - const template = await fs.readFile('./templates/upload.ejs', { encoding: 'utf-8' }) + const template = await getUploadTemplate() const url = new URL(process.env.VITE_BACKEND_URL + '/upload/new') url.searchParams.append('userId', userId) url.searchParams.append('tokenId', tokenId) diff --git a/backend/mail/mail.ts b/backend/mail/mail.ts index c060fe7b..3d595edf 100644 --- a/backend/mail/mail.ts +++ b/backend/mail/mail.ts @@ -1,5 +1,4 @@ import ejs from 'ejs' -import fs from 'fs/promises' import nodemailer from 'nodemailer' import { ExpenseReportSimple, @@ -16,6 +15,7 @@ import { genAuthenticatedLink } from '../helper.js' import i18n, { formatter } from '../i18n.js' import User from '../models/user.js' import { mapSmtpConfig } from '../settingsValidator.js' +import { getMailTemplate } from '../templates/cache.js' export async function getClient() { const connectionSettings = await getConnectionSettings() @@ -68,7 +68,7 @@ async function _sendMail( url: process.env.VITE_FRONTEND_URL } - const template = await fs.readFile('./templates/mail.ejs', { encoding: 'utf-8' }) + const template = await getMailTemplate() const renderedHTML = ejs.render(template, { salutation, paragraph, diff --git a/backend/templates/cache.ts b/backend/templates/cache.ts new file mode 100644 index 00000000..8b883331 --- /dev/null +++ b/backend/templates/cache.ts @@ -0,0 +1,18 @@ +import fs from 'fs/promises' + +let mail: string | null = null +let upload: string | null = null + +export async function getMailTemplate() { + if (!mail) { + mail = await fs.readFile('./templates/mail.ejs', { encoding: 'utf-8' }) + } + return mail +} + +export async function getUploadTemplate() { + if (!upload) { + upload = await fs.readFile('./templates/upload.ejs', { encoding: 'utf-8' }) + } + return upload +} From 7f3984b83a3339bbcde765f8b1ba8e11e0c72a7f Mon Sep 17 00:00:00 2001 From: david-loe <56305409+david-loe@users.noreply.github.com> Date: Thu, 21 Nov 2024 09:42:30 +0100 Subject: [PATCH 12/14] use consistent language --- backend/pdf/helper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/pdf/helper.ts b/backend/pdf/helper.ts index e5e2c69b..d96cff92 100644 --- a/backend/pdf/helper.ts +++ b/backend/pdf/helper.ts @@ -74,7 +74,7 @@ export async function sendViaMail(report: Travel | ExpenseReport | HealthCareCos pdf = await generateExpenseReportReport(report, lng) } const appName = i18n.t('headlines.title', { lng }) + ' ' + i18n.t('headlines.emoji', { lng }) - formatter.setLocale(i18n.language as Locale) + formatter.setLocale(lng) const totalSum = formatter.money(addUp(report).total) const text = From 295bc729cdf24bcddb1d2ee82f39c36a601764b8 Mon Sep 17 00:00:00 2001 From: david-loe <56305409+david-loe@users.noreply.github.com> Date: Thu, 21 Nov 2024 10:05:42 +0100 Subject: [PATCH 13/14] refactor keydown --- .../elements/ConnectionSettingsForm.vue | 20 +++++++------------ .../settings/elements/DisplaySettingsForm.vue | 19 ++++++------------ .../settings/elements/SettingsForm.vue | 19 ++++++------------ 3 files changed, 19 insertions(+), 39 deletions(-) diff --git a/frontend/src/components/settings/elements/ConnectionSettingsForm.vue b/frontend/src/components/settings/elements/ConnectionSettingsForm.vue index 557cc457..85aaf41d 100644 --- a/frontend/src/components/settings/elements/ConnectionSettingsForm.vue +++ b/frontend/src/components/settings/elements/ConnectionSettingsForm.vue @@ -1,5 +1,10 @@