276 lines
12 KiB
HTML
276 lines
12 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: #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>
|