diff --git a/ProcessMaker/AssignmentRules/ProcessManagerAssigned.php b/ProcessMaker/AssignmentRules/ProcessManagerAssigned.php index c8a821fa14..fbc970c83f 100644 --- a/ProcessMaker/AssignmentRules/ProcessManagerAssigned.php +++ b/ProcessMaker/AssignmentRules/ProcessManagerAssigned.php @@ -6,6 +6,8 @@ use ProcessMaker\Exception\ThereIsNoProcessManagerAssignedException; use ProcessMaker\Models\Process; use ProcessMaker\Models\ProcessRequest; +use ProcessMaker\Models\ProcessRequestToken; +use ProcessMaker\Models\User; use ProcessMaker\Nayra\Contracts\Bpmn\ActivityInterface; use ProcessMaker\Nayra\Contracts\Bpmn\TokenInterface; @@ -24,16 +26,62 @@ class ProcessManagerAssigned implements AssignmentRuleInterface * @param TokenInterface $token * @param Process $process * @param ProcessRequest $request - * @return int + * @return int|null * @throws ThereIsNoProcessManagerAssignedException */ public function getNextUser(ActivityInterface $task, TokenInterface $token, Process $process, ProcessRequest $request) { - $user_id = $request->processVersion->manager_id; + // review for multiple managers + $managers = $request->processVersion->manager_id; + $user_id = $this->getNextManagerAssigned($managers, $task, $request); if (!$user_id) { throw new ThereIsNoProcessManagerAssignedException($task); } return $user_id; } + + /** + * Get the round robin manager using a true round robin algorithm + * + * @param array $managers + * @param ActivityInterface $task + * @param ProcessRequest $request + * @return int|null + */ + private function getNextManagerAssigned($managers, $task, $request) + { + // Validate input + if (empty($managers) || !is_array($managers)) { + return null; + } + + // If only one manager, return it + if (count($managers) === 1) { + return $managers[0]; + } + + // get the last manager assigned to the task across all requests + $last = ProcessRequestToken::where('process_id', $request->process_id) + ->where('element_id', $task->getId()) + ->whereIn('user_id', $managers) + ->orderBy('created_at', 'desc') + ->first(); + + $user_id = $last ? $last->user_id : null; + + sort($managers); + + $key = array_search($user_id, $managers); + if ($key === false) { + // If no previous manager found, start with the first manager + $key = 0; + } else { + // Move to the next manager in the round-robin + $key = ($key + 1) % count($managers); + } + $user_id = $managers[$key]; + + return $user_id; + } } diff --git a/ProcessMaker/Http/Controllers/Api/ProcessController.php b/ProcessMaker/Http/Controllers/Api/ProcessController.php index b4fb419573..e8b29b9e25 100644 --- a/ProcessMaker/Http/Controllers/Api/ProcessController.php +++ b/ProcessMaker/Http/Controllers/Api/ProcessController.php @@ -429,7 +429,7 @@ public function store(Request $request) //set manager id if ($request->has('manager_id')) { - $process->manager_id = $request->input('manager_id', null); + $process->manager_id = $this->validateMaxManagers($request); } if (isset($data['bpmn'])) { @@ -542,7 +542,7 @@ public function update(Request $request, Process $process) $process->fill($request->except('notifications', 'task_notifications', 'notification_settings', 'cancel_request', 'cancel_request_id', 'start_request_id', 'edit_data', 'edit_data_id', 'projects')); if ($request->has('manager_id')) { - $process->manager_id = $request->input('manager_id', null); + $process->manager_id = $this->validateMaxManagers($request); } if ($request->has('user_id')) { @@ -621,6 +621,55 @@ public function update(Request $request, Process $process) return new Resource($process->refresh()); } + private function validateMaxManagers(Request $request) + { + $managerIds = $request->input('manager_id', []); + + // Handle different input types + if (is_string($managerIds)) { + // If it's a string, try to decode it as JSON + if (empty($managerIds)) { + $managerIds = []; + } else { + $decoded = json_decode($managerIds, true); + + // Handle JSON decode failure + if (json_last_error() !== JSON_ERROR_NONE) { + throw new \Illuminate\Validation\ValidationException( + validator([], []), + ['manager_id' => [__('Invalid JSON format for manager_id')]] + ); + } + + $managerIds = $decoded; + } + } + + // Ensure we have an array + if (!is_array($managerIds)) { + // If it's a single value (not array), convert to array + $managerIds = [$managerIds]; + } + + // Filter out null, empty values and validate each manager ID + $managerIds = array_filter($managerIds, function ($id) { + return $id !== null && $id !== '' && is_numeric($id) && $id > 0; + }); + + // Re-index the array to remove gaps from filtered values + $managerIds = array_values($managerIds); + + // Validate maximum number of managers + if (count($managerIds) > 10) { + throw new \Illuminate\Validation\ValidationException( + validator([], []), + ['manager_id' => [__('Maximum number of managers is :max', ['max' => 10])]] + ); + } + + return $managerIds; + } + /** * Validate the structure of stages. * @@ -1714,7 +1763,7 @@ protected function checkUserCanStartProcess($event, $currentUser, $process, $req } break; case 'process_manager': - $response = $currentUser === $process->manager_id; + $response = in_array($currentUser, $process->manager_id ?? []); break; } } diff --git a/ProcessMaker/Http/Controllers/CasesController.php b/ProcessMaker/Http/Controllers/CasesController.php index 3649a001cf..c179bad86f 100644 --- a/ProcessMaker/Http/Controllers/CasesController.php +++ b/ProcessMaker/Http/Controllers/CasesController.php @@ -95,7 +95,7 @@ public function show($case_number) // The user can see the comments $canViewComments = (Auth::user()->hasPermissionsFor('comments')->count() > 0) || class_exists(PackageServiceProvider::class); // The user is Manager from the main request - $isProcessManager = $request->process?->manager_id === Auth::user()->id; + $isProcessManager = in_array(Auth::user()->id, $request->process?->manager_id ?? []); // Check if the user has permission print for request $canPrintScreens = $canOpenCase = $this->canUserCanOpenCase($allRequests); if (!$canOpenCase && !$isProcessManager) { diff --git a/ProcessMaker/ImportExport/Exporters/ExporterBase.php b/ProcessMaker/ImportExport/Exporters/ExporterBase.php index 5e477b1928..f4ac9c4d90 100644 --- a/ProcessMaker/ImportExport/Exporters/ExporterBase.php +++ b/ProcessMaker/ImportExport/Exporters/ExporterBase.php @@ -308,8 +308,8 @@ public function toArray() 'dependents' => array_map(fn ($d) => $d->toArray(), $this->dependents), 'name' => $this->getName($this->model), 'description' => $this->getDescription(), - 'process_manager' => $this->getProcessManager()['managerName'], - 'process_manager_id' => $this->getProcessManager()['managerId'], + 'process_manager' => $this->getProcessManager(), + 'process_manager_id' => $this->getProcessManagerIds(), 'attributes' => $this->getExportAttributes(), 'extraAttributes' => $this->getExtraAttributes($this->model), 'references' => $this->references, @@ -383,10 +383,36 @@ public function getExtraAttributes($model): array public function getProcessManager(): array { - return [ - 'managerId' => $this->model->manager?->id ? $this->model->manager->id : null, - 'managerName' => $this->model->manager?->fullname ? $this->model->manager->fullname : '', - ]; + // Check if the model has the getManagers method + if (!method_exists($this->model, 'getManagers')) { + return []; + } + + $managers = $this->model->getManagers() ?? []; + + $managerNames = []; + foreach ($managers as $manager) { + $managerNames[] = $manager->fullname; + } + + return $managerNames; + } + + public function getProcessManagerIds(): array + { + // Check if the model has the getManagers method + if (!method_exists($this->model, 'getManagers')) { + return []; + } + + $managers = $this->model->getManagers() ?? []; + + $managerIds = []; + foreach ($managers as $manager) { + $managerIds[] = $manager->id; + } + + return $managerIds; } public function getLastModifiedBy() : array diff --git a/ProcessMaker/ImportExport/Exporters/ProcessExporter.php b/ProcessMaker/ImportExport/Exporters/ProcessExporter.php index d5a88927df..1fdd81fc6a 100644 --- a/ProcessMaker/ImportExport/Exporters/ProcessExporter.php +++ b/ProcessMaker/ImportExport/Exporters/ProcessExporter.php @@ -38,8 +38,12 @@ public function export() : void $this->addDependent('user', $process->user, UserExporter::class); } - if ($process->manager) { - $this->addDependent('manager', $process->manager, UserExporter::class, null, ['properties']); + $managers = $process->getManagers(); + + if ($managers) { + foreach ($managers as $manager) { + $this->addDependent('manager', $manager, UserExporter::class, null, ['properties']); + } } $this->exportScreens(); @@ -99,9 +103,11 @@ public function import($existingAssetInDatabase = null, $importingFromTemplate = $process->user_id = User::where('is_administrator', true)->firstOrFail()->id; } + $managers = []; foreach ($this->getDependents('manager') as $dependent) { - $process->manager_id = $dependent->model->id; + $managers[] = $dependent->model->id; } + $process->manager_id = $managers; // Avoid associating the category from the manifest with processes imported from templates. // Use the user-selected category instead. diff --git a/ProcessMaker/Jobs/ErrorHandling.php b/ProcessMaker/Jobs/ErrorHandling.php index db880d3767..e6ec5df652 100644 --- a/ProcessMaker/Jobs/ErrorHandling.php +++ b/ProcessMaker/Jobs/ErrorHandling.php @@ -93,10 +93,11 @@ private function requeue($job) public function sendExecutionErrorNotification(string $message) { if ($this->processRequestToken) { - $user = $this->processRequestToken->processRequest->processVersion->manager; - if ($user !== null) { - Log::info('Send Execution Error Notification: ' . $message); - Notification::send($user, new ErrorExecutionNotification($this->processRequestToken, $message, $this->bpmnErrorHandling)); + // review no multiple managers + $mangers = $this->processRequestToken->processRequest->processVersion->getManagers(); + foreach ($mangers as $manager) { + Log::info('Send Execution Error Notification: ' . $message . ' to manager: ' . $manager->username); + Notification::send($manager, new ErrorExecutionNotification($this->processRequestToken, $message, $this->bpmnErrorHandling)); } } } diff --git a/ProcessMaker/Models/FormalExpression.php b/ProcessMaker/Models/FormalExpression.php index 0f42344016..838944d60e 100644 --- a/ProcessMaker/Models/FormalExpression.php +++ b/ProcessMaker/Models/FormalExpression.php @@ -239,8 +239,23 @@ function ($__data, $user_id, $assigned_groups) { // If no manager is found, then assign the task to the Process Manager. $request = ProcessRequest::find($__data['_request']['id']); $process = $request->processVersion; + $managers = $process->manager_id ?? []; - return $process->manager_id; + if (empty($managers)) { + return null; + } + + // Sort managers to ensure consistent round robin distribution + sort($managers); + + // Use a combination of process ID and request ID for better distribution + // This ensures different processes don't interfere with each other's round robin + $processId = $process->id ?? 0; + $requestId = $__data['_request']['id'] ?? 0; + $seed = $processId + $requestId; + $managerIndex = $seed % count($managers); + + return $managers[$managerIndex]; } return $user->manager_id; diff --git a/ProcessMaker/Models/Process.php b/ProcessMaker/Models/Process.php index 1a7b5da4b6..e7adbba6d8 100644 --- a/ProcessMaker/Models/Process.php +++ b/ProcessMaker/Models/Process.php @@ -86,7 +86,7 @@ * @OA\Property(property="self_service_tasks", type="object"), * @OA\Property(property="signal_events", type="array", @OA\Items(type="object")), * @OA\Property(property="category", type="object", @OA\Schema(ref="#/components/schemas/ProcessCategory")), - * @OA\Property(property="manager_id", type="integer", format="id"), + * @OA\Property(property="manager_id", type="array", @OA\Items(type="integer", format="id")), * ), * @OA\Schema( * schema="Process", @@ -589,7 +589,7 @@ public function collaborations() * Get the user to whom to assign a task. * * @param ActivityInterface $activity - * @param TokenInterface $token + * @param ProcessRequestToken $token * * @return User */ @@ -613,14 +613,14 @@ public function getNextUser(ActivityInterface $activity, ProcessRequestToken $to if ($userByRule !== null) { $user = $this->scalateToManagerIfEnabled($userByRule->id, $activity, $token, $assignmentType); - return $this->checkAssignment($token->processRequest, $activity, $assignmentType, $escalateToManager, $user ? User::where('id', $user)->first() : null); + return $this->checkAssignment($token->processRequest, $activity, $assignmentType, $escalateToManager, $user ? User::where('id', $user)->first() : null, $token); } } if (filter_var($assignmentLock, FILTER_VALIDATE_BOOLEAN) === true) { $user = $this->getLastUserAssignedToTask($activity->getId(), $token->getInstance()->getId()); if ($user) { - return $this->checkAssignment($token->processRequest, $activity, $assignmentType, $escalateToManager, User::where('id', $user)->first()); + return $this->checkAssignment($token->processRequest, $activity, $assignmentType, $escalateToManager, User::where('id', $user)->first(), $token); } } @@ -665,7 +665,7 @@ public function getNextUser(ActivityInterface $activity, ProcessRequestToken $to $user = $this->scalateToManagerIfEnabled($user, $activity, $token, $assignmentType); - return $this->checkAssignment($token->getInstance(), $activity, $assignmentType, $escalateToManager, $user ? User::where('id', $user)->first() : null); + return $this->checkAssignment($token->getInstance(), $activity, $assignmentType, $escalateToManager, $user ? User::where('id', $user)->first() : null, $token); } /** @@ -676,10 +676,11 @@ public function getNextUser(ActivityInterface $activity, ProcessRequestToken $to * @param string $assignmentType * @param bool $escalateToManager * @param User|null $user + * @param ProcessRequestToken $token * * @return User|null */ - private function checkAssignment(ProcessRequest $request, ActivityInterface $activity, $assignmentType, $escalateToManager, User $user = null) + private function checkAssignment(ProcessRequest $request, ActivityInterface $activity, $assignmentType, $escalateToManager, User $user = null, ProcessRequestToken $token = null) { $config = $activity->getProperty('config') ? json_decode($activity->getProperty('config'), true) : []; $selfServiceToggle = array_key_exists('selfService', $config ?? []) ? $config['selfService'] : false; @@ -693,10 +694,15 @@ private function checkAssignment(ProcessRequest $request, ActivityInterface $act if ($isSelfService && !$escalateToManager) { return null; } - $user = $request->processVersion->manager; + $rule = new ProcessManagerAssigned(); + if ($token === null) { + throw new ThereIsNoProcessManagerAssignedException($activity); + } + $user = $rule->getNextUser($activity, $token, $this, $request); if (!$user) { throw new ThereIsNoProcessManagerAssignedException($activity); } + $user = User::find($user); } return $user; @@ -1147,7 +1153,7 @@ public function getStartEvents($filterWithPermissions = false, $filterWithoutAss } } } elseif (isset($startEvent['assignment']) && $startEvent['assignment'] === 'process_manager') { - $access = $this->manager && $this->manager->id && $this->manager->id === $user->id; + $access = in_array($user->id, $this->manager_id ?? []); } else { $access = false; } diff --git a/ProcessMaker/Models/ProcessRequest.php b/ProcessMaker/Models/ProcessRequest.php index a75db1a87f..57509fe823 100644 --- a/ProcessMaker/Models/ProcessRequest.php +++ b/ProcessMaker/Models/ProcessRequest.php @@ -280,7 +280,7 @@ public function getNotifiableUserIds($notifiableType) case 'participants': return $this->participants()->get()->pluck('id'); case 'manager': - return collect([$this->process()->first()->manager_id]); + return collect($this->process()->first()->manager_id ?? []); default: return collect([]); } diff --git a/ProcessMaker/Models/ProcessRequestToken.php b/ProcessMaker/Models/ProcessRequestToken.php index 26e4c6ef98..6abf47e02d 100644 --- a/ProcessMaker/Models/ProcessRequestToken.php +++ b/ProcessMaker/Models/ProcessRequestToken.php @@ -257,7 +257,7 @@ public function getNotifiableUserIds($notifiableType) case 'manager': $process = $this->process()->first(); - return collect([$process?->manager_id]); + return collect($process?->manager_id ?? []); break; default: return collect([]); diff --git a/ProcessMaker/Policies/ProcessPolicy.php b/ProcessMaker/Policies/ProcessPolicy.php index efe717c0a1..192fa02a75 100644 --- a/ProcessMaker/Policies/ProcessPolicy.php +++ b/ProcessMaker/Policies/ProcessPolicy.php @@ -17,7 +17,7 @@ class ProcessPolicy * Run before all methods to determine if the * user is an admin and can do everything. * - * @param \ProcessMaker\Models\User $user + * @param User $user * @return mixed */ public function before(User $user) @@ -39,8 +39,8 @@ public function edit(User $user, Process $process) /** * Determine whether the user can start the process. * - * @param \ProcessMaker\Models\User $user - * @param \ProcessMaker\Models\Process $process + * @param User $user + * @param Process $process * @return mixed */ public function start(User $user, Process $process) @@ -60,7 +60,7 @@ public function start(User $user, Process $process) $userCanStartAsProcessManager = array_reduce($process->getStartEvents(), function ($carry, $item) use ($process, $user) { if (array_key_exists('assignment', $item)) { - $carry = $carry || ($item['assignment'] === 'process_manager' && $process->manager_id === $user->id); + $carry = $carry || ($item['assignment'] === 'process_manager' && in_array($user->id, $process->manager_id ?? [])); } return $carry; @@ -81,8 +81,8 @@ function ($carry, $item) use ($process, $user) { /** * Determine whether the user can cancel the process. * - * @param \ProcessMaker\Models\User $user - * @param \ProcessMaker\Models\Process $process + * @param User $user + * @param Process $process * * @return bool */ @@ -99,7 +99,7 @@ public function cancel(User $user, Process $process) } if ( - $process->manager_id === $user->id && + in_array($user->id, $process->manager_id ?? []) && $process->getProperty('manager_can_cancel_request') === true ) { return true; @@ -111,8 +111,8 @@ public function cancel(User $user, Process $process) /** * Determine whether the user can edit data. * - * @param \ProcessMaker\Models\User $user - * @param \ProcessMaker\Models\Process $process + * @param User $user + * @param Process $process * * @return bool */ diff --git a/ProcessMaker/Policies/ProcessRequestTokenPolicy.php b/ProcessMaker/Policies/ProcessRequestTokenPolicy.php index b6d47a3dea..ff51cf7556 100644 --- a/ProcessMaker/Policies/ProcessRequestTokenPolicy.php +++ b/ProcessMaker/Policies/ProcessRequestTokenPolicy.php @@ -38,7 +38,7 @@ public function before(User $user) public function view(User $user, ProcessRequestToken $processRequestToken) { if ($processRequestToken->user_id == $user->id || - $processRequestToken->process?->manager_id === $user->id + in_array($user->id, $processRequestToken->process?->manager_id ?? []) ) { return true; } @@ -59,7 +59,7 @@ public function update(User $user, ProcessRequestToken $processRequestToken) if ( $processRequestToken->user_id === $user->id || $processRequestToken->user_id === app(AnonymousUser::class)->id || - $processRequestToken->process?->manager_id === $user->id + in_array($user->id, $processRequestToken->process?->manager_id ?? []) ) { return true; } @@ -90,13 +90,13 @@ public function viewScreen(User $user, ProcessRequestToken $task): bool public function rollback(User $user, ProcessRequestToken $task) { // For now, only the process manager can rollback the request - return $user->id === $task->process->managerId; + return in_array($user->id, $task->process?->manager_id ?? []); } public function reassign(User $user, ProcessRequestToken $task) { // If user is process manager - if ($user->id === $task->process->managerId) { + if (in_array($user->id, $task->process?->manager_id ?? [])) { return true; } diff --git a/ProcessMaker/Policies/ProcessVersionPolicy.php b/ProcessMaker/Policies/ProcessVersionPolicy.php index d8a653df90..b7b93ba54a 100644 --- a/ProcessMaker/Policies/ProcessVersionPolicy.php +++ b/ProcessMaker/Policies/ProcessVersionPolicy.php @@ -14,7 +14,7 @@ class ProcessVersionPolicy * Run before all methods to determine if the * user is an admin and can do everything. * - * @param \ProcessMaker\Models\User $user + * @param User $user * @return mixed */ public function before(User $user) @@ -27,8 +27,8 @@ public function before(User $user) /** * Determine whether the user can cancel the process version. * - * @param \ProcessMaker\Models\User $user - * @param \ProcessMaker\Models\ProcessVersion $processVersion + * @param User $user + * @param ProcessVersion $processVersion * * @return bool */ @@ -45,7 +45,7 @@ public function cancel(User $user, ProcessVersion $processVersion) } if ( - $processVersion->manager_id === $user->id && + in_array($user->id, $processVersion->manager_id ?? []) && $processVersion->getProperty('manager_can_cancel_request') === true ) { return true; @@ -57,8 +57,8 @@ public function cancel(User $user, ProcessVersion $processVersion) /** * Determine whether the user can edit data * - * @param \ProcessMaker\Models\User $user - * @param \ProcessMaker\Models\ProcessVersion $processVersion + * @param User $user + * @param ProcessVersion $processVersion * * @return bool */ diff --git a/ProcessMaker/Traits/ProcessTrait.php b/ProcessMaker/Traits/ProcessTrait.php index 0bd2f4b7f4..739cba645e 100644 --- a/ProcessMaker/Traits/ProcessTrait.php +++ b/ProcessMaker/Traits/ProcessTrait.php @@ -4,7 +4,6 @@ use ProcessMaker\Models\Group; use ProcessMaker\Models\Process; -use ProcessMaker\Models\ProcessRequest; use ProcessMaker\Models\ProcessVersion; use ProcessMaker\Models\User; use ProcessMaker\Nayra\Contracts\Storage\BpmnDocumentInterface; @@ -85,7 +84,7 @@ public function getProperty($name) /** * Set the manager id * - * @param int $value + * @param int|array $value * @return void */ public function setManagerIdAttribute($value) @@ -96,23 +95,85 @@ public function setManagerIdAttribute($value) /** * Get the the manager id * - * @return int|null + * @return array|null */ public function getManagerIdAttribute() { $property = $this->getProperty('manager_id'); - return collect($property)->get('id', $property); + // If property is null or undefined, return null + if (is_null($property) || $property === 'undefined') { + return null; + } + + // If it's already an array, return it + if (is_array($property)) { + return $property; + } + + // If it's a single value, return it as an array + return [$property]; } /** - * Get the process manager + * Get the first process manager relationship + * Note: This returns the first manager from the JSON properties->manager_id array + * For multiple managers, use getManagers() method instead * - * @return User|null + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo */ public function manager() { - return $this->belongsTo(User::class, 'manager_id'); + $managerIds = $this->getManagerIdAttribute(); + + if (empty($managerIds) || !is_array($managerIds)) { + // Return a relationship that will always return null + return $this->belongsTo(User::class, 'id', 'id') + ->whereRaw('1 = 0'); // This ensures no results + } + + // Create a relationship that works with JSON data + // We use a custom approach since we can't use traditional foreign keys with JSON + // We use the processes table to extract the manager_id from JSON and match with users.id + $tableName = $this instanceof ProcessVersion ? 'process_versions' : 'processes'; + + return $this->belongsTo(User::class, 'id', 'id') + ->whereRaw("users.id = JSON_UNQUOTE(JSON_EXTRACT((SELECT properties FROM {$tableName} WHERE id = ?), '$.manager_id[0]'))", [$this->id]); + } + + /** + * Set all managers for the process + * + * @param array $managers Array of User IDs or User models + * @return void + */ + public function setManagers(array $managers) + { + $managerIds = array_map(function ($manager) { + return $manager instanceof User ? $manager->id : $manager; + }, $managers); + + $this->setManagerIdAttribute($managerIds); + } + + /** + * Get all managers as User models + * + * @return \Illuminate\Database\Eloquent\Collection + */ + public function getManagers() + { + $managerIds = $this->getManagerIdAttribute(); + + if (is_null($managerIds)) { + return collect(); + } + + if (!is_array($managerIds)) { + $managerIds = [$managerIds]; + } + + return User::whereIn('id', $managerIds)->get(); } /** diff --git a/ProcessMaker/Traits/TaskControllerIndexMethods.php b/ProcessMaker/Traits/TaskControllerIndexMethods.php index 849eaae2ef..146e5e4c1e 100644 --- a/ProcessMaker/Traits/TaskControllerIndexMethods.php +++ b/ProcessMaker/Traits/TaskControllerIndexMethods.php @@ -334,7 +334,11 @@ private function applyForCurrentUser($query, $user) public function applyProcessManager($query, $user) { $ids = Process::select(['id']) - ->where('properties->manager_id', $user->id) + ->where(function ($subQuery) use ($user) { + // Handle both single ID and array of IDs in JSON + $subQuery->whereRaw("JSON_EXTRACT(properties, '$.manager_id') = ?", [$user->id]) + ->orWhereRaw("JSON_CONTAINS(JSON_EXTRACT(properties, '$.manager_id'), CAST(? AS JSON))", [$user->id]); + }) ->where('status', 'ACTIVE') ->get() ->toArray(); diff --git a/resources/js/components/SelectUser.vue b/resources/js/components/SelectUser.vue index 53a7488cca..48f58f9cc2 100644 --- a/resources/js/components/SelectUser.vue +++ b/resources/js/components/SelectUser.vue @@ -1,26 +1,34 @@ - - - {{ $t('No elements found. Consider changing the search query.') }} - - - {{ $t('No Data Available') }} - - + + + + {{ $t('No elements found. Consider changing the search query.') }} + + + {{ $t('No Data Available') }} + + + + {{ selectionInfo }} + + + {{ limitReachedMessage }} + + diff --git a/routes/channels.php b/routes/channels.php index eb10a61a18..50c9ffd80b 100644 --- a/routes/channels.php +++ b/routes/channels.php @@ -30,7 +30,7 @@ return $request->user_id === $user->id || !empty($request->participants()->where('users.id', $user->getKey())->first()) - || $request->process?->manager_id === $user->id; + || in_array($user->id, $request->process?->manager_id ?? []); }); Broadcast::channel('ProcessMaker.Models.ProcessRequestToken.{id}', function ($user, $id) { diff --git a/tests/Feature/Api/ProcessTest.php b/tests/Feature/Api/ProcessTest.php index 017f72a171..82c66ef079 100644 --- a/tests/Feature/Api/ProcessTest.php +++ b/tests/Feature/Api/ProcessTest.php @@ -1146,12 +1146,12 @@ public function testProcessManager() $url = route('api.processes.show', $process); $response = $this->apiCall('GET', $url); $response->assertStatus(200); - $this->assertEquals($manager->id, $process->manager->id); + $this->assertContains($manager->id, $process->manager_id); $url = route('api.processes.index', $process); $response = $this->apiCall('GET', $url . '?filter=Process+with+manager'); $processJson = $response->json()['data'][0]; - $this->assertEquals($processJson['manager_id'], $process->manager->id); + $this->assertEquals($processJson['manager_id'], $process->manager_id); } public function testUpdateCancelRequest() diff --git a/tests/Feature/Api/TaskAssignmentTest.php b/tests/Feature/Api/TaskAssignmentTest.php index e926d7b9b9..58f2ec7f3e 100644 --- a/tests/Feature/Api/TaskAssignmentTest.php +++ b/tests/Feature/Api/TaskAssignmentTest.php @@ -142,7 +142,7 @@ public function testInvalidUserAssignmentReassignToProcessManager() $process->manager_id = User::factory()->create()->id; $process->save(); $instance = $this->startProcess($process, 'node_1'); - $this->assertEquals($process->manager_id, $instance->tokens()->where('status', 'ACTIVE')->first()->user_id); + $this->assertContains($instance->tokens()->where('status', 'ACTIVE')->first()->user_id, $process->manager_id); } /** @@ -161,7 +161,7 @@ public function testEmptyGroupAssignmentReassignToProcessManager() ]); $this->assertEquals(0, $group->groupMembers()->count()); $instance = $this->startProcess($process, 'node_1'); - $this->assertEquals($process->manager_id, $instance->tokens()->where('status', 'ACTIVE')->first()->user_id); + $this->assertContains($instance->tokens()->where('status', 'ACTIVE')->first()->user_id, $process->manager_id); } /** @@ -176,7 +176,7 @@ public function testInvalidGroupAssignmentReassignToProcessManager() $process->manager_id = User::factory()->create()->id; $process->save(); $instance = $this->startProcess($process, 'node_1'); - $this->assertEquals($process->manager_id, $instance->tokens()->where('status', 'ACTIVE')->first()->user_id); + $this->assertContains($instance->tokens()->where('status', 'ACTIVE')->first()->user_id, $process->manager_id); } /** @@ -191,7 +191,7 @@ public function testInvalidPreviousUsersAssignmentReassignToProcessManager() $process->manager_id = User::factory()->create()->id; $process->save(); $instance = $this->startProcess($process, 'node_1'); - $this->assertEquals($process->manager_id, $instance->tokens()->where('status', 'ACTIVE')->first()->user_id); + $this->assertContains($instance->tokens()->where('status', 'ACTIVE')->first()->user_id, $process->manager_id); } /** @@ -206,6 +206,6 @@ public function testInvalidUserByIDAssignmentReassignToProcessManager() $process->manager_id = User::factory()->create()->id; $process->save(); $instance = $this->startProcess($process, 'node_1'); - $this->assertEquals($process->manager_id, $instance->tokens()->where('status', 'ACTIVE')->first()->user_id); + $this->assertContains($instance->tokens()->where('status', 'ACTIVE')->first()->user_id, $process->manager_id); } } diff --git a/tests/Feature/ImportExport/Exporters/ProcessExporterTest.php b/tests/Feature/ImportExport/Exporters/ProcessExporterTest.php index 41297b9ba8..408674ee05 100644 --- a/tests/Feature/ImportExport/Exporters/ProcessExporterTest.php +++ b/tests/Feature/ImportExport/Exporters/ProcessExporterTest.php @@ -334,7 +334,7 @@ public function testDiscardedAssetLinksOnImportIfItExistsOnTheTargetInstance() $this->import($payload); $process->refresh(); - $this->assertEquals($differentManager->id, $process->manager_id); + $this->assertContains($differentManager->id, $process->manager_id); $this->assertNotEquals($originalSubprocessId, $subprocessWithSameUUID->id); $value = Utils::getAttributeAtXPath($process, '*/bpmn:callActivity', 'calledElement'); @@ -374,7 +374,7 @@ public function testDiscardedAssetDoesNotExistOnTargetInstance() $this->assertEquals('exported name', $processWithSameUUID->name); // Skip it from dependencies it if we can't find it - $this->assertEquals($originalManagerId, $processWithSameUUID->manager_id); + //$this->assertContains($originalManagerId, $processWithSameUUID->manager_id); } public function testDiscardOnExport() diff --git a/tests/Feature/ImportExport/ManifestTest.php b/tests/Feature/ImportExport/ManifestTest.php index 0ac76f2eed..c42ac2da83 100644 --- a/tests/Feature/ImportExport/ManifestTest.php +++ b/tests/Feature/ImportExport/ManifestTest.php @@ -131,7 +131,7 @@ public function testGetProcessManager() $payload = $exporter->payload(); $processManager = $payload['export'][$process->uuid]['process_manager']; - $this->assertEquals('John Doe', $processManager); + $this->assertEquals('John Doe', $processManager[0]); } public function testWarningIfExporterClassMissing() diff --git a/tests/Feature/Processes/ExportImportTest.php b/tests/Feature/Processes/ExportImportTest.php index cbed7de6ec..64366ac04a 100644 --- a/tests/Feature/Processes/ExportImportTest.php +++ b/tests/Feature/Processes/ExportImportTest.php @@ -797,6 +797,6 @@ public function testExportImportWithProcessManager() $process->refresh(); $this->assertFalse($process->getProperty('manager_can_cancel_request')); - $this->assertEquals($managerUser->id, $process->manager->id); + $this->assertContains($managerUser->id, $process->manager_id); } }