310 lines
12 KiB
HTML
310 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>
|
||
:root{--ink:#0F1218;--cyan:#00E5FF;--cyan-50:#E8FCFF;--white:#fff;--gray-500:#5B6573;--gray-100:#F2F4F7;--success:#10B981;--warning:#F59E0B;--danger:#EF4444}
|
||
*{box-sizing:border-box;margin:0;padding:0}
|
||
body{font:14px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI",Inter,system-ui,sans-serif;color:var(--ink);background:#f8f9fa}
|
||
.container{max-width:1600px;margin:0 auto;padding:24px}
|
||
.header{background:var(--ink);color:var(--white);padding:20px 24px;margin-bottom:24px;border-radius:12px}
|
||
.header h1{font-size:24px;font-weight:700}
|
||
.stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:16px;margin-bottom:24px}
|
||
.stat-card{background:var(--white);border-radius:12px;padding:20px;box-shadow:0 2px 8px rgba(0,0,0,0.08)}
|
||
.stat-label{font-size:13px;color:var(--gray-500);margin-bottom:8px;text-transform:uppercase}
|
||
.stat-value{font-size:28px;font-weight:700;color:var(--ink)}
|
||
.stat-card.primary .stat-value{color:var(--cyan)}
|
||
.stat-card.success .stat-value{color:var(--success)}
|
||
.stat-card.warning .stat-value{color:var(--warning)}
|
||
.stat-card.danger .stat-value{color:var(--danger)}
|
||
.main-grid{display:grid;grid-template-columns:1fr 450px;gap:24px}
|
||
.panel{background:var(--white);border-radius:12px;padding:24px;box-shadow:0 2px 8px rgba(0,0,0,0.08)}
|
||
.panel h2{font-size:18px;font-weight:700;margin-bottom:20px;display:flex;align-items:center;gap:10px}
|
||
.table-container{overflow-x:auto;max-height:600px;overflow-y:auto}
|
||
.table{width:100%;border-collapse:collapse;font-size:13px}
|
||
.table th,.table td{padding:10px 8px;text-align:left;border-bottom:1px solid var(--gray-100);white-space:nowrap}
|
||
.table th{font-size:12px;font-weight:600;color:var(--gray-500);text-transform:uppercase;position:sticky;top:0;background:var(--white)}
|
||
.table tr:hover{background:var(--cyan-50)}
|
||
.table td:nth-child(2){max-width:300px;white-space:normal}
|
||
.badge{display:inline-block;padding:4px 10px;border-radius:20px;font-size:11px;font-weight:600}
|
||
.badge-в_работе{background:#FEF3C7;color:#92400E}
|
||
.badge-выполнен{background:#DCFCE7;color:#166534}
|
||
.badge-ОТКАЗ{background:#FEE2E2;color:#991B1B}
|
||
.form-group{margin-bottom:14px}
|
||
.form-group label{display:block;font-size:13px;font-weight:600;margin-bottom:6px;color:var(--gray-500)}
|
||
.form-group input,.form-group select,.form-group textarea{width:100%;padding:10px;border:1px solid var(--gray-100);border-radius:8px;font-size:14px;font-family:inherit}
|
||
.form-group input:focus,.form-group select:focus,.form-group textarea:focus{outline:none;border-color:var(--cyan)}
|
||
.form-group textarea{min-height:80px;resize:vertical}
|
||
.btn{display:inline-block;background:var(--cyan);color:var(--ink);padding:12px 20px;border-radius:8px;font-weight:700;text-decoration:none;cursor:pointer;border:none;font-size:14px;width:100%}
|
||
.btn:hover{opacity:0.9}
|
||
.btn-sm{padding:5px 10px;font-size:12px;width:auto}
|
||
.btn-danger{background:var(--danger);color:var(--white)}
|
||
.btn-edit{background:var(--warning);color:var(--ink)}
|
||
.actions{display:flex;gap:6px}
|
||
.empty{text-align:center;padding:40px;color:var(--gray-500)}
|
||
.filter-row{display:flex;gap:12px;margin-bottom:16px;flex-wrap:wrap}
|
||
.filter-row input,.filter-row select{padding:8px 12px;border:1px solid var(--gray-100);border-radius:6px;font-size:13px}
|
||
.search-input{flex:1;min-width:200px}
|
||
@media(max-width:1100px){.main-grid{grid-template-columns:1fr}}
|
||
@media(max-width:640px){.stats{grid-template-columns:1fr}.container{padding:16px}}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<div class="container">
|
||
<div class="header">
|
||
<h1>📊 Дашборд заказов - Костанай/Петропавловск</h1>
|
||
</div>
|
||
|
||
<div class="stats">
|
||
<div class="stat-card primary">
|
||
<div class="stat-label">Всего заказов</div>
|
||
<div class="stat-value" id="stat-total">0</div>
|
||
</div>
|
||
<div class="stat-card warning">
|
||
<div class="stat-label">В работе</div>
|
||
<div class="stat-value" id="stat-active">0</div>
|
||
</div>
|
||
<div class="stat-card success">
|
||
<div class="stat-label">Выполнено</div>
|
||
<div class="stat-value" id="stat-completed">0</div>
|
||
</div>
|
||
<div class="stat-card danger">
|
||
<div class="stat-label">Отказы</div>
|
||
<div class="stat-value" id="stat-rejected">0</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="main-grid">
|
||
<div class="panel">
|
||
<h2>📋 Все заказы</h2>
|
||
<div class="filter-row">
|
||
<input type="text" class="search-input" id="search" placeholder="Поиск по адресу или № заказа...">
|
||
<select id="filter-channel">
|
||
<option value="">Все каналы</option>
|
||
<option value="ЦАП">ЦАП</option>
|
||
<option value="УП">УП</option>
|
||
<option value="Дилер">Дилер</option>
|
||
<option value="ПСС">ПСС</option>
|
||
<option value="OAPI">OAPI</option>
|
||
<option value="Онлайн">Онлайн</option>
|
||
</select>
|
||
<select id="filter-status">
|
||
<option value="">Все статусы</option>
|
||
<option value="в работе">В работе</option>
|
||
<option value="выполнен">Выполнен</option>
|
||
<option value="ОТКАЗ">ОТКАЗ</option>
|
||
</select>
|
||
</div>
|
||
<div class="table-container">
|
||
<table class="table">
|
||
<thead>
|
||
<tr><th>№ заказа</th><th>Адрес</th><th>Канал</th><th>Оператор</th><th>Статус</th><th>Примечание</th><th>Действия</th></tr>
|
||
</thead>
|
||
<tbody id="table-body"></tbody>
|
||
</table>
|
||
</div>
|
||
<div id="empty" class="empty" style="display:none">Нет записей</div>
|
||
</div>
|
||
|
||
<div class="panel">
|
||
<h2 id="form-title">➕ Добавить заказ</h2>
|
||
<form id="form">
|
||
<div class="form-group">
|
||
<label>№ заказа *</label>
|
||
<input type="text" name="id" placeholder="86844268" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Адрес *</label>
|
||
<input type="text" name="address" placeholder="г.Костанай, ул...." required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Канал продаж</label>
|
||
<select name="channel">
|
||
<option value="">Выбрать...</option>
|
||
<option value="ЦАП">ЦАП</option>
|
||
<option value="УП">УП</option>
|
||
<option value="Дилер">Дилер</option>
|
||
<option value="ПСС">ПСС</option>
|
||
<option value="OAPI">OAPI</option>
|
||
<option value="Онлайн">Онлайн</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Оператор</label>
|
||
<input type="text" name="operator" placeholder="ФИО оператора">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Статус *</label>
|
||
<select name="status" required>
|
||
<option value="в работе">В работе</option>
|
||
<option value="выполнен">Выполнен</option>
|
||
<option value="ОТКАЗ">ОТКАЗ</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Примечание</label>
|
||
<textarea name="note" placeholder="Комментарий к заказу"></textarea>
|
||
</div>
|
||
<button type="submit" class="btn" id="submit-btn">Добавить</button>
|
||
<button type="button" class="btn btn-danger" id="cancel-btn" style="display:none;margin-top:8px">Отмена</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
let editingId = null;
|
||
let allOrders = [];
|
||
|
||
function getStatusClass(status) {
|
||
if (status === 'в работе') return 'badge-в_работе';
|
||
if (status === 'выполнен') return 'badge-выполнен';
|
||
if (status === 'ОТКАЗ') return 'badge-ОТКАЗ';
|
||
return '';
|
||
}
|
||
|
||
async function load() {
|
||
const res = await fetch("/api/data");
|
||
const data = await res.json();
|
||
allOrders = data.orders || [];
|
||
renderTable(allOrders);
|
||
updateStats(allOrders);
|
||
}
|
||
|
||
function updateStats(orders) {
|
||
document.getElementById("stat-total").textContent = orders.length;
|
||
document.getElementById("stat-active").textContent = orders.filter(o => o.status === "в работе").length;
|
||
document.getElementById("stat-completed").textContent = orders.filter(o => o.status === "выполнен").length;
|
||
document.getElementById("stat-rejected").textContent = orders.filter(o => o.status === "ОТКАЗ").length;
|
||
}
|
||
|
||
function renderTable(orders) {
|
||
const tbody = document.getElementById("table-body");
|
||
const empty = document.getElementById("empty");
|
||
|
||
if (orders.length === 0) {
|
||
tbody.innerHTML = "";
|
||
empty.style.display = "block";
|
||
} else {
|
||
empty.style.display = "none";
|
||
tbody.innerHTML = orders.map(order => `
|
||
<tr>
|
||
<td><strong>${order.id}</strong></td>
|
||
<td title="${order.address}">${order.address.substring(0, 50)}${order.address.length > 50 ? '...' : ''}</td>
|
||
<td>${order.channel || '-'}</td>
|
||
<td>${order.operator || '-'}</td>
|
||
<td><span class="badge ${getStatusClass(order.status)}">${order.status}</span></td>
|
||
<td title="${order.note || ''}">${(order.note || '-').substring(0, 30)}${(order.note||'').length > 30 ? '...' : ''}</td>
|
||
<td class="actions">
|
||
<button class="btn btn-sm btn-edit" onclick="editItem(${order.id})">✏️</button>
|
||
<button class="btn btn-sm btn-danger" onclick="deleteItem(${order.id})">🗑️</button>
|
||
</td>
|
||
</tr>
|
||
`).join("");
|
||
}
|
||
}
|
||
|
||
function filterTable() {
|
||
const search = document.getElementById("search").value.toLowerCase();
|
||
const channel = document.getElementById("filter-channel").value;
|
||
const status = document.getElementById("filter-status").value;
|
||
|
||
let filtered = allOrders;
|
||
|
||
if (search) {
|
||
filtered = filtered.filter(o =>
|
||
String(o.id).includes(search) ||
|
||
o.address.toLowerCase().includes(search) ||
|
||
(o.operator && o.operator.toLowerCase().includes(search))
|
||
);
|
||
}
|
||
if (channel) filtered = filtered.filter(o => o.channel === channel);
|
||
if (status) filtered = filtered.filter(o => o.status === status);
|
||
|
||
renderTable(filtered);
|
||
}
|
||
|
||
document.getElementById("search").addEventListener("input", filterTable);
|
||
document.getElementById("filter-channel").addEventListener("change", filterTable);
|
||
document.getElementById("filter-status").addEventListener("change", filterTable);
|
||
|
||
document.getElementById("form").addEventListener("submit", async (e) => {
|
||
e.preventDefault();
|
||
const form = e.target;
|
||
const data = {
|
||
id: form.id.value,
|
||
address: form.address.value,
|
||
channel: form.channel.value,
|
||
operator: form.operator.value,
|
||
status: form.status.value,
|
||
note: form.note.value
|
||
};
|
||
|
||
if (editingId) {
|
||
await fetch(`/api/data/${editingId}`, {
|
||
method: "PUT",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify(data)
|
||
});
|
||
} else {
|
||
await fetch("/api/data", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify(data)
|
||
});
|
||
}
|
||
|
||
form.reset();
|
||
editingId = null;
|
||
document.getElementById("form-title").textContent = "➕ Добавить заказ";
|
||
document.getElementById("submit-btn").textContent = "Добавить";
|
||
document.getElementById("cancel-btn").style.display = "none";
|
||
load();
|
||
});
|
||
|
||
async function editItem(id) {
|
||
const order = allOrders.find(o => o.id === id);
|
||
if (!order) return;
|
||
|
||
const form = document.getElementById("form");
|
||
form.id.value = order.id;
|
||
form.address.value = order.address;
|
||
form.channel.value = order.channel || "";
|
||
form.operator.value = order.operator || "";
|
||
form.status.value = order.status;
|
||
form.note.value = order.note || "";
|
||
|
||
editingId = id;
|
||
document.getElementById("form-title").textContent = "✏️ Редактировать заказ";
|
||
document.getElementById("submit-btn").textContent = "Сохранить";
|
||
document.getElementById("cancel-btn").style.display = "inline-block";
|
||
}
|
||
|
||
document.getElementById("cancel-btn").addEventListener("click", () => {
|
||
document.getElementById("form").reset();
|
||
editingId = null;
|
||
document.getElementById("form-title").textContent = "➕ Добавить заказ";
|
||
document.getElementById("submit-btn").textContent = "Добавить";
|
||
document.getElementById("cancel-btn").style.display = "none";
|
||
});
|
||
|
||
async function deleteItem(id) {
|
||
if (!confirm("Удалить заказ №" + id + "?")) return;
|
||
await fetch(`/api/data/${id}`, { method: "DELETE" });
|
||
document.getElementById("form").reset();
|
||
editingId = null;
|
||
document.getElementById("form-title").textContent = "➕ Добавить заказ";
|
||
document.getElementById("submit-btn").textContent = "Добавить";
|
||
document.getElementById("cancel-btn").style.display = "none";
|
||
load();
|
||
}
|
||
|
||
load();
|
||
</script>
|
||
|
||
</body>
|
||
</html>
|