You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

295 lines
12 KiB

  1. <?php
  2. namespace App\Http\Controllers;
  3. use Carbon\Carbon;
  4. use App\Models\Task;
  5. use App\Models\Work;
  6. use App\Models\TagTask;
  7. use App\Rules\MaxBound;
  8. use Illuminate\Http\Request;
  9. use Illuminate\Http\Response;
  10. use Illuminate\Support\Facades\DB;
  11. use App\Http\Resources\TaskResource;
  12. use Spatie\QueryBuilder\QueryBuilder;
  13. use App\Http\Resources\TaskCollection;
  14. use Spatie\QueryBuilder\AllowedFilter;
  15. class TaskController extends Controller
  16. {
  17. public function index($business, Request $request)
  18. {
  19. permit('businessAccess');
  20. $per_page = $request->limit > 100 ? 10 : $request->limit;
  21. $this->indexValidation($request);
  22. $tasks = $this->indexFiltering($business)
  23. ->when($request->filled('group'), function ($q) use ($request) {
  24. return $q->report($request->group);
  25. });
  26. return $request->filled('group') ?
  27. $tasks->get()->groupBy($request->group)
  28. ->map(function ($q) { return $q->keyBy('status_id'); })
  29. : new TaskCollection($tasks->paginate($per_page));
  30. }
  31. public function indexValidation($request)
  32. {
  33. $bound = 10;
  34. $this->validate($request, [
  35. 'filter.project_id' => [new MaxBound($bound)] ,
  36. 'filter.creator_id' => [new MaxBound($bound)] ,
  37. 'filter.assignee_id' => [new MaxBound($bound)] ,
  38. 'filter.system_id' => [new MaxBound($bound)] ,
  39. 'filter.workflow_id' => [new MaxBound($bound)] ,
  40. 'filter.status_id' => [new MaxBound($bound)] ,
  41. 'filter.approver_id' => [new MaxBound($bound)] ,
  42. 'filter.priority_min' => 'nullable|numeric|between:1,10' ,
  43. 'filter.priority_max' => 'nullable|numeric|between:1,10' ,
  44. 'filter.ready_to_test' => 'nullable|boolean' ,
  45. 'filter.on_time' => 'nullable|boolean' ,
  46. ]);
  47. }
  48. public function indexFiltering($business)
  49. {
  50. $query = Task::where('business_id', $business);
  51. $taskQ = QueryBuilder::for($query)
  52. // ->with('tags')
  53. ->select(DB::raw('tasks.* , (spent_time - estimated_time) as over_spent'))
  54. ->allowedFilters([
  55. AllowedFilter::exact('project_id'),
  56. AllowedFilter::exact('system_id'),
  57. AllowedFilter::exact('creator_id'),
  58. AllowedFilter::exact('assignee_id'),
  59. AllowedFilter::exact('approver_id'),
  60. AllowedFilter::exact('sprint_id'),
  61. AllowedFilter::exact('workflow_id'),
  62. AllowedFilter::exact('status_id'),
  63. AllowedFilter::exact('on_time'),
  64. AllowedFilter::exact('ready_to_test'),
  65. AllowedFilter::exact('tagTask.tag_id'),
  66. AllowedFilter::scope('priority_min'),
  67. AllowedFilter::scope('priority_max'),
  68. AllowedFilter::scope('creates_before'),
  69. AllowedFilter::scope('creates_after'),
  70. AllowedFilter::scope('creates_in'),
  71. AllowedFilter::scope('updates_before'),
  72. AllowedFilter::scope('updates_after'),
  73. AllowedFilter::scope('updates_in'),
  74. AllowedFilter::scope('spent_from'),
  75. AllowedFilter::scope('spent_to'),
  76. AllowedFilter::scope('estimated_from'),
  77. AllowedFilter::scope('estimated_to'),
  78. AllowedFilter::scope('starts_before'),
  79. AllowedFilter::scope('starts_after'),
  80. AllowedFilter::scope('starts_in'),
  81. AllowedFilter::scope('finish_before'),
  82. AllowedFilter::scope('finish_after'),
  83. AllowedFilter::scope('finish_in'),
  84. AllowedFilter::scope('completes_before'),
  85. AllowedFilter::scope('completes_after'),
  86. AllowedFilter::scope('completes_in'),
  87. AllowedFilter::scope('over_spent_from'),
  88. AllowedFilter::scope('over_spent_to'),
  89. AllowedFilter::scope('due_date_before'),
  90. AllowedFilter::scope('due_date_after'),
  91. AllowedFilter::scope('due_date_in'),
  92. AllowedFilter::scope('my_watching'),
  93. AllowedFilter::scope('over_due'),
  94. ]);
  95. if (\request('_business_info')['info']['users'][\auth()->id()]['level'] != enum('levels.owner.id')) {
  96. $requested_projects = isset(\request('filter')['project_id']) ?
  97. array_unique(explode(',',\request('filter')['project_id'] ?? null )) :
  98. null;
  99. $requested_projects = collect($requested_projects)->keyBy(null)->toArray();
  100. $project_ids = $this->myStateProjects($requested_projects);
  101. $taskQ->where(function ($q) use ($project_ids) {
  102. $q->whereIn('project_id', $project_ids['non_guest_ids'])
  103. ->orWhere(function ($q) use ($project_ids) {
  104. $q->whereIn('project_id', $project_ids['guest_ids'])
  105. ->where('assignee_id', auth()->id());
  106. });
  107. });
  108. }
  109. return $taskQ;
  110. }
  111. public function myStateProjects($requested_projects)
  112. {
  113. $non_guest_ids = [];
  114. $guest_ids = [];
  115. $is_empty = empty($requested_projects);
  116. foreach (\request('_business_info')['info']['projects'] as $p_id => $p) {
  117. $level = \request('_business_info')['info']['projects'][$p_id]['members'][\auth()->id()]['level'];
  118. if (( $is_empty || isset($requested_projects[$p_id]))
  119. && $level > enum('levels.guest.id')) {
  120. array_push($non_guest_ids, $p_id);
  121. }
  122. if (( $is_empty || isset($requested_projects[$p_id]))
  123. && $level == enum('levels.guest.id')) {
  124. array_push($guest_ids, $p_id);
  125. }
  126. }
  127. return ['non_guest_ids' => $non_guest_ids, 'guest_ids' => $guest_ids];
  128. }
  129. public function store($business, $project, Request $request)
  130. {
  131. permit('projectTasks', ['project_id' => $project]);
  132. $task = Task::create($request->merge(
  133. ['business_id' => $business, 'project_id' => $project, 'creator_id' => \auth()->id()]
  134. )->except(['_business_info', 'completed_at', 'on_time', 'ready_to_test']));
  135. return new TaskResource($task);
  136. }
  137. public function storeTagTasks($tags, $task) {
  138. $tagModels = [];
  139. if (isset($tags)) {
  140. foreach ($tags as $tag) {
  141. array_push($tagModels,
  142. new TagTask(['tag_id' => $tag, 'task_id' => $task->id])
  143. );
  144. }
  145. $task->tags()->saveMany($tagModels);
  146. }
  147. }
  148. /**
  149. * Rule's:
  150. * 1) guest's only can see self task
  151. * 2) user is active in project
  152. */
  153. public function show($business, $project, $task)
  154. {
  155. $task = Task::findOrFail($task);
  156. $project = $task->project_id;
  157. permit('projectAccess', ['project_id' => $project]);
  158. if (can('isDefiniteGuestInProject', ['project_id' => $project])){ // is guest in project (only guest)
  159. return $task->assignee_id == \auth()->id() ?
  160. new TaskResource($task) :
  161. abort(Response::HTTP_METHOD_NOT_ALLOWED); // not allowed
  162. } else {
  163. return new TaskResource($task);
  164. }
  165. }
  166. /**
  167. * Rule's:
  168. * 1) update assignee_id when not exist work time and active user
  169. * 2) update approver_id when atLeast colleague
  170. * 3) update ready_to_test when assignee_id == auth xor assignee_id == null and isAdmin
  171. * 4) update tags
  172. * 5) update completed_at when status in [done, close]
  173. * 6) due_date before sprint end_time
  174. */
  175. public function update($business, $project, $task, Request $request)
  176. {
  177. permit('isProjectGuest', ['project_id' => $project]);
  178. $taskModel = Task::where([['project_id', $project ], ['id', $task]])->firstOrFail();
  179. if ($taskModel->assignee_id != \auth()->id() && can('isDefiniteGuestInProject', ['project_id' => $project])) {
  180. permit('isDefiniteGuestInProject', ['project_id' => $project]);
  181. }
  182. if($request->filled('watchers')) {
  183. $watchers = $taskModel->watchers ?? [];
  184. if(!can('isDefiniteGuestInProject', ['project_id' => $project]) && !can('projectTasks', ['project_id' => $project])) {
  185. if (array_search(auth()->id(), $watchers) !== false) {
  186. // remove auth from watchers
  187. $watchers = array_values(\array_diff($watchers, [auth()->id()]));
  188. } else {
  189. // add auth to watchers
  190. $watchers = array_merge($watchers, [auth()->id()]);
  191. }
  192. }
  193. if(can('projectTasks', ['project_id' => $project])) {
  194. $watchers = array_intersect($watchers ?? [], $request->watchers);
  195. }
  196. $request->merge(['watchers' => $watchers]);
  197. }
  198. if (!can('projectTasks', ['project_id' => $project])) {
  199. $request->replace($request->only(['_business_info', 'ready_to_test', 'status_id', 'watchers']));
  200. }
  201. if ($taskModel->assignee_id != \auth()->id()) {
  202. $request->request->remove('ready_to_test');
  203. }
  204. if (isset(\request('_business_info')['workflows'][$request->workflow_id]['statuses'][$request->status_id]['state'])) {
  205. $state = \request('_business_info')['workflows'][$request->workflow_id]['statuses'][$request->status_id]['state'];
  206. if ($state == enum('status.states.close.id') || $state == enum('status.states.done.id')) {
  207. //ToDo: is needed to check before state is done or close?
  208. $request->merge([
  209. 'completed_at' => Carbon::now(),
  210. 'work_finish' => Work::where('task_id', $task)->latest()->first()->ended_at ?? null
  211. ]);
  212. if (isset($taskModel->due_date) && $taskModel->due_date < date('yy-m-d')) {
  213. //check if before set due date and miss, we change on time flag
  214. $request->merge(['on_time' => false]);
  215. }
  216. }
  217. }
  218. if (!$request->has('tags')) {
  219. $request->merge(['tags' => []]);
  220. }
  221. $taskModel->update($request->except('_business_info'));
  222. return new TaskResource($taskModel);
  223. }
  224. public function updateReadyToTest($business, $project, $task)
  225. {
  226. permit('isProjectGuest', ['project_id' => $project]);
  227. $task = Task::where([['project_id', $project ], ['id', $task]])->firstOrFail();
  228. if ($task->assignee_id == \auth()->id()) {
  229. $task->update([
  230. 'ready_to_test' => 1
  231. ]);
  232. } else {
  233. return abort(Response::HTTP_FORBIDDEN); // not allowed
  234. }
  235. return $task->load(['tagTask'=> fn($q) => $q->select('id', 'tag_id', 'task_id'), 'works', 'comments']);
  236. }
  237. /**
  238. * Rule's:
  239. * 1) delete only not work time (soft delete)
  240. */
  241. public function destroy($task)
  242. {
  243. $taskModel = Task::findOrFail($task);
  244. if (Work::where('task_id', $task)->exists()) {
  245. return \response()->json(['task_id' => 'The task id cannot be deleted.'], Response::HTTP_UNPROCESSABLE_ENTITY);
  246. }
  247. $taskModel->delete();
  248. return \response()->json(['message' => 'task deleted successfully.'], Response::HTTP_OK);
  249. }
  250. public function toggleWatcher($business, $task, $project =null)
  251. {
  252. permit('isProjectGuest', ['project_id' => $project]);
  253. $taskModel = Task::findOrFail($task);
  254. $watchers = $taskModel->watchers ?? [];
  255. if (array_search(auth()->id(), $watchers) !== false) {
  256. // remove auth from watchers
  257. $new_watchers = array_values(\array_diff($watchers, [auth()->id()]));
  258. } else {
  259. // add auth to watchers
  260. $new_watchers = array_merge($watchers, [auth()->id()]);
  261. }
  262. $taskModel->update([
  263. 'watchers' => $new_watchers
  264. ]);
  265. return new TaskResource($taskModel);
  266. }
  267. }