| @@ -3,8 +3,9 @@ | ||
| 3 | 3 | namespace FluentBoards\App\Hooks\Handlers; |
| 4 | 4 | |
| 5 | 5 | use FluentBoards\App\Models\Board; |
| 6 | 6 | use FluentBoards\App\Models\Stage; |
| 7 | +use FluentBoards\App\Models\Task; | |
| 7 | 8 | use FluentBoards\App\Models\User; |
| 8 | 9 | use FluentBoards\App\Services\Constant; |
| 9 | 10 | use FluentCrm\App\Models\Subscriber; |
| 10 | 11 | use FluentBoards\App\Models\Comment; |
| @@ -23,8 +24,13 @@ | ||
| 23 | 24 | 'new_value' => $newValue, |
| 24 | 25 | 'description' => $description, |
| 25 | 26 | 'settings' => $settings |
| 26 | 27 | ]; |
| 28 | + $userId = get_current_user_id(); | |
| 29 | + if($userId == 0){ | |
| 30 | + $task = Task::find($taskId); | |
| 31 | + $data['created_by'] = $task->created_by; | |
| 32 | + } | |
| 27 | 33 | |
| 28 | 34 | Helper::createActivity($data); |
| 29 | 35 | } |
| 30 | 36 | public function logMoveTaskToAnotherBoardActivity($task, $oldTask) |
| @@ -33,16 +39,40 @@ | ||
| 33 | 39 | $new = $this->getBoardTitleById($task->board_id); |
| 34 | 40 | $this->createLogActivity( $task->id,'changed', 'board', $old, $new); |
| 35 | 41 | } |
| 36 | 42 | |
| 37 | - public function logAssigneeChangeActivity($task, $newAssigneeId, $operation) | |
| 43 | + public function logAssigneeAddedActivity($task, $newAssigneeId) | |
| 38 | 44 | { |
| 45 | + $currentUserId = get_current_user_id(); | |
| 46 | + if($newAssigneeId == $currentUserId) { | |
| 47 | + $this->taskAssigneeJoin($task->id); | |
| 48 | + return; | |
| 49 | + } | |
| 39 | 50 | $user = User::findOrFail($newAssigneeId); |
| 40 | - if($user) { | |
| 41 | - $this->createLogActivity($task->id, $operation, 'assignee', null, $user->display_name); | |
| 51 | + $currentUserId = get_current_user_id(); | |
| 52 | + if($currentUserId === $user->ID){ | |
| 53 | + $this->taskAssigneeJoin($task->id); | |
| 54 | + } elseif ($user) { | |
| 55 | + $this->createLogActivity($task->id, 'added', 'assignee', null, $user->display_name); | |
| 42 | 56 | } |
| 43 | 57 | } |
| 44 | 58 | |
| 59 | + public function logAssigneeRemovedActivity($task, $newAssigneeId) | |
| 60 | + { | |
| 61 | + $currentUserId = get_current_user_id(); | |
| 62 | + if($newAssigneeId == $currentUserId) { | |
| 63 | + $this->taskAssigneeLeave($task->id); | |
| 64 | + return; | |
| 65 | + } | |
| 66 | + $user = User::findOrFail($newAssigneeId); | |
| 67 | + $currentUserId = get_current_user_id(); | |
| 68 | + if($currentUserId === $user->ID){ | |
| 69 | + $this->taskAssigneeLeave($task->id); | |
| 70 | + } else if($user) { | |
| 71 | + $this->createLogActivity($task->id, 'removed', 'assignee', null, $user->display_name); | |
| 72 | + } | |
| 73 | + } | |
| 74 | + | |
| 45 | 75 | public function logTaskCreationActivity($task) |
| 46 | 76 | { |
| 47 | 77 | $this->createLogActivity( $task->id,'created', 'task', null, $task->title, null ); |
| 48 | 78 | } |
| @@ -48,17 +78,18 @@ | ||
| 48 | 78 | } |
| 49 | 79 | |
| 50 | 80 | public function logTaskContentUpdatedActivity($task, $col, $oldTask = null) |
| 51 | 81 | { |
| 82 | + // Description autosaves are intentionally excluded to avoid flooding the activity feed. | |
| 83 | + if ($col === 'description') { | |
| 84 | + return; | |
| 85 | + } | |
| 86 | + | |
| 52 | 87 | $action = 'updated'; |
| 53 | - if($col == 'description'){ | |
| 54 | - $this->createLogActivity( $task->id, $action, $col, $oldTask->description, $task->description ); | |
| 88 | + if($task->parent_id){ | |
| 89 | + $this->createLogActivity( $task->parent_id, $action, 'subtask', $oldTask->title, $task->title ); | |
| 55 | 90 | }else{ |
| 56 | - if($task->parent_id){ | |
| 57 | - $this->createLogActivity( $task->parent_id, $action, 'subtask', $oldTask->title, $task->title ); | |
| 58 | - }else{ | |
| 59 | - $this->createLogActivity( $task->id, $action, $col, $oldTask->title, $task->title ); | |
| 60 | - } | |
| 91 | + $this->createLogActivity( $task->id, $action, $col, $oldTask->title, $task->title ); | |
| 61 | 92 | } |
| 62 | 93 | } |
| 63 | 94 | |
| 64 | 95 | public function taskLabelActivity($task, $label, $action) |
| @@ -72,61 +103,71 @@ | ||
| 72 | 103 | |
| 73 | 104 | $this->createLogActivity($task->id, $action, $column, null, null, null, $settings); |
| 74 | 105 | } |
| 75 | 106 | |
| 76 | - public function logDueDateActivity($task, $oldDate, $dateType) | |
| 107 | + public function logDueDateActivity($task, $oldDate) | |
| 77 | 108 | { |
| 78 | - $column = ''; | |
| 109 | + $column = 'Due Date'; | |
| 79 | 110 | $action = 'changed'; |
| 80 | - if($dateType == 'Due Date'){ | |
| 81 | - if($oldDate){ | |
| 82 | - $new = gmdate("F j, Y, g:i a", strtotime($task->due_at)); | |
| 83 | - $old = gmdate("F j, Y, g:i a", strtotime($oldDate)); | |
| 84 | - if($new == $old) | |
| 85 | - return; | |
| 111 | + | |
| 112 | + if($oldDate){ | |
| 113 | + $new = gmdate("F j, Y, g:i a", strtotime($task->due_at)); | |
| 114 | + $old = gmdate("F j, Y, g:i a", strtotime($oldDate)); | |
| 115 | + if($new == $old) | |
| 116 | + return; | |
| 117 | + } | |
| 118 | + $newDueDate = $task->due_at; | |
| 119 | + $new = null; | |
| 120 | + if ($newDueDate) { | |
| 121 | + $new = gmdate("F j, Y, g:i a", strtotime($newDueDate)); | |
| 122 | + if(str_contains($new, '12:00 am')){ | |
| 123 | + $new = gmdate("F j, Y", strtotime($newDueDate)); | |
| 86 | 124 | } |
| 87 | - $column = 'Due Date'; | |
| 88 | - $newDueDate = $task->due_at; | |
| 89 | - $new = null; | |
| 90 | - if ($newDueDate) { | |
| 91 | - $new = gmdate("F j, Y, g:i a", strtotime($newDueDate)); | |
| 92 | - if(str_contains($new, '12:00 am')){ | |
| 93 | - $new = gmdate("F j, Y", strtotime($newDueDate)); | |
| 94 | - } | |
| 95 | - } | |
| 96 | - $old = null; | |
| 97 | - if(!$oldDate){ | |
| 98 | - $action = 'added'; | |
| 99 | - }else{ | |
| 100 | - $old = gmdate("F j, Y, g:i a", strtotime($oldDate)); | |
| 101 | - if(str_contains($old, '12:00 am')){ | |
| 102 | - $old = gmdate("F j, Y", strtotime($oldDate)); | |
| 103 | - } | |
| 104 | - } | |
| 105 | - }else { | |
| 106 | - if($oldDate){ | |
| 107 | - $new = gmdate("F j, Y", strtotime($task->started_at)); | |
| 125 | + } | |
| 126 | + $old = null; | |
| 127 | + if(!$oldDate){ | |
| 128 | + $action = 'added'; | |
| 129 | + }else{ | |
| 130 | + $old = gmdate("F j, Y, g:i a", strtotime($oldDate)); | |
| 131 | + if(str_contains($old, '12:00 am')){ | |
| 108 | 132 | $old = gmdate("F j, Y", strtotime($oldDate)); |
| 109 | - if($new == $old) | |
| 110 | - return; | |
| 111 | 133 | } |
| 112 | - $column = 'Start Date'; | |
| 113 | - $newStartDate = $task->started_at; | |
| 114 | - $new = null; | |
| 115 | - if ($newStartDate) { | |
| 116 | - $new = gmdate("F j, Y", strtotime($newStartDate)); | |
| 117 | - } | |
| 118 | - $old = null; | |
| 119 | - if(!$oldDate){ | |
| 120 | - $action = 'added'; | |
| 121 | - }else{ | |
| 122 | - $old = gmdate("F j, Y", strtotime($oldDate)); | |
| 123 | - } | |
| 124 | 134 | } |
| 125 | 135 | |
| 126 | 136 | $this->createLogActivity($task->id, $action, $column, $old, $new); |
| 127 | 137 | } |
| 138 | + public function logDueDateRemoveActivity($task) | |
| 139 | + { | |
| 140 | + $this->createLogActivity( $task->id, 'removed', 'Due Date', null); | |
| 141 | + } | |
| 128 | 142 | |
| 143 | + public function logStartDateActivity($task, $oldDate) | |
| 144 | + { | |
| 145 | + $column = 'Start Date'; | |
| 146 | + $action = 'changed'; | |
| 147 | + | |
| 148 | + if($oldDate){ | |
| 149 | + $new = gmdate("F j, Y", strtotime($task->started_at)); | |
| 150 | + $old = gmdate("F j, Y", strtotime($oldDate)); | |
| 151 | + if($new == $old) | |
| 152 | + return; | |
| 153 | + } | |
| 154 | + $newStartDate = $task->started_at; | |
| 155 | + $new = null; | |
| 156 | + if ($newStartDate) { | |
| 157 | + $new = gmdate("F j, Y", strtotime($newStartDate)); | |
| 158 | + } | |
| 159 | + $old = null; | |
| 160 | + if(!$oldDate){ | |
| 161 | + $action = 'added'; | |
| 162 | + }else{ | |
| 163 | + $old = gmdate("F j, Y", strtotime($oldDate)); | |
| 164 | + } | |
| 165 | + | |
| 166 | + $this->createLogActivity($task->id, $action, $column, $old, $new); | |
| 167 | + } | |
| 168 | + | |
| 169 | + | |
| 129 | 170 | public function logPriorityChangeActivity($task, $old) |
| 130 | 171 | { |
| 131 | 172 | $new = $task->priority; |
| 132 | 173 | $this->createLogActivity($task->id, 'changed', 'priority', $old, $new); |
| @@ -139,19 +180,31 @@ | ||
| 139 | 180 | $parent = Comment::findOrFail($comment->parent_id); |
| 140 | 181 | $parentDescription = $parent->description; |
| 141 | 182 | $this->createLogActivity($comment->task_id, 'added', 'a reply'); |
| 142 | 183 | }else{ |
| 143 | - $commentPlainText = strip_tags($comment->description); | |
| 184 | + $commentPlainText = wp_strip_all_tags($comment->description); | |
| 144 | 185 | $this->createLogActivity($comment->task_id, 'added', 'comment', null, $commentPlainText, null); |
| 145 | 186 | } |
| 146 | 187 | } |
| 147 | - public function logCommentUpdateActivity($id, $oldComment, $newComment) | |
| 188 | + public function logCommentUpdateActivity($comment, $oldComment) | |
| 148 | 189 | { |
| 149 | - $this->createLogActivity($id, 'updated', 'comment', $oldComment, $newComment); | |
| 190 | + $taskId = $comment->task_id; | |
| 191 | + $newComment = $comment->settings['raw_description'] ?? $comment->description; | |
| 192 | + | |
| 193 | + // Activity previews are plain text, while comment drafts now contain inline HTML. | |
| 194 | + $plainText = static function ($content) { | |
| 195 | + $content = preg_replace('~<br\s*/?>|</p\s*>~i', "\n", $content); | |
| 196 | + return trim(html_entity_decode(wp_strip_all_tags($content), ENT_QUOTES | ENT_HTML5, 'UTF-8')); | |
| 197 | + }; | |
| 198 | + | |
| 199 | + $this->createLogActivity($taskId, 'updated', 'comment', $plainText($oldComment), $plainText($newComment)); | |
| 150 | 200 | } |
| 151 | - public function logCommentDeleteActivity($id, $commentDescription) | |
| 201 | + public function logCommentDeleteActivity($comment) | |
| 152 | 202 | { |
| 153 | - $this->createLogActivity($id, 'removed', 'comment', null, $commentDescription, null); | |
| 203 | + $commentDescription = wp_strip_all_tags($comment->settings['raw_description'] ?? $comment->description ); | |
| 204 | + $taskId = $comment->task_id; | |
| 205 | + | |
| 206 | + $this->createLogActivity($taskId, 'removed', 'comment', null, $commentDescription, null); | |
| 154 | 207 | } |
| 155 | 208 | |
| 156 | 209 | public function logSubtaskAddedActivity($parentTask, $subTask) |
| 157 | 210 | { |
| @@ -157,20 +210,37 @@ | ||
| 157 | 210 | { |
| 158 | 211 | $this->createLogActivity($parentTask->id, 'added', 'subtask', null, $subTask->title); |
| 159 | 212 | } |
| 160 | 213 | |
| 214 | + public function logSubtaskCloneActivity($parentTask, $subTask) | |
| 215 | + { | |
| 216 | + $this->createLogActivity($parentTask->id, 'cloned', 'subtask', null, $subTask->title); | |
| 217 | + } | |
| 218 | + public function logSubtaskGroupAddedActivity($task_id, $subTaskGroup) | |
| 219 | + { | |
| 220 | + $this->createLogActivity($task_id, 'added', 'subtask group', null, $subTaskGroup->value); | |
| 221 | + } | |
| 222 | + | |
| 161 | 223 | public function logSubtaskDeletedActivity($id, $subTaskTitle) |
| 162 | 224 | { |
| 163 | 225 | $this->createLogActivity($id, 'removed', 'subtask', null, $subTaskTitle); |
| 164 | 226 | } |
| 165 | 227 | |
| 228 | + public function logSubtaskGroupDeletedActivity($task_id, $subTaskGroup) | |
| 229 | + { | |
| 230 | + $this->createLogActivity($task_id, 'removed', 'subtask group', null, $subTaskGroup->value); | |
| 231 | + } | |
| 232 | + public function logSubtaskGroupTitleUpdatedActivity($oldTitle, $group) | |
| 233 | + { | |
| 234 | + $this->createLogActivity($group->task_id, 'changed', 'subtask group title', $oldTitle, $group->value); | |
| 235 | + } | |
| 166 | 236 | public function logTaskCompletedOrReopenActivity($task, $status) |
| 167 | 237 | { |
| 168 | - if($status == 'completed'){ | |
| 238 | + if($status == 'closed'){ | |
| 169 | 239 | if($task->parent_id){ |
| 170 | - $this->createLogActivity($task->parent_id, 'completed', 'subtask', $task->title); | |
| 240 | + $this->createLogActivity($task->parent_id, 'closed', 'subtask', $task->title); | |
| 171 | 241 | }else{ |
| 172 | - $this->createLogActivity($task->id, 'completed', 'task', $task->title); | |
| 242 | + $this->createLogActivity($task->id, 'closed', 'task', $task->title); | |
| 173 | 243 | } |
| 174 | 244 | }else{ |
| 175 | 245 | if($task->parent_id){ |
| 176 | 246 | $this->createLogActivity($task->parent_id, 'reopened', 'subtask', $task->title); |
| @@ -179,12 +249,18 @@ | ||
| 179 | 249 | } |
| 180 | 250 | } |
| 181 | 251 | } |
| 182 | 252 | |
| 183 | - public function logTaskStageUpdatedActivity($task, $oldStageId) | |
| 253 | + public function logTaskStageUpdatedActivity($task, $oldStageId, $stageContext = null) | |
| 184 | 254 | { |
| 185 | - $oldStage = Stage::findOrFail($oldStageId); | |
| 186 | - $newStage = Stage::findOrFail($task->stage_id); | |
| 255 | + $oldStage = $stageContext['source'] ?? null; | |
| 256 | + $newStage = $stageContext['target'] ?? null; | |
| 257 | + | |
| 258 | + if (!$oldStage instanceof Stage || !$newStage instanceof Stage) { | |
| 259 | + $oldStage = Stage::findOrFail($oldStageId); | |
| 260 | + $newStage = Stage::findOrFail($task->stage_id); | |
| 261 | + } | |
| 262 | + | |
| 187 | 263 | if($oldStage && $newStage){ |
| 188 | 264 | $this->createLogActivity($task->id, 'changed', 'stage', $oldStage->title, $newStage->title); |
| 189 | 265 | } |
| 190 | 266 | } |
| @@ -284,6 +360,119 @@ | ||
| 284 | 360 | } |
| 285 | 361 | public function taskAttachmentDeleted($attachment) |
| 286 | 362 | { |
| 287 | 363 | $this->createLogActivity($attachment->object_id , 'deleted', 'attachment', null, strlen($attachment->title) > 0 ? $attachment->title : $attachment->full_url); |
| 364 | + } | |
| 365 | + | |
| 366 | + public function taskArchived($task) | |
| 367 | + { | |
| 368 | + if(!$task->archived_at){ | |
| 369 | + $this->createLogActivity($task->id, 'restored', 'task', $task->title); | |
| 370 | + }else{ | |
| 371 | + $this->createLogActivity($task->id, 'archived', 'task', $task->title); | |
| 372 | + } | |
| 373 | + } | |
| 374 | + | |
| 375 | + public function logRepeatTaskCreatedActivity($newTask, $parentTask) | |
| 376 | + { | |
| 377 | + $this->createLogActivity( | |
| 378 | + $newTask->id, | |
| 379 | + 'created', | |
| 380 | + 'repeat task', | |
| 381 | + $newTask->title, | |
| 382 | + $parentTask->title, | |
| 383 | + null, | |
| 384 | + $parentTask | |
| 385 | + ); | |
| 386 | + } | |
| 387 | + | |
| 388 | + public function logRepeatTaskSet($newTask) | |
| 389 | + { | |
| 390 | + $this->createLogActivity( | |
| 391 | + $newTask->id, | |
| 392 | + 'set', | |
| 393 | + 'Repeat Task', | |
| 394 | + null, | |
| 395 | + null, | |
| 396 | + ); | |
| 397 | + } | |
| 398 | + | |
| 399 | + public function logRepeatTaskUpdated($newTask) | |
| 400 | + { | |
| 401 | + $this->createLogActivity( | |
| 402 | + $newTask->id, | |
| 403 | + 'updated', | |
| 404 | + 'Repeat Task', | |
| 405 | + null, | |
| 406 | + null | |
| 407 | + ); | |
| 408 | + } | |
| 409 | + public function taskMovedFromBoard($task, $oldBoard, $newBoard) | |
| 410 | + { | |
| 411 | + $this->createLogActivity($task->id, 'moved', 'board', $oldBoard->title, $newBoard->title); | |
| 412 | + } | |
| 413 | + | |
| 414 | + public function logCustomFieldActivity($taskId, $customField, $oldValue, $newValue, $isNewCustomField) | |
| 415 | + { | |
| 416 | + // Format values for display in activity | |
| 417 | + $displayNewValue = $this->formatValueForDisplay($newValue, $customField->settings['custom_field_type']); | |
| 418 | + | |
| 419 | + $action = 'changed'; | |
| 420 | + $column = $customField->title; | |
| 421 | + | |
| 422 | + // Ensure we have a value to display | |
| 423 | + if (empty($displayNewValue)) { | |
| 424 | + $displayNewValue = 'empty'; | |
| 425 | + } | |
| 426 | + | |
| 427 | + // Handle old value display logic | |
| 428 | + $displayOldValue = null; | |
| 429 | + if (!$isNewCustomField && $oldValue !== null && $oldValue !== '') { | |
| 430 | + // Format old value for display if it exists | |
| 431 | + $displayOldValue = $this->formatValueForDisplay($oldValue, $customField->settings['custom_field_type']); | |
| 432 | + } | |
| 433 | + | |
| 434 | + $settings = [ | |
| 435 | + 'custom_field_id' => $customField->id, | |
| 436 | + 'custom_field_title' => $customField->title, | |
| 437 | + 'custom_field_type' => $customField->settings['custom_field_type'] | |
| 438 | + ]; | |
| 439 | + | |
| 440 | + $this->createLogActivity($taskId, $action, $column, $displayOldValue, $displayNewValue, null, $settings); | |
| 441 | + } | |
| 442 | + | |
| 443 | + /** | |
| 444 | + * Format custom field value for display in activity logs | |
| 445 | + */ | |
| 446 | + private function formatValueForDisplay($value, $fieldType) | |
| 447 | + { | |
| 448 | + if ($value === null || $value === '') { | |
| 449 | + return 'empty'; | |
| 450 | + } | |
| 451 | + | |
| 452 | + switch ($fieldType) { | |
| 453 | + case 'checkbox': | |
| 454 | + return $value ? 'checked' : 'unchecked'; | |
| 455 | + case 'date': | |
| 456 | + // If it's already formatted as Y-m-d H:i:s, convert to readable format | |
| 457 | + if (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/', $value)) { | |
| 458 | + return gmdate('M j, Y g:i A', strtotime($value)); | |
| 459 | + } | |
| 460 | + return $value; | |
| 461 | + case 'select': | |
| 462 | + case 'text': | |
| 463 | + case 'textarea': | |
| 464 | + case 'number': | |
| 465 | + default: | |
| 466 | + return (string) $value; | |
| 467 | + } | |
| 468 | + } | |
| 469 | + public function taskCloned($originalTask, $clonedTask) | |
| 470 | + { | |
| 471 | + $this->createLogActivity( | |
| 472 | + $clonedTask->id, | |
| 473 | + 'cloned', | |
| 474 | + 'task', | |
| 475 | + 'from ' . $originalTask->title, | |
| 476 | + ); | |
| 288 | 477 | } |
| 289 | 478 | } |