feat(lessons): тема занятия в отчёте о занятии
В модалку #lessonModal добавлено текстовое поле «Тема занятия» (#lessonTopic) между датой/временем и текстом отчёта. Поле необязательное, лимит 300 символов. Протянуто по всему срезу: - колонка lesson_reports.topic в db/init.sql, db/migration.sql и ensureLessonReportsTable() — миграция идемпотентная, ALTER IF NOT EXISTS - POST /api/lesson-reports принимает topic, PUT обновляет; в GET-списке колонка добавлена в явный SELECT (там не SELECT *, без правки не пришла бы) - тема в normalizeRestoreData, чтобы бэкап/restore её не теряли - тема уходит в ИИ-контекст воркера строкой «Тема занятия: ...» - вывод в списке отчётов (lessons.js + .lesson-topic в admin.css) В PUT пустая строка очищает тему, а отсутствие поля в теле запроса её не трогает: CASE WHEN $6::boolean THEN $3::text ELSE topic END, а не COALESCE — при COALESCE пустая тема затиралась бы старым значением и поле нельзя было бы очистить. Версии не трогаем: lesson_report_versions хранит только text. Тесты: +5 проверок (создание, список, редактирование, очистка, >300 символов), api.smoketest.js — 76 PASS / 0 FAIL.
This commit is contained in:
@@ -250,6 +250,37 @@ async function main() {
|
|||||||
plain.status === 201 && plain.data.ai_status === 'none' && plain.data.text_original === null,
|
plain.status === 201 && plain.data.ai_status === 'none' && plain.data.text_original === null,
|
||||||
{ status: plain.status, ai_status: plain.data.ai_status });
|
{ status: plain.status, ai_status: plain.data.ai_status });
|
||||||
|
|
||||||
|
const topicName = 'Циклы for и while';
|
||||||
|
const withTopic = await api('/api/lesson-reports', {
|
||||||
|
token, method: 'POST',
|
||||||
|
body: { group_id: gid, lesson_date: '2019-05-18', lesson_time: '10:00', topic: topicName, text: 'Тема занятия для смоук-теста.', ai_check: false },
|
||||||
|
});
|
||||||
|
ok('lesson-report: тема занятия сохраняется при создании',
|
||||||
|
withTopic.status === 201 && withTopic.data.topic === topicName, { status: withTopic.status, topic: withTopic.data.topic });
|
||||||
|
|
||||||
|
const listed = await api(`/api/lesson-reports?group_id=${gid}&date_from=2019-05-18&date_to=2019-05-18`, { token });
|
||||||
|
ok('lesson-report: тема занятия в списке',
|
||||||
|
listed.status === 200 && (listed.data.items || []).some(i => i.topic === topicName),
|
||||||
|
{ status: listed.status, item: (listed.data.items || [])[0] && (listed.data.items || [])[0].topic });
|
||||||
|
|
||||||
|
const edited = await api(`/api/lesson-reports/${withTopic.data.id}`, {
|
||||||
|
token, method: 'PUT', body: { topic: 'Обновлённая тема' },
|
||||||
|
});
|
||||||
|
ok('lesson-report: тема занятия обновляется при редактировании',
|
||||||
|
edited.status === 200 && edited.data.topic === 'Обновлённая тема', { status: edited.status, topic: edited.data.topic });
|
||||||
|
|
||||||
|
const cleared = await api(`/api/lesson-reports/${withTopic.data.id}`, {
|
||||||
|
token, method: 'PUT', body: { topic: '' },
|
||||||
|
});
|
||||||
|
ok('lesson-report: тему занятия можно очистить',
|
||||||
|
cleared.status === 200 && !cleared.data.topic, { status: cleared.status, topic: cleared.data.topic });
|
||||||
|
|
||||||
|
const tooLong = await api(`/api/lesson-reports/${withTopic.data.id}`, {
|
||||||
|
token, method: 'PUT', body: { topic: 'я'.repeat(301) },
|
||||||
|
});
|
||||||
|
ok('lesson-report: слишком длинная тема -> 400', tooLong.status === 400, { status: tooLong.status, error: tooLong.data && tooLong.data.error });
|
||||||
|
await api(`/api/lesson-reports/${withTopic.data.id}`, { token, method: 'DELETE' });
|
||||||
|
|
||||||
const versions = await api(`/api/lesson-reports/${plain.data.id}/versions`, { token });
|
const versions = await api(`/api/lesson-reports/${plain.data.id}/versions`, { token });
|
||||||
ok('lesson-report: история версий содержит исходный текст',
|
ok('lesson-report: история версий содержит исходный текст',
|
||||||
versions.status === 200 && Array.isArray(versions.data.items) && versions.data.items.length >= 1
|
versions.status === 200 && Array.isArray(versions.data.items) && versions.data.items.length >= 1
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
const PHOTO_JOB_ACTIONS = new Set(['ai', 'ai_face', 'ai_upscale', 'enhance', 'restore', 'rollback']);
|
const PHOTO_JOB_ACTIONS = new Set(['ai', 'ai_face', 'ai_upscale', 'enhance', 'restore', 'rollback']);
|
||||||
const LESSON_REPORT_TEXT_MAX = 5000;
|
const LESSON_REPORT_TEXT_MAX = 5000;
|
||||||
|
const LESSON_REPORT_TOPIC_MAX = 300;
|
||||||
const BACKUP_FORMAT_VERSION = 2;
|
const BACKUP_FORMAT_VERSION = 2;
|
||||||
const BACKUP_MIN_FORMAT_VERSION = 1;
|
const BACKUP_MIN_FORMAT_VERSION = 1;
|
||||||
const BACKUP_TABLES = [
|
const BACKUP_TABLES = [
|
||||||
@@ -390,6 +391,7 @@ function normalizeRestoreData(data) {
|
|||||||
group_id: reqInt(x.group_id),
|
group_id: reqInt(x.group_id),
|
||||||
lesson_date: reqDate(x.lesson_date),
|
lesson_date: reqDate(x.lesson_date),
|
||||||
lesson_time: optTime(x.lesson_time),
|
lesson_time: optTime(x.lesson_time),
|
||||||
|
topic: optStr(x.topic, LESSON_REPORT_TOPIC_MAX),
|
||||||
text: reqStr(x.text, LESSON_REPORT_TEXT_MAX),
|
text: reqStr(x.text, LESSON_REPORT_TEXT_MAX),
|
||||||
text_original: optStr(x.text_original, LESSON_REPORT_TEXT_MAX),
|
text_original: optStr(x.text_original, LESSON_REPORT_TEXT_MAX),
|
||||||
text_ai: optStr(x.text_ai, LESSON_REPORT_TEXT_MAX),
|
text_ai: optStr(x.text_ai, LESSON_REPORT_TEXT_MAX),
|
||||||
@@ -466,6 +468,7 @@ function normalizeRestoreData(data) {
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
PHOTO_JOB_ACTIONS,
|
PHOTO_JOB_ACTIONS,
|
||||||
LESSON_REPORT_TEXT_MAX,
|
LESSON_REPORT_TEXT_MAX,
|
||||||
|
LESSON_REPORT_TOPIC_MAX,
|
||||||
SAFE_NAME,
|
SAFE_NAME,
|
||||||
isSafeUploadPath,
|
isSafeUploadPath,
|
||||||
photoRefKey,
|
photoRefKey,
|
||||||
|
|||||||
@@ -230,6 +230,7 @@ CREATE TABLE IF NOT EXISTS lesson_reports (
|
|||||||
group_id INT NOT NULL REFERENCES groups(id) ON DELETE CASCADE,
|
group_id INT NOT NULL REFERENCES groups(id) ON DELETE CASCADE,
|
||||||
lesson_date DATE NOT NULL,
|
lesson_date DATE NOT NULL,
|
||||||
lesson_time TIME,
|
lesson_time TIME,
|
||||||
|
topic TEXT,
|
||||||
text TEXT NOT NULL,
|
text TEXT NOT NULL,
|
||||||
text_original TEXT,
|
text_original TEXT,
|
||||||
text_ai TEXT,
|
text_ai TEXT,
|
||||||
|
|||||||
@@ -326,6 +326,7 @@ ALTER TABLE lesson_reports ADD COLUMN IF NOT EXISTS text_ai TEXT;
|
|||||||
ALTER TABLE lesson_reports ADD COLUMN IF NOT EXISTS ai_status VARCHAR(20) NOT NULL DEFAULT 'none';
|
ALTER TABLE lesson_reports ADD COLUMN IF NOT EXISTS ai_status VARCHAR(20) NOT NULL DEFAULT 'none';
|
||||||
ALTER TABLE lesson_reports ADD COLUMN IF NOT EXISTS ai_checked_at TIMESTAMPTZ;
|
ALTER TABLE lesson_reports ADD COLUMN IF NOT EXISTS ai_checked_at TIMESTAMPTZ;
|
||||||
ALTER TABLE lesson_reports ADD COLUMN IF NOT EXISTS ai_error TEXT;
|
ALTER TABLE lesson_reports ADD COLUMN IF NOT EXISTS ai_error TEXT;
|
||||||
|
ALTER TABLE lesson_reports ADD COLUMN IF NOT EXISTS topic TEXT;
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS lesson_report_versions (
|
CREATE TABLE IF NOT EXISTS lesson_report_versions (
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
|
|||||||
@@ -689,6 +689,7 @@ body[data-page=settings] .page-head{align-items:flex-end}
|
|||||||
.lesson-when i{width:13px;height:13px}
|
.lesson-when i{width:13px;height:13px}
|
||||||
.lesson-badge{font-size:.7rem;font-weight:700;padding:2px 9px;border-radius:999px;background:rgba(37,99,235,.1);color:var(--accent)}
|
.lesson-badge{font-size:.7rem;font-weight:700;padding:2px 9px;border-radius:999px;background:rgba(37,99,235,.1);color:var(--accent)}
|
||||||
.lesson-badge.today{background:rgba(16,185,129,.14);color:#059669}
|
.lesson-badge.today{background:rgba(16,185,129,.14);color:#059669}
|
||||||
|
.lesson-topic{font-size:.85rem;font-weight:600;color:var(--accent)}
|
||||||
.lesson-text{font-size:.88rem;color:var(--text);white-space:pre-wrap;word-break:break-word;line-height:1.55}
|
.lesson-text{font-size:.88rem;color:var(--text);white-space:pre-wrap;word-break:break-word;line-height:1.55}
|
||||||
.lesson-author{font-size:.75rem;color:var(--muted)}
|
.lesson-author{font-size:.75rem;color:var(--muted)}
|
||||||
.lesson-actions{display:flex;gap:8px;flex-wrap:wrap;margin-left:auto}
|
.lesson-actions{display:flex;gap:8px;flex-wrap:wrap;margin-left:auto}
|
||||||
|
|||||||
+11
-1
@@ -534,6 +534,11 @@ function ensureLessonModal() {
|
|||||||
<input type="time" id="lessonTime" class="settings-input">
|
<input type="time" id="lessonTime" class="settings-input">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="settings-field">
|
||||||
|
<label>Тема занятия</label>
|
||||||
|
<input type="text" id="lessonTopic" class="settings-input" maxlength="300"
|
||||||
|
placeholder="Например: циклы for и while, задачи на списки">
|
||||||
|
</div>
|
||||||
<div class="settings-field">
|
<div class="settings-field">
|
||||||
<label>Что прошли на занятии <span style="color:#ef4444">*</span></label>
|
<label>Что прошли на занятии <span style="color:#ef4444">*</span></label>
|
||||||
<textarea id="lessonText" class="settings-input" rows="6" maxlength="5000" required
|
<textarea id="lessonText" class="settings-input" rows="6" maxlength="5000" required
|
||||||
@@ -591,6 +596,7 @@ async function submitLessonModal(e) {
|
|||||||
const dateEl = modal.querySelector('#lessonDate');
|
const dateEl = modal.querySelector('#lessonDate');
|
||||||
const timeEl = modal.querySelector('#lessonTime');
|
const timeEl = modal.querySelector('#lessonTime');
|
||||||
const textEl = modal.querySelector('#lessonText');
|
const textEl = modal.querySelector('#lessonText');
|
||||||
|
const topicEl = modal.querySelector('#lessonTopic');
|
||||||
const submitBtn = modal.querySelector('#lessonSubmit');
|
const submitBtn = modal.querySelector('#lessonSubmit');
|
||||||
const aiCheckEl = modal.querySelector('#lessonAiCheck');
|
const aiCheckEl = modal.querySelector('#lessonAiCheck');
|
||||||
const groupId = groupSel.value;
|
const groupId = groupSel.value;
|
||||||
@@ -606,7 +612,7 @@ async function submitLessonModal(e) {
|
|||||||
const res = await fetch(`${API}/api/lesson-reports${id ? '/' + id : ''}`, {
|
const res = await fetch(`${API}/api/lesson-reports${id ? '/' + id : ''}`, {
|
||||||
method: id ? 'PUT' : 'POST',
|
method: id ? 'PUT' : 'POST',
|
||||||
headers: hdrJson(),
|
headers: hdrJson(),
|
||||||
body: JSON.stringify({ group_id: Number(groupId), lesson_date: date, lesson_time: timeEl.value || null, text, ai_check: aiCheck }),
|
body: JSON.stringify({ group_id: Number(groupId), lesson_date: date, lesson_time: timeEl.value || null, topic: topicEl.value.trim(), text, ai_check: aiCheck }),
|
||||||
});
|
});
|
||||||
const data = await res.json().catch(() => ({}));
|
const data = await res.json().catch(() => ({}));
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
@@ -675,6 +681,9 @@ function fillLessonModalFromReport(modal, r) {
|
|||||||
textEl.value = r.text || '';
|
textEl.value = r.text || '';
|
||||||
textEl.dispatchEvent(new Event('input'));
|
textEl.dispatchEvent(new Event('input'));
|
||||||
}
|
}
|
||||||
|
if (!modal.querySelector('#lessonTopic').value) {
|
||||||
|
modal.querySelector('#lessonTopic').value = r.topic || '';
|
||||||
|
}
|
||||||
const aiCheck = modal.querySelector('#lessonAiCheck');
|
const aiCheck = modal.querySelector('#lessonAiCheck');
|
||||||
if (aiCheck) aiCheck.checked = false;
|
if (aiCheck) aiCheck.checked = false;
|
||||||
updateLessonAiRow(modal, r);
|
updateLessonAiRow(modal, r);
|
||||||
@@ -713,6 +722,7 @@ function resetLessonModalFields(modal) {
|
|||||||
showLessonDuplicate(modal, null);
|
showLessonDuplicate(modal, null);
|
||||||
modal.querySelector('#lessonDate').value = lessonTodayIso();
|
modal.querySelector('#lessonDate').value = lessonTodayIso();
|
||||||
modal.querySelector('#lessonTime').value = lessonNowTime();
|
modal.querySelector('#lessonTime').value = lessonNowTime();
|
||||||
|
modal.querySelector('#lessonTopic').value = '';
|
||||||
const textEl = modal.querySelector('#lessonText');
|
const textEl = modal.querySelector('#lessonText');
|
||||||
textEl.value = '';
|
textEl.value = '';
|
||||||
textEl.dispatchEvent(new Event('input'));
|
textEl.dispatchEvent(new Event('input'));
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ function lessonItemHtml(r) {
|
|||||||
${isAdmin() ? `<button type="button" class="btn-link danger" data-del="${r.id}">Удалить</button>` : ''}
|
${isAdmin() ? `<button type="button" class="btn-link danger" data-del="${r.id}">Удалить</button>` : ''}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
${r.topic ? `<div class="lesson-topic">${esc(r.topic)}</div>` : ''}
|
||||||
<div class="lesson-text">${esc(r.text || '')}</div>
|
<div class="lesson-text">${esc(r.text || '')}</div>
|
||||||
${author ? `<div class="lesson-author">${esc(author)}</div>` : ''}
|
${author ? `<div class="lesson-author">${esc(author)}</div>` : ''}
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ const { buildEntryDiff, textDiff, normalizeEditSource, stripDiffs } = require('.
|
|||||||
const {
|
const {
|
||||||
PHOTO_JOB_ACTIONS,
|
PHOTO_JOB_ACTIONS,
|
||||||
LESSON_REPORT_TEXT_MAX,
|
LESSON_REPORT_TEXT_MAX,
|
||||||
|
LESSON_REPORT_TOPIC_MAX,
|
||||||
SAFE_NAME,
|
SAFE_NAME,
|
||||||
isSafeUploadPath,
|
isSafeUploadPath,
|
||||||
photoRefKey,
|
photoRefKey,
|
||||||
@@ -1307,6 +1308,7 @@ async function ensureLessonReportsTable() {
|
|||||||
group_id INT NOT NULL REFERENCES groups(id) ON DELETE CASCADE,
|
group_id INT NOT NULL REFERENCES groups(id) ON DELETE CASCADE,
|
||||||
lesson_date DATE NOT NULL,
|
lesson_date DATE NOT NULL,
|
||||||
lesson_time TIME,
|
lesson_time TIME,
|
||||||
|
topic TEXT,
|
||||||
text TEXT NOT NULL,
|
text TEXT NOT NULL,
|
||||||
text_original TEXT,
|
text_original TEXT,
|
||||||
text_ai TEXT,
|
text_ai TEXT,
|
||||||
@@ -1327,6 +1329,7 @@ async function ensureLessonReportsTable() {
|
|||||||
await pool.query("ALTER TABLE lesson_reports ADD COLUMN IF NOT EXISTS ai_status VARCHAR(20) NOT NULL DEFAULT 'none'");
|
await pool.query("ALTER TABLE lesson_reports ADD COLUMN IF NOT EXISTS ai_status VARCHAR(20) NOT NULL DEFAULT 'none'");
|
||||||
await pool.query('ALTER TABLE lesson_reports ADD COLUMN IF NOT EXISTS ai_checked_at TIMESTAMPTZ');
|
await pool.query('ALTER TABLE lesson_reports ADD COLUMN IF NOT EXISTS ai_checked_at TIMESTAMPTZ');
|
||||||
await pool.query('ALTER TABLE lesson_reports ADD COLUMN IF NOT EXISTS ai_error TEXT');
|
await pool.query('ALTER TABLE lesson_reports ADD COLUMN IF NOT EXISTS ai_error TEXT');
|
||||||
|
await pool.query('ALTER TABLE lesson_reports ADD COLUMN IF NOT EXISTS topic TEXT');
|
||||||
await pool.query('CREATE UNIQUE INDEX IF NOT EXISTS idx_lesson_reports_group_date ON lesson_reports(group_id, lesson_date)');
|
await pool.query('CREATE UNIQUE INDEX IF NOT EXISTS idx_lesson_reports_group_date ON lesson_reports(group_id, lesson_date)');
|
||||||
await pool.query('CREATE INDEX IF NOT EXISTS idx_lesson_reports_date ON lesson_reports(lesson_date DESC)');
|
await pool.query('CREATE INDEX IF NOT EXISTS idx_lesson_reports_date ON lesson_reports(lesson_date DESC)');
|
||||||
await pool.query(`CREATE TABLE IF NOT EXISTS lesson_report_versions (
|
await pool.query(`CREATE TABLE IF NOT EXISTS lesson_report_versions (
|
||||||
@@ -2474,8 +2477,8 @@ app.post('/api/restore', requireAdmin, uploadBackup.single('backup'), async (req
|
|||||||
const ex = await client.query('SELECT 1 FROM groups WHERE id = $1', [x.group_id]);
|
const ex = await client.query('SELECT 1 FROM groups WHERE id = $1', [x.group_id]);
|
||||||
if (!ex.rowCount) continue;
|
if (!ex.rowCount) continue;
|
||||||
await client.query(
|
await client.query(
|
||||||
'INSERT INTO lesson_reports (id, group_id, lesson_date, lesson_time, text, text_original, text_ai, ai_status, ai_checked_at, ai_error, author_id, branch_id, created_at, updated_at) VALUES ($1,$2,$3::date,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)',
|
'INSERT INTO lesson_reports (id, group_id, lesson_date, lesson_time, topic, text, text_original, text_ai, ai_status, ai_checked_at, ai_error, author_id, branch_id, created_at, updated_at) VALUES ($1,$2,$3::date,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15)',
|
||||||
[x.id, x.group_id, x.lesson_date, x.lesson_time, x.text, x.text_original, x.text_ai, x.ai_status || 'none', x.ai_checked_at, x.ai_error, x.author_id, x.branch_id, x.created_at, x.updated_at]
|
[x.id, x.group_id, x.lesson_date, x.lesson_time, x.topic, x.text, x.text_original, x.text_ai, x.ai_status || 'none', x.ai_checked_at, x.ai_error, x.author_id, x.branch_id, x.created_at, x.updated_at]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
for (const x of ndata.lesson_report_versions) {
|
for (const x of ndata.lesson_report_versions) {
|
||||||
@@ -3624,7 +3627,7 @@ app.get('/api/lesson-reports', requireAuth, async (req, res) => {
|
|||||||
const key = ['lessons:list:', scopeKey(req.user), date_from || '', date_to || '', group_id || '', search || '', limit || '', offset || ''].join(':');
|
const key = ['lessons:list:', scopeKey(req.user), date_from || '', date_to || '', group_id || '', search || '', limit || '', offset || ''].join(':');
|
||||||
const rows = await cacheWrap(key, LESSON_REPORT_LIST_TTL_MS, async () => {
|
const rows = await cacheWrap(key, LESSON_REPORT_LIST_TTL_MS, async () => {
|
||||||
const qparams = params.slice();
|
const qparams = params.slice();
|
||||||
let q = `SELECT lr.id, lr.group_id, lr.lesson_date, lr.lesson_time, lr.text, lr.author_id,
|
let q = `SELECT lr.id, lr.group_id, lr.lesson_date, lr.lesson_time, lr.topic, lr.text, lr.author_id,
|
||||||
lr.text_original, lr.text_ai, lr.ai_status, lr.ai_checked_at, lr.ai_error,
|
lr.text_original, lr.text_ai, lr.ai_status, lr.ai_checked_at, lr.ai_error,
|
||||||
lr.created_at, lr.updated_at, g.name AS group_name,
|
lr.created_at, lr.updated_at, g.name AS group_name,
|
||||||
u.name AS author_name, u.username AS author_username
|
u.name AS author_name, u.username AS author_username
|
||||||
@@ -3647,13 +3650,15 @@ app.get('/api/lesson-reports/:id', requireAuth, async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
app.post('/api/lesson-reports', requireAuth, async (req, res) => {
|
app.post('/api/lesson-reports', requireAuth, async (req, res) => {
|
||||||
const { group_id, lesson_date, lesson_time, text, ai_check } = req.body || {};
|
const { group_id, lesson_date, lesson_time, topic, text, ai_check } = req.body || {};
|
||||||
const grp = await lessonReportGroup(req.user, group_id);
|
const grp = await lessonReportGroup(req.user, group_id);
|
||||||
if (grp.error) return res.status(grp.code || 400).json({ error: grp.error });
|
if (grp.error) return res.status(grp.code || 400).json({ error: grp.error });
|
||||||
const date = parseLessonReportDate(lesson_date);
|
const date = parseLessonReportDate(lesson_date);
|
||||||
if (!date) return res.status(400).json({ error: 'Некорректная дата занятия' });
|
if (!date) return res.status(400).json({ error: 'Некорректная дата занятия' });
|
||||||
const time = parseLessonReportTime(lesson_time);
|
const time = parseLessonReportTime(lesson_time);
|
||||||
if (time === undefined) return res.status(400).json({ error: 'Некорректное время занятия' });
|
if (time === undefined) return res.status(400).json({ error: 'Некорректное время занятия' });
|
||||||
|
const topicText = typeof topic === 'string' ? topic.trim() : '';
|
||||||
|
if (topicText.length > LESSON_REPORT_TOPIC_MAX) return res.status(400).json({ error: `Тема занятия длиннее ${LESSON_REPORT_TOPIC_MAX} символов` });
|
||||||
const body = typeof text === 'string' ? text.trim() : '';
|
const body = typeof text === 'string' ? text.trim() : '';
|
||||||
if (!body) return res.status(400).json({ error: 'Введите текст отчёта' });
|
if (!body) return res.status(400).json({ error: 'Введите текст отчёта' });
|
||||||
if (body.length > LESSON_REPORT_TEXT_MAX) return res.status(400).json({ error: `Текст отчёта длиннее ${LESSON_REPORT_TEXT_MAX} символов` });
|
if (body.length > LESSON_REPORT_TEXT_MAX) return res.status(400).json({ error: `Текст отчёта длиннее ${LESSON_REPORT_TEXT_MAX} символов` });
|
||||||
@@ -3666,10 +3671,10 @@ app.post('/api/lesson-reports', requireAuth, async (req, res) => {
|
|||||||
}
|
}
|
||||||
const aiWanted = ai_check === true && (await getSetting('lesson_ai_enabled', 'true')) !== 'false';
|
const aiWanted = ai_check === true && (await getSetting('lesson_ai_enabled', 'true')) !== 'false';
|
||||||
const { rows } = await pool.query(
|
const { rows } = await pool.query(
|
||||||
`INSERT INTO lesson_reports (group_id, lesson_date, lesson_time, text, text_original, text_ai, ai_status, ai_checked_at, ai_error, author_id, branch_id)
|
`INSERT INTO lesson_reports (group_id, lesson_date, lesson_time, topic, text, text_original, text_ai, ai_status, ai_checked_at, ai_error, author_id, branch_id)
|
||||||
VALUES ($1, $2::date, $3, $4, $5, NULL, $6::text, CASE WHEN $6::text = 'none' THEN NULL ELSE now() END, NULL, $7, $8) RETURNING *`,
|
VALUES ($1, $2::date, $3, $4, $5, $6, NULL, $7::text, CASE WHEN $7::text = 'none' THEN NULL ELSE now() END, NULL, $8, $9) RETURNING *`,
|
||||||
[
|
[
|
||||||
grp.group.id, date, time, body,
|
grp.group.id, date, time, topicText || null, body,
|
||||||
aiWanted ? body : null,
|
aiWanted ? body : null,
|
||||||
aiWanted ? 'pending' : 'none',
|
aiWanted ? 'pending' : 'none',
|
||||||
req.user.id || null,
|
req.user.id || null,
|
||||||
@@ -3695,7 +3700,7 @@ app.post('/api/lesson-reports', requireAuth, async (req, res) => {
|
|||||||
app.put('/api/lesson-reports/:id', requireAuth, async (req, res) => {
|
app.put('/api/lesson-reports/:id', requireAuth, async (req, res) => {
|
||||||
const { report, error, code } = await lessonReportById(req.user, req.params.id);
|
const { report, error, code } = await lessonReportById(req.user, req.params.id);
|
||||||
if (!report) return res.status(code || 404).json({ error });
|
if (!report) return res.status(code || 404).json({ error });
|
||||||
const { lesson_date, lesson_time, text, ai_check } = req.body || {};
|
const { lesson_date, lesson_time, topic, text, ai_check } = req.body || {};
|
||||||
const curDate = String(report.lesson_date).slice(0, 10);
|
const curDate = String(report.lesson_date).slice(0, 10);
|
||||||
let date = curDate;
|
let date = curDate;
|
||||||
if (lesson_date !== undefined && lesson_date !== null && lesson_date !== '') {
|
if (lesson_date !== undefined && lesson_date !== null && lesson_date !== '') {
|
||||||
@@ -3710,6 +3715,11 @@ app.put('/api/lesson-reports/:id', requireAuth, async (req, res) => {
|
|||||||
const body = text === undefined ? null : (typeof text === 'string' ? text.trim() : '');
|
const body = text === undefined ? null : (typeof text === 'string' ? text.trim() : '');
|
||||||
if (text !== undefined && !body) return res.status(400).json({ error: 'Введите текст отчёта' });
|
if (text !== undefined && !body) return res.status(400).json({ error: 'Введите текст отчёта' });
|
||||||
if (body && body.length > LESSON_REPORT_TEXT_MAX) return res.status(400).json({ error: `Текст отчёта длиннее ${LESSON_REPORT_TEXT_MAX} символов` });
|
if (body && body.length > LESSON_REPORT_TEXT_MAX) return res.status(400).json({ error: `Текст отчёта длиннее ${LESSON_REPORT_TEXT_MAX} символов` });
|
||||||
|
let topicText;
|
||||||
|
if (topic !== undefined) {
|
||||||
|
topicText = typeof topic === 'string' ? topic.trim() : '';
|
||||||
|
if (topicText.length > LESSON_REPORT_TOPIC_MAX) return res.status(400).json({ error: `Тема занятия длиннее ${LESSON_REPORT_TOPIC_MAX} символов` });
|
||||||
|
}
|
||||||
if (date !== curDate) {
|
if (date !== curDate) {
|
||||||
const { rows: clash } = await pool.query(
|
const { rows: clash } = await pool.query(
|
||||||
'SELECT id FROM lesson_reports WHERE group_id = $1 AND lesson_date = $2 AND id <> $3',
|
'SELECT id FROM lesson_reports WHERE group_id = $1 AND lesson_date = $2 AND id <> $3',
|
||||||
@@ -3723,15 +3733,16 @@ app.put('/api/lesson-reports/:id', requireAuth, async (req, res) => {
|
|||||||
`UPDATE lesson_reports SET
|
`UPDATE lesson_reports SET
|
||||||
lesson_date = $1::date,
|
lesson_date = $1::date,
|
||||||
lesson_time = $2,
|
lesson_time = $2,
|
||||||
text = COALESCE($3, text),
|
topic = CASE WHEN $6::boolean THEN $3::text ELSE topic END,
|
||||||
text_original = CASE WHEN $4::boolean THEN $3::text ELSE text_original END,
|
text = COALESCE($4, text),
|
||||||
text_ai = CASE WHEN $4::boolean THEN NULL ELSE text_ai END,
|
text_original = CASE WHEN $5::boolean THEN $4::text ELSE text_original END,
|
||||||
ai_status = CASE WHEN $4::boolean THEN 'pending'::varchar ELSE ai_status END,
|
text_ai = CASE WHEN $5::boolean THEN NULL ELSE text_ai END,
|
||||||
ai_checked_at = CASE WHEN $4::boolean THEN now() ELSE ai_checked_at END,
|
ai_status = CASE WHEN $5::boolean THEN 'pending'::varchar ELSE ai_status END,
|
||||||
ai_error = CASE WHEN $4::boolean THEN NULL ELSE ai_error END,
|
ai_checked_at = CASE WHEN $5::boolean THEN now() ELSE ai_checked_at END,
|
||||||
|
ai_error = CASE WHEN $5::boolean THEN NULL ELSE ai_error END,
|
||||||
updated_at = now()
|
updated_at = now()
|
||||||
WHERE id = $5 RETURNING *`,
|
WHERE id = $7 RETURNING *`,
|
||||||
[date, nextTime, body, aiWanted, report.id]
|
[date, nextTime, topicText === undefined ? null : (topicText || null), body, aiWanted, topic !== undefined, report.id]
|
||||||
);
|
);
|
||||||
const updated = rows[0];
|
const updated = rows[0];
|
||||||
if (body) await saveLessonReportVersion(report.id, body, 'manual', req.user.id);
|
if (body) await saveLessonReportVersion(report.id, body, 'manual', req.user.id);
|
||||||
|
|||||||
@@ -809,7 +809,7 @@ function createLessonReportChecker({ pool, getSetting, logAudit, aiUrl, model, d
|
|||||||
try {
|
try {
|
||||||
await client.query('BEGIN');
|
await client.query('BEGIN');
|
||||||
const { rows } = await client.query(
|
const { rows } = await client.query(
|
||||||
`SELECT lr.id, lr.text, lr.text_original, lr.lesson_date, lr.lesson_time, lr.author_id,
|
`SELECT lr.id, lr.text, lr.text_original, lr.lesson_date, lr.lesson_time, lr.topic, lr.author_id,
|
||||||
g.name AS group_name, g.branch_id
|
g.name AS group_name, g.branch_id
|
||||||
FROM lesson_reports lr
|
FROM lesson_reports lr
|
||||||
JOIN groups g ON g.id = lr.group_id
|
JOIN groups g ON g.id = lr.group_id
|
||||||
@@ -928,10 +928,12 @@ async function finish(row, status, text, error, aiText) {
|
|||||||
const date = String(row.lesson_date).slice(0, 10);
|
const date = String(row.lesson_date).slice(0, 10);
|
||||||
const time = row.lesson_time ? String(row.lesson_time).slice(0, 5) : '';
|
const time = row.lesson_time ? String(row.lesson_time).slice(0, 5) : '';
|
||||||
const input = original.length > MAX_INPUT_CHARS ? original.slice(0, MAX_INPUT_CHARS) : original;
|
const input = original.length > MAX_INPUT_CHARS ? original.slice(0, MAX_INPUT_CHARS) : original;
|
||||||
|
const topic = String(row.topic || '').trim();
|
||||||
const context = [
|
const context = [
|
||||||
`Группа: ${row.group_name || '—'}`,
|
`Группа: ${row.group_name || '—'}`,
|
||||||
`Дата занятия: ${date}`,
|
`Дата занятия: ${date}`,
|
||||||
time ? `Время занятия: ${time}` : '',
|
time ? `Время занятия: ${time}` : '',
|
||||||
|
topic ? `Тема занятия: ${topic}` : '',
|
||||||
'',
|
'',
|
||||||
'Текст отчёта:',
|
'Текст отчёта:',
|
||||||
input,
|
input,
|
||||||
|
|||||||
Reference in New Issue
Block a user