PluginProbe
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration / 2.0.0
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration v2.0.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 1.45 All 41 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.0.0, at app/Modules/MCP/Tools/BoardTools.php

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