zhurnal-remonta-oborudovaniy/index.html

434 lines
20 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>
:root {
--bg: #f5f7fa;
--card: #ffffff;
--text: #1f2937;
--text-secondary: #6b7280;
--primary: #2563eb;
--primary-hover: #1d4ed8;
--success: #10b981;
--warning: #f59e0b;
--danger: #ef4444;
--info: #3b82f6;
--border: #e5e7eb;
--shadow: 0 1px 3px rgba(0,0,0,0.1), 0 1px 2px rgba(0,0,0,0.06);
--shadow-lg: 0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -2px rgba(0,0,0,0.05);
}
* { margin: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: var(--bg); color: var(--text); line-height: 1.5; }
.container { max-width: 1400px; margin: 0 auto; padding: 24px; }
/* Header */
.header { background: var(--card); border-radius: 12px; padding: 24px; margin-bottom: 24px; box-shadow: var(--shadow); }
.header h1 { font-size: 28px; font-weight: 700; margin-bottom: 8px; color: var(--text); }
.header p { color: var(--text-secondary); font-size: 14px; }
/* Stats */
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 16px; margin-bottom: 24px; }
.stat-card { background: var(--card); border-radius: 10px; padding: 20px; box-shadow: var(--shadow); text-align: center; }
.stat-value { font-size: 32px; font-weight: 700; color: var(--primary); }
.stat-label { font-size: 13px; color: var(--text-secondary); margin-top: 4px; text-transform: uppercase; letter-spacing: 0.5px; }
.stat-card.success .stat-value { color: var(--success); }
.stat-card.warning .stat-value { color: var(--warning); }
.stat-card.danger .stat-value { color: var(--danger); }
.stat-card.info .stat-value { color: var(--info); }
/* Controls */
.controls { background: var(--card); border-radius: 12px; padding: 20px; margin-bottom: 24px; box-shadow: var(--shadow); display: flex; flex-wrap: wrap; gap: 16px; align-items: center; justify-content: space-between; }
.search-box { flex: 1; min-width: 280px; position: relative; }
.search-box input { width: 100%; padding: 12px 16px 12px 44px; border: 1px solid var(--border); border-radius: 8px; font-size: 14px; transition: all 0.2s; }
.search-box input:focus { outline: none; border-color: var(--primary); box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1); }
.search-box::before { content: "🔍"; position: absolute; left: 14px; top: 50%; transform: translateY(-50%); font-size: 16px; }
.chips { display: flex; gap: 8px; flex-wrap: wrap; }
.chip { padding: 8px 16px; border-radius: 20px; font-size: 13px; font-weight: 500; cursor: pointer; border: 2px solid var(--border); background: var(--card); transition: all 0.2s; }
.chip:hover { border-color: var(--primary); }
.chip.active { background: var(--primary); color: white; border-color: var(--primary); }
.btn { padding: 12px 24px; border-radius: 8px; font-size: 14px; font-weight: 600; cursor: pointer; border: none; transition: all 0.2s; display: inline-flex; align-items: center; gap: 8px; }
.btn-primary { background: var(--primary); color: white; }
.btn-primary:hover { background: var(--primary-hover); transform: translateY(-1px); box-shadow: var(--shadow-lg); }
.btn-secondary { background: var(--card); color: var(--text); border: 1px solid var(--border); }
.btn-secondary:hover { background: var(--bg); }
.btn-danger { background: var(--danger); color: white; }
.btn-danger:hover { background: #dc2626; }
.btn-sm { padding: 6px 12px; font-size: 13px; }
/* Table */
.table-container { background: var(--card); border-radius: 12px; overflow: hidden; box-shadow: var(--shadow); margin-bottom: 24px; }
table { width: 100%; border-collapse: collapse; }
th, td { padding: 14px 16px; text-align: left; border-bottom: 1px solid var(--border); }
th { background: var(--bg); font-weight: 600; font-size: 13px; text-transform: uppercase; letter-spacing: 0.5px; color: var(--text-secondary); }
tr:hover { background: #f9fafb; }
.status-badge { padding: 4px 12px; border-radius: 12px; font-size: 12px; font-weight: 600; text-transform: uppercase; }
.status-new { background: #dbeafe; color: #1e40af; }
.status-work { background: #fef3c7; color: #92400e; }
.status-done { background: #d1fae5; color: #065f46; }
.status-overdue { background: #fee2e2; color: #991b1b; }
.overdue-row { background: #fef2f2 !important; }
.overdue-row:hover { background: #fee2e2 !important; }
.actions { display: flex; gap: 8px; }
/* Log */
.log-container { background: var(--card); border-radius: 12px; padding: 20px; box-shadow: var(--shadow); }
.log-container h2 { font-size: 18px; margin-bottom: 16px; }
.log-list { max-height: 200px; overflow-y: auto; }
.log-item { padding: 8px 0; border-bottom: 1px solid var(--border); font-size: 13px; }
.log-item:last-child { border-bottom: none; }
.log-time { color: var(--text-secondary); margin-right: 8px; }
/* Modal */
.modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); display: none; align-items: center; justify-content: center; z-index: 1000; }
.modal-overlay.open { display: flex; }
.modal { background: var(--card); border-radius: 12px; padding: 32px; width: 100%; max-width: 560px; max-height: 90vh; overflow-y: auto; box-shadow: var(--shadow-lg); }
.modal h2 { font-size: 22px; margin-bottom: 24px; }
.form-group { margin-bottom: 20px; }
.form-group label { display: block; font-size: 14px; font-weight: 500; margin-bottom: 8px; color: var(--text); }
.form-group input, .form-group select, .form-group textarea { width: 100%; padding: 12px; border: 1px solid var(--border); border-radius: 8px; font-size: 14px; transition: all 0.2s; }
.form-group input:focus, .form-group select:focus, .form-group textarea:focus { outline: none; border-color: var(--primary); box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1); }
.form-actions { display: flex; gap: 12px; justify-content: flex-end; margin-top: 24px; }
/* Empty state */
.empty { text-align: center; padding: 60px 20px; color: var(--text-secondary); }
.empty-icon { font-size: 48px; margin-bottom: 16px; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>📋 Журнал ремонта оборудования</h1>
<p>Учёт заявок на ремонт: от создания до выполнения</p>
</div>
<div class="stats" id="stats"></div>
<div class="controls">
<div class="search-box">
<input type="text" id="search" placeholder="Поиск по таблице...">
</div>
<div class="chips" id="chips"></div>
<button class="btn btn-primary" onclick="openModal()">+ Добавить заявку</button>
</div>
<div class="table-container">
<table>
<thead>
<tr>
<th>№</th>
<th>Что сломалось</th>
<th>Где стоит</th>
<th>Кто заявил</th>
<th>Кто чинит</th>
<th>Срок</th>
<th>Статус</th>
<th>Действия</th>
</tr>
</thead>
<tbody id="tableBody"></tbody>
</table>
<div class="empty" id="empty" style="display:none;">
<div class="empty-icon">📭</div>
<p>Нет заявок. Создайте первую!</p>
</div>
</div>
<div class="log-container">
<h2>📜 Журнал изменений</h2>
<div class="log-list" id="logList"></div>
</div>
</div>
<!-- Modal -->
<div class="modal-overlay" id="modalOverlay">
<div class="modal">
<h2 id="modalTitle">Добавить заявку</h2>
<form id="form">
<input type="hidden" id="editId">
<div class="form-group">
<label>Что сломалось *</label>
<input type="text" id="issue" required placeholder="Например: Принтер не печатает">
</div>
<div class="form-group">
<label>Где стоит *</label>
<input type="text" id="location" required placeholder="Например: Офис Алматы, 2 этаж">
</div>
<div class="form-group">
<label>Кто заявил *</label>
<input type="text" id="requester" required placeholder="ФИО заявителя">
</div>
<div class="form-group">
<label>Кто чинит *</label>
<input type="text" id="assignee" required placeholder="ФИО исполнителя">
</div>
<div class="form-group">
<label>Срок *</label>
<input type="text" id="deadline" required placeholder="ДД.ММ.ГГГГ">
</div>
<div class="form-group">
<label>Статус *</label>
<select id="status" required>
<option value="new">Новая</option>
<option value="work">В работе</option>
<option value="done">Выполнена</option>
<option value="overdue">Просрочена</option>
</select>
</div>
<div class="form-actions">
<button type="button" class="btn btn-secondary" onclick="closeModal()">Отмена</button>
<button type="submit" class="btn btn-primary">Сохранить</button>
</div>
</form>
</div>
</div>
<script>
// Demo data
const DEMO = [
{ id: 1, issue: "Принтер не печатает", location: "Офис Алматы, 2 этаж", requester: "Айдос К.", assignee: "Ерлан М.", deadline: formatDate(addDays(5)), status: "new" },
{ id: 2, issue: "Замена блока питания", location: "Склад Астана", requester: "Гульнар С.", assignee: "Серик Б.", deadline: formatDate(addDays(3)), status: "new" },
{ id: 3, issue: "Не работает моноблок", location: "Офис Шымкент", requester: "Мадина Т.", assignee: "Ерлан М.", deadline: formatDate(addDays(2)), status: "work" },
{ id: 4, issue: "Шумит кондиционер", location: "Караганда, офис 3", requester: "Нургуль А.", assignee: "Айдос К.", deadline: formatDate(addDays(7)), status: "work" },
{ id: 5, issue: "Роутер не включается", location: "Тараз, филиал", requester: "Серик Б.", assignee: "Гульнар С.", deadline: formatDate(-3), status: "overdue" },
{ id: 6, issue: "Замена картриджа", location: "Алматы, главный офис", requester: "Айдос К.", assignee: "Серик Б.", deadline: formatDate(-1), status: "done" },
{ id: 7, issue: "Настройка сканера", location: "Астана, отделение 2", requester: "Мадина Т.", assignee: "Ерлан М.", deadline: formatDate(1), status: "done" }
];
let items = load("items") || DEMO;
let log = load("log") || [];
let filterStatus = "all";
let searchTerm = "";
function load(key) { try { return JSON.parse(localStorage.getItem(key)); } catch { return null; } }
function save(key, val) { localStorage.setItem(key, JSON.stringify(val)); }
function addDays(d) { const dt = new Date(); dt.setDate(dt.getDate() + d); return dt; }
function formatDate(dt) { const d = typeof dt === "number" ? addDays(dt) : new Date(dt); return d.toLocaleDateString("ru-RU", { day: "2-digit", month: "2-digit", year: "numeric" }); }
function parseDate(s) { const [d, m, y] = s.split(".").map(Number); return new Date(y, m - 1, d); }
function isOverdue(s) { return parseDate(s) < new Date(new Date().toDateString()); }
function uid() { return Date.now(); }
function addLog(action, item) {
log.unshift({ time: new Date().toLocaleString("ru-RU"), action, item: item.issue, who: "Текущий пользователь" });
save("log", log);
renderLog();
}
function getStatusLabel(s) {
return { new: "Новая", work: "В работе", done: "Выполнена", overdue: "Просрочена" }[s];
}
function getStatusClass(s) {
return { new: "status-new", work: "status-work", done: "status-done", overdue: "status-overdue" }[s];
}
function getStats() {
const stats = { total: items.length, new: 0, work: 0, done: 0, overdue: 0 };
items.forEach(i => {
if (i.status === "overdue" || (i.status !== "done" && isOverdue(i.deadline))) stats.overdue++;
else stats[i.status]++;
});
return stats;
}
function renderStats() {
const s = getStats();
document.getElementById("stats").innerHTML = `
<div class="stat-card"><div class="stat-value">${s.total}</div><div class="stat-label">Всего</div></div>
<div class="stat-card info"><div class="stat-value">${s.new}</div><div class="stat-label">Новые</div></div>
<div class="stat-card warning"><div class="stat-value">${s.work}</div><div class="stat-label">В работе</div></div>
<div class="stat-card success"><div class="stat-value">${s.done}</div><div class="stat-label">Выполнены</div></div>
<div class="stat-card danger"><div class="stat-value">${s.overdue}</div><div class="stat-label">Просрочены</div></div>
`;
}
function renderChips() {
const chips = [
{ id: "all", label: "Все" },
{ id: "new", label: "Новые" },
{ id: "work", label: "В работе" },
{ id: "done", label: "Выполнены" },
{ id: "overdue", label: "Просрочены" }
];
document.getElementById("chips").innerHTML = chips.map(c =>
`<span class="chip${filterStatus === c.id ? " active" : ""}" onclick="setFilter('${c.id}')">${c.label}</span>`
).join("");
}
function setFilter(f) { filterStatus = f; render(); }
function getFiltered() {
let res = items;
if (filterStatus !== "all") {
res = res.filter(i => filterStatus === "overdue"
? (i.status === "overdue" || (i.status !== "done" && isOverdue(i.deadline)))
: i.status === filterStatus);
}
if (searchTerm) {
const s = searchTerm.toLowerCase();
res = res.filter(i =>
i.issue.toLowerCase().includes(s) ||
i.location.toLowerCase().includes(s) ||
i.requester.toLowerCase().includes(s) ||
i.assignee.toLowerCase().includes(s) ||
i.deadline.includes(s) ||
getStatusLabel(i.status).toLowerCase().includes(s)
);
}
return res;
}
function renderTable() {
const filtered = getFiltered();
const tbody = document.getElementById("tableBody");
const empty = document.getElementById("empty");
if (filtered.length === 0) {
tbody.innerHTML = "";
empty.style.display = "block";
return;
}
empty.style.display = "none";
tbody.innerHTML = filtered.map((i, idx) => {
const overdue = i.status === "overdue" || (i.status !== "done" && isOverdue(i.deadline));
return `
<tr class="${overdue ? "overdue-row" : ""}">
<td>${idx + 1}</td>
<td>${i.issue}</td>
<td>${i.location}</td>
<td>${i.requester}</td>
<td>${i.assignee}</td>
<td>${i.deadline}</td>
<td><span class="status-badge ${getStatusClass(i.status)}">${getStatusLabel(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("");
}
function renderLog() {
document.getElementById("logList").innerHTML = log.map(l =>
`<div class="log-item"><span class="log-time">${l.time}</span>${l.action}: <b>${l.item}</b> (${l.who})</div>`
).join("");
}
function render() {
renderStats();
renderChips();
renderTable();
}
function openModal(id = null) {
const overlay = document.getElementById("modalOverlay");
const title = document.getElementById("modalTitle");
const form = document.getElementById("form");
form.reset();
document.getElementById("editId").value = "";
if (id) {
const item = items.find(i => i.id === id);
if (item) {
title.textContent = "Редактировать заявку";
document.getElementById("editId").value = item.id;
document.getElementById("issue").value = item.issue;
document.getElementById("location").value = item.location;
document.getElementById("requester").value = item.requester;
document.getElementById("assignee").value = item.assignee;
document.getElementById("deadline").value = item.deadline;
document.getElementById("status").value = item.status;
}
} else {
title.textContent = "Добавить заявку";
}
overlay.classList.add("open");
}
function closeModal() {
document.getElementById("modalOverlay").classList.remove("open");
}
function editItem(id) {
openModal(id);
}
function deleteItem(id) {
const item = items.find(i => i.id === id);
if (!item) return;
if (confirm(`Удалить заявку "${item.issue}"?`)) {
addLog("Удалена", item);
items = items.filter(i => i.id !== id);
save("items", items);
render();
}
}
function exportCSV() {
const head = ["№", "Что сломалось", "Где стоит", "Кто заявил", "Кто чинит", "Срок", "Статус"];
const rows = getFiltered().map((i, idx) => [
idx + 1, i.issue, i.location, i.requester, i.assignee, i.deadline, getStatusLabel(i.status)
]);
const csv = "\uFEFF" + [head, ...rows].map(r => r.map(v => '"' + String(v ?? "").replace(/"/g, '""') + '"').join(";")).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", { issue: "Экспорт" });
}
// Event listeners
document.getElementById("form").addEventListener("submit", (e) => {
e.preventDefault();
const id = document.getElementById("editId").value;
const data = {
issue: document.getElementById("issue").value,
location: document.getElementById("location").value,
requester: document.getElementById("requester").value,
assignee: document.getElementById("assignee").value,
deadline: document.getElementById("deadline").value,
status: document.getElementById("status").value
};
if (id) {
const idx = items.findIndex(i => i.id == id);
if (idx !== -1) {
const old = items[idx];
items[idx] = { ...old, ...data };
addLog("Изменена", { issue: data.issue });
}
} else {
const newItem = { id: uid(), ...data };
items.push(newItem);
addLog("Создана", { issue: data.issue });
}
save("items", items);
closeModal();
render();
});
document.getElementById("search").addEventListener("input", (e) => {
searchTerm = e.target.value;
renderTable();
});
// Add export button
document.querySelector(".controls").insertAdjacentHTML("beforeend",
`<button class="btn btn-secondary" onclick="exportCSV()">📥 Выгрузить в CSV</button>`
);
// Init
render();
renderLog();
</script>
</body>
</html>