338 lines
11 KiB
JavaScript
338 lines
11 KiB
JavaScript
// Демо-данные для первого запуска
|
||
const DEMO_DATA = [
|
||
{ id: 1, submitter: "Алия К.", description: "Закупка СИЗ для склада", deadline: "2026-09-10", status: "Новая", dateCreated: "2026-09-01" },
|
||
{ id: 2, submitter: "Ержан М.", description: "Ремонт серверной", deadline: "2026-09-15", status: "В работе", dateCreated: "2026-09-02" },
|
||
{ id: 3, submitter: "Данияр С.", description: "Выдача пропуска", deadline: "2026-09-05", status: "Выполнено", dateCreated: "2026-09-01" },
|
||
{ id: 4, submitter: "Гульнара Т.", description: "Обновление ПО на компьютерах", deadline: "2026-09-20", status: "Новая", dateCreated: "2026-09-03" },
|
||
{ id: 5, submitter: "Серик Б.", description: "Замена картриджа в принтере", deadline: "2026-09-08", status: "В работе", dateCreated: "2026-09-02" }
|
||
];
|
||
|
||
// Ключи localStorage
|
||
const KEY_REQUESTS = "requests_data";
|
||
const KEY_HISTORY = "requests_history";
|
||
|
||
// Состояние
|
||
let requests = [];
|
||
let history = [];
|
||
let currentFilter = "all";
|
||
let searchQuery = "";
|
||
|
||
// Инициализация
|
||
document.addEventListener("DOMContentLoaded", () => {
|
||
loadData();
|
||
setupEventListeners();
|
||
render();
|
||
});
|
||
|
||
// Загрузка данных
|
||
function loadData() {
|
||
const storedRequests = localStorage.getItem(KEY_REQUESTS);
|
||
if (storedRequests) {
|
||
requests = JSON.parse(storedRequests);
|
||
} else {
|
||
requests = [...DEMO_DATA];
|
||
saveData();
|
||
}
|
||
|
||
const storedHistory = localStorage.getItem(KEY_HISTORY);
|
||
if (storedHistory) {
|
||
history = JSON.parse(storedHistory);
|
||
}
|
||
}
|
||
|
||
// Сохранение данных
|
||
function saveData() {
|
||
localStorage.setItem(KEY_REQUESTS, JSON.stringify(requests));
|
||
}
|
||
|
||
function saveHistory() {
|
||
localStorage.setItem(KEY_HISTORY, JSON.stringify(history));
|
||
}
|
||
|
||
// Настройка обработчиков
|
||
function setupEventListeners() {
|
||
document.getElementById("btn-add").addEventListener("click", openModal);
|
||
document.getElementById("modal-close").addEventListener("click", closeModal);
|
||
document.getElementById("form-cancel").addEventListener("click", closeModal);
|
||
document.getElementById("modal-overlay").addEventListener("click", (e) => {
|
||
if (e.target === document.getElementById("modal-overlay")) closeModal();
|
||
});
|
||
document.getElementById("request-form").addEventListener("submit", handleFormSubmit);
|
||
document.getElementById("search-input").addEventListener("input", (e) => {
|
||
searchQuery = e.target.value.toLowerCase();
|
||
renderTable();
|
||
});
|
||
document.getElementById("btn-export").addEventListener("click", exportToExcel);
|
||
document.getElementById("btn-history").addEventListener("click", openHistory);
|
||
document.getElementById("history-close").addEventListener("click", closeHistory);
|
||
document.getElementById("history-overlay").addEventListener("click", (e) => {
|
||
if (e.target === document.getElementById("history-overlay")) closeHistory();
|
||
});
|
||
|
||
document.querySelectorAll(".chip").forEach(chip => {
|
||
chip.addEventListener("click", () => {
|
||
document.querySelectorAll(".chip").forEach(c => c.classList.remove("active"));
|
||
chip.classList.add("active");
|
||
currentFilter = chip.dataset.status;
|
||
renderTable();
|
||
});
|
||
});
|
||
}
|
||
|
||
// Открытие модального окна
|
||
function openModal(editId = null) {
|
||
const modal = document.getElementById("modal-overlay");
|
||
const title = document.getElementById("modal-title");
|
||
const form = document.getElementById("request-form");
|
||
|
||
form.reset();
|
||
document.getElementById("form-id").value = "";
|
||
|
||
if (editId) {
|
||
const req = requests.find(r => r.id === editId);
|
||
if (req) {
|
||
title.textContent = "Изменить заявку";
|
||
document.getElementById("form-id").value = req.id;
|
||
document.getElementById("form-submitter").value = req.submitter;
|
||
document.getElementById("form-description").value = req.description;
|
||
document.getElementById("form-deadline").value = req.deadline;
|
||
document.getElementById("form-status").value = req.status;
|
||
}
|
||
} else {
|
||
title.textContent = "Добавить заявку";
|
||
}
|
||
|
||
modal.classList.add("active");
|
||
}
|
||
|
||
// Закрытие модального окна
|
||
function closeModal() {
|
||
document.getElementById("modal-overlay").classList.remove("active");
|
||
}
|
||
|
||
// Обработка формы
|
||
function handleFormSubmit(e) {
|
||
e.preventDefault();
|
||
|
||
const id = document.getElementById("form-id").value;
|
||
const submitter = document.getElementById("form-submitter").value.trim();
|
||
const description = document.getElementById("form-description").value.trim();
|
||
const deadline = document.getElementById("form-deadline").value;
|
||
const status = document.getElementById("form-status").value;
|
||
|
||
if (id) {
|
||
// Редактирование
|
||
const index = requests.findIndex(r => r.id === parseInt(id));
|
||
if (index !== -1) {
|
||
requests[index] = {
|
||
...requests[index],
|
||
submitter,
|
||
description,
|
||
deadline,
|
||
status
|
||
};
|
||
addToHistory(`Изменена заявка #${id}: ${description}`, "Текущий пользователь");
|
||
}
|
||
} else {
|
||
// Создание
|
||
const newId = Date.now();
|
||
requests.push({
|
||
id: newId,
|
||
submitter,
|
||
description,
|
||
deadline,
|
||
status,
|
||
dateCreated: new Date().toISOString().split("T")[0]
|
||
});
|
||
addToHistory(`Добавлена заявка #${newId}: ${description}`, "Текущий пользователь");
|
||
}
|
||
|
||
saveData();
|
||
closeModal();
|
||
render();
|
||
}
|
||
|
||
// Добавление в историю
|
||
function addToHistory(action, who) {
|
||
const now = new Date();
|
||
history.unshift({
|
||
datetime: now.toLocaleString("ru-RU"),
|
||
action,
|
||
who
|
||
});
|
||
saveHistory();
|
||
}
|
||
|
||
// Удаление заявки
|
||
function deleteRequest(id) {
|
||
const req = requests.find(r => r.id === id);
|
||
if (!req) return;
|
||
|
||
if (confirm(`Вы уверены, что хотите удалить заявку #${id}?\n\n${req.description}`)) {
|
||
requests = requests.filter(r => r.id !== id);
|
||
addToHistory(`Удалена заявка #${id}: ${req.description}`, "Текущий пользователь");
|
||
saveData();
|
||
render();
|
||
}
|
||
}
|
||
|
||
// Фильтрация данных
|
||
function getFilteredData() {
|
||
let filtered = [...requests];
|
||
|
||
// Фильтр по статусу
|
||
if (currentFilter !== "all") {
|
||
filtered = filtered.filter(r => r.status === currentFilter);
|
||
}
|
||
|
||
// Поиск
|
||
if (searchQuery) {
|
||
filtered = filtered.filter(r =>
|
||
r.submitter.toLowerCase().includes(searchQuery) ||
|
||
r.description.toLowerCase().includes(searchQuery)
|
||
);
|
||
}
|
||
|
||
return filtered;
|
||
}
|
||
|
||
// Рендеринг
|
||
function render() {
|
||
renderStats();
|
||
renderTable();
|
||
}
|
||
|
||
function renderStats() {
|
||
const total = requests.length;
|
||
const newCount = requests.filter(r => r.status === "Новая").length;
|
||
const progressCount = requests.filter(r => r.status === "В работе").length;
|
||
const doneCount = requests.filter(r => r.status === "Выполнено").length;
|
||
const rejectedCount = requests.filter(r => r.status === "Отклонено").length;
|
||
|
||
document.getElementById("stat-total").textContent = total;
|
||
document.getElementById("stat-new").textContent = newCount;
|
||
document.getElementById("stat-progress").textContent = progressCount;
|
||
document.getElementById("stat-done").textContent = doneCount;
|
||
document.getElementById("stat-rejected").textContent = rejectedCount;
|
||
}
|
||
|
||
function renderTable() {
|
||
const tbody = document.getElementById("table-body");
|
||
const filtered = getFilteredData();
|
||
|
||
if (filtered.length === 0) {
|
||
tbody.innerHTML = `
|
||
<tr>
|
||
<td colspan="6" class="empty-state">
|
||
<p>Нет заявок для отображения</p>
|
||
</td>
|
||
</tr>
|
||
`;
|
||
return;
|
||
}
|
||
|
||
tbody.innerHTML = filtered.map((req, index) => {
|
||
const statusClass = getStatusClass(req.status);
|
||
const formattedDate = formatDate(req.deadline);
|
||
return `
|
||
<tr>
|
||
<td>${index + 1}</td>
|
||
<td>${escapeHtml(req.submitter)}</td>
|
||
<td>${escapeHtml(req.description)}</td>
|
||
<td>${formattedDate}</td>
|
||
<td><span class="status-badge ${statusClass}">${req.status}</span></td>
|
||
<td>
|
||
<div class="action-buttons">
|
||
<button class="btn btn-small btn-edit" onclick="openModal(${req.id})">Изменить</button>
|
||
<button class="btn btn-small btn-delete" onclick="deleteRequest(${req.id})">Удалить</button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
`;
|
||
}).join("");
|
||
}
|
||
|
||
function getStatusClass(status) {
|
||
switch (status) {
|
||
case "Новая": return "status-new";
|
||
case "В работе": return "status-progress";
|
||
case "Выполнено": return "status-done";
|
||
case "Отклонено": return "status-rejected";
|
||
default: return "";
|
||
}
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
// Экспорт в Excel (CSV)
|
||
function exportToExcel() {
|
||
const filtered = getFilteredData();
|
||
|
||
if (filtered.length === 0) {
|
||
alert("Нет данных для экспорта");
|
||
return;
|
||
}
|
||
|
||
const headers = ["№", "Кто подал", "Что нужно", "Срок", "Статус"];
|
||
const rows = filtered.map((req, index) => [
|
||
index + 1,
|
||
req.submitter,
|
||
req.description,
|
||
formatDate(req.deadline),
|
||
req.status
|
||
]);
|
||
|
||
const csvContent = "\uFEFF" + [
|
||
headers.join(";"),
|
||
...rows.map(row => row.map(cell => `"${String(cell).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_zayavok_${new Date().toISOString().split("T")[0]}.csv`;
|
||
link.click();
|
||
URL.revokeObjectURL(link.href);
|
||
|
||
addToHistory("Экспорт данных в Excel", "Текущий пользователь");
|
||
}
|
||
|
||
// История
|
||
function openHistory() {
|
||
const tbody = document.getElementById("history-body");
|
||
|
||
if (history.length === 0) {
|
||
tbody.innerHTML = `
|
||
<tr>
|
||
<td colspan="3" class="empty-state">
|
||
<p>История пуста</p>
|
||
</td>
|
||
</tr>
|
||
`;
|
||
} else {
|
||
tbody.innerHTML = history.map(h => `
|
||
<tr>
|
||
<td>${h.datetime}</td>
|
||
<td>${escapeHtml(h.action)}</td>
|
||
<td>${escapeHtml(h.who)}</td>
|
||
</tr>
|
||
`).join("");
|
||
}
|
||
|
||
document.getElementById("history-overlay").classList.add("active");
|
||
}
|
||
|
||
function closeHistory() {
|
||
document.getElementById("history-overlay").classList.remove("active");
|
||
}
|