_index($request); } /** * @throws Throwable * @api {post} /tasks/edit Edit * @apiDescription Edit Task * * @apiVersion 4.0.0 * @apiName Edit * @apiGroup Task * * @apiUse AuthHeader * * @apiPermission tasks_edit * @apiPermission tasks_full_access * * @apiParam {Integer} id ID * @apiParam {Integer} project_id Project * @apiParam {Integer} active Is Task active. Available value: {0,1} * @apiParam {Array} users Task Users * @apiParam {Integer} priority_id Priority ID * * * @apiParamExample {json} Request Example: * { * "start_date": null, * "due_date": null, * "id": 54, * "project_id": 159, * "project_phase_id": null, * "task_name": "Quo consequatur mollitia nam.", * "description": "
Aut iure minima vero voluptates nisi placeat. Distinctio fuga aut sit quia sequi. Cupiditate tenetur sit ut voluptatem ratione culpa. Voluptatibus id perspiciatis ipsa quas cumque laudantium repudiandae.
", * "assigned_by": 7, * "url": null, * "created_at": "2023-11-07T13:23:46.000000Z", * "updated_at": "2023-11-07T13:23:46.000000Z", * "deleted_at": null, * "priority_id": 2, * "important": 0, * "project_milestone_id": null, * "status_id": 2, * "relative_position": 54, * "estimate": null, * "total_spent_time": null, * "total_offset": null, * "priority": { * "id": 2, * "name": "Normal", * "created_at": "2023-10-26T10:26:17.000000Z", * "updated_at": "2024-06-21T10:06:50.000000Z", * "color": "#49E637" * }, * "project": { * "id": 159, * "company_id": 1, * "name": "Voluptas ab et ea.", * "description": "Cum aut sunt in fuga quia. Similique autem et quod qui eveniet omnis consequatur. Molestias tenetur est tempora tenetur.", * "deleted_at": null, * "created_at": "2023-11-07T13:23:45.000000Z", * "updated_at": "2023-11-07T13:23:45.000000Z", * "important": 0, * "source": "internal", * "default_priority_id": null, * "screenshots_state": 1 * }, * "phase": null, * "parents": [], * "children": [], * "users": [7], * "status": { * "id": 2, * "name": "Closed", * "active": false, * "created_at": "2024-02-28T16:47:00.000000Z", * "updated_at": "2024-06-08T16:17:04.000000Z", * "color": null, * "order": 4 * }, * "changes": [], * "comments": [ * { * "id": 22, * "task_id": 54, * "user_id": 1, * "content": "lll", * "created_at": "2024-08-15T14:49:16.000000Z", * "updated_at": "2024-08-15T14:49:16.000000Z", * "deleted_at": null, * "user": { * "id": 1, * "full_name": "Admin", * "email": "admin@cattr.app", * ... * } * }, * ... * ], * "workers": [] * } * * @apiSuccess {Integer} id Task ID. * @apiSuccess {Integer} project_id Project ID associated with the task. * @apiSuccess {Integer} [project_phase_id] ID of the project phase (if any). * @apiSuccess {String} task_name Name of the task. * @apiSuccess {String} description Description of the task. * @apiSuccess {Integer} assigned_by ID of the user who assigned the task. * @apiSuccess {String} url URL of the task. * @apiSuccess {String} created_at Task creation date in ISO format. * @apiSuccess {String} updated_at Task update date in ISO format. * @apiSuccess {String} [deleted_at] Deletion date if the task is soft-deleted. * @apiSuccess {Integer} priority_id Priority level of the task. * @apiSuccess {Boolean} important Indicates whether the task is marked as important. * @apiSuccess {String} [start_date] Start date of the task (if set). * @apiSuccess {String} [due_date] Due date of the task (if set). * @apiSuccess {Integer} status_id Status ID of the task. * @apiSuccess {Integer} relative_position Relative position of the task in the list. * * @apiSuccessExample {json} Success Response: * { * "id": 54, * "project_id": 159, * "project_phase_id": null, * "task_name": "Quo consequatur mollitia nam.", * "description": "Aut iure minima vero voluptates nisi placeat. Distinctio fuga aut sit quia sequi...
", * "assigned_by": 7, * "url": null, * "created_at": "2023-11-07T13:23:46.000000Z", * "updated_at": "2024-08-15T15:39:53.000000Z", * "deleted_at": null, * "priority_id": 2, * "important": 0, * "start_date": null, * "project_milestone_id": null, * "status_id": 2, * "relative_position": 54, * "estimate": null, * "due_date": null * } * @apiUse 400Error * @apiUse ValidationError * @apiUse UnauthorizedError * @apiUse ItemNotFoundError */ public function edit(EditTaskRequest $request): JsonResponse { Filter::listen( Filter::getRequestFilterName(), static function (array $requestData) { if (empty($requestData['due_date'])) { $requestData['due_date'] = null; } if (isset($requestData['estimate']) && $requestData['estimate'] <= 0) { $requestData['estimate'] = null; } if (!empty($requestData['priority_id'])) { return $requestData; } if (($project = Project::findOrFail($requestData['project_id'])) && !empty($project->default_priority_id)) { $requestData['priority_id'] = $project->default_priority_id; return $requestData; } if ($priority = Settings::scope('core')->get('default_priority_id')) { $requestData['priority_id'] = $priority; return $requestData; } $requestData['priority_id'] = Priority::firstOrFail()->id; return $requestData; } ); CatEvent::listen(Filter::getBeforeActionEventName(), static function (Task $task, array $requestData) { if (isset($requestData['start_date'])) { throw_if( $task->ancestors() ->where(fn(Builder $q) => $q ->where('due_date', '>', $requestData['start_date']) ->orWhere('start_date', '>', $requestData['start_date'])) ->exists(), new TaskRelationException(TaskRelationException::CANNOT_START_BEFORE_PARENT_ENDS) ); } elseif (isset($requestData['due_date'])) { throw_if( $task->ancestors() ->where(fn(Builder $q) => $q ->where('due_date', '>', $requestData['due_date']) ->orWhere('start_date', '>', $requestData['due_date'])) ->exists(), new TaskRelationException(TaskRelationException::CANNOT_START_BEFORE_PARENT_ENDS) ); } else { return; } $newDate = $requestData['due_date'] ?? $requestData['start_date']; $nearestChildStartDate = $task->descendants() ->where('start_date', '<', $newDate) ->orderBy('start_date') ->first()?->start_date; $nearestChildDueDate = $task->descendants() ->where('due_date', '<', $newDate) ->orderBy('due_date') ->first()?->due_date; if (is_null($nearestChildStartDate) && is_null($nearestChildDueDate)) { // no date overlap return; } if (is_null($nearestChildStartDate)) { $nearestChildDate = $nearestChildDueDate; } elseif (is_null($nearestChildDueDate)) { $nearestChildDate = $nearestChildStartDate; } else { $nearestChildDate = min($nearestChildStartDate, $nearestChildDueDate); } $delta = $nearestChildDate->diffInDays($newDate, false); dispatch(fn() => Task::withInitialQueryConstraint(function (Builder $query) use ($newDate) { $query->where(fn(Builder $q) => $q ->where('start_date', '<', $newDate) ->orWhere('due_date', '<', $newDate)); }, fn() => $task->descendants()->groupBy('id')->lazyById()->each( static function (Task $child) use ($delta) { $child->start_date = $child->start_date?->addDays($delta); $child->due_date = $child->due_date?->addDays($delta); $child->save(); } ))); }); $taskBeforeChanges = null; CatEvent::listen(Filter::getBeforeActionEventName(), static function (Task $task) use (&$taskBeforeChanges) { $taskBeforeChanges = $task->getOriginal(); $taskBeforeChanges['_old_phase_name'] = $task->phase?->name; $taskBeforeChanges['_old_users'] = $task->users()->select('id', 'full_name')->get()->map(fn($item)=>$item->full_name)->join(', '); }); CatEvent::listen(Filter::getAfterActionEventName(), static function (Task $data) use (&$taskBeforeChanges, $request) { $oldUsers = $taskBeforeChanges['_old_users']; $changes = $data->users()->sync($request->get('users')); if (!empty($changes['attached']) || !empty($changes['detached']) || !empty($changes['updated'])) { SaveTaskEditHistory::dispatch( $data, $request->user(), [ 'users' => User::withoutGlobalScopes() ->whereIn('id', $request->get('users')) ->select(['id', 'full_name'])->get()->map(fn($item)=>$item->full_name)->join(', ') ], [ 'users' => $oldUsers, ] ); } SaveTaskEditHistory::dispatch($data, request()->user(), null, $taskBeforeChanges); }); return $this->_edit($request); } /** * @param CreateTaskRequest $request * @return JsonResponse * * @throws Throwable * @api {post} /tasks/create Create * @apiDescription Create Task * * @apiVersion 4.0.0 * @apiName Create * @apiGroup Task * * @apiUse AuthHeader * * @apiPermission tasks_create * @apiPermission tasks_full_access * * @apiParam {String} start_date Start date of the task (in YYYY-MM-DD format). * @apiParam {String} due_date Due date of the task (in YYYY-MM-DD format). * @apiParam {Array} users List of user IDs assigned to the task. * @apiParam {Integer} [project_phase_id] ID of the project phase (if any). * @apiParam {Integer} project_id Project ID to which the task belongs. * @apiParam {String} task_name Name of the task. * @apiParam {String} description Description of the task in HTML format. * @apiParam {Boolean} important Indicates if the task is marked as important. * @apiParam {Integer} priority_id Priority level ID of the task. * @apiParam {Integer} status_id Status ID of the task. * @apiParamExample {json} Simple Request Example * { * "start_date": "2024-08-14", * "due_date": "2024-08-16", * "users": [ * 1, * 6, * 7 * ], * "project_phase_id": null, * "project_id": 2, * "task_name": "test", * "description": "test
", * "important": true, * "priority_id": 2, * "status_id": 5 * } * @apiSuccess {Integer} id Task ID. * @apiSuccess {Integer} project_id Project ID associated with the task. * @apiSuccess {Integer} [project_phase_id] ID of the project phase (if any). * @apiSuccess {String} task_name Name of the task. * @apiSuccess {String} description Description of the task. * @apiSuccess {Boolean} important Indicates whether the task is marked as important. * @apiSuccess {Integer} priority_id Priority level ID of the task. * @apiSuccess {Integer} status_id Status ID of the task. * @apiSuccess {String} start_date Start date of the task in ISO format. * @apiSuccess {String} due_date Due date of the task in ISO format. * @apiSuccess {String} created_at Task creation date in ISO format. * @apiSuccess {String} updated_at Task update date in ISO format. * * @apiSuccessExample {json} Success Response: * { * "project_id": 2, * "project_phase_id": null, * "task_name": "test", * "description": "test
", * "important": 1, * "priority_id": 2, * "status_id": 5, * "start_date": "2024-08-14T00:00:00.000000Z", * "due_date": "2024-08-16T00:00:00.000000Z", * "updated_at": "2024-08-16T09:07:47.000000Z", * "created_at": "2024-08-16T09:07:47.000000Z", * "id": 116 * } * * @apiUse 400Error * @apiUse ValidationError * @apiUse UnauthorizedError * @apiUse ForbiddenError */ public function create(CreateTaskRequest $request): JsonResponse { CatEvent::listen( Filter::getAfterActionEventName(), static fn (Task $task) => $task->users()->sync($request->get('users')) ); Filter::listen( Filter::getRequestFilterName(), static function (array $requestData) { if (empty($requestData['due_date'])) { $requestData['due_date'] = null; } if (isset($requestData['estimate']) && $requestData['estimate'] <= 0) { $requestData['estimate'] = null; } if (!empty($requestData['priority_id'])) { return $requestData; } if (($project = Project::findOrFail($requestData['project_id'])) && !empty($project->default_priority_id)) { $requestData['priority_id'] = $project->default_priority_id; return $requestData; } if ($priority = Settings::scope('core')->get('default_priority_id')) { $requestData['priority_id'] = $priority; return $requestData; } $requestData['priority_id'] = Priority::firstOrFail()->id; return $requestData; } ); Filter::listen(Filter::getRequestFilterName(), static function ($requestData) { $maxPosition = Task::max('relative_position'); $requestData['relative_position'] = $maxPosition + 1; return $requestData; }); return $this->_create($request); } /** * @throws Throwable * @api {post} /tasks/remove Destroy * @apiDescription Destroy Task * * @apiVersion 4.0.0 * @apiName Destroy * @apiGroup Task * * @apiUse AuthHeader * * @apiPermission tasks_remove * @apiPermission tasks_full_access * * @apiParam {Integer} id ID of the target task * * @apiParamExample {json} Request Example * { * "id": 54 * } * * @apiSuccess {String} message Destroy status * * @apiSuccessExample {json} Response Example * HTTP/1.1 204 No Content * * @apiUse 400Error * @apiUse ValidationError * @apiUse ForbiddenError * @apiUse UnauthorizedError */ public function destroy(DestroyTaskRequest $request): JsonResponse { return $this->_destroy($request); } /** * @throws Exception * @api {get,post} /tasks/count Count * @apiDescription Count Tasks * * @apiVersion 4.0.0 * @apiName Count * @apiGroup Task * * @apiUse AuthHeader * * @apiPermission tasks_count * @apiPermission tasks_full_access * * @apiSuccess {Integer} total Amount of tasks that we have * * @apiSuccessExample {json} Response Example * HTTP/1.1 200 OK * { * "total": 2 * } * * @apiUse 400Error * @apiUse ForbiddenError * @apiUse UnauthorizedError */ public function count(ListTaskRequest $request): JsonResponse { return $this->_count($request); } /** * @throws Throwable * @api {post} /tasks/show Show * @apiDescription Show Task * * @apiVersion 4.0.0 * @apiName Show * @apiGroup Task * * @apiUse AuthHeader * * @apiPermission tasks_show * @apiPermission tasks_full_access * * @apiParamExample {json} Request Example: * { * "id": "55", * "with": [ * "priority", * "project", * "phase:id,name", * "parents", * "children", * "users", * "status", * "changes", * "changes.user", * "comments", * "comments.user", * "workers", * "workers.user:id,full_name" * ], * "withSum": [ * ["workers as total_spent_time", "duration"], * ["workers as total_offset", "offset"] * ] * } * @apiSuccess {Integer} id The ID of the task. * @apiSuccess {Integer} project_id The ID of the project the task belongs to. * @apiSuccess {String} task_name The name of the task. * @apiSuccess {String} description The description of the task. * @apiSuccess {Boolean} important Indicates if the task is marked as important. * @apiSuccess {Object} priority Priority details including ID, name, and color. * @apiSuccess {Object} project Project details including ID, name, description, and more. * @apiSuccess {Array} users List of users assigned to the task. * @apiSuccess {Object} status Status details of the task. * @apiSuccess {Array} changes List of changes made to the task. * @apiSuccess {Array} comments List of comments related to the task. * @apiSuccess {Array} workers List of workers associated with the task. * @apiSuccess {String} total_spent_time Sum of the time spent by workers on the task. * @apiSuccess {String} total_offset Sum of the offset time for the task. * * @apiSuccessExample {json} Success Response: * { * "id": 55, * "project_id": 159, * "task_name": "Nisi qui ut et.", * "description": "Molestias libero deleniti laboriosam sit libero voluptas aut quibusdam...", * "important": 1, * "priority": { * "id": 3, * "name": "High", * "color": "#D40C0C" * }, * "project": { * "id": 159, * "name": "Voluptas ab et ea.", * "description": "Cum aut sunt in fuga quia..." * }, * "users": [ * { * "id": 7, * "full_name": "Dr. Adaline Toy", * "email": "projectManager1231@example.com" * } * ], * "status": { * "id": 2, * "name": "Closed", * "active": false * }, * "changes": [], * "comments": [], * "workers": [], * "total_spent_time": null, * "total_offset": null * } * * @apiUse 400Error * @apiUse UnauthorizedError * @apiUse ItemNotFoundError * @apiUse ForbiddenError * @apiUse ValidationError */ public function show(ShowTaskRequest $request): JsonResponse { Filter::listen(Filter::getQueryFilterName(), static function ($query) { return $query->withAvg('users as efficiency', 'efficiency'); }); CatEvent::listen(Filter::getAfterActionEventName(), static function (Task $task) { $task->mergeCasts(['forecast_completion_date' => 'date']); if ($task->start_date !== null && $task->estimate !== null && $task->efficiency !== null) { $task->forecast_completion_date = $task->start_date->addSeconds($task->estimate * $task->efficiency); } else { $task->forecast_completion_date = null; } }); return $this->_show($request); } /** * @throws Throwable * @api {post} /tasks/create-relation Create Relation * @apiDescription Creates a relation between two tasks, ensuring they belong to the same project and no cyclic dependencies exist. * * @apiVersion 4.0.0 * @apiName CreateRelation * @apiGroup Task * * * @apiUse AuthHeader * * @apiPermission tasks_create_relation * @apiPermission tasks_full_access * * @apiParam {Integer} task_id The ID of the task. * @apiParam {Integer} related_task_id The ID of the related task. * @apiParam {String="FOLLOWS","PRECEDES"} relation_type The type of the relation between the tasks. * * @apiParamExample {json} Request Example * { * "task_id": 1, * "related_task_id": 2, * "relation_type": "follows" * } * * * @apiSuccess {Number} id The ID of the related task. * @apiSuccess {Number} project_id The project ID of the related task. * @apiSuccess {Number} project_phase_id The project phase ID of the related task, if any. * @apiSuccess {String} task_name The name of the related task. * @apiSuccess {String} description The description of the related task. * @apiSuccess {Number} assigned_by The ID of the user who assigned the task. * @apiSuccess {String} url The URL of the related task, if any. * @apiSuccess {String} created_at The creation timestamp of the related task. * @apiSuccess {String} updated_at The last update timestamp of the related task. * @apiSuccess {String} deleted_at The deletion timestamp of the related task, if any. * @apiSuccess {Number} priority_id The priority ID of the related task. * @apiSuccess {Boolean} important Indicates if the task is marked as important. * @apiSuccess {String} start_date The start date of the related task, if any. * @apiSuccess {Number} project_milestone_id The project milestone ID of the related task, if any. * @apiSuccess {Number} status_id The status ID of the related task. * @apiSuccess {Number} relative_position The relative position of the related task. * @apiSuccess {String} estimate The estimate for the task, if any. * @apiSuccess {String} due_date The due date of the related task, if any. * @apiSuccess {Object} pivot The pivot data for the relation. * @apiSuccess {Number} pivot.child_id The child task ID. * @apiSuccess {Number} pivot.parent_id The parent task ID. * * @apiSuccessExample {json} Response Example * HTTP/1.1 200 OK * { * "id": 5, * "project_id": 1, * "project_phase_id": null, * "task_name": "Laudantium sapiente voluptas.", * "description": "Quam incidunt nemo", * "assigned_by": 2, * "url": null, * "created_at": "2023-10-26T10:26:27.000000Z", * "updated_at": "2024-06-16T05:13:13.000000Z", * "deleted_at": null, * "priority_id": 1, * "important": 1, * "start_date": null, * "project_milestone_id": null, * "status_id": 4, * "relative_position": 68.8125, * "estimate": null, * "due_date": null, * "pivot": { * "child_id": 1, * "parent_id": 5 * } * } * * @apiUse 400Error * @apiUse UnauthorizedError * @apiUse ItemNotFoundError * @apiUse ForbiddenError * @apiUse ValidationError */ public function createRelation(CreateRelationRequest $request): JsonResponse { $requestData = $request->validated(); $task = Task::find($requestData['task_id']); $relatedTask = Task::find($requestData['related_task_id']); $relationType = TaskRelationType::tryFrom($requestData['relation_type']); throw_if( $task->project_id !== $relatedTask->project_id, new TaskRelationException(TaskRelationException::NOT_SAME_PROJECT) ); throw_if( $task->children()->where('id', $relatedTask->id)->exists() || $task->parents()->where('id', $relatedTask->id)->exists(), new TaskRelationException(TaskRelationException::ALREADY_EXISTS) ); throw_if( match ($relationType) { TaskRelationType::FOLLOWS => $task->descendants()->where('id', $relatedTask->id)->exists(), TaskRelationType::PRECEDES => $task->ancestors()->where('id', $relatedTask->id)->exists() }, new TaskRelationException(TaskRelationException::CYCLIC) ); match ($relationType) { TaskRelationType::FOLLOWS => $task->parents()->attach($relatedTask), TaskRelationType::PRECEDES => $task->children()->attach($relatedTask), }; RegisterModulesEvents::broadcastEvent('tasks', 'edit', $task); RegisterModulesEvents::broadcastEvent('tasks', 'edit', $relatedTask); $relatedTask->pivot = match ($relationType) { // only for frontend update TaskRelationType::FOLLOWS => [ 'child_id' => $task->id, 'parent_id' => $relatedTask->id, ], TaskRelationType::PRECEDES => [ 'child_id' => $relatedTask->id, 'parent_id' => $task->id, ] }; // move dates after relation attached $parent = $relationType === TaskRelationType::FOLLOWS ? $relatedTask : $task; $child = $relationType === TaskRelationType::FOLLOWS ? $task : $relatedTask; $parentDateKey = $parent->due_date ? 'due_date' : ($parent->start_date ? 'start_date' : null); $childDateKey = $child->start_date ? 'start_date' : ($child->due_date ? 'due_date' : null); if (is_null($childDateKey) && $parentDateKey) { // no date on child - add any available date from parent $child->start_date = $parent->$parentDateKey; $child->due_date = $parent->$parentDateKey; $child->save(); RegisterModulesEvents::broadcastEvent('gantt', 'updateAll', $child->project); } elseif ($parentDateKey && $childDateKey && $child->$childDateKey->lt($parent->$parentDateKey)) { // child date is before parent date - move child and its descendants dates $delta = $child->$childDateKey->diffInDays($parent->$parentDateKey, false); dispatch(function () use ($delta, $child) { $child->descendantsAndSelf()->groupBy('id')->lazyById() ->each(static function (Task $child) use ($delta) { $child->start_date = $child->start_date?->addDays($delta); $child->due_date = $child->due_date?->addDays($delta); $child->save(); }); RegisterModulesEvents::broadcastEvent('gantt', 'updateAll', $child->project); }); } else { RegisterModulesEvents::broadcastEvent('gantt', 'updateAll', $child->project); } return responder()->success($relatedTask)->respond(); } /** * @throws Throwable * @api {post} /tasks/remove-relation Destroy Relation * @apiDescription Removes a relation between two tasks. * * @apiVersion 4.0.0 * @apiName DestroyRelation * @apiGroup Task * * @apiUse AuthHeader * * @apiPermission tasks_destroy_relation * @apiPermission tasks_full_access * * @apiParam {Integer} parent_id The ID of the parent task. * @apiParam {Integer} child_id The ID of the child task. * * @apiParamExample {json} Request Example * { * "parent_id": 1, * "child_id": 2 * } * * @apiSuccess {Boolean} success Indicates if the operation was successful. * @apiSuccess {Number} status The HTTP status code. * @apiSuccess {Null} data No content is returned. * * @apiError TaskRelationException The specified relation does not exist or another error occurred. * * @apiUse 400Error * @apiUse UnauthorizedError * @apiUse ItemNotFoundError * @apiUse ForbiddenError * @apiUse ValidationError */ public function destroyRelation(DestroyRelationRequest $request): JsonResponse { $requestData = $request->validated(); $parentTask = Task::find($requestData['parent_id']); $parentTask->children()->detach($requestData['child_id']); RegisterModulesEvents::broadcastEvent('gantt', 'updateAll', $parentTask->project); RegisterModulesEvents::broadcastEvent('tasks', 'edit', $parentTask); RegisterModulesEvents::broadcastEvent('tasks', 'edit', Task::find($requestData['child_id'])); return responder()->success()->respond(204); } /** * @throws BindingResolutionException * @api {get} /offline-sync/download-projects-and-tasks/{user} Download Projects and Tasks * @apiDescription Downloads all projects and tasks associated with a specific user. * * @apiVersion 4.0.0 * @apiName DownloadProjectsAndTasks * @apiGroup User * * @apiUse AuthHeader * * @apiPermission users_download_projects_tasks * @apiPermission users_full_access * * @apiParam {Integer} user The ID of the user. * * @apiSuccess {Boolean} success Indicates if the operation was successful. * @apiSuccess {Number} status The HTTP status code. * @apiSuccess {File} data A binary file containing the packed projects and tasks data. * @apiSuccess {String} Content-type The content type of the file, which is `application/octet-stream`. * @apiSuccess {String} Content-Disposition The content disposition of the file, indicating an attachment with the filename `ProjectsAndTasks.cattr`. * * * @apiError UserNotFound The user with the specified ID was not found. * * @apiUse 400Error * @apiUse UnauthorizedError * @apiUse ItemNotFoundError * @apiUse ForbiddenError */ public function downloadProjectsAndTasks(User $user): Response { $projectsAndTasks = collect($user->load([ 'projects' => fn(BelongsToMany $q) => $q->select([ 'projects.id', 'projects.name', 'projects.description', 'projects.source', 'projects.updated_at' ])->withoutGlobalScopes(), 'tasks' => fn(BelongsToMany $q) => $q->select([ 'tasks.id', 'tasks.project_id', 'tasks.task_name', 'tasks.description', 'tasks.url', 'tasks.priority_id', 'tasks.status_id', 'tasks.updated_at' ])->withoutGlobalScopes(), ]))->only(['id', 'projects', 'tasks'])->all(); foreach ($projectsAndTasks['projects'] as $project) { unset($project['pivot']); } foreach ($projectsAndTasks['tasks'] as $task) { unset($task['pivot']); } $packed = MessagePack::pack($projectsAndTasks); return response()->make($packed, 200, [ 'Content-type: application/octet-stream', 'Content-Disposition: attachment; filename=ProjectsAndTasks.cattr' ]); } protected const ISO8601_DATE_FORMAT = 'Y-m-d'; /** * @param CalendarRequest $request * @return JsonResponse * * @throws Throwable * @api {get} /tasks/calendar Calendar * @apiDescription Get calendar report data * * @apiVersion 4.0.0 * @apiName Calendar * @apiGroup Task * * @apiUse AuthHeader * * @apiParam {Integer|Integer[]} project_id Filter by project ids * @apiParam {ISO8601} start_at Start date * @apiParam {ISO8601} end_at End date * * @apiParamExample {json} Request Example * { * "project_id": 1, * "start_at": "2024-10-01", * "end_at": "2024-10-31" * } * * @apiSuccessExample {json} Response Example * HTTP/1.1 200 OK * { * "status": 200, * "success": true, * "data": { * "tasks": { * "1": { * "id": 1, * "task_name": "Eveniet non laudantium pariatur quia.", * "project_id": 1, * "estimate": null, * "total_spent_time": 0, * "start_date": "2024-10-03", * "due_date": "2024-10-03" * } * }, * "tasks_by_day": [ * { * "date": "2024-10-03", * "month": 10, * "day": 3, * "task_ids": [1] * } * ], * "tasks_by_week": [ * { * "days": [ * { "day": 30, "month": 9 }, * { "day": 1, "month": 10 }, * { "day": 2, "month": 10 }, * { "day": 3, "month": 10 }, * { "day": 4, "month": 10 }, * { "day": 5, "month": 10 }, * { "day": 6, "month": 10 } * ], * "tasks": [ * { * "task_id": 1, * "start_week_day": 3, * "end_week_day": 3 * } * ] * } * ] * } * } * * @apiUse 400Error * @apiUse ValidationError * @apiUse UnauthorizedError * @apiUse ForbiddenError */ public function calendar(CalendarRequest $request): JsonResponse { $requestData = $request->validated(); $startAt = Carbon::parse($requestData['start_at'])->startOfWeek(); $endAt = Carbon::parse($requestData['end_at'])->endOfWeek(); /** @var Builder $query */ $query = Task::query() ->select( 'id', 'task_name', 'project_id', 'status_id', 'priority_id', 'estimate', DB::raw('COALESCE(start_date, due_date) AS start_date'), DB::raw('COALESCE(due_date, start_date) AS due_date'), ) ->with('status') ->with('priority') ->withAvg('users as efficiency', 'efficiency') ->withSum('workers as total_spent_time', 'duration') ->where(static fn(Builder $query) => $query ->whereNotNull('start_date') ->orWhereNotNull('due_date')) ->where(static fn(Builder $query) => $query ->whereBetween('start_date', [$startAt, $endAt]) ->orWhereBetween('due_date', [$startAt, $endAt]) ->orWhereBetween(DB::raw(DB::escape($startAt->format(static::ISO8601_DATE_FORMAT))), [DB::raw('start_date'), DB::raw('due_date')]) ->orWhereBetween(DB::raw(DB::escape($endAt->format(static::ISO8601_DATE_FORMAT))), [DB::raw('start_date'), DB::raw('due_date')])) ->orderBy('start_date') ->orderBy('id'); if (isset($requestData['project_id'])) { if (is_array($requestData['project_id'])) { $query->whereIn('project_id', array_values($requestData['project_id'])); } else { $query->where('project_id', (int)$requestData['project_id']); } } /** @var \Illuminate\Support\Collection