v4: дашборд с данными из Google Таблицы

This commit is contained in:
hff 2026-08-27 06:53:30 +00:00
parent 02e4d456f8
commit 4291390583
6 changed files with 250 additions and 106 deletions

4
.vibe42-run.log Normal file
View 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
View File

@ -0,0 +1 @@
86

View File

@ -1,8 +1,33 @@
{
"items": [
{ "id": 1, "title": "Заказ #001", "client": "Иванов А.", "amount": 15000, "status": "active", "createdAt": "2026-08-25" },
{ "id": 2, "title": "Заказ #002", "client": "Петрова Б.", "amount": 28000, "status": "completed", "createdAt": "2026-08-24" },
{ "id": 3, "title": "Заказ #003", "client": "Сидоров В.", "amount": 42000, "status": "active", "createdAt": "2026-08-23" }
],
"stats": { "total": 3, "active": 2, "completed": 1 }
"orders": [
{"id": 86844268, "address": "г.Костанай, ул.КАРБЫШЕВА, дом 13, кв(оф) 20", "channel": "ЦАП", "operator": "Борецкая Настя", "status": "в работе", "note": ""},
{"id": 86950503, "address": "г.Костанай Улица ВОРОШИЛОВА дом 54/ кв. 44/", "channel": "УП", "operator": "Аманбаева Жанат", "status": "выполнен", "note": ""},
{"id": 86921257, "address": "г.Костанай, ул.МИРОШНИЧЕНКО АЛЕКСАНДРА, дом 10, кв(оф) 69", "channel": "ЦАП", "operator": "Шохман Лена", "status": "выполнен", "note": ""},
{"id": 86932019, "address": "г.Костанай, мкр.Наурыз, дом 3/А, кв(оф) 10", "channel": "ЦАП", "operator": "Шохман Лена", "status": "выполнен", "note": "после 17:00"},
{"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": ""}
]
}

View File

@ -3,57 +3,64 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Дашборд</title>
<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:15px/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}
.header{background:var(--ink);color:var(--white);padding:20px 24px;margin-bottom:24px}
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(200px,1fr));gap:20px;margin-bottom:32px}
.stat-card{background:var(--white);border-radius:12px;padding:24px;box-shadow:0 2px 8px rgba(0,0,0,0.08)}
.stat-label{font-size:14px;color:var(--gray-500);margin-bottom:8px}
.stat-value{font-size:32px;font-weight:700;color:var(--ink)}
.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)}
.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 h2{font-size:18px;font-weight:700;margin-bottom:20px}
.table{width:100%;border-collapse:collapse}
.table th,.table td{padding:12px;text-align:left;border-bottom:1px solid var(--gray-100)}
.table th{font-size:13px;font-weight:600;color:var(--gray-500);text-transform:uppercase}
.table tr:hover{background:var(--gray-100)}
.badge{display:inline-block;padding:4px 12px;border-radius:20px;font-size:12px;font-weight:600}
.badge-active{background:#DCFCE7;color:#166534}
.badge-completed{background:var(--cyan-50);color:#006999}
.form-group{margin-bottom:16px}
.form-group label{display:block;font-size:14px;font-weight:600;margin-bottom:8px;color:var(--gray-500)}
.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}
.form-group input:focus,.form-group select:focus{outline:none;border-color:var(--cyan)}
.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%}
.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: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)}
.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)}
@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}}
</style>
</head>
<body>
<div class="header">
<div class="container">
<h1>📊 Панель управления</h1>
</div>
<div class="header">
<h1>📊 Дашборд заказов - Костанай/Петропавловск</h1>
</div>
<div class="container">
<div class="stats">
<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>
<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>
<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>
<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>
<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>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>
<tbody id="table-body"></tbody>
</table>
</div>
<div id="empty" class="empty" style="display:none">Нет записей</div>
</div>
<div class="panel">
<h2>➕ Добавить</h2>
<h2 id="form-title">➕ Добавить заказ</h2>
<form id="form">
<div class="form-group">
<label>Название</label>
<input type="text" name="title" placeholder="Например: Заказ #001" required>
<label>№ заказа *</label>
<input type="text" name="id" placeholder="86844268" required>
</div>
<div class="form-group">
<label>Клиент</label>
<input type="text" name="client" placeholder="ФИО клиента" required>
<label>Адрес *</label>
<input type="text" name="address" placeholder="г.Костанай, ул...." required>
</div>
<div class="form-group">
<label>Сумма (₸)</label>
<input type="number" name="amount" placeholder="0" required>
</div>
<div class="form-group">
<label>Статус</label>
<select name="status">
<option value="active">В работе</option>
<option value="completed">Завершено</option>
<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>
<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>
</div>
</div>
@ -108,84 +157,148 @@ body{font:15px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI",Inter,system-ui,s
<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);
}
document.getElementById("stat-total").textContent = data.stats.total;
document.getElementById("stat-active").textContent = data.stats.active;
document.getElementById("stat-completed").textContent = data.stats.completed;
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 (data.items.length === 0) {
if (orders.length === 0) {
tbody.innerHTML = "";
empty.style.display = "block";
} else {
empty.style.display = "none";
tbody.innerHTML = data.items.map(item => `
tbody.innerHTML = orders.map(order => `
<tr>
<td>${item.id}</td>
<td>${item.title}</td>
<td>${item.client}</td>
<td>${item.amount.toLocaleString()}</td>
<td><span class="badge badge-${item.status}">${item.status === "active" ? "В работе" : "Завершено"}</span></td>
<td>${item.createdAt}</td>
<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" onclick="editItem(${item.id})">✏️</button>
<button class="btn btn-sm btn-danger" onclick="deleteItem(${item.id})">🗑️</button>
<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 = {
title: form.title.value,
client: form.client.value,
amount: parseInt(form.amount.value),
status: form.status.value
id: form.id.value,
address: form.address.value,
channel: form.channel.value,
operator: form.operator.value,
status: form.status.value,
note: form.note.value
};
const url = editingId ? `/api/data/${editingId}` : "/api/data";
const method = editingId ? "PUT" : "POST";
await fetch(url, {
method,
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;
form.querySelector("button").textContent = "Добавить";
document.getElementById("form-title").textContent = "➕ Добавить заказ";
document.getElementById("submit-btn").textContent = "Добавить";
document.getElementById("cancel-btn").style.display = "none";
load();
});
async function editItem(id) {
const res = await fetch("/api/data");
const data = await res.json();
const item = data.items.find(i => i.id === id);
if (!item) return;
const order = allOrders.find(o => o.id === id);
if (!order) return;
const form = document.getElementById("form");
form.title.value = item.title;
form.client.value = item.client;
form.amount.value = item.amount;
form.status.value = item.status;
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;
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) {
if (!confirm("Удалить эту запись?")) return;
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();
}

10
package-lock.json generated Normal file
View File

@ -0,0 +1,10 @@
{
"name": "dashboard-app",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "dashboard-app"
}
}
}

View File

@ -6,7 +6,7 @@ const PORT = process.env.PORT || 3000;
const DATA = path.join(__dirname, "data.json");
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) {
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") {
const data = readData();
const item = await body(req);
item.id = Date.now();
item.createdAt = new Date().toISOString().slice(0, 10);
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;
item.id = parseInt(item.id) || Date.now();
data.orders.push(item);
writeData(data);
res.writeHead(200, { "Content-Type": "application/json" });
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/")) {
const id = parseInt(req.url.split("/").pop());
const data = readData();
const idx = data.items.findIndex(i => i.id === id);
const idx = data.orders.findIndex(i => i.id === id);
if (idx !== -1) {
const update = await body(req);
data.items[idx] = { ...data.items[idx], ...update };
data.stats.active = data.items.filter(i => i.status === "active").length;
data.stats.completed = data.items.filter(i => i.status === "completed").length;
data.orders[idx] = { ...data.orders[idx], ...update };
writeData(data);
res.writeHead(200, { "Content-Type": "application/json" });
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/")) {
const id = parseInt(req.url.split("/").pop());
const data = readData();
data.items = data.items.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;
data.orders = data.orders.filter(i => i.id !== id);
writeData(data);
res.writeHead(200, { "Content-Type": "application/json" });
return res.end(JSON.stringify({ ok: true }));