/
Coderdev
/
web-nodejs-labs
Обзор
Документация
Войти
/
Coderdev
/
web-nodejs-labs
Код
Запросы
0
Задачи
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
public/lab10/task2/index.html
850 строк
42 KB
Coderdev
1
12 май 2026, 12:34
12 май 2026, 12:34
f5e899f
Код
Авторство
О чём код?
<!DOCTYPE html> <html lang="ru"> <head> <meta charset="UTF-8"> <title>Задание 2 — Расписание</title> <style> *{box-sizing:border-box;margin:0;padding:0} body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:#f0f2f5;color:#1e293b} .app{display:grid;grid-template-columns:210px 1fr;min-height:100vh} /* ── Sidebar ── */ .sidebar{background:#1e293b;color:#e2e8f0;padding:16px 0;display:flex;flex-direction:column} .sidebar-logo{padding:0 16px 16px;font-size:1rem;font-weight:700;color:#fff;border-bottom:1px solid #334155} .sidebar-section{padding:12px 16px 4px;font-size:0.7rem;text-transform:uppercase; letter-spacing:1px;color:#64748b;font-weight:600} .sidebar a{display:block;padding:8px 16px;color:#cbd5e1;text-decoration:none; font-size:0.85rem;cursor:pointer;transition:background .12s} .sidebar a:hover{background:#334155;color:#fff} .sidebar a.active{background:#2563eb;color:#fff} /* ── Main ── */ .main{padding:20px;overflow-y:auto} .page-head{display:flex;align-items:center;gap:12px;margin-bottom:16px;flex-wrap:wrap} .page-head h1{font-size:1.25rem;font-weight:700;flex:1} /* ── Cards ── */ .card{background:#fff;border-radius:12px;border:1px solid #e2e8f0;overflow:hidden;margin-bottom:14px} .card-head{padding:11px 16px;border-bottom:1px solid #f1f5f9;display:flex; align-items:center;justify-content:space-between} .card-head h2{font-size:0.9rem;font-weight:700} .card-body{padding:14px} /* ── Inputs / Buttons ── */ input[type=text],input[type=number],select{ padding:7px 10px;border:1px solid #e2e8f0;border-radius:8px; font-size:0.83rem;background:#fff;width:100%} input:focus,select:focus{outline:2px solid #2563eb;border-color:#2563eb} label{font-size:0.77rem;color:#64748b;font-weight:500;display:flex;flex-direction:column;gap:3px} button{padding:6px 13px;border:none;border-radius:8px;cursor:pointer; font-size:0.83rem;font-weight:500;transition:background .15s} .btn{background:#2563eb;color:#fff}.btn:hover{background:#1d4ed8} .btn-sm{padding:3px 8px;font-size:0.76rem} .btn-outline{background:#fff;border:1px solid #e2e8f0;color:#475569}.btn-outline:hover{background:#f8fafc} .btn-danger{background:#dc2626;color:#fff}.btn-danger:hover{background:#b91c1c} button:disabled{opacity:.4;cursor:not-allowed} /* ── Table ── */ table{width:100%;border-collapse:collapse} th{background:#f8fafc;padding:8px 10px;text-align:left;font-size:0.76rem; color:#64748b;font-weight:600;border-bottom:1px solid #f1f5f9} td{padding:8px 10px;border-bottom:1px solid #f8fafc;font-size:0.82rem;vertical-align:middle} tr:last-child td{border-bottom:none} tr:hover td{background:#fafafa} .td-actions{white-space:nowrap;display:flex;gap:4px} .empty-td{padding:24px;text-align:center;color:#94a3b8;font-size:0.85rem} /* ── Inline form row ── */ .form-row{display:flex;gap:8px;align-items:flex-end;flex-wrap:wrap} .form-row label{flex:1;min-width:120px} .form-grid{display:grid;gap:8px} .form-grid-2{grid-template-columns:1fr 1fr} .form-grid-3{grid-template-columns:1fr 1fr 1fr} /* ── Status ── */ .status{padding:6px 10px;border-radius:8px;font-size:0.8rem;margin-top:8px} .ok{background:#dcfce7;color:#15803d} .err{background:#fee2e2;color:#b91c1c} /* ── Schedule view ── */ .week-row{display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-bottom:10px} .wtab{padding:5px 16px;border-radius:20px;cursor:pointer;font-size:0.85rem; border:1px solid #cbd5e1;background:#fff;color:#475569;transition:all .15s} .wtab.active{background:#2563eb;color:#fff;border-color:#2563eb;font-weight:600} .gtab{padding:4px 13px;border-radius:20px;cursor:pointer;font-size:0.82rem; border:1px solid #cbd5e1;background:#fff;color:#475569;transition:all .15s} .gtab.active{background:#0f172a;color:#fff;border-color:#0f172a} .filters-row{display:flex;gap:8px;flex-wrap:wrap;align-items:flex-end; background:#fff;border-radius:10px;border:1px solid #e2e8f0; padding:10px 12px;margin-bottom:12px} .filters-row label{font-size:0.73rem;color:#64748b;display:flex;flex-direction:column;gap:2px} .filters-row select{min-width:120px} .sched-card{background:#fff;border-radius:12px;border:1px solid #e2e8f0;overflow:hidden} .day-head{background:#f8fafc;padding:9px 14px;font-weight:700;font-size:0.85rem; color:#1e293b;border-bottom:1px solid #e2e8f0;display:flex;align-items:center;gap:8px} .date-badge{background:#1e293b;color:#fff;font-size:0.7rem;padding:2px 7px;border-radius:10px} .lesson-row{display:grid;grid-template-columns:85px 1fr;border-bottom:1px solid #f1f5f9} .lesson-row:last-child{border-bottom:none} .time-cell{padding:9px 10px;border-right:1px solid #f1f5f9;display:flex;flex-direction:column;gap:2px} .time-num{font-size:0.7rem;color:#94a3b8;font-weight:600} .time-str{font-size:0.75rem;color:#475569;line-height:1.4} .entries-cell{padding:0} .entry{padding:8px 12px;border-bottom:1px solid #f8fafc;display:flex;flex-direction:column;gap:3px} .entry:last-child{border-bottom:none} .entry-top{display:flex;align-items:center;gap:7px;flex-wrap:wrap} .disc-name{font-size:0.85rem;font-weight:600} .badge{display:inline-block;padding:1px 6px;border-radius:10px;font-size:0.68rem;font-weight:600} .badge-lec{background:#dbeafe;color:#1d4ed8} .badge-prac{background:#dcfce7;color:#15803d} .badge-sport{background:#fae8ff;color:#7e22ce} .group-tag{background:#f1f5f9;color:#475569;font-size:0.7rem;padding:1px 6px;border-radius:7px} .entry-meta{font-size:0.75rem;color:#64748b;display:flex;gap:12px;flex-wrap:wrap} .entry-actions{margin-left:auto;display:flex;gap:3px} .empty-sched{padding:36px;text-align:center;color:#94a3b8;font-size:0.88rem} /* ── Sched form ── */ .sched-form-card{background:#fff;border-radius:12px;border:1px solid #e2e8f0; padding:16px;display:flex;flex-direction:column;gap:9px} .sched-form-card h3{font-size:0.88rem;font-weight:700} .sched-form-card label{font-size:0.76rem;color:#64748b;font-weight:500; display:flex;flex-direction:column;gap:3px} .sched-form-card select{width:100%} .layout2{display:grid;grid-template-columns:1fr 270px;gap:14px;align-items:start} @media(max-width:900px){.layout2{grid-template-columns:1fr}} </style> </head> <body> <div class="app"> <!-- ══ Sidebar ══ --> <nav class="sidebar"> <div class="sidebar-logo">📅 Расписание</div> <div class="sidebar-section">Просмотр</div> <a class="active" onclick="showPage('schedule',this)">📋 Расписание</a> <div class="sidebar-section">Справочники</div> <a onclick="showPage('groups',this)">👥 Группы</a> <a onclick="showPage('disciplines',this)">📚 Дисциплины</a> <a onclick="showPage('teachers',this)">👤 Преподаватели</a> <a onclick="showPage('persons',this)">🪪 Физические лица</a> <a onclick="showPage('departments',this)">🏛 Кафедры</a> <a onclick="showPage('positions',this)">📌 Должности</a> <a onclick="showPage('buildings',this)">🏢 Корпуса</a> <a onclick="showPage('rooms',this)">🚪 Аудитории</a> <div style="margin-top:auto;padding:16px"> <a href="/lab10/" style="font-size:0.8rem;color:#64748b">← Назад</a> </div> </nav> <!-- ══ Main ══ --> <main class="main"> <!-- ─ Расписание ─ --> <div id="page-schedule"> <div class="page-head"><h1>Расписание занятий — курс ПИ24</h1></div> <div class="week-row"> <span style="font-size:0.78rem;color:#64748b;font-weight:500">Неделя:</span> <div class="wtab active" onclick="setWeek(1,this)">1 <span style="font-weight:400;font-size:0.78rem">13–17 апр</span></div> <div class="wtab" onclick="setWeek(2,this)">2 <span style="font-weight:400;font-size:0.78rem">20–24 апр</span></div> </div> <div class="week-row" style="margin-top:6px"> <span style="font-size:0.78rem;color:#64748b;font-weight:500">Группа:</span> <div class="gtab active" onclick="setGroup('',this)">Все</div> <div class="gtab" onclick="setGroupByName('ПИ2401',this)">ПИ2401</div> <div class="gtab" onclick="setGroupByName('ПИ2402',this)">ПИ2402</div> <div class="gtab" onclick="setGroupByName('ПИ2403',this)">ПИ2403</div> <div class="gtab" onclick="setGroupByName('ПИ2404',this)">ПИ2404</div> </div> <div class="filters-row" style="margin-top:10px"> <label>Преподаватель <select id="f-teacher" onchange="applyFilters()"><option value="">Все</option></select></label> <label>Дисциплина <select id="f-disc" onchange="applyFilters()"><option value="">Все</option></select></label> <label>Аудитория <select id="f-room" onchange="applyFilters()"><option value="">Все</option></select></label> <label>Тип <select id="f-type" onchange="applyFilters()"> <option value="">Все</option><option>Лекция</option><option>Практика</option> </select></label> <button class="btn-outline btn-sm" onclick="resetFilters()" style="align-self:flex-end">Сброс</button> </div> <div class="layout2"> <div id="scheduleView"><div class="sched-card"><div class="empty-sched">Загрузка...</div></div></div> <div class="sched-form-card"> <h3 id="sched-form-title">+ Добавить запись</h3> <input type="hidden" id="sf-id"> <label>Неделя <select id="sf-week"><option value="1">1</option><option value="2">2</option></select></label> <label>День <select id="sf-day"> <option value="1">Пн</option><option value="2">Вт</option><option value="3">Ср</option> <option value="4">Чт</option><option value="5">Пт</option> </select> </label> <label>Пара <select id="sf-lesson"> <option value="1">1 — 08:00</option><option value="2">2 — 09:45</option> <option value="3">3 — 11:30</option><option value="4">4 — 13:50</option> <option value="5">5 — 15:35</option><option value="6">6 — 17:20</option> </select> </label> <label>Тип <select id="sf-type"><option>Лекция</option><option>Практика</option></select></label> <hr style="border:none;border-top:1px solid #f1f5f9"> <label>Группа <select id="sf-group"></select></label> <label>Дисциплина <select id="sf-disc"></select></label> <label>Аудитория <select id="sf-room"></select></label> <label>Преподаватель <select id="sf-teacher"></select></label> <div style="display:flex;gap:7px"> <button class="btn" style="flex:1" onclick="saveSchedule()">Сохранить</button> <button class="btn-outline" onclick="cancelSchedule()">Сброс</button> </div> <div id="sf-status"></div> </div> </div> </div> <!-- ─ Группы ─ --> <div id="page-groups" style="display:none"> <div class="page-head"><h1>Группы</h1></div> <div class="card"> <div class="card-head"><h2 id="gr-form-title">Добавить группу</h2></div> <div class="card-body"> <input type="hidden" id="gr-id"> <div class="form-row"> <label>Название группы <input type="text" id="gr-name" placeholder="например: ПИ2405"></label> <button class="btn" onclick="saveRef('groups','gr')" style="align-self:flex-end">Сохранить</button> <button class="btn-outline" onclick="cancelRef('groups','gr')" style="align-self:flex-end">Сброс</button> </div> <div id="gr-status"></div> </div> </div> <div class="card"> <table><thead><tr><th>ID</th><th>Название</th><th></th></tr></thead> <tbody id="groups-tbody"></tbody></table> </div> </div> <!-- ─ Дисциплины ─ --> <div id="page-disciplines" style="display:none"> <div class="page-head"><h1>Дисциплины</h1></div> <div class="card"> <div class="card-head"><h2 id="di-form-title">Добавить дисциплину</h2></div> <div class="card-body"> <input type="hidden" id="di-id"> <div class="form-row"> <label>Название дисциплины <input type="text" id="di-name" placeholder="например: Базы данных"></label> <button class="btn" onclick="saveRef('disciplines','di')" style="align-self:flex-end">Сохранить</button> <button class="btn-outline" onclick="cancelRef('disciplines','di')" style="align-self:flex-end">Сброс</button> </div> <div id="di-status"></div> </div> </div> <div class="card"> <table><thead><tr><th>ID</th><th>Название</th><th></th></tr></thead> <tbody id="disciplines-tbody"></tbody></table> </div> </div> <!-- ─ Преподаватели ─ --> <div id="page-teachers" style="display:none"> <div class="page-head"><h1>Преподаватели</h1></div> <div class="card"> <div class="card-head"><h2 id="tc-form-title">Добавить преподавателя</h2></div> <div class="card-body"> <input type="hidden" id="tc-id"> <div class="form-grid form-grid-3" style="margin-bottom:8px"> <label>Физическое лицо <select id="tc-person"></select></label> <label>Кафедра <select id="tc-dept"><option value="">—</option></select></label> <label>Должность <select id="tc-pos"><option value="">—</option></select></label> </div> <div style="display:flex;gap:7px"> <button class="btn" onclick="saveTeacher()">Сохранить</button> <button class="btn-outline" onclick="cancelTeacher()">Сброс</button> </div> <div id="tc-status"></div> </div> </div> <div class="card"> <table> <thead><tr><th>ID</th><th>ФИО</th><th>Кафедра</th><th>Должность</th><th></th></tr></thead> <tbody id="teachers-tbody"></tbody> </table> </div> </div> <!-- ─ Физические лица ─ --> <div id="page-persons" style="display:none"> <div class="page-head"><h1>Физические лица</h1></div> <div class="card"> <div class="card-head"><h2 id="pe-form-title">Добавить физическое лицо</h2></div> <div class="card-body"> <input type="hidden" id="pe-id"> <div class="form-grid form-grid-3" style="margin-bottom:8px"> <label>Фамилия <input type="text" id="pe-last" placeholder="Иванов"></label> <label>Имя <input type="text" id="pe-first" placeholder="Иван"></label> <label>Отчество <input type="text" id="pe-middle" placeholder="Иванович (необяз.)"></label> </div> <div style="display:flex;gap:7px"> <button class="btn" onclick="savePerson()">Сохранить</button> <button class="btn-outline" onclick="cancelPerson()">Сброс</button> </div> <div id="pe-status"></div> </div> </div> <div class="card"> <table> <thead><tr><th>ID</th><th>Фамилия</th><th>Имя</th><th>Отчество</th><th></th></tr></thead> <tbody id="persons-tbody"></tbody> </table> </div> </div> <!-- ─ Кафедры ─ --> <div id="page-departments" style="display:none"> <div class="page-head"><h1>Кафедры</h1></div> <div class="card"> <div class="card-head"><h2 id="dp-form-title">Добавить кафедру</h2></div> <div class="card-body"> <input type="hidden" id="dp-id"> <div class="form-row"> <label>Название кафедры <input type="text" id="dp-name" placeholder="например: Информационных систем"></label> <button class="btn" onclick="saveRef('departments','dp')" style="align-self:flex-end">Сохранить</button> <button class="btn-outline" onclick="cancelRef('departments','dp')" style="align-self:flex-end">Сброс</button> </div> <div id="dp-status"></div> </div> </div> <div class="card"> <table><thead><tr><th>ID</th><th>Название</th><th></th></tr></thead> <tbody id="departments-tbody"></tbody></table> </div> </div> <!-- ─ Должности ─ --> <div id="page-positions" style="display:none"> <div class="page-head"><h1>Должности</h1></div> <div class="card"> <div class="card-head"><h2 id="po-form-title">Добавить должность</h2></div> <div class="card-body"> <input type="hidden" id="po-id"> <div class="form-row"> <label>Название должности <input type="text" id="po-name" placeholder="например: Доцент"></label> <button class="btn" onclick="saveRef('positions','po')" style="align-self:flex-end">Сохранить</button> <button class="btn-outline" onclick="cancelRef('positions','po')" style="align-self:flex-end">Сброс</button> </div> <div id="po-status"></div> </div> </div> <div class="card"> <table><thead><tr><th>ID</th><th>Название</th><th></th></tr></thead> <tbody id="positions-tbody"></tbody></table> </div> </div> <!-- ─ Корпуса ─ --> <div id="page-buildings" style="display:none"> <div class="page-head"><h1>Корпуса</h1></div> <div class="card"> <div class="card-head"><h2 id="bu-form-title">Добавить корпус</h2></div> <div class="card-body"> <input type="hidden" id="bu-id"> <div class="form-row"> <label>Название корпуса <input type="text" id="bu-name" placeholder="например: эк"></label> <button class="btn" onclick="saveRef('buildings','bu')" style="align-self:flex-end">Сохранить</button> <button class="btn-outline" onclick="cancelRef('buildings','bu')" style="align-self:flex-end">Сброс</button> </div> <div id="bu-status"></div> </div> </div> <div class="card"> <table><thead><tr><th>ID</th><th>Название</th><th></th></tr></thead> <tbody id="buildings-tbody"></tbody></table> </div> </div> <!-- ─ Аудитории ─ --> <div id="page-rooms" style="display:none"> <div class="page-head"><h1>Аудитории</h1></div> <div class="card"> <div class="card-head"><h2 id="rm-form-title">Добавить аудиторию</h2></div> <div class="card-body"> <input type="hidden" id="rm-id"> <div class="form-grid form-grid-2" style="margin-bottom:8px"> <label>Номер аудитории <input type="text" id="rm-number" placeholder="например: 310"></label> <label>Корпус <select id="rm-building"></select></label> </div> <div style="display:flex;gap:7px"> <button class="btn" onclick="saveRoom()">Сохранить</button> <button class="btn-outline" onclick="cancelRoom()">Сброс</button> </div> <div id="rm-status"></div> </div> </div> <div class="card"> <table> <thead><tr><th>ID</th><th>Номер</th><th>Корпус</th><th></th></tr></thead> <tbody id="rooms-tbody"></tbody> </table> </div> </div> </main> </div> <script> const API = '/api/lab10'; const DAYS = ['','Пн','Вт','Ср','Чт','Пт','Сб']; const TIMES = ['','08:00\n09:30','09:45\n11:15','11:30\n13:00','13:50\n15:20','15:35\n17:05','17:20\n18:50']; const DATES = { 1:{1:'13 апр',2:'14 апр',3:'15 апр',4:'16 апр',5:'17 апр'}, 2:{1:'20 апр',2:'21 апр',3:'22 апр',4:'23 апр',5:'24 апр'} }; let currentWeek = 1; let currentGroupId = ''; let groupsMap = {}; // Кэши справочников let _persons=[], _depts=[], _positions=[], _buildings=[]; const pages = ['schedule','groups','disciplines','teachers','persons','departments','positions','buildings','rooms']; // ══ Навигация ══ function showPage(name, el) { pages.forEach(p => document.getElementById('page-'+p).style.display = 'none'); document.getElementById('page-'+name).style.display = ''; document.querySelectorAll('.sidebar a').forEach(a => a.classList.remove('active')); if (el) el.classList.add('active'); const loaders = { schedule: () => applyFilters(), groups: () => loadRefTable('groups'), disciplines: () => loadRefTable('disciplines'), teachers: () => loadTeachersTable(), persons: () => loadPersonsTable(), departments: () => loadRefTable('departments'), positions: () => loadRefTable('positions'), buildings: () => loadBuildingsTable(), rooms: () => loadRoomsTable(), }; loaders[name]?.(); } function setStatus(id, msg, type) { const el = document.getElementById(id); el.className = 'status '+type; el.textContent = msg; } // ══ Расписание ══ function setWeek(w, el) { currentWeek = w; document.querySelectorAll('.wtab').forEach(t => t.classList.remove('active')); el.classList.add('active'); applyFilters(); } function setGroup(id, el) { currentGroupId = id; document.querySelectorAll('.gtab').forEach(t => t.classList.remove('active')); el.classList.add('active'); applyFilters(); } function setGroupByName(name, el) { setGroup(groupsMap[name]||'', el); } async function loadSchedSelects() { const [groups, rooms, discs, teachers] = await Promise.all([ fetch(`${API}/schedule/groups`).then(r=>r.json()), fetch(`${API}/schedule/rooms`).then(r=>r.json()), fetch(`${API}/schedule/disciplines`).then(r=>r.json()), fetch(`${API}/schedule/teachers`).then(r=>r.json()), ]); groups.forEach(g => groupsMap[g.name] = g.id); const fillF = (id, data, fn) => { const s = document.getElementById(id); s.innerHTML = s.options[0].outerHTML + data.map(d=>`<option value="${d.id}">${fn(d)}</option>`).join(''); }; const fillS = (id, data, fn) => { document.getElementById(id).innerHTML = data.map(d=>`<option value="${d.id}">${fn(d)}</option>`).join(''); }; const tL = t => `${t.last_name} ${t.first_name}.`; const rL = r => `${r.number} ${r.building_name}`; fillF('f-teacher', teachers, tL); fillF('f-disc', discs, d=>d.name); fillF('f-room', rooms, rL); fillS('sf-group', groups, d=>d.name); fillS('sf-disc', discs, d=>d.name); fillS('sf-room', rooms, rL); fillS('sf-teacher', teachers, tL); } function applyFilters() { const get = id => document.getElementById(id).value; const p = new URLSearchParams({week: currentWeek}); if (currentGroupId) p.set('group_id', currentGroupId); if (get('f-teacher')) p.set('teacher_id', get('f-teacher')); if (get('f-disc')) p.set('discipline_id', get('f-disc')); if (get('f-room')) p.set('room_id', get('f-room')); if (get('f-type')) p.set('lesson_type', get('f-type')); fetch(`${API}/schedule?${p}`).then(r=>r.json()).then(renderSchedule); } function resetFilters() { ['f-teacher','f-disc','f-room','f-type'].forEach(id => document.getElementById(id).value = ''); applyFilters(); } function renderSchedule(rows) { if (!rows.length) { document.getElementById('scheduleView').innerHTML = '<div class="sched-card"><div class="empty-sched">Нет занятий по выбранным фильтрам</div></div>'; return; } const byDay = {}; for (const r of rows) { const d=r.day_of_week, n=r.lesson_num; if (!byDay[d]) byDay[d]={}; if (!byDay[d][n]) byDay[d][n]={}; const k=`${r.discipline_id}_${r.group_id}_${r.room_id}`; if (!byDay[d][n][k]) byDay[d][n][k]={...r, teachers:[]}; byDay[d][n][k].teachers.push(r.teacher_name); } let html = '<div class="sched-card">'; for (const day of Object.keys(byDay).map(Number).sort()) { const dateStr = DATES[currentWeek]?.[day]||''; html += `<div><div class="day-head">📅 ${DAYS[day]} ${dateStr?`<span class="date-badge">${dateStr}</span>`:''} </div>`; for (const num of Object.keys(byDay[day]).map(Number).sort()) { const entries = Object.values(byDay[day][num]); const [t1,t2] = TIMES[num].split('\n'); html += `<div class="lesson-row"> <div class="time-cell"> <span class="time-num">${num} пара</span> <span class="time-str">${t1}<br>${t2}</span> </div><div class="entries-cell">`; for (const e of entries) { const bc = e.lesson_type==='Лекция'?'badge-lec':e.lesson_type==='Практика'?'badge-prac':'badge-sport'; const safe = JSON.stringify({id:e.id,week:e.week,day_of_week:e.day_of_week,lesson_num:e.lesson_num, lesson_type:e.lesson_type,group_id:e.group_id,discipline_id:e.discipline_id, room_id:e.room_id,teacher_id:e.teacher_id}).replace(/'/g,"'"); html += `<div class="entry"> <div class="entry-top"> <span class="disc-name">${e.discipline_name}</span> <span class="badge ${bc}">${e.lesson_type}</span> <span class="group-tag">${e.group_name}</span> <div class="entry-actions"> <button class="btn-outline btn-sm" onclick='prefillSchedule(${safe})'>✏</button> <button class="btn-danger btn-sm" onclick="deleteSchedule(${e.id})">✕</button> </div> </div> <div class="entry-meta"> <span>👤 ${e.teachers.join(', ')}</span> <span>🏛 ${e.room_number} <b>${e.building_name}</b></span> </div> </div>`; } html += `</div></div>`; } html += `</div>`; } html += '</div>'; document.getElementById('scheduleView').innerHTML = html; } function prefillSchedule(r) { ['sf-id','sf-week','sf-day','sf-lesson','sf-type','sf-group','sf-disc','sf-room','sf-teacher'].forEach(id => { const map = {'sf-id':r.id,'sf-week':r.week,'sf-day':r.day_of_week,'sf-lesson':r.lesson_num, 'sf-type':r.lesson_type,'sf-group':r.group_id,'sf-disc':r.discipline_id, 'sf-room':r.room_id,'sf-teacher':r.teacher_id}; document.getElementById(id).value = map[id]; }); document.getElementById('sched-form-title').textContent = 'Редактировать запись'; document.getElementById('sf-status').textContent = ''; } function cancelSchedule() { document.getElementById('sf-id').value=''; document.getElementById('sched-form-title').textContent='+ Добавить запись'; document.getElementById('sf-status').textContent=''; } async function saveSchedule() { const id=document.getElementById('sf-id').value; const get=s=>document.getElementById(s).value; const body={week:+get('sf-week'),day_of_week:+get('sf-day'),lesson_num:+get('sf-lesson'), lesson_type:get('sf-type'),group_id:+get('sf-group'),discipline_id:+get('sf-disc'), room_id:+get('sf-room'),teacher_id:+get('sf-teacher')}; const res = await fetch(id?`${API}/schedule/${id}`:`${API}/schedule`,{ method:id?'PATCH':'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(body) }); const el=document.getElementById('sf-status'); if (res.ok) { el.className='status ok'; el.textContent=id?'✓ Обновлено':'✓ Добавлено'; cancelSchedule(); applyFilters(); } else { el.className='status err'; el.textContent='✕ Ошибка'; } } async function deleteSchedule(id) { if (!confirm(`Удалить запись #${id}?`)) return; await fetch(`${API}/schedule/${id}`,{method:'DELETE'}); applyFilters(); } // ══ Универсальный CRUD для простых справочников (name only) ══ // prefix = 'groups'|'disciplines'|'departments'|'positions'|'buildings' // pfx = двухбуквенный префикс полей ('gr','di','dp','po','bu') async function loadRefTable(ref) { const data = await fetch(`${API}/schedule/${ref}`).then(r=>r.json()); const labelMap = { groups:'Название',disciplines:'Название',departments:'Название', positions:'Название',buildings:'Название' }; document.getElementById(`${ref}-tbody`).innerHTML = data.length ? data.map(d=>`<tr> <td style="color:#94a3b8">${d.id}</td> <td>${d.name}</td> <td><div class="td-actions"> <button class="btn-outline btn-sm" onclick="editSimpleRef('${ref}',${d.id},'${d.name.replace(/'/g,"\\'")}')">✏</button> <button class="btn-danger btn-sm" onclick="deleteRef('${ref}',${d.id})">✕</button> </div></td> </tr>`).join('') : `<tr><td colspan="3" class="empty-td">Нет записей</td></tr>`; } const refPfxMap = {groups:'gr',disciplines:'di',departments:'dp',positions:'po',buildings:'bu'}; function editSimpleRef(ref, id, name) { const pfx = refPfxMap[ref]; document.getElementById(`${pfx}-id`).value = id; document.getElementById(`${pfx}-name`).value = name; document.getElementById(`${pfx}-form-title`).textContent = 'Редактировать'; document.getElementById(`${pfx}-status`).textContent = ''; } function cancelRef(ref, pfx) { document.getElementById(`${pfx}-id`).value = ''; document.getElementById(`${pfx}-name`).value = ''; document.getElementById(`${pfx}-form-title`).textContent = 'Добавить'; document.getElementById(`${pfx}-status`).textContent = ''; } async function saveRef(ref, pfx) { const id = document.getElementById(`${pfx}-id`).value; const name = document.getElementById(`${pfx}-name`).value.trim(); if (!name) return setStatus(`${pfx}-status`,'Введите название','err'); const res = await fetch(id?`${API}/schedule/${ref}/${id}`:`${API}/schedule/${ref}`,{ method:id?'PATCH':'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({name}) }); if (res.ok) { setStatus(`${pfx}-status`, id?'✓ Обновлено':'✓ Добавлено', 'ok'); cancelRef(ref, pfx); loadRefTable(ref); if (ref==='groups'||ref==='disciplines') loadSchedSelects(); if (ref==='buildings') { loadBuildingsTable(); loadRoomsTable(); } } else { setStatus(`${pfx}-status`,'✕ Ошибка','err'); } } async function deleteRef(ref, id) { if (!confirm(`Удалить запись #${id}?`)) return; const res = await fetch(`${API}/schedule/${ref}/${id}`,{method:'DELETE'}); if (res.ok) loadRefTable(ref); else { const d=await res.json(); alert(d.error||'Ошибка'); } } // ══ Физические лица ══ async function loadPersonsTable() { _persons = await fetch(`${API}/schedule/persons`).then(r=>r.json()); // Обновляем селект в преподавателях const sel = document.getElementById('tc-person'); if (sel) sel.innerHTML = _persons.map(p=>`<option value="${p.id}">${p.last_name} ${p.first_name}${p.middle_name?' '+p.middle_name:''}</option>`).join(''); document.getElementById('persons-tbody').innerHTML = _persons.length ? _persons.map(p=>`<tr> <td style="color:#94a3b8">${p.id}</td> <td>${p.last_name}</td><td>${p.first_name}</td><td>${p.middle_name||'—'}</td> <td><div class="td-actions"> <button class="btn-outline btn-sm" onclick="editPerson(${p.id},'${p.last_name}','${p.first_name}','${p.middle_name||''}')">✏</button> <button class="btn-danger btn-sm" onclick="deletePerson(${p.id})">✕</button> </div></td> </tr>`).join('') : `<tr><td colspan="5" class="empty-td">Нет записей</td></tr>`; } function editPerson(id,last,first,middle) { document.getElementById('pe-id').value = id; document.getElementById('pe-last').value = last; document.getElementById('pe-first').value = first; document.getElementById('pe-middle').value= middle; document.getElementById('pe-form-title').textContent='Редактировать'; document.getElementById('pe-status').textContent=''; } function cancelPerson() { document.getElementById('pe-id').value=''; ['pe-last','pe-first','pe-middle'].forEach(id=>document.getElementById(id).value=''); document.getElementById('pe-form-title').textContent='Добавить физическое лицо'; document.getElementById('pe-status').textContent=''; } async function savePerson() { const id = document.getElementById('pe-id').value; const last = document.getElementById('pe-last').value.trim(); const first = document.getElementById('pe-first').value.trim(); const middle = document.getElementById('pe-middle').value.trim(); if (!last||!first) return setStatus('pe-status','Фамилия и имя обязательны','err'); const res = await fetch(id?`${API}/schedule/persons/${id}`:`${API}/schedule/persons`,{ method:id?'PATCH':'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({last_name:last,first_name:first,middle_name:middle||null}) }); if (res.ok) { setStatus('pe-status',id?'✓ Обновлено':'✓ Добавлено','ok'); cancelPerson(); loadPersonsTable(); loadSchedSelects(); } else { setStatus('pe-status','✕ Ошибка','err'); } } async function deletePerson(id) { if (!confirm(`Удалить лицо #${id}?`)) return; const res = await fetch(`${API}/schedule/persons/${id}`,{method:'DELETE'}); if (res.ok) loadPersonsTable(); else { const d=await res.json(); alert(d.error||'Ошибка'); } } // ══ Преподаватели ══ async function loadTeachersTable() { const [teachers, persons, depts, positions] = await Promise.all([ fetch(`${API}/schedule/teachers`).then(r=>r.json()), fetch(`${API}/schedule/persons`).then(r=>r.json()), fetch(`${API}/schedule/departments`).then(r=>r.json()), fetch(`${API}/schedule/positions`).then(r=>r.json()), ]); _persons=persons; _depts=depts; _positions=positions; const fillSel = (id, data, fn, withEmpty=false) => { document.getElementById(id).innerHTML = (withEmpty?'<option value="">—</option>':'')+ data.map(d=>`<option value="${d.id}">${fn(d)}</option>`).join(''); }; fillSel('tc-person', persons, p=>`${p.last_name} ${p.first_name}${p.middle_name?' '+p.middle_name:''}`); fillSel('tc-dept', depts, d=>d.name, true); fillSel('tc-pos', positions, p=>p.name, true); document.getElementById('teachers-tbody').innerHTML = teachers.length ? teachers.map(t=>`<tr> <td style="color:#94a3b8">${t.id}</td> <td>${t.last_name} ${t.first_name}${t.middle_name?' '+t.middle_name:''}</td> <td>${t.department||'—'}</td><td>${t.position||'—'}</td> <td><div class="td-actions"> <button class="btn-outline btn-sm" onclick="editTeacher(${t.id})">✏</button> <button class="btn-danger btn-sm" onclick="deleteTeacher(${t.id})">✕</button> </div></td> </tr>`).join('') : `<tr><td colspan="5" class="empty-td">Нет записей</td></tr>`; } async function editTeacher(id) { const res = await fetch(`${API}/schedule/teachers`).then(r=>r.json()); // Находим нужного по id — придётся запросить отдельно, но у нас нет GET /teachers/:id // Берём из таблицы через data-атрибуты — проще перезапросить список // Вместо этого просто выделим строку визуально (id уже в select) document.getElementById('tc-id').value = id; document.getElementById('tc-form-title').textContent='Редактировать преподавателя'; document.getElementById('tc-status').textContent=''; } function cancelTeacher() { document.getElementById('tc-id').value=''; document.getElementById('tc-form-title').textContent='Добавить преподавателя'; document.getElementById('tc-status').textContent=''; } async function saveTeacher() { const id = document.getElementById('tc-id').value; const person_id = document.getElementById('tc-person').value; const dept_id = document.getElementById('tc-dept').value; const pos_id = document.getElementById('tc-pos').value; if (!person_id) return setStatus('tc-status','Выберите физическое лицо','err'); const res = await fetch(id?`${API}/schedule/teachers/${id}`:`${API}/schedule/teachers`,{ method:id?'PATCH':'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({person_id:+person_id, department_id:dept_id?+dept_id:null, position_id:pos_id?+pos_id:null}) }); if (res.ok) { setStatus('tc-status',id?'✓ Обновлено':'✓ Добавлено','ok'); cancelTeacher(); loadTeachersTable(); loadSchedSelects(); } else { setStatus('tc-status','✕ Ошибка','err'); } } async function deleteTeacher(id) { if (!confirm(`Удалить преподавателя #${id}?`)) return; const res = await fetch(`${API}/schedule/teachers/${id}`,{method:'DELETE'}); if (res.ok) loadTeachersTable(); else { const d=await res.json(); alert(d.error||'Ошибка'); } } // ══ Корпуса ══ async function loadBuildingsTable() { _buildings = await fetch(`${API}/schedule/buildings`).then(r=>r.json()); // Обновляем селект аудиторий const sel = document.getElementById('rm-building'); if (sel) sel.innerHTML = _buildings.map(b=>`<option value="${b.id}">${b.name}</option>`).join(''); document.getElementById('buildings-tbody').innerHTML = _buildings.length ? _buildings.map(b=>`<tr> <td style="color:#94a3b8">${b.id}</td><td>${b.name}</td> <td><div class="td-actions"> <button class="btn-outline btn-sm" onclick="editSimpleRef('buildings',${b.id},'${b.name.replace(/'/g,"\\'")}')">✏</button> <button class="btn-danger btn-sm" onclick="deleteRef('buildings',${b.id})">✕</button> </div></td> </tr>`).join('') : `<tr><td colspan="3" class="empty-td">Нет записей</td></tr>`; } // ══ Аудитории ══ async function loadRoomsTable() { const [rooms, buildings] = await Promise.all([ fetch(`${API}/schedule/rooms`).then(r=>r.json()), fetch(`${API}/schedule/buildings`).then(r=>r.json()), ]); _buildings = buildings; const sel = document.getElementById('rm-building'); if (sel) sel.innerHTML = buildings.map(b=>`<option value="${b.id}">${b.name}</option>`).join(''); document.getElementById('rooms-tbody').innerHTML = rooms.length ? rooms.map(r=>`<tr> <td style="color:#94a3b8">${r.id}</td> <td>${r.number}</td><td>${r.building_name}</td> <td><div class="td-actions"> <button class="btn-outline btn-sm" onclick="editRoom(${r.id},'${r.number}',${r.building_id})">✏</button> <button class="btn-danger btn-sm" onclick="deleteRoom(${r.id})">✕</button> </div></td> </tr>`).join('') : `<tr><td colspan="4" class="empty-td">Нет записей</td></tr>`; } function editRoom(id, number, building_id) { document.getElementById('rm-id').value = id; document.getElementById('rm-number').value = number; document.getElementById('rm-building').value = building_id; document.getElementById('rm-form-title').textContent='Редактировать аудиторию'; document.getElementById('rm-status').textContent=''; } function cancelRoom() { document.getElementById('rm-id').value=''; document.getElementById('rm-number').value=''; document.getElementById('rm-form-title').textContent='Добавить аудиторию'; document.getElementById('rm-status').textContent=''; } async function saveRoom() { const id = document.getElementById('rm-id').value; const number = document.getElementById('rm-number').value.trim(); const building_id = document.getElementById('rm-building').value; if (!number||!building_id) return setStatus('rm-status','Заполните все поля','err'); const res = await fetch(id?`${API}/schedule/rooms/${id}`:`${API}/schedule/rooms`,{ method:id?'PATCH':'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({number, building_id:+building_id}) }); if (res.ok) { setStatus('rm-status',id?'✓ Обновлено':'✓ Добавлено','ok'); cancelRoom(); loadRoomsTable(); loadSchedSelects(); } else { setStatus('rm-status','✕ Ошибка','err'); } } async function deleteRoom(id) { if (!confirm(`Удалить аудиторию #${id}?`)) return; const res = await fetch(`${API}/schedule/rooms/${id}`,{method:'DELETE'}); if (res.ok) loadRoomsTable(); else { const d=await res.json(); alert(d.error||'Ошибка'); } } // ══ Инициализация ══ loadSchedSelects().then(() => applyFilters()); </script> </body> </html>