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

|
||||
|
||||
### 7.4.2 Каталог продукции
|
||||
|
||||
Назначение страницы:
|
||||
|
||||
- отображение товарных направлений
|
||||
- переход к карточке выбранного типа товара
|
||||
|
||||
Состав страницы:
|
||||
|
||||
- заголовок раздела
|
||||
- поиск при необходимости
|
||||
- сетка карточек товарных направлений
|
||||
- карточка каждого товарного направления с изображением и наименованием
|
||||
- переход в карточку выбранного товарного направления
|
||||
|
||||
Wireframe-прототип:
|
||||
|
||||

|
||||
|
||||
### 7.4.3 Карточка товара
|
||||
|
||||
Назначение страницы:
|
||||
|
||||
- выбор параметров товара
|
||||
- просмотр стандартных вариантов
|
||||
- просмотр складских остатков
|
||||
- добавление выбранной позиции в корзину
|
||||
|
||||
Состав страницы:
|
||||
|
||||
- заголовок товара
|
||||
- изображение товара
|
||||
- блок выбора параметров
|
||||
- блок пояснений по параметрам
|
||||
- блок индивидуальных возможностей, если они разрешены
|
||||
- блок добавления в корзину
|
||||
- таблица доступных вариантов
|
||||
- блок навигации к соседним товарным направлениям
|
||||
|
||||
Маршрут страницы:
|
||||
|
||||
- `/products/[slug]`
|
||||
|
||||
Wireframe-прототип:
|
||||
|
||||

|
||||
|
||||
Состав блока выбора параметров:
|
||||
|
||||
- ширина
|
||||
- длина
|
||||
- толщина
|
||||
- тип втулки
|
||||
- цвет
|
||||
- надпись
|
||||
- индивидуальные опции при наличии разрешения
|
||||
|
||||
Состав блока пояснений:
|
||||
|
||||
- описание каждого параметра простым деловым языком
|
||||
- ограничения по индивидуальной длине
|
||||
- правила по втулке с логотипом
|
||||
- правила по нанесению индивидуальной надписи
|
||||
|
||||
### 7.4.4 Корзина
|
||||
|
||||
Назначение страницы:
|
||||
|
||||
- просмотр выбранных позиций
|
||||
- изменение количества
|
||||
- удаление позиции
|
||||
- отправка заявки на заказ
|
||||
|
||||
Состав страницы:
|
||||
|
||||
- список позиций
|
||||
- параметры и количество
|
||||
- комментарий клиента
|
||||
- действие отправки заявки
|
||||
- выбранный адрес доставки
|
||||
- итоговая сводка по количеству позиций
|
||||
|
||||
Wireframe-прототип:
|
||||
|
||||

|
||||
|
||||
### 7.4.5 Карточка заявки или заказа
|
||||
|
||||
Назначение страницы:
|
||||
|
||||
- просмотр состава
|
||||
- просмотр статуса
|
||||
- просмотр стоимости и условий поставки после публикации
|
||||
- просмотр истории изменений
|
||||
|
||||
Wireframe-прототип:
|
||||
|
||||

|
||||
|
||||
Состав страницы:
|
||||
|
||||
- номер документа
|
||||
- статус
|
||||
- состав позиций
|
||||
- стоимость после публикации менеджером
|
||||
- условия поставки и доставки
|
||||
- история статусов
|
||||
- системные комментарии
|
||||
|
||||
### 7.4.6 Страница логина и регистрации
|
||||
|
||||
Назначение страницы:
|
||||
|
||||
- запуск входа в систему
|
||||
- запуск самостоятельной заявки на подключение
|
||||
- запуск сценариев входа через мессенджеры
|
||||
|
||||
Состав страницы:
|
||||
|
||||
- форма запроса кода входа
|
||||
- выбор канала входа
|
||||
- ссылка на самостоятельную заявку на подключение
|
||||
- блок пояснения по дальнейшему сценарию доступа
|
||||
|
||||
Wireframe-прототип:
|
||||
|
||||

|
||||
|
||||
### 7.4.7 Список заказов
|
||||
|
||||
Назначение страницы:
|
||||
|
||||
- просмотр текущих и архивных заказов
|
||||
- фильтрация по периоду и статусу
|
||||
|
||||
Состав страницы:
|
||||
|
||||
- фильтры
|
||||
- таблица заказов
|
||||
- переход в карточку конкретного заказа
|
||||
|
||||
### 7.4.8 Уведомления
|
||||
|
||||
Назначение страницы:
|
||||
|
||||
- просмотр истории уведомлений по заказам, заявкам и бонусным операциям
|
||||
|
||||
### 7.4.9 Бонусный кабинет
|
||||
|
||||
Назначение страницы:
|
||||
|
||||
- просмотр текущего бонусного баланса
|
||||
- просмотр истории операций
|
||||
- инициирование действий, допускаемых правилами бонусной программы
|
||||
|
||||
Состав страницы:
|
||||
|
||||
- текущий баланс
|
||||
- история начислений и списаний
|
||||
- связанные реферальные сведения
|
||||
- форма подачи заявки на использование либо вывод бонусов
|
||||
- правила бонусной программы
|
||||
|
||||
Wireframe-прототип:
|
||||
|
||||

|
||||
|
||||
## 7.5 Менеджерские экранные формы
|
||||
|
||||
### 7.5.1 Список клиентов
|
||||
|
||||
Назначение страницы:
|
||||
|
||||
- просмотр клиентской базы
|
||||
- переход в карточку конкретного клиента
|
||||
|
||||
Состав страницы:
|
||||
|
||||
- поиск и фильтры
|
||||
- таблица клиентов
|
||||
- индикаторы активности и количества заказов
|
||||
- действие приглашения нового клиента
|
||||
|
||||
Wireframe-прототип:
|
||||
|
||||

|
||||
|
||||
### 7.5.2 Карточка клиента
|
||||
|
||||
Назначение страницы:
|
||||
|
||||
- просмотр сведений о компании
|
||||
- просмотр истории заявок и заказов
|
||||
- просмотр бонусных и реферальных данных
|
||||
|
||||
Состав страницы:
|
||||
|
||||
- карточка компании и контактных данных
|
||||
- реквизиты контрагента
|
||||
- список заказов клиента
|
||||
- список бонусных операций
|
||||
- связанные рефералы
|
||||
|
||||
Wireframe-прототип:
|
||||
|
||||

|
||||
|
||||
### 7.5.3 Карточка обработки заявки
|
||||
|
||||
Назначение страницы:
|
||||
|
||||
- просмотр состава заявки
|
||||
- ввод коммерческих условий
|
||||
- публикация условий клиенту
|
||||
- перевод заявки в работу либо отмена
|
||||
|
||||
Wireframe-прототип:
|
||||
|
||||

|
||||
|
||||
Состав страницы:
|
||||
|
||||
- клиент и контрагент
|
||||
- состав позиции или расчетный payload
|
||||
- стоимость
|
||||
- доставка
|
||||
- комментарий менеджера
|
||||
- история изменений
|
||||
- блок действий со статусом
|
||||
|
||||
### 7.5.4 Список заказов менеджера
|
||||
|
||||
Назначение страницы:
|
||||
|
||||
- просмотр заказов по клиентам
|
||||
- фильтрация по статусам
|
||||
- переход к обработке конкретного заказа
|
||||
|
||||
Wireframe-прототип:
|
||||
|
||||

|
||||
|
||||
### 7.5.5 Настройки каталога
|
||||
|
||||
Назначение страницы:
|
||||
|
||||
- управление параметрами товарных направлений
|
||||
- управление стандартными значениями параметров
|
||||
- управление возможностями кастомизации
|
||||
- управление описаниями параметров
|
||||
|
||||
Состав страницы:
|
||||
|
||||
- список товарных направлений
|
||||
- карточка настроек конкретного направления
|
||||
- чекбоксы разрешений кастомизации
|
||||
- списки стандартных параметров
|
||||
- единое действие сохранения настроек
|
||||
|
||||
Wireframe-прототип:
|
||||
|
||||

|
||||
|
||||
### 7.5.6 Настройки синхронизации и уведомлений
|
||||
|
||||
Назначение страницы:
|
||||
|
||||
- управление шаблонами уведомлений
|
||||
- управление параметрами интеграционного обмена
|
||||
|
||||
Состав страницы:
|
||||
|
||||
- список шаблонов уведомлений
|
||||
- каналы отправки
|
||||
- статусы последних синхронизаций
|
||||
- диагностическая информация по обмену
|
||||
|
||||
Wireframe-прототип:
|
||||
|
||||

|
||||
|
||||
## 7.6 Дополнительные профильные и сервисные страницы
|
||||
|
||||
Помимо основных клиентских и менеджерских экранов, программный продукт включает дополнительные экранные формы:
|
||||
|
||||
- профиль пользователя
|
||||
- реквизиты контрагента
|
||||
- список адресов доставки
|
||||
- создание нового адреса доставки
|
||||
- настройки уведомлений пользователя
|
||||
- экран успешного подключения канала уведомлений
|
||||
- менеджерский экран сообщений
|
||||
- бонусная система для менеджера
|
||||
- карточка бонусного счета клиента
|
||||
- создание реферальной связи
|
||||
- создание бонусной транзакции
|
||||
- проверка заявки на вывод бонусов
|
||||
|
||||
Прототипы служебных и дополнительных экранов:
|
||||
|
||||

|
||||
|
||||

|
||||
@@ -1,286 +0,0 @@
|
||||
# 9. Техническая архитектура, стек, компоненты и эксплуатационный контур
|
||||
|
||||
## 9.1 Общая архитектурная схема
|
||||
|
||||
Программный продукт реализуется по клиент-серверной модели и включает веб-клиент, сервер бизнес-логики, базу данных, модуль интеграции и вспомогательные сервисы уведомлений.
|
||||
|
||||

|
||||
|
||||
## 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 Карта слоев и компонентов
|
||||
|
||||

|
||||
|
||||
## 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-контур.
|
||||
|
||||

|
||||
|
||||
Сервисы проекта и способ их развёртывания:
|
||||
|
||||
| Сервис | Путь в репозитории | Роль | 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
|
||||
Reference in New Issue
Block a user