/
ring0
/
support-system
Обзор
Документация
Войти
/
ring0
/
support-system
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
static/user.html
216 строк
10 KB
ring0
Create: user.html
22 июн 2026, 17:13
Верифицирован
22 июн 2026, 17:13
c01bc72
Код
Авторство
О чём код?
<!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: 1100px; margin: 0 auto; } h1 { color: #333; margin-bottom: 20px; } .bar { background: white; padding: 20px; border-radius: 5px; margin-bottom: 20px; display: flex; gap: 10px; align-items: center; flex-wrap: wrap; } input, select, button, textarea { padding: 10px; 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.green:hover { background: #45a049; } button.cancel { background: #F44336; } .hidden { display: none; } .ticket-card { background: white; padding: 16px; border-radius: 5px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); margin-bottom: 14px; } .ticket-card h3 { color: #333; margin-bottom: 8px; } .ticket-card p { color: #666; margin-bottom: 6px; font-size: 14px; } .meta { display: flex; gap: 8px; flex-wrap: wrap; margin: 8px 0; } .badge { padding: 4px 10px; 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; } .prio { background: #607D8B; } .cat { background: #795548; } .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: 420px; max-width: 92%; } .modal-content input, .modal-content select, .modal-content textarea { width: 100%; margin-bottom: 10px; } .error { color: #F44336; margin-bottom: 10px; min-height: 18px; } .comments { margin-top: 10px; border-top: 1px solid #eee; padding-top: 10px; } .comment { font-size: 13px; color: #444; padding: 4px 0; } .row { display: flex; gap: 8px; } </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> <button class="green" onclick="openModal()">Создать заявку</button> <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> <button class="cancel" onclick="logout()" style="margin-left:auto">Выйти</button> </div> <div id="ticketsList"></div> </div> <!-- Модальное окно создания заявки --> <div id="createModal" class="modal hidden"> <div class="modal-content"> <h2>Новая заявка</h2> <div class="error" id="createError"></div> <input type="text" id="title" placeholder="Тема заявки" /> <textarea id="description" rows="4" placeholder="Описание проблемы"></textarea> <select id="categoryId"></select> <select id="priorityId"></select> <div class="row"> <button class="green" onclick="createTicket()">Отправить</button> <button class="cancel" onclick="closeModal()">Отмена</button> </div> </div> </div> </div> <script> let token = null; let currentUser = null; 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; } 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; document.getElementById('categoryId').innerHTML = cats.map(c => `<option value="${c.id}">${c.name}</option>`).join(''); document.getElementById('priorityId').innerHTML = prios.map(p => `<option value="${p.id}">${p.name}</option>`).join(''); } async function loadTickets() { const status = document.getElementById('filterStatus').value; let url = '/api/tickets'; if (status) url += '?status=' + status; const { data } = await api(url); const list = document.getElementById('ticketsList'); if (!data || data.length === 0) { list.innerHTML = '<p>Заявок нет.</p>'; return; } list.innerHTML = data.map(renderTicket).join(''); for (const t of data) loadComments(t.id); } function renderTicket(t) { const statusNames = { open: 'Открыта', in_progress: 'В работе', resolved: 'Решена', closed: 'Закрыта' }; return ` <div class="ticket-card"> <h3>№${t.id}. ${t.title}</h3> <p>${t.description}</p> <div class="meta"> <span class="badge cat">${t.category_name}</span> <span class="badge prio">${t.priority_name}</span> <span class="badge status-${t.status}">${statusNames[t.status] || t.status}</span> </div> <p>Исполнитель: ${t.assignee_name || 'не назначен'} • создана: ${t.created_at}</p> <div class="comments" id="comments-${t.id}"></div> <div class="row" style="margin-top:8px"> <input type="text" id="comment-input-${t.id}" placeholder="Комментарий..." style="flex:1" /> <button onclick="addComment(${t.id})">Добавить</button> </div> </div>`; } async function loadComments(ticketId) { const { data } = await api(`/api/tickets/${ticketId}/comments`); const box = document.getElementById('comments-' + ticketId); if (!box) return; box.innerHTML = (data || []).map(c => `<div class="comment"><b>${c.author_name}:</b> ${c.text}</div>`).join(''); } async function addComment(ticketId) { const input = document.getElementById('comment-input-' + ticketId); const text = input.value.trim(); if (!text) return; await api(`/api/tickets/${ticketId}/comments`, { method: 'POST', body: JSON.stringify({ text }) }); input.value = ''; loadComments(ticketId); } function openModal() { document.getElementById('createModal').classList.remove('hidden'); document.getElementById('createError').textContent = ''; } function closeModal() { document.getElementById('createModal').classList.add('hidden'); } async function createTicket() { const title = document.getElementById('title').value.trim(); const description = document.getElementById('description').value.trim(); const category_id = parseInt(document.getElementById('categoryId').value); const priority_id = parseInt(document.getElementById('priorityId').value); if (!title || !description) { document.getElementById('createError').textContent = 'Заполните тему и описание'; return; } const { ok, status, data } = await api('/api/tickets', { method: 'POST', body: JSON.stringify({ title, description, category_id, priority_id }) }); if (!ok) { let msg = (data && data.error) || 'Ошибка создания заявки'; if (status === 409 && data.existing_ticket_id) msg += ` (существующая заявка №${data.existing_ticket_id})`; document.getElementById('createError').textContent = msg; return; } document.getElementById('title').value = ''; document.getElementById('description').value = ''; closeModal(); loadTickets(); } </script> </body> </html>