v1.22: Verwaltung sendet Direkt-E-Mails an Mieter mit CC an die Verwaltung

This commit is contained in:
2026-10-04 14:17:12 +02:00
parent 497abfcd4b
commit f15fc6ee0c
10 changed files with 410 additions and 8 deletions
+50
View File
@@ -1238,11 +1238,61 @@ app.patch('/api/users/:id', auth, adminOnly, async (req, res, next) => {
await pool.query('UPDATE users SET aktiv = $1 WHERE id = $2', [Boolean(b.aktiv), req.params.id])
if (b.einheitId !== undefined)
await pool.query('UPDATE users SET einheit_id = $1 WHERE id = $2', [b.einheitId || null, req.params.id])
if (b.email !== undefined) {
const email = String(b.email || '').trim()
if (email && !/^\S+@\S+\.\S+$/.test(email))
return res.status(400).json({ fehler: 'Ungültige E-Mail-Adresse.' })
if (email) {
const { rows: fremde } = await pool.query(
'SELECT id FROM users WHERE lower(email) = lower($1) AND id <> $2', [email, req.params.id],
)
if (fremde.length)
return res.status(400).json({ fehler: 'Diese E-Mail-Adresse wird bereits verwendet.' })
}
await pool.query('UPDATE users SET email = $1 WHERE id = $2', [email, req.params.id])
}
const { rows: neu } = await pool.query('SELECT * FROM users WHERE id = $1', [req.params.id])
res.json({ benutzer: oeffentlicherBenutzer(neu[0]) })
} catch (e) { next(e) }
})
// Direkte E-Mail der Verwaltung an einen Mieter (immer mit CC an die
// Verwaltung, damit die Kommunikation dort vollständig nachvollziehbar ist)
app.post('/api/users/:id/email', auth, adminOnly, async (req, res, next) => {
try {
const betreff = String((req.body || {}).betreff || '').trim()
const text = String((req.body || {}).text || '').trim()
if (!betreff || !text)
return res.status(400).json({ fehler: 'Betreff und Nachricht erforderlich.' })
const { rows } = await pool.query(
`SELECT * FROM users WHERE id = $1 AND rolle = 'mieter'`, [req.params.id],
)
const mieter = rows[0]
if (!mieter) return res.status(404).json({ fehler: 'Mieter nicht gefunden.' })
if (!mieter.email)
return res.status(400).json({ fehler: `${mieter.name} hat keine E-Mail-Adresse hinterlegt (Profil).` })
const { rows: einheitRows } = await pool.query('SELECT * FROM units WHERE id = $1', [mieter.einheit_id])
const label = mail.einheitLabel(mapUnit(einheitRows[0]))
const html = mail.baueHtmlMail({
titel: 'Nachricht der Verwaltung',
untertitel: label,
einleitung: `Persönliche Nachricht von ${req.user.name} (Hinterstein Immobilien):`,
zeilen: [
['Wohnung', label],
['Mieter', mieter.name],
],
zitat: text,
aktionenHtml: mail.buttonHtml('Zum Mieterportal', `${process.env.PORTAL_URL || 'https://mieterportal.hocks.eu'}/nachrichten`),
})
const cc = await mail.verwaltungEmails()
const textVersion =
`Nachricht der Verwaltung an ${mieter.name} (${label}):\n\n„${text}“\n\n` +
`— Gesendet von ${req.user.name} über das Mieterportal`
mail.sendeMail(mieter.email, betreff, textVersion, html, cc)
res.json({ ok: true })
} catch (e) { next(e) }
})
// ── Registrierungscodes (nur Verwaltung) ─────────────────────
app.get('/api/codes', auth, adminOnly, async (_req, res, next) => {
try {
+15 -2
View File
@@ -108,13 +108,17 @@ function buttonHtml(text, url) {
return `<p style="margin:24px 0 0;"><a href="${htmlEscape(url)}" style="display:inline-block;background:${FARBE_BUTTON};color:${FARBE_BUTTON_TEXT};text-decoration:none;font-size:14px;font-weight:700;padding:12px 22px;border-radius:10px;">${htmlEscape(text)}</a></p>`
}
async function sendeMail(an, betreff, text, html) {
async function sendeMail(an, betreff, text, html, cc) {
if (!MAIL_API_URL || !MAIL_API_TOKEN || !an) return
try {
const res = await fetch(`${MAIL_API_URL}/send`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-Api-Token': MAIL_API_TOKEN },
body: JSON.stringify({ to: an, subject: betreff, text: `${text}\n\n${FUSSZEILE_TEXT}`, ...(html ? { html } : {}) }),
body: JSON.stringify({
to: an, subject: betreff, text: `${text}\n\n${FUSSZEILE_TEXT}`,
...(html ? { html } : {}),
...(cc ? { cc } : {}), // v1.22: Komma-getrennte CC-Empfänger
}),
})
if (!res.ok) console.warn(`[mail] Versand an ${an} fehlgeschlagen: HTTP ${res.status}`)
} catch (e) {
@@ -122,6 +126,14 @@ async function sendeMail(an, betreff, text, html) {
}
}
/** E-Mail-Adressen aller aktiven Verwaltungsnutzer (komma-getrennt, für CC). */
async function verwaltungEmails() {
const { rows } = await require('./db').pool.query(
`SELECT email FROM users WHERE rolle = 'verwaltung' AND aktiv AND email <> ''`,
)
return rows.map((r) => r.email).join(', ')
}
/** Alle aktiven Verwaltungsnutzer benachrichtigen. */
async function benachrichtigeVerwaltung(betreff, text, html) {
const { rows } = await require('./db').pool.query(
@@ -147,6 +159,7 @@ module.exports = {
sendeMail,
benachrichtigeVerwaltung,
benachrichtigeMieter,
verwaltungEmails,
einheitLabel,
baueHtmlMail,
buttonHtml,