v4: дашборд с данными из Google Таблицы
This commit is contained in:
parent
02e4d456f8
commit
4291390583
4
.vibe42-run.log
Normal file
4
.vibe42-run.log
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
--- run Thu Aug 27 06:43:16 AM UTC 2026 :: npm start --silent
|
||||||
|
|
||||||
|
up to date in 393ms
|
||||||
|
Dashboard running on port 3000
|
||||||
1
.vibe42-run.pid
Normal file
1
.vibe42-run.pid
Normal file
@ -0,0 +1 @@
|
|||||||
|
86
|
||||||
37
data.json
37
data.json
@ -1,8 +1,33 @@
|
|||||||
{
|
{
|
||||||
"items": [
|
"orders": [
|
||||||
{ "id": 1, "title": "Заказ #001", "client": "Иванов А.", "amount": 15000, "status": "active", "createdAt": "2026-08-25" },
|
{"id": 86844268, "address": "г.Костанай, ул.КАРБЫШЕВА, дом 13, кв(оф) 20", "channel": "ЦАП", "operator": "Борецкая Настя", "status": "в работе", "note": ""},
|
||||||
{ "id": 2, "title": "Заказ #002", "client": "Петрова Б.", "amount": 28000, "status": "completed", "createdAt": "2026-08-24" },
|
{"id": 86950503, "address": "г.Костанай Улица ВОРОШИЛОВА дом 54/ кв. 44/", "channel": "УП", "operator": "Аманбаева Жанат", "status": "выполнен", "note": ""},
|
||||||
{ "id": 3, "title": "Заказ #003", "client": "Сидоров В.", "amount": 42000, "status": "active", "createdAt": "2026-08-23" }
|
{"id": 86921257, "address": "г.Костанай, ул.МИРОШНИЧЕНКО АЛЕКСАНДРА, дом 10, кв(оф) 69", "channel": "ЦАП", "operator": "Шохман Лена", "status": "выполнен", "note": ""},
|
||||||
],
|
{"id": 86932019, "address": "г.Костанай, мкр.Наурыз, дом 3/А, кв(оф) 10", "channel": "ЦАП", "operator": "Шохман Лена", "status": "выполнен", "note": "после 17:00"},
|
||||||
"stats": { "total": 3, "active": 2, "completed": 1 }
|
{"id": 86931896, "address": "г.Костанай, ул.ВОИНОВ ИНТЕРНАЦИОНАЛИСТОВ, дом 2, кв(оф) 66", "channel": "Дилер", "operator": "", "status": "выполнен", "note": "30.04 установка с 14.00-16.00"},
|
||||||
|
{"id": 86931637, "address": "г.Костанай, ул.СЕЙФУЛЛИНА, дом 2/А, кв(оф) 51", "channel": "ЦАП", "operator": "Мухамеджанова Фаризат", "status": "выполнен", "note": ""},
|
||||||
|
{"id": 86928438, "address": "г.Костанай, ул.ДОЩАНОВА ОМАРА, дом 133/Б, кв(оф) 21", "channel": "ПСС", "operator": "Байнакатов Султан", "status": "выполнен", "note": ""},
|
||||||
|
{"id": 86917938, "address": "г.Костанай, ул.ТЕКСТИЛЬЩИКОВ, дом 9/А, кв(оф) 14", "channel": "Дилер", "operator": "", "status": "выполнен", "note": ""},
|
||||||
|
{"id": 86927827, "address": "г.Костанай, ул.Шакшак Жанибек батыр, дом 45/Г, кв(оф) 105", "channel": "", "operator": "", "status": "в работе", "note": ""},
|
||||||
|
{"id": 86949844, "address": "г.Костанай, ул.Султана Баймагамбетова, дом 3/А, кв(оф) 27", "channel": "", "operator": "", "status": "выполнен", "note": ""},
|
||||||
|
{"id": 86939780, "address": "г.Костанай Улица Герасимова Д. дом 7/ кв. 47", "channel": "ЦАП", "operator": "", "status": "в работе", "note": "Примечание VCIP:[пересоздать в 16.00 идет объединение програмно]]"},
|
||||||
|
{"id": 86955974, "address": "г.Костанай, ул.МАЙЛИНА, дом 83, кв(оф) 34", "channel": "", "operator": "", "status": "в работе", "note": ""},
|
||||||
|
{"id": 87055596, "address": "п.Карабалык, ул.КОСМОНАВТОВ, дом 22, кв(оф) 7", "channel": "", "operator": "Магамбетов Талгат", "status": "в работе", "note": ""},
|
||||||
|
{"id": 86730778, "address": "г.Петропавловск, ул.Георгия Жукова, дом 36, кв(оф) 14", "channel": "ЦАП", "operator": "Латыпов Руслан", "status": "выполнен", "note": ""},
|
||||||
|
{"id": 86889241, "address": "г. Петропавловск улица Горького 173 кв 52", "channel": "ЦАП", "operator": "Белоусова Виктория", "status": "выполнен", "note": ""},
|
||||||
|
{"id": 86894019, "address": " г.Петропавловск Улица Нурсултана Назарбаева дом 127/ кв. 63/", "channel": "ЦАП", "operator": "Латыпов Руслан", "status": "выполнен", "note": ""},
|
||||||
|
{"id": 86844130, "address": "г.Петропавловск, ул.ЗАВОДСКАЯ, дом 23, кв(оф) 500", "channel": "ЦАП", "operator": "Белоусова Виктория", "status": "ОТКАЗ", "note": "(по согл. ЦАП)"},
|
||||||
|
{"id": 86901154, "address": "г.Петропавловск, ул.Александра Попова, дом 42, кв(оф) 58", "channel": "ПСС", "operator": "ZHANKESH.A", "status": "ОТКАЗ", "note": "ОТКАЗ (абоненту не нужен интернет,очень дорого, аб. хочет только телевидиние. будут искать оператора который предоставляет услуги только телевидиние без интернета)"},
|
||||||
|
{"id": 86909513, "address": "г.Петропавловск, ул.Зои Космодемьянской, дом 7, кв(оф) 24", "channel": "OAPI", "operator": "", "status": "выполнен", "note": ""},
|
||||||
|
{"id": 86915894, "address": "г. Петропавловск улица Уалиханова дом 19 кВ 144", "channel": "ЦАП", "operator": "Белоуосва Виктроия", "status": "выполнен", "note": ""},
|
||||||
|
{"id": 86842967, "address": "г.Петропавловск, ул.Орда, дом 17/Д", "channel": "ЦАП", "operator": "Белоуосва Виктроия", "status": "выполнен", "note": ""},
|
||||||
|
{"id": 86891330, "address": "г.Петропавловск улица Казына дом 12 ", "channel": "ЦАП", "operator": "Белоуосва Виктроия", "status": "выполнен", "note": ""},
|
||||||
|
{"id": 86911676, "address": "г.Петропавловск, ул.Магжана Жумабаева, дом 76, кв(оф) 24", "channel": "ПСС", "operator": "ZHANKESH.A", "status": "выполнен", "note": ""},
|
||||||
|
{"id": 86918617, "address": "г.Петропавловск, ул.Абая, дом 71, кв(оф) 64", "channel": "ПСС", "operator": "Кокичева Диана", "status": "выполнен", "note": ""},
|
||||||
|
{"id": 86918931, "address": "г.Петропавловск, ул.Шокана Уалиханова, дом 48, кв(оф) 7", "channel": "ПСС", "operator": "ZHANKESH.A", "status": "выполнен", "note": ""},
|
||||||
|
{"id": 86895490, "address": "г.Петропавловск, ул.Георгия Жукова, дом 40, кв(оф) 55", "channel": "Дилер", "operator": "", "status": "выполнен", "note": ""},
|
||||||
|
{"id": 86931868, "address": "г.Петропавловск ул. Шокана Уалиханова дом 27 А кв. 9", "channel": "Онлайн", "operator": "Кудайберген Дамир", "status": "ОТКАЗ", "note": "не верно указан адрес"},
|
||||||
|
{"id": 86925108, "address": "г.Петропавловск ул. СТУДЕНЧЕСКАЯ дом 6 кв. 40", "channel": "Дилер", "operator": "", "status": "выполнен", "note": ""},
|
||||||
|
{"id": 86938401, "address": "г.Петропавловск ул. Магжана Жумабаева дом 95 кв. 49", "channel": "", "operator": "", "status": "выполнен", "note": ""}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
279
index.html
279
index.html
@ -3,57 +3,64 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
<title>Дашборд</title>
|
<title>Дашборд заказов</title>
|
||||||
<style>
|
<style>
|
||||||
:root{--ink:#0F1218;--cyan:#00E5FF;--cyan-50:#E8FCFF;--white:#fff;--gray-500:#5B6573;--gray-100:#F2F4F7;--success:#10B981;--warning:#F59E0B;--danger:#EF4444}
|
: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}
|
*{box-sizing:border-box;margin:0;padding:0}
|
||||||
body{font:15px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI",Inter,system-ui,sans-serif;color:var(--ink);background:#f8f9fa}
|
body{font:14px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI",Inter,system-ui,sans-serif;color:var(--ink);background:#f8f9fa}
|
||||||
.container{max-width:1400px;margin:0 auto;padding:24px}
|
.container{max-width:1600px;margin:0 auto;padding:24px}
|
||||||
.header{background:var(--ink);color:var(--white);padding:20px 24px;margin-bottom: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}
|
.header h1{font-size:24px;font-weight:700}
|
||||||
.stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:20px;margin-bottom:32px}
|
.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:24px;box-shadow:0 2px 8px rgba(0,0,0,0.08)}
|
.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:14px;color:var(--gray-500);margin-bottom:8px}
|
.stat-label{font-size:13px;color:var(--gray-500);margin-bottom:8px;text-transform:uppercase}
|
||||||
.stat-value{font-size:32px;font-weight:700;color:var(--ink)}
|
.stat-value{font-size:28px;font-weight:700;color:var(--ink)}
|
||||||
.stat-card.primary .stat-value{color:var(--cyan)}
|
.stat-card.primary .stat-value{color:var(--cyan)}
|
||||||
.stat-card.success .stat-value{color:var(--success)}
|
.stat-card.success .stat-value{color:var(--success)}
|
||||||
.stat-card.warning .stat-value{color:var(--warning)}
|
.stat-card.warning .stat-value{color:var(--warning)}
|
||||||
.main-grid{display:grid;grid-template-columns:1fr 400px;gap:24px}
|
.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{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}
|
.panel h2{font-size:18px;font-weight:700;margin-bottom:20px;display:flex;align-items:center;gap:10px}
|
||||||
.table{width:100%;border-collapse:collapse}
|
.table-container{overflow-x:auto;max-height:600px;overflow-y:auto}
|
||||||
.table th,.table td{padding:12px;text-align:left;border-bottom:1px solid var(--gray-100)}
|
.table{width:100%;border-collapse:collapse;font-size:13px}
|
||||||
.table th{font-size:13px;font-weight:600;color:var(--gray-500);text-transform:uppercase}
|
.table th,.table td{padding:10px 8px;text-align:left;border-bottom:1px solid var(--gray-100);white-space:nowrap}
|
||||||
.table tr:hover{background:var(--gray-100)}
|
.table th{font-size:12px;font-weight:600;color:var(--gray-500);text-transform:uppercase;position:sticky;top:0;background:var(--white)}
|
||||||
.badge{display:inline-block;padding:4px 12px;border-radius:20px;font-size:12px;font-weight:600}
|
.table tr:hover{background:var(--cyan-50)}
|
||||||
.badge-active{background:#DCFCE7;color:#166534}
|
.table td:nth-child(2){max-width:300px;white-space:normal}
|
||||||
.badge-completed{background:var(--cyan-50);color:#006999}
|
.badge{display:inline-block;padding:4px 10px;border-radius:20px;font-size:11px;font-weight:600}
|
||||||
.form-group{margin-bottom:16px}
|
.badge-в_работе{background:#FEF3C7;color:#92400E}
|
||||||
.form-group label{display:block;font-size:14px;font-weight:600;margin-bottom:8px;color:var(--gray-500)}
|
.badge-выполнен{background:#DCFCE7;color:#166534}
|
||||||
.form-group input,.form-group select{width:100%;padding:12px;border:1px solid var(--gray-100);border-radius:8px;font-size:15px;font-family:inherit}
|
.badge-ОТКАЗ{background:#FEE2E2;color:#991B1B}
|
||||||
.form-group input:focus,.form-group select:focus{outline:none;border-color:var(--cyan)}
|
.form-group{margin-bottom:14px}
|
||||||
.btn{display:inline-block;background:var(--cyan);color:var(--ink);padding:12px 24px;border-radius:8px;font-weight:700;text-decoration:none;cursor:pointer;border:none;font-size:15px;width:100%}
|
.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:hover{opacity:0.9}
|
||||||
.btn-sm{padding:6px 12px;font-size:13px;width:auto}
|
.btn-sm{padding:5px 10px;font-size:12px;width:auto}
|
||||||
.btn-danger{background:var(--danger);color:var(--white)}
|
.btn-danger{background:var(--danger);color:var(--white)}
|
||||||
.actions{display:flex;gap:8px}
|
.btn-edit{background:var(--warning);color:var(--ink)}
|
||||||
|
.actions{display:flex;gap:6px}
|
||||||
.empty{text-align:center;padding:40px;color:var(--gray-500)}
|
.empty{text-align:center;padding:40px;color:var(--gray-500)}
|
||||||
@media(max-width:900px){.main-grid{grid-template-columns:1fr}}
|
.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}}
|
@media(max-width:640px){.stats{grid-template-columns:1fr}.container{padding:16px}}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<div class="header">
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1>📊 Панель управления</h1>
|
<div class="header">
|
||||||
</div>
|
<h1>📊 Дашборд заказов - Костанай/Петропавловск</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="container">
|
|
||||||
<div class="stats">
|
<div class="stats">
|
||||||
<div class="stat-card primary">
|
<div class="stat-card primary">
|
||||||
<div class="stat-label">Всего</div>
|
<div class="stat-label">Всего заказов</div>
|
||||||
<div class="stat-value" id="stat-total">0</div>
|
<div class="stat-value" id="stat-total">0</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card warning">
|
<div class="stat-card warning">
|
||||||
@ -61,46 +68,88 @@ body{font:15px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI",Inter,system-ui,s
|
|||||||
<div class="stat-value" id="stat-active">0</div>
|
<div class="stat-value" id="stat-active">0</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card success">
|
<div class="stat-card success">
|
||||||
<div class="stat-label">Завершено</div>
|
<div class="stat-label">Выполнено</div>
|
||||||
<div class="stat-value" id="stat-completed">0</div>
|
<div class="stat-value" id="stat-completed">0</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="stat-card danger">
|
||||||
|
<div class="stat-label">Отказы</div>
|
||||||
|
<div class="stat-value" id="stat-rejected">0</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="main-grid">
|
<div class="main-grid">
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<h2>📋 Заказы</h2>
|
<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">
|
<table class="table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr><th>ID</th><th>Название</th><th>Клиент</th><th>Сумма</th><th>Статус</th><th>Дата</th><th>Действия</th></tr>
|
<tr><th>№ заказа</th><th>Адрес</th><th>Канал</th><th>Оператор</th><th>Статус</th><th>Примечание</th><th>Действия</th></tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="table-body"></tbody>
|
<tbody id="table-body"></tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
<div id="empty" class="empty" style="display:none">Нет записей</div>
|
<div id="empty" class="empty" style="display:none">Нет записей</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<h2>➕ Добавить</h2>
|
<h2 id="form-title">➕ Добавить заказ</h2>
|
||||||
<form id="form">
|
<form id="form">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Название</label>
|
<label>№ заказа *</label>
|
||||||
<input type="text" name="title" placeholder="Например: Заказ #001" required>
|
<input type="text" name="id" placeholder="86844268" required>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Клиент</label>
|
<label>Адрес *</label>
|
||||||
<input type="text" name="client" placeholder="ФИО клиента" required>
|
<input type="text" name="address" placeholder="г.Костанай, ул...." required>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Сумма (₸)</label>
|
<label>Канал продаж</label>
|
||||||
<input type="number" name="amount" placeholder="0" required>
|
<select name="channel">
|
||||||
</div>
|
<option value="">Выбрать...</option>
|
||||||
<div class="form-group">
|
<option value="ЦАП">ЦАП</option>
|
||||||
<label>Статус</label>
|
<option value="УП">УП</option>
|
||||||
<select name="status">
|
<option value="Дилер">Дилер</option>
|
||||||
<option value="active">В работе</option>
|
<option value="ПСС">ПСС</option>
|
||||||
<option value="completed">Завершено</option>
|
<option value="OAPI">OAPI</option>
|
||||||
|
<option value="Онлайн">Онлайн</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn">Добавить</button>
|
<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>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -108,84 +157,148 @@ body{font:15px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI",Inter,system-ui,s
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
let editingId = null;
|
let editingId = null;
|
||||||
|
let allOrders = [];
|
||||||
|
|
||||||
|
function getStatusClass(status) {
|
||||||
|
if (status === 'в работе') return 'badge-в_работе';
|
||||||
|
if (status === 'выполнен') return 'badge-выполнен';
|
||||||
|
if (status === 'ОТКАЗ') return 'badge-ОТКАЗ';
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
const res = await fetch("/api/data");
|
const res = await fetch("/api/data");
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
allOrders = data.orders || [];
|
||||||
|
renderTable(allOrders);
|
||||||
|
updateStats(allOrders);
|
||||||
|
}
|
||||||
|
|
||||||
document.getElementById("stat-total").textContent = data.stats.total;
|
function updateStats(orders) {
|
||||||
document.getElementById("stat-active").textContent = data.stats.active;
|
document.getElementById("stat-total").textContent = orders.length;
|
||||||
document.getElementById("stat-completed").textContent = data.stats.completed;
|
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 tbody = document.getElementById("table-body");
|
||||||
const empty = document.getElementById("empty");
|
const empty = document.getElementById("empty");
|
||||||
|
|
||||||
if (data.items.length === 0) {
|
if (orders.length === 0) {
|
||||||
tbody.innerHTML = "";
|
tbody.innerHTML = "";
|
||||||
empty.style.display = "block";
|
empty.style.display = "block";
|
||||||
} else {
|
} else {
|
||||||
empty.style.display = "none";
|
empty.style.display = "none";
|
||||||
tbody.innerHTML = data.items.map(item => `
|
tbody.innerHTML = orders.map(order => `
|
||||||
<tr>
|
<tr>
|
||||||
<td>${item.id}</td>
|
<td><strong>${order.id}</strong></td>
|
||||||
<td>${item.title}</td>
|
<td title="${order.address}">${order.address.substring(0, 50)}${order.address.length > 50 ? '...' : ''}</td>
|
||||||
<td>${item.client}</td>
|
<td>${order.channel || '-'}</td>
|
||||||
<td>${item.amount.toLocaleString()}</td>
|
<td>${order.operator || '-'}</td>
|
||||||
<td><span class="badge badge-${item.status}">${item.status === "active" ? "В работе" : "Завершено"}</span></td>
|
<td><span class="badge ${getStatusClass(order.status)}">${order.status}</span></td>
|
||||||
<td>${item.createdAt}</td>
|
<td title="${order.note || ''}">${(order.note || '-').substring(0, 30)}${(order.note||'').length > 30 ? '...' : ''}</td>
|
||||||
<td class="actions">
|
<td class="actions">
|
||||||
<button class="btn btn-sm" onclick="editItem(${item.id})">✏️</button>
|
<button class="btn btn-sm btn-edit" onclick="editItem(${order.id})">✏️</button>
|
||||||
<button class="btn btn-sm btn-danger" onclick="deleteItem(${item.id})">🗑️</button>
|
<button class="btn btn-sm btn-danger" onclick="deleteItem(${order.id})">🗑️</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
`).join("");
|
`).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) => {
|
document.getElementById("form").addEventListener("submit", async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const form = e.target;
|
const form = e.target;
|
||||||
const data = {
|
const data = {
|
||||||
title: form.title.value,
|
id: form.id.value,
|
||||||
client: form.client.value,
|
address: form.address.value,
|
||||||
amount: parseInt(form.amount.value),
|
channel: form.channel.value,
|
||||||
status: form.status.value
|
operator: form.operator.value,
|
||||||
|
status: form.status.value,
|
||||||
|
note: form.note.value
|
||||||
};
|
};
|
||||||
|
|
||||||
const url = editingId ? `/api/data/${editingId}` : "/api/data";
|
if (editingId) {
|
||||||
const method = editingId ? "PUT" : "POST";
|
await fetch(`/api/data/${editingId}`, {
|
||||||
|
method: "PUT",
|
||||||
await fetch(url, {
|
headers: { "Content-Type": "application/json" },
|
||||||
method,
|
body: JSON.stringify(data)
|
||||||
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();
|
form.reset();
|
||||||
editingId = null;
|
editingId = null;
|
||||||
form.querySelector("button").textContent = "Добавить";
|
document.getElementById("form-title").textContent = "➕ Добавить заказ";
|
||||||
|
document.getElementById("submit-btn").textContent = "Добавить";
|
||||||
|
document.getElementById("cancel-btn").style.display = "none";
|
||||||
load();
|
load();
|
||||||
});
|
});
|
||||||
|
|
||||||
async function editItem(id) {
|
async function editItem(id) {
|
||||||
const res = await fetch("/api/data");
|
const order = allOrders.find(o => o.id === id);
|
||||||
const data = await res.json();
|
if (!order) return;
|
||||||
const item = data.items.find(i => i.id === id);
|
|
||||||
if (!item) return;
|
|
||||||
|
|
||||||
const form = document.getElementById("form");
|
const form = document.getElementById("form");
|
||||||
form.title.value = item.title;
|
form.id.value = order.id;
|
||||||
form.client.value = item.client;
|
form.address.value = order.address;
|
||||||
form.amount.value = item.amount;
|
form.channel.value = order.channel || "";
|
||||||
form.status.value = item.status;
|
form.operator.value = order.operator || "";
|
||||||
|
form.status.value = order.status;
|
||||||
|
form.note.value = order.note || "";
|
||||||
|
|
||||||
editingId = id;
|
editingId = id;
|
||||||
form.querySelector("button").textContent = "Сохранить";
|
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) {
|
async function deleteItem(id) {
|
||||||
if (!confirm("Удалить эту запись?")) return;
|
if (!confirm("Удалить заказ №" + id + "?")) return;
|
||||||
await fetch(`/api/data/${id}`, { method: "DELETE" });
|
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
10
package-lock.json
generated
Normal file
10
package-lock.json
generated
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"name": "dashboard-app",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "dashboard-app"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
21
server.js
21
server.js
@ -6,7 +6,7 @@ const PORT = process.env.PORT || 3000;
|
|||||||
const DATA = path.join(__dirname, "data.json");
|
const DATA = path.join(__dirname, "data.json");
|
||||||
|
|
||||||
function readData() {
|
function readData() {
|
||||||
try { return JSON.parse(fs.readFileSync(DATA, "utf8")); } catch (_) { return { items: [], stats: { total: 0, active: 0, completed: 0 } }; }
|
try { return JSON.parse(fs.readFileSync(DATA, "utf8")); } catch (_) { return { orders: [] }; }
|
||||||
}
|
}
|
||||||
function writeData(data) {
|
function writeData(data) {
|
||||||
fs.writeFileSync(DATA, JSON.stringify(data, null, 2));
|
fs.writeFileSync(DATA, JSON.stringify(data, null, 2));
|
||||||
@ -37,12 +37,8 @@ http.createServer(async (req, res) => {
|
|||||||
if (req.method === "POST" && req.url === "/api/data") {
|
if (req.method === "POST" && req.url === "/api/data") {
|
||||||
const data = readData();
|
const data = readData();
|
||||||
const item = await body(req);
|
const item = await body(req);
|
||||||
item.id = Date.now();
|
item.id = parseInt(item.id) || Date.now();
|
||||||
item.createdAt = new Date().toISOString().slice(0, 10);
|
data.orders.push(item);
|
||||||
data.items.push(item);
|
|
||||||
data.stats.total = data.items.length;
|
|
||||||
data.stats.active = data.items.filter(i => i.status === "active").length;
|
|
||||||
data.stats.completed = data.items.filter(i => i.status === "completed").length;
|
|
||||||
writeData(data);
|
writeData(data);
|
||||||
res.writeHead(200, { "Content-Type": "application/json" });
|
res.writeHead(200, { "Content-Type": "application/json" });
|
||||||
return res.end(JSON.stringify({ ok: true, item }));
|
return res.end(JSON.stringify({ ok: true, item }));
|
||||||
@ -51,12 +47,10 @@ http.createServer(async (req, res) => {
|
|||||||
if (req.method === "PUT" && req.url.startsWith("/api/data/")) {
|
if (req.method === "PUT" && req.url.startsWith("/api/data/")) {
|
||||||
const id = parseInt(req.url.split("/").pop());
|
const id = parseInt(req.url.split("/").pop());
|
||||||
const data = readData();
|
const data = readData();
|
||||||
const idx = data.items.findIndex(i => i.id === id);
|
const idx = data.orders.findIndex(i => i.id === id);
|
||||||
if (idx !== -1) {
|
if (idx !== -1) {
|
||||||
const update = await body(req);
|
const update = await body(req);
|
||||||
data.items[idx] = { ...data.items[idx], ...update };
|
data.orders[idx] = { ...data.orders[idx], ...update };
|
||||||
data.stats.active = data.items.filter(i => i.status === "active").length;
|
|
||||||
data.stats.completed = data.items.filter(i => i.status === "completed").length;
|
|
||||||
writeData(data);
|
writeData(data);
|
||||||
res.writeHead(200, { "Content-Type": "application/json" });
|
res.writeHead(200, { "Content-Type": "application/json" });
|
||||||
return res.end(JSON.stringify({ ok: true }));
|
return res.end(JSON.stringify({ ok: true }));
|
||||||
@ -68,10 +62,7 @@ http.createServer(async (req, res) => {
|
|||||||
if (req.method === "DELETE" && req.url.startsWith("/api/data/")) {
|
if (req.method === "DELETE" && req.url.startsWith("/api/data/")) {
|
||||||
const id = parseInt(req.url.split("/").pop());
|
const id = parseInt(req.url.split("/").pop());
|
||||||
const data = readData();
|
const data = readData();
|
||||||
data.items = data.items.filter(i => i.id !== id);
|
data.orders = data.orders.filter(i => i.id !== id);
|
||||||
data.stats.total = data.items.length;
|
|
||||||
data.stats.active = data.items.filter(i => i.status === "active").length;
|
|
||||||
data.stats.completed = data.items.filter(i => i.status === "completed").length;
|
|
||||||
writeData(data);
|
writeData(data);
|
||||||
res.writeHead(200, { "Content-Type": "application/json" });
|
res.writeHead(200, { "Content-Type": "application/json" });
|
||||||
return res.end(JSON.stringify({ ok: true }));
|
return res.end(JSON.stringify({ ok: true }));
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user