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 `${esc(text)}`; } function svgRect({ x, y, width, height, fill = palette.frame, stroke = palette.border, rx = 18, dashed = false, strokeWidth = 1.5 }) { return ``; } function svgLine({ x1, y1, x2, y2, stroke = palette.border, strokeWidth = 1.5, dashed = false }) { return ``; } 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 [ ``, ``, 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 }), ``, ``, ``, ``, svgText({ x: 136, y: 58, text: title, size: 17, weight: 700, fill: titleFill }), ].join(''); } function footer() { return ''; } 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(``); 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(``); 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(``); if (i === 0) { parts.push(``); } 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(``); 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.`);