Browse Source

Merge branch 'mahdi' of https://gitea.hooradev.ir/mahdihty/liwo into mohammad

pull/2/head
Mohammad Akbari 4 years ago
parent
commit
272b519ca1
Signed by: akbarjimi GPG Key ID: 55726AEFECE5E683
  1. 90
      app/Http/Controllers/CommentController.php
  2. 112
      app/Http/Controllers/StatisticController.php
  3. 295
      app/Http/Controllers/TaskController.php
  4. 242
      app/Http/Controllers/WorkController.php
  5. 34
      app/Http/Resources/CommentResource.php
  6. 23
      app/Http/Resources/TaskCollection.php
  7. 30
      app/Http/Resources/TaskResource.php
  8. 19
      app/Models/Comment.php
  9. 307
      app/Models/Task.php
  10. 126
      app/Models/Work.php
  11. 3
      composer.json
  12. 72
      composer.lock
  13. 55
      database/migrations/2021_03_02_082607_create_tasks_table.php
  14. 39
      database/migrations/2021_03_02_085913_create_works_table.php
  15. 36
      database/migrations/2021_03_02_092444_create_comments_table.php

90
app/Http/Controllers/CommentController.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
}
}

112
app/Http/Controllers/StatisticController.php

@ -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);
}
}

295
app/Http/Controllers/TaskController.php

@ -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);
}
}

242
app/Http/Controllers/WorkController.php

@ -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']);
}
}

34
app/Http/Resources/CommentResource.php

@ -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;
}
}

23
app/Http/Resources/TaskCollection.php

@ -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()
];
}
}

30
app/Http/Resources/TaskResource.php

@ -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;
}
}

19
app/Models/Comment.php

@ -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',
];
}
}

307
app/Models/Task.php

@ -1,25 +1,308 @@
<?php
namespace App;
namespace App\Models;
use App\HiLib\Models\RemoteModel;
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 RemoteModel
class Task extends Model
{
public string $host;
public ?string $path;
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'
];
public function __construct(array $attributes = [])
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()
{
parent::__construct($attributes);
return $this->hasMany(Work::class, 'task_id', 'id');
}
$this->host = 'hi-task-app';
$this->path = 'task/v1/businesses/'.request('_business_info')['id'].'/tasks/';
public function comments()
{
return $this->hasMany(Comment::class, 'task_id', 'id');
}
public function files()
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 $this->hasMany(File::class, 'attached_to_id', 'id')
->where('attached_to_table', enum('tables.tasks.id'));
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');
}
}

126
app/Models/Work.php

@ -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');
}
}

3
composer.json

@ -28,7 +28,8 @@
"fruitcake/laravel-cors": "^2.0",
"guzzlehttp/guzzle": "^7.0.1",
"laravel/framework": "^8.12",
"laravel/tinker": "^2.5"
"laravel/tinker": "^2.5",
"spatie/laravel-query-builder": "^3.3"
},
"require-dev": {
"facade/ignition": "^2.5",

72
composer.lock

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "990d5c126e6930062665fabddea7749a",
"content-hash": "727372edb248a9d83eed7a324b8eda99",
"packages": [
{
"name": "anik/amqp",
@ -4363,6 +4363,76 @@
},
"time": "2020-11-09T15:54:21+00:00"
},
{
"name": "spatie/laravel-query-builder",
"version": "3.3.4",
"source": {
"type": "git",
"url": "https://github.com/spatie/laravel-query-builder.git",
"reference": "2e131b0c8ae600b6e3aabb5a1501c721862a0b8f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/spatie/laravel-query-builder/zipball/2e131b0c8ae600b6e3aabb5a1501c721862a0b8f",
"reference": "2e131b0c8ae600b6e3aabb5a1501c721862a0b8f",
"shasum": ""
},
"require": {
"illuminate/database": "^6.0|^7.0|^8.0",
"illuminate/http": "^6.0|^7.0|^8.0",
"illuminate/support": "^6.0|^7.0|^8.0",
"php": "^7.3|^8.0"
},
"require-dev": {
"ext-json": "*",
"laravel/legacy-factories": "^1.0.4",
"mockery/mockery": "^1.4",
"orchestra/testbench": "^4.9|^5.8|^6.3",
"phpunit/phpunit": "^9.0"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Spatie\\QueryBuilder\\QueryBuilderServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"Spatie\\QueryBuilder\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Alex Vanderbist",
"email": "alex@spatie.be",
"homepage": "https://spatie.be",
"role": "Developer"
}
],
"description": "Easily build Eloquent queries from API requests",
"homepage": "https://github.com/spatie/laravel-query-builder",
"keywords": [
"laravel-query-builder",
"spatie"
],
"support": {
"issues": "https://github.com/spatie/laravel-query-builder/issues",
"source": "https://github.com/spatie/laravel-query-builder"
},
"funding": [
{
"url": "https://spatie.be/open-source/support-us",
"type": "custom"
}
],
"time": "2020-11-26T14:51:30+00:00"
},
{
"name": "swiftmailer/swiftmailer",
"version": "v6.2.5",

55
database/migrations/2021_03_02_082607_create_tasks_table.php

@ -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');
}
}

39
database/migrations/2021_03_02_085913_create_works_table.php

@ -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');
}
}

36
database/migrations/2021_03_02_092444_create_comments_table.php

@ -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');
}
}
Loading…
Cancel
Save