From 98ae168a938cb2204154f1c6ae6b576e52d47495 Mon Sep 17 00:00:00 2001 From: Ruslan Bakiev <572431+veikab@users.noreply.github.com> Date: Fri, 1 May 2026 16:20:19 +0700 Subject: [PATCH] Add wireframe screen prototypes to specification --- docs/public/prototypes/bonus-cabinet.svg | 2 +- docs/public/prototypes/bonus-manager.svg | 1 + docs/public/prototypes/cart.svg | 2 +- docs/public/prototypes/catalog-grid.svg | 2 +- docs/public/prototypes/catalog-settings.svg | 1 + docs/public/prototypes/client-card.svg | 1 + docs/public/prototypes/client-list.svg | 1 + docs/public/prototypes/client-order.svg | 2 +- docs/public/prototypes/dashboard.svg | 2 +- docs/public/prototypes/login.svg | 1 + docs/public/prototypes/manager-order.svg | 2 +- docs/public/prototypes/manager-orders.svg | 2 +- docs/public/prototypes/product-card.svg | 2 +- docs/public/prototypes/profile.svg | 1 + docs/public/prototypes/sync-settings.svg | 1 + .../scripts/generate-wireframe-prototypes.mjs | 906 ++++++++++++++++++ docs/tz/stage-1/index.md | 44 +- 17 files changed, 957 insertions(+), 16 deletions(-) create mode 100644 docs/public/prototypes/bonus-manager.svg create mode 100644 docs/public/prototypes/catalog-settings.svg create mode 100644 docs/public/prototypes/client-card.svg create mode 100644 docs/public/prototypes/client-list.svg create mode 100644 docs/public/prototypes/login.svg create mode 100644 docs/public/prototypes/profile.svg create mode 100644 docs/public/prototypes/sync-settings.svg create mode 100644 docs/scripts/generate-wireframe-prototypes.mjs diff --git a/docs/public/prototypes/bonus-cabinet.svg b/docs/public/prototypes/bonus-cabinet.svg index 7135a71..210d971 100644 --- a/docs/public/prototypes/bonus-cabinet.svg +++ b/docs/public/prototypes/bonus-cabinet.svg @@ -1 +1 @@ -Бонусный кабинетТекущий бонусный балансИстория операцийРеферальные связиПодача заявки на использование или вывод \ No newline at end of file +Бонусный кабинет клиентаЧёрный кабинет бонусной программыОтдельный контур для бонусного баланса, истории, карт и заявок на выводАккаунт клиентаИмя пользователя, пояснение по программе и статус подключения12 400доступный балансРефералы8Начисления42Выводы3Вывод бонусовФорма подачи заявки с проверкой минимального порога и доступного балансаСумма заявки на выводПодать заявку на выводИстория бонусных операцийНачисления, списания и ссылки на связанные заказы+1000 бонусовНачисление за подтвержденный заказ / ручная транзакцияОткрыть заказ+1500 бонусовНачисление за подтвержденный заказ / ручная транзакцияОткрыть заказ+2000 бонусовНачисление за подтвержденный заказ / ручная транзакцияОткрыть заказ+2500 бонусовНачисление за подтвержденный заказ / ручная транзакцияОткрыть заказ+3000 бонусовНачисление за подтвержденный заказ / ручная транзакцияОткрыть заказМагазин наград и активные выводыПодарочные карты и блок очереди заявок на выводOzon3000 бонусовWildberries4000 бонусовМ.Видео5000 бонусовАктивные заявки на выводЗаявка #1 · 1 500 бонусов · на проверкеЗаявка #2 · 3 000 бонусов · подтвержденаЗаявка #3 · 500 бонусов · отклонена \ No newline at end of file diff --git a/docs/public/prototypes/bonus-manager.svg b/docs/public/prototypes/bonus-manager.svg new file mode 100644 index 0000000..df38fa2 --- /dev/null +++ b/docs/public/prototypes/bonus-manager.svg @@ -0,0 +1 @@ +Бонусная система менеджераБонусная системаРефералы, ручные транзакции, заявки на вывод и переход в карточку бонусного счетаКлиентТип операцииСтатусПериодНовая реферальная связьНовая транзакцияОчередь бонусных сущностейКлиенты, начисления, выводы и связанные действияКлиентБалансРефералыВыводыПоследнее действиеОткрытьООО Клиент 115002На проверкеЗаявка на выводОткрыть счетООО Клиент 230003НетНачисление за заказОткрыть счетООО Клиент 345004На проверкеЗаявка на выводОткрыть счетООО Клиент 460005НетНачисление за заказОткрыть счетООО Клиент 575006На проверкеЗаявка на выводОткрыть счетООО Клиент 690007НетНачисление за заказОткрыть счет \ No newline at end of file diff --git a/docs/public/prototypes/cart.svg b/docs/public/prototypes/cart.svg index cf2b432..4677a47 100644 --- a/docs/public/prototypes/cart.svg +++ b/docs/public/prototypes/cart.svg @@ -1 +1 @@ -КорзинаСписок выбранных позицийАдрес доставкиКомментарий клиентаОтправить заявку \ No newline at end of file +КорзинаКорзинаСостав заказа, адрес доставки и отправка заявки на расчетСостав заказаТекущие позиции из каталога с параметрами и количествомТоварПараметрыКол-воУправлениеУпаковочный скотчFRG-10148 мм · 50 м · 43 мкм · прозрачный1ИзменитьАлюминиевый скотчFRG-10248 мм · 50 м · 43 мкм · прозрачный2ИзменитьУпаковочный скотчFRG-10348 мм · 50 м · 43 мкм · прозрачный3ИзменитьАлюминиевый скотчFRG-10448 мм · 50 м · 43 мкм · прозрачный4ИзменитьОформлениеПроверка профиля клиента и отправка заявкиПрофиль контрагента заполненЕсли профиль неполный, здесь показывается предупреждение.Адрес доставкиОсновной склад клиентаМосква, улица и зона разгрузкиАдрес доставки 2Москва, улица и зона разгрузкиАдрес доставки 3Москва, улица и зона разгрузкиОформить заявкуКомментарий и итоговая сводкаДополнительные инструкции клиента и итог по количеству позицийКомментарий клиента к заказу / пожелания по доставке4 позиции в корзине, 2 уникальных SKU, адрес выбран \ No newline at end of file diff --git a/docs/public/prototypes/catalog-grid.svg b/docs/public/prototypes/catalog-grid.svg index 311fc45..1abc93d 100644 --- a/docs/public/prototypes/catalog-grid.svg +++ b/docs/public/prototypes/catalog-grid.svg @@ -1 +1 @@ -Каталог продукцииЗаголовок разделаПоискСетка карточек товарных направленийУпаковочный скотч | Алюминиевый скотч | Крепп | Вспененный | Двусторонний ПП | Двусторонний PVC \ No newline at end of file +Каталог продукцииКаталогВыбор товарного направления до перехода в детальную карточкуПоиск по типу товараАлюминиевый скотчКарточка товарного направленияАрмированный скотчКарточка товарного направленияВспененный скотчКарточка товарного направленияДвусторонний ППКарточка товарного направленияДвусторонний PVCКарточка товарного направленияКреппКарточка товарного направленияМеталлизированныйКарточка товарного направленияСигнальная лентаКарточка товарного направленияУпаковочный скотчКарточка товарного направленияЦветной скотчКарточка товарного направления \ No newline at end of file diff --git a/docs/public/prototypes/catalog-settings.svg b/docs/public/prototypes/catalog-settings.svg new file mode 100644 index 0000000..410e8df --- /dev/null +++ b/docs/public/prototypes/catalog-settings.svg @@ -0,0 +1 @@ +Настройки каталогаНастройки каталогаУправление типами товаров, параметрами и возможностями кастомизацииТипы товаровПереход между настройками направленийУпаковочный скотчАлюминиевый скотчКреппВспененный скотчPVCПППараметры товарного направленияСтандартные значения, кастомизация и пояснения по параметрам✓Любая длина✓Логотип на втулкеНанесение надписиШирина384875+Длина405066100+Толщина384345+Втулкастандартлоготип+Цветпрозрачныйкоричневый+Надписьбез надписихрупкое+Сохранить настройки \ No newline at end of file diff --git a/docs/public/prototypes/client-card.svg b/docs/public/prototypes/client-card.svg new file mode 100644 index 0000000..c0474d3 --- /dev/null +++ b/docs/public/prototypes/client-card.svg @@ -0,0 +1 @@ +Карточка клиента← Назад к клиентамООО Клиент 1Компания, реквизиты, заказы, бонусы и реферальные связи клиентаКарточка компанииЮридические и контактные данныеИНН / КПП: значениеМенеджер: значениеТелефон: значениеEmail: значениеГород: значениеДата регистрации: значениеКонтрагент и доставкаРеквизиты и адреса клиентаБанковские реквизитыЮридический адресАдрес доставки #1Адрес доставки #2Бонусный контурСвязанные бонусные сущности клиентаБаланс: 12 400Рефералы: 8Активные выводы: 2Переход в бонусный кабинет менеджераИстория заказов клиентаЗаказы, расчеты и текущее состояние отношенийНомерТипСтатусСуммаДатаОткрытьFRG-301ЗаказВ работе145 000 ₽01.05.2026ОткрытьFRG-302РасчетНужен расчет—01.05.2026ОткрытьFRG-303ЗаказВ работе145 000 ₽01.05.2026ОткрытьFRG-304РасчетНужен расчет—01.05.2026ОткрытьFRG-305ЗаказВ работе145 000 ₽01.05.2026Открыть \ No newline at end of file diff --git a/docs/public/prototypes/client-list.svg b/docs/public/prototypes/client-list.svg new file mode 100644 index 0000000..989f9d0 --- /dev/null +++ b/docs/public/prototypes/client-list.svg @@ -0,0 +1 @@ +Список клиентовКлиентыПоиск компаний, переход в карточку клиента и приглашение нового пользователяПоиск по компанииМенеджерСтатусГородПригласить клиентаКлиентская базаКарточка компании, активность, заказы и бонусный статусКомпанияКонтактЗаказыБонусыСтатусДействиеООО Клиент 1Имя пользователя / телефон41200АктивенОткрыть карточкуООО Клиент 2Имя пользователя / телефон52400На проверкеОткрыть карточкуООО Клиент 3Имя пользователя / телефон63600АктивенОткрыть карточкуООО Клиент 4Имя пользователя / телефон74800На проверкеОткрыть карточкуООО Клиент 5Имя пользователя / телефон86000АктивенОткрыть карточкуООО Клиент 6Имя пользователя / телефон97200На проверкеОткрыть карточку \ No newline at end of file diff --git a/docs/public/prototypes/client-order.svg b/docs/public/prototypes/client-order.svg index 41e4db5..080cbed 100644 --- a/docs/public/prototypes/client-order.svg +++ b/docs/public/prototypes/client-order.svg @@ -1 +1 @@ -Карточка заявки / заказаНомер документа и статусСостав позицийСтоимость и условия поставкиИстория изменений \ No newline at end of file +Карточка заказа клиента← Назад к заказамЗаказ FRG-1042Клиент видит только статус, состав, условия поставки и историю измененийСтатусная линия заказаСоздан → Нужен расчет → Условия опубликованы → Подтвержден → ОтгруженСозданРасчетУсловияПодтвержденОтгрузкаСостав заказаПозиции, параметры и количество без менеджерских внутренних полейТоварSKUПараметрыКол-воУпаковочный скотчFRG-20148 мм · 50 м · 43 мкм1Алюминиевый скотчFRG-20248 мм · 50 м · 43 мкм2Упаковочный скотчFRG-20348 мм · 50 м · 43 мкм3Алюминиевый скотчFRG-20448 мм · 50 м · 43 мкм4Условия и доставкаПоявляются после публикации менеджеромСтоимость опубликованаЦена зафиксирована и доступна клиенту в карточке заказа.• Доставка: Санкт-Петербург → Москва• Адрес: Основной склад клиента• Комментарий менеджера: подтверждены сроки 3–5 днейИстория статусовСоздан клиентомПередан менеджеруУсловия опубликованыСистемные комментарии и событияЖурнал изменений доступный клиенту• Менеджер обновил условия поставки• Клиент подтвердил получение условий• Система отправила уведомление в Telegram \ No newline at end of file diff --git a/docs/public/prototypes/dashboard.svg b/docs/public/prototypes/dashboard.svg index 78a97da..8ca1d10 100644 --- a/docs/public/prototypes/dashboard.svg +++ b/docs/public/prototypes/dashboard.svg @@ -1 +1 @@ -Главная страница клиентаHeader / навигацияБыстрые действияАктуальные заказы и заявкиПоследние уведомленияБонусный блок \ No newline at end of file +Главная страница клиентаГлавнаяБыстрые действия, заказы, уведомления и бонусный контур на одном экранеБыстрые действияПереход в каталог, заказы, профиль и бонусную программуКаталогКорзинаМои заказыПрофильУведомленияБонусыСводка клиентаСтатус профиля, активные заявки и бонусный балансПрофиль заполнен на 82%Реквизиты, адреса доставки, уведомления2 активных заказаОжидают расчет и публикацию условий12 400 бонусовДоступно для подарочных карт и выводаПоследние заявки и заказыОчередь клиента с текущими статусами и действиямиНомерТипСтатусДоставкаДействиеFRG-101ЗаказВ работеМоскваОткрытьFRG-102РасчетНужен расчетСанкт-ПетербургОткрытьFRG-103ЗаказВ работеМоскваОткрытьFRG-104РасчетНужен расчетСанкт-ПетербургОткрытьFRG-105ЗаказВ работеМоскваОткрытьУведомления и бонусыИнформационные блоки и отдельные CTAПоследние уведомленияМенеджер обновил стоимость заказаПодтвержден адрес доставкиСоздана заявка на вывод бонусовБонусный кабинетПереход в отдельный интерфейс истории бонусов, карт и выводовОткрыть кабинетЗаполненность профиля• Реквизиты контрагента• Адреса доставки• Уведомления Telegram / Max \ No newline at end of file diff --git a/docs/public/prototypes/login.svg b/docs/public/prototypes/login.svg new file mode 100644 index 0000000..267066b --- /dev/null +++ b/docs/public/prototypes/login.svg @@ -0,0 +1 @@ +Логин и подключениеЛичный кабинет ФрегатВход по коду, заявка на подключение и быстрый выбор канала авторизации.• Каталог готовой продукции• Индивидуальный расчет• Заказы, бонусы, уведомленияПоясняющий блокОписание сценария для нового клиента:Оставить заявку на подключениеДождаться проверки менеджеромПолучить код и войти в кабинетВходВведите номер телефона или email для получения кода доступа.Телефон / EmailКод подтвержденияПолучить код / ВойтиАльтернативные каналы входаTelegramMaxПриглашение от менеджераСсылка: оставить самостоятельную заявку на подключение \ No newline at end of file diff --git a/docs/public/prototypes/manager-order.svg b/docs/public/prototypes/manager-order.svg index 1a1e053..6f7e4b6 100644 --- a/docs/public/prototypes/manager-order.svg +++ b/docs/public/prototypes/manager-order.svg @@ -1 +1 @@ -Карточка обработки заявкиКлиент / контрагент / менеджерСостав заявки или расчетный payloadСтоимость и условия поставкиОпубликовать условия / перевести в работу / отменить \ No newline at end of file +Карточка заказа менеджера← Назад к заказамЗаказ FRG-2034Обработка условий, доставки, бонусных эффектов и журнал событийПанель статуса и действийПереключение статуса, публикация цены, фиксация доставкиОпубликовать условияЗапросить уточнениеПодтвердить отгрузкуТекущий статус: нужен расчет → следующий шаг: публикация условий клиентуСостав заказа и расчетПозиции клиента, параметры, ручной расчет и итоговая публикацияТоварПараметрыКол-воЦенаИтогУпаковочный скотч48 мм · 50 м · стандарт1цена24 000Вспененный скотч48 мм · 50 м · стандарт2цена18 500Упаковочный скотч48 мм · 50 м · стандарт3цена24 000Вспененный скотч48 мм · 50 м · стандарт4цена18 500Блок публикации итоговых условий: сумма, комментарий, сроки, вид доставкиДоставка и коммуникацииАдрес, стоимость логистики, комментарии и история сообщений• Адрес доставки клиента• Стоимость доставки / самовывоз• Окно разгрузки и ограниченияВлияние на бонусный контур• Начислить бонусы после подтверждения• Проверить реферальную привязку• Показать менеджеру связанный бонусный счетЖурнал событийМенеджер открыл заказКлиент уточнил параметры товараСистема создала уведомление о расчетеВнутренние комментарии и системные интеграцииЗаметки менеджера, данные для 1С и служебные идентификаторыПоле комментария менеджера к заказу / служебные заметкиСтатус синхронизации с 1С / внешний идентификатор \ No newline at end of file diff --git a/docs/public/prototypes/manager-orders.svg b/docs/public/prototypes/manager-orders.svg index 787ac27..820a3c5 100644 --- a/docs/public/prototypes/manager-orders.svg +++ b/docs/public/prototypes/manager-orders.svg @@ -1 +1 @@ -Список заказов менеджераЗаголовок разделаФильтры: статус / клиент / периодТаблица заказов \ No newline at end of file +Список заказов менеджераЗаказыФильтрация очереди, приоритеты и переход в карточку обработкиФильтры и быстрые статусыСтатус, клиент, период, город доставки и приоритетСтатусКлиентПериодГородПриоритетНовые14Нужен расчет9Условия опубликованы18Ожидают отгрузку7Таблица заказовОсновная рабочая очередь менеджераНомерКлиентСтатусДоставкаСуммаДействиеFRG-2030ООО Клиент 1Нужен расчетМосква—Открыть карточкуFRG-2031ООО Клиент 2Условия готовыСПб → Москва145 000 ₽Открыть карточкуFRG-2032ООО Клиент 3Нужен расчетМосква—Открыть карточкуFRG-2033ООО Клиент 4Условия готовыСПб → Москва145 000 ₽Открыть карточкуFRG-2034ООО Клиент 5Нужен расчетМосква—Открыть карточкуFRG-2035ООО Клиент 6Условия готовыСПб → Москва145 000 ₽Открыть карточку \ No newline at end of file diff --git a/docs/public/prototypes/product-card.svg b/docs/public/prototypes/product-card.svg index adc5ca0..4799f83 100644 --- a/docs/public/prototypes/product-card.svg +++ b/docs/public/prototypes/product-card.svg @@ -1 +1 @@ -Карточка товараВерхний блокИзображение товараПараметры выбораSKU / действие В корзинуПояснения по параметрамТаблица доступных вариантовЗаголовок товара и навигация \ No newline at end of file +Карточка товара← НазадАлюминиевый скотчВыбор параметров, индивидуальные опции, остатки и добавление в корзинуСоседнийтоварКреппСледующийтоварPVCПараметры выбораШирина, длина, толщина, втулка, цвет и надписьШирина48 мм75 ммДлина25 м50 м100 мТолщина43 мкм45 мкмВтулкаСтандартЛоготипЦветСеребристыйНадписьБез надписиПод заказSKU и действиеFRG-ALU-48-50В наличии: 2 140Добавить в корзинуЕсли включены кастомные опции, под кнопкой появляется дополнительное поле заявки.Под заказ и ограниченияПояснения по любой длине, логотипу на втулке и нанесению надписиЛюбая длинаДопустимый диапазон 25–150 м с шагом 5 м.Логотип на втулкеДоступно после согласования макета и минимального тиража.Нанесение надписиМаркировка согласуется менеджером и попадает в расчет.Доступные вариантыТаблица складских вариантов с параметрами и остатками по складамSKUПараметрыОстатокСкладыДействиеFRG-ALU-48-5048 мм · 50 м · 43 мкм2100СПб / МоскваВыбратьFRG-ALU-49-6049 мм · 60 м · 44 мкм1800СПбВыбратьFRG-ALU-50-7050 мм · 70 м · 45 мкм1500СПбВыбрать \ No newline at end of file diff --git a/docs/public/prototypes/profile.svg b/docs/public/prototypes/profile.svg new file mode 100644 index 0000000..97080c1 --- /dev/null +++ b/docs/public/prototypes/profile.svg @@ -0,0 +1 @@ +Профиль клиентаПрофильБазовые данные, контрагент, адреса доставки и уведомленияНавигация профиляВнутренние подразделы клиентаОсновные данныеКонтрагентАдреса доставкиУведомленияКарточка пользователяРедактирование данных и контроль заполненности профиляФИОРуслан БакиевТелефон+7 9xx xxx-xx-xxEmailclient@fregat.ruДолжностьРуководитель закупокСтатус заполненности82% заполнено. Не хватает банковских реквизитов и второго адреса.Быстрые переходы• Редактировать контрагента• Открыть адреса доставки• Настроить уведомленияСохранить изменения \ No newline at end of file diff --git a/docs/public/prototypes/sync-settings.svg b/docs/public/prototypes/sync-settings.svg new file mode 100644 index 0000000..622d818 --- /dev/null +++ b/docs/public/prototypes/sync-settings.svg @@ -0,0 +1 @@ +Настройки синхронизацииСинхронизация и уведомленияСтатусы обмена, шаблоны сообщений, диагностические ошибки и ручные действияКонтур обмена1С, каталог, остатки, заявкиШаблоны сообщенийTelegram, email, MaxПоследние ошибкиЖурнал нештатных событийОстатки: успешноКаталог: 136 позицийЗаказы: webhook включенШаблон заказаШаблон расчетаШаблон бонусного уведомленияОшибка 1С: таймаутОшибка webhook: 500Переотправка из очередиЖурнал синхронизаций и ручные операцииИстория запусков, статусы и диагностические поляВремяСервисСценарийСтатусКомментарийДействие01.05 10:20apollo-backendimport catalogУспешноБез замечанийПовторить01.05 11:20web-frontendsend notificationsУспешноБез замечанийПовторить01.05 12:20apollo-backendimport catalogОшибкаТаймаут 1СПовторить01.05 13:20web-frontendsend notificationsУспешноБез замечанийПовторить01.05 14:20apollo-backendimport catalogУспешноБез замечанийПовторить \ No newline at end of file diff --git a/docs/scripts/generate-wireframe-prototypes.mjs b/docs/scripts/generate-wireframe-prototypes.mjs new file mode 100644 index 0000000..77dd694 --- /dev/null +++ b/docs/scripts/generate-wireframe-prototypes.mjs @@ -0,0 +1,906 @@ +import { mkdirSync, writeFileSync } from 'node:fs'; +import { join } from 'node:path'; + +const OUTPUT_DIR = '/Users/ruslanbakiev/workspace/fregat/web-frontend/docs/public/prototypes'; +const WIDTH = 1440; + +const palette = { + bg: '#f3f4f6', + frame: '#ffffff', + border: '#d1d5db', + muted: '#e5e7eb', + line: '#9ca3af', + text: '#111827', + subtext: '#4b5563', + accent: '#dbeafe', + accentStroke: '#60a5fa', + success: '#dcfce7', + warning: '#fef3c7', + danger: '#fee2e2', + dark: '#101418', + darkCard: '#1b2228', + darkMuted: '#2d3741', + darkText: '#f9fafb', + darkSubtext: '#cbd5e1', +}; + +function esc(value) { + return String(value) + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"'); +} + +function svgText({ x, y, text, size = 16, weight = 500, fill = palette.text, anchor = 'start' }) { + return `${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.`); diff --git a/docs/tz/stage-1/index.md b/docs/tz/stage-1/index.md index 9ab213e..65e19a6 100644 --- a/docs/tz/stage-1/index.md +++ b/docs/tz/stage-1/index.md @@ -94,6 +94,8 @@ - остатки и доступные варианты отображаются в наглядном виде - пользователь понимает ограничения выбора и возможность кастомизации +Ниже приведены низкодетализированные wireframe-прототипы. Они отражают текущую структуру экранов по действующей frontend-реализации и используются как визуальная фиксация состава страниц, ключевых блоков и пользовательских действий. + ## 5.4 Клиентские экранные формы ### 5.4.1 Главная страница клиента @@ -112,7 +114,7 @@ - блок бонусной информации при наличии подключенного бонусного контура - индикатор статуса заполненности профиля клиента -Схематичный прототип: +Wireframe-прототип:  @@ -131,7 +133,7 @@ - карточка каждого товарного направления с изображением и наименованием - переход в карточку выбранного товарного направления -Схематичный прототип: +Wireframe-прототип:  @@ -159,7 +161,7 @@ - `/products/[slug]` -Схематичный прототип: +Wireframe-прототип:  @@ -198,7 +200,7 @@ - выбранный адрес доставки - итоговая сводка по количеству позиций -Схематичный прототип: +Wireframe-прототип:  @@ -211,7 +213,7 @@ - просмотр стоимости и условий поставки после публикации - просмотр истории изменений -Схематичный прототип: +Wireframe-прототип:  @@ -240,6 +242,10 @@ - ссылка на самостоятельную заявку на подключение - блок пояснения по дальнейшему сценарию доступа +Wireframe-прототип: + + + ### 5.4.7 Список заказов Назначение страницы: @@ -275,7 +281,7 @@ - форма подачи заявки на использование либо вывод бонусов - правила бонусной программы -Схематичный прототип: +Wireframe-прототип:  @@ -295,6 +301,10 @@ - индикаторы активности и количества заказов - действие приглашения нового клиента +Wireframe-прототип: + + + ### 5.5.2 Карточка клиента Назначение страницы: @@ -311,6 +321,10 @@ - список бонусных операций - связанные рефералы +Wireframe-прототип: + + + ### 5.5.3 Карточка обработки заявки Назначение страницы: @@ -320,7 +334,7 @@ - публикация условий клиенту - перевод заявки в работу либо отмена -Схематичный прототип: +Wireframe-прототип:  @@ -342,7 +356,7 @@ - фильтрация по статусам - переход к обработке конкретного заказа -Схематичный прототип: +Wireframe-прототип:  @@ -363,6 +377,10 @@ - списки стандартных параметров - единое действие сохранения настроек +Wireframe-прототип: + + + ### 5.5.6 Настройки синхронизации и уведомлений Назначение страницы: @@ -377,6 +395,10 @@ - статусы последних синхронизаций - диагностическая информация по обмену +Wireframe-прототип: + + + ## 5.6 Дополнительные профильные и сервисные страницы Помимо основных клиентских и менеджерских экранов, текущая реализация содержит дополнительные экранные формы: @@ -393,3 +415,9 @@ - создание реферальной связи - создание бонусной транзакции - проверка заявки на вывод бонусов + +Прототипы служебных и дополнительных экранов: + + + +
Бонусный кабинет
Текущий бонусный баланс
История операций
Реферальные связи
Подача заявки на использование или вывод
Корзина
Список выбранных позиций
Адрес доставки
Комментарий клиента
Отправить заявку
Каталог продукции
Заголовок раздела
Поиск
Сетка карточек товарных направлений
Упаковочный скотч | Алюминиевый скотч | Крепп | Вспененный | Двусторонний ПП | Двусторонний PVC
Карточка заявки / заказа
Номер документа и статус
Состав позиций
Стоимость и условия поставки
История изменений
Главная страница клиента
Header / навигация
Быстрые действия
Актуальные заказы и заявки
Последние уведомления
Бонусный блок
Карточка обработки заявки
Клиент / контрагент / менеджер
Состав заявки или расчетный payload
Опубликовать условия / перевести в работу / отменить
Список заказов менеджера
Фильтры: статус / клиент / период
Таблица заказов
Карточка товара
Верхний блок
Изображение товара
Параметры выбора
SKU / действие В корзину
Пояснения по параметрам
Таблица доступных вариантов
Заголовок товара и навигация