342 lines
16 KiB
HTML
342 lines
16 KiB
HTML
<!doctype html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>Журнал ремонта техники</title>
|
||
<style>
|
||
* { margin: 0; box-sizing: border-box; }
|
||
body { font-family: -apple-system, "Segoe UI", Roboto, sans-serif; background: #0f1218; color: #f4f4f5; min-height: 100vh; padding: 24px; }
|
||
.container { max-width: 1200px; margin: 0 auto; }
|
||
h1 { font-size: 28px; margin-bottom: 20px; color: #fff; }
|
||
|
||
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 12px; margin-bottom: 20px; }
|
||
.stat-card { background: #1a1d24; border-radius: 12px; padding: 16px; text-align: center; border: 1px solid #2a2d35; }
|
||
.stat-card.total { border-color: #00b8cc; }
|
||
.stat-card.new { border-color: #f59e0b; }
|
||
.stat-card.progress { border-color: #3b82f6; }
|
||
.stat-card.done { border-color: #10b981; }
|
||
.stat-num { font-size: 32px; font-weight: bold; color: #fff; }
|
||
.stat-label { font-size: 13px; color: #8a8a92; margin-top: 4px; }
|
||
|
||
.controls { display: flex; gap: 12px; margin-bottom: 20px; flex-wrap: wrap; align-items: center; }
|
||
.search { flex: 1; min-width: 200px; background: #1a1d24; border: 1px solid #2a2d35; border-radius: 8px; padding: 12px 16px; color: #fff; font-size: 14px; }
|
||
.search:focus { outline: none; border-color: #00b8cc; }
|
||
.btn { background: #00b8cc; color: #fff; border: none; border-radius: 8px; padding: 12px 20px; font-size: 14px; cursor: pointer; font-weight: 500; }
|
||
.btn:hover { background: #00d4ec; }
|
||
.btn-secondary { background: #2a2d35; }
|
||
.btn-secondary:hover { background: #3a3d45; }
|
||
.btn-danger { background: #ef4444; }
|
||
.btn-danger:hover { background: #f87171; }
|
||
|
||
.filters { display: flex; gap: 8px; margin-bottom: 20px; flex-wrap: wrap; }
|
||
.chip { background: #1a1d24; border: 1px solid #2a2d35; border-radius: 20px; padding: 8px 16px; font-size: 13px; cursor: pointer; color: #8a8a92; }
|
||
.chip:hover { border-color: #00b8cc; color: #fff; }
|
||
.chip.active { background: #00b8cc; border-color: #00b8cc; color: #fff; }
|
||
|
||
.table-container { background: #1a1d24; border-radius: 12px; border: 1px solid #2a2d35; overflow: hidden; margin-bottom: 20px; }
|
||
table { width: 100%; border-collapse: collapse; }
|
||
th, td { padding: 14px 16px; text-align: left; border-bottom: 1px solid #2a2d35; }
|
||
th { background: #20232b; font-weight: 600; font-size: 13px; color: #8a8a92; text-transform: uppercase; }
|
||
tr:hover { background: #20232b; }
|
||
.status { display: inline-block; padding: 4px 10px; border-radius: 12px; font-size: 12px; font-weight: 500; }
|
||
.status-new { background: rgba(245, 158, 11, 0.2); color: #f59e0b; }
|
||
.status-progress { background: rgba(59, 130, 246, 0.2); color: #3b82f6; }
|
||
.status-done { background: rgba(16, 185, 129, 0.2); color: #10b981; }
|
||
.actions { display: flex; gap: 8px; }
|
||
.btn-sm { padding: 6px 12px; font-size: 12px; }
|
||
|
||
.log-section { margin-top: 30px; }
|
||
.log-section h2 { font-size: 20px; margin-bottom: 16px; color: #fff; }
|
||
.log-container { background: #1a1d24; border-radius: 12px; border: 1px solid #2a2d35; max-height: 300px; overflow-y: auto; }
|
||
.log-entry { padding: 12px 16px; border-bottom: 1px solid #2a2d35; font-size: 13px; }
|
||
.log-entry:last-child { border-bottom: none; }
|
||
.log-when { color: #00b8cc; font-weight: 500; }
|
||
.log-what { color: #f4f4f5; }
|
||
.log-who { color: #8a8a92; }
|
||
|
||
.modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.7); display: none; align-items: center; justify-content: center; z-index: 1000; }
|
||
.modal-overlay.open { display: flex; }
|
||
.modal { background: #1a1d24; border-radius: 12px; border: 1px solid #2a2d35; width: 100%; max-width: 480px; padding: 24px; }
|
||
.modal h2 { font-size: 20px; margin-bottom: 20px; color: #fff; }
|
||
.form-group { margin-bottom: 16px; }
|
||
.form-group label { display: block; font-size: 13px; color: #8a8a92; margin-bottom: 6px; }
|
||
.form-group input, .form-group select, .form-group textarea { width: 100%; background: #0f1218; border: 1px solid #2a2d35; border-radius: 8px; padding: 12px; color: #fff; font-size: 14px; }
|
||
.form-group input:focus, .form-group select:focus, .form-group textarea:focus { outline: none; border-color: #00b8cc; }
|
||
.modal-actions { display: flex; gap: 12px; justify-content: flex-end; margin-top: 24px; }
|
||
|
||
.empty { text-align: center; padding: 40px; color: #8a8a92; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<h1>📋 Журнал ремонта техники</h1>
|
||
|
||
<div class="stats">
|
||
<div class="stat-card total">
|
||
<div class="stat-num" id="stat-total">0</div>
|
||
<div class="stat-label">Всего заявок</div>
|
||
</div>
|
||
<div class="stat-card new">
|
||
<div class="stat-num" id="stat-new">0</div>
|
||
<div class="stat-label">Новая</div>
|
||
</div>
|
||
<div class="stat-card progress">
|
||
<div class="stat-num" id="stat-progress">0</div>
|
||
<div class="stat-label">В работе</div>
|
||
</div>
|
||
<div class="stat-card done">
|
||
<div class="stat-num" id="stat-done">0</div>
|
||
<div class="stat-label">Выполнена</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="controls">
|
||
<input type="text" class="search" id="search" placeholder="Поиск по заявке, технике, заявителю, сроку...">
|
||
<button class="btn" id="btn-add">➕ Добавить заявку</button>
|
||
<button class="btn btn-secondary" id="btn-export">📥 Выгрузить в CSV</button>
|
||
</div>
|
||
|
||
<div class="filters" id="filters">
|
||
<button class="chip active" data-status="">Все</button>
|
||
<button class="chip" data-status="новая">Новая</button>
|
||
<button class="chip" data-status="в работе">В работе</button>
|
||
<button class="chip" data-status="выполнена">Выполнена</button>
|
||
</div>
|
||
|
||
<div class="table-container">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>Что сломалось</th>
|
||
<th>Кто заявил</th>
|
||
<th>Срок</th>
|
||
<th>Статус</th>
|
||
<th>Действия</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="table-body"></tbody>
|
||
</table>
|
||
<div class="empty" id="empty" style="display:none">Нет заявок</div>
|
||
</div>
|
||
|
||
<div class="log-section">
|
||
<h2>📜 Журнал изменений</h2>
|
||
<div class="log-container" id="log-container"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal-overlay" id="modal-overlay">
|
||
<div class="modal">
|
||
<h2 id="modal-title">Новая заявка</h2>
|
||
<form id="form">
|
||
<input type="hidden" id="f-id">
|
||
<div class="form-group">
|
||
<label>Что сломалось</label>
|
||
<textarea id="f-issue" rows="3" required placeholder="Например: ноутбук не включается"></textarea>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Кто заявил</label>
|
||
<input type="text" id="f-person" required placeholder="Фамилия и имя">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Срок выполнения</label>
|
||
<input type="date" id="f-deadline" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Статус</label>
|
||
<select id="f-status">
|
||
<option value="новая">Новая</option>
|
||
<option value="в работе">В работе</option>
|
||
<option value="выполнена">Выполнена</option>
|
||
</select>
|
||
</div>
|
||
<div class="modal-actions">
|
||
<button type="button" class="btn btn-secondary" id="btn-cancel">Отмена</button>
|
||
<button type="submit" class="btn">Сохранить</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
const DEMO_DATA = [
|
||
{ id: 1, issue: "Ноутбук не включается", person: "Айдана Серикова", deadline: "2026-08-12", status: "выполнена" },
|
||
{ id: 2, issue: "Принтер не печатает", person: "Нурлан Касымов", deadline: "2026-08-14", status: "в работе" },
|
||
{ id: 3, issue: "Монитор мерцает", person: "Мадина Омарова", deadline: "2026-08-18", status: "новая" },
|
||
{ id: 4, issue: "Телефон не заряжается", person: "Данияр Токташов", deadline: "2026-08-21", status: "в работе" },
|
||
{ id: 5, issue: "МФУ зажёвывает бумагу", person: "Ержан Асанов", deadline: "2026-08-25", status: "новая" },
|
||
{ id: 6, issue: "Зарядное устройство не работает", person: "Асель Нурбекова", deadline: "2026-08-29", status: "выполнена" }
|
||
];
|
||
|
||
let items = [];
|
||
let log = [];
|
||
let currentFilter = "";
|
||
|
||
function load() {
|
||
try { items = JSON.parse(localStorage.getItem("repair-items")); } catch(_) { items = []; }
|
||
try { log = JSON.parse(localStorage.getItem("repair-log")); } catch(_) { log = []; }
|
||
if (!items || items.length === 0) {
|
||
items = [...DEMO_DATA];
|
||
log.push({ when: new Date().toLocaleString("ru-RU"), what: "Загружены демо-данные (6 заявок)", who: "Система" });
|
||
save();
|
||
}
|
||
}
|
||
|
||
function save() {
|
||
localStorage.setItem("repair-items", JSON.stringify(items));
|
||
localStorage.setItem("repair-log", JSON.stringify(log));
|
||
}
|
||
|
||
function addLog(what, who = "Пользователь") {
|
||
log.unshift({ when: new Date().toLocaleString("ru-RU"), what, who });
|
||
if (log.length > 50) log.pop();
|
||
save();
|
||
}
|
||
|
||
function fmtDate(d) {
|
||
if (!d) return "";
|
||
const [y, m, day] = d.split("-");
|
||
return `${day}.${m}.${y}`;
|
||
}
|
||
|
||
function render() {
|
||
const search = document.getElementById("search").value.toLowerCase();
|
||
const tbody = document.getElementById("table-body");
|
||
const empty = document.getElementById("empty");
|
||
|
||
let filtered = items.filter(i => {
|
||
const matchSearch = !search ||
|
||
i.issue.toLowerCase().includes(search) ||
|
||
i.person.toLowerCase().includes(search) ||
|
||
i.deadline.includes(search) ||
|
||
fmtDate(i.deadline).includes(search);
|
||
const matchFilter = !currentFilter || i.status === currentFilter;
|
||
return matchSearch && matchFilter;
|
||
});
|
||
|
||
tbody.innerHTML = filtered.map(i => `
|
||
<tr>
|
||
<td>${i.issue}</td>
|
||
<td>${i.person}</td>
|
||
<td>${fmtDate(i.deadline)}</td>
|
||
<td><span class="status status-${i.status === 'новая' ? 'new' : i.status === 'в работе' ? 'progress' : 'done'}">${i.status}</span></td>
|
||
<td class="actions">
|
||
<button class="btn btn-sm btn-secondary" onclick="editItem(${i.id})">✏️</button>
|
||
<button class="btn btn-sm btn-danger" onclick="deleteItem(${i.id})">🗑️</button>
|
||
</td>
|
||
</tr>
|
||
`).join("");
|
||
|
||
empty.style.display = filtered.length === 0 ? "block" : "none";
|
||
|
||
document.getElementById("stat-total").textContent = items.length;
|
||
document.getElementById("stat-new").textContent = items.filter(i => i.status === "новая").length;
|
||
document.getElementById("stat-progress").textContent = items.filter(i => i.status === "в работе").length;
|
||
document.getElementById("stat-done").textContent = items.filter(i => i.status === "выполнена").length;
|
||
|
||
const logContainer = document.getElementById("log-container");
|
||
logContainer.innerHTML = log.length === 0
|
||
? '<div class="empty">Журнал пуст</div>'
|
||
: log.map(l => `<div class="log-entry"><span class="log-when">${l.when}</span> — <span class="log-what">${l.what}</span> <span class="log-who">(${l.who})</span></div>`).join("");
|
||
}
|
||
|
||
function openModal(id = null) {
|
||
const overlay = document.getElementById("modal-overlay");
|
||
const title = document.getElementById("modal-title");
|
||
const form = document.getElementById("form");
|
||
|
||
if (id) {
|
||
const item = items.find(i => i.id === id);
|
||
title.textContent = "Редактировать заявку";
|
||
document.getElementById("f-id").value = item.id;
|
||
document.getElementById("f-issue").value = item.issue;
|
||
document.getElementById("f-person").value = item.person;
|
||
document.getElementById("f-deadline").value = item.deadline;
|
||
document.getElementById("f-status").value = item.status;
|
||
} else {
|
||
title.textContent = "Новая заявка";
|
||
form.reset();
|
||
document.getElementById("f-id").value = "";
|
||
document.getElementById("f-deadline").value = new Date().toISOString().split("T")[0];
|
||
}
|
||
|
||
overlay.classList.add("open");
|
||
}
|
||
|
||
function closeModal() {
|
||
document.getElementById("modal-overlay").classList.remove("open");
|
||
}
|
||
|
||
function editItem(id) {
|
||
openModal(id);
|
||
}
|
||
|
||
function deleteItem(id) {
|
||
if (!confirm("Удалить эту заявку?")) return;
|
||
const item = items.find(i => i.id === id);
|
||
items = items.filter(i => i.id !== id);
|
||
addLog(`Удалена заявка: "${item.issue}" (заявитель: ${item.person})`);
|
||
save();
|
||
render();
|
||
}
|
||
|
||
document.getElementById("btn-add").addEventListener("click", () => openModal());
|
||
document.getElementById("btn-cancel").addEventListener("click", closeModal);
|
||
document.getElementById("modal-overlay").addEventListener("click", (e) => {
|
||
if (e.target === e.currentTarget) closeModal();
|
||
});
|
||
|
||
document.getElementById("form").addEventListener("submit", (e) => {
|
||
e.preventDefault();
|
||
const id = document.getElementById("f-id").value;
|
||
const issue = document.getElementById("f-issue").value;
|
||
const person = document.getElementById("f-person").value;
|
||
const deadline = document.getElementById("f-deadline").value;
|
||
const status = document.getElementById("f-status").value;
|
||
|
||
if (id) {
|
||
const idx = items.findIndex(i => i.id == id);
|
||
const old = items[idx];
|
||
items[idx] = { ...old, issue, person, deadline, status };
|
||
addLog(`Изменена заявка: "${old.issue}" → "${issue}", статус: ${old.status} → ${status}`);
|
||
} else {
|
||
const newItem = { id: Date.now(), issue, person, deadline, status };
|
||
items.push(newItem);
|
||
addLog(`Создана заявка: "${issue}" от ${person}, срок: ${fmtDate(deadline)}, статус: ${status}`);
|
||
}
|
||
|
||
save();
|
||
closeModal();
|
||
render();
|
||
});
|
||
|
||
document.getElementById("search").addEventListener("input", render);
|
||
|
||
document.getElementById("filters").addEventListener("click", (e) => {
|
||
if (e.target.classList.contains("chip")) {
|
||
document.querySelectorAll(".chip").forEach(c => c.classList.remove("active"));
|
||
e.target.classList.add("active");
|
||
currentFilter = e.target.dataset.status;
|
||
render();
|
||
}
|
||
});
|
||
|
||
document.getElementById("btn-export").addEventListener("click", () => {
|
||
const head = ["Что сломалось", "Кто заявил", "Срок", "Статус"];
|
||
const body = items.map(i => [i.issue, i.person, fmtDate(i.deadline), i.status]
|
||
.map(v => '"' + String(v ?? "").replace(/"/g, '""') + '"').join(";"));
|
||
const csv = "\uFEFF" + [head.join(";"), ...body].join("\n");
|
||
const a = document.createElement("a");
|
||
a.href = URL.createObjectURL(new Blob([csv], { type: "text/csv" }));
|
||
a.download = "zhurnal-remonta.csv";
|
||
a.click();
|
||
addLog("Выгружен список заявок в CSV");
|
||
});
|
||
|
||
load();
|
||
render();
|
||
</script>
|
||
</body>
|
||
</html>
|