diff --git a/app/Entities/Projects/Issue.php b/app/Entities/Projects/Issue.php new file mode 100644 index 0000000..2ec06fc --- /dev/null +++ b/app/Entities/Projects/Issue.php @@ -0,0 +1,62 @@ +belongsTo(Project::class); + } + + public function pic() + { + return $this->belongsTo(User::class)->withDefault(['name' => __('issue.no_pic')]); + } + + public function creator() + { + return $this->belongsTo(User::class); + } + + public function getPriorityAttribute() + { + return Priority::getNameById($this->priority_id); + } + + public function getPriorityLabelAttribute() + { + $classColor = Priority::getColorById($this->priority_id); + + return ''.$this->priority.''; + } + + public function getStatusAttribute() + { + return IssueStatus::getNameById($this->status_id); + } + + public function getStatusLabelAttribute() + { + return ''.$this->status.''; + } + + /** + * Issue has many comments relation. + * + * @return \Illuminate\Database\Eloquent\Relations\MorphMany + */ + public function comments() + { + return $this->morphMany(Comment::class, 'commentable'); + } +} diff --git a/app/Entities/Projects/IssueStatus.php b/app/Entities/Projects/IssueStatus.php new file mode 100644 index 0000000..159b60a --- /dev/null +++ b/app/Entities/Projects/IssueStatus.php @@ -0,0 +1,39 @@ + 'open', + 1 => 'resolved', + 2 => 'closed', + 3 => 'on_hold', + 4 => 'invalid', + ]; + + protected static $colors = [ + 0 => 'yellow', + 1 => 'green', + 2 => 'primary', + 3 => 'default', + 4 => 'warning', + ]; + + public static function getNameById($singleId) + { + return trans('issue.'.static::getById($singleId)); + } + + public static function toArray() + { + $lists = []; + foreach (static::$lists as $key => $value) { + $lists[$key] = trans('issue.'.$value); + } + + return $lists; + } +} diff --git a/app/Entities/Projects/Priority.php b/app/Entities/Projects/Priority.php new file mode 100644 index 0000000..da953f1 --- /dev/null +++ b/app/Entities/Projects/Priority.php @@ -0,0 +1,35 @@ + 'minor', + 2 => 'major', + 3 => 'critical', + ]; + + protected static $colors = [ + 1 => 'info', + 2 => 'warning', + 3 => 'danger', + ]; + + public static function getNameById($singleId) + { + return trans('issue.'.static::getById($singleId)); + } + + public static function toArray() + { + $lists = []; + foreach (static::$lists as $key => $value) { + $lists[$key] = trans('issue.'.$value); + } + + return $lists; + } +} diff --git a/app/Entities/Projects/Project.php b/app/Entities/Projects/Project.php index 1607f58..6c6d59e 100755 --- a/app/Entities/Projects/Project.php +++ b/app/Entities/Projects/Project.php @@ -3,6 +3,7 @@ namespace App\Entities\Projects; use DB; +use App\Entities\Projects\Issue; use App\Entities\Invoices\Invoice; use App\Entities\Payments\Payment; use App\Entities\Partners\Customer; @@ -264,4 +265,14 @@ class Project extends Model return parent::delete(); } + + /** + * Project has many Issues relation. + * + * @return \Illuminate\Database\Eloquent\Relations\HasMany + */ + public function issues() + { + return $this->hasMany(Issue::class); + } } diff --git a/app/Http/Controllers/Issues/CommentController.php b/app/Http/Controllers/Issues/CommentController.php new file mode 100644 index 0000000..1b3d312 --- /dev/null +++ b/app/Http/Controllers/Issues/CommentController.php @@ -0,0 +1,83 @@ +authorize('comment-on', $issue); + + $newComment = $request->validate([ + 'body' => 'required|string|max:255', + ]); + + $issue->comments()->create([ + 'body' => $newComment['body'], + 'creator_id' => auth()->id(), + ]); + $issue->touch(); + + flash(__('comment.created'), 'success'); + + return back(); + } + + /** + * Update the specified comment. + * + * @param \Illuminate\Http\Request $request + * @param \App\Entities\Projects\Issue $issue + * @param \App\Entities\Projects\Comment $comment + * @return \Illuminate\Http\Response + */ + public function update(Request $request, Issue $issue, Comment $comment) + { + $this->authorize('update', $comment); + + $commentData = $request->validate([ + 'body' => 'required|string|max:255', + ]); + $comment->update($commentData); + flash(__('comment.updated'), 'success'); + + return redirect()->route('projects.issues.show', [$issue->project, $issue]); + } + + /** + * Remove the specified comment. + * + * @param \App\Entities\Projects\Issue $issue + * @param \\App\Entities\Projects\Comment $comment + * @return \Illuminate\Routing\Redirector + */ + public function destroy(Issue $issue, Comment $comment) + { + $this->authorize('delete', $comment); + + request()->validate([ + 'comment_id' => 'required|exists:comments,id', + ]); + + if (request('comment_id') == $comment->id && $comment->delete()) { + flash(__('comment.deleted'), 'warning'); + + return redirect()->route('projects.issues.show', [$issue->project, $issue]); + } + flash(__('comment.undeleted'), 'error'); + + return back(); + } +} diff --git a/app/Http/Controllers/Issues/OptionController.php b/app/Http/Controllers/Issues/OptionController.php new file mode 100644 index 0000000..a37a200 --- /dev/null +++ b/app/Http/Controllers/Issues/OptionController.php @@ -0,0 +1,26 @@ +validate([ + 'priority_id' => 'required|in:1,2,3', + 'status_id' => 'required|in:0,1,2,3,4', + 'pic_id' => 'nullable|exists:users,id', + ]); + $issue->priority_id = $issueData['priority_id']; + $issue->status_id = $issueData['status_id']; + $issue->pic_id = $issueData['pic_id']; + $issue->save(); + flash(__('issue.updated'), 'success'); + + return back(); + } +} diff --git a/app/Http/Controllers/Projects/IssueController.php b/app/Http/Controllers/Projects/IssueController.php new file mode 100644 index 0000000..97c46e2 --- /dev/null +++ b/app/Http/Controllers/Projects/IssueController.php @@ -0,0 +1,116 @@ +issues() + ->orderBy('updated_at', 'desc') + ->with(['pic', 'creator']) + ->withCount(['comments']); + + if ($priorityId = request('priority_id')) { + $issueQuery->where('priority_id', $priorityId); + } + + if ($statusId = request('status_id')) { + $issueQuery->where('status_id', $priorityId); + } + + $issues = $issueQuery->get(); + + return view('projects.issues.index', compact('project', 'issues')); + } + + public function create(Project $project) + { + $users = User::pluck('name', 'id'); + $priorities = Priority::toArray(); + + return view('projects.issues.create', compact('project', 'users', 'priorities')); + } + + public function store(Request $request, Project $project) + { + $issueData = $request->validate([ + 'title' => 'required|max:60', + 'body' => 'required|max:255', + 'priority_id' => 'required|in:1,2,3', + 'pic_id' => 'nullable|exists:users,id', + ]); + Issue::create([ + 'project_id' => $project->id, + 'creator_id' => auth()->id(), + 'title' => $issueData['title'], + 'body' => $issueData['body'], + 'priority_id' => $issueData['priority_id'], + 'pic_id' => $issueData['pic_id'], + ]); + flash(__('issue.created'), 'success'); + + return redirect()->route('projects.issues.index', $project); + } + + public function show(Project $project, Issue $issue) + { + $editableComment = null; + $priorities = Priority::toArray(); + $statuses = IssueStatus::toArray(); + $users = User::pluck('name', 'id'); + $comments = $issue->comments()->with('creator')->get(); + + if (request('action') == 'comment-edit' && request('comment_id') != null) { + $editableComment = Comment::find(request('comment_id')); + } + + return view('projects.issues.show', compact( + 'project', 'issue', 'users', 'statuses', 'priorities', 'comments', + 'editableComment' + )); + } + + public function edit(Project $project, Issue $issue) + { + return view('projects.issues.edit', compact('project', 'issue')); + } + + public function update(Request $request, Project $project, Issue $issue) + { + $issueData = $request->validate([ + 'title' => 'required|max:60', + 'body' => 'required|max:255', + ]); + $issue->title = $issueData['title']; + $issue->body = $issueData['body']; + $issue->save(); + + flash(__('issue.updated'), 'success'); + + return redirect()->route('projects.issues.show', [$project, $issue]); + } + + public function destroy(Request $request, Project $project, Issue $issue) + { + $request->validate(['issue_id' => 'required']); + + if ($request->get('issue_id') == $issue->id && $issue->delete()) { + flash(__('issue.deleted'), 'warning'); + + return redirect()->route('projects.issues.index', $project); + } + flash(__('issue.undeleted'), 'danger'); + + return back(); + } +} diff --git a/app/Policies/Projects/IssuePolicy.php b/app/Policies/Projects/IssuePolicy.php new file mode 100644 index 0000000..7fe975a --- /dev/null +++ b/app/Policies/Projects/IssuePolicy.php @@ -0,0 +1,29 @@ + 'App\Entities\Projects\Project', + 'issues' => 'App\Entities\Projects\Issue', 'jobs' => 'App\Entities\Projects\Job', ]); } diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php index 4584098..353f008 100644 --- a/app/Providers/AuthServiceProvider.php +++ b/app/Providers/AuthServiceProvider.php @@ -18,6 +18,7 @@ class AuthServiceProvider extends ServiceProvider 'App\Entities\Projects\Project' => 'App\Policies\Projects\ProjectPolicy', 'App\Entities\Projects\Comment' => 'App\Policies\Projects\CommentPolicy', 'App\Entities\Projects\Job' => 'App\Policies\Projects\JobPolicy', + 'App\Entities\Projects\Issue' => 'App\Policies\Projects\IssuePolicy', 'App\Entities\Projects\Task' => 'App\Policies\Projects\TaskPolicy', 'App\Entities\Payments\Payment' => 'App\Policies\PaymentPolicy', 'App\Entities\Users\User' => 'App\Policies\UserPolicy', diff --git a/composer.lock b/composer.lock index 49256f0..03b7677 100644 --- a/composer.lock +++ b/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": "a298d20cfcff4c54b1cce7bb8db3515e", + "content-hash": "935f96036f78f878a265f90061c07ece", "packages": [ { "name": "backup-manager/backup-manager", @@ -1230,16 +1230,16 @@ }, { "name": "luthfi/formfield", - "version": "1.0.4", + "version": "1.0.5", "source": { "type": "git", "url": "https://github.com/nafiesl/FormField.git", - "reference": "59e3ae44a0e04ae6eb6ff66a7f95f29acc5ea109" + "reference": "38d206d7b5e1e7893b67b06689d70305b998f0a1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nafiesl/FormField/zipball/59e3ae44a0e04ae6eb6ff66a7f95f29acc5ea109", - "reference": "59e3ae44a0e04ae6eb6ff66a7f95f29acc5ea109", + "url": "https://api.github.com/repos/nafiesl/FormField/zipball/38d206d7b5e1e7893b67b06689d70305b998f0a1", + "reference": "38d206d7b5e1e7893b67b06689d70305b998f0a1", "shasum": "" }, "require": { @@ -1278,7 +1278,7 @@ } ], "description": "Laravel Form Field the extension of Laravelcollective Form for Laravel 5.3 and newer with Twitter Bootstrap 3", - "time": "2018-09-16T11:40:29+00:00" + "time": "2019-03-12T15:08:48+00:00" }, { "name": "monolog/monolog", diff --git a/database/factories/IssueFactory.php b/database/factories/IssueFactory.php new file mode 100644 index 0000000..2941f50 --- /dev/null +++ b/database/factories/IssueFactory.php @@ -0,0 +1,21 @@ +define(Issue::class, function (Faker $faker) { + return [ + 'project_id' => function () { + return factory(Project::class)->create()->id; + }, + 'title' => $faker->words(3, true), + 'body' => $faker->sentences(3, true), + 'creator_id' => function () { + return factory(User::class)->create()->id; + }, + 'status_id' => 0, + 'priority_id' => 1, + ]; +}); diff --git a/database/migrations/2019_03_03_210017_create_issues_table.php b/database/migrations/2019_03_03_210017_create_issues_table.php new file mode 100644 index 0000000..ea55458 --- /dev/null +++ b/database/migrations/2019_03_03_210017_create_issues_table.php @@ -0,0 +1,38 @@ +increments('id'); + $table->unsignedInteger('project_id'); + $table->string('title', 60); + $table->string('body'); + $table->unsignedInteger('creator_id'); + $table->unsignedTinyInteger('priority_id'); + $table->unsignedInteger('pic_id')->nullable(); + $table->unsignedTinyInteger('status_id')->default(0); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('issues'); + } +} diff --git a/resources/lang/de/app.php b/resources/lang/de/app.php index 2547e4e..6b6d461 100644 --- a/resources/lang/de/app.php +++ b/resources/lang/de/app.php @@ -17,6 +17,7 @@ return [ 'total' => 'gesamt', 'count' => 'Summe', 'remark' => 'Remark', + 'last_update' => 'Last Update', // Action 'add' => 'Hinzufügen', diff --git a/resources/lang/de/issue.php b/resources/lang/de/issue.php new file mode 100644 index 0000000..2841cb4 --- /dev/null +++ b/resources/lang/de/issue.php @@ -0,0 +1,57 @@ + 'Issue', + 'list' => 'Issue List', + 'search' => 'Search Issue', + 'search_text' => 'Title ...', + 'all' => 'All Issue', + 'select' => 'Select Issue', + 'detail' => 'Issue Detail', + 'not_found' => 'Issue not found.', + 'empty' => 'Issue is empty.', + 'back_to_show' => 'Back to Issue Detail', + 'back_to_index' => 'Back to Issue List', + 'no_pic' => 'No issue PIC', + + // Actions + 'create' => 'Create new Issue', + 'created' => 'A new Issue has been created.', + 'show' => 'View Issue Detail', + 'edit' => 'Edit Issue', + 'update' => 'Update Issue', + 'updated' => 'Issue data has been updated.', + 'delete' => 'Delete Issue', + 'delete_confirm' => 'Are you sure to delete this Issue?', + 'deleted' => 'Issue has been deleted.', + 'undeleted' => 'Issue not deleted.', + 'undeleteable' => 'Issue data cannot be deleted.', + 'assign_pic' => 'Assign PIC', + 'select_pic' => 'Select a PIC', + 'pic_assigned' => 'Issue PIC has been assigned.', + 'pic_removed' => 'Issue PIC has been removed.', + + // Attributes + 'title' => 'Issue Title', + 'body' => 'Issue Description', + + // Relations + 'project' => 'Issue Project', + 'pic' => 'Issue PIC', + 'creator' => 'Issue Creator', + + // Priority + 'minor' => 'Minor', + 'major' => 'Major', + 'critical' => 'Critical', + 'all_priority' => 'All Priority', + + // Statuses + 'open' => 'Open', + 'resolved' => 'Resolved', + 'closed' => 'Closed', + 'on_hold' => 'On Hold', + 'invalid' => 'Invalid', + 'all_status' => 'All Status', +]; diff --git a/resources/lang/de/project.php b/resources/lang/de/project.php index fcbeb7f..07898c5 100644 --- a/resources/lang/de/project.php +++ b/resources/lang/de/project.php @@ -71,6 +71,7 @@ return [ 'subscriptions' => 'Abonnements', 'status' => 'Projektstatus', 'payments' => 'Zahlungen', + 'issues' => 'Issues', // Statuses 'planned' => 'geplant', diff --git a/resources/lang/en/app.php b/resources/lang/en/app.php index 2f55d05..2fb3794 100644 --- a/resources/lang/en/app.php +++ b/resources/lang/en/app.php @@ -17,6 +17,7 @@ return [ 'total' => 'Total', 'count' => 'Count', 'remark' => 'Remark', + 'last_update' => 'Last Update', // Action 'add' => 'Add', diff --git a/resources/lang/en/issue.php b/resources/lang/en/issue.php new file mode 100644 index 0000000..4c68a2a --- /dev/null +++ b/resources/lang/en/issue.php @@ -0,0 +1,58 @@ + 'Issue', + 'list' => 'Issue List', + 'search' => 'Search Issue', + 'search_text' => 'Title ...', + 'all' => 'All Issue', + 'select' => 'Select Issue', + 'detail' => 'Issue Detail', + 'not_found' => 'Issue not found.', + 'empty' => 'Issue is empty.', + 'back_to_show' => 'Back to Issue Detail', + 'back_to_index' => 'Back to Issue List', + 'no_pic' => 'No issue PIC', + + // Actions + 'create' => 'Create new Issue', + 'created' => 'A new Issue has been created.', + 'show' => 'View Issue Detail', + 'edit' => 'Edit Issue', + 'update' => 'Update Issue', + 'updated' => 'Issue data has been updated.', + 'delete' => 'Delete Issue', + 'delete_confirm' => 'Are you sure to delete this Issue?', + 'deleted' => 'Issue has been deleted.', + 'undeleted' => 'Issue not deleted.', + 'undeleteable' => 'Issue data cannot be deleted.', + 'assign_pic' => 'Assign PIC', + 'select_pic' => 'Select a PIC', + 'pic_assigned' => 'Issue PIC has been assigned.', + 'pic_removed' => 'Issue PIC has been removed.', + + // Attributes + 'title' => 'Issue Title', + 'body' => 'Issue Description', + + // Relations + 'project' => 'Issue Project', + 'pic' => 'Issue PIC', + 'creator' => 'Issue Creator', + + // Priority + 'priority' => 'Priority', + 'minor' => 'Minor', + 'major' => 'Major', + 'critical' => 'Critical', + 'all_priority' => 'All Priority', + + // Statuses + 'open' => 'Open', + 'resolved' => 'Resolved', + 'closed' => 'Closed', + 'on_hold' => 'On Hold', + 'invalid' => 'Invalid', + 'all_status' => 'All Status', +]; diff --git a/resources/lang/en/project.php b/resources/lang/en/project.php index 31886ef..c3a5c40 100644 --- a/resources/lang/en/project.php +++ b/resources/lang/en/project.php @@ -71,6 +71,7 @@ return [ 'subscriptions' => 'Subscriptions', 'status' => 'Project Status', 'payments' => 'Payments', + 'issues' => 'Issues', // Statuses 'planned' => 'Planned', diff --git a/resources/lang/id/app.php b/resources/lang/id/app.php index 3906fd0..99fffa1 100644 --- a/resources/lang/id/app.php +++ b/resources/lang/id/app.php @@ -17,6 +17,7 @@ return [ 'total' => 'Total', 'count' => 'Jumlah', 'remark' => 'Keterangan', + 'last_update' => 'Update', // Action 'add' => 'Tambah', diff --git a/resources/lang/id/comment.php b/resources/lang/id/comment.php index 95a1586..b79cd50 100644 --- a/resources/lang/id/comment.php +++ b/resources/lang/id/comment.php @@ -12,7 +12,7 @@ return [ 'created' => 'Input Komentar berhasil.', 'edit' => 'Edit Komentar', 'update' => 'Update Komentar', - 'updated' => 'Update data Komentar telah berhasil.', + 'updated' => 'Update Komentar berhasil.', 'delete' => 'Hapus Komentar', 'delete_confirm' => 'Anda yakin akan menghapus Komentar ini?', 'deleted' => 'Komentar berhasil dihapus.', diff --git a/resources/lang/id/issue.php b/resources/lang/id/issue.php new file mode 100644 index 0000000..120fd46 --- /dev/null +++ b/resources/lang/id/issue.php @@ -0,0 +1,57 @@ + 'Issue', + 'list' => 'Daftar Issue', + 'search' => 'Cari Issue', + 'search_text' => 'Nama ...', + 'all' => 'Semua Issue', + 'select' => 'Pilih Issue', + 'detail' => 'Detail Issue', + 'not_found' => 'Issue tidak ditemukan.', + 'empty' => 'Belum ada Issue', + 'back_to_show' => 'Kembali ke detail Issue', + 'back_to_index' => 'Kembali ke daftar Issue', + 'no_pic' => 'Belum ada PIC', + + // Actions + 'create' => 'Input Issue Baru', + 'created' => 'Input Issue baru telah berhasil.', + 'show' => 'Lihat Detail Issue', + 'edit' => 'Edit Issue', + 'update' => 'Update Issue', + 'updated' => 'Update data Issue telah berhasil.', + 'delete' => 'Hapus Issue', + 'delete_confirm' => 'Anda yakin akan menghapus Issue ini?', + 'deleted' => 'Hapus data Issue telah berhasil.', + 'undeleted' => 'Data Issue gagal dihapus.', + 'undeleteable' => 'Data Issue tidak dapat dihapus.', + 'assign_pic' => 'Tugaskan PIC', + 'select_pic' => 'Pilih PIC', + 'pic_assigned' => 'PIC telah ditugaskan.', + 'pic_removed' => 'PIC telah dihapus.', + + // Attributes + 'title' => 'Judul Issue', + 'body' => 'Deskripsi Issue', + + // Relations + 'project' => 'Project Issue', + 'pic' => 'PIC Issue', + 'creator' => 'Pembuat Issue', + + // Priority + 'minor' => 'Minor', + 'major' => 'Major', + 'critical' => 'Critical', + 'all_priority' => 'Semua Priority', + + // Statuses + 'open' => 'Open', + 'resolved' => 'Selesai', + 'closed' => 'Ditutup', + 'on_hold' => 'Ditunda', + 'invalid' => 'Tidak Valid', + 'all_status' => 'Semua Status', +]; diff --git a/resources/lang/id/project.php b/resources/lang/id/project.php index 998a19b..2ea3b04 100644 --- a/resources/lang/id/project.php +++ b/resources/lang/id/project.php @@ -71,6 +71,7 @@ return [ 'subscriptions' => 'Langganan', 'status' => 'Status Project', 'payments' => 'Pembayaran', + 'issues' => 'Issue', // Statuses 'planned' => 'Rencana', diff --git a/resources/views/projects/issues/create.blade.php b/resources/views/projects/issues/create.blade.php new file mode 100755 index 0000000..1422715 --- /dev/null +++ b/resources/views/projects/issues/create.blade.php @@ -0,0 +1,32 @@ +@extends('layouts.project') + +@section('subtitle', __('issue.create')) + +@section('action-buttons') +@can('create', new App\Entities\Projects\Issue) + {!! html_link_to_route('projects.issues.create', __('issue.create'), $project, ['class' => 'btn btn-success', 'icon' => 'plus']) !!} +@endcan +@endsection + +@section('content-project') + +
+
+ {{ Form::open(['route' => ['projects.issues.store', $project]]) }} +
+

{{ __('issue.create') }}

+
+ {!! FormField::text('title', ['label' => __('issue.title')]) !!} + {!! FormField::textarea('body', ['label' => __('issue.body')]) !!} + {!! FormField::radios('priority_id', $priorities, ['label' => __('issue.priority'), 'placeholder' => false]) !!} + {!! FormField::select('pic_id', $users, ['label' => __('issue.pic')]) !!} +
+ +
+ {{ Form::close() }} +
+
+@endsection diff --git a/resources/views/projects/issues/edit.blade.php b/resources/views/projects/issues/edit.blade.php new file mode 100755 index 0000000..0de5462 --- /dev/null +++ b/resources/views/projects/issues/edit.blade.php @@ -0,0 +1,54 @@ +@extends('layouts.project') + +@section('subtitle', __('issue.update')) + +@section('action-buttons') +@can('create', new App\Entities\Projects\Issue) + {!! html_link_to_route('projects.issues.create', __('issue.create'), $project, ['class' => 'btn btn-success', 'icon' => 'plus']) !!} +@endcan +@endsection + +@section('content-project') +
+
+ @if (request('action') == 'delete' && $issue) +
+

{{ __('issue.delete') }}

+
+ +

{{ $issue->title }}

+ +

{{ $issue->body }}

+ {!! $errors->first('issue_id', ':message') !!} +
+
+
{{ __('issue.delete_confirm') }}
+ +
+ @else + {{ Form::model($issue, ['route' => ['projects.issues.update', $project, $issue], 'method' => 'patch']) }} +
+

{{ __('issue.update') }}

+
+ {!! FormField::text('title', ['label' => __('issue.title')]) !!} + {!! FormField::textarea('body', ['label' => __('issue.body')]) !!} +
+ +
+ {{ Form::close() }} + @endif +
+
+@endsection diff --git a/resources/views/projects/issues/index.blade.php b/resources/views/projects/issues/index.blade.php new file mode 100755 index 0000000..383a6bf --- /dev/null +++ b/resources/views/projects/issues/index.blade.php @@ -0,0 +1,67 @@ +@inject('priorities', 'App\Entities\Projects\Priority') +@inject('issueStatuses', 'App\Entities\Projects\IssueStatus') +@extends('layouts.project') + +@section('subtitle', __('project.issues')) + +@section('action-buttons') +{{ Form::open(['method' => 'get', 'class' => 'form-inline', 'style' => 'display:inline']) }} +{!! FormField::select('priority_id', $priorities::toArray(), ['label' => false, 'placeholder' => __('issue.all_priority'), 'value' => request('priority_id')]) !!} +{!! FormField::select('status_id', $issueStatuses::toArray(), ['label' => false, 'placeholder' => __('issue.all_status'), 'value' => request('status_id')]) !!} +{{ Form::submit(__('app.filter'), ['class' => 'btn btn-info']) }} +@if (request(['priority_id', 'status_id'])) + {{ link_to_route('projects.issues.index', __('app.reset'), $project, ['class' => 'btn btn-default']) }} +@endif +{{ Form::close() }} +@can('create', new App\Entities\Projects\Issue) + {!! html_link_to_route('projects.issues.create', __('issue.create'), $project, ['class' => 'btn btn-success', 'icon' => 'plus']) !!} +@endcan +@endsection + +@section('content-project') +
+
+

{{ __('project.issues') }}

+
+ + + + + + + + + + + + + + @forelse($issues as $key => $issue) + @php + $no = 1 + $key; + @endphp + + + + + + + + + + + + @empty + + @endforelse + +
{{ __('app.table_no') }}{{ __('issue.title') }}{{ __('issue.priority') }}{{ __('app.status') }}{{ __('comment.comment') }}{{ __('issue.pic') }}{{ __('issue.creator') }}{{ __('app.last_update') }}{{ __('app.action') }}
{{ $no }}{{ $issue->title }}{!! $issue->priority_label !!}{!! $issue->status_label !!}{{ $issue->comments_count }}{{ $issue->pic->name }}{{ $issue->creator->name }}{{ $issue->updated_at->diffForHumans() }} + {{ link_to_route( + 'projects.issues.show', + __('app.show'), + [$project, $issue], + ['title' => __('issue.show')] + ) }} +
{{ __('issue.not_found') }}
+
+@endsection diff --git a/resources/views/projects/issues/partials/comment-section.blade.php b/resources/views/projects/issues/partials/comment-section.blade.php new file mode 100644 index 0000000..73eec00 --- /dev/null +++ b/resources/views/projects/issues/partials/comment-section.blade.php @@ -0,0 +1,54 @@ +@foreach($comments as $comment) +
+ + {{ $comment->time_display }} + {{ $comment->creator->name }} + +
+ @can('update', $comment) + {{ link_to_route('projects.issues.show', __('app.edit'), [$project, $issue, 'action' => 'comment-edit', 'comment_id' => $comment->id], ['id' => 'edit-comment-'.$comment->id, 'class' => 'small', 'title' => __('comment.edit')]) }} + @endcan + @can('delete', $comment) + {!! FormField::delete( + ['route' => ['issues.comments.destroy', $issue, $comment], 'class' => ''], + '×', + ['class' => 'btn-link', 'id' => 'delete-comment-'.$comment->id], + ['comment_id' => $comment->id] + ) !!} + @endcan +
+ {!! nl2br($comment->body) !!} +
+@endforeach + +@can('comment-on', $issue) +{{ Form::open(['route' => ['issues.comments.store', $issue]]) }} +{!! FormField::textarea('body', ['required' => true, 'label' => false, 'placeholder' => __('comment.create_text')]) !!} +{{ Form::submit(__('comment.create'), ['class' => 'btn btn-success pull-right']) }} +{{ Form::close() }} +

+@endcan + +@if (Request::get('action') == 'comment-edit' && $editableComment) + +@endif diff --git a/resources/views/projects/issues/show.blade.php b/resources/views/projects/issues/show.blade.php new file mode 100755 index 0000000..beb3017 --- /dev/null +++ b/resources/views/projects/issues/show.blade.php @@ -0,0 +1,65 @@ +@extends('layouts.project') + +@section('subtitle', __('issue.detail')) + +@section('action-buttons') +@can('create', new App\Entities\Projects\Issue) + {!! html_link_to_route('projects.issues.create', __('issue.create'), $project, ['class' => 'btn btn-success', 'icon' => 'plus']) !!} +@endcan +@endsection + +@section('content-project') +
+
+
+
+

+
{!! $issue->status_label !!}
+ {{ __('issue.detail') }} +

+
+ + + + + + + + +
{{ __('issue.title') }}{{ $issue->title }}
{{ __('issue.body') }}{{ $issue->body }}
{{ __('issue.priority') }}{!! $issue->priority_label !!}
{{ __('issue.pic') }}{{ $issue->pic->name }}
{{ __('app.created_by') }}{{ $issue->creator->name }}
+ +
+
+ @include('projects.issues.partials.comment-section') +
+
+ {{ Form::model($issue, ['route' => ['issues.options.update', $issue], 'method' => 'patch']) }} +
+

{{ __('app.action') }}

+
+ {!! FormField::radios('priority_id', $priorities, ['label' => __('issue.priority')]) !!} + {!! FormField::radios('status_id', $statuses, ['label' => __('app.status')]) !!} + {!! FormField::select('pic_id', $users, ['label' => __('issue.assign_pic'), 'placeholder' => __('issue.select_pic')]) !!} +
+ +
+ {{ Form::close() }} +
+
+@endsection + +@section('script') + +@endsection diff --git a/resources/views/projects/partials/nav-tabs.blade.php b/resources/views/projects/partials/nav-tabs.blade.php index ca809c1..ed2ee19 100644 --- a/resources/views/projects/partials/nav-tabs.blade.php +++ b/resources/views/projects/partials/nav-tabs.blade.php @@ -8,6 +8,9 @@ {!! link_to_route('projects.jobs.index', __('project.jobs').' ('.$project->jobs->count().')', $project) !!} @endcan +
  • + {!! link_to_route('projects.issues.index', __('project.issues').' ('.$project->issues->count().')', $project) !!} +
  • @can('view-comments', $project)
  • {!! link_to_route('projects.comments.index', __('comment.list').' ('.$project->comments->count().')', $project) !!} diff --git a/routes/web/projects.php b/routes/web/projects.php index c1b6deb..2f4ae02 100644 --- a/routes/web/projects.php +++ b/routes/web/projects.php @@ -46,6 +46,17 @@ Route::group(['middleware' => ['auth'], 'namespace' => 'Projects'], function () Route::delete('projects/{project}/comments/{comment}', 'CommentsController@destroy')->name('projects.comments.destroy'); /* + * Project Issues Routes + */ + Route::get('projects/{project}/issues', 'IssueController@index')->name('projects.issues.index'); + Route::get('projects/{project}/issues/create', 'IssueController@create')->name('projects.issues.create'); + Route::post('projects/{project}/issues', 'IssueController@store')->name('projects.issues.store'); + Route::get('projects/{project}/issues/{issue}', 'IssueController@show')->name('projects.issues.show'); + Route::get('projects/{project}/issues/{issue}/edit', 'IssueController@edit')->name('projects.issues.edit'); + Route::patch('projects/{project}/issues/{issue}', 'IssueController@update')->name('projects.issues.update'); + Route::delete('projects/{project}/issues/{issue}', 'IssueController@destroy')->name('projects.issues.destroy'); + + /* * Tasks Routes */ Route::get('jobs/{job}/tasks/create', ['as' => 'tasks.create', 'uses' => 'TasksController@create']); @@ -89,3 +100,15 @@ Route::group(['middleware' => ['auth']], function () { Route::patch('jobs/{job}/comments/{comment}', 'Jobs\CommentsController@update')->name('jobs.comments.update'); Route::delete('jobs/{job}/comments/{comment}', 'Jobs\CommentsController@destroy')->name('jobs.comments.destroy'); }); + +/** + * Issue Options Routes + */ +Route::patch('issues/{issue}/options', 'Issues\OptionController@update')->name('issues.options.update'); + +/** + * Issue Comments Routes + */ +Route::post('issues/{issue}/comments', 'Issues\CommentController@store')->name('issues.comments.store'); +Route::patch('issues/{issue}/comments/{comment}', 'Issues\CommentController@update')->name('issues.comments.update'); +Route::delete('issues/{issue}/comments/{comment}', 'Issues\CommentController@destroy')->name('issues.comments.destroy'); diff --git a/tests/Feature/Projects/IssueCommentsTest.php b/tests/Feature/Projects/IssueCommentsTest.php new file mode 100644 index 0000000..a85c290 --- /dev/null +++ b/tests/Feature/Projects/IssueCommentsTest.php @@ -0,0 +1,106 @@ +adminUserSigningIn(); + $issue = factory(Issue::class)->create(); + $comment = factory(Comment::class)->create([ + 'commentable_type' => 'issues', + 'commentable_id' => $issue->id, + 'body' => 'This is issue comment.', + ]); + + $this->visitRoute('projects.issues.show', [$issue->project, $issue]); + + $this->seeText('This is issue comment.'); + } + + /** @test */ + public function admin_can_add_comment_to_an_issue() + { + $admin = $this->adminUserSigningIn(); + $issue = factory(Issue::class)->create(); + + $this->visitRoute('projects.issues.show', [$issue->project, $issue]); + + $this->submitForm(__('comment.create'), [ + 'body' => 'First comment.', + ]); + + $this->seePageIs(route('projects.issues.show', [$issue->project, $issue])); + $this->see(__('comment.created')); + + $this->seeInDatabase('comments', [ + 'commentable_type' => 'issues', + 'commentable_id' => $issue->id, + 'body' => 'First comment.', + 'creator_id' => $admin->id, + ]); + } + + /** @test */ + public function user_can_edit_an_issue_comment() + { + $this->adminUserSigningIn(); + $issue = factory(Issue::class)->create(); + $comment = factory(Comment::class)->create([ + 'commentable_type' => 'issues', + 'commentable_id' => $issue->id, + 'body' => 'This is issue comment.', + ]); + + $this->visitRoute('projects.issues.show', [$issue->project, $issue]); + $this->seeElement('a', ['id' => 'edit-comment-'.$comment->id]); + $this->click('edit-comment-'.$comment->id); + $this->seeRouteIs('projects.issues.show', [$issue->project, $issue, 'action' => 'comment-edit', 'comment_id' => $comment->id]); + + $this->submitForm(__('comment.update'), [ + 'body' => 'Edited comment.', + ]); + + $this->seePageIs(route('projects.issues.show', [$issue->project, $issue])); + $this->see(__('comment.updated')); + + $this->seeInDatabase('comments', [ + 'id' => $comment->id, + 'commentable_type' => 'issues', + 'commentable_id' => $issue->id, + 'body' => 'Edited comment.', + ]); + } + + /** @test */ + public function user_can_delete_an_issue_comment() + { + $this->adminUserSigningIn(); + $issue = factory(Issue::class)->create(); + $comment = factory(Comment::class)->create([ + 'commentable_type' => 'issues', + 'commentable_id' => $issue->id, + 'body' => 'This is issue comment.', + ]); + + $this->visitRoute('projects.issues.show', [$issue->project, $issue]); + $this->seeElement('button', ['id' => 'delete-comment-'.$comment->id]); + $this->press('delete-comment-'.$comment->id); + + $this->seePageIs(route('projects.issues.show', [$issue->project, $issue])); + $this->see(__('comment.deleted')); + + $this->dontSeeInDatabase('comments', [ + 'id' => $comment->id, + ]); + } +} diff --git a/tests/Feature/Projects/ProjectIssuesTest.php b/tests/Feature/Projects/ProjectIssuesTest.php new file mode 100644 index 0000000..9d27f9f --- /dev/null +++ b/tests/Feature/Projects/ProjectIssuesTest.php @@ -0,0 +1,219 @@ +adminUserSigningIn(); + $project = factory(Project::class)->create(); + $issue = factory(Issue::class)->create([ + 'project_id' => $project->id, + 'title' => 'The issue title.', + 'body' => 'This is a project issue body.', + ]); + + $this->visitRoute('projects.issues.index', $project); + $this->seeRouteIs('projects.issues.index', $project); + + $this->seeText('The issue title.'); + } + + /** @test */ + public function admin_can_add_issue_to_a_project() + { + $admin = $this->adminUserSigningIn(); + $project = factory(Project::class)->create(); + + $this->visitRoute('projects.issues.create', $project); + + $this->submitForm(__('issue.create'), [ + 'title' => 'First Issue.', + 'body' => 'First Issue description.', + 'priority_id' => 1, + 'pic_id' => $admin->id, + ]); + + $this->seePageIs(route('projects.issues.index', $project)); + $this->see(__('issue.created')); + + $this->seeInDatabase('issues', [ + 'project_id' => $project->id, + 'title' => 'First Issue.', + 'body' => 'First Issue description.', + 'priority_id' => 1, + 'pic_id' => $admin->id, + 'creator_id' => $admin->id, + ]); + } + + /** @test */ + public function user_can_view_an_issue_detail() + { + $this->adminUserSigningIn(); + $project = factory(Project::class)->create(); + $issue = factory(Issue::class)->create([ + 'project_id' => $project->id, + 'title' => 'The issue title.', + 'body' => 'This is a project issue body.', + ]); + + $this->visitRoute('projects.issues.show', [$project, $issue]); + $this->seeText($issue->title); + $this->seeText($issue->body); + } + + /** @test */ + public function user_can_edit_issue() + { + $this->adminUserSigningIn(); + $project = factory(Project::class)->create(); + $issue = factory(Issue::class)->create([ + 'project_id' => $project->id, + 'title' => 'The issue title.', + 'body' => 'This is a project issue body.', + ]); + + $this->visitRoute('projects.issues.show', [$project, $issue]); + $this->seeElement('a', ['id' => 'edit-issue-'.$issue->id]); + $this->click('edit-issue-'.$issue->id); + $this->seeRouteIs('projects.issues.edit', [$project, $issue]); + + $this->submitForm(__('issue.update'), [ + 'title' => 'First Issue.', + 'body' => 'This is a project issue body.', + ]); + + $this->seePageIs(route('projects.issues.show', [$project, $issue])); + $this->see(__('issue.updated')); + + $this->seeInDatabase('issues', [ + 'id' => $issue->id, + 'project_id' => $project->id, + 'title' => 'First Issue.', + 'body' => 'This is a project issue body.', + ]); + } + + /** @test */ + public function user_can_delete_issue() + { + $this->adminUserSigningIn(); + $project = factory(Project::class)->create(); + $issue = factory(Issue::class)->create([ + 'project_id' => $project->id, + ]); + + $this->visitRoute('projects.issues.edit', [$project, $issue]); + $this->seeElement('a', ['id' => 'delete-issue-'.$issue->id]); + + $this->click('delete-issue-'.$issue->id); + + $this->seePageIs(route('projects.issues.edit', [$project, $issue, 'action' => 'delete'])); + $this->seeElement('button', ['id' => 'delete-issue-'.$issue->id]); + + $this->press('delete-issue-'.$issue->id); + + $this->seePageIs(route('projects.issues.index', $project)); + $this->seeText(__('issue.deleted')); + + $this->dontSeeInDatabase('issues', [ + 'id' => $issue->id, + ]); + } + + /** @test */ + public function user_can_assign_someone_to_an_issue_as_pic() + { + $this->adminUserSigningIn(); + $worker = $this->createUser('worker'); + $issue = factory(Issue::class)->create(); + + $this->visitRoute('projects.issues.show', [$issue->project, $issue]); + $this->submitForm(__('issue.update'), [ + 'pic_id' => $worker->id, + ]); + $this->seeRouteIs('projects.issues.show', [$issue->project, $issue]); + $this->seeText(__('issue.updated')); + + $this->seeInDatabase('issues', [ + 'id' => $issue->id, + 'pic_id' => $worker->id, + ]); + } + + /** @test */ + public function user_can_remove_pic_assignment() + { + $this->adminUserSigningIn(); + $worker = $this->createUser('worker'); + $issue = factory(Issue::class)->create(['pic_id' => $worker->id]); + + $this->visitRoute('projects.issues.show', [$issue->project, $issue]); + $this->submitForm(__('issue.update'), [ + 'pic_id' => null, + ]); + $this->seeRouteIs('projects.issues.show', [$issue->project, $issue]); + $this->seeText(__('issue.updated')); + + $this->seeInDatabase('issues', [ + 'id' => $issue->id, + 'pic_id' => null, + ]); + } + + /** @test */ + public function user_can_change_issue_status() + { + $this->adminUserSigningIn(); + $worker = $this->createUser('worker'); + $issue = factory(Issue::class)->create(); + + $this->visitRoute('projects.issues.show', [$issue->project, $issue]); + $this->submitForm(__('issue.update'), [ + 'status_id' => 2, // resolved + 'pic_id' => $worker->id, + ]); + $this->seeRouteIs('projects.issues.show', [$issue->project, $issue]); + $this->seeText(__('issue.updated')); + + $this->seeInDatabase('issues', [ + 'id' => $issue->id, + 'pic_id' => $worker->id, + 'status_id' => 2, // resolved + ]); + } + + /** @test */ + public function user_can_change_issue_priority() + { + $this->adminUserSigningIn(); + $worker = $this->createUser('worker'); + $issue = factory(Issue::class)->create(); + + $this->visitRoute('projects.issues.show', [$issue->project, $issue]); + $this->submitForm(__('issue.update'), [ + 'priority_id' => 2, // major + 'status_id' => 2, // resolved + 'pic_id' => $worker->id, + ]); + $this->seeRouteIs('projects.issues.show', [$issue->project, $issue]); + $this->seeText(__('issue.updated')); + + $this->seeInDatabase('issues', [ + 'id' => $issue->id, + 'pic_id' => $worker->id, + 'priority_id' => 2, // major + 'status_id' => 2, // resolved + ]); + } +} diff --git a/tests/Unit/Models/IssueTest.php b/tests/Unit/Models/IssueTest.php new file mode 100644 index 0000000..a43ed72 --- /dev/null +++ b/tests/Unit/Models/IssueTest.php @@ -0,0 +1,99 @@ +make(); + + $this->assertInstanceOf(Project::class, $issue->project); + $this->assertEquals($issue->project_id, $issue->project->id); + } + + /** @test */ + public function an_issue_has_belongs_to_pic_relation() + { + $pic = $this->createUser('worker'); + $issue = factory(Issue::class)->make(['pic_id' => $pic->id]); + + $this->assertInstanceOf(User::class, $issue->pic); + $this->assertEquals($issue->pic_id, $issue->pic->id); + } + + /** @test */ + public function issue_pic_name_has_default_value() + { + $issue = factory(Issue::class)->make(['pic_id' => null]); + + $this->assertEquals(__('issue.no_pic'), $issue->pic->name); + } + + /** @test */ + public function an_issue_has_belongs_to_creator_relation() + { + $issue = factory(Issue::class)->make(); + + $this->assertInstanceOf(User::class, $issue->creator); + $this->assertEquals($issue->creator_id, $issue->creator->id); + } + + /** @test */ + public function an_issue_has_status_attribute() + { + $issue = factory(Issue::class)->make(); + + $this->assertEquals(__('issue.open'), $issue->status); + } + + /** @test */ + public function an_issue_has_status_label_attribute() + { + $issue = factory(Issue::class)->make(); + + $this->assertEquals(''.$issue->status.'', $issue->status_label); + } + + /** @test */ + public function an_issue_has_priority_attribute() + { + $issue = factory(Issue::class)->make(); + + $this->assertEquals(__('issue.minor'), $issue->priority); + } + + /** @test */ + public function an_issue_has_priority_label_attribute() + { + $issue = factory(Issue::class)->make(); + $colorClass = Priority::getColorById($issue->priority_id); + + $this->assertEquals(''.$issue->priority.'', $issue->priority_label); + } + + /** @test */ + public function an_issue_has_many_comments_relation() + { + $issue = factory(Issue::class)->create(); + $comment = factory(Comment::class)->create([ + 'commentable_type' => 'issues', + 'commentable_id' => $issue->id, + ]); + + $this->assertInstanceOf(Collection::class, $issue->comments); + $this->assertInstanceOf(Comment::class, $issue->comments->first()); + } +} diff --git a/tests/Unit/Models/ProjectTest.php b/tests/Unit/Models/ProjectTest.php index ff9a2f7..5164333 100644 --- a/tests/Unit/Models/ProjectTest.php +++ b/tests/Unit/Models/ProjectTest.php @@ -6,6 +6,7 @@ use Tests\TestCase; use App\Entities\Projects\Job; use App\Entities\Projects\File; use App\Entities\Projects\Task; +use App\Entities\Projects\Issue; use App\Entities\Invoices\Invoice; use App\Entities\Payments\Payment; use App\Entities\Projects\Comment; @@ -350,4 +351,14 @@ class ProjectTest extends TestCase 'project_id' => $project->id, ]); } + + /** @test */ + public function a_project_has_many_issues_relation() + { + $project = factory(Project::class)->create(); + $issue = factory(Issue::class)->create(['project_id' => $project->id]); + + $this->assertInstanceOf(Collection::class, $project->issues); + $this->assertInstanceOf(Issue::class, $project->issues->first()); + } } diff --git a/tests/Unit/Policies/IssuePolicyTest.php b/tests/Unit/Policies/IssuePolicyTest.php new file mode 100644 index 0000000..a5b1016 --- /dev/null +++ b/tests/Unit/Policies/IssuePolicyTest.php @@ -0,0 +1,34 @@ + + */ +class IssuePolicyTest extends TestCase +{ + use RefreshDatabase; + + /** @test */ + public function admin_can_create_issue() + { + $admin = $this->createUser('admin'); + + $this->assertTrue($admin->can('create', new Issue())); + } + + /** @test */ + public function admin_can_add_comment_to_an_issue() + { + $admin = $this->createUser('admin'); + $issue = factory(Issue::class)->create(); + + $this->assertTrue($admin->can('comment-on', $issue)); + } +}