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:
dev
2026-09-18 18:56:42 +03:00
parent d434732f41
commit e6291a0235
10 changed files with 455 additions and 18 deletions
+26
View File
@@ -4,6 +4,8 @@ const preview = document.getElementById('preview');
const placeholderSrc = preview.src;
const removePhotoBtn = document.getElementById('removePhotoBtn');
const groupInput = document.getElementById('groupInput');
const moduleInput = document.getElementById('moduleInput');
const moduleField = document.getElementById('moduleField');
const toast = document.getElementById('toast');
const camVideo = document.getElementById('camVideo');
const camCanvas = document.getElementById('camCanvas');
@@ -133,6 +135,23 @@ if (params.get('nameInput')) {
}
(async () => {
try {
const mRes = await fetch(`${API}/api/modules`);
if (mRes.ok) {
const mData = await mRes.json();
const list = mData.modules || [];
if (list.length) {
list.forEach(m => {
const opt = document.createElement('option');
opt.value = m.id;
opt.textContent = m.lessons_count > 0 ? `${m.name} · ${m.lessons_count} зан.` : m.name;
moduleInput.appendChild(opt);
});
moduleInput.required = true;
moduleField.style.display = '';
}
}
} catch (e) { /* ignore */ }
const res = await fetch(`${API}/api/students`);
const students = await res.json();
const me = studentName ? students.find(s => s.name === studentName) : null;
@@ -219,6 +238,12 @@ form.addEventListener('submit', async (e) => {
setTimeout(() => toast.classList.remove('show', 'error'), 3200);
return;
}
if (moduleField.style.display !== 'none' && !moduleInput.value) {
toast.textContent = 'Выберите тему модуля';
toast.classList.add('show', 'error');
setTimeout(() => toast.classList.remove('show', 'error'), 3200);
return;
}
const btn = document.getElementById('submitBtn');
btn.disabled = true;
btn.textContent = 'Отправка...';
@@ -228,6 +253,7 @@ form.addEventListener('submit', async (e) => {
selectedFiles.forEach(f => fd.append('files', f, f.name));
fd.append('student_name', studentName || document.getElementById('nameInput').value);
fd.append('group_id', groupInput.value);
fd.append('module_id', moduleInput.value);
fd.append('description', document.getElementById('descInput').value);
fd.append('website', document.getElementById('hpWebsite').value);