zhurnal-vydachi-spetsodezhdy-2/index.html

435 lines
18 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>
a.kt-ai-btn { text-decoration: none; }
.status-chip { font-size: 13px; padding: 4px 10px; }
.table-wrap { overflow-x: auto; }
.kt-ai-table { width: 100%; border-collapse: collapse; font-size: 14px; }
.kt-ai-table th { text-align: left; padding: 12px 16px; font-weight: 600; color: var(--kt-ai-fg-muted); border-bottom: 2px solid var(--kt-ai-border); }
.kt-ai-table td { padding: 12px 16px; border-bottom: 1px solid var(--kt-ai-border); vertical-align: middle; }
.kt-ai-table tbody tr:hover { background: var(--kt-ai-bg-soft); }
.num-cell { text-align: right; font-variant-numeric: tabular-nums; }
.actions-cell { white-space: nowrap; }
.search-row { display: flex; gap: 12px; flex-wrap: wrap; align-items: center; margin-bottom: 20px; }
.search-row input { flex: 1; min-width: 200px; }
.filters-row { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 20px; }
.drawer-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.5); display: none; z-index: 100; }
.drawer-overlay.open { display: block; }
.drawer { position: fixed; right: -420px; top: 0; bottom: 0; width: 420px; background: var(--kt-ai-bg); padding: 24px; overflow-y: auto; transition: right 0.3s ease; z-index: 101; }
.drawer.open { right: 0; }
.drawer-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; }
.drawer-header h2 { margin: 0; }
.close-btn { background: none; border: none; font-size: 24px; cursor: pointer; color: var(--kt-ai-fg-muted); }
.form-group { margin-bottom: 16px; }
.form-group label { display: block; margin-bottom: 6px; font-weight: 500; font-size: 14px; color: var(--kt-ai-fg-muted); }
.form-group input, .form-group select, .form-group textarea { width: 100%; padding: 10px 12px; border: 1px solid var(--kt-ai-border); border-radius: var(--kt-ai-radius-lg); background: var(--kt-ai-bg); color: var(--kt-ai-fg); font-size: 14px; }
.form-group textarea { resize: vertical; min-height: 80px; }
.form-actions { display: flex; gap: 12px; margin-top: 24px; }
.empty-state { text-align: center; padding: 60px 20px; color: var(--kt-ai-fg-muted); }
.empty-state svg { width: 64px; height: 64px; margin-bottom: 16px; opacity: 0.5; }
</style>
</head>
<body class="kt-ai-app">
<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="exportToCSV()">📥 Экспорт</button>
<button class="kt-ai-btn" data-variant="primary" onclick="openDrawer('issue')">➕ Выдать</button>
</div>
</header>
<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">
<div class="kt-ai-kpi-strip" style="margin-bottom: 24px;">
<div class="kt-ai-kpi">
<div class="value" id="kpi-total">0</div>
<div class="label">Всего записей</div>
</div>
<div class="kt-ai-kpi">
<div class="value" id="kpi-issued" style="color: var(--kt-ai-chart-blue)"></div>
<div class="label">Выдано</div>
</div>
<div class="kt-ai-kpi">
<div class="value" id="kpi-returned" style="color: var(--kt-ai-chart-green)"></div>
<div class="label">Возвращено</div>
</div>
<div class="kt-ai-kpi">
<div class="value" id="kpi-overdue" style="color: var(--kt-ai-chart-red)"></div>
<div class="label">Просрочено</div>
</div>
</div>
<div class="search-row">
<input type="text" class="kt-ai-input" id="search-input" placeholder="🔍 Поиск по сотруднику или наименованию..." oninput="renderTable()">
</div>
<div class="filters-row" id="status-filters">
<button class="kt-ai-btn" data-variant="primary" data-size="sm" onclick="setFilter('all', this)">Все</button>
<button class="kt-ai-btn" data-variant="ghost" data-size="sm" onclick="setFilter('issued', this)">📤 Выдано</button>
<button class="kt-ai-btn" data-variant="ghost" data-size="sm" onclick="setFilter('returned', this)">✅ Возвращено</button>
<button class="kt-ai-btn" data-variant="ghost" data-size="sm" onclick="setFilter('overdue', this)">⚠️ Просрочено</button>
</div>
<div class="table-wrap">
<table class="kt-ai-table">
<thead>
<tr>
<th>Дата выдачи</th>
<th>Сотрудник</th>
<th>Наименование</th>
<th class="num-cell">Кол-во</th>
<th>Дата возврата</th>
<th>Статус</th>
<th class="actions-cell">Действия</th>
</tr>
</thead>
<tbody id="table-body"></tbody>
</table>
</div>
<div id="empty-state" class="empty-state" style="display: none;">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<path d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"/>
</svg>
<p>Нет записей. Нажмите «Выдать», чтобы добавить первую запись.</p>
</div>
</div>
</section>
<footer class="kt-ai-wrap kt-ai-pagefoot">
<span>© 2026 Журнал выдачи спецодежды</span>
<span class="kt-ai-spacer"></span>
<span>Данные сохраняются локально</span>
</footer>
<div class="drawer-overlay" id="drawer-overlay" onclick="closeDrawer()"></div>
<div class="drawer" id="drawer">
<div class="drawer-header">
<h2 id="drawer-title">Выдать спецодежду</h2>
<button class="close-btn" onclick="closeDrawer()">×</button>
</div>
<form id="issue-form" onsubmit="handleSubmit(event)">
<input type="hidden" id="edit-id">
<div class="form-group">
<label for="employee">Сотрудник *</label>
<input type="text" id="employee" name="employee" required placeholder="ФИО сотрудника">
</div>
<div class="form-group">
<label for="item">Наименование *</label>
<input type="text" id="item" name="item" required placeholder="Каска защитная, перчатки, ботинки...">
</div>
<div class="form-group">
<label for="quantity">Количество *</label>
<input type="number" id="quantity" name="quantity" min="1" value="1" required>
</div>
<div class="form-group">
<label for="issue-date">Дата выдачи *</label>
<input type="date" id="issue-date" name="issueDate" required>
</div>
<div class="form-group">
<label for="return-date">Дата возврата *</label>
<input type="date" id="return-date" name="returnDate" required>
</div>
<div class="form-group">
<label for="note">Примечание</label>
<textarea id="note" name="note" placeholder="Серийный номер, состояние..."></textarea>
</div>
<div class="form-actions">
<button type="submit" class="kt-ai-btn" data-variant="primary" id="save-btn">Сохранить</button>
<button type="button" class="kt-ai-btn" data-variant="ghost" onclick="closeDrawer()">Отмена</button>
</div>
</form>
<div id="return-actions" style="display: none; margin-top: 24px; padding-top: 24px; border-top: 1px solid var(--kt-ai-border);">
<p style="margin-bottom: 16px; color: var(--kt-ai-fg-muted);">Запись возвращена</p>
<button class="kt-ai-btn" data-variant="primary" onclick="reopenRecord()">Вернуть в работу</button>
</div>
</div>
<script>
let records = [];
let currentFilter = 'all';
let currentEditId = null;
const DEMO_DATA = [
{ id: 1, employee: 'Иванов А.С.', item: 'Каска защитная', quantity: 1, issueDate: '2026-08-15', returnDate: '2026-09-15', status: 'issued', note: 'Серийный №12345' },
{ id: 2, employee: 'Петров В.К.', item: 'Перчатки рабочие', quantity: 5, issueDate: '2026-08-20', returnDate: '2026-09-01', status: 'returned', note: '' },
{ id: 3, employee: 'Сидорова М.И.', item: 'Ботинки рабочие', quantity: 1, issueDate: '2026-07-10', returnDate: '2026-08-10', status: 'overdue', note: 'Размер 39' },
{ id: 4, employee: 'Кузнецов П.Н.', item: 'Жилет сигнальный', quantity: 2, issueDate: '2026-08-25', returnDate: '2026-09-25', status: 'issued', note: 'Оранжевый, L' },
{ id: 5, employee: 'Иванов А.С.', item: 'Очки защитные', quantity: 1, issueDate: '2026-08-01', returnDate: '2026-08-31', status: 'returned', note: '' }
];
function init() {
const saved = localStorage.getItem('special-clothing-records');
records = saved ? JSON.parse(saved) : [...DEMO_DATA];
document.getElementById('issue-date').valueAsDate = new Date();
const nextMonth = new Date();
nextMonth.setMonth(nextMonth.getMonth() + 1);
document.getElementById('return-date').valueAsDate = nextMonth;
renderTable();
updateKPI();
}
function saveRecords() {
localStorage.setItem('special-clothing-records', JSON.stringify(records));
renderTable();
updateKPI();
}
function getStatus(record) {
const today = new Date().toISOString().slice(0, 10);
if (record.status === 'returned') return 'returned';
if (record.returnDate < today) return 'overdue';
return 'issued';
}
function getStatusBadge(status) {
const badges = {
issued: '<span class="kt-ai-chip status-chip" style="background: var(--kt-ai-chart-blue); color: white;">📤 Выдано</span>',
returned: '<span class="kt-ai-chip status-chip" style="background: var(--kt-ai-chart-green); color: white;">✅ Возвращено</span>',
overdue: '<span class="kt-ai-chip status-chip" style="background: var(--kt-ai-chart-red); color: white;">⚠️ Просрочено</span>'
};
return badges[status] || badges.issued;
}
function renderTable() {
const search = document.getElementById('search-input').value.toLowerCase();
const filtered = records.filter(r => {
const status = getStatus(r);
const matchesSearch = r.employee.toLowerCase().includes(search) || r.item.toLowerCase().includes(search);
const matchesFilter = currentFilter === 'all' || status === currentFilter;
return matchesSearch && matchesFilter;
});
const tbody = document.getElementById('table-body');
const emptyState = document.getElementById('empty-state');
const tableWrap = document.querySelector('.table-wrap');
if (filtered.length === 0) {
tbody.innerHTML = '';
emptyState.style.display = 'block';
tableWrap.style.display = 'none';
} else {
emptyState.style.display = 'none';
tableWrap.style.display = 'block';
tbody.innerHTML = filtered.map(r => {
const status = getStatus(r);
return `<tr>
<td>${formatDate(r.issueDate)}</td>
<td>${escapeHtml(r.employee)}</td>
<td>${escapeHtml(r.item)}</td>
<td class="num-cell">${r.quantity}</td>
<td>${formatDate(r.returnDate)}</td>
<td>${getStatusBadge(status)}</td>
<td class="actions-cell">
<button class="kt-ai-btn" data-variant="ghost" data-size="sm" onclick="editRecord(${r.id})">✏️</button>
<button class="kt-ai-btn" data-variant="danger" data-size="sm" onclick="deleteRecord(${r.id})">🗑️</button>
</td>
</tr>`;
}).join('');
}
updateKPI();
}
function updateKPI() {
const today = new Date().toISOString().slice(0, 10);
const total = records.length;
const issued = records.filter(r => getStatus(r) === 'issued').length;
const returned = records.filter(r => r.status === 'returned').length;
const overdue = records.filter(r => getStatus(r) === 'overdue').length;
document.getElementById('kpi-total').textContent = total;
document.getElementById('kpi-issued').textContent = issued;
document.getElementById('kpi-returned').textContent = returned;
document.getElementById('kpi-overdue').textContent = overdue;
}
function openDrawer(mode, id = null) {
const drawer = document.getElementById('drawer');
const overlay = document.getElementById('drawer-overlay');
const form = document.getElementById('issue-form');
const returnActions = document.getElementById('return-actions');
const title = document.getElementById('drawer-title');
currentEditId = id;
form.reset();
if (id !== null) {
const record = records.find(r => r.id === id);
if (record) {
document.getElementById('edit-id').value = record.id;
document.getElementById('employee').value = record.employee;
document.getElementById('item').value = record.item;
document.getElementById('quantity').value = record.quantity;
document.getElementById('issue-date').value = record.issueDate;
document.getElementById('return-date').value = record.returnDate;
document.getElementById('note').value = record.note || '';
if (record.status === 'returned') {
returnActions.style.display = 'block';
document.getElementById('save-btn').style.display = 'none';
title.textContent = 'Просмотр записи';
setFormDisabled(true);
} else {
returnActions.style.display = 'none';
document.getElementById('save-btn').style.display = 'inline-block';
title.textContent = 'Редактировать запись';
setFormDisabled(false);
}
}
} else {
document.getElementById('issue-date').valueAsDate = new Date();
const nextMonth = new Date();
nextMonth.setMonth(nextMonth.getMonth() + 1);
document.getElementById('return-date').valueAsDate = nextMonth;
returnActions.style.display = 'none';
document.getElementById('save-btn').style.display = 'inline-block';
title.textContent = mode === 'issue' ? 'Выдать спецодежду' : 'Редактировать';
setFormDisabled(false);
}
drawer.classList.add('open');
overlay.classList.add('open');
}
function setFormDisabled(disabled) {
['employee', 'item', 'quantity', 'issue-date', 'return-date', 'note'].forEach(id => {
document.getElementById(id).disabled = disabled;
});
}
function closeDrawer() {
document.getElementById('drawer').classList.remove('open');
document.getElementById('drawer-overlay').classList.remove('open');
currentEditId = null;
}
function handleSubmit(e) {
e.preventDefault();
const id = document.getElementById('edit-id').value;
const record = {
employee: document.getElementById('employee').value.trim(),
item: document.getElementById('item').value.trim(),
quantity: parseInt(document.getElementById('quantity').value),
issueDate: document.getElementById('issue-date').value,
returnDate: document.getElementById('return-date').value,
note: document.getElementById('note').value.trim()
};
if (id) {
const index = records.findIndex(r => r.id === parseInt(id));
if (index !== -1) {
records[index] = { ...records[index], ...record };
}
} else {
records.push({
id: Date.now(),
...record,
status: 'issued'
});
}
saveRecords();
closeDrawer();
}
function editRecord(id) {
openDrawer('edit', id);
}
function deleteRecord(id) {
if (confirm('Удалить эту запись?')) {
records = records.filter(r => r.id !== id);
saveRecords();
}
}
function reopenRecord() {
if (currentEditId !== null) {
const index = records.findIndex(r => r.id === currentEditId);
if (index !== -1) {
records[index].status = 'issued';
saveRecords();
closeDrawer();
}
}
}
function returnRecord(id) {
if (confirm('Отметить как возвращённое?')) {
const index = records.findIndex(r => r.id === id);
if (index !== -1) {
records[index].status = 'returned';
saveRecords();
}
}
}
function setFilter(filter, btn) {
currentFilter = filter;
document.querySelectorAll('#status-filters .kt-ai-btn').forEach(b => {
b.setAttribute('data-variant', 'ghost');
});
btn.setAttribute('data-variant', 'primary');
renderTable();
}
function formatDate(dateStr) {
const d = new Date(dateStr + 'T00:00:00');
return d.toLocaleDateString('ru-RU', { day: '2-digit', month: '2-digit', year: '2-digit' });
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function exportToCSV() {
const headers = ['Дата выдачи', 'Сотрудник', 'Наименование', 'Кол-во', 'Дата возврата', 'Статус', 'Примечание'];
const rows = records.map(r => {
const status = getStatus(r);
const statusText = { issued: 'Выдано', returned: 'Возвращено', overdue: 'Просрочено' }[status];
return [
r.issueDate,
r.employee,
r.item,
r.quantity,
r.returnDate,
statusText,
r.note || ''
].map(v => '"' + String(v).replace(/"/g, '""') + '"').join(';');
});
const csv = '\uFEFF' + [headers.join(';'), ...rows].join('\n');
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'zhurnal_spetsodezhdy_' + new Date().toISOString().slice(0, 10) + '.csv';
a.click();
URL.revokeObjectURL(url);
}
init();
</script>
</body>
</html>