feat(export): отчёты о занятиях в ZIP-профиле резидента
В архив экспорта студента (GET /api/export/student, кнопка «Профиль» на /students.html) добавлена секция «Отчёты о занятиях»: тема, дата, время, группа и текст конспекта преподавателя. Секция рендерится между «Занятия» и «О себе», попадает в навигацию и футер, появляется только если есть хотя бы один отчёт. Отчёты привязаны к группе (в lesson_reports нет student_id), поэтому выбираются по группам резидента: текущая группа плюс все группы, где у него есть записи. Фильтры филиалов (branchWhere) и период из date_from/date_to соблюдаются, как и в остальной выборке. Ограничение 60 последних отчётов, при усечении в подзаголовке секции есть соответствующая пометка. Новый флаг include_reports (по умолчанию включён) + чекбокс в модалке экспорта. Даты lesson_date/lesson_time форматируются как чистые DATE/TIME строки, без new Date(), чтобы не сдвинуть день; учёт настройки time_format сделан через appHour12. Заодно в отчёте и в сообщениях эндпоинта «ученик» заменён на «резидент». Co-Authored-By: Cline <cline@cline.bot>
This commit is contained in:
@@ -144,6 +144,7 @@ function openExportModal(name) {
|
||||
document.getElementById('exportForm').reset();
|
||||
document.getElementById('exportDateFrom').value = '';
|
||||
document.getElementById('expEntries').checked = true;
|
||||
document.getElementById('expReports').checked = true;
|
||||
document.getElementById('expPhotos').checked = true;
|
||||
document.getElementById('expCaptions').checked = true;
|
||||
document.getElementById('expFiles').checked = true;
|
||||
@@ -168,6 +169,7 @@ async function exportStudent(name, opts) {
|
||||
if (opts.dateFrom) q.set('date_from', opts.dateFrom);
|
||||
if (opts.dateTo) q.set('date_to', opts.dateTo);
|
||||
q.set('include_entries', opts.entries ? '1' : '0');
|
||||
q.set('include_reports', opts.reports === false ? '0' : '1');
|
||||
q.set('include_photos', opts.photos ? '1' : '0');
|
||||
q.set('include_captions', opts.captions ? '1' : '0');
|
||||
q.set('include_files', opts.files ? '1' : '0');
|
||||
@@ -192,6 +194,7 @@ async function submitExport() {
|
||||
const captionsEl = document.getElementById('expCaptions');
|
||||
const opts = {
|
||||
entries: document.getElementById('expEntries').checked,
|
||||
reports: document.getElementById('expReports').checked,
|
||||
photos: document.getElementById('expPhotos').checked,
|
||||
files: document.getElementById('expFiles').checked,
|
||||
captions: captionsEl.checked && !captionsEl.disabled,
|
||||
|
||||
@@ -73,6 +73,12 @@
|
||||
Записи занятий (текст)
|
||||
</label>
|
||||
</div>
|
||||
<div class="settings-field full">
|
||||
<label style="display:flex;align-items:center;gap:8px;cursor:pointer">
|
||||
<input type="checkbox" id="expReports" checked>
|
||||
Отчёты о занятиях (конспекты преподавателя)
|
||||
</label>
|
||||
</div>
|
||||
<div class="settings-field full">
|
||||
<label style="display:flex;align-items:center;gap:8px;cursor:pointer">
|
||||
<input type="checkbox" id="expPhotos" checked>
|
||||
|
||||
@@ -4158,13 +4158,15 @@ app.get('/api/export/student', requireAuth, async (req, res) => {
|
||||
try {
|
||||
name = reqStr(req.query.name, 150);
|
||||
} catch {
|
||||
return res.status(400).json({ error: 'Укажите имя ученика' });
|
||||
return res.status(400).json({ error: 'Укажите имя резидента' });
|
||||
}
|
||||
const LESSON_REPORTS_LIMIT = 60;
|
||||
const opts = {
|
||||
includeEntries: req.query.include_entries !== '0',
|
||||
includePhotos: req.query.include_photos !== '0',
|
||||
includeFiles: req.query.include_files !== '0',
|
||||
includeCaptions: req.query.include_captions !== '0',
|
||||
includeReports: req.query.include_reports !== '0',
|
||||
showDates: req.query.show_dates !== '0',
|
||||
};
|
||||
if (!opts.includeEntries && !opts.includePhotos && !opts.includeFiles) {
|
||||
@@ -4211,7 +4213,7 @@ app.get('/api/export/student', requireAuth, async (req, res) => {
|
||||
WHERE s.name = $1`,
|
||||
[name]
|
||||
),
|
||||
pool.query(`SELECT e.id, e.description, e.created_at, e.module_id, e.ai_status, g.name AS group_name, m.name AS module_name
|
||||
pool.query(`SELECT e.id, e.description, e.created_at, e.module_id, e.ai_status, e.group_id, g.name AS group_name, m.name AS module_name
|
||||
FROM entries e JOIN groups g ON g.id = e.group_id
|
||||
LEFT JOIN modules m ON m.id = e.module_id${whereStr}
|
||||
ORDER BY e.created_at DESC`, params),
|
||||
@@ -4247,12 +4249,41 @@ app.get('/api/export/student', requireAuth, async (req, res) => {
|
||||
]);
|
||||
const entryRows = entriesRes.rows;
|
||||
if (!entryRows.length && !photosRes.rows.length && !filesRes.rows.length) {
|
||||
return res.status(404).json({ error: hasPeriod ? 'Нет данных за выбранный период' : 'У ученика нет данных для отчёта' });
|
||||
return res.status(404).json({ error: hasPeriod ? 'Нет данных за выбранный период' : 'У резидента нет данных для отчёта' });
|
||||
}
|
||||
const student = studRes.rows[0] || { name };
|
||||
const studentPhotosRes = student.id
|
||||
? await pool.query('SELECT photo_path, created_at FROM student_photos WHERE student_id = $1 ORDER BY id DESC', [student.id])
|
||||
: { rows: [] };
|
||||
const lessonReportsRes = opts.includeReports
|
||||
? await (async () => {
|
||||
const groupIds = [...new Set([student.group_id, ...entryRows.map(e => e.group_id)].filter(id => Number.isInteger(id)))];
|
||||
if (!groupIds.length) return { rows: [], truncated: false };
|
||||
const conds = ['lr.group_id = ANY($1::int[])'];
|
||||
const rp = [groupIds];
|
||||
if (dateFrom) {
|
||||
rp.push(dateFrom);
|
||||
conds.push(`lr.lesson_date >= $${rp.length}`);
|
||||
}
|
||||
if (dateTo) {
|
||||
rp.push(dateTo);
|
||||
conds.push(`lr.lesson_date <= $${rp.length}`);
|
||||
}
|
||||
if (bw.params.length) {
|
||||
const start = rp.length + 1;
|
||||
conds.push(`g.branch_id IN (${bw.params.map((_, i) => '$' + (start + i)).join(',')})`);
|
||||
rp.push(...bw.params);
|
||||
}
|
||||
const { rows } = await pool.query(
|
||||
`SELECT lr.id, lr.group_id, lr.lesson_date, lr.lesson_time, lr.topic, lr.text, g.name AS group_name
|
||||
FROM lesson_reports lr
|
||||
JOIN groups g ON g.id = lr.group_id
|
||||
WHERE ${conds.join(' AND ')}
|
||||
ORDER BY lr.lesson_date DESC, lr.lesson_time DESC NULLS LAST
|
||||
LIMIT $${rp.length + 1}`, [...rp, LESSON_REPORTS_LIMIT + 1]);
|
||||
return { rows, truncated: rows.length > LESSON_REPORTS_LIMIT };
|
||||
})()
|
||||
: { rows: [], truncated: false };
|
||||
const zip = createZipWriter();
|
||||
if (opts.includePhotos) zip.addDir('photos');
|
||||
if (opts.includeFiles) zip.addDir('files');
|
||||
@@ -4410,6 +4441,9 @@ app.get('/api/export/student', requireAuth, async (req, res) => {
|
||||
groupPhotos: groupPhotosBuilt,
|
||||
studentPhotos: studentPhotosBuilt,
|
||||
files: filesBuilt,
|
||||
lessonReports: opts.includeReports ? lessonReportsRes.rows.slice(0, LESSON_REPORTS_LIMIT) : [],
|
||||
lessonReportsTruncated: opts.includeReports && lessonReportsRes.truncated,
|
||||
hour12: await appHour12(),
|
||||
generatedAt: new Date(),
|
||||
period,
|
||||
logo: logoFile,
|
||||
@@ -4421,12 +4455,13 @@ app.get('/api/export/student', requireAuth, async (req, res) => {
|
||||
student: reportData.student,
|
||||
profile: student.profile || null,
|
||||
period: period || null,
|
||||
totals: { entries: entryRows.length, modules: modulesRes.rows.length, photos: photosBuilt.length, files: filesBuilt.length, groupPhotos: groupPhotosBuilt.length, studentPhotos: studentPhotosBuilt.length },
|
||||
totals: { entries: entryRows.length, modules: modulesRes.rows.length, photos: photosBuilt.length, files: filesBuilt.length, groupPhotos: groupPhotosBuilt.length, studentPhotos: studentPhotosBuilt.length, lessonReports: reportData.lessonReports.length },
|
||||
options: opts,
|
||||
entries: entryRows.map(e => ({ id: e.id, group: e.group_name, module: e.module_name || null, created_at: e.created_at, ai_status: e.ai_status, description: e.description })),
|
||||
photos: photosBuilt.map(p => ({ file: 'photos/' + p.stored, caption: p.caption, created_at: p.createdAt, entry_id: p.entryId })),
|
||||
student_photos: studentPhotosBuilt.map(p => ({ file: 'photos/' + p.stored, main: !!p.main, created_at: p.createdAt })),
|
||||
files: filesBuilt.map(f => ({ file: 'files/' + f.saved, name: f.original, size: f.size, created_at: f.createdAt, entry_id: f.entryId })),
|
||||
lesson_reports: reportData.lessonReports.map(r => ({ id: r.id, group_id: r.group_id, group_name: r.group_name, lesson_date: r.lesson_date, lesson_time: r.lesson_time, topic: r.topic || null, text: r.text })),
|
||||
};
|
||||
zip.addFile('data.json', Buffer.from(JSON.stringify(meta, null, 2), 'utf8'));
|
||||
|
||||
@@ -4436,6 +4471,7 @@ app.get('/api/export/student', requireAuth, async (req, res) => {
|
||||
entries: entryRows.length,
|
||||
photos: photosBuilt.length,
|
||||
files: filesBuilt.length,
|
||||
lesson_reports: reportData.lessonReports.length,
|
||||
opts,
|
||||
date_from: dateFrom,
|
||||
date_to: dateTo,
|
||||
|
||||
+107
-15
@@ -122,6 +122,23 @@ function longDate(iso) {
|
||||
return d.getDate() + ' ' + MONTHS_GEN[d.getMonth()] + ' ' + d.getFullYear();
|
||||
}
|
||||
|
||||
function isoDateLabel(value) {
|
||||
const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(String(value || '').trim());
|
||||
if (!m) return '';
|
||||
const mi = Number(m[2]) - 1;
|
||||
if (mi < 0 || mi > 11) return '';
|
||||
return Number(m[3]) + ' ' + MONTHS_GEN[mi] + ' ' + m[1];
|
||||
}
|
||||
|
||||
function hmLabel(value, hour12) {
|
||||
const m = /^(\d{1,2}):(\d{2})/.exec(String(value || '').trim());
|
||||
if (!m) return '';
|
||||
const h = Number(m[1]);
|
||||
if (h > 23) return '';
|
||||
if (!hour12) return (h < 10 ? '0' + h : String(h)) + ':' + m[2];
|
||||
return (h % 12 === 0 ? 12 : h % 12) + ':' + m[2] + (h < 12 ? ' AM' : ' PM');
|
||||
}
|
||||
|
||||
function monthYear(iso) {
|
||||
const d = asDate(iso);
|
||||
if (!d) return '';
|
||||
@@ -236,6 +253,25 @@ function iconForGroup(title) {
|
||||
return 'sparkles';
|
||||
}
|
||||
|
||||
function iconForReport(topic) {
|
||||
const t = String(topic || '').toLowerCase();
|
||||
if (/защит|практи|лаборатор|проект|работа/.test(t)) return 'hammer';
|
||||
if (/разбор|рефактор|дебаг|код|ревью/.test(t)) return 'code-2';
|
||||
if (/введ|теори|лекц|знакомств/.test(t)) return 'book-open-check';
|
||||
return 'clipboard-list';
|
||||
}
|
||||
|
||||
function paragraphs(value) {
|
||||
return String(value === null || value === undefined ? '' : value)
|
||||
.replace(/\r\n?/g, '\n')
|
||||
.split(/\n{2,}/)
|
||||
.map(p => p.split('\n').map(x => x.trim()).filter(Boolean).join(' '))
|
||||
.map(p => p.trim())
|
||||
.filter(Boolean)
|
||||
.slice(0, 40)
|
||||
.map(p => p.slice(0, 2000));
|
||||
}
|
||||
|
||||
function buildProfileView(data, opts) {
|
||||
const o = opts || {};
|
||||
const showEntries = o.includeEntries !== false;
|
||||
@@ -243,6 +279,8 @@ function buildProfileView(data, opts) {
|
||||
const showFiles = o.includeFiles !== false;
|
||||
const showCaptions = o.includeCaptions !== false;
|
||||
const showDates = o.showDates !== false;
|
||||
const showReports = o.includeReports !== false;
|
||||
const hour12 = data.hour12 === true;
|
||||
const student = data.student || {};
|
||||
const profile = (data.profile && typeof data.profile === 'object' && !Array.isArray(data.profile)) ? data.profile : {};
|
||||
const entries = arr(data.entries);
|
||||
@@ -297,7 +335,7 @@ function buildProfileView(data, opts) {
|
||||
|
||||
const studentPhotoCards = showPhotos ? arr(data.studentPhotos).map(sp => {
|
||||
const src = 'photos/' + sp.stored;
|
||||
const g = galleryAdd('image', src, sp.main ? 'Главное фото' : 'Фото ученика');
|
||||
const g = galleryAdd('image', src, sp.main ? 'Главное фото' : 'Фото резидента');
|
||||
return {
|
||||
src,
|
||||
g,
|
||||
@@ -361,6 +399,17 @@ function buildProfileView(data, opts) {
|
||||
};
|
||||
}) : [];
|
||||
|
||||
const lessonReports = showReports ? arr(data.lessonReports).map(r => {
|
||||
const date = showDates ? isoDateLabel(r.lesson_date) : '';
|
||||
const time = showDates ? hmLabel(r.lesson_time, hour12) : '';
|
||||
return {
|
||||
icon: iconForReport(r.topic),
|
||||
title: str(r.topic, 300) || 'Отчёт о занятии',
|
||||
sub: [date, time, str(r.group_name, 90)].filter(Boolean).join(' · '),
|
||||
paragraphs: paragraphs(r.text),
|
||||
};
|
||||
}).filter(r => r.paragraphs.length || r.sub) : [];
|
||||
|
||||
const groupSet = new Set();
|
||||
if (student.group_name) groupSet.add(student.group_name);
|
||||
for (const e of entries) if (e.group_name) groupSet.add(e.group_name);
|
||||
@@ -457,6 +506,7 @@ function buildProfileView(data, opts) {
|
||||
if (showPhotos && photos.length) autoAchievements.push(photos.length + ' ' + plural(photos.length, 'фотография', 'фотографии', 'фотографий') + ' в архиве занятий');
|
||||
if (showFiles && files.length) autoAchievements.push(files.length + ' ' + plural(files.length, 'работа', 'работы', 'работ') + ' в портфолио');
|
||||
if (education.length) autoAchievements.push('Пройдено тем программы: ' + education.length + ' — ' + education.slice(0, 3).map(m => m.title).join(', '));
|
||||
if (lessonReports.length) autoAchievements.push('Отчётов о занятиях: ' + lessonReports.length);
|
||||
const achievements = profAchievements.length
|
||||
? profAchievements.map(a => str(a, 200)).filter(Boolean)
|
||||
: autoAchievements;
|
||||
@@ -488,7 +538,7 @@ function buildProfileView(data, opts) {
|
||||
const logo = /^[a-zA-Z0-9._-]+\.[a-zA-Z0-9]{1,8}$/.test(String(data.logo || '')) ? String(data.logo) : null;
|
||||
|
||||
return {
|
||||
name, student, profile, entries, education, projects, photoCards, groupPhotoCards, studentPhotoCards, tasks, gallery, groups, stats,
|
||||
name, student, profile, entries, education, projects, photoCards, groupPhotoCards, studentPhotoCards, tasks, lessonReports, gallery, groups, stats,
|
||||
skillGroups, experience, contacts, achievements, facts, heroTags,
|
||||
avatar, cover, idLabel, mainPhotoCard, profilePhotoCard, logo,
|
||||
role: str(profile.role, 200),
|
||||
@@ -739,6 +789,20 @@ svg{display:block;flex:none}
|
||||
.about__list{display:flex;flex-direction:column;gap:10px}
|
||||
.about__list li{display:flex;gap:10px;align-items:flex-start;font-size:.86rem;font-weight:600;color:var(--muted)}
|
||||
.about__list svg{width:17px;height:17px;color:var(--ok);margin-top:2px;flex:none}
|
||||
.lesson-reports{display:flex;flex-direction:column;gap:12px}
|
||||
.lesson-report{padding:clamp(14px,1.8vw,20px);display:flex;flex-direction:column;gap:12px;min-width:0;transition:border-color .2s ease}
|
||||
.lesson-report:hover{border-color:var(--stroke-strong)}
|
||||
.lesson-report__head{display:flex;align-items:flex-start;gap:12px;min-width:0}
|
||||
.lesson-report__icon{width:40px;height:40px;flex:none;display:grid;place-items:center;border-radius:12px;background:rgba(109,140,255,.16);border:1px solid rgba(109,140,255,.34);color:var(--brand)}
|
||||
.lesson-report__icon svg{width:19px;height:19px;stroke-width:2.2}
|
||||
.lesson-report__headmain{min-width:0;display:flex;flex-direction:column;gap:4px}
|
||||
.lesson-report__title{font-size:1.02rem;font-weight:800;overflow-wrap:anywhere}
|
||||
.lesson-report__sub{display:flex;align-items:center;gap:7px;font-size:.79rem;font-weight:600;color:var(--muted);flex-wrap:wrap;overflow-wrap:anywhere}
|
||||
.lesson-report__sub svg{width:14px;height:14px;flex:none}
|
||||
.lesson-report__text{display:flex;flex-direction:column;gap:9px;padding-top:12px;border-top:1px solid var(--stroke)}
|
||||
.lesson-report__text p{font-size:.92rem;line-height:1.6;color:var(--muted);overflow-wrap:anywhere}
|
||||
@media (max-width:520px){.lesson-report__icon{width:34px;height:34px;border-radius:10px}.lesson-report__icon svg{width:17px;height:17px}.lesson-report__title{font-size:.95rem}}
|
||||
@media print{.lesson-report{page-break-inside:avoid}.lesson-reports{gap:10px}}
|
||||
.empty{display:flex;flex-direction:column;align-items:center;gap:10px;padding:clamp(22px,3vw,38px);text-align:center;color:var(--muted);font-size:.9rem;font-weight:600}
|
||||
.empty svg{width:30px;height:30px;stroke-width:1.8;opacity:.8}
|
||||
.empty b{color:var(--text);font-weight:800;font-size:.98rem}
|
||||
@@ -845,7 +909,7 @@ function heroHtml(v) {
|
||||
actions.push('<button class="btn btn--primary" type="button" data-action="print">' + iconTag('printer') + 'Печать / PDF</button>');
|
||||
const mail = v.contacts.find(c => (c.href || '').toLowerCase().startsWith('mailto:')) || v.contacts.find(c => c.href);
|
||||
if (mail) actions.push('<a class="btn" href="' + escapeHtml(mail.href) + '">' + iconTag('send') + 'Написать</a>');
|
||||
if (v.studentPhotoCards.length) actions.push('<a class="btn btn--ghost" href="#student-photos">' + iconTag('user-round') + 'Фото ученика</a>');
|
||||
if (v.studentPhotoCards.length) actions.push('<a class="btn btn--ghost" href="#student-photos">' + iconTag('user-round') + 'Фото резидента</a>');
|
||||
if (v.photoCards.length) actions.push('<a class="btn btn--ghost" href="#photos">' + iconTag('images') + 'Фотографии</a>');
|
||||
const facts = v.facts.length
|
||||
? v.facts.map(f => '<span class="fact">' + iconTag(f.icon) + '<span>' + escapeHtml(f.text) + '</span></span>').join('')
|
||||
@@ -957,7 +1021,7 @@ function groupPhotosHtml(v) {
|
||||
|
||||
function studentPhotosHtml(v) {
|
||||
if (!v.studentPhotoCards.length) {
|
||||
return emptyBlock('image', 'Фотографий ученика пока нет',
|
||||
return emptyBlock('image', 'Фотографий резидента пока нет',
|
||||
'Личные фотографии резидента появятся здесь, как только их добавят в данных профиля.');
|
||||
}
|
||||
const total = v.studentPhotoCards.length;
|
||||
@@ -966,7 +1030,7 @@ function studentPhotosHtml(v) {
|
||||
mediaBlock(p.src, '4x3', v.name + ' — фото', p.g) +
|
||||
'<div class="prj-card__body">' +
|
||||
'<span class="badge ' + (p.main ? 'badge--ok' : 'badge--info') + '">' +
|
||||
iconTag(p.main ? 'star' : 'image') + (p.main ? 'Главное фото' : 'Фото ученика') + '</span>' +
|
||||
iconTag(p.main ? 'star' : 'image') + (p.main ? 'Главное фото' : 'Фото резидента') + '</span>' +
|
||||
'<h3 class="prj-card__title">' + escapeHtml(total > 1 ? 'Фото ' + (i + 1) + ' из ' + total : 'Фотография резидента') + '</h3>' +
|
||||
'<p class="prj-card__text">' + escapeHtml(
|
||||
(p.main ? 'Личная фотография резидента' : 'Дополнительная фотография резидента') +
|
||||
@@ -1000,6 +1064,25 @@ function tasksHtml(v) {
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function lessonReportsHtml(v) {
|
||||
if (!v.lessonReports.length) {
|
||||
return emptyBlock('notebook-pen', 'Отчётов о занятиях пока нет',
|
||||
'Конспекты занятий группы появятся здесь, как только их заполнит преподаватель.');
|
||||
}
|
||||
return v.lessonReports.map(r =>
|
||||
'<article class="card lesson-report reveal">' +
|
||||
'<div class="lesson-report__head">' +
|
||||
'<span class="lesson-report__icon">' + iconTag(r.icon) + '</span>' +
|
||||
'<div class="lesson-report__headmain">' +
|
||||
'<h3 class="lesson-report__title">' + escapeHtml(r.title) + '</h3>' +
|
||||
(r.sub ? '<p class="lesson-report__sub">' + iconTag('calendar-days') + escapeHtml(r.sub) + '</p>' : '') +
|
||||
'</div></div>' +
|
||||
(r.paragraphs.length
|
||||
? '<div class="lesson-report__text">' + r.paragraphs.map(p => '<p>' + escapeHtml(p) + '</p>').join('') + '</div>'
|
||||
: '') +
|
||||
'</article>').join('');
|
||||
}
|
||||
|
||||
function aboutHtml(v) {
|
||||
const bio = '<article class="card about__bio reveal">' +
|
||||
'<h3 class="section__title" style="font-size:1.15rem">' + iconTag('user-round') + 'Коротко о себе</h3>' +
|
||||
@@ -1039,9 +1122,10 @@ function aboutHtml(v) {
|
||||
function footerHtml(v, generatedAt) {
|
||||
const links = [];
|
||||
if (v.entries.length) links.push('<a href="#lessons">Занятия</a>');
|
||||
if (v.studentPhotoCards.length) links.push('<a href="#student-photos">Фото ученика</a>');
|
||||
if (v.studentPhotoCards.length) links.push('<a href="#student-photos">Фото резидента</a>');
|
||||
if (v.photoCards.length) links.push('<a href="#photos">Фотографии</a>');
|
||||
if (v.groupPhotoCards.length) links.push('<a href="#group-photos">Фото группы</a>');
|
||||
if (v.lessonReports.length) links.push('<a href="#lesson-reports">Отчёты о занятиях</a>');
|
||||
if (v.projects.length) links.push('<a href="#projects">Работы</a>');
|
||||
links.push('<a href="#top">Наверх</a>');
|
||||
return '<footer class="footer"><p>© <span id="year"></span> KIBERone · профиль резидента ' + escapeHtml(v.name) +
|
||||
@@ -1052,15 +1136,16 @@ function footerHtml(v, generatedAt) {
|
||||
function navItems(v) {
|
||||
const all = [
|
||||
{ id: 'profile', icon: 'user-round', label: 'Профиль', rank: 0 },
|
||||
{ id: 'student-photos', icon: 'images', label: 'Фото ученика', rank: 1 },
|
||||
{ id: 'student-photos', icon: 'images', label: 'Фото резидента', rank: 1 },
|
||||
{ id: 'lessons', icon: 'list-checks', label: 'Занятия', rank: 2 },
|
||||
{ id: 'projects', icon: 'folder-git-2', label: 'Работы', rank: 3 },
|
||||
{ id: 'education', icon: 'book-open', label: 'Образование', rank: 4 },
|
||||
{ id: 'about', icon: 'info', label: 'О себе', rank: 5 },
|
||||
{ id: 'photos', icon: 'images', label: 'Фотографии', rank: 6 },
|
||||
{ id: 'group-photos', icon: 'users-round', label: 'Фото группы', rank: 7 },
|
||||
{ id: 'skills', icon: 'sparkles', label: 'Навыки', rank: 8 },
|
||||
{ id: 'experience', icon: 'briefcase', label: 'Опыт', rank: 9 },
|
||||
{ id: 'lesson-reports', icon: 'notebook-pen', label: 'Отчёты о занятиях', rank: 3 },
|
||||
{ id: 'projects', icon: 'folder-git-2', label: 'Работы', rank: 4 },
|
||||
{ id: 'education', icon: 'book-open', label: 'Образование', rank: 5 },
|
||||
{ id: 'about', icon: 'info', label: 'О себе', rank: 6 },
|
||||
{ id: 'photos', icon: 'images', label: 'Фотографии', rank: 7 },
|
||||
{ id: 'group-photos', icon: 'users-round', label: 'Фото группы', rank: 8 },
|
||||
{ id: 'skills', icon: 'sparkles', label: 'Навыки', rank: 9 },
|
||||
{ id: 'experience', icon: 'briefcase', label: 'Опыт', rank: 10 },
|
||||
];
|
||||
const sections = {
|
||||
'profile': true,
|
||||
@@ -1072,6 +1157,7 @@ function navItems(v) {
|
||||
'skills': v.skillGroups.length > 0,
|
||||
'projects': v.projects.length > 0,
|
||||
'lessons': v.tasks.length > 0,
|
||||
'lesson-reports': v.lessonReports.length > 0,
|
||||
'about': true,
|
||||
};
|
||||
return all
|
||||
@@ -1218,7 +1304,7 @@ function renderStudentReport(data, opts) {
|
||||
(periodChip ? '<div class="hero__tags">' + periodChip + '</div>' : '') +
|
||||
'<div class="stats">' + statsHtml(v) + '</div></section>');
|
||||
sections.push('<section class="section" id="student-photos">' +
|
||||
sectionHead('user-round', 'Фотографии ученика', v.studentPhotoCards.length > 1
|
||||
sectionHead('user-round', 'Фотографии резидента', v.studentPhotoCards.length > 1
|
||||
? 'Личные фотографии резидента. Нажмите «Просмотр», чтобы открыть снимок крупнее.'
|
||||
: 'Личная фотография резидента. Нажмите «Просмотр», чтобы открыть её крупнее.') +
|
||||
studentPhotosHtml(v) + '</section>');
|
||||
@@ -1247,6 +1333,12 @@ function renderStudentReport(data, opts) {
|
||||
sections.push('<section class="section" id="lessons">' +
|
||||
sectionHead('list-checks', 'Занятия', 'Записи журнала: что резидент делал, фотографии и файлы занятий.') +
|
||||
'<div class="tasks">' + tasksHtml(v) + '</div></section>');
|
||||
if (v.lessonReports.length) {
|
||||
sections.push('<section class="section" id="lesson-reports">' +
|
||||
sectionHead('notebook-pen', 'Отчёты о занятиях', 'Что проходили на занятиях группы: тема, конспект и разбор, зафиксированные преподавателем.' +
|
||||
(data.lessonReportsTruncated ? ' Показаны последние ' + v.lessonReports.length + ' отчётов.' : '')) +
|
||||
'<div class="lesson-reports">' + lessonReportsHtml(v) + '</div></section>');
|
||||
}
|
||||
sections.push('<section class="section" id="about">' +
|
||||
sectionHead('info', 'Немного о себе', 'Рассказ резидента, контакты и достижения.') +
|
||||
'<div class="about">' + aboutHtml(v) + '</div></section>');
|
||||
|
||||
Reference in New Issue
Block a user