zhurnal-vydachi-noutbukov/index.html

401 lines
17 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-pagehead-in { align-items: center; }
.stats-row { display: flex; gap: var(--kt-ai-space-4); flex-wrap: wrap; margin-bottom: var(--kt-ai-space-6); }
.stat-card { background: var(--kt-ai-card-bg); border: 1px solid var(--kt-ai-border); border-radius: var(--kt-ai-radius-xl); padding: var(--kt-ai-space-5) var(--kt-ai-space-6); min-width: 140px; }
.stat-value { font-size: var(--kt-ai-text-3xl); font-weight: var(--kt-ai-weight-bold); color: var(--kt-ai-fg); line-height: 1; }
.stat-label { font-size: var(--kt-ai-text-sm); color: var(--kt-ai-fg-muted); margin-top: var(--kt-ai-space-2); }
.toolbar { display: flex; gap: var(--kt-ai-space-3); align-items: center; flex-wrap: wrap; margin-bottom: var(--kt-ai-space-5); }
.search-input { flex: 1; min-width: 200px; }
.chips { display: flex; gap: var(--kt-ai-space-2); flex-wrap: wrap; }
.laptop-table { width: 100%; border-collapse: collapse; font-size: var(--kt-ai-text-sm); }
.laptop-table th, .laptop-table td { text-align: left; padding: var(--kt-ai-space-3) var(--kt-ai-space-4); border-bottom: 1px solid var(--kt-ai-divider); }
.laptop-table th { font-weight: var(--kt-ai-weight-semibold); color: var(--kt-ai-fg-muted); background: var(--kt-ai-bg-soft); }
.laptop-table tbody tr:hover { background: var(--kt-ai-bg-soft); }
.actions-cell { display: flex; gap: var(--kt-ai-space-2); }
.modal-backdrop { position: fixed; inset: 0; background: rgba(0,0,0,0.5); display: none; align-items: center; justify-content: center; z-index: 1000; }
.modal-backdrop.on { display: flex; }
.modal { background: var(--kt-ai-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-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--kt-ai-space-5); }
.modal-title { font-size: var(--kt-ai-text-xl); font-weight: var(--kt-ai-weight-semibold); }
.form-grid { display: grid; gap: var(--kt-ai-space-4); }
.modal-actions { display: flex; gap: var(--kt-ai-space-3); justify-content: flex-end; margin-top: var(--kt-ai-space-6); }
.empty-state { text-align: center; padding: var(--kt-ai-space-12) var(--kt-ai-space-6); color: var(--kt-ai-fg-muted); }
.status-chip { display: inline-block; padding: var(--kt-ai-space-1) var(--kt-ai-space-3); border-radius: var(--kt-ai-radius-full); font-size: var(--kt-ai-text-xs); font-weight: var(--kt-ai-weight-medium); }
.status-vydan { background: #dbeafe; color: #1e40af; }
.status-na-sklade { background: #dcfce7; color: #166534; }
.status-v-remonte { background: #fee2e2; color: #991b1b; }
a.kt-ai-btn { text-decoration: none; }
@media (max-width: 768px) {
.laptop-table { font-size: var(--kt-ai-text-xs); }
.laptop-table th, .laptop-table td { padding: var(--kt-ai-space-2); }
.toolbar { flex-direction: column; align-items: stretch; }
.search-input { min-width: 100%; }
}
</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>
<button class="kt-ai-btn" data-variant="primary" id="addBtn">+ Добавить ноутбук</button>
</div>
</header>
<main class="kt-ai-wrap" style="padding-top: var(--kt-ai-space-8); padding-bottom: var(--kt-ai-space-12);">
<section class="kt-ai-section">
<div class="kt-ai-section-head">
<h2>Учёт корпоративных ноутбуков</h2>
<p>Отслеживайте выдачу техники сотрудникам, статусы и инвентарные номера</p>
</div>
<div class="stats-row" id="statsRow">
<div class="stat-card">
<div class="stat-value" id="statTotal">0</div>
<div class="stat-label">Всего</div>
</div>
<div class="stat-card">
<div class="stat-value" id="statVydan">0</div>
<div class="stat-label">Выдано</div>
</div>
<div class="stat-card">
<div class="stat-value" id="statNaSklade">0</div>
<div class="stat-label">На складе</div>
</div>
<div class="stat-card">
<div class="stat-value" id="statVRemonte">0</div>
<div class="stat-label">В ремонте</div>
</div>
</div>
<div class="toolbar">
<input type="text" class="kt-ai-input search-input" id="searchInput" placeholder="Поиск по любой колонке...">
<div class="chips" id="filterChips">
<button class="kt-ai-btn" data-variant="primary" data-filter="all">Все</button>
<button class="kt-ai-btn" data-variant="ghost" data-filter="Выдан">Выдан</button>
<button class="kt-ai-btn" data-variant="ghost" data-filter="На складе">На складе</button>
<button class="kt-ai-btn" data-variant="ghost" data-filter="В ремонте">В ремонте</button>
</div>
<button class="kt-ai-btn" data-variant="ghost" id="exportBtn">📥 Excel</button>
</div>
<div class="kt-ai-card" style="overflow-x: auto;">
<table class="laptop-table" id="laptopTable">
<thead>
<tr>
<th>Инвентарный №</th>
<th>Модель</th>
<th>Серийный №</th>
<th>Сотрудник</th>
<th>Дата выдачи</th>
<th>Статус</th>
<th style="width: 180px;">Действия</th>
</tr>
</thead>
<tbody id="tableBody"></tbody>
</table>
<div class="empty-state" id="emptyState" style="display: none;">
<p>Нет записей</p>
<p style="font-size: var(--kt-ai-text-sm); margin-top: var(--kt-ai-space-2);">Нажмите «Добавить ноутбук» чтобы создать первую запись</p>
</div>
</div>
</section>
</main>
<div class="modal-backdrop" id="modalBackdrop">
<div class="modal">
<div class="modal-header">
<h3 class="modal-title" id="modalTitle">Добавить ноутбук</h3>
<button class="kt-ai-btn" data-variant="ghost" id="closeModal">×</button>
</div>
<form id="laptopForm" class="form-grid">
<input type="hidden" id="editId">
<div class="kt-ai-field">
<label>ФИО сотрудника</label>
<input type="text" class="kt-ai-input" id="employeeName" required placeholder="Иванов Иван Иванович">
</div>
<div class="kt-ai-field">
<label>Модель ноутбука</label>
<input type="text" class="kt-ai-input" id="laptopModel" required placeholder="MacBook Pro 14 M3">
</div>
<div class="kt-ai-field">
<label>Серийный номер (S/N)</label>
<input type="text" class="kt-ai-input" id="serialNumber" required placeholder="C02XY1ABCD12">
</div>
<div class="kt-ai-field">
<label>Инвентарный номер</label>
<input type="text" class="kt-ai-input" id="inventoryNumber" required placeholder="INV-2024-001">
</div>
<div class="kt-ai-field">
<label>Дата выдачи</label>
<input type="date" class="kt-ai-input" id="issueDate" required>
</div>
<div class="kt-ai-field">
<label>Статус</label>
<select class="kt-ai-select" id="status" required>
<option value="Выдан">Выдан</option>
<option value="На складе">На складе</option>
<option value="В ремонте">В ремонте</option>
</select>
</div>
<div class="modal-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, employee: "Аханов Ержан", model: "MacBook Pro 14 M3", serial: "C02XY1ABCD12", inventory: "INV-2024-001", date: "15.01.2024", status: "Выдан" },
{ id: 2, employee: "Смирнова Анна", model: "Dell XPS 15", serial: "7KDH823", inventory: "INV-2024-002", date: "20.01.2024", status: "Выдан" },
{ id: 3, employee: "Искаков Бауыржан", model: "HP EliteBook 850", serial: "5CD9450KLM", inventory: "INV-2024-003", date: "05.02.2024", status: "На складе" },
{ id: 4, employee: "Петров Сергей", model: "Lenovo ThinkPad X1", serial: "PF3M8N2Q", inventory: "INV-2024-004", date: "10.02.2024", status: "В ремонте" },
{ id: 5, employee: "Ким Юлия", model: "Asus ZenBook 14", serial: "N4L6P8R2T0", inventory: "INV-2024-005", date: "01.03.2024", status: "Выдан" }
];
// === Хранилище ===
const STORAGE_KEY = "laptop_journal_data";
const LOG_KEY = "laptop_journal_log";
function loadData() {
const stored = localStorage.getItem(STORAGE_KEY);
if (stored) return JSON.parse(stored);
localStorage.setItem(STORAGE_KEY, JSON.stringify(DEMO_DATA));
return DEMO_DATA;
}
function saveData(data) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
}
function logAction(action) {
const log = JSON.parse(localStorage.getItem(LOG_KEY) || "[]");
log.push({ timestamp: new Date().toISOString(), action });
localStorage.setItem(LOG_KEY, JSON.stringify(log));
}
// === Состояние ===
let allData = loadData();
let currentFilter = "all";
let searchTerm = "";
// === Элементы ===
const tableBody = document.getElementById("tableBody");
const emptyState = document.getElementById("emptyState");
const modalBackdrop = document.getElementById("modalBackdrop");
const modalTitle = document.getElementById("modalTitle");
const laptopForm = document.getElementById("laptopForm");
const searchInput = document.getElementById("searchInput");
const filterChips = document.getElementById("filterChips");
const addBtn = document.getElementById("addBtn");
const exportBtn = document.getElementById("exportBtn");
const themeToggle = document.getElementById("themeToggle");
const closeModal = document.getElementById("closeModal");
const cancelBtn = document.getElementById("cancelBtn");
// === Рендер таблицы ===
function getStatusClass(status) {
if (status === "Выдан") return "status-vydan";
if (status === "На складе") return "status-na-sklade";
if (status === "В ремонте") return "status-v-remonte";
return "";
}
function renderTable() {
let filtered = allData;
if (currentFilter !== "all") {
filtered = filtered.filter(item => item.status === currentFilter);
}
if (searchTerm.trim()) {
const term = searchTerm.toLowerCase();
filtered = filtered.filter(item =>
item.employee.toLowerCase().includes(term) ||
item.model.toLowerCase().includes(term) ||
item.serial.toLowerCase().includes(term) ||
item.inventory.toLowerCase().includes(term) ||
item.date.toLowerCase().includes(term) ||
item.status.toLowerCase().includes(term)
);
}
if (filtered.length === 0) {
tableBody.style.display = "none";
emptyState.style.display = "block";
} else {
tableBody.style.display = "";
emptyState.style.display = "none";
tableBody.innerHTML = filtered.map(item => `
<tr>
<td>${item.inventory}</td>
<td>${item.model}</td>
<td>${item.serial}</td>
<td>${item.employee}</td>
<td>${item.date}</td>
<td><span class="status-chip ${getStatusClass(item.status)}">${item.status}</span></td>
<td class="actions-cell">
<button class="kt-ai-btn" data-variant="ghost" data-size="sm" onclick="editItem(${item.id})">Изменить</button>
<button class="kt-ai-btn" data-variant="ghost" data-size="sm" onclick="deleteItem(${item.id}, '${item.employee}', '${item.model}')">Удалить</button>
</td>
</tr>
`).join("");
}
updateStats();
}
function updateStats() {
document.getElementById("statTotal").textContent = allData.length;
document.getElementById("statVydan").textContent = allData.filter(i => i.status === "Выдан").length;
document.getElementById("statNaSklade").textContent = allData.filter(i => i.status === "На складе").length;
document.getElementById("statVRemonte").textContent = allData.filter(i => i.status === "В ремонте").length;
}
// === Модальное окно ===
function openModal(editMode = false) {
modalBackdrop.classList.add("on");
modalTitle.textContent = editMode ? "Редактировать ноутбук" : "Добавить ноутбук";
if (!editMode) {
laptopForm.reset();
document.getElementById("editId").value = "";
document.getElementById("issueDate").valueAsDate = new Date();
}
}
function closeModalFunc() {
modalBackdrop.classList.remove("on");
}
// === CRUD ===
window.editItem = function(id) {
const item = allData.find(i => i.id === id);
if (!item) return;
document.getElementById("editId").value = item.id;
document.getElementById("employeeName").value = item.employee;
document.getElementById("laptopModel").value = item.model;
document.getElementById("serialNumber").value = item.serial;
document.getElementById("inventoryNumber").value = item.inventory;
document.getElementById("issueDate").value = item.date.split(".").reverse().join("-");
document.getElementById("status").value = item.status;
openModal(true);
};
window.deleteItem = function(id, employee, model) {
if (!confirm(`Удалить ноутбук ${model} (сотрудник: ${employee})?`)) return;
const item = allData.find(i => i.id === id);
allData = allData.filter(i => i.id !== id);
saveData(allData);
logAction(`Удалён ноутбук: ${model}, S/N: ${item.serial}, инвентарный: ${item.inventory}, сотрудник: ${employee}`);
renderTable();
};
laptopForm.addEventListener("submit", function(e) {
e.preventDefault();
const editId = document.getElementById("editId").value;
const formData = {
employee: document.getElementById("employeeName").value,
model: document.getElementById("laptopModel").value,
serial: document.getElementById("serialNumber").value,
inventory: document.getElementById("inventoryNumber").value,
date: document.getElementById("issueDate").value.split("-").reverse().join("."),
status: document.getElementById("status").value
};
if (editId) {
const index = allData.findIndex(i => i.id == editId);
if (index !== -1) {
const oldItem = allData[index];
allData[index] = { ...oldItem, ...formData };
logAction(`Изменён ноутбук #${editId}: ${formData.model}, S/N: ${formData.serial}, инвентарный: ${formData.inventory}, сотрудник: ${formData.employee}, статус: ${formData.status}`);
}
} else {
const newItem = {
id: Date.now(),
...formData
};
allData.push(newItem);
logAction(`Добавлен ноутбук: ${formData.model}, S/N: ${formData.serial}, инвентарный: ${formData.inventory}, сотрудник: ${formData.employee}, статус: ${formData.status}`);
}
saveData(allData);
closeModalFunc();
renderTable();
});
// === Поиск и фильтры ===
searchInput.addEventListener("input", function(e) {
searchTerm = e.target.value;
renderTable();
});
filterChips.addEventListener("click", function(e) {
if (e.target.tagName !== "BUTTON") return;
currentFilter = e.target.dataset.filter;
filterChips.querySelectorAll("button").forEach(btn => {
btn.dataset.variant = btn.dataset.filter === currentFilter ? "primary" : "ghost";
});
renderTable();
});
// === Экспорт в CSV ===
exportBtn.addEventListener("click", function() {
const head = ["Инвентарный №", "Модель", "Серийный №", "Сотрудник", "Дата выдачи", "Статус"];
const body = allData.map(item =>
[item.inventory, item.model, item.serial, item.employee, item.date, item.status]
.map(v => '"' + String(v ?? "").replace(/"/g, '""') + '"')
.join(";")
);
const csv = "\uFEFF" + [head.join(";"), ...body].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_noutbukov_" + new Date().toISOString().slice(0, 10) + ".csv";
link.click();
logAction("Выгрузка данных в Excel (CSV)");
});
// === Переключатель темы ===
themeToggle.addEventListener("click", function() {
const html = document.documentElement;
const current = html.dataset.theme;
html.dataset.theme = current === "light" ? "dark" : "light";
});
// === Инициализация ===
addBtn.addEventListener("click", () => openModal(false));
closeModal.addEventListener("click", closeModalFunc);
cancelBtn.addEventListener("click", closeModalFunc);
modalBackdrop.addEventListener("click", function(e) {
if (e.target === modalBackdrop) closeModalFunc();
});
renderTable();
</script>
</body>
</html>