Add email notification on new lesson comment
Notify admins/curators (or configured notificationEmails) when a student leaves a comment under a lesson. New setting notifyOnComment (default on) + toggle in admin settings. Fires only for student comments, not admin/curator replies; comment text is HTML-escaped in the email. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -274,6 +274,11 @@ export function SettingsForm({ initial }: { initial: Settings }) {
|
|||||||
checked={bool("notifyOnHomework")}
|
checked={bool("notifyOnHomework")}
|
||||||
onChange={(v) => set("notifyOnHomework", v ? "true" : "false")}
|
onChange={(v) => set("notifyOnHomework", v ? "true" : "false")}
|
||||||
/>
|
/>
|
||||||
|
<Toggle
|
||||||
|
label="Уведомлять о новом комментарии под уроком"
|
||||||
|
checked={bool("notifyOnComment")}
|
||||||
|
onChange={(v) => set("notifyOnComment", v ? "true" : "false")}
|
||||||
|
/>
|
||||||
<Toggle
|
<Toggle
|
||||||
label="Уведомлять о новой регистрации ученика"
|
label="Уведомлять о новой регистрации ученика"
|
||||||
checked={bool("notifyOnRegistration")}
|
checked={bool("notifyOnRegistration")}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { prisma } from "@/lib/prisma";
|
|||||||
import { auth } from "@/lib/auth";
|
import { auth } from "@/lib/auth";
|
||||||
import { headers } from "next/headers";
|
import { headers } from "next/headers";
|
||||||
import { revalidatePath } from "next/cache";
|
import { revalidatePath } from "next/cache";
|
||||||
import { sendHomeworkSubmittedEmail } from "@/lib/email";
|
import { sendHomeworkSubmittedEmail, sendCommentNotificationEmail } from "@/lib/email";
|
||||||
import { getSettings, parseNotificationEmails, asBool } from "@/lib/settings";
|
import { getSettings, parseNotificationEmails, asBool } from "@/lib/settings";
|
||||||
|
|
||||||
// ── Lesson Progress ───────────────────────────────────────────────────────────
|
// ── Lesson Progress ───────────────────────────────────────────────────────────
|
||||||
@@ -156,7 +156,7 @@ export async function addComment(lessonId: string, slug: string, text: string, p
|
|||||||
|
|
||||||
const lesson = await prisma.lesson.findUnique({
|
const lesson = await prisma.lesson.findUnique({
|
||||||
where: { id: lessonId },
|
where: { id: lessonId },
|
||||||
select: { module: { select: { course: { select: { id: true } } } } },
|
select: { title: true, module: { select: { course: { select: { id: true, slug: true } } } } },
|
||||||
});
|
});
|
||||||
if (!lesson) throw new Error("Lesson not found");
|
if (!lesson) throw new Error("Lesson not found");
|
||||||
|
|
||||||
@@ -185,6 +185,26 @@ export async function addComment(lessonId: string, slug: string, text: string, p
|
|||||||
data: { lessonId, userId: session.user.id, text: trimmed, ...(parentId ? { parentId } : {}) },
|
data: { lessonId, userId: session.user.id, text: trimmed, ...(parentId ? { parentId } : {}) },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Уведомление админам/кураторам о новом комментарии студента (не о своих ответах)
|
||||||
|
if (!isAdmin && !isCurator) {
|
||||||
|
const settings = await getSettings();
|
||||||
|
if (asBool(settings.notifyOnComment)) {
|
||||||
|
const configured = parseNotificationEmails(settings.notificationEmails);
|
||||||
|
const recipients = configured.length > 0
|
||||||
|
? configured.map((email) => ({ email, name: "" }))
|
||||||
|
: await prisma.user.findMany({
|
||||||
|
where: { role: { in: ["admin", "curator"] } },
|
||||||
|
select: { email: true, name: true },
|
||||||
|
});
|
||||||
|
const lessonUrl = `${process.env.BETTER_AUTH_URL ?? "https://school.second-brain.ru"}/courses/${slug}/lessons/${lessonId}`;
|
||||||
|
await Promise.all(
|
||||||
|
recipients.map((r) =>
|
||||||
|
sendCommentNotificationEmail(r.email, r.name, session.user.name, lesson.title, trimmed, lessonUrl)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
revalidatePath(`/courses/${slug}/lessons/${lessonId}`);
|
revalidatePath(`/courses/${slug}/lessons/${lessonId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -137,6 +137,31 @@ export async function sendHomeworkSubmittedEmail(
|
|||||||
}).catch((e) => console.error("[email] sendHomeworkSubmittedEmail:", e));
|
}).catch((e) => console.error("[email] sendHomeworkSubmittedEmail:", e));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function sendCommentNotificationEmail(
|
||||||
|
to: string,
|
||||||
|
recipientName: string,
|
||||||
|
studentName: string,
|
||||||
|
lessonTitle: string,
|
||||||
|
commentText: string,
|
||||||
|
lessonUrl: string
|
||||||
|
) {
|
||||||
|
const school = await getSchoolName();
|
||||||
|
const esc = (t: string) => t.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||||
|
const preview = commentText.length > 400 ? commentText.slice(0, 400) + "…" : commentText;
|
||||||
|
await getResend().emails.send({
|
||||||
|
from: FROM,
|
||||||
|
to,
|
||||||
|
subject: `Новый комментарий под уроком — ${lessonTitle}`,
|
||||||
|
html: base(`
|
||||||
|
<p ${p}>Привет${recipientName ? `, ${esc(recipientName)}` : ""}!</p>
|
||||||
|
<p ${p}><strong>${esc(studentName)}</strong> оставил комментарий под уроком <strong>«${lessonTitle}»</strong>:</p>
|
||||||
|
${quote(esc(preview))}
|
||||||
|
<p ${pLast}>Открыть урок и ответить:</p>
|
||||||
|
${btn(lessonUrl, "Открыть урок")}
|
||||||
|
`, school),
|
||||||
|
}).catch((e) => console.error("[email] sendCommentNotificationEmail:", e));
|
||||||
|
}
|
||||||
|
|
||||||
export async function sendFeedbackReceivedEmail(
|
export async function sendFeedbackReceivedEmail(
|
||||||
to: string,
|
to: string,
|
||||||
studentName: string,
|
studentName: string,
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export const SETTINGS_DEFAULTS = {
|
|||||||
// Notifications
|
// Notifications
|
||||||
notificationEmails: "", // newline-separated list
|
notificationEmails: "", // newline-separated list
|
||||||
notifyOnHomework: "true",
|
notifyOnHomework: "true",
|
||||||
|
notifyOnComment: "true",
|
||||||
notifyOnRegistration: "true",
|
notifyOnRegistration: "true",
|
||||||
notifyStudentOnFeedback: "true",
|
notifyStudentOnFeedback: "true",
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user