zhurnal-vydachi-spetsodezhdy/script.js

331 lines
12 KiB
JavaScript
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.

// Демо-данные: 10 записей с реалистичными именами и датами
const DEMO_DATA = [
{ id: 1, fio: "Алиев А.С.", item: "Каска защитная", issueDate: "2025-01-15", returnDate: "2025-07-15", status: "В носке" },
{ id: 2, fio: "Смирнова Е.В.", item: "Ботинки кожаные", issueDate: "2025-02-01", returnDate: "2025-06-01", status: "Просрочено" },
{ id: 3, fio: "Ибрагимов Б.К.", item: "Костюм х/б", issueDate: "2025-03-10", returnDate: "2025-09-10", status: "В носке" },
{ id: 4, fio: "Петров И.Д.", item: "Перчатки рабочие", issueDate: "2025-01-20", returnDate: "2025-04-20", status: "Просрочено" },
{ id: 5, fio: "Ахметова Г.Т.", item: "Очки защитные", issueDate: "2025-04-05", returnDate: "2025-10-05", status: "В носке" },
{ id: 6, fio: "Козлов М.А.", item: "Респиратор", issueDate: "2025-02-15", returnDate: "2025-05-15", status: "Сдано" },
{ id: 7, fio: "Нурланова А.Е.", item: "Каска защитная", issueDate: "2025-05-01", returnDate: "2025-11-01", status: "В носке" },
{ id: 8, fio: "Васильченко П.П.", item: "Наушники противошумные", issueDate: "2025-03-20", returnDate: "2025-08-20", status: "Просрочено" },
{ id: 9, fio: "Омаров Д.Ж.", item: "Жилет сигнальный", issueDate: "2025-06-01", returnDate: "2025-12-01", status: "В носке" },
{ id: 10, fio: "Тимофеева О.Л.", item: "Сапоги резиновые", issueDate: "2025-01-10", returnDate: "2025-03-10", status: "Сдано" }
];
// Ключи для localStorage
const STORAGE_KEY = "specod_storage_v1";
const LOG_KEY = "specod_log_v1";
// Текущие данные
let items = [];
let logs = [];
let currentFilter = "Все";
// Инициализация при загрузке
document.addEventListener("DOMContentLoaded", () => {
loadData();
renderFilters();
renderTable();
renderLog();
updateCounts();
});
// Загрузка данных из localStorage или демо
function loadData() {
const stored = localStorage.getItem(STORAGE_KEY);
if (stored) {
items = JSON.parse(stored);
} else {
items = [...DEMO_DATA];
saveToStorage();
}
const storedLog = localStorage.getItem(LOG_KEY);
if (storedLog) {
logs = JSON.parse(storedLog);
} else {
logs = [{ type: "add", message: "Система инициализирована с демо-данными", timestamp: new Date().toISOString() }];
saveLogToStorage();
}
}
function saveToStorage() {
localStorage.setItem(STORAGE_KEY, JSON.stringify(items));
}
function saveLogToStorage() {
localStorage.setItem(LOG_KEY, JSON.stringify(logs));
}
// Фильтры
function renderFilters() {
const filters = ["Все", "В носке", "Сдано", "Просрочено"];
const container = document.getElementById("filters");
container.innerHTML = filters.map(f =>
`<button class="filter-chip ${f === currentFilter ? "active" : ""}" onclick="setFilter('${f}')">${f}</button>`
).join("");
}
function setFilter(filter) {
currentFilter = filter;
renderFilters();
renderTable();
}
// Таблица
function renderTable() {
const tbody = document.getElementById("table-body");
const search = document.getElementById("search").value.toLowerCase();
let filtered = items.filter(item => {
// Обновляем статус просрочки
item = updateItemStatus(item);
// Фильтр по статусу
if (currentFilter !== "Все" && item.status !== currentFilter) return false;
// Поиск
if (search) {
return item.fio.toLowerCase().includes(search) || item.item.toLowerCase().includes(search);
}
return true;
});
if (filtered.length === 0) {
tbody.innerHTML = `
<tr>
<td colspan="7" class="px-4 py-12 text-center text-gray-500">
<div class="empty-state">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"></path></svg>
<p>Нет записей для отображения</p>
</div>
</td>
</tr>
`;
updateCounts();
return;
}
tbody.innerHTML = filtered.map((item, idx) => {
const isOverdue = item.status === "Просрочено";
return `
<tr class="${isOverdue ? "row-overdue" : "hover:bg-gray-50"} transition-colors">
<td class="px-4 py-3 text-sm text-gray-700">${idx + 1}</td>
<td class="px-4 py-3 text-sm font-medium text-gray-800">${escapeHtml(item.fio)}</td>
<td class="px-4 py-3 text-sm text-gray-700">${escapeHtml(item.item)}</td>
<td class="px-4 py-3 text-sm text-gray-600">${formatDate(item.issueDate)}</td>
<td class="px-4 py-3 text-sm text-gray-600">${formatDate(item.returnDate)}</td>
<td class="px-4 py-3">
<span class="status-badge ${getStatusClass(item.status)}">${item.status}</span>
</td>
<td class="px-4 py-3 text-sm">
<button onclick="editItem(${item.id})" class="text-blue-600 hover:text-blue-800 mr-3 font-medium">✏️ Изменить</button>
<button onclick="deleteItem(${item.id})" class="text-red-600 hover:text-red-800 font-medium">🗑️ Удалить</button>
</td>
</tr>
`;
}).join("");
updateCounts();
}
// Обновление статуса на основе даты
function updateItemStatus(item) {
const today = new Date();
today.setHours(0, 0, 0, 0);
const returnDate = new Date(item.returnDate);
if (item.status !== "Сдано" && returnDate < today) {
item.status = "Просрочено";
}
return item;
}
function getStatusClass(status) {
switch (status) {
case "В носке": return "status-worn";
case "Сдано": return "status-returned";
case "Просрочено": return "status-overdue";
default: return "status-worn";
}
}
// Счётчики
function updateCounts() {
// Пересчитываем статусы перед подсчётом
items = items.map(updateItemStatus);
saveToStorage();
const total = items.length;
const worn = items.filter(i => i.status === "В носке").length;
const returned = items.filter(i => i.status === "Сдано").length;
const overdue = items.filter(i => i.status === "Просрочено").length;
document.getElementById("count-total").textContent = total;
document.getElementById("count-worn").textContent = worn;
document.getElementById("count-returned").textContent = returned;
document.getElementById("count-overdue").textContent = overdue;
}
// Модальное окно
function openModal(isEdit = false) {
document.getElementById("modal").style.display = "flex";
document.getElementById("modal-title").textContent = isEdit ? "Редактирование выдачи" : "Новая выдача";
if (!isEdit) {
document.getElementById("form").reset();
document.getElementById("edit-id").value = "";
// Устанавливаем сегодняшнюю дату по умолчанию
document.getElementById("form-issue-date").value = new Date().toISOString().split("T")[0];
}
}
function closeModal() {
document.getElementById("modal").style.display = "none";
}
// Сохранение (добавление/редактирование)
function saveItem(event) {
event.preventDefault();
const id = document.getElementById("edit-id").value;
const fio = document.getElementById("form-fio").value.trim();
const item = document.getElementById("form-item").value.trim();
const issueDate = document.getElementById("form-issue-date").value;
const returnDate = document.getElementById("form-return-date").value;
if (id) {
// Редактирование
const idx = items.findIndex(i => i.id == id);
if (idx !== -1) {
const oldItem = { ...items[idx] };
items[idx] = { ...items[idx], fio, item, issueDate, returnDate };
logs.unshift({
type: "edit",
message: `Изменено: ${oldItem.fio} — ${oldItem.item}`,
timestamp: new Date().toISOString()
});
}
} else {
// Добавление
const newId = Date.now();
let status = "В носке";
const today = new Date();
today.setHours(0, 0, 0, 0);
const retDate = new Date(returnDate);
if (retDate < today) status = "Просрочено";
items.unshift({ id: newId, fio, item, issueDate, returnDate, status });
logs.unshift({
type: "add",
message: `Добавлено: ${fio} — ${item}`,
timestamp: new Date().toISOString()
});
}
saveToStorage();
saveLogToStorage();
closeModal();
renderTable();
renderLog();
}
// Редактирование
function editItem(id) {
const item = items.find(i => i.id === id);
if (!item) return;
document.getElementById("edit-id").value = item.id;
document.getElementById("form-fio").value = item.fio;
document.getElementById("form-item").value = item.item;
document.getElementById("form-issue-date").value = item.issueDate;
document.getElementById("form-return-date").value = item.returnDate;
openModal(true);
}
// Удаление
function deleteItem(id) {
const item = items.find(i => i.id === id);
if (!item) return;
if (confirm(`Вы уверены, что хотите удалить запись:\n${item.fio} — ${item.item}?`)) {
items = items.filter(i => i.id !== id);
logs.unshift({
type: "delete",
message: `Удалено: ${item.fio} — ${item.item}`,
timestamp: new Date().toISOString()
});
saveToStorage();
saveLogToStorage();
renderTable();
renderLog();
}
}
// Журнал изменений
function renderLog() {
const container = document.getElementById("log-container");
if (logs.length === 0) {
container.innerHTML = '<div class="text-gray-500 text-sm">История пуста</div>';
return;
}
container.innerHTML = logs.map(log => {
const date = new Date(log.timestamp);
const dateStr = date.toLocaleDateString("ru-RU") + " " + date.toLocaleTimeString("ru-RU", { hour: "2-digit", minute: "2-digit" });
return `
<div class="log-entry ${log.type}">
<span class="font-medium text-gray-700">${log.message}</span>
<span class="text-gray-500 text-xs ml-2">(${dateStr})</span>
</div>
`;
}).join("");
}
// Выгрузка в Excel (CSV с BOM)
function exportToExcel() {
const head = ["№", "ФИО", "Наименование", "Дата выдачи", "Дата возврата", "Статус"];
const rows = items.map((item, idx) => [
idx + 1,
item.fio,
item.item,
formatDate(item.issueDate),
formatDate(item.returnDate),
item.status
]);
const csvContent = "\uFEFF" + [
head.join(";"),
...rows.map(r => r.map(v => `"${String(v).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_${new Date().toISOString().slice(0, 10)}.csv`;
link.click();
}
// Утилиты
function formatDate(dateStr) {
if (!dateStr) return "";
const [year, month, day] = dateStr.split("-");
return `${day}.${month}.${year}`;
}
function escapeHtml(text) {
const div = document.createElement("div");
div.textContent = text;
return div.innerHTML;
}
// Закрытие модального окна по клику вне
document.getElementById("modal").addEventListener("click", (e) => {
if (e.target.id === "modal") closeModal();
});