Remove obsolete markdown documentation artifacts

This commit is contained in:
Ruslan Bakiev
2026-05-04 11:01:31 +07:00
parent d21ff3437f
commit 3885782afd
26 changed files with 0 additions and 3657 deletions

View File

@@ -1,30 +0,0 @@
import { defineConfig } from 'vitepress';
export default defineConfig({
title: 'Техническое задание',
description: 'Техническое задание на разработку личного кабинета Фрегат',
lang: 'ru-RU',
cleanUrls: true,
themeConfig: {
nav: [
{ text: 'Техническое задание', link: '/' },
],
sidebar: [
{
text: 'Техническое задание',
items: [
{ text: 'Единый документ ТЗ', link: '/' },
{ text: 'Содержание разделов', link: '/tz/' },
],
},
],
outline: [2, 3],
search: {
provider: 'local',
},
footer: {
message: 'Техническое задание на разработку программного продукта',
copyright: 'Фрегат Групп / ИП Бакиев',
},
},
});

View File

@@ -1,99 +0,0 @@
<script setup lang="ts">
import mermaid from 'mermaid';
import { computed, onMounted, ref, watch } from 'vue';
const props = defineProps<{
chart: string;
}>();
const container = ref<HTMLElement | null>(null);
const error = ref('');
let initialized = false;
let diagramId = 0;
function initMermaid() {
if (initialized) {
return;
}
mermaid.initialize({
startOnLoad: false,
theme: 'neutral',
securityLevel: 'loose',
fontFamily: 'Inter, Arial, sans-serif',
flowchart: {
useMaxWidth: true,
htmlLabels: true,
},
});
initialized = true;
}
const source = computed(() => props.chart.trim());
async function renderDiagram() {
if (!import.meta.client || !container.value) {
return;
}
try {
initMermaid();
error.value = '';
diagramId += 1;
container.value.removeAttribute('data-processed');
container.value.id = `mermaid-diagram-${diagramId}`;
container.value.textContent = source.value;
await mermaid.run({
nodes: [container.value],
});
} catch (cause) {
error.value = cause instanceof Error ? cause.message : 'Failed to render Mermaid diagram.';
}
}
onMounted(() => {
void renderDiagram();
});
watch(source, () => {
void renderDiagram();
});
</script>
<template>
<div class="mermaid-diagram">
<div v-if="error" class="mermaid-diagram__error">{{ error }}</div>
<pre v-else ref="container" class="mermaid-diagram__canvas mermaid" />
</div>
</template>
<style scoped>
.mermaid-diagram {
margin: 1.25rem 0 1.75rem;
border: 1px solid #d9dee7;
border-radius: 20px;
background: #fff;
overflow: hidden;
}
.mermaid-diagram__canvas {
padding: 1rem;
margin: 0;
white-space: pre-wrap;
}
.mermaid-diagram__canvas :deep(svg) {
display: block;
width: 100%;
height: auto;
}
.mermaid-diagram__error {
padding: 1rem 1.25rem;
color: #b42318;
background: #fef3f2;
font-size: 0.95rem;
}
</style>

View File

@@ -1,12 +0,0 @@
<script setup lang="ts">
import MermaidDiagram from './MermaidDiagram.vue';
import { diagramSources } from './diagramSources';
const props = defineProps<{
name: string;
}>();
</script>
<template>
<MermaidDiagram :chart="diagramSources[props.name] || 'flowchart TB\\nA[Diagram not found]'" />
</template>

View File

@@ -1,238 +0,0 @@
export const diagramSources: Record<string, string> = {
'architecture-overview': `flowchart LR
Browser["Браузер клиента"]
Frontend["web-frontend<br/>Nuxt 4 + Vue 3"]
Proxy["/api/graphql proxy"]
Backend["apollo-backend<br/>Apollo Server + Express"]
Prisma["Prisma ORM"]
Db[("PostgreSQL")]
Vault["Vault"]
OneC["1С"]
Bots["Telegram / MAX / email"]
Dokploy["Dokploy"]
Browser --> Frontend
Frontend --> Proxy
Proxy --> Backend
Backend --> Prisma
Prisma --> Db
Backend -.bootstrap env.-> Vault
Backend -.sync / events.-> OneC
Backend -.notifications.-> Bots
Frontend -.deploy.-> Dokploy
Backend -.deploy.-> Dokploy`,
'component-map': `flowchart LR
subgraph UI["UI и маршруты"]
Pages["app/pages"]
Components["app/components"]
Composables["app/composables"]
Middleware["middleware / plugins"]
end
subgraph Transport["Контракт и transport"]
Ops["graphql/operations"]
Generated["generated.ts"]
Apollo["Apollo client"]
Api["server/api/graphql"]
end
subgraph Server["Backend logic"]
Schema["schema.graphql"]
Resolvers["resolvers.js"]
Auth["auth.js / access.js"]
Context["context.js"]
Messenger["messenger / telegram / max"]
end
subgraph Data["Data layer"]
Prisma["prisma-client.js"]
PrismaSchema["schema.prisma"]
Db[("PostgreSQL")]
end
Pages --> Components
Pages --> Composables
Components --> Composables
Composables --> Apollo
Middleware --> Pages
Ops --> Generated
Generated --> Apollo
Apollo --> Api
Api --> Schema
Schema --> Resolvers
Resolvers --> Auth
Resolvers --> Context
Resolvers --> Messenger
Resolvers --> Prisma
Prisma --> PrismaSchema
Prisma --> Db`,
'infrastructure-topology': `flowchart TB
subgraph Repo["Репозиторий fregat"]
FrontendRepo["web-frontend"]
BackendRepo["apollo-backend"]
TgRepo["tg-bot"]
MaxRepo["max-bot"]
BonusRepo["bonus-bot"]
WorkerRepo["hatchet-worker"]
VaultRepo["vault"]
end
subgraph Dokploy["Dokploy"]
FrontendSvc["web-frontend service"]
BackendSvc["apollo-backend service"]
TgSvc["tg-bot service"]
MaxSvc["max-bot service"]
BonusSvc["bonus-bot service"]
WorkerSvc["hatchet-worker service"]
VaultSvc["vault service"]
end
subgraph Infra["Инфраструктурный контур"]
VaultData["Vault raft storage /vault/data"]
BackendDb[("PostgreSQL for app")]
HatchetEngine["Hatchet engine"]
HatchetPg[("PostgreSQL for Hatchet")]
OneC["1С"]
Tailscale["Tailscale SSH / diagnostics"]
end
FrontendRepo --> FrontendSvc
BackendRepo --> BackendSvc
TgRepo --> TgSvc
MaxRepo --> MaxSvc
BonusRepo --> BonusSvc
WorkerRepo --> WorkerSvc
VaultRepo --> VaultSvc
FrontendSvc --> BackendSvc
BackendSvc --> BackendDb
BackendSvc --> VaultSvc
TgSvc --> VaultSvc
MaxSvc --> VaultSvc
BonusSvc --> VaultSvc
WorkerSvc --> VaultSvc
VaultSvc --> VaultData
WorkerSvc --> HatchetEngine
HatchetEngine --> HatchetPg
BackendSvc -.exchange.-> OneC
FrontendSvc -.ops / debug.-> Tailscale
BackendSvc -.ops / debug.-> Tailscale`,
'database-model': `classDiagram
direction LR
class Company
class User
class CounterpartyProfile
class DeliveryAddress
class RegistrationRequest
class Invitation
class MessengerConnection
class Product
class Warehouse
class ProductStock
class CatalogProductTypeSetting
class Cart
class CartItem
class Order
class OrderItem
class OrderStatusEvent
class ReferralLink
class BonusTransaction
class RewardWithdrawalRequest
Company "1" --> "*" User : users
User "1" --> "0..1" CounterpartyProfile : profile
User "1" --> "*" DeliveryAddress : addresses
User "1" --> "*" MessengerConnection : channels
User "1" --> "0..1" Cart : cart
User "1" --> "*" Order : orders
User "1" --> "*" RegistrationRequest : requests
User "1" --> "*" Invitation : invitations
Product "1" --> "*" ProductStock : balances
Warehouse "1" --> "*" ProductStock : inventory
Product "1" --> "*" CartItem : cartItems
Product "1" --> "*" OrderItem : orderItems
Cart "1" --> "*" CartItem : items
Order "1" --> "*" OrderItem : items
Order "1" --> "*" OrderStatusEvent : history
User "1" --> "*" BonusTransaction : bonus
User "1" --> "*" RewardWithdrawalRequest : withdrawals
User "1" --> "*" ReferralLink : referrals
CatalogProductTypeSetting --> Product : productType`,
dashboard: `flowchart TB
subgraph Page["Главная страница клиента"]
Header["Header / навигация"]
Quick["Быстрые действия"]
Active["Актуальные заказы и заявки"]
Notes["Последние уведомления"]
Bonus["Бонусный блок"]
end
Header --> Quick --> Active --> Notes --> Bonus`,
'catalog-grid': `flowchart TB
subgraph Catalog["Каталог продукции"]
Title["Заголовок раздела"]
Search["Поиск"]
Grid["Сетка карточек товарных направлений"]
Cards["Упаковочный скотч | Алюминиевый скотч | Крепп | Вспененный | Двусторонний ПП | Двусторонний PVC"]
end
Title --> Search --> Grid --> Cards`,
'product-card': `flowchart TB
subgraph ProductPage["Карточка товара"]
Title["Заголовок товара и навигация"]
subgraph TopRow["Верхний блок"]
Image["Изображение товара"]
Params["Параметры выбора"]
Action["SKU / действие В корзину"]
end
Help["Пояснения по параметрам"]
Table["Таблица доступных вариантов"]
end
Title --> TopRow --> Help --> Table`,
cart: `flowchart TB
subgraph CartPage["Корзина"]
Items["Список выбранных позиций"]
Delivery["Адрес доставки"]
Comment["Комментарий клиента"]
Submit["Отправить заявку"]
end
Items --> Delivery --> Comment --> Submit`,
'client-order': `flowchart TB
subgraph ClientOrder["Карточка заявки / заказа"]
Summary["Номер документа и статус"]
Composition["Состав позиций"]
Terms["Стоимость и условия поставки"]
History["История изменений"]
end
Summary --> Composition --> Terms --> History`,
'bonus-cabinet': `flowchart TB
subgraph BonusPage["Бонусный кабинет"]
Balance["Текущий бонусный баланс"]
History["История операций"]
Referrals["Реферальные связи"]
Action["Подача заявки на использование или вывод"]
end
Balance --> History --> Referrals --> Action`,
'manager-order': `flowchart TB
subgraph ManagerOrder["Карточка обработки заявки"]
Summary["Клиент / контрагент / менеджер"]
Payload["Состав заявки или расчетный payload"]
Terms["Стоимость и условия поставки"]
Actions["Опубликовать условия / перевести в работу / отменить"]
end
Summary --> Payload --> Terms --> Actions`,
'manager-orders': `flowchart TB
subgraph ManagerOrders["Список заказов менеджера"]
Header["Заголовок раздела"]
Filters["Фильтры: статус / клиент / период"]
Table["Таблица заказов"]
end
Header --> Filters --> Table`,
};

View File

@@ -1,11 +0,0 @@
import DefaultTheme from 'vitepress/theme';
import type { Theme } from 'vitepress';
const theme: Theme = {
...DefaultTheme,
enhanceApp(ctx) {
DefaultTheme.enhanceApp?.(ctx);
},
};
export default theme;

View File

@@ -1,29 +0,0 @@
# Техническое задание на разработку программного продукта
<!--@include: ./tz/project-overview.md-->
<!--@include: ./tz/normative-base.md-->
<!--@include: ./tz/product-scope.md-->
<!--@include: ./tz/functional-requirements.md-->
<!--@include: ./tz/roles-access.md-->
<!--@include: ./tz/data-entities.md-->
<!--@include: ./tz/stage-1/index.md-->
<!--@include: ./tz/integrations.md-->
<!--@include: ./tz/technical-architecture.md-->
<!--@include: ./tz/non-functional-requirements.md-->
<!--@include: ./tz/documentation-requirements.md-->
<!--@include: ./tz/economic-indicators.md-->
<!--@include: ./tz/development-stages.md-->
<!--@include: ./tz/acceptance.md-->

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 86 KiB

View File

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

View File

@@ -1,9 +0,0 @@
{
"theme": "neutral",
"securityLevel": "loose",
"flowchart": {
"useMaxWidth": true,
"htmlLabels": true
},
"fontFamily": "Inter, Arial, sans-serif"
}

View File

@@ -1,9 +0,0 @@
{
"headless": true,
"executablePath": "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
"args": [
"--no-sandbox",
"--disable-setuid-sandbox",
"--disable-dev-shm-usage"
]
}

View File

@@ -1,80 +0,0 @@
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { spawn } from 'node:child_process';
import { diagramSources } from '../.vitepress/theme/components/diagramSources.ts';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const docsRoot = path.resolve(__dirname, '..');
const publicRoot = path.join(docsRoot, 'public');
const diagramsDir = path.join(publicRoot, 'diagrams');
const prototypesDir = path.join(publicRoot, 'prototypes');
const puppeteerConfig = path.join(__dirname, 'puppeteer-config.json');
const mermaidConfig = path.join(__dirname, 'mermaid-config.json');
const prototypeNames = new Set([
'dashboard',
'catalog-grid',
'product-card',
'cart',
'client-order',
'bonus-cabinet',
'manager-order',
'manager-orders',
]);
function runCommand(command, args) {
return new Promise((resolve, reject) => {
const child = spawn(command, args, {
cwd: docsRoot,
stdio: 'inherit',
});
child.on('exit', (code) => {
if (code === 0) {
resolve();
return;
}
reject(new Error(`${command} ${args.join(' ')} exited with code ${code ?? 'unknown'}`));
});
child.on('error', reject);
});
}
async function renderDiagram(name, source) {
const targetDir = prototypeNames.has(name) ? prototypesDir : diagramsDir;
const tmpDir = await mkdtemp(path.join(os.tmpdir(), `mermaid-${name}-`));
const inputFile = path.join(tmpDir, `${name}.mmd`);
const outputFile = path.join(targetDir, `${name}.svg`);
try {
await writeFile(inputFile, source, 'utf8');
await runCommand('pnpm', [
'exec',
'mmdc',
'-i',
inputFile,
'-o',
outputFile,
'-b',
'transparent',
'-c',
mermaidConfig,
'-p',
puppeteerConfig,
]);
} finally {
await rm(tmpDir, { recursive: true, force: true });
}
}
await mkdir(diagramsDir, { recursive: true });
await mkdir(prototypesDir, { recursive: true });
for (const [name, source] of Object.entries(diagramSources)) {
await renderDiagram(name, source);
}

View File

@@ -1,109 +0,0 @@
# 14. Порядок контроля, приемки и гарантийного сопровождения
## 14.1 Общие положения приемки
Приемка результата работ должна подтверждать соответствие программного продукта требованиям настоящего технического задания, условиям договора и согласованным требованиям заказчика.
При приемке подлежат проверке:
- клиентский контур
- менеджерский контур
- каталог готовой продукции
- сценарии заявок на заказ
- сценарии заявок на расчет
- сопровождение заказов
- уведомления
- бонусный и реферальный контур
- интеграционный обмен с 1С в согласованном объеме
- пользовательская и эксплуатационная документация в согласованном объеме
## 14.2 Виды проверок
Для контроля результата работ используются следующие виды проверок:
- функциональная проверка основных пользовательских сценариев
- проверка разграничения ролей и прав доступа
- проверка корректности данных, статусов и истории изменений
- проверка интерфейсов на desktop и mobile
- проверка уведомлений по согласованным каналам
- проверка интеграционного обмена с 1С
- проверка запуска и работы сервисов в согласованном эксплуатационном контуре
## 14.3 Критерии приемки
Программный продукт считается соответствующим требованиям, если:
- обязательные пользовательские сценарии выполняются корректно
- разграничение ролей и прав доступа реализовано корректно
- заявкам, заказам и бонусным операциям присваиваются и отображаются корректные статусы
- каталог и остатки отображаются корректно
- цена не отображается клиенту до публикации условий менеджером
- менеджер имеет возможность обработать заявку и опубликовать условия
- история изменений сохраняется и доступна в предусмотренных сценариях
- сведения из 1С отображаются в согласованном объеме
- текущая задолженность клиента и дата актуальности данных отображаются при наличии этих сведений из 1С
- критичные дефекты, препятствующие выполнению основных сценариев, устранены до передачи результата
## 14.4 Передаваемые материалы
В состав передаваемых заказчику материалов входят:
- программный продукт, размещенный в согласованном эксплуатационном контуре
- исходный код разработанных компонентов в репозитории проекта
- согласованная редакция настоящего технического задания
- пользовательская документация в согласованном объеме
- эксплуатационная документация в согласованном объеме
- интеграционная спецификация 1С, если точные форматы обмена фиксируются отдельно от настоящего технического задания
- перечень ключевых сторонних компонентов, сформированный на основании фактических файлов проекта
Технические схемы, модель данных, роли, архитектура, стек, состав сервисов и требования к интеграциям являются частью настоящего технического задания и не дублируются в отдельных документах без отдельного согласования сторон.
## 14.5 Порядок фиксации замечаний
Каждое замечание, выявленное при приемке, должно содержать:
- описание проблемы
- сценарий воспроизведения
- ожидаемый результат
- фактический результат
- уровень критичности
- статус устранения
Замечания, не препятствующие выполнению основных пользовательских и интеграционных сценариев, могут быть зафиксированы сторонами для последующего устранения в согласованном порядке.
## 14.6 Гарантийный срок
Гарантийный срок на разработанные модули, сервисы и дополнительный функционал составляет 6 месяцев с даты подписания акта приемки выполненных работ, если иной порядок не согласован сторонами.
Гарантия распространяется на дефекты разработанного программного продукта, проявившиеся при штатной эксплуатации и относящиеся к функционалу, реализованному исполнителем.
## 14.7 Порядок гарантийного обращения
Гарантийное обращение должно быть передано исполнителю в письменной форме или иным согласованным сторонами способом.
В обращении должны быть указаны:
- описание дефекта
- пользовательская роль или контур, в котором проявляется дефект
- шаги воспроизведения
- ожидаемый результат
- фактический результат
- дата и время обнаружения
- дополнительные материалы, если они нужны для диагностики
Исполнитель выполняет диагностику дефекта и, если дефект относится к гарантийной зоне ответственности, устраняет его без дополнительной оплаты.
Срок устранения гарантийного дефекта составляет не более 3 дней с даты получения обращения либо иной срок, согласованный сторонами с учетом критичности и характера дефекта.
## 14.8 Ограничения гарантийного сопровождения
Гарантийное сопровождение не распространяется на случаи, когда некорректная работа вызвана:
- самостоятельным изменением программного продукта заказчиком или третьими лицами без согласования с исполнителем
- ошибками сервера, хостинга, инфраструктуры или базы данных, не связанными с разработанным функционалом
- атакой, компрометацией доступа или нарушением требований информационной безопасности со стороны заказчика
- некорректной работой стороннего программного обеспечения
- недоступностью или некорректной работой внешних систем, включая 1С, Telegram, Max, почтовые сервисы и иные внешние API
- изменением форматов или правил работы внешних систем без предварительного согласования и обновления интеграционной спецификации
Если дефект связан с внешней системой или инфраструктурой, исполнитель фиксирует результат диагностики и передает заказчику сведения, достаточные для дальнейшего устранения причины на стороне соответствующей системы или поставщика.

View File

@@ -1,484 +0,0 @@
# 6. Требования к данным и сущностям
## 6.1 Общие требования к данным
Основное хранилище данных программного продукта реализуется на `PostgreSQL`. Прикладной доступ к данным осуществляется через `Prisma ORM`.
Система должна обеспечивать хранение:
- пользователей и ролей
- компаний и профилей контрагентов
- адресов доставки
- каталога и складских остатков
- корзины и ее позиций
- заказов и расчетных заявок
- событий изменения статусов
- подключений мессенджеров
- бонусных и реферальных сущностей
В прикладной реализации должны использоваться фактические сущности базы данных, определенные в `schema.prisma`. Наименование сущностей в документации и в базе данных должно сопоставляться однозначно.
## 6.2 Справочник сущностей базы данных
| Модель в базе данных | Русское наименование | Назначение |
| --- | --- | --- |
| `Company` | Компания | Клиентская организация |
| `User` | Пользователь | Учетная запись клиента, менеджера или суперменеджера |
| `DeliveryAddress` | Адрес доставки | Справочник адресов доставки клиента |
| `CounterpartyProfile` | Профиль контрагента | Юридические и банковские реквизиты клиента |
| `RegistrationRequest` | Заявка на подключение | Самостоятельная заявка клиента на подключение |
| `Invitation` | Приглашение | Менеджерское приглашение на регистрацию |
| `MessengerConnection` | Подключение мессенджера | Связка пользователя с Telegram или MAX |
| `Product` | Товар | Карточка товарной позиции каталога |
| `CatalogProductTypeSetting` | Настройки типа товара | Правила параметров и кастомизации по товарному направлению |
| `Cart` | Корзина | Корзина клиента |
| `CartItem` | Позиция корзины | Конкретный выбранный товар в корзине |
| `Warehouse` | Склад | Справочник складов |
| `ProductStock` | Складской остаток | Остаток товара на складе |
| `Order` | Заказ / заявка | Единая заказная сущность для готовой продукции и расчета |
| `OrderItem` | Позиция заказа | Состав заказа |
| `OrderStatusEvent` | Событие статуса заказа | История изменения статусов |
| `ReferralLink` | Реферальная связь | Связь между рекомендателем и приглашенным клиентом |
| `BonusTransaction` | Бонусная транзакция | Начисление или списание бонусов |
| `RewardWithdrawalRequest` | Заявка на вывод бонусов | Заявка клиента на использование или вывод бонусов |
## 6.3 Служебные перечисления и статусы
В модели данных используются следующие перечисления:
- `UserRole`: `CLIENT`, `MANAGER`, `SUPER_MANAGER`
- `RegistrationStatus`: `PENDING`, `APPROVED`, `REJECTED`
- `MessengerType`: `TELEGRAM`, `MAX`
- `OrderKind`: `READY`, `CALCULATION`
- `OrderStatus`: `NEW`, `MANAGER_PROCESSING`, `WAITING_DOUBLE_CONFIRM`, `CLIENT_REJECTED`, `MANAGER_REJECTED`, `MANAGER_BLOCKED`, `CONFIRMED`, `IN_PROGRESS`, `COMPLETED`
- `WithdrawalStatus`: `PENDING`, `APPROVED`, `REJECTED`
## 6.4 Пользователи и компании
### 6.4.1 Company
Русское наименование: `Компания`
Назначение:
- хранение клиентской организации
- объединение пользователей одной компании
Основные поля:
- `id`
- `name`
- `inn`
- `createdAt`
- `updatedAt`
Связи:
- одна компания связана со многими пользователями
### 6.4.2 User
Русское наименование: `Пользователь`
Назначение:
- хранение клиентской, менеджерской или административной учетной записи
- связывание пользователя с заказами, корзиной, бонусами и адресами
Основные поля:
- `id`
- `email`
- `fullName`
- `role`
- `companyId`
- `defaultDeliveryAddressId`
- `createdAt`
- `updatedAt`
Связи:
- пользователь может быть связан с компанией
- пользователь может иметь профиль контрагента
- пользователь может иметь адреса доставки
- пользователь может иметь корзину
- пользователь может выступать клиентом или менеджером в заказах
- пользователь может иметь бонусные операции и заявки на вывод
### 6.4.3 DeliveryAddress
Русское наименование: `Адрес доставки`
Назначение:
- хранение адресов доставки клиента
- выбор адреса по умолчанию для корзины и заказов
Основные поля:
- `id`
- `userId`
- `label`
- `address`
- `unrestrictedValue`
- `fiasId`
- `createdAt`
- `updatedAt`
### 6.4.4 CounterpartyProfile
Русское наименование: `Профиль контрагента`
Назначение:
- хранение полных реквизитов клиента для договоров, счетов и поставки
Основные поля:
- `id`
- `userId`
- `companyName`
- `companyFullName`
- `inn`
- `kpp`
- `ogrn`
- `legalAddress`
- `bankName`
- `bik`
- `correspondentAccount`
- `checkingAccount`
- `signerFullName`
- `signerPosition`
- `signerBasis`
- `createdAt`
- `updatedAt`
### 6.4.5 RegistrationRequest
Русское наименование: `Заявка на подключение`
Назначение:
- хранение самостоятельной заявки клиента на подключение
Основные поля:
- `id`
- `companyName`
- `inn`
- `contactName`
- `email`
- `status`
- `rejectionReason`
- `requesterId`
- `reviewedById`
- `createdAt`
- `updatedAt`
### 6.4.6 Invitation
Русское наименование: `Приглашение`
Назначение:
- хранение менеджерского приглашения клиента на регистрацию
Основные поля:
- `id`
- `token`
- `email`
- `companyName`
- `managerId`
- `acceptedById`
- `expiresAt`
- `acceptedAt`
- `createdAt`
### 6.4.7 MessengerConnection
Русское наименование: `Подключение мессенджера`
Назначение:
- хранение подключенного Telegram или MAX-канала пользователя
Основные поля:
- `id`
- `userId`
- `type`
- `channelId`
- `displayName`
- `username`
- `avatarFileId`
- `avatarFileUniqueId`
- `isActive`
- `createdAt`
## 6.5 Каталог и складской контур
### 6.5.1 Product
Русское наименование: `Товар`
Назначение:
- хранение карточки товарной позиции каталога
- хранение параметров товара и признаков кастомизации
Основные поля:
- `id`
- `sku`
- `name`
- `productType`
- `widthMm`
- `lengthM`
- `thicknessMicron`
- `sleeveBrand`
- `quantityPerBox`
- `tags`
- `description`
- `isCustomizable`
- `isActive`
- `createdAt`
- `updatedAt`
### 6.5.2 CatalogProductTypeSetting
Русское наименование: `Настройки типа товара`
Назначение:
- хранение правил параметров по товарному направлению
- хранение разрешений на кастомизацию
Основные поля:
- `id`
- `productType`
- `showQuantityPerBox`
- `allowCustomLength`
- `customLengthMinM`
- `customLengthMaxM`
- `customLengthStepM`
- `allowCustomSleeveBrand`
- `allowCustomLabel`
- `widthOptionsMm`
- `lengthOptionsM`
- `thicknessOptionsMicron`
- `sleeveOptions`
- `colorOptions`
- `labelOptions`
- `createdAt`
- `updatedAt`
### 6.5.3 Warehouse
Русское наименование: `Склад`
Назначение:
- хранение справочника складов
Основные поля:
- `id`
- `code`
- `name`
- `createdAt`
- `updatedAt`
### 6.5.4 ProductStock
Русское наименование: `Складской остаток`
Назначение:
- хранение остатка товара на конкретном складе
Основные поля:
- `id`
- `productId`
- `warehouseId`
- `availableQty`
- `updatedAt`
## 6.6 Корзина, заявки и заказы
### 6.6.1 Cart
Русское наименование: `Корзина`
Назначение:
- хранение текущего набора выбранных клиентом позиций
Основные поля:
- `id`
- `userId`
- `deliveryAddressId`
- `createdAt`
- `updatedAt`
### 6.6.2 CartItem
Русское наименование: `Позиция корзины`
Назначение:
- хранение одной выбранной клиентом позиции с параметрами и количеством
Основные поля:
- `id`
- `cartId`
- `productId`
- `productName`
- `sku`
- `isCustomizable`
- `quantity`
- `parameters`
- `createdAt`
- `updatedAt`
### 6.6.3 Order
Русское наименование: `Заказ / заявка`
Назначение:
- хранение готовой заказной заявки и расчетной заявки в единой сущности
- хранение согласованных менеджером условий и статуса работы
Основные поля:
- `id`
- `code`
- `kind`
- `customerId`
- `deliveryAddressId`
- `deliveryAddress`
- `managerId`
- `status`
- `clientApproved`
- `managerApproved`
- `blockReason`
- `deliveryTerms`
- `deliveryFee`
- `totalPrice`
- `calculationPayload`
- `createdAt`
- `updatedAt`
Комментарий к модели:
- `kind = READY` означает сценарий заказа готовой продукции
- `kind = CALCULATION` означает сценарий расчета индивидуальной продукции
- поле `calculationPayload` хранит параметры расчетной заявки
### 6.6.4 OrderItem
Русское наименование: `Позиция заказа`
Назначение:
- хранение состава заказа
Основные поля:
- `id`
- `orderId`
- `productId`
- `productName`
- `quantity`
- `unitPrice`
- `createdAt`
### 6.6.5 OrderStatusEvent
Русское наименование: `Событие статуса заказа`
Назначение:
- хранение истории изменения статусов заказа или заявки
Основные поля:
- `id`
- `orderId`
- `status`
- `actorUserId`
- `note`
- `createdAt`
## 6.7 Бонусный и реферальный контур
### 6.7.1 ReferralLink
Русское наименование: `Реферальная связь`
Назначение:
- фиксация связи между рекомендателем и приглашенным клиентом
Основные поля:
- `id`
- `referrerId`
- `refereeId`
- `createdById`
- `bonusPercent`
- `createdAt`
### 6.7.2 BonusTransaction
Русское наименование: `Бонусная транзакция`
Назначение:
- хранение начисления или списания бонусов
Основные поля:
- `id`
- `userId`
- `amount`
- `reason`
- `orderId`
- `referralLinkId`
- `createdAt`
### 6.7.3 RewardWithdrawalRequest
Русское наименование: `Заявка на вывод бонусов`
Назначение:
- хранение заявки клиента на использование или вывод бонусов
Основные поля:
- `id`
- `requesterId`
- `amount`
- `status`
- `reviewedById`
- `reviewComment`
- `createdAt`
- `updatedAt`
## 6.8 Основные связи между сущностями
Укрупненная структура связей определяется следующими правилами:
- `Company` объединяет пользователей одной клиентской организации
- `User` связан с `CounterpartyProfile`, `DeliveryAddress`, `MessengerConnection`, `Cart`, `Order`, `BonusTransaction` и `RewardWithdrawalRequest`
- `Cart` содержит набор `CartItem`, привязанных к конкретным `Product`
- `Order` содержит набор `OrderItem` и историю `OrderStatusEvent`
- `Product` связан с остатками `ProductStock`, распределенными по сущностям `Warehouse`
- настройки параметров по товарному направлению хранятся в `CatalogProductTypeSetting`
- реферальные связи реализуются через `ReferralLink`, связывающий одного пользователя с другим пользователем

View File

@@ -1,99 +0,0 @@
# 13. Стадии и этапы разработки
## 13.1 Общий порядок выполнения работ
Работы выполняются поэтапно, чтобы согласовывать ключевые решения до перехода к следующей части реализации.
Переход к следующему этапу выполняется после согласования сторонами результата предыдущего этапа либо после фиксации замечаний, не препятствующих продолжению работ.
## 13.2 Этап 1. Разработка и согласование технического задания
На этапе разрабатывается и согласуется настоящее техническое задание.
Результат этапа:
- согласованная редакция технического задания
- зафиксированные границы продукта
- зафиксированный состав пользовательских ролей
- зафиксированные функциональные, интеграционные, технические и эксплуатационные требования
Критерий завершения этапа: утверждение технического задания сторонами.
## 13.3 Этап 2. UX/UI и согласование визуального подхода
На этапе подготавливаются 2-3 сверстанные страницы личного кабинета с основными элементами интерфейса.
В состав страниц для согласования могут входить:
- страница входа или регистрации
- каталог либо карточка товара
- корзина либо карточка заявки
- менеджерская карточка клиента или заказа
Результат этапа:
- согласованный визуальный подход
- согласованные базовые интерфейсные элементы
- подтверждение применимости выбранного подхода для клиентского и менеджерского контуров
Критерий завершения этапа: согласование визуального подхода сторонами.
## 13.4 Этап 3. Функциональная реализация без интеграции с 1С
На этапе реализуются основные пользовательские и менеджерские сценарии без подключения обмена с 1С.
В состав этапа входят:
- регистрация и подключение клиентов
- роли и разграничение доступа
- каталог готовой продукции
- корзина и заявки на заказ
- заявки на расчет индивидуальной продукции
- обработка заявок менеджером
- статусы и история изменений
- уведомления в согласованном объеме
- бонусный и реферальный контур
- административные настройки, необходимые для работы продукта
Результат этапа:
- работоспособный программный продукт с основным функционалом
- возможность проверки клиентских, менеджерских и бонусных сценариев без обмена с 1С
Критерий завершения этапа: готовность и приемка основного функционала без интеграции с 1С.
## 13.5 Этап 4. Интеграция с 1С и отладка обмена
На этапе выполняются подключение, настройка и отладка интеграции с 1С.
В состав этапа входят:
- согласование или уточнение интеграционной спецификации
- настройка приема webhook-событий от 1С
- настройка получения данных из 1С через согласованные методы
- сопоставление внутренних идентификаторов и идентификаторов 1С
- проверка получения каталога, остатков, заказов, статусов и задолженности
- проверка обработки дублей и ошибок обмена
- проверка отображения даты актуальности данных
Результат этапа:
- работоспособный интеграционный обмен с 1С в согласованном объеме
- журналирование ключевых интеграционных событий
- подтвержденная работоспособность сценариев, зависящих от данных 1С
Критерий завершения этапа: подтвержденная сторонами работоспособность сценариев с 1С в согласованном объеме.
## 13.6 Этап 5. Передача результата и приемка
На этапе выполняются итоговая проверка, устранение критичных замечаний и передача результата работ.
Результат этапа:
- размещенный программный продукт в согласованном эксплуатационном контуре
- согласованная редакция технического задания
- пользовательская и эксплуатационная документация в согласованном объеме
- перечень ключевых сторонних компонентов
- акт приемки выполненных работ
Критерий завершения этапа: подписание акта приемки либо наступление условий приемки, предусмотренных договором.

View File

@@ -1,89 +0,0 @@
# 11. Требования к программной документации
## 11.1 Общий подход
Настоящее техническое задание является основным техническим документом программного продукта.
В составе настоящего технического задания фиксируются:
- назначение и границы продукта
- функциональные требования
- роли пользователей и права доступа
- требования к данным, сущностям и модели базы данных
- требования к интерфейсам
- требования к интеграциям
- архитектура, стек, компоненты и эксплуатационный контур
- нефункциональные требования
- порядок контроля, приемки и гарантийного сопровождения
Отдельные документы не должны дублировать техническое задание. Дополнительная документация должна описывать только то, что необходимо для использования, эксплуатации или интеграции программного продукта и не раскрыто в настоящем документе в достаточном объеме.
## 11.2 Пользовательская документация
Пользовательская документация должна быть подготовлена в объеме, достаточном для работы пользователей в предусмотренных ролях:
- клиент
- менеджер
- суперменеджер
Пользовательская документация должна описывать:
- вход в личный кабинет и завершение регистрации
- работу с профилем и каналами уведомлений
- просмотр каталога готовой продукции
- добавление товаров в корзину и отправку заявки
- создание заявки на расчет индивидуальной продукции
- просмотр заказов, статусов, условий и истории изменений
- работу с бонусным кабинетом, бонусным балансом и заявками на вывод
- действия менеджера по обработке клиентов, заявок, заказов и бонусных операций
- действия суперменеджера в административных разделах, если они отличаются от действий менеджера
Документация должна быть написана прикладным языком и ориентирована на выполнение пользовательских сценариев, а не на описание внутренней реализации.
## 11.3 Эксплуатационная документация
Эксплуатационная документация должна быть подготовлена в объеме, достаточном для сопровождения программного продукта после передачи результата работ.
Эксплуатационная документация должна описывать:
- состав сервисов и их назначение
- порядок запуска и перезапуска сервисов через согласованный контур деплоя
- используемые окружения и общие принципы конфигурации
- порядок загрузки секретов из Vault
- порядок просмотра логов и диагностики типовых сбоев
- порядок проверки работоспособности клиентского, менеджерского и интеграционного контуров
- порядок обновления приложения через Git и Dokploy
- перечень технических контактов или зон ответственности, если они согласованы сторонами
Эксплуатационная документация не должна содержать бизнес-секреты, токены, пароли и иные чувствительные значения. Для секретов указываются только имена переменных, назначение и источник получения.
## 11.4 Интеграционная документация
Для интеграции с 1С должна быть подготовлена интеграционная спецификация либо отдельный раздел настоящего технического задания, если к моменту согласования ТЗ формат обмена уже определен.
Интеграционная документация должна описывать:
- состав событий, передаваемых из 1С
- состав методов получения данных из 1С
- структуру payload для каждого события и метода
- обязательные и необязательные поля
- правила сопоставления идентификаторов
- требования к авторизации, подписи или иному механизму защиты запросов
- порядок обработки дублей
- порядок фиксации ошибок и повторной обработки сообщений
- критерии приемки интеграционного обмена
Если точный формат обмена с 1С не определен на момент утверждения ТЗ, он фиксируется отдельной согласованной интеграционной спецификацией до начала завершающего этапа интеграции.
## 11.5 Перечень сторонних компонентов
Перечень сторонних компонентов формируется на основании фактических файлов проекта, включая `package.json`, lock-файлы, Dockerfile и конфигурационные файлы сервисов.
Перечень должен содержать:
- наименование компонента
- версию или диапазон версий
- назначение компонента в продукте
- источник установки или репозиторий, если он отличается от стандартного пакетного менеджера
Ключевые сторонние компоненты, используемые в текущей реализации, перечислены в разделе технической архитектуры настоящего технического задания.

View File

@@ -1,25 +0,0 @@
# 12. Технико-экономические показатели
## 12.1 Назначение показателей
Технико-экономические показатели используются для фиксации ожидаемого прикладного эффекта от разработки программного продукта.
Расчет финансовой эффективности, окупаемости или экономического эффекта в денежном выражении не входит в состав настоящего технического задания, если стороны не согласуют такой расчет отдельно.
## 12.2 Ожидаемый прикладной эффект
Разработка программного продукта должна обеспечить:
- снижение объема ручной коммуникации при приеме и сопровождении заказов
- единый интерфейс для клиента, менеджера и суперменеджера
- ускорение обработки заявок за счет фиксации состава, параметров и статусов в системе
- снижение риска потери информации по заказам, заявкам и бонусным операциям
- повышение прозрачности статусов заказов и актуальности данных для клиента
- централизованное хранение истории изменений
- возможность дальнейшего развития клиентского, менеджерского, бонусного и интеграционного контуров
## 12.3 Ограничения
Программный продукт не заменяет учетную систему 1С и не является первичным источником бухгалтерских, складских или финансовых данных.
Экономический эффект зависит от полноты внедрения продукта в рабочие процессы заказчика, качества данных 1С, доступности внешних каналов уведомлений и соблюдения эксплуатационных требований.

View File

@@ -1,183 +0,0 @@
# 4. Функциональные требования
## 4.1 Требования к регистрации и подключению клиентов
Система должна поддерживать два базовых сценария подключения клиента:
- регистрация по персональному приглашению
- самостоятельная заявка на подключение
Функциональные требования:
1. Менеджер должен иметь возможность направить клиенту приглашение на регистрацию по электронной почте.
2. Клиент должен иметь возможность завершить регистрацию по персональной ссылке.
3. Клиент должен иметь возможность подать заявку на подключение через публичную форму.
4. Самостоятельная заявка должна поступать в менеджерский контур на рассмотрение.
5. Менеджер должен иметь возможность подтвердить либо отклонить заявку на подключение.
6. При подтверждении заявки система должна предоставить клиенту возможность завершить регистрацию.
7. После завершения регистрации клиент должен получить доступ к личному кабинету.
8. Система должна поддерживать подключение доступных каналов уведомлений для клиентской учетной записи.
## 4.2 Требования к каталогу готовой продукции
Система должна предоставлять клиенту каталог готовой продукции без отображения цены до обработки менеджером.
Функциональные требования:
1. Система должна отображать список товарных направлений.
2. Для каждого товарного направления система должна предоставлять отдельную карточку товара.
3. В карточке товара система должна отображать параметры выбора, применимые к данному типу продукции.
4. В карточке товара система должна отображать доступные стандартные варианты.
5. Для каждой доступной позиции система должна отображать складские остатки.
6. Система должна позволять клиенту выбрать параметры и добавить позицию в корзину.
7. Система должна исключать отображение стоимости до момента публикации условий менеджером.
8. Для параметров товара система должна отображать пояснения, помогающие клиенту понять назначение параметра и ограничения выбора.
## 4.3 Требования к параметрам каталога и кастомизации
Система должна поддерживать настройку параметров по каждому товарному направлению.
Функциональные требования:
1. Для каждого типа продукции должен задаваться перечень стандартных параметров выбора.
2. Для параметров длины должна поддерживаться настройка доступных стандартных значений.
3. Для параметров длины должна поддерживаться возможность индивидуального значения при наличии соответствующего разрешения.
4. Для параметров втулки должна поддерживаться возможность заказа втулки с логотипом при наличии соответствующего разрешения.
5. Для параметров надписи должна поддерживаться возможность заказа индивидуального нанесения при наличии соответствующего разрешения.
6. Наборы стандартных параметров должны редактироваться в административном контуре.
7. Изменение набора стандартных параметров не должно приводить к потере уже сохраненных заказных данных.
## 4.4 Требования к корзине и заявке на заказ
Система должна позволять клиенту собрать корзину и направить заявку на заказ.
Функциональные требования:
1. Клиент должен видеть перечень выбранных позиций.
2. Для каждой позиции клиент должен иметь возможность изменить количество.
3. Клиент должен иметь возможность удалить позицию из корзины.
4. Клиент должен иметь возможность направить заявку менеджеру.
5. После отправки заявки система должна зафиксировать состав, параметры и количество позиций.
6. Для заявки должны сохраняться дата создания, инициатор и закрепленный менеджер.
7. До обработки менеджером стоимость в заявке не должна отображаться клиенту.
## 4.5 Требования к обработке заявки менеджером
Менеджер должен иметь возможность обработать клиентскую заявку вручную.
Функциональные требования:
1. Менеджер должен видеть состав заявки и параметры заказанных позиций.
2. Менеджер должен видеть карточку клиента и сведения о контрагенте.
3. Менеджер должен иметь возможность указать стоимость.
4. Менеджер должен иметь возможность указать условия поставки и доставки.
5. Менеджер должен иметь возможность оставить комментарий к заявке.
6. Менеджер должен иметь возможность опубликовать согласованные условия клиенту.
7. До перевода заявки в работу менеджер должен иметь возможность скорректировать опубликованные условия.
8. Менеджер должен иметь возможность перевести заявку в работу.
9. Менеджер должен иметь возможность отменить заявку с фиксацией основания отмены.
## 4.6 Требования к заявке на расчет индивидуальной продукции
Система должна поддерживать отдельный сценарий расчета продукции с индивидуальными параметрами.
Функциональные требования:
1. Клиент должен иметь возможность перейти из каталога в сценарий расчета индивидуальной продукции.
2. Клиент должен иметь возможность указать параметры изделия.
3. Клиент должен иметь возможность приложить комментарий к заявке.
4. Клиент должен иметь возможность направить заявку менеджеру.
5. Менеджер должен иметь возможность обработать такую заявку по правилам, аналогичным заявке на заказ.
6. Стоимость и условия поставки должны публиковаться клиенту только после ручной обработки менеджером.
Минимальный состав параметров расчетной заявки должен поддерживать:
- тип продукции
- ширину
- длину
- толщину
- цвет
- надпись или маркировку
- иные параметры в зависимости от вида продукции
- текстовый комментарий клиента
## 4.7 Требования к статусам заявок
Система должна обеспечивать сквозное сопровождение заявок по статусам.
Для заявок на заказ и заявок на расчет должны поддерживаться следующие базовые статусы:
- создана
- направлена менеджеру
- обработана менеджером
- условия опубликованы
- в работе
- отменена
Для каждого изменения статуса система должна сохранять:
- предыдущее состояние
- новое состояние
- дату и время изменения
- пользователя или источник, выполнивший изменение
- комментарий, если он предусмотрен сценарием
## 4.8 Требования к заказам и их сопровождению
Система должна предоставлять клиенту и менеджеру доступ к списку заказов, карточке каждого заказа и актуальным учетным сведениям, полученным из 1С.
Функциональные требования:
1. Система должна отображать перечень заказов клиента.
2. Система должна поддерживать фильтрацию заказов по периоду и статусу.
3. Для каждого заказа система должна предоставлять отдельную карточку.
4. В карточке заказа должны отображаться состав, статус, стоимость, условия поставки и история изменений.
5. В карточке заказа должна отображаться дата актуальности данных.
6. При наличии обновлений из внешней системы сведения по заказу должны синхронизироваться и отображаться пользователю.
7. Система должна отображать текущую задолженность клиента, если такие сведения получены из 1С.
8. Для задолженности должна отображаться дата актуальности данных.
## 4.9 Требования к уведомлениям
Система должна поддерживать уведомления по нескольким каналам связи.
Поддерживаемые каналы:
- электронная почта
- Telegram
- Max
Система должна поддерживать уведомления по следующим событиям:
- приглашение к регистрации
- подтверждение либо отклонение заявки на подключение
- публикация условий по заявке
- изменение статуса заказа
- изменение бонусного баланса
- обработка заявки на использование либо вывод бонусов
## 4.10 Требования к бонусной и реферальной программе
Система должна включать бонусный контур как самостоятельную функциональную область с отдельным пользовательским интерфейсом.
Функциональные требования:
1. Система должна хранить правила участия клиента в бонусной программе.
2. Система должна поддерживать фиксацию реферальных связей.
3. Система должна хранить начисления, списания и текущий остаток бонусов.
4. Клиент должен видеть текущий бонусный баланс.
5. Клиент должен видеть историю бонусных операций.
6. Клиент должен иметь возможность использовать бонусы в пределах установленных правил.
7. Клиент должен иметь возможность подать заявку на вывод либо иную операцию, если это предусмотрено правилами программы.
8. Менеджер должен иметь возможность обрабатывать операции бонусного контура.
9. Система должна уведомлять клиента об изменениях бонусного состояния.
## 4.11 Требования к административным настройкам
Система должна содержать административные разделы для управления следующими объектами:
- параметрами каталога
- пользовательскими описаниями параметров
- шаблонами уведомлений
- параметрами синхронизации
- отдельными настройками бонусного контура

View File

@@ -1,26 +0,0 @@
# Разделы технического задания
Настоящий раздел содержит полный состав технического задания на разработку программного продукта `Личный кабинет Фрегат`.
## Содержание
1. [Введение, основание, цель и состав проекта](/tz/project-overview)
2. [Основания для разработки и нормативные материалы](/tz/normative-base)
3. [Назначение и границы программного продукта](/tz/product-scope)
4. [Функциональные требования](/tz/functional-requirements)
5. [Роли пользователей и права доступа](/tz/roles-access)
6. [Требования к данным, сущностям и модели базы данных](/tz/data-entities)
7. [Требования к интерфейсу и прототипам](/tz/stage-1/)
8. [Требования к интеграции с 1С и внешним интерфейсам](/tz/integrations)
9. [Техническая архитектура, стек и эксплуатационный контур](/tz/technical-architecture)
10. [Нефункциональные требования](/tz/non-functional-requirements)
11. [Требования к программной документации](/tz/documentation-requirements)
12. [Технико-экономические показатели](/tz/economic-indicators)
13. [Стадии и этапы разработки](/tz/development-stages)
14. [Порядок контроля, приемки и гарантийного сопровождения](/tz/acceptance)
## Назначение раздела
Материалы раздела используются для согласования требований к программному продукту, его функциям, данным, интерфейсам, интеграциям и условиям приемки.
Технические схемы, описание данных, архитектура, роли и интеграции являются частью настоящего технического задания и не требуют дублирования в отдельных документах, если иное не согласовано сторонами.

View File

@@ -1,137 +0,0 @@
# 8. Требования к интеграции с 1С и внешним интерфейсам
## 8.1 Общие требования к интеграционному контуру
Интеграционный слой должен обеспечивать обмен данными между личным кабинетом и внешними системами без потери целостности внутренних сущностей и статусов.
Интеграционный контур должен обеспечивать:
- получение данных из 1С
- прием событий от 1С
- передачу во внешние системы данных, необходимых для сопровождения заказов и клиентов, если такой обмен согласован сторонами
- сопоставление внутренних идентификаторов и идентификаторов внешних систем
- регистрацию входящих и исходящих операций обмена
- повторную обработку неуспешных сообщений
- хранение истории обновлений по интеграционным операциям
## 8.2 Интеграция с 1С
Интеграция с 1С должна обеспечивать обмен данными, необходимыми для сопровождения каталога, заказов, статусов, остатков и сведений о задолженности клиента.
Система должна обеспечивать получение из 1С следующих данных:
- каталог товаров
- характеристики товаров
- складские остатки
- сведения о заказах
- статусы заказов
- изменения состава, стоимости, доставки и иных существенных параметров заказа
- текущая задолженность клиента
- дата актуальности сведений, полученных из 1С
1С рассматривается как первичный источник учетных данных по заказам, складам, статусам, стоимости, доставке и задолженности. Личный кабинет отображает эти сведения и фиксирует дату их актуальности.
## 8.3 События от 1С
Система должна поддерживать прием webhook-событий от 1С в согласованном формате.
Минимальный состав событий:
- создание нового заказа
- изменение информации по заказу
- изменение статуса заказа
- изменение сроков, условий или параметров доставки
- изменение состава заказа
- изменение сведений о задолженности клиента, если такие данные передаются событийно
Для каждого события должны фиксироваться:
- тип события
- внешний идентификатор объекта 1С
- внутренний идентификатор объекта, если сопоставление выполнено
- дата и время события
- источник события
- статус обработки
- текст ошибки при неуспешной обработке
Система должна защищаться от повторной обработки дублей webhook-событий.
## 8.4 Методы получения данных из 1С
Система должна поддерживать получение данных из 1С через согласованные методы.
Минимальный состав методов:
- получение заказов клиента
- получение товарного каталога
- получение характеристик товаров
- получение доступных остатков по складам
- получение статусов и изменений по заказам
- получение текущей задолженности клиента и даты актуальности этих сведений
Точный набор методов, параметры запросов, формат ответов и ограничения по частоте вызовов фиксируются в интеграционной спецификации.
## 8.5 Требования к структурам обмена
Входные и выходные данные интеграционного слоя должны описываться в структурированном виде и позволять однозначно восстанавливать:
- тип объекта обмена
- идентификатор объекта
- дату и время события
- полезную нагрузку объекта
- статус обработки
- источник изменения
Для обмена должны использоваться структурированные payload-форматы, пригодные для сериализации в `JSON`.
Точная структура payload, схема подписи запросов и набор обязательных полей согласуются сторонами до начала этапа интеграции с 1С.
## 8.6 Интеграционные поля и служебные атрибуты
Для сущностей, участвующих в обмене, должны поддерживаться:
- внешний идентификатор учетной системы
- дата последней синхронизации
- источник последнего обновления
- признак успешной или неуспешной обработки
- журнал интеграционных ошибок при наличии
- технический идентификатор последнего обработанного события, если он передается 1С
## 8.7 Журналирование интеграционных операций
Для ключевых операций обмена система должна сохранять:
- тип объекта
- идентификатор объекта
- предыдущее состояние
- новое состояние
- дату и время изменения
- пользователя либо внешний источник, выполнивший изменение
- статус обработки сообщения
- текст ошибки при наличии
## 8.8 Требования к защите интеграционного обмена
Интеграционные запросы должны выполняться с использованием согласованного механизма авторизации или подписи.
Система должна отклонять интеграционные запросы, если:
- отсутствуют обязательные параметры авторизации
- подпись или токен не прошли проверку
- payload не соответствует согласованной структуре
- невозможно определить тип события или объект обработки
Секреты, используемые для интеграции с 1С, должны храниться только в Vault и передаваться сервисам через runtime-конфигурацию.
## 8.9 Критерии приемки интеграции с 1С
Интеграция с 1С считается готовой в согласованном объеме, если:
- каталог и характеристики товаров получаются и отображаются в личном кабинете
- остатки по складам отображаются в карточках товаров
- заказы клиента получаются и отображаются с актуальными статусами
- изменения заказа из 1С отображаются в карточке заказа
- текущая задолженность клиента и дата актуальности данных отображаются в предусмотренных интерфейсах
- webhook-события не обрабатываются повторно при дублях
- ошибки интеграционного обмена фиксируются в журнале
- неуспешные сообщения могут быть проанализированы и повторно обработаны в согласованном порядке

View File

@@ -1,69 +0,0 @@
# 10. Нефункциональные требования
## 10.1 Общие требования к архитектуре
Программный продукт должен быть реализован как веб-система с разделением клиентского и менеджерского контуров, серверной бизнес-логикой, постоянным хранением данных и возможностью интеграционного обмена с внешними системами.
## 10.2 Требования к доступности интерфейсов
Система должна обеспечивать корректную работу:
- в десктопных браузерах
- в мобильных браузерах
- на основных пользовательских разрешениях экрана
Интерфейсы должны сохранять работоспособность и читаемость при адаптивном отображении.
## 10.3 Требования к производительности
При штатной эксплуатации система должна обеспечивать:
- приемлемое время открытия основных экранов
- приемлемое время отправки заявок и выполнения пользовательских действий
- отображение каталогов, карточек и заказов без заметных задержек при типовом объеме данных
Точные количественные показатели производительности подлежат фиксации в рабочей документации по инфраструктуре и тестированию.
## 10.4 Требования к безопасности
Система должна обеспечивать:
- аутентификацию пользователей
- авторизацию по ролям
- ограничение доступа клиента только к данным своего контрагента
- хранение и передачу чувствительных данных с использованием защищенных механизмов
- исключение несанкционированного доступа к административным функциям
## 10.5 Требования к надежности и журналированию
Система должна обеспечивать:
- сохранность пользовательских данных
- сохранность истории изменений по заявкам, заказам и бонусным операциям
- фиксацию ошибок интеграционного обмена
- фиксацию значимых системных и пользовательских событий
## 10.6 Требования к сопровождаемости
Реализация должна обеспечивать возможность:
- сопровождения и развития клиентского контура
- сопровождения и развития менеджерского контура
- изменения параметров каталога и уведомлений без переработки базовой структуры системы
- расширения интеграционного обмена с 1С и иными внешними системами
## 10.7 Требования к данным и актуальности сведений
Система должна обеспечивать:
- хранение актуального состояния пользовательских данных
- отображение даты актуальности сведений, полученных из внешних систем, когда это применимо
- защиту от потери данных при обновлении параметров каталога и заказных сущностей
## 10.8 Требования к документации
По результатам выполнения работ должна быть сформирована документация, достаточная для:
- приемки результата работ
- дальнейшего сопровождения программного продукта
- понимания состава функций, данных, ролей и интеграций

View File

@@ -1,37 +0,0 @@
# 2. Основания для разработки и нормативные материалы
## 2.1 Основания для разработки
Разработка программного продукта выполняется на основании следующих документов:
- договор на разработку программного продукта №28/04-26ПО от 28 апреля 2026 года
- приложение №1 к договору: спецификация основных требований к программному обеспечению `Личный кабинет Фрегат`
- согласованные требования заказчика к клиентскому, менеджерскому, бонусному и интеграционному контурам
## 2.2 Нормативные и методические материалы
Настоящее техническое задание подготовлено как документ для согласования требований к разработке веб-программного продукта.
Настоящий документ устанавливает требования к программному продукту применительно к современному веб-решению с разграничением ролей, интеграцией с учетной системой, журналированием событий и поддержкой клиентских и менеджерских сценариев.
## 2.3 Исходные материалы для детализации требований
При разработке технического задания использованы следующие исходные материалы:
- договорная документация заказчика
- согласованные бизнес-сценарии работы клиента и менеджера
- перечень разделов личного кабинета
- перечень основных товарных направлений и параметров каталога
- требования к обмену данными с учетной системой 1С
- требования к уведомлениям, бонусной программе и административным настройкам
## 2.4 Назначение настоящего документа
Настоящий документ предназначен для:
- фиксации полного объема требований к программному продукту
- определения состава реализуемых функций
- определения состава данных и интеграционных точек
- определения требований к пользовательским и административным интерфейсам
- определения критериев приемки результата работ
- определения состава артефактов, передаваемых заказчику

View File

@@ -1,97 +0,0 @@
# 3. Назначение и границы программного продукта
## 3.1 Назначение системы
Программный продукт `Личный кабинет Фрегат` предназначен для организации единого цифрового канала взаимодействия между ООО `Фрегат Групп` и клиентами компании.
Система должна обеспечивать:
- подключение новых клиентов и ведение клиентских учетных записей
- предоставление клиенту каталога готовой продукции без публичного отображения цены
- прием заявок на заказ готовой продукции
- прием заявок на расчет продукции с индивидуальными параметрами
- обработку заявок менеджером с публикацией стоимости и условий поставки
- сопровождение заказов по статусам
- информирование клиентов о значимых изменениях
- ведение бонусной и реферальной программы
## 3.2 Границы программного продукта
В состав программного продукта входят следующие функциональные области:
- регистрация и подключение клиента
- профиль клиента и данные компании
- каталог готовой продукции
- карточка товара и выбор параметров
- корзина и заявка на заказ
- заявка на расчет индивидуальной продукции
- обработка заявок менеджером
- список заказов и карточка заказа
- уведомления
- бонусный кабинет
- административные настройки
## 3.3 Функции, не входящие в состав программного продукта
Программный продукт не предназначен для выполнения следующих функций:
- самостоятельного ценообразования клиентом
- ведения бухгалтерского учета
- выполнения функций публичного B2C-магазина
- прямого редактирования клиентом внутренних бизнес-правил компании
- замены учетной системы 1С как первичного источника учетных данных
## 3.4 Пользовательские контуры
В системе должны быть предусмотрены следующие пользовательские контуры:
- клиентский контур
- менеджерский контур
- административный контур суперменеджера
Клиентский контур предназначен для работы клиента с каталогом, заявками, заказами, уведомлениями и бонусным кабинетом.
Менеджерский контур предназначен для обработки клиентских заявок, публикации коммерческих условий, сопровождения заказов, работы с клиентскими карточками и бонусными операциями.
Административный контур предназначен для управления настройками каталога, уведомлений, интеграционных параметров и отдельных сервисных настроек системы.
## 3.5 Основные бизнес-сценарии
### 3.5.1 Подключение клиента
1. Потенциальный клиент получает приглашение на регистрацию либо подает заявку на подключение самостоятельно.
2. Менеджер проверяет сведения о клиенте и принимает решение о подтверждении либо отклонении заявки.
3. При положительном решении клиенту предоставляется доступ к завершению регистрации.
4. После завершения регистрации клиент получает доступ к личному кабинету.
### 3.5.2 Заказ готовой продукции
1. Клиент открывает каталог готовой продукции.
2. Клиент выбирает товарное направление.
3. Клиент выбирает параметры товара.
4. Клиент просматривает доступные варианты и остатки.
5. Клиент добавляет выбранные позиции в корзину.
6. Клиент отправляет заявку на заказ.
7. Менеджер указывает стоимость, условия поставки и комментарий.
8. Система публикует обновленные условия клиенту.
### 3.5.3 Заявка на расчет индивидуальной продукции
1. Клиент переходит в режим расчета индивидуальной продукции.
2. Клиент указывает параметры требуемого изделия.
3. Клиент направляет заявку менеджеру.
4. Менеджер подготавливает коммерческие условия и публикует их клиенту.
### 3.5.4 Сопровождение заказа
1. Заказ получает уникальный идентификатор и статус.
2. Данные по заказу обновляются в системе по мере обработки.
3. Клиент отслеживает состав, статус, сроки и иные существенные сведения.
4. При изменении статуса либо условий система направляет уведомления.
### 3.5.5 Бонусная и реферальная программа
1. Для клиента фиксируются правила участия в бонусной программе и, при наличии, реферальные связи.
2. Система ведет учет начислений, списаний и остатка бонусов.
3. Клиент получает доступ к истории бонусных операций.
4. Менеджер обрабатывает заявки, связанные с использованием либо выводом бонусов, в пределах установленных правил.

View File

@@ -1,79 +0,0 @@
# 1. Введение, основание, цель и состав проекта
Настоящее техническое задание описывает разработку программного продукта `Личный кабинет Фрегат` для поддержки клиентских, менеджерских, заказных, бонусных и интеграционных сценариев в едином веб-интерфейсе.
Документ используется для согласования состава работ, функциональных и технических требований, этапов разработки, требований к программной документации, порядка приемки и гарантийного сопровождения.
## 1.1 Основание для разработки
Разработка программного продукта выполняется на основании следующих документов и материалов:
- договор на разработку программного продукта №28/04-26ПО от 28 апреля 2026 года
- приложение №1 к договору: спецификация основных требований к программному обеспечению `Личный кабинет Фрегат`
- согласованные требования заказчика к клиентскому, менеджерскому, бонусному и интеграционному контурам
## 1.2 Цель разработки
Программный продукт `Личный кабинет Фрегат` предназначен для организации единого цифрового канала взаимодействия между ООО `Фрегат Групп` и B2B-клиентами компании.
Система должна обеспечивать:
- подключение и регистрацию клиентов
- выбор готовой продукции из каталога
- подачу заявок на заказ готовой продукции
- подачу заявок на расчет продукции с индивидуальными параметрами
- согласование стоимости и условий поставки
- сопровождение заказов по статусам
- отправку клиентских уведомлений
- ведение бонусной и реферальной программы
## 1.3 Объект автоматизации
Объектом автоматизации являются процессы клиентского обслуживания и внутренней обработки заявок, выполняемые менеджерами ООО `Фрегат Групп` при работе с готовой и индивидуальной продукцией.
## 1.4 Состав системы
В состав программного продукта входят:
- клиентский веб-интерфейс
- менеджерский веб-интерфейс
- серверная бизнес-логика
- база данных
- модуль синхронизации с внешними системами
- модуль уведомлений
- модуль бонусной программы
- модуль административных настроек
## 1.5 Границы реализации
В состав программного продукта входят следующие функциональные области:
- регистрация и подключение клиента
- профиль клиента и данные компании
- каталог готовой продукции
- карточка товара и выбор параметров
- корзина и заявка на заказ
- заявка на расчет индивидуальной продукции
- обработка заявок менеджером
- список заказов и карточка заказа
- уведомления
- бонусный кабинет
- административные настройки
Программный продукт не предназначен для выполнения следующих функций:
- самостоятельного ценообразования клиентом
- ведения бухгалтерского учета
- выполнения функций публичного B2C-магазина
- прямого редактирования клиентом внутренних бизнес-правил компании
- замены учетной системы 1С как первичного источника учетных данных
## 1.6 Основные принципы работы
Система должна обеспечивать следующие базовые принципы:
- доступ к функциям и данным определяется ролью пользователя
- клиент работает только в пределах собственных данных и данных своего контрагента
- стоимость товара и условия поставки публикуются только после обработки менеджером
- история изменений по заявкам, заказам и бонусным операциям фиксируется в системе
- сведения о товарах, остатках, заказах и статусах могут обновляться из внешней учетной системы

View File

@@ -1,90 +0,0 @@
# 5. Требования к ролям и правам доступа
## 5.1 Состав ролей
В системе должны быть предусмотрены следующие роли пользователей:
- клиент
- менеджер
- суперменеджер
## 5.2 Роль клиента
Пользователь с ролью `Клиент` представляет организацию-контрагента и работает только с данными своей компании.
Клиенту должны быть доступны следующие действия:
- завершение регистрации по персональному приглашению
- подача заявки на подключение
- просмотр и изменение разрешенных профильных данных
- подключение доступных каналов уведомлений
- просмотр каталога готовой продукции
- выбор параметров товара
- добавление позиций в корзину
- отправка заявки на заказ
- отправка заявки на расчет
- просмотр списка заявок и заказов
- просмотр карточки заявки и карточки заказа
- просмотр истории уведомлений
- просмотр бонусного баланса и истории бонусных операций
- подача заявки на использование либо вывод бонусов при наличии соответствующих правил
## 5.3 Роль менеджера
Пользователь с ролью `Менеджер` представляет сотрудника компании, закрепленного за клиентами.
Менеджеру должны быть доступны следующие действия:
- рассмотрение заявок на подключение клиентов
- подтверждение либо отклонение заявок на подключение
- привязка клиента к контрагенту и назначение ответственного сопровождения
- просмотр и обработка заявок на заказ
- просмотр и обработка заявок на расчет
- указание стоимости, условий поставки и сопроводительного комментария
- публикация условий клиенту
- перевод заявки в работу
- отмена заявки при наличии оснований
- просмотр карточек клиентов, заявок и заказов
- выполнение операций в бонусном контуре в пределах полномочий
## 5.4 Роль суперменеджера
Пользователь с ролью `Суперменеджер` обладает всеми правами менеджера и дополнительными административными полномочиями.
Суперменеджеру должны быть доступны следующие действия:
- доступ ко всем клиентам, заявкам и заказам
- управление параметрами каталога
- управление описаниями и наборами параметров товаров
- управление настройками уведомлений
- управление параметрами интеграции и синхронизации
- расширенное управление бонусным и реферальным контуром
## 5.5 Матрица доступа
| Действие | Клиент | Менеджер | Суперменеджер |
| --- | --- | --- | --- |
| Завершение регистрации | Да | Нет | Нет |
| Подача заявки на подключение | Да | Нет | Нет |
| Просмотр каталога | Да | Да | Да |
| Выбор параметров и добавление в корзину | Да | Нет | Нет |
| Отправка заявки на заказ | Да | Нет | Нет |
| Отправка заявки на расчет | Да | Нет | Нет |
| Назначение стоимости и условий поставки | Нет | Да | Да |
| Публикация условий клиенту | Нет | Да | Да |
| Перевод заявки в работу | Нет | Да | Да |
| Отмена заявки | Нет | Да | Да |
| Управление параметрами каталога | Нет | Нет | Да |
| Управление уведомлениями | Нет | Нет | Да |
| Управление параметрами синхронизации | Нет | Нет | Да |
| Управление бонусными правилами | Нет | Да | Да |
## 5.6 Ограничения доступа и требования к безопасности
Система должна обеспечивать:
- раздельные интерфейсы для клиентского и менеджерского контуров
- доступ клиента только к данным собственного контрагента
- ограничение административных функций в соответствии с ролью
- журналирование значимых пользовательских действий
- хранение истории изменения статусов, условий заявок и бонусных операций

View File

@@ -1,423 +0,0 @@
# 7. Требования к интерфейсу и прототипам
## 7.1 Карта экранов
Ниже приведен базовый состав экранов, подлежащих реализации и сопровождению в рамках программного продукта.
| Раздел | Маршрут | Роль | Назначение |
| --- | --- | --- | --- |
| Главная страница | `/` | клиент | стартовая точка, быстрые действия, актуальные события |
| Каталог | `/products` | клиент | выбор товарного направления |
| Карточка товара | `/products/[slug]` | клиент | выбор параметров, просмотр вариантов, добавление в корзину |
| Корзина | `/cart` | клиент | формирование и отправка заявки |
| Список заказов клиента | `/client-orders` | клиент | просмотр истории заявок и заказов |
| Карточка заказа клиента | `/client-orders/[id]` | клиент | просмотр статуса, состава, условий и истории |
| Профиль | `/profile` | клиент | базовые данные учетной записи |
| Контрагент | `/profile/counterparty` | клиент | реквизиты и юридические данные |
| Адреса доставки | `/profile/addresses` | клиент | адресный справочник |
| Уведомления | `/notifications` | клиент | история уведомлений |
| Бонусный кабинет | `/bonus-program` | клиент | баланс, история и бонусные действия |
| Список клиентов | `/clients` | менеджер | клиентская база |
| Карточка клиента | `/clients/[id]` | менеджер | данные компании, заказы, бонусы |
| Приглашение клиента | `/clients/invite` | менеджер | выдача приглашения на регистрацию |
| Список заказов | `/orders` | менеджер | обработка заказного контура |
| Карточка заказа | `/orders/[id]` | менеджер | обработка условий, статуса и доставки |
| Настройки каталога | `/catalog-settings` | суперменеджер | параметры товарных направлений |
| Настройки синхронизации | `/settings-sync` | суперменеджер | мониторинг и управление обменом |
| Бонусная система | `/bonus-system/*` | менеджер/суперменеджер | рефералы, транзакции, выводы |
## 7.2 Маршруты и экранные формы
Ниже приведен перечень экранных форм, предусмотренных в составе frontend-контура программного продукта.
### 7.2.1 Публичные и клиентские страницы
| Маршрут | Экран | Назначение |
| --- | --- | --- |
| `/` | Главная страница | Стартовая страница личного кабинета |
| `/login` | Вход | Вход и первичный сценарий доступа |
| `/products` | Каталог продукции | Список товарных направлений |
| `/products/[slug]` | Карточка товара | Выбор параметров, просмотр вариантов, добавление в корзину |
| `/cart` | Корзина | Состав выбранных позиций и отправка заявки |
| `/client-orders` | Список заказов клиента | История заявок и заказов клиента |
| `/client-orders/[id]` | Карточка заказа клиента | Детали конкретного заказа клиента |
| `/notifications` | Уведомления | Список системных уведомлений |
| `/bonus-program` | Бонусный кабинет | Бонусный баланс, подарочные карты и бонусные действия |
### 7.2.2 Профиль клиента
| Маршрут | Экран | Назначение |
| --- | --- | --- |
| `/profile` | Профиль | Основные данные пользователя |
| `/profile/counterparty` | Реквизиты контрагента | Юридические и банковские данные |
| `/profile/addresses` | Адреса доставки | Список адресов доставки |
| `/profile/addresses/new` | Новый адрес доставки | Создание адреса доставки |
| `/profile/notifications` | Настройки уведомлений | Подключение и настройка каналов уведомлений |
| `/profile/notifications/success` | Успешное подключение уведомлений | Финальный экран сценария подключения канала |
### 7.2.3 Менеджерские и административные страницы
| Маршрут | Экран | Назначение |
| --- | --- | --- |
| `/clients` | Клиенты | Список клиентских компаний и пользователей |
| `/clients/[id]` | Карточка клиента | Детали клиента, его заказы и бонусные данные |
| `/clients/invite` | Пригласить клиента | Создание приглашения на регистрацию |
| `/orders` | Список заказов | Очередь заказов и заявок для менеджера |
| `/orders/[id]` | Карточка заказа | Обработка стоимости, условий поставки и статуса |
| `/catalog-settings` | Настройки каталога | Параметры товарных направлений и кастомизации |
| `/settings-sync` | 1С / синхронизация | Управление и мониторинг синхронизации |
| `/messages` | Сообщения | Шаблоны и тексты менеджерских сообщений |
### 7.2.4 Бонусный менеджерский контур
| Маршрут | Экран | Назначение |
| --- | --- | --- |
| `/bonus-system` | Бонусная система | Список клиентов и бонусных сущностей |
| `/bonus-system/[userId]` | Карточка бонусного счета клиента | История и состояние бонусного счета |
| `/bonus-system/referrals/new` | Создать бонусный счет | Создание реферальной связи |
| `/bonus-system/transactions/new` | Добавить бонусную транзакцию | Ручное начисление или списание |
| `/bonus-system/withdrawals/[id]` | Проверка заявки на вывод | Рассмотрение заявки клиента на вывод бонусов |
## 7.3 Общие требования к экранным формам
Экранные формы должны обеспечивать:
- однозначное понимание текущего объекта работы
- явное отображение статуса объекта
- соответствие доступных действий роли пользователя
- единый визуальный подход для клиентского и менеджерского контуров
- понятное отображение параметров товара, условий заказа и бонусных операций
Для экранов, связанных с товарами, заявками и заказами, должны выполняться дополнительные требования:
- цена не отображается клиенту до публикации условий менеджером
- остатки и доступные варианты отображаются в наглядном виде
- пользователь понимает ограничения выбора и возможность кастомизации
Ниже приведены низкодетализированные wireframe-прототипы. Они используются как визуальная фиксация состава страниц, ключевых блоков и пользовательских действий.
## 7.4 Клиентские экранные формы
### 7.4.1 Главная страница клиента
Назначение страницы:
- вход в основные разделы личного кабинета
- отображение актуальных действий и событий
Состав страницы:
- верхняя навигация
- быстрые переходы по основным разделам
- блок актуальных заказов и заявок
- блок последних уведомлений
- блок бонусной информации при наличии подключенного бонусного контура
- индикатор статуса заполненности профиля клиента
Wireframe-прототип:
![Прототип главной страницы клиента](/prototypes/dashboard.svg)
### 7.4.2 Каталог продукции
Назначение страницы:
- отображение товарных направлений
- переход к карточке выбранного типа товара
Состав страницы:
- заголовок раздела
- поиск при необходимости
- сетка карточек товарных направлений
- карточка каждого товарного направления с изображением и наименованием
- переход в карточку выбранного товарного направления
Wireframe-прототип:
![Прототип каталога продукции](/prototypes/catalog-grid.svg)
### 7.4.3 Карточка товара
Назначение страницы:
- выбор параметров товара
- просмотр стандартных вариантов
- просмотр складских остатков
- добавление выбранной позиции в корзину
Состав страницы:
- заголовок товара
- изображение товара
- блок выбора параметров
- блок пояснений по параметрам
- блок индивидуальных возможностей, если они разрешены
- блок добавления в корзину
- таблица доступных вариантов
- блок навигации к соседним товарным направлениям
Маршрут страницы:
- `/products/[slug]`
Wireframe-прототип:
![Прототип карточки товара](/prototypes/product-card.svg)
Состав блока выбора параметров:
- ширина
- длина
- толщина
- тип втулки
- цвет
- надпись
- индивидуальные опции при наличии разрешения
Состав блока пояснений:
- описание каждого параметра простым деловым языком
- ограничения по индивидуальной длине
- правила по втулке с логотипом
- правила по нанесению индивидуальной надписи
### 7.4.4 Корзина
Назначение страницы:
- просмотр выбранных позиций
- изменение количества
- удаление позиции
- отправка заявки на заказ
Состав страницы:
- список позиций
- параметры и количество
- комментарий клиента
- действие отправки заявки
- выбранный адрес доставки
- итоговая сводка по количеству позиций
Wireframe-прототип:
![Прототип корзины](/prototypes/cart.svg)
### 7.4.5 Карточка заявки или заказа
Назначение страницы:
- просмотр состава
- просмотр статуса
- просмотр стоимости и условий поставки после публикации
- просмотр истории изменений
Wireframe-прототип:
![Прототип карточки заявки или заказа](/prototypes/client-order.svg)
Состав страницы:
- номер документа
- статус
- состав позиций
- стоимость после публикации менеджером
- условия поставки и доставки
- история статусов
- системные комментарии
### 7.4.6 Страница логина и регистрации
Назначение страницы:
- запуск входа в систему
- запуск самостоятельной заявки на подключение
- запуск сценариев входа через мессенджеры
Состав страницы:
- форма запроса кода входа
- выбор канала входа
- ссылка на самостоятельную заявку на подключение
- блок пояснения по дальнейшему сценарию доступа
Wireframe-прототип:
![Прототип страницы логина и подключения](/prototypes/login.svg)
### 7.4.7 Список заказов
Назначение страницы:
- просмотр текущих и архивных заказов
- фильтрация по периоду и статусу
Состав страницы:
- фильтры
- таблица заказов
- переход в карточку конкретного заказа
### 7.4.8 Уведомления
Назначение страницы:
- просмотр истории уведомлений по заказам, заявкам и бонусным операциям
### 7.4.9 Бонусный кабинет
Назначение страницы:
- просмотр текущего бонусного баланса
- просмотр истории операций
- инициирование действий, допускаемых правилами бонусной программы
Состав страницы:
- текущий баланс
- история начислений и списаний
- связанные реферальные сведения
- форма подачи заявки на использование либо вывод бонусов
- правила бонусной программы
Wireframe-прототип:
![Прототип бонусного кабинета](/prototypes/bonus-cabinet.svg)
## 7.5 Менеджерские экранные формы
### 7.5.1 Список клиентов
Назначение страницы:
- просмотр клиентской базы
- переход в карточку конкретного клиента
Состав страницы:
- поиск и фильтры
- таблица клиентов
- индикаторы активности и количества заказов
- действие приглашения нового клиента
Wireframe-прототип:
![Прототип списка клиентов](/prototypes/client-list.svg)
### 7.5.2 Карточка клиента
Назначение страницы:
- просмотр сведений о компании
- просмотр истории заявок и заказов
- просмотр бонусных и реферальных данных
Состав страницы:
- карточка компании и контактных данных
- реквизиты контрагента
- список заказов клиента
- список бонусных операций
- связанные рефералы
Wireframe-прототип:
![Прототип карточки клиента](/prototypes/client-card.svg)
### 7.5.3 Карточка обработки заявки
Назначение страницы:
- просмотр состава заявки
- ввод коммерческих условий
- публикация условий клиенту
- перевод заявки в работу либо отмена
Wireframe-прототип:
![Прототип карточки обработки заявки](/prototypes/manager-order.svg)
Состав страницы:
- клиент и контрагент
- состав позиции или расчетный payload
- стоимость
- доставка
- комментарий менеджера
- история изменений
- блок действий со статусом
### 7.5.4 Список заказов менеджера
Назначение страницы:
- просмотр заказов по клиентам
- фильтрация по статусам
- переход к обработке конкретного заказа
Wireframe-прототип:
![Прототип списка заказов менеджера](/prototypes/manager-orders.svg)
### 7.5.5 Настройки каталога
Назначение страницы:
- управление параметрами товарных направлений
- управление стандартными значениями параметров
- управление возможностями кастомизации
- управление описаниями параметров
Состав страницы:
- список товарных направлений
- карточка настроек конкретного направления
- чекбоксы разрешений кастомизации
- списки стандартных параметров
- единое действие сохранения настроек
Wireframe-прототип:
![Прототип настроек каталога](/prototypes/catalog-settings.svg)
### 7.5.6 Настройки синхронизации и уведомлений
Назначение страницы:
- управление шаблонами уведомлений
- управление параметрами интеграционного обмена
Состав страницы:
- список шаблонов уведомлений
- каналы отправки
- статусы последних синхронизаций
- диагностическая информация по обмену
Wireframe-прототип:
![Прототип настроек синхронизации](/prototypes/sync-settings.svg)
## 7.6 Дополнительные профильные и сервисные страницы
Помимо основных клиентских и менеджерских экранов, программный продукт включает дополнительные экранные формы:
- профиль пользователя
- реквизиты контрагента
- список адресов доставки
- создание нового адреса доставки
- настройки уведомлений пользователя
- экран успешного подключения канала уведомлений
- менеджерский экран сообщений
- бонусная система для менеджера
- карточка бонусного счета клиента
- создание реферальной связи
- создание бонусной транзакции
- проверка заявки на вывод бонусов
Прототипы служебных и дополнительных экранов:
![Прототип профиля клиента](/prototypes/profile.svg)
![Прототип бонусной системы менеджера](/prototypes/bonus-manager.svg)

View File

@@ -1,286 +0,0 @@
# 9. Техническая архитектура, стек, компоненты и эксплуатационный контур
## 9.1 Общая архитектурная схема
Программный продукт реализуется по клиент-серверной модели и включает веб-клиент, сервер бизнес-логики, базу данных, модуль интеграции и вспомогательные сервисы уведомлений.
![Общая архитектурная схема](/diagrams/architecture-overview.svg)
## 9.2 Состав прикладных сервисов
Согласно действующей карте деплоя в составе проекта используются следующие сервисы:
- `web-frontend` — клиентский и менеджерский веб-интерфейс
- `apollo-backend` — сервер GraphQL и бизнес-логика
- `vault` — централизованное хранилище секретов
- `tg-bot` — Telegram-контур
- `max-bot` — MAX-контур
- `bonus-bot` — бонусный мессенджерный контур
Основные прикладные сервисы `web-frontend` и `apollo-backend` разворачиваются через `dokploy_webhook` на сервере `main`.
## 9.3 Технологический стек фронтенда
Клиентская часть программного продукта реализуется на следующих технологиях:
- `Nuxt 4` — прикладной веб-фреймворк
- `Vue 3` — библиотека пользовательского интерфейса
- `@nuxtjs/apollo` и `@vue/apollo-composable` — работа с GraphQL
- `GraphQL Code Generator` — генерация типизированных GraphQL-документов
- `Tailwind CSS` и `daisyUI` — базовая стилизация интерфейсов
- `Storybook` — контур изоляционного просмотра компонентов
- `VitePress` — подготовка проектной документации
## 9.4 Технологический стек серверной части
Серверная часть программного продукта реализуется на следующих технологиях:
- `Node.js` — среда выполнения
- `Express 5` — HTTP-сервер
- `Apollo Server 5` — GraphQL-сервер
- `Prisma 7` — доступ к данным и ORM-слой
- `PostgreSQL` — основное хранилище данных
- `Zod` — валидация структур данных в прикладных сценариях
- `Nodemailer` — отправка уведомлений по электронной почте
## 9.5 Архитектура клиентской части
Клиентская часть организована по следующим слоям:
- страницы `app/pages` — входные экранные формы
- компоненты `app/components` — переиспользуемые элементы интерфейса
- composables `app/composables` — клиентская логика и представление данных
- GraphQL-документы `graphql/operations` — отдельные запросы и мутации
- сгенерированная типизированная схема `app/composables/graphql/generated.ts`
- серверные proxy и интеграционные обработчики `server/api`
Ключевые экранные маршруты текущей реализации:
- `/products` и `/products/[slug]` — каталог и карточка товара
- `/cart` — корзина
- `/client-orders` и `/client-orders/[id]` — клиентские заявки и заказы
- `/clients` и `/clients/[id]` — менеджерский контур клиентов
- `/orders` и `/orders/[id]` — менеджерский контур заказов
- `/catalog-settings` — административные настройки каталога
- `/bonus-program`, `/bonus-system/*` — бонусный контур
## 9.6 Карта слоев и компонентов
![Карта слоев и компонентов](/diagrams/component-map.svg)
## 9.7 Архитектура серверной части
Серверная часть организована по следующим основным модулям:
- `src/server.js` — инициализация HTTP-сервера и GraphQL-сервера
- `src/schema.graphql` — контракт GraphQL API
- `src/resolvers.js` — реализация GraphQL-операций
- `src/context.js` — построение контекста запроса
- `src/auth.js` — аутентификация и токены доступа
- `src/access.js` — правила авторизации и проверки ролей
- `src/prisma-client.js` — точка подключения Prisma
- `src/messenger*.js`, `src/telegram*.js`, `src/max-mini-app.js` — мессенджерный контур и мини-приложения
## 9.8 Взаимодействие фронтенда и backend
Взаимодействие клиентской и серверной части строится через GraphQL API.
На стороне Nuxt используется серверный proxy-маршрут `/api/graphql`, который:
- принимает запросы браузера
- прокидывает cookie и авторизационные заголовки
- перенаправляет запрос в `apollo-backend`
- возвращает результат в клиентское приложение
Такой подход позволяет:
- изолировать прямой backend endpoint от браузерного клиента
- централизованно передавать авторизационные данные
- поддерживать единый клиентский GraphQL endpoint
## 9.9 Интеграционные и вспомогательные HTTP-точки
Помимо GraphQL API, во фронтенд-контуре реализованы вспомогательные серверные маршруты:
- `/api/graphql` — proxy в backend GraphQL API
- `/api/auth/messenger-start` — запуск сценариев messenger login / connect
- `/api/dadata/address` — подсказки адресов
- `/api/dadata/bank` — подсказки банковских реквизитов
- `/api/dadata/party` — подсказки контрагентов
- `/api/messenger-avatar/[connectionId]` — выдача изображений, связанных с мессенджерными подключениями
## 9.10 Компонентные требования к реализации
Архитектура программного продукта должна сохранять следующие правила:
- экранная логика должна находиться на уровне страниц и composables
- переиспользуемые элементы интерфейса должны быть вынесены в компоненты
- каждый GraphQL-документ должен храниться в отдельном `.graphql` файле
- клиентский код должен использовать сгенерированные типизированные документы
- серверная логика доступа к данным должна проходить через Prisma
- бизнес-правила доступа должны контролироваться серверной частью, а не только интерфейсом
## 9.11 Требования к конфигурации и секретам
Сервисы программного продукта должны получать прикладные секреты из `Vault`.
В конфигурации сервисов допускается хранение только bootstrap-параметров для подключения к Vault. Бизнес-секреты, ключи интеграций и иные чувствительные данные должны загружаться из Vault при старте приложения.
## 9.12 Инфраструктура, деплой и эксплуатационный контур
Текущая инфраструктурная схема проекта включает прикладные сервисы, сервис секретов, мессенджерные сервисы и вспомогательный worker-контур.
![Инфраструктура, деплой и эксплуатационный контур](/diagrams/infrastructure-topology.svg)
Сервисы проекта и способ их развёртывания:
| Сервис | Путь в репозитории | Роль | Deploy mode | Сервер |
| --- | --- | --- | --- | --- |
| web-frontend | `web-frontend` | клиентский и менеджерский веб-интерфейс | `dokploy_webhook` | `main` |
| apollo-backend | `apollo-backend` | GraphQL API и бизнес-логика | `dokploy_webhook` | `main` |
| hatchet-worker | `hatchet-worker` | фоновый worker-контур | `dokploy_webhook` | `main` |
| tg-bot | `tg-bot` | Telegram-контур | `dokploy_webhook` | `main` |
| max-bot | `max-bot` | MAX-контур | `dokploy_webhook` | `main` |
| bonus-bot | `bonus-bot` | бонусный сервис | `dokploy_webhook` | `main` |
| vault | `vault` | сервис секретов | `dokploy_webhook` | `main` |
Серверная карта текущего проекта:
- сервер `main`
- Tailscale user: `root`
- Tailscale host: `dsrptlab`
Эксплуатационные операции доступа и диагностики выполняются через Tailscale SSH.
## 9.13 Dokploy и цепочка развёртывания
Для основных сервисов проекта используется режим `dokploy_webhook`.
Это означает следующую последовательность:
1. изменения фиксируются в Git-репозитории;
2. изменения публикуются в `main`;
3. Dokploy получает webhook-событие;
4. Dokploy выполняет сборку соответствующего сервиса;
5. сервис перезапускается с загрузкой секретов из Vault;
6. после старта выполняются bootstrap-действия, предусмотренные образом и entrypoint-командой.
Текущие особенности прикладных сервисов:
- `web-frontend` стартует через загрузку Vault env и запуск `.output/server/index.mjs`
- `apollo-backend` стартует через загрузку Vault env, `prisma migrate deploy` и запуск `src/server.js`
- bot-сервисы стартуют через общий паттерн загрузки Vault env и запуск `node src/server.js`
## 9.14 Vault и хранение секретов
Сервис `vault` развернут как отдельное приложение на базе образа `hashicorp/vault:1.21.3`.
Текущие инфраструктурные правила работы Vault:
- используется `raft` storage
- данные Vault сохраняются в `/vault/data`
- конфигурация сервиса хранится в `vault/config/vault.hcl`
- при старте выполняется проверка и, при необходимости, автоматический unseal из переменных окружения
- прикладные сервисы подключаются к Vault через bootstrap-переменные `VAULT_ADDR`, `VAULT_TOKEN`, `VAULT_KV_MOUNT`, `VAULT_SHARED_PATH`, `VAULT_PROJECT_PATH`, `VAULT_ENABLED`
Прикладные сервисы используют общий shell-bootstrap `load-vault-env.sh`, который:
- ожидает доступность Vault по health endpoint
- загружает shared secrets
- загружает project secrets
- экспортирует секреты в runtime environment процесса
## 9.15 Структура сервисных каталогов
Текущая сервисная структура репозитория:
- `web-frontend` — Nuxt-приложение, GraphQL operations, VitePress-документация
- `apollo-backend` — Apollo Server, Prisma schema, миграции, импорт каталога
- `tg-bot` — Telegram-сервис
- `max-bot` — MAX-сервис
- `bonus-bot` — бонусный сервис
- `hatchet-worker` — worker-контур
- `vault` — Dockerfile, конфигурация и entrypoint Vault
- `hatchet` — docker-compose инфраструктурного контура Hatchet
## 9.16 Контур фоновых процессов
В проекте присутствует отдельный контур фонового исполнения задач:
- `hatchet-worker` как прикладной worker
- `hatchet-engine` как движок исполнения
- `hatchet-dashboard` как интерфейс мониторинга
- отдельный `postgres` для Hatchet-контура
Конфигурация этого контура находится в `hatchet/docker-compose.yml`.
## 9.17 Зафиксированные runtime-версии и технологические зависимости
### Базовые runtime и контейнерные образы
| Компонент | Текущая версия |
| --- | --- |
| Node base image для web/backend/bots | `node:22-bookworm-slim` |
| Vault image | `hashicorp/vault:1.21.3` |
| Hatchet Postgres | `postgres:15.6` |
| Nuxt | `4.4.2` |
| Vue | `3.5.30` |
| Apollo Server | `5.5.0` |
| Prisma / Prisma Client | `7.6.0` |
| Mermaid | `11.14.0` |
| VitePress | `1.6.4` |
### Основные зависимости фронтенда
| Библиотека | Версия | Назначение |
| --- | --- | --- |
| `@nuxtjs/apollo` | `5.0.0-alpha.16` | GraphQL-клиентский слой |
| `@vue/apollo-composable` | `4.2.2` | composable API для GraphQL |
| `@apollo/client` | `3.14.1` | Apollo client runtime |
| `@fullcalendar/core` | `6.1.20` | calendar runtime |
| `@fullcalendar/daygrid` | `6.1.20` | calendar day grid |
| `@fullcalendar/vue3` | `6.1.20` | Vue integration for calendar |
| `@nuxt/eslint` | `1.15.2` | linting Nuxt-проекта |
| `@nuxtjs/tailwindcss` | `6.14.0` | Tailwind integration |
| `daisyui` | `5.5.19` | UI primitives |
| `graphql` | `16.13.2` | GraphQL runtime |
| `mermaid` | `11.14.0` | diagrams in documentation |
| `@sentry/vue` | `10.46.0` | error tracking |
| `storybook` | `8.6.14` | UI component review |
| `vue-router` | `5.0.4` | Vue routing runtime |
### Основные зависимости backend
| Библиотека | Версия | Назначение |
| --- | --- | --- |
| `@apollo/server` | `5.5.0` | GraphQL server |
| `@as-integrations/express5` | `1.1.2` | Apollo + Express integration |
| `express` | `5.2.1` | HTTP server |
| `@prisma/client` | `7.6.0` | data access |
| `@prisma/adapter-pg` | `7.6.0` | Prisma adapter |
| `pg` | `8.20.0` | PostgreSQL driver |
| `graphql` | `16.13.2` | GraphQL runtime |
| `zod` | `4.3.6` | validation |
| `nodemailer` | `8.0.4` | email notifications |
| `dotenv` | `17.3.1` | env bootstrap in local/runtime layers |
### Основные зависимости worker-контура
| Библиотека | Версия | Назначение |
| --- | --- | --- |
| `@hatchet-dev/typescript-sdk` | `1.19.0` | worker runtime |
| `dotenv` | `17.3.1` | env bootstrap in local/runtime layers |
## 9.18 Технические конфигурационные файлы
Ключевые технические файлы текущей реализации:
- `deploy-map.toml` — карта сервисов, deploy mode и серверов
- `web-frontend/nuxt.config.ts` — конфигурация Nuxt и Apollo
- `web-frontend/codegen.ts` — конфигурация GraphQL codegen
- `apollo-backend/prisma.config.ts` — конфигурация Prisma
- `apollo-backend/prisma/schema.prisma` — модель данных
- `web-frontend/scripts/load-vault-env.sh` — Vault bootstrap frontend
- `apollo-backend/scripts/load-vault-env.sh` — Vault bootstrap backend
- `vault/config/vault.hcl` — конфигурация Vault
- `vault/entrypoint.sh` — startup/unseal логика Vault