]> gitweb.erp-flowers.ru Git - erp24_rep/yii-erp24/.git/commitdiff
Правки по грейдам и группам
authorVladimir Fomichev <vladimir.fomichev@erp-flowers.ru>
Wed, 19 Nov 2025 15:12:23 +0000 (18:12 +0300)
committerVladimir Fomichev <vladimir.fomichev@erp-flowers.ru>
Wed, 19 Nov 2025 15:12:23 +0000 (18:12 +0300)
erp24/actions/grade/AdminUpdateAction.php
erp24/actions/grade/UpdateAction.php
erp24/api3/core/services/ReportService.php
erp24/commands/AdminController.php
erp24/records/Admin.php
erp24/views/grade/admin-update.php
erp24/views/grade/update.php
erp24/views/store-staffing/_form.php
erp24/views/store-staffing/index.php

index 4aca3a8d5af8d100dbcd745ebcfd9691c7cef9a3..fbe8480019e793186b21ce2c72ffd1ec5e4b66ac 100644 (file)
@@ -20,7 +20,8 @@ class AdminUpdateAction extends Action
     /**
      * Обновляет данные администратора, включая назначение должности и группы
      * 
-     * Для специальных групп формирует group_name из EmployeePosition и смены.
+     * Для рабочих групп (group_id = 50 и все группы с parent_id = 50) при заполнении
+     * employee_position_id заполняет group_name значением из AdminGroup->name.
      * Для остальных групп использует текстовое поле custom_position.
      * 
      * @param int $id ID администратора
@@ -51,9 +52,11 @@ class AdminUpdateAction extends Action
                 unset($attributes['storeArray']);
                 $attributes['store_arr_guid'] = empty($attributes['storeGuidArray']) ? '' : implode(',', $attributes['storeGuidArray']);
                 unset($attributes['storeGuidArray']);
-                if (!Yii::$app->user->can("updateAdminSettingsGroupId", ['group_id' => $attributes['group_id']])) {
-                    unset($attributes['group_id']);
-                }
+                
+                // Сохраняем флаг, можно ли изменять group_id вручную
+                $originalGroupId = $attributes['group_id'] ?? $model->group_id;
+                $canChangeGroupId = Yii::$app->user->can("updateAdminSettingsGroupId", ['group_id' => $originalGroupId]);
+                
                 if (!Yii::$app->user->can("updateAdminSettingsOnlyByHrAndAdministrator")) {
                     unset($attributes['store_dostup_all']);
                     unset($attributes['store_id']);
@@ -105,23 +108,94 @@ class AdminUpdateAction extends Action
                     $specialGroups[] = $workersGroup->id;
                 }
 
+                // Определяем рабочие группы: group_id = 50 и все группы с parent_id = 50
+                $workGroups = [AdminGroup::GROUP_ADMINISTRATORS]; // 50
+                $childGroups = AdminGroup::find()->where(['parent_id' => AdminGroup::GROUP_ADMINISTRATORS])->all();
+                foreach ($childGroups as $childGroup) {
+                    $workGroups[] = $childGroup->id;
+                }
+
                 $isSpecialGroup = in_array((int)$attributes['group_id'], $specialGroups);
+                $isWorkGroup = in_array((int)$attributes['group_id'], $workGroups);
 
+                // Сохраняем старое значение employee_position_id для проверки изменения
+                $oldPositionId = $model->employee_position_id;
+                $oldGroupId = $model->group_id;
+                
                 if ($isSpecialGroup) {
-                    // Для специальных групп формируем group_name из employee_position + shift
-                    if (!empty($attributes['employee_position_id'])) {
-                        $employeePosition = EmployeePosition::findOne($attributes['employee_position_id']);
-                        if ($employeePosition) {
-                            $groupName = $employeePosition->name;
-                            // Если выбрана смена, добавляем её к названию
-                            if (!empty($attributes['shift'])) {
-                                $groupName .= ' ' . $attributes['shift'];
+                    if ($isWorkGroup) {
+                        // Для рабочих групп (group_id = 50 и parent_id = 50) при заполнении employee_position_id
+                        // заполняем group_name значением из AdminGroup->name
+                        if (!empty($attributes['employee_position_id'])) {
+                            $adminGroup = AdminGroup::findOne($attributes['group_id']);
+                            if ($adminGroup) {
+                                $attributes['group_name'] = $adminGroup->name;
+                            }
+                        }
+                    } else {
+                        // Для остальных специальных групп синхронизируем группу с грейдом
+                        if (!empty($attributes['employee_position_id'])) {
+                            $employeePosition = EmployeePosition::findOne($attributes['employee_position_id']);
+                            if ($employeePosition) {
+                                $positionName = trim($employeePosition->name);
+                                $currentGroup = AdminGroup::findOne($attributes['group_id']);
+                                $currentGroupName = $currentGroup ? trim($currentGroup->name) : '';
+                                
+                                // Проверяем частичное совпадение группы и грейда
+                                $hasPartialMatch = false;
+                                if ($currentGroupName) {
+                                    // Нормализуем названия для сравнения (убираем "день", "ночь" и лишние пробелы)
+                                    $normalizedGroupName = mb_strtolower(preg_replace('/\s*(день|ночь)\s*/iu', '', $currentGroupName));
+                                    $normalizedPositionName = mb_strtolower($positionName);
+                                    
+                                    // Проверяем частичное совпадение
+                                    if (mb_strpos($normalizedGroupName, $normalizedPositionName) !== false || 
+                                        mb_strpos($normalizedPositionName, $normalizedGroupName) !== false) {
+                                        $hasPartialMatch = true;
+                                    }
+                                }
+                                
+                                if ($hasPartialMatch) {
+                                    // Если группа и грейд частично совпадают - оставляем группу
+                                    $attributes['group_name'] = $positionName;
+                                } else {
+                                    // Если не совпадают - ищем группу по названию грейда
+                                    $matchingGroup = self::findGroupByPositionName($positionName, $attributes['group_id']);
+                                    
+                                    if ($matchingGroup) {
+                                        // Найдена группа - меняем группу (только если есть права или группа не менялась вручную)
+                                        if ($canChangeGroupId || $originalGroupId == $oldGroupId) {
+                                            $attributes['group_id'] = $matchingGroup->id;
+                                            $attributes['group_name'] = $matchingGroup->name;
+                                            
+                                            if ($oldGroupId != $matchingGroup->id) {
+                                                Yii::$app->session->setFlash('info', 
+                                                    "Группа изменена с '{$currentGroupName}' на '{$matchingGroup->name}' в соответствии с грейдом '{$positionName}'"
+                                                );
+                                            }
+                                        } else {
+                                            // Нет прав на изменение группы - только обновляем group_name
+                                            $attributes['group_name'] = $positionName;
+                                            Yii::$app->session->setFlash('warning', 
+                                                "Грейд '{$positionName}' не соответствует группе '{$currentGroupName}'. Недостаточно прав для автоматического изменения группы."
+                                            );
+                                        }
+                                    } else {
+                                        // Группа не найдена - оставляем текущую группу
+                                        $attributes['group_name'] = $positionName;
+                                        
+                                        if ($oldGroupId != $attributes['group_id']) {
+                                            Yii::$app->session->setFlash('warning', 
+                                                "Группа для грейда '{$positionName}' не найдена. Текущая группа сохранена."
+                                            );
+                                        }
+                                    }
+                                }
                             }
-                            $attributes['group_name'] = $groupName;
                         }
                     }
 
-                    // Очищаем shift для не-специальных групп
+                    // Очищаем shift
                     unset($attributes['shift']);
                 } else {
                     // Для остальных групп group_name берем из текстового поля
@@ -134,6 +208,12 @@ class AdminUpdateAction extends Action
                     unset($attributes['shift']);
                 }
 
+                // Проверяем права на изменение group_id после всех автоматических изменений
+                if (!$canChangeGroupId) {
+                    // Если нет прав - убираем group_id из атрибутов, чтобы не изменить его
+                    unset($attributes['group_id']);
+                }
+
                 $model->setAttributes($attributes, false);
 
                 if (Yii::$app->user->can("manageAvatarka", ['id' => $model->id])) {
@@ -212,25 +292,114 @@ class AdminUpdateAction extends Action
 
         $companies = ArrayHelper::map(Companies::find()->all(), 'id', 'name');
 
-        // Извлекаем смену из group_name, если она там есть
-        $shift = '';
-        if ($model->employee_position_id && !empty($model->group_name)) {
-            $position = EmployeePosition::findOne($model->employee_position_id);
-            if ($position) {
-                $positionName = $position->name;
-                // Проверяем, что group_name начинается с названия должности и заканчивается на " день" или " ночь"
-                if (mb_strpos($model->group_name, $positionName) === 0) {
-                    $remaining = mb_substr($model->group_name, mb_strlen($positionName));
-                    if ($remaining === ' день') {
-                        $shift = 'день';
-                    } elseif ($remaining === ' ночь') {
-                        $shift = 'ночь';
+        return $this->controller->render('admin-update', compact('model', 'adminGroups', 'admins',
+            'cityStores', 'adminHistoryCategories', 'companies', 'positions'));
+    }
+
+    /**
+     * Поиск группы администраторов по названию грейда (должности)
+     * 
+     * @param string $positionName Название грейда
+     * @param int|null $currentGroupId ID текущей группы (для приоритета)
+     * @return AdminGroup|null Найденная группа или null
+     */
+    private static function findGroupByPositionName($positionName, $currentGroupId = null) {
+        $normalizedPositionName = mb_strtolower(trim($positionName));
+        $exactMatches = []; // Точные совпадения (после нормализации)
+        $containsMatches = []; // Группы, содержащие полное название грейда
+        $partialMatches = []; // Частичные совпадения
+        
+        // Ищем все группы, соответствующие грейду
+        $groups = AdminGroup::find()->all();
+        
+        foreach ($groups as $group) {
+            $groupName = mb_strtolower(trim($group->name));
+            $normalizedGroupName = preg_replace('/\s*(день|ночь)\s*/iu', '', $groupName);
+            
+            // 1. Проверяем точное совпадение после нормализации
+            if ($normalizedGroupName === $normalizedPositionName) {
+                $exactMatches[] = $group;
+                continue;
+            }
+            
+            // 2. Проверяем, содержит ли группа полное название грейда (более специфичное совпадение)
+            if (mb_strpos($normalizedGroupName, $normalizedPositionName) === 0 || 
+                mb_strpos($normalizedGroupName, ' ' . $normalizedPositionName) !== false ||
+                mb_strpos($normalizedGroupName, $normalizedPositionName . ' ') !== false) {
+                $containsMatches[] = $group;
+                continue;
+            }
+            
+            // 3. Проверяем обратное - содержит ли грейд название группы (менее специфичное)
+            if (mb_strpos($normalizedPositionName, $normalizedGroupName) !== false) {
+                $partialMatches[] = $group;
+            }
+        }
+        
+        // Приоритет выбора:
+        // 1. Точные совпадения
+        if (!empty($exactMatches)) {
+            // Если текущая группа в точных совпадениях - оставляем её
+            if ($currentGroupId) {
+                foreach ($exactMatches as $group) {
+                    if ($group->id == $currentGroupId) {
+                        return $group;
                     }
                 }
             }
+            // Ищем базовую группу без "день"/"ночь" среди точных совпадений
+            foreach ($exactMatches as $group) {
+                $groupName = mb_strtolower(trim($group->name));
+                if (mb_stripos($groupName, 'день') === false && mb_stripos($groupName, 'ночь') === false) {
+                    return $group;
+                }
+            }
+            // Если все с "день"/"ночь" - возвращаем первую
+            return $exactMatches[0];
         }
-
-        return $this->controller->render('admin-update', compact('model', 'adminGroups', 'admins',
-            'cityStores', 'adminHistoryCategories', 'companies', 'positions', 'shift'));
+        
+        // 2. Группы, содержащие полное название грейда (более специфичные)
+        if (!empty($containsMatches)) {
+            // Если текущая группа в списке - оставляем её
+            if ($currentGroupId) {
+                foreach ($containsMatches as $group) {
+                    if ($group->id == $currentGroupId) {
+                        return $group;
+                    }
+                }
+            }
+            // Ищем базовую группу без "день"/"ночь"
+            foreach ($containsMatches as $group) {
+                $groupName = mb_strtolower(trim($group->name));
+                if (mb_stripos($groupName, 'день') === false && mb_stripos($groupName, 'ночь') === false) {
+                    return $group;
+                }
+            }
+            // Если все с "день"/"ночь" - возвращаем первую
+            return $containsMatches[0];
+        }
+        
+        // 3. Частичные совпадения (менее приоритетные)
+        if (!empty($partialMatches)) {
+            // Если текущая группа в списке - оставляем её
+            if ($currentGroupId) {
+                foreach ($partialMatches as $group) {
+                    if ($group->id == $currentGroupId) {
+                        return $group;
+                    }
+                }
+            }
+            // Ищем базовую группу без "день"/"ночь"
+            foreach ($partialMatches as $group) {
+                $groupName = mb_strtolower(trim($group->name));
+                if (mb_stripos($groupName, 'день') === false && mb_stripos($groupName, 'ночь') === false) {
+                    return $group;
+                }
+            }
+            // Если все с "день"/"ночь" - возвращаем первую
+            return $partialMatches[0];
+        }
+        
+        return null;
     }
 }
index 2b35edef4bd8236e49b37b1bb9360750418b49bb..b543301753c3a31b9299c3584f3b4770c3169047 100755 (executable)
@@ -7,6 +7,7 @@ use yii\base\Action;
 use yii\base\DynamicModel;
 use yii\helpers\ArrayHelper;
 use yii_app\records\Admin;
+use yii_app\records\AdminGroup;
 use yii_app\records\EmployeePosition;
 use yii_app\records\EmployeePositionSkill;
 use yii_app\records\EmployeePositionStatus;
@@ -21,57 +22,97 @@ class UpdateAction extends Action
 
         $positions = EmployeePosition::find()->orderBy('posit')->all();
 
-        $modelPosition = DynamicModel::validateData(['position_id' => null, 'shift' => '', 'action' => 'updatePosition'], 
-            [['position_id', 'integer'], ['shift', 'string'], ['action', 'string']]);
+        $modelPosition = DynamicModel::validateData(['position_id' => null, 'action' => 'updatePosition'], 
+            [['position_id', 'integer'], ['action', 'string']]);
 
         if ($modelPosition->load(Yii::$app->request->post()) && $modelPosition->action == 'updatePosition') {
+            // Сохраняем старое значение для проверки изменения
+            $oldPositionId = $admin->employee_position_id;
+            
             // Гибридный подход: обновляем основное поле (источник истины)
+            // Важно: присваиваем значение напрямую, чтобы Yii2 отследил изменение
             $admin->employee_position_id = $modelPosition->position_id;
             
-            // Синхронизируем group_name с выбранной должностью и сменой (как в AdminUpdateAction)
+            // Явно помечаем атрибут как измененный для корректной работы afterSave
+            if ($oldPositionId != $admin->employee_position_id) {
+                $admin->markAttributeDirty('employee_position_id');
+            }
+            
+            // Определяем рабочие группы: group_id = 50 и все группы с parent_id = 50
+            $workGroups = [AdminGroup::GROUP_ADMINISTRATORS]; // 50
+            $childGroups = AdminGroup::find()->where(['parent_id' => AdminGroup::GROUP_ADMINISTRATORS])->all();
+            foreach ($childGroups as $childGroup) {
+                $workGroups[] = $childGroup->id;
+            }
+            
+            $isWorkGroup = in_array((int)$admin->group_id, $workGroups);
+            
+            // Проверяем соответствие группы и грейда (без автоматической смены группы)
             if ($modelPosition->position_id) {
                 $position = EmployeePosition::findOne($modelPosition->position_id);
+                
                 if ($position) {
-                    $groupName = $position->name;
-                    // Если выбрана смена, добавляем её к названию
-                    if (!empty($modelPosition->shift)) {
-                        $groupName .= ' ' . $modelPosition->shift;
+                    $positionName = trim($position->name);
+                    $currentGroup = AdminGroup::findOne($admin->group_id);
+                    $currentGroupName = $currentGroup ? trim($currentGroup->name) : '';
+                    
+                    // Проверяем частичное совпадение группы и грейда
+                    $hasPartialMatch = false;
+                    if ($currentGroupName) {
+                        // Нормализуем названия для сравнения (убираем "день", "ночь" и лишние пробелы)
+                        $normalizedGroupName = mb_strtolower(preg_replace('/\s*(день|ночь)\s*/iu', '', $currentGroupName));
+                        $normalizedPositionName = mb_strtolower($positionName);
+                        
+                        // Проверяем частичное совпадение
+                        if (mb_strpos($normalizedGroupName, $normalizedPositionName) !== false || 
+                            mb_strpos($normalizedPositionName, $normalizedGroupName) !== false) {
+                            $hasPartialMatch = true;
+                        }
+                    }
+                    
+                    // Обновляем group_name в зависимости от типа группы
+                    if ($isWorkGroup && $currentGroup) {
+                        $admin->group_name = $currentGroup->name;
+                    } else {
+                        $admin->group_name = $positionName;
+                    }
+                    
+                    // Показываем уведомление, если группа не соответствует грейду
+                    if ($oldPositionId != $modelPosition->position_id) {
+                        $oldPosition = EmployeePosition::findOne($oldPositionId);
+                        $oldPositionName = $oldPosition ? $oldPosition->name : 'не установлен';
+                        
+                        if ($hasPartialMatch) {
+                            Yii::$app->session->setFlash('success', 
+                                "Грейд успешно изменен с '{$oldPositionName}' на '{$positionName}'."
+                            );
+                        } else {
+                            // Группа не соответствует грейду - предупреждаем
+                            Yii::$app->session->setFlash('warning', 
+                                "Грейд изменен с '{$oldPositionName}' на '{$positionName}'. " .
+                                "⚠️ Внимание: текущая группа '{$currentGroupName}' не соответствует новому грейду. " .
+                                "Пожалуйста, измените группу в разделе 'Редактирование данных пользователя'."
+                            );
+                        }
                     }
-                    $admin->group_name = $groupName;
                 }
             }
             
             // Сохраняем Admin, что автоматически вызовет afterSave 
             // и создаст запись в EmployeePositionStatus для истории
             if ($admin->save(false)) {
-                Yii::$app->session->setFlash('success', 'Грейд успешно сохранён');
+                if ($oldPositionId == $modelPosition->position_id) {
+                    // Если ничего не изменилось - показываем стандартное сообщение
+                    Yii::$app->session->setFlash('success', 'Данные успешно сохранены');
+                }
             } else {
-                Yii::$app->session->setFlash('error', 'Ошибка при сохранении грейда');
+                Yii::$app->session->setFlash('error', 'Ошибка при сохранении грейда: ' . implode(', ', $admin->getFirstErrors()));
             }
             return $this->controller->redirect(['/grade/update', 'admin_id' => $admin_id]);
         } else {
             // Показываем текущий грейд из основного поля (источник истины)
             $modelPosition->position_id = $admin->employee_position_id;
             $modelPosition->action = 'updatePosition';
-            
-            // Извлекаем смену из group_name, если она там есть
-            $shift = '';
-            if ($admin->employee_position_id && !empty($admin->group_name)) {
-                $position = EmployeePosition::findOne($admin->employee_position_id);
-                if ($position) {
-                    $positionName = $position->name;
-                    // Проверяем, что group_name начинается с названия должности и заканчивается на " день" или " ночь"
-                    if (mb_strpos($admin->group_name, $positionName) === 0) {
-                        $remaining = mb_substr($admin->group_name, mb_strlen($positionName));
-                        if ($remaining === ' день') {
-                            $shift = 'день';
-                        } elseif ($remaining === ' ночь') {
-                            $shift = 'ночь';
-                        }
-                    }
-                }
-            }
-            $modelPosition->shift = $shift;
             $modelPosition->validate();
         }
 
index 5e4cf0285bf4fc7b303d23ff2ce90af8a620e3fd..7d183959f2a2b1b2ffc606e95f4fd998f171bd6a 100644 (file)
@@ -27,7 +27,7 @@ class ReportService
      * Получает средний уровень обученности по штатному расписанию магазина
      * 
      * @param int $storeId ID магазина
-     * @return float|null Средний уровень (посит) или null если нет данных
+     * @return float|null Средний уровень (posit) или null если нет данных
      */
     private function getStoreStaffingSkillScore($storeId)
     {
index 8180d1e7bc05854a479bd2d1567ebee69002ef9f..508d7e768b9d089ea6d7898f4c9f42f00cfc2968 100644 (file)
@@ -5,7 +5,9 @@ namespace yii_app\commands;
 use yii\console\Controller;
 use yii\helpers\ArrayHelper;
 use yii_app\records\Admin;
+use yii_app\records\AdminGroup;
 use yii_app\records\AdminStores;
+use yii_app\records\EmployeePosition;
 use yii_app\records\ExportImportTable;
 
 class AdminController extends Controller {
@@ -48,4 +50,221 @@ class AdminController extends Controller {
             }
         }
     }
+
+    /**
+     * Очистка group_name от подстрок "ночь" и "день" для записей с заполненным employee_position_id
+     * 
+     * Команда выполняет:
+     * 1. Выборку Admin записей с заполненным employee_position_id и наличием "ночь"/"день" в group_name
+     * 2. Сравнение group_name с AdminGroup->name
+     * 3. Очистку group_name от "ночь"/"день" при необходимости
+     * 
+     * Примеры работы:
+     * - "Старший флорист ночь" + группа "Старший флорист" → "Старший флорист"
+     * - "Администратор день" + группа "Администратор" → "Администратор"
+     * - "Новичок ночь" + группа "Помощник флориста" → "Новичок"
+     */
+    public function actionCleanGroupNameFromShift() {
+        $this->stdout("=== Начало очистки group_name от подстрок 'ночь' и 'день' ===\n", \yii\helpers\Console::FG_CYAN);
+        $this->stdout("Дата запуска: " . date('Y-m-d H:i:s') . "\n\n");
+
+        // Выборка записей с заполненным employee_position_id и наличием "ночь" или "день" в group_name
+        $admins = Admin::find()
+            ->where(['IS NOT', 'employee_position_id', null])
+            ->andWhere(['!=', 'employee_position_id', 0])
+            ->andWhere([
+                'OR',
+                ['ILIKE', 'group_name', 'ночь'],
+                ['ILIKE', 'group_name', 'день']
+            ])
+            ->all();
+
+        $totalCount = count($admins);
+        $this->stdout("Найдено записей для обработки: {$totalCount}\n\n", \yii\helpers\Console::FG_YELLOW);
+
+        if ($totalCount === 0) {
+            $this->stdout("Нет записей для обработки. Завершение работы.\n", \yii\helpers\Console::FG_GREEN);
+            return;
+        }
+
+        // Оптимизация: собираем все уникальные group_id и employee_position_id и загружаем одним запросом
+        $groupIds = [];
+        $positionIds = [];
+        foreach ($admins as $admin) {
+            if ($admin->group_id && !in_array($admin->group_id, $groupIds)) {
+                $groupIds[] = $admin->group_id;
+            }
+            if ($admin->employee_position_id && !in_array($admin->employee_position_id, $positionIds)) {
+                $positionIds[] = $admin->employee_position_id;
+            }
+        }
+
+        // Загружаем все нужные группы одним запросом
+        $adminGroups = AdminGroup::find()
+            ->where(['id' => $groupIds])
+            ->indexBy('id')
+            ->all();
+
+        // Загружаем все нужные должности одним запросом
+        $employeePositions = EmployeePosition::find()
+            ->where(['id' => $positionIds])
+            ->indexBy('id')
+            ->all();
+
+        $this->stdout("Загружено групп для обработки: " . count($adminGroups) . "\n", \yii\helpers\Console::FG_CYAN);
+        $this->stdout("Загружено должностей для обработки: " . count($employeePositions) . "\n\n", \yii\helpers\Console::FG_CYAN);
+
+        $processedCount = 0;
+        $skippedCount = 0;
+        $updatedCount = 0;
+        $errors = [];
+
+        foreach ($admins as $index => $admin) {
+            $processedCount++;
+            $this->stdout("--- Обработка записи {$processedCount}/{$totalCount} ---\n", \yii\helpers\Console::FG_CYAN);
+            $this->stdout("ID: {$admin->id}\n");
+            $this->stdout("Имя: {$admin->name}\n");
+            $this->stdout("Текущий group_name: '{$admin->group_name}'\n");
+            $this->stdout("group_id: {$admin->group_id}\n");
+            $this->stdout("employee_position_id: {$admin->employee_position_id}\n");
+
+            // Получаем группу администратора из словаря
+            $adminGroup = $adminGroups[$admin->group_id] ?? null;
+            
+            if (!$adminGroup) {
+                $errorMsg = "Группа с ID {$admin->group_id} не найдена";
+                $this->stdout("⚠ ОШИБКА: {$errorMsg}\n", \yii\helpers\Console::FG_RED);
+                $errors[] = [
+                    'admin_id' => $admin->id,
+                    'admin_name' => $admin->name,
+                    'error' => $errorMsg
+                ];
+                $this->stdout("\n");
+                continue;
+            }
+
+            $groupName = $adminGroup->name;
+            $this->stdout("Название группы (AdminGroup->name): '{$groupName}'\n");
+
+            // Сравниваем group_name и AdminGroup->name
+            $currentGroupName = trim($admin->group_name);
+            $targetGroupName = trim($groupName);
+
+            // Если значения одинаковые - пропускаем
+            if ($currentGroupName === $targetGroupName) {
+                $this->stdout("✓ Значения идентичны, пропускаем\n", \yii\helpers\Console::FG_GREEN);
+                $skippedCount++;
+                $this->stdout("\n");
+                continue;
+            }
+
+            // Определяем, есть ли в текущем group_name подстроки "ночь" или "день"
+            $hasNight = mb_stripos($currentGroupName, 'ночь') !== false;
+            $hasDay = mb_stripos($currentGroupName, 'день') !== false;
+
+            if (!$hasNight && !$hasDay) {
+                $this->stdout("⚠ В group_name нет подстрок 'ночь' или 'день', но значения различаются\n", \yii\helpers\Console::FG_YELLOW);
+                $this->stdout("Текущее: '{$currentGroupName}'\n");
+                $this->stdout("Ожидаемое: '{$targetGroupName}'\n");
+                $skippedCount++;
+                $this->stdout("\n");
+                continue;
+            }
+
+            // Убираем "ночь" или "день" с пробелом (в любом месте строки)
+            $cleanedName = $currentGroupName;
+            
+            // Убираем " ночь" или " ночь" (с пробелом до и/или после)
+            $cleanedName = preg_replace('/\s*ночь\s*/iu', ' ', $cleanedName);
+            
+            // Убираем " день" или " день" (с пробелом до и/или после)
+            $cleanedName = preg_replace('/\s*день\s*/iu', ' ', $cleanedName);
+            
+            // Убираем лишние пробелы и обрезаем
+            $cleanedName = trim(preg_replace('/\s+/', ' ', $cleanedName));
+
+            $this->stdout("Очищенное group_name: '{$cleanedName}'\n");
+
+            // Проверяем, совпадает ли очищенное имя с названием группы
+            if ($cleanedName === $targetGroupName) {
+                // Если совпадает - используем название группы
+                $newGroupName = $targetGroupName;
+                $this->stdout("✓ Очищенное имя совпадает с названием группы\n", \yii\helpers\Console::FG_GREEN);
+            } else {
+                // Если полное несовпадение - проверяем название грейда по employee_position_id
+                $employeePosition = $employeePositions[$admin->employee_position_id] ?? null;
+                
+                if ($employeePosition) {
+                    $positionName = trim($employeePosition->name);
+                    $this->stdout("Название грейда (EmployeePosition->name): '{$positionName}'\n");
+                    
+                    // Сравниваем EmployeePosition->name с AdminGroup->name
+                    if ($positionName === $targetGroupName) {
+                        // Если совпадают - ставим AdminGroup->name
+                        $newGroupName = $targetGroupName;
+                        $this->stdout("✓ Название грейда совпадает с названием группы, используем группу\n", \yii\helpers\Console::FG_GREEN);
+                    } else {
+                        // Если не совпадают - ставим EmployeePosition->name (название грейда)
+                        $newGroupName = $positionName;
+                        $this->stdout("⚠ Название грейда не совпадает с группой, используем название грейда\n", \yii\helpers\Console::FG_YELLOW);
+                    }
+                } else {
+                    // Если должность не найдена - оставляем очищенное имя
+                    $newGroupName = $cleanedName;
+                    $this->stdout("⚠ Должность не найдена, оставляем очищенное имя\n", \yii\helpers\Console::FG_YELLOW);
+                }
+            }
+
+            // Сохраняем изменения только если значение изменилось
+            if ($newGroupName !== $currentGroupName) {
+                $oldValue = $admin->group_name;
+                $admin->group_name = $newGroupName;
+                
+                if ($admin->save(false)) {
+                    $this->stdout("✓ УСПЕШНО ОБНОВЛЕНО:\n", \yii\helpers\Console::FG_GREEN);
+                    $this->stdout("  Было: '{$oldValue}'\n");
+                    $this->stdout("  Стало: '{$newGroupName}'\n");
+                    $updatedCount++;
+                } else {
+                    $errorMsg = "Ошибка при сохранении: " . implode(', ', $admin->getFirstErrors());
+                    $this->stdout("✗ ОШИБКА СОХРАНЕНИЯ: {$errorMsg}\n", \yii\helpers\Console::FG_RED);
+                    $errors[] = [
+                        'admin_id' => $admin->id,
+                        'admin_name' => $admin->name,
+                        'old_value' => $oldValue,
+                        'new_value' => $newGroupName,
+                        'error' => $errorMsg
+                    ];
+                }
+            } else {
+                $this->stdout("⚠ Значение не изменилось, пропускаем\n", \yii\helpers\Console::FG_YELLOW);
+                $skippedCount++;
+            }
+
+            $this->stdout("\n");
+        }
+
+        // Итоговая статистика
+        $this->stdout("\n=== ИТОГОВАЯ СТАТИСТИКА ===\n", \yii\helpers\Console::FG_CYAN);
+        $this->stdout("Всего обработано: {$processedCount}\n");
+        $this->stdout("Обновлено: {$updatedCount}\n", \yii\helpers\Console::FG_GREEN);
+        $this->stdout("Пропущено: {$skippedCount}\n", \yii\helpers\Console::FG_YELLOW);
+        $this->stdout("Ошибок: " . count($errors) . "\n", \yii\helpers\Console::FG_RED);
+
+        if (!empty($errors)) {
+            $this->stdout("\n=== СПИСОК ОШИБОК ===\n", \yii\helpers\Console::FG_RED);
+            foreach ($errors as $error) {
+                $this->stdout("ID: {$error['admin_id']}, Имя: {$error['admin_name']}\n");
+                $this->stdout("Ошибка: {$error['error']}\n");
+                if (isset($error['old_value'])) {
+                    $this->stdout("Было: '{$error['old_value']}'\n");
+                    $this->stdout("Должно было стать: '{$error['new_value']}'\n");
+                }
+                $this->stdout("\n");
+            }
+        }
+
+        $this->stdout("\n=== Завершение работы ===\n", \yii\helpers\Console::FG_CYAN);
+        $this->stdout("Дата завершения: " . date('Y-m-d H:i:s') . "\n");
+    }
 }
\ No newline at end of file
index 157cc93199ac6e4a0f6112ae12514f4656849d0c..28a99f79cbeb5b52c8a748e7e197532b7c6e1bfd 100755 (executable)
@@ -776,49 +776,59 @@ class Admin extends ActiveRecord implements IdentityInterface
         parent::afterSave($insert, $changedAttributes);
 
         // Проверяем, изменилось ли поле employee_position_id
-        if (array_key_exists('employee_position_id', $changedAttributes) || $insert) {
-            $oldPositionId = $changedAttributes['employee_position_id'] ?? null;
-            $newPositionId = $this->employee_position_id;
-
+        // Важно: проверяем как через changedAttributes, так и через сравнение значений
+        $oldPositionId = $changedAttributes['employee_position_id'] ?? null;
+        $newPositionId = $this->employee_position_id;
+        
+        // Если это новая запись или изменилось employee_position_id
+        if ($insert || ($oldPositionId != $newPositionId && array_key_exists('employee_position_id', $changedAttributes))) {
             // === ГИБРИДНЫЙ ПОДХОД: Ведение истории в EmployeePositionStatus ===
             if ($newPositionId && $oldPositionId != $newPositionId) {
                 try {
-                    // Ð\97акÑ\80Ñ\8bÑ\82Ñ\8c Ð¿Ñ\80едÑ\8bдÑ\83Ñ\89Ñ\83Ñ\8e Ð´Ð¾Ð»Ð¶Ð½Ð¾Ñ\81Ñ\82Ñ\8c, ÐµÑ\81ли Ð±Ñ\8bла
-                    if ($oldPositionId) {
-                        EmployeePositionStatus::updateAll(
-                            ['closed_at' => date('Y-m-d H:i:s')],
-                            ['admin_id' => $this->id, 'closed_at' => null]
-                        );
-                    }
+                    // Ð\97акÑ\80Ñ\8bÑ\82Ñ\8c Ð\92СÐ\95 Ð°ÐºÑ\82ивнÑ\8bе Ð·Ð°Ð¿Ð¸Ñ\81и Ð´Ð»Ñ\8f Ñ\8dÑ\82ого Ð°Ð´Ð¼Ð¸Ð½Ð° (на Ñ\81лÑ\83Ñ\87ай ÐµÑ\81ли ÐµÑ\81Ñ\82Ñ\8c Ð½ÐµÑ\81колÑ\8cко Ð°ÐºÑ\82ивнÑ\8bÑ\85)
+                    $closedCount = EmployeePositionStatus::updateAll(
+                        ['closed_at' => date('Y-m-d H:i:s')],
+                        ['admin_id' => $this->id, 'closed_at' => null]
+                    );
+                    
+                    Yii::info("Закрыто активных записей для admin_id={$this->id}: {$closedCount}", 'grade-sync');
                     
-                    // Создать новую запись об истории
+                    // Создать новую запись об истории (даже если возвращаемся к предыдущей должности)
                     $positionStatus = new EmployeePositionStatus();
                     $positionStatus->admin_id = $this->id;
                     $positionStatus->position_id = $newPositionId;
                     $positionStatus->created_at = date('Y-m-d H:i:s');
-                    $positionStatus->save(false);
+                    $positionStatus->closed_at = null; // Явно устанавливаем null для новой активной записи
                     
-                    Yii::info("История грейда обновлена для admin_id={$this->id}, position_id={$newPositionId}", 'grade-sync');
+                    if (!$positionStatus->save(false)) {
+                        Yii::error("Не удалось сохранить EmployeePositionStatus для admin_id={$this->id}, position_id={$newPositionId}. Ошибки: " . 
+                            implode(', ', $positionStatus->getFirstErrors()), 'grade-sync');
+                    } else {
+                        Yii::info("История грейда обновлена для admin_id={$this->id}, position_id={$newPositionId} (старая: " . ($oldPositionId ?? 'null') . ")", 'grade-sync');
+                    }
                 } catch (\Exception $e) {
-                    Yii::error("Ошибка при обновлении истории EmployeePositionStatus для admin_id={$this->id}: " . $e->getMessage(), 'grade-sync');
+                    Yii::error("Ошибка при обновлении истории EmployeePositionStatus для admin_id={$this->id}: " . $e->getMessage() . 
+                        " Trace: " . $e->getTraceAsString(), 'grade-sync');
                     // Не бросаем исключение, чтобы не прервать сохранение Admin
                 }
             }
+        }
 
-            // === СИНХРОНИЗАЦИЯ ЗАРПЛАТЫ ===
-            // Если должность была добавлена или изменена, и сотрудник не уволен
-            if ($newPositionId && ($oldPositionId != $newPositionId || $insert) && $this->group_id != AdminGroup::GROUP_FIRED) {
-                try {
-                    $syncService = new SalarySyncService();
-                    $result = $syncService->createPaymentFromPosition($this->id);
-                    
-                    if ($result) {
-                        Yii::info("Автоматически создана запись EmployeePayment для admin_id={$this->id}, position_id={$newPositionId}", 'salary-sync');
-                    }
-                } catch (\Exception $e) {
-                    Yii::error("Ошибка автоматической синхронизации оклада для admin_id={$this->id}: " . $e->getMessage(), 'salary-sync');
-                    // Не бросаем исключение, чтобы не прервать сохранение Admin
+        // === СИНХРОНИЗАЦИЯ ЗАРПЛАТЫ ===
+        // Если должность была добавлена или изменена, и сотрудник не уволен
+        $oldPositionIdForSalary = $changedAttributes['employee_position_id'] ?? null;
+        $newPositionIdForSalary = $this->employee_position_id;
+        if ($newPositionIdForSalary && ($oldPositionIdForSalary != $newPositionIdForSalary || $insert) && $this->group_id != AdminGroup::GROUP_FIRED) {
+            try {
+                $syncService = new SalarySyncService();
+                $result = $syncService->createPaymentFromPosition($this->id);
+                
+                if ($result) {
+                    Yii::info("Автоматически создана запись EmployeePayment для admin_id={$this->id}, position_id={$newPositionIdForSalary}", 'salary-sync');
                 }
+            } catch (\Exception $e) {
+                Yii::error("Ошибка автоматической синхронизации оклада для admin_id={$this->id}: " . $e->getMessage(), 'salary-sync');
+                // Не бросаем исключение, чтобы не прервать сохранение Admin
             }
         }
     }
index f974f6bc68c50d3dcaab4fd950287826e2572e42..fcb9abd5a41db646fa98d340847350571013c28d 100644 (file)
@@ -33,7 +33,7 @@ use yii_app\services\FileService;
 <div class="admin-form">
     <?php $form = ActiveForm::begin(); ?>
 
-    <?php PrintBlockHelper::printBlock('*Должность (группа)', $form->field($model, 'group_id')->dropDownList($adminGroups, ['onchange' => 'changeWorkRateVisibility(this);'])->label(false)) ?>
+    <?php PrintBlockHelper::printBlock('*Должность (группа)', $form->field($model, 'group_id')->dropDownList($adminGroups, ['onchange' => 'changeWorkRateVisibility(this); checkGradeGroupMatch();', 'id' => 'group-id-select'])->label(false)) ?>
     <?php
     if (!empty($adminHistoryCategories['group'])) {
         $adminHistoryCategory = $adminHistoryCategories['group'];
@@ -58,6 +58,7 @@ use yii_app\services\FileService;
         AdminGroup::GROUP_WORKERS,          // 45
         AdminGroup::GROUP_ADMINISTRATORS,   // 50
         AdminGroup::GROUP_FLORIST_SUPPORT_NIGHT, // 72
+        AdminGroup::GROUP_FLORIST,          // 89
     ];
 
     // Ищем группу "Работники магазинов" по имени
@@ -74,14 +75,14 @@ use yii_app\services\FileService;
 
     <div id="positionFieldSpecial" style="display: <?= $isSpecialGroup ? 'block' : 'none' ?>;">
         <?php PrintBlockHelper::printBlock('Должность', $form->field($model, 'employee_position_id')->dropDownList(
-            ArrayHelper::map($positions, 'id', 'name'), ['prompt' => 'Выберите должность']
+            ArrayHelper::map($positions, 'id', 'name'), ['prompt' => 'Выберите должность', 'id' => 'employee-position-select', 'onchange' => 'checkGradeGroupMatch();']
         )->label(false)) ?>
-
-        <?php PrintBlockHelper::printBlock('Смена', Html::dropDownList('Admin[shift]', $shift ?? '', [
-            '' => 'Не выбрана',
-            'день' => 'День',
-            'ночь' => 'Ночь'
-        ], ['class' => 'form-control'])) ?>
+        
+        <!-- Уведомление о несовпадении группы и грейда -->
+        <div id="grade-group-mismatch-alert" class="alert alert-warning mt-2" style="display: none;">
+            <strong>⚠️ Внимание!</strong> Выбранный грейд не соответствует текущей группе. 
+            Пожалуйста, выберите соответствующую группу в поле "Должность (группа)" выше.
+        </div>
     </div>
 
     <div id="positionFieldRegular" style="display: <?= !$isSpecialGroup ? 'block' : 'none' ?>;">
@@ -260,6 +261,10 @@ use yii_app\services\FileService;
     .hidden {
         display: none;
     }
+    .has-error select {
+        border-color: #dc3545 !important;
+        box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25) !important;
+    }
 </style>
 
 <script>
@@ -291,8 +296,62 @@ use yii_app\services\FileService;
             $('#positionFieldRegular').show();
             // Очищаем поле employee_position_id для не-специальных групп
             $('select[name="Admin[employee_position_id]"]').val('');
-            // Очищаем поле shift для не-специальных групп
-            $('select[name="Admin[shift]"]').val('');
+        }
+    }
+
+    // Данные для проверки соответствия грейда и группы
+    var positionsData = <?= json_encode(ArrayHelper::map($positions, 'id', 'name'), JSON_UNESCAPED_UNICODE) ?>;
+    var groupsData = <?= json_encode($adminGroups, JSON_UNESCAPED_UNICODE) ?>;
+    
+    /**
+     * Проверка соответствия выбранного грейда и группы
+     */
+    function checkGradeGroupMatch() {
+        var positionSelect = $('#employee-position-select');
+        var groupSelect = $('#group-id-select');
+        var alertDiv = $('#grade-group-mismatch-alert');
+        
+        // Проверяем только если выбраны оба значения
+        if (!positionSelect.length || !groupSelect.length || !positionSelect.val() || !groupSelect.val()) {
+            alertDiv.hide();
+            groupSelect.closest('.form-group').removeClass('has-error');
+            return;
+        }
+        
+        var selectedPositionId = positionSelect.val();
+        var selectedGroupId = groupSelect.val();
+        var positionName = positionsData[selectedPositionId] || '';
+        var groupName = groupsData[selectedGroupId] || '';
+        
+        if (!positionName || !groupName) {
+            alertDiv.hide();
+            groupSelect.closest('.form-group').removeClass('has-error');
+            return;
+        }
+        
+        // Нормализуем названия для сравнения (убираем "день", "ночь" и приводим к нижнему регистру)
+        var normalizedPositionName = positionName.toLowerCase().replace(/\s*(день|ночь)\s*/gi, '').trim();
+        var normalizedGroupName = groupName.toLowerCase().replace(/\s*(день|ночь)\s*/gi, '').trim();
+        
+        // Проверяем частичное совпадение
+        var hasMatch = false;
+        if (normalizedGroupName.indexOf(normalizedPositionName) !== -1 || 
+            normalizedPositionName.indexOf(normalizedGroupName) !== -1) {
+            hasMatch = true;
+        }
+        
+        if (!hasMatch) {
+            // Несовпадение - показываем предупреждение и выделяем поле группы
+            alertDiv.show();
+            groupSelect.closest('.form-group').addClass('has-error');
+            groupSelect.css('border-color', '#dc3545');
+            groupSelect.css('box-shadow', '0 0 0 0.2rem rgba(220, 53, 69, 0.25)');
+        } else {
+            // Совпадение - скрываем предупреждение и убираем выделение
+            alertDiv.hide();
+            groupSelect.closest('.form-group').removeClass('has-error');
+            groupSelect.css('border-color', '');
+            groupSelect.css('box-shadow', '');
         }
     }
 
@@ -301,12 +360,14 @@ use yii_app\services\FileService;
         var initialGroupId = $('select[name="Admin[group_id]"]').val();
         changePositionFieldVisibility(initialGroupId);
 
-        // Ð\94лÑ\8f Ð½Ðµ-Ñ\81пеÑ\86иалÑ\8cнÑ\8bÑ\85 Ð³Ñ\80Ñ\83пп Ð¾Ñ\87иÑ\89аем employee_position_id Ð¸ shift Ð¿Ñ\80и Ð·Ð°Ð³Ñ\80Ñ\83зке
+        // Для не-специальных групп очищаем employee_position_id при загрузке
         var specialGroups = <?= $specialGroupsJson ?>;
         var isSpecialGroup = specialGroups.includes(parseInt(initialGroupId));
         if (!isSpecialGroup) {
             $('select[name="Admin[employee_position_id]"]').val('');
-            $('select[name="Admin[shift]"]').val('');
         }
+        
+        // Проверяем соответствие при загрузке страницы
+        checkGradeGroupMatch();
     });
 </script>
\ No newline at end of file
index 3db9687272eb787d0521287c24cce12abe819078..6a033451975d5f19113ea5bc8dce350176315350 100755 (executable)
@@ -37,6 +37,51 @@ use dosamigos\datetimepicker\DateTimePicker;
     </div>
 </div>
 
+<!-- Flash сообщения -->
+<?php if (Yii::$app->session->hasFlash('success')): ?>
+    <div class="row mb-3">
+        <div class="col">
+            <div class="alert alert-success alert-dismissible fade show" role="alert">
+                <?= Yii::$app->session->getFlash('success') ?>
+                <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
+            </div>
+        </div>
+    </div>
+<?php endif; ?>
+
+<?php if (Yii::$app->session->hasFlash('info')): ?>
+    <div class="row mb-3">
+        <div class="col">
+            <div class="alert alert-info alert-dismissible fade show" role="alert">
+                <strong>ℹ️ Информация:</strong> <?= Yii::$app->session->getFlash('info') ?>
+                <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
+            </div>
+        </div>
+    </div>
+<?php endif; ?>
+
+<?php if (Yii::$app->session->hasFlash('warning')): ?>
+    <div class="row mb-3">
+        <div class="col">
+            <div class="alert alert-warning alert-dismissible fade show" role="alert">
+                <strong>⚠️ Внимание:</strong> <?= Yii::$app->session->getFlash('warning') ?>
+                <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
+            </div>
+        </div>
+    </div>
+<?php endif; ?>
+
+<?php if (Yii::$app->session->hasFlash('error')): ?>
+    <div class="row mb-3">
+        <div class="col">
+            <div class="alert alert-danger alert-dismissible fade show" role="alert">
+                <strong>❌ Ошибка:</strong> <?= Yii::$app->session->getFlash('error') ?>
+                <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
+            </div>
+        </div>
+    </div>
+<?php endif; ?>
+
 <!-- Информация о текущем грейде -->
 <div class="row mb-3 mt-2">
     <div class="col">
@@ -52,6 +97,9 @@ use dosamigos\datetimepicker\DateTimePicker;
             ?>
                 <strong>Текущий грейд:</strong> <?= Html::encode($currentPosition->name) ?>
                 <br><small>Установлен с: <?= $currentStatus ? Yii::$app->formatter->asDatetime($currentStatus->created_at, 'php:d.m.Y H:i') : 'дата неизвестна' ?></small>
+                <?php if ($admin->adminGroup): ?>
+                    <br><strong>Текущая группа:</strong> <?= Html::encode($admin->adminGroup->name) ?>
+                <?php endif; ?>
             <?php else: ?>
                 <strong style="color: #dc3545;">⚠️ Грейд не установлен</strong>
                 <br><small>Пожалуйста, выберите грейд из списка ниже</small>
@@ -74,11 +122,6 @@ use dosamigos\datetimepicker\DateTimePicker;
                 ['' => '-- Выберите грейд --'] + ArrayHelper::map($positions, 'id', 'name'),
                 ['class' => 'form-control']
             )->label(false); ?>
-             <?= $gradeForm->field($modelPosition, 'shift')->dropDownList([
-                '' => 'Не выбрана',
-                'день' => 'День',
-                'ночь' => 'Ночь'
-            ], ['class' => 'form-control'])->label('Смена') ?>
         </div>
 
         <div class="col-2">
@@ -161,6 +204,7 @@ use dosamigos\datetimepicker\DateTimePicker;
 </div>
 
 <?php 
+// Получаем всю историю назначений грейда (включая закрытые)
 $positionHistory = \yii_app\records\EmployeePositionStatus::find()
     ->where(['admin_id' => $admin->id])
     ->orderBy(['created_at' => SORT_DESC])
index 96fd3b3be372083b13b51c0a05bc52c120ac3518..c0f0e6a7e68793823ae24df4db85ad5abb4ae660 100644 (file)
@@ -25,7 +25,7 @@ use yii_app\records\EmployeePosition;
     ]) ?>
 
     <div class="alert alert-info">
-        <strong>Грейд (посит) должности:</strong> <span id="position-posit">-</span>
+        <strong>Грейд должности (цифровое значение от 0 до 5):</strong> <span id="position-posit">-</span>
     </div>
 
     <?= $form->field($model, 'count')->textInput(['type' => 'number', 'min' => 0.1, 'step' => 0.1]) ?>
index baed117b897f35506fbf8fc21594aaa8814bcadc..1a090ed7f25c1ed7d8953a73173686437dc2eac7 100644 (file)
@@ -84,7 +84,7 @@ $this->params['breadcrumbs'][] = $this->title;
         <thead>
             <tr>
                 <th>Магазин</th>
-                <th>Средний уровень (посит)</th>
+                <th>Средний уровень</th>
             </tr>
         </thead>
         <tbody>