318 lines
15 KiB
HTML
318 lines
15 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ru" data-theme="light">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||
<title>Журнал заявок — Ремонт оргтехники</title>
|
||
<link rel="stylesheet" href="design-system/kt-ai-fonts.css">
|
||
<link rel="stylesheet" href="design-system/kt-ai-tokens.css">
|
||
<link rel="stylesheet" href="design-system/kt-ai-components.css">
|
||
<link rel="stylesheet" href="design-system/kt-ai-page.css">
|
||
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🖨️</text></svg>">
|
||
<style>
|
||
.kt-ai-app { min-height: 100vh; display: flex; flex-direction: column; }
|
||
.main-content { flex: 1; padding: var(--kt-ai-space-6) 0; }
|
||
.kpi-row { display: flex; gap: var(--kt-ai-space-4); flex-wrap: wrap; margin-bottom: var(--kt-ai-space-6); }
|
||
.toolbar { display: flex; gap: var(--kt-ai-space-3); flex-wrap: wrap; align-items: center; margin-bottom: var(--kt-ai-space-4); }
|
||
.toolbar input { flex: 1; min-width: 200px; }
|
||
.table-wrap { overflow-x: auto; border: 1px solid var(--kt-ai-card-border); border-radius: var(--kt-ai-radius-xl); }
|
||
table { width: 100%; border-collapse: collapse; font-size: var(--kt-ai-text-sm); }
|
||
th, td { padding: var(--kt-ai-space-3) var(--kt-ai-space-4); text-align: left; border-bottom: 1px solid var(--kt-ai-border); }
|
||
th { background: var(--kt-ai-bg-soft); font-weight: var(--kt-ai-weight-semibold); color: var(--kt-ai-fg-muted); }
|
||
tr:hover { background: var(--kt-ai-bg-soft); }
|
||
td.num { text-align: right; font-variant-numeric: tabular-nums; }
|
||
.actions { display: flex; gap: var(--kt-ai-space-2); }
|
||
.drawer-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.4); display: none; z-index: 100; }
|
||
.drawer-overlay.on { display: flex; justify-content: flex-end; }
|
||
.drawer { width: 420px; max-width: 100%; background: var(--kt-ai-card-bg); padding: var(--kt-ai-space-6); overflow-y: auto; }
|
||
.drawer h2 { margin-bottom: var(--kt-ai-space-4); }
|
||
.form-group { margin-bottom: var(--kt-ai-space-4); }
|
||
.form-group label { display: block; margin-bottom: var(--kt-ai-space-2); font-weight: var(--kt-ai-weight-medium); }
|
||
.form-group input, .form-group select, .form-group textarea { width: 100%; }
|
||
.form-group textarea { min-height: 80px; resize: vertical; }
|
||
.status-chip { display: inline-block; padding: 2px 10px; border-radius: var(--kt-ai-radius-full); font-size: var(--kt-ai-text-xs); font-weight: var(--kt-ai-weight-semibold); }
|
||
.status-new { background: #dbeafe; color: #1d4ed8; }
|
||
.status-in-work { background: #fef3c7; color: #b45309; }
|
||
.status-done { background: #d1fae5; color: #047857; }
|
||
.status-closed { background: #e5e7eb; color: #374151; }
|
||
</style>
|
||
</head>
|
||
<body class="kt-ai-app">
|
||
|
||
<header class="kt-ai-pagehead">
|
||
<div class="kt-ai-wrap kt-ai-pagehead-in">
|
||
<div style="display:flex;align-items:center;gap:var(--kt-ai-space-3)">
|
||
<img src="https://code.vibe42.kz/tore/aitu/kt-logo.svg" alt="Казахтелеком" height="28">
|
||
<strong>Журнал заявок — Ремонт оргтехники</strong>
|
||
</div>
|
||
<span class="kt-ai-spacer"></span>
|
||
<button class="kt-ai-btn" data-variant="ghost" onclick="toggleTheme()">🌓 Тема</button>
|
||
<button class="kt-ai-btn" data-variant="primary" onclick="openDrawer()">+ Новая заявка</button>
|
||
</div>
|
||
</header>
|
||
|
||
<main class="main-content">
|
||
<div class="kt-ai-wrap">
|
||
|
||
<div class="kt-ai-kpi-strip" style="margin-bottom:var(--kt-ai-space-6)">
|
||
<div class="kt-ai-kpi">
|
||
<div class="value" id="kpi-total">0</div>
|
||
<div class="label">Всего заявок</div>
|
||
<div class="hint">за всё время</div>
|
||
</div>
|
||
<div class="kt-ai-kpi">
|
||
<div class="value" id="kpi-new" style="color:var(--kt-ai-chart-blue)">0</div>
|
||
<div class="label">Новые</div>
|
||
<div class="hint">ожидают</div>
|
||
</div>
|
||
<div class="kt-ai-kpi">
|
||
<div class="value" id="kpi-in-work" style="color:var(--kt-ai-chart-orange)">0</div>
|
||
<div class="label">В работе</div>
|
||
<div class="hint">в ремонте</div>
|
||
</div>
|
||
<div class="kt-ai-kpi">
|
||
<div class="value" id="kpi-done" style="color:var(--kt-ai-chart-green)">0</div>
|
||
<div class="label">Готово</div>
|
||
<div class="hint">выполнено</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="toolbar">
|
||
<input type="text" class="kt-ai-input" id="search-input" placeholder="Поиск по заявке, заявителю, технике..." oninput="renderTable()">
|
||
<select class="kt-ai-select" id="status-filter" onchange="renderTable()">
|
||
<option value="">Все статусы</option>
|
||
<option value="new">Новые</option>
|
||
<option value="in-work">В работе</option>
|
||
<option value="done">Готово</option>
|
||
<option value="closed">Закрыто</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="table-wrap">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th style="width:60px">№</th>
|
||
<th>Дата</th>
|
||
<th>Заявитель</th>
|
||
<th>Техника</th>
|
||
<th>Неисправность</th>
|
||
<th>Срочность</th>
|
||
<th>Статус</th>
|
||
<th>Исполнитель</th>
|
||
<th style="width:100px">Действия</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="table-body"></tbody>
|
||
</table>
|
||
</div>
|
||
|
||
</div>
|
||
</main>
|
||
|
||
<div class="drawer-overlay" id="drawer-overlay" onclick="if(event.target===this)closeDrawer()">
|
||
<div class="drawer">
|
||
<h2 id="drawer-title">Новая заявка</h2>
|
||
<form id="ticket-form" onsubmit="saveTicket(event)">
|
||
<input type="hidden" id="ticket-id">
|
||
<div class="form-group">
|
||
<label>Заявитель *</label>
|
||
<input type="text" class="kt-ai-input" id="ticket-requester" required placeholder="ФИО или отдел">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Тип техники *</label>
|
||
<select class="kt-ai-select" id="ticket-type" required>
|
||
<option value="">Выберите...</option>
|
||
<option>Принтер</option>
|
||
<option>МФУ</option>
|
||
<option>Компьютер</option>
|
||
<option>Ноутбук</option>
|
||
<option>Ксерокс</option>
|
||
<option>Сканер</option>
|
||
<option>Факс</option>
|
||
<option>Другое</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Описание неисправности *</label>
|
||
<textarea class="kt-ai-textarea" id="ticket-description" required placeholder="Что случилось?"></textarea>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Срочность</label>
|
||
<select class="kt-ai-select" id="ticket-urgency">
|
||
<option value="normal">Обычная</option>
|
||
<option value="high">Высокая</option>
|
||
<option value="critical">Критическая</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Исполнитель</label>
|
||
<input type="text" class="kt-ai-input" id="ticket-assignee" placeholder="Кто в работе">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Статус</label>
|
||
<select class="kt-ai-select" id="ticket-status">
|
||
<option value="new">Новая</option>
|
||
<option value="in-work">В работе</option>
|
||
<option value="done">Готово</option>
|
||
<option value="closed">Закрыто</option>
|
||
</select>
|
||
</div>
|
||
<div style="display:flex;gap:var(--kt-ai-space-3);margin-top:var(--kt-ai-space-6)">
|
||
<button type="button" class="kt-ai-btn" onclick="closeDrawer()">Отмена</button>
|
||
<button type="submit" class="kt-ai-btn" data-variant="primary">Сохранить</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
const DEMO_DATA = [
|
||
{ id: 1709884800000, date: '08.03.2026', requester: 'Иванов А.С. (Бухгалтерия)', type: 'Принтер', description: 'Замятие бумаги, печатает с полосами', urgency: 'high', status: 'in-work', assignee: 'Петров К.' },
|
||
{ id: 1709971200000, date: '09.03.2026', requester: 'Смирнова Е. (Отдел кадров)', type: 'МФУ', description: 'Не сканирует, ошибка драйвера', urgency: 'normal', status: 'new', assignee: '' },
|
||
{ id: 1710057600000, date: '10.03.2026', requester: 'Алиев Н. (Приёмная)', type: 'Ксерокс', description: 'Не включается, нет индикации', urgency: 'critical', status: 'in-work', assignee: 'Серикбаев М.' },
|
||
{ id: 1710144000000, date: '11.03.2026', requester: 'Оралов Д. (IT-отдел)', type: 'Компьютер', description: 'Шумит системный блок', urgency: 'normal', status: 'done', assignee: 'Петров К.' },
|
||
{ id: 1710230400000, date: '12.03.2026', requester: 'Ким Ю. (Маркетинг)', type: 'Ноутбук', description: 'Не работает клавиатура, залит кофе', urgency: 'high', status: 'new', assignee: '' },
|
||
{ id: 1710316800000, date: '13.03.2026', requester: 'Нурсултанов А. (Юр.отдел)', type: 'Принтер', description: 'Закончился тонер, нужна заправка', urgency: 'normal', status: 'closed', assignee: 'Петров К.' },
|
||
{ id: 1710403200000, date: '14.03.2026', requester: 'Ахметова Г. (Приёмная)', type: 'Факс', description: 'Не принимает входящие', urgency: 'critical', status: 'in-work', assignee: 'Серикбаев М.' },
|
||
{ id: 1710489600000, date: '15.03.2026', requester: 'Тимофеев Р. (Склад)', type: 'Сканер', description: 'Полосы при сканировании', urgency: 'normal', status: 'new', assignee: '' }
|
||
];
|
||
|
||
function loadTickets() {
|
||
const stored = localStorage.getItem('tickets');
|
||
return stored ? JSON.parse(stored) : DEMO_DATA;
|
||
}
|
||
|
||
function saveTickets(tickets) {
|
||
localStorage.setItem('tickets', JSON.stringify(tickets));
|
||
}
|
||
|
||
function renderKPI() {
|
||
const tickets = loadTickets();
|
||
document.getElementById('kpi-total').textContent = tickets.length;
|
||
document.getElementById('kpi-new').textContent = tickets.filter(t => t.status === 'new').length;
|
||
document.getElementById('kpi-in-work').textContent = tickets.filter(t => t.status === 'in-work').length;
|
||
document.getElementById('kpi-done').textContent = tickets.filter(t => t.status === 'done' || t.status === 'closed').length;
|
||
}
|
||
|
||
function getStatusChip(status) {
|
||
const labels = { 'new': 'Новая', 'in-work': 'В работе', 'done': 'Готово', 'closed': 'Закрыто' };
|
||
return '<span class="status-chip status-' + status + '">' + labels[status] + '</span>';
|
||
}
|
||
|
||
function getUrgencyLabel(urgency) {
|
||
const labels = { 'normal': 'Обычная', 'high': 'Высокая', 'critical': 'Критическая' };
|
||
return labels[urgency] || urgency;
|
||
}
|
||
|
||
function renderTable() {
|
||
const tickets = loadTickets();
|
||
const search = document.getElementById('search-input').value.toLowerCase();
|
||
const statusFilter = document.getElementById('status-filter').value;
|
||
|
||
const filtered = tickets.filter(t => {
|
||
const matchesSearch = !search ||
|
||
t.requester.toLowerCase().includes(search) ||
|
||
t.type.toLowerCase().includes(search) ||
|
||
t.description.toLowerCase().includes(search) ||
|
||
(t.assignee && t.assignee.toLowerCase().includes(search));
|
||
const matchesStatus = !statusFilter || t.status === statusFilter;
|
||
return matchesSearch && matchesStatus;
|
||
});
|
||
|
||
document.getElementById('table-body').innerHTML = filtered.map(t =>
|
||
'<tr>' +
|
||
'<td class="num">' + t.id.toString().slice(-4) + '</td>' +
|
||
'<td>' + t.date + '</td>' +
|
||
'<td>' + t.requester + '</td>' +
|
||
'<td>' + t.type + '</td>' +
|
||
'<td>' + t.description + '</td>' +
|
||
'<td>' + getUrgencyLabel(t.urgency) + '</td>' +
|
||
'<td>' + getStatusChip(t.status) + '</td>' +
|
||
'<td>' + (t.assignee || '—') + '</td>' +
|
||
'<td class="actions">' +
|
||
'<button class="kt-ai-btn" data-size="sm" onclick="editTicket(' + t.id + ')">✏️</button>' +
|
||
'<button class="kt-ai-btn" data-size="sm" data-variant="danger" onclick="deleteTicket(' + t.id + ')">🗑️</button>' +
|
||
'</td>' +
|
||
'</tr>'
|
||
).join('');
|
||
}
|
||
|
||
function openDrawer() {
|
||
document.getElementById('ticket-form').reset();
|
||
document.getElementById('ticket-id').value = '';
|
||
document.getElementById('drawer-title').textContent = 'Новая заявка';
|
||
document.getElementById('drawer-overlay').classList.add('on');
|
||
}
|
||
|
||
function closeDrawer() {
|
||
document.getElementById('drawer-overlay').classList.remove('on');
|
||
}
|
||
|
||
function editTicket(id) {
|
||
const tickets = loadTickets();
|
||
const ticket = tickets.find(t => t.id === id);
|
||
if (!ticket) return;
|
||
|
||
document.getElementById('ticket-id').value = ticket.id;
|
||
document.getElementById('ticket-requester').value = ticket.requester;
|
||
document.getElementById('ticket-type').value = ticket.type;
|
||
document.getElementById('ticket-description').value = ticket.description;
|
||
document.getElementById('ticket-urgency').value = ticket.urgency;
|
||
document.getElementById('ticket-assignee').value = ticket.assignee || '';
|
||
document.getElementById('ticket-status').value = ticket.status;
|
||
document.getElementById('drawer-title').textContent = 'Заявка №' + ticket.id.toString().slice(-4);
|
||
document.getElementById('drawer-overlay').classList.add('on');
|
||
}
|
||
|
||
function saveTicket(e) {
|
||
e.preventDefault();
|
||
const id = document.getElementById('ticket-id').value;
|
||
const tickets = loadTickets();
|
||
|
||
const ticket = {
|
||
id: id ? parseInt(id) : Date.now(),
|
||
date: id ? tickets.find(t => t.id == id).date : new Date().toLocaleDateString('ru-RU'),
|
||
requester: document.getElementById('ticket-requester').value,
|
||
type: document.getElementById('ticket-type').value,
|
||
description: document.getElementById('ticket-description').value,
|
||
urgency: document.getElementById('ticket-urgency').value,
|
||
assignee: document.getElementById('ticket-assignee').value,
|
||
status: document.getElementById('ticket-status').value
|
||
};
|
||
|
||
if (id) {
|
||
const idx = tickets.findIndex(t => t.id == id);
|
||
tickets[idx] = ticket;
|
||
} else {
|
||
tickets.unshift(ticket);
|
||
}
|
||
|
||
saveTickets(tickets);
|
||
closeDrawer();
|
||
renderKPI();
|
||
renderTable();
|
||
}
|
||
|
||
function deleteTicket(id) {
|
||
if (!confirm('Удалить заявку №' + id.toString().slice(-4) + '?')) return;
|
||
const tickets = loadTickets().filter(t => t.id !== id);
|
||
saveTickets(tickets);
|
||
renderKPI();
|
||
renderTable();
|
||
}
|
||
|
||
function toggleTheme() {
|
||
const html = document.documentElement;
|
||
const current = html.getAttribute('data-theme');
|
||
html.setAttribute('data-theme', current === 'light' ? 'dark' : 'light');
|
||
}
|
||
|
||
// Init
|
||
renderKPI();
|
||
renderTable();
|
||
</script>
|
||
|
||
</body>
|
||
</html>
|