zhurnal-ucheta-zayavok-sotru/index.html

366 lines
15 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
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>