Add wireframe screen prototypes to specification
This commit is contained in:
906
docs/scripts/generate-wireframe-prototypes.mjs
Normal file
906
docs/scripts/generate-wireframe-prototypes.mjs
Normal file
@@ -0,0 +1,906 @@
|
||||
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('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
.replaceAll('"', '"');
|
||||
}
|
||||
|
||||
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 = [
|
||||
['Любая длина', 'Допустимый диапазон 25–150 м с шагом 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 }));
|
||||
['Доставка: Санкт-Петербург → Москва', 'Адрес: Основной склад клиента', 'Комментарий менеджера: подтверждены сроки 3–5 дней'].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.`);
|
||||
Reference in New Issue
Block a user