Skip to content

Commit 8a60016

Browse files
authored
Merge pull request #8549 from ProcessMaker/task/FOUR-26777
Process Manager - Number of Users to Assign
2 parents 2535d62 + be56304 commit 8a60016

30 files changed

+407
-110
lines changed

ProcessMaker/AssignmentRules/ProcessManagerAssigned.php

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
use ProcessMaker\Exception\ThereIsNoProcessManagerAssignedException;
77
use ProcessMaker\Models\Process;
88
use ProcessMaker\Models\ProcessRequest;
9+
use ProcessMaker\Models\ProcessRequestToken;
10+
use ProcessMaker\Models\User;
911
use ProcessMaker\Nayra\Contracts\Bpmn\ActivityInterface;
1012
use ProcessMaker\Nayra\Contracts\Bpmn\TokenInterface;
1113

@@ -24,16 +26,62 @@ class ProcessManagerAssigned implements AssignmentRuleInterface
2426
* @param TokenInterface $token
2527
* @param Process $process
2628
* @param ProcessRequest $request
27-
* @return int
29+
* @return int|null
2830
* @throws ThereIsNoProcessManagerAssignedException
2931
*/
3032
public function getNextUser(ActivityInterface $task, TokenInterface $token, Process $process, ProcessRequest $request)
3133
{
32-
$user_id = $request->processVersion->manager_id;
34+
// review for multiple managers
35+
$managers = $request->processVersion->manager_id;
36+
$user_id = $this->getNextManagerAssigned($managers, $task, $request);
3337
if (!$user_id) {
3438
throw new ThereIsNoProcessManagerAssignedException($task);
3539
}
3640

3741
return $user_id;
3842
}
43+
44+
/**
45+
* Get the round robin manager using a true round robin algorithm
46+
*
47+
* @param array $managers
48+
* @param ActivityInterface $task
49+
* @param ProcessRequest $request
50+
* @return int|null
51+
*/
52+
private function getNextManagerAssigned($managers, $task, $request)
53+
{
54+
// Validate input
55+
if (empty($managers) || !is_array($managers)) {
56+
return null;
57+
}
58+
59+
// If only one manager, return it
60+
if (count($managers) === 1) {
61+
return $managers[0];
62+
}
63+
64+
// get the last manager assigned to the task across all requests
65+
$last = ProcessRequestToken::where('process_id', $request->process_id)
66+
->where('element_id', $task->getId())
67+
->whereIn('user_id', $managers)
68+
->orderBy('created_at', 'desc')
69+
->first();
70+
71+
$user_id = $last ? $last->user_id : null;
72+
73+
sort($managers);
74+
75+
$key = array_search($user_id, $managers);
76+
if ($key === false) {
77+
// If no previous manager found, start with the first manager
78+
$key = 0;
79+
} else {
80+
// Move to the next manager in the round-robin
81+
$key = ($key + 1) % count($managers);
82+
}
83+
$user_id = $managers[$key];
84+
85+
return $user_id;
86+
}
3987
}

ProcessMaker/Http/Controllers/Api/ProcessController.php

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -429,7 +429,7 @@ public function store(Request $request)
429429

430430
//set manager id
431431
if ($request->has('manager_id')) {
432-
$process->manager_id = $request->input('manager_id', null);
432+
$process->manager_id = $this->validateMaxManagers($request);
433433
}
434434

435435
if (isset($data['bpmn'])) {
@@ -542,7 +542,7 @@ public function update(Request $request, Process $process)
542542

543543
$process->fill($request->except('notifications', 'task_notifications', 'notification_settings', 'cancel_request', 'cancel_request_id', 'start_request_id', 'edit_data', 'edit_data_id', 'projects'));
544544
if ($request->has('manager_id')) {
545-
$process->manager_id = $request->input('manager_id', null);
545+
$process->manager_id = $this->validateMaxManagers($request);
546546
}
547547

548548
if ($request->has('user_id')) {
@@ -621,6 +621,55 @@ public function update(Request $request, Process $process)
621621
return new Resource($process->refresh());
622622
}
623623

624+
private function validateMaxManagers(Request $request)
625+
{
626+
$managerIds = $request->input('manager_id', []);
627+
628+
// Handle different input types
629+
if (is_string($managerIds)) {
630+
// If it's a string, try to decode it as JSON
631+
if (empty($managerIds)) {
632+
$managerIds = [];
633+
} else {
634+
$decoded = json_decode($managerIds, true);
635+
636+
// Handle JSON decode failure
637+
if (json_last_error() !== JSON_ERROR_NONE) {
638+
throw new \Illuminate\Validation\ValidationException(
639+
validator([], []),
640+
['manager_id' => [__('Invalid JSON format for manager_id')]]
641+
);
642+
}
643+
644+
$managerIds = $decoded;
645+
}
646+
}
647+
648+
// Ensure we have an array
649+
if (!is_array($managerIds)) {
650+
// If it's a single value (not array), convert to array
651+
$managerIds = [$managerIds];
652+
}
653+
654+
// Filter out null, empty values and validate each manager ID
655+
$managerIds = array_filter($managerIds, function ($id) {
656+
return $id !== null && $id !== '' && is_numeric($id) && $id > 0;
657+
});
658+
659+
// Re-index the array to remove gaps from filtered values
660+
$managerIds = array_values($managerIds);
661+
662+
// Validate maximum number of managers
663+
if (count($managerIds) > 10) {
664+
throw new \Illuminate\Validation\ValidationException(
665+
validator([], []),
666+
['manager_id' => [__('Maximum number of managers is :max', ['max' => 10])]]
667+
);
668+
}
669+
670+
return $managerIds;
671+
}
672+
624673
/**
625674
* Validate the structure of stages.
626675
*
@@ -1714,7 +1763,7 @@ protected function checkUserCanStartProcess($event, $currentUser, $process, $req
17141763
}
17151764
break;
17161765
case 'process_manager':
1717-
$response = $currentUser === $process->manager_id;
1766+
$response = in_array($currentUser, $process->manager_id ?? []);
17181767
break;
17191768
}
17201769
}

ProcessMaker/Http/Controllers/CasesController.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ public function show($case_number)
9595
// The user can see the comments
9696
$canViewComments = (Auth::user()->hasPermissionsFor('comments')->count() > 0) || class_exists(PackageServiceProvider::class);
9797
// The user is Manager from the main request
98-
$isProcessManager = $request->process?->manager_id === Auth::user()->id;
98+
$isProcessManager = in_array(Auth::user()->id, $request->process?->manager_id ?? []);
9999
// Check if the user has permission print for request
100100
$canPrintScreens = $canOpenCase = $this->canUserCanOpenCase($allRequests);
101101
if (!$canOpenCase && !$isProcessManager) {

ProcessMaker/ImportExport/Exporters/ExporterBase.php

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -308,8 +308,8 @@ public function toArray()
308308
'dependents' => array_map(fn ($d) => $d->toArray(), $this->dependents),
309309
'name' => $this->getName($this->model),
310310
'description' => $this->getDescription(),
311-
'process_manager' => $this->getProcessManager()['managerName'],
312-
'process_manager_id' => $this->getProcessManager()['managerId'],
311+
'process_manager' => $this->getProcessManager(),
312+
'process_manager_id' => $this->getProcessManagerIds(),
313313
'attributes' => $this->getExportAttributes(),
314314
'extraAttributes' => $this->getExtraAttributes($this->model),
315315
'references' => $this->references,
@@ -383,10 +383,36 @@ public function getExtraAttributes($model): array
383383

384384
public function getProcessManager(): array
385385
{
386-
return [
387-
'managerId' => $this->model->manager?->id ? $this->model->manager->id : null,
388-
'managerName' => $this->model->manager?->fullname ? $this->model->manager->fullname : '',
389-
];
386+
// Check if the model has the getManagers method
387+
if (!method_exists($this->model, 'getManagers')) {
388+
return [];
389+
}
390+
391+
$managers = $this->model->getManagers() ?? [];
392+
393+
$managerNames = [];
394+
foreach ($managers as $manager) {
395+
$managerNames[] = $manager->fullname;
396+
}
397+
398+
return $managerNames;
399+
}
400+
401+
public function getProcessManagerIds(): array
402+
{
403+
// Check if the model has the getManagers method
404+
if (!method_exists($this->model, 'getManagers')) {
405+
return [];
406+
}
407+
408+
$managers = $this->model->getManagers() ?? [];
409+
410+
$managerIds = [];
411+
foreach ($managers as $manager) {
412+
$managerIds[] = $manager->id;
413+
}
414+
415+
return $managerIds;
390416
}
391417

392418
public function getLastModifiedBy() : array

ProcessMaker/ImportExport/Exporters/ProcessExporter.php

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,12 @@ public function export() : void
3838
$this->addDependent('user', $process->user, UserExporter::class);
3939
}
4040

41-
if ($process->manager) {
42-
$this->addDependent('manager', $process->manager, UserExporter::class, null, ['properties']);
41+
$managers = $process->getManagers();
42+
43+
if ($managers) {
44+
foreach ($managers as $manager) {
45+
$this->addDependent('manager', $manager, UserExporter::class, null, ['properties']);
46+
}
4347
}
4448

4549
$this->exportScreens();
@@ -99,9 +103,11 @@ public function import($existingAssetInDatabase = null, $importingFromTemplate =
99103
$process->user_id = User::where('is_administrator', true)->firstOrFail()->id;
100104
}
101105

106+
$managers = [];
102107
foreach ($this->getDependents('manager') as $dependent) {
103-
$process->manager_id = $dependent->model->id;
108+
$managers[] = $dependent->model->id;
104109
}
110+
$process->manager_id = $managers;
105111

106112
// Avoid associating the category from the manifest with processes imported from templates.
107113
// Use the user-selected category instead.

ProcessMaker/Jobs/ErrorHandling.php

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -93,10 +93,11 @@ private function requeue($job)
9393
public function sendExecutionErrorNotification(string $message)
9494
{
9595
if ($this->processRequestToken) {
96-
$user = $this->processRequestToken->processRequest->processVersion->manager;
97-
if ($user !== null) {
98-
Log::info('Send Execution Error Notification: ' . $message);
99-
Notification::send($user, new ErrorExecutionNotification($this->processRequestToken, $message, $this->bpmnErrorHandling));
96+
// review no multiple managers
97+
$mangers = $this->processRequestToken->processRequest->processVersion->getManagers();
98+
foreach ($mangers as $manager) {
99+
Log::info('Send Execution Error Notification: ' . $message . ' to manager: ' . $manager->username);
100+
Notification::send($manager, new ErrorExecutionNotification($this->processRequestToken, $message, $this->bpmnErrorHandling));
100101
}
101102
}
102103
}

ProcessMaker/Models/FormalExpression.php

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,8 +239,23 @@ function ($__data, $user_id, $assigned_groups) {
239239
// If no manager is found, then assign the task to the Process Manager.
240240
$request = ProcessRequest::find($__data['_request']['id']);
241241
$process = $request->processVersion;
242+
$managers = $process->manager_id ?? [];
242243

243-
return $process->manager_id;
244+
if (empty($managers)) {
245+
return null;
246+
}
247+
248+
// Sort managers to ensure consistent round robin distribution
249+
sort($managers);
250+
251+
// Use a combination of process ID and request ID for better distribution
252+
// This ensures different processes don't interfere with each other's round robin
253+
$processId = $process->id ?? 0;
254+
$requestId = $__data['_request']['id'] ?? 0;
255+
$seed = $processId + $requestId;
256+
$managerIndex = $seed % count($managers);
257+
258+
return $managers[$managerIndex];
244259
}
245260

246261
return $user->manager_id;

ProcessMaker/Models/Process.php

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@
8686
* @OA\Property(property="self_service_tasks", type="object"),
8787
* @OA\Property(property="signal_events", type="array", @OA\Items(type="object")),
8888
* @OA\Property(property="category", type="object", @OA\Schema(ref="#/components/schemas/ProcessCategory")),
89-
* @OA\Property(property="manager_id", type="integer", format="id"),
89+
* @OA\Property(property="manager_id", type="array", @OA\Items(type="integer", format="id")),
9090
* ),
9191
* @OA\Schema(
9292
* schema="Process",
@@ -589,7 +589,7 @@ public function collaborations()
589589
* Get the user to whom to assign a task.
590590
*
591591
* @param ActivityInterface $activity
592-
* @param TokenInterface $token
592+
* @param ProcessRequestToken $token
593593
*
594594
* @return User
595595
*/
@@ -613,14 +613,14 @@ public function getNextUser(ActivityInterface $activity, ProcessRequestToken $to
613613
if ($userByRule !== null) {
614614
$user = $this->scalateToManagerIfEnabled($userByRule->id, $activity, $token, $assignmentType);
615615

616-
return $this->checkAssignment($token->processRequest, $activity, $assignmentType, $escalateToManager, $user ? User::where('id', $user)->first() : null);
616+
return $this->checkAssignment($token->processRequest, $activity, $assignmentType, $escalateToManager, $user ? User::where('id', $user)->first() : null, $token);
617617
}
618618
}
619619

620620
if (filter_var($assignmentLock, FILTER_VALIDATE_BOOLEAN) === true) {
621621
$user = $this->getLastUserAssignedToTask($activity->getId(), $token->getInstance()->getId());
622622
if ($user) {
623-
return $this->checkAssignment($token->processRequest, $activity, $assignmentType, $escalateToManager, User::where('id', $user)->first());
623+
return $this->checkAssignment($token->processRequest, $activity, $assignmentType, $escalateToManager, User::where('id', $user)->first(), $token);
624624
}
625625
}
626626

@@ -665,7 +665,7 @@ public function getNextUser(ActivityInterface $activity, ProcessRequestToken $to
665665

666666
$user = $this->scalateToManagerIfEnabled($user, $activity, $token, $assignmentType);
667667

668-
return $this->checkAssignment($token->getInstance(), $activity, $assignmentType, $escalateToManager, $user ? User::where('id', $user)->first() : null);
668+
return $this->checkAssignment($token->getInstance(), $activity, $assignmentType, $escalateToManager, $user ? User::where('id', $user)->first() : null, $token);
669669
}
670670

671671
/**
@@ -676,10 +676,11 @@ public function getNextUser(ActivityInterface $activity, ProcessRequestToken $to
676676
* @param string $assignmentType
677677
* @param bool $escalateToManager
678678
* @param User|null $user
679+
* @param ProcessRequestToken $token
679680
*
680681
* @return User|null
681682
*/
682-
private function checkAssignment(ProcessRequest $request, ActivityInterface $activity, $assignmentType, $escalateToManager, User $user = null)
683+
private function checkAssignment(ProcessRequest $request, ActivityInterface $activity, $assignmentType, $escalateToManager, User $user = null, ProcessRequestToken $token = null)
683684
{
684685
$config = $activity->getProperty('config') ? json_decode($activity->getProperty('config'), true) : [];
685686
$selfServiceToggle = array_key_exists('selfService', $config ?? []) ? $config['selfService'] : false;
@@ -693,10 +694,15 @@ private function checkAssignment(ProcessRequest $request, ActivityInterface $act
693694
if ($isSelfService && !$escalateToManager) {
694695
return null;
695696
}
696-
$user = $request->processVersion->manager;
697+
$rule = new ProcessManagerAssigned();
698+
if ($token === null) {
699+
throw new ThereIsNoProcessManagerAssignedException($activity);
700+
}
701+
$user = $rule->getNextUser($activity, $token, $this, $request);
697702
if (!$user) {
698703
throw new ThereIsNoProcessManagerAssignedException($activity);
699704
}
705+
$user = User::find($user);
700706
}
701707

702708
return $user;
@@ -1147,7 +1153,7 @@ public function getStartEvents($filterWithPermissions = false, $filterWithoutAss
11471153
}
11481154
}
11491155
} elseif (isset($startEvent['assignment']) && $startEvent['assignment'] === 'process_manager') {
1150-
$access = $this->manager && $this->manager->id && $this->manager->id === $user->id;
1156+
$access = in_array($user->id, $this->manager_id ?? []);
11511157
} else {
11521158
$access = false;
11531159
}

ProcessMaker/Models/ProcessRequest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,7 @@ public function getNotifiableUserIds($notifiableType)
280280
case 'participants':
281281
return $this->participants()->get()->pluck('id');
282282
case 'manager':
283-
return collect([$this->process()->first()->manager_id]);
283+
return collect($this->process()->first()->manager_id ?? []);
284284
default:
285285
return collect([]);
286286
}

ProcessMaker/Models/ProcessRequestToken.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@ public function getNotifiableUserIds($notifiableType)
257257
case 'manager':
258258
$process = $this->process()->first();
259259

260-
return collect([$process?->manager_id]);
260+
return collect($process?->manager_id ?? []);
261261
break;
262262
default:
263263
return collect([]);

0 commit comments

Comments
 (0)