401 lines
17 KiB
HTML
401 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">
|
||
<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>
|