/
Coderdev
/
web-nodejs-labs
Обзор
Документация
Войти
/
Coderdev
/
web-nodejs-labs
Код
Запросы
0
Задачи
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
public/lab11/task3/index.html
852 строки
54 KB
Coderdev
1
12 май 2026, 12:34
12 май 2026, 12:34
f5e899f
Код
Авторство
О чём код?
<!DOCTYPE html> <html lang="ru"> <head> <meta charset="UTF-8"> <title>Задание 3 — Защищённые области</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:220px 1fr;min-height:100vh} /* ── Sidebar ── */ .sidebar{background:#1e293b;color:#e2e8f0;padding:0;display:flex;flex-direction:column;overflow-y:auto} .sidebar-logo{padding:14px 16px;font-size:0.95rem;font-weight:700;color:#fff;border-bottom:1px solid #334155} /* Auth блок */ .auth-block{padding:12px 16px;border-bottom:1px solid #334155;display:flex;flex-direction:column;gap:6px} .auth-block input{padding:5px 8px;border:1px solid #334155;border-radius:6px;font-size:0.78rem; background:#0f172a;color:#e2e8f0;width:100%} .auth-block input::placeholder{color:#475569} .auth-btns{display:flex;gap:5px} .auth-btns button{flex:1;padding:5px;font-size:0.75rem;border:none;border-radius:6px;cursor:pointer;font-weight:500} .btn-in{background:#2563eb;color:#fff} .btn-out{background:#334155;color:#94a3b8} .auth-badge{font-size:0.72rem;padding:2px 7px;border-radius:8px;display:inline-block;margin-top:2px} .badge-g{background:#1e293b;color:#64748b} .badge-a{background:#fef9c3;color:#854d0e} .badge-u{background:#dbeafe;color:#1d4ed8} .auth-msg{font-size:0.72rem;color:#94a3b8;min-height:14px} .sidebar-section{padding:10px 16px 3px;font-size:0.68rem;text-transform:uppercase; letter-spacing:1px;color:#475569;font-weight:600} .sidebar a{display:block;padding:7px 16px;color:#94a3b8;text-decoration:none; font-size:0.83rem;cursor:pointer;transition:background .12s} .sidebar a:hover{background:#334155;color:#e2e8f0} .sidebar a.active{background:#2563eb;color:#fff} /* ── Main ── */ .main{padding:20px;overflow-y:auto} .ph{display:flex;align-items:center;gap:12px;margin-bottom:16px;flex-wrap:wrap} .ph h1{font-size:1.2rem;font-weight:700;flex:1} /* ── Lock ── */ .lock{background:#fef9c3;border:1px solid #fde68a;border-radius:12px; padding:28px;text-align:center;color:#854d0e} .lock-icon{font-size:2.2rem;margin-bottom:6px} .lock p{font-size:0.87rem} /* ── Cards ── */ .card{background:#fff;border-radius:12px;border:1px solid #e2e8f0;overflow:hidden;margin-bottom:14px} .ch{padding:11px 16px;border-bottom:1px solid #f1f5f9;display:flex;align-items:center;justify-content:space-between} .ch h2{font-size:0.9rem;font-weight:700} .cb{padding:14px} /* ── Forms ── */ 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} .btn{background:#2563eb;color:#fff}.btn:hover{background:#1d4ed8} .btn-sm{padding:3px 8px;font-size:0.75rem} .btn-o{background:#fff;border:1px solid #e2e8f0;color:#475569}.btn-o:hover{background:#f8fafc} .btn-d{background:#dc2626;color:#fff}.btn-d:hover{background:#b91c1c} .btn-s{background:#16a34a;color:#fff}.btn-s:hover{background:#15803d} .fr{display:flex;gap:8px;align-items:flex-end;flex-wrap:wrap} .fr label{flex:1;min-width:120px} .g2{display:grid;grid-template-columns:1fr 1fr;gap:8px} .g3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px} .fc{display:flex;flex-direction:column;gap:9px} .full{grid-column:1/-1} .status{padding:6px 10px;border-radius:8px;font-size:0.8rem;margin-top:6px} .ok{background:#dcfce7;color:#15803d} .err{background:#fee2e2;color:#b91c1c} /* ── 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} .tda{white-space:nowrap;display:flex;gap:4px} .etd{padding:20px;text-align:center;color:#94a3b8;font-size:0.85rem} /* ── Products ── */ .pgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(185px,1fr));gap:12px} .pc{background:#fff;border:1px solid #e2e8f0;border-radius:10px;padding:13px;display:flex;flex-direction:column;gap:6px} .pc-cat{font-size:0.69rem;color:#94a3b8;text-transform:uppercase;font-weight:600} .pc-title{font-size:0.88rem;font-weight:600} .pc-price{color:#16a34a;font-weight:700} .pc-stock{font-size:0.75rem;color:#94a3b8} .pag{display:flex;gap:8px;margin-top:12px;align-items:center;font-size:0.83rem} /* ── Schedule ── */ .wtab{padding:5px 14px;border-radius:20px;cursor:pointer;font-size:0.82rem; border:1px solid #cbd5e1;background:#fff;color:#475569} .wtab.active{background:#2563eb;color:#fff;border-color:#2563eb;font-weight:600} .gtab{padding:4px 12px;border-radius:20px;cursor:pointer;font-size:0.8rem; border:1px solid #cbd5e1;background:#fff;color:#475569} .gtab.active{background:#0f172a;color:#fff;border-color:#0f172a} .frow{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:10px} .frow label{font-size:0.72rem;color:#64748b;display:flex;flex-direction:column;gap:2px} .frow select{min-width:110px} .sched-card{background:#fff;border-radius:12px;border:1px solid #e2e8f0;overflow:hidden} .dh{background:#f8fafc;padding:8px 12px;font-weight:700;font-size:0.83rem; color:#1e293b;border-bottom:1px solid #e2e8f0;display:flex;align-items:center;gap:7px} .db{background:#1e293b;color:#fff;font-size:0.68rem;padding:2px 6px;border-radius:8px} .lr{display:grid;grid-template-columns:82px 1fr;border-bottom:1px solid #f1f5f9} .lr:last-child{border-bottom:none} .tc{padding:8px 10px;border-right:1px solid #f1f5f9;display:flex;flex-direction:column;gap:2px} .tn{font-size:0.68rem;color:#94a3b8;font-weight:600} .ts{font-size:0.73rem;color:#475569;line-height:1.4} .ec{padding:0} .en{padding:7px 12px;border-bottom:1px solid #f8fafc;display:flex;flex-direction:column;gap:3px} .en:last-child{border-bottom:none} .et{display:flex;align-items:center;gap:6px;flex-wrap:wrap} .dn{font-size:0.83rem;font-weight:600} .badge{display:inline-block;padding:1px 6px;border-radius:9px;font-size:0.67rem;font-weight:600} .bl{background:#dbeafe;color:#1d4ed8} .bp{background:#dcfce7;color:#15803d} .gt{background:#f1f5f9;color:#475569;font-size:0.68rem;padding:1px 5px;border-radius:6px} .em{font-size:0.73rem;color:#64748b;display:flex;gap:10px;flex-wrap:wrap} .ea{margin-left:auto;display:flex;gap:3px} .es{padding:30px;text-align:center;color:#94a3b8;font-size:0.87rem} .l2{display:grid;grid-template-columns:1fr 260px;gap:12px;align-items:start} .sf{background:#fff;border-radius:12px;border:1px solid #e2e8f0;padding:14px} .sf h3{font-size:0.87rem;font-weight:700;margin-bottom:8px} .order-card{border:1px solid #e2e8f0;border-radius:10px;padding:12px;margin-bottom:8px} </style> </head> <body> <div class="app"> <!-- ══ SIDEBAR ══ --> <nav class="sidebar"> <div class="sidebar-logo">🔐 Защищённые области</div> <!-- Auth --> <div class="auth-block"> <input type="text" id="a-login" placeholder="Логин"> <input type="password" id="a-pass" placeholder="Пароль"> <div class="auth-btns"> <button class="btn-in" onclick="doLogin()">Войти</button> <button class="btn-out" onclick="doLogout()">Выйти</button> </div> <span id="auth-badge" class="auth-badge badge-g">👤 Гость</span> <div id="auth-msg" class="auth-msg"></div> </div> <div class="sidebar-section">Магазин</div> <a onclick="nav('catalog',this)">📦 Каталог</a> <a onclick="nav('orders',this)">📋 Заказы</a> <a onclick="nav('shop-cats',this)">🗂 Категории</a> <a onclick="nav('shop-prods',this)">✏️ Товары</a> <div class="sidebar-section">Расписание</div> <a onclick="nav('sched',this)">📋 Расписание</a> <a onclick="nav('r-groups',this)">👥 Группы</a> <a onclick="nav('r-discs',this)">📚 Дисциплины</a> <a onclick="nav('r-teachers',this)">👤 Преподаватели</a> <a onclick="nav('r-persons',this)">🪪 Физ. лица</a> <a onclick="nav('r-depts',this)">🏛 Кафедры</a> <a onclick="nav('r-pos',this)">📌 Должности</a> <a onclick="nav('r-blds',this)">🏢 Корпуса</a> <a onclick="nav('r-rooms',this)">🚪 Аудитории</a> <div style="margin-top:auto;padding:14px"> <a href="/lab11/" style="font-size:0.78rem;color:#475569">← Назад</a> </div> </nav> <!-- ══ MAIN ══ --> <main class="main"> <!-- Каталог --> <div id="p-catalog"> <div class="ph"><h1>Каталог товаров</h1> <input type="text" id="s-q" placeholder="Поиск..." style="width:150px" oninput="loadCatalog()"> <select id="s-cat" onchange="loadCatalog()" style="width:140px"><option value="">Все категории</option></select> </div> <div id="catalog-grid" class="pgrid">Загрузка...</div> <div id="catalog-pag" class="pag"></div> </div> <!-- Заказы --> <div id="p-orders" style="display:none"> <div class="ph"><h1>Заказы</h1><button class="btn-o" onclick="loadOrders()">↻</button></div> <div id="orders-list">Загрузка...</div> </div> <!-- Категории магазина --> <div id="p-shop-cats" style="display:none"> <div class="ph"><h1>Категории магазина</h1></div> <div id="lock-shop-cats" class="lock"><div class="lock-icon">🔒</div><p>Только для администратора</p></div> <div id="cnt-shop-cats" style="display:none"> <div class="card"> <div class="ch"><h2 id="sc-title">Добавить категорию</h2></div> <div class="cb"> <input type="hidden" id="sc-id"> <div class="fr"> <label>Название <input type="text" id="sc-name" placeholder="например: Смартфоны"></label> <button class="btn" onclick="saveCat()" style="align-self:flex-end">Сохранить</button> <button class="btn-o" onclick="cancelCat()" style="align-self:flex-end">Сброс</button> </div> <div id="sc-status"></div> </div> </div> <div class="card"> <table><thead><tr><th>ID</th><th>Название</th><th></th></tr></thead> <tbody id="sc-tbody"></tbody></table> </div> </div> </div> <!-- Товары магазина --> <div id="p-shop-prods" style="display:none"> <div class="ph"><h1>Управление товарами</h1></div> <div id="lock-shop-prods" class="lock"><div class="lock-icon">🔒</div><p>Только для администратора</p></div> <div id="cnt-shop-prods" style="display:none"> <div class="card"> <div class="ch"><h2 id="sp-title">Добавить товар</h2></div> <div class="cb"> <input type="hidden" id="sp-id"> <div class="g2" style="margin-bottom:8px"> <label class="full">Название <input type="text" id="sp-name" placeholder="Название товара"></label> <label>Цена (руб) <input type="number" id="sp-price" step="0.01" min="0"></label> <label>Количество <input type="number" id="sp-amount" min="0"></label> <label class="full">Категория <select id="sp-cat"><option value="">— без категории —</option></select></label> </div> <div style="display:flex;gap:7px"> <button class="btn" onclick="saveProd()">Сохранить</button> <button class="btn-o" onclick="cancelProd()">Сброс</button> </div> <div id="sp-status"></div> </div> </div> <div class="card"> <div class="ch"> <h2>Все товары</h2> <div style="display:flex;gap:6px"> <input type="text" id="sp-sq" placeholder="Поиск..." style="width:130px" oninput="loadProdsTable()"> <select id="sp-sf" onchange="loadProdsTable()" style="width:130px"><option value="">Все категории</option></select> </div> </div> <table> <thead><tr><th>ID</th><th>Название</th><th>Категория</th><th>Цена</th><th>Склад</th><th></th></tr></thead> <tbody id="sp-tbody"></tbody> </table> </div> </div> </div> <!-- Расписание --> <div id="p-sched" style="display:none"> <div class="ph"><h1>Расписание — ПИ24</h1></div> <div style="display:flex;gap:7px;flex-wrap:wrap;margin-bottom:8px"> <span style="font-size:0.78rem;color:#64748b;line-height:28px">Неделя:</span> <div class="wtab active" onclick="setWeek(1,this)">1 <small>13–17 апр</small></div> <div class="wtab" onclick="setWeek(2,this)">2 <small>20–24 апр</small></div> </div> <div style="display:flex;gap:6px;flex-wrap:wrap;margin-bottom:8px"> <span style="font-size:0.78rem;color:#64748b;line-height:26px">Группа:</span> <div class="gtab active" onclick="setGr('',this)">Все</div> <div class="gtab" onclick="setGrN('ПИ2401',this)">ПИ2401</div> <div class="gtab" onclick="setGrN('ПИ2402',this)">ПИ2402</div> <div class="gtab" onclick="setGrN('ПИ2403',this)">ПИ2403</div> <div class="gtab" onclick="setGrN('ПИ2404',this)">ПИ2404</div> </div> <div class="frow"> <label>Преподаватель <select id="ff-t" onchange="loadSched()"><option value="">Все</option></select></label> <label>Дисциплина <select id="ff-d" onchange="loadSched()"><option value="">Все</option></select></label> <label>Аудитория <select id="ff-r" onchange="loadSched()"><option value="">Все</option></select></label> <label>Тип <select id="ff-ty" onchange="loadSched()"><option value="">Все</option><option>Лекция</option><option>Практика</option></select></label> <button class="btn-o btn-sm" onclick="resetF()" style="align-self:flex-end">Сброс</button> </div> <div class="l2"> <div id="sched-view"><div class="sched-card"><div class="es">Загрузка...</div></div></div> <div class="sf"> <h3 id="sv-title">+ Добавить запись</h3> <div id="sv-lock" class="lock" style="padding:14px"><div class="lock-icon" style="font-size:1.4rem">🔒</div><p style="font-size:0.8rem">Войдите как администратор</p></div> <div id="sv-fields" class="fc" style="display:none"> <input type="hidden" id="sv-id"> <label>Неделя <select id="sv-w"><option value="1">1</option><option value="2">2</option></select></label> <label>День <select id="sv-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="sv-p"> <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> </select> </label> <label>Тип <select id="sv-ty"><option>Лекция</option><option>Практика</option></select></label> <hr style="border:none;border-top:1px solid #f1f5f9"> <label>Группа <select id="sv-gr"></select></label> <label>Дисциплина <select id="sv-di"></select></label> <label>Аудитория <select id="sv-ro"></select></label> <label>Преподаватель <select id="sv-te"></select></label> <div style="display:flex;gap:6px"> <button class="btn" style="flex:1" onclick="saveSched()">Сохранить</button> <button class="btn-o" onclick="cancelSched()">Сброс</button> </div> <div id="sv-status"></div> </div> </div> </div> </div> <!-- Справочник: Группы --> <div id="p-r-groups" style="display:none"> <div class="ph"><h1>Группы</h1></div> <div id="lock-r-groups" class="lock"><div class="lock-icon">🔒</div><p>Только для администратора</p></div> <div id="cnt-r-groups" style="display:none"> <div class="card"><div class="ch"><h2 id="gr-title">Добавить группу</h2></div><div class="cb"> <input type="hidden" id="gr-id"> <div class="fr"><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-o" onclick="cancelRef('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="gr-tbody"></tbody></table></div> </div> </div> <!-- Справочник: Дисциплины --> <div id="p-r-discs" style="display:none"> <div class="ph"><h1>Дисциплины</h1></div> <div id="lock-r-discs" class="lock"><div class="lock-icon">🔒</div><p>Только для администратора</p></div> <div id="cnt-r-discs" style="display:none"> <div class="card"><div class="ch"><h2 id="di-title">Добавить дисциплину</h2></div><div class="cb"> <input type="hidden" id="di-id"> <div class="fr"><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-o" onclick="cancelRef('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="di-tbody"></tbody></table></div> </div> </div> <!-- Справочник: Кафедры --> <div id="p-r-depts" style="display:none"> <div class="ph"><h1>Кафедры</h1></div> <div id="lock-r-depts" class="lock"><div class="lock-icon">🔒</div><p>Только для администратора</p></div> <div id="cnt-r-depts" style="display:none"> <div class="card"><div class="ch"><h2 id="dp-title">Добавить кафедру</h2></div><div class="cb"> <input type="hidden" id="dp-id"> <div class="fr"><label>Название <input type="text" id="dp-name"></label> <button class="btn" onclick="saveRef('departments','dp')" style="align-self:flex-end">Сохранить</button> <button class="btn-o" onclick="cancelRef('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="dp-tbody"></tbody></table></div> </div> </div> <!-- Справочник: Должности --> <div id="p-r-pos" style="display:none"> <div class="ph"><h1>Должности</h1></div> <div id="lock-r-pos" class="lock"><div class="lock-icon">🔒</div><p>Только для администратора</p></div> <div id="cnt-r-pos" style="display:none"> <div class="card"><div class="ch"><h2 id="po-title">Добавить должность</h2></div><div class="cb"> <input type="hidden" id="po-id"> <div class="fr"><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-o" onclick="cancelRef('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="po-tbody"></tbody></table></div> </div> </div> <!-- Справочник: Корпуса --> <div id="p-r-blds" style="display:none"> <div class="ph"><h1>Корпуса</h1></div> <div id="lock-r-blds" class="lock"><div class="lock-icon">🔒</div><p>Только для администратора</p></div> <div id="cnt-r-blds" style="display:none"> <div class="card"><div class="ch"><h2 id="bu-title">Добавить корпус</h2></div><div class="cb"> <input type="hidden" id="bu-id"> <div class="fr"><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-o" onclick="cancelRef('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="bu-tbody"></tbody></table></div> </div> </div> <!-- Справочник: Аудитории --> <div id="p-r-rooms" style="display:none"> <div class="ph"><h1>Аудитории</h1></div> <div id="lock-r-rooms" class="lock"><div class="lock-icon">🔒</div><p>Только для администратора</p></div> <div id="cnt-r-rooms" style="display:none"> <div class="card"><div class="ch"><h2 id="rm-title">Добавить аудиторию</h2></div><div class="cb"> <input type="hidden" id="rm-id"> <div class="g2" style="margin-bottom:8px"> <label>Номер <input type="text" id="rm-num" placeholder="310"></label> <label>Корпус <select id="rm-bld"></select></label> </div> <div style="display:flex;gap:7px"> <button class="btn" onclick="saveRoom()">Сохранить</button> <button class="btn-o" 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="rm-tbody"></tbody></table></div> </div> </div> <!-- Справочник: Физ. лица --> <div id="p-r-persons" style="display:none"> <div class="ph"><h1>Физические лица</h1></div> <div id="lock-r-persons" class="lock"><div class="lock-icon">🔒</div><p>Только для администратора</p></div> <div id="cnt-r-persons" style="display:none"> <div class="card"><div class="ch"><h2 id="pe-title">Добавить физическое лицо</h2></div><div class="cb"> <input type="hidden" id="pe-id"> <div class="g3" style="margin-bottom:8px"> <label>Фамилия <input type="text" id="pe-last"></label> <label>Имя <input type="text" id="pe-first"></label> <label>Отчество <input type="text" id="pe-mid" placeholder="необязательно"></label> </div> <div style="display:flex;gap:7px"> <button class="btn" onclick="savePerson()">Сохранить</button> <button class="btn-o" 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="pe-tbody"></tbody></table></div> </div> </div> <!-- Справочник: Преподаватели --> <div id="p-r-teachers" style="display:none"> <div class="ph"><h1>Преподаватели</h1></div> <div id="lock-r-teachers" class="lock"><div class="lock-icon">🔒</div><p>Только для администратора</p></div> <div id="cnt-r-teachers" style="display:none"> <div class="card"><div class="ch"><h2 id="tc-title">Добавить преподавателя</h2></div><div class="cb"> <input type="hidden" id="tc-id"> <div class="g3" style="margin-bottom:8px"> <label>Физ. лицо <select id="tc-per"></select></label> <label>Кафедра <select id="tc-dep"><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-o" 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="tc-tbody"></tbody></table></div> </div> </div> </main> </div> <script> const A10 = '/api/lab10'; const A11 = '/api/lab11'; const fmt = v => (v/100).toLocaleString('ru-RU',{style:'currency',currency:'RUB'}); const fmtD = s => new Date(s).toLocaleString('ru-RU'); 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 token = localStorage.getItem('lab11_token') || null; let role = null; let week = 1, grId = '', grMap = {}; // ── Все страницы ──────────────────────────────────────────────────────── const PAGES = ['catalog','orders','shop-cats','shop-prods','sched', 'r-groups','r-discs','r-depts','r-pos','r-blds','r-rooms','r-persons','r-teachers']; // Страницы с замками (только admin) const LOCKED = ['shop-cats','shop-prods','r-groups','r-discs','r-depts', 'r-pos','r-blds','r-rooms','r-persons','r-teachers']; function nav(page, el) { PAGES.forEach(p => { const d=document.getElementById('p-'+p); if(d) d.style.display='none'; }); const target = document.getElementById('p-'+page); if (target) target.style.display=''; document.querySelectorAll('.sidebar a').forEach(a=>a.classList.remove('active')); if (el) el.classList.add('active'); ({ catalog: loadCatalog, orders: loadOrders, 'shop-cats': loadShopCats, 'shop-prods': () => { loadProdsTable(); fillCatSels(); }, sched: loadSched, 'r-groups': () => loadRef('groups','gr'), 'r-discs': () => loadRef('disciplines','di'), 'r-depts': () => loadRef('departments','dp'), 'r-pos': () => loadRef('positions','po'), 'r-blds': loadBlds, 'r-rooms': loadRooms, 'r-persons': loadPersons, 'r-teachers': loadTeachers, })[page]?.(); } // ── Auth ──────────────────────────────────────────────────────────────── function ah() { const h = {'Content-Type':'application/json'}; if (token) h['Authorization'] = 'Bearer '+token; return h; } async function doLogin() { const login = document.getElementById('a-login').value.trim(); const pass = document.getElementById('a-pass').value; const res = await fetch(`${A11}/login`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({login,password:pass})}); const data = await res.json(); const msg = document.getElementById('auth-msg'); if (res.ok && data.token) { token = data.token; role = data.user.role; localStorage.setItem('lab11_token', token); msg.style.color='#16a34a'; msg.textContent='✓ '+data.user.login; updateUI(); } else { msg.style.color='#dc2626'; msg.textContent='✕ '+(data.error||'Ошибка'); } } function doLogout() { token=null; role=null; localStorage.removeItem('lab11_token'); document.getElementById('auth-msg').textContent=''; updateUI(); } function updateUI() { const badge = document.getElementById('auth-badge'); const isA = role==='admin'; if (!role) { badge.className='auth-badge badge-g'; badge.textContent='👤 Гость'; } else if(isA){ badge.className='auth-badge badge-a'; badge.textContent='🛡 Администратор'; } else { badge.className='auth-badge badge-u'; badge.textContent='✓ Пользователь'; } // Открываем/закрываем замки LOCKED.forEach(k => { const lk = document.getElementById('lock-'+k); const cn = document.getElementById('cnt-'+k); if (lk) lk.style.display = isA ? 'none' : ''; if (cn) cn.style.display = isA ? '' : 'none'; }); // Форма расписания document.getElementById('sv-lock').style.display = isA ? 'none' : ''; document.getElementById('sv-fields').style.display = isA ? '' : 'none'; } function ss(id,msg,type){ const e=document.getElementById(id); e.className='status '+type; e.textContent=msg; } // ── Каталог ───────────────────────────────────────────────────────────── let catPage=1, catQ='', catCat='', cats=[]; async function loadAllCats() { cats = await fetch(`${A10}/categories`).then(r=>r.json()); const opts = cats.map(c=>`<option value="${c.id}">${c.name}</option>`).join(''); document.getElementById('s-cat').innerHTML = '<option value="">Все категории</option>'+opts; } async function loadCatalog() { await loadAllCats(); catQ = document.getElementById('s-q').value.trim(); catCat = document.getElementById('s-cat').value; const p = new URLSearchParams({page:catPage}); if (catQ) p.set('q',catQ); if (catCat) p.set('category',catCat); const data = await fetch(`${A10}/products?${p}`).then(r=>r.json()); const grid = document.getElementById('catalog-grid'); grid.innerHTML = data.data?.length ? data.data.map(p=>`<div class="pc"> <div class="pc-cat">${p.category_title||'—'}</div> <div class="pc-title">${p.title}</div> <div class="pc-price">${fmt(p.price)}</div> <div class="pc-stock">Склад: ${p.amount} шт.</div> </div>`).join('') : '<p style="color:#94a3b8">Товаров нет</p>'; const pages = Math.ceil((data.total||0)/(data.limit||6)); document.getElementById('catalog-pag').innerHTML = pages>1 ? `<button class="btn-o" onclick="catPage=${catPage-1};loadCatalog()" ${catPage<=1?'disabled':''}>← Пред.</button> <span>Стр. ${catPage} из ${pages}</span> <button class="btn-o" onclick="catPage=${catPage+1};loadCatalog()" ${catPage>=pages?'disabled':''}>След. →</button>` : ''; } // ── Заказы ─────────────────────────────────────────────────────────────── async function loadOrders() { const orders = await fetch(`${A10}/task1/orders`).then(r=>r.json()); document.getElementById('orders-list').innerHTML = orders.length ? orders.map(o=>`<div class="order-card"> <strong>Заказ #${o.id}</strong> <div style="font-size:0.8rem;color:#64748b;margin-top:4px"> ${fmtD(o.created_at)} · Позиций: ${o.items_count} · Сумма: <strong>${fmt(o.total_sum||0)}</strong> </div> </div>`).join('') : '<p style="color:#94a3b8;padding:20px">Заказов нет</p>'; } // ── Категории магазина ─────────────────────────────────────────────────── async function loadShopCats() { cats = await fetch(`${A10}/categories`).then(r=>r.json()); document.getElementById('sc-tbody').innerHTML = cats.length ? cats.map(c=>`<tr> <td style="color:#94a3b8">${c.id}</td><td><strong>${c.name}</strong></td> <td><div class="tda"> <button class="btn-o btn-sm" onclick="editCat(${c.id},'${c.name.replace(/'/g,"\\'")}')">✏ Изменить</button> <button class="btn-d btn-sm" onclick="delCat(${c.id})">✕</button> </div></td> </tr>`).join('') : `<tr><td colspan="3" class="etd">Нет категорий</td></tr>`; } function editCat(id,name){ document.getElementById('sc-id').value=id; document.getElementById('sc-name').value=name; document.getElementById('sc-title').textContent='Редактировать категорию'; } function cancelCat(){ document.getElementById('sc-id').value=''; document.getElementById('sc-name').value=''; document.getElementById('sc-title').textContent='Добавить категорию'; document.getElementById('sc-status').textContent=''; } async function saveCat(){ const id=document.getElementById('sc-id').value, name=document.getElementById('sc-name').value.trim(); if(!name) return ss('sc-status','Введите название','err'); const res=await fetch(id?`${A10}/categories/${id}`:`${A10}/categories`,{method:id?'PATCH':'POST',headers:ah(),body:JSON.stringify({name})}); if(res.ok){ ss('sc-status',id?'✓ Обновлено':'✓ Добавлено','ok'); cancelCat(); loadShopCats(); } else ss('sc-status','✕ Ошибка','err'); } async function delCat(id){ if(!confirm('Удалить категорию #'+id+'?'))return; await fetch(`${A10}/categories/${id}`,{method:'DELETE',headers:ah()}); loadShopCats(); } // ── Товары магазина ────────────────────────────────────────────────────── async function fillCatSels() { cats = await fetch(`${A10}/categories`).then(r=>r.json()); const opts = cats.map(c=>`<option value="${c.id}">${c.name}</option>`).join(''); document.getElementById('sp-cat').innerHTML = '<option value="">— без категории —</option>'+opts; document.getElementById('sp-sf').innerHTML = '<option value="">Все категории</option>'+opts; } async function loadProdsTable(){ const q=document.getElementById('sp-sq').value, cat=document.getElementById('sp-sf').value; const p=new URLSearchParams({limit:100}); if(q)p.set('q',q); if(cat)p.set('category',cat); const data=await fetch(`${A10}/products?${p}`).then(r=>r.json()); document.getElementById('sp-tbody').innerHTML=data.data?.length ?data.data.map(p=>`<tr> <td style="color:#94a3b8">${p.id}</td><td><strong>${p.title}</strong></td> <td>${p.category_title||'—'}</td><td>${fmt(p.price)}</td><td>${p.amount} шт.</td> <td><div class="tda"> <button class="btn-o btn-sm" onclick='editProd(${JSON.stringify({id:p.id,title:p.title,price:p.price,amount:p.amount,category_id:p.category_id}).replace(/'/g,"'")})'>✏</button> <button class="btn-d btn-sm" onclick="delProd(${p.id})">✕</button> </div></td> </tr>`).join('') :`<tr><td colspan="6" class="etd">Товаров нет</td></tr>`; } function editProd(p){ document.getElementById('sp-id').value=p.id; document.getElementById('sp-name').value=p.title; document.getElementById('sp-price').value=(p.price/100).toFixed(2); document.getElementById('sp-amount').value=p.amount; document.getElementById('sp-cat').value=p.category_id||''; document.getElementById('sp-title').textContent='Редактировать товар'; document.getElementById('sp-status').textContent=''; } function cancelProd(){ document.getElementById('sp-id').value=''; ['sp-name','sp-price','sp-amount'].forEach(i=>document.getElementById(i).value=''); document.getElementById('sp-cat').value=''; document.getElementById('sp-title').textContent='Добавить товар'; document.getElementById('sp-status').textContent=''; } async function saveProd(){ const id=document.getElementById('sp-id').value, title=document.getElementById('sp-name').value.trim(); const price=Math.round(parseFloat(document.getElementById('sp-price').value)*100); const amount=parseInt(document.getElementById('sp-amount').value), catId=document.getElementById('sp-cat').value; if(!title||isNaN(price)||isNaN(amount)) return ss('sp-status','Заполните все поля','err'); const res=await fetch(id?`${A10}/products/${id}`:`${A10}/products`,{method:id?'PATCH':'POST',headers:ah(),body:JSON.stringify({title,price,amount,category_id:catId?+catId:null})}); if(res.ok){ ss('sp-status',id?'✓ Обновлено':'✓ Добавлено','ok'); cancelProd(); loadProdsTable(); loadCatalog(); } else ss('sp-status','✕ Ошибка','err'); } async function delProd(id){ if(!confirm('Удалить товар #'+id+'?'))return; await fetch(`${A10}/products/${id}`,{method:'DELETE',headers:ah()}); loadProdsTable(); loadCatalog(); } // ── Расписание ─────────────────────────────────────────────────────────── function setWeek(w,el){ week=w; document.querySelectorAll('.wtab').forEach(t=>t.classList.remove('active')); el.classList.add('active'); loadSched(); } function setGr(id,el){ grId=id; document.querySelectorAll('.gtab').forEach(t=>t.classList.remove('active')); el.classList.add('active'); loadSched(); } function setGrN(name,el){ setGr(grMap[name]||'',el); } function resetF(){ ['ff-t','ff-d','ff-r','ff-ty'].forEach(i=>document.getElementById(i).value=''); loadSched(); } async function loadSchedSels(){ const [groups,rooms,discs,teachers]=await Promise.all([ fetch(`${A10}/schedule/groups`).then(r=>r.json()), fetch(`${A10}/schedule/rooms`).then(r=>r.json()), fetch(`${A10}/schedule/disciplines`).then(r=>r.json()), fetch(`${A10}/schedule/teachers`).then(r=>r.json()), ]); groups.forEach(g=>grMap[g.name]=g.id); const fF=(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 fS=(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}`; fF('ff-t',teachers,tL); fF('ff-d',discs,d=>d.name); fF('ff-r',rooms,rL); fS('sv-gr',groups,d=>d.name); fS('sv-di',discs,d=>d.name); fS('sv-ro',rooms,rL); fS('sv-te',teachers,tL); } async function loadSched(){ const g=id=>document.getElementById(id).value; const p=new URLSearchParams({week}); if(grId) p.set('group_id',grId); if(g('ff-t')) p.set('teacher_id',g('ff-t')); if(g('ff-d')) p.set('discipline_id',g('ff-d')); if(g('ff-r')) p.set('room_id',g('ff-r')); if(g('ff-ty')) p.set('lesson_type',g('ff-ty')); const rows=await fetch(`${A10}/schedule?${p}`).then(r=>r.json()); renderSched(rows); } function renderSched(rows){ const isA=role==='admin'; if(!rows.length){ document.getElementById('sched-view').innerHTML='<div class="sched-card"><div class="es">Нет занятий</div></div>'; return; } const byD={}; for(const r of rows){ const d=r.day_of_week,n=r.lesson_num; if(!byD[d])byD[d]={}; if(!byD[d][n])byD[d][n]={}; const k=`${r.discipline_id}_${r.group_id}_${r.room_id}`; if(!byD[d][n][k])byD[d][n][k]={...r,teachers:[]}; byD[d][n][k].teachers.push(r.teacher_name); } let h='<div class="sched-card">'; for(const day of Object.keys(byD).map(Number).sort()){ const ds=DATES[week]?.[day]||''; h+=`<div><div class="dh">📅 ${DAYS[day]} ${ds?`<span class="db">${ds}</span>`:''}</div>`; for(const num of Object.keys(byD[day]).map(Number).sort()){ const entries=Object.values(byD[day][num]), [t1,t2]=TIMES[num].split('\n'); h+=`<div class="lr"><div class="tc"><span class="tn">${num} пара</span><span class="ts">${t1}<br>${t2}</span></div><div class="ec">`; for(const e of entries){ const bc=e.lesson_type==='Лекция'?'bl':'bp'; 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,"'"); h+=`<div class="en"><div class="et"><span class="dn">${e.discipline_name}</span><span class="badge ${bc}">${e.lesson_type}</span><span class="gt">${e.group_name}</span> ${isA?`<div class="ea"><button class="btn-o btn-sm" onclick='prefSched(${safe})'>✏</button><button class="btn-d btn-sm" onclick="delSched(${e.id})">✕</button></div>`:''} </div><div class="em"><span>👤 ${e.teachers.join(', ')}</span><span>🏛 ${e.room_number} <b>${e.building_name}</b></span></div></div>`; } h+=`</div></div>`; } h+=`</div>`; } h+='</div>'; document.getElementById('sched-view').innerHTML=h; } function prefSched(r){ ['sv-id','sv-w','sv-day','sv-p','sv-ty','sv-gr','sv-di','sv-ro','sv-te'].forEach(id=>{ const m={'sv-id':r.id,'sv-w':r.week,'sv-day':r.day_of_week,'sv-p':r.lesson_num,'sv-ty':r.lesson_type,'sv-gr':r.group_id,'sv-di':r.discipline_id,'sv-ro':r.room_id,'sv-te':r.teacher_id}; document.getElementById(id).value=m[id]; }); document.getElementById('sv-title').textContent='Редактировать запись'; document.getElementById('sv-status').textContent=''; } function cancelSched(){ document.getElementById('sv-id').value=''; document.getElementById('sv-title').textContent='+ Добавить запись'; document.getElementById('sv-status').textContent=''; } async function saveSched(){ const id=document.getElementById('sv-id').value, g=s=>document.getElementById(s).value; const body={week:+g('sv-w'),day_of_week:+g('sv-day'),lesson_num:+g('sv-p'),lesson_type:g('sv-ty'),group_id:+g('sv-gr'),discipline_id:+g('sv-di'),room_id:+g('sv-ro'),teacher_id:+g('sv-te')}; const res=await fetch(id?`${A10}/schedule/${id}`:`${A10}/schedule`,{method:id?'PATCH':'POST',headers:ah(),body:JSON.stringify(body)}); if(res.ok){ ss('sv-status',id?'✓ Обновлено':'✓ Добавлено','ok'); cancelSched(); loadSched(); } else ss('sv-status','✕ Ошибка','err'); } async function delSched(id){ if(!confirm('Удалить запись #'+id+'?'))return; await fetch(`${A10}/schedule/${id}`,{method:'DELETE',headers:ah()}); loadSched(); } // ── Универсальный CRUD простых справочников ────────────────────────────── const refPfx={groups:'gr',disciplines:'di',departments:'dp',positions:'po',buildings:'bu'}; async function loadRef(ref,pfx){ const data=await fetch(`${A10}/schedule/${ref}`).then(r=>r.json()); document.getElementById(`${pfx}-tbody`).innerHTML=data.length ?data.map(d=>`<tr><td style="color:#94a3b8">${d.id}</td><td>${d.name}</td><td><div class="tda"> <button class="btn-o btn-sm" onclick="editRef('${pfx}',${d.id},'${d.name.replace(/'/g,"\\'")}')">✏</button> <button class="btn-d btn-sm" onclick="delRef('${ref}','${pfx}',${d.id})">✕</button> </div></td></tr>`).join('') :`<tr><td colspan="3" class="etd">Нет записей</td></tr>`; } function editRef(pfx,id,name){ document.getElementById(`${pfx}-id`).value=id; document.getElementById(`${pfx}-name`).value=name; document.getElementById(`${pfx}-title`).textContent='Редактировать'; } function cancelRef(pfx){ document.getElementById(`${pfx}-id`).value=''; document.getElementById(`${pfx}-name`).value=''; document.getElementById(`${pfx}-title`).textContent='Добавить'; document.getElementById(`${pfx}-status`).textContent=''; } async function saveRef(ref,pfx){ const id=document.getElementById(`${pfx}-id`).value, name=document.getElementById(`${pfx}-name`).value.trim(); if(!name) return ss(`${pfx}-status`,'Введите название','err'); const res=await fetch(id?`${A10}/schedule/${ref}/${id}`:`${A10}/schedule/${ref}`,{method:id?'PATCH':'POST',headers:ah(),body:JSON.stringify({name})}); if(res.ok){ ss(`${pfx}-status`,id?'✓ Обновлено':'✓ Добавлено','ok'); cancelRef(pfx); loadRef(ref,pfx); loadSchedSels(); } else ss(`${pfx}-status`,'✕ Ошибка','err'); } async function delRef(ref,pfx,id){ if(!confirm('Удалить #'+id+'?'))return; const res=await fetch(`${A10}/schedule/${ref}/${id}`,{method:'DELETE',headers:ah()}); if(res.ok)loadRef(ref,pfx); else{const d=await res.json();alert(d.error||'Ошибка');} } // ── Корпуса ────────────────────────────────────────────────────────────── async function loadBlds(){ const blds=await fetch(`${A10}/schedule/buildings`).then(r=>r.json()); const sel=document.getElementById('rm-bld'); if(sel) sel.innerHTML=blds.map(b=>`<option value="${b.id}">${b.name}</option>`).join(''); document.getElementById('bu-tbody').innerHTML=blds.length ?blds.map(b=>`<tr><td style="color:#94a3b8">${b.id}</td><td>${b.name}</td><td><div class="tda"> <button class="btn-o btn-sm" onclick="editRef('bu',${b.id},'${b.name.replace(/'/g,"\\'")}')">✏</button> <button class="btn-d btn-sm" onclick="delRef('buildings','bu',${b.id})">✕</button> </div></td></tr>`).join('') :`<tr><td colspan="3" class="etd">Нет записей</td></tr>`; } // ── Аудитории ──────────────────────────────────────────────────────────── async function loadRooms(){ const [rooms,blds]=await Promise.all([fetch(`${A10}/schedule/rooms`).then(r=>r.json()),fetch(`${A10}/schedule/buildings`).then(r=>r.json())]); const sel=document.getElementById('rm-bld'); if(sel) sel.innerHTML=blds.map(b=>`<option value="${b.id}">${b.name}</option>`).join(''); document.getElementById('rm-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="tda"> <button class="btn-o btn-sm" onclick="editRoom(${r.id},'${r.number}',${r.building_id})">✏</button> <button class="btn-d btn-sm" onclick="delRoom(${r.id})">✕</button> </div></td></tr>`).join('') :`<tr><td colspan="4" class="etd">Нет записей</td></tr>`; } function editRoom(id,num,bid){ document.getElementById('rm-id').value=id; document.getElementById('rm-num').value=num; document.getElementById('rm-bld').value=bid; document.getElementById('rm-title').textContent='Редактировать аудиторию'; } function cancelRoom(){ document.getElementById('rm-id').value=''; document.getElementById('rm-num').value=''; document.getElementById('rm-title').textContent='Добавить аудиторию'; document.getElementById('rm-status').textContent=''; } async function saveRoom(){ const id=document.getElementById('rm-id').value, num=document.getElementById('rm-num').value.trim(), bid=document.getElementById('rm-bld').value; if(!num||!bid) return ss('rm-status','Заполните все поля','err'); const res=await fetch(id?`${A10}/schedule/rooms/${id}`:`${A10}/schedule/rooms`,{method:id?'PATCH':'POST',headers:ah(),body:JSON.stringify({number:num,building_id:+bid})}); if(res.ok){ ss('rm-status',id?'✓ Обновлено':'✓ Добавлено','ok'); cancelRoom(); loadRooms(); loadSchedSels(); } else ss('rm-status','✕ Ошибка','err'); } async function delRoom(id){ if(!confirm('Удалить аудиторию #'+id+'?'))return; const res=await fetch(`${A10}/schedule/rooms/${id}`,{method:'DELETE',headers:ah()}); if(res.ok)loadRooms(); else{const d=await res.json();alert(d.error||'Ошибка');} } // ── Физ. лица ──────────────────────────────────────────────────────────── async function loadPersons(){ const persons=await fetch(`${A10}/schedule/persons`).then(r=>r.json()); const sel=document.getElementById('tc-per'); 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('pe-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="tda"> <button class="btn-o btn-sm" onclick="editPerson(${p.id},'${p.last_name}','${p.first_name}','${p.middle_name||''}')">✏</button> <button class="btn-d btn-sm" onclick="delPerson(${p.id})">✕</button> </div></td></tr>`).join('') :`<tr><td colspan="5" class="etd">Нет записей</td></tr>`; } function editPerson(id,l,f,m){ document.getElementById('pe-id').value=id; document.getElementById('pe-last').value=l; document.getElementById('pe-first').value=f; document.getElementById('pe-mid').value=m; document.getElementById('pe-title').textContent='Редактировать'; } function cancelPerson(){ document.getElementById('pe-id').value=''; ['pe-last','pe-first','pe-mid'].forEach(i=>document.getElementById(i).value=''); document.getElementById('pe-title').textContent='Добавить физическое лицо'; document.getElementById('pe-status').textContent=''; } async function savePerson(){ const id=document.getElementById('pe-id').value, last=document.getElementById('pe-last').value.trim(), first=document.getElementById('pe-first').value.trim(), mid=document.getElementById('pe-mid').value.trim(); if(!last||!first) return ss('pe-status','Фамилия и имя обязательны','err'); const res=await fetch(id?`${A10}/schedule/persons/${id}`:`${A10}/schedule/persons`,{method:id?'PATCH':'POST',headers:ah(),body:JSON.stringify({last_name:last,first_name:first,middle_name:mid||null})}); if(res.ok){ ss('pe-status',id?'✓ Обновлено':'✓ Добавлено','ok'); cancelPerson(); loadPersons(); loadSchedSels(); } else ss('pe-status','✕ Ошибка','err'); } async function delPerson(id){ if(!confirm('Удалить #'+id+'?'))return; const res=await fetch(`${A10}/schedule/persons/${id}`,{method:'DELETE',headers:ah()}); if(res.ok)loadPersons(); else{const d=await res.json();alert(d.error||'Ошибка');} } // ── Преподаватели ───────────────────────────────────────────────────────── async function loadTeachers(){ const [teachers,persons,depts,pos]=await Promise.all([ fetch(`${A10}/schedule/teachers`).then(r=>r.json()), fetch(`${A10}/schedule/persons`).then(r=>r.json()), fetch(`${A10}/schedule/departments`).then(r=>r.json()), fetch(`${A10}/schedule/positions`).then(r=>r.json()), ]); const fS=(id,data,fn,e=false)=>{ document.getElementById(id).innerHTML=(e?'<option value="">—</option>':'')+data.map(d=>`<option value="${d.id}">${fn(d)}</option>`).join(''); }; fS('tc-per',persons,p=>`${p.last_name} ${p.first_name}${p.middle_name?' '+p.middle_name:''}`); fS('tc-dep',depts,d=>d.name,true); fS('tc-pos',pos,p=>p.name,true); document.getElementById('tc-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="tda"> <button class="btn-o btn-sm" onclick="editTeacher(${t.id})">✏</button> <button class="btn-d btn-sm" onclick="delTeacher(${t.id})">✕</button> </div></td></tr>`).join('') :`<tr><td colspan="5" class="etd">Нет записей</td></tr>`; } function editTeacher(id){ document.getElementById('tc-id').value=id; document.getElementById('tc-title').textContent='Редактировать преподавателя'; document.getElementById('tc-status').textContent=''; } function cancelTeacher(){ document.getElementById('tc-id').value=''; document.getElementById('tc-title').textContent='Добавить преподавателя'; document.getElementById('tc-status').textContent=''; } async function saveTeacher(){ const id=document.getElementById('tc-id').value, pid=document.getElementById('tc-per').value, did=document.getElementById('tc-dep').value, posid=document.getElementById('tc-pos').value; if(!pid) return ss('tc-status','Выберите физ. лицо','err'); const res=await fetch(id?`${A10}/schedule/teachers/${id}`:`${A10}/schedule/teachers`,{method:id?'PATCH':'POST',headers:ah(),body:JSON.stringify({person_id:+pid,department_id:did?+did:null,position_id:posid?+posid:null})}); if(res.ok){ ss('tc-status',id?'✓ Обновлено':'✓ Добавлено','ok'); cancelTeacher(); loadTeachers(); loadSchedSels(); } else ss('tc-status','✕ Ошибка','err'); } async function delTeacher(id){ if(!confirm('Удалить преподавателя #'+id+'?'))return; const res=await fetch(`${A10}/schedule/teachers/${id}`,{method:'DELETE',headers:ah()}); if(res.ok)loadTeachers(); else{const d=await res.json();alert(d.error||'Ошибка');} } // ── Инициализация ───────────────────────────────────────────────────────── if (token) { try { role = JSON.parse(atob(token.split('.')[1])).role; } catch(_){} } updateUI(); loadSchedSels(); loadCatalog(); </script> </body> </html>