/
ring0
/
support-system
Обзор
Документация
Войти
/
ring0
/
support-system
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
static/admin.html
167 строк
8 KB
ring0
Create: specialist.html, admin.html
22 июн 2026, 17:14
Верифицирован
22 июн 2026, 17:14
3da1277
Код
Авторство
О чём код?
<!DOCTYPE html> <html lang="ru"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Административная панель</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: Arial, sans-serif; background: #f5f5f5; padding: 20px; } .container { max-width: 1300px; margin: 0 auto; } h1 { color: #333; margin-bottom: 20px; } .bar { background: white; padding: 16px 20px; border-radius: 5px; margin-bottom: 20px; display: flex; gap: 10px; align-items: center; flex-wrap: wrap; } input, select, button { padding: 9px; border: 1px solid #ddd; border-radius: 3px; font-family: inherit; } button { background: #2196F3; color: white; cursor: pointer; border: none; } button:hover { background: #0b7dda; } button.green { background: #4CAF50; } button.cancel { background: #F44336; } .hidden { display: none; } table { width: 100%; border-collapse: collapse; background: white; border-radius: 5px; overflow: hidden; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } th, td { padding: 10px; text-align: left; border-bottom: 1px solid #eee; font-size: 14px; } th { background: #37474F; color: white; } .badge { padding: 3px 8px; border-radius: 3px; font-size: 12px; color: white; } .status-open { background: #2196F3; } .status-in_progress { background: #FFC107; color: #333; } .status-resolved { background: #4CAF50; } .status-closed { background: #9E9E9E; } .modal { position: fixed; inset: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; } .modal-content { background: white; padding: 24px; border-radius: 5px; width: 380px; max-width: 92%; } .modal-content input { width: 100%; margin-bottom: 10px; } .error { color: #F44336; margin-bottom: 10px; min-height: 18px; } </style> </head> <body> <div class="container"> <h1>Административная панель</h1> <div id="loginView" class="modal"> <div class="modal-content"> <h2>Вход для администратора</h2> <div class="error" id="loginError"></div> <input type="text" id="loginUsername" placeholder="Логин" /> <input type="password" id="loginPassword" placeholder="Пароль" /> <button class="green" onclick="login()">Войти</button> </div> </div> <div id="appView" class="hidden"> <div class="bar"> <span id="userInfo"></span> <select id="filterStatus" onchange="loadTickets()"> <option value="">Все статусы</option> <option value="open">Открыта</option> <option value="in_progress">В работе</option> <option value="resolved">Решена</option> <option value="closed">Закрыта</option> </select> <select id="filterPriority" onchange="loadTickets()"><option value="">Все приоритеты</option></select> <select id="filterCategory" onchange="loadTickets()"><option value="">Все категории</option></select> <button class="cancel" onclick="logout()" style="margin-left:auto">Выйти</button> </div> <table> <thead> <tr> <th>№</th><th>Тема</th><th>Категория</th><th>Приоритет</th> <th>Заявитель</th><th>Исполнитель</th><th>Статус</th><th>Действия</th> </tr> </thead> <tbody id="ticketsBody"></tbody> </table> </div> </div> <script> let token = null, currentUser = null, specialists = []; const statusNames = { open: 'Открыта', in_progress: 'В работе', resolved: 'Решена', closed: 'Закрыта' }; async function api(path, options = {}) { options.headers = Object.assign({ 'Content-Type': 'application/json' }, options.headers || {}); if (token) options.headers['Authorization'] = 'Bearer ' + token; const res = await fetch(path, options); let data = null; try { data = await res.json(); } catch (e) {} return { ok: res.ok, status: res.status, data }; } async function login() { const username = document.getElementById('loginUsername').value; const password = document.getElementById('loginPassword').value; const { ok, data } = await api('/api/auth/login', { method: 'POST', body: JSON.stringify({ username, password }) }); if (!ok) { document.getElementById('loginError').textContent = (data && data.detail) || 'Ошибка входа'; return; } if (data.user.role !== 'admin') { document.getElementById('loginError').textContent = 'Доступ только для администратора'; return; } token = data.access_token; currentUser = data.user; document.getElementById('loginView').classList.add('hidden'); document.getElementById('appView').classList.remove('hidden'); document.getElementById('userInfo').textContent = currentUser.full_name + ' (администратор)'; await loadReferences(); await loadTickets(); } function logout() { location.reload(); } async function loadReferences() { const cats = (await api('/api/categories')).data; const prios = (await api('/api/priorities')).data; specialists = (await api('/api/users?role=specialist')).data || []; document.getElementById('filterCategory').innerHTML = '<option value="">Все категории</option>' + cats.map(c => `<option value="${c.id}">${c.name}</option>`).join(''); document.getElementById('filterPriority').innerHTML = '<option value="">Все приоритеты</option>' + prios.map(p => `<option value="${p.id}">${p.name}</option>`).join(''); } function specialistOptions(selectedId) { let opts = '<option value="">— не назначен —</option>'; opts += specialists.map(s => `<option value="${s.id}" ${s.id === selectedId ? 'selected' : ''}>${s.full_name}</option>`).join(''); return opts; } async function loadTickets() { const params = new URLSearchParams(); const st = document.getElementById('filterStatus').value; const pr = document.getElementById('filterPriority').value; const ct = document.getElementById('filterCategory').value; if (st) params.append('status', st); if (pr) params.append('priority_id', pr); if (ct) params.append('category_id', ct); const { data } = await api('/api/tickets?' + params.toString()); const body = document.getElementById('ticketsBody'); if (!data || data.length === 0) { body.innerHTML = '<tr><td colspan="8">Заявок нет.</td></tr>'; return; } body.innerHTML = data.map(t => ` <tr> <td>${t.id}</td> <td>${t.title}</td> <td>${t.category_name}</td> <td>${t.priority_name}</td> <td>${t.author_name}</td> <td> <select id="assignee-${t.id}">${specialistOptions(t.assignee_id)}</select> <button onclick="assign(${t.id})">Назначить</button> </td> <td><span class="badge status-${t.status}">${statusNames[t.status] || t.status}</span></td> <td> <select id="status-${t.id}"> ${Object.keys(statusNames).map(s => `<option value="${s}" ${s === t.status ? 'selected' : ''}>${statusNames[s]}</option>`).join('')} </select> <button onclick="changeStatus(${t.id})">OK</button> </td> </tr>`).join(''); } async function assign(id) { const assignee_id = parseInt(document.getElementById('assignee-' + id).value) || null; const { ok, data } = await api('/api/tickets/' + id, { method: 'PUT', body: JSON.stringify({ assignee_id }) }); if (!ok) { alert((data && data.error) || 'Ошибка назначения'); return; } loadTickets(); } async function changeStatus(id) { const status = document.getElementById('status-' + id).value; const { ok, data } = await api('/api/tickets/' + id, { method: 'PUT', body: JSON.stringify({ status }) }); if (!ok) { alert((data && data.error) || 'Ошибка обновления'); return; } loadTickets(); } </script> </body> </html>