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

484 lines
19 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{--ink:#0F1218;--cyan:#00E5FF;--cyan-50:#E8FCFF;--white:#fff;--gray-500:#5B6573;--gray-100:#F2F4F7;--success:#10B981;--warning:#F59E0B;--danger:#EF4444;--purple:#8B5CF6}
*{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(160px,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:12px;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)}
.stat-card.purple .stat-value{color:var(--purple)}
.charts-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:24px;margin-bottom:24px}
.chart-panel{background:var(--white);border-radius:12px;padding:24px;box-shadow:0 2px 8px rgba(0,0,0,0.08)}
.chart-panel h3{font-size:16px;font-weight:700;margin-bottom:16px}
.chart-container{height:250px;position:relative}
.bar-chart{display:flex;align-items:flex-end;gap:4px;height:200px;padding-top:20px}
.bar{flex:1;background:var(--cyan);border-radius:4px 4px 0 0;min-width:20px;position:relative;transition:opacity 0.3s}
.bar:hover{opacity:0.7}
.bar-label{position:absolute;bottom:-25px;left:50%;transform:translateX(-50%);font-size:10px;white-space:nowrap}
.bar-value{position:absolute;top:-20px;left:50%;transform:translateX(-50%);font-size:11px;font-weight:600}
.pie-chart{width:200px;height:200px;border-radius:50%;margin:0 auto;position:relative}
.legend{display:grid;gap:8px;margin-top:16px}
.legend-item{display:flex;align-items:center;gap:8px;font-size:12px}
.legend-color{width:16px;height:16px;border-radius:4px}
.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:500px;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:250px;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}
.date-filter{display:flex;gap:8px;align-items:center;margin-bottom:16px}
.date-filter input{padding:8px 12px;border:1px solid var(--gray-100);border-radius:6px;font-size:13px}
@media(max-width:1100px){.main-grid{grid-template-columns:1fr}.charts-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 class="stat-card purple">
<div class="stat-label">Конверсия</div>
<div class="stat-value" id="stat-conversion">0%</div>
</div>
</div>
<div class="charts-grid">
<div class="chart-panel">
<h3>📈 Динамика по дням</h3>
<div class="chart-container">
<div class="bar-chart" id="daily-chart"></div>
</div>
</div>
<div class="chart-panel">
<h3>📊 Статусы заказов</h3>
<div class="chart-container" style="display:flex;align-items:center;justify-content:center">
<div class="pie-chart" id="status-pie" style="background:conic-gradient(var(--cyan) 0deg 0deg,#10B981 0deg 0deg,#F59E0B 0deg 0deg)"></div>
</div>
<div class="legend" id="status-legend"></div>
</div>
</div>
<div class="charts-grid">
<div class="chart-panel">
<h3>📱 Каналы продаж</h3>
<div class="chart-container">
<div class="bar-chart" id="channel-chart"></div>
</div>
</div>
<div class="chart-panel">
<h3>👥 Топ операторов</h3>
<div class="chart-container">
<div class="bar-chart" id="operator-chart"></div>
</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 '';
}
function extractDateFromId(id) {
const idStr = String(id);
if (idStr.length >= 6) {
const day = idStr.slice(4, 6);
const month = idStr.slice(2, 4);
const year = '20' + idStr.slice(0, 2);
return year + '-' + month + '-' + day;
}
return new Date().toISOString().slice(0, 10);
}
function groupByDate(orders) {
const groups = {};
orders.forEach(o => {
const date = extractDateFromId(o.id);
if (!groups[date]) groups[date] = 0;
groups[date]++;
});
return Object.entries(groups).sort((a, b) => a[0].localeCompare(b[0]));
}
function groupByChannel(orders) {
const groups = {};
orders.forEach(o => {
const ch = o.channel || 'Без канала';
if (!groups[ch]) groups[ch] = 0;
groups[ch]++;
});
return Object.entries(groups).sort((a, b) => b[1] - a[1]);
}
function groupByOperator(orders) {
const groups = {};
orders.forEach(o => {
const op = o.operator || 'Без оператора';
if (!groups[op]) groups[op] = 0;
groups[op]++;
});
return Object.entries(groups).sort((a, b) => b[1] - a[1]).slice(0, 10);
}
function groupByStatus(orders) {
const groups = {'в работе': 0, 'выполнен': 0, 'ОТКАЗ': 0};
orders.forEach(o => {
if (groups[o.status] !== undefined) groups[o.status]++;
});
return groups;
}
function renderDailyChart(data) {
const container = document.getElementById("daily-chart");
if (data.length === 0) {
container.innerHTML = '<div style="color:#999;text-align:center;padding:40px">Нет данных</div>';
return;
}
const max = Math.max(...data.map(d => d[1]));
container.innerHTML = data.map(d => {
const height = (d[1] / max) * 180;
const date = d[0].slice(5) + '.' + d[0].slice(0, 4);
return '<div class="bar" style="height:' + height + 'px"><span class="bar-value">' + d[1] + '</span><span class="bar-label">' + date + '</span></div>';
}).join("");
}
function renderChannelChart(data) {
const container = document.getElementById("channel-chart");
if (data.length === 0) {
container.innerHTML = '<div style="color:#999;text-align:center;padding:40px">Нет данных</div>';
return;
}
const max = Math.max(...data.map(d => d[1]));
container.innerHTML = data.map(d => {
const height = (d[1] / max) * 180;
return '<div class="bar" style="height:' + height + 'px;background:#8B5CF6"><span class="bar-value">' + d[1] + '</span><span class="bar-label" style="transform:translateX(-50%) rotate(-45deg);font-size:9px">' + d[0].slice(0, 8) + '</span></div>';
}).join("");
}
function renderOperatorChart(data) {
const container = document.getElementById("operator-chart");
if (data.length === 0) {
container.innerHTML = '<div style="color:#999;text-align:center;padding:40px">Нет данных</div>';
return;
}
const max = Math.max(...data.map(d => d[1]));
container.innerHTML = data.map(d => {
const height = (d[1] / max) * 180;
return '<div class="bar" style="height:' + height + 'px;background:#F59E0B"><span class="bar-value">' + d[1] + '</span><span class="bar-label" style="transform:translateX(-50%) rotate(-45deg);font-size:9px">' + d[0].slice(0, 10) + '</span></div>';
}).join("");
}
function renderStatusPie(statuses) {
const total = statuses['в работе'] + statuses['выполнен'] + statuses['ОТКАЗ'];
if (total === 0) return;
const activeDeg = (statuses['в работе'] / total) * 360;
const completedDeg = (statuses['выполнен'] / total) * 360;
const rejectedDeg = (statuses['ОТКАЗ'] / total) * 360;
const pie = document.getElementById("status-pie");
pie.style.background = 'conic-gradient(#F59E0B 0deg ' + activeDeg + 'deg, #10B981 ' + activeDeg + 'deg ' + (activeDeg + completedDeg) + 'deg, #EF4444 ' + (activeDeg + completedDeg) + 'deg 360deg)';
const legend = document.getElementById("status-legend");
legend.innerHTML = [
{label: 'В работе', count: statuses['в работе'], color: '#F59E0B'},
{label: 'Выполнен', count: statuses['выполнен'], color: '#10B981'},
{label: 'ОТКАЗ', count: statuses['ОТКАЗ'], color: '#EF4444'}
].map(item => '<div class="legend-item"><div class="legend-color" style="background:' + item.color + '"></div><span>' + item.label + ': ' + item.count + '</span></div>').join("");
}
async function load() {
const res = await fetch("/api/data");
const data = await res.json();
allOrders = data.orders || [];
renderTable(allOrders);
updateStats(allOrders);
updateCharts(allOrders);
}
function updateStats(orders) {
const total = orders.length;
const active = orders.filter(o => o.status === "в работе").length;
const completed = orders.filter(o => o.status === "выполнен").length;
const rejected = orders.filter(o => o.status === "ОТКАЗ").length;
const conversion = total > 0 ? Math.round((completed / total) * 100) : 0;
document.getElementById("stat-total").textContent = total;
document.getElementById("stat-active").textContent = active;
document.getElementById("stat-completed").textContent = completed;
document.getElementById("stat-rejected").textContent = rejected;
document.getElementById("stat-conversion").textContent = conversion + '%';
}
function updateCharts(orders) {
renderDailyChart(groupByDate(orders));
renderChannelChart(groupByChannel(orders));
renderOperatorChart(groupByOperator(orders));
renderStatusPie(groupByStatus(orders));
}
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>