mahdihty
4 years ago
15 changed files with 1483 additions and 3 deletions
-
90app/Http/Controllers/CommentController.php
-
112app/Http/Controllers/StatisticController.php
-
295app/Http/Controllers/TaskController.php
-
242app/Http/Controllers/WorkController.php
-
34app/Http/Resources/CommentResource.php
-
23app/Http/Resources/TaskCollection.php
-
30app/Http/Resources/TaskResource.php
-
19app/Models/Comment.php
-
308app/Models/Task.php
-
126app/Models/Work.php
-
3composer.json
-
74composer.lock
-
55database/migrations/2021_03_02_082607_create_tasks_table.php
-
39database/migrations/2021_03_02_085913_create_works_table.php
-
36database/migrations/2021_03_02_092444_create_comments_table.php
@ -0,0 +1,90 @@ |
|||
<?php |
|||
|
|||
namespace App\Http\Controllers; |
|||
|
|||
use App\Models\Comment; |
|||
use App\Models\Task; |
|||
use Illuminate\Http\Request; |
|||
use Illuminate\Http\Response; |
|||
|
|||
class CommentController extends Controller |
|||
{ |
|||
public function index($business, $project, $task) |
|||
{ |
|||
permit('projectAccess', ['project_id' => $project]); |
|||
$taskModel = Task::where([['project_id', $project ], ['id', $task]])->firstOrFail(); |
|||
if (can('isDefiniteGuestInProject', ['project_id' => $project])){ // is guest in project (only guest)
|
|||
return $taskModel->assignee_id == \auth()->id() ? |
|||
Comment::where([ |
|||
['business_id', $business], |
|||
['project_id', $project], |
|||
['task_id', $task], |
|||
])->get(): |
|||
abort(Response::HTTP_FORBIDDEN); // not allowed
|
|||
} else { |
|||
return Comment::where([ |
|||
['business_id', $business], |
|||
['project_id', $project], |
|||
['task_id', $task], |
|||
])->get(); |
|||
} |
|||
} |
|||
|
|||
public function store($business, $project, $task, Request $request) |
|||
{ |
|||
permit('projectAccess', ['project_id' => $project]); |
|||
$taskModel = Task::where([['project_id', $project ], ['id', $task]])->firstOrFail(); |
|||
if (can('isDefiniteGuestInProject', ['project_id' => $project])){ // is guest in project (only guest)
|
|||
return $taskModel->assignee_id == \auth()->id() ? |
|||
Comment::create($request->merge([ |
|||
'business_id' => $business, |
|||
'project_id' => $project, |
|||
'task_id' => $task, |
|||
'user_id' => \auth()->id(), |
|||
])->except('_business_info')) : |
|||
abort(Response::HTTP_FORBIDDEN); // not allowed
|
|||
} else { |
|||
return Comment::create($request->merge([ |
|||
'business_id' => $business, |
|||
'project_id' => $project, |
|||
'task_id' => $task, |
|||
'user_id' => \auth()->id(), |
|||
])->except('_business_info')); |
|||
} |
|||
} |
|||
|
|||
public function show($business, $project, $task, $comment) |
|||
{ |
|||
permit('projectAccess', ['project_id' => $project]); |
|||
$taskModel = Task::where([['project_id', $project ], ['id', $task]])->firstOrFail(); |
|||
if (can('isDefiniteGuestInProject', ['project_id' => $project])){ // is guest in project (only guest)
|
|||
return $taskModel->assignee_id == \auth()->id() ? |
|||
Comment::findOrFail($comment) : |
|||
abort(Response::HTTP_FORBIDDEN); // not allowed
|
|||
} else { |
|||
return Comment::findOrFail($comment); |
|||
} |
|||
} |
|||
|
|||
public function update($business, $project, $task, $comment, Request $request) |
|||
{ |
|||
permit('projectAccess', ['project_id' => $project]); |
|||
$comment = Comment::findOrFail($comment); |
|||
if ($comment->user_id == \auth()->id()) { |
|||
$comment->update($request->except('_business_info')); |
|||
return $comment; |
|||
} |
|||
return abort(Response::HTTP_FORBIDDEN); // not allowed
|
|||
} |
|||
|
|||
public function destroy($business, $project, $task, $comment) |
|||
{ |
|||
permit('projectAccess', ['project_id' => $project]); |
|||
$comment = Comment::findOrFail($comment); |
|||
if ($comment->user_id == \auth()->id()) { |
|||
$comment->delete(); |
|||
return \response()->json(['message' => 'comment deleted successfully.'], Response::HTTP_OK); |
|||
} |
|||
return abort(Response::HTTP_FORBIDDEN); // not allowed
|
|||
} |
|||
} |
@ -0,0 +1,112 @@ |
|||
<?php |
|||
|
|||
namespace App\Http\Controllers; |
|||
|
|||
use Illuminate\Http\Request; |
|||
use Illuminate\Support\Arr; |
|||
use Illuminate\Support\Facades\DB; |
|||
|
|||
class StatisticController extends Controller |
|||
{ |
|||
public $map = [ |
|||
'test' => 0, // The sum of all tasks that are under review
|
|||
'total' => 0, // Sum of all tasks
|
|||
'overdue' => 0, // The sum of all overworked tasks
|
|||
'spent_time' => 0, // The sum of all the minutes spent on tasks
|
|||
'estimated_time' => 0, // The sum of all the minutes spent performing tasks is estimated
|
|||
'over_spent_time' => 0, // The sum of all the minutes spent overworking tasks
|
|||
'under_spent_time' => 0, // The sum of all the minutes left until estimated time
|
|||
]; |
|||
|
|||
public function index(Request $request, int $business, ?int $project = null) |
|||
{ |
|||
$builder = DB::table('tasks')->where('business_id','=',$business); |
|||
if ($project) { |
|||
$builder->where('project_id', '=', $project); |
|||
} |
|||
$tasks = $builder->get(); |
|||
|
|||
$tags = DB::table('tag_task') |
|||
->whereIn('task_id', $tasks->pluck('id')->toArray()) |
|||
->get() |
|||
->groupBy('task_id') |
|||
->toArray(); |
|||
|
|||
|
|||
$result = []; |
|||
|
|||
foreach ($tasks as $task) { |
|||
|
|||
$this->addProjects($result, $task); |
|||
|
|||
$this->addSprints($result, $task); |
|||
|
|||
$this->addSystems($result, $task); |
|||
|
|||
$this->addTags($result, $task, $tags[$task->id]); |
|||
} |
|||
|
|||
return $result; |
|||
} |
|||
|
|||
|
|||
public function addProjects(&$result, $task) |
|||
{ |
|||
$key = "projects.{$task->project_id}"; |
|||
|
|||
$this->subset($key, $result, $task); |
|||
} |
|||
|
|||
public function addSprints(&$result, $task) |
|||
{ |
|||
$key = "projects.{$task->project_id}."; |
|||
$key .= 'sprints.'; |
|||
$key .= ($task->sprint_id ?? 0) . '.'; |
|||
$key .= ($task->assignee_id ?? 0) . '.'; |
|||
$key .= $task->workflow_id . '.'; |
|||
$key .= $task->status_id; |
|||
|
|||
$this->subset($key, $result, $task); |
|||
} |
|||
|
|||
public function addSystems(&$result, $task) |
|||
{ |
|||
$key = "projects.{$task->project_id}."; |
|||
$key .= 'systems.'; |
|||
$key .= ($task->system_id ?? 0) . '.'; |
|||
$key .= ($task->assignee_id ?? 0) . '.'; |
|||
$key .= $task->workflow_id . '.'; |
|||
$key .= $task->status_id; |
|||
|
|||
$this->subset($key, $result, $task); |
|||
} |
|||
|
|||
public function addTags(&$result, $task, $tags) |
|||
{ |
|||
foreach ($tags as $tag) { |
|||
$key = "projects.{$task->project_id}."; |
|||
$key .= 'tags.'; |
|||
$key .= $tag->id . '.'; |
|||
$key .= ($task->assignee_id ?? 0) . '.'; |
|||
$key .= $task->workflow_id . '.'; |
|||
$key .= $task->status_id; |
|||
|
|||
$this->subset($key, $result, $task); |
|||
} |
|||
} |
|||
|
|||
public function subset($key , &$result, $task) |
|||
{ |
|||
$node = Arr::get($result, $key, $this->map); |
|||
|
|||
$node['test'] += $task->ready_to_test; |
|||
$node['total'] += 1; |
|||
$node['overdue'] += !$task->on_time; |
|||
$node['spent_time'] += $task->spent_time; |
|||
$node['estimated_time'] += $task->estimated_time; |
|||
$node['over_spent_time'] += $task->estimated_time < $task->spent_time ? $task->spent_time - $task->estimated_time : 0; |
|||
$node['under_spent_time'] += $task->estimated_time > $task->spent_time ? $task->estimated_time - $task->spent_time : 0; |
|||
|
|||
Arr::set($result, $key, $node); |
|||
} |
|||
} |
@ -0,0 +1,295 @@ |
|||
<?php |
|||
|
|||
namespace App\Http\Controllers; |
|||
|
|||
use App\Http\Resources\TaskCollection; |
|||
use App\Http\Resources\TaskResource; |
|||
use App\Rules\maxBound; |
|||
use App\TagTask; |
|||
use App\Models\Task; |
|||
use App\Models\Work; |
|||
use Carbon\Carbon; |
|||
use Illuminate\Http\Request; |
|||
use Illuminate\Http\Response; |
|||
use Illuminate\Support\Facades\DB; |
|||
use Spatie\QueryBuilder\AllowedFilter; |
|||
use Spatie\QueryBuilder\QueryBuilder; |
|||
|
|||
class TaskController extends Controller |
|||
{ |
|||
public function index($business, Request $request) |
|||
{ |
|||
permit('businessAccess'); |
|||
$per_page = $request->limit > 100 ? 10 : $request->limit; |
|||
$this->indexValidation($request); |
|||
$tasks = $this->indexFiltering($business) |
|||
->when($request->filled('group'), function ($q) use ($request) { |
|||
return $q->report($request->group); |
|||
}); |
|||
|
|||
return $request->filled('group') ? |
|||
$tasks->get()->groupBy($request->group) |
|||
->map(function ($q) { return $q->keyBy('status_id'); }) |
|||
: /*response()->json(collect(['now' => Carbon::now()->toDateString()])->merge(*/new TaskCollection($tasks->paginate($per_page));//));
|
|||
} |
|||
|
|||
public function indexValidation($request) |
|||
{ |
|||
$bound = 10; |
|||
$this->validate($request, [ |
|||
'filter.project_id' => [new maxBound($bound)] , |
|||
'filter.creator_id' => [new maxBound($bound)] , |
|||
'filter.assignee_id' => [new maxBound($bound)] , |
|||
'filter.system_id' => [new maxBound($bound)] , |
|||
'filter.workflow_id' => [new maxBound($bound)] , |
|||
'filter.status_id' => [new maxBound($bound)] , |
|||
'filter.approver_id' => [new maxBound($bound)] , |
|||
'filter.priority_min' => 'nullable|numeric|between:1,10' , |
|||
'filter.priority_max' => 'nullable|numeric|between:1,10' , |
|||
'filter.ready_to_test' => 'nullable|boolean' , |
|||
'filter.on_time' => 'nullable|boolean' , |
|||
]); |
|||
} |
|||
|
|||
public function indexFiltering($business) |
|||
{ |
|||
$query = Task::where('business_id', $business); |
|||
$taskQ = QueryBuilder::for($query) |
|||
->with('tagTask') |
|||
->select(DB::raw('tasks.* , (spent_time - estimated_time) as over_spent')) |
|||
->allowedFilters([ |
|||
AllowedFilter::exact('project_id'), |
|||
AllowedFilter::exact('system_id'), |
|||
AllowedFilter::exact('creator_id'), |
|||
AllowedFilter::exact('assignee_id'), |
|||
AllowedFilter::exact('approver_id'), |
|||
AllowedFilter::exact('sprint_id'), |
|||
AllowedFilter::exact('workflow_id'), |
|||
AllowedFilter::exact('status_id'), |
|||
AllowedFilter::exact('on_time'), |
|||
AllowedFilter::exact('ready_to_test'), |
|||
AllowedFilter::exact('tagTask.tag_id'), |
|||
AllowedFilter::scope('priority_min'), |
|||
AllowedFilter::scope('priority_max'), |
|||
AllowedFilter::scope('creates_before'), |
|||
AllowedFilter::scope('creates_after'), |
|||
AllowedFilter::scope('creates_in'), |
|||
AllowedFilter::scope('updates_before'), |
|||
AllowedFilter::scope('updates_after'), |
|||
AllowedFilter::scope('updates_in'), |
|||
AllowedFilter::scope('spent_from'), |
|||
AllowedFilter::scope('spent_to'), |
|||
AllowedFilter::scope('estimated_from'), |
|||
AllowedFilter::scope('estimated_to'), |
|||
AllowedFilter::scope('starts_before'), |
|||
AllowedFilter::scope('starts_after'), |
|||
AllowedFilter::scope('starts_in'), |
|||
AllowedFilter::scope('finish_before'), |
|||
AllowedFilter::scope('finish_after'), |
|||
AllowedFilter::scope('finish_in'), |
|||
AllowedFilter::scope('completes_before'), |
|||
AllowedFilter::scope('completes_after'), |
|||
AllowedFilter::scope('completes_in'), |
|||
AllowedFilter::scope('over_spent_from'), |
|||
AllowedFilter::scope('over_spent_to'), |
|||
AllowedFilter::scope('due_date_before'), |
|||
AllowedFilter::scope('due_date_after'), |
|||
AllowedFilter::scope('due_date_in'), |
|||
AllowedFilter::scope('my_watching'), |
|||
AllowedFilter::scope('over_due'), |
|||
]); |
|||
if (\request('_business_info')['info']['users'][\auth()->id()]['level'] != enum('levels.owner.id')) { |
|||
$requested_projects = isset(\request('filter')['project_id']) ? |
|||
array_unique(explode(',',\request('filter')['project_id'] ?? null )) : |
|||
null; |
|||
$requested_projects = collect($requested_projects)->keyBy(null)->toArray(); |
|||
$project_ids = $this->myStateProjects($requested_projects); |
|||
$taskQ->where(function ($q) use ($project_ids) { |
|||
$q->whereIn('project_id', $project_ids['non_guest_ids']) |
|||
->orWhere(function ($q) use ($project_ids) { |
|||
$q->whereIn('project_id', $project_ids['guest_ids']) |
|||
->where('assignee_id', auth()->id()); |
|||
}); |
|||
}); |
|||
} |
|||
return $taskQ; |
|||
} |
|||
|
|||
public function myStateProjects($requested_projects) |
|||
{ |
|||
$non_guest_ids = []; |
|||
$guest_ids = []; |
|||
$is_empty = empty($requested_projects); |
|||
|
|||
foreach (\request('_business_info')['info']['projects'] as $p_id => $p) { |
|||
|
|||
$level = \request('_business_info')['info']['projects'][$p_id]['members'][\auth()->id()]['level']; |
|||
|
|||
if (( $is_empty || isset($requested_projects[$p_id])) |
|||
&& $level > enum('levels.guest.id')) { |
|||
array_push($non_guest_ids, $p_id); |
|||
} |
|||
if (( $is_empty || isset($requested_projects[$p_id])) |
|||
&& $level == enum('levels.guest.id')) { |
|||
array_push($guest_ids, $p_id); |
|||
} |
|||
} |
|||
|
|||
return ['non_guest_ids' => $non_guest_ids, 'guest_ids' => $guest_ids]; |
|||
} |
|||
|
|||
public function store($business, $project, Request $request) |
|||
{ |
|||
permit('projectTasks', ['project_id' => $project]); |
|||
|
|||
$task = Task::create($request->merge( |
|||
['business_id' => $business, 'project_id' => $project, 'creator_id' => \auth()->id()] |
|||
)->except(['_business_info', 'completed_at', 'on_time', 'ready_to_test'])); |
|||
|
|||
|
|||
return new TaskResource($task); |
|||
} |
|||
|
|||
public function storeTagTasks($tags, $task) { |
|||
$tagModels = []; |
|||
if (isset($tags)) { |
|||
foreach ($tags as $tag) { |
|||
array_push($tagModels, |
|||
new TagTask(['tag_id' => $tag, 'task_id' => $task->id]) |
|||
); |
|||
} |
|||
$task->tags()->saveMany($tagModels); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Rule's: |
|||
* 1) guest's only can see self task |
|||
* 2) user is active in project |
|||
*/ |
|||
public function show($business, $task, $project =null) |
|||
{ |
|||
$task = Task::findOrFail($task); |
|||
$project = $task->project_id; |
|||
permit('projectAccess', ['project_id' => $project]); |
|||
if (can('isDefiniteGuestInProject', ['project_id' => $project])){ // is guest in project (only guest)
|
|||
return $task->assignee_id == \auth()->id() ? |
|||
new TaskResource($task) : |
|||
abort(Response::HTTP_METHOD_NOT_ALLOWED); // not allowed
|
|||
} else { |
|||
return new TaskResource($task); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Rule's: |
|||
* 1) update assignee_id when not exist work time and active user |
|||
* 2) update approver_id when atLeast colleague |
|||
* 3) update ready_to_test when assignee_id == auth xor assignee_id == null and isAdmin |
|||
* 4) update tags |
|||
* 5) update completed_at when status in [done, close] |
|||
* 6) due_date before sprint end_time |
|||
*/ |
|||
public function update($business, $project, $task, Request $request) |
|||
{ |
|||
permit('isProjectGuest', ['project_id' => $project]); |
|||
$taskModel = Task::where([['project_id', $project ], ['id', $task]])->firstOrFail(); |
|||
|
|||
if ($taskModel->assignee_id != \auth()->id() && can('isDefiniteGuestInProject', ['project_id' => $project])) { |
|||
permit('isDefiniteGuestInProject', ['project_id' => $project]); |
|||
} |
|||
|
|||
if($request->filled('watchers')) { |
|||
$watchers = $taskModel->watchers ?? []; |
|||
if(!can('isDefiniteGuestInProject', ['project_id' => $project]) && !can('projectTasks', ['project_id' => $project])) { |
|||
if (array_search(auth()->id(), $watchers) !== false) { |
|||
// remove auth from watchers
|
|||
$watchers = array_values(\array_diff($watchers, [auth()->id()])); |
|||
} else { |
|||
// add auth to watchers
|
|||
$watchers = array_merge($watchers, [auth()->id()]); |
|||
} |
|||
} |
|||
if(can('projectTasks', ['project_id' => $project])) { |
|||
$watchers = array_intersect($watchers ?? [], $request->watchers); |
|||
} |
|||
$request->merge(['watchers' => $watchers]); |
|||
} |
|||
|
|||
if (!can('projectTasks', ['project_id' => $project])) { |
|||
$request->replace($request->only(['_business_info', 'ready_to_test', 'status_id', 'watchers'])); |
|||
} |
|||
if ($taskModel->assignee_id != \auth()->id()) { |
|||
$request->request->remove('ready_to_test'); |
|||
} |
|||
|
|||
|
|||
if (isset(\request('_business_info')['workflows'][$request->workflow_id]['statuses'][$request->status_id]['state'])) { |
|||
$state = \request('_business_info')['workflows'][$request->workflow_id]['statuses'][$request->status_id]['state']; |
|||
if ($state == enum('status.states.close.id') || $state == enum('status.states.done.id')) { |
|||
//ToDo: is needed to check before state is done or close?
|
|||
$request->merge([ |
|||
'completed_at' => Carbon::now(), |
|||
'work_finish' => Work::where('task_id', $task)->latest()->first()->ended_at ?? null |
|||
]); |
|||
if (isset($taskModel->due_date) && $taskModel->due_date < date('yy-m-d')) { |
|||
//check if before set due date and miss, we change on time flag
|
|||
$request->merge(['on_time' => false]); |
|||
} |
|||
} |
|||
} |
|||
if (!$request->has('tags')) { |
|||
$request->merge(['tags' => []]); |
|||
} |
|||
$taskModel->update($request->except('_business_info')); |
|||
return new TaskResource($taskModel); |
|||
} |
|||
|
|||
public function updateReadyToTest($business, $project, $task) |
|||
{ |
|||
permit('isProjectGuest', ['project_id' => $project]); |
|||
$task = Task::where([['project_id', $project ], ['id', $task]])->firstOrFail(); |
|||
if ($task->assignee_id == \auth()->id()) { |
|||
$task->update([ |
|||
'ready_to_test' => 1 |
|||
]); |
|||
} else { |
|||
return abort(Response::HTTP_FORBIDDEN); // not allowed
|
|||
} |
|||
return $task->load(['tagTask'=> fn($q) => $q->select('id', 'tag_id', 'task_id'), 'works', 'comments']); |
|||
} |
|||
|
|||
/** |
|||
* Rule's: |
|||
* 1) delete only not work time (soft delete) |
|||
*/ |
|||
public function destroy($task) |
|||
{ |
|||
$taskModel = Task::findOrFail($task); |
|||
if (Work::where('task_id', $task)->exists()) { |
|||
return \response()->json(['task_id' => 'The task id cannot be deleted.'], Response::HTTP_UNPROCESSABLE_ENTITY); |
|||
} |
|||
$taskModel->delete(); |
|||
return \response()->json(['message' => 'task deleted successfully.'], Response::HTTP_OK); |
|||
} |
|||
|
|||
public function toggleWatcher($business, $task, $project =null) |
|||
{ |
|||
permit('isProjectGuest', ['project_id' => $project]); |
|||
$taskModel = Task::findOrFail($task); |
|||
$watchers = $taskModel->watchers ?? []; |
|||
|
|||
if (array_search(auth()->id(), $watchers) !== false) { |
|||
// remove auth from watchers
|
|||
$new_watchers = array_values(\array_diff($watchers, [auth()->id()])); |
|||
} else { |
|||
// add auth to watchers
|
|||
$new_watchers = array_merge($watchers, [auth()->id()]); |
|||
} |
|||
|
|||
$taskModel->update([ |
|||
'watchers' => $new_watchers |
|||
]); |
|||
return new TaskResource($taskModel); |
|||
} |
|||
} |
@ -0,0 +1,242 @@ |
|||
<?php |
|||
|
|||
namespace App\Http\Controllers; |
|||
|
|||
use App\Models\Task; |
|||
use App\Models\Work; |
|||
use Carbon\Carbon; |
|||
use Illuminate\Http\Request; |
|||
use Illuminate\Http\Response; |
|||
use Spatie\QueryBuilder\AllowedFilter; |
|||
use Spatie\QueryBuilder\QueryBuilder; |
|||
|
|||
class WorkController extends Controller |
|||
{ |
|||
public function index($business, Request $request) |
|||
{ |
|||
permit('businessAccess'); |
|||
$per_page = $request->limit > 100 ? 10 : $request->limit; |
|||
$workQ = $this->indexFiltering($business) |
|||
->when($request->filled('group'), function ($q) use ($request) { |
|||
return $request->group == 'user' ? $q->report() : $q->reportByDate(); |
|||
}); |
|||
|
|||
return $request->filled('group') ? $workQ->get() : |
|||
$workQ->defaultSort('-id') |
|||
->allowedSorts('id', 'started_at')->paginate($per_page); |
|||
} |
|||
|
|||
public function indexFiltering($business) |
|||
{ |
|||
$query = Work::where('works.business_id', $business); |
|||
$workQ = queryBuilder::for($query) |
|||
->join('tasks', 'tasks.id', 'works.task_id') |
|||
->select('works.*', 'tasks.title', 'tasks.sprint_id', 'tasks.system_id') |
|||
->allowedFilters([ |
|||
AllowedFilter::exact('project_id'), |
|||
AllowedFilter::exact('tasks.sprint_id', null, false), |
|||
AllowedFilter::exact('tasks.system_id', null, false), |
|||
AllowedFilter::exact('user_id'), |
|||
AllowedFilter::scope('started_at_in'), |
|||
AllowedFilter::scope('started_at'), |
|||
AllowedFilter::scope('spent_time_from'), |
|||
AllowedFilter::scope('spent_time_to'), |
|||
]); |
|||
if (\request('_business_info')['info']['users'][\auth()->id()]['level'] != enum('levels.owner.id')) { |
|||
$requested_projects = isset(\request('filter')['project_id']) ? |
|||
array_unique(explode(',',\request('filter')['project_id'] ?? null )) : |
|||
null; |
|||
$requested_projects = collect($requested_projects)->keyBy(null)->toArray(); |
|||
$project_ids = $this->myStateProjects($requested_projects); |
|||
$workQ->where(function ($q) use ($project_ids) { |
|||
$q->whereIn('project_id', $project_ids['non_guest_ids']) |
|||
->orWhere(function ($q) use ($project_ids) { |
|||
$q->whereIn('project_id', $project_ids['guest_ids']) |
|||
->where('assignee_id', auth()->id()); |
|||
}); |
|||
}); |
|||
} |
|||
return $workQ; |
|||
} |
|||
|
|||
public function myStateProjects($requested_projects) |
|||
{ |
|||
$non_guest_ids = []; |
|||
$guest_ids = []; |
|||
$is_empty = empty($requested_projects); |
|||
|
|||
foreach (\request('_business_info')['info']['projects'] as $p_id => $p) { |
|||
|
|||
$level = \request('_business_info')['info']['projects'][$p_id]['members'][\auth()->id()]['level']; |
|||
|
|||
if (( $is_empty || isset($requested_projects[$p_id])) |
|||
&& $level > enum('levels.guest.id')) { |
|||
array_push($non_guest_ids, $p_id); |
|||
} |
|||
if (( $is_empty || isset($requested_projects[$p_id])) |
|||
&& $level == enum('levels.guest.id')) { |
|||
array_push($guest_ids, $p_id); |
|||
} |
|||
} |
|||
|
|||
return ['non_guest_ids' => $non_guest_ids, 'guest_ids' => $guest_ids]; |
|||
} |
|||
|
|||
public function show($business, $project, $task, $work) |
|||
{ |
|||
permit('projectAccess', ['project_id' => $project]); |
|||
$work = Work::where([['project_id', $project ], ['task_id', $task], ['id', $work]])->firstOrFail(); |
|||
if (can('isDefiniteGuestInProject', ['project_id' => $project])){ // is guest in project (only guest)
|
|||
return $work->user_id == \auth()->id() ? $work : abort(Response::HTTP_FORBIDDEN); // not allowed
|
|||
} else { |
|||
return $work; |
|||
} |
|||
} |
|||
/** |
|||
* Rule's: |
|||
* 1) only assignee_id can store work |
|||
* 2) started_at after task created_at |
|||
* 3) ended_at after started_at |
|||
* 4) not any work before in this work |
|||
*/ |
|||
public function store($business, $project, $task, Request $request) |
|||
{ |
|||
$taskModel = Task::findOrFail($task); |
|||
if ($taskModel->assignee_id != auth()->id()) { |
|||
abort(Response::HTTP_FORBIDDEN); // not allowed
|
|||
} |
|||
|
|||
$end = Carbon::createFromFormat('Y-m-d H:i', $request->ended_at); |
|||
$start = Carbon::createFromFormat('Y-m-d H:i', $request->started_at); |
|||
$diff_in_min = $end->diffInMinutes($start); |
|||
$work = Work::create($request->merge([ |
|||
'business_id' => $business, |
|||
'project_id' => $project, |
|||
'task_id' => $task, |
|||
'user_id' => auth()->id(), |
|||
'minute_sum' => $diff_in_min, |
|||
'task' => [ |
|||
'spent_time' => $taskModel->spent_time + $diff_in_min |
|||
] |
|||
])->except('_business_info')); |
|||
$taskModel->refresh(); |
|||
// $taskModel->update([
|
|||
// 'work_start' => Work::where('task_id', $taskModel->id)->orderBy('started_at')->first()->started_at ?? null,
|
|||
// 'spent_time' => $taskModel->spent_time + $diff_in_min
|
|||
// ]);
|
|||
return $taskModel->load(['tagTask'=> fn($q) => $q->select('id', 'tag_id', 'task_id'), 'works', 'comments']); |
|||
} |
|||
|
|||
public function storeValidation($request, $taskModel) |
|||
{ |
|||
$this->validate($request, [ |
|||
'message' => 'nullable|string|min:3|max:225', |
|||
'started_at' => 'required|date_format:Y-m-d H:i|after:'.$taskModel->created_at, |
|||
'ended_at' => 'required|date_format:Y-m-d H:i|after:started_at', |
|||
]); |
|||
$state = \request('_business_info')['workflows'][$taskModel->workflow_id]['statuses'][$taskModel->status_id]['state'] ?? null; |
|||
if ($state == enum('status.states.close.id') || $state == enum('status.states.done.id')) { |
|||
throw ValidationException::withMessages(['task' => 'The selected task is invalid.']); |
|||
} |
|||
$works = Work::where([ |
|||
['ended_at', '>', $request->started_at], |
|||
['ended_at', '<', $request->ended_at], |
|||
])->orWhere([ |
|||
['started_at', '>', $request->started_at], |
|||
['started_at', '<', $request->ended_at], |
|||
])->orWhere([ |
|||
['started_at', '>=', $request->started_at], |
|||
['ended_at', '<=', $request->ended_at], |
|||
])->exists(); |
|||
if ($works) { |
|||
throw ValidationException::withMessages(['work' => 'The selected work is invalid.']); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Rule's: |
|||
* 1) only assignee_id can store work |
|||
* 2) started_at after task created_at |
|||
* 3) ended_at after started_at |
|||
* 4) not any work before in this work |
|||
*/ |
|||
public function update($business, $project, $task, $work, Request $request) |
|||
{ |
|||
$taskModel = Task::findOrFail($task); |
|||
$workModel = Work::findOrFail($work); |
|||
if ($taskModel->assignee_id != auth()->id()) { |
|||
abort(Response::HTTP_FORBIDDEN); // not allowed
|
|||
} |
|||
|
|||
$end = Carbon::createFromFormat('Y-m-d H:i', $request->ended_at); |
|||
$start = Carbon::createFromFormat('Y-m-d H:i', $request->started_at); |
|||
$new_diff_in_min = $end->diffInMinutes($start); |
|||
$old_diff_in_min = $workModel->minute_sum; |
|||
$workModel->update($request->merge([ |
|||
'business_id' => $business, |
|||
'project_id' => $project, |
|||
'task_id' => $task, |
|||
'user_id' => auth()->id(), |
|||
'minute_sum' => $new_diff_in_min, |
|||
'task' => [ |
|||
'spent_time' => ($taskModel->spent_time - $old_diff_in_min) + $new_diff_in_min |
|||
] |
|||
])->except('_business_info')); |
|||
$taskModel->refresh(); |
|||
// $taskModel->update([
|
|||
// 'work_start' => Work::where('task_id', $taskModel->id)->orderBy('started_at')->first()->started_at ?? null,
|
|||
// 'spent_time' => ($taskModel->spent_time - $old_diff_in_min) + $new_diff_in_min
|
|||
// ]);
|
|||
return $taskModel->load(['tagTask'=> fn($q) => $q->select('id', 'tag_id', 'task_id'), 'works', 'comments']); |
|||
} |
|||
|
|||
public function updateValidation($request, $taskModel, $workModel) |
|||
{ |
|||
$this->validate($request, [ |
|||
'message' => 'nullable|string|min:3|max:225', |
|||
'started_at' => 'nullable|date_format:Y-m-d H:i|after:'.$taskModel->created_at, |
|||
'ended_at' => 'nullable|date_format:Y-m-d H:i|after:started_at', |
|||
]); |
|||
//ToDo: is needed to check status is active or idea??
|
|||
$works = false; |
|||
if ($request->filled('started_at') || $request->filled('ended_at')) { |
|||
$started_at = $request->started_at ?? $workModel->started_at->format('Y-m-d H:i'); |
|||
$ended_at = $request->ended_at ?? $workModel->ended_at->format('Y-m-d H:i'); |
|||
if (strtotime($ended_at) <= strtotime($started_at)) { |
|||
throw ValidationException::withMessages(['ended_at' => 'The ended at must be a date after started at.']); |
|||
} |
|||
$works = Work::where([ |
|||
['ended_at', '>', $started_at], |
|||
['ended_at', '<', $ended_at], |
|||
])->orWhere([ |
|||
['started_at', '>', $started_at], |
|||
['started_at', '<', $ended_at], |
|||
])->orWhere([ |
|||
['started_at', '>=', $started_at], |
|||
['ended_at', '<=', $ended_at], |
|||
])->where('id', '!=', $workModel->id)->exists(); |
|||
$end = Carbon::createFromFormat('Y-m-d H:i', $ended_at); |
|||
$start = Carbon::createFromFormat('Y-m-d H:i', $started_at); |
|||
\request()->merge(['minute_sum' => $end->diffInMinutes($start)]); |
|||
} |
|||
if ($works) { |
|||
throw ValidationException::withMessages(['work' => 'The selected work is invalid.']); |
|||
} |
|||
} |
|||
|
|||
public function destroy($business, $project, $task, $work) |
|||
{ |
|||
$taskModel = Task::findOrFail($task); |
|||
$workModel = Work::findOrFail($work); |
|||
if ($taskModel->assignee_id != auth()->id()) { |
|||
abort(Response::HTTP_FORBIDDEN); // not allowed
|
|||
} |
|||
$diff_in_min = $workModel->minute_sum; |
|||
$workModel->delete(); |
|||
$taskModel->update([ |
|||
'work_start' => Work::where('task_id', $taskModel->id)->orderBy('started_at')->first()->started_at ?? null, |
|||
'spent_time' => ($taskModel->spent_time - $diff_in_min) |
|||
]); |
|||
return $taskModel->load(['tagTask'=> fn($q) => $q->select('id', 'tag_id', 'task_id'), 'works','comments']); |
|||
} |
|||
} |
@ -0,0 +1,34 @@ |
|||
<?php |
|||
|
|||
namespace App\Http\Resources; |
|||
|
|||
use Illuminate\Http\Resources\Json\JsonResource; |
|||
|
|||
class CommentResource extends JsonResource |
|||
{ |
|||
/** |
|||
* Transform the resource into an array. |
|||
* |
|||
* @param \Illuminate\Http\Request $request |
|||
* @return array |
|||
*/ |
|||
public function toArray($request) |
|||
{ |
|||
$resource = [ |
|||
'_service' => 'task', |
|||
'_resource' => 'comment', |
|||
]; |
|||
|
|||
foreach ($this->getAttributes() as $attribute => $value) { |
|||
switch ($attribute) { |
|||
case 'task_id' : |
|||
case 'user_id' : |
|||
case 'body' : |
|||
$resource[$attribute] = $value; |
|||
break; |
|||
} |
|||
} |
|||
|
|||
return $resource; |
|||
} |
|||
} |
@ -0,0 +1,23 @@ |
|||
<?php |
|||
|
|||
namespace App\Http\Resources; |
|||
|
|||
use Carbon\Carbon; |
|||
use Illuminate\Http\Resources\Json\ResourceCollection; |
|||
|
|||
class TaskCollection extends ResourceCollection |
|||
{ |
|||
/** |
|||
* Transform the resource collection into an array. |
|||
* |
|||
* @param \Illuminate\Http\Request $request |
|||
* @return array |
|||
*/ |
|||
public function toArray($request) |
|||
{ |
|||
return [ |
|||
'tasks' => TaskResource::collection($this->collection), |
|||
'now' => Carbon::now()->toDateString() |
|||
]; |
|||
} |
|||
} |
@ -0,0 +1,30 @@ |
|||
<?php |
|||
|
|||
namespace App\Http\Resources; |
|||
|
|||
use Illuminate\Http\Resources\Json\JsonResource; |
|||
|
|||
class TaskResource extends JsonResource |
|||
{ |
|||
/** |
|||
* Transform the resource into an array. |
|||
* |
|||
* @param \Illuminate\Http\Request $request |
|||
* @return array |
|||
*/ |
|||
public function toArray($request) |
|||
{ |
|||
foreach ($this->getAttributes() as $attribute => $value) { |
|||
$resource[$attribute] = $value; |
|||
if ($attribute == 'watchers') { |
|||
$resource[$attribute] = json_decode($value); |
|||
} |
|||
} |
|||
|
|||
$resource['tags'] = $this->tagTask()->pluck('tag_id')->toArray(); |
|||
$resource['works'] = $this->works; |
|||
$resource['comments'] = $this->comments; |
|||
|
|||
return $resource; |
|||
} |
|||
} |
@ -0,0 +1,19 @@ |
|||
<?php |
|||
|
|||
namespace App\Models; |
|||
|
|||
use Illuminate\Database\Eloquent\Factories\HasFactory; |
|||
use Illuminate\Database\Eloquent\Model; |
|||
|
|||
class Comment extends Model |
|||
{ |
|||
use HasFactory; |
|||
protected $fillable = ['business_id', 'project_id', 'task_id', 'user_id', 'body']; |
|||
|
|||
public function rules() |
|||
{ |
|||
return [ |
|||
'body' => 'required|string|min:3|max:1000', |
|||
]; |
|||
} |
|||
} |
@ -0,0 +1,308 @@ |
|||
<?php |
|||
|
|||
namespace App\Models; |
|||
|
|||
use App\Models\Business; |
|||
use App\Models\Comment; |
|||
use App\HiLib\Models\ReportableRelation; |
|||
use App\Models\Project; |
|||
use App\Models\Tag; |
|||
use App\Models\TagTask; |
|||
use App\Models\User; |
|||
use App\Models\Work; |
|||
use Carbon\Carbon; |
|||
use Illuminate\Database\Eloquent\Factories\HasFactory; |
|||
use Illuminate\Database\Eloquent\Model; |
|||
use Illuminate\Support\Facades\DB; |
|||
use Illuminate\Validation\Rule; |
|||
use Illuminate\Validation\Rules\RequiredIf; |
|||
|
|||
class Task extends Model |
|||
{ |
|||
use HasFactory; |
|||
protected $fillable = [ |
|||
'business_id', 'project_id', 'creator_id', 'workflow_id', 'assignee_id', 'system_id', 'sprint_id', |
|||
'status_id', 'approver_id', 'title', 'description', 'priority', 'on_time', 'estimated_time', 'due_date', 'completed_at', |
|||
'work_start', 'spent_time', 'work_finish', 'ready_to_test', 'watchers', 'tags' |
|||
]; |
|||
|
|||
protected $reportable = [ |
|||
'priority', 'title', 'description', 'priority', 'on_time', 'estimated_time', 'due_date', 'completed_at', |
|||
'work_finish', 'ready_to_test', 'assignee_id', 'approver_id', ['tags' => 'tag_task'] |
|||
]; |
|||
protected $fillable_relations = [ |
|||
'tags' |
|||
]; |
|||
|
|||
protected $casts = [ |
|||
'work_start' => 'datetime:Y-m-d H:i', |
|||
'work_finish' => 'datetime:Y-m-d H:i', |
|||
'watchers' => 'array', |
|||
]; |
|||
|
|||
public function rules() |
|||
{ |
|||
$validations = [ |
|||
'assignee_id' => ['nullable', 'numeric', |
|||
function ($attribute, $value, $fail) { |
|||
//check assignee at least guest in project
|
|||
if (!can('isProjectGuest', ['project_id' => request()->route('project'), 'user_id' => $value])) { |
|||
$fail('The selected '.$attribute.' is invalid.'); |
|||
} |
|||
if (request()->method() === 'PUT' && ($this->assignee_id != $value && Work::where('task_id', $this->id)->exists())) { |
|||
//when update execute
|
|||
//check if change assignee then should be on task not work set
|
|||
$fail('The selected '.$attribute.' is invalid.'); |
|||
} |
|||
}], |
|||
'system_id' => ['nullable', 'numeric', |
|||
Rule::in(\request('_business_info')['info']['projects'][request()->route('project')]['systems'])], |
|||
'sprint_id' => ['nullable', 'numeric', |
|||
Rule::in(\request('_business_info')['info']['projects'][request()->route('project')]['sprints'])], |
|||
'workflow_id' => ['required', 'numeric', |
|||
Rule::in(array_keys(\request('_business_info')['info']['workflows']))], |
|||
'status_id' => 'required|numeric', |
|||
'approver_id' => ['nullable', 'numeric', |
|||
function ($attribute, $value, $fail) { |
|||
//check approval at least colleague in project
|
|||
if (!can('isProjectColleague', ['project_id' => request()->route('project'), 'user_id' => $value])) { |
|||
$fail('The selected '.$attribute.' is invalid.'); |
|||
} |
|||
}], |
|||
'title' => 'required|string|min:3|max:254', |
|||
'description' => 'nullable|string|min:2|max:1000', |
|||
'priority' => 'nullable|numeric|between:1,10', |
|||
'estimated_time' => 'nullable|numeric|min:30', |
|||
'due_date' => 'bail|nullable|date|date_format:Y-m-d|after_or_equal:'. |
|||
((request()->method() === 'POST') ? date('yy-m-d') : $this->created_at->toDateString()), |
|||
'tags' => 'nullable|array', |
|||
'tags.*' => [new RequiredIf(isset($request->tags)), 'numeric', |
|||
Rule::in(\request('_business_info')['info']['tags'])], |
|||
] ; |
|||
$workflow_id = $this->workflow_id; |
|||
if (request()->filled('workflow_id') && isset(request('_business_info')['info']['workflows'][request()->workflow_id])) { |
|||
$workflow_id = request()->workflow_id; |
|||
} |
|||
if (isset($workflow_id)) { |
|||
$validations['status_id'] = ['bail', 'required', 'numeric', |
|||
//check status exists in status list
|
|||
Rule::in(\request('_business_info')['info']['workflows'][$workflow_id]['statuses']), |
|||
function ($attribute, $value, $fail) use ($workflow_id) { |
|||
//check not close or done
|
|||
$state = \request('_business_info')['workflows'][$workflow_id]['statuses'][$value]['state']; |
|||
if (request()->method() == 'POST' && ($state == enum('status.states.close.id') || $state == enum('status.states.done.id'))) { |
|||
$fail('The selected '.$attribute.' is invalid.'); |
|||
} |
|||
if ((request()->method() == 'PUT') && !can('projectTasks', ['project_id' => request()->route('project')]) |
|||
&& $state != enum('status.states.active.id')) { |
|||
$fail('The selected '.$attribute.' is invalid.'); |
|||
} |
|||
}]; |
|||
} |
|||
|
|||
return $validations; |
|||
} |
|||
public function updateRelations() |
|||
{ |
|||
// tags relations
|
|||
$this->dirties['tags'] = $this->tags()->sync($this->filled_relations['tags']); |
|||
|
|||
//old code
|
|||
// if (!empty($this->filled_relations['tags'])) {
|
|||
// $this->dirties['tags'] = $this->tags()->sync($this->filled_relations['tags']);
|
|||
// }
|
|||
|
|||
} |
|||
|
|||
public function getValueOf(?string $key) |
|||
{ |
|||
$values = [ |
|||
'business_id' => $this->business_id, |
|||
'project_id' => $this->project_id, |
|||
'sprint_id' => $this->sprint_id, |
|||
'workflow_id' => $this->workflow_id, |
|||
'status_id' => $this->status_id, |
|||
'system_id' => $this->system_id, |
|||
'task_id' => $this->id, |
|||
'subject_id' => $this->id, |
|||
'user_id' => $this->assignee_id, |
|||
]; |
|||
|
|||
if ($key && isset($values, $key)) { |
|||
return $values[$key]; |
|||
} |
|||
|
|||
return $values; |
|||
} |
|||
|
|||
public function business() |
|||
{ |
|||
return $this->belongsTo(Business::class, 'business_id', 'id', __FUNCTION__); |
|||
} |
|||
|
|||
public function creator() |
|||
{ |
|||
return $this->belongsTo(User::class, 'creator_id', 'id', __FUNCTION__); |
|||
} |
|||
|
|||
public function user() |
|||
{ |
|||
return $this->belongsTo(User::class, 'user_id', 'id', __FUNCTION__); |
|||
} |
|||
|
|||
public function project() |
|||
{ |
|||
return $this->belongsTo(Project::class, 'project_id', 'id', __FUNCTION__); |
|||
} |
|||
|
|||
public function tags() |
|||
{ |
|||
return $this->belongsToMany( |
|||
Tag::class, 'tag_task', 'task_id', 'tag_id', |
|||
'id', 'id', __FUNCTION__ |
|||
)->using(ReportableRelation::class); |
|||
} |
|||
|
|||
public function tagTask() |
|||
{ |
|||
return $this->hasMany(TagTask::class, 'task_id', 'id'); |
|||
} |
|||
|
|||
public function works() |
|||
{ |
|||
return $this->hasMany(Work::class, 'task_id', 'id'); |
|||
} |
|||
|
|||
public function comments() |
|||
{ |
|||
return $this->hasMany(Comment::class, 'task_id', 'id'); |
|||
} |
|||
|
|||
public function scopePriorityMin($query, $min) |
|||
{ |
|||
return $query->where('tasks.priority', '>=', $min); |
|||
} |
|||
public function scopePriorityMax($query, $max) |
|||
{ |
|||
return $query->where('tasks.priority', '<=', $max); |
|||
} |
|||
public function scopeCreatesBefore($query, $date) |
|||
{ |
|||
//ToDo: comment lines have better performance but should be test
|
|||
// return $query->whereDate('tasks.created_at', '<=', Carbon::parse($date.' 23:59:59'));
|
|||
// return $query->where('tasks.created_at', '<', (new DateTime('2014-07-10'))->modify('+1 day')->format('Y-m-d'));
|
|||
return $query->whereDate('tasks.created_at', '<=', Carbon::parse($date)); |
|||
} |
|||
public function scopeCreatesAfter($query, $date) |
|||
{ |
|||
return $query->whereDate('tasks.created_at', '>=', Carbon::parse($date)); |
|||
} |
|||
public function scopeCreatesIn($query, $days) |
|||
{ |
|||
return $query->whereDate('tasks.created_at', '>=', Carbon::now()->modify('-'.$days.' day')->toDate()); |
|||
} |
|||
public function scopeUpdatesBefore($query, $date) |
|||
{ |
|||
return $query->whereDate('tasks.updated_at', '<=', Carbon::parse($date)); |
|||
} |
|||
public function scopeUpdatesAfter($query, $date) |
|||
{ |
|||
return $query->whereDate('tasks.updated_at', '>=', Carbon::parse($date)); |
|||
} |
|||
public function scopeUpdatesIn($query, $days) |
|||
{ |
|||
return $query->whereDate('tasks.updated_at', '>=', Carbon::now()->modify('-'.$days.' day')->toDate()); |
|||
} |
|||
public function scopeSpentFrom($query, $minute) |
|||
{ |
|||
return $query->where('tasks.spent_time', '>=', $minute); |
|||
} |
|||
public function scopeSpentTo($query, $minute) |
|||
{ |
|||
return $query->where('tasks.spent_time', '<=', $minute); |
|||
} |
|||
public function scopeEstimatedFrom($query, $minute) |
|||
{ |
|||
return $query->where('tasks.estimated_time', '>=', $minute); |
|||
} |
|||
public function scopeEstimatedTo($query, $minute) |
|||
{ |
|||
return $query->where('tasks.estimated_time', '<=', $minute); |
|||
} |
|||
public function scopeStartsBefore($query, $date) |
|||
{ |
|||
return $query->whereDate('tasks.work_start', '<=', Carbon::parse($date)); |
|||
} |
|||
public function scopeStartsAfter($query, $date) |
|||
{ |
|||
return $query->whereDate('tasks.work_start', '>=', Carbon::parse($date)); |
|||
} |
|||
public function scopeStartsIn($query, $days) |
|||
{ |
|||
return $query->whereDate('tasks.work_start', '>=', Carbon::now()->modify('-'.$days.' day')->toDate()); |
|||
} |
|||
public function scopeFinishBefore($query, $date) |
|||
{ |
|||
return $query->whereDate('tasks.work_finish', '<=', Carbon::parse($date)); |
|||
} |
|||
public function scopeFinishAfter($query, $date) |
|||
{ |
|||
return $query->whereDate('tasks.work_finish', '>=', Carbon::parse($date)); |
|||
} |
|||
public function scopeFinishIn($query, $days) |
|||
{ |
|||
return $query->whereDate('tasks.work_finish', '>=', Carbon::now()->modify('-'.$days.' day')->toDate()); |
|||
} |
|||
public function scopeCompletesBefore($query, $date) |
|||
{ |
|||
return $query->where('tasks.completed_at', '<=', $date); |
|||
} |
|||
public function scopeCompletesAfter($query, $date) |
|||
{ |
|||
return $query->where('tasks.completed_at', '>=', $date); |
|||
} |
|||
public function scopeCompletesIn($query, $days) |
|||
{ |
|||
return $query->whereDate('tasks.completed_at', '>=', Carbon::now()->modify('-'.$days.' day')->toDate()); |
|||
} |
|||
public function scopeDueDateBefore($query, $date) |
|||
{ |
|||
return $query->where('tasks.due_date', '<=', $date); |
|||
} |
|||
public function scopeDueDateAfter($query, $date) |
|||
{ |
|||
return $query->where('tasks.due_date', '>=', $date); |
|||
} |
|||
public function scopeDueDateIn($query, $days) |
|||
{ |
|||
return $query->whereDate('tasks.due_date', '>=', Carbon::now()->modify('-'.$days.' day')->toDate()); |
|||
} |
|||
public function scopeOverSpentFrom($query, $minute) |
|||
{ |
|||
return $query->whereColumn('spent_time', '>', 'estimated_time') |
|||
->having('over_spent', '>=', $minute); |
|||
} |
|||
public function scopeOverSpentTo($query, $minute) |
|||
{ |
|||
return $query->whereColumn('spent_time', '>', 'estimated_time') |
|||
->having('over_spent', '<=', $minute); |
|||
} |
|||
public function scopeMyWatching($query) |
|||
{ |
|||
return $query->whereJsonContains('watchers', auth()->id()); |
|||
} |
|||
public function scopeOverDue($query) |
|||
{ |
|||
return $query->whereColumn('due_date', '<', 'completed_at'); |
|||
} |
|||
public function scopeReport($query, $group_field) |
|||
{ |
|||
return $query->select(DB::raw($group_field.' , status_id, COUNT(*) as total, SUM(spent_time) as spent_sum, |
|||
SUM(estimated_time) as estimated_sum,SUM(ready_to_test) as test_sum, |
|||
COUNT(completed_at) as completed_total, SUM(on_time) as on_time_total, |
|||
SUM(Case When spent_time > estimated_time Then (spent_time - estimated_time) Else 0 End) as over_spent_sum, |
|||
SUM(Case When (spent_time <= estimated_time) And (completed_at) Then (estimated_time - spent_time) Else 0 End) |
|||
as under_spent_sum')) |
|||
->groupBy($group_field, 'status_id'); |
|||
} |
|||
} |
@ -0,0 +1,126 @@ |
|||
<?php |
|||
|
|||
namespace App\Models; |
|||
|
|||
use App\Models\Task; |
|||
use Carbon\Carbon; |
|||
use Illuminate\Database\Eloquent\Factories\HasFactory; |
|||
use Illuminate\Database\Eloquent\Model; |
|||
use Illuminate\Support\Facades\DB; |
|||
|
|||
class Work extends Model |
|||
{ |
|||
use HasFactory; |
|||
protected $fillable = [ |
|||
'business_id', 'project_id', 'task_id', 'user_id', 'message', 'minute_sum', 'started_at', 'ended_at', |
|||
'task' |
|||
]; |
|||
|
|||
protected $fillable_relations = ['task']; |
|||
|
|||
protected $casts = [ |
|||
'started_at' => 'datetime:Y-m-d H:i', |
|||
'ended_at' => 'datetime:Y-m-d H:i' |
|||
]; |
|||
|
|||
protected $reportable = [ |
|||
'message', 'minute_sum', 'started_at', 'ended_at', |
|||
]; |
|||
|
|||
public function rules() |
|||
{ |
|||
$started_at = request()->started_at ?? $this->started_at->format('Y-m-d H:i'); |
|||
$ended_at = request()->ended_at ?? $this->ended_at->format('Y-m-d H:i'); |
|||
return [ |
|||
'message' => 'nullable|string|min:3|max:225', |
|||
'started_at' => 'required|date_format:Y-m-d H:i|after:'.$this->task()->first()->created_at, |
|||
'ended_at' => [ |
|||
'required', 'date_format:Y-m-d H:i', 'after:'.$started_at, |
|||
function ($attribute, $value, $fail) use ($ended_at, $started_at) { |
|||
$works = \App\Work::where([ |
|||
['ended_at', '>', $started_at], |
|||
['ended_at', '<', $ended_at], |
|||
])->orWhere([ |
|||
['started_at', '>', $started_at], |
|||
['started_at', '<', $ended_at], |
|||
])->orWhere([ |
|||
['started_at', '>=', $started_at], |
|||
['ended_at', '<=', $ended_at], |
|||
])->when(isset($this->id), function ($query) { |
|||
return $query->where('id', '!=', $this->id); |
|||
})->exists(); |
|||
if ($works) { |
|||
$fail('The selected work is invalid.'); |
|||
} |
|||
} |
|||
], |
|||
]; |
|||
|
|||
} |
|||
|
|||
public function getValueOf(?string $key) |
|||
{ |
|||
$values = [ |
|||
'business_id' => $this->business_id, |
|||
'project_id' => $this->project_id, |
|||
'sprint_id' => null, |
|||
'workflow_id' => null, |
|||
'status_id' => null, |
|||
'system_id' => null, |
|||
'task_id' => $this->task->id, |
|||
'subject_id' => $this->id, |
|||
'user_id' => null, |
|||
]; |
|||
|
|||
if ($key && isset($values, $key)) { |
|||
return $values[$key]; |
|||
} |
|||
|
|||
return $values; |
|||
} |
|||
|
|||
public function updateRelations() |
|||
{ |
|||
$this->filled_relations['task']['work_start'] = Work::where('task_id', $this->task_id)->orderBy('started_at')->first()->started_at ?? null; |
|||
$this->task()->update($this->filled_relations['task']); |
|||
} |
|||
|
|||
public function task() |
|||
{ |
|||
return $this->belongsTo(Task::class, 'task_id', 'id'); |
|||
} |
|||
|
|||
public function scopeStartedAtIn($query, $days) |
|||
{ |
|||
return $query->whereDate('started_at', '>=', Carbon::now()->modify('-'.$days.' day')->toDate()); |
|||
} |
|||
|
|||
public function scopeStartedAt($query, $date) |
|||
{ |
|||
return $query->whereDate('started_at', '=', $date); |
|||
} |
|||
|
|||
public function scopeSpentTimeFrom($query, $data) |
|||
{ |
|||
return $query->where('minute_sum', '>=', $data); |
|||
} |
|||
|
|||
public function scopeSpentTimeTo($query, $data) |
|||
{ |
|||
return $query->where('minute_sum', '<=', $data); |
|||
} |
|||
|
|||
public function scopeReport($query) |
|||
{ |
|||
return $query->select( |
|||
DB::raw('user_id , MIN(started_at) as started_at_min, MAX(ended_at) as ended_at_max, SUM(minute_sum) as work_minute_sum') |
|||
)->groupBy('user_id'); |
|||
} |
|||
|
|||
public function scopeReportByDate($query) |
|||
{ |
|||
return $query->select( |
|||
DB::raw('DATE(started_at) as date, SUM(minute_sum) as work_minute_sum') |
|||
)->groupBy('date'); |
|||
} |
|||
} |
@ -0,0 +1,55 @@ |
|||
<?php |
|||
|
|||
use Illuminate\Database\Migrations\Migration; |
|||
use Illuminate\Database\Schema\Blueprint; |
|||
use Illuminate\Support\Facades\Schema; |
|||
|
|||
class CreateTasksTable extends Migration |
|||
{ |
|||
/** |
|||
* Run the migrations. |
|||
* |
|||
* @return void |
|||
*/ |
|||
public function up() |
|||
{ |
|||
Schema::create('tasks', function (Blueprint $table) { |
|||
$table->id(); |
|||
$table->unsignedBigInteger('business_id'); |
|||
$table->unsignedBigInteger('project_id'); |
|||
$table->unsignedBigInteger('creator_id'); |
|||
$table->unsignedBigInteger('assignee_id')->nullable(); |
|||
$table->unsignedBigInteger('system_id')->nullable(); |
|||
$table->unsignedBigInteger('sprint_id')->nullable(); |
|||
$table->unsignedBigInteger('workflow_id'); |
|||
$table->unsignedBigInteger('status_id'); |
|||
$table->unsignedBigInteger('approver_id')->nullable(); |
|||
|
|||
$table->string('title'); |
|||
$table->text('description')->nullable(); |
|||
$table->integer('priority')->default(1); |
|||
$table->boolean('on_time')->default(true); |
|||
$table->boolean('ready_to_test')->default(false); |
|||
|
|||
$table->json('watchers')->nullable(); |
|||
|
|||
$table->integer('spent_time')->default(0); |
|||
$table->integer('estimated_time')->default(0); |
|||
$table->date('due_date')->nullable(); |
|||
$table->date('completed_at')->nullable(); |
|||
$table->timestamp('work_start')->nullable(); |
|||
$table->timestamp('work_finish')->nullable(); |
|||
$table->timestamps(); |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* Reverse the migrations. |
|||
* |
|||
* @return void |
|||
*/ |
|||
public function down() |
|||
{ |
|||
Schema::dropIfExists('tasks'); |
|||
} |
|||
} |
@ -0,0 +1,39 @@ |
|||
<?php |
|||
|
|||
use Illuminate\Database\Migrations\Migration; |
|||
use Illuminate\Database\Schema\Blueprint; |
|||
use Illuminate\Support\Facades\Schema; |
|||
|
|||
class CreateWorksTable extends Migration |
|||
{ |
|||
/** |
|||
* Run the migrations. |
|||
* |
|||
* @return void |
|||
*/ |
|||
public function up() |
|||
{ |
|||
Schema::create('works', function (Blueprint $table) { |
|||
$table->id(); |
|||
$table->unsignedBigInteger('business_id'); |
|||
$table->unsignedBigInteger('project_id'); |
|||
$table->unsignedBigInteger('task_id'); |
|||
$table->unsignedBigInteger('user_id'); |
|||
$table->string('message')->nullable(); |
|||
$table->integer('minute_sum')->default(0); |
|||
$table->dateTime('started_at'); |
|||
$table->dateTime('ended_at'); |
|||
$table->timestamps(); |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* Reverse the migrations. |
|||
* |
|||
* @return void |
|||
*/ |
|||
public function down() |
|||
{ |
|||
Schema::dropIfExists('works'); |
|||
} |
|||
} |
@ -0,0 +1,36 @@ |
|||
<?php |
|||
|
|||
use Illuminate\Database\Migrations\Migration; |
|||
use Illuminate\Database\Schema\Blueprint; |
|||
use Illuminate\Support\Facades\Schema; |
|||
|
|||
class CreateCommentsTable extends Migration |
|||
{ |
|||
/** |
|||
* Run the migrations. |
|||
* |
|||
* @return void |
|||
*/ |
|||
public function up() |
|||
{ |
|||
Schema::create('comments', function (Blueprint $table) { |
|||
$table->id(); |
|||
$table->unsignedBigInteger('business_id'); |
|||
$table->unsignedBigInteger('project_id'); |
|||
$table->unsignedBigInteger('task_id'); |
|||
$table->unsignedBigInteger('user_id'); |
|||
$table->text('body'); |
|||
$table->timestamps(); |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* Reverse the migrations. |
|||
* |
|||
* @return void |
|||
*/ |
|||
public function down() |
|||
{ |
|||
Schema::dropIfExists('comments'); |
|||
} |
|||
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue