366 lines
15 KiB
HTML
366 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">
|
||
<style>
|
||
.reveal{opacity:0;transform:translateY(20px);transition:opacity .6s ease,transform .6s ease}
|
||
.reveal.in{opacity:1;transform:none}
|
||
.status-chip[data-status="Новая"]{background:var(--kt-ai-primary);color:#fff}
|
||
.status-chip[data-status="В работе"]{background:#f59e0b;color:#fff}
|
||
.status-chip[data-status="Выполнено"]{background:#10b981;color:#fff}
|
||
.status-chip[data-status="Отменено"]{background:var(--kt-ai-danger);color:#fff}
|
||
.table-wrap{overflow-x:auto}
|
||
table{width:100%;border-collapse:collapse}
|
||
th,td{padding:12px 16px;text-align:left;border-bottom:1px solid var(--kt-ai-divider)}
|
||
th{font-weight:600;color:var(--kt-ai-fg-muted);font-size:var(--kt-ai-text-sm)}
|
||
tr:hover{background:var(--kt-ai-bg-soft)}
|
||
.log-entry{font-size:var(--kt-ai-text-sm);padding:8px 0;border-bottom:1px solid var(--kt-ai-divider)}
|
||
.log-time{color:var(--kt-ai-fg-faint);font-size:var(--kt-ai-text-xs)}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<header class="kt-ai-pagehead">
|
||
<div class="kt-ai-wrap kt-ai-pagehead-in">
|
||
<strong>Журнал заявок</strong>
|
||
<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="openModal()">+ Добавить заявку</button>
|
||
</div>
|
||
</header>
|
||
|
||
<main>
|
||
<section class="kt-ai-hero" data-align="center">
|
||
<div class="kt-ai-wrap">
|
||
<h1>Учёт заявок сотрудников</h1>
|
||
<p class="kt-ai-hero-sub">Отслеживайте статусы заявок в режиме реального времени</p>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="kt-ai-section">
|
||
<div class="kt-ai-wrap" data-width="wide">
|
||
|
||
<!-- KPI карточки -->
|
||
<div class="kt-ai-grid" data-cols="4" style="margin-bottom:var(--kt-ai-space-9)">
|
||
<article class="kt-ai-tile" style="text-align:center">
|
||
<div style="font-size:32px;font-weight:bold;color:var(--kt-ai-primary)" id="kpi-total">0</div>
|
||
<div style="color:var(--kt-ai-fg-muted);font-size:var(--kt-ai-text-sm)">Всего заявок</div>
|
||
</article>
|
||
<article class="kt-ai-tile" style="text-align:center">
|
||
<div style="font-size:32px;font-weight:bold;color:#3b82f6" id="kpi-new">0</div>
|
||
<div style="color:var(--kt-ai-fg-muted);font-size:var(--kt-ai-text-sm)">Новые</div>
|
||
</article>
|
||
<article class="kt-ai-tile" style="text-align:center">
|
||
<div style="font-size:32px;font-weight:bold;color:#f59e0b" id="kpi-progress">0</div>
|
||
<div style="color:var(--kt-ai-fg-muted);font-size:var(--kt-ai-text-sm)">В работе</div>
|
||
</article>
|
||
<article class="kt-ai-tile" style="text-align:center">
|
||
<div style="font-size:32px;font-weight:bold;color:#10b981" id="kpi-done">0</div>
|
||
<div style="color:var(--kt-ai-fg-muted);font-size:var(--kt-ai-text-sm)">Выполнено</div>
|
||
</article>
|
||
</div>
|
||
|
||
<!-- Поиск и фильтры -->
|
||
<div style="display:flex;gap:12px;flex-wrap:wrap;margin-bottom:var(--kt-ai-space-6);align-items:center">
|
||
<div class="kt-ai-field" style="flex:1;min-width:280px;margin:0">
|
||
<input class="kt-ai-input" id="search-input" placeholder="Поиск по заявке..." oninput="applyFilters()">
|
||
</div>
|
||
<div style="display:flex;gap:8px;flex-wrap:wrap" id="filter-chips">
|
||
<button class="kt-ai-btn" data-variant="primary" onclick="filterByStatus(null)">Все</button>
|
||
<button class="kt-ai-btn" data-variant="ghost" onclick="filterByStatus('Новая')">Новая</button>
|
||
<button class="kt-ai-btn" data-variant="ghost" onclick="filterByStatus('В работе')">В работе</button>
|
||
<button class="kt-ai-btn" data-variant="ghost" onclick="filterByStatus('Выполнено')">Выполнено</button>
|
||
<button class="kt-ai-btn" data-variant="ghost" onclick="filterByStatus('Отменено')">Отменено</button>
|
||
</div>
|
||
<button class="kt-ai-btn" data-variant="ghost" onclick="exportToExcel()">📥 Excel</button>
|
||
</div>
|
||
|
||
<!-- Таблица -->
|
||
<div class="kt-ai-card table-wrap reveal">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>Дата</th>
|
||
<th>ФИО</th>
|
||
<th>Суть заявки</th>
|
||
<th>Срок</th>
|
||
<th>Статус</th>
|
||
<th>Действия</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="requests-table"></tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<!-- Журнал изменений -->
|
||
<div class="kt-ai-card" style="margin-top:var(--kt-ai-space-6)">
|
||
<h2 style="margin-bottom:var(--kt-ai-space-4)">Журнал изменений</h2>
|
||
<div id="activity-log" style="max-height:300px;overflow-y:auto"></div>
|
||
</div>
|
||
|
||
</div>
|
||
</section>
|
||
</main>
|
||
|
||
<footer class="kt-ai-wrap kt-ai-pagefoot">
|
||
<span>© 2026 Журнал заявок</span>
|
||
<span class="kt-ai-spacer"></span>
|
||
<span style="color:var(--kt-ai-fg-fainted)">Данные сохраняются локально в браузере</span>
|
||
</footer>
|
||
|
||
<!-- Модальное окно -->
|
||
<div id="modal-overlay" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:1000" onclick="closeModal(event)">
|
||
<div class="kt-ai-card" style="max-width:520px;margin:60px auto;padding:var(--kt-ai-space-8);position:relative;max-height:90vh;overflow-y:auto">
|
||
<button class="kt-ai-btn" data-variant="ghost" style="position:absolute;right:16px;top:16px" onclick="closeModal()">✕</button>
|
||
<h2 id="modal-title" style="margin-bottom:var(--kt-ai-space-6)">Новая заявка</h2>
|
||
<form id="request-form" onsubmit="saveRequest(event)">
|
||
<input type="hidden" id="req-id">
|
||
<div class="kt-ai-field">
|
||
<label>ФИО сотрудника</label>
|
||
<input class="kt-ai-input" id="req-name" required placeholder="Иванов Иван Иванович">
|
||
</div>
|
||
<div class="kt-ai-field">
|
||
<label>Суть заявки</label>
|
||
<textarea class="kt-ai-input" id="req-desc" required rows="3" placeholder="Опишите, что требуется"></textarea>
|
||
</div>
|
||
<div class="kt-ai-field">
|
||
<label>Срок исполнения</label>
|
||
<input class="kt-ai-input" id="req-deadline" type="date" required>
|
||
</div>
|
||
<div class="kt-ai-field">
|
||
<label>Статус</label>
|
||
<select class="kt-ai-input" id="req-status">
|
||
<option value="Новая">Новая</option>
|
||
<option value="В работе">В работе</option>
|
||
<option value="Выполнено">Выполнено</option>
|
||
<option value="Отменено">Отменено</option>
|
||
</select>
|
||
</div>
|
||
<div style="display:flex;gap:12px;margin-top:var(--kt-ai-space-6)">
|
||
<button class="kt-ai-btn" data-variant="primary" type="submit">Сохранить</button>
|
||
<button class="kt-ai-btn" data-variant="ghost" type="button" onclick="closeModal()">Отмена</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// Демо-данные
|
||
const DEMO_DATA = [
|
||
{id:1,name:"Асет Каримов",desc:"Настройка ПО для нового сотрудника",deadline:"2026-09-10",status:"Новая",date:"2026-09-03"},
|
||
{id:2,name:"Айгуль Мухамедова",desc:"Выдача СИЗ (каска, перчатки)",deadline:"2026-09-05",status:"В работе",date:"2026-09-02"},
|
||
{id:3,name:"Ержан Асанов",desc:"Ремонт принтера в кабинете 304",deadline:"2026-09-08",status:"Выполнено",date:"2026-09-01"},
|
||
{id:4,name:"Сауле Нурланова",desc:"Предоставление доступа к базе данных",deadline:"2026-09-12",status:"Новая",date:"2026-09-03"},
|
||
{id:5,name:"Бауыржан Омаров",desc:"Замена картриджа для МФУ",deadline:"2026-09-04",status:"Отменено",date:"2026-09-01"}
|
||
];
|
||
|
||
const STATUSES = ["Новая","В работе","Выполнено","Отменено"];
|
||
let requests = [];
|
||
let activityLog = [];
|
||
let currentFilter = null;
|
||
|
||
// Инициализация
|
||
function init() {
|
||
const saved = localStorage.getItem('requests');
|
||
const savedLog = localStorage.getItem('activityLog');
|
||
if (saved) {
|
||
requests = JSON.parse(saved);
|
||
activityLog = JSON.parse(savedLog) || [];
|
||
} else {
|
||
requests = [...DEMO_DATA];
|
||
activityLog = [{time:new Date().toISOString(),action:"Инициализация",user:"Система",details:"Загружены демо-данные"}];
|
||
saveData();
|
||
}
|
||
renderTable();
|
||
renderKPI();
|
||
renderLog();
|
||
}
|
||
|
||
function saveData() {
|
||
localStorage.setItem('requests', JSON.stringify(requests));
|
||
localStorage.setItem('activityLog', JSON.stringify(activityLog));
|
||
}
|
||
|
||
// Рендер таблицы
|
||
function renderTable() {
|
||
const tbody = document.getElementById('requests-table');
|
||
const search = document.getElementById('search-input').value.toLowerCase();
|
||
|
||
let filtered = requests.filter(r => {
|
||
const matchSearch = r.name.toLowerCase().includes(search) || r.desc.toLowerCase().includes(search);
|
||
const matchStatus = !currentFilter || r.status === currentFilter;
|
||
return matchSearch && matchStatus;
|
||
});
|
||
|
||
tbody.innerHTML = filtered.map(r => `
|
||
<tr>
|
||
<td>${formatDate(r.date)}</td>
|
||
<td>${escapeHtml(r.name)}</td>
|
||
<td>${escapeHtml(r.desc)}</td>
|
||
<td>${formatDate(r.deadline)}</td>
|
||
<td><span class="kt-ai-chip status-chip" data-status="${r.status}">${r.status}</span></td>
|
||
<td>
|
||
<button class="kt-ai-btn" data-variant="ghost" data-size="sm" onclick="editRequest(${r.id})">✏️</button>
|
||
<button class="kt-ai-btn" data-variant="danger" data-size="sm" onclick="deleteRequest(${r.id})">🗑️</button>
|
||
</td>
|
||
</tr>
|
||
`).join('');
|
||
}
|
||
|
||
// KPI
|
||
function renderKPI() {
|
||
document.getElementById('kpi-total').textContent = requests.length;
|
||
document.getElementById('kpi-new').textContent = requests.filter(r=>r.status==="Новая").length;
|
||
document.getElementById('kpi-progress').textContent = requests.filter(r=>r.status==="В работе").length;
|
||
document.getElementById('kpi-done').textContent = requests.filter(r=>r.status==="Выполнено").length;
|
||
}
|
||
|
||
// Журнал
|
||
function renderLog() {
|
||
const log = document.getElementById('activity-log');
|
||
log.innerHTML = activityLog.slice().reverse().map(e => `
|
||
<div class="log-entry">
|
||
<div><strong>${escapeHtml(e.action)}</strong> — ${escapeHtml(e.user)}</div>
|
||
<div style="color:var(--kt-ai-fg-muted)">${escapeHtml(e.details||"")}</div>
|
||
<div class="log-time">${new Date(e.time).toLocaleString('ru-RU')}</div>
|
||
</div>
|
||
`).join('');
|
||
}
|
||
|
||
function logAction(action, details) {
|
||
activityLog.push({time:new Date().toISOString(),action,user:"Пользователь",details});
|
||
saveData();
|
||
renderLog();
|
||
}
|
||
|
||
// Фильтры
|
||
function filterByStatus(status) {
|
||
currentFilter = status;
|
||
const buttons = document.querySelectorAll('#filter-chips .kt-ai-btn');
|
||
buttons.forEach(btn => {
|
||
btn.dataset.variant = (!status && btn.textContent==="Все") || btn.textContent===status ? 'primary' : 'ghost';
|
||
});
|
||
renderTable();
|
||
}
|
||
|
||
function applyFilters() {
|
||
renderTable();
|
||
}
|
||
|
||
// Модальное окно
|
||
function openModal() {
|
||
document.getElementById('modal-overlay').style.display = 'block';
|
||
document.getElementById('request-form').reset();
|
||
document.getElementById('req-id').value = '';
|
||
document.getElementById('modal-title').textContent = 'Новая заявка';
|
||
}
|
||
|
||
function closeModal(e) {
|
||
if (!e || e.target === document.getElementById('modal-overlay')) {
|
||
document.getElementById('modal-overlay').style.display = 'none';
|
||
}
|
||
}
|
||
|
||
// CRUD
|
||
function saveRequest(e) {
|
||
e.preventDefault();
|
||
const id = document.getElementById('req-id').value;
|
||
const name = document.getElementById('req-name').value.trim();
|
||
const desc = document.getElementById('req-desc').value.trim();
|
||
const deadline = document.getElementById('req-deadline').value;
|
||
const status = document.getElementById('req-status').value;
|
||
|
||
if (id) {
|
||
// Редактирование
|
||
const idx = requests.findIndex(r => r.id == id);
|
||
if (idx !== -1) {
|
||
const old = requests[idx];
|
||
requests[idx] = {...old, name, desc, deadline, status};
|
||
logAction('Редактирование', `Заявка #${id}: ${old.desc} → статус "${status}"`);
|
||
}
|
||
} else {
|
||
// Создание
|
||
const newReq = {
|
||
id: Date.now(),
|
||
name, desc, deadline, status,
|
||
date: new Date().toISOString().slice(0,10)
|
||
};
|
||
requests.push(newReq);
|
||
logAction('Создание', `Новая заявка от ${name}: ${desc}`);
|
||
}
|
||
saveData();
|
||
renderTable();
|
||
renderKPI();
|
||
closeModal();
|
||
}
|
||
|
||
function editRequest(id) {
|
||
const r = requests.find(x => x.id === id);
|
||
if (!r) return;
|
||
document.getElementById('req-id').value = r.id;
|
||
document.getElementById('req-name').value = r.name;
|
||
document.getElementById('req-desc').value = r.desc;
|
||
document.getElementById('req-deadline').value = r.deadline;
|
||
document.getElementById('req-status').value = r.status;
|
||
document.getElementById('modal-title').textContent = 'Редактирование заявки';
|
||
document.getElementById('modal-overlay').style.display = 'block';
|
||
}
|
||
|
||
function deleteRequest(id) {
|
||
const r = requests.find(x => x.id === id);
|
||
if (!r) return;
|
||
if (confirm(`Удалить заявку "${r.desc}"?`)) {
|
||
requests = requests.filter(x => x.id !== id);
|
||
logAction('Удаление', `Заявка #${id} от ${r.name}: ${r.desc}`);
|
||
saveData();
|
||
renderTable();
|
||
renderKPI();
|
||
}
|
||
}
|
||
|
||
// Экспорт в Excel (CSV)
|
||
function exportToExcel() {
|
||
const head = ["Дата","ФИО","Суть заявки","Срок","Статус"];
|
||
const rows = requests.map(r => [r.date, r.name, r.desc, r.deadline, r.status]);
|
||
const csv = '\uFEFF' + [head.join(';'), ...rows.map(r => r.map(v => '"'+String(v).replace(/"/g,'""')+'"').join(';'))].join('\n');
|
||
const blob = new Blob([csv], {type:'text/csv;charset=utf-8;'});
|
||
const link = document.createElement('a');
|
||
link.href = URL.createObjectURL(blob);
|
||
link.download = 'zhurnal_zayavok_' + new Date().toISOString().slice(0,10) + '.csv';
|
||
link.click();
|
||
logAction('Экспорт', 'Выгрузка в Excel');
|
||
}
|
||
|
||
// Утилиты
|
||
function formatDate(iso) {
|
||
const d = new Date(iso);
|
||
return d.toLocaleDateString('ru-RU');
|
||
}
|
||
|
||
function escapeHtml(str) {
|
||
return String(str).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||
}
|
||
|
||
function toggleTheme() {
|
||
const html = document.documentElement;
|
||
html.dataset.theme = html.dataset.theme === 'light' ? 'dark' : 'light';
|
||
}
|
||
|
||
// Анимация появления
|
||
const io = new IntersectionObserver(entries => {
|
||
entries.forEach(e => { if(e.isIntersecting) e.target.classList.add('in'); });
|
||
}, {threshold:.15});
|
||
document.querySelectorAll('.reveal').forEach(el => io.observe(el));
|
||
|
||
// Старт
|
||
init();
|
||
</script>
|
||
</body>
|
||
</html>
|