feat(modules): module topics for student form, admin CRUD with pagination
- db: modules table (name, lessons_count) + entries.module_id (ON DELETE SET NULL), migration + idempotent startup ensure - api: GET /api/modules (public, search + limit/offset, entries_count), POST/PUT/DELETE (admin, audit-logged) - entries: accept/validate module_id on create/update, return module_name, module_id filter - backup/restore: include modules and entries.module_id - student form: required module select, hidden while no modules exist - admin: modules.html + js/modules.js list with pagination, search, create/edit/delete modal - journal: module filter, module select in edit modal, module badge, CSV column
This commit is contained in:
+40
-2
@@ -4,6 +4,8 @@ let highlightNewIds = new Set();
|
||||
let highlightClearTimer = null;
|
||||
let editId = null;
|
||||
let editGroups = [];
|
||||
let editModules = [];
|
||||
let modulesLoaded = false;
|
||||
let groupsLoaded = false;
|
||||
let studentsLoaded = false;
|
||||
const PAGE_SIZE = 20;
|
||||
@@ -99,7 +101,7 @@ function gridEntryHTML(e) {
|
||||
${mp ? `<img src="${thumbSrc(mp)}" data-img="${API}${mp}" loading="lazy" decoding="async" alt="">` : ''}
|
||||
<div class="info">
|
||||
<div class="name">${esc(e.student_name)}</div>
|
||||
<div class="group">${esc(e.group_name)}</div>
|
||||
<div class="group">${esc(e.group_name)}${e.module_name ? ' · ' + esc(e.module_name) : ''}</div>
|
||||
<div class="desc">${esc(e.description)}${aiBadge(e)}</div>
|
||||
${filesHTML(e.files)}
|
||||
<div class="time">${new Date(e.created_at).toLocaleString('ru')}</div>
|
||||
@@ -119,6 +121,7 @@ function cardEntryHTML(e) {
|
||||
? `<img src="${thumbSrc(mp)}" data-img="${API}${mp}" loading="lazy" decoding="async" alt="">`
|
||||
: `<div class="no-photo"><svg viewBox="0 0 240 160" preserveAspectRatio="xMidYMid meet" aria-hidden="true"><circle cx="105" cy="48" r="15"/><path d="M74 140 C74 106 88 92 105 92 C122 92 136 106 136 140 Z"/><circle cx="145" cy="58" r="18"/><path d="M108 144 C108 104 124 88 145 88 C166 88 182 104 182 144 Z"/></svg></div>`}
|
||||
${e.group_name ? `<span class="group-badge" title="${esc(e.group_name)}">${esc(e.group_name)}</span>` : ''}
|
||||
${e.module_name ? `<span class="group-badge" style="left:auto;right:8px;background:rgba(37,99,235,.8)" title="Модуль: ${esc(e.module_name)}">${esc(e.module_name)}</span>` : ''}
|
||||
<div class="body">
|
||||
<div class="name">${esc(e.student_name)}</div>
|
||||
<div class="desc">${esc(e.description)}${aiBadge(e)}</div>
|
||||
@@ -216,6 +219,31 @@ async function loadGroupOptions() {
|
||||
groupsLoaded = true;
|
||||
}
|
||||
|
||||
async function loadModuleOptions() {
|
||||
const res = await fetch(`${API}/api/modules`);
|
||||
if (!res.ok) return;
|
||||
const data = await res.json();
|
||||
editModules = data.modules || [];
|
||||
modulesLoaded = true;
|
||||
const filt = document.getElementById('moduleFilter');
|
||||
if (filt) {
|
||||
while (filt.options.length > 1) filt.remove(1);
|
||||
editModules.forEach(m => {
|
||||
const o = document.createElement('option');
|
||||
o.value = m.id; o.textContent = m.name;
|
||||
filt.appendChild(o);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function fillEditModuleSelect(selected) {
|
||||
const sel = document.getElementById('editModule');
|
||||
if (!sel) return;
|
||||
sel.innerHTML = '<option value="">— не выбрано —</option>' +
|
||||
editModules.map(m => `<option value="${m.id}">${esc(m.name)}</option>`).join('');
|
||||
sel.value = selected ? String(selected) : '';
|
||||
}
|
||||
|
||||
async function loadStudentOptions() {
|
||||
if (studentsLoaded) return;
|
||||
const dl = document.getElementById('studentFilterList');
|
||||
@@ -237,6 +265,8 @@ async function loadEntries() {
|
||||
if (gid) p.set('group_id', gid);
|
||||
if (df) p.set('date_from', df);
|
||||
if (dt) p.set('date_to', dt);
|
||||
const mid = document.getElementById('moduleFilter') ? document.getElementById('moduleFilter').value : '';
|
||||
if (mid) p.set('module_id', mid);
|
||||
p.set('limit', PAGE_SIZE);
|
||||
p.set('offset', (page - 1) * PAGE_SIZE);
|
||||
const res = await fetch(`${API}/api/entries?${p}`, { headers: hdr() });
|
||||
@@ -337,6 +367,8 @@ async function openEdit(id) {
|
||||
sel.innerHTML = editGroups.map(g => `<option value="${g.id}">${esc(g.name)}</option>`).join('');
|
||||
}
|
||||
sel.value = e.group_id;
|
||||
if (!modulesLoaded) await loadModuleOptions();
|
||||
fillEditModuleSelect(e.module_id);
|
||||
renderEditPhotos(e.photos || [], e.photo_path);
|
||||
document.getElementById('editModal').classList.add('open');
|
||||
}
|
||||
@@ -1130,6 +1162,7 @@ async function saveEdit() {
|
||||
formData.append('student_name', name);
|
||||
formData.append('group_id', group_id);
|
||||
formData.append('description', description);
|
||||
formData.append('module_id', document.getElementById('editModule').value);
|
||||
|
||||
const photoFiles = document.getElementById('editPhoto').files;
|
||||
for (const f of photoFiles) formData.append('photo', f);
|
||||
@@ -1173,6 +1206,7 @@ async function delEntry(id) {
|
||||
|
||||
document.getElementById('studentFilter').addEventListener('change', () => { page = 1; loadEntries(); });
|
||||
document.getElementById('groupFilter').addEventListener('change', () => { page = 1; loadEntries(); });
|
||||
document.getElementById('moduleFilter').addEventListener('change', () => { page = 1; loadEntries(); });
|
||||
document.getElementById('dateFrom').addEventListener('change', () => { page = 1; loadEntries(); });
|
||||
document.getElementById('dateTo').addEventListener('change', () => { page = 1; loadEntries(); });
|
||||
document.getElementById('searchInput').addEventListener('input', () => {
|
||||
@@ -1227,14 +1261,17 @@ async function exportCSV() {
|
||||
if (gid) p.set('group_id', gid);
|
||||
if (df) p.set('date_from', df);
|
||||
if (dt) p.set('date_to', dt);
|
||||
const mid = document.getElementById('moduleFilter') ? document.getElementById('moduleFilter').value : '';
|
||||
if (mid) p.set('module_id', mid);
|
||||
const res = await fetch(`${API}/api/entries?${p}`, { headers: hdr() });
|
||||
const entries = (await res.json()).entries;
|
||||
if (!entries.length) { alert('Нет данных для экспорта'); return; }
|
||||
const rows = [['Фамилия и имя', 'Группа', 'Описание', 'Дата', 'Фото']];
|
||||
const rows = [['Фамилия и имя', 'Группа', 'Тема модуля', 'Описание', 'Дата', 'Фото']];
|
||||
entries.forEach(e => {
|
||||
rows.push([
|
||||
e.student_name,
|
||||
e.group_name,
|
||||
e.module_name || '',
|
||||
e.description.replace(/\n/g, ' '),
|
||||
new Date(e.created_at).toLocaleString('ru'),
|
||||
e.photo_path ? `${API}${e.photo_path}` : ''
|
||||
@@ -1440,6 +1477,7 @@ document.getElementById('linkCopyBtn').addEventListener('click', copyLink);
|
||||
});
|
||||
setDefaultDates();
|
||||
loadGroupOptions();
|
||||
loadModuleOptions();
|
||||
loadStudentOptions();
|
||||
loadEntries();
|
||||
connectLiveUpdates();
|
||||
|
||||
Reference in New Issue
Block a user