v1.4: HTML-E-Mail-Benachrichtigungen im Schulportal-Stil

- server/src/mail.js: baueHtmlMail/buttonHtml (600-px-Karte, Kopfzeile in
  Hinterstein-Farben #22333b + Amber, Zeilen-Tabelle, Status-Box, Zitat-Box,
  Aktions-Button 'Zum Mieterportal', Footer), immer mit Plain-Text-Fallback
- index.js: Ticket, neue Unterhaltung und Antwort versenden HTML + Text
  (multipart/alternative ueber die Mail-API)
- Vorschauen: preview/08-mail-ticket.png, preview/08-mail-nachricht.png
- E2E live verifiziert (Zustellung an axel@hocks.eu, status=sent), Doku
This commit is contained in:
2026-10-02 13:09:50 +02:00
parent 311153b3c3
commit 0bdf6e7b28
5 changed files with 177 additions and 26 deletions
+47 -11
View File
@@ -258,12 +258,25 @@ app.post('/api/tickets', auth, async (req, res, next) => {
deutschesDatum()],
)
const label = mail.einheitLabel(mapUnit(unitRows[0]))
mail.benachrichtigeVerwaltung(
`Neue Schadensmeldung: ${String(title).trim()} (${label})`,
const betreff = `Neue Schadensmeldung: ${String(title).trim()} (${label})`
const text =
`Im Mieterportal wurde eine neue Schadensmeldung erstellt.\n\n` +
`Wohnung: ${label}\nMeldung von: ${mieterName}\nKategorie: ${String(category || 'Sonstiges')}\n\n` +
`${String(title).trim()}\n${String(description || '–').trim()}`,
)
`Wohnung: ${label}\nMeldung von: ${mieterName}\nKategorie: ${String(category || 'Sonstiges')}\n\n` +
`${String(title).trim()}\n${String(description || '–').trim()}`
const html = mail.baueHtmlMail({
titel: 'Neue Schadensmeldung',
untertitel: label,
einleitung: 'Im Mieterportal wurde eine neue Schadensmeldung erstellt:',
zeilen: [
['Wohnung', label],
['Meldung von', mieterName],
['Kategorie', String(category || 'Sonstiges')],
],
statusBox: { hintergrund: '#f0fdfa', farbe: '#0f766e', text: 'Status: offen – neu eingegangen' },
zitat: `${String(title).trim()}\n${String(description || '–').trim()}`,
aktionenHtml: mail.buttonHtml('Zum Mieterportal', `${process.env.PORTAL_URL || 'https://mieterportal.hocks.eu'}/schadensmeldungen`),
})
mail.benachrichtigeVerwaltung(betreff, text, html)
res.json({ ticket: mapTicket(rows[0]) })
} catch (e) { next(e) }
})
@@ -308,9 +321,20 @@ app.post('/api/threads', auth, async (req, res, next) => {
const betreff = vonRolle === 'tenant'
? `Neue Nachricht im Mieterportal: ${String(subject).trim()}`
: `Nachricht der Verwaltung: ${String(subject).trim()}`
const inhalt = `Neue Unterhaltung von ${autor}:\n\n„${String(text).trim()}“`
if (vonRolle === 'tenant') mail.benachrichtigeVerwaltung(betreff, inhalt)
else mail.benachrichtigeMieter(unitId, betreff, inhalt)
const textVersion = `Neue Unterhaltung von ${autor}:\n\n„${String(text).trim()}“`
const html = mail.baueHtmlMail({
titel: vonRolle === 'tenant' ? 'Neue Nachricht' : 'Nachricht der Verwaltung',
untertitel: String(subject).trim(),
einleitung: `Neue Unterhaltung von ${autor}:`,
zeilen: [
['Wohnung', mail.einheitLabel(mapUnit(unitRows[0]))],
['Betreff', String(subject).trim()],
],
zitat: String(text).trim(),
aktionenHtml: mail.buttonHtml('Zum Mieterportal', `${process.env.PORTAL_URL || 'https://mieterportal.hocks.eu'}/nachrichten`),
})
if (vonRolle === 'tenant') mail.benachrichtigeVerwaltung(betreff, textVersion, html)
else mail.benachrichtigeMieter(unitId, betreff, textVersion, html)
const { rows } = await pool.query('SELECT * FROM threads WHERE id = $1', [id])
const { rows: msgRows } = await pool.query('SELECT * FROM messages WHERE thread_id = $1 ORDER BY at', [id])
res.json({ thread: { ...mapThread(rows[0]), messages: msgRows.map(mapMessage) } })
@@ -340,9 +364,21 @@ app.post('/api/threads/:id/messages', auth, async (req, res, next) => {
const betreff = vonRolle === 'tenant'
? `Neue Antwort im Mieterportal: ${th.subject}`
: `Antwort der Verwaltung: ${th.subject}`
const inhalt = `Neue Nachricht von ${autor} in der Unterhaltung „${th.subject}“:\n\n„${text}“`
if (vonRolle === 'tenant') mail.benachrichtigeVerwaltung(betreff, inhalt)
else mail.benachrichtigeMieter(th.unit_id, betreff, inhalt)
const { rows: einheitRows } = await pool.query('SELECT * FROM units WHERE id = $1', [th.unit_id])
const textVersion = `Neue Nachricht von ${autor} in der Unterhaltung „${th.subject}“:\n\n„${text}“`
const html = mail.baueHtmlMail({
titel: vonRolle === 'tenant' ? 'Neue Antwort' : 'Antwort der Verwaltung',
untertitel: th.subject,
einleitung: `Neue Nachricht von ${autor}:`,
zeilen: [
['Wohnung', mail.einheitLabel(mapUnit(einheitRows[0]))],
['Unterhaltung', th.subject],
],
zitat: text,
aktionenHtml: mail.buttonHtml('Zum Mieterportal', `${process.env.PORTAL_URL || 'https://mieterportal.hocks.eu'}/nachrichten`),
})
if (vonRolle === 'tenant') mail.benachrichtigeVerwaltung(betreff, textVersion, html)
else mail.benachrichtigeMieter(th.unit_id, betreff, textVersion, html)
const { rows: msgRows } = await pool.query('SELECT * FROM messages WHERE thread_id = $1 ORDER BY at', [th.id])
const { rows } = await pool.query('SELECT * FROM threads WHERE id = $1', [th.id])
res.json({ thread: { ...mapThread(rows[0]), messages: msgRows.map(mapMessage) } })