Files
web-frontend/docs/scripts/generate-wireframe-prototypes.mjs
2026-05-01 16:20:19 +07:00

907 lines
68 KiB
JavaScript
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.

import { mkdirSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
const OUTPUT_DIR = '/Users/ruslanbakiev/workspace/fregat/web-frontend/docs/public/prototypes';
const WIDTH = 1440;
const palette = {
bg: '#f3f4f6',
frame: '#ffffff',
border: '#d1d5db',
muted: '#e5e7eb',
line: '#9ca3af',
text: '#111827',
subtext: '#4b5563',
accent: '#dbeafe',
accentStroke: '#60a5fa',
success: '#dcfce7',
warning: '#fef3c7',
danger: '#fee2e2',
dark: '#101418',
darkCard: '#1b2228',
darkMuted: '#2d3741',
darkText: '#f9fafb',
darkSubtext: '#cbd5e1',
};
function esc(value) {
return String(value)
.replaceAll('&', '&')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;');
}
function svgText({ x, y, text, size = 16, weight = 500, fill = palette.text, anchor = 'start' }) {
return `<text x="${x}" y="${y}" font-family="Inter, Arial, sans-serif" font-size="${size}" font-weight="${weight}" fill="${fill}" text-anchor="${anchor}">${esc(text)}</text>`;
}
function svgRect({ x, y, width, height, fill = palette.frame, stroke = palette.border, rx = 18, dashed = false, strokeWidth = 1.5 }) {
return `<rect x="${x}" y="${y}" width="${width}" height="${height}" rx="${rx}" fill="${fill}" stroke="${stroke}" stroke-width="${strokeWidth}"${dashed ? ' stroke-dasharray="8 6"' : ''} />`;
}
function svgLine({ x1, y1, x2, y2, stroke = palette.border, strokeWidth = 1.5, dashed = false }) {
return `<line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" stroke="${stroke}" stroke-width="${strokeWidth}"${dashed ? ' stroke-dasharray="7 5"' : ''} />`;
}
function pill({ x, y, width, label, fill = '#f9fafb', stroke = palette.border, textFill = palette.subtext }) {
return [
svgRect({ x, y, width, height: 34, rx: 17, fill, stroke }),
svgText({ x: x + width / 2, y: y + 22, text: label, size: 13, weight: 600, fill: textFill, anchor: 'middle' }),
].join('');
}
function cardTitle({ x, y, text, subtitle }) {
return [
svgText({ x, y, text, size: 18, weight: 700 }),
subtitle ? svgText({ x, y: y + 24, text: subtitle, size: 12, weight: 500, fill: palette.subtext }) : '',
].join('');
}
function windowFrame({ title, height, dark = false }) {
const bg = dark ? palette.dark : palette.bg;
const frame = dark ? palette.darkCard : palette.frame;
const stroke = dark ? palette.darkMuted : palette.border;
const titleFill = dark ? palette.darkText : palette.text;
const barFill = dark ? '#0b0f13' : '#f9fafb';
return [
`<svg xmlns="http://www.w3.org/2000/svg" width="${WIDTH}" height="${height}" viewBox="0 0 ${WIDTH} ${height}" fill="none">`,
`<rect width="${WIDTH}" height="${height}" fill="${bg}" />`,
svgRect({ x: 24, y: 24, width: WIDTH - 48, height: height - 48, fill: frame, stroke, rx: 28 }),
svgRect({ x: 24, y: 24, width: WIDTH - 48, height: 56, fill: barFill, stroke, rx: 28 }),
`<rect x="24" y="52" width="${WIDTH - 48}" height="28" fill="${barFill}" />`,
`<circle cx="58" cy="52" r="7" fill="${dark ? '#ef4444' : '#fca5a5'}" />`,
`<circle cx="82" cy="52" r="7" fill="${dark ? '#f59e0b' : '#fcd34d'}" />`,
`<circle cx="106" cy="52" r="7" fill="${dark ? '#22c55e' : '#86efac'}" />`,
svgText({ x: 136, y: 58, text: title, size: 17, weight: 700, fill: titleFill }),
].join('');
}
function footer() {
return '</svg>';
}
function makeDashboard() {
const height = 1040;
const parts = [windowFrame({ title: 'Главная страница клиента', height })];
parts.push(svgText({ x: 72, y: 130, text: 'Главная', size: 30, weight: 800 }));
parts.push(svgText({ x: 72, y: 160, text: 'Быстрые действия, заказы, уведомления и бонусный контур на одном экране', size: 14, fill: palette.subtext }));
const quickActions = { x: 72, y: 196, w: 430, h: 220 };
parts.push(svgRect({ x: quickActions.x, y: quickActions.y, width: quickActions.w, height: quickActions.h }));
parts.push(cardTitle({ x: 96, y: 228, text: 'Быстрые действия', subtitle: 'Переход в каталог, заказы, профиль и бонусную программу' }));
const actionLabels = ['Каталог', 'Корзина', 'Мои заказы', 'Профиль', 'Уведомления', 'Бонусы'];
actionLabels.forEach((label, index) => {
const col = index % 3;
const row = Math.floor(index / 3);
const x = 96 + col * 108;
const y = 274 + row * 72;
parts.push(svgRect({ x, y, width: 92, height: 56, rx: 16, fill: palette.muted }));
parts.push(svgText({ x: x + 46, y: y + 33, text: label, size: 12, weight: 700, fill: palette.subtext, anchor: 'middle' }));
});
const profile = { x: 526, y: 196, w: 842, h: 220 };
parts.push(svgRect({ x: profile.x, y: profile.y, width: profile.w, height: profile.h }));
parts.push(cardTitle({ x: 550, y: 228, text: 'Сводка клиента', subtitle: 'Статус профиля, активные заявки и бонусный баланс' }));
parts.push(svgRect({ x: 550, y: 266, width: 290, height: 118, fill: palette.accent, stroke: palette.accentStroke }));
parts.push(svgText({ x: 574, y: 302, text: 'Профиль заполнен на 82%', size: 16, weight: 700 }));
parts.push(svgText({ x: 574, y: 326, text: 'Реквизиты, адреса доставки, уведомления', size: 13, fill: palette.subtext }));
parts.push(svgRect({ x: 866, y: 266, width: 222, height: 118, fill: palette.success, stroke: '#86efac' }));
parts.push(svgText({ x: 890, y: 302, text: '2 активных заказа', size: 16, weight: 700 }));
parts.push(svgText({ x: 890, y: 326, text: 'Ожидают расчет и публикацию условий', size: 13, fill: palette.subtext }));
parts.push(svgRect({ x: 1114, y: 266, width: 230, height: 118, fill: palette.warning, stroke: '#facc15' }));
parts.push(svgText({ x: 1138, y: 302, text: '12 400 бонусов', size: 16, weight: 700 }));
parts.push(svgText({ x: 1138, y: 326, text: 'Доступно для подарочных карт и вывода', size: 13, fill: palette.subtext }));
const orders = { x: 72, y: 446, w: 830, h: 500 };
parts.push(svgRect({ x: orders.x, y: orders.y, width: orders.w, height: orders.h }));
parts.push(cardTitle({ x: 96, y: 478, text: 'Последние заявки и заказы', subtitle: 'Очередь клиента с текущими статусами и действиями' }));
parts.push(svgRect({ x: 96, y: 512, width: 782, height: 52, fill: '#f9fafb', stroke: palette.border, rx: 14 }));
['Номер', 'Тип', 'Статус', 'Доставка', 'Действие'].forEach((label, index) => {
parts.push(svgText({ x: 118 + [0, 144, 338, 510, 678][index], y: 544, text: label, size: 12, weight: 700, fill: palette.subtext }));
});
for (let i = 0; i < 5; i += 1) {
const y = 576 + i * 70;
parts.push(svgRect({ x: 96, y, width: 782, height: 58, rx: 14, fill: i % 2 === 0 ? '#ffffff' : '#fbfbfb' }));
parts.push(svgText({ x: 118, y: y + 33, text: `FRG-10${i + 1}`, size: 14, weight: 700 }));
parts.push(svgText({ x: 262, y: y + 33, text: i % 2 === 0 ? 'Заказ' : 'Расчет', size: 14 }));
parts.push(pill({ x: 420, y: y + 12, width: 120, label: i % 2 === 0 ? 'В работе' : 'Нужен расчет', fill: i % 2 === 0 ? palette.success : palette.warning }));
parts.push(svgText({ x: 628, y: y + 33, text: i % 2 === 0 ? 'Москва' : 'Санкт-Петербург', size: 14 }));
parts.push(svgRect({ x: 746, y: y + 12, width: 104, height: 34, rx: 17, fill: palette.muted }));
parts.push(svgText({ x: 798, y: y + 34, text: 'Открыть', size: 13, weight: 700, fill: palette.subtext, anchor: 'middle' }));
}
const side = { x: 926, y: 446, w: 442, h: 500 };
parts.push(svgRect({ x: side.x, y: side.y, width: side.w, height: side.h }));
parts.push(cardTitle({ x: 950, y: 478, text: 'Уведомления и бонусы', subtitle: 'Информационные блоки и отдельные CTA' }));
parts.push(svgRect({ x: 950, y: 518, width: 394, height: 110, fill: palette.muted }));
parts.push(svgText({ x: 974, y: 550, text: 'Последние уведомления', size: 16, weight: 700 }));
parts.push(svgLine({ x1: 974, y1: 566, x2: 1320, y2: 566 }));
['Менеджер обновил стоимость заказа', 'Подтвержден адрес доставки', 'Создана заявка на вывод бонусов'].forEach((text, index) => {
parts.push(svgText({ x: 974, y: 590 + index * 20, text, size: 13, fill: palette.subtext }));
});
parts.push(svgRect({ x: 950, y: 652, width: 394, height: 118, fill: palette.accent, stroke: palette.accentStroke }));
parts.push(svgText({ x: 974, y: 686, text: 'Бонусный кабинет', size: 18, weight: 800 }));
parts.push(svgText({ x: 974, y: 710, text: 'Переход в отдельный интерфейс истории бонусов, карт и выводов', size: 13, fill: palette.subtext }));
parts.push(svgRect({ x: 974, y: 726, width: 156, height: 34, rx: 17, fill: palette.frame }));
parts.push(svgText({ x: 1052, y: 748, text: 'Открыть кабинет', size: 13, weight: 700, anchor: 'middle' }));
parts.push(svgRect({ x: 950, y: 794, width: 394, height: 128, fill: '#f9fafb' }));
parts.push(svgText({ x: 974, y: 826, text: 'Заполненность профиля', size: 16, weight: 700 }));
parts.push(svgRect({ x: 974, y: 846, width: 330, height: 14, rx: 7, fill: palette.muted, stroke: palette.muted }));
parts.push(svgRect({ x: 974, y: 846, width: 272, height: 14, rx: 7, fill: '#86efac', stroke: '#86efac' }));
['Реквизиты контрагента', 'Адреса доставки', 'Уведомления Telegram / Max'].forEach((text, index) => {
parts.push(svgText({ x: 974, y: 890 + index * 18, text: `${text}`, size: 13, fill: palette.subtext }));
});
parts.push(footer());
return parts.join('');
}
function makeCatalogGrid() {
const height = 900;
const parts = [windowFrame({ title: 'Каталог продукции', height })];
parts.push(svgText({ x: 72, y: 130, text: 'Каталог', size: 30, weight: 800 }));
parts.push(svgText({ x: 72, y: 160, text: 'Выбор товарного направления до перехода в детальную карточку', size: 14, fill: palette.subtext }));
parts.push(svgRect({ x: 72, y: 192, width: 820, height: 54, rx: 20, fill: '#f9fafb' }));
parts.push(svgText({ x: 104, y: 226, text: 'Поиск по типу товара', size: 14, fill: '#6b7280' }));
const labels = [
'Алюминиевый скотч',
'Армированный скотч',
'Вспененный скотч',
'Двусторонний ПП',
'Двусторонний PVC',
'Крепп',
'Металлизированный',
'Сигнальная лента',
'Упаковочный скотч',
'Цветной скотч',
];
labels.forEach((label, index) => {
const col = index % 5;
const row = Math.floor(index / 5);
const x = 72 + col * 266;
const y = 278 + row * 272;
parts.push(svgRect({ x, y, width: 226, height: 236, rx: 28 }));
parts.push(svgRect({ x: x + 16, y: y + 16, width: 194, height: 154, rx: 22, fill: palette.muted }));
parts.push(svgLine({ x1: x + 36, y1: y + 48, x2: x + 190, y2: y + 48, stroke: '#cbd5e1', strokeWidth: 10 }));
parts.push(svgLine({ x1: x + 36, y1: y + 74, x2: x + 170, y2: y + 74, stroke: '#d1d5db', strokeWidth: 10 }));
parts.push(`<circle cx="${x + 160}" cy="${y + 120}" r="28" fill="#e5e7eb" stroke="#cbd5e1" stroke-width="4"/>`);
const titleY = y + 196;
parts.push(svgText({ x: x + 16, y: titleY, text: label, size: 15, weight: 700 }));
parts.push(svgText({ x: x + 16, y: titleY + 24, text: 'Карточка товарного направления', size: 12, fill: palette.subtext }));
});
parts.push(footer());
return parts.join('');
}
function makeProductCard() {
const height = 1180;
const parts = [windowFrame({ title: 'Карточка товара', height })];
parts.push(svgText({ x: 72, y: 124, text: '← Назад', size: 14, weight: 700, fill: palette.subtext }));
parts.push(svgText({ x: 72, y: 170, text: 'Алюминиевый скотч', size: 32, weight: 800 }));
parts.push(svgText({ x: 72, y: 200, text: 'Выбор параметров, индивидуальные опции, остатки и добавление в корзину', size: 14, fill: palette.subtext }));
parts.push(svgRect({ x: 20, y: 250, width: 132, height: 280, rx: 24, fill: '#eef2f7', stroke: '#cbd5e1' }));
parts.push(svgRect({ x: 1288, y: 250, width: 132, height: 280, rx: 24, fill: '#eef2f7', stroke: '#cbd5e1' }));
parts.push(svgText({ x: 86, y: 284, text: 'Соседний', size: 12, weight: 700, fill: palette.subtext, anchor: 'middle' }));
parts.push(svgText({ x: 86, y: 304, text: 'товар', size: 12, weight: 700, fill: palette.subtext, anchor: 'middle' }));
parts.push(svgRect({ x: 38, y: 324, width: 96, height: 118, rx: 20, fill: palette.muted }));
parts.push(svgText({ x: 86, y: 468, text: 'Крепп', size: 13, weight: 700, anchor: 'middle' }));
parts.push(svgText({ x: 1354, y: 284, text: 'Следующий', size: 12, weight: 700, fill: palette.subtext, anchor: 'middle' }));
parts.push(svgText({ x: 1354, y: 304, text: 'товар', size: 12, weight: 700, fill: palette.subtext, anchor: 'middle' }));
parts.push(svgRect({ x: 1306, y: 324, width: 96, height: 118, rx: 20, fill: palette.muted }));
parts.push(svgText({ x: 1354, y: 468, text: 'PVC', size: 13, weight: 700, anchor: 'middle' }));
parts.push(svgRect({ x: 176, y: 244, width: 1088, height: 360, rx: 28 }));
parts.push(svgRect({ x: 208, y: 280, width: 300, height: 284, rx: 28, fill: palette.muted }));
parts.push(svgLine({ x1: 242, y1: 338, x2: 472, y2: 338, stroke: '#cbd5e1', strokeWidth: 14 }));
parts.push(svgLine({ x1: 242, y1: 374, x2: 438, y2: 374, stroke: '#d1d5db', strokeWidth: 14 }));
parts.push(`<circle cx="356" cy="460" r="52" fill="#f3f4f6" stroke="#cbd5e1" stroke-width="6" />`);
parts.push(svgText({ x: 542, y: 310, text: 'Параметры выбора', size: 22, weight: 800 }));
parts.push(svgText({ x: 542, y: 334, text: 'Ширина, длина, толщина, втулка, цвет и надпись', size: 13, fill: palette.subtext }));
const groups = [
{ title: 'Ширина', options: ['48 мм', '75 мм'] },
{ title: 'Длина', options: ['25 м', '50 м', '100 м'] },
{ title: 'Толщина', options: ['43 мкм', '45 мкм'] },
{ title: 'Втулка', options: ['Стандарт', 'Логотип'] },
{ title: 'Цвет', options: ['Серебристый'] },
{ title: 'Надпись', options: ['Без надписи', 'Под заказ'] },
];
groups.forEach((group, index) => {
const col = index % 2;
const row = Math.floor(index / 2);
const x = 542 + col * 250;
const y = 372 + row * 64;
parts.push(svgText({ x, y, text: group.title, size: 13, weight: 700, fill: palette.subtext }));
group.options.forEach((option, optionIndex) => {
parts.push(pill({
x: x + optionIndex * 96,
y: y + 14,
width: Math.max(84, option.length * 7.2),
label: option,
fill: optionIndex === 0 ? palette.accent : '#f9fafb',
stroke: optionIndex === 0 ? palette.accentStroke : palette.border,
textFill: optionIndex === 0 ? '#1d4ed8' : palette.subtext,
}));
});
});
parts.push(svgRect({ x: 940, y: 280, width: 292, height: 284, rx: 24, fill: '#fbfbfb' }));
parts.push(svgText({ x: 968, y: 314, text: 'SKU и действие', size: 18, weight: 800 }));
parts.push(svgText({ x: 968, y: 344, text: 'FRG-ALU-48-50', size: 16, weight: 700, fill: palette.subtext }));
parts.push(svgRect({ x: 968, y: 372, width: 236, height: 62, rx: 20, fill: palette.success, stroke: '#86efac' }));
parts.push(svgText({ x: 992, y: 408, text: 'В наличии: 2 140', size: 18, weight: 800 }));
parts.push(svgRect({ x: 968, y: 454, width: 236, height: 46, rx: 23, fill: palette.text, stroke: palette.text }));
parts.push(svgText({ x: 1086, y: 484, text: 'Добавить в корзину', size: 14, weight: 700, fill: '#ffffff', anchor: 'middle' }));
parts.push(svgText({ x: 968, y: 528, text: 'Если включены кастомные опции, под кнопкой появляется дополнительное поле заявки.', size: 12, fill: palette.subtext }));
parts.push(svgRect({ x: 176, y: 636, width: 1088, height: 194, rx: 28 }));
parts.push(cardTitle({ x: 208, y: 670, text: 'Под заказ и ограничения', subtitle: 'Пояснения по любой длине, логотипу на втулке и нанесению надписи' }));
const infoBlocks = [
['Любая длина', 'Допустимый диапазон 25150 м с шагом 5 м.'],
['Логотип на втулке', 'Доступно после согласования макета и минимального тиража.'],
['Нанесение надписи', 'Маркировка согласуется менеджером и попадает в расчет.'],
];
infoBlocks.forEach(([title, copy], index) => {
const x = 208 + index * 284;
parts.push(svgRect({ x, y: 706, width: 252, height: 92, rx: 18, fill: '#f9fafb' }));
parts.push(svgText({ x: x + 18, y: 734, text: title, size: 15, weight: 700 }));
parts.push(svgText({ x: x + 18, y: 758, text: copy, size: 12, fill: palette.subtext }));
});
parts.push(svgText({ x: 176, y: 874, text: 'Доступные варианты', size: 26, weight: 800 }));
parts.push(svgText({ x: 176, y: 902, text: 'Таблица складских вариантов с параметрами и остатками по складам', size: 13, fill: palette.subtext }));
parts.push(svgRect({ x: 176, y: 926, width: 1088, height: 196, rx: 24 }));
parts.push(svgRect({ x: 200, y: 950, width: 1040, height: 42, rx: 12, fill: '#f9fafb' }));
['SKU', 'Параметры', 'Остаток', 'Склады', 'Действие'].forEach((label, index) => {
parts.push(svgText({ x: 220 + [0, 190, 640, 790, 946][index], y: 976, text: label, size: 12, weight: 700, fill: palette.subtext }));
});
for (let i = 0; i < 3; i += 1) {
const y = 1006 + i * 38;
parts.push(svgLine({ x1: 200, y1: y, x2: 1240, y2: y }));
parts.push(svgText({ x: 220, y: y + 24, text: `FRG-ALU-${48 + i}-${50 + i * 10}`, size: 13, weight: 700 }));
parts.push(svgText({ x: 410, y: y + 24, text: `${48 + i} мм · ${50 + i * 10} м · ${43 + i} мкм`, size: 13, fill: palette.subtext }));
parts.push(pill({ x: 840, y: y + 6, width: 86, label: `${2100 - i * 300}`, fill: i === 2 ? palette.warning : palette.success }));
parts.push(svgText({ x: 1010, y: y + 24, text: i === 0 ? 'СПб / Москва' : 'СПб', size: 13, fill: palette.subtext }));
parts.push(svgRect({ x: 1122, y: y + 4, width: 98, height: 28, rx: 14, fill: '#f3f4f6' }));
parts.push(svgText({ x: 1171, y: y + 22, text: 'Выбрать', size: 12, weight: 700, fill: palette.subtext, anchor: 'middle' }));
}
parts.push(footer());
return parts.join('');
}
function makeCart() {
const height = 980;
const parts = [windowFrame({ title: 'Корзина', height })];
parts.push(svgText({ x: 72, y: 130, text: 'Корзина', size: 30, weight: 800 }));
parts.push(svgText({ x: 72, y: 160, text: 'Состав заказа, адрес доставки и отправка заявки на расчет', size: 14, fill: palette.subtext }));
parts.push(svgRect({ x: 72, y: 204, width: 916, height: 518, rx: 28 }));
parts.push(cardTitle({ x: 96, y: 238, text: 'Состав заказа', subtitle: 'Текущие позиции из каталога с параметрами и количеством' }));
parts.push(svgRect({ x: 96, y: 276, width: 868, height: 44, rx: 12, fill: '#f9fafb' }));
['Товар', 'Параметры', 'Кол-во', 'Управление'].forEach((label, index) => {
parts.push(svgText({ x: 118 + [0, 320, 640, 768][index], y: 304, text: label, size: 12, weight: 700, fill: palette.subtext }));
});
for (let i = 0; i < 4; i += 1) {
const y = 334 + i * 84;
parts.push(svgRect({ x: 96, y, width: 868, height: 68, rx: 16, fill: i % 2 === 0 ? '#ffffff' : '#fbfbfb' }));
parts.push(svgText({ x: 118, y: y + 28, text: i % 2 === 0 ? 'Упаковочный скотч' : 'Алюминиевый скотч', size: 15, weight: 700 }));
parts.push(svgText({ x: 118, y: y + 48, text: `FRG-10${i + 1}`, size: 12, fill: palette.subtext }));
parts.push(svgText({ x: 438, y: y + 34, text: '48 мм · 50 м · 43 мкм · прозрачный', size: 13, fill: palette.subtext }));
parts.push(svgRect({ x: 736, y: y + 18, width: 70, height: 30, rx: 15, fill: '#f3f4f6' }));
parts.push(svgText({ x: 771, y: y + 38, text: String(i + 1), size: 13, weight: 700, anchor: 'middle' }));
parts.push(svgRect({ x: 830, y: y + 18, width: 108, height: 30, rx: 15, fill: palette.muted }));
parts.push(svgText({ x: 884, y: y + 38, text: 'Изменить', size: 12, weight: 700, fill: palette.subtext, anchor: 'middle' }));
}
parts.push(svgRect({ x: 1012, y: 204, width: 356, height: 518, rx: 28, fill: '#fbfbfb' }));
parts.push(cardTitle({ x: 1038, y: 238, text: 'Оформление', subtitle: 'Проверка профиля клиента и отправка заявки' }));
parts.push(svgRect({ x: 1038, y: 278, width: 304, height: 84, rx: 18, fill: palette.warning, stroke: '#facc15' }));
parts.push(svgText({ x: 1060, y: 310, text: 'Профиль контрагента заполнен', size: 16, weight: 700 }));
parts.push(svgText({ x: 1060, y: 334, text: 'Если профиль неполный, здесь показывается предупреждение.', size: 12, fill: palette.subtext }));
parts.push(svgText({ x: 1038, y: 398, text: 'Адрес доставки', size: 15, weight: 700 }));
for (let i = 0; i < 3; i += 1) {
const y = 420 + i * 78;
parts.push(svgRect({ x: 1038, y, width: 304, height: 62, rx: 18, fill: i === 0 ? palette.accent : '#ffffff', stroke: i === 0 ? palette.accentStroke : palette.border }));
parts.push(`<circle cx="1064" cy="${y + 30}" r="9" fill="${i === 0 ? '#3b82f6' : '#ffffff'}" stroke="${i === 0 ? '#3b82f6' : '#9ca3af'}" stroke-width="2"/>`);
if (i === 0) {
parts.push(`<circle cx="1064" cy="${y + 30}" r="4" fill="#ffffff" />`);
}
parts.push(svgText({ x: 1084, y: y + 26, text: i === 0 ? 'Основной склад клиента' : `Адрес доставки ${i + 1}`, size: 13, weight: 700 }));
parts.push(svgText({ x: 1084, y: y + 46, text: 'Москва, улица и зона разгрузки', size: 12, fill: palette.subtext }));
}
parts.push(svgRect({ x: 1038, y: 676, width: 304, height: 44, rx: 22, fill: palette.text, stroke: palette.text }));
parts.push(svgText({ x: 1190, y: 704, text: 'Оформить заявку', size: 14, weight: 700, fill: '#ffffff', anchor: 'middle' }));
parts.push(svgRect({ x: 72, y: 752, width: 1296, height: 152, rx: 28 }));
parts.push(cardTitle({ x: 96, y: 786, text: 'Комментарий и итоговая сводка', subtitle: 'Дополнительные инструкции клиента и итог по количеству позиций' }));
parts.push(svgRect({ x: 96, y: 820, width: 900, height: 52, rx: 18, fill: '#f9fafb', dashed: true }));
parts.push(svgText({ x: 120, y: 852, text: 'Комментарий клиента к заказу / пожелания по доставке', size: 13, fill: palette.subtext }));
parts.push(svgRect({ x: 1026, y: 820, width: 318, height: 52, rx: 18, fill: palette.success, stroke: '#86efac' }));
parts.push(svgText({ x: 1050, y: 852, text: '4 позиции в корзине, 2 уникальных SKU, адрес выбран', size: 13, weight: 700 }));
parts.push(footer());
return parts.join('');
}
function makeClientOrder() {
const height = 1040;
const parts = [windowFrame({ title: 'Карточка заказа клиента', height })];
parts.push(svgText({ x: 72, y: 124, text: '← Назад к заказам', size: 14, weight: 700, fill: palette.subtext }));
parts.push(svgText({ x: 72, y: 168, text: 'Заказ FRG-1042', size: 32, weight: 800 }));
parts.push(svgText({ x: 72, y: 196, text: 'Клиент видит только статус, состав, условия поставки и историю изменений', size: 14, fill: palette.subtext }));
parts.push(svgRect({ x: 72, y: 234, width: 1296, height: 132, rx: 28, fill: palette.accent, stroke: palette.accentStroke }));
parts.push(cardTitle({ x: 98, y: 270, text: 'Статусная линия заказа', subtitle: 'Создан → Нужен расчет → Условия опубликованы → Подтвержден → Отгружен' }));
const steps = ['Создан', 'Расчет', 'Условия', 'Подтвержден', 'Отгрузка'];
steps.forEach((step, index) => {
const x = 132 + index * 240;
parts.push(`<circle cx="${x}" cy="320" r="18" fill="${index < 3 ? '#3b82f6' : '#ffffff'}" stroke="${index < 3 ? '#3b82f6' : '#9ca3af'}" stroke-width="3"/>`);
parts.push(svgText({ x, y: 353, text: step, size: 12, weight: 700, fill: palette.subtext, anchor: 'middle' }));
if (index < steps.length - 1) {
parts.push(svgLine({ x1: x + 18, y1: 320, x2: x + 222, y2: 320, stroke: index < 2 ? '#60a5fa' : '#d1d5db', strokeWidth: 6 }));
}
});
parts.push(svgRect({ x: 72, y: 396, width: 846, height: 420, rx: 28 }));
parts.push(cardTitle({ x: 96, y: 430, text: 'Состав заказа', subtitle: 'Позиции, параметры и количество без менеджерских внутренних полей' }));
parts.push(svgRect({ x: 96, y: 468, width: 798, height: 42, rx: 12, fill: '#f9fafb' }));
['Товар', 'SKU', 'Параметры', 'Кол-во'].forEach((label, index) => {
parts.push(svgText({ x: 118 + [0, 240, 420, 720][index], y: 494, text: label, size: 12, weight: 700, fill: palette.subtext }));
});
for (let i = 0; i < 4; i += 1) {
const y = 522 + i * 64;
parts.push(svgLine({ x1: 96, y1: y, x2: 894, y2: y }));
parts.push(svgText({ x: 118, y: y + 28, text: i % 2 === 0 ? 'Упаковочный скотч' : 'Алюминиевый скотч', size: 14, weight: 700 }));
parts.push(svgText({ x: 358, y: y + 28, text: `FRG-20${i + 1}`, size: 13, fill: palette.subtext }));
parts.push(svgText({ x: 538, y: y + 28, text: '48 мм · 50 м · 43 мкм', size: 13, fill: palette.subtext }));
parts.push(svgText({ x: 794, y: y + 28, text: String(i + 1), size: 13, weight: 700 }));
}
parts.push(svgRect({ x: 948, y: 396, width: 420, height: 420, rx: 28, fill: '#fbfbfb' }));
parts.push(cardTitle({ x: 972, y: 430, text: 'Условия и доставка', subtitle: 'Появляются после публикации менеджером' }));
parts.push(svgRect({ x: 972, y: 468, width: 372, height: 92, rx: 18, fill: palette.success, stroke: '#86efac' }));
parts.push(svgText({ x: 996, y: 500, text: 'Стоимость опубликована', size: 17, weight: 800 }));
parts.push(svgText({ x: 996, y: 524, text: 'Цена зафиксирована и доступна клиенту в карточке заказа.', size: 12, fill: palette.subtext }));
['Доставка: Санкт-Петербург → Москва', 'Адрес: Основной склад клиента', 'Комментарий менеджера: подтверждены сроки 35 дней'].forEach((text, index) => {
parts.push(svgText({ x: 972, y: 604 + index * 28, text: `${text}`, size: 13, fill: palette.subtext }));
});
parts.push(svgRect({ x: 972, y: 704, width: 372, height: 88, rx: 18, fill: '#f9fafb' }));
parts.push(svgText({ x: 996, y: 736, text: 'История статусов', size: 16, weight: 700 }));
['Создан клиентом', 'Передан менеджеру', 'Условия опубликованы'].forEach((text, index) => {
parts.push(svgText({ x: 996, y: 760 + index * 18, text, size: 12, fill: palette.subtext }));
});
parts.push(svgRect({ x: 72, y: 846, width: 1296, height: 126, rx: 28 }));
parts.push(cardTitle({ x: 96, y: 880, text: 'Системные комментарии и события', subtitle: 'Журнал изменений доступный клиенту' }));
['Менеджер обновил условия поставки', 'Клиент подтвердил получение условий', 'Система отправила уведомление в Telegram'].forEach((text, index) => {
parts.push(svgText({ x: 96, y: 918 + index * 18, text: `${text}`, size: 13, fill: palette.subtext }));
});
parts.push(footer());
return parts.join('');
}
function makeManagerOrders() {
const height = 980;
const parts = [windowFrame({ title: 'Список заказов менеджера', height })];
parts.push(svgText({ x: 72, y: 130, text: 'Заказы', size: 30, weight: 800 }));
parts.push(svgText({ x: 72, y: 160, text: 'Фильтрация очереди, приоритеты и переход в карточку обработки', size: 14, fill: palette.subtext }));
parts.push(svgRect({ x: 72, y: 198, width: 1296, height: 108, rx: 28 }));
parts.push(cardTitle({ x: 96, y: 234, text: 'Фильтры и быстрые статусы', subtitle: 'Статус, клиент, период, город доставки и приоритет' }));
const filterLabels = ['Статус', 'Клиент', 'Период', 'Город', 'Приоритет'];
filterLabels.forEach((label, index) => {
const x = 96 + index * 246;
parts.push(svgText({ x, y: 270, text: label, size: 12, weight: 700, fill: palette.subtext }));
parts.push(svgRect({ x, y: 278, width: 210, height: 32, rx: 16, fill: '#f9fafb' }));
});
const stats = [
['Новые', '14', palette.warning, '#facc15'],
['Нужен расчет', '9', palette.danger, '#fca5a5'],
['Условия опубликованы', '18', palette.success, '#86efac'],
['Ожидают отгрузку', '7', palette.accent, palette.accentStroke],
];
stats.forEach(([title, value, fill, stroke], index) => {
const x = 72 + index * 324;
parts.push(svgRect({ x, y: 338, width: 300, height: 90, rx: 24, fill, stroke }));
parts.push(svgText({ x: x + 24, y: 372, text: title, size: 15, weight: 700 }));
parts.push(svgText({ x: x + 24, y: 404, text: value, size: 28, weight: 800 }));
});
parts.push(svgRect({ x: 72, y: 458, width: 1296, height: 440, rx: 28 }));
parts.push(cardTitle({ x: 96, y: 492, text: 'Таблица заказов', subtitle: 'Основная рабочая очередь менеджера' }));
parts.push(svgRect({ x: 96, y: 530, width: 1248, height: 44, rx: 12, fill: '#f9fafb' }));
['Номер', 'Клиент', 'Статус', 'Доставка', 'Сумма', 'Действие'].forEach((label, index) => {
parts.push(svgText({ x: 118 + [0, 184, 470, 706, 946, 1098][index], y: 558, text: label, size: 12, weight: 700, fill: palette.subtext }));
});
for (let i = 0; i < 6; i += 1) {
const y = 590 + i * 50;
parts.push(svgLine({ x1: 96, y1: y, x2: 1344, y2: y }));
parts.push(svgText({ x: 118, y: y + 30, text: `FRG-20${30 + i}`, size: 14, weight: 700 }));
parts.push(svgText({ x: 302, y: y + 30, text: `ООО Клиент ${i + 1}`, size: 13, fill: palette.subtext }));
parts.push(pill({
x: 556,
y: y + 10,
width: 132,
label: i % 2 === 0 ? 'Нужен расчет' : 'Условия готовы',
fill: i % 2 === 0 ? palette.warning : palette.success,
}));
parts.push(svgText({ x: 824, y: y + 30, text: i % 2 === 0 ? 'Москва' : 'СПб → Москва', size: 13, fill: palette.subtext }));
parts.push(svgText({ x: 1064, y: y + 30, text: i % 2 === 0 ? '—' : '145 000 ₽', size: 13, weight: 700 }));
parts.push(svgRect({ x: 1160, y: y + 10, width: 136, height: 30, rx: 15, fill: palette.muted }));
parts.push(svgText({ x: 1228, y: y + 30, text: 'Открыть карточку', size: 12, weight: 700, fill: palette.subtext, anchor: 'middle' }));
}
parts.push(footer());
return parts.join('');
}
function makeManagerOrder() {
const height = 1100;
const parts = [windowFrame({ title: 'Карточка заказа менеджера', height })];
parts.push(svgText({ x: 72, y: 124, text: '← Назад к заказам', size: 14, weight: 700, fill: palette.subtext }));
parts.push(svgText({ x: 72, y: 168, text: 'Заказ FRG-2034', size: 32, weight: 800 }));
parts.push(svgText({ x: 72, y: 196, text: 'Обработка условий, доставки, бонусных эффектов и журнал событий', size: 14, fill: palette.subtext }));
parts.push(svgRect({ x: 72, y: 232, width: 1296, height: 126, rx: 28, fill: palette.accent, stroke: palette.accentStroke }));
parts.push(cardTitle({ x: 96, y: 266, text: 'Панель статуса и действий', subtitle: 'Переключение статуса, публикация цены, фиксация доставки' }));
const actionX = [842, 1010, 1178];
['Опубликовать условия', 'Запросить уточнение', 'Подтвердить отгрузку'].forEach((label, index) => {
parts.push(svgRect({ x: actionX[index], y: 260, width: 150, height: 42, rx: 21, fill: index === 0 ? palette.text : palette.frame, stroke: index === 0 ? palette.text : palette.border }));
parts.push(svgText({ x: actionX[index] + 75, y: 287, text: label, size: 12, weight: 700, fill: index === 0 ? '#ffffff' : palette.subtext, anchor: 'middle' }));
});
parts.push(svgText({ x: 96, y: 320, text: 'Текущий статус: нужен расчет → следующий шаг: публикация условий клиенту', size: 13, weight: 700 }));
parts.push(svgRect({ x: 72, y: 388, width: 780, height: 470, rx: 28 }));
parts.push(cardTitle({ x: 96, y: 422, text: 'Состав заказа и расчет', subtitle: 'Позиции клиента, параметры, ручной расчет и итоговая публикация' }));
parts.push(svgRect({ x: 96, y: 460, width: 732, height: 42, rx: 12, fill: '#f9fafb' }));
['Товар', 'Параметры', 'Кол-во', 'Цена', 'Итог'].forEach((label, index) => {
parts.push(svgText({ x: 118 + [0, 240, 492, 586, 678][index], y: 488, text: label, size: 12, weight: 700, fill: palette.subtext }));
});
for (let i = 0; i < 4; i += 1) {
const y = 514 + i * 60;
parts.push(svgLine({ x1: 96, y1: y, x2: 828, y2: y }));
parts.push(svgText({ x: 118, y: y + 28, text: i % 2 === 0 ? 'Упаковочный скотч' : 'Вспененный скотч', size: 14, weight: 700 }));
parts.push(svgText({ x: 358, y: y + 28, text: '48 мм · 50 м · стандарт', size: 13, fill: palette.subtext }));
parts.push(svgText({ x: 612, y: y + 28, text: String(i + 1), size: 13, weight: 700 }));
parts.push(svgRect({ x: 662, y: y + 10, width: 68, height: 26, rx: 13, fill: '#f9fafb' }));
parts.push(svgText({ x: 696, y: y + 28, text: 'цена', size: 11, fill: palette.subtext, anchor: 'middle' }));
parts.push(svgText({ x: 776, y: y + 28, text: i % 2 === 0 ? '24 000' : '18 500', size: 12, weight: 700 }));
}
parts.push(svgRect({ x: 96, y: 772, width: 732, height: 62, rx: 18, fill: palette.success, stroke: '#86efac' }));
parts.push(svgText({ x: 120, y: 806, text: 'Блок публикации итоговых условий: сумма, комментарий, сроки, вид доставки', size: 14, weight: 700 }));
parts.push(svgRect({ x: 882, y: 388, width: 486, height: 470, rx: 28, fill: '#fbfbfb' }));
parts.push(cardTitle({ x: 906, y: 422, text: 'Доставка и коммуникации', subtitle: 'Адрес, стоимость логистики, комментарии и история сообщений' }));
parts.push(svgRect({ x: 906, y: 460, width: 438, height: 88, rx: 18, fill: '#f9fafb' }));
['Адрес доставки клиента', 'Стоимость доставки / самовывоз', 'Окно разгрузки и ограничения'].forEach((text, index) => {
parts.push(svgText({ x: 930, y: 492 + index * 20, text: `${text}`, size: 13, fill: palette.subtext }));
});
parts.push(svgRect({ x: 906, y: 576, width: 438, height: 112, rx: 18, fill: palette.warning, stroke: '#facc15' }));
parts.push(svgText({ x: 930, y: 610, text: 'Влияние на бонусный контур', size: 16, weight: 700 }));
['Начислить бонусы после подтверждения', 'Проверить реферальную привязку', 'Показать менеджеру связанный бонусный счет'].forEach((text, index) => {
parts.push(svgText({ x: 930, y: 636 + index * 18, text: `${text}`, size: 12, fill: palette.subtext }));
});
parts.push(svgRect({ x: 906, y: 716, width: 438, height: 118, rx: 18, fill: '#ffffff' }));
parts.push(svgText({ x: 930, y: 748, text: 'Журнал событий', size: 16, weight: 700 }));
['Менеджер открыл заказ', 'Клиент уточнил параметры товара', 'Система создала уведомление о расчете'].forEach((text, index) => {
parts.push(svgText({ x: 930, y: 774 + index * 18, text: text, size: 12, fill: palette.subtext }));
});
parts.push(svgRect({ x: 72, y: 888, width: 1296, height: 144, rx: 28 }));
parts.push(cardTitle({ x: 96, y: 922, text: 'Внутренние комментарии и системные интеграции', subtitle: 'Заметки менеджера, данные для 1С и служебные идентификаторы' }));
parts.push(svgRect({ x: 96, y: 956, width: 864, height: 48, rx: 16, fill: '#f9fafb', dashed: true }));
parts.push(svgText({ x: 120, y: 986, text: 'Поле комментария менеджера к заказу / служебные заметки', size: 13, fill: palette.subtext }));
parts.push(svgRect({ x: 990, y: 956, width: 354, height: 48, rx: 16, fill: palette.muted }));
parts.push(svgText({ x: 1014, y: 986, text: 'Статус синхронизации с 1С / внешний идентификатор', size: 13, fill: palette.subtext }));
parts.push(footer());
return parts.join('');
}
function makeBonusCabinet() {
const height = 980;
const parts = [windowFrame({ title: 'Бонусный кабинет клиента', height, dark: true })];
parts.push(svgText({ x: 72, y: 132, text: 'Чёрный кабинет бонусной программы', size: 30, weight: 800, fill: palette.darkText }));
parts.push(svgText({ x: 72, y: 162, text: 'Отдельный контур для бонусного баланса, истории, карт и заявок на вывод', size: 14, fill: palette.darkSubtext }));
parts.push(svgRect({ x: 72, y: 198, width: 820, height: 220, rx: 28, fill: palette.darkCard, stroke: palette.darkMuted }));
parts.push(svgText({ x: 96, y: 232, text: 'Аккаунт клиента', size: 18, weight: 700, fill: palette.darkText }));
parts.push(svgText({ x: 96, y: 258, text: 'Имя пользователя, пояснение по программе и статус подключения', size: 13, fill: palette.darkSubtext }));
parts.push(svgText({ x: 96, y: 332, text: '12 400', size: 58, weight: 900, fill: palette.darkText }));
parts.push(svgText({ x: 96, y: 364, text: 'доступный баланс', size: 15, weight: 700, fill: palette.darkSubtext }));
const darkStats = [
['Рефералы', '8'],
['Начисления', '42'],
['Выводы', '3'],
];
darkStats.forEach(([title, value], index) => {
const x = 454 + index * 128;
parts.push(svgRect({ x, y: 294, width: 110, height: 88, rx: 18, fill: '#0f1419', stroke: palette.darkMuted }));
parts.push(svgText({ x: x + 18, y: 326, text: title, size: 12, weight: 700, fill: palette.darkSubtext }));
parts.push(svgText({ x: x + 18, y: 356, text: value, size: 26, weight: 800, fill: palette.darkText }));
});
parts.push(svgRect({ x: 918, y: 198, width: 450, height: 220, rx: 28, fill: palette.darkCard, stroke: palette.darkMuted }));
parts.push(svgText({ x: 944, y: 232, text: 'Вывод бонусов', size: 18, weight: 700, fill: palette.darkText }));
parts.push(svgText({ x: 944, y: 258, text: 'Форма подачи заявки с проверкой минимального порога и доступного баланса', size: 13, fill: palette.darkSubtext }));
parts.push(svgRect({ x: 944, y: 292, width: 398, height: 44, rx: 14, fill: '#0f1419', stroke: palette.darkMuted }));
parts.push(svgText({ x: 968, y: 320, text: 'Сумма заявки на вывод', size: 13, fill: '#93a2b5' }));
parts.push(svgRect({ x: 944, y: 354, width: 398, height: 48, rx: 24, fill: '#e5f7ea', stroke: '#86efac' }));
parts.push(svgText({ x: 1143, y: 384, text: 'Подать заявку на вывод', size: 14, weight: 700, fill: '#0f172a', anchor: 'middle' }));
parts.push(svgRect({ x: 72, y: 448, width: 666, height: 470, rx: 28, fill: palette.darkCard, stroke: palette.darkMuted }));
parts.push(svgText({ x: 96, y: 482, text: 'История бонусных операций', size: 18, weight: 700, fill: palette.darkText }));
parts.push(svgText({ x: 96, y: 508, text: 'Начисления, списания и ссылки на связанные заказы', size: 13, fill: palette.darkSubtext }));
for (let i = 0; i < 5; i += 1) {
const y = 536 + i * 74;
parts.push(svgRect({ x: 96, y, width: 618, height: 58, rx: 18, fill: i % 2 === 0 ? '#11181f' : '#0f1419', stroke: palette.darkMuted, strokeWidth: 1 }));
parts.push(svgText({ x: 120, y: y + 28, text: `+${(i + 2) * 500} бонусов`, size: 15, weight: 700, fill: palette.darkText }));
parts.push(svgText({ x: 120, y: y + 46, text: 'Начисление за подтвержденный заказ / ручная транзакция', size: 12, fill: palette.darkSubtext }));
parts.push(svgText({ x: 580, y: y + 34, text: 'Открыть заказ', size: 12, weight: 700, fill: '#b8d4ff' }));
}
parts.push(svgRect({ x: 764, y: 448, width: 604, height: 470, rx: 28, fill: palette.darkCard, stroke: palette.darkMuted }));
parts.push(svgText({ x: 788, y: 482, text: 'Магазин наград и активные выводы', size: 18, weight: 700, fill: palette.darkText }));
parts.push(svgText({ x: 788, y: 508, text: 'Подарочные карты и блок очереди заявок на вывод', size: 13, fill: palette.darkSubtext }));
for (let i = 0; i < 3; i += 1) {
const x = 788 + i * 184;
parts.push(svgRect({ x, y: 536, width: 160, height: 160, rx: 22, fill: '#0f1419', stroke: palette.darkMuted }));
parts.push(svgRect({ x: x + 18, y: 556, width: 124, height: 72, rx: 16, fill: '#1f2937', stroke: palette.darkMuted }));
parts.push(svgText({ x: x + 18, y: 650, text: i === 0 ? 'Ozon' : i === 1 ? 'Wildberries' : 'М.Видео', size: 14, weight: 700, fill: palette.darkText }));
parts.push(svgText({ x: x + 18, y: 672, text: `${(i + 3) * 1000} бонусов`, size: 12, fill: palette.darkSubtext }));
}
parts.push(svgRect({ x: 788, y: 724, width: 556, height: 164, rx: 22, fill: '#0f1419', stroke: palette.darkMuted }));
parts.push(svgText({ x: 812, y: 756, text: 'Активные заявки на вывод', size: 16, weight: 700, fill: palette.darkText }));
['Заявка #1 · 1 500 бонусов · на проверке', 'Заявка #2 · 3 000 бонусов · подтверждена', 'Заявка #3 · 500 бонусов · отклонена'].forEach((text, index) => {
parts.push(svgText({ x: 812, y: 788 + index * 24, text, size: 12, fill: palette.darkSubtext }));
});
parts.push(footer());
return parts.join('');
}
function makeLogin() {
const height = 860;
const parts = [windowFrame({ title: 'Логин и подключение', height })];
parts.push(svgRect({ x: 110, y: 126, width: 1220, height: 658, rx: 36, fill: '#ffffff', stroke: palette.border }));
parts.push(svgRect({ x: 110, y: 126, width: 486, height: 658, rx: 36, fill: palette.accent, stroke: palette.accentStroke }));
parts.push(svgText({ x: 156, y: 210, text: 'Личный кабинет Фрегат', size: 34, weight: 800 }));
parts.push(svgText({ x: 156, y: 246, text: 'Вход по коду, заявка на подключение и быстрый выбор канала авторизации.', size: 15, fill: palette.subtext }));
['Каталог готовой продукции', 'Индивидуальный расчет', 'Заказы, бонусы, уведомления'].forEach((text, index) => {
parts.push(svgText({ x: 156, y: 330 + index * 34, text: `${text}`, size: 15, fill: palette.subtext }));
});
parts.push(svgRect({ x: 156, y: 480, width: 360, height: 220, rx: 28, fill: '#ffffff', stroke: palette.accentStroke }));
parts.push(svgText({ x: 184, y: 522, text: 'Поясняющий блок', size: 18, weight: 700 }));
parts.push(svgText({ x: 184, y: 552, text: 'Описание сценария для нового клиента:', size: 13, fill: palette.subtext }));
['Оставить заявку на подключение', 'Дождаться проверки менеджером', 'Получить код и войти в кабинет'].forEach((text, index) => {
parts.push(svgText({ x: 184, y: 588 + index * 24, text: text, size: 13, fill: palette.subtext }));
});
parts.push(svgRect({ x: 650, y: 172, width: 580, height: 566, rx: 30, fill: '#fbfbfb' }));
parts.push(svgText({ x: 694, y: 228, text: 'Вход', size: 32, weight: 800 }));
parts.push(svgText({ x: 694, y: 258, text: 'Введите номер телефона или email для получения кода доступа.', size: 14, fill: palette.subtext }));
['Телефон / Email', 'Код подтверждения'].forEach((label, index) => {
const y = 318 + index * 96;
parts.push(svgText({ x: 694, y, text: label, size: 13, weight: 700, fill: palette.subtext }));
parts.push(svgRect({ x: 694, y: y + 14, width: 492, height: 48, rx: 16, fill: '#ffffff' }));
});
parts.push(svgRect({ x: 694, y: 520, width: 492, height: 48, rx: 24, fill: palette.text, stroke: palette.text }));
parts.push(svgText({ x: 940, y: 550, text: 'Получить код / Войти', size: 15, weight: 700, fill: '#ffffff', anchor: 'middle' }));
parts.push(svgText({ x: 694, y: 606, text: 'Альтернативные каналы входа', size: 13, weight: 700, fill: palette.subtext }));
['Telegram', 'Max', 'Приглашение от менеджера'].forEach((label, index) => {
parts.push(svgRect({ x: 694 + index * 164, y: 626, width: 148, height: 42, rx: 21, fill: palette.muted }));
parts.push(svgText({ x: 768 + index * 164, y: 653, text: label, size: 13, weight: 700, fill: palette.subtext, anchor: 'middle' }));
});
parts.push(svgText({ x: 694, y: 708, text: 'Ссылка: оставить самостоятельную заявку на подключение', size: 13, weight: 700, fill: '#2563eb' }));
parts.push(footer());
return parts.join('');
}
function makeProfile() {
const height = 980;
const parts = [windowFrame({ title: 'Профиль клиента', height })];
parts.push(svgText({ x: 72, y: 130, text: 'Профиль', size: 30, weight: 800 }));
parts.push(svgText({ x: 72, y: 160, text: 'Базовые данные, контрагент, адреса доставки и уведомления', size: 14, fill: palette.subtext }));
parts.push(svgRect({ x: 72, y: 198, width: 282, height: 700, rx: 28, fill: '#fbfbfb' }));
parts.push(cardTitle({ x: 98, y: 234, text: 'Навигация профиля', subtitle: 'Внутренние подразделы клиента' }));
['Основные данные', 'Контрагент', 'Адреса доставки', 'Уведомления'].forEach((item, index) => {
parts.push(svgRect({ x: 98, y: 278 + index * 66, width: 230, height: 46, rx: 16, fill: index === 0 ? palette.accent : '#ffffff', stroke: index === 0 ? palette.accentStroke : palette.border }));
parts.push(svgText({ x: 122, y: 307 + index * 66, text: item, size: 14, weight: 700, fill: index === 0 ? '#1d4ed8' : palette.subtext }));
});
parts.push(svgRect({ x: 384, y: 198, width: 984, height: 700, rx: 28 }));
parts.push(cardTitle({ x: 412, y: 234, text: 'Карточка пользователя', subtitle: 'Редактирование данных и контроль заполненности профиля' }));
const fields = [
['ФИО', 'Руслан Бакиев'],
['Телефон', '+7 9xx xxx-xx-xx'],
['Email', 'client@fregat.ru'],
['Должность', 'Руководитель закупок'],
];
fields.forEach(([label, value], index) => {
const col = index % 2;
const row = Math.floor(index / 2);
const x = 412 + col * 470;
const y = 286 + row * 112;
parts.push(svgText({ x, y, text: label, size: 13, weight: 700, fill: palette.subtext }));
parts.push(svgRect({ x, y: y + 14, width: 430, height: 52, rx: 16, fill: '#f9fafb' }));
parts.push(svgText({ x: x + 20, y: y + 46, text: value, size: 14 }));
});
parts.push(svgRect({ x: 412, y: 534, width: 430, height: 122, rx: 22, fill: palette.success, stroke: '#86efac' }));
parts.push(svgText({ x: 438, y: 570, text: 'Статус заполненности', size: 18, weight: 700 }));
parts.push(svgRect({ x: 438, y: 592, width: 320, height: 14, rx: 7, fill: palette.muted, stroke: palette.muted }));
parts.push(svgRect({ x: 438, y: 592, width: 272, height: 14, rx: 7, fill: '#86efac', stroke: '#86efac' }));
parts.push(svgText({ x: 438, y: 628, text: '82% заполнено. Не хватает банковских реквизитов и второго адреса.', size: 12, fill: palette.subtext }));
parts.push(svgRect({ x: 882, y: 534, width: 430, height: 122, rx: 22, fill: palette.warning, stroke: '#facc15' }));
parts.push(svgText({ x: 908, y: 570, text: 'Быстрые переходы', size: 18, weight: 700 }));
['Редактировать контрагента', 'Открыть адреса доставки', 'Настроить уведомления'].forEach((text, index) => {
parts.push(svgText({ x: 908, y: 602 + index * 18, text: `${text}`, size: 12, fill: palette.subtext }));
});
parts.push(svgRect({ x: 412, y: 712, width: 900, height: 62, rx: 20, fill: palette.text, stroke: palette.text }));
parts.push(svgText({ x: 862, y: 751, text: 'Сохранить изменения', size: 15, weight: 700, fill: '#ffffff', anchor: 'middle' }));
parts.push(footer());
return parts.join('');
}
function makeClientList() {
const height = 920;
const parts = [windowFrame({ title: 'Список клиентов', height })];
parts.push(svgText({ x: 72, y: 130, text: 'Клиенты', size: 30, weight: 800 }));
parts.push(svgText({ x: 72, y: 160, text: 'Поиск компаний, переход в карточку клиента и приглашение нового пользователя', size: 14, fill: palette.subtext }));
parts.push(svgRect({ x: 72, y: 198, width: 1296, height: 92, rx: 28 }));
['Поиск по компании', 'Менеджер', 'Статус', 'Город'].forEach((label, index) => {
const x = 96 + index * 270;
parts.push(svgText({ x, y: 232, text: label, size: 12, weight: 700, fill: palette.subtext }));
parts.push(svgRect({ x, y: 242, width: 230, height: 32, rx: 16, fill: '#f9fafb' }));
});
parts.push(svgRect({ x: 1160, y: 232, width: 184, height: 42, rx: 21, fill: palette.text, stroke: palette.text }));
parts.push(svgText({ x: 1252, y: 258, text: 'Пригласить клиента', size: 13, weight: 700, fill: '#ffffff', anchor: 'middle' }));
parts.push(svgRect({ x: 72, y: 320, width: 1296, height: 520, rx: 28 }));
parts.push(cardTitle({ x: 96, y: 354, text: 'Клиентская база', subtitle: 'Карточка компании, активность, заказы и бонусный статус' }));
parts.push(svgRect({ x: 96, y: 390, width: 1248, height: 42, rx: 12, fill: '#f9fafb' }));
['Компания', 'Контакт', 'Заказы', 'Бонусы', 'Статус', 'Действие'].forEach((label, index) => {
parts.push(svgText({ x: 118 + [0, 282, 576, 738, 908, 1088][index], y: 416, text: label, size: 12, weight: 700, fill: palette.subtext }));
});
for (let i = 0; i < 6; i += 1) {
const y = 446 + i * 58;
parts.push(svgLine({ x1: 96, y1: y, x2: 1344, y2: y }));
parts.push(svgText({ x: 118, y: y + 30, text: `ООО Клиент ${i + 1}`, size: 14, weight: 700 }));
parts.push(svgText({ x: 400, y: y + 30, text: 'Имя пользователя / телефон', size: 13, fill: palette.subtext }));
parts.push(svgText({ x: 694, y: y + 30, text: String(4 + i), size: 13, weight: 700 }));
parts.push(svgText({ x: 856, y: y + 30, text: `${(i + 1) * 1200}`, size: 13, weight: 700 }));
parts.push(pill({ x: 958, y: y + 10, width: 104, label: i % 2 === 0 ? 'Активен' : 'На проверке', fill: i % 2 === 0 ? palette.success : palette.warning }));
parts.push(svgRect({ x: 1104, y: y + 8, width: 132, height: 30, rx: 15, fill: palette.muted }));
parts.push(svgText({ x: 1170, y: y + 28, text: 'Открыть карточку', size: 12, weight: 700, fill: palette.subtext, anchor: 'middle' }));
}
parts.push(footer());
return parts.join('');
}
function makeClientCard() {
const height = 1040;
const parts = [windowFrame({ title: 'Карточка клиента', height })];
parts.push(svgText({ x: 72, y: 124, text: '← Назад к клиентам', size: 14, weight: 700, fill: palette.subtext }));
parts.push(svgText({ x: 72, y: 168, text: 'ООО Клиент 1', size: 32, weight: 800 }));
parts.push(svgText({ x: 72, y: 196, text: 'Компания, реквизиты, заказы, бонусы и реферальные связи клиента', size: 14, fill: palette.subtext }));
parts.push(svgRect({ x: 72, y: 234, width: 412, height: 320, rx: 28, fill: '#fbfbfb' }));
parts.push(cardTitle({ x: 98, y: 268, text: 'Карточка компании', subtitle: 'Юридические и контактные данные' }));
['ИНН / КПП', 'Менеджер', 'Телефон', 'Email', 'Город', 'Дата регистрации'].forEach((label, index) => {
parts.push(svgText({ x: 98, y: 308 + index * 34, text: `${label}: значение`, size: 13, fill: palette.subtext }));
});
parts.push(svgRect({ x: 514, y: 234, width: 412, height: 320, rx: 28 }));
parts.push(cardTitle({ x: 540, y: 268, text: 'Контрагент и доставка', subtitle: 'Реквизиты и адреса клиента' }));
['Банковские реквизиты', 'Юридический адрес', 'Адрес доставки #1', 'Адрес доставки #2'].forEach((label, index) => {
parts.push(svgRect({ x: 540, y: 298 + index * 58, width: 360, height: 40, rx: 14, fill: '#f9fafb' }));
parts.push(svgText({ x: 560, y: 324 + index * 58, text: label, size: 13, fill: palette.subtext }));
});
parts.push(svgRect({ x: 956, y: 234, width: 412, height: 320, rx: 28, fill: palette.warning, stroke: '#facc15' }));
parts.push(cardTitle({ x: 982, y: 268, text: 'Бонусный контур', subtitle: 'Связанные бонусные сущности клиента' }));
['Баланс: 12 400', 'Рефералы: 8', 'Активные выводы: 2', 'Переход в бонусный кабинет менеджера'].forEach((label, index) => {
parts.push(svgText({ x: 982, y: 308 + index * 34, text: label, size: 13, fill: palette.subtext }));
});
parts.push(svgRect({ x: 72, y: 584, width: 1296, height: 384, rx: 28 }));
parts.push(cardTitle({ x: 96, y: 618, text: 'История заказов клиента', subtitle: 'Заказы, расчеты и текущее состояние отношений' }));
parts.push(svgRect({ x: 96, y: 654, width: 1248, height: 42, rx: 12, fill: '#f9fafb' }));
['Номер', 'Тип', 'Статус', 'Сумма', 'Дата', 'Открыть'].forEach((label, index) => {
parts.push(svgText({ x: 118 + [0, 240, 402, 642, 836, 1086][index], y: 680, text: label, size: 12, weight: 700, fill: palette.subtext }));
});
for (let i = 0; i < 5; i += 1) {
const y = 710 + i * 50;
parts.push(svgLine({ x1: 96, y1: y, x2: 1344, y2: y }));
parts.push(svgText({ x: 118, y: y + 30, text: `FRG-30${i + 1}`, size: 14, weight: 700 }));
parts.push(svgText({ x: 358, y: y + 30, text: i % 2 === 0 ? 'Заказ' : 'Расчет', size: 13, fill: palette.subtext }));
parts.push(pill({ x: 474, y: y + 10, width: 128, label: i % 2 === 0 ? 'В работе' : 'Нужен расчет', fill: i % 2 === 0 ? palette.success : palette.warning }));
parts.push(svgText({ x: 760, y: y + 30, text: i % 2 === 0 ? '145 000 ₽' : '—', size: 13, weight: 700 }));
parts.push(svgText({ x: 954, y: y + 30, text: '01.05.2026', size: 13, fill: palette.subtext }));
parts.push(svgRect({ x: 1110, y: y + 8, width: 120, height: 30, rx: 15, fill: palette.muted }));
parts.push(svgText({ x: 1170, y: y + 28, text: 'Открыть', size: 12, weight: 700, fill: palette.subtext, anchor: 'middle' }));
}
parts.push(footer());
return parts.join('');
}
function makeCatalogSettings() {
const height = 980;
const parts = [windowFrame({ title: 'Настройки каталога', height })];
parts.push(svgText({ x: 72, y: 130, text: 'Настройки каталога', size: 30, weight: 800 }));
parts.push(svgText({ x: 72, y: 160, text: 'Управление типами товаров, параметрами и возможностями кастомизации', size: 14, fill: palette.subtext }));
parts.push(svgRect({ x: 72, y: 198, width: 302, height: 704, rx: 28, fill: '#fbfbfb' }));
parts.push(cardTitle({ x: 98, y: 234, text: 'Типы товаров', subtitle: 'Переход между настройками направлений' }));
['Упаковочный скотч', 'Алюминиевый скотч', 'Крепп', 'Вспененный скотч', 'PVC', 'ПП'].forEach((item, index) => {
parts.push(svgRect({ x: 98, y: 278 + index * 62, width: 250, height: 42, rx: 16, fill: index === 0 ? palette.accent : '#ffffff', stroke: index === 0 ? palette.accentStroke : palette.border }));
parts.push(svgText({ x: 118, y: 305 + index * 62, text: item, size: 13, weight: 700, fill: index === 0 ? '#1d4ed8' : palette.subtext }));
});
parts.push(svgRect({ x: 404, y: 198, width: 964, height: 704, rx: 28 }));
parts.push(cardTitle({ x: 432, y: 234, text: 'Параметры товарного направления', subtitle: 'Стандартные значения, кастомизация и пояснения по параметрам' }));
const checkboxes = ['Любая длина', 'Логотип на втулке', 'Нанесение надписи'];
checkboxes.forEach((label, index) => {
const y = 286 + index * 46;
parts.push(svgRect({ x: 432, y, width: 24, height: 24, rx: 6, fill: index < 2 ? palette.accent : '#ffffff', stroke: index < 2 ? palette.accentStroke : palette.border }));
if (index < 2) {
parts.push(svgText({ x: 444, y: y + 17, text: '✓', size: 14, weight: 800, fill: '#1d4ed8', anchor: 'middle' }));
}
parts.push(svgText({ x: 470, y: y + 17, text: label, size: 14, weight: 700 }));
});
const paramGroups = [
['Ширина', ['38', '48', '75']],
['Длина', ['40', '50', '66', '100']],
['Толщина', ['38', '43', '45']],
['Втулка', ['стандарт', 'логотип']],
['Цвет', ['прозрачный', 'коричневый']],
['Надпись', ['без надписи', 'хрупкое']],
];
paramGroups.forEach(([title, values], index) => {
const col = index % 2;
const row = Math.floor(index / 2);
const x = 432 + col * 430;
const y = 446 + row * 116;
parts.push(svgText({ x, y, text: title, size: 14, weight: 700 }));
parts.push(svgRect({ x, y: y + 14, width: 390, height: 74, rx: 18, fill: '#f9fafb' }));
values.forEach((value, tagIndex) => {
const width = Math.max(72, String(value).length * 9 + 28);
parts.push(pill({ x: x + 18 + tagIndex * 92, y: y + 34, width, label: String(value) }));
});
parts.push(svgText({ x: x + 330, y: y + 64, text: '+', size: 20, weight: 800, fill: '#2563eb' }));
});
parts.push(svgRect({ x: 432, y: 814, width: 220, height: 48, rx: 24, fill: palette.text, stroke: palette.text }));
parts.push(svgText({ x: 542, y: 844, text: 'Сохранить настройки', size: 14, weight: 700, fill: '#ffffff', anchor: 'middle' }));
parts.push(footer());
return parts.join('');
}
function makeSyncSettings() {
const height = 920;
const parts = [windowFrame({ title: 'Настройки синхронизации', height })];
parts.push(svgText({ x: 72, y: 130, text: 'Синхронизация и уведомления', size: 30, weight: 800 }));
parts.push(svgText({ x: 72, y: 160, text: 'Статусы обмена, шаблоны сообщений, диагностические ошибки и ручные действия', size: 14, fill: palette.subtext }));
const blocks = [
{ x: 72, y: 198, w: 410, h: 220, title: 'Контур обмена', subtitle: '1С, каталог, остатки, заявки', fill: palette.accent, stroke: palette.accentStroke },
{ x: 512, y: 198, w: 410, h: 220, title: 'Шаблоны сообщений', subtitle: 'Telegram, email, Max', fill: '#ffffff', stroke: palette.border },
{ x: 952, y: 198, w: 416, h: 220, title: 'Последние ошибки', subtitle: 'Журнал нештатных событий', fill: palette.warning, stroke: '#facc15' },
];
blocks.forEach((block) => {
parts.push(svgRect({ x: block.x, y: block.y, width: block.w, height: block.h, rx: 28, fill: block.fill, stroke: block.stroke }));
parts.push(cardTitle({ x: block.x + 24, y: block.y + 36, text: block.title, subtitle: block.subtitle }));
});
['Остатки: успешно', 'Каталог: 136 позиций', 'Заказы: webhook включен'].forEach((text, index) => {
parts.push(svgText({ x: 96, y: 288 + index * 24, text, size: 13, fill: palette.subtext }));
});
['Шаблон заказа', 'Шаблон расчета', 'Шаблон бонусного уведомления'].forEach((text, index) => {
parts.push(svgText({ x: 536, y: 288 + index * 24, text, size: 13, fill: palette.subtext }));
});
['Ошибка 1С: таймаут', 'Ошибка webhook: 500', 'Переотправка из очереди'].forEach((text, index) => {
parts.push(svgText({ x: 976, y: 288 + index * 24, text, size: 13, fill: palette.subtext }));
});
parts.push(svgRect({ x: 72, y: 448, width: 1296, height: 392, rx: 28 }));
parts.push(cardTitle({ x: 96, y: 482, text: 'Журнал синхронизаций и ручные операции', subtitle: 'История запусков, статусы и диагностические поля' }));
parts.push(svgRect({ x: 96, y: 518, width: 1248, height: 42, rx: 12, fill: '#f9fafb' }));
['Время', 'Сервис', 'Сценарий', 'Статус', 'Комментарий', 'Действие'].forEach((label, index) => {
parts.push(svgText({ x: 118 + [0, 180, 390, 650, 840, 1090][index], y: 544, text: label, size: 12, weight: 700, fill: palette.subtext }));
});
for (let i = 0; i < 5; i += 1) {
const y = 574 + i * 52;
parts.push(svgLine({ x1: 96, y1: y, x2: 1344, y2: y }));
parts.push(svgText({ x: 118, y: y + 30, text: `01.05 1${i}:20`, size: 13, fill: palette.subtext }));
parts.push(svgText({ x: 298, y: y + 30, text: i % 2 === 0 ? 'apollo-backend' : 'web-frontend', size: 13, weight: 700 }));
parts.push(svgText({ x: 508, y: y + 30, text: i % 2 === 0 ? 'import catalog' : 'send notifications', size: 13, fill: palette.subtext }));
parts.push(pill({ x: 700, y: y + 10, width: 108, label: i === 2 ? 'Ошибка' : 'Успешно', fill: i === 2 ? palette.danger : palette.success }));
parts.push(svgText({ x: 858, y: y + 30, text: i === 2 ? 'Таймаут 1С' : 'Без замечаний', size: 13, fill: palette.subtext }));
parts.push(svgRect({ x: 1110, y: y + 8, width: 122, height: 30, rx: 15, fill: palette.muted }));
parts.push(svgText({ x: 1171, y: y + 28, text: 'Повторить', size: 12, weight: 700, fill: palette.subtext, anchor: 'middle' }));
}
parts.push(footer());
return parts.join('');
}
function makeBonusManager() {
const height = 980;
const parts = [windowFrame({ title: 'Бонусная система менеджера', height })];
parts.push(svgText({ x: 72, y: 130, text: 'Бонусная система', size: 30, weight: 800 }));
parts.push(svgText({ x: 72, y: 160, text: 'Рефералы, ручные транзакции, заявки на вывод и переход в карточку бонусного счета', size: 14, fill: palette.subtext }));
parts.push(svgRect({ x: 72, y: 198, width: 1296, height: 104, rx: 28 }));
['Клиент', 'Тип операции', 'Статус', 'Период'].forEach((label, index) => {
const x = 96 + index * 270;
parts.push(svgText({ x, y: 234, text: label, size: 12, weight: 700, fill: palette.subtext }));
parts.push(svgRect({ x, y: 244, width: 230, height: 32, rx: 16, fill: '#f9fafb' }));
});
['Новая реферальная связь', 'Новая транзакция'].forEach((label, index) => {
parts.push(svgRect({ x: 1012 + index * 164, y: 234, width: 148, height: 42, rx: 21, fill: index === 0 ? palette.frame : palette.text, stroke: index === 0 ? palette.border : palette.text }));
parts.push(svgText({ x: 1086 + index * 164, y: 260, text: label, size: 12, weight: 700, fill: index === 0 ? palette.subtext : '#ffffff', anchor: 'middle' }));
});
parts.push(svgRect({ x: 72, y: 332, width: 1296, height: 548, rx: 28 }));
parts.push(cardTitle({ x: 96, y: 366, text: 'Очередь бонусных сущностей', subtitle: 'Клиенты, начисления, выводы и связанные действия' }));
parts.push(svgRect({ x: 96, y: 402, width: 1248, height: 42, rx: 12, fill: '#f9fafb' }));
['Клиент', 'Баланс', 'Рефералы', 'Выводы', 'Последнее действие', 'Открыть'].forEach((label, index) => {
parts.push(svgText({ x: 118 + [0, 262, 454, 636, 834, 1098][index], y: 428, text: label, size: 12, weight: 700, fill: palette.subtext }));
});
for (let i = 0; i < 6; i += 1) {
const y = 458 + i * 58;
parts.push(svgLine({ x1: 96, y1: y, x2: 1344, y2: y }));
parts.push(svgText({ x: 118, y: y + 30, text: `ООО Клиент ${i + 1}`, size: 14, weight: 700 }));
parts.push(svgText({ x: 380, y: y + 30, text: `${(i + 1) * 1500}`, size: 13, weight: 700 }));
parts.push(svgText({ x: 572, y: y + 30, text: String(i + 2), size: 13, weight: 700 }));
parts.push(pill({ x: 688, y: y + 10, width: 118, label: i % 2 === 0 ? 'На проверке' : 'Нет', fill: i % 2 === 0 ? palette.warning : '#f3f4f6' }));
parts.push(svgText({ x: 952, y: y + 30, text: i % 2 === 0 ? 'Заявка на вывод' : 'Начисление за заказ', size: 13, fill: palette.subtext }));
parts.push(svgRect({ x: 1110, y: y + 8, width: 122, height: 30, rx: 15, fill: palette.muted }));
parts.push(svgText({ x: 1171, y: y + 28, text: 'Открыть счет', size: 12, weight: 700, fill: palette.subtext, anchor: 'middle' }));
}
parts.push(footer());
return parts.join('');
}
const prototypes = {
'login.svg': makeLogin(),
'profile.svg': makeProfile(),
'dashboard.svg': makeDashboard(),
'catalog-grid.svg': makeCatalogGrid(),
'product-card.svg': makeProductCard(),
'cart.svg': makeCart(),
'client-order.svg': makeClientOrder(),
'client-list.svg': makeClientList(),
'client-card.svg': makeClientCard(),
'manager-orders.svg': makeManagerOrders(),
'manager-order.svg': makeManagerOrder(),
'catalog-settings.svg': makeCatalogSettings(),
'sync-settings.svg': makeSyncSettings(),
'bonus-cabinet.svg': makeBonusCabinet(),
'bonus-manager.svg': makeBonusManager(),
};
mkdirSync(OUTPUT_DIR, { recursive: true });
for (const [fileName, contents] of Object.entries(prototypes)) {
writeFileSync(join(OUTPUT_DIR, fileName), contents, 'utf8');
}
console.log(`Generated ${Object.keys(prototypes).length} wireframe prototypes.`);