PluginProbe
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration / 2.1.0
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration v2.1.0
2.1.0 2.0.15 2.0.12 2.0.10 2.0.4 2.0.1 2.0.0 1.95.3 1.95.2 1.95 1.91.6 trunk 1.11 1.12 1.13 1.20 1.21 1.22 1.23 1.30 1.31 1.32 1.35 1.40 1.41 All 42 releases
fluent-boards / app / Modules / MCP / Tools / BoardTools.php

BoardTools.php in FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration 2.1.0, at app/Modules/MCP/Tools/BoardTools.php

419 lines 13.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentBoards\App\Modules\MCP\Tools;
4
5 use FluentBoards\App\Models\Board;
6 use FluentBoards\App\Models\Stage;
7 use FluentBoards\App\Modules\MCP\Helpers\MCPHelper;
8 use FluentBoards\App\Services\BoardService;
9 use FluentBoards\App\Services\Constant;
10 use FluentBoards\App\Services\FolderService;
11 use FluentBoards\App\Services\Helper;
12 use FluentBoards\App\Services\LabelService;
13 use FluentBoards\App\Services\PermissionManager;
14 use FluentBoards\App\Services\StageService;
15
16 /**
17 * Board read/write tools and board permission helpers.
18 */
19 class BoardTools
20 {
21 const MAX_MEMBERS_PER_BOARD_CREATE = 50;
22
23 public static function canReadBoard($params = [])
24 {
25 $boardId = isset($params['board_id']) ? absint($params['board_id']) : 0;
26 return $boardId && MCPHelper::canReadBoard($boardId);
27 }
28
29 public static function canWriteBoard($params = [])
30 {
31 $boardId = isset($params['board_id']) ? absint($params['board_id']) : 0;
32 return $boardId && MCPHelper::canWriteBoard($boardId);
33 }
34
35 public static function listBoards($params = [])
36 {
37 $pagination = MCPHelper::normalizePagination($params);
38 $userId = get_current_user_id();
39 $boardIds = PermissionManager::getBoardIdsForUser($userId);
40
41 if (!$boardIds) {
42 return [
43 'items' => [],
44 'pagination' => self::paginationMeta(null, $pagination),
45 ];
46 }
47
48 $query = Board::with(['stages', 'labels', 'users'])
49 ->whereIn('id', $boardIds);
50
51 if (empty($params['include_archived'])) {
52 $query->whereNull('archived_at');
53 }
54
55 if (!empty($params['type'])) {
56 $query->where('type', sanitize_text_field($params['type']));
57 }
58
59 if (!empty($params['search'])) {
60 $search = sanitize_text_field($params['search']);
61 $query->where('title', 'like', '%' . $search . '%');
62 }
63
64 $allowedSort = ['id', 'title', 'created_at', 'updated_at'];
65 $sortBy = !empty($params['sort_by']) && in_array($params['sort_by'], $allowedSort, true)
66 ? $params['sort_by']
67 : 'created_at';
68 $sortType = !empty($params['sort_type']) && strtoupper($params['sort_type']) === 'ASC' ? 'ASC' : 'DESC';
69
70 $paginated = $query->orderBy($sortBy, $sortType)
71 ->paginate($pagination['per_page'], ['*'], 'page', $pagination['page']);
72
73 $items = [];
74 foreach ($paginated->items() as $board) {
75 $items[] = MCPHelper::formatBoardSummary($board);
76 }
77
78 return [
79 'items' => $items,
80 'pagination' => self::paginationMeta($paginated, $pagination),
81 ];
82 }
83
84 public static function getBoard($params = [])
85 {
86 $board = MCPHelper::resolveBoard($params);
87 if (is_wp_error($board)) {
88 return $board;
89 }
90
91 if (!MCPHelper::canReadBoard($board->id)) {
92 return MCPHelper::error('forbidden', __('You do not have access to this board', 'fluent-boards'));
93 }
94
95 return [
96 'board' => MCPHelper::formatBoard($board, !empty($params['include_tasks'])),
97 ];
98 }
99
100 public static function createBoard($params = [])
101 {
102 if (!PermissionManager::userHasBoardCreationPermission()) {
103 return MCPHelper::error('forbidden', __('You do not have permission to create boards', 'fluent-boards'));
104 }
105
106 $title = isset($params['title']) ? sanitize_text_field($params['title']) : '';
107 if ($title === '') {
108 return MCPHelper::error('invalid_param', __('Board title is required', 'fluent-boards'));
109 }
110
111 $type = !empty($params['type']) ? sanitize_text_field($params['type']) : 'to-do';
112 if (!in_array($type, ['to-do', 'roadmap'], true)) {
113 return MCPHelper::error('invalid_param', __('Invalid board type', 'fluent-boards'), [
114 'allowed' => ['to-do', 'roadmap'],
115 ]);
116 }
117
118 $memberIds = self::validateBoardMemberIds($params['member_ids'] ?? []);
119 if (is_wp_error($memberIds)) {
120 return $memberIds;
121 }
122
123 $labelPresetValidation = self::validateLabelPresets($params['labels'] ?? null);
124 if (is_wp_error($labelPresetValidation)) {
125 return $labelPresetValidation;
126 }
127
128 $description = isset($params['description']) ? MCPHelper::sanitizeMarkdown($params['description']) : '';
129
130 $boardData = Helper::sanitizeBoard([
131 'title' => $title,
132 'description' => '',
133 'type' => $type,
134 'currency' => !empty($params['currency']) ? sanitize_text_field($params['currency']) : 'USD',
135 'crm_contact_id' => !empty($params['crm_contact_id']) ? absint($params['crm_contact_id']) : 0,
136 ]);
137 $boardData['description'] = $description;
138
139 $boardService = new BoardService();
140 $labelService = new LabelService();
141 $stageService = new StageService();
142
143 $board = $boardService->createBoard($boardData);
144
145 self::createBoardLabels($labelService, $board->id, $params['labels'] ?? null);
146
147 $stages = self::sanitizeStages($params['stages'] ?? []);
148
149 if ($type === 'roadmap') {
150 if (!$stages) {
151 $stages = self::defaultRoadmapStages();
152 }
153 $stageService->createRoadmapStages($board, $stages);
154 } elseif ($stages) {
155 $stageService->createStages($board, $stages);
156 self::applyStageStatusOverrides($board->id, $stages);
157 } else {
158 $stageService->createDefaultStages($board);
159 }
160
161 self::addBoardMembers($boardService, $board->id, $memberIds);
162
163 if (!empty($boardData['crm_contact_id'])) {
164 $boardService->updateAssociateMember($boardData['crm_contact_id'], $board->id);
165 }
166
167 do_action('fluent_boards/board_created', $board);
168
169 if (!empty($params['folder_id'])) {
170 (new FolderService())->addBoardToFolder(absint($params['folder_id']), [$board->id]);
171 }
172
173 $board = Board::with(['stages', 'labels', 'users'])->find($board->id);
174
175 return [
176 'board' => MCPHelper::formatBoard($board),
177 'message' => __('Board has been created successfully', 'fluent-boards'),
178 ];
179 }
180
181 private static function paginationMeta($paginated, $fallback)
182 {
183 if (!$paginated) {
184 return [
185 'total' => 0,
186 'current_page' => (int) $fallback['page'],
187 'per_page' => (int) $fallback['per_page'],
188 'last_page' => 0,
189 ];
190 }
191
192 return [
193 'total' => (int) $paginated->total(),
194 'current_page' => (int) $paginated->currentPage(),
195 'per_page' => (int) $paginated->perPage(),
196 'last_page' => (int) $paginated->lastPage(),
197 ];
198 }
199
200 private static function sanitizeStages($stages)
201 {
202 $items = [];
203
204 if (!is_array($stages)) {
205 return $items;
206 }
207
208 foreach ($stages as $index => $stage) {
209 if (!is_array($stage)) {
210 continue;
211 }
212
213 $title = isset($stage['title']) ? sanitize_text_field($stage['title']) : '';
214 if ($title === '') {
215 continue;
216 }
217
218 $item = [
219 'title' => $title,
220 'slug' => !empty($stage['slug']) ? sanitize_title($stage['slug']) : sanitize_title($title),
221 'position' => !empty($stage['position']) ? absint($stage['position']) : $index + 1,
222 ];
223
224 if (!empty($stage['default_task_status']) && in_array($stage['default_task_status'], ['open', 'closed'], true)) {
225 $item['default_task_status'] = $stage['default_task_status'];
226 }
227
228 $items[] = $item;
229 }
230
231 return $items;
232 }
233
234 private static function defaultRoadmapStages()
235 {
236 return [
237 [
238 'title' => 'Pending',
239 'slug' => 'pending',
240 'position' => 1,
241 ],
242 [
243 'title' => 'Under Consideration',
244 'slug' => 'under_consideration',
245 'position' => 2,
246 ],
247 [
248 'title' => 'Planned',
249 'slug' => 'planned',
250 'position' => 3,
251 ],
252 [
253 'title' => 'Launched',
254 'slug' => 'launched',
255 'position' => 4,
256 ],
257 ];
258 }
259
260 private static function validateBoardMemberIds($memberIds)
261 {
262 if (!is_array($memberIds)) {
263 return MCPHelper::error('invalid_param', __('member_ids must be an array', 'fluent-boards'));
264 }
265
266 if (count($memberIds) > self::MAX_MEMBERS_PER_BOARD_CREATE) {
267 return MCPHelper::error('invalid_param', __('Too many members in one call', 'fluent-boards'), [
268 'max' => self::MAX_MEMBERS_PER_BOARD_CREATE,
269 ]);
270 }
271
272 $memberIds = MCPHelper::sanitizeIdArray($memberIds);
273 if (!$memberIds) {
274 return [];
275 }
276
277 $found = get_users([
278 'include' => $memberIds,
279 'fields' => 'ID',
280 'number' => count($memberIds),
281 ]);
282 $found = array_map('intval', (array) $found);
283 $unknownIds = array_values(array_diff($memberIds, $found));
284
285 if ($unknownIds) {
286 return MCPHelper::error('not_found', __('Some users do not exist', 'fluent-boards'), [
287 'unknown_user_ids' => $unknownIds,
288 ]);
289 }
290
291 return $memberIds;
292 }
293
294 /**
295 * StageService::createStages() only marks stages titled "completed"/"done" as closing stages,
296 * so honour explicit per-stage statuses once the stages exist.
297 */
298 private static function applyStageStatusOverrides($boardId, $stages)
299 {
300 $overrides = [];
301 foreach ($stages as $index => $stage) {
302 if (!empty($stage['default_task_status'])) {
303 $overrides[$index] = $stage['default_task_status'];
304 }
305 }
306
307 if (!$overrides) {
308 return;
309 }
310
311 $created = Stage::where('board_id', $boardId)
312 ->whereNull('archived_at')
313 ->orderBy('position', 'asc')
314 ->get();
315
316 foreach ($overrides as $index => $status) {
317 $stage = $created[$index] ?? null;
318 if (!$stage) {
319 continue;
320 }
321
322 $settings = $stage->settings ?: [];
323 if (($settings['default_task_status'] ?? '') === $status) {
324 continue;
325 }
326
327 $settings['default_task_status'] = $status;
328 $stage->settings = $settings;
329 $stage->save();
330 }
331 }
332
333 /**
334 * Mirrors BoardController::createBoardLabelsFromRequest().
335 */
336 private static function createBoardLabels($labelService, $boardId, $labels)
337 {
338 if (!is_array($labels) || !$labels) {
339 $labelService->createDefaultLabel($boardId);
340 return;
341 }
342
343 foreach ($labels as $label) {
344 if (!is_array($label)) {
345 continue;
346 }
347
348 $labelData = Helper::sanitizeLabel([
349 'label' => $label['title'] ?? ($label['label'] ?? ''),
350 'bg_color' => $label['bg_color'] ?? '',
351 'color' => $label['color'] ?? '',
352 'color_preset' => $label['color_preset'] ?? '',
353 ]);
354
355 if (empty($labelData['label']) && empty($labelData['bg_color'])) {
356 continue;
357 }
358
359 $labelService->createLabel([
360 'label' => $labelData['label'] ?? '',
361 'bg_color' => !empty($labelData['bg_color']) ? $labelData['bg_color'] : '#f3f4f6',
362 'color' => !empty($labelData['color']) ? $labelData['color'] : '#1B2533',
363 'color_preset' => $labelData['color_preset'] ?? '',
364 ], $boardId);
365 }
366 }
367
368 /**
369 * Validate label preset ids before the board is persisted.
370 *
371 * @param mixed $labels
372 * @return true|\WP_Error
373 */
374 private static function validateLabelPresets($labels)
375 {
376 if (!is_array($labels)) {
377 return true;
378 }
379
380 foreach ($labels as $label) {
381 if (!is_array($label)) {
382 continue;
383 }
384
385 $labelData = Helper::sanitizeLabel([
386 Constant::LABEL_COLOR_PRESET_SETTING => $label[Constant::LABEL_COLOR_PRESET_SETTING] ?? null,
387 ]);
388 $presetId = $labelData[Constant::LABEL_COLOR_PRESET_SETTING] ?? null;
389
390 if ($presetId === null || $presetId === '') {
391 continue;
392 }
393
394 if (!is_string($presetId) || !Constant::getLabelColorPreset($presetId)) {
395 return MCPHelper::error('invalid_param', __('Invalid label color preset', 'fluent-boards'));
396 }
397 }
398
399 return true;
400 }
401
402 private static function addBoardMembers($boardService, $boardId, $memberIds)
403 {
404 if (!$memberIds) {
405 return;
406 }
407
408 $currentUserId = get_current_user_id();
409
410 foreach ($memberIds as $memberId) {
411 if ($memberId === $currentUserId) {
412 continue;
413 }
414
415 $boardService->addMembersInBoard($boardId, $memberId);
416 }
417 }
418 }
419