diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e43b0f9 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.DS_Store diff --git a/App/Controllers/ModuleUsersGroupsController.php b/App/Controllers/ModuleUsersGroupsController.php index f7ea66d..7984250 100644 --- a/App/Controllers/ModuleUsersGroupsController.php +++ b/App/Controllers/ModuleUsersGroupsController.php @@ -638,47 +638,4 @@ public function deleteAction(string $groupId): void $this->deleteEntity($group, 'module-users-groups/module-users-groups/index'); } - /** - * Changes the default user group action. - * - * @return void - */ - public function changeDefaultAction(): void - { - if (!$this->request->isPost()) { - return; - } - - // Get the POST data - $data = $this->request->getPost(); - - // Find all user groups - $groups = UsersGroups::find(); - foreach ($groups as $group) { - // Check if the current group is the selected default group - if ($group->defaultGroup === '1' and $group->id !== $data['defaultGroup']) { - $group->defaultGroup = '0'; - $this->saveEntity($group); - } - if ($group->defaultGroup !== '1' and $group->id === $data['defaultGroup']) { - $group->defaultGroup = '1'; - $this->saveEntity($group); - } - } - - // Get current user group memberships - $currentUsersGroups = GroupMembers::find()->toArray(); - $users = Users::find(); - foreach ($users as $user) { - // Check if the user is not already in a group - $key = array_search($user->id, array_column($currentUsersGroups, 'user_id')); - if (!$key) { - // Create a new group membership record - $record = new GroupMembers(); - $record->group_id = $data['defaultGroup']; - $record->user_id = $user->id; - $this->saveEntity($record); - } - } - } } diff --git a/App/Forms/DefaultGroupForm.php b/App/Forms/DefaultGroupForm.php index 746fbb1..39692a4 100644 --- a/App/Forms/DefaultGroupForm.php +++ b/App/Forms/DefaultGroupForm.php @@ -28,26 +28,34 @@ class DefaultGroupForm extends BaseForm public function initialize($entity = null, $options = null): void { - $variants = []; - $defaultGroupValue = null; $usersGroups = UsersGroups::find(); + $defaultGroupValue = ''; + + // Find default group value foreach ($usersGroups as $usersGroup) { - $variants[$usersGroup->id] = $usersGroup->name; if ($usersGroup->defaultGroup === '1') { - $defaultGroupValue = $usersGroup->id; + $defaultGroupValue = (string)$usersGroup->id; + break; } } + + // Create select with groups as options $defaultGroupSelect = new Select( - 'defaultGroup', $variants, [ + 'defaultGroup', + $usersGroups, + [ 'using' => [ 'id', 'name', ], 'useEmpty' => true, + 'emptyText' => 'Choose...', + 'emptyValue' => '', 'value' => $defaultGroupValue, 'class' => 'ui selection dropdown search select-default-group', ] ); + $this->add($defaultGroupSelect); } } \ No newline at end of file diff --git a/App/Forms/ExtensionEditAdditionalForm.php b/App/Forms/ExtensionEditAdditionalForm.php index db90977..d45b9de 100644 --- a/App/Forms/ExtensionEditAdditionalForm.php +++ b/App/Forms/ExtensionEditAdditionalForm.php @@ -21,63 +21,27 @@ use MikoPBX\AdminCabinet\Forms\BaseForm; use MikoPBX\AdminCabinet\Forms\ExtensionEditForm; -use Modules\ModuleUsersGroups\Models\GroupMembers; use Modules\ModuleUsersGroups\Models\UsersGroups as ModelUsersGroups; -use Phalcon\Forms\Element\Select; +use Phalcon\Forms\Element\Hidden; class ExtensionEditAdditionalForm extends BaseForm { - public static function prepareAdditionalFields(ExtensionEditForm $form, \stdClass $entity, array $options = []){ - - // Prepare groups for select - $parameters = [ - 'columns' => [ - 'id', - 'name' - ] - ]; - $arrGroups = ModelUsersGroups::find($parameters); - $arrGroupsForSelect = []; - foreach ($arrGroups as $group) { - $arrGroupsForSelect[$group->id] = $group->name; - } - - // Find current value - $userGroupId = null; - if (isset($entity->user_id)) { - $parameters = [ - 'conditions' => 'user_id = :user_id:', - 'bind' => ['user_id' => $entity->user_id] - ]; - - $curUserGroup = GroupMembers::findFirst($parameters); - if ($curUserGroup !== null) { - // Get the group ID from the existing group membership - $userGroupId = $curUserGroup->group_id; - } else { - // Get the group ID from the default group - $defaultGroup = ModelUsersGroups::findFirst('defaultGroup=1'); - if ($defaultGroup){ - $userGroupId = $defaultGroup->id; - } - } - } - - $groupForSelect = new Select( - 'mod_usrgr_select_group', $arrGroupsForSelect, [ - 'using' => [ - 'id', - 'name', - ], - 'value' => $userGroupId, - 'useEmpty' => false, - 'class' => 'ui selection dropdown search select-group-field', - ] - ); - - // Add the group select field to the form - $form->add($groupForSelect); - } - + public static function prepareAdditionalFields(ExtensionEditForm $form, \stdClass $entity, array $options = []): void + { + // Add hidden field for the group ID (value will be set by JavaScript) + $form->add(new Hidden('mod_usrgr_select_group', [ + 'value' => '', + 'id' => 'mod_usrgr_select_group' + ])); + + // Get all groups for dropdown (will be used in Volt template) + $groups = ModelUsersGroups::find([ + 'columns' => ['id', 'name'], + 'order' => 'name ASC' + ]); + + // Store groups data in form for access in Volt template + $form->setUserOption('mod_usrgr_groups', $groups); + } } \ No newline at end of file diff --git a/App/Views/Extensions/modify.volt b/App/Views/Extensions/modify.volt index 5698c61..58c9d8e 100644 --- a/App/Views/Extensions/modify.volt +++ b/App/Views/Extensions/modify.volt @@ -1,6 +1,19 @@
- + {{ form.render('mod_usrgr_select_group') }} +
\ No newline at end of file diff --git a/Lib/RestAPI/UsersGroups/CleanupOrphanedMembersAction.php b/Lib/RestAPI/UsersGroups/CleanupOrphanedMembersAction.php new file mode 100644 index 0000000..ad609a9 --- /dev/null +++ b/Lib/RestAPI/UsersGroups/CleanupOrphanedMembersAction.php @@ -0,0 +1,89 @@ +. + */ + +namespace Modules\ModuleUsersGroups\Lib\RestAPI\UsersGroups; + +use MikoPBX\PBXCoreREST\Lib\PBXApiResult; +use MikoPBX\Common\Models\Users; +use Modules\ModuleUsersGroups\Models\GroupMembers; + +/** + * Cleanup orphaned group member records + * + * @package Modules\ModuleUsersGroups\Lib\RestAPI\UsersGroups + */ +class CleanupOrphanedMembersAction +{ + /** + * Remove group member records for deleted users + * + * @param array $data Request data (empty) + * @return PBXApiResult + */ + public static function main(array $data): PBXApiResult + { + $result = new PBXApiResult(); + $result->processor = __METHOD__; + + try { + // Get valid user IDs using simple find + $validUsers = Users::find(['columns' => 'id']); + + if (count($validUsers) === 0) { + $result->success = true; + $result->data = [ + 'deleted' => 0, + 'message' => 'No users in system' + ]; + $result->httpCode = 200; + return $result; + } + + // Build list of valid user IDs + $validIds = []; + foreach ($validUsers as $user) { + $validIds[] = (int)$user->id; + } + + // Get module database connection through model instance + $groupMember = new GroupMembers(); + $connection = $groupMember->getReadConnection(); + $validIdsList = implode(',', $validIds); + + // Use direct SQL DELETE for performance + $sql = "DELETE FROM m_ModuleUsersGroups_GroupMembers WHERE user_id NOT IN ({$validIdsList})"; + $success = $connection->execute($sql); + $deletedCount = $success ? $connection->affectedRows() : 0; + + $result->success = true; + $result->data = [ + 'deleted' => $deletedCount, + 'valid_users' => count($validIds) + ]; + $result->httpCode = 200; + + } catch (\Throwable $e) { + $result->success = false; + $result->messages[] = 'Failed to cleanup orphaned members: ' . $e->getMessage(); + $result->httpCode = 500; + } + + return $result; + } +} diff --git a/Lib/RestAPI/UsersGroups/DataStructure.php b/Lib/RestAPI/UsersGroups/DataStructure.php new file mode 100644 index 0000000..acf87cc --- /dev/null +++ b/Lib/RestAPI/UsersGroups/DataStructure.php @@ -0,0 +1,92 @@ +. + */ + +namespace Modules\ModuleUsersGroups\Lib\RestAPI\UsersGroups; + +/** + * Data structure for Users Groups API + * + * @package Modules\ModuleUsersGroups\Lib\RestAPI\UsersGroups + */ +class DataStructure +{ + /** + * Get parameter definitions for Users Groups API + * + * @return array>> + */ + public static function getParameterDefinitions(): array + { + return [ + 'request' => [ + 'user_id' => [ + 'type' => 'integer', + 'description' => 'User ID from Users table', + 'sanitize' => 'int', + 'minimum' => 1, + 'example' => 1 + ], + 'group_id' => [ + 'type' => 'integer', + 'description' => 'Group ID from UsersGroups table', + 'sanitize' => 'int', + 'minimum' => 1, + 'required' => true, + 'example' => 1 + ] + ], + 'response' => [ + 'group_id' => [ + 'type' => 'integer', + 'description' => 'Group ID', + 'example' => 1 + ] + ] + ]; + } + + /** + * Get sanitization rules auto-generated from definitions + * + * @return array + */ + public static function getSanitizationRules(): array + { + $definitions = static::getParameterDefinitions(); + $rules = []; + + foreach ($definitions['request'] as $field => $def) { + $rule = [$def['type'] ?? 'string']; + + if (isset($def['sanitize'])) { + $rule[] = 'sanitize:' . $def['sanitize']; + } + if (isset($def['maxLength'])) { + $rule[] = 'max:' . $def['maxLength']; + } + if (!isset($def['required'])) { + $rule[] = 'empty_to_null'; + } + + $rules[$field] = implode('|', $rule); + } + + return $rules; + } +} diff --git a/Lib/RestAPI/UsersGroups/GetDefaultGroupAction.php b/Lib/RestAPI/UsersGroups/GetDefaultGroupAction.php new file mode 100644 index 0000000..9ace748 --- /dev/null +++ b/Lib/RestAPI/UsersGroups/GetDefaultGroupAction.php @@ -0,0 +1,60 @@ +. + */ + +namespace Modules\ModuleUsersGroups\Lib\RestAPI\UsersGroups; + +use MikoPBX\PBXCoreREST\Lib\PBXApiResult; +use Modules\ModuleUsersGroups\Models\UsersGroups as ModelUsersGroups; + +/** + * Get default group + * + * @package Modules\ModuleUsersGroups\Lib\RestAPI\UsersGroups + */ +class GetDefaultGroupAction +{ + /** + * Get default group + * + * @param array $data Request data (not used) + * @return PBXApiResult + */ + public static function main(array $data): PBXApiResult + { + $result = new PBXApiResult(); + $result->processor = __METHOD__; + + // Get default group from model + $defaultGroup = ModelUsersGroups::getDefaultGroup(); + + if ($defaultGroup !== null) { + $result->success = true; + $result->data = [ + 'group_id' => (int)$defaultGroup->id + ]; + $result->httpCode = 200; + } else { + $result->success = false; + $result->messages[] = 'No default group configured'; + $result->httpCode = 404; + } + + return $result; + } +} diff --git a/Lib/RestAPI/UsersGroups/GetGroupsStatsAction.php b/Lib/RestAPI/UsersGroups/GetGroupsStatsAction.php new file mode 100644 index 0000000..9ad7c4a --- /dev/null +++ b/Lib/RestAPI/UsersGroups/GetGroupsStatsAction.php @@ -0,0 +1,85 @@ +. + */ + +namespace Modules\ModuleUsersGroups\Lib\RestAPI\UsersGroups; + +use MikoPBX\PBXCoreREST\Lib\PBXApiResult; +use Modules\ModuleUsersGroups\Models\GroupMembers; +use Modules\ModuleUsersGroups\Models\UsersGroups; + +/** + * Get statistics for all groups (member counts) + * + * @package Modules\ModuleUsersGroups\Lib\RestAPI\UsersGroups + */ +class GetGroupsStatsAction +{ + /** + * Get member counts for all groups + * + * @param array $data Request data (optional group_id for single group) + * @return PBXApiResult + */ + public static function main(array $data): PBXApiResult + { + $result = new PBXApiResult(); + $result->processor = __METHOD__; + + // Optional: filter by specific group_id + $groupId = isset($data['group_id']) ? filter_var($data['group_id'], FILTER_VALIDATE_INT) : null; + + try { + $stats = []; + + if ($groupId !== null && $groupId !== false) { + // Get count for specific group + $count = GroupMembers::count([ + 'conditions' => 'group_id = :groupId:', + 'bind' => ['groupId' => $groupId] + ]); + + $stats[$groupId] = (int)$count; + } else { + // Get counts for all groups + $groups = UsersGroups::find(); + + foreach ($groups as $group) { + $count = GroupMembers::count([ + 'conditions' => 'group_id = :groupId:', + 'bind' => ['groupId' => $group->id] + ]); + + $stats[$group->id] = (int)$count; + } + } + + $result->success = true; + $result->data = [ + 'stats' => $stats + ]; + $result->httpCode = 200; + } catch (\Throwable $e) { + $result->success = false; + $result->messages[] = 'Failed to get group statistics: ' . $e->getMessage(); + $result->httpCode = 500; + } + + return $result; + } +} diff --git a/Lib/RestAPI/UsersGroups/GetUserGroupAction.php b/Lib/RestAPI/UsersGroups/GetUserGroupAction.php new file mode 100644 index 0000000..f7a3bc2 --- /dev/null +++ b/Lib/RestAPI/UsersGroups/GetUserGroupAction.php @@ -0,0 +1,118 @@ +. + */ + +namespace Modules\ModuleUsersGroups\Lib\RestAPI\UsersGroups; + +use MikoPBX\PBXCoreREST\Lib\PBXApiResult; +use Modules\ModuleUsersGroups\Models\UsersGroups as ModelUsersGroups; + +/** + * Get user's group by user_id + * + * @package Modules\ModuleUsersGroups\Lib\RestAPI\UsersGroups + */ +class GetUserGroupAction +{ + /** + * Get user's group by user_id + * + * @param array $data Request data with user_id or id + * @return PBXApiResult + */ + public static function main(array $data): PBXApiResult + { + $result = new PBXApiResult(); + $result->processor = __METHOD__; + + // ============ PHASE 1: SANITIZATION ============ + // WHY: Security - never trust user input + $sanitizationRules = DataStructure::getSanitizationRules(); + $sanitizedData = self::sanitizeInputData($data, $sanitizationRules); + + // ============ PHASE 2: REQUIRED VALIDATION ============ + // WHY: Fail fast - don't waste resources + // Accept both 'user_id' and 'id' for backwards compatibility + $userId = $sanitizedData['user_id'] ?? null; + + // If user_id not found, try 'id' parameter (fallback for old API calls) + if (empty($userId) && isset($data['id'])) { + $userId = filter_var($data['id'], FILTER_VALIDATE_INT); + if ($userId === false) { + $result->success = false; + $result->messages[] = 'Invalid id format'; + $result->httpCode = 400; + return $result; + } + } + + if (empty($userId)) { + $result->success = false; + $result->messages[] = 'user_id or id parameter is required'; + $result->httpCode = 400; + return $result; + } + + // ============ PHASE 3: GET GROUP ID ============ + // WHY: Get group ID from model (will return default if user has no group) + $groupId = ModelUsersGroups::getUserGroupId($userId); + + if ($groupId !== null) { + $result->success = true; + $result->data = [ + 'group_id' => $groupId + ]; + $result->httpCode = 200; + } else { + $result->success = false; + $result->messages[] = 'No group found for user'; + $result->httpCode = 404; + } + + return $result; + } + + /** + * Sanitize input data + * + * @param array $data Raw input data + * @param array $rules Sanitization rules + * @return array Sanitized data + */ + private static function sanitizeInputData(array $data, array $rules): array + { + $sanitized = []; + + foreach ($rules as $field => $rule) { + if (!isset($data[$field])) { + continue; + } + + $value = $data[$field]; + + // Apply int sanitization + if (strpos($rule, 'sanitize:int') !== false) { + $value = filter_var($value, FILTER_VALIDATE_INT); + } + + $sanitized[$field] = $value; + } + + return $sanitized; + } +} diff --git a/Lib/RestAPI/UsersGroups/SetDefaultGroupAction.php b/Lib/RestAPI/UsersGroups/SetDefaultGroupAction.php new file mode 100644 index 0000000..7422856 --- /dev/null +++ b/Lib/RestAPI/UsersGroups/SetDefaultGroupAction.php @@ -0,0 +1,186 @@ +. + */ + +namespace Modules\ModuleUsersGroups\Lib\RestAPI\UsersGroups; + +use MikoPBX\PBXCoreREST\Lib\PBXApiResult; +use MikoPBX\Common\Models\Users; +use Modules\ModuleUsersGroups\Models\UsersGroups as ModelUsersGroups; +use Modules\ModuleUsersGroups\Models\GroupMembers; + +/** + * Set default group + * + * @package Modules\ModuleUsersGroups\Lib\RestAPI\UsersGroups + */ +class SetDefaultGroupAction +{ + /** + * Set default group + * + * @param array $data Request data with group_id + * @return PBXApiResult + */ + public static function main(array $data): PBXApiResult + { + $result = new PBXApiResult(); + $result->processor = __METHOD__; + + // ============ PHASE 1: SANITIZATION ============ + // WHY: Security - never trust user input + $sanitizationRules = DataStructure::getSanitizationRules(); + $sanitizedData = self::sanitizeInputData($data, $sanitizationRules); + + // ============ PHASE 2: REQUIRED VALIDATION ============ + // WHY: Fail fast - don't waste resources + $groupId = $sanitizedData['group_id'] ?? null; + + if (empty($groupId)) { + $result->success = false; + $result->messages[] = 'group_id parameter is required'; + $result->httpCode = 400; + return $result; + } + + // ============ PHASE 3: FIND GROUP TO MAKE DEFAULT ============ + // WHY: Verify group exists before attempting to update + $newDefaultGroup = ModelUsersGroups::findFirst((int)$groupId); + if ($newDefaultGroup === null) { + $result->success = false; + $result->messages[] = 'Group not found'; + $result->httpCode = 404; + return $result; + } + + // ============ PHASE 4: CLEAR CURRENT DEFAULT GROUP ============ + // WHY: Only one group can be default at a time + $currentDefaultGroup = ModelUsersGroups::getDefaultGroup(); + if ($currentDefaultGroup !== null && $currentDefaultGroup->id !== $newDefaultGroup->id) { + $currentDefaultGroup->defaultGroup = '0'; + if (!$currentDefaultGroup->save()) { + $result->success = false; + $result->messages[] = 'Failed to clear current default group'; + $result->httpCode = 500; + return $result; + } + } + + // ============ PHASE 5: SET NEW DEFAULT GROUP ============ + // WHY: Mark the new group as default + $newDefaultGroup->defaultGroup = '1'; + if (!$newDefaultGroup->save()) { + $result->success = false; + $result->messages[] = 'Failed to set new default group'; + $result->httpCode = 500; + return $result; + } + + // ============ PHASE 6: AUTO-FILL USERS WITHOUT GROUP ============ + // WHY: When default group is set, automatically assign users without group + $addedCount = self::fillUsersWithoutGroup($newDefaultGroup->id); + + $result->success = true; + $result->data = [ + 'group_id' => (int)$newDefaultGroup->id, + 'users_added' => $addedCount + ]; + $result->httpCode = 200; + + return $result; + } + + /** + * Fill users without group into the default group + * + * @param int $groupId Default group ID + * @return int Number of users added + */ + private static function fillUsersWithoutGroup(int $groupId): int + { + try { + // Get all users + $allUsers = Users::find(['columns' => 'id']); + if (count($allUsers) === 0) { + return 0; + } + + // Build list of all user IDs + $allUserIds = []; + foreach ($allUsers as $user) { + $allUserIds[] = (int)$user->id; + } + + // Get users who already have group membership + $existingMembers = GroupMembers::find(['columns' => 'user_id']); + $existingUserIds = []; + foreach ($existingMembers as $member) { + $existingUserIds[] = (int)$member->user_id; + } + + // Find users without group (difference) + $usersWithoutGroup = array_diff($allUserIds, $existingUserIds); + + // Add each user without group to default group + $addedCount = 0; + foreach ($usersWithoutGroup as $userId) { + $member = new GroupMembers(); + $member->user_id = $userId; + $member->group_id = $groupId; + + if ($member->save()) { + $addedCount++; + } + } + + return $addedCount; + } catch (\Throwable $e) { + // Log error but don't fail the whole operation + return 0; + } + } + + /** + * Sanitize input data + * + * @param array $data Raw input data + * @param array $rules Sanitization rules + * @return array Sanitized data + */ + private static function sanitizeInputData(array $data, array $rules): array + { + $sanitized = []; + + foreach ($rules as $field => $rule) { + if (!isset($data[$field])) { + continue; + } + + $value = $data[$field]; + + // Apply int sanitization + if (strpos($rule, 'sanitize:int') !== false) { + $value = filter_var($value, FILTER_VALIDATE_INT); + } + + $sanitized[$field] = $value; + } + + return $sanitized; + } +} diff --git a/Lib/RestAPI/UsersGroups/UpdateUserGroupAction.php b/Lib/RestAPI/UsersGroups/UpdateUserGroupAction.php new file mode 100644 index 0000000..f4779b7 --- /dev/null +++ b/Lib/RestAPI/UsersGroups/UpdateUserGroupAction.php @@ -0,0 +1,181 @@ +. + */ + +namespace Modules\ModuleUsersGroups\Lib\RestAPI\UsersGroups; + +use MikoPBX\Common\Models\Extensions; +use MikoPBX\Common\Models\Users; +use MikoPBX\Common\Providers\MikoPBXVersionProvider as MikoPBXVersion; +use MikoPBX\Core\System\Util; +use MikoPBX\PBXCoreREST\Lib\PBXApiResult; +use Modules\ModuleUsersGroups\Models\GroupMembers; + +/** + * Update user's group membership + * + * @package Modules\ModuleUsersGroups\Lib\RestAPI\UsersGroups + */ +class UpdateUserGroupAction +{ + /** + * Update user's group membership + * + * @param array $data Request data with user_id/number and mod_usrgr_select_group + * @return PBXApiResult + */ + public static function main(array $data): PBXApiResult + { + $result = new PBXApiResult(); + $result->processor = __METHOD__; + + // ============ PHASE 1: EXTRACT PARAMETERS ============ + $groupId = $data['mod_usrgr_select_group'] ?? ''; + $userId = $data['user_id'] ?? ''; + $number = $data['number'] ?? ''; + + // ============ PHASE 2: VALIDATE GROUP ID ============ + if (empty($groupId)) { + $result->success = false; + $result->messages[] = 'Group ID is required'; + $result->httpCode = 400; + return $result; + } + + // ============ PHASE 3: GET USER ID ============ + if (empty($userId)) { + $userId = self::getUserIdFromNumber($number); + } + + if (empty($userId)) { + $result->success = false; + $result->messages[] = 'User ID could not be determined'; + $result->httpCode = 400; + return $result; + } + + // ============ PHASE 4: UPDATE GROUP MEMBERSHIP ============ + $updateResult = self::updateOrCreateGroupMembership($userId, $groupId); + + if ($updateResult) { + $result->success = true; + $result->data = [ + 'user_id' => $userId, + 'group_id' => $groupId + ]; + $result->httpCode = 200; + } else { + $result->success = false; + $result->messages[] = 'Failed to update group membership'; + $result->httpCode = 500; + } + + return $result; + } + + /** + * Retrieves a user ID based on a given phone number. + * + * Waits and attempts multiple times to retrieve the user ID for a newly created user + * based on the provided phone number. This method performs a maximum of 10 attempts + * with a 1-second pause between each attempt. It returns the user ID if found, + * otherwise null after exhausting all attempts. + * + * @param string $number The phone number used to lookup the user ID. + * @return string|null The user ID if found, otherwise null. + */ + private static function getUserIdFromNumber(string $number): ?string + { + if (empty($number)) { + return null; + } + + $userId = null; + // New user we have to wait until it will be created + $di = MikoPBXVersion::getDefaultDi(); + $parameters = [ + 'models' => [ + 'Users' => Users::class, + ], + 'conditions' => 'Extensions.number=:number:', + 'bind' => [ + 'number' => $number + ], + 'joins' => [ + 'Extensions' => [ + 0 => Extensions::class, + 1 => 'Extensions.userid = Users.id', + 2 => 'Extensions', + 3 => 'INNER', + ], + ], + 'columns' => [ + 'id' => 'Users.id', + ] + ]; + + // Wait for user to be created (max 10 attempts) + for ($i = 0; $i < 10; $i++) { + $query = $di->get('modelsManager')->createBuilder($parameters)->getQuery(); + $user = $query->getSingleResult(); + if ($user !== null) { + $userId = $user->id; + break; + } + sleep(1); + } + + return $userId; + } + + /** + * Update or create group membership for a user + * + * @param string $userId User ID + * @param string $groupId Group ID + * @return bool True if successful, false otherwise + */ + private static function updateOrCreateGroupMembership(string $userId, string $groupId): bool + { + $parameters = [ + 'conditions' => 'user_id = :user_id:', + 'bind' => [ + 'user_id' => $userId, + ] + ]; + + // Find the existing group membership based on user ID + $curUserGroup = GroupMembers::findFirst($parameters); + + // Update or create the group membership + if ($curUserGroup === null) { + // Create a new group membership + $curUserGroup = new GroupMembers(); + $curUserGroup->user_id = $userId; + } + $curUserGroup->group_id = $groupId; + + // Save the changes to the database + if (!$curUserGroup->save()) { + Util::sysLogMsg(__METHOD__, implode($curUserGroup->getMessages()), LOG_ERR); + return false; + } + + return true; + } +} diff --git a/Lib/RestAPI/UsersGroupsManagementProcessor.php b/Lib/RestAPI/UsersGroupsManagementProcessor.php new file mode 100644 index 0000000..0a21e79 --- /dev/null +++ b/Lib/RestAPI/UsersGroupsManagementProcessor.php @@ -0,0 +1,87 @@ +. + */ + +namespace Modules\ModuleUsersGroups\Lib\RestAPI; + +use Modules\ModuleUsersGroups\Lib\RestAPI\UsersGroups\GetUserGroupAction; +use Modules\ModuleUsersGroups\Lib\RestAPI\UsersGroups\GetDefaultGroupAction; +use Modules\ModuleUsersGroups\Lib\RestAPI\UsersGroups\SetDefaultGroupAction; +use Modules\ModuleUsersGroups\Lib\RestAPI\UsersGroups\GetGroupsStatsAction; +use Modules\ModuleUsersGroups\Lib\RestAPI\UsersGroups\CleanupOrphanedMembersAction; +use MikoPBX\PBXCoreREST\Lib\PBXApiResult; +use Phalcon\Di\Injectable; + +/** + * Class UsersGroupsManagementProcessor + * + * Processes users groups management requests + * + * API methods: + * - getUserGroup -> Get user's group by user_id + * - getDefaultGroup -> Get default group + * - setDefaultGroup -> Set default group + * - getGroupsStats -> Get member counts for all groups + * - cleanupOrphanedMembers -> Remove group members for deleted users + * + * @package Modules\ModuleUsersGroups\Lib\RestAPI + */ +class UsersGroupsManagementProcessor extends Injectable +{ + // Available actions (PHP 7.4 compatible constants) + public const ACTION_GET_USER_GROUP = 'getUserGroup'; + public const ACTION_GET_DEFAULT_GROUP = 'getDefaultGroup'; + public const ACTION_SET_DEFAULT_GROUP = 'setDefaultGroup'; + public const ACTION_GET_GROUPS_STATS = 'getGroupsStats'; + public const ACTION_CLEANUP_ORPHANED_MEMBERS = 'cleanupOrphanedMembers'; + + /** + * Main entry point for processing actions + * + * @param string $actionName Name of the action to execute + * @param array $parameters Action parameters + * @return PBXApiResult + */ + public function callBack(string $actionName, array $parameters): PBXApiResult + { + // PHP 7.4 compatible switch instead of match + switch ($actionName) { + case self::ACTION_GET_USER_GROUP: + return GetUserGroupAction::main($parameters); + + case self::ACTION_GET_DEFAULT_GROUP: + return GetDefaultGroupAction::main($parameters); + + case self::ACTION_SET_DEFAULT_GROUP: + return SetDefaultGroupAction::main($parameters); + + case self::ACTION_GET_GROUPS_STATS: + return GetGroupsStatsAction::main($parameters); + + case self::ACTION_CLEANUP_ORPHANED_MEMBERS: + return CleanupOrphanedMembersAction::main($parameters); + + default: + $result = new PBXApiResult(); + $result->processor = __METHOD__; + $result->success = false; + $result->messages[] = "Unknown action: {$actionName}"; + return $result; + } + } +} diff --git a/Lib/UsersGroups.php b/Lib/UsersGroups.php index 083222e..7c0d670 100644 --- a/Lib/UsersGroups.php +++ b/Lib/UsersGroups.php @@ -21,10 +21,8 @@ namespace Modules\ModuleUsersGroups\Lib; use MikoPBX\Common\Models\Extensions; -use MikoPBX\Common\Models\Users; use MikoPBX\Core\Asterisk\AstDB; use MikoPBX\Core\System\PBX; -use MikoPBX\Core\System\Util; use MikoPBX\Modules\PbxExtensionBase; use MikoPBX\Modules\PbxExtensionUtils; use Modules\ModuleUsersGroups\Models\AllowedOutboundRules; @@ -184,116 +182,4 @@ private function initChannelVariables($group_id, array $allowedRules): string return "ARRAY({$varNames})={$varValues}"; } - /** - * Updates the user group based on the provided post data. - * - * This method updates the group membership of a user based on the provided post data. - * If the user ID is not provided, it attempts to retrieve it based on a given phone number. - * If the user group is not provided or the user cannot be found, the function exits without making any changes. - * - * @param array $postData The post data containing user and group information. - * Expected keys: - * - 'mod_usrgr_select_group': The new group ID for the user. - * - 'user_id': The ID of the user whose group is being updated. - * - 'number': The phone number of the user, used if the user ID is not provided. - * @return void - */ - public static function updateUserGroup(array $postData): void - { - $userGroup = $postData['mod_usrgr_select_group'] ?? ''; - $userId = $postData['user_id'] ?? ''; - if (empty($userGroup)) { - return; - } - - if (empty($userId)) { - $userId = self::getUserIdFromNumber($postData['number'] ?? ''); - } - - if (empty($userId)) { - return; - } - - self::updateOrCreateGroupMembership($userId, $userGroup); - } - - /** - * Retrieves a user ID based on a given phone number. - * - * Waits and attempts multiple times to retrieve the user ID for a newly created user - * based on the provided phone number. This method performs a maximum of 10 attempts - * with a 1-second pause between each attempt. It returns the user ID if found, - * otherwise null after exhausting all attempts. - * - * @param string $number The phone number used to lookup the user ID. - * @return string|null The user ID if found, otherwise null. - */ - private static function getUserIdFromNumber(string $number): ?string - { - $userId = null; - // New user we have to wait until it will be created - $di = MikoPBXVersion::getDefaultDi(); - $parameters = [ - 'models' => [ - 'Users' => Users::class, - ], - 'conditions' => 'Extensions.number=:number:', - 'bind' => [ - 'number' => $number - ], - 'joins' => [ - 'Extensions' => [ - 0 => Extensions::class, - 1 => 'Extensions.userid = Users.id', - 2 => 'Extensions', - 3 => 'INNER', - ], - ], - ]; - $counter = 0; - while (empty($userId) and $counter < 10) { - $counter++; - sleep(1); - $userData = $di->get('modelsManager')->createBuilder($parameters)->getQuery()->execute()->toArray(); - $userId = $userData[0]['id'] ?? ''; - } - return $userId; - } - - /** - * Updates or creates a group membership for a user. - * - * Checks if a group membership exists for the given user ID. If it exists, the method - * updates the group ID; if not, it creates a new group membership record with the - * provided user ID and group ID. Changes are then saved to the database. Logs an error - * message using Util::sysLogMsg() if saving to the database fails. - * - * @param string $userId The ID of the user whose group membership is being updated or created. - * @param string $userGroup The ID of the group to assign to the user. - * @return void - */ - private static function updateOrCreateGroupMembership(string $userId, string $userGroup): void - { - $parameters = [ - 'conditions' => 'user_id = :user_id:', - 'bind' => [ - 'user_id' => $userId, - ] - ]; - - // Find the existing group membership based on user ID - $curUserGroup = GroupMembers::findFirst($parameters); - - // Update or create the group membership - if ($curUserGroup === null) { - // Create a new group membership - $curUserGroup = new GroupMembers(); - $curUserGroup->user_id = $userId; - } - $curUserGroup->group_id = $userGroup; - // Save the changes to the database - if (!$curUserGroup->save()) { - Util::sysLogMsg(__METHOD__, implode($curUserGroup->getMessages()), LOG_ERR); - } - } } \ No newline at end of file diff --git a/Lib/UsersGroupsConf.php b/Lib/UsersGroupsConf.php index d53c550..12e1046 100644 --- a/Lib/UsersGroupsConf.php +++ b/Lib/UsersGroupsConf.php @@ -20,11 +20,17 @@ namespace Modules\ModuleUsersGroups\Lib; use MikoPBX\AdminCabinet\Forms\ExtensionEditForm; +use MikoPBX\Common\Models\PbxSettings; +use MikoPBX\Common\Models\Users; +use MikoPBX\Core\System\Directories; +use MikoPBX\Core\System\SystemMessages; use MikoPBX\Modules\Config\ConfigClass; +use MikoPBX\PBXCoreREST\Lib\PBXApiResult; +use Modules\ModuleUsersGroups\App\Forms\ExtensionEditAdditionalForm; +use Modules\ModuleUsersGroups\Lib\RestAPI\UsersGroups\UpdateUserGroupAction; use Modules\ModuleUsersGroups\Models\AllowedOutboundRules; use Modules\ModuleUsersGroups\Models\GroupMembers; use Modules\ModuleUsersGroups\Models\UsersGroups as ModelUsersGroups; -use Modules\ModuleUsersGroups\App\Forms\ExtensionEditAdditionalForm; use Phalcon\Forms\Form; use Phalcon\Mvc\Micro; use Phalcon\Mvc\View; @@ -32,6 +38,11 @@ class UsersGroupsConf extends ConfigClass { + // Constants for API endpoints (PHP 7.4 compatible) + private const API_V2_EXTENSIONS_SAVE = '/api/extensions/saveRecord'; + private const API_V3_EMPLOYEES = '/pbxcore/api/v3/employees'; + private const MODULE_PREFIX = 'mod_usrgr_'; + /** * [ * 'extension-1' => 'numberGroup1', @@ -78,8 +89,8 @@ public function extensionGenAllPeersContext(): string // Check and set isolation flags based on conditions. $conf .= 'same => n,ExecIf($[ ${srcIsolate} && ${dstIsolateGroup} != 1 && dstIsolate != 1 && ${DIALPLAN_EXISTS(internal,${EXTEN},1)} != 1 ]?Set(srcIsolate=0))' . PHP_EOL; - $conf .= 'same => n,ExecIf($[ ${srcIsolate} && ${dstIsolateGroup} == 0 ]?Goto(internal-num-undefined,${EXTEN},1))' . PHP_EOL; - $conf .= 'same => n,ExecIf($[ ${srcIsolate} == 0 && ${dstIsolate} ]?Goto(internal-num-undefined,${EXTEN},1))' . PHP_EOL; + $conf .= 'same => n,ExecIf($[ ${srcIsolate} && ${dstIsolateGroup} == 0 ]?Goto(users-group-forbidden,${EXTEN},1))' . PHP_EOL; + $conf .= 'same => n,ExecIf($[ ${srcIsolate} == 0 && ${dstIsolate} ]?Goto(users-group-forbidden,${EXTEN},1))' . PHP_EOL; return $conf; } @@ -146,9 +157,50 @@ public function extensionGenContexts(): string $dialplan .= $isolateDialplan; } + // Add context for forbidden calls with voice notification + $dialplan .= $this->generateForbiddenCallContext(); + return $dialplan; } + /** + * Generate context for handling forbidden calls with voice notification + * + * This context is triggered when a user tries to call a number that is + * forbidden by their group isolation settings. It plays a custom voice + * message in the system language and provides a hook for custom implementations. + * + * Sound file structure: + * ModuleUsersGroups/sounds/{lang}/forbidden.mp3 + * Where {lang} is language code from system settings (ru, en, de, etc.) + * + * @return string The generated context as a string. + */ + private function generateForbiddenCallContext(): string + { + $conf = PHP_EOL . '[users-group-forbidden]' . PHP_EOL; + $conf .= 'exten => _X.,1,NoOp(--- Call to ${EXTEN} forbidden by UsersGroups module ---)' . PHP_EOL; + $conf .= 'same => n,Answer()' . PHP_EOL; + $conf .= 'same => n,Wait(1)' . PHP_EOL; + + // Try to play module custom sound file with automatic language selection + // Asterisk will use CHANNEL(language) to find: ModuleUsersGroups/sounds/{lang}/forbidden + // Supported formats: mp3, wav, gsm (Asterisk auto-selects best available) + $conf .= 'same => n,Playback(moduleusersgroups-forbidden)' . PHP_EOL; + + // Fallback to standard Asterisk sound if custom sound not found + // ss-noservice = "The number you have dialed is not in service" + $conf .= 'same => n,ExecIf($["${PLAYBACKSTATUS}" = "FAILED"]?Playback(ss-noservice))' . PHP_EOL; + + // Allow custom dialplan extension for additional processing + $conf .= 'same => n,GosubIf(${DIALPLAN_EXISTS(users-group-forbidden-custom,${EXTEN},1)}?users-group-forbidden-custom,${EXTEN},1)' . PHP_EOL; + + $conf .= 'same => n,Hangup()' . PHP_EOL; + $conf .= PHP_EOL; + + return $conf; + } + /** * Prepares additional parameters for each outgoing route context * before dial call in the extensions.conf file @@ -161,7 +213,9 @@ public function extensionGenContexts(): string public function generateOutRoutContext(array $rout): string { $conf = "\t" . 'same => n,ExecIf($["x${FROM_PEER}" == "x" && "${CHANNEL(channeltype)}" == "PJSIP" ]?Gosub(set_from_peer,s,1))' . " \n\t"; - $conf .= 'same => n,Set(GR_VARS=${DB(UsersGroups/${FROM_PEER})})' . " \n\t"; + // If call is forwarded, use the forwarding source peer instead of calling peer for CallerID rules + $conf .= 'same => n,Set(EFFECTIVE_FROM_PEER=${IF($["${FW_SOURCE_PEER}x" != "x"]?${FW_SOURCE_PEER}:${FROM_PEER})})' . " \n\t"; + $conf .= 'same => n,Set(GR_VARS=${DB(UsersGroups/${EFFECTIVE_FROM_PEER})})' . " \n\t"; $conf .= 'same => n,ExecIf($["${GR_VARS}x" != "x"]?Exec(Set(${GR_VARS})))' . " \n\t"; $conf .= 'same => n,ExecIf($["${GR_PERM_ENABLE}" == "1" && "${GR_ID_' . $rout['id'] . '}" != "1"]?return)' . " \n\t"; $conf .= 'same => n,ExecIf($["${GR_PERM_ENABLE}" == "1" && "${GR_CID_' . $rout['id'] . '}x" != "x"]?MSet(GR_OLD_CALLERID=${CALLERID(num)},OUTGOING_CID=${GR_CID_' . $rout['id'] . '}))' . "\n\t"; @@ -229,6 +283,9 @@ public function modelsEventChangeData($data): void */ public function onAfterModuleEnable(): void { + // Clean up orphaned group member records + RestAPI\UsersGroups\CleanupOrphanedMembersAction::main([]); + $this->getSettings(); UsersGroups::reloadConfigs(); } @@ -313,6 +370,27 @@ public function onVoltBlockCompile(string $controller, string $blockName, View $ return $result; } + /** + * Modifies the system assets. + * @see https://docs.mikopbx.com/mikopbx-development/module-developement/module-class#onafterassetsprepared + * + * @param \Phalcon\Assets\Manager $assets The assets manager for additional modifications from module. + * @param \Phalcon\Mvc\Dispatcher $dispatcher The dispatcher instance. + * + * @return void + */ + public function onAfterAssetsPrepared($assets, $dispatcher): void + { + $currentController = $dispatcher->getControllerName(); + $currentAction = $dispatcher->getActionName(); + + // Add JS for extension-modify page + if ($currentController === 'Extensions' && $currentAction === 'modify') { + $assets->collection('footerJS') + ->addJs("js/cache/ModuleUsersGroups/module-users-groups-extension-dropdown.js", true); + } + } + /** * Called from BaseForm before the form is initialized. * @see https://docs.mikopbx.com/mikopbx-development/module-developement/module-class#onbeforeforminitialize @@ -330,26 +408,274 @@ public function onBeforeFormInitialize(Form $form, $entity, $options): void } } + /** + * Process REST API requests + * @see https://docs.mikopbx.com/mikopbx-development/module-developement/module-class#modulerestapicallback + * + * @param array $request The request data + * + * @return PBXApiResult The response data + */ + public function moduleRestAPICallback(array $request): PBXApiResult + { + $action = $request['action'] ?? ''; + $data = $request['data'] ?? []; + + // Use the processor to handle the action + $processor = new \Modules\ModuleUsersGroups\Lib\RestAPI\UsersGroupsManagementProcessor(); + return $processor->callBack($action, $data); + } + /** * This method is called from RouterProvider's onAfterExecuteRoute function. * It handles the form submission and updates the user credentials. * + * Supports both: + * - Old API v2: /api/extensions/saveRecord + * - New REST API v3: API_V3_EMPLOYEES + * * @param Micro $app The micro application instance. + * @phpstan-param Micro<\MikoPBX\PBXCoreREST\Http\Request> $app * * @return void */ public function onAfterExecuteRestAPIRoute(Micro $app): void { - // Intercept the form submission of Extensions, only save action $calledUrl = $app->request->get('_url'); - if ($calledUrl!=='/api/extensions/saveRecord') { + + // Handle API v2 + if ($calledUrl === self::API_V2_EXTENSIONS_SAVE) { + $this->handleApiV2Request($app); return; } + + // Handle REST API v3 + $router = $app->getRouter(); + $matchedRoute = $router->getMatchedRoute(); + if ($matchedRoute !== null) { + $this->handleApiV3Request($app, $matchedRoute); + } + } + + /** + * Handle API v2 request: /api/extensions/saveRecord + * + * @param Micro $app The micro application instance. + * @return void + */ + private function handleApiV2Request(Micro $app): void + { $response = json_decode($app->response->getContent()); - if (!empty($response->result) and $response->result===true){ - // Intercept the form submission of Extensions with fields mod_usrgr_select_group and user_id - $postData = $app->request->getPost(); - UsersGroups::updateUserGroup($postData); + + if (!$this->isSuccessfulResponseV2($response)) { + return; + } + + $postData = $app->request->getPost(); + if (!$this->hasModuleData($postData)) { + return; + } + + // Execute update through REST API Action + $result = UpdateUserGroupAction::main($postData); + + // Log the result + if (!$result->success) { + SystemMessages::sysLogMsg( + __METHOD__, + "REST API v2: Failed to update group: " . implode(', ', $result->messages), + LOG_ERR + ); + } + } + + /** + * Handle REST API v3 request: API_V3_EMPLOYEES + * + * @param Micro $app The micro application instance. + * @param mixed $matchedRoute The matched route object. + * @return void + */ + private function handleApiV3Request(Micro $app, $matchedRoute): void + { + $pattern = $matchedRoute->getPattern(); + $httpMethod = $app->request->getMethod(); + + // Check if this is an employee-related request + $isEmployeeRoute = strpos($pattern, self::API_V3_EMPLOYEES) === 0; + if (!$isEmployeeRoute) { + return; + } + + // Handle POST/PUT requests - save group data + if (in_array($httpMethod, ['POST', 'PUT'], true)) { + /** @var \MikoPBX\PBXCoreREST\Http\Request $request */ + $request = $app->request; + $requestData = $request->getData(); + $response = json_decode($app->response->getContent(), false); + + $this->processEmployeeGroupUpdate($requestData, $response); + } + } + + /** + * Check if API v2 response is successful + * + * @param mixed $response The decoded JSON response + * @return bool True if successful + */ + private function isSuccessfulResponseV2($response): bool + { + return !empty($response->result) && $response->result === true; + } + + /** + * Check if POST data contains module-specific fields + * PHP 7.4 compatible - using strpos instead of str_starts_with + * + * @param array $postData The POST data array + * @return bool True if module data exists + */ + private function hasModuleData(array $postData): bool + { + foreach (array_keys($postData) as $key) { + if (strpos($key, self::MODULE_PREFIX) === 0) { + return true; + } + } + return false; + } + + + + /** + * Process employee group update for REST API v3 + * + * @param array $requestData The request data + * @param mixed $response The decoded JSON response + * @return void + */ + private function processEmployeeGroupUpdate(array $requestData, $response): void + { + // Extract IDs from request and response + $employeeId = $response->data->id ?? null; + $groupId = $requestData['mod_usrgr_select_group'] ?? null; + + // Validate employee ID (silent fail) + if (!$this->isValidEmployeeId($employeeId)) { + return; + } + + // Skip if no group data or empty string (silent fail) + if ($groupId === null || $groupId === '') { + return; + } + + // Validate group ID (silent fail) + if (!$this->isValidGroupId($groupId)) { + return; + } + + // Prepare and execute update through REST API Action + $postData = [ + 'mod_usrgr_select_group' => $groupId, + 'user_id' => $employeeId, + 'number' => $requestData['number'] ?? null + ]; + + $result = UpdateUserGroupAction::main($postData); + + // Log the result + if ($result->success) { + SystemMessages::sysLogMsg( + __METHOD__, + "REST API v3: Updated group for employee #{$employeeId} to group #{$groupId}", + LOG_INFO + ); + } else { + SystemMessages::sysLogMsg( + __METHOD__, + "REST API v3: Failed to update group for employee #{$employeeId}: " . implode(', ', $result->messages), + LOG_ERR + ); + } + } + + /** + * Validate employee ID + * PHP 7.4 compatible validation + * + * @param mixed $employeeId The employee ID to validate + * @return bool True if valid + */ + private function isValidEmployeeId($employeeId): bool + { + return $employeeId !== null + && $employeeId !== '' + && is_numeric($employeeId); + } + + /** + * Validate group ID + * PHP 7.4 compatible validation + * + * @param mixed $groupId The group ID to validate + * @return bool True if valid + */ + private function isValidGroupId($groupId): bool + { + return is_numeric($groupId); + } + + /** + * Clean up orphaned group member records + * + * Removes GroupMembers records that reference non-existent users. + * This happens after module reinstallation or restore from backup + * when employee records no longer exist in the main database. + * + * @return void + */ + private function cleanupOrphanedGroupMembers(): void + { + try { + // Get valid user IDs using simple find (works cross-database) + $validUsers = Users::find(['columns' => 'id']); + + if (count($validUsers) === 0) { + SystemMessages::sysLogMsg(__METHOD__, 'No users in system, skipping cleanup', LOG_INFO); + return; + } + + // Build list of valid user IDs + $validIds = []; + foreach ($validUsers as $user) { + $validIds[] = (int)$user->id; + } + + // Get module database connection through model + $connection = GroupMembers::getReadConnection(); + $validIdsList = implode(',', $validIds); + + // Use direct SQL DELETE for performance + $sql = "DELETE FROM m_ModuleUsersGroups_GroupMembers WHERE user_id NOT IN ({$validIdsList})"; + $success = $connection->execute($sql); + $deletedCount = $success ? $connection->affectedRows() : 0; + + // Log cleanup results + if ($deletedCount > 0) { + SystemMessages::sysLogMsg( + __METHOD__, + "Cleaned up {$deletedCount} orphaned group member record(s)", + LOG_INFO + ); + } + } catch (\Throwable $e) { + SystemMessages::sysLogMsg( + __METHOD__, + "Failed to cleanup orphaned members: " . $e->getMessage(), + LOG_ERR + ); } } } \ No newline at end of file diff --git a/Messages/fa.php b/Messages/fa.php new file mode 100644 index 0000000..b3d9bbc --- /dev/null +++ b/Messages/fa.php @@ -0,0 +1 @@ + 'ダイヤル グループ モジュール - %repesent%', + 'repModuleUsersGroups' => 'ダイヤルグループモジュール - %repesent%', 'mo_ModuleModuleUsersGroups' => 'ダイヤルグループ管理', 'BreadcrumbModuleUsersGroups' => 'ダイヤルグループ管理', - 'SubHeaderModuleUsersGroups' => '発信通話の権限の設定、発信 CallerID の管理、コール ピックアップ グループの整理', + 'SubHeaderModuleUsersGroups' => '発信通話の権限の設定、発信者IDの管理、通話ピックアップグループの編成', 'mod_usrgr_Connected' => 'モジュールが接続されています', 'mod_usrgr_Disconnected' => 'モジュールが無効になっています', 'mod_usrgr_Groups' => 'ダイヤルグループリスト', @@ -26,21 +26,21 @@ 'mod_usrgr_RoutingRules' => 'アウトバウンドルーティングルール', 'mod_usrgr_ColumnCallerId' => '発信者ID', 'mod_usrgr_SelectMemberToAddToGroup' => '従業員を選択する', - 'mod_usrgr_PatternsInstructions7' => '7XXXXXXXXXX - 7 で始まる任意の 11 桁の数字', + 'mod_usrgr_PatternsInstructions7' => '7XXXXXXXXXX - 7で始まる11桁の数字', 'mod_usrgr_DefaultGroup' => 'デフォルトグループ', - 'mod_usrgr_patterns' => 'グループに関連する数字のパターン。グループメンバーは通話のみ可能です', + 'mod_usrgr_patterns' => 'グループに関連する番号のパターン。グループメンバーは、その番号にのみ電話をかけることができます', 'mod_usrgr_isolate' => '従業員のグループを隔離する', - 'mod_usrgr_SelectUserGroup' => '従業員のダイヤル プランを選択します', + 'mod_usrgr_SelectUserGroup' => '従業員のダイヤルプランを選択します', 'mod_usrgr_isolatePickUp' => 'ピックアップ機能を分離する', 'mod_usrgr_PatternsInstructions3' => ' ', 'BreadcrumbModuleUsersGroupsModify' => 'ダイヤルプランの設定', 'mod_usrgr_PatternsInstructions4' => 'テンプレートの例:', - 'mod_usrgr_PatternsInstructions1' => 'パターンでは、1 ~ 9 の文字と文字 X (1 ~ 9 の任意の数字) を使用できます。', - 'mod_usrgr_PatternsInstructions5' => '2XX - 200 から 299 までの数字', - 'mod_usrgr_PatternsInstructions6' => '200001 - 明示的に指定された内部番号 (キュー番号など)', - 'mod_usrgr_PatternsInstructions2' => 'グループメンバーは、パターンに一致する番号のみをダイヤルできます。', + 'mod_usrgr_PatternsInstructions1' => 'パターンでは、1 ~ 9 の文字と文字 X (1 ~ 9 の任意の数字) を使用できます', + 'mod_usrgr_PatternsInstructions5' => '2XX - 200から299までの数字', + 'mod_usrgr_PatternsInstructions6' => '200001 - 明示的に指定された内部番号(例:キュー番号)', + 'mod_usrgr_PatternsInstructions2' => 'グループメンバーは、パターンに一致する番号のみをダイヤルできます', 'mod_usrgr_IsolateInstructions1' => 'グループメンバーは、自分のグループの番号にのみ電話をかけることができます。', - 'mod_usrgr_IsolateInstructions2' => '他のグループの従業員は、隔離されたグループに電話をかけることができなくなります。', + 'mod_usrgr_IsolateInstructions2' => '他のグループの従業員は、分離されたグループに電話をかけることができません。', 'mod_usrgr_ColumnDefaultGroup' => 'デフォルトグループ', 'mod_usrgr_ErrorOnDeleteDefaultGroup' => 'デフォルトグループを削除できません', 'mod_usrgr_SelectDefaultGroup' => 'グループを選択してください', diff --git a/Models/UsersGroups.php b/Models/UsersGroups.php index f60fe62..e3df0da 100644 --- a/Models/UsersGroups.php +++ b/Models/UsersGroups.php @@ -107,4 +107,45 @@ public function initialize(): void ); } + /** + * Get user's group by user_id + * + * @param int|string $userId User ID from Users table + * @return int|null Group ID or null if not found + */ + public static function getUserGroupId($userId): ?int + { + if (empty($userId)) { + return null; + } + + // Find user's group membership + $groupMember = GroupMembers::findFirst([ + 'conditions' => 'user_id = :user_id:', + 'bind' => ['user_id' => $userId] + ]); + + if ($groupMember !== null) { + return (int)$groupMember->group_id; + } + + // Return default group if user has no group assigned + $defaultGroup = self::findFirst('defaultGroup=1'); + if ($defaultGroup) { + return (int)$defaultGroup->id; + } + + return null; + } + + /** + * Get default group + * + * @return UsersGroups|null + */ + public static function getDefaultGroup(): ?UsersGroups + { + return self::findFirst('defaultGroup=1'); + } + } \ No newline at end of file diff --git a/README.md b/README.md index f233026..e348d49 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,276 @@ -## Module Users Groups +# Phone Groups Management Module -This module provides functionality for managing user groups and their associated permissions, including: +The Phone Groups Management module for MikoPBX allows you to organize employees into groups with customizable calling permissions and restrictions. This provides flexible access control and helps enforce corporate calling policies. -**Inputs** +## Overview -* **Group Name**: Defines a unique name for the user group. -* **Description**: A brief explanation or details about the purpose of the group. -* **Patterns**: A list of number patterns that group members are allowed to call. This can include specific numbers, ranges, or patterns using wildcards. -* **Isolate**: Enables isolation for the group, restricting members to call only within their group or to numbers matching the defined patterns. -* **Isolate Pickup**: Enables isolation for the call pickup function, allowing only group members to pick up calls from other members within the same group. -* **Default Group**: Sets a specific group as the default group for new users. -* **Users**: Selects individual users to assign to a specific group. -* **Routing Rules**: Defines outbound routing rules and applies them to specific groups, along with custom caller IDs for each rule. +This module enables administrators to: -**Outputs** +- **Organize users into groups** - Create departments, teams, or any logical groupings +- **Control calling permissions** - Restrict who can call whom based on group membership +- **Manage outbound routing** - Control which groups can use specific outbound routes +- **Customize caller IDs** - Set different caller IDs per group for outbound calls +- **Isolate call pickup** - Limit call pickup functionality within groups +- **Voice notifications** - Play customizable messages when calls are restricted -* **User Groups**: Creates and manages user groups with the specified settings. -* **Group Membership**: Assigns individual users to specific groups. -* **Call Restrictions**: Implements call restrictions based on group settings, allowing or denying calls based on isolation and defined patterns. -* **Call Pickup Restrictions**: Implements restrictions on call pickup based on group settings, allowing only members within the same group to pick up calls from each other. -* **Outbound Routing Rules**: Applies outbound routing rules to specific groups and sets custom caller IDs for those rules. +## Key Features -This module enhances call management and security by providing granular control over user permissions and call routing based on group affiliations. It allows administrators to define specific communication policies for different user groups, ensuring efficient and secure call handling within the organization. \ No newline at end of file +### Phone Groups Management + +Create unlimited user groups and assign employees to them. Each group can have: + +- **Unique name and description** for easy identification +- **Custom calling patterns** - Define which numbers the group can call +- **Isolation settings** - Restrict calls within the group or to specific patterns +- **Default group** - Automatically assign new users to a designated group + +### Call Restrictions + +#### Isolation Mode + +When isolation is enabled for a group: +- Members can only call other members within the same group +- Members can call external numbers matching defined patterns +- Members cannot call users in other groups +- Users outside the group can still call isolated members + +**Example patterns:** +``` +_7XXX # 4-digit internal numbers starting with 7 +_8XXXXXXXXXX # 11-digit numbers starting with 8 +*80 # Specific service codes +911 # Emergency numbers +``` + +#### Use Cases + +- **Reception Group** - No restrictions, can call everyone +- **Finance Department** - Isolated to internal finance team + specific external numbers +- **Sales Team** - Can call within team + customer number patterns +- **Management** - No restrictions, full access + +### Outbound Route Control + +Assign specific outbound routes to groups and set custom caller IDs: + +- Control which groups can use expensive international routes +- Display department-specific caller IDs for outbound calls +- Prevent unauthorized use of premium routes +- Automatically restore original caller ID after call completion + +### Call Pickup Restrictions + +When pickup isolation is enabled: +- Users can only pick up calls from their group members +- Prevents accidental or unauthorized call interception +- Maintains privacy within departments + +### Voice Notifications + +When a user attempts a forbidden call: +- System plays a customizable voice message +- Supports 30+ languages with automatic selection +- Falls back to standard system message if custom audio unavailable +- Allows custom call handling hooks for advanced scenarios + +## Installation + +### Requirements + +- MikoPBX version **2023.2.179** or higher +- PHP 7.4 or higher + +### Installation Steps + +1. Open MikoPBX web interface +2. Navigate to **System** → **Extension Modules** +3. Click **Upload Module** and select the module file +4. Click **Enable** to activate the module +5. The module menu will appear in **Routing** → **Phone Groups Management** + +## Configuration + +### Creating Your First Group + +1. Go to **Routing** → **Phone Groups Management** +2. Click **Add Group** button +3. Enter group information: + - **Name** - Unique identifier (e.g., "Sales Department") + - **Description** - Optional detailed description +4. Configure isolation (if needed): + - Enable **Isolation** checkbox to restrict calls + - Add allowed number patterns (one per line) +5. Assign users: + - Switch to **Users** tab + - Drag users from available to selected list +6. Configure outbound routes (optional): + - Switch to **Outbound Rules** tab + - Check allowed routes + - Enter custom caller ID for each route +7. Click **Save** + +### Setting a Default Group + +After creating a group, set it as default and it will be automatically assigned to all employees. + +**Note:** New users will automatically be assigned to the default group. + +### Assigning Users to Groups + +**Method 1: During group creation** +- Use the Users tab to select group members + +**Method 2: From Users tab** +- Go to **Routing** → **Phone Groups Management** → **Users** tab +- Use the dropdown in each user's row to change their group + +**Method 3: When editing employee card** +- Go to **Employees** → **Open employee card** +- Select group and save + +### Configuring Call Restrictions + +To restrict calling for a group: + +1. Edit the group +2. Enable **Isolation** checkbox +3. Add allowed patterns in the text area (one per line) +4. Save changes + +**Pattern Examples:** +- `_7XXX` - Allow 4-digit numbers starting with 7 +- `_8[0-5]XXXXXXX` - Allow 8-digit numbers starting with 8,80,81,82,83,84,85 +- `*80` - Allow specific code +- `911` - Allow emergency number + +**Testing:** +- Call from isolated user to another member in same group → Should work +- Call from isolated user to number matching pattern → Should work +- Call from isolated user to non-matching number → Should be blocked + +### Configuring Outbound Routes + +To control which routes a group can use: + +1. Edit the group +2. Go to **Outbound Rules** tab +3. Check routes this group should access +4. Optionally enter custom caller ID for each route +5. Save changes + +**Example:** +- Sales group: Allow "Local" and "Toll Free" routes with caller ID 555-0100 +- Support group: Allow "Local" only with caller ID 555-0200 +- Management: Allow all routes without custom caller IDs + +## Voice Notifications Setup + +### Basic Setup + +The module automatically plays a message when calls are blocked. By default, it uses the standard Asterisk "number not in service" message. + +## REST API + +The module provides REST API endpoints for integration with external systems. + +**Base URL:** `http://your-pbx.com/pbxcore/api/modules/ModuleUsersGroups/` + +### Available Actions + +- `getUserGroup` - Get user's assigned group +- `updateUserGroup` - Change user's group membership +- `getDefaultGroup` - Get the default group +- `setDefaultGroup` - Set a group as default +- `getGroupsStats` - Get member counts for all groups +- `cleanupOrphanedMembers` - Remove invalid group memberships + +## Troubleshooting + +### Users Not Restricted Despite Isolation + +**Check:** +1. User is actually assigned to the isolated group +2. Called number doesn't match allowed patterns + +### Outbound Routes Not Respecting Permissions + +**Check:** +1. Group has route enabled in Outbound Rules tab +2. Module is enabled and configuration reloaded + +**Solution:** +Disable and re-enable the module to rebuild configuration. + +### Call Pickup Not Isolated + +**Check:** +1. Pickup isolation checkbox is enabled for the group +2. Both users are in the same group + +### Changes Not Taking Effect + +After making any changes: +1. Save the group settings +2. Wait for automatic configuration reload (5-10 seconds) + +## FAQ + +**Q: Can I assign a user to multiple groups?** +A: No, each user can only belong to one group at a time. + +**Q: What happens if I delete a group?** +A: Users from the deleted group will be automatically moved to the default group. + +**Q: Can I delete the default group?** +A: No, you must first set another group as default before deleting it. + +**Q: Do isolated groups block emergency calls?** +A: No, if you include emergency patterns (like 911) in allowed patterns, they will work. + +**Q: Can non-isolated users call isolated users?** +A: Yes, isolation only restricts outgoing calls from isolated group members. + +**Q: How many groups can I create?** +A: Unlimited. The module doesn't impose any limit on the number of groups. + +**Q: Will isolation work for SIP trunks?** +A: Yes, isolation works for all call types - internal, external, and trunk calls. + +**Q: Can I use different voice messages for different groups?** +A: Currently, one message per language is used for all groups. Custom per-group messages require custom dialplan modifications. + +## Support + +### Documentation + +- **English:** [https://docs.mikopbx.com/mikopbx/v/english/modules/miko/module-users-groups](https://docs.mikopbx.com/mikopbx/v/english/modules/miko/module-users-groups) +- **Russian:** [https://docs.mikopbx.com/mikopbx/modules/miko/module-users-groups](https://docs.mikopbx.com/mikopbx/modules/miko/module-users-groups) + +### Contact + +- **Email:** help@miko.ru +- **Developer:** MIKO LLC +- **Website:** [https://www.mikopbx.com](https://www.mikopbx.com) + +### System Requirements + +- **Minimum MikoPBX Version:** 2023.2.179 +- **PHP Version:** 7.4 or higher + +## License + +This module is licensed under the GNU GPL v3.0. See the [LICENSE](LICENSE) file for details. + +--- + +## Recent Updates + +**Version November 2025:** +- Added voice notification system with multi-language support +- Automatic fallback to standard Asterisk sounds +- Fixed auto-population of users when setting default group +- Added cleanup of old group membership records after backup restoration when some employees were deleted +- Improved REST API with dedicated action classes +- Enhanced error handling and logging + +--- \ No newline at end of file diff --git a/README_RU.md b/README_RU.md new file mode 100644 index 0000000..5580a87 --- /dev/null +++ b/README_RU.md @@ -0,0 +1,279 @@ +# Модуль "Управление телефонными группами" + +Модуль "Управление телефонными группами" для MikoPBX позволяет организовывать сотрудников в группы с настраиваемыми правами на звонки и ограничениями. Это обеспечивает гибкий контроль доступа и помогает соблюдать корпоративные политики звонков. + +## Описание + +Модуль позволяет администраторам: + +- **Организовывать пользователей в группы** - создавать отделы, команды или любые логические группировки +- **Контролировать права на звонки** - ограничивать, кто может звонить кому на основе принадлежности к группе +- **Управлять исходящей маршрутизацией** - контролировать, какие группы могут использовать определенные исходящие маршруты +- **Настраивать caller ID** - устанавливать разные номера определителя для исходящих звонков по группам +- **Изолировать перехват вызовов** - ограничивать функцию перехвата вызовов внутри групп +- **Голосовые уведомления** - воспроизводить настраиваемые сообщения при ограничении звонков + +## Основные возможности + +### Управление телефонными группами + +Создавайте неограниченное количество групп пользователей и назначайте в них сотрудников. Каждая группа может иметь: + +- **Уникальное название и описание** для простой идентификации +- **Пользовательские шаблоны звонков** - определение номеров, на которые группа может звонить +- **Настройки изоляции** - ограничение звонков внутри группы или на определенные шаблоны +- **Группа по умолчанию** - автоматическое назначение новых пользователей в выбранную группу + +### Ограничения звонков + +#### Режим изоляции + +Когда для группы включена изоляция: +- Члены группы могут звонить только другим членам той же группы +- Члены группы могут звонить на внешние номера, соответствующие определенным шаблонам +- Члены группы не могут звонить пользователям из других групп +- Пользователи вне группы все еще могут звонить изолированным членам + +**Примеры шаблонов:** +``` +_7XXX # 4-значные внутренние номера, начинающиеся с 7 +_8XXXXXXXXXX # 11-значные номера, начинающиеся с 8 +*80 # Конкретные сервисные коды +112 # Экстренные номера +``` + +#### Примеры использования + +- **Группа ресепшн** - без ограничений, может звонить всем +- **Финансовый отдел** - изолирован на внутренние номера финансов + конкретные внешние номера +- **Отдел продаж** - может звонить внутри команды + шаблоны номеров клиентов +- **Руководство** - без ограничений, полный доступ + +### Контроль исходящих маршрутов + +Назначайте определенные исходящие маршруты группам и устанавливайте пользовательские caller ID: + +- Контролируйте, какие группы могут использовать дорогие международные маршруты +- Отображайте определители номера конкретных отделов для исходящих звонков +- Предотвращайте несанкционированное использование премиум-маршрутов +- Автоматически восстанавливайте исходный caller ID после завершения звонка + +### Ограничения перехвата вызовов + +Когда включена изоляция перехвата: +- Пользователи могут перехватывать только звонки членов своей группы +- Предотвращается случайный или несанкционированный перехват вызовов +- Сохраняется конфиденциальность внутри отделов + +### Голосовые уведомления + +Когда пользователь пытается совершить запрещенный звонок: +- Система воспроизводит настраиваемое голосовое сообщение +- Поддержка 30+ языков с автоматическим выбором +- Резервный вариант со стандартным системным сообщением, если пользовательское аудио недоступно +- Возможность использования пользовательских хуков обработки вызовов для расширенных сценариев + +## Установка + +### Требования + +- MikoPBX версии **2023.2.179** или выше +- PHP 7.4 или выше + +### Шаги установки + +1. Откройте веб-интерфейс MikoPBX +2. Перейдите в **Система** → **Модули расширений** +3. Нажмите **Загрузить модуль** и выберите файл модуля +4. Нажмите **Включить** для активации модуля +5. Меню модуля появится в **Маршрутизация** → **Управление телефонными группами** + +## Настройка + +### Создание первой группы + +1. Перейдите в **Маршрутизация** → **Управление телефонными группами** +2. Нажмите кнопку **Добавить группу** +3. Введите информацию о группе: + - **Название** - уникальный идентификатор (например, "Отдел продаж") + - **Описание** - необязательное подробное описание +4. Настройте изоляцию (при необходимости): + - Включите чекбокс **Изоляция** для ограничения звонков + - Добавьте разрешенные шаблоны номеров (по одному на строку) +5. Назначьте пользователей: + - Переключитесь на вкладку **Пользователи** + - Перетащите пользователей из доступного списка в выбранный +6. Настройте исходящие маршруты (опционально): + - Переключитесь на вкладку **Исходящие правила** + - Отметьте разрешенные маршруты + - Введите пользовательский caller ID для каждого маршрута +7. Нажмите **Сохранить** + +### Установка группы по умолчанию + +После создания группы укажите ее группой по умолчанию и она автоматически прсвоится всем сотрудникам. + +**Примечание:** Новые пользователи будут автоматически назначаться в группу по умолчанию. + +### Назначение пользователей в группы + +**Способ 1: При создании группы** +- Используйте вкладку "Пользователи" для выбора членов группы + +**Способ 2: Из вкладки "Пользователи"** +- Перейдите в **Маршрутизация** → **Управление телефонными группами** → вкладка **Пользователи** +- Используйте выпадающий список в строке каждого пользователя для смены группы + +**Способ 3: При редактировании карточки сотрудника** +- Перейдите в **Сотрудники** → **Откройте карточку** +- Выберите группу и сохраните + +### Настройка ограничений звонков + +Чтобы ограничить звонки для группы: + +1. Отредактируйте группу +2. Включите чекбокс **Изоляция** +3. Добавьте разрешенные шаблоны в текстовое поле (по одному на строку) +4. Сохраните изменения + +**Примеры шаблонов:** +- `_7XXX` - Разрешить 4-значные номера, начинающиеся с 7 +- `_8[0-5]XXXXXXX` - Разрешить 8-значные номера, начинающиеся с 8,80,81,82,83,84,85 +- `*80` - Разрешить конкретный код +- `112` - Разрешить экстренный номер + +**Тестирование:** +- Звонок от изолированного пользователя другому члену той же группы → Должен работать +- Звонок от изолированного пользователя на номер, соответствующий шаблону → Должен работать +- Звонок от изолированного пользователя на несоответствующий номер → Должен быть заблокирован + +### Настройка исходящих маршрутов + +Чтобы контролировать, какие маршруты может использовать группа: + +1. Отредактируйте группу +2. Перейдите на вкладку **Исходящие правила** +3. Отметьте маршруты, к которым должна иметь доступ эта группа +4. Опционально введите пользовательский caller ID для каждого маршрута +5. Сохраните изменения + +**Пример:** +- Группа продаж: Разрешить маршруты "Городские" и "Бесплатные" с caller ID 555-0100 +- Группа поддержки: Разрешить только "Городские" с caller ID 555-0200 +- Руководство: Разрешить все маршруты без пользовательских caller ID + +## Настройка голосовых уведомлений + +### Базовая настройка + +Модуль автоматически воспроизводит сообщение при блокировке звонков. По умолчанию используется стандартное сообщение Asterisk "номер не обслуживается". + + +## REST API + +Модуль предоставляет REST API эндпоинты для интеграции с внешними системами. + +**Базовый URL:** `http://your-pbx.com/pbxcore/api/modules/ModuleUsersGroups/` + +### Доступные действия + +- `getUserGroup` - Получить назначенную группу пользователя +- `updateUserGroup` - Изменить членство пользователя в группе +- `getDefaultGroup` - Получить группу по умолчанию +- `setDefaultGroup` - Установить группу по умолчанию +- `getGroupsStats` - Получить количество членов для всех групп +- `cleanupOrphanedMembers` - Удалить недействительные членства в группах + + +## Устранение неполадок + +### Пользователи не ограничены, несмотря на изоляцию + +**Проверьте:** +1. Пользователь действительно назначен в изолированную группу +2. Набираемый номер не соответствует разрешенным шаблонам + + +### Исходящие маршруты не учитывают права + +**Проверьте:** +1. Для группы включен маршрут на вкладке "Исходящие правила" +2. Модуль включен и конфигурация перезагружена + +**Решение:** +Отключите и снова включите модуль для пересоздания конфигурации. + +### Перехват вызовов не изолирован + +**Проверьте:** +1. Чекбокс изоляции перехвата включен для группы +2. Оба пользователя находятся в одной группе + +### Изменения не вступают в силу + +После внесения любых изменений: +1. Сохраните настройки группы +2. Дождитесь автоматической перезагрузки конфигурации (5-10 секунд) + +## Часто задаваемые вопросы + +**В: Могу ли я назначить пользователя в несколько групп?** +О: Нет, каждый пользователь может принадлежать только к одной группе одновременно. + +**В: Что происходит при удалении группы?** +О: Пользователи из удаленной группы будут автоматически перемещены в группу по умолчанию. + +**В: Могу ли я удалить группу по умолчанию?** +О: Нет, сначала необходимо установить другую группу по умолчанию перед ее удалением. + +**В: Изолированные группы блокируют экстренные вызовы?** +О: Нет, если вы включите экстренные шаблоны (например, 112) в разрешенные шаблоны, они будут работать. + +**В: Могут ли неизолированные пользователи звонить изолированным пользователям?** +О: Да, изоляция ограничивает только исходящие звонки от членов изолированной группы. + +**В: Сколько групп я могу создать?** +О: Неограниченно. Модуль не накладывает никаких ограничений на количество групп. + +**В: Будет ли изоляция работать для SIP транков?** +О: Да, изоляция работает для всех типов звонков - внутренних, внешних и через транки. + +**В: Могу ли я использовать разные голосовые сообщения для разных групп?** +О: В настоящее время используется одно сообщение на язык для всех групп. Пользовательские сообщения для каждой группы требуют модификаций диалплана. + +## Поддержка + +### Документация + +- **Английский:** [https://docs.mikopbx.com/mikopbx/v/english/modules/miko/module-users-groups](https://docs.mikopbx.com/mikopbx/v/english/modules/miko/module-users-groups) +- **Русский:** [https://docs.mikopbx.com/mikopbx/modules/miko/module-users-groups](https://docs.mikopbx.com/mikopbx/modules/miko/module-users-groups) + +### Контакты + +- **Email:** help@miko.ru +- **Разработчик:** MIKO LLC +- **Веб-сайт:** [https://www.mikopbx.com](https://www.mikopbx.com) + +### Системные требования + +- **Минимальная версия MikoPBX:** 2023.2.179 +- **Версия PHP:** 7.4 или выше + +## Лицензия + +Этот модуль лицензирован под GNU GPL v3.0. См. файл [LICENSE](LICENSE) для деталей. + +--- + +## Последние обновления + +**Версия Ноябрь 2025:** +- Добавлена система голосовых уведомлений с поддержкой многоязычности +- Автоматический резервный вариант со стандартными звуками Asterisk +- Исправлено автоматическое заполнение пользователей при установке группы по умолчанию +- Добавлена очистка зтарых записей членства в группе при восстановлении из бекапа, когда часть сотрудников было удалено. +- Улучшен REST API с выделенными классами действий +- Расширена обработка ошибок и логирование + +--- \ No newline at end of file diff --git a/Sounds/ru-ru/forbidden.mp3 b/Sounds/ru-ru/forbidden.mp3 new file mode 100644 index 0000000..1d29cfa Binary files /dev/null and b/Sounds/ru-ru/forbidden.mp3 differ diff --git a/public/assets/js/module-users-groups-extension-dropdown.js b/public/assets/js/module-users-groups-extension-dropdown.js new file mode 100644 index 0000000..c407b0f --- /dev/null +++ b/public/assets/js/module-users-groups-extension-dropdown.js @@ -0,0 +1,155 @@ +"use strict"; + +/* + * MikoPBX - free phone system for small business + * Copyright © 2017-2023 Alexey Portnov and Nikolay Beketov + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program. + * If not, see . + */ + +/* global globalRootUrl, PbxApi */ + +/** + * ModuleUsersGroupsExtensionDropdown - Initialize user groups dropdown on extension edit page + * Works with both old and new MikoPBX releases via FormPopulated event + */ +var ModuleUsersGroupsExtensionDropdown = { + /** + * jQuery object for the dropdown + */ + $dropdown: $('#mod_usrgr_select_group_dropdown'), + + /** + * Hidden input field for storing selected value + */ + $hiddenInput: $('#mod_usrgr_select_group'), + + /** + * Initialize the dropdown + */ + initialize: function initialize() { + // Check if dropdown element exists + if (ModuleUsersGroupsExtensionDropdown.$dropdown.length === 0) { + return; + } // Initialize Fomantic UI dropdown (items are already in HTML from server) + + + ModuleUsersGroupsExtensionDropdown.$dropdown.dropdown({ + onChange: function onChange(value) { + // Sync value to hidden input + ModuleUsersGroupsExtensionDropdown.$hiddenInput.val(value); // Trigger form change to enable save button + + if (typeof Form !== 'undefined' && typeof Form.dataChanged === 'function') { + Form.dataChanged(); + } + }, + clearable: false, + fullTextSearch: true, + forceSelection: false + }); // Smart initialization - check if user_id or id exists in DOM + // Old release: + // New release: + + var userId = $('#user_id').val() || $('#id').val(); + + if (userId && userId !== '' && userId !== 'new') { + // Existing user - load their group + ModuleUsersGroupsExtensionDropdown.loadUserGroup(userId); + } else { + // New user - load default group + ModuleUsersGroupsExtensionDropdown.loadDefaultGroup(); + } // Listen for form population event (for new release with AJAX forms) + + + $(document).on('FormPopulated', function (event, formData) { + // Get user ID from DOM (new release always uses 'id') + var populatedUserId = $('#id').val(); + + if (populatedUserId && populatedUserId !== '' && populatedUserId !== 'new') { + // Load user's actual group (will override default) + ModuleUsersGroupsExtensionDropdown.loadUserGroup(populatedUserId); + } + }); + }, + + /** + * Update dropdown value from hidden input + */ + updateDropdownValue: function updateDropdownValue() { + var value = ModuleUsersGroupsExtensionDropdown.$hiddenInput.val(); + + if (value && value !== '') { + ModuleUsersGroupsExtensionDropdown.$dropdown.dropdown('set selected', String(value)); + } + }, + + /** + * Load default group from module API + */ + loadDefaultGroup: function loadDefaultGroup() { + $.ajax({ + url: '/pbxcore/api/modules/ModuleUsersGroups/getDefaultGroup', + method: 'POST', + dataType: 'json', + success: function success(response) { + if (response.result === true && response.data && response.data.group_id) { + // Set value to hidden input + ModuleUsersGroupsExtensionDropdown.$hiddenInput.val(response.data.group_id); // Update dropdown + + ModuleUsersGroupsExtensionDropdown.updateDropdownValue(); + } + }, + error: function error(xhr, status, _error) { + console.error('ModuleUsersGroups: Failed to load default group', _error); + } + }); + }, + + /** + * Load user's group from module API + * @param {string|number} userId - User ID + */ + loadUserGroup: function loadUserGroup(userId) { + if (!userId) { + // For new users, load default group + ModuleUsersGroupsExtensionDropdown.loadDefaultGroup(); + return; + } + + $.ajax({ + url: '/pbxcore/api/modules/ModuleUsersGroups/getUserGroup', + method: 'POST', + dataType: 'json', + data: { + user_id: userId + }, + success: function success(response) { + if (response.result === true && response.data && response.data.group_id) { + // Set value to hidden input + ModuleUsersGroupsExtensionDropdown.$hiddenInput.val(response.data.group_id); // Update dropdown + + ModuleUsersGroupsExtensionDropdown.updateDropdownValue(); + } + }, + error: function error(xhr, status, _error2) { + console.error('ModuleUsersGroups: Failed to load user group', _error2); + } + }); + } +}; // Initialize when document is ready + +$(document).ready(function () { + ModuleUsersGroupsExtensionDropdown.initialize(); +}); +//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbInNyYy9tb2R1bGUtdXNlcnMtZ3JvdXBzLWV4dGVuc2lvbi1kcm9wZG93bi5qcyJdLCJuYW1lcyI6WyJNb2R1bGVVc2Vyc0dyb3Vwc0V4dGVuc2lvbkRyb3Bkb3duIiwiJGRyb3Bkb3duIiwiJCIsIiRoaWRkZW5JbnB1dCIsImluaXRpYWxpemUiLCJsZW5ndGgiLCJkcm9wZG93biIsIm9uQ2hhbmdlIiwidmFsdWUiLCJ2YWwiLCJGb3JtIiwiZGF0YUNoYW5nZWQiLCJjbGVhcmFibGUiLCJmdWxsVGV4dFNlYXJjaCIsImZvcmNlU2VsZWN0aW9uIiwidXNlcklkIiwibG9hZFVzZXJHcm91cCIsImxvYWREZWZhdWx0R3JvdXAiLCJkb2N1bWVudCIsIm9uIiwiZXZlbnQiLCJmb3JtRGF0YSIsInBvcHVsYXRlZFVzZXJJZCIsInVwZGF0ZURyb3Bkb3duVmFsdWUiLCJTdHJpbmciLCJhamF4IiwidXJsIiwibWV0aG9kIiwiZGF0YVR5cGUiLCJzdWNjZXNzIiwicmVzcG9uc2UiLCJyZXN1bHQiLCJkYXRhIiwiZ3JvdXBfaWQiLCJlcnJvciIsInhociIsInN0YXR1cyIsImNvbnNvbGUiLCJ1c2VyX2lkIiwicmVhZHkiXSwibWFwcGluZ3MiOiI7O0FBQUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBLElBQU1BLGtDQUFrQyxHQUFHO0FBQ3ZDO0FBQ0o7QUFDQTtBQUNJQyxFQUFBQSxTQUFTLEVBQUVDLENBQUMsQ0FBQyxrQ0FBRCxDQUoyQjs7QUFNdkM7QUFDSjtBQUNBO0FBQ0lDLEVBQUFBLFlBQVksRUFBRUQsQ0FBQyxDQUFDLHlCQUFELENBVHdCOztBQVd2QztBQUNKO0FBQ0E7QUFDSUUsRUFBQUEsVUFkdUMsd0JBYzFCO0FBQ1Q7QUFDQSxRQUFJSixrQ0FBa0MsQ0FBQ0MsU0FBbkMsQ0FBNkNJLE1BQTdDLEtBQXdELENBQTVELEVBQStEO0FBQzNEO0FBQ0gsS0FKUSxDQU1UOzs7QUFDQUwsSUFBQUEsa0NBQWtDLENBQUNDLFNBQW5DLENBQTZDSyxRQUE3QyxDQUFzRDtBQUNsREMsTUFBQUEsUUFEa0Qsb0JBQ3pDQyxLQUR5QyxFQUNsQztBQUNaO0FBQ0FSLFFBQUFBLGtDQUFrQyxDQUFDRyxZQUFuQyxDQUFnRE0sR0FBaEQsQ0FBb0RELEtBQXBELEVBRlksQ0FJWjs7QUFDQSxZQUFJLE9BQU9FLElBQVAsS0FBZ0IsV0FBaEIsSUFBK0IsT0FBT0EsSUFBSSxDQUFDQyxXQUFaLEtBQTRCLFVBQS9ELEVBQTJFO0FBQ3ZFRCxVQUFBQSxJQUFJLENBQUNDLFdBQUw7QUFDSDtBQUNKLE9BVGlEO0FBVWxEQyxNQUFBQSxTQUFTLEVBQUUsS0FWdUM7QUFXbERDLE1BQUFBLGNBQWMsRUFBRSxJQVhrQztBQVlsREMsTUFBQUEsY0FBYyxFQUFFO0FBWmtDLEtBQXRELEVBUFMsQ0FzQlQ7QUFDQTtBQUNBOztBQUNBLFFBQU1DLE1BQU0sR0FBR2IsQ0FBQyxDQUFDLFVBQUQsQ0FBRCxDQUFjTyxHQUFkLE1BQXVCUCxDQUFDLENBQUMsS0FBRCxDQUFELENBQVNPLEdBQVQsRUFBdEM7O0FBRUEsUUFBSU0sTUFBTSxJQUFJQSxNQUFNLEtBQUssRUFBckIsSUFBMkJBLE1BQU0sS0FBSyxLQUExQyxFQUFpRDtBQUM3QztBQUNBZixNQUFBQSxrQ0FBa0MsQ0FBQ2dCLGFBQW5DLENBQWlERCxNQUFqRDtBQUNILEtBSEQsTUFHTztBQUNIO0FBQ0FmLE1BQUFBLGtDQUFrQyxDQUFDaUIsZ0JBQW5DO0FBQ0gsS0FqQ1EsQ0FtQ1Q7OztBQUNBZixJQUFBQSxDQUFDLENBQUNnQixRQUFELENBQUQsQ0FBWUMsRUFBWixDQUFlLGVBQWYsRUFBZ0MsVUFBQ0MsS0FBRCxFQUFRQyxRQUFSLEVBQXFCO0FBQ2pEO0FBQ0EsVUFBTUMsZUFBZSxHQUFHcEIsQ0FBQyxDQUFDLEtBQUQsQ0FBRCxDQUFTTyxHQUFULEVBQXhCOztBQUVBLFVBQUlhLGVBQWUsSUFBSUEsZUFBZSxLQUFLLEVBQXZDLElBQTZDQSxlQUFlLEtBQUssS0FBckUsRUFBNEU7QUFDeEU7QUFDQXRCLFFBQUFBLGtDQUFrQyxDQUFDZ0IsYUFBbkMsQ0FBaURNLGVBQWpEO0FBQ0g7QUFDSixLQVJEO0FBU0gsR0EzRHNDOztBQTZEdkM7QUFDSjtBQUNBO0FBQ0lDLEVBQUFBLG1CQWhFdUMsaUNBZ0VqQjtBQUNsQixRQUFNZixLQUFLLEdBQUdSLGtDQUFrQyxDQUFDRyxZQUFuQyxDQUFnRE0sR0FBaEQsRUFBZDs7QUFFQSxRQUFJRCxLQUFLLElBQUlBLEtBQUssS0FBSyxFQUF2QixFQUEyQjtBQUN2QlIsTUFBQUEsa0NBQWtDLENBQUNDLFNBQW5DLENBQTZDSyxRQUE3QyxDQUFzRCxjQUF0RCxFQUFzRWtCLE1BQU0sQ0FBQ2hCLEtBQUQsQ0FBNUU7QUFDSDtBQUNKLEdBdEVzQzs7QUF3RXZDO0FBQ0o7QUFDQTtBQUNJUyxFQUFBQSxnQkEzRXVDLDhCQTJFcEI7QUFDZmYsSUFBQUEsQ0FBQyxDQUFDdUIsSUFBRixDQUFPO0FBQ0hDLE1BQUFBLEdBQUcsRUFBRSx3REFERjtBQUVIQyxNQUFBQSxNQUFNLEVBQUUsTUFGTDtBQUdIQyxNQUFBQSxRQUFRLEVBQUUsTUFIUDtBQUlIQyxNQUFBQSxPQUpHLG1CQUlLQyxRQUpMLEVBSWU7QUFDZCxZQUFJQSxRQUFRLENBQUNDLE1BQVQsS0FBb0IsSUFBcEIsSUFBNEJELFFBQVEsQ0FBQ0UsSUFBckMsSUFBNkNGLFFBQVEsQ0FBQ0UsSUFBVCxDQUFjQyxRQUEvRCxFQUF5RTtBQUNyRTtBQUNBakMsVUFBQUEsa0NBQWtDLENBQUNHLFlBQW5DLENBQWdETSxHQUFoRCxDQUFvRHFCLFFBQVEsQ0FBQ0UsSUFBVCxDQUFjQyxRQUFsRSxFQUZxRSxDQUlyRTs7QUFDQWpDLFVBQUFBLGtDQUFrQyxDQUFDdUIsbUJBQW5DO0FBQ0g7QUFDSixPQVpFO0FBYUhXLE1BQUFBLEtBYkcsaUJBYUdDLEdBYkgsRUFhUUMsTUFiUixFQWFnQkYsTUFiaEIsRUFhdUI7QUFDdEJHLFFBQUFBLE9BQU8sQ0FBQ0gsS0FBUixDQUFjLGlEQUFkLEVBQWlFQSxNQUFqRTtBQUNIO0FBZkUsS0FBUDtBQWlCSCxHQTdGc0M7O0FBK0Z2QztBQUNKO0FBQ0E7QUFDQTtBQUNJbEIsRUFBQUEsYUFuR3VDLHlCQW1HekJELE1Bbkd5QixFQW1HakI7QUFDbEIsUUFBSSxDQUFDQSxNQUFMLEVBQWE7QUFDVDtBQUNBZixNQUFBQSxrQ0FBa0MsQ0FBQ2lCLGdCQUFuQztBQUNBO0FBQ0g7O0FBRURmLElBQUFBLENBQUMsQ0FBQ3VCLElBQUYsQ0FBTztBQUNIQyxNQUFBQSxHQUFHLEVBQUUscURBREY7QUFFSEMsTUFBQUEsTUFBTSxFQUFFLE1BRkw7QUFHSEMsTUFBQUEsUUFBUSxFQUFFLE1BSFA7QUFJSEksTUFBQUEsSUFBSSxFQUFFO0FBQUNNLFFBQUFBLE9BQU8sRUFBRXZCO0FBQVYsT0FKSDtBQUtIYyxNQUFBQSxPQUxHLG1CQUtLQyxRQUxMLEVBS2U7QUFDZCxZQUFJQSxRQUFRLENBQUNDLE1BQVQsS0FBb0IsSUFBcEIsSUFBNEJELFFBQVEsQ0FBQ0UsSUFBckMsSUFBNkNGLFFBQVEsQ0FBQ0UsSUFBVCxDQUFjQyxRQUEvRCxFQUF5RTtBQUNyRTtBQUNBakMsVUFBQUEsa0NBQWtDLENBQUNHLFlBQW5DLENBQWdETSxHQUFoRCxDQUFvRHFCLFFBQVEsQ0FBQ0UsSUFBVCxDQUFjQyxRQUFsRSxFQUZxRSxDQUlyRTs7QUFDQWpDLFVBQUFBLGtDQUFrQyxDQUFDdUIsbUJBQW5DO0FBQ0g7QUFDSixPQWJFO0FBY0hXLE1BQUFBLEtBZEcsaUJBY0dDLEdBZEgsRUFjUUMsTUFkUixFQWNnQkYsT0FkaEIsRUFjdUI7QUFDdEJHLFFBQUFBLE9BQU8sQ0FBQ0gsS0FBUixDQUFjLDhDQUFkLEVBQThEQSxPQUE5RDtBQUNIO0FBaEJFLEtBQVA7QUFrQkg7QUE1SHNDLENBQTNDLEMsQ0ErSEE7O0FBQ0FoQyxDQUFDLENBQUNnQixRQUFELENBQUQsQ0FBWXFCLEtBQVosQ0FBa0IsWUFBTTtBQUNwQnZDLEVBQUFBLGtDQUFrQyxDQUFDSSxVQUFuQztBQUNILENBRkQiLCJzb3VyY2VzQ29udGVudCI6WyIvKlxuICogTWlrb1BCWCAtIGZyZWUgcGhvbmUgc3lzdGVtIGZvciBzbWFsbCBidXNpbmVzc1xuICogQ29weXJpZ2h0IMKpIDIwMTctMjAyMyBBbGV4ZXkgUG9ydG5vdiBhbmQgTmlrb2xheSBCZWtldG92XG4gKlxuICogVGhpcyBwcm9ncmFtIGlzIGZyZWUgc29mdHdhcmU6IHlvdSBjYW4gcmVkaXN0cmlidXRlIGl0IGFuZC9vciBtb2RpZnlcbiAqIGl0IHVuZGVyIHRoZSB0ZXJtcyBvZiB0aGUgR05VIEdlbmVyYWwgUHVibGljIExpY2Vuc2UgYXMgcHVibGlzaGVkIGJ5XG4gKiB0aGUgRnJlZSBTb2Z0d2FyZSBGb3VuZGF0aW9uOyBlaXRoZXIgdmVyc2lvbiAzIG9mIHRoZSBMaWNlbnNlLCBvclxuICogKGF0IHlvdXIgb3B0aW9uKSBhbnkgbGF0ZXIgdmVyc2lvbi5cbiAqXG4gKiBUaGlzIHByb2dyYW0gaXMgZGlzdHJpYnV0ZWQgaW4gdGhlIGhvcGUgdGhhdCBpdCB3aWxsIGJlIHVzZWZ1bCxcbiAqIGJ1dCBXSVRIT1VUIEFOWSBXQVJSQU5UWTsgd2l0aG91dCBldmVuIHRoZSBpbXBsaWVkIHdhcnJhbnR5IG9mXG4gKiBNRVJDSEFOVEFCSUxJVFkgb3IgRklUTkVTUyBGT1IgQSBQQVJUSUNVTEFSIFBVUlBPU0UuICBTZWUgdGhlXG4gKiBHTlUgR2VuZXJhbCBQdWJsaWMgTGljZW5zZSBmb3IgbW9yZSBkZXRhaWxzLlxuICpcbiAqIFlvdSBzaG91bGQgaGF2ZSByZWNlaXZlZCBhIGNvcHkgb2YgdGhlIEdOVSBHZW5lcmFsIFB1YmxpYyBMaWNlbnNlIGFsb25nIHdpdGggdGhpcyBwcm9ncmFtLlxuICogSWYgbm90LCBzZWUgPGh0dHBzOi8vd3d3LmdudS5vcmcvbGljZW5zZXMvPi5cbiAqL1xuXG4vKiBnbG9iYWwgZ2xvYmFsUm9vdFVybCwgUGJ4QXBpICovXG5cbi8qKlxuICogTW9kdWxlVXNlcnNHcm91cHNFeHRlbnNpb25Ecm9wZG93biAtIEluaXRpYWxpemUgdXNlciBncm91cHMgZHJvcGRvd24gb24gZXh0ZW5zaW9uIGVkaXQgcGFnZVxuICogV29ya3Mgd2l0aCBib3RoIG9sZCBhbmQgbmV3IE1pa29QQlggcmVsZWFzZXMgdmlhIEZvcm1Qb3B1bGF0ZWQgZXZlbnRcbiAqL1xuY29uc3QgTW9kdWxlVXNlcnNHcm91cHNFeHRlbnNpb25Ecm9wZG93biA9IHtcbiAgICAvKipcbiAgICAgKiBqUXVlcnkgb2JqZWN0IGZvciB0aGUgZHJvcGRvd25cbiAgICAgKi9cbiAgICAkZHJvcGRvd246ICQoJyNtb2RfdXNyZ3Jfc2VsZWN0X2dyb3VwX2Ryb3Bkb3duJyksXG5cbiAgICAvKipcbiAgICAgKiBIaWRkZW4gaW5wdXQgZmllbGQgZm9yIHN0b3Jpbmcgc2VsZWN0ZWQgdmFsdWVcbiAgICAgKi9cbiAgICAkaGlkZGVuSW5wdXQ6ICQoJyNtb2RfdXNyZ3Jfc2VsZWN0X2dyb3VwJyksXG5cbiAgICAvKipcbiAgICAgKiBJbml0aWFsaXplIHRoZSBkcm9wZG93blxuICAgICAqL1xuICAgIGluaXRpYWxpemUoKSB7XG4gICAgICAgIC8vIENoZWNrIGlmIGRyb3Bkb3duIGVsZW1lbnQgZXhpc3RzXG4gICAgICAgIGlmIChNb2R1bGVVc2Vyc0dyb3Vwc0V4dGVuc2lvbkRyb3Bkb3duLiRkcm9wZG93bi5sZW5ndGggPT09IDApIHtcbiAgICAgICAgICAgIHJldHVybjtcbiAgICAgICAgfVxuXG4gICAgICAgIC8vIEluaXRpYWxpemUgRm9tYW50aWMgVUkgZHJvcGRvd24gKGl0ZW1zIGFyZSBhbHJlYWR5IGluIEhUTUwgZnJvbSBzZXJ2ZXIpXG4gICAgICAgIE1vZHVsZVVzZXJzR3JvdXBzRXh0ZW5zaW9uRHJvcGRvd24uJGRyb3Bkb3duLmRyb3Bkb3duKHtcbiAgICAgICAgICAgIG9uQ2hhbmdlKHZhbHVlKSB7XG4gICAgICAgICAgICAgICAgLy8gU3luYyB2YWx1ZSB0byBoaWRkZW4gaW5wdXRcbiAgICAgICAgICAgICAgICBNb2R1bGVVc2Vyc0dyb3Vwc0V4dGVuc2lvbkRyb3Bkb3duLiRoaWRkZW5JbnB1dC52YWwodmFsdWUpO1xuXG4gICAgICAgICAgICAgICAgLy8gVHJpZ2dlciBmb3JtIGNoYW5nZSB0byBlbmFibGUgc2F2ZSBidXR0b25cbiAgICAgICAgICAgICAgICBpZiAodHlwZW9mIEZvcm0gIT09ICd1bmRlZmluZWQnICYmIHR5cGVvZiBGb3JtLmRhdGFDaGFuZ2VkID09PSAnZnVuY3Rpb24nKSB7XG4gICAgICAgICAgICAgICAgICAgIEZvcm0uZGF0YUNoYW5nZWQoKTtcbiAgICAgICAgICAgICAgICB9XG4gICAgICAgICAgICB9LFxuICAgICAgICAgICAgY2xlYXJhYmxlOiBmYWxzZSxcbiAgICAgICAgICAgIGZ1bGxUZXh0U2VhcmNoOiB0cnVlLFxuICAgICAgICAgICAgZm9yY2VTZWxlY3Rpb246IGZhbHNlXG4gICAgICAgIH0pO1xuXG4gICAgICAgIC8vIFNtYXJ0IGluaXRpYWxpemF0aW9uIC0gY2hlY2sgaWYgdXNlcl9pZCBvciBpZCBleGlzdHMgaW4gRE9NXG4gICAgICAgIC8vIE9sZCByZWxlYXNlOiA8aW5wdXQgbmFtZT1cInVzZXJfaWRcIj5cbiAgICAgICAgLy8gTmV3IHJlbGVhc2U6IDxpbnB1dCBuYW1lPVwiaWRcIj5cbiAgICAgICAgY29uc3QgdXNlcklkID0gJCgnI3VzZXJfaWQnKS52YWwoKSB8fCAkKCcjaWQnKS52YWwoKTtcblxuICAgICAgICBpZiAodXNlcklkICYmIHVzZXJJZCAhPT0gJycgJiYgdXNlcklkICE9PSAnbmV3Jykge1xuICAgICAgICAgICAgLy8gRXhpc3RpbmcgdXNlciAtIGxvYWQgdGhlaXIgZ3JvdXBcbiAgICAgICAgICAgIE1vZHVsZVVzZXJzR3JvdXBzRXh0ZW5zaW9uRHJvcGRvd24ubG9hZFVzZXJHcm91cCh1c2VySWQpO1xuICAgICAgICB9IGVsc2Uge1xuICAgICAgICAgICAgLy8gTmV3IHVzZXIgLSBsb2FkIGRlZmF1bHQgZ3JvdXBcbiAgICAgICAgICAgIE1vZHVsZVVzZXJzR3JvdXBzRXh0ZW5zaW9uRHJvcGRvd24ubG9hZERlZmF1bHRHcm91cCgpO1xuICAgICAgICB9XG5cbiAgICAgICAgLy8gTGlzdGVuIGZvciBmb3JtIHBvcHVsYXRpb24gZXZlbnQgKGZvciBuZXcgcmVsZWFzZSB3aXRoIEFKQVggZm9ybXMpXG4gICAgICAgICQoZG9jdW1lbnQpLm9uKCdGb3JtUG9wdWxhdGVkJywgKGV2ZW50LCBmb3JtRGF0YSkgPT4ge1xuICAgICAgICAgICAgLy8gR2V0IHVzZXIgSUQgZnJvbSBET00gKG5ldyByZWxlYXNlIGFsd2F5cyB1c2VzICdpZCcpXG4gICAgICAgICAgICBjb25zdCBwb3B1bGF0ZWRVc2VySWQgPSAkKCcjaWQnKS52YWwoKTtcblxuICAgICAgICAgICAgaWYgKHBvcHVsYXRlZFVzZXJJZCAmJiBwb3B1bGF0ZWRVc2VySWQgIT09ICcnICYmIHBvcHVsYXRlZFVzZXJJZCAhPT0gJ25ldycpIHtcbiAgICAgICAgICAgICAgICAvLyBMb2FkIHVzZXIncyBhY3R1YWwgZ3JvdXAgKHdpbGwgb3ZlcnJpZGUgZGVmYXVsdClcbiAgICAgICAgICAgICAgICBNb2R1bGVVc2Vyc0dyb3Vwc0V4dGVuc2lvbkRyb3Bkb3duLmxvYWRVc2VyR3JvdXAocG9wdWxhdGVkVXNlcklkKTtcbiAgICAgICAgICAgIH1cbiAgICAgICAgfSk7XG4gICAgfSxcblxuICAgIC8qKlxuICAgICAqIFVwZGF0ZSBkcm9wZG93biB2YWx1ZSBmcm9tIGhpZGRlbiBpbnB1dFxuICAgICAqL1xuICAgIHVwZGF0ZURyb3Bkb3duVmFsdWUoKSB7XG4gICAgICAgIGNvbnN0IHZhbHVlID0gTW9kdWxlVXNlcnNHcm91cHNFeHRlbnNpb25Ecm9wZG93bi4kaGlkZGVuSW5wdXQudmFsKCk7XG5cbiAgICAgICAgaWYgKHZhbHVlICYmIHZhbHVlICE9PSAnJykge1xuICAgICAgICAgICAgTW9kdWxlVXNlcnNHcm91cHNFeHRlbnNpb25Ecm9wZG93bi4kZHJvcGRvd24uZHJvcGRvd24oJ3NldCBzZWxlY3RlZCcsIFN0cmluZyh2YWx1ZSkpO1xuICAgICAgICB9XG4gICAgfSxcblxuICAgIC8qKlxuICAgICAqIExvYWQgZGVmYXVsdCBncm91cCBmcm9tIG1vZHVsZSBBUElcbiAgICAgKi9cbiAgICBsb2FkRGVmYXVsdEdyb3VwKCkge1xuICAgICAgICAkLmFqYXgoe1xuICAgICAgICAgICAgdXJsOiAnL3BieGNvcmUvYXBpL21vZHVsZXMvTW9kdWxlVXNlcnNHcm91cHMvZ2V0RGVmYXVsdEdyb3VwJyxcbiAgICAgICAgICAgIG1ldGhvZDogJ1BPU1QnLFxuICAgICAgICAgICAgZGF0YVR5cGU6ICdqc29uJyxcbiAgICAgICAgICAgIHN1Y2Nlc3MocmVzcG9uc2UpIHtcbiAgICAgICAgICAgICAgICBpZiAocmVzcG9uc2UucmVzdWx0ID09PSB0cnVlICYmIHJlc3BvbnNlLmRhdGEgJiYgcmVzcG9uc2UuZGF0YS5ncm91cF9pZCkge1xuICAgICAgICAgICAgICAgICAgICAvLyBTZXQgdmFsdWUgdG8gaGlkZGVuIGlucHV0XG4gICAgICAgICAgICAgICAgICAgIE1vZHVsZVVzZXJzR3JvdXBzRXh0ZW5zaW9uRHJvcGRvd24uJGhpZGRlbklucHV0LnZhbChyZXNwb25zZS5kYXRhLmdyb3VwX2lkKTtcblxuICAgICAgICAgICAgICAgICAgICAvLyBVcGRhdGUgZHJvcGRvd25cbiAgICAgICAgICAgICAgICAgICAgTW9kdWxlVXNlcnNHcm91cHNFeHRlbnNpb25Ecm9wZG93bi51cGRhdGVEcm9wZG93blZhbHVlKCk7XG4gICAgICAgICAgICAgICAgfVxuICAgICAgICAgICAgfSxcbiAgICAgICAgICAgIGVycm9yKHhociwgc3RhdHVzLCBlcnJvcikge1xuICAgICAgICAgICAgICAgIGNvbnNvbGUuZXJyb3IoJ01vZHVsZVVzZXJzR3JvdXBzOiBGYWlsZWQgdG8gbG9hZCBkZWZhdWx0IGdyb3VwJywgZXJyb3IpO1xuICAgICAgICAgICAgfVxuICAgICAgICB9KTtcbiAgICB9LFxuXG4gICAgLyoqXG4gICAgICogTG9hZCB1c2VyJ3MgZ3JvdXAgZnJvbSBtb2R1bGUgQVBJXG4gICAgICogQHBhcmFtIHtzdHJpbmd8bnVtYmVyfSB1c2VySWQgLSBVc2VyIElEXG4gICAgICovXG4gICAgbG9hZFVzZXJHcm91cCh1c2VySWQpIHtcbiAgICAgICAgaWYgKCF1c2VySWQpIHtcbiAgICAgICAgICAgIC8vIEZvciBuZXcgdXNlcnMsIGxvYWQgZGVmYXVsdCBncm91cFxuICAgICAgICAgICAgTW9kdWxlVXNlcnNHcm91cHNFeHRlbnNpb25Ecm9wZG93bi5sb2FkRGVmYXVsdEdyb3VwKCk7XG4gICAgICAgICAgICByZXR1cm47XG4gICAgICAgIH1cblxuICAgICAgICAkLmFqYXgoe1xuICAgICAgICAgICAgdXJsOiAnL3BieGNvcmUvYXBpL21vZHVsZXMvTW9kdWxlVXNlcnNHcm91cHMvZ2V0VXNlckdyb3VwJyxcbiAgICAgICAgICAgIG1ldGhvZDogJ1BPU1QnLFxuICAgICAgICAgICAgZGF0YVR5cGU6ICdqc29uJyxcbiAgICAgICAgICAgIGRhdGE6IHt1c2VyX2lkOiB1c2VySWR9LFxuICAgICAgICAgICAgc3VjY2VzcyhyZXNwb25zZSkge1xuICAgICAgICAgICAgICAgIGlmIChyZXNwb25zZS5yZXN1bHQgPT09IHRydWUgJiYgcmVzcG9uc2UuZGF0YSAmJiByZXNwb25zZS5kYXRhLmdyb3VwX2lkKSB7XG4gICAgICAgICAgICAgICAgICAgIC8vIFNldCB2YWx1ZSB0byBoaWRkZW4gaW5wdXRcbiAgICAgICAgICAgICAgICAgICAgTW9kdWxlVXNlcnNHcm91cHNFeHRlbnNpb25Ecm9wZG93bi4kaGlkZGVuSW5wdXQudmFsKHJlc3BvbnNlLmRhdGEuZ3JvdXBfaWQpO1xuXG4gICAgICAgICAgICAgICAgICAgIC8vIFVwZGF0ZSBkcm9wZG93blxuICAgICAgICAgICAgICAgICAgICBNb2R1bGVVc2Vyc0dyb3Vwc0V4dGVuc2lvbkRyb3Bkb3duLnVwZGF0ZURyb3Bkb3duVmFsdWUoKTtcbiAgICAgICAgICAgICAgICB9XG4gICAgICAgICAgICB9LFxuICAgICAgICAgICAgZXJyb3IoeGhyLCBzdGF0dXMsIGVycm9yKSB7XG4gICAgICAgICAgICAgICAgY29uc29sZS5lcnJvcignTW9kdWxlVXNlcnNHcm91cHM6IEZhaWxlZCB0byBsb2FkIHVzZXIgZ3JvdXAnLCBlcnJvcik7XG4gICAgICAgICAgICB9XG4gICAgICAgIH0pO1xuICAgIH1cbn07XG5cbi8vIEluaXRpYWxpemUgd2hlbiBkb2N1bWVudCBpcyByZWFkeVxuJChkb2N1bWVudCkucmVhZHkoKCkgPT4ge1xuICAgIE1vZHVsZVVzZXJzR3JvdXBzRXh0ZW5zaW9uRHJvcGRvd24uaW5pdGlhbGl6ZSgpO1xufSk7XG4iXX0= \ No newline at end of file diff --git a/public/assets/js/module-users-groups-index.js b/public/assets/js/module-users-groups-index.js index 6619530..20531f8 100644 --- a/public/assets/js/module-users-groups-index.js +++ b/public/assets/js/module-users-groups-index.js @@ -49,6 +49,12 @@ var ModuleCGIndex = { */ $selectGroup: $('.select-group'), + /** + * jQuery object for default group dropdown. + * @type {jQuery} + */ + $defaultGroupDropdown: $('.select-default-group'), + /** * jQuery object for current form disability fields * @type {jQuery} @@ -76,6 +82,10 @@ var ModuleCGIndex = { ModuleCGIndex.$selectGroup.dropdown({ onChange: ModuleCGIndex.changeGroupInList + }); // Initialize default group dropdown + + ModuleCGIndex.$defaultGroupDropdown.dropdown({ + onChange: ModuleCGIndex.changeDefaultGroup }); }, @@ -153,8 +163,9 @@ var ModuleCGIndex = { user_id: $($choice).closest('tr').attr('id'), group_id: value }, - onSuccess: function onSuccess() {// ModuleCGIndex.initializeDataTable(); - // console.log('updated'); + onSuccess: function onSuccess() { + // Update group member counters after successful group change + ModuleCGIndex.updateGroupCounters(); }, onError: function onError(response) { console.log(response); @@ -162,6 +173,73 @@ var ModuleCGIndex = { }); }, + /** + * Update group member counters via API + */ + updateGroupCounters: function updateGroupCounters() { + $.ajax({ + url: '/pbxcore/api/modules/ModuleUsersGroups/getGroupsStats', + method: 'GET', + dataType: 'json', + success: function success(response) { + if (response.result === true && response.data && response.data.stats) { + var stats = response.data.stats; // Update each group's member count in the table + + Object.keys(stats).forEach(function (groupId) { + var count = stats[groupId]; + var $row = $("#users-groups-table tr#".concat(groupId)); + + if ($row.length > 0) { + // Find the counter cell (second td with center aligned class) + $row.find('td.center.aligned').first().text(count); + } + }); + } + }, + error: function error(xhr, status, _error) { + console.error('ModuleUsersGroups: Failed to update group counters', _error); + } + }); + }, + + /** + * Handles default group change. + * @param {string} value - The new default group value. + * @param {string} text - The new group text. + * @param {jQuery} $choice - The selected choice. + */ + changeDefaultGroup: function changeDefaultGroup(value, text, $choice) { + if (!value || value === '') { + return; + } // Add loading state to dropdown + + + ModuleCGIndex.$defaultGroupDropdown.addClass('loading'); + $.ajax({ + url: '/pbxcore/api/modules/ModuleUsersGroups/setDefaultGroup', + method: 'POST', + dataType: 'json', + data: { + group_id: value + }, + success: function success(response) { + if (response.result !== true) { + // Show error notification only on failure + var errorMessage = response.messages && response.messages.length > 0 ? response.messages.join(', ') : 'Failed to update default group'; + UserMessage.showError(errorMessage); + } + }, + error: function error(xhr, status, _error2) { + console.error('ModuleUsersGroups: Failed to set default group', _error2); + UserMessage.showError('Failed to update default group'); + }, + complete: function complete() { + // Remove loading state from dropdown + ModuleCGIndex.$defaultGroupDropdown.removeClass('loading'); + } + }); + }, + /** * Calculate data table page length * @@ -185,4 +263,4 @@ var ModuleCGIndex = { $(document).ready(function () { ModuleCGIndex.initialize(); }); -//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbInNyYy9tb2R1bGUtdXNlcnMtZ3JvdXBzLWluZGV4LmpzIl0sIm5hbWVzIjpbIk1vZHVsZUNHSW5kZXgiLCIkc3RhdHVzVG9nZ2xlIiwiJCIsIiR1c2Vyc1RhYmxlIiwidXNlckRhdGFUYWJsZSIsIiRzZWxlY3RHcm91cCIsIiRkaXNhYmlsaXR5RmllbGRzIiwiaW5pdGlhbGl6ZSIsInRhYiIsImNoZWNrU3RhdHVzVG9nZ2xlIiwid2luZG93IiwiYWRkRXZlbnRMaXN0ZW5lciIsImluaXRpYWxpemVVc2Vyc0RhdGFUYWJsZSIsImVhY2giLCJpbmRleCIsIm9iaiIsImRyb3Bkb3duIiwidmFsdWVzIiwibWFrZURyb3Bkb3duTGlzdCIsImF0dHIiLCJvbkNoYW5nZSIsImNoYW5nZUdyb3VwSW5MaXN0Iiwib25WaXNpYmxlIiwiZGF0YSIsIm5ld1BhZ2VMZW5ndGgiLCJjYWxjdWxhdGVQYWdlTGVuZ3RoIiwicGFnZSIsImxlbiIsImRyYXciLCJEYXRhVGFibGUiLCJsZW5ndGhDaGFuZ2UiLCJwYWdpbmciLCJwYWdlTGVuZ3RoIiwic2Nyb2xsQ29sbGFwc2UiLCJjb2x1bW5zIiwib3JkZXIiLCJsYW5ndWFnZSIsIlNlbWFudGljTG9jYWxpemF0aW9uIiwiZGF0YVRhYmxlTG9jYWxpc2F0aW9uIiwiY2hlY2tib3giLCJyZW1vdmVDbGFzcyIsImFkZENsYXNzIiwic2VsZWN0ZWQiLCJ2YWx1ZSIsInB1c2giLCJuYW1lIiwidGV4dCIsIiRjaG9pY2UiLCJhcGkiLCJ1cmwiLCJnbG9iYWxSb290VXJsIiwib24iLCJtZXRob2QiLCJ1c2VyX2lkIiwiY2xvc2VzdCIsImdyb3VwX2lkIiwib25TdWNjZXNzIiwib25FcnJvciIsInJlc3BvbnNlIiwiY29uc29sZSIsImxvZyIsInJvd0hlaWdodCIsImZpbmQiLCJmaXJzdCIsIm91dGVySGVpZ2h0Iiwid2luZG93SGVpZ2h0IiwiaW5uZXJIZWlnaHQiLCJoZWFkZXJGb290ZXJIZWlnaHQiLCJNYXRoIiwibWF4IiwiZmxvb3Ii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akQsQ0FBVCxFQUFzRSxFQUF0RSxDQUFQO0FBQ0E7QUFwS29CLENBQXRCO0FBdUtBO0FBQ0E7QUFDQTs7QUFDQTNELENBQUMsQ0FBQ3FFLFFBQUQsQ0FBRCxDQUFZQyxLQUFaLENBQWtCLFlBQU07QUFDdkJ4RSxFQUFBQSxhQUFhLENBQUNPLFVBQWQ7QUFDQSxDQUZEIiwic291cmNlc0NvbnRlbnQiOlsiLypcbiAqIE1pa29QQlggLSBmcmVlIHBob25lIHN5c3RlbSBmb3Igc21hbGwgYnVzaW5lc3NcbiAqIENvcHlyaWdodCDCqSAyMDE3LTIwMjMgQWxleGV5IFBvcnRub3YgYW5kIE5pa29sYXkgQmVrZXRvdlxuICpcbiAqIFRoaXMgcHJvZ3JhbSBpcyBmcmVlIHNvZnR3YXJlOiB5b3UgY2FuIHJlZGlzdHJpYnV0ZSBpdCBhbmQvb3IgbW9kaWZ5XG4gKiBpdCB1bmRlciB0aGUgdGVybXMgb2YgdGhlIEdOVSBHZW5lcmFsIFB1YmxpYyBMaWNlbnNlIGFzIHB1Ymxpc2hlZCBieVxuICogdGhlIEZyZWUgU29mdHdhcmUgRm91bmRhdGlvbjsgZWl0aGVyIHZlcnNpb24gMyBvZiB0aGUgTGljZW5zZSwgb3JcbiAqIChhdCB5b3VyIG9wdGlvbikgYW55IGxhdGVyIHZlcnNpb24uXG4gKlxuICogVGhpcyBwcm9ncmFtIGlzIGRpc3RyaWJ1dGVkIGluIHRoZSBob3BlIHRoYXQgaXQgd2lsbCBiZSB1c2VmdWwsXG4gKiBidXQgV0lUSE9VVCBBTlkgV0FSUkFOVFk7IHdpdGhvdXQgZXZlbiB0aGUgaW1wbGllZCB3YXJyYW50eSBvZlxuICogTUVSQ0hBTlRBQklMSVRZIG9yIEZJVE5FU1MgRk9SIEEgUEFSVElDVUxBUiBQVVJQT1NFLiAgU2VlIHRoZVxuICogR05VIEdlbmVyYWwgUHVibGljIExpY2Vuc2UgZm9yIG1vcmUgZGV0YWlscy5cbiAqXG4gKiBZb3Ugc2hvdWxkIGhhdmUgcmVjZWl2ZWQgYSBjb3B5IG9mIHRoZSBHTlUgR2VuZXJhbCBQdWJsaWMgTGljZW5zZSBhbG9uZyB3aXRoIHRoaXMgcHJvZ3JhbS5cbiAqIElmIG5vdCwgc2VlIDxodHRwczovL3d3dy5nbnUub3JnL2xpY2Vuc2VzLz4uXG4gKi9cblxuLyogZ2xvYmFsIFNlbWFudGljTG9jYWxpemF0aW9uLCBnbG9iYWxSb290VXJsLCBEYXRhdGFibGUgKi9cblxuLyoqXG4gKiBNb2R1bGUgZm9yIG1hbmFnaW5nIGNhbGwgZ3JvdXBzIGFuZCByZWxhdGVkIGZ1bmN0aW9uYWxpdHkuXG4gKiBAbmFtZXNwYWNlXG4gKi9cbmNvbnN0IE1vZHVsZUNHSW5kZXggPSB7XG5cdC8qKlxuXHQgKiBqUXVlcnkgb2JqZWN0IGZvciB0aGUgc3RhdHVzIHRvZ2dsZS5cblx0ICogQHR5cGUge2pRdWVyeX1cblx0ICovXG5cdCRzdGF0dXNUb2dnbGU6ICQoJyNtb2R1bGUtc3RhdHVzLXRvZ2dsZScpLFxuXG5cdC8qKlxuXHQgKiBqUXVlcnkgb2JqZWN0IGZvciB0aGUgdXNlcnMgdGFibGUuXG5cdCAqIEB0eXBlIHtqUXVlcnl9XG5cdCAqL1xuXHQkdXNlcnNUYWJsZTogJCgnI3VzZXJzLXRhYmxlJyksXG5cblx0LyoqXG5cdCAqIFVzZXIgZGF0YSB0YWJsZS5cblx0ICogQHR5cGUge0RhdGF0YWJsZX1cblx0ICovXG5cdHVzZXJEYXRhVGFibGU6IG51bGwsXG5cblx0LyoqXG5cdCAqIGpRdWVyeSBvYmplY3QgZm9yIHNlbGVjdCBncm91cCBlbGVtZW50cy5cblx0ICogQHR5cGUge2pRdWVyeX1cblx0ICovXG5cdCRzZWxlY3RHcm91cDogJCgnLnNlbGVjdC1ncm91cCcpLFxuXG5cdC8qKlxuXHQgKiBqUXVlcnkgb2JqZWN0IGZvciBjdXJyZW50IGZvcm0gZGlzYWJpbGl0eSBmaWVsZHNcblx0ICogQHR5cGUge2pRdWVyeX1cblx0ICovXG5cdCRkaXNhYmlsaXR5RmllbGRzOiAkKCcjbW9kdWxlLXVzZXJzLWdyb3VwcycpLFxuXHQvKipcblx0ICogSW5pdGlhbGl6ZXMgdGhlIG1vZHVsZS5cblx0ICovXG5cdGluaXRpYWxpemUoKSB7XG5cdFx0Ly8gSW5pdGlhbGl6ZSB0YWIgbWVudVxuXHRcdCQoJyNtYWluLXVzZXJzLWdyb3Vwcy10YWItbWVudSAuaXRlbScpLnRhYigpO1xuXG5cdFx0Ly8gQ2hlY2sgc3RhdHVzIHRvZ2dsZSBpbml0aWFsbHlcblx0XHRNb2R1bGVDR0luZGV4LmNoZWNrU3RhdHVzVG9nZ2xlKCk7XG5cdFx0Ly8gQWRkIGV2ZW50IGxpc3RlbmVyIGZvciBzdGF0dXMgY2hhbmdlc1xuXHRcdHdpbmRvdy5hZGRFdmVudExpc3RlbmVyKCdNb2R1bGVTdGF0dXNDaGFuZ2VkJywgTW9kdWxlQ0dJbmRleC5jaGVja1N0YXR1c1RvZ2dsZSk7XG5cblx0XHQvLyBJbml0aWFsaXplIHVzZXJzIGRhdGEgdGFibGVcblx0XHRNb2R1bGVDR0luZGV4LmluaXRpYWxpemVVc2Vyc0RhdGFUYWJsZSgpO1xuXG5cdFx0Ly8gSW5pdGlhbGl6ZSBkcm9wZG93bnMgZm9yIHNlbGVjdCBncm91cCBlbGVtZW50c1xuXHRcdE1vZHVsZUNHSW5kZXguJHNlbGVjdEdyb3VwLmVhY2goKGluZGV4LCBvYmopID0+IHtcblx0XHRcdCQob2JqKS5kcm9wZG93bih7XG5cdFx0XHRcdHZhbHVlczogTW9kdWxlQ0dJbmRleC5tYWtlRHJvcGRvd25MaXN0KCQob2JqKS5hdHRyKCdkYXRhLXZhbHVlJykpLFxuXHRcdFx0fSk7XG5cdFx0fSk7XG5cblx0XHQvLyBJbml0aWFsaXplIGRyb3Bkb3duIGZvciBzZWxlY3QgZ3JvdXBcblx0XHRNb2R1bGVDR0luZGV4LiRzZWxlY3RHcm91cC5kcm9wZG93bih7XG5cdFx0XHRvbkNoYW5nZTogTW9kdWxlQ0dJbmRleC5jaGFuZ2VHcm91cEluTGlzdCxcblx0XHR9KTtcblxuXHR9LFxuXG5cdC8qKlxuXHQgKiBJbml0aWFsaXplcyB0aGUgRGF0YVRhYmxlIGZvciB1c2VycyB0YWJsZS5cblx0ICovXG5cdGluaXRpYWxpemVVc2Vyc0RhdGFUYWJsZSgpIHtcblxuXHRcdCQoJyNtYWluLXVzZXJzLWdyb3Vwcy10YWItbWVudSAuaXRlbScpLnRhYih7XG5cdFx0XHRvblZpc2libGUoKXtcblx0XHRcdFx0aWYgKCQodGhpcykuZGF0YSgndGFiJyk9PT0ndXNlcnMnICYmIE1vZHVsZUNHSW5kZXgudXNlckRhdGFUYWJsZSE9PW51bGwpe1xuXHRcdFx0XHRcdGNvbnN0IG5ld1BhZ2VMZW5ndGggPSBNb2R1bGVDR0luZGV4LmNhbGN1bGF0ZVBhZ2VMZW5ndGgoKTtcblx0XHRcdFx0XHRNb2R1bGVDR0luZGV4LnVzZXJEYXRhVGFibGUucGFnZS5sZW4obmV3UGFnZUxlbmd0aCkuZHJhdyhmYWxzZSk7XG5cdFx0XHRcdH1cblx0XHRcdH1cblx0XHR9KTtcblxuXHRcdE1vZHVsZUNHSW5kZXgudXNlckRhdGFUYWJsZSA9IE1vZHVsZUNHSW5kZXguJHVzZXJzVGFibGUuRGF0YVRhYmxlKHtcblx0XHRcdC8vIGRlc3Ryb3k6IHRydWUsXG5cdFx0XHRsZW5ndGhDaGFuZ2U6IGZhbHNlLFxuXHRcdFx0cGFnaW5nOiB0cnVlLFxuXHRcdFx0cGFnZUxlbmd0aDogTW9kdWxlQ0dJbmRleC5jYWxjdWxhdGVQYWdlTGVuZ3RoKCksXG5cdFx0XHRzY3JvbGxDb2xsYXBzZTogdHJ1ZSxcblx0XHRcdGNvbHVtbnM6IFtcblx0XHRcdFx0bnVsbCxcblx0XHRcdFx0bnVsbCxcblx0XHRcdFx0bnVsbCxcblx0XHRcdFx0bnVsbCxcblx0XHRcdF0sXG5cdFx0XHRvcmRlcjogWzEsICdhc2MnXSxcblx0XHRcdGxhbmd1YWdlOiBTZW1hbnRpY0xvY2FsaXphdGlvbi5kYXRhVGFibGVMb2NhbGlzYXRpb24sXG5cdFx0fSk7XG5cdH0sXG5cblx0LyoqXG5cdCAqIENoZWNrcyBhbmQgdXBkYXRlcyBidXR0b24gc3RhdHVzIGJhc2VkIG9uIG1vZHVsZSBzdGF0dXMuXG5cdCAqL1xuXHRjaGVja1N0YXR1c1RvZ2dsZSgpIHtcblx0XHRpZiAoTW9kdWxlQ0dJbmRleC4kc3RhdHVzVG9nZ2xlLmNoZWNrYm94KCdpcyBjaGVja2VkJykpIHtcblx0XHRcdE1vZHVsZUNHSW5kZXguJGRpc2FiaWxpdHlGaWVsZHMucmVtb3ZlQ2xhc3MoJ2Rpc2FibGVkJyk7XG5cdFx0fSBlbHNlIHtcblx0XHRcdE1vZHVsZUNHSW5kZXguJGRpc2FiaWxpdHlGaWVsZHMuYWRkQ2xhc3MoJ2Rpc2FibGVkJyk7XG5cdFx0fVxuXHR9LFxuXG5cdC8qKlxuXHQgKiBQcmVwYXJlcyBhIGRyb3Bkb3duIGxpc3QgZm9yIHVzZXIgc2VsZWN0aW9uLlxuXHQgKiBAcGFyYW0ge3N0cmluZ30gc2VsZWN0ZWQgLSBUaGUgc2VsZWN0ZWQgdmFsdWUuXG5cdCAqIEByZXR1cm5zIHtBcnJheX0gLSBUaGUgcHJlcGFyZWQgZHJvcGRvd24gbGlzdC5cblx0ICovXG5cdG1ha2VEcm9wZG93bkxpc3Qoc2VsZWN0ZWQpIHtcblx0XHRjb25zdCB2YWx1ZXMgPSBbXTtcblx0XHQkKCcjdXNlcnMtZ3JvdXBzLWxpc3Qgb3B0aW9uJykuZWFjaCgoaW5kZXgsIG9iaikgPT4ge1xuXHRcdFx0aWYgKHNlbGVjdGVkID09PSBvYmoudmFsdWUpIHtcblx0XHRcdFx0dmFsdWVzLnB1c2goe1xuXHRcdFx0XHRcdG5hbWU6IG9iai50ZXh0LFxuXHRcdFx0XHRcdHZhbHVlOiBvYmoudmFsdWUsXG5cdFx0XHRcdFx0c2VsZWN0ZWQ6IHRydWUsXG5cdFx0XHRcdH0pO1xuXHRcdFx0fSBlbHNlIHtcblx0XHRcdFx0dmFsdWVzLnB1c2goe1xuXHRcdFx0XHRcdG5hbWU6IG9iai50ZXh0LFxuXHRcdFx0XHRcdHZhbHVlOiBvYmoudmFsdWUsXG5cdFx0XHRcdH0pO1xuXHRcdFx0fVxuXHRcdH0pO1xuXHRcdHJldHVybiB2YWx1ZXM7XG5cdH0sXG5cblx0LyoqXG5cdCAqIEhhbmRsZXMgZ3JvdXAgY2hhbmdlIGluIHRoZSBsaXN0LlxuXHQgKiBAcGFyYW0ge3N0cmluZ30gdmFsdWUgLSBUaGUgbmV3IGdyb3VwIHZhbHVlLlxuXHQgKiBAcGFyYW0ge3N0cmluZ30gdGV4dCAtIFRoZSBuZXcgZ3JvdXAgdGV4dC5cblx0ICogQHBhcmFtIHtqUXVlcnl9ICRjaG9pY2UgLSBUaGUgc2VsZWN0ZWQgY2hvaWNlLlxuXHQgKi9cblx0Y2hhbmdlR3JvdXBJbkxpc3QodmFsdWUsIHRleHQsICRjaG9pY2UpIHtcblx0XHQkLmFwaSh7XG5cdFx0XHR1cmw6IGAke2dsb2JhbFJvb3RVcmx9bW9kdWxlLXVzZXJzLWdyb3Vwcy9tb2R1bGUtdXNlcnMtZ3JvdXBzL2NoYW5nZS11c2VyLWdyb3VwL2AsXG5cdFx0XHRvbjogJ25vdycsXG5cdFx0XHRtZXRob2Q6ICdQT1NUJyxcblx0XHRcdGRhdGE6IHtcblx0XHRcdFx0dXNlcl9pZDogJCgkY2hvaWNlKS5jbG9zZXN0KCd0cicpLmF0dHIoJ2lkJyksXG5cdFx0XHRcdGdyb3VwX2lkOiB2YWx1ZSxcblx0XHRcdH0sXG5cdFx0XHRvblN1Y2Nlc3MoKSB7XG5cdFx0XHRcdC8vXHRNb2R1bGVDR0luZGV4LmluaXRpYWxpemVEYXRhVGFibGUoKTtcblx0XHRcdFx0Ly9cdGNvbnNvbGUubG9nKCd1cGRhdGVkJyk7XG5cdFx0XHR9LFxuXHRcdFx0b25FcnJvcihyZXNwb25zZSkge1xuXHRcdFx0XHRjb25zb2xlLmxvZyhyZXNwb25zZSk7XG5cdFx0XHR9LFxuXHRcdH0pO1xuXHR9LFxuXG5cdC8qKlxuXHQgKiBDYWxjdWxhdGUgZGF0YSB0YWJsZSBwYWdlIGxlbmd0aFxuXHQgKlxuXHQgKiBAcmV0dXJucyB7bnVtYmVyfVxuXHQgKi9cblx0Y2FsY3VsYXRlUGFnZUxlbmd0aCgpIHtcblx0XHQvLyBDYWxjdWxhdGUgcm93IGhlaWdodFxuXHRcdGxldCByb3dIZWlnaHQgPSBNb2R1bGVDR0luZGV4LiR1c2Vyc1RhYmxlLmZpbmQoJ3RyJykuZmlyc3QoKS5vdXRlckhlaWdodCgpO1xuXHRcdC8vIENhbGN1bGF0ZSB3aW5kb3cgaGVpZ2h0IGFuZCBhdmFpbGFibGUgc3BhY2UgZm9yIHRhYmxlXG5cdFx0Y29uc3Qgd2luZG93SGVpZ2h0ID0gd2luZG93LmlubmVySGVpZ2h0O1xuXHRcdGNvbnN0IGhlYWRlckZvb3RlckhlaWdodCA9IDUwMDsgLy8gRXN0aW1hdGUgaGVpZ2h0IGZvciBoZWFkZXIsIGZvb3RlciwgYW5kIG90aGVyIGVsZW1lbnRzXG5cblx0XHQvLyBDYWxjdWxhdGUgbmV3IHBhZ2UgbGVuZ3RoXG5cdFx0cmV0dXJuIE1hdGgubWF4KE1hdGguZmxvb3IoKHdpbmRvd0hlaWdodCAtIGhlYWRlckZvb3RlckhlaWdodCkgLyByb3dIZWlnaHQpLCAxMCk7XG5cdH0sXG59O1xuXG4vKipcbiAqIEluaXRpYWxpemUgdGhlIG1vZHVsZSB3aGVuIHRoZSBkb2N1bWVudCBpcyByZWFkeS5cbiAqL1xuJChkb2N1bWVudCkucmVhZHkoKCkgPT4ge1xuXHRNb2R1bGVDR0luZGV4LmluaXRpYWxpemUoKTtcbn0pO1xuXG4iXX0= \ No newline at end of file +//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbInNyYy9tb2R1bGUtdXNlcnMtZ3JvdXBzLWluZGV4LmpzIl0sIm5hbWVzIjpbIk1vZHVsZUNHSW5kZXgiLCIkc3RhdHVzVG9nZ2xlIiwiJCIsIiR1c2Vyc1RhYmxlIiwidXNlckRhdGFUYWJsZSIsIiRzZWxlY3RHcm91cCIsIiRkZWZhdWx0R3JvdXBEcm9wZG93biIsIiRkaXNhYmlsaXR5RmllbGRzIiwiaW5pdGlhbGl6ZSIsInRhYiIsImNoZWNrU3RhdHVzVG9nZ2xlIiwid2luZG93IiwiYWRkRXZlbnRMaXN0ZW5lciIsImluaXRpYWxpemVVc2Vyc0RhdGFUYWJsZSIsImVhY2giLCJpbmRleCIsIm9iaiIsImRyb3Bkb3duIiwidmFsdWVzIiwibWFrZURyb3Bkb3duTGlzdCIsImF0dHIiLCJvbkNoYW5nZSIsImNoYW5nZUdyb3VwSW5MaXN0IiwiY2hhbmdlRGVmYXVsdEdyb3VwIiwib25WaXNpYmxlIiwiZGF0YSIsIm5ld1BhZ2VMZW5ndGgiLCJjYWxjdWxhdGVQYWdlTGVuZ3RoIiwicGFnZSIsImxlbiIsImRyYXciLCJEYXRhVGFibGUiLCJsZW5ndGhDaGFuZ2UiLCJwYWdpbmciLCJwYWdlTGVuZ3RoIiwic2Nyb2xsQ29sbGFwc2UiLCJjb2x1bW5zIiwib3JkZXIiLCJsYW5ndWFnZSIsIlNlbWFudGljTG9jYWxpemF0aW9uIiwiZGF0YVRhYmxlTG9jYWxpc2F0aW9uIiwiY2hlY2tib3giLCJyZW1vdmVDbGFzcyIsImFkZENsYXNzIiwic2VsZWN0ZWQiLCJ2YWx1ZSIsInB1c2giLCJuYW1lIiwidGV4dCIsIiRjaG9pY2UiLCJhcGkiLCJ1cmwiLCJnbG9iYWxSb290VXJsIiwib24iLCJtZXRob2QiLCJ1c2VyX2lkIiwiY2xvc2VzdCIsImdyb3VwX2lkIiwib25TdWNjZXNzIiwidXBkYXRlR3JvdXBDb3VudGVycyIsIm9uRXJyb3IiLCJyZXNwb25zZSIsImNvbnNvbGUiLCJsb2ciLCJhamF4IiwiZGF0YVR5cGUiLCJzdWNjZXNzIiwicmVzdWx0Iiwic3RhdHMiLCJPYmplY3QiLCJrZXlzIiwiZm9yRWFjaCIsImdyb3VwSWQiLCJjb3VudCIsIiRyb3ciLCJsZW5ndGgiLCJmaW5kIiwiZmlyc3QiLCJlcnJvciIsInhociIsInN0YXR1cyIsImVycm9yTWVzc2FnZSIsIm1lc3NhZ2VzIiwiam9pbiIsIlVzZXJNZXNzYWdlIiwic2hvd0Vycm9yIiwiY29tcGxldGUiLCJyb3dIZWlnaHQiLCJvdXRlckhlaWdodCIsIndpbmRvd0hlaWdodCIsImlubmVySGVpZ2h0IiwiaGVhZGVyRm9vdGVySGVpZ2h0IiwiTWF0aCIsIm1heCIsImZsb29yIiwiZG9jdW1lbnQiLCJyZWFkeSJdLCJtYXBwaW5ncyI6Ijs7QUFBQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQUVBOztBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsSUFBTUEsYUFBYSxHQUFHO0FBQ3JCO0FBQ0Q7QUFDQTtBQUNBO0FBQ0NDLEVBQUFBLGFBQWEsRUFBRUMsQ0FBQyxDQUFDLHVCQUFELENBTEs7O0FBT3JCO0FBQ0Q7QUFDQTtBQUNBO0FBQ0NDLEVBQUFBLFdBQVcsRUFBRUQsQ0FBQyxDQUFDLGNBQUQsQ0FYTzs7QUFhckI7QUFDRDtBQUNBO0FBQ0E7QUFDQ0UsRUFBQUEsYUFBYSxFQUFFLElBakJNOztBQW1CckI7QUFDRDtBQUNBO0FBQ0E7QUFDQ0MsRUFBQUEsWUFBWSxFQUFFSCxDQUFDLENBQUMsZUFBRCxDQXZCTTs7QUF5QnJCO0FBQ0Q7QUFDQTtBQUNBO0FBQ0NJLEVBQUFBLHFCQUFxQixFQUFFSixDQUFDLENBQUMsdUJBQUQsQ0E3Qkg7O0FBK0JyQjtBQUNEO0FBQ0E7QUFDQTtBQUNDSyxFQUFBQSxpQkFBaUIsRUFBRUwsQ0FBQyxDQUFDLHNCQUFELENBbkNDOztBQW9DckI7QUFDRDtBQUNBO0FBQ0NNLEVBQUFBLFVBdkNxQix3QkF1Q1I7QUFDWjtBQUNBTixJQUFBQSxDQUFDLENBQUMsbUNBQUQsQ0FB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T0FBaEU7QUFDQU0sUUFBQUEsV0FBVyxDQUFDQyxTQUFaLENBQXNCLGdDQUF0QjtBQUNBLE9BbkJLO0FBb0JOQyxNQUFBQSxRQXBCTSxzQkFvQks7QUFDVjtBQUNBdEYsUUFBQUEsYUFBYSxDQUFDTSxxQkFBZCxDQUFvQ29DLFdBQXBDLENBQWdELFNBQWhEO0FBQ0E7QUF2QkssS0FBUDtBQXlCQSxHQXJPb0I7O0FBdU9yQjtBQUNEO0FBQ0E7QUFDQTtBQUNBO0FBQ0NmLEVBQUFBLG1CQTVPcUIsaUNBNE9DO0FBQ3JCO0FBQ0EsUUFBSTRELFNBQVMsR0FBR3ZGLGFBQWEsQ0FBQ0csV0FBZCxDQUEwQnlFLElBQTFCLENBQStCLElBQS9CLEVBQXFDQyxLQUFyQyxHQUE2Q1csV0FBN0MsRUFBaEIsQ0FGcUIsQ0FHckI7O0FBQ0EsUUFBTUMsWUFBWSxHQUFHOUUsTUFBTSxDQUFDK0UsV0FBNUI7QUFDQSxRQUFNQyxrQkFBa0IsR0FBRyxHQUEzQixDQUxxQixDQUtXO0FBRWhDOztBQUNBLFdBQU9DLElBQUksQ0FBQ0MsR0FBTCxDQUFTRCxJQUFJLENBQUNFLEtBQUwsQ0FBVyxDQUFDTCxZQUFZLEdBQUdFLGtCQUFoQixJQUFzQ0osU0FBakQsQ0FBVCxFQUFzRSxFQUF0RSxDQUFQO0FBQ0E7QUFyUG9CLENBQXRCO0FBd1BBO0FBQ0E7QUFDQTs7QUFDQXJGLENBQUMsQ0FBQzZGLFFBQUQsQ0FBRCxDQUFZQyxLQUFaLENBQWtCLFlBQU07QUFDdkJoRyxFQUFBQSxhQUFhLENBQUNRLFVBQWQ7QUFDQSxDQUZEIiwic291cmNlc0NvbnRlbnQiOlsiLypcbiAqIE1pa29QQlggLSBmcmVlIHBob25lIHN5c3RlbSBmb3Igc21hbGwgYnVzaW5lc3NcbiAqIENvcHlyaWdodCDCqSAyMDE3LTIwMjMgQWxleGV5IFBvcnRub3YgYW5kIE5pa29sYXkgQmVrZXRvdlxuICpcbiAqIFRoaXMgcHJvZ3JhbSBpcyBmcmVlIHNvZnR3YXJlOiB5b3UgY2FuIHJlZGlzdHJpYnV0ZSBpdCBhbmQvb3IgbW9kaWZ5XG4gKiBpdCB1bmRlciB0aGUgdGVybXMgb2YgdGhlIEdOVSBHZW5lcmFsIFB1YmxpYyBMaWNlbnNlIGFzIHB1Ymxpc2hlZCBieVxuICogdGhlIEZyZWUgU29mdHdhcmUgRm91bmRhdGlvbjsgZWl0aGVyIHZlcnNpb24gMyBvZiB0aGUgTGljZW5zZSwgb3JcbiAqIChhdCB5b3VyIG9wdGlvbikgYW55IGxhdGVyIHZlcnNpb24uXG4gKlxuICogVGhpcyBwcm9ncmFtIGlzIGRpc3RyaWJ1dGVkIGluIHRoZSBob3BlIHRoYXQgaXQgd2lsbCBiZSB1c2VmdWwsXG4gKiBidXQgV0lUSE9VVCBBTlkgV0FSUkFOVFk7IHdpdGhvdXQgZXZlbiB0aGUgaW1wbGllZCB3YXJyYW50eSBvZlxuICogTUVSQ0hBTlRBQklMSVRZIG9yIEZJVE5FU1MgRk9SIEEgUEFSVElDVUxBUiBQVVJQT1NFLiAgU2VlIHRoZVxuICogR05VIEdlbmVyYWwgUHVibGljIExpY2Vuc2UgZm9yIG1vcmUgZGV0YWlscy5cbiAqXG4gKiBZb3Ugc2hvdWxkIGhhdmUgcmVjZWl2ZWQgYSBjb3B5IG9mIHRoZSBHTlUgR2VuZXJhbCBQdWJsaWMgTGljZW5zZSBhbG9uZyB3aXRoIHRoaXMgcHJvZ3JhbS5cbiAqIElmIG5vdCwgc2VlIDxodHRwczovL3d3dy5nbnUub3JnL2xpY2Vuc2VzLz4uXG4gKi9cblxuLyogZ2xvYmFsIFNlbWFudGljTG9jYWxpemF0aW9uLCBnbG9iYWxSb290VXJsLCBEYXRhdGFibGUgKi9cblxuLyoqXG4gKiBNb2R1bGUgZm9yIG1hbmFnaW5nIGNhbGwgZ3JvdXBzIGFuZCByZWxhdGVkIGZ1bmN0aW9uYWxpdHkuXG4gKiBAbmFtZXNwYWNlXG4gKi9cbmNvbnN0IE1vZHVsZUNHSW5kZXggPSB7XG5cdC8qKlxuXHQgKiBqUXVlcnkgb2JqZWN0IGZvciB0aGUgc3RhdHVzIHRvZ2dsZS5cblx0ICogQHR5cGUge2pRdWVyeX1cblx0ICovXG5cdCRzdGF0dXNUb2dnbGU6ICQoJyNtb2R1bGUtc3RhdHVzLXRvZ2dsZScpLFxuXG5cdC8qKlxuXHQgKiBqUXVlcnkgb2JqZWN0IGZvciB0aGUgdXNlcnMgdGFibGUuXG5cdCAqIEB0eXBlIHtqUXVlcnl9XG5cdCAqL1xuXHQkdXNlcnNUYWJsZTogJCgnI3VzZXJzLXRhYmxlJyksXG5cblx0LyoqXG5cdCAqIFVzZXIgZGF0YSB0YWJsZS5cblx0ICogQHR5cGUge0RhdGF0YWJsZX1cblx0ICovXG5cdHVzZXJEYXRhVGFibGU6IG51bGwsXG5cblx0LyoqXG5cdCAqIGpRdWVyeSBvYmplY3QgZm9yIHNlbGVjdCBncm91cCBlbGVtZW50cy5cblx0ICogQHR5cGUge2pRdWVyeX1cblx0ICovXG5cdCRzZWxlY3RHcm91cDogJCgnLnNlbGVjdC1ncm91cCcpLFxuXG5cdC8qKlxuXHQgKiBqUXVlcnkgb2JqZWN0IGZvciBkZWZhdWx0IGdyb3VwIGRyb3Bkb3duLlxuXHQgKiBAdHlwZSB7alF1ZXJ5fVxuXHQgKi9cblx0JGRlZmF1bHRHcm91cERyb3Bkb3duOiAkKCcuc2VsZWN0LWRlZmF1bHQtZ3JvdXAnKSxcblxuXHQvKipcblx0ICogalF1ZXJ5IG9iamVjdCBmb3IgY3VycmVudCBmb3JtIGRpc2FiaWxpdHkgZmllbGRzXG5cdCAqIEB0eXBlIHtqUXVlcnl9XG5cdCAqL1xuXHQkZGlzYWJpbGl0eUZpZWxkczogJCgnI21vZHVsZS11c2Vycy1ncm91cHMnKSxcblx0LyoqXG5cdCAqIEluaXRpYWxpemVzIHRoZSBtb2R1bGUuXG5cdCAqL1xuXHRpbml0aWFsaXplKCkge1xuXHRcdC8vIEluaXRpYWxpemUgdGFiIG1lbnVcblx0XHQkKCcjbWFpbi11c2Vycy1ncm91cHMtdGFiLW1lbnUgLml0ZW0nKS50YWIoKTtcblxuXHRcdC8vIENoZWNrIHN0YXR1cyB0b2dnbGUgaW5pdGlhbGx5XG5cdFx0TW9kdWxlQ0dJbmRleC5jaGVja1N0YXR1c1RvZ2dsZSgpO1xuXHRcdC8vIEFkZCBldmVudCBsaXN0ZW5lciBmb3Igc3RhdHVzIGNoYW5nZXNcblx0XHR3aW5kb3cuYWRkRXZlbnRMaXN0ZW5lcignTW9kdWxlU3RhdHVzQ2hhbmdlZCcsIE1vZHVsZUNHSW5kZXguY2hlY2tTdGF0dXNUb2dnbGUpO1xuXG5cdFx0Ly8gSW5pdGlhbGl6ZSB1c2VycyBkYXRhIHRhYmxlXG5cdFx0TW9kdWxlQ0dJbmRleC5pbml0aWFsaXplVXNlcnNEYXRhVGFibGUoKTtcblxuXHRcdC8vIEluaXRpYWxpemUgZHJvcGRvd25zIGZvciBzZWxlY3QgZ3JvdXAgZWxlbWVudHNcblx0XHRNb2R1bGVDR0luZGV4LiRzZWxlY3RHcm91cC5lYWNoKChpbmRleCwgb2JqKSA9PiB7XG5cdFx0XHQkKG9iaikuZHJvcGRvd24oe1xuXHRcdFx0XHR2YWx1ZXM6IE1vZHVsZUNHSW5kZXgubWFrZURyb3Bkb3duTGlzdCgkKG9iaikuYXR0cignZGF0YS12YWx1ZScpKSxcblx0XHRcdH0pO1xuXHRcdH0pO1xuXG5cdFx0Ly8gSW5pdGlhbGl6ZSBkcm9wZG93biBmb3Igc2VsZWN0IGdyb3VwXG5cdFx0TW9kdWxlQ0dJbmRleC4kc2VsZWN0R3JvdXAuZHJvcGRvd24oe1xuXHRcdFx0b25DaGFuZ2U6IE1vZHVsZUNHSW5kZXguY2hhbmdlR3JvdXBJbkxpc3QsXG5cdFx0fSk7XG5cblx0XHQvLyBJbml0aWFsaXplIGRlZmF1bHQgZ3JvdXAgZHJvcGRvd25cblx0XHRNb2R1bGVDR0luZGV4LiRkZWZhdWx0R3JvdXBEcm9wZG93bi5kcm9wZG93bih7XG5cdFx0XHRvbkNoYW5nZTogTW9kdWxlQ0dJbmRleC5jaGFuZ2VEZWZhdWx0R3JvdXAsXG5cdFx0fSk7XG5cblx0fSxcblxuXHQvKipcblx0ICogSW5pdGlhbGl6ZXMgdGhlIERhdGFUYWJsZSBmb3IgdXNlcnMgdGFibGUuXG5cdCAqL1xuXHRpbml0aWFsaXplVXNlcnNEYXRhVGFibGUoKSB7XG5cblx0XHQkKCcjbWFpbi11c2Vycy1ncm91cHMtdGFiLW1lbnUgLml0ZW0nKS50YWIoe1xuXHRcdFx0b25WaXNpYmxlKCl7XG5cdFx0XHRcdGlmICgkKHRoaXMpLmRhdGEoJ3RhYicpPT09J3VzZXJzJyAmJiBNb2R1bGVDR0luZGV4LnVzZXJEYXRhVGFibGUhPT1udWxsKXtcblx0XHRcdFx0XHRjb25zdCBuZXdQYWdlTGVuZ3RoID0gTW9kdWxlQ0dJbmRleC5jYWxjdWxhdGVQYWdlTGVuZ3RoKCk7XG5cdFx0XHRcdFx0TW9kdWxlQ0dJbmRleC51c2VyRGF0YVRhYmxlLnBhZ2UubGVuKG5ld1BhZ2VMZW5ndGgpLmRyYXcoZmFsc2UpO1xuXHRcdFx0XHR9XG5cdFx0XHR9XG5cdFx0fSk7XG5cblx0XHRNb2R1bGVDR0luZGV4LnVzZXJEYXRhVGFibGUgPSBNb2R1bGVDR0luZGV4LiR1c2Vyc1RhYmxlLkRhdGFUYWJsZSh7XG5cdFx0XHQvLyBkZXN0cm95OiB0cnVlLFxuXHRcdFx0bGVuZ3RoQ2hhbmdlOiBmYWxzZSxcblx0XHRcdHBhZ2luZzogdHJ1ZSxcblx0XHRcdHBhZ2VMZW5ndGg6IE1vZHVsZUNHSW5kZXguY2FsY3VsYXRlUGFnZUxlbmd0aCgpLFxuXHRcdFx0c2Nyb2xsQ29sbGFwc2U6IHRydWUsXG5cdFx0XHRjb2x1bW5zOiBbXG5cdFx0XHRcdG51bGwsXG5cdFx0XHRcdG51bGwsXG5cdFx0XHRcdG51bGwsXG5cdFx0XHRcdG51bGwsXG5cdFx0XHRdLFxuXHRcdFx0b3JkZXI6IFsxLCAnYXNjJ10sXG5cdFx0XHRsYW5ndWFnZTogU2VtYW50aWNMb2NhbGl6YXRpb24uZGF0YVRhYmxlTG9jYWxpc2F0aW9uLFxuXHRcdH0pO1xuXHR9LFxuXG5cdC8qKlxuXHQgKiBDaGVja3MgYW5kIHVwZGF0ZXMgYnV0dG9uIHN0YXR1cyBiYXNlZCBvbiBtb2R1bGUgc3RhdHVzLlxuXHQgKi9cblx0Y2hlY2tTdGF0dXNUb2dnbGUoKSB7XG5cdFx0aWYgKE1vZHVsZUNHSW5kZXguJHN0YXR1c1RvZ2dsZS5jaGVja2JveCgnaXMgY2hlY2tlZCcpKSB7XG5cdFx0XHRNb2R1bGVDR0luZGV4LiRkaXNhYmlsaXR5RmllbGRzLnJlbW92ZUNsYXNzKCdkaXNhYmxlZCcpO1xuXHRcdH0gZWxzZSB7XG5cdFx0XHRNb2R1bGVDR0luZGV4LiRkaXNhYmlsaXR5RmllbGRzLmFkZENsYXNzKCdkaXNhYmxlZCcpO1xuXHRcdH1cblx0fSxcblxuXHQvKipcblx0ICogUHJlcGFyZXMgYSBkcm9wZG93biBsaXN0IGZvciB1c2VyIHNlbGVjdGlvbi5cblx0ICogQHBhcmFtIHtzdHJpbmd9IHNlbGVjdGVkIC0gVGhlIHNlbGVjdGVkIHZhbHVlLlxuXHQgKiBAcmV0dXJucyB7QXJyYXl9IC0gVGhlIHByZXBhcmVkIGRyb3Bkb3duIGxpc3QuXG5cdCAqL1xuXHRtYWtlRHJvcGRvd25MaXN0KHNlbGVjdGVkKSB7XG5cdFx0Y29uc3QgdmFsdWVzID0gW107XG5cdFx0JCgnI3VzZXJzLWdyb3Vwcy1saXN0IG9wdGlvbicpLmVhY2goKGluZGV4LCBvYmopID0+IHtcblx0XHRcdGlmIChzZWxlY3RlZCA9PT0gb2JqLnZhbHVlKSB7XG5cdFx0XHRcdHZhbHVlcy5wdXNoKHtcblx0XHRcdFx0XHRuYW1lOiBvYmoudGV4dCxcblx0XHRcdFx0XHR2YWx1ZTogb2JqLnZhbHVlLFxuXHRcdFx0XHRcdHNlbGVjdGVkOiB0cnVlLFxuXHRcdFx0XHR9KTtcblx0XHRcdH0gZWxzZSB7XG5cdFx0XHRcdHZhbHVlcy5wdXNoKHtcblx0XHRcdFx0XHRuYW1lOiBvYmoudGV4dCxcblx0XHRcdFx0XHR2YWx1ZTogb2JqLnZhbHVlLFxuXHRcdFx0XHR9KTtcblx0XHRcdH1cblx0XHR9KTtcblx0XHRyZXR1cm4gdmFsdWVzO1xuXHR9LFxuXG5cdC8qKlxuXHQgKiBIYW5kbGVzIGdyb3VwIGNoYW5nZSBpbiB0aGUgbGlzdC5cblx0ICogQHBhcmFtIHtzdHJpbmd9IHZhbHVlIC0gVGhlIG5ldyBncm91cCB2YWx1ZS5cblx0ICogQHBhcmFtIHtzdHJpbmd9IHRleHQgLSBUaGUgbmV3IGdyb3VwIHRleHQuXG5cdCAqIEBwYXJhbSB7alF1ZXJ5fSAkY2hvaWNlIC0gVGhlIHNlbGVjdGVkIGNob2ljZS5cblx0ICovXG5cdGNoYW5nZUdyb3VwSW5MaXN0KHZhbHVlLCB0ZXh0LCAkY2hvaWNlKSB7XG5cdFx0JC5hcGkoe1xuXHRcdFx0dXJsOiBgJHtnbG9iYWxSb290VXJsfW1vZHVsZS11c2Vycy1ncm91cHMvbW9kdWxlLXVzZXJzLWdyb3Vwcy9jaGFuZ2UtdXNlci1ncm91cC9gLFxuXHRcdFx0b246ICdub3cnLFxuXHRcdFx0bWV0aG9kOiAnUE9TVCcsXG5cdFx0XHRkYXRhOiB7XG5cdFx0XHRcdHVzZXJfaWQ6ICQoJGNob2ljZSkuY2xvc2VzdCgndHInKS5hdHRyKCdpZCcpLFxuXHRcdFx0XHRncm91cF9pZDogdmFsdWUsXG5cdFx0XHR9LFxuXHRcdFx0b25TdWNjZXNzKCkge1xuXHRcdFx0XHQvLyBVcGRhdGUgZ3JvdXAgbWVtYmVyIGNvdW50ZXJzIGFmdGVyIHN1Y2Nlc3NmdWwgZ3JvdXAgY2hhbmdlXG5cdFx0XHRcdE1vZHVsZUNHSW5kZXgudXBkYXRlR3JvdXBDb3VudGVycygpO1xuXHRcdFx0fSxcblx0XHRcdG9uRXJyb3IocmVzcG9uc2UpIHtcblx0XHRcdFx0Y29uc29sZS5sb2cocmVzcG9uc2UpO1xuXHRcdFx0fSxcblx0XHR9KTtcblx0fSxcblxuXHQvKipcblx0ICogVXBkYXRlIGdyb3VwIG1lbWJlciBjb3VudGVycyB2aWEgQVBJXG5cdCAqL1xuXHR1cGRhdGVHcm91cENvdW50ZXJzKCkge1xuXHRcdCQuYWpheCh7XG5cdFx0XHR1cmw6ICcvcGJ4Y29yZS9hcGkvbW9kdWxlcy9Nb2R1bGVVc2Vyc0dyb3Vwcy9nZXRHcm91cHNTdGF0cycsXG5cdFx0XHRtZXRob2Q6ICdHRVQnLFxuXHRcdFx0ZGF0YVR5cGU6ICdqc29uJyxcblx0XHRcdHN1Y2Nlc3MocmVzcG9uc2UpIHtcblx0XHRcdFx0aWYgKHJlc3BvbnNlLnJlc3VsdCA9PT0gdHJ1ZSAmJiByZXNwb25zZS5kYXRhICYmIHJlc3BvbnNlLmRhdGEuc3RhdHMpIHtcblx0XHRcdFx0XHRjb25zdCBzdGF0cyA9IHJlc3BvbnNlLmRhdGEuc3RhdHM7XG5cblx0XHRcdFx0XHQvLyBVcGRhdGUgZWFjaCBncm91cCdzIG1lbWJlciBjb3VudCBpbiB0aGUgdGFibGVcblx0XHRcdFx0XHRPYmplY3Qua2V5cyhzdGF0cykuZm9yRWFjaCgoZ3JvdXBJZCkgPT4ge1xuXHRcdFx0XHRcdFx0Y29uc3QgY291bnQgPSBzdGF0c1tncm91cElkXTtcblx0XHRcdFx0XHRcdGNvbnN0ICRyb3cgPSAkKGAjdXNlcnMtZ3JvdXBzLXRhYmxlIHRyIyR7Z3JvdXBJZH1gKTtcblx0XHRcdFx0XHRcdGlmICgkcm93Lmxlbmd0aCA+IDApIHtcblx0XHRcdFx0XHRcdFx0Ly8gRmluZCB0aGUgY291bnRlciBjZWxsIChzZWNvbmQgdGQgd2l0aCBjZW50ZXIgYWxpZ25lZCBjbGFzcylcblx0XHRcdFx0XHRcdFx0JHJvdy5maW5kKCd0ZC5jZW50ZXIuYWxpZ25lZCcpLmZpcnN0KCkudGV4dChjb3VudCk7XG5cdFx0XHRcdFx0XHR9XG5cdFx0XHRcdFx0fSk7XG5cdFx0XHRcdH1cblx0XHRcdH0sXG5cdFx0XHRlcnJvcih4aHIsIHN0YXR1cywgZXJyb3IpIHtcblx0XHRcdFx0Y29uc29sZS5lcnJvcignTW9kdWxlVXNlcnNHcm91cHM6IEZhaWxlZCB0byB1cGRhdGUgZ3JvdXAgY291bnRlcnMnLCBlcnJvcik7XG5cdFx0XHR9LFxuXHRcdH0pO1xuXHR9LFxuXG5cdC8qKlxuXHQgKiBIYW5kbGVzIGRlZmF1bHQgZ3JvdXAgY2hhbmdlLlxuXHQgKiBAcGFyYW0ge3N0cmluZ30gdmFsdWUgLSBUaGUgbmV3IGRlZmF1bHQgZ3JvdXAgdmFsdWUuXG5cdCAqIEBwYXJhbSB7c3RyaW5nfSB0ZXh0IC0gVGhlIG5ldyBncm91cCB0ZXh0LlxuXHQgKiBAcGFyYW0ge2pRdWVyeX0gJGNob2ljZSAtIFRoZSBzZWxlY3RlZCBjaG9pY2UuXG5cdCAqL1xuXHRjaGFuZ2VEZWZhdWx0R3JvdXAodmFsdWUsIHRleHQsICRjaG9pY2UpIHtcblx0XHRpZiAoIXZhbHVlIHx8IHZhbHVlID09PSAnJykge1xuXHRcdFx0cmV0dXJuO1xuXHRcdH1cblxuXHRcdC8vIEFkZCBsb2FkaW5nIHN0YXRlIHRvIGRyb3Bkb3duXG5cdFx0TW9kdWxlQ0dJbmRleC4kZGVmYXVsdEdyb3VwRHJvcGRvd24uYWRkQ2xhc3MoJ2xvYWRpbmcnKTtcblxuXHRcdCQuYWpheCh7XG5cdFx0XHR1cmw6ICcvcGJ4Y29yZS9hcGkvbW9kdWxlcy9Nb2R1bGVVc2Vyc0dyb3Vwcy9zZXREZWZhdWx0R3JvdXAnLFxuXHRcdFx0bWV0aG9kOiAnUE9TVCcsXG5cdFx0XHRkYXRhVHlwZTogJ2pzb24nLFxuXHRcdFx0ZGF0YToge1xuXHRcdFx0XHRncm91cF9pZDogdmFsdWUsXG5cdFx0XHR9LFxuXHRcdFx0c3VjY2VzcyhyZXNwb25zZSkge1xuXHRcdFx0XHRpZiAocmVzcG9uc2UucmVzdWx0ICE9PSB0cnVlKSB7XG5cdFx0XHRcdFx0Ly8gU2hvdyBlcnJvciBub3RpZmljYXRpb24gb25seSBvbiBmYWlsdXJlXG5cdFx0XHRcdFx0Y29uc3QgZXJyb3JNZXNzYWdlID0gcmVzcG9uc2UubWVzc2FnZXMgJiYgcmVzcG9uc2UubWVzc2FnZXMubGVuZ3RoID4gMFxuXHRcdFx0XHRcdFx0PyByZXNwb25zZS5tZXNzYWdlcy5qb2luKCcsICcpXG5cdFx0XHRcdFx0XHQ6ICdGYWlsZWQgdG8gdXBkYXRlIGRlZmF1bHQgZ3JvdXAnO1xuXHRcdFx0XHRcdFVzZXJNZXNzYWdlLnNob3dFcnJvcihlcnJvck1lc3NhZ2UpO1xuXHRcdFx0XHR9XG5cdFx0XHR9LFxuXHRcdFx0ZXJyb3IoeGhyLCBzdGF0dXMsIGVycm9yKSB7XG5cdFx0XHRcdGNvbnNvbGUuZXJyb3IoJ01vZHVsZVVzZXJzR3JvdXBzOiBGYWlsZWQgdG8gc2V0IGRlZmF1bHQgZ3JvdXAnLCBlcnJvcik7XG5cdFx0XHRcdFVzZXJNZXNzYWdlLnNob3dFcnJvcignRmFpbGVkIHRvIHVwZGF0ZSBkZWZhdWx0IGdyb3VwJyk7XG5cdFx0XHR9LFxuXHRcdFx0Y29tcGxldGUoKSB7XG5cdFx0XHRcdC8vIFJlbW92ZSBsb2FkaW5nIHN0YXRlIGZyb20gZHJvcGRvd25cblx0XHRcdFx0TW9kdWxlQ0dJbmRleC4kZGVmYXVsdEdyb3VwRHJvcGRvd24ucmVtb3ZlQ2xhc3MoJ2xvYWRpbmcnKTtcblx0XHRcdH0sXG5cdFx0fSk7XG5cdH0sXG5cblx0LyoqXG5cdCAqIENhbGN1bGF0ZSBkYXRhIHRhYmxlIHBhZ2UgbGVuZ3RoXG5cdCAqXG5cdCAqIEByZXR1cm5zIHtudW1iZXJ9XG5cdCAqL1xuXHRjYWxjdWxhdGVQYWdlTGVuZ3RoKCkge1xuXHRcdC8vIENhbGN1bGF0ZSByb3cgaGVpZ2h0XG5cdFx0bGV0IHJvd0hlaWdodCA9IE1vZHVsZUNHSW5kZXguJHVzZXJzVGFibGUuZmluZCgndHInKS5maXJzdCgpLm91dGVySGVpZ2h0KCk7XG5cdFx0Ly8gQ2FsY3VsYXRlIHdpbmRvdyBoZWlnaHQgYW5kIGF2YWlsYWJsZSBzcGFjZSBmb3IgdGFibGVcblx0XHRjb25zdCB3aW5kb3dIZWlnaHQgPSB3aW5kb3cuaW5uZXJIZWlnaHQ7XG5cdFx0Y29uc3QgaGVhZGVyRm9vdGVySGVpZ2h0ID0gNTAwOyAvLyBFc3RpbWF0ZSBoZWlnaHQgZm9yIGhlYWRlciwgZm9vdGVyLCBhbmQgb3RoZXIgZWxlbWVudHNcblxuXHRcdC8vIENhbGN1bGF0ZSBuZXcgcGFnZSBsZW5ndGhcblx0XHRyZXR1cm4gTWF0aC5tYXgoTWF0aC5mbG9vcigod2luZG93SGVpZ2h0IC0gaGVhZGVyRm9vdGVySGVpZ2h0KSAvIHJvd0hlaWdodCksIDEwKTtcblx0fSxcbn07XG5cbi8qKlxuICogSW5pdGlhbGl6ZSB0aGUgbW9kdWxlIHdoZW4gdGhlIGRvY3VtZW50IGlzIHJlYWR5LlxuICovXG4kKGRvY3VtZW50KS5yZWFkeSgoKSA9PiB7XG5cdE1vZHVsZUNHSW5kZXguaW5pdGlhbGl6ZSgpO1xufSk7XG5cbiJdfQ== \ No newline at end of file diff --git a/public/assets/js/src/module-users-groups-extension-dropdown.js b/public/assets/js/src/module-users-groups-extension-dropdown.js new file mode 100644 index 0000000..0cc733e --- /dev/null +++ b/public/assets/js/src/module-users-groups-extension-dropdown.js @@ -0,0 +1,155 @@ +/* + * MikoPBX - free phone system for small business + * Copyright © 2017-2023 Alexey Portnov and Nikolay Beketov + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program. + * If not, see . + */ + +/* global globalRootUrl, PbxApi */ + +/** + * ModuleUsersGroupsExtensionDropdown - Initialize user groups dropdown on extension edit page + * Works with both old and new MikoPBX releases via FormPopulated event + */ +const ModuleUsersGroupsExtensionDropdown = { + /** + * jQuery object for the dropdown + */ + $dropdown: $('#mod_usrgr_select_group_dropdown'), + + /** + * Hidden input field for storing selected value + */ + $hiddenInput: $('#mod_usrgr_select_group'), + + /** + * Initialize the dropdown + */ + initialize() { + // Check if dropdown element exists + if (ModuleUsersGroupsExtensionDropdown.$dropdown.length === 0) { + return; + } + + // Initialize Fomantic UI dropdown (items are already in HTML from server) + ModuleUsersGroupsExtensionDropdown.$dropdown.dropdown({ + onChange(value) { + // Sync value to hidden input + ModuleUsersGroupsExtensionDropdown.$hiddenInput.val(value); + + // Trigger form change to enable save button + if (typeof Form !== 'undefined' && typeof Form.dataChanged === 'function') { + Form.dataChanged(); + } + }, + clearable: false, + fullTextSearch: true, + forceSelection: false + }); + + // Smart initialization - check if user_id or id exists in DOM + // Old release: + // New release: + const userId = $('#user_id').val() || $('#id').val(); + + if (userId && userId !== '' && userId !== 'new') { + // Existing user - load their group + ModuleUsersGroupsExtensionDropdown.loadUserGroup(userId); + } else { + // New user - load default group + ModuleUsersGroupsExtensionDropdown.loadDefaultGroup(); + } + + // Listen for form population event (for new release with AJAX forms) + $(document).on('FormPopulated', (event, formData) => { + // Get user ID from DOM (new release always uses 'id') + const populatedUserId = $('#id').val(); + + if (populatedUserId && populatedUserId !== '' && populatedUserId !== 'new') { + // Load user's actual group (will override default) + ModuleUsersGroupsExtensionDropdown.loadUserGroup(populatedUserId); + } + }); + }, + + /** + * Update dropdown value from hidden input + */ + updateDropdownValue() { + const value = ModuleUsersGroupsExtensionDropdown.$hiddenInput.val(); + + if (value && value !== '') { + ModuleUsersGroupsExtensionDropdown.$dropdown.dropdown('set selected', String(value)); + } + }, + + /** + * Load default group from module API + */ + loadDefaultGroup() { + $.ajax({ + url: '/pbxcore/api/modules/ModuleUsersGroups/getDefaultGroup', + method: 'POST', + dataType: 'json', + success(response) { + if (response.result === true && response.data && response.data.group_id) { + // Set value to hidden input + ModuleUsersGroupsExtensionDropdown.$hiddenInput.val(response.data.group_id); + + // Update dropdown + ModuleUsersGroupsExtensionDropdown.updateDropdownValue(); + } + }, + error(xhr, status, error) { + console.error('ModuleUsersGroups: Failed to load default group', error); + } + }); + }, + + /** + * Load user's group from module API + * @param {string|number} userId - User ID + */ + loadUserGroup(userId) { + if (!userId) { + // For new users, load default group + ModuleUsersGroupsExtensionDropdown.loadDefaultGroup(); + return; + } + + $.ajax({ + url: '/pbxcore/api/modules/ModuleUsersGroups/getUserGroup', + method: 'POST', + dataType: 'json', + data: {user_id: userId}, + success(response) { + if (response.result === true && response.data && response.data.group_id) { + // Set value to hidden input + ModuleUsersGroupsExtensionDropdown.$hiddenInput.val(response.data.group_id); + + // Update dropdown + ModuleUsersGroupsExtensionDropdown.updateDropdownValue(); + } + }, + error(xhr, status, error) { + console.error('ModuleUsersGroups: Failed to load user group', error); + } + }); + } +}; + +// Initialize when document is ready +$(document).ready(() => { + ModuleUsersGroupsExtensionDropdown.initialize(); +}); diff --git a/public/assets/js/src/module-users-groups-index.js b/public/assets/js/src/module-users-groups-index.js index 61347be..e38f495 100644 --- a/public/assets/js/src/module-users-groups-index.js +++ b/public/assets/js/src/module-users-groups-index.js @@ -47,6 +47,12 @@ const ModuleCGIndex = { */ $selectGroup: $('.select-group'), + /** + * jQuery object for default group dropdown. + * @type {jQuery} + */ + $defaultGroupDropdown: $('.select-default-group'), + /** * jQuery object for current form disability fields * @type {jQuery} @@ -79,6 +85,11 @@ const ModuleCGIndex = { onChange: ModuleCGIndex.changeGroupInList, }); + // Initialize default group dropdown + ModuleCGIndex.$defaultGroupDropdown.dropdown({ + onChange: ModuleCGIndex.changeDefaultGroup, + }); + }, /** @@ -163,8 +174,8 @@ const ModuleCGIndex = { group_id: value, }, onSuccess() { - // ModuleCGIndex.initializeDataTable(); - // console.log('updated'); + // Update group member counters after successful group change + ModuleCGIndex.updateGroupCounters(); }, onError(response) { console.log(response); @@ -172,6 +183,76 @@ const ModuleCGIndex = { }); }, + /** + * Update group member counters via API + */ + updateGroupCounters() { + $.ajax({ + url: '/pbxcore/api/modules/ModuleUsersGroups/getGroupsStats', + method: 'GET', + dataType: 'json', + success(response) { + if (response.result === true && response.data && response.data.stats) { + const stats = response.data.stats; + + // Update each group's member count in the table + Object.keys(stats).forEach((groupId) => { + const count = stats[groupId]; + const $row = $(`#users-groups-table tr#${groupId}`); + if ($row.length > 0) { + // Find the counter cell (second td with center aligned class) + $row.find('td.center.aligned').first().text(count); + } + }); + } + }, + error(xhr, status, error) { + console.error('ModuleUsersGroups: Failed to update group counters', error); + }, + }); + }, + + /** + * Handles default group change. + * @param {string} value - The new default group value. + * @param {string} text - The new group text. + * @param {jQuery} $choice - The selected choice. + */ + changeDefaultGroup(value, text, $choice) { + if (!value || value === '') { + return; + } + + // Add loading state to dropdown + ModuleCGIndex.$defaultGroupDropdown.addClass('loading'); + + $.ajax({ + url: '/pbxcore/api/modules/ModuleUsersGroups/setDefaultGroup', + method: 'POST', + dataType: 'json', + data: { + group_id: value, + }, + success(response) { + if (response.result !== true) { + // Show error notification only on failure + const errorMessage = response.messages && response.messages.length > 0 + ? response.messages.join(', ') + : 'Failed to update default group'; + UserMessage.showError(errorMessage); + } + }, + error(xhr, status, error) { + console.error('ModuleUsersGroups: Failed to set default group', error); + UserMessage.showError('Failed to update default group'); + }, + complete() { + // Remove loading state from dropdown + ModuleCGIndex.$defaultGroupDropdown.removeClass('loading'); + }, + }); + }, + /** * Calculate data table page length *