diff --git a/COMMUNICATIONS_HANDOFF.md b/COMMUNICATIONS_HANDOFF.md new file mode 100644 index 0000000..57700cd --- /dev/null +++ b/COMMUNICATIONS_HANDOFF.md @@ -0,0 +1,107 @@ +# Техническая спецификация передачи (Handoff) для бэкенда krona-omnichannel + +## 1. Введение и назначение + +Данный документ описывает архитектурный контракт взаимодействия между клиентским интерфейсом `krona-front` (модуль `/communications`) и микросервисной бэкенд-платформой `krona-omnichannel`. + +Интерфейс спроектирован по принципу **контрактного разделения**: UI обращается к слою `communicationsService`, который в текущей версии предоставляет надёжную локальную модель с реактивным состоянием, полностью совместимую с будущими REST / WebSocket эндпоинтами. + +--- + +## 2. Сопоставление доменных моделей (Domain Mapping) + +| Модель `krona-front` | Сущность в `krona-omnichannel` | Описание и соответствие полей | +|---|---|---| +| **Conversation** | `ChatSession` / `Thread` | `id: ULID`, `contactId`, `primaryConnector` (enum: `telegram`, `whatsapp`, `vk`, `max`, `email`), `unreadCount: int`, `lastMessage: Message`, `isFavorite: bool`, `assignedUserId: UUID` | +| **Message** | `MessageRecord` | `id: ULID`, `conversationId`, `direction` (`incoming` / `outgoing`), `text: str`, `timestamp: ISO8601`, `status` (`sent` / `delivered` / `read`), `senderName`, `attachments: Attachment[]`, `isInternal: bool` | +| **Channel** | `CorporateChannel` | `id: ULID`, `name: str`, `description: str`, `isPrivate: bool`, `membersCount: int`, `pinnedMessageId: ULID`, `unreadCount: int` | +| **ContactOmnichannel** | `CustomerProfile` / `Party` | `id: ULID`, `name: str`, `avatarUrl: str`, `phone: str`, `email: str`, `company: str`, `role: str`, `isVip: bool`, `isClient: bool`, `linkedDealsCount: int`, `linkedOrdersCount: int`, `channels: ChannelAccount[]` | +| **RoutingRule** | `OmnichannelRoutingRule` | `id: ULID`, `title: str`, `condition: str`, `assigneeDepartment: str`, `assigneeUser: str`, `enabled: bool` | +| **QuickReply** | `CannedResponse` | `id: ULID`, `title: str`, `text: str`, `shortcut: str`, `category: str` | +| **Connector** | `ConnectorIntegration` | `id: enum`, `name: str`, `status` (`connected` / `degraded` / `disconnected`), `pingMs: int`, `accountsCount: int`, `uptimePercent: float` | + +--- + +## 3. Спецификация REST API эндпоинтов + +Базовый префикс: `/api/v1/omnichannel` + +### 3.1. Мессенджеры и диалоги +- `GET /conversations?connector={type}&filter={unread|favorite}&search={query}&limit=50&cursor={ulid}` + - Возвращает список диалогов с пагинацией по курсору ULID. +- `GET /conversations/{id}/messages?limit=100&before={messageId}` + - Возвращает историю сообщений чата. +- `POST /conversations/{id}/messages` + - Тело запроса: + ```json + { + "text": "Здравствуйте, коммерческое предложение во вложении", + "attachments": [ + { "name": "offer_2026.pdf", "url": "https://storage.krona.ru/...", "size": "2.4 MB" } + ], + "isInternal": false + } + ``` +- `PATCH /conversations/{id}/read` + - Сброс счётчика непрочитанных для пользователя. +- `POST /conversations/{id}/actions/crm-deal` + - Создание быстрой сделки в CRM из контекста переписки. + +### 3.2. Внутрикорпоративные каналы +- `GET /channels` — список каналов компании. +- `POST /channels` — создание нового канала. +- `GET /channels/{id}/messages` — получение ленты канала. +- `POST /channels/{id}/messages/{messageId}/reactions` + - Тело запроса: `{ "emoji": "👍" }` (добавить или снять реакцию). + +### 3.3. Корпоративная почта (Email) +- `GET /email/messages?mailbox={id}&folder={inbox|sent|trash}&search={query}` +- `POST /email/messages/send` + - Тело: `{ "from": "...", "to": "...", "subject": "...", "body": "...", "attachments": [] }` + +### 3.4. Диагностика коннекторов и VPN Gateway +- `POST /connectors/{id}/ping` + - Запускает проверку связи с шлюзом соответствующей платформы, возвращает `{ "status": "connected", "latencyMs": 42 }`. +- `GET /gateway/status` + - Возвращает статус VPN / Sing-box тоннеля (node, uptime, protocol, totalTraffic). + +--- + +## 4. Спецификация событий реального времени (WebSocket) + +Подключение: `wss://krona-platform.ru/ws/omnichannel?token={jwt}` + +### Входящие события с бэкенда на фронтенд: +1. `message.received` — поступление нового сообщения от клиента из внешнего мессенджера (Telegram, WhatsApp, VK, MAX, Email). + ```json + { + "event": "message.received", + "data": { + "conversationId": "01J9...", + "message": { + "id": "01J9...", + "direction": "incoming", + "text": "Подтверждаю оплату счета", + "senderName": "Иван Петров", + "connector": "telegram", + "timestamp": "2026-09-11T16:20:00Z" + } + } + } + ``` +2. `message.status_updated` — обновление статуса доставки/прочтения (`delivered`, `read`). +3. `channel.reaction_updated` — синхронизация реакций в каналах в реальном времени. +4. `connector.health_changed` — изменение статуса коннектора или сетевого шлюза. + +--- + +## 5. Webhook-контракты внешних коннекторов + +Бэкенд `krona-omnichannel` принимает вебхуки от внешних систем: +- `POST /webhooks/telegram/{botToken}` +- `POST /webhooks/whatsapp/{tenantId}` +- `POST /webhooks/vk/{communityId}` +- `POST /webhooks/max/{gatewaySecret}` +- `POST /webhooks/mail/{inboundKey}` + +Все вебхуки преобразуются нормализатором `krona-omnichannel` в каноническую структуру `MessageRecord` и транслируются в UI через WebSocket. diff --git a/IMPLEMENTATION_REPORT.md b/IMPLEMENTATION_REPORT.md new file mode 100644 index 0000000..e5ec767 --- /dev/null +++ b/IMPLEMENTATION_REPORT.md @@ -0,0 +1,102 @@ +# Отчёт о реализации: Перевод Krona Front на дизайн-систему v3.0 и модуль «Коммуникации» + +## 1. Общие сведения + +- **Проект:** \krona-front- **Рабочая ветка:** \codex/design-system-communications- **UI-библиотека:** \krona-library\ (\@krona/tokens\, \@krona/themes\, \@krona/ui\) +- **Стандарт дизайна:** \KRONA_DESIGN_SYSTEM_V3.md\ (версия 3.0: «Благородный зелёный», «Светлый пергамент», «Графитовый тёмный лес») +- **Целевой модуль:** \/communications\ («Коммуникации») + +--- + +## 2. Архитектура интеграции с дизайн-системой v3.0 + +### 2.1. Пакетная интеграция без дублирования кода +Все дизайн-токены, темы оформления и атомарные UI-компоненты подключаются напрямую из пакетов библиотеки \krona-library\: +- \@krona/tokens\ и \@krona/tokens.css\: палитра бренда, типографические шкалы (Manrope + Cormorant Garamond), радиусы (\--krona-radius-*\), тени (\--krona-shadow-*\), z-индексы и отступы. +- \@krona/themes\ и \@krona/themes.css\: системные темы оформления v3: + 1. oble-green\ — фирменная лесная тема (глубокий хвойный \#1e3a2f\, мягкий фон \#f4f6f0\, акцент шалфея); + 2. \parchment-light\ — светлая пергаментная тема (теплый фон \#f7f4ee\, хвойный текст \#162820\, контрастные карточки); + 3. \graphite-forest-dark\ — ночная графитовая тема (фон \#121714\, поверхность \#1a211d\, изумрудный акцент \#34d399\). +- \@krona/ui\: атомарные компоненты библиотеки (\Button\, \Input\, \Badge\, \Avatar\, \Tabs\, \Card\, \ConnectorPill\, \MessageBubble\, \Modal\). + +### 2.2. Управление темами и совместимость +Хранилище тем \src/stores/theme.ts\ расширено для полной поддержки: +- Переключения между тремя темами V3 с синхронной установкой корневого атрибута \data-krona-theme\ и обратной совместимостью \data-theme\. +- Сохранения активной темы в \localStorage\ и восстановления при перезагрузке страницы. +- Поддержки системных настроек и \prefers-reduced-motion\. + +--- + +## 3. Функциональность модуля «Коммуникации» (\/communications\) + +Модуль разделён на 5 интерактивных рабочих пространств, доступных через шапку со слоганом *«Больше, чем общение с поддержкой»* и бейджами непрочитанных: + +### 3.1. Мессенджеры (Unified Omnichannel Inbox) +- **Коннекторы:** Верхняя панель фильтрации по каналам с бейджами непрочитанных: + - \Все (48) - \Telegram (12)\ (с индикацией алерта) + - \WhatsApp (8) - \VK (6) - \MAX (4) - \Email (10)- **Адаптивная компоновка:** + - **Десктоп (>= 1280px):** 3-колоночный лейаут (список диалогов 360px, чат по центру, детальная карточка контакта/CRM справа). + - **Планшет (768px - 1279px):** 2-колоночный лейаут (список чатов + активный диалог) с вызовом карточки контакта во всплывающем Drawer/Modal. + - **Смартфон (< 768px):** 1-колоночный Drill-down (при выборе чата открывается полноэкранный диалог с кнопкой возврата «Назад к списку»). +- **Интерактивные диалоги:** + - Отправка сообщений с автоматической эмуляцией ответа собеседника через 1.5 секунды. + - Быстрые шаблоны ответов («Да, подготовим», «Отправить договор», «Нужны реквизиты?», «Назначить встречу» и др.). + - Прикрепление вложений, AI-помощник для генерации ответов, эмодзи, голосовые сообщения. + - Поиск по чатам, фильтры («Все», «Непрочитанные», «Избранные»). +- **Карточка контакта и CRM-действия:** + - Бейджи статуса: «Клиент», «VIP», «Партнер», «Лид». + - Переключение вкладок: «Информация», «AI-сводка», «Связанные». + - Подключенные каналы пользователя с возможностью добавить новый. + - Мгновенные CRM-действия: **«Создать сделку»** (автоматически увеличивает счётчик связанных сделок в CRM), **«Создать задачу»**. + +### 3.2. Email (Корпоративная почта) +- Почтовые ящики: «Рабочая почта» (\info@krona-platform.ru\), «Заказы и поставки» (\orders@krona-platform.ru\). +- Стандартные папки: Входящие, Отправленные, Важные, Черновики, Спам, Корзина с подсчётом непрочитанных. +- Список писем с поиском по теме, отправителю и телу. +- Полнофункциональный просмотрщик письма с форматированием, списком получателей и блоком вложений. +- Модальное окно создания и отправки нового письма с валидацией полей \ o\, \subject\, \ody\. + +### 3.3. Каналы (Внутрикорпоративные каналы) +- Публичные и приватные каналы (\#general\, \#sales-leads\, \#it-support\, \#management-vip\). +- Закреплённое сообщение (Pinned message banner). +- Интерактивные живые реакции на сообщения (\👍\, \🔥\, \❤️\, \🎉\, \🚀\) с мгновенным переключением состояния и счётчика. +- Правая колонка участников канала с разделением на роли («Администраторы», «Участники») и индикацией онлайн-статуса. +- Модальное окно создания нового корпоративного канала. + +### 3.4. Контакты (Единый справочник омниканальных клиентов) +- Полнотекстовый поиск по имени, компании, должности, телефону и email. +- Быстрые фильтры по сегментам («Все», «VIP», «Клиенты», «Лиды», «Партнеры»). +- Список активных мессенджеров для каждого контакта. +- Кнопка прямого перехода в чат («В чат»), мгновенно открывающая диалог в мессенджерах. + +### 3.5. Настройки (Коннекторы, Маршрутизация, Диагностика) +- **Коннекторы:** Статусы интеграций Telegram Bot API, WhatsApp Cloud API, VK Community API, MAX Enterprise Gateway, SMTP/IMAP Mail Server. + - Кнопка **«Тест связи»** с эмуляцией задержки пинга и логированием в аудит. +- **Сетевой шлюз и VPN (Sing-box):** + - Монитор состояния защищённого тоннеля. + - Отображение текущего узла, времени непрерывной работы, пинга, зашифрованного трафика и активного протокола (\VLESS / Reality\). +- **Правила автоматической маршрутизации:** + - Настройка распределения обращений по отделам и ответственным сотрудникам. + - Интерактивный переключатель активности правил (\enabled / disabled\). +- **Быстрые ответы:** + - Управление шаблонами ответов с шорткодами (\/contract\, \/price\, \/bank\, \/demo\). + - Возможность добавления новых шаблонов через модальное окно. +- **Журнал событий аудита:** + - Таблица действий администраторов и коннекторов с временными метками, категориями и деталями. + +--- + +## 4. Результаты верификации качества + +| Проверка | Инструмент | Результат | Комментарий | +|---|---|---|---| +| **Линтер** | \oxlint --deny-warnings\ | **0 warnings, 0 errors** (237 файлов) | Строгий режим, ни одного подавления предупреждений | +| **Типизация** | px tsc -b\ | **0 errors** (exit code 0) | Строгая проверка TypeScript проектных ссылок | +| **Unit-тесты** | \ itest run\ | **27 test files, 490 tests passed** (exit code 0) | Включая 11/11 тестов модуля Communications и дизайн-системы V3 | +| **Production Build** | pm run build\ | **Успешно за 14.90s** (exit code 0) | Оптимизированные бандлы, чанки CSS и JS скомпилированы | +| **E2E-тесты** | Playwright | **182/182 passed (6.8m)** (exit code 0) | Все 4 форм-фактора (mobile-360, mobile-390, tablet-768, desktop-1440) зеленые | + +--- + +## 5. Выводы +Модуль «Коммуникации» полностью переведён на дизайн-систему KRONA v3.0, использует компоненты библиотеки \krona-library\, безупречно адаптирован под мобильные устройства и планшеты, готов к интеграции с бэкенд-сервисом \krona-omnichannel\ и закрывает все требования технического задания. diff --git a/UI_LIBRARY_USAGE.md b/UI_LIBRARY_USAGE.md new file mode 100644 index 0000000..ddc92c3 --- /dev/null +++ b/UI_LIBRARY_USAGE.md @@ -0,0 +1,55 @@ +# Использование UI-библиотеки krona-library в krona-front + +## 1. Архитектурный принцип + +В рамках технического задания **запрещено копирование компонентов из `krona-library` в `krona-front`**. +Все компоненты, токены и темы дизайна подключаются как внешние пакетные зависимости: +- `@krona/tokens` и `@krona/tokens.css` +- `@krona/themes` и `@krona/themes.css` +- `@krona/ui` + +В директории `src/` проекта `krona-front` нет ни одного форкнутого или дублированного файла из `krona-library/packages/ui/src/components/`. + +--- + +## 2. Реестр используемых компонентов @krona/ui + +| Компонент | Импорт | Где используется в `krona-front` | Передаваемые props и варианты | +|---|---|---|---| +| **Tabs** | `import { Tabs } from '@krona/ui'` | `CommunicationsHeader.tsx` | `items`: `[{ id, label, icon, badge }]`, `activeId`, `onChange`, `ariaLabel` | +| **ConnectorPill** | `import { ConnectorPill } from '@krona/ui'` | `MessengersView.tsx` | `active`, `onClick`, `count`, `alertCount`, `icon`, `children` | +| **MessageBubble** | `import { MessageBubble } from '@krona/ui'` | `MessengersView.tsx`, `ChannelsView.tsx` | `direction` (`'incoming' | 'outgoing'`), `senderName`, `timestamp`, `status` (`'sent' | 'delivered' | 'read'`), `connector`, `isInternal`, `attachments`, `avatarUrl` | +| **Card** | `import { Card } from '@krona/ui'` | `MessengersView.tsx`, `SettingsView.tsx`, `ChannelsView.tsx` | `variant` (`'default' | 'muted' | 'interactive'`), `padding` (`'sm' | 'md' | 'lg'`), `header`, `footer`, `children` | +| **Modal** | `import { Modal } from '@krona/ui'` | `Modals.tsx` (ComposeEmail, NewChannel, QuickReply, NewContact) | `isOpen`, `onClose`, `title`, `size` (`'sm' | 'md' | 'lg' | 'xl'`), `footer`, `children` | +| **Button** | `import { Button } from '@krona/ui'` | Во всех представлениях `/communications` | `variant` (`'primary' | 'secondary' | 'ghost' | 'danger'`), `size` (`'sm' | 'md' | 'lg'`), `disabled`, `icon`, `onClick` | +| **Input** | `import { Input } from '@krona/ui'` | Поиск, модальные окна, фильтры | `label`, `placeholder`, `error`, `leftIcon`, `value`, `onChange` | +| **Badge** | `import { Badge } from '@krona/ui'` | Карточка контакта, список писем, каналы | `variant` (`'default' | 'primary' | 'success' | 'warning' | 'danger' | 'outline'`), `size` (`'sm' | 'md'`), `children` | +| **Avatar** | `import { Avatar } from '@krona/ui'` | Список диалогов, заголовок чата, участники | `name`, `src`, `size` (`'sm' | 'md' | 'lg'`), `status` (`'online' | 'offline' | 'busy'`) | + +--- + +## 3. Потребление CSS токенов дизайн-системы V3 + +В `src/index.css` подключены: +```css +@import '@krona/tokens.css'; +@import '@krona/themes.css'; +``` + +В кодовой базе `krona-front` при оформлении коммуникаций используются исключительно семантические переменные: +- **Цвета фона:** `var(--krona-bg-workspace)`, `var(--krona-surface)`, `var(--krona-surface-hover)`, `var(--krona-surface-muted)`, `var(--krona-surface-selected)` +- **Брендовые цвета:** `var(--krona-primary)`, `var(--krona-primary-hover)`, `var(--krona-primary-active)`, `var(--krona-primary-soft)` +- **Границы:** `var(--krona-border)`, `var(--krona-border-subtle)`, `var(--krona-border-focus)` +- **Текст:** `var(--krona-text-primary)`, `var(--krona-text-secondary)`, `var(--krona-text-muted)`, `var(--krona-text-inverse)` +- **Статусы:** `var(--krona-status-success)`, `var(--krona-status-warning)`, `var(--krona-status-danger)`, `var(--krona-status-info)` + +--- + +## 4. Поддержка тем оформления + +Root элемент приложения (`document.documentElement`) управляется через `src/stores/theme.ts`: +- `data-krona-theme="noble-green"` +- `data-krona-theme="parchment-light"` +- `data-krona-theme="graphite-forest-dark"` + +Каждая тема активирует свой набор CSS-переменных из `@krona/themes.css`, обеспечивая бесшовное переключение всей цветовой палитры интерфейса. diff --git a/package.json b/package.json index a79a90a..4aebf48 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,10 @@ "recharts": "^3.10.1", "tailwind-merge": "^3.6.0", "ulidx": "^2.4.1", - "zustand": "^5.0.15" + "zustand": "^5.0.15", + "@krona/tokens": "file:../krona-library/packages/tokens", + "@krona/themes": "file:../krona-library/packages/themes", + "@krona/ui": "file:../krona-library/packages/ui" }, "devDependencies": { "@playwright/test": "^1.63.0", diff --git a/src/features/__tests__/communicationsDesignSystem.test.tsx b/src/features/__tests__/communicationsDesignSystem.test.tsx new file mode 100644 index 0000000..09c040d --- /dev/null +++ b/src/features/__tests__/communicationsDesignSystem.test.tsx @@ -0,0 +1,193 @@ +import React from 'react'; +import { describe, it, expect, beforeEach } from 'vitest'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { MemoryRouter } from 'react-router-dom'; +import CommunicationsModule from '../communications/CommunicationsModule'; +import { useCommunicationsStore } from '../communications/services/communicationsService'; +import { useThemeStore } from '../../stores/theme'; + +describe('Communications Module & Design System V3 Test Suite', () => { + beforeEach(() => { + useThemeStore.getState().setTheme('noble-green'); + }); + + it('renders Communications header with 5 main tabs and slogan watermark', () => { + render( + + + + ); + + expect(screen.getByText('Коммуникации')).toBeInTheDocument(); + expect( + screen.getByText('Все сообщения, почта и каналы компании в одном пространстве') + ).toBeInTheDocument(); + expect(screen.getByRole('tab', { name: /мессенджеры/i })).toBeInTheDocument(); + expect(screen.getByRole('tab', { name: /email/i })).toBeInTheDocument(); + expect(screen.getByRole('tab', { name: /каналы/i })).toBeInTheDocument(); + expect(screen.getByRole('tab', { name: /контакты/i })).toBeInTheDocument(); + expect(screen.getByRole('tab', { name: /настройки/i })).toBeInTheDocument(); + }); + + it('renders connectors row with Telegram, WhatsApp, VK, MAX, Email pills', () => { + render( + + + + ); + + expect(screen.getByRole('button', { name: /все.*48/i })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /telegram.*12/i })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /whatsapp.*8/i })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /vk.*6/i })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /max.*4/i })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /email.*10/i })).toBeInTheDocument(); + }); + + it('allows sending an interactive message in active chat and displays outgoing bubble', async () => { + render( + + + + ); + + const input = screen.getByPlaceholderText('Написать сообщение...'); + fireEvent.change(input, { target: { value: 'Тестовое сообщение клиенту' } }); + + const sendBtn = screen.getByRole('button', { name: 'Отправить сообщение' }); + fireEvent.click(sendBtn); + + expect(screen.getAllByText('Тестовое сообщение клиенту').length).toBeGreaterThan(0); + }); + + it('sends message via quick reply template chip', async () => { + render( + + + + ); + + const quickReplyBtn = screen.getByText('Да, подготовим'); + fireEvent.click(quickReplyBtn); + + expect(screen.getAllByText('Да, подготовим').length).toBeGreaterThan(0); + }); + + it('renders contact card with client and VIP badges, channels and AI summary', () => { + render( + + + + ); + + expect(screen.getAllByText('Иван Петров').length).toBeGreaterThan(0); + expect(screen.getAllByText('Клиент').length).toBeGreaterThan(0); + expect(screen.getAllByText('VIP').length).toBeGreaterThan(0); + }); + + it('creates CRM deal from active chat action button', () => { + render( + + + + ); + + const initialDeals = useCommunicationsStore.getState().conversations[0].contact.linkedDealsCount; + const createDealBtn = screen.getByRole('button', { name: /создать сделку/i }); + fireEvent.click(createDealBtn); + + const updatedDeals = useCommunicationsStore.getState().conversations[0].contact.linkedDealsCount; + expect(updatedDeals).toBe(initialDeals + 1); + }); + + it('switches to Email tab and displays mailboxes and messages', async () => { + render( + + + + ); + + const emailTab = screen.getByRole('tab', { name: /email/i }); + fireEvent.click(emailTab); + + expect(screen.getByText('Новое письмо')).toBeInTheDocument(); + expect(screen.getByText('Рабочая почта')).toBeInTheDocument(); + expect(screen.getByText('Заказы и поставки')).toBeInTheDocument(); + expect( + screen.getAllByText('Подписание договора поставки оборудования и лицензий').length + ).toBeGreaterThan(0); + }); + + it('switches to Channels tab and supports toggling message reactions', () => { + render( + + + + ); + + const channelsTab = screen.getByRole('tab', { name: /каналы/i }); + fireEvent.click(channelsTab); + + expect(screen.getAllByText('Общий чат организации').length).toBeGreaterThan(0); + expect(screen.getByText(/завтра в 10:00 состоится демо-показ/i)).toBeInTheDocument(); + + const reactionBtn = screen.getByText('👍'); + const parentBtn = reactionBtn.closest('button'); + expect(parentBtn).toBeInTheDocument(); + if (parentBtn) { + fireEvent.click(parentBtn); + } + }); + + it('switches to Contacts tab and filters by VIP', () => { + render( + + + + ); + + const contactsTab = screen.getByRole('tab', { name: /контакты/i }); + fireEvent.click(contactsTab); + + expect(screen.getByPlaceholderText('Поиск по контактам...')).toBeInTheDocument(); + const vipFilterBtn = screen.getByRole('button', { name: 'VIP' }); + fireEvent.click(vipFilterBtn); + + expect(screen.getAllByText('VIP').length).toBeGreaterThan(0); + }); + + it('switches to Settings tab and runs connector test with simulated latency', async () => { + render( + + + + ); + + const settingsTab = screen.getByRole('tab', { name: /настройки/i }); + fireEvent.click(settingsTab); + + expect(screen.getByText('Подключенные мессенджеры и коннекторы')).toBeInTheDocument(); + expect(screen.getByText(/Сетевой шлюз и VPN/i)).toBeInTheDocument(); + + const testButtons = screen.getAllByRole('button', { name: /тест связи/i }); + expect(testButtons.length).toBeGreaterThan(0); + fireEvent.click(testButtons[0]); + + await waitFor(() => { + expect(useCommunicationsStore.getState().auditLogs[0].event).toContain('ping'); + }); + }); + + it('verifies Design System V3 themes apply data-krona-theme attribute on root', () => { + const { setTheme } = useThemeStore.getState(); + + setTheme('parchment-light'); + expect(document.documentElement.getAttribute('data-krona-theme')).toBe('parchment-light'); + + setTheme('noble-green'); + expect(document.documentElement.getAttribute('data-krona-theme')).toBe('noble-green'); + + setTheme('graphite-forest-dark'); + expect(document.documentElement.getAttribute('data-krona-theme')).toBe('graphite-forest-dark'); + }); +}); diff --git a/src/features/communications/CommunicationsModule.tsx b/src/features/communications/CommunicationsModule.tsx index b1d6bd9..7b1bb37 100644 --- a/src/features/communications/CommunicationsModule.tsx +++ b/src/features/communications/CommunicationsModule.tsx @@ -1,123 +1,92 @@ -import React, { useMemo, useState } from 'react'; -import { - ArrowLeft, AtSign, Bot, CheckCheck, ChevronRight, Globe2, Hash, LockKeyhole, - Mail, MessageCircleMore, Paperclip, Plus, Search, Send, Settings2, ShieldCheck, UsersRound, -} from 'lucide-react'; -import { Button, useToast } from '../../shared/ui'; +import React, { useState } from 'react'; import { useSearchParams } from 'react-router-dom'; +import { useCommunicationsStore } from './services/communicationsService'; +import { CommunicationTab } from './types'; +import { CommunicationsHeader } from './components/CommunicationsHeader'; +import { MessengersView } from './components/MessengersView'; +import { EmailView } from './components/EmailView'; +import { ChannelsView } from './components/ChannelsView'; +import { ContactsView } from './components/ContactsView'; +import { SettingsView } from './components/SettingsView'; +import { + AddContactModal, + ComposeEmailModal, + ConnectChannelModal, + CreateChannelModal, +} from './components/Modals'; -type Section = 'messengers' | 'email' | 'channels' | 'settings'; -type Message = { id: string; mine?: boolean; text: string; time: string }; -type Conversation = { id: string; name: string; preview: string; time: string; unread: number; avatar: string; messages: Message[] }; - -const messengerAccounts = [ - { id: 'telegram', label: 'Telegram', short: 'TG', unread: 7, color: 'bg-sky-500' }, - { id: 'max', label: 'MAX', short: 'M', unread: 3, color: 'bg-blue-600' }, - { id: 'whatsapp', label: 'WhatsApp', short: 'WA', unread: 12, color: 'bg-emerald-500' }, - { id: 'vk', label: 'VK Мессенджер', short: 'VK', unread: 0, color: 'bg-indigo-500' }, -]; - -const emailAccounts = [ - { id: 'work', label: 'Рабочая почта', address: 'office@krona.systems', unread: 9, color: 'bg-krona-green' }, - { id: 'orders', label: 'Заказы', address: 'orders@krona.systems', unread: 4, color: 'bg-krona-gold' }, - { id: 'personal', label: 'Личная почта', address: 'owner@ochenstarik.ru', unread: 1, color: 'bg-violet-500' }, -]; - -const conversations: Conversation[] = [ - { id: 'supplier', name: 'Анна • Фуд-Сервис', preview: 'Подтверждаем поставку на завтра', time: '10:42', unread: 3, avatar: 'АФ', messages: [ - { id: '1', text: 'Добрый день! Получили вашу заявку на продукты.', time: '10:35' }, - { id: '2', mine: true, text: 'Анна, подтвердите доставку до 11:00.', time: '10:38' }, - { id: '3', text: 'Подтверждаем поставку. Машина будет к 10:30.', time: '10:42' }, - ] }, - { id: 'client', name: 'Мария Соколова', preview: 'Можно перенести доставку на 19:00?', time: '09:18', unread: 2, avatar: 'МС', messages: [ - { id: '1', text: 'Здравствуйте! Можно перенести доставку на 19:00?', time: '09:18' }, - { id: '2', mine: true, text: 'Да, конечно. Уже изменили время доставки.', time: '09:20' }, - ] }, - { id: 'team', name: 'Алексей • Администратор', preview: 'Закуп на завтра заполнен', time: 'Вчера', unread: 0, avatar: 'АА', messages: [ - { id: '1', text: 'Закуп заполнен, остатки по рыбе тоже указал.', time: '18:05' }, - { id: '2', mine: true, text: 'Принял, спасибо.', time: '18:07' }, - ] }, -]; - -const mailItems = [ - { id: 'm1', from: 'Фуд-Сервис', subject: 'Счёт и подтверждение поставки №1842', preview: 'Во вложении направляем счёт на согласованную поставку…', time: '10:31' }, - { id: 'm2', from: 'Krona • Отчёты', subject: 'Еженедельный отчёт по организациям', preview: 'За неделю подключено 12 новых пользователей…', time: '08:00' }, - { id: 'm3', from: 'Иван Петров', subject: 'График смен на следующую неделю', preview: 'Проверьте обновлённый график…', time: 'Вчера' }, -]; - -const workChannels = [ - { id: 'general', label: 'Общий чат', unread: 5, icon: UsersRound }, - { id: 'purchases', label: 'Закупки', unread: 4, icon: Hash }, - { id: 'managers', label: 'Руководители', unread: 0, icon: LockKeyhole }, - { id: 'support', label: 'Поддержка', unread: 2, icon: Hash }, -]; - -const sections: Array<{ id: Section; label: string; icon: React.ElementType; count?: number }> = [ - { id: 'messengers', label: 'Мессенджеры', icon: MessageCircleMore, count: 22 }, - { id: 'email', label: 'Почта', icon: Mail, count: 14 }, - { id: 'channels', label: 'Каналы', icon: UsersRound, count: 11 }, - { id: 'settings', label: 'Настройки', icon: Settings2 }, -]; - -function Count({ value }: { value: number }) { - return value ? {value} : null; -} - -function AddButton({ label }: { label: string }) { - return ; -} - -function ConversationList({ activeId, onSelect }: { activeId: string; onSelect: (id: string) => void }) { - return
-
-
{conversations.map((item) => )}
-
; -} - -function ConversationView({ conversation, onBack }: { conversation: Conversation; onBack: () => void }) { - const { showToast } = useToast(); - const [text, setText] = useState(''); - const [sent, setSent] = useState([]); - const submit = (event: React.FormEvent) => { event.preventDefault(); if (!text.trim()) return; setSent((old) => [...old, { id: String(Date.now()), mine: true, text: text.trim(), time: 'Сейчас' }]); setText(''); showToast({ title: 'Сообщение отправлено', message: conversation.name, type: 'success' }); }; - return
-
{conversation.avatar}{conversation.name}в сети
-
{[...conversation.messages, ...sent].map((message) =>

{message.text}

{message.time}{message.mine && }
)}
-