331 lines
12 KiB
JavaScript
331 lines
12 KiB
JavaScript
// Демо-данные: 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();
|
||
});
|