uchebnyy-zhurnal-vydachi-kni/index.html

396 lines
17 KiB
HTML
Raw Permalink 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">
<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>