import "server-only"; import { prisma } from "./db"; import { sendeMail, htmlRahmen } from "./mail"; import type { NotificationType } from "@prisma/client"; /** * Eine Benachrichtigung landet immer im System (Glocke) und – je nach * Einstellung des Empfängers – zusätzlich per E-Mail. Damit ersetzt der * interne Nachrichtenverkehr den E-Mail-Pingpong, ohne dass jemand etwas * verpasst, der nur sein Postfach liest. */ export async function benachrichtige(opts: { userIds: string[]; typ: NotificationType; titel: string; text?: string; link?: string; projectId?: string; ausgeloestVon?: string; }) { const empfaenger = [...new Set(opts.userIds)].filter((id) => id !== opts.ausgeloestVon); if (empfaenger.length === 0) return; const users = await prisma.user.findMany({ where: { id: { in: empfaenger }, active: true }, select: { id: true, email: true, name: true, notifyMode: true, notifyMentions: true, notifyAssignments: true, notifyDeadlines: true, }, }); await prisma.notification.createMany({ data: users.map((u) => ({ userId: u.id, typ: opts.typ, titel: opts.titel, text: opts.text ?? null, link: opts.link ?? null, projectId: opts.projectId ?? null, })), }); const sofort = users.filter((u) => { if (u.notifyMode !== "SOFORT") return false; if (opts.typ === "ERWAEHNUNG" && !u.notifyMentions) return false; if (opts.typ === "ZUWEISUNG" && !u.notifyAssignments) return false; if (opts.typ === "FRIST" && !u.notifyDeadlines) return false; return true; }); await Promise.all( sofort.map((u) => sendeMail({ an: u.email, betreff: opts.titel, text: opts.text ?? opts.titel, html: htmlRahmen(opts.titel, opts.text ?? "", opts.link), }), ), ); } /** @-Erwähnungen aus einem Nachrichtentext auflösen. */ export async function findeErwaehnungen(text: string, projectId?: string) { const namen = [...text.matchAll(/@([\wäöüÄÖÜß.-]{2,40})/g)].map((m) => m[1].toLowerCase()); if (namen.length === 0) return []; const kandidaten = await prisma.user.findMany({ where: projectId ? { memberships: { some: { projectId } } } : { active: true }, select: { id: true, name: true, email: true }, }); const treffer = new Set(); for (const k of kandidaten) { const handle = k.email.split("@")[0].toLowerCase(); const nachname = k.name.split(" ").slice(-1)[0].toLowerCase(); if (namen.some((n) => n === handle || n === nachname || k.name.toLowerCase().startsWith(n))) { treffer.add(k.id); } } return [...treffer]; } export async function protokolliere(opts: { userId?: string | null; aktion: string; entitaet: string; entitaetId?: string; projectId?: string; details?: unknown; }) { await prisma.auditLog.create({ data: { userId: opts.userId ?? null, aktion: opts.aktion, entitaet: opts.entitaet, entitaetId: opts.entitaetId ?? null, projectId: opts.projectId ?? null, details: (opts.details ?? undefined) as never, }, }); }