* точной,
* исчерпывающей,
+* не пиши И т.д. дай развёрнутый ответ
+* нужны развёрнутое описание не только ссылайся на другой метод
+* нужно углублённое описание логики всех методов
+* больше вводных данных
+* больше описаний потоков данных
+* отображать все паратры которые есть у метода
+* список вызовов сторонних методов с кратким описанием
* соответствующей коду в репозитории,
* удобной для онбординга новых разработчиков,
* формализованной (Markdown + Mermaid + ссылки между файлами).
# Быстрый индекс документации 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 консольные команды
Добро пожаловать в документацию системы 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 система, разработанная для автоматизации всех бизнес-процессов сети цветочных магазинов:
|-----------|-----------|------------------------|
| 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** |
## 🚀 Быстрый старт
*Документация поддерживается в актуальном состоянии командой разработки ERP24.*
-**Версия документации:** 2.0
-**Последнее обновление:** 2025-11-27
+**Версия документации:** 2.1
+**Последнее обновление:** 2025-11-29
**Статус:** ✅ Активно поддерживается
## 🎉 Статус документации: АКТИВНО ПОДДЕРЖИВАЕТСЯ
-**Версия:** 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 |
## 📚 Документированные модули
├── 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 # Обзор БД ✅
- 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. **База данных**
- Обзор структуры БД
### 📊 Новая статистика
-- **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-контроллер
Документация системы 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 для отслеживания изменений
---
-**Версия:** 2.0
-**Последнее обновление:** 2025-11-27
+**Версия:** 2.1
+**Последнее обновление:** 2025-11-28
**Статус:** ✅ Активно поддерживается
**См. также:**
**Статус:** 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
+```
+
---
## Быстрая навигация
# 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. Содержит полную информацию о сотрудниках компании: личные данные, должность, права доступа, данные для расчета зарплаты, график работы и связи с другими сущностями системы.
---
+### Хуки жизненного цикла
+
+#### 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()
Данный раздел содержит подробную документацию всех бизнес-доменов (модулей) системы 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)
# Модуль 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 отвечает за управление системой бонусов для клиентов и сотрудников. Включает начисление, списание, конвертацию бонусов, управление уровнями бонусов, командными бонусами и историей операций.
# 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)
): 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()` (главная страница ЛК)
#### 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` — гибридный массив с данными ЛК (зафиксированные значения приоритетнее динамических)
+
+**Преимущества:** Быстрая загрузка, стабильные данные для закрытых периодов.
**Недостатки:** Не отражает изменения в реальном времени.
#### 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 строк |
---
]
```
+**Логика работы:**
+
+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();
]
```
+**Параметры:**
+
+| Параметр | Тип | По умолчанию | Описание |
+|----------|-----|--------------|----------|
+| `$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
| 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 дней
**Назначение:** Расчет базовых метрик продаж (офлайн, доставка, самовывоз).
+**Сигнатура:**
+```php
+public static function return_sales_stores($dateFrom, $dateTo): array
+```
+
+**Параметры:**
+
+| Параметр | Тип | По умолчанию | Описание |
+|----------|-----|--------------|----------|
+| `$dateFrom` | `string` | - | Начальная дата периода (YYYY-MM-DD) |
+| `$dateTo` | `string` | - | Конечная дата периода (YYYY-MM-DD) |
+
**Возвращает:**
```php
[
]
```
+**Логика работы:**
+
+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,
$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: Продажи по классу товаров (аналогичный)
+```
---
**Назначение:** Получение трафика посетителей из таблицы `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
AND
date <= :date_to
GROUP BY
- date, store_id
+ date,
+ store_id
```
---
**Назначение:** Сохранение агрегированных данных в таблицу `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(
$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
+);
+```
---
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
```
**Используется в:**
+
- `motivation/IndexAction` — отображение таблицы мотивации
- `motivation/index.php` (view) — рендеринг таблицы
}
```
+**Вызовы сторонних методов:**
+
+| Метод | Класс | Описание |
+|-------|-------|----------|
+| `MotivationValue::find()` | `MotivationValue` | Создание ActiveQuery для поиска значения |
+| `->where(['motivation_id' => ..., 'motivation_group_id' => ..., 'value_id' => ...])` | `ActiveQuery` | Фильтрация по трём ключевым полям |
+| `->one()` | `ActiveQuery` | Получение единственной записи |
+
**Использование:**
```php
$motivationValue->save();
```
+**Вызовы сторонних методов:**
+
+| Метод | Класс | Описание |
+|-------|-------|----------|
+| `MotivationValueGroup::find()` | `MotivationValueGroup` | Поиск группы по алиасу |
+| `->where(['alias' => $groupAlias])` | `ActiveQuery` | Фильтрация по алиасу ('plan', 'fact', 'week1' и т.д.) |
+| `->one()` | `ActiveQuery` | Получение записи группы |
+| `MotivationValue::find()` | `MotivationValue` | Поиск существующего значения |
+| `new MotivationValue()` | `MotivationValue` | Создание нового экземпляра при отсутствии записи |
+| `->save()` | `ActiveRecord` | Сохранение записи в БД |
+
**Использование:**
```php
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 задача
];
```
+**Вызовы сторонних методов:**
+
+| Метод | Класс | Описание |
+|-------|-------|----------|
+| `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`
# PayrollService
+## 🧠 Mindmap: PayrollService
+
+```mermaid
+mindmap
+ root((PayrollService))
+ Характеристики
+ 72 LOC
+ 2 публичных метода
+ Standalone класс
+ Назначение
+ Зарплатные расчеты
+ Проверка прав доступа
+ Валидация временных окон
+ Методы
+ __construct инициализация
+ checkPayrollUpdateAllowed валидация
+ Зависимости модели
+ Admin сотрудники
+ AdminGroup группы
+ AdminRating рейтинг
+ CityStore магазины
+ Timetable табель
+ Sales продажи
+ WriteOffs списания
+ Зависимости хелперы
+ DateHelper
+ HtmlHelper
+ SalaryHelper
+ Сервисы
+ CabinetService вывод ошибок
+```
+
+---
+
## Назначение
Сервис для работы с зарплатными расчетами и проверкой прав доступа к обновлению данных по зарплате. Обеспечивает валидацию временных окон для редактирования зарплатных данных определенными группами пользователей.
### `__construct($config = [])`
**Описание:**
-Конструктор класса. Инициализирует зависимость от CabinetService.
+Конструктор класса. Инициализирует зависимость от CabinetService через Dependency Injection via constructor.
**Параметры:**
-- `$config` (array) - массив конфигурации (не используется в текущей реализации)
-**Возвращает:** void
+| Параметр | Тип | По умолчанию | Описание |
+|----------|-----|--------------|----------|
+| `$config` | `array` | `[]` | Массив конфигурации (не используется в текущей реализации) |
+
+**Возвращает:** `void`
+
+**Логика работы:**
+
+1. Принимает опциональный массив конфигурации
+2. Создаёт новый экземпляр `CabinetService`
+3. Присваивает его свойству `$this->cabinetService`
+
+**Вызовы сторонних методов:**
+
+| Метод | Класс | Описание |
+|-------|-------|----------|
+| `new CabinetService()` | `CabinetService` | Создаёт экземпляр сервиса для работы с кабинетом пользователя |
**Пример:**
```php
### `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
$buttonParams,
$this
);
+
+// Результат:
+// [
+// 'errorText' => 'Период редактирования закрыт',
+// 'buttonParams' => ['url' => '/payroll/index', 'name' => 'Вернуться к списку'],
+// // ... другие данные для view
+// ]
```
---
**Описание:**
Проверяет, разрешено ли обновление данных по зарплате для указанной группы пользователей и даты. Реализует бизнес-правила доступа к редактированию зарплатных данных.
+**Параметры:**
+
+| Параметр | Тип | Описание |
+|----------|-----|----------|
+| `$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)
+```
**Примеры:**
Документация всех сервисов 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 и фоновых задач.
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();
Проверяет, разрешен ли расчет рейтинга для указанного периода. Рейтинг можно рассчитать до 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';
### 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
### 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
### 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
```
**Описание:**
-Сохраняет или обновляет запись рейтинга сотрудника в базе данных.
+Сохраняет или обновляет запись рейтинга сотрудника в базе данных. Реализует паттерн 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
```
**Описание:**
-Получает рейтинги администраторов кластера (группы магазинов).
+Получает рейтинги администраторов кластера (группы магазинов) с подгрузкой связанных данных о сотрудниках.
+
+**Параметры:**
+
+| Параметр | Тип | Описание |
+|----------|-----|----------|
+| `$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
### 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
### 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
# 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. Сервис отвечает за получение, расчет и анализ данных о продажах, возвратах, чеках и бонусных клиентах. Используется в личном кабинете сотрудников, дашбордах, отчетах и системе мотивации (начисление бонусов за матричные продажи, пиротехнику, авторские букеты).
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`
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');
- Если ночной — применяется сдвиг даты (ночь с 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);
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',
- Товары отбираются по `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);
```
- `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);
]
```
+**Вызовы сторонних методов:**
+
+| Метод | Класс | Описание |
+|-------|-------|----------|
+| `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);
Сервис для интеграции с 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 для отправки |