Files
lms-sb/src/lib/settings.ts
T
adminsandClaude Opus 5 8223cbb963 feat(комментарии): письмо ученику об ответе под уроком
Ответы персонала в ветке комментариев никуда не уведомляли: ученик видел
ответ, только если сам возвращался в урок. У тредов вопросов письмо есть,
у комментариев не было — из-за этого ответ под уроком 6.3 «Холст» висел
непрочитанным, пока его не продублировали руками.

- sendCommentReplyEmail: тот же шаблон ДС-2, что у ответов в тредах, с текстом
  ответа внутри письма и кнопкой на урок. Свой лимит превью 4000 символов:
  ввод разрешает 10 000, а порог вопросов (2000) резал бы развёрнутые разборы.
- commentReplyRecipient: правила «шлём / не шлём» вынесены чистой функцией и
  покрыты тестами — не шлём при выключенной настройке, не-staff, удалённом
  родителе, ответе самому себе и пустой почте.
- notifyStudentOnCommentReply: тумблер в админке рядом с уведомлением о фидбеке,
  по умолчанию включён.
- Удалён неподключённый дубль comment-actions.ts: компонент комментариев ходит
  в lib/actions/student-actions.ts, а в дубле жил свой лимит 2000 — правка в нём
  не давала эффекта.

Проверено: type-check и lint чистые (3 ошибки линтера — существующий долг в
quick-enroll-modal и kinescope-player), vitest 13/13, письмо отправлено боем
и доставлено (Resend, last_event=delivered).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 11:49:45 +05:00

95 lines
2.9 KiB
TypeScript

import { cache } from "react";
import { prisma } from "./prisma";
// ── Defaults ──────────────────────────────────────────────────────────────────
export const SETTINGS_DEFAULTS = {
// Basic
schoolName: "Second Brain",
schoolDescription: "Образовательная платформа Second Brain",
schoolKeywords: "",
maintenanceMode: "false",
registrationEnabled: "true",
// Notifications
notificationEmails: "", // newline-separated list
notifyOnHomework: "true",
notifyOnComment: "true",
notifyOnRegistration: "true",
notifyStudentOnFeedback: "true",
notifyStudentOnCommentReply: "true",
// Student profile
requireEmailVerification: "true",
lastNameField: "optional", // required | optional | hidden
phoneField: "hidden", // required | optional | hidden
// Legal
privacyPolicyUrl: "",
termsUrl: "",
offerUrl: "",
showTermsCheckbox: "false",
orgRequisites: "",
// Curator permissions
curatorHomeworkScope: "all", // all | assigned
curatorCanAnswerQuestions: "true",
curatorCanSeeStudents: "true",
// Logo
logoUrl: "",
showLogo: "true",
// Social networks
socialYoutube: "",
socialVk: "",
socialTelegram: "",
// Code injection
headCode: "",
bodyCode: "",
} as const;
export type SettingsKey = keyof typeof SETTINGS_DEFAULTS;
export type Settings = Record<SettingsKey, string>;
// ── Getters ───────────────────────────────────────────────────────────────────
export const getSettings = cache(async (): Promise<Settings> => {
try {
const rows = await prisma.settings.findMany();
const stored: Record<string, string> = {};
for (const row of rows) stored[row.key] = row.value;
return Object.fromEntries(
Object.entries(SETTINGS_DEFAULTS).map(([k, v]) => [k, stored[k] ?? v])
) as Settings;
} catch {
// DB unavailable at build time — return defaults
return { ...SETTINGS_DEFAULTS } as Settings;
}
});
export async function getSetting(key: SettingsKey): Promise<string> {
try {
const row = await prisma.settings.findUnique({ where: { key } });
return row?.value ?? SETTINGS_DEFAULTS[key];
} catch {
return SETTINGS_DEFAULTS[key];
}
}
// ── Convenience helpers ───────────────────────────────────────────────────────
/** Parse a boolean setting ("true" / "false") */
export function asBool(value: string): boolean {
return value === "true";
}
/** Parse notification emails (newline-separated string → array) */
export function parseNotificationEmails(value: string): string[] {
return value
.split("\n")
.map((e) => e.trim())
.filter(Boolean);
}