← All changes
|
app/Services/Intergrations/FluentCRM/Automations/TaskCreateAction.php
+239
-33
1.21
→
2.2.0
View file →
| @@ -2,13 +2,17 @@ | ||
| 2 | 2 | |
| 3 | 3 | namespace FluentBoards\App\Services\Intergrations\FluentCRM\Automations; |
| 4 | 4 | |
| 5 | 5 | use FluentBoards\App\Models\Task; |
| 6 | +use FluentBoards\App\Models\Stage; | |
| 6 | 7 | use FluentBoards\App\Services\TaskService; |
| 8 | +use FluentBoards\App\Services\PermissionManager; | |
| 9 | +use FluentCrm\App\Models\Funnel; | |
| 7 | 10 | use FluentCrm\App\Services\Funnel\BaseAction; |
| 8 | 11 | use FluentCrm\App\Services\Funnel\FunnelHelper; |
| 9 | 12 | use FluentCrm\Framework\Support\Arr; |
| 10 | 13 | use FluentBoards\App\Services\Helper; |
| 14 | +use FluentBoardsPro\App\Services\ProTaskService; | |
| 11 | 15 | |
| 12 | 16 | class TaskCreateAction extends BaseAction |
| 13 | 17 | { |
| 14 | 18 | public function __construct() |
| @@ -26,9 +30,12 @@ | ||
| 26 | 30 | 'description' => __('Create Task to the selected Board', 'fluent-boards'), |
| 27 | 31 | 'icon' => 'fc-icon-apply_list', |
| 28 | 32 | 'settings' => [ |
| 29 | 33 | 'stage' => [], |
| 30 | - 'title' => 'Task from automation of {{contact.email}}' | |
| 34 | + 'create_task_type' => 'new', | |
| 35 | + 'title' => 'Task from automation of {{contact.email}}', | |
| 36 | + 'created_by' => get_current_user_id(), | |
| 37 | + 'assignees' => [], | |
| 31 | 38 | ] |
| 32 | 39 | ]; |
| 33 | 40 | } |
| 34 | 41 | |
| @@ -44,19 +51,88 @@ | ||
| 44 | 51 | 'label' => __('Select Board & It\'s stage', 'fluent-boards'), |
| 45 | 52 | 'placeholder' => __('Select Board & It\'s stage', 'fluent-boards'), |
| 46 | 53 | 'options' => Helper::getStagesByBoardGroup() |
| 47 | 54 | ], |
| 55 | + | |
| 56 | + 'created_by' => [ | |
| 57 | + 'type' => 'rest_selector', | |
| 58 | + 'option_key' => 'board_members', | |
| 59 | + 'is_multiple' => false, | |
| 60 | + 'label' => __('Created by', 'fluent-boards'), | |
| 61 | + 'placeholder' => __('Select a task creator', 'fluent-boards'), | |
| 62 | + ], | |
| 63 | + | |
| 64 | + 'assignees' => [ | |
| 65 | + 'type' => 'rest_selector', | |
| 66 | + 'option_key' => 'board_members', | |
| 67 | + 'is_multiple' => true, | |
| 68 | + 'label' => __('Assign to', 'fluent-boards'), | |
| 69 | + 'placeholder' => __('Select task assignees', 'fluent-boards'), | |
| 70 | + ], | |
| 71 | + | |
| 72 | + 'create_task_type' => [ | |
| 73 | + 'type' => 'radio', | |
| 74 | + 'wrapper_class' => 'fc_half_field', | |
| 75 | + 'label' => __('Task Create Type', 'fluent-boards'), | |
| 76 | + 'options' => [ | |
| 77 | + [ | |
| 78 | + 'id' => 'new', | |
| 79 | + 'title' => __('Create from stratch', 'fluent-boards') | |
| 80 | + ], | |
| 81 | + [ | |
| 82 | + 'id' => 'template', | |
| 83 | + 'title' => __('Create from template', 'fluent-boards') | |
| 84 | + ] | |
| 85 | + ] | |
| 86 | + ], | |
| 87 | + | |
| 88 | + 'task_template' => [ | |
| 89 | + 'type' => 'rest_selector', | |
| 90 | + 'option_key' => 'task_templates', | |
| 91 | + 'is_multiple' => false, | |
| 92 | + 'label' => __('Select Task Template', 'fluent-boards'), | |
| 93 | + 'placeholder' => __('Select Template', 'fluent-boards'), | |
| 94 | + 'dependency' => [ | |
| 95 | + 'depends_on' => 'create_task_type', | |
| 96 | + 'operator' => '=', | |
| 97 | + 'value' => 'template' | |
| 98 | + ] | |
| 99 | + ], | |
| 100 | + | |
| 101 | + 'title_template' => [ | |
| 102 | + 'type' => 'input-text-popper', | |
| 103 | + 'field_type' => 'text', | |
| 104 | + 'label' => __('Task Title', 'fluent-boards'), | |
| 105 | + 'placeholder' => __('Task Title', 'fluent-boards'), | |
| 106 | + 'inline_help' => __('Leaving it blank will make the title the same as the template task title.', 'fluent-boards'), | |
| 107 | + 'dependency' => [ | |
| 108 | + 'depends_on' => 'create_task_type', | |
| 109 | + 'operator' => '=', | |
| 110 | + 'value' => 'template' | |
| 111 | + ] | |
| 112 | + ], | |
| 113 | + | |
| 48 | 114 | 'title' => [ |
| 49 | 115 | 'type' => 'input-text-popper', |
| 50 | 116 | 'field_type' => 'text', |
| 51 | 117 | 'label' => __('Task Title', 'fluent-boards'), |
| 52 | - 'placeholder' => __('Task Title', 'fluent-boards') | |
| 118 | + 'placeholder' => __('Task Title', 'fluent-boards'), | |
| 119 | + 'dependency' => [ | |
| 120 | + 'depends_on' => 'create_task_type', | |
| 121 | + 'operator' => '=', | |
| 122 | + 'value' => 'new' | |
| 123 | + ] | |
| 53 | 124 | ], |
| 54 | 125 | |
| 55 | 126 | 'due_day' => [ |
| 56 | 127 | 'label' => __('Due Date', 'fluent-boards'), |
| 57 | 128 | 'type' => 'input-number', |
| 58 | - 'inline_help' => __('Set to your_input days after task creation, values less than zero will set due date to null', 'fluent-boards') | |
| 129 | + 'inline_help' => __('Set to your_input days after task creation, values less than zero will set due date to null', 'fluent-boards'), | |
| 130 | + 'dependency' => [ | |
| 131 | + 'depends_on' => 'create_task_type', | |
| 132 | + 'operator' => '=', | |
| 133 | + 'value' => 'new' | |
| 134 | + ] | |
| 59 | 135 | ], |
| 60 | 136 | |
| 61 | 137 | 'description' => [ |
| 62 | 138 | 'type' => 'html_editor', |
| @@ -61,9 +137,14 @@ | ||
| 61 | 137 | 'description' => [ |
| 62 | 138 | 'type' => 'html_editor', |
| 63 | 139 | 'smart_codes' => 'yes', |
| 64 | 140 | 'context_codes' => 'yes', |
| 65 | - 'label' => __('Description', 'fluent-boards') | |
| 141 | + 'label' => __('Description', 'fluent-boards'), | |
| 142 | + 'dependency' => [ | |
| 143 | + 'depends_on' => 'create_task_type', | |
| 144 | + 'operator' => '=', | |
| 145 | + 'value' => 'new' | |
| 146 | + ] | |
| 66 | 147 | ], |
| 67 | 148 | |
| 68 | 149 | 'priority' => [ |
| 69 | 150 | 'type' => 'select', |
| @@ -68,58 +149,183 @@ | ||
| 68 | 149 | 'priority' => [ |
| 69 | 150 | 'type' => 'select', |
| 70 | 151 | 'label' => __('Select Priority', 'fluent-boards'), |
| 71 | 152 | 'options' => Helper::getPriorityOptions(), |
| 72 | - 'inline_help' => __('Keeping it blank will select priority to low', 'fluent-boards') | |
| 153 | + 'inline_help' => __('Keeping it blank will create the task with no priority', 'fluent-boards'), | |
| 154 | + 'dependency' => [ | |
| 155 | + 'depends_on' => 'create_task_type', | |
| 156 | + 'operator' => '=', | |
| 157 | + 'value' => 'new' | |
| 158 | + ] | |
| 73 | 159 | ] |
| 74 | 160 | ] |
| 75 | 161 | ]; |
| 76 | 162 | } |
| 77 | 163 | |
| 164 | + /** Create a task for the triggering contact, using the shared template copier. */ | |
| 78 | 165 | public function handle( $subscriber, $sequence, $funnelSubscriberId, $funnelMetric ) |
| 79 | 166 | { |
| 80 | 167 | $data = $sequence->settings; |
| 81 | 168 | |
| 169 | + $createType = Arr::get($data, 'create_task_type'); | |
| 82 | 170 | $stage = Arr::get($data, 'stage'); // this is a string of the pipeline stage id |
| 83 | - $title = Arr::get( $data, 'title'); | |
| 84 | - $priority = Arr::get( $data, 'priority'); | |
| 85 | - $due_day = Arr::get( $data, 'due_day'); | |
| 171 | + $creatorId = $this->resolveCreatorId($data, $sequence); | |
| 172 | + $configuredAssignees = Arr::get($data, 'assignees', []); | |
| 173 | + $hasConfiguredAssignees = !empty($configuredAssignees); | |
| 174 | + $assigneeIds = $this->resolveAssigneeIds($configuredAssignees); | |
| 86 | 175 | |
| 87 | - if(isset($due_day)) { | |
| 88 | - $due_date = Helper::dueDateConversion($due_day, 'day'); | |
| 89 | - } | |
| 90 | - | |
| 91 | - $description = Arr::get( $data, 'description'); | |
| 92 | - | |
| 93 | 176 | if ( empty($stage) ) { |
| 94 | 177 | FunnelHelper::changeFunnelSubSequenceStatus( $funnelSubscriberId, $sequence->id, 'skipped' ); |
| 95 | 178 | return; |
| 96 | 179 | } |
| 97 | - $stageId = intval( $stage ); | |
| 98 | - $board = Helper::getBoardByStageId( $stageId ); | |
| 180 | + | |
| 181 | + $stageId = (int) $stage; | |
| 182 | + if ($stageId <= 0) { | |
| 183 | + return; | |
| 184 | + } | |
| 99 | 185 | |
| 100 | - if ( !$stage ) { | |
| 186 | + $destinationStage = Stage::find($stageId); | |
| 187 | + $board = $destinationStage ? $destinationStage->board : null; | |
| 188 | + | |
| 189 | + if (!$board) { | |
| 190 | + FunnelHelper::changeFunnelSubSequenceStatus($funnelSubscriberId, $sequence->id, 'skipped'); | |
| 101 | 191 | return; |
| 102 | 192 | } |
| 103 | 193 | |
| 104 | - $description = apply_filters('fluent_crm/parse_campaign_email_text', $description, $subscriber); | |
| 105 | - $title = apply_filters('fluent_crm/parse_campaign_email_text', $title, $subscriber); | |
| 194 | + if ($createType === 'template') { | |
| 195 | + $templateTaskId = absint(Arr::get($data, 'task_template')); | |
| 196 | + $templateTask = $templateTaskId ? Task::whereHas('taskMeta', function ($query) { | |
| 197 | + $query->where('key', 'is_template')->where('value', 'yes'); | |
| 198 | + })->find($templateTaskId) : null; | |
| 106 | 199 | |
| 107 | - (new Task())->createTask([ | |
| 108 | - 'title' => $title, | |
| 109 | - 'board_id' => $board->id, | |
| 110 | - 'crm_contact_id' => $subscriber->id, | |
| 111 | - 'type' => 'task', | |
| 112 | - 'status' => 'open', | |
| 113 | - 'stage_id' => $stageId, | |
| 114 | - 'source' => 'funnel', | |
| 115 | - 'description' => $description, | |
| 116 | - 'priority' => $priority, | |
| 117 | - 'due_at' => $due_date ?? null, | |
| 118 | - 'position' => (new TaskService())->getLastPositionOfTasks($stageId), | |
| 119 | - ]); | |
| 200 | + // Authorize the stored automation owner, not the configurable task creator or runner. | |
| 201 | + $ownerId = Funnel::where('id', absint($sequence->funnel_id))->value('created_by'); | |
| 202 | + $ownerId = $this->getExistingUserIds([$ownerId])[0] ?? 0; | |
| 203 | + if (!$templateTask || !$ownerId | |
| 204 | + || !PermissionManager::userHasPermission($templateTask->board_id, $ownerId) | |
| 205 | + || !PermissionManager::userHasPermission($board->id, $ownerId)) { | |
| 206 | + FunnelHelper::changeFunnelSubSequenceStatus($funnelSubscriberId, $sequence->id, 'skipped'); | |
| 207 | + return; | |
| 208 | + } | |
| 120 | 209 | |
| 210 | + $title = Arr::get($data, 'title_template'); | |
| 211 | + if ($title === null || trim($title) === '') { | |
| 212 | + $title = $templateTask->title; | |
| 213 | + } | |
| 214 | + $title = apply_filters('fluent_crm/parse_campaign_email_text', $title, $subscriber); | |
| 215 | + | |
| 216 | + $context = [ | |
| 217 | + 'created_by' => $creatorId, | |
| 218 | + 'crm_contact_id' => absint($subscriber->id), | |
| 219 | + 'source' => 'funnel', | |
| 220 | + ]; | |
| 221 | + if ($hasConfiguredAssignees) { | |
| 222 | + $context['assignees'] = $assigneeIds; | |
| 223 | + } elseif ((int) $templateTask->board_id !== (int) $board->id) { | |
| 224 | + // Preserve inherited assignments only for users who can access the destination. | |
| 225 | + $context['assignees'] = PermissionManager::getUserIdsWithBoardAccess( | |
| 226 | + $board->id, | |
| 227 | + $templateTask->assignees()->get()->pluck('ID')->toArray() | |
| 228 | + ); | |
| 229 | + } | |
| 230 | + | |
| 231 | + $taskData = [ | |
| 232 | + 'title' => sanitize_text_field($title), | |
| 233 | + 'board_id' => $board->id, | |
| 234 | + 'stage_id' => $stageId, | |
| 235 | + 'assignee' => !$hasConfiguredAssignees, | |
| 236 | + 'label' => true, | |
| 237 | + 'subtask' => true, | |
| 238 | + 'custom_field' => true, | |
| 239 | + ]; | |
| 240 | + | |
| 241 | + // Free and Pro update independently; old Pro must not discard context or skip tasks. | |
| 242 | + if (class_exists(ProTaskService::class) | |
| 243 | + && method_exists(ProTaskService::class, 'createFromTemplateForAutomation')) { | |
| 244 | + (new ProTaskService())->createFromTemplateForAutomation($templateTaskId, $taskData, $context); | |
| 245 | + } else { | |
| 246 | + (new TaskService())->createAutomationTaskFromLegacyTemplate($templateTask, $taskData, $context); | |
| 247 | + } | |
| 248 | + } else { | |
| 249 | + $title = Arr::get( $data, 'title'); | |
| 250 | + $priority = Arr::get( $data, 'priority'); | |
| 251 | + $due_day = Arr::get( $data, 'due_day'); | |
| 252 | + | |
| 253 | + if(isset($due_day)) { | |
| 254 | + $due_date = Helper::dueDateConversion($due_day, 'day'); | |
| 255 | + } | |
| 256 | + | |
| 257 | + $description = Arr::get( $data, 'description'); | |
| 258 | + | |
| 259 | + $description = apply_filters('fluent_crm/parse_campaign_email_text', $description, $subscriber); | |
| 260 | + $title = apply_filters('fluent_crm/parse_campaign_email_text', $title, $subscriber); | |
| 261 | + | |
| 262 | + (new Task())->createTask([ | |
| 263 | + 'title' => $title, | |
| 264 | + 'board_id' => $board->id, | |
| 265 | + 'crm_contact_id' => $subscriber->id, | |
| 266 | + 'type' => 'task', | |
| 267 | + 'status' => 'open', | |
| 268 | + 'stage_id' => $stageId, | |
| 269 | + 'source' => 'funnel', | |
| 270 | + 'description' => $description, | |
| 271 | + 'priority' => $priority ?? '', | |
| 272 | + 'due_at' => $due_date ?? null, | |
| 273 | + 'position' => (new TaskService())->getLastPositionOfTasks($stageId), | |
| 274 | + 'created_by' => $creatorId, | |
| 275 | + 'assignees' => $assigneeIds, | |
| 276 | + ]); | |
| 277 | + } | |
| 278 | + | |
| 121 | 279 | FunnelHelper::changeFunnelSubSequenceStatus($funnelSubscriberId, $sequence->id, 'completed'); |
| 122 | 280 | |
| 123 | 281 | } |
| 124 | 282 | |
| 125 | -} | |
| 283 | + /** | |
| 284 | + * Resolve a valid WordPress user to own tasks created by the automation. | |
| 285 | + */ | |
| 286 | + private function resolveCreatorId($data, $sequence): int | |
| 287 | + { | |
| 288 | + $candidateIds = [ | |
| 289 | + Arr::get($data, 'created_by'), | |
| 290 | + $sequence->created_by ?? 0, | |
| 291 | + get_current_user_id(), | |
| 292 | + ]; | |
| 293 | + $existingIds = $this->getExistingUserIds($candidateIds); | |
| 294 | + | |
| 295 | + return $existingIds[0] ?? 0; | |
| 296 | + } | |
| 297 | + | |
| 298 | + /** | |
| 299 | + * Normalize configured task assignees to existing WordPress user IDs. | |
| 300 | + */ | |
| 301 | + private function resolveAssigneeIds($assignees): array | |
| 302 | + { | |
| 303 | + return $this->getExistingUserIds((array) $assignees); | |
| 304 | + } | |
| 305 | + | |
| 306 | + /** | |
| 307 | + * Keep positive, unique IDs that still belong to WordPress users. | |
| 308 | + */ | |
| 309 | + private function getExistingUserIds($userIds): array | |
| 310 | + { | |
| 311 | + $userIds = array_values(array_unique(array_filter(array_map('absint', (array) $userIds)))); | |
| 312 | + | |
| 313 | + if (!$userIds) { | |
| 314 | + return []; | |
| 315 | + } | |
| 316 | + | |
| 317 | + $users = get_users([ | |
| 318 | + 'include' => $userIds, | |
| 319 | + 'fields' => 'ID', | |
| 320 | + ]); | |
| 321 | + $existingIds = array_map(function ($user) { | |
| 322 | + return absint(is_object($user) ? $user->ID : $user); | |
| 323 | + }, $users); | |
| 324 | + $existingLookup = array_flip($existingIds); | |
| 325 | + | |
| 326 | + return array_values(array_filter($userIds, function ($userId) use ($existingLookup) { | |
| 327 | + return isset($existingLookup[$userId]); | |
| 328 | + })); | |
| 329 | + } | |
| 330 | + | |
| 331 | +} | |