feat(admin): eigene Vorlage fuer die Willkommensmail mit Platzhaltern, Vorschau und Testmail
Administrator -> Willkommensmail: Betreff, Ueberschrift, Einleitung, Abschluss je
Mandant (Tabelle WelcomeMailTemplate, RLS je Mandant, Migration 20260930150000);
Platzhalter {{name}} {{vorname}} {{benutzername}} {{email}} {{adresse}} {{firma}},
unbekannte -> 400 bzw. Hinweis beim Tippen; Werte escaped, Vorlage reiner Text.
Live-Vorschau per API gerendert, Testmail an die eigene Adresse ohne Token,
Zuruecksetzen auf Standard. Feste Bausteine (Kopf, Zugangsdaten, Anmeldehinweis,
Knoepfe, Fusszeile) bleiben immer drin.
Kopf: Wellenzelle dunkel statt weiss, Streifen 600x40, Inhalt 24 px naeher –
keine weisse Luecke, wenn OWA das CID-Bild nicht zeigt.
Lokal nachgewiesen: Hinweis/Sperre bei {{xyz}}, Speichern, Testmail (Link nur
/login), echte Mail mit eigener Vorlage und 7-Tage-Link.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { DEFAULT_WELCOME_MAIL_TEXTS, type WelcomeMailTexts } from '@tessera/shared';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { forTenant } from '../prisma/prisma-tenant.extension';
|
||||
|
||||
/**
|
||||
* WelcomeMailTemplateService — eigene Vorlage der Willkommensmail
|
||||
* (Administrator → Willkommensmail).
|
||||
*
|
||||
* Hoechstens eine Vorlage je Mandant (Tabelle "WelcomeMailTemplate",
|
||||
* `tenantId` eindeutig). Gespeichert werden nur die vier Texte; Kopf,
|
||||
* Zugangsdaten, Anmeldehinweis, Knoepfe und Fusszeile bleiben fest in
|
||||
* `welcome-mail.template.ts`. Fehlt die Vorlage, gelten
|
||||
* `DEFAULT_WELCOME_MAIL_TEXTS` aus `@tessera/shared`.
|
||||
*
|
||||
* Mandantenbindung: jeder Zugriff laeuft ueber `forTenant(prisma, tenantId)`.
|
||||
* Beim Versand ist `tenantId` der Mandant des ZIEL-Benutzers
|
||||
* (`WelcomeMailService.send`), in der Verwaltung der Mandant des
|
||||
* angemeldeten Administrators (`WelcomeMailTemplateController`). Die
|
||||
* Platzhalter-Pruefung liegt im DTO (400 bei unbekanntem Platzhalter).
|
||||
*/
|
||||
|
||||
/** Antwort von GET /welcome-mail-template. */
|
||||
export interface WelcomeMailTemplateState {
|
||||
/** `true` = eigene Vorlage gespeichert, `false` = Standardtexte. */
|
||||
custom: boolean;
|
||||
/** Wirksame Texte (eigene Vorlage oder Standard) — Vorbelegung im Formular. */
|
||||
texts: WelcomeMailTexts;
|
||||
/** Standardtexte fuer "Auf Standard zuruecksetzen". */
|
||||
defaults: WelcomeMailTexts;
|
||||
updatedAt: Date | null;
|
||||
updatedBy: string | null;
|
||||
}
|
||||
|
||||
const TEXT_SELECT = {
|
||||
subject: true,
|
||||
heading: true,
|
||||
intro: true,
|
||||
closing: true,
|
||||
updatedAt: true,
|
||||
updatedBy: true,
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export class WelcomeMailTemplateService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async getState(tenantId: string): Promise<WelcomeMailTemplateState> {
|
||||
const tenantPrisma = forTenant(this.prisma, tenantId);
|
||||
const row = await tenantPrisma.welcomeMailTemplate.findUnique({
|
||||
where: { tenantId },
|
||||
select: TEXT_SELECT,
|
||||
});
|
||||
const defaults = { ...DEFAULT_WELCOME_MAIL_TEXTS };
|
||||
if (!row) {
|
||||
return { custom: false, texts: defaults, defaults, updatedAt: null, updatedBy: null };
|
||||
}
|
||||
return {
|
||||
custom: true,
|
||||
texts: { subject: row.subject, heading: row.heading, intro: row.intro, closing: row.closing },
|
||||
defaults,
|
||||
updatedAt: row.updatedAt,
|
||||
updatedBy: row.updatedBy,
|
||||
};
|
||||
}
|
||||
|
||||
/** Texte fuer den Versand: eigene Vorlage des Mandanten, sonst `null` (= Standard). */
|
||||
async getCustomTexts(tenantId: string): Promise<WelcomeMailTexts | null> {
|
||||
const state = await this.getState(tenantId);
|
||||
return state.custom ? state.texts : null;
|
||||
}
|
||||
|
||||
/** Name des Mandanten fuer `{{firma}}`; leer, wenn der Mandant fehlt. */
|
||||
async getTenantName(tenantId: string): Promise<string> {
|
||||
const tenantPrisma = forTenant(this.prisma, tenantId);
|
||||
const tenant = await tenantPrisma.tenant.findUnique({
|
||||
where: { id: tenantId },
|
||||
select: { name: true },
|
||||
});
|
||||
return tenant?.name ?? '';
|
||||
}
|
||||
|
||||
async save(
|
||||
tenantId: string,
|
||||
texts: WelcomeMailTexts,
|
||||
updatedBy: string,
|
||||
): Promise<WelcomeMailTemplateState> {
|
||||
const tenantPrisma = forTenant(this.prisma, tenantId);
|
||||
const data = {
|
||||
subject: texts.subject,
|
||||
heading: texts.heading,
|
||||
intro: texts.intro,
|
||||
closing: texts.closing,
|
||||
updatedBy,
|
||||
};
|
||||
await tenantPrisma.welcomeMailTemplate.upsert({
|
||||
where: { tenantId },
|
||||
create: { tenantId, ...data },
|
||||
update: data,
|
||||
});
|
||||
return this.getState(tenantId);
|
||||
}
|
||||
|
||||
/** "Auf Standard zuruecksetzen": loescht die eigene Vorlage (idempotent). */
|
||||
async reset(tenantId: string): Promise<WelcomeMailTemplateState> {
|
||||
const tenantPrisma = forTenant(this.prisma, tenantId);
|
||||
await tenantPrisma.welcomeMailTemplate.deleteMany({ where: { tenantId } });
|
||||
return this.getState(tenantId);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user