feat: Kundenportal-Ausbau - Tabs, Projekt-Einblicke, Git-Zeiterfassung, Rechnungen, Chat
Kunden-Dashboard: - Tab-Navigation: Projekte | Was wurde gemacht | Rechnungen | Chat - Projekt-Details pro Projekt: letzte Git-Commits (Titel+Beschreibung), Projektgroesse, Datei-Uebersicht (Top-Level aggregiert), Ticket-Arbeiten - Rechnungen: gestylte Liste mit Status-Pillen, Ansehen/Zahlen-Link, PDF-Download ueber Server-Proxy (IDOR-geschuetzt) - Chat mit dem Webklar-Team (Polling, Ungelesen-Badge, viewAs blockiert) Admin-Dashboard: - Chat-Tab: Konversationsliste + Thread + Antwortfeld, Ungelesen-Badge Backend: - giteaAdmin: getRepoInfo, listRepoCommits, getRepoTreeSummary - projectInsights: 5-min-Cache, Invalidierung per Gitea-Webhook - /api/projects/:id/git|files|work mit Ownership-Check (404) - /api/chat/* (Kunde) + /api/portal-admin/chats/* (Admin), portalMessages-Collection - mailer: E-Mail-Benachrichtigung an Admins bei Kundennachricht (15-min-Throttle) - gitWorksheet: dd.mm.yyyy, voller Commit-Body, startTime leer (Zeit-Nachtrag), Auto-Webpage-Ticket bei Push auf Projekt ohne Ticket (ensureProjectTicket) - customerActivity: Git-Push-Eintraege fuer Kunden sichtbar Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
56
server/services/mailer.js
Normal file
56
server/services/mailer.js
Normal file
@@ -0,0 +1,56 @@
|
||||
import nodemailer from 'nodemailer'
|
||||
import { config } from '../config.js'
|
||||
|
||||
let transporter = null
|
||||
|
||||
function getTransporter() {
|
||||
if (!config.smtp.host || !config.smtp.user) return null
|
||||
if (!transporter) {
|
||||
transporter = nodemailer.createTransport({
|
||||
host: config.smtp.host,
|
||||
port: config.smtp.port,
|
||||
secure: config.smtp.secure,
|
||||
auth: { user: config.smtp.user, pass: config.smtp.pass },
|
||||
})
|
||||
}
|
||||
return transporter
|
||||
}
|
||||
|
||||
// Throttle: pro Kunde maximal eine Benachrichtigung je Zeitfenster,
|
||||
// damit ein aktiver Chat nicht pro Nachricht eine Mail ausloest.
|
||||
const lastNotifyByCustomer = new Map() // customerId -> timestamp
|
||||
|
||||
/**
|
||||
* Benachrichtigt die Admins per E-Mail ueber eine neue Portal-Chatnachricht.
|
||||
* Fehler werden nur geloggt - der Chat darf am Mailversand nie scheitern.
|
||||
*/
|
||||
export async function sendChatNotification(customer, text) {
|
||||
try {
|
||||
const mailer = getTransporter()
|
||||
if (!mailer || !config.chatNotify.emails.length) return { sent: false, reason: 'nicht konfiguriert' }
|
||||
|
||||
const now = Date.now()
|
||||
const last = lastNotifyByCustomer.get(customer.$id) || 0
|
||||
const throttleMs = config.chatNotify.throttleMin * 60 * 1000
|
||||
if (now - last < throttleMs) return { sent: false, reason: 'throttled' }
|
||||
lastNotifyByCustomer.set(customer.$id, now)
|
||||
|
||||
const name = customer.name || customer.email || 'Kunde'
|
||||
await mailer.sendMail({
|
||||
from: `"Webklar Kundenportal" <${config.smtp.from}>`,
|
||||
to: config.chatNotify.emails.join(', '),
|
||||
subject: `Neue Portalnachricht von ${name}`,
|
||||
text: [
|
||||
`${name} hat im Kundenportal geschrieben:`,
|
||||
'',
|
||||
text,
|
||||
'',
|
||||
'Antworten: https://project.webklar.com/admin.html (Tab Chat)',
|
||||
].join('\n'),
|
||||
})
|
||||
return { sent: true }
|
||||
} catch (err) {
|
||||
console.error('[mailer] Chat-Benachrichtigung fehlgeschlagen:', err.message)
|
||||
return { sent: false, reason: err.message }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user