povrezhdenie-po-stpk-avtomat/index.html

276 lines
12 KiB
HTML
Raw 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: #f5f5f7; color: #1d1d1f; padding: 20px; }
.container { max-width: 1200px; margin: 0 auto; }
h1 { font-size: 28px; margin-bottom: 20px; color: #1d1d1f; }
.stats { display: flex; gap: 16px; margin-bottom: 24px; flex-wrap: wrap; }
.stat { background: white; padding: 16px 24px; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
.stat-value { font-size: 32px; font-weight: 700; color: #0071e3; }
.stat-label { font-size: 14px; color: #86868b; margin-top: 4px; }
.filters { display: flex; gap: 8px; margin-bottom: 24px; flex-wrap: wrap; align-items: center; }
.filter-btn { padding: 8px 16px; border: 1px solid #d2d2d7; background: white; border-radius: 20px; cursor: pointer; font-size: 14px; }
.filter-btn.active { background: #0071e3; color: white; border-color: #0071e3; }
.search { flex: 1; min-width: 200px; padding: 8px 16px; border: 1px solid #d2d2d7; border-radius: 8px; font-size: 14px; }
.form-card { background: white; padding: 24px; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-bottom: 24px; }
.form-row { display: flex; gap: 16px; margin-bottom: 16px; flex-wrap: wrap; }
.form-group { flex: 1; min-width: 200px; }
label { display: block; font-size: 14px; color: #86868b; margin-bottom: 6px; }
input, select, textarea { width: 100%; padding: 10px 14px; border: 1px solid #d2d2d7; border-radius: 8px; font-size: 14px; }
textarea { resize: vertical; min-height: 80px; }
.btn { padding: 10px 24px; background: #0071e3; color: white; border: none; border-radius: 8px; cursor: pointer; font-size: 14px; font-weight: 500; }
.btn:hover { background: #005bb5; }
.btn-secondary { background: #86868b; }
.btn-danger { background: #e30000; }
.table-card { background: white; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); overflow: hidden; }
table { width: 100%; border-collapse: collapse; }
th, td { padding: 12px 16px; text-align: left; border-bottom: 1px solid #e5e5e5; }
th { background: #f5f5f7; font-weight: 600; font-size: 13px; color: #86868b; }
tr:hover { background: #f9f9f9; }
.status { padding: 4px 12px; border-radius: 12px; font-size: 12px; font-weight: 500; }
.status-новая { background: #e3f2fd; color: #1976d2; }
.status-в-работе { background: #fff3e0; color: #f57c00; }
.status-выполнено { background: #e8f5e9; color: #388e3c; }
.status-закрыто { background: #f5f5f5; color: #86868b; }
.actions { display: flex; gap: 8px; }
.btn-sm { padding: 6px 12px; font-size: 12px; }
.export-btn { margin-bottom: 16px; }
@media (max-width: 768px) {
.form-row { flex-direction: column; }
.stats { flex-direction: column; }
table { font-size: 13px; }
th, td { padding: 8px; }
}
</style>
</head>
<body>
<div class="container">
<h1>📋 Заявки на повреждение СТПК</h1>
<div class="stats">
<div class="stat">
<div class="stat-value" id="stat-total">0</div>
<div class="stat-label">Всего заявок</div>
</div>
<div class="stat">
<div class="stat-value" id="stat-new">0</div>
<div class="stat-label">Новые</div>
</div>
<div class="stat">
<div class="stat-value" id="stat-work">0</div>
<div class="stat-label">В работе</div>
</div>
<div class="stat">
<div class="stat-value" id="stat-done">0</div>
<div class="stat-label">Выполнено</div>
</div>
</div>
<div class="form-card">
<h2 style="margin-bottom: 16px; font-size: 18px;">Новая заявка</h2>
<form id="form">
<div class="form-row">
<div class="form-group">
<label>Объект</label>
<input type="text" name="object" placeholder="Например: СТПК-1" required>
</div>
<div class="form-group">
<label>Тип повреждения</label>
<select name="damageType" required>
<option value="">Выберите тип</option>
<option value="механическое">Механическое</option>
<option value="электрическое">Электрическое</option>
<option value="коррозия">Коррозия</option>
<option value="другое">Другое</option>
</select>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>Заявитель</label>
<input type="text" name="person" placeholder="ФИО" required>
</div>
<div class="form-group">
<label>Телефон</label>
<input type="tel" name="phone" placeholder="+7 (___) ___-__-__" required>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>Описание повреждения</label>
<textarea name="description" placeholder="Опишите характер повреждения..." required></textarea>
</div>
</div>
<button type="submit" class="btn">Подать заявку</button>
</form>
</div>
<div class="filters">
<button class="filter-btn active" data-status="all">Все</button>
<button class="filter-btn" data-status="новая">Новые</button>
<button class="filter-btn" data-status="в-работе">В работе</button>
<button class="filter-btn" data-status="выполнено">Выполнено</button>
<button class="filter-btn" data-status="закрыто">Закрыто</button>
<input type="text" class="search" id="search" placeholder="Поиск по объекту или заявителю...">
<button class="btn export-btn" onclick="exportCsv()">📥 Выгрузить в Excel</button>
</div>
<div class="table-card">
<table>
<thead>
<tr>
<th>№</th>
<th>Объект</th>
<th>Тип</th>
<th>Заявитель</th>
<th>Телефон</th>
<th>Описание</th>
<th>Статус</th>
<th>Дата</th>
<th>Действия</th>
</tr>
</thead>
<tbody id="table-body"></tbody>
</table>
</div>
</div>
<script>
let items = [];
let currentFilter = "all";
let editId = null;
async function load() {
const res = await fetch("/api/items");
items = await res.json();
render();
}
function render() {
const search = document.getElementById("search").value.toLowerCase();
const filtered = items.filter(item => {
const matchStatus = currentFilter === "all" || item.status === currentFilter;
const matchSearch = !search ||
(item.object && item.object.toLowerCase().includes(search)) ||
(item.person && item.person.toLowerCase().includes(search));
return matchStatus && matchSearch;
});
document.getElementById("table-body").innerHTML = filtered.map(item => {
const date = new Date(item.createdAt).toLocaleDateString("ru-RU");
return `<tr>
<td>${item.id}</td>
<td>${item.object || ""}</td>
<td>${item.damageType || ""}</td>
<td>${item.person || ""}</td>
<td>${item.phone || ""}</td>
<td>${item.description || ""}</td>
<td><span class="status status-${(item.status||'новая').replace(' ','-')}">${item.status || "новая"}</span></td>
<td>${date}</td>
<td class="actions">
<button class="btn btn-sm btn-secondary" onclick="editItem(${item.id})">Изменить</button>
<button class="btn btn-sm btn-danger" onclick="deleteItem(${item.id})">Удалить</button>
</td>
</tr>`;
}).join("");
// Статистика
document.getElementById("stat-total").textContent = items.length;
document.getElementById("stat-new").textContent = items.filter(i => i.status === "новая").length;
document.getElementById("stat-work").textContent = items.filter(i => i.status === "в работе").length;
document.getElementById("stat-done").textContent = items.filter(i => i.status === "выполнено").length;
}
document.getElementById("form").addEventListener("submit", async (e) => {
e.preventDefault();
const form = e.target;
const data = {
object: form.object.value,
damageType: form.damageType.value,
person: form.person.value,
phone: form.phone.value,
description: form.description.value
};
if (editId) {
await fetch("/api/items/" + editId, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data)
});
editId = null;
form.querySelector("button[type=submit]").textContent = "Подать заявку";
} else {
await fetch("/api/items", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data)
});
}
form.reset();
load();
});
async function editItem(id) {
const item = items.find(i => i.id === id);
if (!item) return;
const form = document.getElementById("form");
form.object.value = item.object || "";
form.damageType.value = item.damageType || "";
form.person.value = item.person || "";
form.phone.value = item.phone || "";
form.description.value = item.description || "";
editId = id;
form.querySelector("button[type=submit]").textContent = "Сохранить изменения";
window.scrollTo({ top: 0, behavior: "smooth" });
}
async function deleteItem(id) {
if (!confirm("Вы уверены, что хотите удалить заявку №" + id + "?")) return;
await fetch("/api/items/" + id, { method: "DELETE" });
load();
}
document.querySelectorAll(".filter-btn").forEach(btn => {
btn.addEventListener("click", () => {
document.querySelectorAll(".filter-btn").forEach(b => b.classList.remove("active"));
btn.classList.add("active");
currentFilter = btn.dataset.status;
render();
});
});
document.getElementById("search").addEventListener("input", render);
function exportCsv() {
const head = ["№", "Объект", "Тип повреждения", "Заявитель", "Телефон", "Описание", "Статус", "Дата"];
const body = items.map(r => [
r.id, r.object, r.damageType, r.person, r.phone, r.description, r.status,
new Date(r.createdAt).toLocaleDateString("ru-RU")
].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 = "zayavki_stpk.csv";
a.click();
}
load();
</script>
</body>
</html>