feat(journal): настраиваемые поля карточки отчёта по ученику
Добавлен плавающий виджет «Поля карточки» в журнале: набор видимых полей карточки отчёта по ученику (фото, фамилия, имя, группа, модуль, тема, дата, файлы) задаётся пользователем и сохраняется в localStorage. Имя теперь рендерится двумя спанами, чтобы скрывать частично; добавлен блок мета-информации с датой и чипами файлов — картинки открываются в лайтбоксе, играбельное видео воспроизводится в #videoModal через ?play=1, остальное скачивается. Каталог полей вынесен в REPORT_CARD_FIELDS как единственный источник правды, без дублирования в HTML. Также добавлена документация HTTP API (API.md, 45 разделов): адреса и транспорт, аутентификация сессиями и API-ключами, лимиты частоты, дата/время/часовой пояс, матрица соответствия и полный индекс эндпоинтов с ссылками на строки server.js.
This commit is contained in:
+142
-8
@@ -48,12 +48,75 @@ function mainPhotoPath(e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function openReportCard(id) {
|
||||
const e = currentEntries.find(x => x.id === id);
|
||||
if (!e) return;
|
||||
const mp = mainPhotoPath(e);
|
||||
const REPORT_CARD_FIELDS = [
|
||||
{ key: 'photo', label: 'Фото' },
|
||||
{ key: 'lastname', label: 'Фамилия' },
|
||||
{ key: 'firstname', label: 'Имя' },
|
||||
{ key: 'group', label: 'Группа' },
|
||||
{ key: 'module', label: 'Модуль' },
|
||||
{ key: 'topic', label: 'Тема' },
|
||||
{ key: 'date', label: 'Дата и время' },
|
||||
{ key: 'files', label: 'Файлы' },
|
||||
];
|
||||
const REPORT_CARD_FIELDS_KEY = 'reportCardFields';
|
||||
const reportCardFields = loadReportCardFields();
|
||||
let reportCardEntryId = null;
|
||||
|
||||
function loadReportCardFields() {
|
||||
const all = REPORT_CARD_FIELDS.map(f => f.key);
|
||||
try {
|
||||
const raw = JSON.parse(localStorage.getItem(REPORT_CARD_FIELDS_KEY) || 'null');
|
||||
if (Array.isArray(raw)) return all.filter(k => raw.includes(k));
|
||||
} catch (err) {
|
||||
return all;
|
||||
}
|
||||
return all;
|
||||
}
|
||||
|
||||
function saveReportCardFields() {
|
||||
try {
|
||||
localStorage.setItem(REPORT_CARD_FIELDS_KEY, JSON.stringify(reportCardFields));
|
||||
} catch (err) {
|
||||
showToast('Не удалось сохранить настройки полей', true);
|
||||
}
|
||||
}
|
||||
|
||||
function reportCardFieldOn(key) {
|
||||
return reportCardFields.includes(key);
|
||||
}
|
||||
|
||||
function splitStudentName(full) {
|
||||
const s = String(full || '').trim();
|
||||
const i = s.indexOf(' ');
|
||||
if (i < 0) return { lastname: s, firstname: '' };
|
||||
return { lastname: s.slice(0, i), firstname: s.slice(i + 1).trim() };
|
||||
}
|
||||
|
||||
function reportCardFilesHTML(e) {
|
||||
if (!e.files || !e.files.length) return '';
|
||||
return `<div class="student-card__meta-files">${e.files.map(f => {
|
||||
const url = `${API}/api/files/${f.token}`;
|
||||
if (isImageFile(f.name)) return `<a href="#" data-img="${url}" title="Открыть">${esc(f.name)}</a>`;
|
||||
if (isPlayableVideoFile(f.name)) return `<a href="${url}?play=1" data-video="${url}?play=1" data-video-name="${esc(f.name)}" data-video-token="${f.token}" title="Смотреть">${esc(f.name)}</a>`;
|
||||
const note = isVideoFile(f.name) ? 'Скачать (браузер не умеет играть этот формат)' : 'Скачать';
|
||||
return `<a href="${url}" download title="${note}">${esc(f.name)}</a>`;
|
||||
}).join('')}</div>`;
|
||||
}
|
||||
|
||||
function reportCardMetaHTML(e) {
|
||||
let html = '';
|
||||
if (reportCardFieldOn('date') && e.created_at) {
|
||||
html += `<div class="student-card__meta-val">${esc(fmtFull(e.created_at))}</div>`;
|
||||
}
|
||||
if (reportCardFieldOn('files')) html += reportCardFilesHTML(e);
|
||||
return html;
|
||||
}
|
||||
|
||||
function renderReportCard(e) {
|
||||
const photo = document.getElementById('reportCardPhoto');
|
||||
const noPhoto = document.getElementById('reportCardNoPhoto');
|
||||
document.getElementById('reportCardMedia').hidden = !reportCardFieldOn('photo');
|
||||
const mp = mainPhotoPath(e);
|
||||
if (mp) {
|
||||
photo.src = thumbSrc(mp);
|
||||
photo.dataset.img = `${API}${mp}`;
|
||||
@@ -67,21 +130,47 @@ function openReportCard(id) {
|
||||
photo.hidden = true;
|
||||
noPhoto.hidden = false;
|
||||
}
|
||||
document.getElementById('reportCardName').textContent = e.student_name || '';
|
||||
document.getElementById('reportCardText').textContent = e.description || '';
|
||||
const name = splitStudentName(e.student_name);
|
||||
const lastEl = document.getElementById('reportCardLastName');
|
||||
const firstEl = document.getElementById('reportCardFirstName');
|
||||
lastEl.textContent = reportCardFieldOn('lastname') ? name.lastname : '';
|
||||
firstEl.textContent = reportCardFieldOn('firstname') ? name.firstname : '';
|
||||
document.getElementById('reportCardName').hidden = !lastEl.textContent && !firstEl.textContent;
|
||||
const topic = e.description || '';
|
||||
const topicEl = document.getElementById('reportCardText');
|
||||
topicEl.textContent = topic;
|
||||
topicEl.hidden = !reportCardFieldOn('topic') || !topic;
|
||||
const groupBadge = document.getElementById('reportCardTime');
|
||||
const group = e.group_name || '';
|
||||
groupBadge.textContent = group;
|
||||
groupBadge.parentElement.hidden = !group;
|
||||
groupBadge.parentElement.hidden = !reportCardFieldOn('group') || !group;
|
||||
const toolBadge = document.getElementById('reportCardModule').parentElement;
|
||||
const tool = e.module_name || '';
|
||||
document.getElementById('reportCardModule').textContent = tool;
|
||||
toolBadge.hidden = !tool;
|
||||
toolBadge.hidden = !reportCardFieldOn('module') || !tool;
|
||||
document.getElementById('reportCardMeta').innerHTML = reportCardMetaHTML(e);
|
||||
}
|
||||
|
||||
function refreshReportCard() {
|
||||
if (reportCardEntryId === null) return;
|
||||
const e = currentEntries.find(x => x.id === reportCardEntryId);
|
||||
if (e) renderReportCard(e);
|
||||
}
|
||||
|
||||
function openReportCard(id) {
|
||||
const e = currentEntries.find(x => x.id === id);
|
||||
if (!e) return;
|
||||
reportCardEntryId = id;
|
||||
renderReportCard(e);
|
||||
document.getElementById('reportCardModal').classList.add('open');
|
||||
document.body.classList.add('report-card-open');
|
||||
}
|
||||
|
||||
function closeReportCard() {
|
||||
reportCardEntryId = null;
|
||||
document.getElementById('reportCardModal').classList.remove('open');
|
||||
document.body.classList.remove('report-card-open');
|
||||
setReportFieldsOpen(false);
|
||||
}
|
||||
|
||||
function filesHTML(files) {
|
||||
@@ -1555,9 +1644,54 @@ document.getElementById('linkModal').addEventListener('click', e => { if (e.targ
|
||||
document.getElementById('reportCardModal').addEventListener('click', e => { if (e.target === e.currentTarget) closeReportCard(); });
|
||||
document.addEventListener('keydown', e => {
|
||||
if (e.key !== 'Escape') return;
|
||||
if (document.getElementById('reportFields').classList.contains('open')) { setReportFieldsOpen(false); return; }
|
||||
if (document.getElementById('reportCardModal').classList.contains('open')) closeReportCard();
|
||||
});
|
||||
|
||||
function setReportCardFields(keys) {
|
||||
reportCardFields.length = 0;
|
||||
REPORT_CARD_FIELDS.map(f => f.key).filter(k => keys.includes(k)).forEach(k => reportCardFields.push(k));
|
||||
saveReportCardFields();
|
||||
renderReportFieldsList();
|
||||
refreshReportCard();
|
||||
}
|
||||
|
||||
function renderReportFieldsList() {
|
||||
const list = document.getElementById('reportFieldsList');
|
||||
if (!list) return;
|
||||
list.innerHTML = REPORT_CARD_FIELDS.map(f => `
|
||||
<label class="report-fields__item">
|
||||
<input type="checkbox" data-report-field="${f.key}"${reportCardFieldOn(f.key) ? ' checked' : ''}>
|
||||
<span>${esc(f.label)}</span>
|
||||
</label>`).join('');
|
||||
}
|
||||
|
||||
const reportFieldsRoot = document.getElementById('reportFields');
|
||||
const reportFieldsBtn = document.getElementById('reportFieldsBtn');
|
||||
const reportFieldsPanel = document.getElementById('reportFieldsPanel');
|
||||
|
||||
function setReportFieldsOpen(open) {
|
||||
reportFieldsRoot.classList.toggle('open', open);
|
||||
reportFieldsBtn.setAttribute('aria-expanded', open ? 'true' : 'false');
|
||||
}
|
||||
|
||||
renderReportFieldsList();
|
||||
reportFieldsBtn.addEventListener('click', () => setReportFieldsOpen(!reportFieldsRoot.classList.contains('open')));
|
||||
reportFieldsPanel.addEventListener('change', e => {
|
||||
const cb = e.target.closest('[data-report-field]');
|
||||
if (!cb) return;
|
||||
const key = cb.dataset.reportField;
|
||||
const keys = cb.checked
|
||||
? REPORT_CARD_FIELDS.map(f => f.key).filter(k => reportCardFieldOn(k) || k === key)
|
||||
: reportCardFields.filter(k => k !== key);
|
||||
setReportCardFields(keys);
|
||||
});
|
||||
document.getElementById('reportFieldsAll').addEventListener('click', () => setReportCardFields(REPORT_CARD_FIELDS.map(f => f.key)));
|
||||
document.getElementById('reportFieldsNone').addEventListener('click', () => setReportCardFields([]));
|
||||
document.addEventListener('click', e => {
|
||||
if (!reportFieldsRoot.contains(e.target)) setReportFieldsOpen(false);
|
||||
});
|
||||
|
||||
document.getElementById('grid').addEventListener('click', e => {
|
||||
const det = e.target.closest('[data-detach]');
|
||||
if (det) { detachFile(parseInt(det.dataset.detach, 10)); return; }
|
||||
|
||||
Reference in New Issue
Block a user