From 37d01e049636eb6804590664800542f531b99258 Mon Sep 17 00:00:00 2001 From: "Marco A. Nina Mena" Date: Thu, 14 May 2026 18:14:38 -0400 Subject: [PATCH 1/3] Add self-service status handling and refactor status filter logic in TaskControllerIndexMethods --- .../Traits/TaskControllerIndexMethods.php | 97 ++++++++++++++----- 1 file changed, 72 insertions(+), 25 deletions(-) diff --git a/ProcessMaker/Traits/TaskControllerIndexMethods.php b/ProcessMaker/Traits/TaskControllerIndexMethods.php index d773ffb241..b23a7a612a 100644 --- a/ProcessMaker/Traits/TaskControllerIndexMethods.php +++ b/ProcessMaker/Traits/TaskControllerIndexMethods.php @@ -19,6 +19,8 @@ trait TaskControllerIndexMethods { + private const SELF_SERVICE_STATUS = 'self service'; + private function indexBaseQuery($request) { // Parse the includes parameter @@ -291,6 +293,19 @@ private function applyStatusFilter($query, $request) } } + private function requestHasStatusFilter($request): bool + { + if ($request->filled('status') || $request->filled('statusfilter')) { + return true; + } + + if ($this->advancedFilterHasStatus($request)) { + return true; + } + + return preg_match('/(?:^|[\s(])status\s*(?:=|!=|<>)/i', $request->input('pmql', '')) === 1; + } + private function applyPmql($query, $request, $user) { $pmql = $request->input('pmql', ''); @@ -327,7 +342,7 @@ private function advancedFilterHasSelfServiceStatus($request): bool foreach ($this->getAdvancedFilterArray($request) as $filter) { $values = (array) ($filter['value'] ?? []); foreach ($values as $v) { - if (mb_strtolower($v) === 'self service') { + if (mb_strtolower($v) === self::SELF_SERVICE_STATUS) { return true; } } @@ -384,34 +399,62 @@ private function applyAdvancedFilter($query, $request) // If processesIManage is active, handle "Self Service" status filter specially if ($isProcessManager && is_array($filterArray)) { $hasSelfServiceFilter = false; - $filteredArray = []; + $nonStatusFilters = []; + $statusFilters = []; foreach ($filterArray as $filter) { - // Check if this is a "Self Service" status filter - if (isset($filter['subject']['type']) && - $filter['subject']['type'] === 'Status' && - isset($filter['value']) && - mb_strtolower($filter['value']) === 'self service') { - $hasSelfServiceFilter = true; - // Don't add this filter to the array - we'll handle it manually + $isStatusFilter = isset($filter['subject']['type']) && $filter['subject']['type'] === 'Status'; + if (!$isStatusFilter) { + $nonStatusFilters[] = $filter; continue; } - $filteredArray[] = $filter; + + $values = is_array($filter['value']) ? $filter['value'] : [$filter['value']]; + $hasSelfServiceValue = in_array( + self::SELF_SERVICE_STATUS, + array_map(fn ($value) => is_string($value) ? mb_strtolower($value) : $value, $values), + true + ); + + if ($hasSelfServiceValue) { + $hasSelfServiceFilter = true; + $values = array_values(array_filter($values, function ($value) { + return !is_string($value) || mb_strtolower($value) !== self::SELF_SERVICE_STATUS; + })); + + if (empty($values)) { + continue; + } + + $filter['value'] = is_array($filter['value']) ? $values : $values[0]; + } + + $statusFilters[] = $filter; } - // Apply the filtered advanced_filter (without Self Service) - if (!empty($filteredArray)) { - Filter::filter($query, $filteredArray); + if (!$hasSelfServiceFilter) { + Filter::filter($query, $filterArray); + + return; } - // Manually apply the Self Service filter for process managers - if ($hasSelfServiceFilter) { - $selfServiceTaskIds = ProcessRequestToken::select(['id']) - ->whereIn('process_id', $processManagerIds) - ->where('is_self_service', 1) - ->whereNull('user_id') - ->where('status', 'ACTIVE'); + if (!empty($nonStatusFilters)) { + Filter::filter($query, $nonStatusFilters); + } + // Manually apply the Self Service filter for process managers + $selfServiceTaskIds = ProcessRequestToken::select(['id']) + ->whereIn('process_id', $processManagerIds) + ->where('is_self_service', 1) + ->whereNull('user_id') + ->where('status', 'ACTIVE'); + + if (!empty($statusFilters)) { + $query->where(function ($query) use ($statusFilters, $selfServiceTaskIds) { + Filter::filter($query, $statusFilters); + $query->orWhereIn('process_request_tokens.id', $selfServiceTaskIds); + }); + } else { $query->whereIn('process_request_tokens.id', $selfServiceTaskIds); } } else { @@ -453,7 +496,7 @@ private function applyForCurrentUser($query, $user) }); } - public function applyProcessManager($query, $user) + public function applyProcessManager($query, $user, $request) { $ids = Process::select(['id']) ->where(function ($subQuery) use ($user) { @@ -472,7 +515,8 @@ public function applyProcessManager($query, $user) return; } - // Show tasks from processes the user manages that are ACTIVE + // Show tasks from processes the user manages. Default to ACTIVE unless the request + // already has an explicit status filter that will be applied later. // OR show self-service tasks from those processes // Store the process IDs in the query so we can use them later to add self-service tasks // We'll add self-service tasks after PMQL is applied to avoid the is_self_service = 0 filter @@ -480,9 +524,12 @@ public function applyProcessManager($query, $user) // Apply condition for regular tasks from managed processes // Self-service tasks will be added after PMQL to avoid conflicts - $query->where(function ($query) use ($ids) { - $query->whereIn('process_request_tokens.process_id', $ids) - ->where('process_request_tokens.status', 'ACTIVE'); + $query->where(function ($query) use ($ids, $request) { + $query->whereIn('process_request_tokens.process_id', $ids); + + if (!$this->requestHasStatusFilter($request)) { + $query->where('process_request_tokens.status', 'ACTIVE'); + } }); } From 8ddeda55ced70c797c998df88a716082fb2ae6d0 Mon Sep 17 00:00:00 2001 From: "Marco A. Nina Mena" Date: Thu, 14 May 2026 18:16:04 -0400 Subject: [PATCH 2/3] Update applyProcessManager method in TaskController to include request parameter for improved filtering --- ProcessMaker/Http/Controllers/Api/TaskController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ProcessMaker/Http/Controllers/Api/TaskController.php b/ProcessMaker/Http/Controllers/Api/TaskController.php index af2948abf8..0679b42853 100644 --- a/ProcessMaker/Http/Controllers/Api/TaskController.php +++ b/ProcessMaker/Http/Controllers/Api/TaskController.php @@ -156,7 +156,7 @@ public function index(Request $request, $getTotal = false, User $user = null) // Apply process manager filter BEFORE PMQL to avoid conflicts with is_self_service filtering if ($request->input('processesIManage') === 'true') { - $this->applyProcessManager($query, $user); + $this->applyProcessManager($query, $user, $request); } else { $this->applyForCurrentUser($query, $user); } From 1621d6f82846bafd6a953b0680f0087db398b6b2 Mon Sep 17 00:00:00 2001 From: "Marco A. Nina Mena" Date: Thu, 14 May 2026 19:00:57 -0400 Subject: [PATCH 3/3] Add tests for advanced status filtering in Tasks API --- tests/Feature/Api/TasksTest.php | 103 ++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) diff --git a/tests/Feature/Api/TasksTest.php b/tests/Feature/Api/TasksTest.php index 2cc9abbcb8..212fc34d19 100644 --- a/tests/Feature/Api/TasksTest.php +++ b/tests/Feature/Api/TasksTest.php @@ -870,6 +870,109 @@ public function testAdvancedStatusFilterOverridesPmqlStatus() $this->assertNotContains($activeTask->id, $returnedIds); } + public function testProcessManagerAdvancedStatusFilterCanReturnCompletedTasks() + { + $manager = User::factory()->create(); + $process = Process::factory()->create([ + 'properties' => ['manager_id' => $manager->id], + ]); + $unmanagedProcess = Process::factory()->create(); + $request = ProcessRequest::factory()->create(['process_id' => $process->id]); + + $activeTask = ProcessRequestToken::factory()->create([ + 'status' => 'ACTIVE', + 'element_type' => 'task', + 'process_id' => $process->id, + 'process_request_id' => $request->id, + 'is_self_service' => 0, + ]); + + $completedTask = ProcessRequestToken::factory()->create([ + 'status' => 'CLOSED', + 'element_type' => 'task', + 'process_id' => $process->id, + 'process_request_id' => $request->id, + 'is_self_service' => 0, + ]); + + $unmanagedCompletedTask = ProcessRequestToken::factory()->create([ + 'status' => 'CLOSED', + 'element_type' => 'task', + 'process_id' => $unmanagedProcess->id, + 'is_self_service' => 0, + ]); + + $statusFilter = json_encode([ + [ + 'subject' => ['type' => 'Status'], + 'operator' => '=', + 'value' => 'Completed', + ], + ]); + + $response = $this->actingAs($manager, 'api')->get(route('api.tasks.index', [ + 'processesIManage' => 'true', + 'advanced_filter' => $statusFilter, + ])); + + $response->assertStatus(200); + $returnedIds = collect($response->json('data'))->pluck('id')->toArray(); + + $this->assertContains($completedTask->id, $returnedIds); + $this->assertNotContains($activeTask->id, $returnedIds); + $this->assertNotContains($unmanagedCompletedTask->id, $returnedIds); + } + + public function testProcessManagerAdvancedStatusFilterCanReturnInProgressCompletedAndSelfServiceTasks() + { + $manager = User::factory()->create(); + $process = Process::factory()->create([ + 'properties' => ['manager_id' => $manager->id], + ]); + + $activeTask = ProcessRequestToken::factory()->create([ + 'status' => 'ACTIVE', + 'element_type' => 'task', + 'process_id' => $process->id, + 'is_self_service' => 0, + ]); + + $completedTask = ProcessRequestToken::factory()->create([ + 'status' => 'CLOSED', + 'element_type' => 'task', + 'process_id' => $process->id, + 'is_self_service' => 0, + ]); + + $selfServiceTask = ProcessRequestToken::factory()->create([ + 'status' => 'ACTIVE', + 'element_type' => 'task', + 'process_id' => $process->id, + 'user_id' => null, + 'is_self_service' => 1, + ]); + + $statusFilter = json_encode([ + [ + 'subject' => ['type' => 'Status'], + 'operator' => '=', + 'value' => ['In Progress', 'Completed', 'Self Service'], + ], + ]); + + $response = $this->actingAs($manager, 'api')->get(route('api.tasks.index', [ + 'processesIManage' => 'true', + 'advanced_filter' => $statusFilter, + ])); + + $response->assertStatus(200); + $returnedIds = collect($response->json('data'))->pluck('id')->toArray(); + + $this->assertContains($activeTask->id, $returnedIds); + $this->assertContains($completedTask->id, $returnedIds); + $this->assertContains($selfServiceTask->id, $returnedIds); + } + public function testPmqlStatusPreservedWhenNoAdvancedStatusFilter() { $user = User::factory()->create(['is_administrator' => true]);