@@ -1,906 +0,0 @@
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. ` ) ;