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') + +
{{ $issue->title }}
+ +{{ $issue->body }}
+ {!! $errors->first('issue_id', ':message') !!} +| {{ __('app.table_no') }} | +{{ __('issue.title') }} | +{{ __('issue.priority') }} | +{{ __('app.status') }} | +{{ __('comment.comment') }} | +{{ __('issue.pic') }} | +{{ __('issue.creator') }} | +{{ __('app.last_update') }} | +{{ __('app.action') }} | + + + @forelse($issues as $key => $issue) + @php + $no = 1 + $key; + @endphp +
|---|---|---|---|---|---|---|---|---|
| {{ $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') }} | ||||||||
| {{ __('issue.title') }} | {{ $issue->title }} |
|---|---|
| {{ __('issue.body') }} | {{ $issue->body }} |
| {{ __('issue.priority') }} | {!! $issue->priority_label !!} |
| {{ __('issue.pic') }} | {{ $issue->pic->name }} |
| {{ __('app.created_by') }} | {{ $issue->creator->name }} |