diff --git a/ROADMAP.md b/ROADMAP.md index adc7322..5316ab4 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -36,6 +36,15 @@ --- +**Доработки таблицы пользователей (добавить в рамках Этапа 9):** +- [ ] Фильтры: по роли (Ученик / Куратор / Администратор), по статусу email (подтверждён/нет) +- [ ] Поиск по имени / email +- [ ] Пагинация + выбор кол-ва записей на странице (20/50/100) +- [ ] Ховер-попап на строке: список курсов пользователя со сроками доступа + email + телефон +- [ ] Быстрые действия в строке: кнопка «Выдать доступ» без перехода на страницу пользователя + +--- + ## Этап 1.5 — Расширенное управление доступом ✅ ЗАВЕРШЁН (07.04.2026) - [x] Срок доступа: `expiresAt` в `CourseEnrollment`, просроченный подсвечивается красным @@ -80,6 +89,13 @@ - [x] Admin: AdminShell на `/curator/*` маршрутах (сайдбар не пропадает) - [x] Admin: реальная статистика на дашборде (студенты, курсы, ДЗ, прогресс) +**Доработки (добавить в рамках Этапа 9):** +- [ ] Фильтры в списке ДЗ: по имени/email ученика, по уроку, по курсу, по статусу +- [ ] Поиск по имени/email ученика +- [ ] Пагинация + выбор кол-ва записей на странице (20/50/100) +- [ ] Расширенные статусы: «Новое» / «Просмотрено» / «Прокомментировано» +- [ ] Шаблоны ответов куратора — готовые тексты фидбека, выбираемые из списка + --- ## Этап 6 — Обсуждения под уроками ✅ ЗАВЕРШЁН (07.04.2026) @@ -90,6 +106,13 @@ - [x] Модерация: автор, куратор или admin может удалить комментарий - [x] Счётчик активных комментариев в заголовке секции +**Не реализовано (добавить в Этап 9 или отдельно):** +- [ ] `/admin/comments` — сводная таблица всех комментариев по всем урокам + - Колонки: №, Имя ученика, Урок (ссылка), Время, кол-во ответов, превью текста + - Удалить комментарий прямо из списка + - Пагинация + - Ссылка в сайдбаре AdminNav + --- ## Этап 7 — Email-уведомления ✅ ЗАВЕРШЁН (07.04.2026) @@ -114,31 +137,128 @@ --- -## Этап 9 — Миграция с emdesell ← СЛЕДУЮЩИЙ -**Цель:** все пользователи и контент перенесены в новую LMS. +## Этап 9 — Настройки платформы (Admin Settings) ✅ ЗАВЕРШЁН (08.04.2026) +**Цель:** администратор управляет ключевыми параметрами платформы без правки кода. -- [ ] Скрипт импорта пользователей из CSV-экспорта emdesell (email, имя, курсы) -- [ ] Создание пользователей без пароля + письмо «установите пароль» -- [ ] Назначение доступов к курсам по данным из CSV +### Основное +- [ ] Название школы (используется в заголовке сайта, подписи писем) +- [ ] Описание школы (мета-тег description) +- [ ] Ключевые слова (мета-тег keywords) +- [ ] Режим тех. работ: вкл/выкл (показывает заглушку всем кроме admin) +- [ ] Регистрация учеников: вкл/выкл + +### Оформление +- [ ] Логотип школы (загрузка → Object Storage, отображается в шапке) +- [ ] Фавикон (загрузка → Object Storage) +- [ ] Показывать логотип: да/нет + +### Уведомления +- [ ] Email(ы) для системных уведомлений (кому слать письма о ДЗ, вопросах, регистрациях) +- [ ] Уведомление куратору/админу о новом ДЗ: вкл/выкл +- [ ] Уведомление куратору/админу о новом вопросе ученика: вкл/выкл +- [ ] Уведомление админу о новой регистрации: вкл/выкл +- [ ] Уведомление ученику при ответе на ДЗ/вопрос: вкл/выкл + +### Данные ученика +- [ ] Требовать подтверждение email перед доступом к курсам: да/нет +- [ ] Фамилия при регистрации: обязательная / необязательная / выключена +- [ ] Телефон при регистрации: обязательный / необязательный / выключен + +### Защита +- [ ] Одна активная сессия на аккаунт: вкл/выкл +- [ ] CAPTCHA на форме регистрации: вкл/выкл (reCAPTCHA v3) + +### Права куратора +- [ ] Куратор видит ДЗ: по всем курсам / только по назначенным курсам +- [ ] Куратор может отвечать на вопросы учеников: да/нет +- [ ] Куратор видит список всех студентов: да/нет + +### Вставка кода +- [ ] Произвольный код в `
` (Yandex.Metrika, Google Analytics, пиксели) +- [ ] Произвольный код в `` (виджеты, чаты поддержки) + +### Юридические документы +- [ ] URL Политики конфиденциальности (ссылка на внешний документ) +- [ ] URL Согласия на обработку персональных данных +- [ ] URL Договора-оферты +- [ ] Показывать чекбокс «Я принимаю условия» при регистрации: да/нет +- [ ] Реквизиты организации (текстовое поле, отображается в подвале) + +### Соц. сети +- [ ] YouTube: одна ссылка +- [ ] VK: несколько ссылок (название + URL), например «Основная группа» и «Канал» +- [ ] Telegram: несколько ссылок (название + URL), например «Основной канал» и «Канал курса» +(отображаются в подвале личного кабинета ученика; хранятся как JSON-массив в Settings) + +### Вопросы учеников +- [ ] Система вопросов глобально: вкл/выкл +- [ ] Куратор/админ может написать ученику первым: да/нет +- [ ] Вопросы только по курсам ученика: да/нет +- [ ] Включать вопросы для новых курсов автоматически: да/нет + +**Хранение:** таблица `Settings` (key-value), доступна через `getSettings()` в server components. +**Критерий готовности:** меняю название школы → оно появляется в заголовке. Включаю тех. работы → ученики видят заглушку. Куратор привязан к курсу — видит только его ДЗ. + +--- + +## Этап 11 — Импорт/Экспорт учеников и миграция с emdesell +**Цель:** все пользователи и контент перенесены в новую LMS. Раздел `/admin/import-export`. + +### Импорт учеников (CSV) +- [ ] Скачать файл-шаблон CSV (Email, Имя, Фамилия, Телефон) +- [ ] Загрузка CSV, поддержка кодировок Windows-1251 и UTF-8 +- [ ] Опция: подтверждать email автоматически (да/нет) +- [ ] Опция: обновлять уже существующие аккаунты (да/нет) +- [ ] Присвоение доступов к курсам при импорте (выбор курса + срок в днях, 0 = бессрочно) +- [ ] Опция: отправить письмо-уведомление ученику (со ссылкой для установки пароля) +- [ ] Предпросмотр перед применением (таблица: кто создаётся, кто обновляется, кому даётся доступ) +- [ ] Применить импорт — создать пользователей, выдать доступы, отправить письма + +### Экспорт учеников (CSV) +- [ ] Все ученики или фильтр по конкретному курсу/доступу +- [ ] Фильтр по просмотрам уроков (экспортировать только тех кто смотрел) +- [ ] Выбор кодировки: Windows-1251 (для Excel) / UTF-8 +- [ ] Поля: Email, Имя, Фамилия, Телефон, Дата регистрации, Курсы, Прогресс + +### Миграция контента - [ ] Чек-лист ручного переноса контента (уроки, PDF, структура курсов) - [ ] Скрипт проверки целостности: все enrolled пользователи имеют доступ к нужным курсам - [ ] QA: проверить 10 случайных аккаунтов после импорта -**Критерий готовности:** все ученики из emdesell могут войти в новую LMS и продолжить обучение. +**Критерий готовности:** загружаю CSV из emdesell → предпросмотр показывает корректные данные → применяю → ученики получают письма → могут войти и продолжить обучение. --- -## Этап 10 — Telegram-бот и аналитика -**Цель:** получаю уведомления в Telegram, вижу базовую аналитику. +## Этап 12 — Telegram-бот и аналитика +**Цель:** уведомления в Telegram для всех участников, базовая аналитика. -- [ ] Telegram-бот: уведомление куратору/админу о новом ДЗ -- [ ] Telegram-бот: уведомление об ошибках (500-е, failed email и т.д.) +**Настройки (в разделе Настройки → Telegram):** +- Токен бота (вводится в админке, хранится в Settings) +- Интеграция вкл/выкл глобально +- Показывать кнопку «Подключить Telegram» в кабинете ученика: да/нет + +**Уведомления куратору/админу:** +- [ ] Новое ДЗ на проверку +- [ ] Новый вопрос от ученика +- [ ] Новая регистрация студента +- [ ] Ошибки платформы (500-е, failed email и т.д.) + +**Уведомления ученику:** +- [ ] Получен фидбек по ДЗ +- [ ] Ответ куратора на вопрос +- [ ] Открыт доступ к новому курсу + +**Реализация:** +- [ ] Ученик привязывает Telegram через `/start` в боте (сохраняется `telegramChatId` в User) +- [ ] Кнопка «Подключить Telegram» в личном кабинете ученика +- [ ] Админ/куратор вводит свой `chatId` в профиле или через `/start` +- [ ] Настройки бота в разделе Настройки → Telegram - [ ] Yandex.Metrika: базовое подключение (pageviews) - [ ] Admin: простая страница аналитики (активные ученики, прогресс по курсам) --- -## Этап 11 — Тесты и квизы +## Этап 13 — Тесты и квизы **Цель:** можно добавить тест к уроку, ученик проходит и получает результат. - [ ] Prisma: Quiz, QuizQuestion, QuizOption, QuizAttempt (схема уже есть) @@ -164,3 +284,35 @@ - Kinescope DRM (signed URLs) — при переходе на платный план - Водяные знаки на PDF и картинках - Мобильное приложение +- **Вопросы учеников** — система тикетов `/admin/questions` и `/questions` для ученика: + - Таблица в админке: №, Имя, Курс, Тема, Статус (Ожидает / Отвечено), Дата + - Статусы отсортированы: сначала «Ожидает ответа» + - Куратор/Admin может создать обращение первым (написать ученику) + - Внутри тикета: история переписки, смена статуса + - **База знаний** — FAQ, который ученик видит до отправки вопроса + - **Шаблоны ответов** — куратор выбирает готовый ответ из списка + - Email + Telegram уведомления обеим сторонам + +- **Главная страница ученика** — кастомизируемый экран после входа: + - Приветственный баннер с описанием школы (редактируется в настройках) + - Список курсов ученика с прогрессом + - Блок бесплатных/открытых материалов (статьи, PDF, видео) + - Анонсы ближайших событий и новых курсов + +- **Медиатека (Файлы)** — централизованное файловое хранилище `/admin/files`: + - Prisma: `MediaFolder` (id, name, courseId?, createdAt) + `MediaFile` (id, folderId?, name, url, size, mimeType, uploadedById, createdAt) + - Папки автоматически создаются по курсам + «Common» для общих файлов + - Вид: грид (карточки с иконкой типа) или список — переключатель + - Breadcrumb-навигация: Все файлы / Название папки + - Загрузка файлов (PDF, изображения, любые) → Object Storage + - Создание папки вручную + - Клик на файл → диалог: имя (редактируемое), дата загрузки, размер, автор + - Действия в диалоге: скопировать ссылку, скачать, удалить + - Вставка файлов из медиатеки в урок (вместо повторной загрузки) + +- **Цифровой сад** — публичный раздел платформы для сообщества: + - Методические материалы и статьи (PKM, Obsidian, Second Brain) + - Рекомендованная литература с аннотациями + - Записи открытых встреч и вебинаров + - Календарь: предстоящие открытые уроки, запуски курсов, события + - Возможно: публичный Obsidian-like граф знаний diff --git a/prisma/migrations/20260408100000_add_settings/migration.sql b/prisma/migrations/20260408100000_add_settings/migration.sql new file mode 100644 index 0000000..64ffdeb --- /dev/null +++ b/prisma/migrations/20260408100000_add_settings/migration.sql @@ -0,0 +1,8 @@ +-- CreateTable +CREATE TABLE "Settings" ( + "key" TEXT NOT NULL, + "value" TEXT NOT NULL, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Settings_pkey" PRIMARY KEY ("key") +); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 39ec51b..462d932 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -295,3 +295,13 @@ model LessonComment { lesson Lesson @relation(fields: [lessonId], references: [id], onDelete: Cascade) user User @relation(fields: [userId], references: [id], onDelete: Cascade) } + +// ───────────────────────────────────────────── +// Platform Settings (key-value store) +// ───────────────────────────────────────────── + +model Settings { + key String @id + value String @db.Text + updatedAt DateTime @updatedAt +} diff --git a/src/app/(student)/layout.tsx b/src/app/(student)/layout.tsx index 04b7953..058a13e 100644 --- a/src/app/(student)/layout.tsx +++ b/src/app/(student)/layout.tsx @@ -3,11 +3,18 @@ import { auth } from "@/lib/auth"; import { redirect } from "next/navigation"; import Link from "next/link"; import { LogoutButton } from "@/components/layout/logout-button"; +import { getSetting } from "@/lib/settings"; export default async function StudentLayout({ children }: { children: React.ReactNode }) { const session = await auth.api.getSession({ headers: await headers() }); if (!session) redirect("/login"); + // Maintenance mode: non-admin users see the maintenance page + if (session.user.role !== "admin") { + const maintenance = await getSetting("maintenanceMode"); + if (maintenance === "true") redirect("/maintenance"); + } + return (+ Технические работы +
++ Платформа временно недоступна. Мы проводим обновление — пожалуйста, зайдите позже. +
++ {hint} +
+ )} ++ {hint} +
+ )} + {children} ++ {label} +
+ {hint && ( ++ {hint} +
+ )} +Second Brain · Образовательная платформа
+${schoolName} · Образовательная платформа
Вам открыт доступ к курсу «${courseTitle}».
Перейдите на платформу чтобы начать обучение:
${btn(`${BASE_URL}/dashboard`, "Перейти к курсам")} - `), + `, school), }).catch((e) => console.error("[email] sendCourseAccessEmail:", e)); } @@ -117,6 +123,7 @@ export async function sendHomeworkSubmittedEmail( lessonTitle: string, submissionId: string ) { + const school = await getSchoolName(); await getResend().emails.send({ from: FROM, to, @@ -126,7 +133,7 @@ export async function sendHomeworkSubmittedEmail(Студент ${studentName} сдал работу по уроку «${lessonTitle}».
Откройте работу чтобы проверить и оставить фидбек:
${btn(`${BASE_URL}/curator/homework/${submissionId}`, "Проверить работу")} - `), + `, school), }).catch((e) => console.error("[email] sendHomeworkSubmittedEmail:", e)); } @@ -137,6 +144,7 @@ export async function sendFeedbackReceivedEmail( feedbackText: string, lessonUrl: string ) { + const school = await getSchoolName(); await getResend().emails.send({ from: FROM, to, @@ -147,35 +155,37 @@ export async function sendFeedbackReceivedEmail( ${quote(feedbackText)}Вернитесь к уроку чтобы увидеть полный фидбек:
${btn(lessonUrl, "Открыть урок")} - `), + `, school), }).catch((e) => console.error("[email] sendFeedbackReceivedEmail:", e)); } export async function sendWelcomeEmail(to: string, name: string) { + const school = await getSchoolName(); await getResend().emails.send({ from: FROM, to, - subject: "Добро пожаловать в Second Brain", + subject: `Добро пожаловать в ${school}`, html: base(`Привет, ${name}!
-Ваш аккаунт на образовательной платформе Second Brain подтверждён.
+Ваш аккаунт на образовательной платформе ${school} подтверждён.
После того как администратор откроет вам доступ к курсу, вы получите письмо и сможете начать обучение.
${btn(`${BASE_URL}/dashboard`, "Перейти на платформу")} - `), + `, school), }).catch((e) => console.error("[email] sendWelcomeEmail:", e)); } export async function sendTestEmail(to: string) { + const school = await getSchoolName(); await getResend().emails.send({ from: FROM, to, - subject: "Тест — Second Brain LMS", + subject: `Тест — ${school} LMS`, html: base(`Привет!
-Это тестовое письмо от платформы Second Brain LMS.
+Это тестовое письмо от платформы ${school} LMS.
Так будут выглядеть все уведомления — доступ к курсу, фидбек по ДЗ и другие.
Если письмо отображается корректно, значит email-уведомления настроены и работают.
${btn(`${BASE_URL}/dashboard`, "Открыть платформу")} - `), + `, school), }).catch((e) => console.error("[email] sendTestEmail:", e)); } diff --git a/src/lib/settings.ts b/src/lib/settings.ts new file mode 100644 index 0000000..15a6069 --- /dev/null +++ b/src/lib/settings.ts @@ -0,0 +1,73 @@ +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", + notifyOnRegistration: "true", + notifyStudentOnFeedback: "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", + + // Code injection + headCode: "", + bodyCode: "", +} as const; + +export type SettingsKey = keyof typeof SETTINGS_DEFAULTS; +export type Settings = Record