396 lines
17 KiB
HTML
396 lines
17 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-btn { text-decoration: none; }
|
||
.journal-header { display: flex; align-items: center; gap: var(--kt-ai-space-4); margin-bottom: var(--kt-ai-space-6); }
|
||
.journal-controls { display: flex; gap: var(--kt-ai-space-3); flex-wrap: wrap; align-items: center; }
|
||
.search-input { min-width: 240px; }
|
||
.status-filters { display: flex; gap: var(--kt-ai-space-2); }
|
||
.kpi-strip { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: var(--kt-ai-space-4); margin-bottom: var(--kt-ai-space-6); }
|
||
.kpi-card { background: var(--kt-ai-card-bg); border: 1px solid var(--kt-ai-card-border); border-radius: var(--kt-ai-radius-xl); padding: var(--kt-ai-space-5); }
|
||
.kpi-value { font-size: var(--kt-ai-text-3xl); font-weight: var(--kt-ai-weight-bold); color: var(--kt-ai-fg); line-height: 1.2; }
|
||
.kpi-label { font-size: var(--kt-ai-text-sm); color: var(--kt-ai-fg-muted); margin-top: var(--kt-ai-space-1); }
|
||
.table-container { background: var(--kt-ai-card-bg); border: 1px solid var(--kt-ai-card-border); border-radius: var(--kt-ai-radius-xl); overflow: hidden; }
|
||
.books-table { width: 100%; border-collapse: collapse; }
|
||
.books-table th { text-align: left; padding: var(--kt-ai-space-3) var(--kt-ai-space-4); font-size: var(--kt-ai-text-xs); color: var(--kt-ai-fg-muted); font-weight: var(--kt-ai-weight-medium); border-bottom: 1px solid var(--kt-ai-border); }
|
||
.books-table td { padding: var(--kt-ai-space-3) var(--kt-ai-space-4); font-size: var(--kt-ai-text-sm); border-bottom: 1px solid var(--kt-ai-border); }
|
||
.books-table tr:last-child td { border-bottom: none; }
|
||
.books-table tr:hover { background: var(--kt-ai-bg-soft); }
|
||
.text-right { text-align: right; }
|
||
.tabular-nums { font-variant-numeric: tabular-nums; }
|
||
.empty-state { text-align: center; padding: var(--kt-ai-space-10); color: var(--kt-ai-fg-muted); }
|
||
.modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.5); display: none; align-items: center; justify-content: center; z-index: 1000; }
|
||
.modal-overlay.active { display: flex; }
|
||
.modal { background: var(--kt-ai-card-bg); border-radius: var(--kt-ai-radius-2xl); padding: var(--kt-ai-space-6); width: 100%; max-width: 480px; max-height: 90vh; overflow-y: auto; }
|
||
.modal-title { font-size: var(--kt-ai-text-xl); font-weight: var(--kt-ai-weight-semibold); margin-bottom: var(--kt-ai-space-4); }
|
||
.form-group { margin-bottom: var(--kt-ai-space-4); }
|
||
.form-label { display: block; font-size: var(--kt-ai-text-sm); color: var(--kt-ai-fg-muted); margin-bottom: var(--kt-ai-space-2); }
|
||
.form-actions { display: flex; gap: var(--kt-ai-space-3); justify-content: flex-end; margin-top: var(--kt-ai-space-6); }
|
||
.chip-ok { color: var(--kt-ai-status-ok-fg); }
|
||
.chip-warn { color: var(--kt-ai-status-warn-fg); }
|
||
.chip-risk { color: var(--kt-ai-status-risk-fg); }
|
||
</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" id="themeToggle">🌓</button>
|
||
<button class="kt-ai-btn" data-variant="primary" id="addBookBtn">+ Выдать книгу</button>
|
||
</div>
|
||
</header>
|
||
|
||
<main class="kt-ai-wrap" style="padding-top:var(--kt-ai-space-8);padding-bottom:var(--kt-ai-space-8)">
|
||
|
||
<section class="kpi-strip">
|
||
<div class="kpi-card">
|
||
<div class="kpi-value tabular-nums" id="kpiTotal">0</div>
|
||
<div class="kpi-label">Всего записей</div>
|
||
</div>
|
||
<div class="kpi-card">
|
||
<div class="kpi-value tabular-nums" id="kpiIssued">0</div>
|
||
<div class="kpi-label">Выдано сейчас</div>
|
||
</div>
|
||
<div class="kpi-card">
|
||
<div class="kpi-value tabular-nums" id="kpiReturned">0</div>
|
||
<div class="kpi-label">Возвращено</div>
|
||
</div>
|
||
<div class="kpi-card">
|
||
<div class="kpi-value tabular-nums" id="kpiOverdue">0</div>
|
||
<div class="kpi-label">Просрочено</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="kt-ai-card" style="padding:var(--kt-ai-space-5)">
|
||
<div class="journal-header">
|
||
<div style="flex:1">
|
||
<h1 style="font-size:var(--kt-ai-text-2xl);font-weight:var(--kt-ai-weight-bold);margin:0">Учёт выдачи книг</h1>
|
||
<p style="color:var(--kt-ai-fg-muted);margin-top:var(--kt-ai-space-1)">Журнал движения библиотечного фонда</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="journal-controls" style="margin-bottom:var(--kt-ai-space-5)">
|
||
<input type="text" class="kt-ai-input search-input" id="searchInput" placeholder="Поиск по читателю или книге...">
|
||
<div class="status-filters">
|
||
<button class="kt-ai-btn" data-variant="primary" data-size="sm" data-status="all">Все</button>
|
||
<button class="kt-ai-btn" data-variant="ghost" data-size="sm" data-status="issued">Выдано</button>
|
||
<button class="kt-ai-btn" data-variant="ghost" data-size="sm" data-status="returned">Возвращено</button>
|
||
<button class="kt-ai-btn" data-variant="ghost" data-size="sm" data-status="overdue">Просрочено</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="table-container">
|
||
<table class="books-table">
|
||
<thead>
|
||
<tr>
|
||
<th>Книга</th>
|
||
<th>Читатель</th>
|
||
<th>Дата выдачи</th>
|
||
<th>Дата возврата</th>
|
||
<th>Статус</th>
|
||
<th class="text-right">Действия</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="booksTableBody">
|
||
</tbody>
|
||
</table>
|
||
<div class="empty-state" id="emptyState" style="display:none">
|
||
<p>Записей не найдено</p>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
</main>
|
||
|
||
<div class="modal-overlay" id="modalOverlay">
|
||
<div class="modal">
|
||
<h2 class="modal-title" id="modalTitle">Выдача книги</h2>
|
||
<form id="bookForm">
|
||
<input type="hidden" id="recordId">
|
||
<div class="form-group">
|
||
<label class="form-label" for="bookTitle">Название книги *</label>
|
||
<input type="text" class="kt-ai-input" id="bookTitle" required placeholder="Введите название книги">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label" for="readerName">ФИО читателя *</label>
|
||
<input type="text" class="kt-ai-input" id="readerName" required placeholder="Иванов Иван Иванович">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label" for="issueDate">Дата выдачи *</label>
|
||
<input type="date" class="kt-ai-input" id="issueDate" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label" for="dueDate">Дата возврата *</label>
|
||
<input type="date" class="kt-ai-input" id="dueDate" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label" for="status">Статус</label>
|
||
<select class="kt-ai-select" id="status">
|
||
<option value="issued">Выдано</option>
|
||
<option value="returned">Возвращено</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-actions">
|
||
<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, bookTitle: "Абай жолы", readerName: "Ахметов Арман Бауыржанович", issueDate: "2025-08-15", dueDate: "2025-09-15", status: "issued" },
|
||
{ id: 2, bookTitle: "Қазақ тілі оқулығы", readerName: "Бейсенова Айгуль Маратовна", issueDate: "2025-08-20", dueDate: "2025-09-20", status: "returned" },
|
||
{ id: 3, bookTitle: "Алгоритмы и структуры данных", readerName: "Васильев Петр Сергеевич", issueDate: "2025-07-01", dueDate: "2025-08-01", status: "overdue" },
|
||
{ id: 4, bookTitle: "Физика для начинающих", readerName: "Газиева Наргиз Асетовна", issueDate: "2025-08-25", dueDate: "2025-09-25", status: "issued" },
|
||
{ id: 5, bookTitle: "История Казахстана", readerName: "Дюсенов Ержан Кенесович", issueDate: "2025-08-10", dueDate: "2025-09-10", status: "issued" },
|
||
{ id: 6, bookTitle: "Математический анализ", readerName: "Еремеева Ольга Викторовна", issueDate: "2025-07-15", dueDate: "2025-08-15", status: "overdue" },
|
||
{ id: 7, bookTitle: "Әдебиеттік оқу", readerName: "Жандосов Болат Нурланович", issueDate: "2025-08-01", dueDate: "2025-09-01", status: "returned" },
|
||
{ id: 8, bookTitle: "Химия 10 класс", readerName: "Ибраева Салтанат Жумашевна", issueDate: "2025-08-28", dueDate: "2025-09-28", status: "issued" }
|
||
];
|
||
|
||
let records = [];
|
||
let currentFilter = 'all';
|
||
let searchTerm = '';
|
||
|
||
function loadRecords() {
|
||
const stored = localStorage.getItem('bookJournalRecords');
|
||
if (stored) {
|
||
records = JSON.parse(stored);
|
||
} else {
|
||
records = [...DEMO_DATA];
|
||
saveRecords();
|
||
}
|
||
}
|
||
|
||
function saveRecords() {
|
||
localStorage.setItem('bookJournalRecords', JSON.stringify(records));
|
||
}
|
||
|
||
function getStatusLabel(status) {
|
||
const labels = { issued: 'Выдано', returned: 'Возвращено', overdue: 'Просрочено' };
|
||
return labels[status] || status;
|
||
}
|
||
|
||
function getStatusChipClass(status) {
|
||
if (status === 'returned') return 'chip-ok';
|
||
if (status === 'overdue') return 'chip-risk';
|
||
return 'chip-warn';
|
||
}
|
||
|
||
function checkOverdue(record) {
|
||
if (record.status === 'returned') return false;
|
||
const today = new Date().toISOString().split('T')[0];
|
||
return record.dueDate < today;
|
||
}
|
||
|
||
function updateOverdueStatus() {
|
||
let changed = false;
|
||
records.forEach(rec => {
|
||
if (rec.status !== 'returned' && rec.status !== 'overdue' && checkOverdue(rec)) {
|
||
rec.status = 'overdue';
|
||
changed = true;
|
||
}
|
||
});
|
||
if (changed) saveRecords();
|
||
}
|
||
|
||
function renderKPIs() {
|
||
const total = records.length;
|
||
const issued = records.filter(r => r.status === 'issued').length;
|
||
const returned = records.filter(r => r.status === 'returned').length;
|
||
const overdue = records.filter(r => r.status === 'overdue').length;
|
||
|
||
document.getElementById('kpiTotal').textContent = total;
|
||
document.getElementById('kpiIssued').textContent = issued;
|
||
document.getElementById('kpiReturned').textContent = returned;
|
||
document.getElementById('kpiOverdue').textContent = overdue;
|
||
}
|
||
|
||
function getFilteredRecords() {
|
||
return records.filter(rec => {
|
||
const matchesFilter = currentFilter === 'all' || rec.status === currentFilter;
|
||
const matchesSearch = searchTerm === '' ||
|
||
rec.bookTitle.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||
rec.readerName.toLowerCase().includes(searchTerm.toLowerCase());
|
||
return matchesFilter && matchesSearch;
|
||
});
|
||
}
|
||
|
||
function renderTable() {
|
||
const tbody = document.getElementById('booksTableBody');
|
||
const emptyState = document.getElementById('emptyState');
|
||
const filtered = getFilteredRecords();
|
||
|
||
if (filtered.length === 0) {
|
||
tbody.innerHTML = '';
|
||
emptyState.style.display = 'block';
|
||
return;
|
||
}
|
||
|
||
emptyState.style.display = 'none';
|
||
tbody.innerHTML = filtered.map(rec => {
|
||
const chipClass = getStatusChipClass(rec.status);
|
||
return `
|
||
<tr>
|
||
<td><strong>${escapeHtml(rec.bookTitle)}</strong></td>
|
||
<td>${escapeHtml(rec.readerName)}</td>
|
||
<td class="tabular-nums">${formatDate(rec.issueDate)}</td>
|
||
<td class="tabular-nums">${formatDate(rec.dueDate)}</td>
|
||
<td><span class="kt-ai-chip ${chipClass}">${getStatusLabel(rec.status)}</span></td>
|
||
<td class="text-right">
|
||
<button class="kt-ai-btn" data-variant="ghost" data-size="sm" onclick="editRecord(${rec.id})">✏️</button>
|
||
<button class="kt-ai-btn" data-variant="ghost" data-size="sm" onclick="deleteRecord(${rec.id})">🗑️</button>
|
||
</td>
|
||
</tr>
|
||
`;
|
||
}).join('');
|
||
}
|
||
|
||
function escapeHtml(text) {
|
||
const div = document.createElement('div');
|
||
div.textContent = text;
|
||
return div.innerHTML;
|
||
}
|
||
|
||
function formatDate(dateStr) {
|
||
const date = new Date(dateStr);
|
||
return date.toLocaleDateString('ru-RU', { day: '2-digit', month: '2-digit', year: 'numeric' });
|
||
}
|
||
|
||
function openModal(recordId = null) {
|
||
const modal = document.getElementById('modalOverlay');
|
||
const form = document.getElementById('bookForm');
|
||
const title = document.getElementById('modalTitle');
|
||
|
||
form.reset();
|
||
|
||
if (recordId) {
|
||
const rec = records.find(r => r.id === recordId);
|
||
if (rec) {
|
||
title.textContent = 'Редактирование записи';
|
||
document.getElementById('recordId').value = rec.id;
|
||
document.getElementById('bookTitle').value = rec.bookTitle;
|
||
document.getElementById('readerName').value = rec.readerName;
|
||
document.getElementById('issueDate').value = rec.issueDate;
|
||
document.getElementById('dueDate').value = rec.dueDate;
|
||
document.getElementById('status').value = rec.status;
|
||
}
|
||
} else {
|
||
title.textContent = 'Выдача книги';
|
||
document.getElementById('recordId').value = '';
|
||
document.getElementById('issueDate').value = new Date().toISOString().split('T')[0];
|
||
const dueDate = new Date();
|
||
dueDate.setDate(dueDate.getDate() + 30);
|
||
document.getElementById('dueDate').value = dueDate.toISOString().split('T')[0];
|
||
}
|
||
|
||
modal.classList.add('active');
|
||
}
|
||
|
||
function closeModal() {
|
||
document.getElementById('modalOverlay').classList.remove('active');
|
||
}
|
||
|
||
function editRecord(id) {
|
||
openModal(id);
|
||
}
|
||
|
||
function deleteRecord(id) {
|
||
if (confirm('Удалить эту запись?')) {
|
||
records = records.filter(r => r.id !== id);
|
||
saveRecords();
|
||
updateOverdueStatus();
|
||
renderKPIs();
|
||
renderTable();
|
||
}
|
||
}
|
||
|
||
document.getElementById('addBookBtn').addEventListener('click', () => openModal());
|
||
document.getElementById('cancelBtn').addEventListener('click', closeModal);
|
||
document.getElementById('modalOverlay').addEventListener('click', (e) => {
|
||
if (e.target === document.getElementById('modalOverlay')) closeModal();
|
||
});
|
||
|
||
document.getElementById('bookForm').addEventListener('submit', (e) => {
|
||
e.preventDefault();
|
||
|
||
const recordId = document.getElementById('recordId').value;
|
||
const bookTitle = document.getElementById('bookTitle').value.trim();
|
||
const readerName = document.getElementById('readerName').value.trim();
|
||
const issueDate = document.getElementById('issueDate').value;
|
||
const dueDate = document.getElementById('dueDate').value;
|
||
const status = document.getElementById('status').value;
|
||
|
||
if (!bookTitle || !readerName || !issueDate || !dueDate) {
|
||
alert('Заполните все обязательные поля');
|
||
return;
|
||
}
|
||
|
||
if (recordId) {
|
||
const rec = records.find(r => r.id == recordId);
|
||
if (rec) {
|
||
rec.bookTitle = bookTitle;
|
||
rec.readerName = readerName;
|
||
rec.issueDate = issueDate;
|
||
rec.dueDate = dueDate;
|
||
rec.status = status;
|
||
}
|
||
} else {
|
||
const newId = records.length > 0 ? Math.max(...records.map(r => r.id)) + 1 : 1;
|
||
records.push({ id: newId, bookTitle, readerName, issueDate, dueDate, status });
|
||
}
|
||
|
||
saveRecords();
|
||
closeModal();
|
||
updateOverdueStatus();
|
||
renderKPIs();
|
||
renderTable();
|
||
});
|
||
|
||
document.querySelectorAll('.status-filters .kt-ai-btn').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
document.querySelectorAll('.status-filters .kt-ai-btn').forEach(b => {
|
||
b.setAttribute('data-variant', 'ghost');
|
||
});
|
||
btn.setAttribute('data-variant', 'primary');
|
||
currentFilter = btn.getAttribute('data-status');
|
||
renderTable();
|
||
});
|
||
});
|
||
|
||
document.getElementById('searchInput').addEventListener('input', (e) => {
|
||
searchTerm = e.target.value.trim();
|
||
renderTable();
|
||
});
|
||
|
||
document.getElementById('themeToggle').addEventListener('click', () => {
|
||
const html = document.documentElement;
|
||
const current = html.getAttribute('data-theme');
|
||
html.setAttribute('data-theme', current === 'dark' ? 'light' : 'dark');
|
||
});
|
||
|
||
loadRecords();
|
||
updateOverdueStatus();
|
||
renderKPIs();
|
||
renderTable();
|
||
</script>
|
||
|
||
</body>
|
||
</html>
|