Техническая документация

Архитектура

Приложение построено на Next.js 16 (App Router) с использованием серверных и клиентских компонентов. API реализован через Route Handlers. Тёмная тема реализована через CSS-переменные и @custom-variant dark (Tailwind 4).

app/
├── api/                # API Route Handlers
│   ├── segments/       # CRUD сегментов + overview отчёт
│   ├── programs/       # CRUD программ + overview отчёт
│   ├── initiatives/    # CRUD инициатив
│   ├── tasks/          # CRUD задач
│   ├── relations/      # EntityRelation CRUD
│   ├── history/        # EntityHistory (GET)
│   ├── decision-forks/ # DecisionFork CRUD
│   ├── milestones/     # Milestone CRUD (с toggle completed)
│   ├── qa/             # QAItem CRUD
│   ├── health/         # Расчёт здоровья (инициативы, программы, сегменты)
│   ├── health-trend/   # Снимки здоровья для графика тренда
│   ├── attention/      # Элементы, требующие внимания
│   ├── stats/          # Агрегированная статистика для дашборда
│   ├── settings/       # Настройки (GET/PUT)
│   └── excel/          # Импорт/экспорт Excel
├── segments/           # Страницы сегментов
├── programs/           # Страницы программ
├── initiatives/        # Страницы инициатив
├── tasks/              # Страницы задач
├── graph/              # Страница графа
├── roadmap/            # Страница роадмапа
├── settings/           # Настройки (тема, Jira, типы связей, блоки отчёта)
└── docs/               # Документация

components/
├── graph/              # GraphView, CustomNode, CustomEdge, FilterPanel, Legend
├── roadmap/            # RoadmapTimeline
├── layout/             # Sidebar, Header, SidebarContext, LayoutShell
├── providers/          # ThemeProvider
└── ui/                 # Card, Badge, Modal, RelationsSection, QABlock,
                        # HealthBadge, HealthTrendChart и др.

lib/
├── db.ts               # Prisma-клиент (PrismaPg adapter)
├── health.ts           # Расчёт здоровья (инициативы, программы, сегменты, каскад)
├── progress.ts         # Расчёт прогресса (инициативы, программы)
├── constants.ts        # Метки, цвета, статусы, матрица влияния
└── relations.ts        # Навигация по иерархии (getTaskInitiativeId и др.)

Модель данных (Prisma)

ORM: Prisma 7 с адаптером @prisma/adapter-pg. Подключение через PrismaPg в lib/db.ts.

InvestmentSegment
├── id, name, description, status (LifecycleStatus), owner, label (EntityLabel)
├── startDate, endDate
├── asIs, limitations, targetState, coreDescription
├── impact (Json), attributes (String[])
├── jiraKey (String?)
├── healthStatus, healthIssues (Json), healthUpdatedAt
├── manualStatus, manualStatusDescription
├── manualStatusProblems (Json), manualStatusUpdatedAt
└── programs: Program[]

Program
├── segmentId → InvestmentSegment
├── (те же поля: status, owner, label, dates, narrative, health, manual...)
├── jiraKey (String?)
└── initiatives: Initiative[]

Initiative
├── programId → Program
├── healthStatus, healthIssues (Json), healthUpdatedAt
├── manualStatus, manualStatusDescription
├── manualStatusProblems (Json), manualStatusUpdatedAt
├── risks (Json) — массив {id, description, severity, mitigationDate, status}
├── jiraKey (String?)
├── tasks: Task[]
└── milestones: Milestone[]

Task
├── initiativeId → Initiative
├── id, name, description, status, owner, startDate, endDate
└── jiraKey (String?)

Milestone
├── description, date, completed (Boolean)
└── initiativeId → Initiative (cascade delete)

EntityRelation (полиморфная)
├── sourceType, sourceId → targetType, targetId
├── type: PART_OF | PREREQUISITE | ENABLES
├── sourceAnchor, targetAnchor (default "FINISH"/"START")
└── decisionForkId, decisionForkOptionIdx (условные связи)

EntityHistory (полиморфная)
├── entityType, entityId, entityName
├── field, oldValue, newValue
└── changedAt, changedBy

DecisionFork (полиморфная)
├── entityType, entityId
├── question, status: OPEN | RESOLVED | DEFERRED
├── date (DateTime?)
└── options (Json) — массив {title, approved}

QAItem (полиморфная)
├── entityType, entityId
├── question, answer
└── createdAt

HealthSnapshot (тренды здоровья)
├── entityType, entityId
├── healthStatus, manualStatus
└── snapshotDate (с кулдауном 1 час)

Settings (key-value)
├── key (unique)
└── value (Json)

Enum LifecycleStatus: DRAFT, PLANNED, IN_PROGRESS, ON_HOLD, COMPLETED, CANCELLED

Enum EntityLabel: IT_STRATEGY, IT_INITIATIVE, BUSINESS_INITIATIVE, EXTERNAL_FACTOR

Enum RelationType: PART_OF, PREREQUISITE, ENABLES

Enum DecisionForkStatus: OPEN, RESOLVED, DEFERRED

API Endpoints

МетодПутьОписание
GET/api/segmentsСписок сегментов
POST/api/segmentsСоздать сегмент
GET/api/segments/[id]Получить сегмент
PUT/api/segments/[id]Обновить сегмент
DELETE/api/segments/[id]Удалить сегмент
GET/api/segments/[id]/overviewHTML-отчёт по сегменту
GET/POST/api/programsСписок / создание программ
GET/PUT/DEL/api/programs/[id]CRUD программы
GET/api/programs/[id]/overviewHTML-отчёт по программе
GET/POST/api/initiativesСписок / создание инициатив
GET/PUT/DEL/api/initiatives/[id]CRUD инициативы
GET/POST/api/tasksСписок / создание задач
GET/PUT/DEL/api/tasks/[id]CRUD задачи
GET/POST/api/relationsСвязи (фильтр entityType/entityId)
PUT/DEL/api/relations/[id]Обновить/удалить связь
GET/POST/api/milestonesВехи (фильтр initiativeId)
PUT/DEL/api/milestones/[id]Обновить (toggle completed) / удалить веху
GET/api/history/[type]/[id]История изменений сущности
GET/POST/api/decision-forksРазвилки (фильтр entityType/entityId)
GET/PUT/DEL/api/decision-forks/[id]CRUD развилки
GET/POST/api/qaQ&A (фильтр entityType/entityId)
POST/api/healthРасчёт здоровья (инициативы + программы + сегменты)
GET/api/health-trendСнимки здоровья (entityType, entityId, days)
GET/api/attentionЭлементы, требующие внимания (severity фильтр)
GET/api/statsСтатистика для дашборда
GET/PUT/api/settingsНастройки приложения
GET/api/excel/exportЭкспорт в Excel
POST/api/excel/importИмпорт из Excel (multipart)

Система здоровья (health.ts)

Расчёт здоровья реализован в lib/health.ts. Endpoint: POST /api/health.

// Расчёт одной инициативы

POST /api/health { initiativeId: "..." }

// Расчёт всех (инициативы → программы → сегменты)

POST /api/health {}

Инициативы

Функция evaluateHealth() выполняет 14 проверок RED + 12 проверок YELLOW. Проверяются: даты, вехи, задачи, зависимости (PREREQUISITE/ENABLES), развилки, риски, актуальность ручного статуса. Результат записывается в поля:

  • healthStatus — GREEN | YELLOW | RED
  • healthIssues — JSON-массив {code, severity, message}
  • healthUpdatedAt — дата/время расчёта

Программы и сегменты

Функция evaluateEntityHealth() агрегирует здоровье дочерних элементов по принципу worst-case (наихудший статус) + собственные проверки (даты, ответственный, ручной статус). Программы агрегируют инициативы, сегменты агрегируют программы.

Каскад и снимки

recalculateHealthCascade() — неблокирующий фоновый пересчёт вверх по иерархии при изменении сущности. При каждом пересчёте создаётся HealthSnapshot с кулдауном 1 час (recordSnapshot()).

Оптимизация

getEntityInfoBatch() — пакетная загрузка статусов и имён сущностей. Группирует по типу и выполняет 4 параллельных запроса вместо N отдельных, устраняя N+1 проблему.

Расчёт прогресса (progress.ts)

Реализован в lib/progress.ts.

Инициатива

Веса: задачи = 0.7, вехи = 0.3

progress = round((completedTasks/totalTasks × 0.7 + passedMilestones/totalMilestones × 0.3) × 100)

// Нет задач → 100% вехи; нет вех → 100% задачи; нет ничего → 0%

Программа

progress = round(completedInitiatives / totalInitiatives × 100)

Система внимания (attention)

Endpoint: GET /api/attention. Собирает элементы, требующие действий.

ТипSeverityТриггер
RED_HEALTHREDeffective health = RED
OVERDUEREDendDate < now, status ≠ COMPLETED/CANCELLED
MANUAL_PROBLEMREDmanual problem severity=RED, resolved=false
HIGH_RISKREDrisk severity=HIGH, status=OPEN
OPEN_FORKYELLOWfork OPEN > 14 days

Сортировка: RED → YELLOW. Каждый элемент содержит ID, имя инициативы, программу, тип и описание.

Тренд здоровья (health-trend)

Endpoint: GET /api/health-trend?entityType=...&entityId=...&days=90.

Возвращает массив снимков HealthSnapshot за указанный период (по умолчанию 90 дней). Каждый снимок содержит: date, healthStatus, manualStatus. Отображается компонентом HealthTrendChart (Recharts).

Запуск и развёртывание

Требования

  • Node.js 18+
  • Docker (для PostgreSQL)
  • npm

Переменные окружения

DATABASE_URL=postgresql://user:password@localhost:5433/itstrategy

Команды

docker compose up -d postgres # Запуск БД (порт 5433)

npm install # Установка зависимостей

npm run db:migrate # Миграции Prisma

npm run dev # Dev-сервер (http://localhost:3000)

Конфигурация Prisma 7

  • prisma.config.ts — URL для миграций
  • lib/db.tsPrismaPg({ connectionString }) для runtime
  • next.config.tsserverExternalPackages: ["@prisma/client", "@prisma/adapter-pg", "pg"]

Ключевые зависимости

@xyflow/react — граф (React Flow v12)
dagre — автоматическая раскладка графа
recharts — диаграммы (дашборд, тренд здоровья)
exceljs — генерация и парсинг Excel
date-fns — форматирование дат
@prisma/client — ORM (Prisma 7)
@prisma/adapter-pg — PostgreSQL адаптер
lucide-react — иконки
clsx / tailwind-merge — утилиты CSS-классов

Тёмная тема

Тема управляется через ThemeProvider, который переключает класс .dark на document.documentElement.

  • CSS-переменные в globals.css: --background, --foreground, --card-bg, --border, --muted, --accent и др.
  • Директива @custom-variant dark (&:where(.dark, .dark *)) — необходима для Tailwind 4, чтобы dark: утилиты работали с классом .dark
  • Специальные стили для Recharts (fill), ReactFlow (CSS-переменные --xy-*) и скроллбаров

Полиморфный паттерн

Несколько моделей используют полиморфный паттерн entityType + entityId вместо отдельных FK для каждого типа. Это позволяет привязывать записи к любому типу сущности:

  • EntityRelation — связи между любыми парами объектов
  • EntityHistory — аудит-лог изменений всех сущностей
  • DecisionFork — развилки решений на инициативах и задачах
  • QAItem — блоки вопрос-ответ на любых объектах
  • HealthSnapshot — снимки здоровья всех типов
// Пример запроса prisma.entityHistory.findMany({ where: { entityType: "Initiative", entityId: someId }, orderBy: { changedAt: "desc" } })