Improve delivery workflows and add versioned guided tours

This commit is contained in:
2026-10-08 01:35:24 +06:00
parent 1860e52b88
commit 55b5ca49e2
25 changed files with 933 additions and 1431 deletions
+66
View File
@@ -0,0 +1,66 @@
// Local, in-memory review environment. It serves the production build and
// fictional API data on its own loopback port, never contacts the real backend.
import { createServer } from 'node:http';
import { readFile } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
import { resolve, sep } from 'node:path';
import { randomUUID } from 'node:crypto';
const root = resolve(fileURLToPath(new URL('../dist/', import.meta.url)));
const dateKey = date => [date.getFullYear(), String(date.getMonth() + 1).padStart(2, '0'), String(date.getDate()).padStart(2, '0')].join('-');
const today = dateKey(new Date());
const tomorrowDate = new Date(); tomorrowDate.setDate(tomorrowDate.getDate() + 1);
const tomorrow = dateKey(tomorrowDate);
const base = { date: today, pickup_location: 'symbat', pickup_location_2: null, warehouse_request_source: null, warehouse_request_source_2: null, product_name_2: null, address: 'Кокшетау, Тестовая улица, 1', street: 'Тестовая', house: '1', apartment: '12', entrance: '2', floor: '4', phone: '+7 (000) 000-00-00', additional_phone: null, has_elevator: true, payment_status: 'prepaid', service_info: null, comment: '', status: 'new', created_at: new Date().toISOString(), updated_at: new Date().toISOString() };
let deliveries = [
{ ...base, id: 'demo-1', customer_name: 'Демо-клиент Анна', product_name: 'Диван «Комфорт»', service_info: 'Сборка после доставки', comment: 'Позвонить за 30 минут до приезда.' },
{ ...base, id: 'demo-2', customer_name: 'Демо-клиент Иван', product_name: 'Обеденный стол и четыре стула', street: 'Примерная', house: '24А', apartment: null, entrance: '1', floor: '2', pickup_location: 'warehouse', warehouse_request_source: 'nursaya', has_elevator: false, payment_status: 'paid' },
{ ...base, id: 'demo-3', customer_name: 'Демо-клиент Мария', product_name: 'Кровать и матрас', street: 'Учебная', house: '7', status: 'delivered', payment_status: 'paid' },
{ ...base, id: 'demo-4', date: tomorrow, customer_name: 'Демо-клиент Алексей', product_name: 'Шкаф', pickup_location: 'galaktika' },
];
deliveries = deliveries.map(d => ({ ...d, address: `Кокшетау, ул. ${d.street}, д. ${d.house}` }));
let failNext = false;
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
const json = (response, status, data) => { response.writeHead(status, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' }); response.end(JSON.stringify(data)); };
const toDate = value => { const [day, month, year] = value.split('-'); return `${year}-${month}-${day}`; };
const server = createServer(async (request, response) => {
try {
const url = new URL(request.url, 'http://localhost');
if (url.pathname.startsWith('/api/') || url.pathname === '/__demo__/fail-next') {
let body = ''; for await (const chunk of request) body += chunk;
const data = body ? JSON.parse(body) : {};
if (url.pathname === '/__demo__/fail-next') { failNext = true; return json(response, 200, { ok: true }); }
if (url.pathname === '/api/auth/login') {
if (data.username !== 'demo' || data.password !== 'demo123') return json(response, 401, { error: 'Демо-вход: demo / demo123' });
return json(response, 200, { token: `demo.${Buffer.from(JSON.stringify({ sub: 'preview-demo-user' })).toString('base64')}.demo` });
}
if (url.pathname === '/api/ws') return json(response, 501, { error: 'No websocket in demo' });
if (request.method === 'GET' && url.pathname === '/api/deliveries/count') {
const dates = [...new Set(deliveries.map(d => d.date))];
return json(response, 200, { counts: dates.map(date => ({ date, count: deliveries.filter(d => d.date === date).length })) });
}
if (request.method === 'GET' && url.pathname === '/api/deliveries') return json(response, 200, { deliveries: deliveries.filter(d => d.date === toDate(url.searchParams.get('date'))) });
await sleep(500);
if (failNext) { failNext = false; return json(response, 500, { error: 'Simulated failure' }); }
if (request.method === 'POST' && url.pathname === '/api/deliveries') {
const id = randomUUID(); deliveries.push({ ...base, ...data, id, date: toDate(data.date) }); return json(response, 200, { id, message: 'created' });
}
const id = url.pathname.split('/')[3];
const existing = deliveries.find(d => d.id === id);
if (!existing) return json(response, 404, { error: 'Not found' });
if (request.method === 'DELETE') deliveries = deliveries.filter(d => d.id !== id);
else if (url.pathname.endsWith('/status')) existing.status = existing.status === 'new' ? 'delivered' : 'new';
else Object.assign(existing, data, { date: toDate(data.date), updated_at: new Date().toISOString() });
return json(response, 200, { message: 'ok' });
}
if (url.pathname === '/sw.js') { response.writeHead(404); return response.end(); }
const path = resolve(root, `.${decodeURIComponent(url.pathname === '/' ? '/index.html' : url.pathname)}`);
if (!path.startsWith(root + sep)) { response.writeHead(403); return response.end(); }
let content = await readFile(path);
const type = path.endsWith('.html') ? 'text/html' : path.endsWith('.js') ? 'text/javascript' : path.endsWith('.css') ? 'text/css' : path.endsWith('.svg') ? 'image/svg+xml' : path.endsWith('.json') ? 'application/json' : 'application/octet-stream';
if (type === 'text/html') content = Buffer.from(content.toString().replace('<body>', '<body><div style="background:#fff1e5;color:#6e3900;padding:8px 16px;text-align:center;font:13px system-ui">Демо-просмотр · Вымышленные заявки · Изменения не затрагивают рабочую базу</div>'));
response.writeHead(200, { 'Content-Type': type, 'Cache-Control': 'no-store' }); response.end(content);
} catch { response.writeHead(404); response.end('Not found'); }
});
server.listen(4174, '127.0.0.1', () => console.log('Demo UI: http://127.0.0.1:4174 — login demo / demo123'));
process.on('SIGINT', () => server.close(() => process.exit(0)));