zhurnal-remonta-tehniki/index.html

342 lines
16 KiB
HTML
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.

<!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>