zhurnal-vydachi-propuskov/index.html

488 lines
20 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>
.kt-ai-btn { text-decoration: none; }
.table-container { overflow-x: auto; }
.status-chip { display: inline-block; padding: 4px 12px; border-radius: var(--kt-ai-radius-full); font-size: var(--kt-ai-text-sm); font-weight: var(--kt-ai-weight-medium); }
.status-na-rukah { background: var(--kt-ai-primary); color: #fff; }
.status-vozvrashen { background: var(--kt-ai-success, #10b981); color: #fff; }
.status-prosrochen { background: var(--kt-ai-danger); color: #fff; }
.modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); display: none; align-items: center; justify-content: center; z-index: 1000; }
.modal-overlay.on { display: flex; }
.modal { background: var(--kt-ai-bg); border-radius: var(--kt-ai-radius-2xl); padding: var(--kt-ai-space-8); max-width: 520px; width: 90%; max-height: 90vh; overflow-y: auto; }
.modal-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--kt-ai-space-6); }
.modal-close { background: none; border: none; font-size: 24px; cursor: pointer; color: var(--kt-ai-fg-muted); }
.tab-content { display: none; }
.tab-content.on { display: block; }
.tabs { display: flex; gap: var(--kt-ai-space-2); margin-bottom: var(--kt-ai-space-6); }
.tab-btn { padding: var(--kt-ai-space-2) var(--kt-ai-space-4); border-radius: var(--kt-ai-radius-md); background: transparent; border: 1px solid transparent; cursor: pointer; color: var(--kt-ai-fg-muted); }
.tab-btn[aria-selected="true"] { background: var(--kt-ai-primary); color: #fff; }
.log-entry { padding: var(--kt-ai-space-3) 0; border-bottom: 1px solid var(--kt-ai-divider); font-size: var(--kt-ai-text-sm); }
.log-entry:last-child { border-bottom: none; }
.log-time { color: var(--kt-ai-fg-muted); 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" id="themeToggle">🌓 Тема</button>
</div>
</header>
<main class="kt-ai-wrap" style="padding-top: var(--kt-ai-space-8); padding-bottom: var(--kt-ai-space-12);">
<div class="tabs">
<button class="tab-btn" aria-selected="true" data-tab="records">Записи</button>
<button class="tab-btn" data-tab="log">Журнал изменений</button>
</div>
<div id="tab-records" class="tab-content on">
<section class="kt-ai-section">
<div class="kt-ai-section-head">
<h2>Учёт пропусков</h2>
<p>Ведите журнал выдачи и возврата пропусков сотрудников</p>
</div>
<div class="kt-ai-card" style="padding: var(--kt-ai-space-6); margin-bottom: var(--kt-ai-space-6);">
<div style="display: flex; gap: var(--kt-ai-space-4); flex-wrap: wrap; align-items: center;">
<div style="flex: 1; min-width: 200px;">
<div class="kt-ai-field">
<label>Поиск</label>
<input class="kt-ai-input" id="searchInput" placeholder="ФИО или номер пропуска">
</div>
</div>
<button class="kt-ai-btn" data-variant="primary" id="addBtn">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="vertical-align: middle; margin-right: 8px;">
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
Добавить пропуск
</button>
<button class="kt-ai-btn" data-variant="ghost" id="exportBtn">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="vertical-align: middle; margin-right: 8px;">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="7 10 12 15 17 10"></polyline>
<line x1="12" y1="15" x2="12" y2="3"></line>
</svg>
Выгрузить в Excel
</button>
</div>
<div style="display: flex; gap: var(--kt-ai-space-2); margin-top: var(--kt-ai-space-4); flex-wrap: wrap;" id="filterChips">
<button class="kt-ai-chip" data-filter="all" data-active="true">Все</button>
<button class="kt-ai-chip" data-filter="На руках">На руках</button>
<button class="kt-ai-chip" data-filter="Возвращен">Возвращен</button>
<button class="kt-ai-chip" data-filter="Просрочен">Просрочен</button>
</div>
<div style="display: flex; gap: var(--kt-ai-space-6); margin-top: var(--kt-ai-space-6); flex-wrap: wrap;">
<div style="text-align: center;">
<div style="font-size: var(--kt-ai-text-3xl); font-weight: var(--kt-ai-font-bold); color: var(--kt-ai-fg);" id="countTotal">0</div>
<div style="font-size: var(--kt-ai-text-sm); color: var(--kt-ai-fg-muted);">Всего</div>
</div>
<div style="text-align: center;">
<div style="font-size: var(--kt-ai-text-3xl); font-weight: var(--kt-ai-font-bold); color: var(--kt-ai-primary);" id="countNaRukah">0</div>
<div style="font-size: var(--kt-ai-text-sm); color: var(--kt-ai-fg-muted);">На руках</div>
</div>
<div style="text-align: center;">
<div style="font-size: var(--kt-ai-text-3xl); font-weight: var(--kt-ai-font-bold); color: var(--kt-ai-success, #10b981);" id="countVozvrashen">0</div>
<div style="font-size: var(--kt-ai-text-sm); color: var(--kt-ai-fg-muted);">Возвращено</div>
</div>
<div style="text-align: center;">
<div style="font-size: var(--kt-ai-text-3xl); font-weight: var(--kt-ai-font-bold); color: var(--kt-ai-danger);" id="countProsrochen">0</div>
<div style="font-size: var(--kt-ai-text-sm); color: var(--kt-ai-fg-muted);">Просрочено</div>
</div>
</div>
</div>
<div class="kt-ai-card table-container">
<table style="width: 100%; border-collapse: collapse;">
<thead>
<tr style="border-bottom: 2px solid var(--kt-ai-divider);">
<th style="text-align: left; padding: var(--kt-ai-space-3); color: var(--kt-ai-fg-muted); font-weight: var(--kt-ai-weight-medium);">Сотрудник</th>
<th style="text-align: left; padding: var(--kt-ai-space-3); color: var(--kt-ai-fg-muted); font-weight: var(--kt-ai-weight-medium);">Номер пропуска</th>
<th style="text-align: left; padding: var(--kt-ai-space-3); color: var(--kt-ai-fg-muted); font-weight: var(--kt-ai-weight-medium);">Дата выдачи</th>
<th style="text-align: left; padding: var(--kt-ai-space-3); color: var(--kt-ai-fg-muted); font-weight: var(--kt-ai-weight-medium);">Дата возврата</th>
<th style="text-align: left; padding: var(--kt-ai-space-3); color: var(--kt-ai-fg-muted); font-weight: var(--kt-ai-weight-medium);">Статус</th>
<th style="text-align: right; padding: var(--kt-ai-space-3); color: var(--kt-ai-fg-muted); font-weight: var(--kt-ai-weight-medium);">Действия</th>
</tr>
</thead>
<tbody id="recordsTableBody"></tbody>
</table>
</div>
</section>
</div>
<div id="tab-log" class="tab-content">
<section class="kt-ai-section">
<div class="kt-ai-section-head">
<h2>Журнал изменений</h2>
<p>История всех действий с записями</p>
</div>
<div class="kt-ai-card" style="padding: var(--kt-ai-space-4);">
<div id="logContainer"></div>
</div>
</section>
</div>
</main>
<div class="modal-overlay" id="modalOverlay">
<div class="modal">
<div class="modal-head">
<h3 id="modalTitle" style="margin: 0;">Добавить пропуск</h3>
<button class="modal-close" id="modalClose">&times;</button>
</div>
<form id="recordForm">
<input type="hidden" id="recordId">
<div class="kt-ai-field" style="margin-bottom: var(--kt-ai-space-4);">
<label for="fio">ФИО сотрудника</label>
<input class="kt-ai-input" id="fio" name="fio" required placeholder="Иванов Иван Иванович">
</div>
<div class="kt-ai-field" style="margin-bottom: var(--kt-ai-space-4);">
<label for="passNumber">Номер пропуска</label>
<input class="kt-ai-input" id="passNumber" name="passNumber" required placeholder="КТ-12345">
</div>
<div class="kt-ai-field" style="margin-bottom: var(--kt-ai-space-4);">
<label for="issueDate">Дата выдачи</label>
<input type="date" class="kt-ai-input" id="issueDate" name="issueDate" required>
</div>
<div class="kt-ai-field" style="margin-bottom: var(--kt-ai-space-4);">
<label for="returnDate">Дата возврата</label>
<input type="date" class="kt-ai-input" id="returnDate" name="returnDate">
</div>
<div class="kt-ai-field" style="margin-bottom: var(--kt-ai-space-6);">
<label for="status">Статус</label>
<select class="kt-ai-select" id="status" name="status" required>
<option value="На руках">На руках</option>
<option value="Возвращен">Возвращен</option>
<option value="Просрочен">Просрочен</option>
</select>
</div>
<div style="display: flex; gap: var(--kt-ai-space-3); justify-content: flex-end;">
<button type="button" class="kt-ai-btn" data-variant="ghost" id="cancelBtn">Отмена</button>
<button type="submit" class="kt-ai-btn" data-variant="primary">Сохранить</button>
</div>
</form>
</div>
</div>
<script>
// Демо-данные
const DEMO_DATA = [
{ id: 1, fio: 'Асетов Арман Бауржанович', passNumber: 'КТ-10234', issueDate: '2026-09-01', returnDate: '', status: 'На руках' },
{ id: 2, fio: 'Иванова Мария Петровна', passNumber: 'КТ-10567', issueDate: '2026-09-02', returnDate: '2026-09-03', status: 'Возвращен' },
{ id: 3, fio: 'Ким Дмитрий Сергеевич', passNumber: 'КТ-10891', issueDate: '2026-08-15', returnDate: '', status: 'Просрочен' },
{ id: 4, fio: 'Омарова Гульнара Талгатовна', passNumber: 'КТ-11023', issueDate: '2026-09-01', returnDate: '', status: 'На руках' },
{ id: 5, fio: 'Смирнов Алексей Владимирович', passNumber: 'КТ-11156', issueDate: '2026-09-03', returnDate: '', status: 'На руках' }
];
// Состояние приложения
let records = [];
let log = [];
let currentFilter = 'all';
let searchQuery = '';
// Инициализация
function init() {
const stored = localStorage.getItem('zhurnalRecords');
if (stored) {
records = JSON.parse(stored);
} else {
records = [...DEMO_DATA];
saveRecords();
}
const storedLog = localStorage.getItem('zhurnalLog');
if (storedLog) {
log = JSON.parse(storedLog);
}
render();
setupEventListeners();
}
function saveRecords() {
localStorage.setItem('zhurnalRecords', JSON.stringify(records));
}
function saveLog() {
localStorage.setItem('zhurnalLog', JSON.stringify(log));
}
function addToLog(action, details) {
log.unshift({
timestamp: new Date().toISOString(),
action: action,
details: details
});
saveLog();
}
function formatDate(dateStr) {
if (!dateStr) return '—';
const d = new Date(dateStr);
return d.toLocaleDateString('ru-RU', { day: '2-digit', month: '2-digit', year: '2-digit' });
}
function getStatusClass(status) {
if (status === 'На руках') return 'status-na-rukah';
if (status === 'Возвращен') return 'status-vozvrashen';
if (status === 'Просрочен') return 'status-prosrochen';
return '';
}
function getFilteredRecords() {
let filtered = [...records];
if (currentFilter !== 'all') {
filtered = filtered.filter(r => r.status === currentFilter);
}
if (searchQuery) {
const q = searchQuery.toLowerCase();
filtered = filtered.filter(r =>
r.fio.toLowerCase().includes(q) ||
r.passNumber.toLowerCase().includes(q)
);
}
return filtered;
}
function updateCounts() {
const total = records.length;
const naRukah = records.filter(r => r.status === 'На руках').length;
const vozvrashen = records.filter(r => r.status === 'Возвращен').length;
const prosrochen = records.filter(r => r.status === 'Просрочен').length;
document.getElementById('countTotal').textContent = total;
document.getElementById('countNaRukah').textContent = naRukah;
document.getElementById('countVozvrashen').textContent = vozvrashen;
document.getElementById('countProsrochen').textContent = prosrochen;
}
function render() {
updateCounts();
renderTable();
renderLog();
}
function renderTable() {
const tbody = document.getElementById('recordsTableBody');
const filtered = getFilteredRecords();
if (filtered.length === 0) {
tbody.innerHTML = '<tr><td colspan="6" style="padding: var(--kt-ai-space-6); text-align: center; color: var(--kt-ai-fg-muted);">Нет записей</td></tr>';
return;
}
tbody.innerHTML = filtered.map(r => `
<tr style="border-bottom: 1px solid var(--kt-ai-divider);">
<td style="padding: var(--kt-ai-space-3);">${escapeHtml(r.fio)}</td>
<td style="padding: var(--kt-ai-space-3);">${escapeHtml(r.passNumber)}</td>
<td style="padding: var(--kt-ai-space-3);">${formatDate(r.issueDate)}</td>
<td style="padding: var(--kt-ai-space-3);">${formatDate(r.returnDate)}</td>
<td style="padding: var(--kt-ai-space-3);"><span class="status-chip ${getStatusClass(r.status)}">${r.status}</span></td>
<td style="padding: var(--kt-ai-space-3); text-align: right;">
<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('');
}
function renderLog() {
const container = document.getElementById('logContainer');
if (log.length === 0) {
container.innerHTML = '<div style="color: var(--kt-ai-fg-muted); text-align: center; padding: var(--kt-ai-space-4);">История пуста</div>';
return;
}
container.innerHTML = log.map(entry => {
const time = new Date(entry.timestamp).toLocaleString('ru-RU', {
day: '2-digit', month: '2-digit', year: '2-digit',
hour: '2-digit', minute: '2-digit'
});
return `
<div class="log-entry">
<div><strong>${entry.action}</strong>: ${escapeHtml(entry.details)}</div>
<div class="log-time">${time}</div>
</div>
`;
}).join('');
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function openModal(recordId = null) {
const overlay = document.getElementById('modalOverlay');
const form = document.getElementById('recordForm');
const title = document.getElementById('modalTitle');
form.reset();
if (recordId) {
const record = records.find(r => r.id === recordId);
if (record) {
document.getElementById('recordId').value = record.id;
document.getElementById('fio').value = record.fio;
document.getElementById('passNumber').value = record.passNumber;
document.getElementById('issueDate').value = record.issueDate;
document.getElementById('returnDate').value = record.returnDate || '';
document.getElementById('status').value = record.status;
title.textContent = 'Редактировать пропуск';
}
} else {
document.getElementById('recordId').value = '';
title.textContent = 'Добавить пропуск';
}
overlay.classList.add('on');
}
function closeModal() {
document.getElementById('modalOverlay').classList.remove('on');
}
function saveRecord(formData) {
const id = formData.get('recordId');
const record = {
fio: formData.get('fio'),
passNumber: formData.get('passNumber'),
issueDate: formData.get('issueDate'),
returnDate: formData.get('returnDate') || '',
status: formData.get('status')
};
if (id) {
const index = records.findIndex(r => r.id === parseInt(id));
if (index !== -1) {
const oldRecord = records[index];
records[index] = { ...record, id: parseInt(id) };
addToLog('Редактирование', `Пропуск ${oldRecord.passNumber} (${oldRecord.fio}) изменён`);
}
} else {
const newId = Date.now();
records.push({ ...record, id: newId });
addToLog('Добавление', `Добавлен пропуск ${record.passNumber} (${record.fio})`);
}
saveRecords();
render();
closeModal();
}
function deleteRecord(id) {
const record = records.find(r => r.id === id);
if (!record) return;
if (confirm(`Удалить запись о пропуске ${record.passNumber} (${record.fio})?`)) {
records = records.filter(r => r.id !== id);
addToLog('Удаление', `Удалён пропуск ${record.passNumber} (${record.fio})`);
saveRecords();
render();
}
}
function exportToExcel() {
const filtered = getFilteredRecords();
const headers = ['ФИО', 'Номер пропуска', 'Дата выдачи', 'Дата возврата', 'Статус'];
const rows = filtered.map(r => [
r.fio,
r.passNumber,
r.issueDate,
r.returnDate || '',
r.status
]);
const csvContent = '\uFEFF' + [
headers.join(';'),
...rows.map(row => row.map(cell => '"' + String(cell).replace(/"/g, '""') + '"').join(';'))
].join('\n');
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = 'zhurnal_propuskov.csv';
link.click();
addToLog('Экспорт', 'Выгрузка данных в Excel');
}
function setupEventListeners() {
document.getElementById('addBtn').addEventListener('click', () => openModal());
document.getElementById('modalClose').addEventListener('click', closeModal);
document.getElementById('cancelBtn').addEventListener('click', closeModal);
document.getElementById('exportBtn').addEventListener('click', exportToExcel);
document.getElementById('searchInput').addEventListener('input', (e) => {
searchQuery = e.target.value;
renderTable();
});
document.getElementById('filterChips').addEventListener('click', (e) => {
if (e.target.classList.contains('kt-ai-chip')) {
document.querySelectorAll('.kt-ai-chip').forEach(c => c.removeAttribute('data-active'));
e.target.setAttribute('data-active', 'true');
currentFilter = e.target.dataset.filter;
renderTable();
}
});
document.getElementById('recordForm').addEventListener('submit', (e) => {
e.preventDefault();
saveRecord(new FormData(e.target));
});
document.getElementById('modalOverlay').addEventListener('click', (e) => {
if (e.target === e.currentTarget) closeModal();
});
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.tab-btn').forEach(b => b.setAttribute('aria-selected', 'false'));
btn.setAttribute('aria-selected', 'true');
const tabName = btn.dataset.tab;
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('on'));
document.getElementById('tab-' + tabName).classList.add('on');
});
});
document.getElementById('themeToggle').addEventListener('click', () => {
const html = document.documentElement;
const current = html.getAttribute('data-theme');
html.setAttribute('data-theme', current === 'light' ? 'dark' : 'light');
});
}
window.editRecord = function(id) {
openModal(id);
};
window.deleteRecord = function(id) {
deleteRecord(id);
};
init();
</script>
</body>
</html>