diff --git a/ProcessMaker/Http/Controllers/Api/TaskController.php b/ProcessMaker/Http/Controllers/Api/TaskController.php index 5c2613e293..2c2647cc40 100644 --- a/ProcessMaker/Http/Controllers/Api/TaskController.php +++ b/ProcessMaker/Http/Controllers/Api/TaskController.php @@ -341,7 +341,8 @@ public function update(Request $request, ProcessRequestToken $task) return new Resource($task->refresh()); } elseif (!empty($request->input('user_id'))) { $userToAssign = $request->input('user_id'); - $task->reassign($userToAssign, $request->user()); + $comments = $request->input('comments'); + $task->reassign($userToAssign, $request->user(), $comments); $taskRefreshed = $task->refresh(); @@ -426,7 +427,7 @@ public function setPriority(Request $request, ProcessRequestToken $task) } /** - * Only send data for a screen’s fields + * Only send data for a screen's fields * * @param ProcessRequestToken $task * diff --git a/ProcessMaker/Http/Controllers/Api/UserController.php b/ProcessMaker/Http/Controllers/Api/UserController.php index 070483b348..0ffb33450f 100644 --- a/ProcessMaker/Http/Controllers/Api/UserController.php +++ b/ProcessMaker/Http/Controllers/Api/UserController.php @@ -186,8 +186,7 @@ public function index(Request $request) */ public function getUsersTaskCount(Request $request) { - $query = User::nonSystem(); - $query->select('id', 'username', 'firstname', 'lastname'); + $query = User::select('id', 'username', 'firstname', 'lastname'); $filter = $request->input('filter', ''); if (!empty($filter)) { @@ -199,23 +198,18 @@ public function getUsersTaskCount(Request $request) }); } - $query->where('status', 'ACTIVE'); - - $query->withCount('activeTasks'); - $include_ids = []; $include_ids_string = $request->input('include_ids', ''); if (!empty($include_ids_string)) { $include_ids = explode(',', $include_ids_string); } elseif ($request->has('assignable_for_task_id')) { - $task = ProcessRequestToken::findOrFail($request->input('assignable_for_task_id')); - $assignmentRule = $task->getAssignmentRule(); - if ($assignmentRule === 'user_group') { - // Limit the list of users to those that can be assigned to the task - $include_ids = $task->process->getAssignableUsers($task->element_id); + $processRequestToken = ProcessRequestToken::findOrFail($request->input('assignable_for_task_id')); + $assignmentRule = $processRequestToken->getAssignmentRule(); + if (config('app.reassign_restrict_to_assignable_users')) { + $include_ids = $processRequestToken->process->getAssignableUsersByAssignmentType($processRequestToken); } if ($assignmentRule === 'rule_expression' && $request->has('form_data')) { - $include_ids = $task->getAssigneesFromExpression($request->input('form_data')); + $include_ids = $processRequestToken->getAssigneesFromExpression($request->input('form_data')); } } @@ -223,10 +217,13 @@ public function getUsersTaskCount(Request $request) $query->whereIn('id', $include_ids); } - $response = $query->orderBy( - $request->input('order_by', 'username'), - $request->input('order_direction', 'ASC') - ) + $response = $query + ->where('is_system', false) + ->where('status', 'ACTIVE') + ->withCount('activeTasks') + ->orderBy( + $request->input('order_by', 'username'), + $request->input('order_direction', 'ASC')) ->paginate(50); return new ApiCollection($response); diff --git a/ProcessMaker/Models/Process.php b/ProcessMaker/Models/Process.php index e7adbba6d8..b3598e1de8 100644 --- a/ProcessMaker/Models/Process.php +++ b/ProcessMaker/Models/Process.php @@ -1044,6 +1044,40 @@ public function getAssignableUsers($processTaskUuid) return array_values($users); } + /** + * This method is used to get the assignable users for a task based on the assignment rule. + * The assignment rule can be: + * - user_group: would assign it to those in the group or the Process Manager. + * - process_variable: would assign it to those in the group or the Process Manager. + * - rule_expression: would assign it to those in the group or the Process Manager. + * - previous_task_assignee: would assign it to the Process Manager. + * - requester: would assign it to the Process Manager. + * - process_manager: would assign it to the same Process Manager. + * + * @param ProcessRequestToken $processRequestToken + * @return array + */ + public function getAssignableUsersByAssignmentType(ProcessRequestToken $processRequestToken): array + { + $users = []; + switch ($processRequestToken->getAssignmentRule()) { + case 'user_group': + case 'process_variable': + case 'rule_expression': + $users = $this->getAssignableUsers($processRequestToken->element_id); + $users[] = $processRequestToken->process->properties["manager_id"]; + break; + case 'previous_task_assignee': + case 'requester': + $users[] = $processRequestToken->process->properties["manager_id"]; + break; + case 'process_manager': + $users[] = $processRequestToken->process->properties["manager_id"]; + break; + } + return $users; + } + /** * Get a consolidated list of users within groups. * diff --git a/ProcessMaker/Models/ProcessRequestToken.php b/ProcessMaker/Models/ProcessRequestToken.php index 87621f1f0a..97111108ce 100644 --- a/ProcessMaker/Models/ProcessRequestToken.php +++ b/ProcessMaker/Models/ProcessRequestToken.php @@ -1340,7 +1340,7 @@ public function sendActivityActivatedNotifications() * @param User $requestingUser * @return void */ - public function reassign($toUserId, User $requestingUser) + public function reassign($toUserId, User $requestingUser, $comments = '') { $sendActivityActivatedNotifications = false; $reassingAction = false; @@ -1365,6 +1365,9 @@ public function reassign($toUserId, User $requestingUser) $this->persistUserData($toUserId); $reassingAction = true; } + if ($comments != null && $comments !== '') { + $this->comments = $comments; + } $this->save(); if ($sendActivityActivatedNotifications) { diff --git a/config/app.php b/config/app.php index 4106d36af8..6745518567 100644 --- a/config/app.php +++ b/config/app.php @@ -300,5 +300,6 @@ 'multitenancy' => env('MULTITENANCY', false), + 'reassign_restrict_to_assignable_users' => env('REASSIGN_RESTRICT_TO_ASSIGNABLE_USERS', true), 'resources_core_path' => base_path('resources-core'), ]; diff --git a/database/migrations/2025_04_08_115507_add_the_comments_field_to_the_process_request_token_table.php b/database/migrations/2025_04_08_115507_add_the_comments_field_to_the_process_request_token_table.php new file mode 100644 index 0000000000..69811599b1 --- /dev/null +++ b/database/migrations/2025_04_08_115507_add_the_comments_field_to_the_process_request_token_table.php @@ -0,0 +1,28 @@ +longText('comments')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('process_request_tokens', function (Blueprint $table) { + $table->dropColumn('comments'); + }); + } +}; diff --git a/resources/js/tasks/api/index.js b/resources/js/tasks/api/index.js index a84ff22a5e..0b55e7e35f 100644 --- a/resources/js/tasks/api/index.js +++ b/resources/js/tasks/api/index.js @@ -1,11 +1,44 @@ -import { api } from "../variables/index"; +import { getApi } from "../variables/index"; -export const updateCollection = async ({ collectionId, recordId, data }) => { - const response = await api.put(`collections/${collectionId}/records/${recordId}`, data); +export const getReassignUsers = async (filter = null, taskId = null, currentTaskUserId = null) => { + const api = getApi(); + const response = await api.get("users_task_count", { params: { filter, assignable_for_task_id: taskId, include_current_user: true } }); + const data = response.data; + if (currentTaskUserId && Array.isArray(data?.data)) { + data.data = data.data.filter((user) => user.id !== currentTaskUserId); + } + return data; +}; + +export const updateReassignUser = async (taskId, userId, comments = null) => { + const api = getApi(); + const response = await api.put(`tasks/${taskId}`, { user_id: userId, comments }); + return response.data; +}; +export const updateComment = async ({ + body, + subject, + commentableId, + commentableType, + parentId = 0, + type = "COMMENT", +}) => { + const api = getApi(); + const response = await api.post("comments/comments", { + body, + subject, + commentable_id: commentableId, + commentable_type: commentableType, + type, + parent_id: parentId, + }); return response.data; }; -export default { - updateCollection, +export const updateCollection = async ({ collectionId, recordId, data }) => { + const api = getApi(); + const response = await api.put(`collections/${collectionId}/records/${recordId}`, data); + + return response.data; }; diff --git a/resources/js/tasks/components/PreviewMixin.js b/resources/js/tasks/components/PreviewMixin.js index c7a5886161..d004bc69d2 100644 --- a/resources/js/tasks/components/PreviewMixin.js +++ b/resources/js/tasks/components/PreviewMixin.js @@ -172,6 +172,7 @@ const PreviewMixin = { } this.prevTask = prevTask; this.nextTask = nextTask; + this.showReassignment = false; }, /** * Expand Open task diff --git a/resources/js/tasks/components/TasksPreview.vue b/resources/js/tasks/components/TasksPreview.vue index fd45f5817a..e9b27459df 100644 --- a/resources/js/tasks/components/TasksPreview.vue +++ b/resources/js/tasks/components/TasksPreview.vue @@ -1,41 +1,51 @@ - + - + - + class="iframe-container" + > - - + + - - - - + + + - + @@ -166,10 +187,10 @@ @@ -177,41 +198,24 @@ - reassignUser(e,false)" + /> + - - Assign to: - - - - - {{ option.active_tasks_count }} - - - - - {{ $t('Assign') }} - - - {{ $t('Cancel') }} - - - + + + + - - - - - + @@ -260,15 +267,22 @@ import TaskSaveNotification from "./TaskSaveNotification.vue"; import EllipsisMenu from "../../components/shared/EllipsisMenu.vue"; import QuickFillPreview from "./QuickFillPreview.vue"; import PreviewMixin from "./PreviewMixin"; -import autosaveMixins from "../../modules/autosave/autosaveMixin.js" -import PMDropdownSuggest from "../../components/PMDropdownSuggest.vue"; +import autosaveMixins from "../../modules/autosave/autosaveMixin.js"; import reassignMixin from "../../common/reassignMixin"; +import TaskPreviewAssignment from "./taskPreview/TaskPreviewAssignment.vue"; export default { - components: { SplitpaneContainer, TaskLoading, QuickFillPreview, TaskSaveNotification, EllipsisMenu, PMDropdownSuggest }, + components: { + SplitpaneContainer, + TaskLoading, + QuickFillPreview, + TaskSaveNotification, + EllipsisMenu, + TaskPreviewAssignment, + }, mixins: [PreviewMixin, autosaveMixins, reassignMixin], props: ["tooltipButton", "propPreview"], - data(){ + data() { return { }; }, @@ -282,9 +296,9 @@ export default { this.lastAutosave = "-"; } this.isPriority = task.is_priority; - const priorityAction = this.actions.find(action => action.value === 'mark-priority'); + const priorityAction = this.actions.find((action) => action.value === "mark-priority"); if (priorityAction) { - priorityAction.content = this.isPriority ? 'Unmark Priority' : 'Mark as Priority'; + priorityAction.content = this.isPriority ? "Unmark Priority" : "Mark as Priority"; } if (this.task.id) { this.getTaskDefinitionForReassignmentPermission(); @@ -293,19 +307,19 @@ export default { if (task?.id !== previousTask?.id) { this.userHasInteracted = false; this.setAllowReassignment(); - } + } }, }, showPreview(value) { this.$emit("onWatchShowPreview", value); - } + }, }, mounted() { - if(this.propPreview){ + if (this.propPreview) { this.showPreview = true; } - - this.receiveEvent('taskReady', (taskId) => { + + this.receiveEvent("taskReady", (taskId) => { this.taskReady = true; }); @@ -317,42 +331,36 @@ export default { } }); - this.receiveEvent('userHasInteracted', () => { + this.receiveEvent("userHasInteracted", () => { this.userHasInteracted = true; }); - this.$root.$on('pane-size', (value) => { + this.$root.$on("pane-size", (value) => { this.size = value; }); this.screenWidthPx = window.innerWidth; - window.addEventListener('resize', this.updateScreenWidthPx); + window.addEventListener("resize", this.updateScreenWidthPx); this.getUser(); this.setAllowReassignment(); }, - computed: { - disabled() { - return this.selectedUser ? false : true; - }, - }, methods: { fillWithQuickFillData(data) { - const message = this.$t('Task Filled succesfully'); + const message = this.$t("Task Filled succesfully"); this.sendEvent("fillData", data); this.showUseThisTask = false; - ProcessMaker.alert(message, 'success'); + ProcessMaker.alert(message, "success"); this.handleAutosave(); this.disableNavigation = false; }, - sendEvent(name, data) - { + sendEvent(name, data) { const event = new CustomEvent(name, { - detail: data + detail: data, }); - if(this.showFrame1) { - this.$refs["tasksFrame1"].contentWindow.dispatchEvent(event); + if (this.showFrame1) { + this.$refs.tasksFrame1.contentWindow.dispatchEvent(event); } - if(this.showFrame2) { - this.$refs["tasksFrame2"].contentWindow.dispatchEvent(event); + if (this.showFrame2) { + this.$refs.tasksFrame2.contentWindow.dispatchEvent(event); } }, receiveEvent(name, callback) { @@ -370,12 +378,12 @@ export default { this.options.is_loading = true; const draftData = _.omitBy(this.formData, (value, key) => key.startsWith("_")); return ProcessMaker.apiClient - .put("drafts/" + this.task.id, draftData) + .put(`drafts/${this.task.id}`, draftData) .then((response) => { this.task.draft = _.merge( {}, this.task.draft, - response.data + response.data, ); }) .catch(() => { @@ -389,8 +397,8 @@ export default { }, eraseDraft() { ProcessMaker.apiClient - .delete("drafts/" + this.task.id) - .then(response => { + .delete(`drafts/${this.task.id}`) + .then((response) => { // No need to run resetRequestFiles here // because the iframe gets reloaded after // the draft is cleared @@ -403,10 +411,6 @@ export default { this.userHasInteracted = false; }); }, - cancelReassign() { - this.showReassignment = false; - this.selectedUser = null; - }, openReassignment() { this.showReassignment = !this.showReassignment; this.getReassignUsers(); @@ -425,7 +429,17 @@ export default { this.user = response.data; }); }, - } + reassignUser(selectedUser, redirect = false) { + this.$emit("on-reassign-user", selectedUser); + this.showReassignment = false; + if (redirect) { + this.redirect("/tasks"); + } + if (this.showPreview) { + this.showPreview = false; + } + }, + }, }; @@ -515,7 +529,6 @@ export default { color: #C56363; } - .iframe-container { display: flex; flex-direction: column; diff --git a/resources/js/tasks/components/taskPreview/TaskPreviewAssignment.vue b/resources/js/tasks/components/taskPreview/TaskPreviewAssignment.vue new file mode 100644 index 0000000000..52f066c087 --- /dev/null +++ b/resources/js/tasks/components/taskPreview/TaskPreviewAssignment.vue @@ -0,0 +1,160 @@ + + + + {{ $t('Assign to') }}: + + + + {{ option.active_tasks_count }} + + + + + + + {{ $t('Comments') }} + + + + + + {{ $t('Cancel') }} + + + {{ $t('Assign') }} + + + + + + diff --git a/resources/js/tasks/edit.js b/resources/js/tasks/edit.js index eb2133702c..9b61442a98 100644 --- a/resources/js/tasks/edit.js +++ b/resources/js/tasks/edit.js @@ -15,12 +15,12 @@ window.__ = translator; const main = new Vue({ el: "#task", - mixins: addons, components: { TaskSaveNotification, TaskSavePanel, TasksList, }, + mixins: addons, data: { // Edit data fieldsToUpdate: [], @@ -260,7 +260,7 @@ const main = new Vue({ show() { this.selectedUser = null; this.showReassignment = true; - this.getReassignUsers(); + this.getReassignUsers(); // TODO: improve this code }, showQuickFill() { this.redirect(`/tasks/${this.task.id}/edit/quickfill`); diff --git a/resources/js/tasks/variables/index.js b/resources/js/tasks/variables/index.js index 6d650d79b6..212db0cb6e 100644 --- a/resources/js/tasks/variables/index.js +++ b/resources/js/tasks/variables/index.js @@ -1,6 +1,6 @@ export default {}; -export const api = window.ProcessMaker?.apiClient; +export const getApi = () => window.ProcessMaker?.apiClient; export const i18n = window.ProcessMaker?.i18n; diff --git a/tests/Feature/Api/TaskControllerTest.php b/tests/Feature/Api/TaskControllerTest.php index cacba824d3..a545789a58 100644 --- a/tests/Feature/Api/TaskControllerTest.php +++ b/tests/Feature/Api/TaskControllerTest.php @@ -248,4 +248,18 @@ public function testTasksByCaseReturnsCorrectData() $this->assertEquals($data2['form_input_1'], $responseData[1]['taskData']['form_input_1']); $this->assertEquals($data2['form_text_area_1'], $responseData[1]['taskData']['form_text_area_1']); } + + public function testShowTaskIncludesNewProperty() + { + $user = User::factory()->create(); + $this->actingAs($user); + + $comments = "This is a comment"; + + $task = ProcessRequestToken::factory()->create([ + "comments" => $comments + ]); + + $this->assertEquals($comments, $task->comments); + } } diff --git a/tests/Feature/Api/UsersTest.php b/tests/Feature/Api/UsersTest.php index 1afd472bae..42fe239741 100644 --- a/tests/Feature/Api/UsersTest.php +++ b/tests/Feature/Api/UsersTest.php @@ -883,6 +883,7 @@ public function testGetUsersTaskCount() $process = Process::factory()->create([ 'user_id' => $admin->id, + 'manager_id' => $admin->id, ]); $bpmn = file_get_contents(__DIR__ . '/../../Fixtures/task_with_user_group_assignment.bpmn');