otkryt-dannyy-proekt-i-dorab/index.html

310 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>
: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>