]> gitweb.erp-flowers.ru Git - erp24_rep/yii-erp24/.git/commitdiff
Добавление документации origin/feature_filippov_20251128_add_docs
authorAleksey Filippov <Aleksey.Filippov@erp-flowers.ru>
Mon, 1 Dec 2025 09:16:46 +0000 (12:16 +0300)
committerAleksey Filippov <Aleksey.Filippov@erp-flowers.ru>
Mon, 1 Dec 2025 09:16:46 +0000 (12:16 +0300)
17 files changed:
CLAUDE.md
erp24/docs/INDEX.md
erp24/docs/README.md
erp24/docs/SUMMARY.md
erp24/docs/api/api3/README.md
erp24/docs/models/Admin.md
erp24/docs/modules/README.md
erp24/docs/modules/bonus/README.md
erp24/docs/services/BonusService.md
erp24/docs/services/CabinetService.md
erp24/docs/services/DashboardService.md
erp24/docs/services/MotivationService.md
erp24/docs/services/PayrollService.md
erp24/docs/services/README.md
erp24/docs/services/RatingService.md
erp24/docs/services/SalesService.md
erp24/docs/services/TelegramService.md

index 770cae14b282c1b38c11aee72fd238482e5acb9a..0ef08c6318f3f78d6f81e273581ad3f2e134fed0 100644 (file)
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -21,6 +21,13 @@ It establishes a single, consistent behaviour model for both environments.
 
 * точной,
 * исчерпывающей,
+* не пиши И т.д. дай развёрнутый ответ
+* нужны развёрнутое описание не только ссылайся на другой метод 
+* нужно углублённое описание логики всех методов
+* больше вводных данных
+* больше описаний потоков данных
+* отображать все паратры которые есть у метода
+* список вызовов сторонних методов с кратким описанием
 * соответствующей коду в репозитории,
 * удобной для онбординга новых разработчиков,
 * формализованной (Markdown + Mermaid + ссылки между файлами).
index 7ca75f21454d19d136fd6c6737191cd790eee63b..d976975d97883ba80f01b03774959cb472bbea09 100644 (file)
@@ -1,11 +1,76 @@
 # Быстрый индекс документации ERP24
 
+## 🧠 Mindmap: Навигация по документации
+
+```mermaid
+mindmap
+  root((ERP24 Docs))
+    Начните здесь
+      README
+      SUMMARY
+      INDEX
+      CROSS_REFERENCE
+      CHANGELOG
+      GLOSSARY
+      CONSTANTS
+    Архитектура
+      system-overview
+      api-architecture
+      DATA_FLOW
+      DOMAIN_MODEL
+    API
+      API3 50%
+        18 контроллеров
+        76 эндпоинтов
+        10 сервисов
+      API2
+        33 контроллера
+      API1 Legacy
+    Сервисы 48шт
+      P0 Критические 9
+      P1 Высокий 10
+      P2/P3 остальные
+    Контроллеры 161шт
+      Нестандартные 47
+      Стандартные 114
+    Консольные 62 команды
+      CronController 28
+      BonusController 9
+      Прочие 25
+    Модели 393шт
+      Документировано 22
+      HR модели
+      Продажи
+      Товары
+    Модули 12шт
+      HR 5
+      Обучение 2
+      Операции 2
+      Коммуникации 2
+      Аналитика 1
+    Helpers 15шт
+      DateHelper
+      DataHelper
+      SalaryHelper
+    База данных
+      Схема
+      Таблицы
+      Миграции 278
+    Ошибки
+      Бизнес
+      Аутентификация
+      Валидация
+```
+
 ## 🚀 Начните здесь
 
 - **[README.md](./README.md)** - Главная страница документации
 - **[SUMMARY.md](./SUMMARY.md)** - Итоговая сводка (статистика, метрики)
 - **[CROSS_REFERENCE.md](./CROSS_REFERENCE.md)** - Матрица взаимосвязей модулей
-- **[CHANGELOG.md](./CHANGELOG.md)** - История изменений документации ✨ NEW
+- **[CHANGELOG.md](./CHANGELOG.md)** - История изменений документации
+- **[GLOSSARY.md](./GLOSSARY.md)** - Глоссарий терминов ✨ NEW
+- **[CONSTANTS.md](./CONSTANTS.md)** - Справочник констант ✨ NEW
+- **[DATA_ANALYST_REVIEW_REPORT.md](./DATA_ANALYST_REVIEW_REPORT.md)** - Отчёт анализа документации ✨ NEW
 
 ## 🏗️ Архитектура
 
 - MarketplaceController (5 команд) - Yandex Market, Flowwow
 - TimetableController (1 команда) - автозакрытие смен
 
-## 📦 Модели и Records ✨ UPDATED
+## 📦 Модели и Records ✨ UPDATED (22 документа)
 
 - **[Models README](./models/README.md)** - Классификация 393 моделей
 - **[Admin](./models/Admin.md)** - Модель сотрудников (IdentityInterface)
+- **[AdminPayroll](./models/AdminPayroll.md)** - Зарплаты сотрудников (EAV)
+- **[AdminRating](./models/AdminRating.md)** - Рейтинги сотрудников
+- **[Balances](./models/Balances.md)** - Остатки товаров ✨ NEW
+- **[Dashboard](./models/Dashboard.md)** - Конфигурация дашбордов
+- **[Grade](./models/Grade.md)** - Грейды сотрудников ✨ NEW
 - **[Users](./models/Users.md)** - Модель пользователей (клиенты)
 - **[Sales](./models/Sales.md)** - Модель чеков продаж
+- **[SalesProducts](./models/SalesProducts.md)** - Товары в чеках
+- **[Prices](./models/Prices.md)** - Цены товаров
 - **[Task](./models/Task.md)** - Модель задач
-- **[Files](./models/Files.md)** - Универсальное хранилище файлов ✨ NEW
+- **[Files](./models/Files.md)** - Универсальное хранилище файлов
+- **[Lessons](./models/Lessons.md)** - Уроки и курсы
+- **[Regulations](./models/Regulations.md)** - Регламенты
 - **[Store](./models/Store.md)** - Модель магазинов
 - **[Timetable](./models/Timetable.md)** - Модель расписания
 - **[UsersBonus](./models/UsersBonus.md)** - Модель бонусных операций
 - **[MarketplaceOrders](./models/MarketplaceOrders.md)** - Заказы маркетплейсов
 - **[Products1c](./models/Products1c.md)** - Интеграция с 1С
 
+## 🔧 Helpers ✨ NEW (5 документов)
+
+- **[Helpers README](./helpers/README.md)** - Обзор 15 вспомогательных классов
+- **[DateHelper](./helpers/DateHelper.md)** - Работа с датами и сменами
+- **[DataHelper](./helpers/DataHelper.md)** - UUID, JSON, массивы
+- **[SalaryHelper](./helpers/SalaryHelper.md)** - Расчёт зарплат и бонусов
+- **[FormatHelper](./helpers/FormatHelper.md)** - Форматирование чисел
+- **[UtilHelper](./helpers/UtilHelper.md)** - Общие утилиты
+
 ## 🗄️ База данных ✨ NEW
 
 - **[Database README](./database/README.md)** - Обзор структуры БД
 
 | Компонент | Количество | Покрытие |
 |-----------|-----------|----------|
-| **Документов Markdown** | **209** | **100%** |
+| **Документов Markdown** | **218** | **100%** |
 | **Бизнес-модулей** | **12** | **100%** (12/12) |
 | **Web-контроллеров** | **161** | **100%** (161/161) |
 | **Console команд** | **62** | **100%** (62/62) |
 | **Сервисов** | **48** | **100%** (48/48) |
 | **API3 контроллеров** | **18** | **50%** (9/18) |
 | **API3 эндпоинтов** | **76** | **71%** (54/76) |
-| **Моделей** | **393** | **1%** (3/393) |
-| **Диаграмм** | **35+** | - |
-| **Примеров кода** | **100+** | - |
-| **Размер документации** | **~5.1 MB** | - |
+| **Моделей** | **393** | **6%** (22/393) |
+| **Диаграмм** | **40+** | - |
+| **Примеров кода** | **120+** | - |
+| **Размер документации** | **~5.5 MB** | - |
 
 ## 📝 Версии
 
-- **v2.0** (2025-11-27) - Расширенная документация ✨ CURRENT
+- **v2.2** (2025-11-29) - Mindmap схемы и критический анализ ✨ CURRENT
+  - Добавлены mindmap схемы ко всем ключевым документам
+  - Критический анализ и исправление статистики
+  - Обновление версий до актуальных
+  - 218 документов (~5.5 MB)
+
+- **v2.1** (2025-11-29) - Расширение документации для ТЗ
+  - 218 документов (~5.5 MB)
+  - GLOSSARY.md — глоссарий терминов
+  - CONSTANTS.md — справочник констант
+  - DATA_ANALYST_REVIEW_REPORT.md — анализ пробелов
+  - Новые модели: Balances, Grade, AdminPayroll
+  - 22 документированные модели (6% покрытия)
+
+- **v2.0** (2025-11-27) - Расширенная документация
   - 209 документов (~5.1 MB)
   - 161 web-контроллер
   - 62 консольные команды
index 6423bec5a4f4ad79bc29edecc2a58eea671fca26..4b235f098205c2c5e751cf51f1f3b9a4595f52f4 100644 (file)
@@ -2,6 +2,71 @@
 
 Добро пожаловать в документацию системы ERP24 - комплексной ERP-системы для управления сетью цветочных магазинов на базе Yii2 Framework.
 
+## 🧠 Карта документации (Mindmap)
+
+```mermaid
+mindmap
+  root((ERP24))
+    HR и Персонал
+      Payroll
+        Зарплата
+        Оклады
+        Премии
+      Bonus
+        Баллы клиентов
+        Командные бонусы
+        Конвертация
+      Timetable
+        Расписание
+        Смены
+        Чекины
+      Rating
+        Рейтинг сотрудников
+        KPI
+        Метрики
+      Grade
+        Грейды
+        Карьерный рост
+    Обучение
+      Lesson
+        Курсы
+        Тесты
+        Сертификаты
+      Regulations
+        Регламенты
+        Опросники
+    Операции
+      Shipment
+        Поставки
+        Закупки
+        Распределение
+      WriteOffs
+        Списания
+        Брак
+        Естественная убыль
+    Коммуникации
+      Notifications
+        Push
+        Уведомления
+      KIK Feedback
+        Жалобы
+        Благодарности
+    Аналитика
+      Dashboard
+        Метрики
+        Графики
+        KPI
+    API
+      API1 Legacy
+      API2 REST
+      API3 Advanced
+    Интеграции
+      1С
+      Telegram
+      AmoCRM
+      SMS
+```
+
 ## 📋 О системе
 
 **ERP24** - это enterprise resource planning система, разработанная для автоматизации всех бизнес-процессов сети цветочных магазинов:
@@ -227,20 +292,20 @@ php yii timetable/autoclose-shifts
 |-----------|-----------|------------------------|
 | PHP файлы | ~3,771 | - |
 | **Web контроллеры** | **161** ✅ | **100%** (161/161) |
-| **Records/Models** | **393** ✅ | **1%** (3/393) |
+| **Records/Models** | **393** ✅ | **6%** (22/393) |
 | **Сервисы** | **48** ✅ | **100%** (48/48) |
 | Actions | 40+ | Частично |
 | API2 контроллеры | 33 | Не документировано |
 | API3 контроллеры | 18 | **50%** (9/18) |
 | API3 эндпоинты | 76 | **71%** (54/76) |
-| Helpers | 15+ | Не документировано |
+| **Helpers** | **15** ✅ | **33%** (5/15) |
 | Forms | 20+ | Не документировано |
 | **Console контроллеры** | **17** ✅ | **100%** (17/17) |
 | **Console команды** | **62** ✅ | **100%** (62/62) |
 | Migrations | 278 | Не документировано |
 | Jobs | 6 | Не документировано |
 | **Бизнес-модули** | **12** ✅ | **100%** (12/12) |
-| **Документов Markdown** | **209** ✅ | **~5.1 MB** |
+| **Документов Markdown** | **218** ✅ | **~5.5 MB** |
 
 ## 🚀 Быстрый старт
 
@@ -485,6 +550,6 @@ namespace yii_app\actions\{module};
 
 *Документация поддерживается в актуальном состоянии командой разработки ERP24.*
 
-**Версия документации:** 2.0
-**Последнее обновление:** 2025-11-27
+**Версия документации:** 2.1
+**Последнее обновление:** 2025-11-29
 **Статус:** ✅ Активно поддерживается
index 3be64dd5bd6fd41c0966cba7321f400bb2bc0549..6e57bab034ce4ffac3b57a013da1746bc938ce82 100644 (file)
@@ -2,9 +2,79 @@
 
 ## 🎉 Статус документации: АКТИВНО ПОДДЕРЖИВАЕТСЯ
 
-**Версия:** 2.0
-**Дата обновления:** 2025-11-27
-**Статус:** ✅ Завершены основные разделы, расширение продолжается
+**Версия:** 2.2
+**Дата обновления:** 2025-11-29
+**Статус:** ✅ Завершены основные разделы, добавлены mindmap схемы
+
+## 🧠 Mindmap: Структура документации
+
+```mermaid
+mindmap
+  root((Документация ERP24))
+    Навигация
+      README
+      INDEX
+      SUMMARY
+      CROSS_REFERENCE
+      CHANGELOG
+      GLOSSARY
+      CONSTANTS
+    Модули 12шт
+      HR 5шт
+        Bonus
+        Payroll
+        Timetable
+        Rating
+        Grade
+      Обучение 2шт
+        Lesson
+        Regulations
+      Операции 2шт
+        Shipment
+        WriteOffs
+      Коммуникации 2шт
+        Notifications
+        KIK Feedback
+      Аналитика 1шт
+        Dashboard
+    Сервисы 48шт
+      BonusService
+      PayrollService
+      ShipmentService
+      RatingService
+      DashboardService
+      ещё 43 сервиса
+    Контроллеры 161шт
+      Стандартные 114
+      Нестандартные 47
+    Консольные 62 команды
+      CronController 28
+      BonusController 9
+      MarketplaceController 5
+      прочие 20
+    API
+      API1 Legacy
+      API2 Modern
+      API3 Advanced 76 эндпоинтов
+    Модели 393шт
+      Документировано 22
+      В работе 371
+    Helpers 15шт
+      DateHelper
+      DataHelper
+      SalaryHelper
+      FormatHelper
+      UtilHelper
+    База данных
+      Схема
+      Таблицы
+      Миграции 278
+    Ошибки
+      Бизнес-ошибки
+      Аутентификация
+      Валидация
+      Коды ошибок
+```
 
 ## 📊 Общая статистика проекта
 
 
 | Метрика | Значение |
 |---------|----------|
-| **Документов Markdown** | **209 файлов** (~5.1 MB) |
+| **Документов Markdown** | **218 файлов** (~5.3 MB) |
 | **Бизнес-модулей** | 12/12 (100%) ✅ |
 | **Web-контроллеров** | 161/161 (100%) ✅ |
 | **Console команд** | 62/62 (100%) ✅ |
 | **Сервисов** | 48/48 (100%) ✅ |
 | **API3 модулей** | 9/18 (50%) 🔄 |
 | **API3 эндпоинтов** | 54/76 (71%) 🔄 |
-| **Моделей** | 15/393 (4%) 🔄 |
-| **Mermaid диаграмм** | 35+ |
-| **Примеров кода** | 100+ |
-| **ER-диаграмм** | 12 |
-| **Таблиц со статистикой** | 50+ |
+| **Моделей** | 19/393 (5%) 🔄 |
+| **Helpers** | 5/15 (33%) ✨ NEW |
+| **Mermaid диаграмм** | 40+ |
+| **Примеров кода** | 110+ |
+| **ER-диаграмм** | 15 |
+| **Таблиц со статистикой** | 55+ |
 | **FAQ секций** | 12 |
 
 ## 📚 Документированные модули
@@ -277,16 +348,28 @@ erp24/docs/
 ├── models/                       # 393 модели ✨ UPDATED
 │   ├── README.md                # Обзор моделей ✅
 │   ├── Admin.md                 # Сотрудники (IdentityInterface) ✅
+│   ├── AdminRating.md           # Рейтинги сотрудников ✅ NEW
+│   ├── Dashboard.md             # Конфигурация дашбордов ✅ NEW
 │   ├── Users.md                 # Пользователи (клиенты) ✅
 │   ├── Sales.md                 # Чеки продаж ✅
 │   ├── Task.md                  # Задачи ✅
-│   ├── Files.md                 # Файловое хранилище ✅ NEW
+│   ├── Files.md                 # Файловое хранилище ✅
 │   ├── Store.md                 # Магазины ✅
 │   ├── Timetable.md             # Расписание ✅
 │   ├── UsersBonus.md            # Бонусные операции ✅
 │   ├── MarketplaceOrders.md     # Заказы маркетплейсов ✅
 │   ├── Products1c.md            # Интеграция с 1С ✅
-│   └── ... (15 документов)
+│   ├── Lessons.md               # Уроки и курсы ✅ NEW
+│   ├── Regulations.md           # Регламенты ✅ NEW
+│   └── ... (19 документов)
+│
+├── helpers/                      # 15 helpers ✨ NEW
+│   ├── README.md                # Обзор вспомогательных классов ✅
+│   ├── DateHelper.md            # Работа с датами и сменами ✅ NEW
+│   ├── DataHelper.md            # UUID, JSON, массивы ✅ NEW
+│   ├── SalaryHelper.md          # Расчёт зарплат и бонусов ✅ NEW
+│   ├── FormatHelper.md          # Форматирование чисел ✅ NEW
+│   └── UtilHelper.md            # Общие утилиты ✅ NEW
 │
 ├── database/                     # База данных ✨ NEW
 │   ├── README.md                # Обзор БД ✅
@@ -522,9 +605,9 @@ erp24/docs/
    - 9 практических примеров использования
    - Полное описание CronController (28 команд)
 
-3. **Модели и Records** (393 модели → 15 документов)
+3. **Модели и Records** (393 модели → 19 документов)
    - Обзор и классификация моделей
-   - Детальная документация: Admin, Users, Sales, Task, Files, Store, Timetable, UsersBonus, MarketplaceOrders, Products1c
+   - Детальная документация: Admin, AdminRating, Dashboard, Users, Sales, Task, Files, Store, Timetable, UsersBonus, MarketplaceOrders, Products1c, Lessons, Regulations
 
 4. **База данных**
    - Обзор структуры БД
@@ -543,14 +626,22 @@ erp24/docs/
 
 ### 📊 Новая статистика
 
-- **209 документов** Markdown (~5.1 MB)
+- **218 документов** Markdown (~5.3 MB)
 - **100% покрытие** контроллеров (161/161)
 - **100% покрытие** консольных команд (62/62)
 - **100% покрытие** сервисов (48/48)
-- **4% покрытие** моделей (15/393) - расширяется
+- **5% покрытие** моделей (19/393) - расширяется
+- **33% покрытие** helpers (5/15) - расширяется
 
 ## 📜 История создания документации
 
+**Версия 2.1 (2025-11-28):**
+- ✅ Добавлены 4 новые модели: AdminRating, Dashboard, Lessons, Regulations
+- ✅ Создана документация Helpers (5 файлов): DateHelper, DataHelper, SalaryHelper, FormatHelper, UtilHelper
+- ✅ Обновлены INDEX.md и SUMMARY.md
+- ✅ Улучшено покрытие моделей: 15 → 19 документов (5%)
+- ✅ Добавлено покрытие helpers: 5/15 (33%)
+
 **Версия 2.0 (2025-11-27):**
 - ✅ Добавлено 180+ новых документов
 - ✅ Документированы все 161 web-контроллер
@@ -574,20 +665,21 @@ erp24/docs/
 
 Документация системы ERP24 **активно поддерживается** и продолжает расширяться!
 
-**Охват версии 2.0:**
-- ✅ 209 документов Markdown (~5.1 MB)
+**Охват версии 2.1:**
+- ✅ 218 документов Markdown (~5.3 MB)
 - ✅ 12/12 бизнес-модулей (100%)
 - ✅ 161/161 web-контроллеров (100%)
 - ✅ 62/62 консольных команд (100%)
 - ✅ 48/48 сервисов (100%)
 - ✅ 9/18 API3 модулей (50%)
-- 🔄 15/393 моделей (4%)
+- 🔄 19/393 моделей (5%)
+- 🔄 5/15 helpers (33%)
 
 **Качество:**
 - ✅ Единый формат и шаблоны
 - ✅ Русский язык для всей документации
-- ✅ 35+ Mermaid диаграмм
-- ✅ 100+ примеров кода
+- ✅ 40+ Mermaid диаграмм
+- ✅ 110+ примеров кода
 - ✅ Связи и зависимости
 - ✅ CHANGELOG для отслеживания изменений
 
@@ -601,8 +693,8 @@ erp24/docs/
 
 ---
 
-**Версия:** 2.0
-**Последнее обновление:** 2025-11-27
+**Версия:** 2.1
+**Последнее обновление:** 2025-11-28
 **Статус:** ✅ Активно поддерживается
 
 **См. также:**
index 7fa88bd8f12e9bfa7a6b4725243e861a5896a77c..cfd40ef3581dfa7a7d9db1d257abd885269e6fc7 100644 (file)
@@ -4,6 +4,57 @@
 **Статус:** Production
 **База:** Yii2 Framework REST API
 
+## 🧠 Mindmap: Архитектура API3
+
+```mermaid
+mindmap
+  root((API3))
+    CRM и Лояльность 24 endpoints
+      BonusController 8шт
+        Начисление
+        Списание
+        История
+        Уровни
+      ClientController 14шт
+        Профили
+        События
+        Поиск
+      NotifiableController
+        Push
+        Уведомления
+    HR и Персонал 23 endpoints
+      EmployeeController 3шт
+        Данные сотрудников
+      AdminController 4шт
+        Администраторы
+      ClaimWorkerController 4шт
+        Заявки на смены
+      TimetablePlanController 5шт
+        План смен
+      TimetableFactController 6шт
+        Факт явок
+    Операции 10 endpoints
+      StoreController 7шт
+        Магазины
+        Продажи
+        Остатки
+      IncomeController
+        Приходы
+      ProductController
+        Каталог товаров
+    Аналитика 3 endpoints
+      ReportController
+        Продажи
+        Смены
+        KPI
+    Сервисы 10шт
+      BonusService 723 LOC
+      ClientService 571 LOC
+      ReportService 1504 LOC
+      StoreService
+      TimetableService
+```
+
 ---
 
 ## Быстрая навигация
index e86673cffbd0e137a28d6584f3adbb59b9e2af13..d4c1a578f5112489b07d0b4d2611b4532dbdee07 100644 (file)
@@ -1,5 +1,67 @@
 # Class: Admin
 
+## 🧠 Mindmap: Модель Admin
+
+```mermaid
+mindmap
+  root((Admin))
+    Идентификация
+      id / guid
+      login_user
+      pass_user
+      access_token
+      IdentityInterface
+    Персональные данные
+      name / name_full
+      mobile / email
+      birthdate
+      photo / avatarka
+      Паспортные данные
+        seriya / nomer
+        inn / snils
+    Должность
+      group_id
+      employee_position_id
+      work_status
+      work_rate
+      posit
+    Магазины
+      store_id
+      store_arr
+      store_arr_guid
+      dostup права
+    Зарплата
+      summa_oklad
+      summa_oklad_nalog
+      avans_percent
+      sale_percent
+      tabel_number
+    Иерархия
+      parent_admin_id
+      mentor_id
+      d_id
+      childAdmins
+    Связи
+      AdminGroup
+      CityStore
+      AdminPayroll
+      Timetable
+      Sales
+      Task
+    Методы
+      Аутентификация
+        findByUsername
+        validatePassword
+        findIdentityByAccessToken
+      Выборки
+        getAdmins
+        getNames
+        getOptionsForSelect
+      Хуки
+        afterSave
+        SalarySyncService
+```
+
 ## Назначение
 
 Основная модель сотрудника в системе ERP24. Содержит полную информацию о сотрудниках компании: личные данные, должность, права доступа, данные для расчета зарплаты, график работы и связи с другими сущностями системы.
@@ -519,6 +581,102 @@ $options = Admin::getOptionsForSelectAllGroupAndEach();
 
 ---
 
+### Хуки жизненного цикла
+
+#### afterSave($insert, $changedAttributes)
+
+**Описание:** Переопределённый метод жизненного цикла ActiveRecord, вызываемый после успешного сохранения модели. Обеспечивает автоматическую синхронизацию истории должностей и создание записей оплаты при изменении должности сотрудника.
+
+**Параметры:**
+
+| Параметр | Тип | Описание |
+|----------|-----|----------|
+| `$insert` | `bool` | `true` если это создание новой записи, `false` если обновление |
+| `$changedAttributes` | `array` | Массив изменённых атрибутов (ключ - имя атрибута, значение - старое значение) |
+
+**Возвращает:** `void`
+
+**Логика работы:**
+
+1. Вызывает родительский `parent::afterSave($insert, $changedAttributes)`
+2. Проверяет, изменилось ли поле `employee_position_id` (или это новая запись)
+3. **Гибридный подход - ведение истории должностей:**
+   - Если должность изменилась и новая должность установлена:
+     - Закрывает предыдущую запись в `EmployeePositionStatus` (устанавливает `closed_at`)
+     - Создаёт новую запись истории должности
+4. **Синхронизация зарплаты:**
+   - Если должность установлена/изменена и сотрудник не уволен:
+     - Вызывает `SalarySyncService::createPaymentFromPosition()` для создания записи оплаты
+
+**Вызовы сторонних методов:**
+
+| Метод | Класс | Описание |
+|-------|-------|----------|
+| `parent::afterSave()` | `ActiveRecord` | Вызов родительского метода |
+| `array_key_exists()` | PHP | Проверка наличия ключа в массиве изменений |
+| `EmployeePositionStatus::updateAll()` | `EmployeePositionStatus` | Массовое обновление (закрытие старых записей) |
+| `new EmployeePositionStatus()` | `EmployeePositionStatus` | Создание новой записи истории должности |
+| `->save(false)` | `ActiveRecord` | Сохранение без валидации |
+| `new SalarySyncService()` | `SalarySyncService` | Создание сервиса синхронизации зарплаты |
+| `->createPaymentFromPosition()` | `SalarySyncService` | Создание записи EmployeePayment |
+| `Yii::info()` | `Yii` | Логирование успешных операций |
+| `Yii::error()` | `Yii` | Логирование ошибок |
+
+**Триггеры выполнения:**
+
+| Событие | Действие |
+|---------|----------|
+| Создание сотрудника с должностью | Создание истории + запись оплаты |
+| Изменение должности | Закрытие старой истории + создание новой + запись оплаты |
+| Изменение без должности | Ничего не происходит |
+| Увольнение сотрудника | Запись оплаты НЕ создаётся |
+
+**Пример:**
+
+```php
+// При изменении должности автоматически:
+$admin = Admin::findOne($id);
+$admin->employee_position_id = 5; // Новая должность "Старший флорист"
+$admin->save();
+// -> EmployeePositionStatus с предыдущей должностью закрывается
+// -> Создаётся новая запись EmployeePositionStatus
+// -> Создаётся EmployeePayment с окладом для позиции 5
+
+// Логи:
+// [grade-sync] История грейда обновлена для admin_id=123, position_id=5
+// [salary-sync] Автоматически создана запись EmployeePayment для admin_id=123, position_id=5
+```
+
+**Диаграмма потока:**
+
+```mermaid
+flowchart TD
+    A[afterSave вызван] --> B{employee_position_id изменился?}
+    B -->|Нет| Z[Завершение]
+    B -->|Да| C{Новая должность установлена?}
+    C -->|Нет| Z
+    C -->|Да| D{Была старая должность?}
+    D -->|Да| E[Закрыть старую запись EmployeePositionStatus]
+    D -->|Нет| F[Создать новую запись EmployeePositionStatus]
+    E --> F
+    F --> G{Сотрудник не уволен?}
+    G -->|Нет| Z
+    G -->|Да| H[SalarySyncService::createPaymentFromPosition]
+    H --> I{Успех?}
+    I -->|Да| J[Логирование info]
+    I -->|Нет| K[Логирование error]
+    J --> Z
+    K --> Z
+```
+
+**Обработка ошибок:**
+
+- Ошибки при обновлении истории должностей логируются, но НЕ прерывают сохранение Admin
+- Ошибки синхронизации зарплаты также логируются без прерывания
+- Используется try-catch для изоляции побочных эффектов от основного сохранения
+
+---
+
 ### Служебные методы
 
 #### legacyFill()
index d8391b45bea861b947ba84e7ec82fc0e2479896b..0c1ca509a21064a730e87a1e45efa342636671ea 100644 (file)
@@ -2,6 +2,69 @@
 
 Данный раздел содержит подробную документацию всех бизнес-доменов (модулей) системы ERP24.
 
+## 🧠 Mindmap: Бизнес-модули ERP24
+
+```mermaid
+mindmap
+  root((Бизнес-модули))
+    HR и Персонал
+      Bonus
+        BonusService 1200+ LOC
+        50+ методов
+        Конвертация баллов
+        Telegram интеграция
+      Payroll
+        PayrollService
+        EAV паттерн
+        3 сервиса
+        10 моделей
+      Timetable
+        STI паттерн
+        17 actions
+        7 типов смен
+        Чекины
+      Rating
+        4 типа рейтингов
+        Автоматический расчёт
+        KPI
+      Grade
+        Уровни квалификации
+        Влияние на ЗП
+        Карьерный рост
+    Обучение
+      Lesson
+        Курсы и уроки
+        Тестирование
+        Сертификаты
+      Regulations
+        Регламенты
+        Опросники
+        Прохождение
+    Операции
+      Shipment
+        ShipmentService 3786 LOC
+        15 статусов
+        Интеграция 1С
+      WriteOffs
+        Списания
+        9 моделей
+        Причины
+    Коммуникации
+      Notifications
+        Push уведомления
+        TTL 31 день
+        AJAX API
+      KIK Feedback
+        Kanban workflow
+        7 статусов
+        16 actions
+    Аналитика
+      Dashboard
+        13 actions
+        Динамические виджеты
+        Цветовые индикаторы
+```
+
 ## Обзор модулей
 
 ### 1. [Bonus (Бонусная система)](./bonus/README.md)
index 8365690f3cce9c4fb2bfac07bc2f629ad59d2315..f45b2ad79f70bf644299298603678132c25187c0 100644 (file)
@@ -1,5 +1,67 @@
 # Модуль Bonus (Бонусная система)
 
+## 🧠 Mindmap: Модуль Bonus
+
+```mermaid
+mindmap
+  root((Bonus Module))
+    Контроллеры 5шт
+      BonusController
+        stat
+        vozvrat-stats
+        ajax-show-check
+        add-bonuses
+        sex
+      BonusLevelsController
+        Уровни бонусов
+      TeambonusController
+        Командные бонусы
+      UserBonusController
+        Бонусы клиентов
+      AdminPersonBonusesController
+        Личные бонусы админов
+    BonusService
+      50+ методов
+      1200+ строк
+      Начисление
+      Списание
+      Конвертация
+      Расчёт уровней
+    Модели 8шт
+      UsersBonus
+        История операций
+      BonusLevels
+        Уровни лояльности
+      UsersBonusLevels
+        Уровни клиентов
+      AdminPersonBonuses
+        Бонусы сотрудников
+      AdminBonusConversion
+        Конвертация в рубли
+      TeambonusSettings
+        Настройки команд
+    Actions 13шт
+      StatAction
+      VozvratStatsAction
+      AjaxShowCheckAction
+      AddBonuses
+      SexAction
+    Интеграции
+      Telegram Bot
+        Отправка баллов
+        Уведомления
+      1С
+        Синхронизация продаж
+        Бонусы за покупки
+    Формулы
+      Базовые баллы
+        1 балл = 1 рубль
+      Конвертация
+        коэффициент x баллы
+      Уровни
+        Bronze/Silver/Gold/Platinum
+```
+
 ## Описание
 
 Модуль Bonus отвечает за управление системой бонусов для клиентов и сотрудников. Включает начисление, списание, конвертацию бонусов, управление уровнями бонусов, командными бонусами и историей операций.
index c15f2804774736a7abc4113cc161e184c8c6dfe5..51f5123030a2d0755f8215dd575b4d0373ec895b 100644 (file)
@@ -1,5 +1,49 @@
 # BonusService
 
+## 🧠 Mindmap: BonusService
+
+```mermaid
+mindmap
+  root((BonusService))
+    Характеристики
+      1,199 LOC
+      42 публичных метода
+      Очень высокая сложность
+    Бонусы за продажи 8шт
+      getBonusClusterPercentSales кластер
+      getGameBonusPersonSalaryRelated сопутка
+      getGameBonusPersonSalaryPotted горшечка
+      getGameBonusPersonSalaryWrap упаковка
+      getBonusSaloonSale салон
+    Бонусы за конверсию 5шт
+      getGameBonusConversionShift смена
+      getGameBonusConversionStore магазин
+      getBonusByConvertionPercent премия
+      getGameBonusBonusCard карты
+    Бонусы за средний чек 2шт
+      getGamePersonBonusAvgCheck личный
+      getGameBonusAvgCheck магазин
+    Бонусы за списания 4шт
+      getBonusClusterPercentLoss кластер
+      getBonusPercentLoss премия
+      getGameBonusByPercentLoss игровой
+      getBonusForQuality качество
+    Бонусы кластеров 4шт
+      getBonusForDeltaClusterFot экономия ФОТ
+      getPercentDeMotivateYearToYear демотивация
+      getBonusClusterGame рейтинг
+    Коэффициенты 8шт
+      getMatrixBonusCoefficient матричный
+      getAuthorBonusCoefficient авторский
+      getCoefficientPremium премия
+    Зависимости
+      NormaSmenaService
+      SalesService
+      TeambonusSettings
+```
+
+---
+
 ## Назначение
 Центральный сервис для расчета всех типов бонусов и премий сотрудников. Содержит 42 метода расчета различных мотивационных выплат на основе KPI (продажи, конверсия, средний чек, процент списаний, и др.).
 
 | `roundCoefficientQuantity()` | Округление коэффициента количества |
 | Другие вспомогательные методы | - |
 
+## Детальное описание ключевых методов
+
+### getTeamBonus()
+
+**Назначение:** Расчёт командного бонуса магазина для конкретного сотрудника.
+
+**Сигнатура:**
+
+```php
+public function getTeamBonus(
+    $adminId,    // ID сотрудника
+    $storeId,    // ID магазина
+    $storeGuid,  // GUID магазина в 1С
+    $dateFrom,   // Дата начала периода
+    $dateTo      // Дата окончания периода
+): array
+```
+
+**Углублённое описание логики:**
+
+1. **Получение процента командного бонуса:**
+   - Вызов `getPercentTeamBonusInMonth($storeId, $dateFrom)`
+   - По умолчанию 0%, если не настроено в `TeambonusSettings`
+2. **Получение расписания магазина:**
+   - Запрос `Timetable::find()` с фильтрами:
+     - Период: `$dateFrom` - `$dateTo`
+     - Магазин: `$storeId`
+     - Тип слота: `TIMESLOT_WORK` (рабочая смена)
+     - `tabel = 0` (не табельные)
+3. **Формирование массива смен:**
+   - Группировка по `admin_id` и `date`
+   - Извлечение `salary_shift` (зарплата смены)
+4. **Расчёт ФОТ магазина:**
+   - Запрос окладов через `EmployeePayment::find()`
+   - Использование `EmployeePayment::getSalary()` для каждой даты
+   - `adminStoreFotStableSum` — сумма окладов
+5. **Получение переменной части ФОТ:**
+   - Запрос `AdminPayrollDays::find()` для получения премий
+   - `payroll_variable` — переменная часть
+   - `payroll_constant` — постоянная часть
+6. **Расчёт списаний:**
+   - Запрос `WriteOffs::find()` с фильтром `type = 'Брак'`
+   - Суммирование по полю `summ`
+7. **Получение продаж магазина:**
+   - Вызов `CabinetService::getSalesSaleSum()`
+8. **Расчёт командного фонда:**
+
+   ```text
+   salesByStorePart = salesByStore × (percentTeamBonusInMonth / 100)
+   primeFondStore = salesByStorePart - (adminStoreFotSum + writeOffsSum)
+   primeFondStoreOneShift = primeFondStore / shiftCountAll
+   personPrimeFondStore = primeFondStoreOneShift × personShiftCount
+   ```
+
+**Вызовы сторонних методов:**
+
+- `self::getPercentTeamBonusInMonth($storeId, $dateFrom)` — процент командного бонуса из настроек
+- `Timetable::find()` — получение расписания магазина
+- `EmployeePayment::find()` — получение окладов сотрудников
+- `EmployeePayment::getSalary($adminId, $date, $adminPayDayDict)` — оклад на конкретную дату
+- `AdminPayrollDays::find()` — получение данных о начислениях по дням
+- `WriteOffs::find()` — получение списаний магазина
+- `(new CabinetService())->getSalesSaleSum($dateFrom, $dateTo, $storeGuid)` — продажи магазина
+- `DateHelper::getDatesBetween($dateFrom, $dateTo)` — список дат в периоде
+- `ArrayHelper::getColumn($array, $column)` — извлечение колонки из массива
+- `ArrayHelper::getValue($array, $key)` — безопасное получение значения
+
+**Возвращает:** `array` с ключами:
+
+| Ключ | Тип | Описание |
+|------|-----|----------|
+| `adminStoreFotStableSum` | int | Сумма окладов магазина |
+| `adminStoreFotVariableSum` | int | Сумма переменной части ФОТ |
+| `adminStoreFotSum` | int | Общий ФОТ (оклады + премии) |
+| `writeOffsSum` | int | Сумма списаний |
+| `fotStoreAndWriteOff` | int | ФОТ + списания |
+| `salesByStore` | int | Продажи магазина |
+| `salesByStorePart` | int | Премиальный фонд (% от продаж) |
+| `primeFondStore` | int | Командный фонд |
+| `shiftCountAll` | int | Всего смен в магазине |
+| `primeFondStoreOneShift` | int | Бонус за одну смену |
+| `personShiftCount` | int | Смен сотрудника |
+| `personPrimeFondStore` | int | Персональный командный бонус |
+| `percentTeamBonusInMonth` | int | Процент командного бонуса |
+
+---
+
+### getSumConversionGameBonusToMoney()
+
+**Назначение:** Конвертация игровых баллов в денежный эквивалент.
+
+**Сигнатура:**
+
+```php
+public function getSumConversionGameBonusToMoney(
+    $adminSumGameBonus,      // Сумма игровых баллов
+    $yearSelect,             // Год
+    $monthWithZeroSelect     // Месяц с ведущим нулём
+): array
+```
+
+**Углублённое описание логики:**
+
+1. Устанавливает значения по умолчанию: `base = 1`, `cost = 10`
+2. Запрашивает настройки конвертации через `getAdminBonusConversion()`
+3. Если найдены настройки — использует их `base` и `cost`
+4. Рассчитывает: `money = (adminSumGameBonus / base) × cost`
+
+**Вызовы сторонних методов:**
+
+- `$this->getAdminBonusConversion($yearSelect, $monthWithZeroSelect)` — получение коэффициентов конвертации
+
+**Возвращает:** `array`:
+
+```php
+[
+    'money' => 1500,  // Денежный эквивалент
+    'base' => 1,      // База (делитель)
+    'cost' => 10,     // Стоимость одного балла
+]
+```
+
+---
+
+### getAdminBonusConversion()
+
+**Назначение:** Получение коэффициентов конвертации баллов в деньги из БД.
+
+**Сигнатура:**
+
+```php
+public function getAdminBonusConversion(
+    string $yearSelect,          // Год
+    string $monthWithZeroSelect  // Месяц с ведущим нулём
+): array
+```
+
+**Углублённое описание логики:**
+
+1. Формирует дату в формате `YYYY-MM`
+2. Запрашивает `AdminBonusConversion::find()` по дате
+3. Возвращает `base` и `cost` или значения по умолчанию
+
+**Вызовы сторонних методов:**
+
+- `AdminBonusConversion::find()` — поиск настроек конвертации
+- `->andWhere(['date' => $date])` — фильтр по дате
+- `->asArray()->one()` — получение одной записи как массив
+
+**Возвращает:** `array`:
+
+```php
+[
+    'base' => 1,   // База (по умолчанию 1)
+    'cost' => 10,  // Стоимость (по умолчанию 10₽)
+]
+```
+
+---
+
+### getBonusForQuality()
+
+**Назначение:** Расчёт бонуса за процент качества работы.
+
+**Сигнатура:**
+
+```php
+public function getBonusForQuality(float $percent): int
+```
+
+**Углублённое описание логики:**
+
+1. Определяет уровни бонусов:
+   - 80% → 3000₽
+   - 90% → 4000₽
+   - 100% → 5000₽
+2. Вызывает универсальный метод `getValueByLavelsEqualAndMore()`
+
+**Вызовы сторонних методов:**
+
+- `$this->getValueByLavelsEqualAndMore($percent, $levels)` — расчёт по уровням (больше-равно)
+
+**Пример:**
+
+```php
+$bonus = $bonusService->getBonusForQuality(92);
+// Результат: 4000 (т.к. 92% >= 90%)
+```
+
+---
+
+### getMatrixBonusCoefficient()
+
+**Назначение:** Получение коэффициента бонуса за матричные букеты в зависимости от даты.
+
+**Сигнатура:**
+
+```php
+public function getMatrixBonusCoefficient($rowDate): float
+```
+
+**Углублённое описание логики:**
+
+Система использует три исторических коэффициента:
+
+| Период | Коэффициент | Значение |
+|--------|-------------|----------|
+| До 16.11.2022 | oldMatrixBonusCoefficient | 0.025 (2.5%) |
+| 16.11.2022 - 30.11.2022 | newMatrixBonusCoefficient | 2/115 ≈ 0.0174 |
+| С 01.12.2022 | newTwoMatrixBonusCoefficient | 0.02 (2%) |
+
+**Пример:**
+
+```php
+$coef = $bonusService->getMatrixBonusCoefficient('2024-01-15');
+// Результат: 0.02
+
+$coef = $bonusService->getMatrixBonusCoefficient('2022-10-01');
+// Результат: 0.025
+```
+
+---
+
+### getPercentTeamBonusInMonth()
+
+**Назначение:** Получение процента командного бонуса для магазина на конкретный месяц.
+
+**Сигнатура:**
+
+```php
+public static function getPercentTeamBonusInMonth(
+    $storeId,   // ID магазина
+    $dateFrom   // Дата начала периода
+): int
+```
+
+**Углублённое описание логики:**
+
+1. Извлекает год и месяц из `$dateFrom`
+2. Запрашивает `TeambonusSettings::find()` по `store_id`, `year`, `month`
+3. Возвращает `procent` или 0 по умолчанию
+
+**Вызовы сторонних методов:**
+
+- `date("Y", strtotime($dateFrom))` — извлечение года
+- `date("n", strtotime($dateFrom))` — извлечение месяца (без нуля)
+- `TeambonusSettings::find()` — поиск настроек командного бонуса
+- `->select(['procent'])` — выборка только процента
+- `->scalar()` — получение скалярного значения
+
+**Пример:**
+
+```php
+$percent = BonusService::getPercentTeamBonusInMonth(15, '2024-03-01');
+// Результат: 20 (если настроено), иначе 0
+```
+
+---
+
+### getValueByLavelsEqualAndMore()
+
+**Назначение:** Универсальный метод расчёта значения по уровням (условие >=).
+
+**Сигнатура:**
+
+```php
+public function getValueByLavelsEqualAndMore(
+    float $value,   // Входное значение
+    array $levels   // Массив уровней [порог => бонус]
+): mixed
+```
+
+**Углублённое описание логики:**
+
+1. Сортирует уровни по ключам (`ksort`)
+2. Проходит по уровням от меньшего к большему
+3. Для каждого уровня проверяет: `$value >= $compareValue`
+4. Возвращает последний достигнутый бонус
+
+**Пример:**
+
+```php
+$levels = [
+    "70" => 1000,
+    "80" => 2000,
+    "90" => 3000,
+];
+
+$bonus = $bonusService->getValueByLavelsEqualAndMore(85, $levels);
+// Результат: 2000 (т.к. 85 >= 80, но 85 < 90)
+```
+
 ## Ключевые особенности
 
 ### 1. Система уровней (levels)
index 8f7ac446292267d06228f02f2f2539b03b4c8dda..c65406092268ef8611bd068bd465f4d7a415c651 100644 (file)
@@ -209,14 +209,59 @@ public function getData(
 ): array
 ```
 
-**Алгоритм:**
-1. Если `$dateFrom >= '2023-10-01'` → вызывает `getDataDynamic202310()`
-2. Иначе:
-   - Проверяет, есть ли зафиксированные данные в `AdminPayroll`
-   - Если да и период = полный месяц → использует `getDataStatic()`
-   - Иначе → использует `getDataDynamic()`
+**Углублённое описание логики:**
+
+1. Проверка даты начала периода:
+   - Если `$dateFrom >= '2023-10-01'` → вызывает `getDataDynamic202310()` (новая система мотивации)
+2. Для периодов до октября 2023:
+   - Проверяет, запрашивается ли полный месяц (`$dateFrom == $dateFromBeginMonth && $dateTo == $dateToEndMonth`)
+   - Ищет зафиксированные данные в `AdminPayroll` для указанного года/месяца
+   - Если найдены и дата фиксации > конца периода → использует `getDataStatic()`
+   - Флаг `$this->dynamicCalculate = true` отключает статические данные
+   - **ВАЖНО:** В текущей версии `$getDataStatic = false` жёстко, т.е. всегда используется динамический расчёт
+3. Возврат данных через выбранный метод расчёта
+
+**Вызовы сторонних методов:**
+
+- `AdminPayroll::find()` — создание ActiveQuery для поиска зафиксированных расчётов зарплаты
+- `AdminPayroll::select(['date_time'])` — выборка только поля даты фиксации
+- `AdminPayroll::andWhere(['year' => $yearSelect])` — фильтрация по году
+- `AdminPayroll::andWhere(['month' => $monthSelect])` — фильтрация по месяцу
+- `AdminPayroll::orderBy(['date_time' => SORT_DESC])` — сортировка по убыванию даты
+- `AdminPayroll::scalar()` — получение скалярного значения (дата последней фиксации)
+- `$this->getDataDynamic202310()` — вызов нового метода расчёта (с октября 2023)
+- `$this->getDataDynamic()` — вызов динамического расчёта для старых периодов
+- `$this->getDataStatic()` — вызов статического расчёта из зафиксированных данных
+
+**Поток данных:**
+
+```text
+Входные параметры (21 параметр)
+       ↓
+  Проверка даты
+       ↓
+┌──────┴──────┐
+│  >= 2023-10 │ → getDataDynamic202310() → массив данных
+└──────┬──────┘
+       ↓
+  < 2023-10
+       ↓
+  Проверка AdminPayroll
+       ↓
+┌──────┴──────┐
+│  Есть фикс. │ → getDataStatic() → массив данных (отключено)
+└──────┬──────┘
+       ↓
+  getDataDynamic() → массив данных
+```
 
-**Возвращает:** Массив с полными данными для отображения в личном кабинете.
+**Возвращает:** `array` — ассоциативный массив с полными данными для отображения в личном кабинете. Структура включает:
+- `salary` — данные о зарплате
+- `bonus` — бонусы и премии
+- `rating` — рейтинг сотрудника
+- `timetable` — расписание
+- `sales` — продажи
+- и другие секции в зависимости от версии расчёта
 
 **Используется в:**
 - `IndexAction::run()` (главная страница ЛК)
@@ -227,17 +272,69 @@ public function getData(
 
 #### 1.2. getDataStatic()
 
-**Назначение:** Получает зафиксированные данные из таблицы `admin_payroll` для завершённых месяцев.
+**Назначение:** Получает зафиксированные данные из таблицы `admin_payroll` для завершённых месяцев. Создаёт гибридный массив, комбинируя зафиксированные и динамические данные.
 
-**Ð\9fаÑ\80амеÑ\82Ñ\80Ñ\8b:** Ð\90налогиÑ\87нÑ\8b `getData()` + Ð´Ð¾Ð¿Ð¾Ð»Ð½Ð¸Ñ\82елÑ\8cнÑ\8bй Ð¿Ð°Ñ\80амеÑ\82Ñ\80 `$paramDynamic` (динамиÑ\87еÑ\81кие Ð´Ð°Ð½Ð½Ñ\8bе Ð´Ð»Ñ\8f Ñ\81Ñ\80авнениÑ\8f).
+**СигнаÑ\82Ñ\83Ñ\80а:**
 
-**Алгоритм:**
-1. Валидация сотрудника (наличие магазина, GUID из 1С, оклада)
-2. Извлечение зафиксированных данных из `AdminPayroll`
-3. Извлечение значений из `AdminPayrollValues`
-4. Формирование итогового массива
+```php
+public function getDataStatic(
+    $employeeId,              // ID сотрудника
+    $employeeSelect,          // Данные сотрудника (массив)
+    $employeeGroupId,         // ID группы сотрудника
+    $isAdministrator,         // Является ли администратором
+    $ratingId,                // ID записи рейтинга
+    $dateFrom,                // Дата начала периода
+    $dateTo,                  // Дата окончания периода
+    $controller,              // Контроллер для обработки ошибок
+    $winStoreIdDayChallenge,  // Выигравшие магазины в челлендже
+    $exportCityStore,         // Связка магазинов ERP → 1С
+    $exportAdmin,             // Связка сотрудников ERP → 1С
+    $yearSelect,              // Выбранный год
+    $monthSelect,             // Выбранный месяц
+    $monthWithZeroSelect,     // Месяц с ведущим нулём
+    $monthNameSelect,         // Название месяца
+    $dateFromBeginMonth,      // Начало месяца
+    $dateToEndMonth,          // Конец месяца
+    $employeePosition,        // Должности
+    $employeeAdminGroup,      // Группы администраторов
+    $cityStoreNames,          // Названия магазинов
+    $paramDynamic             // Динамические данные для сравнения
+): array
+```
 
-**Преимущества:** Быстрая загрузка (данные уже рассчитаны и сохранены).
+**Углублённое описание логики:**
+
+1. **Валидация сотрудника:**
+   - Проверка наличия `store_id` в `$employeeSelect`
+   - Проверка наличия GUID сотрудника в `$exportAdmin`
+   - Проверка связи магазина с 1С в `$exportCityStore`
+   - Проверка наличия оклада через `EmployeePayment::getMonthlySalary()`
+2. **Получение плана магазина:**
+   - Вызов `StorePlanService::getPlanMonthByStore()` для получения месячного плана
+   - Расчёт дневного плана: `planStoreByDay = planMonthByStore / дней_в_месяце`
+3. **Извлечение зафиксированных данных:**
+   - Поиск записи `AdminPayroll` по `admin_id`, `year`, `month`
+   - Получение значений через `AdminPayroll::getValues($payrollId)`
+4. **Формирование гибридного массива:**
+   - Сравнение ключей `$paramDynamic` и `$paramPayroll`
+   - При различии — запись в `$diffKeys` для отслеживания
+   - Замена динамических значений на зафиксированные в `$paramHybrid`
+
+**Вызовы сторонних методов:**
+
+- `CityStore::getCityStoreById($storeId)` — получение модели магазина по ID
+- `EmployeePayment::getMonthlySalary($employeeId, $dateFrom)` — получение оклада сотрудника на дату
+- `$this->storePlanService->getPlanMonthByStore($month, $year, $storeGuid)` — план магазина на месяц
+- `ArrayHelper::map($array, $from, $to)` — преобразование массива в ассоциативный
+- `ArrayHelper::getValue($array, $key)` — безопасное получение значения из массива
+- `cal_days_in_month(CAL_GREGORIAN, $month, $year)` — количество дней в месяце
+- `AdminPayroll::find()` — создание ActiveQuery для поиска записи зарплаты
+- `AdminPayroll::getValues($payrollId)` — получение всех значений из `admin_payroll_values`
+- `$this->outputCheckError($errorText, '', $controller)` — форматирование ошибки
+
+**Возвращает:** `array` — гибридный массив с данными ЛК (зафиксированные значения приоритетнее динамических)
+
+**Преимущества:** Быстрая загрузка, стабильные данные для закрытых периодов.
 
 **Недостатки:** Не отражает изменения в реальном времени.
 
@@ -245,36 +342,290 @@ public function getData(
 
 #### 1.3. getDataDynamic()
 
-**Назначение:** Рассчитывает данные в реальном времени на основе текущих продаж, расписания и других факторов.
+**Назначение:** Рассчитывает данные в реальном времени на основе текущих продаж, расписания и других факторов. Используется для периодов до октября 2023 года.
 
-**Ð\9fаÑ\80амеÑ\82Ñ\80Ñ\8b:** Ð\90налогиÑ\87нÑ\8b `getData()`.
+**СигнаÑ\82Ñ\83Ñ\80а:**
 
-**Алгоритм (упрощённо):**
-1. Валидация входных данных (сотрудник, магазин, GUID, оклад)
-2. Получение расписания сотрудника (`getTimetableData()`)
-3. Расчёт продаж по сменам
-4. Расчёт бонусов и премий
-5. Расчёт рейтинга
-6. Формирование финального массива с результатами
+```php
+public function getDataDynamic(
+    $employeeId,              // ID сотрудника
+    $employeeSelect,          // Данные сотрудника (массив)
+    $employeeGroupId,         // ID группы сотрудника
+    $isAdministrator,         // Является ли администратором
+    $ratingId,                // ID записи рейтинга
+    $dateFrom,                // Дата начала периода
+    $dateTo,                  // Дата окончания периода
+    $controller,              // Контроллер для обработки ошибок
+    $winStoreIdDayChallenge,  // Выигравшие магазины в челлендже
+    $exportCityStore,         // Связка магазинов ERP → 1С
+    $exportAdmin,             // Связка сотрудников ERP → 1С
+    $yearSelect,              // Выбранный год
+    $monthSelect,             // Выбранный месяц
+    $monthWithZeroSelect,     // Месяц с ведущим нулём
+    $monthNameSelect,         // Название месяца
+    $dateFromBeginMonth,      // Начало месяца
+    $dateToEndMonth,          // Конец месяца
+    $employeePosition,        // Должности
+    $employeeAdminGroup,      // Группы администраторов
+    $cityStoreNames,          // Названия магазинов
+    bool $calculatePersonalRating = true // Пересчитывать ли персональный рейтинг
+): array
+```
 
-**Сложность:** Очень высокая (~1,500 строк кода!).
+**Углублённое описание логики:**
+
+1. **Валидация входных данных:**
+   - Проверка `store_id`, `work_status` сотрудника
+   - Проверка GUID сотрудника в `$exportAdmin`
+   - Проверка связи магазина с 1С
+   - Проверка наличия оклада
+2. **Получение плана магазина:**
+   - Вызов `StorePlanService::getPlanMonthByStore()` для месячного плана
+   - Расчёт дневного плана: `planStoreByDay = planMonthByStore / дней_в_месяце`
+3. **Расчёт продаж:**
+   - `getSalesSaleSum()` — продажи магазина за период
+   - `getSalesSaleSum(..., true)` — все продажи включая возвраты
+   - `WriteOffs::getWriteOffByStore()` — списания магазина
+4. **Расчёт процента списания:**
+   - Для полных месяцев после 2023-03-31 используется `getCustomSumForLoss()`
+   - Иначе: `percentLoss = 100 * writeOff / sales`
+5. **Расчёт выполнения плана:**
+   - `planPercent = 100 * salesByStore / planMonthByStore`
+6. **Средний чек:**
+   - `getAvgSumCheck()` — среднее значение за период
+   - `getSumListAvgCheck()` — список по датам
+7. **Расписание:**
+   - `getTimetableData()` — смены сотрудника за период
+   - Подсчёт дневных/ночных смен
+8. **Расчёт бонусов и премий** (~500 строк логики)
+9. **Расчёт рейтинга** (если `$calculatePersonalRating = true`)
+
+**Вызовы сторонних методов:**
+
+- `CityStore::getCityStoreById($storeId)` — получение магазина
+- `EmployeePayment::getMonthlySalary($employeeId, $dateFrom)` — оклад сотрудника
+- `$this->storePlanService->getPlanMonthByStore()` — план магазина
+- `$this->getSalesSaleSum($dateFrom, $dateTo, $storeGuid, $includeReturns)` — сумма продаж
+- `WriteOffs::getWriteOffByStore($dateFrom, $dateTo, $storeGuid)` — списания
+- `$this->getCustomPercentLoss($storeId, $year, $month)` — кастомный процент списания
+- `$this->getCustomSumForLoss($storeId, $year, $month)` — данные для расчёта списания
+- `$this->getAvgSumCheck($storeId, $dateFrom, $dateTo)` — средний чек
+- `$this->getSumListAvgCheck($storeId, $dateFrom, $dateTo)` — список средних чеков
+- `$this->getTimetableData($adminId, $storeId, $dateFrom, $dateTo)` — расписание
+- `ArrayHelper::map()`, `ArrayHelper::getValue()` — работа с массивами
+- `cal_days_in_month()` — дней в месяце
+- `$this->outputCheckError()` — обработка ошибок
+
+**Поток данных:**
+
+```text
+Входные параметры (21 параметр)
+       ↓
+  Валидация сотрудника
+       ↓
+  Получение плана магазина (StorePlanService)
+       ↓
+  Расчёт продаж (getSalesSaleSum)
+       ↓
+  Расчёт списаний (WriteOffs)
+       ↓
+  Расчёт среднего чека (getAvgSumCheck)
+       ↓
+  Получение расписания (getTimetableData)
+       ↓
+  Расчёт бонусов и премий (~500 строк)
+       ↓
+  Расчёт рейтинга (RatingService)
+       ↓
+  Формирование результата → array
+```
 
-**Производительность:** Медленнее `getDataStatic()`, т.к. делает множество запросов и расчётов.
+**Возвращает:** `array` — полный набор данных для ЛК включая:
+
+- `salary` — данные о зарплате (оклад, рабочие дни)
+- `sales` — продажи (сумма, план, процент)
+- `bonus` — бонусы и премии
+- `rating` — рейтинг сотрудника
+- `timetable` — расписание (смены, часы)
+- `writeOff` — списания
+- `avgCheck` — средний чек
+- `planPercent` — процент выполнения плана
+
+**Сложность:** Очень высокая (~1,500 строк кода).
+
+**Производительность:** Медленнее `getDataStatic()`, т.к. делает множество запросов и расчётов в реальном времени.
 
 ---
 
 #### 1.4. getDataDynamic202310()
 
-**Ð\9dазнаÑ\87ение:** Ð\9dоваÑ\8f Ð²ÐµÑ\80Ñ\81иÑ\8f Ð´Ð¸Ð½Ð°Ð¼Ð¸Ñ\87еÑ\81кого Ñ\80аÑ\81Ñ\87Ñ\91Ñ\82а Ð´Ð»Ñ\8f Ð¿ÐµÑ\80иодов Ð½Ð°Ñ\87инаÑ\8f Ñ\81 Ð¾ÐºÑ\82Ñ\8fбÑ\80Ñ\8f 2023 Ð³Ð¾Ð´Ð°. Ð\92клÑ\8eÑ\87аеÑ\82 Ð½Ð¾Ð²Ñ\83Ñ\8e Ð»Ð¾Ð³Ð¸ÐºÑ\83 Ð¼Ð¾Ñ\82иваÑ\86ии Ð¸ Ð´ÐµÐ¼Ð¾Ñ\82иваÑ\86ии.
+**Ð\9dазнаÑ\87ение:** Ð\9dоваÑ\8f Ð²ÐµÑ\80Ñ\81иÑ\8f Ð´Ð¸Ð½Ð°Ð¼Ð¸Ñ\87еÑ\81кого Ñ\80аÑ\81Ñ\87Ñ\91Ñ\82а Ð´Ð°Ð½Ð½Ñ\8bÑ\85 Ð\9bÐ\9a Ð´Ð»Ñ\8f Ð¿ÐµÑ\80иодов Ð½Ð°Ñ\87инаÑ\8f Ñ\81 Ð¾ÐºÑ\82Ñ\8fбÑ\80Ñ\8f 2023 Ð³Ð¾Ð´Ð°. Ð\92клÑ\8eÑ\87аеÑ\82 Ð¾Ð±Ð½Ð¾Ð²Ð»Ñ\91ннÑ\83Ñ\8e Ð»Ð¾Ð³Ð¸ÐºÑ\83 Ð¼Ð¾Ñ\82иваÑ\86ии, Ð´ÐµÐ¼Ð¾Ñ\82иваÑ\86ии, ÐºÐ¾Ð¼Ð°Ð½Ð´Ð½Ñ\8bÑ\85 Ð±Ð¾Ð½Ñ\83Ñ\81ов Ð¸ Ð¿Ñ\80емий Ð¿Ð¾ Ñ\84окÑ\83Ñ\81-гÑ\80Ñ\83ппам.
 
-**Ð\9fаÑ\80амеÑ\82Ñ\80Ñ\8b:** Ð\90налогиÑ\87нÑ\8b `getDataDynamic()`.
+**СигнаÑ\82Ñ\83Ñ\80а:**
 
-**Размер:** ~1,650 строк кода (самый большой метод!).
+```php
+public function getDataDynamic202310(
+    $employeeId,              // ID сотрудника
+    $employeeSelect,          // Данные сотрудника (массив)
+    $employeeGroupId,         // ID группы сотрудника
+    $isAdministrator,         // Является ли администратором
+    $ratingId,                // ID записи рейтинга
+    $dateFrom,                // Дата начала периода
+    $dateTo,                  // Дата окончания периода
+    $controller,              // Контроллер для обработки ошибок
+    $winStoreIdDayChallenge,  // Выигравшие магазины в челлендже
+    $exportCityStore,         // Связка магазинов ERP → 1С
+    $exportAdmin,             // Связка сотрудников ERP → 1С
+    $yearSelect,              // Выбранный год
+    $monthSelect,             // Выбранный месяц
+    $monthWithZeroSelect,     // Месяц с ведущим нулём
+    $monthNameSelect,         // Название месяца
+    $dateFromBeginMonth,      // Начало месяца
+    $dateToEndMonth,          // Конец месяца
+    $employeePosition,        // Должности
+    $employeeAdminGroup,      // Группы администраторов
+    $cityStoreNames,          // Названия магазинов
+    bool $calculatePersonalRating = true // Пересчитывать ли персональный рейтинг
+): array
+```
+
+**Углублённое описание логики:**
+
+1. **Валидация сотрудника** (аналогично `getDataDynamic()`):
+   - Проверка `store_id`, `work_status`
+   - Проверка GUID в `$exportAdmin`
+   - Проверка связи магазина с 1С
+   - Проверка наличия оклада
+2. **Базовые расчёты:**
+   - План магазина через `StorePlanService`
+   - Продажи магазина (`getSalesSaleSum()`)
+   - Списания (`WriteOffs::getWriteOffByStore()`)
+   - Процент списания и выполнения плана
+3. **Расписание:**
+   - Основное: `getTimetableData()`
+   - Для администраторов: `getTimetableAdministratorData()`
+   - Смены в других магазинах: `getTimetableData(..., notInStore=true)`
+4. **Бонусная игра (SumGame):**
+   - `setTableValues()` — установка значений по сменам
+   - `getSumGameBonus()` — расчёт игровых бонусов
+   - Раздельный учёт для основного и других магазинов
+5. **Персональные надбавки** (`AdminPersonBonuses`):
+   - `bonuses` — персональная премия
+   - `overtime` — переработка
+   - `color_ruble_bonuses` — премия в цвето-рублях
+   - `retention` — персональный вычет
+   - `prepaid_expense` — аванс
+   - `counting` — подсчёт
+   - `vacation_day` — оплаченный отпуск
+   - `part_time_job_hours` — подработки в часах
+6. **Командный бонус:**
+   - `BonusService::getTeamBonus()` — расчёт командного бонуса
+   - `BonusService::getAdminTeamPayrollTable()` — таблица для администраторов
+7. **Премия за качество:**
+   - `QualityRating::getQualityRating()` — процент качества
+   - `BonusService::getBonusForQuality()` — премия за качество
+8. **Премии по фокус-группам:**
+   - `SalaryHelper::getSalariesByFocusGroup()` — зарплаты по группам товаров
+   - `getPremiumByFocusGroups()` — расчёт премий:
+     - услуги (`services`)
+     - сопутствующие товары (`related`)
+     - горшечные (`potted`)
+     - упаковка (`wrap`)
+     - салюты (`salut`)
+     - прочие товары (`other_items`)
+9. **Матричные букеты:**
+   - `getPremiumByMatrix()` — премия за матричные букеты
+   - Учёт продаж, производства, бонусов
+10. **Авторские букеты** (с апреля 2025):
+    - `getPremiumByAuthor()` — премия за авторские букеты
+11. **Праздничные смены:**
+    - `HolidayService::getHolidayVersionShow()` — показ праздничной версии
+    - Расчёт праздничных премий
+
+**Вызовы сторонних методов:**
+
+- `CityStore::getCityStoreById($storeId)` — получение магазина
+- `EmployeePayment::getMonthlySalary($employeeId, $dateFrom)` — оклад
+- `$this->storePlanService->getPlanMonthByStore()` — план магазина
+- `$this->getSalesSaleSum()` — сумма продаж
+- `WriteOffs::getWriteOffByStore()` — списания
+- `$this->getCustomPercentLoss()` / `$this->getCustomSumForLoss()` — расчёт списания
+- `$this->getAvgSumCheck()` / `$this->getSumListAvgCheck()` — средний чек
+- `$this->getTimetableData()` — расписание сотрудника
+- `$this->getTimetableAdministratorData()` — расписание администратора
+- `$this->setTableValues()` — установка значений по сменам
+- `$this->getSumGameBonus()` — бонусы игры
+- `AdminPersonBonuses::find()` — персональные надбавки
+- `$this->bonusService->getTeamBonus()` — командный бонус
+- `$this->bonusService->getAdminTeamPayrollTable()` — таблица админов
+- `QualityRating::getQualityRating()` — процент качества
+- `$this->bonusService->getBonusForQuality()` — премия за качество
+- `Products1c::getProductsFromClass($arrayTypes)` — товары по типам
+- `SalaryHelper::getSalariesByFocusGroup()` — зарплаты по фокус-группам
+- `$this->getPremiumByFocusGroups()` — премии по фокус-группам
+- `$this->getPremiumByMatrix()` — премия за матричные букеты
+- `$this->getPremiumByAuthor()` — премия за авторские букеты
+- `HolidayService::getHolidayVersionShow()` — праздничная версия
+- `Admin::getAdmins()` — список администраторов
+- `ArrayHelper::map()`, `ArrayHelper::getValue()`, `ArrayHelper::getColumn()` — работа с массивами
+
+**Поток данных:**
+
+```text
+Входные параметры (21 параметр)
+           ↓
+    Валидация сотрудника
+           ↓
+    Базовые расчёты (план, продажи, списания)
+           ↓
+    Расписание (основное + другие магазины)
+           ↓
+    Бонусная игра (SumGame)
+           ↓
+    Персональные надбавки (AdminPersonBonuses)
+           ↓
+    Командный бонус (BonusService)
+           ↓
+    Премия за качество (QualityRating)
+           ↓
+    Премии по фокус-группам (6 категорий)
+           ↓
+    Матричные букеты (getPremiumByMatrix)
+           ↓
+    Авторские букеты (getPremiumByAuthor)
+           ↓
+    Праздничные смены
+           ↓
+    Формирование результата → array
+```
 
-**Отличия от старой версии:**
-- Новая система расчёта премий
-- Изменённая логика демотивации
-- Обновлённые формулы для администраторов и флористов
+**Возвращает:** `array` — полный набор данных для ЛК включая:
+
+- `salary` — данные о зарплате (оклад, смены, часы)
+- `sales` — продажи (сумма, план, процент)
+- `bonus` — игровые бонусы (SumGame)
+- `teamBonus` — командный бонус
+- `quality` — качество и премия
+- `focusGroups` — премии по фокус-группам
+- `matrix` — матричные букеты
+- `author` — авторские букеты (с апреля 2025)
+- `personal` — персональные надбавки/вычеты
+- `timetable` — расписание
+- `holiday` — праздничные смены
+
+**Размер:** ~1,650 строк кода (самый большой метод в системе).
+
+**Отличия от `getDataDynamic()` (старая версия):**
+
+| Аспект | getDataDynamic | getDataDynamic202310 |
+|--------|----------------|----------------------|
+| Период | До октября 2023 | С октября 2023 |
+| Командный бонус | Нет | Да (`BonusService::getTeamBonus`) |
+| Фокус-группы | Базовые | Расширенные (6 категорий) |
+| Матричные букеты | Нет | Да |
+| Авторские букеты | Нет | Да (с апреля 2025) |
+| Качество | Базовое | С премией |
+| Размер кода | ~1,500 строк | ~1,650 строк |
 
 ---
 
index a472611d95d6ea89e6c20834a524e4a5ffb9c71d..6fbda33cd5671b845d33f302a25c7c1d79622333 100644 (file)
@@ -84,6 +84,23 @@ $data_store_visitors = [
 ]
 ```
 
+**Логика работы:**
+
+1. Создаёт пустой массив `$store_traffic`
+2. Итерирует по входному массиву `$data_store_visitors`
+3. Для каждой записи извлекает `date`, `counter`, `store_id`
+4. Если `counter` не пустой — суммирует значение по ключу `[date][store_id]`
+5. Возвращает агрегированный массив
+
+**Вызовы сторонних методов:**
+
+| Метод | Класс | Описание |
+|-------|-------|----------|
+| `ArrayHelper::getValue($row, 'date')` | `ArrayHelper` | Безопасное извлечение даты из записи |
+| `ArrayHelper::getValue($row, 'counter')` | `ArrayHelper` | Безопасное извлечение счётчика |
+| `ArrayHelper::getValue($row, 'store_id')` | `ArrayHelper` | Безопасное извлечение ID магазина |
+| `ArrayHelper::getValue($store_traffic, $date . '.' . $storeId)` | `ArrayHelper` | Проверка существования ключа |
+
 **Пример использования:**
 ```php
 $service = new DashboardService();
@@ -138,9 +155,41 @@ public function getSalesSumWithCityStoreId(
 ]
 ```
 
+**Параметры:**
+
+| Параметр | Тип | По умолчанию | Описание |
+|----------|-----|--------------|----------|
+| `$sales_sum` | `array` | - | Массив продаж `['store_id' => summ]` |
+| `$plan` | `array` | - | Массив планов `['store_id' => plan_value]` |
+| `$city_stores` | `array` | - | Массив названий магазинов `['store_id' => store_name]` |
+
+**Логика работы:**
+
+1. Инициализация пустых массивов `$sales` и `$sales_summ_all = 0`
+2. Итерация по массиву `$sales_sum` (store_id => sum)
+3. Для каждого магазина извлечение плана через `ArrayHelper::getValue($plan, $store_id)`
+4. Если план существует — извлечение названия магазина из `$city_stores`
+5. Расчёт процента выполнения плана: `($sum / $plan) * 100`
+6. Округление процента до целого числа через `round($percent)`
+7. Формирование записи с полями: store, store_id, summ, plan, percent
+8. Накопление общей суммы `$sales_summ_all`
+9. Сортировка результатов по убыванию процента через `uasort()`
+10. Возврат массива с ключами 'sales' и 'sales_summ_all'
+
+**Вызовы сторонних методов:**
+
+| Метод | Класс | Описание |
+|-------|-------|----------|
+| `ArrayHelper::getValue($plan, $store_id)` | `ArrayHelper` | Безопасное извлечение плана магазина по ID |
+| `ArrayHelper::getValue($city_stores, $store_id)` | `ArrayHelper` | Безопасное извлечение названия магазина |
+| `round($sum, 2)` | PHP | Округление суммы до 2 знаков после запятой |
+| `round($percent)` | PHP | Округление процента до целого числа |
+| `uasort($sales, $callback)` | PHP | Сортировка массива с сохранением ключей по пользовательской функции |
+
 **Особенности:**
 - Сортирует магазины по убыванию процента выполнения
 - Округляет суммы до 2 знаков после запятой
+- Магазины без плана пропускаются (не включаются в результат)
 
 **Пример:**
 ```php
@@ -274,6 +323,36 @@ public static function setData(
 | cumulative_total_stores | 30 | Нарастающий итог всего |
 | write_offs | 31 | Списания (брак) |
 
+**Вызовы сторонних методов:**
+
+| Метод | Класс | Описание |
+|-------|-------|----------|
+| `ExportImportService::getEntityByCityStore()` | `ExportImportService` | Получение маппинга entity_id ↔ export_val для магазинов |
+| `date($format, $time)` | PHP | Форматирование текущей даты/времени |
+| `DashboardFields::find()` | `DashboardFields` | Построение ActiveQuery для получения полей дашборда |
+| `->select(['id', 'name'])` | `ActiveQuery` | Выборка только полей id и name |
+| `->andWhere(['active' => 1])` | `ActiveQuery` | Фильтрация только активных полей |
+| `->andWhere(['<>', 'name', 'sales_summ'])` | `ActiveQuery` | Исключение поля sales_summ |
+| `->indexBy('id')` | `ActiveQuery` | Индексирование результата по id |
+| `->asArray()` | `ActiveQuery` | Возврат результата как массива (не объектов) |
+| `ArrayHelper::getColumn($fields_arr, 'name')` | `ArrayHelper` | Извлечение колонки name из массива полей |
+| `array_combine($fieldsIds, $fieldsNames)` | PHP | Создание ассоциативного массива id => name |
+| `array_flip($fieldsNames)` | PHP | Инверсия массива name => id |
+| `self::return_sales_stores($dateFrom, $dateTo)` | `DashboardService` | Расчёт базовых метрик продаж |
+| `self::return_sale_products_class(...)` | `DashboardService` | Расчёт продаж по классу товаров |
+| `self::return_incoming_traffic_stores(...)` | `DashboardService` | Получение трафика посетителей |
+| `self::insert_data_in_dashboard_sales(...)` | `DashboardService` | Сохранение агрегированных данных |
+| `Yii::$app->getDb()` | `Yii` | Получение подключения к базе данных |
+| `$connection->createCommand($sql, $params)` | `Connection` | Создание SQL-команды с параметрами |
+| `$command->queryAll()` | `Command` | Выполнение запроса и получение всех строк |
+| `DateHelper::getDateTimeStartDay($date, $format)` | `DateHelper` | Получение начала дня для даты |
+| `DateHelper::getDateTimeEndDay($date, $format)` | `DateHelper` | Получение конца дня для даты |
+| `Sales::OPERATION_SALE` | `Sales` | Константа операции "Продажа" |
+| `Sales::OPERATION_RETURN` | `Sales` | Константа операции "Возврат" |
+| `cal_days_in_month(CAL_GREGORIAN, $month, $year)` | PHP | Получение количества дней в месяце |
+| `explode($delimiter, $string)` | PHP | Разбиение строки даты на компоненты |
+| `round($value, $precision)` | PHP | Округление значений |
+
 **Пример использования:**
 ```php
 // Расчет дашборда за последние 7 дней
@@ -294,6 +373,18 @@ DashboardService::setData('2025-11-17', '2025-11-17', null, true);
 
 **Назначение:** Расчет базовых метрик продаж (офлайн, доставка, самовывоз).
 
+**Сигнатура:**
+```php
+public static function return_sales_stores($dateFrom, $dateTo): array
+```
+
+**Параметры:**
+
+| Параметр | Тип | По умолчанию | Описание |
+|----------|-----|--------------|----------|
+| `$dateFrom` | `string` | - | Начальная дата периода (YYYY-MM-DD) |
+| `$dateTo` | `string` | - | Конечная дата периода (YYYY-MM-DD) |
+
 **Возвращает:**
 ```php
 [
@@ -305,13 +396,48 @@ DashboardService::setData('2025-11-17', '2025-11-17', null, true);
 ]
 ```
 
+**Логика работы:**
+
+1. **Запрос возвратов офлайн:** SQL-запрос к таблице `sales` с `operation = 'Возврат'` и `(order_id = '' OR order_id = '0')`
+2. **Запрос продаж офлайн:** SQL-запрос к таблице `sales` с `operation = 'Продажа'` и `(order_id = '' OR order_id = '0')`
+3. **Расчёт метрик офлайн:** Вычитание возвратов из продаж для получения чистых показателей
+4. **Запрос возвратов доставки:** SQL-запрос с `operation = 'Возврат'` и `order_id > 0`
+5. **Запрос продаж доставки:** SQL-запрос с `operation = 'Продажа'` и `order_id > 0`
+6. **Расчёт метрик доставки:** Формирование delivery_sales_summ, delivery_checks_counter, delivery_sales_avg_check
+7. **Запрос возвратов самовывоза:** SQL-запрос с `order_id > 0` и `store_id != '4'`
+8. **Запрос продаж самовывоза:** SQL-запрос с `order_id > 0` и `store_id != '4'`
+9. **Расчёт метрик самовывоза:** Формирование smovivoz_sales_summ, smovivoz_checks_counter
+10. **Сохранение всех метрик:** Вызов `insert_data_in_dashboard_sales()` для каждого типа метрики
+
+**Вызовы сторонних методов:**
+
+| Метод | Класс | Описание |
+|-------|-------|----------|
+| `Yii::$app->getDb()` | `Yii` | Получение подключения к базе данных PostgreSQL |
+| `$connection->createCommand($sql, $params)` | `Connection` | Создание SQL-команды с prepared statements |
+| `$command->queryAll()` | `Command` | Выполнение запроса и получение всех результатов |
+| `DateHelper::getDateTimeStartDay($dateFrom, true)` | `DateHelper` | Форматирование начала дня для SQL |
+| `DateHelper::getDateTimeEndDay($dateTo, true)` | `DateHelper` | Форматирование конца дня для SQL |
+| `Sales::OPERATION_SALE` | `Sales` | Константа 'Продажа' |
+| `Sales::OPERATION_RETURN` | `Sales` | Константа 'Возврат' |
+| `round($value)` | PHP | Округление среднего чека до целого |
+| `self::insert_data_in_dashboard_sales(...)` | `DashboardService` | Сохранение метрик в dashboard_sales |
+
+**SQL-агрегации:**
+
+- `count(*)` — количество чеков
+- `sum(summ)` — общая сумма
+- `sum(CASE WHEN matrix >= 15 THEN summ ELSE 0 END)` — сумма матричных товаров
+- `sum(CASE WHEN phone IS NOT NULL THEN 1 ELSE 0 END)` — количество бонусных клиентов
+- `TO_CHAR(date,'YYYY-MM-DD')` — форматирование даты для группировки
+
 ---
 
 ### return_sale_products_class()
 
 **Назначение:** Расчет продаж по классу товаров (упаковка, услуги, горшечные).
 
-**Ð\9fаÑ\80амеÑ\82Ñ\80Ñ\8b:**
+**СигнаÑ\82Ñ\83Ñ\80а:**
 ```php
 public static function return_sale_products_class(
     $dateFrom,
@@ -319,14 +445,70 @@ public static function return_sale_products_class(
     $fieldName,    // 'wrap', 'services', 'potted'
     $fieldId,
     $store_id = ""
-)
+): array
+```
+
+**Параметры:**
+
+| Параметр | Тип | По умолчанию | Описание |
+|----------|-----|--------------|----------|
+| `$dateFrom` | `string` | - | Начальная дата периода |
+| `$dateTo` | `string` | - | Конечная дата периода |
+| `$fieldName` | `string` | - | Тип класса товаров: 'wrap', 'services', 'potted' |
+| `$fieldId` | `int` | - | ID поля дашборда для сохранения |
+| `$store_id` | `string` | `""` | Фильтр по магазину (пусто = все магазины) |
+
+**Возвращает:**
+```php
+[
+    'YYYY-MM-DD' => [
+        'store_id_1' => 15000.00,  // Чистая сумма (продажи - возвраты)
+        'store_id_2' => 22000.00,
+    ]
+]
 ```
 
-**Алгоритм:**
-1. Получение ID товаров класса из `products_class` WHERE `tip = $fieldName`
-2. JOIN `sales_items` ON `sales_items.id_1c IN (products_guids)`
-3. Суммирование для продаж и возвратов отдельно
-4. Вычитание возвратов из продаж
+**Логика работы:**
+
+1. **Получение ID товаров класса:** SQL-запрос к `products_1c` с JOIN `products_class` по `parent_id = category_id` и `tip = $fieldName`
+2. **Формирование WHERE-условия:** Создание SQL IN-clause с ID товаров класса
+3. **Запрос возвратов:** SQL с `operation = 'Возврат'`, JOIN `sales_items` по `check_id`, суммирование `sales_items.summa`
+4. **Запрос продаж:** SQL с `operation = 'Продажа'`, JOIN `sales_items` по `check_id`, суммирование `sales_items.summa`
+5. **Расчёт чистой суммы:** Для каждой даты и магазина: `$sum = $sales - $returns`
+6. **Сохранение результатов:** Вызов `setDashboardSalesRow()` для каждой записи
+
+**Вызовы сторонних методов:**
+
+| Метод | Класс | Описание |
+|-------|-------|----------|
+| `Yii::$app->getDb()` | `Yii` | Получение подключения к БД |
+| `$connection->createCommand($sql, $params)` | `Connection` | Создание SQL-команды |
+| `$command->queryAll()` | `Command` | Выполнение запроса |
+| `$command->getRawSql()` | `Command` | Получение сырого SQL для отладки |
+| `implode("','", $arrId)` | PHP | Формирование списка ID для IN-clause |
+| `array_key_exists($key, $array)` | PHP | Проверка существования ключа в массиве |
+| `explode(".", $date_d)` | PHP | Разбиение даты DD.MM.YYYY на компоненты |
+| `self::setDashboardSalesRow(...)` | `DashboardService` | Сохранение одной записи в dashboard_sales |
+
+**SQL-структура:**
+```sql
+-- Запрос 1: Получение ID товаров класса
+SELECT products_1c.id
+FROM products_1c
+RIGHT JOIN products_class ON products_1c.parent_id = products_class.category_id
+    AND products_class.tip = :type_name
+WHERE products_1c.id IS NOT NULL
+
+-- Запрос 2: Возвраты по классу товаров
+SELECT TO_CHAR(sales.date,'YYYY-MM-DD') as date_p, sales.store_id, sum(sales_items.summa) as summa
+FROM sales
+RIGHT JOIN sales_items ON (sales_items.check_id = sales.id AND sales_items.id_1c IN (...))
+WHERE sales.operation = 'Возврат' AND (order_id='' OR order_id='0')
+    AND sales.date >= ... AND sales.date <= ...
+GROUP BY date_p, sales.store_id
+
+-- Запрос 3: Продажи по классу товаров (аналогичный)
+```
 
 ---
 
@@ -334,20 +516,61 @@ public static function return_sale_products_class(
 
 **Назначение:** Получение трафика посетителей из таблицы `store_visitors`.
 
+**Сигнатура:**
 ```php
 public static function return_incoming_traffic_stores(
     $dateFrom,
     $dateTo,
     $field_name,
     $field_id
-)
+): array
+```
+
+**Параметры:**
+
+| Параметр | Тип | По умолчанию | Описание |
+|----------|-----|--------------|----------|
+| `$dateFrom` | `string` | - | Начальная дата периода |
+| `$dateTo` | `string` | - | Конечная дата периода |
+| `$field_name` | `string` | - | Название поля дашборда ('incoming_traffic') |
+| `$field_id` | `int` | - | ID поля дашборда (7) |
+
+**Возвращает:**
+```php
+[
+    'YYYY-MM-DD' => [
+        'store_id_1' => 150,  // Суммарный трафик за день
+        'store_id_2' => 200,
+    ]
+]
 ```
 
+**Логика работы:**
+
+1. **Получение подключения к БД:** `Yii::$app->getDb()`
+2. **Создание SQL-запроса:** Агрегация `sum(counter)` с группировкой по дате и магазину
+3. **Выполнение запроса:** `$command->queryAll()` возвращает все строки результата
+4. **Формирование массива:** Заполнение `$massivSQL[$row["dt"]][$row["store_id"]]`
+5. **Сохранение в БД:** Вызов `insert_data_in_dashboard_sales()` для персистентности
+6. **Возврат результата:** Массив с агрегированным трафиком
+
+**Вызовы сторонних методов:**
+
+| Метод | Класс | Описание |
+|-------|-------|----------|
+| `Yii::$app->getDb()` | `Yii` | Получение подключения к PostgreSQL |
+| `$connection->createCommand($sql, $params)` | `Connection` | Создание SQL-команды с параметрами |
+| `$command->queryAll()` | `Command` | Выполнение запроса и получение результатов |
+| `DateHelper::getDateTimeStartDay($dateFrom, true)` | `DateHelper` | Начало дня в формате для SQL |
+| `DateHelper::getDateTimeEndDay($dateTo, true)` | `DateHelper` | Конец дня в формате для SQL |
+| `self::insert_data_in_dashboard_sales(...)` | `DashboardService` | Сохранение данных в dashboard_sales |
+
 **SQL:**
 ```sql
 SELECT
     sum(counter) as counter,
     store_id,
+    date,
     TO_CHAR(date,'YYYY-MM-DD') as dt
 FROM
     store_visitors
@@ -356,7 +579,8 @@ WHERE
 AND
     date <= :date_to
 GROUP BY
-    date, store_id
+    date,
+    store_id
 ```
 
 ---
@@ -365,24 +589,60 @@ GROUP BY
 
 **Назначение:** Сохранение агрегированных данных в таблицу `dashboard_sales`.
 
+**Сигнатура:**
+
 ```php
 public static function insert_data_in_dashboard_sales(
     $massivSQL,
     $fieldName,
     $fieldId
-)
+): void
 ```
 
-**Процесс:**
-1. Перебор массива данных `['date' => ['store_id' => value]]`
-2. Конвертация даты из формата DD.MM.YYYY в YYYY-MM-DD
-3. Вызов `setDashboardSalesRow()` для каждой записи
+**Параметры:**
+
+| Параметр | Тип | По умолчанию | Описание |
+|----------|-----|--------------|----------|
+| `$massivSQL` | `array` | - | Массив данных `['date' => ['store_id' => value]]` |
+| `$fieldName` | `string` | - | Название поля дашборда |
+| `$fieldId` | `int` | - | ID поля дашборда |
+
+**Логика работы:**
+
+1. Внешний цикл по датам: `foreach ($massivSQL as $date_d => $array)`
+2. Внутренний цикл по магазинам: `foreach ($array as $storeId => $sum)`
+3. Конвертация даты из DD.MM.YYYY в YYYY-MM-DD через `explode(".", $date_d)`
+4. Проверка наличия `$storeId` (пропуск пустых)
+5. Вызов `setDashboardSalesRow()` для каждой записи
+
+**Вызовы сторонних методов:**
+
+| Метод | Класс | Описание |
+|-------|-------|----------|
+| `explode(".", $date_d)` | PHP | Разбиение даты DD.MM.YYYY на компоненты [DD, MM, YYYY] |
+| `self::setDashboardSalesRow(...)` | `DashboardService` | Вставка/обновление одной записи в dashboard_sales |
+
+**Пример входных данных:**
+
+```php
+$massivSQL = [
+    '17.11.2025' => [
+        '1' => 150000.00,  // store_id => value
+        '2' => 200000.00,
+    ],
+    '18.11.2025' => [
+        '1' => 180000.00,
+    ]
+];
+```
 
 ---
 
 ### setDashboardSalesRow()
 
-**Назначение:** Вставка или обновление одной строки в `dashboard_sales`.
+**Назначение:** Вставка или обновление одной строки в `dashboard_sales` (UPSERT паттерн).
+
+**Сигнатура:**
 
 ```php
 public static function setDashboardSalesRow(
@@ -391,14 +651,61 @@ public static function setDashboardSalesRow(
     $fieldName,
     $fieldId,
     $sum
-)
+): void
 ```
 
-**Алгоритм:**
-1. Поиск существующей записи по `date`, `store_id`, `field_name`, `field_id`
-2. Если не найдена — создание новой записи
-3. Установка `summ`, обновление `last_modified`
-4. Валидация и сохранение
+**Параметры:**
+
+| Параметр | Тип | По умолчанию | Описание |
+|----------|-----|--------------|----------|
+| `$date` | `string` | - | Дата в формате YYYY-MM-DD |
+| `$storeId` | `string\|int` | - | ID магазина |
+| `$fieldName` | `string` | - | Название поля дашборда |
+| `$fieldId` | `int` | - | ID поля дашборда |
+| `$sum` | `float` | - | Значение метрики |
+
+**Логика работы:**
+
+1. **Поиск существующей записи:** ActiveQuery к `DashboardSales` с условиями `date`, `store_id`, `field_name`, `field_id`
+2. **Создание новой записи:** Если запись не найдена — `new DashboardSales()` с установкой всех полей
+3. **Установка значения:** Вызов `->setSumm($sum)` для обновления метрики
+4. **Обновление времени:** Вызов `->setLastModified()` для фиксации времени изменения
+5. **Валидация:** Проверка корректности данных через `$dashboardSales->validate()`
+6. **Сохранение:** При успешной валидации — `$dashboardSales->save()`
+
+**Вызовы сторонних методов:**
+
+| Метод | Класс | Описание |
+|-------|-------|----------|
+| `DashboardSales::find()` | `DashboardSales` | Построение ActiveQuery для поиска записи |
+| `->andWhere(['date' => $date])` | `ActiveQuery` | Фильтрация по дате |
+| `->andWhere(['store_id' => $storeId])` | `ActiveQuery` | Фильтрация по магазину |
+| `->andWhere(['field_name' => $fieldName])` | `ActiveQuery` | Фильтрация по названию поля |
+| `->andWhere(['field_id' => $fieldId])` | `ActiveQuery` | Фильтрация по ID поля |
+| `->limit(1)` | `ActiveQuery` | Ограничение до 1 записи |
+| `->one()` | `ActiveQuery` | Выполнение запроса и получение записи |
+| `new DashboardSales()` | `DashboardSales` | Создание нового экземпляра модели |
+| `->setDate($date)` | `DashboardSales` | Установка даты (fluent interface) |
+| `->setStoreId($storeId)` | `DashboardSales` | Установка ID магазина |
+| `->setFieldName($fieldName)` | `DashboardSales` | Установка названия поля |
+| `->setFieldId($fieldId)` | `DashboardSales` | Установка ID поля |
+| `->setSumm($sum)` | `DashboardSales` | Установка значения метрики |
+| `->setLastModified()` | `DashboardSales` | Установка текущего времени изменения |
+| `->validate()` | `ActiveRecord` | Валидация модели по правилам |
+| `->save()` | `ActiveRecord` | Сохранение записи в БД |
+
+**Пример использования:**
+
+```php
+// Сохранение метрики продаж за 17.11.2025 для магазина 1
+DashboardService::setDashboardSalesRow(
+    '2025-11-17',  // date
+    '1',           // store_id
+    'sales_summ',  // field_name
+    14,            // field_id
+    150000.50      // sum
+);
+```
 
 ---
 
index 0fa16e6b93918dc543e7c5858661d6c7256fa333..101c1e6420edad5d96aca92adce9e77c2524463e 100644 (file)
@@ -228,6 +228,22 @@ const CODE_CALCULATION_OF_PREMIUM = 1025;        // Расчет премии
 5. Добавление дополнительных элементов (расчетные показатели)
 6. Сортировка по полю `order`
 
+**Вызовы сторонних методов:**
+
+| Метод | Класс | Описание |
+|-------|-------|----------|
+| `Motivation::find()` | `Motivation` | Создание ActiveQuery для поиска записи мотивации |
+| `->where(['store_id' => ..., 'year' => ..., 'month' => ...])` | `ActiveQuery` | Фильтрация по магазину, году и месяцу |
+| `->one()` | `ActiveQuery` | Получение единственной записи |
+| `MotivationValue::find()` | `MotivationValue` | Запрос значений показателей |
+| `->where(['motivation_id' => ...])` | `ActiveQuery` | Фильтр по ID записи мотивации |
+| `->indexBy('value_id')` | `ActiveQuery` | Индексация результатов по коду показателя |
+| `->all()` | `ActiveQuery` | Получение всех записей |
+| `MotivationCostsItem::find()` | `MotivationCostsItem` | Запрос справочника статей расходов |
+| `->orderBy(['order' => SORT_ASC])` | `ActiveQuery` | Сортировка по полю order |
+| `ArrayHelper::getValue($array, $key)` | `ArrayHelper` | Безопасное извлечение значения из массива |
+| `ArrayHelper::merge($arr1, $arr2)` | `ArrayHelper` | Объединение массивов |
+
 **Использование:**
 
 ```php
@@ -236,6 +252,7 @@ $tableData = MotivationService::getMotivationDataTableSort(1, 2024, 1);
 ```
 
 **Используется в:**
+
 - `motivation/IndexAction` — отображение таблицы мотивации
 - `motivation/index.php` (view) — рендеринг таблицы
 
@@ -278,6 +295,14 @@ switch ($motivationValue->value_type) {
 }
 ```
 
+**Вызовы сторонних методов:**
+
+| Метод | Класс | Описание |
+|-------|-------|----------|
+| `MotivationValue::find()` | `MotivationValue` | Создание ActiveQuery для поиска значения |
+| `->where(['motivation_id' => ..., 'motivation_group_id' => ..., 'value_id' => ...])` | `ActiveQuery` | Фильтрация по трём ключевым полям |
+| `->one()` | `ActiveQuery` | Получение единственной записи |
+
 **Использование:**
 
 ```php
@@ -343,6 +368,17 @@ switch ($valueType) {
 $motivationValue->save();
 ```
 
+**Вызовы сторонних методов:**
+
+| Метод | Класс | Описание |
+|-------|-------|----------|
+| `MotivationValueGroup::find()` | `MotivationValueGroup` | Поиск группы по алиасу |
+| `->where(['alias' => $groupAlias])` | `ActiveQuery` | Фильтрация по алиасу ('plan', 'fact', 'week1' и т.д.) |
+| `->one()` | `ActiveQuery` | Получение записи группы |
+| `MotivationValue::find()` | `MotivationValue` | Поиск существующего значения |
+| `new MotivationValue()` | `MotivationValue` | Создание нового экземпляра при отсутствии записи |
+| `->save()` | `ActiveRecord` | Сохранение записи в БД |
+
 **Использование:**
 
 ```php
@@ -412,7 +448,20 @@ self::saveOrUpdateMotivationValue($motivation->id, 'fact', self::CODE_ASSEMBLY_S
 self::saveOrUpdateMotivationValue($motivation->id, 'fact', self::CODE_DELIVERY_SERVICES, 'float', $deliveryServices);
 ```
 
+**Вызовы сторонних методов:**
+
+| Метод | Класс | Описание |
+|-------|-------|----------|
+| `date("Y-m-d H:i:s", strtotime(...))` | PHP | Форматирование диапазона дат месяца |
+| `Motivation::find()` | `Motivation` | Поиск записи мотивации |
+| `self::getSalesAndReturns($monthStart, $monthEnd, $store_id)` | `MotivationService` | Получение сумм продаж и возвратов |
+| `OrdersAmo::find()->sum('summ')` | `OrdersAmo` | Сумма онлайн продаж через AMO CRM |
+| `self::getSalesProductsDetails(...)` | `MotivationService` | Детализация продаж по типам услуг |
+| `array_sum(array_column($details, 'assembly_services'))` | PHP | Агрегация услуг сборки |
+| `self::saveOrUpdateMotivationValue(...)` | `MotivationService` | Сохранение рассчитанных значений |
+
 **Используется в:**
+
 - `motivation/IndexAction` — расчет при загрузке страницы
 - `task_32_motivation_fact.php` — cron задача
 
@@ -463,6 +512,19 @@ return [
 ];
 ```
 
+**Вызовы сторонних методов:**
+
+| Метод | Класс | Описание |
+|-------|-------|----------|
+| `Sales::find()` | `Sales` | Создание ActiveQuery для таблицы продаж |
+| `->where(['store_id' => $storeId])` | `ActiveQuery` | Фильтрация по магазину |
+| `->andWhere(['between', 'date', ...])` | `ActiveQuery` | Фильтрация по диапазону дат |
+| `->andWhere(['operation' => 'Продажа'])` | `ActiveQuery` | Фильтр операции продажи |
+| `->andWhere(['operation' => 'Возврат'])` | `ActiveQuery` | Фильтр операции возврата |
+| `->andWhere(['held' => 1])` | `ActiveQuery` | Только проведённые чеки |
+| `->andWhere(['!=', 'status', 'deleted'])` | `ActiveQuery` | Исключение удалённых |
+| `->sum('summ')` | `ActiveQuery` | Агрегация суммы |
+
 ---
 
 #### 6. `getSalesProductsDetails($startDate, $endDate, $storeId): array`
index 73e20d8dd7a0b343fc0ce483a3f8d49bbde8b154..fdb279a8c1739d2d744c942918878f4f1adbc311 100644 (file)
@@ -1,5 +1,39 @@
 # PayrollService
 
+## 🧠 Mindmap: PayrollService
+
+```mermaid
+mindmap
+  root((PayrollService))
+    Характеристики
+      72 LOC
+      2 публичных метода
+      Standalone класс
+    Назначение
+      Зарплатные расчеты
+      Проверка прав доступа
+      Валидация временных окон
+    Методы
+      __construct инициализация
+      checkPayrollUpdateAllowed валидация
+    Зависимости модели
+      Admin сотрудники
+      AdminGroup группы
+      AdminRating рейтинг
+      CityStore магазины
+      Timetable табель
+      Sales продажи
+      WriteOffs списания
+    Зависимости хелперы
+      DateHelper
+      HtmlHelper
+      SalaryHelper
+    Сервисы
+      CabinetService вывод ошибок
+```
+
+---
+
 ## Назначение
 Сервис для работы с зарплатными расчетами и проверкой прав доступа к обновлению данных по зарплате. Обеспечивает валидацию временных окон для редактирования зарплатных данных определенными группами пользователей.
 
@@ -62,12 +96,27 @@ use yii_app\records\WriteOffs;
 ### `__construct($config = [])`
 
 **Описание:**
-Конструктор класса. Инициализирует зависимость от CabinetService.
+Конструктор класса. Инициализирует зависимость от CabinetService через Dependency Injection via constructor.
 
 **Параметры:**
-- `$config` (array) - массив конфигурации (не используется в текущей реализации)
 
-**Возвращает:** void
+| Параметр | Тип | По умолчанию | Описание |
+|----------|-----|--------------|----------|
+| `$config` | `array` | `[]` | Массив конфигурации (не используется в текущей реализации) |
+
+**Возвращает:** `void`
+
+**Логика работы:**
+
+1. Принимает опциональный массив конфигурации
+2. Создаёт новый экземпляр `CabinetService`
+3. Присваивает его свойству `$this->cabinetService`
+
+**Вызовы сторонних методов:**
+
+| Метод | Класс | Описание |
+|-------|-------|----------|
+| `new CabinetService()` | `CabinetService` | Создаёт экземпляр сервиса для работы с кабинетом пользователя |
 
 **Пример:**
 ```php
@@ -79,14 +128,36 @@ $payrollService = new PayrollService();
 ### `outputCheckError(string $errorText, $buttonParams, $controller): array`
 
 **Описание:**
-Делегирует вывод ошибки проверки в CabinetService. Используется для формирования стандартизированного вывода ошибок с кнопками действий.
+Делегирует вывод ошибки проверки в CabinetService. Используется для формирования стандартизированного вывода ошибок с кнопками действий. Реализует паттерн Delegation.
 
 **Параметры:**
-- `$errorText` (string) - текст ошибки для отображения пользователю
-- `$buttonParams` (mixed) - параметры кнопки действия (URL, название)
-- `$controller` (mixed) - контроллер для обработки действий
 
-**Возвращает:** array - массив с данными ошибки
+| Параметр | Тип | Описание |
+|----------|-----|----------|
+| `$errorText` | `string` | Текст ошибки для отображения пользователю |
+| `$buttonParams` | `array\|null` | Параметры кнопки действия (ключи: `url`, `name`) |
+| `$controller` | `Controller` | Экземпляр контроллера для рендеринга |
+
+**Структура `$buttonParams`:**
+
+| Ключ | Тип | Описание |
+|------|-----|----------|
+| `url` | `string` | URL для перенаправления при нажатии кнопки |
+| `name` | `string` | Текст на кнопке |
+
+**Возвращает:** `array` — массив с данными ошибки для передачи в view
+
+**Логика работы:**
+
+1. Получает параметры ошибки
+2. Делегирует вызов методу `CabinetService::outputCheckError()`
+3. Возвращает результат без изменений
+
+**Вызовы сторонних методов:**
+
+| Метод | Класс | Описание |
+|-------|-------|----------|
+| `$this->cabinetService->outputCheckError($errorText, $buttonParams, $controller)` | `CabinetService` | Формирует стандартизированный массив ошибки с кнопкой действия для рендеринга в шаблоне |
 
 **Пример:**
 ```php
@@ -103,6 +174,13 @@ $result = $payrollService->outputCheckError(
     $buttonParams,
     $this
 );
+
+// Результат:
+// [
+//     'errorText' => 'Период редактирования закрыт',
+//     'buttonParams' => ['url' => '/payroll/index', 'name' => 'Вернуться к списку'],
+//     // ... другие данные для view
+// ]
 ```
 
 ---
@@ -112,19 +190,65 @@ $result = $payrollService->outputCheckError(
 **Описание:**
 Проверяет, разрешено ли обновление данных по зарплате для указанной группы пользователей и даты. Реализует бизнес-правила доступа к редактированию зарплатных данных.
 
+**Параметры:**
+
+| Параметр | Тип | Описание |
+|----------|-----|----------|
+| `$dateFrom` | `string` | Дата начала периода в формате 'Y-m-d' |
+| `$groupId` | `int` | ID группы пользователя из таблицы admin_group |
+
+**Возвращает:** `bool`
+
+- `true` - редактирование разрешено
+- `false` - редактирование запрещено
+
 **Бизнес-логика:**
+
 1. Доступ разрешен только для определенных групп (1, 8, 9, 51, 81)
 2. Можно редактировать текущий месяц до 16 числа следующего месяца 18:00
 3. Можно редактировать предыдущий месяц до 16 числа текущего месяца 18:00
 4. Более старые периоды редактировать нельзя
 
-**Параметры:**
-- `$dateFrom` (string) - дата начала периода в формате 'Y-m-d'
-- `$groupId` (int) - ID группы пользователя
+**Логика работы (детальная):**
+
+1. Устанавливает `$resultAllowed = true` по умолчанию
+2. Проверяет, входит ли `$groupId` в белый список `[1, 8, 9, 51, 81]`
+3. Если группа не в списке — сразу возвращает `false`
+4. Если группа разрешена:
+   - Вычисляет последний день выбранного месяца (`$dateEndSelectMonth`)
+   - Вычисляет первый день выбранного месяца (`$dateFromBeginMonth`)
+   - Получает текущую дату/время (`$dateCurrent`)
+   - Вычисляет первый день текущего месяца (`$dateFromBeginCurrentMonth`)
+   - Вычисляет первый день предыдущего месяца (`$dateFromBeginPreviousMonth`)
+   - Вычисляет дедлайн: 16-е число текущего месяца 18:00 (`$dateStop`)
+5. Проверяет три условия (t1, t2, t3):
+   - `t1`: выбранный месяц старше предыдущего месяца
+   - `t2`: выбранный месяц закончился до начала текущего месяца
+   - `t3`: текущая дата/время после дедлайна (16-е 18:00)
+6. Если `t1` истинно ИЛИ (`t2` И `t3`) истинны — возвращает `false`
+
+**Вызовы сторонних методов:**
+
+| Метод | Описание |
+|-------|----------|
+| `in_array($groupId, [1, 8, 9, 51, 81])` | Проверка принадлежности группы к белому списку |
+| `date("Y-m-t", strtotime($dateFrom))` | Последний день выбранного месяца |
+| `date("Y-m-01", strtotime($dateFrom))` | Первый день выбранного месяца |
+| `date("Y-m-d H:i:s", time())` | Текущая дата и время |
+| `date("Y-m-01", time())` | Первый день текущего месяца |
+| `date("Y-m-01", strtotime($date." -1 month"))` | Первый день предыдущего месяца |
+| `date("Y-m-16 18:00:00", strtotime($dateCurrent))` | Дедлайн редактирования |
+| `strtotime($dateString)` | Преобразование строки в Unix timestamp |
+| `time()` | Текущий Unix timestamp |
+
+**Формула проверки:**
 
-**Возвращает:** bool
-- `true` - редактирование разрешено
-- `false` - редактирование запрещено
+```
+ЗАПРЕЩЕНО если:
+  (выбранный_месяц < предыдущий_месяц)
+  ИЛИ
+  (выбранный_месяц < текущий_месяц И текущее_время > 16-е_18:00)
+```
 
 **Примеры:**
 
index 4b8ff6fee2c15acbb694eca3972d569ae1ee355d..8834a7eece69a397c9f65b54dd450c8452b3d3dd 100644 (file)
@@ -2,6 +2,70 @@
 
 Документация всех сервисов ERP24 системы.
 
+## 🧠 Mindmap: Архитектура сервисов
+
+```mermaid
+mindmap
+  root((Сервисы ERP24))
+    P0 Критические
+      CabinetService 8410 LOC
+        God Object
+        72 метода
+        Требует рефакторинга
+      SalesService 1962 LOC
+        Продажи
+        Возвраты
+        Чеки
+      AutoPlannogrammaService 3217 LOC
+        Автопланограмма
+        31 метод
+      MarketplaceService 2878 LOC
+        Flowwow
+        Yandex Market
+      DashboardService 1388 LOC
+        Метрики
+        Виджеты
+      UploadService 2349 LOC
+        Импорт 1С
+      MotivationService 2179 LOC
+        P&L мотивация
+        36 методов
+      PayrollService 872 LOC
+        Зарплата
+      TimetableService 1200 LOC
+        Смены
+        Табель
+    P1 Высокий
+      FileService
+      ReportService
+      BonusService_API3
+      ClientService_API3
+      WhatsAppService
+      TelegramService
+      StorePlanService
+    Категории
+      Зарплата 11шт
+        PayrollService
+        RatingService
+        BonusService
+      Персонал 8шт
+        TimetableService
+        ShiftManagementService
+      Продажи 10шт
+        ShipmentService
+        SalesService
+      Аналитика 7шт
+        ReportService
+        DashboardService
+      Интеграции 8шт
+        1С
+        AmoCRM
+        Telegram
+      Вспомогательные 7шт
+        CacheService
+        FileService
+```
+
 ## Обзор
 
 Данная директория содержит полную документацию сервисного слоя приложения ERP24. Сервисы инкапсулируют бизнес-логику и предоставляют переиспользуемый функционал для контроллеров, API и фоновых задач.
index 842668df5b7a98a434d326911401683e57bf0672..4cf0b72d283f32badd01e76df9ad91e015b709d9 100644 (file)
@@ -65,6 +65,37 @@ public function getData(
 6. Подсчет игровых баллов (конверсия, средний чек, и т.д.)
 7. Применение бонусов и штрафов
 
+**Вызовы сторонних методов:**
+
+| Метод | Класс | Описание |
+|-------|-------|----------|
+| `CityStore::getCityStoreById($storeId)` | `CityStore` | Получает данные магазина по ID для определения связки с 1С |
+| `StorePlanService::getPlanMonthByStore($month, $year, $guid)` | `StorePlanService` | Получает плановые показатели продаж магазина на месяц |
+| `EmployeePayment::getMonthlySalary($employeeId, $date)` | `EmployeePayment` | Возвращает оклад сотрудника на указанную дату |
+| `WriteOffs::getWriteOffByStore($dateFrom, $dateTo, $storeGuid)` | `WriteOffs` | Получает сумму списаний по магазину за период |
+| `$cabinetService->getSalesSaleSum($dateFrom, $dateTo, $storeGuid)` | `CabinetService` | Получает сумму продаж магазина за период |
+| `$cabinetService->getSumListAvgCheck($storeId, $dateFrom, $dateTo)` | `CabinetService` | Получает список средних чеков по датам |
+| `$cabinetService->getTimetableData($employeeId, $storeId, $dateFrom, $dateTo)` | `CabinetService` | Получает график смен сотрудника |
+| `$cabinetService->setAdminStore($timetable)` | `CabinetService` | Устанавливает данные администраторов для смен |
+| `$cabinetService->getGuidsByIds($ids, $exportAdmin)` | `CabinetService` | Преобразует ID сотрудников в GUID 1С |
+| `$cabinetService->getSumByAdmin($adminGuid, $dateFrom, $dateTo, $isAdmin)` | `CabinetService` | Получает сумму продаж по конкретному сотруднику |
+| `$cabinetService->rateStoreCategoryService->getRateInfo($storeId, $groupId, $dateFrom, $dateTo)` | `RateStoreCategoryService` | Получает нормативы смен по категории магазина |
+| `SalaryHelper::$normalCountShift` | `SalaryHelper` | Статическое свойство с нормой смен по группам |
+| `HtmlHelper::getWorkDays($month, $year)` | `HtmlHelper` | Возвращает количество рабочих дней в месяце |
+| `$cabinetService->getAdministratorOklad($planOrSales)` | `CabinetService` | Рассчитывает оклад администратора по плану или продажам |
+| `$cabinetService->getTimetableRate($timetable, ...)` | `CabinetService` | Рассчитывает показатели по каждой смене |
+| `$cabinetService->getSumListConversion($storeId, $dateFrom, $dateTo)` | `CabinetService` | Получает данные конверсии по магазину |
+| `$cabinetService->getConversionShift($dateFrom, $dateTo, $storeId)` | `CabinetService` | Получает конверсию по сменам (день/ночь) |
+| `$cabinetService->getSumListConversionBonusClients($storeId, $dateFrom, $dateTo)` | `CabinetService` | Получает процент использования бонусных карт |
+| `$cabinetService->getSumListStoreServicesPercent($storeId, $dateFrom, $dateTo)` | `CabinetService` | Получает процент услуг магазина |
+| `Timetable::find()` | `Timetable` | ActiveRecord запрос для получения смен администратора |
+| `$cabinetService->setGameValues($timetable, ...)` | `CabinetService` | Рассчитывает игровые баллы по сменам |
+| `$cabinetService->bonusService->getGameBonusByPercentLoss($percent)` | `BonusService` | Получает бонус за процент списаний |
+| `$cabinetService->getSumGameBonus($timetable, $possibleBonus)` | `CabinetService` | Суммирует игровые баллы за все смены |
+| `$cabinetService->getSumBonus($array1, $array2)` | `CabinetService` | Объединяет баллы из разных магазинов |
+| `ArrayHelper::map($array, $from, $to)` | `ArrayHelper` (Yii2) | Преобразует массив в ассоциативный |
+| `ArrayHelper::getValue($array, $key)` | `ArrayHelper` (Yii2) | Безопасное получение значения из массива |
+
 **Пример использования:**
 ```php
 $ratingService = new RatingService();
@@ -116,12 +147,31 @@ return $this->render('rating', ['data' => $result]);
 Проверяет, разрешен ли расчет рейтинга для указанного периода. Рейтинг можно рассчитать до 5 числа следующего месяца 18:00.
 
 **Параметры:**
-- `$dateFrom` (string) - дата периода
 
-**Возвращает:** bool
+| Параметр | Тип | Описание |
+|----------|-----|----------|
+| `$dateFrom` | `string` | Дата начала периода в формате 'Y-m-d' |
+
+**Возвращает:** `bool`
 - `true` - расчет разрешен
 - `false` - период закрыт
 
+**Логика работы:**
+
+1. Вычисляет последний день выбранного месяца (`$dateEndSelectMonth`)
+2. Получает текущую дату и время
+3. Определяет дату закрытия расчёта — 5-е число текущего месяца в 18:00
+4. Если выбранный месяц раньше текущего И текущая дата после дедлайна — расчёт запрещён
+
+**Вызовы сторонних методов:**
+
+| Метод | Описание |
+|-------|----------|
+| `date("Y-m-t", strtotime($dateFrom))` | Получает последний день месяца из даты |
+| `date("Y-m-d H:i:s", time())` | Текущая дата и время |
+| `date("Y-m-01", time())` | Первый день текущего месяца |
+| `date("Y-m-05 18:00:00", strtotime($dateCurrent))` | Дедлайн расчёта рейтинга |
+
 **Пример:**
 ```php
 $dateFrom = '2024-01-01';
@@ -138,14 +188,35 @@ if (RatingService::getAllowedCalculateRating($dateFrom)) {
 ### 3. `getAllowedCalculateAdminRating($dateFrom, $dateTo, $employeeId): bool` (static)
 
 **Описание:**
-Проверяет, разрешен ли расчет рейтинга для конкретного администратора с учетом запрещенных периодов.
+Проверяет, разрешен ли расчет рейтинга для конкретного администратора с учетом запрещенных периодов (например, декретный отпуск, увольнение).
 
 **Параметры:**
-- `$dateFrom` (string) - дата начала
-- `$dateTo` (string) - дата окончания
-- `$employeeId` (int) - ID сотрудника
 
-**Возвращает:** bool
+| Параметр | Тип | Описание |
+|----------|-----|----------|
+| `$dateFrom` | `string` | Дата начала периода |
+| `$dateTo` | `string` | Дата окончания периода |
+| `$employeeId` | `int` | ID сотрудника |
+
+**Возвращает:** `bool`
+
+- `true` - расчёт разрешён
+- `false` - сотрудник в запрещённом списке на указанный период
+
+**Логика работы:**
+
+1. Получает статический массив `SalesService::$forbiddenCalculateAdminRating` с запрещёнными периодами
+2. Проверяет, есть ли сотрудник в списке
+3. Если да — вызывает `SalesService::getAllowedStart()` для проверки пересечения дат
+4. Возвращает `false` если период пересекается с запрещённым
+
+**Вызовы сторонних методов:**
+
+| Метод | Класс | Описание |
+|-------|-------|----------|
+| `SalesService::$forbiddenCalculateAdminRating` | `SalesService` | Статический массив с запрещёнными периодами расчёта по сотрудникам |
+| `(new SalesService())->getAllowedStart($dateFrom, $dateTo, $forbidden)` | `SalesService` | Проверяет пересечение дат с запрещённым периодом |
+| `array_key_exists($employeeId, $array)` | PHP | Проверка наличия сотрудника в списке |
 
 **Пример:**
 ```php
@@ -163,15 +234,32 @@ if (!RatingService::getAllowedCalculateAdminRating($dateFrom, $dateTo, $employee
 ### 4. `getRatingId($employeeGroupId): int` (static)
 
 **Описание:**
-Определяет ID типа рейтинга на основе группы сотрудника.
+Определяет ID типа рейтинга на основе группы сотрудника. Используется для классификации рейтингов по категориям персонала.
 
 **Параметры:**
-- `$employeeGroupId` (int) - ID группы сотрудника
 
-**Возвращает:** int
-- `1` - рейтинг администраторов
-- `2` - рейтинг флористов (по умолчанию)
-- `4` - рейтинг подработчиков
+| Параметр | Тип | Описание |
+|----------|-----|----------|
+| `$employeeGroupId` | `int` | ID группы сотрудника из таблицы admin |
+
+**Возвращает:** `int`
+
+- `1` - рейтинг администраторов (group_id = 50)
+- `2` - рейтинг флористов (по умолчанию для всех остальных)
+- `4` - рейтинг подработчиков (group_id = 81)
+
+**Логика работы:**
+
+1. По умолчанию возвращает `2` (флористы)
+2. Если `$employeeGroupId == Admin::ADMINISTRATOR_GROUP_ID (50)` → возвращает `1`
+3. Если `$employeeGroupId == Admin::PART_TIME_WORKER_GROUP_ID (81)` → возвращает `4`
+
+**Вызовы сторонних методов:**
+
+| Константа | Класс | Значение | Описание |
+|-----------|-------|----------|----------|
+| `Admin::ADMINISTRATOR_GROUP_ID` | `Admin` | `50` | ID группы администраторов |
+| `Admin::PART_TIME_WORKER_GROUP_ID` | `Admin` | `81` | ID группы подработчиков |
 
 **Пример:**
 ```php
@@ -189,11 +277,38 @@ $ratingId = RatingService::getRatingId($groupId);
 ### 5. `calculateRating($yearSelect, $monthWithZeroSelect): void`
 
 **Описание:**
-Рассчитывает и обновляет рейтинговые позиции для всех сотрудников за указанный месяц. Сортирует по баллам и присваивает места.
+Рассчитывает и обновляет рейтинговые позиции для всех сотрудников за указанный месяц. Сортирует по баллам и присваивает места (1, 2, 3...).
 
 **Параметры:**
-- `$yearSelect` (int) - год
-- `$monthWithZeroSelect` (string) - месяц с нулем ('01', '02', etc.)
+
+| Параметр | Тип | Описание |
+|----------|-----|----------|
+| `$yearSelect` | `int` | Год расчёта (например, 2024) |
+| `$monthWithZeroSelect` | `string` | Месяц с ведущим нулём ('01', '02', ..., '12') |
+
+**Возвращает:** `void`
+
+**Логика работы:**
+
+1. Итерирует по типам рейтинга (1-4) через `range(1, 4)`
+2. Для типов 2 и 3 сортировка по `avg_value`, для остальных по `value`
+3. Выбирает все записи `AdminRating` за месяц с сортировкой по убыванию
+4. Присваивает места начиная с 1
+5. Сохраняет каждую запись после валидации
+
+**Вызовы сторонних методов:**
+
+| Метод | Класс | Описание |
+|-------|-------|----------|
+| `AdminRating::find()` | `AdminRating` | Создаёт ActiveQuery для поиска записей рейтинга |
+| `->andWhere(['rating_id' => $ratingId])` | `ActiveQuery` | Фильтрует по типу рейтинга |
+| `->andWhere(['date' => $yearSelect . '-' . $monthWithZeroSelect])` | `ActiveQuery` | Фильтрует по периоду |
+| `->orderBy([$valueKey => SORT_DESC])` | `ActiveQuery` | Сортирует по баллам по убыванию |
+| `->all()` | `ActiveQuery` | Выполняет запрос и возвращает массив моделей |
+| `$itemAdminRating->validate()` | `AdminRating` | Валидирует модель перед сохранением |
+| `$itemAdminRating->save()` | `AdminRating` | Сохраняет модель в БД |
+| `range(1, 4)` | PHP | Создаёт массив [1, 2, 3, 4] для итерации |
+| `in_array($ratingId, [2, 3])` | PHP | Проверяет тип рейтинга для выбора поля сортировки |
 
 **Пример:**
 ```php
@@ -222,15 +337,57 @@ public function setRatingValue(
 ```
 
 **Описание:**
-Сохраняет или обновляет запись рейтинга сотрудника в базе данных.
+Сохраняет или обновляет запись рейтинга сотрудника в базе данных. Реализует паттерн Upsert — создаёт новую запись или обновляет существующую.
 
 **Параметры:**
-- `$employeeId` (int) - ID сотрудника
-- `$adminSumGameBonusArray` (array) - массив с рассчитанными значениями
-- `$ratingId` (int) - тип рейтинга
-- `$yearSelect` (int) - год
-- `$monthSelect` (int) - месяц
-- `$monthWithZeroSelect` (string) - месяц с нулем
+
+| Параметр | Тип | Описание |
+|----------|-----|----------|
+| `$employeeId` | `int` | ID сотрудника (admin.id) |
+| `$adminSumGameBonusArray` | `array` | Массив с рассчитанными значениями рейтинга |
+| `$ratingId` | `int` | Тип рейтинга (1-администраторы, 2-флористы, 4-подработчики) |
+| `$yearSelect` | `int` | Год расчёта |
+| `$monthSelect` | `int` | Месяц (числовой формат: 1-12) |
+| `$monthWithZeroSelect` | `string` | Месяц с ведущим нулём ('01'-'12') |
+
+**Структура входного массива `$adminSumGameBonusArray`:**
+
+| Ключ | Тип | Описание |
+|------|-----|----------|
+| `adminSumGameBonusTotal` | `float` | Общая сумма игровых баллов |
+| `adminSumGameCountShiftTotal` | `int` | Количество отработанных смен |
+| `adminSumGameAvgSumTotal` | `float` | Средний балл за смену |
+| `administratorsCount` | `int` | Количество администраторов в смене |
+
+**Возвращает:** `void`
+
+**Логика работы:**
+
+1. Извлекает значения из массива через `ArrayHelper::getValue()`
+2. Проверяет существование записи через `AdminRating::find()->exists()`
+3. Если запись не найдена — создаёт новую модель `AdminRating`
+4. Если найдена — загружает существующую через `->one()`
+5. Устанавливает значения полей: `value`, `count_shift`, `administrators_count`, `avg_value`, `date_time`
+6. Валидирует и сохраняет модель
+
+**Вызовы сторонних методов:**
+
+| Метод | Класс | Описание |
+|-------|-------|----------|
+| `ArrayHelper::getValue($array, 'adminSumGameBonusTotal')` | `ArrayHelper` | Извлекает сумму баллов |
+| `ArrayHelper::getValue($array, 'adminSumGameCountShiftTotal')` | `ArrayHelper` | Извлекает количество смен |
+| `ArrayHelper::getValue($array, 'adminSumGameAvgSumTotal')` | `ArrayHelper` | Извлекает средний балл |
+| `ArrayHelper::getValue($array, 'administratorsCount')` | `ArrayHelper` | Извлекает число админов |
+| `AdminRating::find()` | `AdminRating` | Создаёт запрос для поиска записи |
+| `->andWhere(['admin_id' => $employeeId])` | `ActiveQuery` | Фильтр по сотруднику |
+| `->andWhere(['rating_id' => $ratingId])` | `ActiveQuery` | Фильтр по типу рейтинга |
+| `->andWhere(['date' => $year . '-' . $month])` | `ActiveQuery` | Фильтр по периоду |
+| `->exists()` | `ActiveQuery` | Проверяет наличие записи (boolean) |
+| `->one()` | `ActiveQuery` | Возвращает одну модель |
+| `new AdminRating()` | `AdminRating` | Создаёт новую модель рейтинга |
+| `$adminRating->validate()` | `AdminRating` | Валидирует модель |
+| `$adminRating->save()` | `AdminRating` | Сохраняет в БД |
+| `date("Y-m-d H:i:s")` | PHP | Текущая дата и время |
 
 **Пример:**
 ```php
@@ -267,9 +424,63 @@ public function getClusterRatingAdministrators(
 ```
 
 **Описание:**
-Получает рейтинги администраторов кластера (группы магазинов).
+Получает рейтинги администраторов кластера (группы магазинов) с подгрузкой связанных данных о сотрудниках.
+
+**Параметры:**
+
+| Параметр | Тип | Описание |
+|----------|-----|----------|
+| `$yearSelect` | `int` | Год расчёта |
+| `$monthWithZeroSelect` | `string` | Месяц с ведущим нулём |
+| `$adminIds` | `array` | Массив ID администраторов кластера |
 
-**Возвращает:** array - массив рейтингов с данными админов
+**Возвращает:** `array` — массив записей AdminRating с подгруженной связью `admin`
+
+**Структура возвращаемого массива:**
+
+```php
+[
+    [
+        'id' => 123,
+        'admin_id' => 101,
+        'rating_id' => 1,
+        'value' => 850,
+        'count_shift' => 20,
+        'avg_value' => 42.5,
+        'rating' => 1,
+        'date' => '2024-01',
+        'admin' => [
+            'id' => 101,
+            'name' => 'Иванов Иван',
+            'store_id' => 5,
+            // ...
+        ]
+    ],
+    // ...
+]
+```
+
+**Логика работы:**
+
+1. Создаёт запрос к `AdminRating` с фильтром `rating_id = 1` (только администраторы)
+2. Фильтрует по списку `admin_id`
+3. Фильтрует по периоду `date = 'YYYY-MM'`
+4. Подгружает связь `admin` через `->with('admin')`
+5. Сортирует по `value` по убыванию
+6. Возвращает массив через `->asArray()->all()`
+
+**Вызовы сторонних методов:**
+
+| Метод | Класс | Описание |
+|-------|-------|----------|
+| `AdminRating::find()` | `AdminRating` | Создаёт ActiveQuery для таблицы admin_rating |
+| `->andWhere(['rating_id' => 1])` | `ActiveQuery` | Фильтр по типу рейтинга (администраторы) |
+| `->andWhere(['admin_id' => $adminIds])` | `ActiveQuery` | Фильтр по списку ID сотрудников |
+| `->andWhere(['date' => $year . '-' . $month])` | `ActiveQuery` | Фильтр по периоду |
+| `->with('admin')` | `ActiveQuery` | Eager Loading связи с таблицей admin |
+| `->orderBy(['value' => SORT_DESC])` | `ActiveQuery` | Сортировка по баллам (лучшие сверху) |
+| `->asArray()` | `ActiveQuery` | Возврат в виде массива вместо объектов |
+| `->all()` | `ActiveQuery` | Выполнение запроса |
 
 **Пример:**
 ```php
@@ -292,10 +503,47 @@ foreach ($clusterRating as $rating) {
 
 ### 8. `getClusterAvgRatingAdministrators()` - Средний рейтинг кластера
 
+**Сигнатура:**
+```php
+public function getClusterAvgRatingAdministrators(
+    $yearSelect,
+    $monthWithZeroSelect,
+    $adminIds
+)
+```
+
 **Описание:**
-Рассчитывает средний рейтинг администраторов кластера.
+Рассчитывает средний рейтинг администраторов кластера через SQL-агрегацию AVG().
+
+**Параметры:**
 
-**Возвращает:** float - среднее значение баллов
+| Параметр | Тип | Описание |
+|----------|-----|----------|
+| `$yearSelect` | `int` | Год расчёта |
+| `$monthWithZeroSelect` | `string` | Месяц с ведущим нулём |
+| `$adminIds` | `array` | Массив ID администраторов кластера |
+
+**Возвращает:** `float` — среднее значение баллов, округлённое до 1 знака
+
+**Логика работы:**
+
+1. Формирует SQL-запрос с `SELECT AVG(value) as summ`
+2. Применяет те же фильтры, что и `getClusterRatingAdministrators()`
+3. Выполняет `->scalar()` для получения одного значения
+4. Округляет результат до 1 десятичного знака через `round(..., 1)`
+
+**Вызовы сторонних методов:**
+
+| Метод | Класс | Описание |
+|-------|-------|----------|
+| `AdminRating::find()` | `AdminRating` | Создаёт ActiveQuery |
+| `->select(['summ' => new Expression("AVG(value)")])` | `ActiveQuery` | SQL-агрегация среднего |
+| `new \yii\db\Expression("AVG(value)")` | `Expression` | Raw SQL выражение |
+| `->andWhere(['rating_id' => 1])` | `ActiveQuery` | Фильтр по типу |
+| `->andWhere(['admin_id' => $adminIds])` | `ActiveQuery` | Фильтр по ID |
+| `->andWhere(['date' => $date])` | `ActiveQuery` | Фильтр по дате |
+| `->scalar()` | `ActiveQuery` | Возвращает скалярное значение |
+| `round($value, 1)` | PHP | Округление до 1 знака |
 
 **Пример:**
 ```php
@@ -313,10 +561,74 @@ echo "Средний рейтинг кластера: " . $avgRating;
 
 ### 9. `getClusterGameSumValue()` - Сумма баллов кластера
 
+**Сигнатура:**
+```php
+public function getClusterGameSumValue(
+    $clusterAdmin,
+    $yearSelect,
+    $monthWithZeroSelect
+)
+```
+
 **Описание:**
-Рассчитывает суммарные игровые баллы кластера магазинов с учетом плана продаж.
+Рассчитывает суммарные игровые баллы кластера магазинов с учетом плана продаж. Учитывает только магазины, для которых разрешён расчёт плана.
+
+**Параметры:**
 
-**Возвращает:** array - массив с суммами и метриками
+| Параметр | Тип | Описание |
+|----------|-----|----------|
+| `$clusterAdmin` | `array` | Данные кластер-администратора с ключами `id` и `store_arr` |
+| `$yearSelect` | `int` | Год расчёта |
+| `$monthWithZeroSelect` | `string` | Месяц с ведущим нулём |
+
+**Структура входного массива `$clusterAdmin`:**
+
+| Ключ | Тип | Описание |
+|------|-----|----------|
+| `id` | `int` | ID кластер-администратора |
+| `store_arr` | `string` | Список ID магазинов через запятую ('1,2,3,4,5') |
+
+**Возвращает:** `array` — массив с агрегированными метриками кластера
+
+**Структура возвращаемого массива:**
+
+| Ключ | Тип | Описание |
+|------|-----|----------|
+| `adminSumGameBonusTotal` | `float` | Сумма баллов всех администраторов кластера |
+| `adminSumGameCountShiftTotal` | `int` | Всегда 1 (кластер как единица) |
+| `adminSumGameAvgSumTotal` | `float` | Средний балл = сумма / количество администраторов |
+| `administratorsCount` | `int` | Количество администраторов в кластере |
+
+**Логика работы:**
+
+1. Извлекает `store_arr` и `id` из входного массива
+2. Разбивает строку магазинов в массив через `explode(',')`
+3. Вычисляет период: первый и последний день месяца
+4. Фильтрует магазины через `CabinetService::getAllowedStorePlanCalculate()`
+5. Получает список администраторов через `Admin::getAdmins()` с фильтром по магазинам
+6. Суммирует баллы через `AdminRating::find()->select(['summ' => SUM(value)])`
+7. Вычисляет количество администраторов (с особой логикой для 2022-12 и после 2023)
+8. Рассчитывает средний балл = сумма / количество
+
+**Вызовы сторонних методов:**
+
+| Метод | Класс | Описание |
+|-------|-------|----------|
+| `ArrayHelper::getValue($clusterAdmin, 'store_arr')` | `ArrayHelper` | Извлекает строку магазинов |
+| `ArrayHelper::getValue($clusterAdmin, 'id')` | `ArrayHelper` | Извлекает ID кластер-админа |
+| `explode(',', $storeIdsString)` | PHP | Разбивает строку в массив |
+| `date("Y-m-t", strtotime($dateFrom))` | PHP | Последний день месяца |
+| `(new CabinetService())->getAllowedStorePlanCalculate($storeId, $from, $to)` | `CabinetService` | Проверяет разрешение расчёта плана для магазина |
+| `Admin::getAdmins('', $groupIds, 'ASC', $storeIds, '', Admin::NOT_IN_STORE_IDS)` | `Admin` | Получает список администраторов по магазинам |
+| `ArrayHelper::getColumn($admins, 'id')` | `ArrayHelper` | Извлекает колонку ID |
+| `array_values($ids)` | PHP | Переиндексирует массив |
+| `AdminRating::find()` | `AdminRating` | Создаёт запрос |
+| `->select(['summ' => new Expression("SUM(value)")])` | `ActiveQuery` | SQL-агрегация суммы |
+| `->andWhere(['date' => $date])` | `ActiveQuery` | Фильтр по периоду |
+| `->andWhere(['admin_id' => $adminIds])` | `ActiveQuery` | Фильтр по администраторам |
+| `->scalar()` | `ActiveQuery` | Скалярное значение |
+| `count($storeIds)` | PHP | Количество магазинов (для 2023+) |
+| `round($sum / $count, 0)` | PHP | Округление среднего до целого |
 
 **Пример:**
 ```php
index 501b5e5e2218786753a1df6b4d33d1f66ea9386e..f94af48d43b054b7a6dedf371ce9009d98adddd0 100644 (file)
@@ -1,5 +1,46 @@
 # Service: SalesService
 
+## 🧠 Mindmap: SalesService
+
+```mermaid
+mindmap
+  root((SalesService))
+    Характеристики
+      1,962 LOC
+      29 публичных методов
+      27 вызовов в системе
+      Высокая сложность
+    Основные задачи
+      Расчет сумм продаж
+      Статистика сотрудников
+      Бонусы за матрицу
+      Авторские букеты
+      Дашборды и отчеты
+    Ключевые методы
+      getSalesSum суммы продаж
+      getSalesReturn возвраты
+      getSalesByAdmin по сотруднику
+      getSalesByStore по магазину
+      getMatrixSales матрица
+      getAuthorSales авторские
+    Фильтры
+      По датам
+      По магазинам
+      По типам оплаты
+      По доставке
+    Зависимости модели
+      Sales продажи
+      SalesProducts товары
+      Admin сотрудники
+      AdminGroup группы
+      ProductsClass классы
+    Хелперы
+      DateHelper даты
+      ArrayHelper массивы
+```
+
+---
+
 ## Назначение
 
 SalesService — критический сервис для обработки продаж и возвратов в системе ERP24. Сервис отвечает за получение, расчет и анализ данных о продажах, возвратах, чеках и бонусных клиентах. Используется в личном кабинете сотрудников, дашбордах, отчетах и системе мотивации (начисление бонусов за матричные продажи, пиротехнику, авторские букеты).
@@ -125,7 +166,26 @@ $result = $service->getSalesSum(
 4. Если указан `$payType = '1'` — добавляются варианты: '1', '3', '1,3', '3,1' (наличные + бонусы)
 5. Группировка по `store_id_1c` и `operation`
 
+**Вызовы сторонних методов:**
+
+| Метод | Класс | Описание |
+|-------|-------|----------|
+| `Sales::find()` | `Sales` | Создание ActiveQuery для таблицы sales |
+| `->alias('s')` | `ActiveQuery` | Установка алиаса для таблицы |
+| `new \yii\db\Expression("SUM(s.summ - s.skidka)")` | `Expression` | SQL-выражение для суммирования |
+| `->joinWith('saleCheck')` | `ActiveQuery` | JOIN с таблицей sale_checks |
+| `DateHelper::getDateTimeStartDay($dateFrom, true)` | `DateHelper` | Начало дня для фильтра даты |
+| `DateHelper::getDateTimeEndDay($dateTo, true)` | `DateHelper` | Конец дня для фильтра даты |
+| `->andWhere(['>=', 's.date', $value])` | `ActiveQuery` | Условие >= для даты |
+| `->andWhere(['<=', 's.date', $value])` | `ActiveQuery` | Условие <= для даты |
+| `->leftJoin('create_checks cc', ...)` | `ActiveQuery` | LEFT JOIN с таблицей create_checks |
+| `->andFilterWhere(['or', ...])` | `ActiveQuery` | Условный фильтр с OR |
+| `->groupBy(['s.store_id_1c', 's.operation'])` | `ActiveQuery` | Группировка результатов |
+| `->createCommand()->getRawSql()` | `Command` | Получение сырого SQL для отладки |
+| `->asArray()->all()` | `ActiveQuery` | Выполнение запроса с возвратом массива |
+
 **Производительность:**
+
 - Сложность: O(n) по количеству чеков
 - Среднее время: 50-200 ms (зависит от периода)
 - Использует индексы по `date`, `store_id_1c`, `operation`
@@ -169,7 +229,22 @@ public function getSalesShiftSum(
 3. Для ночной: DateHelper::getDateTimeStartNightSmen() и EndNightShift()
 4. SQL с фильтром по часам (extract(HOUR from date))
 
+**Вызовы сторонних методов:**
+
+| Метод | Класс | Описание |
+|-------|-------|----------|
+| `in_array($shiftType, $shiftTypeValidate)` | PHP | Валидация типа смены |
+| `DateHelper::getDateTimeStartSmen($dateFrom)` | `DateHelper` | Начало дневной смены (08:00) |
+| `DateHelper::getDateTimeEndDaySmen($dateTo)` | `DateHelper` | Конец дневной смены (20:00) |
+| `DateHelper::getDateTimeStartNightSmen($dateFrom)` | `DateHelper` | Начало ночной смены (20:00) |
+| `DateHelper::getDateTimeEndNightShift($dateTo)` | `DateHelper` | Конец ночной смены (08:00 следующего дня) |
+| `Sales::find()` | `Sales` | Создание ActiveQuery |
+| `new \yii\db\Expression("SUM(summ - skidka)")` | `Expression` | SQL-выражение суммирования |
+| `->groupBy(['store_id_1c', 'operation'])` | `ActiveQuery` | Группировка по магазину и операции |
+| `->asArray()->all()` | `ActiveQuery` | Выполнение запроса |
+
 **Пример:**
+
 ```php
 // Продажи дневной смены
 $daySales = $service->getSalesShiftSum('2025-11-17', '2025-11-17', 'day');
@@ -210,7 +285,28 @@ public function getSalesByAdmin(
 - Если ночной — применяется сдвиг даты (ночь с 20:00 до 8:00 относится к предыдущему дню)
 - Для администраторов время не сдвигается
 
+**Вызовы сторонних методов:**
+
+| Метод | Класс | Описание |
+|-------|-------|----------|
+| `array_key_exists($adminGuid, $adminsGuids)` | PHP | Проверка существования сотрудника |
+| `ArrayHelper::getValue($adminsGuids, $adminGuid)` | `ArrayHelper` | Получение ID сотрудника по GUID |
+| `Admin::findOne($adminId)` | `Admin` | Поиск сотрудника по ID |
+| `AdminGroup::GROUP_DAY()` | `AdminGroup` | Массив ID дневных групп |
+| `in_array($groupId, AdminGroup::GROUP_DAY())` | PHP | Проверка дневной смены сотрудника |
+| `DateHelper::getDateTimeEndDay($dateTo, $flag, $adminId)` | `DateHelper` | Конец дня с учётом смены сотрудника |
+| `DateHelper::$hourStartDayShift` | `DateHelper` | Час начала дневной смены (8) |
+| `new DateTime($dateTimeEndDayPrepared)` | `DateTime` | Создание объекта даты для форматирования |
+| `->format('G')` | `DateTime` | Получение часа без ведущего нуля |
+| `Sales::find()` | `Sales` | Создание ActiveQuery |
+| `new \yii\db\Expression("TO_CHAR(date,'YYYY-MM-DD')")` | `Expression` | SQL-форматирование даты |
+| `new \yii\db\Expression("CASE WHEN ... END")` | `Expression` | SQL CASE для сдвига ночных продаж на предыдущий день |
+| `DateHelper::getDateTimeStartDay($dateFrom, $flag, $adminId)` | `DateHelper` | Начало дня с учётом смены |
+| `->andWhere(['seller_id' => $adminGuid])` | `ActiveQuery` | Фильтр по продавцу |
+| `->groupBy(['date', 'operation'])` | `ActiveQuery` | Группировка по дате и операции |
+
 **Пример:**
+
 ```php
 $adminGuid = 'guid-сотрудника';
 $sales = $service->getSalesByAdmin($adminGuid, '2025-11-01', '2025-11-17', false);
@@ -261,7 +357,38 @@ public function getMatrixSalesProducts(
 3. Иначе — только офлайн (order_id = '' OR order_id = '0')
 4. Продукты JOIN с `sales_products` для получения суммы продаж
 
+**Вызовы сторонних методов:**
+
+| Метод | Класс | Описание |
+|-------|-------|----------|
+| `Yii::$app->getDb()` | `Yii` | Получение подключения к БД |
+| `$connection->createCommand($sql, $params)` | `Connection` | Создание SQL-команды с параметрами |
+| `ProductsClass::find()` | `ProductsClass` | Запрос к таблице классов товаров |
+| `->where(['tip' => 'matrix'])` | `ActiveQuery` | Фильтр по типу 'matrix' |
+| `DateHelper::getDateTimeStartDay($dateFrom, ...)` | `DateHelper` | Начало периода с учётом смены |
+| `DateHelper::getDateTimeEndDay($dateTo, ...)` | `DateHelper` | Конец периода с учётом смены |
+| `$command->queryAll()` | `Command` | Выполнение SQL-запроса |
+| `array_key_exists($adminGuid, $adminsGuids)` | PHP | Проверка существования сотрудника |
+| `Admin::findOne($adminId)` | `Admin` | Поиск данных сотрудника |
+| `AdminGroup::GROUP_DAY()` | `AdminGroup` | Получение дневных групп |
+
+**SQL-структура запроса:**
+
+```sql
+SELECT SUM(sales_products.summa) as summ, sales.seller_id
+FROM sales
+RIGHT JOIN sales_products ON sales_products.check_id = sales.id
+RIGHT JOIN products_1c ON products_1c.id = sales_products.id_1c
+RIGHT JOIN products_class ON products_class.category_id = products_1c.parent_id
+    AND products_class.tip = 'matrix'
+WHERE sales.operation = 'Продажа'
+    AND sales.date >= :date_from AND sales.date <= :date_to
+    AND sales.seller_id = :admin_guid
+GROUP BY sales.seller_id
+```
+
 **Пример:**
+
 ```php
 $matrixSales = $service->getMatrixSalesProducts(
     'admin-guid',
@@ -308,7 +435,19 @@ public function getAuthorSalesProducts(
 - Товары отбираются по `products_class.tip = 'author'`
 - JOIN с `sales` по `seller_id` (продавец)
 
+**Вызовы сторонних методов:**
+
+| Метод | Класс | Описание |
+|-------|-------|----------|
+| `max($dateFrom, '2025-04-01')` | PHP | Корректировка даты начала действия функционала |
+| `Yii::$app->getDb()` | `Yii` | Получение подключения к БД |
+| `ProductsClass::find()->where(['tip' => 'author'])` | `ProductsClass` | Получение авторских классов |
+| `$connection->createCommand($sql, $params)` | `Connection` | SQL-команда с prepared statements |
+| `DateHelper::getDateTimeStartDay(...)` | `DateHelper` | Форматирование начала периода |
+| `DateHelper::getDateTimeEndDay(...)` | `DateHelper` | Форматирование конца периода |
+
 **Пример:**
+
 ```php
 $authorSales = $service->getAuthorSalesProducts('admin-guid', '2025-04-01', '2025-11-17', false);
 ```
@@ -345,7 +484,29 @@ public function getAuthorMakeProducts(
 - `getAuthorSalesProducts` — кто продал (sales.seller_id)
 - `getAuthorMakeProducts` — кто изготовил (sales_products.seller_id)
 
+**Вызовы сторонних методов:**
+
+| Метод | Класс | Описание |
+|-------|-------|----------|
+| `max($dateFrom, '2025-04-01')` | PHP | Корректировка даты начала |
+| `Yii::$app->getDb()` | `Yii` | Подключение к БД |
+| `ProductsClass::find()->where(['tip' => 'author'])` | `ProductsClass` | Авторские классы товаров |
+| `DateHelper::getDateTimeStartDay(...)` | `DateHelper` | Форматирование периода |
+| `DateHelper::getDateTimeEndDay(...)` | `DateHelper` | Форматирование периода |
+| `$command->queryAll()` | `Command` | Выполнение SQL |
+
+**SQL-отличие от getAuthorSalesProducts:**
+
+```sql
+-- getAuthorMakeProducts: JOIN по sales_products.seller_id (кто изготовил)
+RIGHT JOIN sales_products ON ... AND sales_products.seller_id = :admin_guid
+
+-- getAuthorSalesProducts: JOIN по sales.seller_id (кто продал)
+WHERE sales.seller_id = :admin_guid
+```
+
 **Пример:**
+
 ```php
 // Флорист создал букеты
 $madeProducts = $service->getAuthorMakeProducts('florist-guid', '2025-04-01', '2025-11-17', false);
@@ -390,7 +551,37 @@ public function getSalesCountSum(
 ]
 ```
 
+**Вызовы сторонних методов:**
+
+| Метод | Класс | Описание |
+|-------|-------|----------|
+| `Yii::$app->getDb()` | `Yii` | Получение подключения к PostgreSQL |
+| `$connection->createCommand($sql, $params)` | `Connection` | Создание SQL-команды |
+| `DateHelper::getDateTimeStartDay($dateFrom, true)` | `DateHelper` | Начало дня для параметра :date_from |
+| `DateHelper::getDateTimeEndDay($dateTo, true)` | `DateHelper` | Конец дня для параметра :date_to |
+| `Sales::OPERATION_SALE` | `Sales` | Константа 'Продажа' |
+| `Sales::OPERATION_RETURN` | `Sales` | Константа 'Возврат' |
+| `$command->queryAll()` | `Command` | Выполнение запроса и получение результатов |
+
+**SQL-запрос:**
+
+```sql
+SELECT
+    count(*) as cnt,
+    sum(case when phone is distinct from NULL THEN 1 ELSE 0 END) as bonus_clients_cnt,
+    sum(summ-skidka) as summ,
+    store_id,
+    to_char(date,'YYYY-MM-DD') as date_t
+FROM sales
+WHERE date >= :date_from AND date <= :date_to
+    AND operation = :operation
+    AND (order_id = '' OR order_id = '0')
+GROUP BY date_t, store_id
+ORDER BY date_t DESC, cnt DESC
+```
+
 **Пример:**
+
 ```php
 // Продажи за месяц
 $stats = $service->getSalesCountSum('2025-11-01', '2025-11-30', Sales::OPERATION_SALE);
index 74cf52120cfbdb3601598ef7e18f90c44c27b167..1931ad9551564a5e7317e675f340d9b4ae5ba769 100644 (file)
 
 Сервис для интеграции с Telegram Bot API. Обеспечивает отправку сообщений через Telegram-ботов, рассылку уведомлений, промо-акций, статистики и управление inline-кнопками для интерактивного взаимодействия с пользователями.
 
+## Пространство имён
+
+```php
+namespace yii_app\services;
+```
+
 ## Расположение
+
 - **Файл:** `/erp24/services/TelegramService.php`
 - **Размер:** 441 LOC
 - **Приоритет:** P1 (высокий)
 - **Интеграция:** Telegram Bot API, EDNA (WhatsApp)
 
-## Ключевые функции
+## Зависимости
+
+| Компонент | Тип | Описание |
+|-----------|-----|----------|
+| `GuzzleHttp\Client` | Library | HTTP-клиент для API запросов |
+| `Yii` | Framework | Логирование и доступ к приложению |
+| `yii\helpers\Json` | Helper | Кодирование/декодирование JSON |
+| `UsersTelegramMessage` | Model | Сохранение истории сообщений |
+
+## Константы
+
+| Константа | Значение | Описание |
+|-----------|----------|----------|
+| `TELEGRAM_BOT_DEV` | `8063257458:AAG...` | Токен бота для разработки |
+| `TELEGRAM_BOT_PROD` | `5456741805:AAG...` | Токен бота для production |
+| `TARGET_PROD_URL` | `erp.erp-flowers.ru` | URL production окружения |
+| `CHAT_CHANNEL_ID` | `-1001861631125` | ID канала для dev-уведомлений |
+| `CHAT_CHANNEL_ERP_ID` | `-1002338329426` | ID канала для prod-уведомлений |
+| `CHATBOT_SALT` | `ASIUdgb762g...` | Соль для генерации hash авторизации |
+
+## Методы
+
+### sendMessage()
+
+**Назначение:** Отправка сообщения через внешний API2 с поддержкой inline-кнопок.
+
+**Сигнатура:**
+
+```php
+public static function sendMessage(
+    $admin_id,       // ID администратора (сотрудника)
+    $message,        // Текст сообщения
+    $reply_markup = null // Разметка кнопок (опционально)
+): \Psr\Http\Message\ResponseInterface
+```
+
+**Углублённое описание логики:**
+
+1. Формирует URL запроса к `api2.bazacvetov24.ru/telegram/send-message`
+2. Добавляет параметры `admin_id` и `message` в query string
+3. Если передан `$reply_markup` — добавляет JSON-кодированную разметку кнопок
+4. Выполняет GET-запрос через GuzzleHttp\Client
+5. Возвращает объект Response
+
+**Вызовы сторонних методов:**
+
+- `new \GuzzleHttp\Client()` — создание HTTP-клиента
+- `$client->request('GET', $url)` — выполнение GET-запроса
+- `json_encode($reply_markup, JSON_UNESCAPED_UNICODE)` — кодирование разметки кнопок в JSON с сохранением Unicode
+
+**Пример:**
+
+```php
+// Простое сообщение
+TelegramService::sendMessage(123, 'Привет!');
+
+// Сообщение с кнопками
+$buttons = [
+    [['text' => 'Кнопка 1', 'callback_data' => 'btn1']]
+];
+TelegramService::sendMessage(123, 'Выберите:', $buttons);
+```
+
+---
+
+### isDevelopmentEnvironment()
+
+**Назначение:** Определение текущего окружения (dev/prod) по URL хоста.
+
+**Сигнатура:**
+
+```php
+public static function isDevelopmentEnvironment(
+    $urlString = null // URL для проверки (опционально)
+): bool
+```
+
+**Углублённое описание логики:**
+
+1. Получает текущий URL хоста через `Yii::$app->request->getHostInfo()`
+2. При ошибке (например, в консоли) устанавливает `$ip = 'console'`
+3. Если передан `$urlString` — использует его, иначе — полученный `$ip`
+4. Проверяет, содержит ли URL строку `TARGET_PROD_URL` (erp.erp-flowers.ru)
+5. Возвращает `true` если это НЕ production
+
+**Вызовы сторонних методов:**
+
+- `Yii::$app->request->getHostInfo()` — получение URL текущего хоста
+- `str_contains($currentUrl, self::TARGET_PROD_URL)` — проверка вхождения подстроки (PHP 8.0+)
+
+**Пример:**
+
+```php
+if (TelegramService::isDevelopmentEnvironment()) {
+    echo "Это dev окружение";
+}
+
+// Проверка конкретного URL
+$isDev = TelegramService::isDevelopmentEnvironment('https://dev.example.com');
+```
+
+---
+
+### isDevEnv()
+
+**Назначение:** Определение dev-окружения через переменную окружения `APP_ENV`.
+
+**Сигнатура:**
+
+```php
+public static function isDevEnv(): bool
+```
+
+**Углублённое описание логики:**
+
+1. Проверяет значение `getenv('APP_ENV')` на равенство `'development'`
+2. Альтернативно проверяет `$_ENV['APP_ENV']` (fallback на `'development'` если не установлено)
+3. Возвращает `true` если любое из условий выполнено
+
+**Вызовы сторонних методов:**
+
+- `getenv('APP_ENV')` — получение переменной окружения
+- `$_ENV['APP_ENV'] ?? 'development'` — null coalescing для fallback значения
+
+**Пример:**
+
+```php
+$botToken = TelegramService::isDevEnv()
+    ? TelegramService::TELEGRAM_BOT_DEV
+    : TelegramService::TELEGRAM_BOT_PROD;
+```
+
+---
+
+### sendErrorToTelegramMessage()
+
+**Назначение:** Отправка сообщения об ошибке в Telegram-канал (dev или prod).
+
+**Сигнатура:**
+
+```php
+public static function sendErrorToTelegramMessage(
+    $message,             // Текст ошибки (в MarkdownV2)
+    $disableNotification, // Отключить звук уведомления (bool)
+    $isDev               // Отправлять в dev-канал (bool)
+): void
+```
+
+**Углублённое описание логики:**
+
+1. Использует токен `TELEGRAM_BOT_DEV` для авторизации
+2. Определяет целевой канал:
+   - `$isDev = true` → `CHAT_CHANNEL_ID` (dev)
+   - `$isDev = false` → `CHAT_CHANNEL_ERP_ID` (prod)
+3. Формирует URL: `https://api.telegram.org/bot{token}/sendMessage`
+4. Отправляет POST-запрос через GuzzleHttp с параметрами:
+   - `chat_id` — ID канала
+   - `text` — текст сообщения
+   - `parse_mode` — `'MarkdownV2'`
+   - `disable_notification` — флаг тихого уведомления
+5. При ошибке логирует через `Yii::error()` в категорию `'telegram'`
+
+**Вызовы сторонних методов:**
+
+- `new Client()` — создание GuzzleHttp клиента
+- `$client->post($apiURL, ['json' => [...]])` — POST-запрос с JSON body
+- `Yii::error($message, 'telegram')` — логирование ошибки
+
+**Пример:**
+
+```php
+try {
+    // код...
+} catch (\Exception $e) {
+    $errorMsg = "*Ошибка:* `" . $e->getMessage() . "`";
+    TelegramService::sendErrorToTelegramMessage(
+        $errorMsg,
+        false,  // со звуком
+        true    // в dev канал
+    );
+}
+```
+
+---
+
+### sendTargetStatToTelegramMessage()
+
+**Назначение:** Отправка статистики целей руководителям (Алексею и Владимиру).
+
+**Сигнатура:**
+
+```php
+public static function sendTargetStatToTelegramMessage(
+    $message // Текст статистики
+): void
+```
+
+**Углублённое описание логики:**
+
+1. Использует токен `TELEGRAM_BOT_DEV`
+2. Определяет список получателей: `['337084327', '730432579']` (Алексей и Владимир)
+3. Экранирует текст через `escapeMarkdown($message)`
+4. Для каждого получателя:
+   - Генерирует кнопки через `getTgButtons($chatId)`
+   - Отправляет POST-запрос с:
+     - `chat_id` — ID получателя
+     - `text` — экранированный текст
+     - `parse_mode` — `'MarkdownV2'`
+     - `reply_markup` — JSON с inline-кнопками
+5. При ошибке логирует через `Yii::error()`
+
+**Вызовы сторонних методов:**
+
+- `self::escapeMarkdown($message)` — экранирование спецсимволов MarkdownV2
+- `self::getTgButtons($chatId)` — генерация массива inline-кнопок
+- `Json::encode(['inline_keyboard' => $buttons])` — кодирование разметки
+- `new Client()` — создание HTTP клиента
+- `$client->post($apiURL, ['json' => [...]])` — отправка запроса
+- `Yii::error()` — логирование ошибок
+
+**Поток данных:**
+
+```text
+$message
+    ↓
+escapeMarkdown() → экранированный текст
+    ↓
+foreach ($chats as $chatId)
+    ↓
+getTgButtons($chatId) → массив кнопок
+    ↓
+POST api.telegram.org/bot.../sendMessage
+    ↓
+Telegram отправляет сообщение получателю
+```
+
+---
+
+### sendPromoMessageToTelegramDocument()
+
+**Назначение:** Отправка промо-рассылки с 3 изображениями через cURL.
+
+**Сигнатура:**
+
+```php
+public static function sendPromoMessageToTelegramDocument(
+    $chatId // ID чата получателя
+): string // 'OK' или 'false'
+```
+
+**Углублённое описание логики:**
+
+1. Выбирает токен бота в зависимости от окружения (`isDevEnv()`)
+2. Формирует URL для MediaGroup и SendMessage
+3. Создаёт файлы изображений через `curl_file_create()`:
+   - `pic_1.jpg`, `pic_2.jpg`, `pic_3.jpg` из `/web/images/`
+4. **Первый запрос (MediaGroup):**
+   - Отправляет 3 фото через `sendMediaGroup` API
+   - Первое фото содержит caption с текстом акции
+5. **Второй запрос (Кнопки):**
+   - Отправляет сообщение "Выберите действие:" с сокращённым меню
+   - Использует `getTgShortButtons()` для генерации кнопок
+6. Возвращает `'OK'` при успехе
+
+**Вызовы сторонних методов:**
+
+- `self::isDevEnv()` — проверка окружения
+- `curl_file_create($path, $mimeType, $filename)` — создание файла для загрузки
+- `self::getTgShortButtons($chatId)` — генерация сокращённого меню
+- `json_encode($media)` — кодирование массива медиа
+- `curl_init()`, `curl_setopt()`, `curl_exec()`, `curl_close()` — работа с cURL
+- `Json::encode(['inline_keyboard' => $buttons])` — кодирование кнопок
+- `Yii::error()` — логирование ошибок
+
+**Структура MediaGroup:**
+
+```php
+[
+    ['type' => 'photo', 'media' => 'attach://pic_1.jpg', 'caption' => $text],
+    ['type' => 'photo', 'media' => 'attach://pic_2.jpg'],
+    ['type' => 'photo', 'media' => 'attach://pic_3.jpg'],
+]
+```
+
+---
+
+### sendPromo2MessageToTelegramDocument()
+
+**Назначение:** Отправка промо-рассылки с 3 изображениями через GuzzleHttp (альтернативная реализация).
+
+**Сигнатура:**
+
+```php
+public static function sendPromo2MessageToTelegramDocument(
+    $chatId // ID чата получателя
+): string // HTTP reason phrase, код ошибки или текст ошибки
+```
+
+**Углублённое описание логики:**
+
+1. Выбирает токен бота через `isDevEnv()`
+2. Формирует пути к изображениям (без `curl_file_create`)
+3. **Первый запрос (MediaGroup через GuzzleHttp):**
+   - Использует `multipart` формат для загрузки файлов
+   - Открывает файлы через `fopen($path, 'r')`
+   - Проверяет статус код ответа (200 = успех)
+4. **Второй запрос (Кнопки через JSON):**
+   - Отправляет JSON с текстом и кнопками
+   - Возвращает `$response->getReasonPhrase()` при успехе
+5. При ошибках возвращает код статуса или текст исключения
+
+**Вызовы сторонних методов:**
+
+- `self::isDevEnv()` — проверка окружения
+- `self::getTgShortButtons($chatId)` — генерация кнопок
+- `new Client()` — создание GuzzleHttp клиента
+- `fopen($path, 'r')` — открытие файла для чтения
+- `$client->post($url, ['multipart' => [...]])` — multipart запрос
+- `$client->post($url, ['json' => [...]])` — JSON запрос
+- `$response->getStatusCode()` — получение HTTP статуса
+- `$response->getReasonPhrase()` — получение текста статуса (OK, Created и т.д.)
+- `Yii::error()` — логирование
+
+**Отличия от sendPromoMessageToTelegramDocument():**
 
-**1. Отправка сообщений:**
-- Текстовые сообщения в чаты
-- Уведомления об ошибках в канал dev/prod
-- Промо-рассылки с 3 изображениями
-- Inline-кнопки с WebApp и URL
+| Аспект | sendPromoMessage... | sendPromo2Message... |
+|--------|---------------------|----------------------|
+| HTTP клиент | cURL | GuzzleHttp |
+| Загрузка файлов | curl_file_create | fopen + multipart |
+| Возврат | 'OK' / 'false' | ReasonPhrase / код / ошибка |
+| Обработка ошибок | Базовая | Детальная с кодами |
 
-**2. Определение окружения:**
-- Dev/Prod по URL и ENV
-- Автоматический выбор токена и канала
+---
 
-**3. Генерация UI:**
-- Полное меню (4 кнопки)
-- Сокращенное меню для промо (2 кнопки)
-- WebApp интеграция с hash-авторизацией
+### sendMessageToTelegramClient()
 
-**4. Форматирование:**
-- Экранирование MarkdownV2 для файлов
-- Экранирование для логов в БД
+**Назначение:** Отправка сообщения клиенту с полным меню кнопок.
 
-## Основные методы (9 методов)
+**Сигнатура:**
 
-### Отправка сообщений
-1. `sendMessage()` — Через API2 с inline-кнопками
-2. `sendMessageToTelegramClient()` — С полным меню
-3. `sendErrorToTelegramMessage()` — Ошибка в канал
-4. `sendTargetStatToTelegramMessage()` — Статистика руководителям
+```php
+public static function sendMessageToTelegramClient(
+    $chatId,         // ID чата клиента
+    $message,        // Текст сообщения
+    $isDev = true    // Использовать dev-бота (по умолчанию)
+): string|int // ReasonPhrase при успехе, код ошибки или текст исключения
+```
 
-### Промо-рассылки
-5. `sendPromoMessageToTelegramDocument()` — 3 фото (cURL)
-6. `sendPromo2MessageToTelegramDocument()` — 3 фото (GuzzleHttp)
+**Углублённое описание логики:**
 
-### Генерация UI
-7. `getTgButtons()` — Полное меню (4 кнопки)
-8. `getTgShortButtons()` — Сокращенное меню (2 кнопки)
+1. Выбирает токен бота: `TELEGRAM_BOT_DEV` если `$isDev`, иначе `TELEGRAM_BOT_PROD`
+2. Экранирует текст через `escapeMarkdown($message)`
+3. Генерирует полное меню через `getTgButtons($chatId)`
+4. Отправляет POST-запрос с:
+   - `chat_id` — ID получателя
+   - `text` — экранированный текст
+   - `parse_mode` — `'MarkdownV2'`
+   - `reply_markup` — JSON с 4 кнопками
+5. Проверяет статус ответа:
+   - 200 → возвращает `getReasonPhrase()` ('OK')
+   - Иначе → логирует и возвращает код
+6. При исключении → логирует и возвращает текст ошибки
 
-### Форматирование текста
-9. `escapeMarkdown()` — Для текстов из файлов
-10. `escapeMarkdownLog()` — Для логов из БД
+**Вызовы сторонних методов:**
 
-### Вспомогательные
-11. `getHashTG()` — Hash для WebApp авторизации
-12. `saveSentMessageToDB()` — Сохранение в логи
-13. `getDateTwoWeekStartEnd()` (InfoTableService) — Даты недель
-14. `isDevelopmentEnvironment()` — Проверка dev
-15. `isDevEnv()` — Проверка через ENV
+- `self::escapeMarkdown($message)` — экранирование спецсимволов
+- `self::getTgButtons($chatId)` — генерация полного меню (4 кнопки)
+- `new Client()` — создание HTTP клиента
+- `$client->post($apiURL, ['json' => [...]])` — отправка запроса
+- `Json::encode(['inline_keyboard' => $buttons])` — кодирование кнопок
+- `$response->getStatusCode()` — проверка статуса
+- `$response->getReasonPhrase()` — получение текста статуса
+- `Yii::error()` — логирование ошибок
 
-## Боты (константы)
+**Пример:**
 
-- **TELEGRAM_BOT_DEV** — для разработки
-- **TELEGRAM_BOT_PROD** — для production
-- **Каналы:** dev и prod для уведомлений
+```php
+// Dev окружение
+$result = TelegramService::sendMessageToTelegramClient(
+    '123456789',
+    'Ваш заказ готов!'
+);
 
-## Кнопки
+// Production
+$result = TelegramService::sendMessageToTelegramClient(
+    '123456789',
+    'Ваш заказ готов!',
+    false // prod-бот
+);
+```
 
-**Полное меню (getTgButtons):**
-1. Адреса магазинов (WebApp)
-2. Заказ на сайте (URL)
-3. Забрать 1800 руб (WebApp + промо)
-4. Списание бонусов (WebApp)
+---
 
-**Сокращенное меню (getTgShortButtons):**
-1. Адреса магазинов (WebApp)
-2. Заказ на сайте (URL)
+### getHashTG()
 
-## WebApp авторизация
+**Назначение:** Генерация hash для авторизации в WebApp.
 
-- Hash генерируется через `getHashTG(chat_id)`
-- Base64-кодирование: `sha1 + "#" + JSON`
-- Используется в URL с параметром `hash=...`
+**Сигнатура:**
+
+```php
+public static function getHashTG(
+    $chatId // ID чата пользователя
+): string // Base64-кодированный hash
+```
+
+**Углублённое описание логики:**
+
+1. Формирует JSON-структуру:
+
+   ```json
+   {"platform": "telegram", "user_id": "chatId"}
+   ```
+
+2. Генерирует SHA1 hash от `CHATBOT_SALT + ";" + $data`
+3. Конкатенирует: `sha1 + "#" + data`
+4. Кодирует результат в Base64
+5. Используется в URL WebApp для безопасной авторизации
+
+**Вызовы сторонних методов:**
+
+- `Json::encode([...])` — кодирование данных в JSON
+- `sha1($string)` — SHA1 хеширование
+- `base64_encode($string)` — Base64 кодирование
+
+**Формат результата:**
+
+```text
+Base64(SHA1(SALT + ";" + JSON) + "#" + JSON)
+```
+
+**Пример:**
+
+```php
+$hash = TelegramService::getHashTG('123456789');
+// Результат: 'YTFiMmMzZDRlNWY2...#eyJwbGF0Zm9ybSI6InRlbGVncmFtIi...'
+
+// Использование в URL
+$url = "https://chatbot.bazacvetov24.ru/bot/telegram?hash=$hash";
+```
+
+---
+
+### getTgButtons()
+
+**Назначение:** Генерация полного меню inline-кнопок (4 кнопки).
+
+**Сигнатура:**
+
+```php
+public static function getTgButtons(
+    $chatId // ID чата для генерации hash
+): array // Массив кнопок для inline_keyboard
+```
+
+**Углублённое описание логики:**
+
+1. Генерирует hash авторизации через `getHashTG($chatId)`
+2. Формирует URL для WebApp:
+   - `$linkAppStore` — адреса магазинов (`?hash=...&start=store`)
+   - `$linkAppEvents` — промо-акция (`?hash=...&start=promo.1000`)
+   - `$linkAppBalance` — баланс бонусов (`?hash=...`)
+3. Возвращает массив 2x2 кнопок:
+   - Строка 1: "Адреса магазинов" (WebApp), "Заказ на сайте" (URL)
+   - Строка 2: "Забрать 1800 руб" (WebApp), "Списание бонусов" (WebApp)
+
+**Вызовы сторонних методов:**
+
+- `self::getHashTG($chatId)` — генерация hash авторизации
+
+**Структура возвращаемого массива:**
+
+```php
+[
+    [
+        ['text' => "Адреса магазинов", "web_app" => ['url' => $linkAppStore]],
+        ['text' => "Заказ на сайте", "url" => $siteBc24Url],
+    ],
+    [
+        ['text' => "Забрать 1800 руб", "web_app" => ['url' => $linkAppEvents]],
+        ['text' => 'Списание бонусов', "web_app" => ['url' => $linkAppBalance]]
+    ]
+]
+```
+
+---
+
+### getTgShortButtons()
+
+**Назначение:** Генерация сокращённого меню inline-кнопок (2 кнопки).
+
+**Сигнатура:**
+
+```php
+public static function getTgShortButtons(
+    $chatId // ID чата для генерации hash
+): array // Массив кнопок для inline_keyboard
+```
+
+**Углублённое описание логики:**
+
+1. Генерирует hash через `getHashTG($chatId)`
+2. Формирует URL (аналогично `getTgButtons`)
+3. Возвращает массив с 1 строкой и 2 кнопками:
+   - "Адреса магазинов" (WebApp)
+   - "Заказ на сайте" (URL)
+
+**Вызовы сторонних методов:**
+
+- `self::getHashTG($chatId)` — генерация hash
+
+**Структура:**
+
+```php
+[
+    [
+        ['text' => "Адреса магазинов", "web_app" => ['url' => $linkAppStore]],
+        ['text' => "Заказ на сайте", "url" => $siteBc24Url],
+    ]
+]
+```
+
+**Отличия от getTgButtons():**
+
+- Только 1 строка вместо 2
+- Нет кнопок "Забрать 1800 руб" и "Списание бонусов"
+- Используется для промо-рассылок (меньше отвлекающих кнопок)
+
+---
+
+### escapeMarkdown()
+
+**Назначение:** Экранирование спецсимволов MarkdownV2 для текстов из файлов.
+
+**Сигнатура:**
+
+```php
+public static function escapeMarkdown(
+    $text // Исходный текст
+): string // Экранированный текст
+```
+
+**Углублённое описание логики:**
+
+1. Определяет список спецсимволов MarkdownV2:
+   `_ * [ ] ( ) ~ \` > # + - = | { } . !`
+2. Для каждого символа применяет regex с callback:
+   - Паттерн: `/(?<!\`)([char])(?!\`)/` — символ НЕ внутри backticks
+   - Замена: добавляет `\` перед символом
+3. Сохраняет символы внутри inline-кода (между \`)
+
+**Вызовы сторонних методов:**
+
+- `preg_replace_callback($pattern, $callback, $text)` — замена с callback
+- `preg_quote($char)` — экранирование символа для regex
+
+**Пример:**
+
+```php
+$text = "Цена: 100.50 руб (скидка!)";
+$escaped = TelegramService::escapeMarkdown($text);
+// Результат: "Цена: 100\.50 руб \(скидка\!\)"
+```
+
+---
+
+### escapeMarkdownLog()
+
+**Назначение:** Экранирование спецсимволов MarkdownV2 для логов из БД (простая версия).
+
+**Сигнатура:**
+
+```php
+public static function escapeMarkdownLog(
+    $text // Исходный текст
+): string // Экранированный текст
+```
+
+**Углублённое описание логики:**
+
+1. Определяет список спецсимволов (тот же, что в `escapeMarkdown`)
+2. Для каждого символа выполняет простую замену:
+   `$text = str_replace($char, '\\' . $char, $text)`
+3. **НЕ сохраняет** символы внутри backticks
+
+**Вызовы сторонних методов:**
+
+- `str_replace($search, $replace, $subject)` — простая замена строк
+
+**Отличия от escapeMarkdown():**
+
+| Аспект | escapeMarkdown | escapeMarkdownLog |
+|--------|----------------|-------------------|
+| Метод замены | preg_replace_callback | str_replace |
+| Inline-код | Сохраняет | Экранирует всё |
+| Производительность | Медленнее | Быстрее |
+| Использование | Тексты из файлов | Логи из БД |
+
+---
+
+### saveSentMessageToDB()
+
+**Назначение:** Сохранение отправленного сообщения в таблицу `users_telegram_message`.
+
+**Сигнатура:**
+
+```php
+public static function saveSentMessageToDB(
+    $messageData // Массив с данными сообщения
+): bool // Успех сохранения
+```
+
+**Углублённое описание логики:**
+
+1. Создаёт новый экземпляр `UsersTelegramMessage`
+2. Заполняет поля из массива `$messageData`:
+   - `chat_id` — ID чата Telegram
+   - `phone` — номер телефона клиента
+   - `message` — текст сообщения
+   - `kogort_date` — дата когорты
+   - `target_date` — целевая дата
+   - `type` — тип рассылки
+3. Вызывает `$userMessage->save()` для сохранения в БД
+4. Возвращает результат сохранения (bool)
+
+**Вызовы сторонних методов:**
+
+- `new UsersTelegramMessage()` — создание модели
+- `$userMessage->save()` — сохранение ActiveRecord в БД
+
+**Структура $messageData:**
+
+```php
+[
+    'chat_id' => '123456789',
+    'phone' => '79001234567',
+    'message' => 'Текст сообщения',
+    'kogort_date' => '2024-01-15',
+    'target_date' => '2024-01-20',
+    'type' => 1, // TYPE_FIRST_MESSAGE
+]
+```
+
+**Пример:**
+
+```php
+$saved = TelegramService::saveSentMessageToDB([
+    'chat_id' => $chatId,
+    'phone' => $user->phone,
+    'message' => $text,
+    'kogort_date' => date('Y-m-d'),
+    'target_date' => date('Y-m-d', strtotime('+7 days')),
+    'type' => UsersTelegramMessage::TYPE_FIRST_MESSAGE,
+]);
+```
+
+## Диаграмма архитектуры
+
+```mermaid
+graph TB
+    subgraph "TelegramService"
+        SM[sendMessage]
+        SMTC[sendMessageToTelegramClient]
+        SETM[sendErrorToTelegramMessage]
+        STSTM[sendTargetStatToTelegramMessage]
+        SPMTD[sendPromoMessageToTelegramDocument]
+        SP2MTD[sendPromo2MessageToTelegramDocument]
+    end
+
+    subgraph "Вспомогательные"
+        IDE[isDevelopmentEnvironment]
+        ISDE[isDevEnv]
+        GTB[getTgButtons]
+        GTSB[getTgShortButtons]
+        GHT[getHashTG]
+        EM[escapeMarkdown]
+        EML[escapeMarkdownLog]
+        SSMTD[saveSentMessageToDB]
+    end
+
+    subgraph "Внешние сервисы"
+        API2[api2.bazacvetov24.ru]
+        TGAPI[api.telegram.org]
+    end
+
+    subgraph "БД"
+        UTM[(users_telegram_message)]
+    end
+
+    SM --> API2
+    SMTC --> TGAPI
+    SETM --> TGAPI
+    STSTM --> TGAPI
+    SPMTD --> TGAPI
+    SP2MTD --> TGAPI
+
+    SMTC --> GTB
+    STSTM --> GTB
+    SPMTD --> GTSB
+    SP2MTD --> GTSB
+
+    GTB --> GHT
+    GTSB --> GHT
+
+    SMTC --> EM
+    STSTM --> EM
+
+    SPMTD --> ISDE
+    SP2MTD --> ISDE
+
+    SSMTD --> UTM
+```
 
 ## Таблица БД
 
-**users_telegram_message:**
-- Логирование всех отправленных сообщений
-- Поля: chat_id, phone, message, kogort_date, target_date, type, status
+**users_telegram_message** — логирование отправленных сообщений:
+
+| Поле | Тип | Описание |
+|------|-----|----------|
+| `id` | int | PK |
+| `chat_id` | varchar | ID чата Telegram |
+| `phone` | varchar | Телефон клиента |
+| `message` | text | Текст сообщения |
+| `kogort_date` | date | Дата когорты клиента |
+| `target_date` | date | Целевая дата рассылки |
+| `type` | int | Тип рассылки (1 или 2) |
+| `status` | int | Статус отправки |
+| `created_at` | timestamp | Дата создания |
 
 ## Типы рассылок
 
-- `TYPE_FIRST_MESSAGE = 1` — первая рассылка
-- `TYPE_SECOND_MESSAGE = 2` — вторая рассылка
+- `TYPE_FIRST_MESSAGE = 1` — первая рассылка (приветственная)
+- `TYPE_SECOND_MESSAGE = 2` — вторая рассылка (напоминание)
 
 ## Лимиты Telegram Bot API
 
-- Сообщений в секунду: 30
-- Максимум текста: 4096 символов
-- Файлов в MediaGroup: 10
-- Кнопок в строке: 8
+| Параметр | Лимит |
+|----------|-------|
+| Сообщений в секунду | 30 |
+| Максимум текста | 4096 символов |
+| Файлов в MediaGroup | 10 |
+| Кнопок в строке | 8 |
+| Общий размер reply_markup | 64 KB |
 
 ## Интеграция
 
-- **Telegram Bot API** — основной канал
+- **Telegram Bot API** — основной канал коммуникации
+- **API2 (bazacvetov24.ru)** — внутренний API для рассылок
+- **WebApp** — интерактивный интерфейс в Telegram
+- **Chatbot (chatbot.bazacvetov24.ru)** — ЛК клиента в браузере
 - **EDNA.ru** — дополнительный канал (WhatsApp)
-- **WebApp** — интерактивный интерфейс в ТГ
-- **Chatbot** — ЛК клиента в браузере
 
-## Статус
+## Связанные компоненты
 
-**Размер документации:** ~2,600 строк
-**Примеры:** 6+
-**Команды бота:** 4+
-**Диаграммы:** последовательность, архитектура, потоки
-**Готовность:** 100% ✅
+| Компонент | Тип | Описание |
+|-----------|-----|----------|
+| [UsersTelegramMessage](../models/UsersTelegramMessage.md) | Model | Логирование сообщений |
+| [Users](../models/Users.md) | Model | Данные клиентов |
+| [InfoTableService](./InfoTableService.md) | Service | Статистика по датам |
+| [Api2TelegramController](../api/api2/TelegramController.md) | Controller | API endpoint для отправки |