diff --git a/app/Entities/Projects/Job.php b/app/Entities/Projects/Job.php index cb87fb5..e658f17 100755 --- a/app/Entities/Projects/Job.php +++ b/app/Entities/Projects/Job.php @@ -17,6 +17,17 @@ class Job extends Model use PresentableTrait; /** + * The event map for the model. + * + * @var array + */ + protected $dispatchesEvents = [ + 'created' => 'App\Events\Jobs\Created', + 'updated' => 'App\Events\Jobs\Updated', + 'deleted' => 'App\Events\Jobs\Deleted', + ]; + + /** * @var \App\Entities\Projects\JobPresenter */ protected $presenter = JobPresenter::class; diff --git a/app/Entities/Projects/Project.php b/app/Entities/Projects/Project.php index 9834a57..47b56e8 100755 --- a/app/Entities/Projects/Project.php +++ b/app/Entities/Projects/Project.php @@ -18,6 +18,15 @@ use Laracasts\Presenter\PresentableTrait; class Project extends Model { use PresentableTrait; + /** + * The event map for the model. + * + * @var array + */ + protected $dispatchesEvents = [ + 'created' => 'App\Events\Projects\Created', + 'updated' => 'App\Events\Projects\Updated', + ]; /** * @var \App\Entities\Projects\ProjectPresenter diff --git a/app/Entities/Projects/Task.php b/app/Entities/Projects/Task.php index cb5e78a..c9167bf 100755 --- a/app/Entities/Projects/Task.php +++ b/app/Entities/Projects/Task.php @@ -6,6 +6,17 @@ use Illuminate\Database\Eloquent\Model; class Task extends Model { + /** + * The event map for the model. + * + * @var array + */ + protected $dispatchesEvents = [ + 'created' => 'App\Events\Tasks\Created', + 'updated' => 'App\Events\Tasks\Updated', + 'deleted' => 'App\Events\Tasks\Deleted', + ]; + protected $guarded = ['id', 'created_at', 'updated_at']; protected $touches = ['job']; diff --git a/app/Entities/Users/Activity.php b/app/Entities/Users/Activity.php new file mode 100644 index 0000000..bc0b04d --- /dev/null +++ b/app/Entities/Users/Activity.php @@ -0,0 +1,24 @@ + 'array']; + + public function user() + { + return $this->belongsTo(User::class)->withDefault(['name' => 'n/a']); + } + + public function object() + { + return $this->morphTo(); + } +} diff --git a/app/Events/Jobs/Created.php b/app/Events/Jobs/Created.php new file mode 100644 index 0000000..624967a --- /dev/null +++ b/app/Events/Jobs/Created.php @@ -0,0 +1,15 @@ +job = $job; + } +} diff --git a/app/Events/Jobs/Deleted.php b/app/Events/Jobs/Deleted.php new file mode 100644 index 0000000..b438550 --- /dev/null +++ b/app/Events/Jobs/Deleted.php @@ -0,0 +1,15 @@ +job = $job; + } +} diff --git a/app/Events/Jobs/Updated.php b/app/Events/Jobs/Updated.php new file mode 100644 index 0000000..e6ed3f1 --- /dev/null +++ b/app/Events/Jobs/Updated.php @@ -0,0 +1,15 @@ +job = $job; + } +} diff --git a/app/Events/Projects/Created.php b/app/Events/Projects/Created.php new file mode 100644 index 0000000..aa78dd6 --- /dev/null +++ b/app/Events/Projects/Created.php @@ -0,0 +1,15 @@ +project = $project; + } +} diff --git a/app/Events/Projects/Updated.php b/app/Events/Projects/Updated.php new file mode 100644 index 0000000..d634086 --- /dev/null +++ b/app/Events/Projects/Updated.php @@ -0,0 +1,15 @@ +project = $project; + } +} diff --git a/app/Events/Tasks/Created.php b/app/Events/Tasks/Created.php new file mode 100644 index 0000000..fa0ef0c --- /dev/null +++ b/app/Events/Tasks/Created.php @@ -0,0 +1,15 @@ +task = $task; + } +} diff --git a/app/Events/Tasks/Deleted.php b/app/Events/Tasks/Deleted.php new file mode 100644 index 0000000..a7b613e --- /dev/null +++ b/app/Events/Tasks/Deleted.php @@ -0,0 +1,15 @@ +task = $task; + } +} diff --git a/app/Events/Tasks/Updated.php b/app/Events/Tasks/Updated.php new file mode 100644 index 0000000..3b6da9d --- /dev/null +++ b/app/Events/Tasks/Updated.php @@ -0,0 +1,15 @@ +task = $task; + } +} diff --git a/app/Http/Controllers/Projects/ActivityController.php b/app/Http/Controllers/Projects/ActivityController.php new file mode 100644 index 0000000..8996fc6 --- /dev/null +++ b/app/Http/Controllers/Projects/ActivityController.php @@ -0,0 +1,34 @@ +where(function ($query) use ($project) { + $query->where('object_id', $project->id); + $query->where('object_type', 'projects'); + }); + + $activityQuery->orWhere(function ($query) use ($project) { + $query->whereIn('object_id', $project->jobs->pluck('id')); + $query->where('object_type', 'jobs'); + }); + + $activityQuery->orWhere(function ($query) use ($project) { + $query->whereIn('object_id', $project->tasks->pluck('id')); + $query->where('object_type', 'tasks'); + }); + + $activities = $activityQuery->latest()->paginate(50); + + return view('projects.activities.index', compact('project', 'activities')); + } +} diff --git a/app/Listeners/Jobs/LogJobCreationActivity.php b/app/Listeners/Jobs/LogJobCreationActivity.php new file mode 100644 index 0000000..1741e8b --- /dev/null +++ b/app/Listeners/Jobs/LogJobCreationActivity.php @@ -0,0 +1,25 @@ +job; + + $activityEntry = [ + 'type' => 'job_created', + 'parent_id' => null, + 'user_id' => auth()->id(), + 'object_id' => $job->id, + 'object_type' => 'jobs', + 'data' => null, + ]; + + Activity::create($activityEntry); + } +} diff --git a/app/Listeners/Jobs/LogJobTaskDeletionActivity.php b/app/Listeners/Jobs/LogJobTaskDeletionActivity.php new file mode 100644 index 0000000..d1684bf --- /dev/null +++ b/app/Listeners/Jobs/LogJobTaskDeletionActivity.php @@ -0,0 +1,30 @@ +task; + $jobId = $task->job_id; + + $activityEntry = [ + 'type' => 'task_deleted', + 'parent_id' => null, + 'user_id' => auth()->id(), + 'object_id' => $jobId, + 'object_type' => 'jobs', + 'data' => [ + 'name' => $task->name, + 'description' => $task->description, + 'progress' => $task->progress, + ], + ]; + + Activity::create($activityEntry); + } +} diff --git a/app/Listeners/Jobs/LogJobUpdateActivity.php b/app/Listeners/Jobs/LogJobUpdateActivity.php new file mode 100644 index 0000000..4bb0c2d --- /dev/null +++ b/app/Listeners/Jobs/LogJobUpdateActivity.php @@ -0,0 +1,52 @@ +job; + $originalJob = $job->getOriginal(); + $attributeChanges = $job->getChanges(); + $attributeKeys = array_keys($job->getChanges()); + + $activityEntry = [ + 'type' => 'job_updated', + 'parent_id' => null, + 'user_id' => auth()->id(), + 'object_id' => $job->id, + 'object_type' => 'jobs', + 'data' => [ + 'before' => $this->getBeforeValues($originalJob, $attributeKeys), + 'after' => $this->getAfterValues($job->toArray(), $attributeKeys), + 'notes' => null, + ], + ]; + + Activity::create($activityEntry); + } + + private function getBeforeValues(array $originalJob, array $attributeKeys) + { + $beforeValues = []; + foreach ($attributeKeys as $attributeKey) { + $beforeValues[$attributeKey] = $originalJob[$attributeKey]; + } + + return $beforeValues; + } + + private function getAfterValues(array $job, array $attributeKeys) + { + $afterValues = []; + foreach ($attributeKeys as $attributeKey) { + $afterValues[$attributeKey] = $job[$attributeKey]; + } + + return $afterValues; + } +} diff --git a/app/Listeners/Projects/LogProjectCreationActivity.php b/app/Listeners/Projects/LogProjectCreationActivity.php new file mode 100644 index 0000000..a7d8455 --- /dev/null +++ b/app/Listeners/Projects/LogProjectCreationActivity.php @@ -0,0 +1,24 @@ +project; + + $activityEntry = [ + 'type' => 'project_created', + 'parent_id' => null, + 'user_id' => auth()->id(), + 'object_id' => $project->id, + 'object_type' => 'projects', + ]; + + Activity::create($activityEntry); + } +} diff --git a/app/Listeners/Projects/LogProjectJobDeletionActivity.php b/app/Listeners/Projects/LogProjectJobDeletionActivity.php new file mode 100644 index 0000000..7a07102 --- /dev/null +++ b/app/Listeners/Projects/LogProjectJobDeletionActivity.php @@ -0,0 +1,30 @@ +job; + $projectId = $job->project_id; + + $activityEntry = [ + 'type' => 'job_deleted', + 'parent_id' => null, + 'user_id' => auth()->id(), + 'object_id' => $projectId, + 'object_type' => 'projects', + 'data' => [ + 'name' => $job->name, + 'description' => $job->description, + 'price' => $job->price, + ], + ]; + + Activity::create($activityEntry); + } +} diff --git a/app/Listeners/Projects/LogProjectUpdateActivity.php b/app/Listeners/Projects/LogProjectUpdateActivity.php new file mode 100644 index 0000000..e597b33 --- /dev/null +++ b/app/Listeners/Projects/LogProjectUpdateActivity.php @@ -0,0 +1,52 @@ +project; + $originalProject = $project->getOriginal(); + $attributeChanges = $project->getChanges(); + $attributeKeys = array_keys($project->getChanges()); + + $activityEntry = [ + 'type' => 'project_updated', + 'parent_id' => null, + 'user_id' => auth()->id(), + 'object_id' => $project->id, + 'object_type' => 'projects', + 'data' => [ + 'before' => $this->getBeforeValues($originalProject, $attributeKeys), + 'after' => $this->getAfterValues($project->toArray(), $attributeKeys), + 'notes' => null, + ], + ]; + + Activity::create($activityEntry); + } + + private function getBeforeValues(array $originalProject, array $attributeKeys) + { + $beforeValues = []; + foreach ($attributeKeys as $attributeKey) { + $beforeValues[$attributeKey] = $originalProject[$attributeKey]; + } + + return $beforeValues; + } + + private function getAfterValues(array $project, array $attributeKeys) + { + $afterValues = []; + foreach ($attributeKeys as $attributeKey) { + $afterValues[$attributeKey] = $project[$attributeKey]; + } + + return $afterValues; + } +} diff --git a/app/Listeners/Tasks/LogTaskCreationActivity.php b/app/Listeners/Tasks/LogTaskCreationActivity.php new file mode 100644 index 0000000..4c1523d --- /dev/null +++ b/app/Listeners/Tasks/LogTaskCreationActivity.php @@ -0,0 +1,25 @@ +task; + + $activityEntry = [ + 'type' => 'task_created', + 'parent_id' => null, + 'user_id' => auth()->id(), + 'object_id' => $task->id, + 'object_type' => 'tasks', + 'data' => null, + ]; + + Activity::create($activityEntry); + } +} diff --git a/app/Listeners/Tasks/LogTaskUpdateActivity.php b/app/Listeners/Tasks/LogTaskUpdateActivity.php new file mode 100644 index 0000000..f9b3bb2 --- /dev/null +++ b/app/Listeners/Tasks/LogTaskUpdateActivity.php @@ -0,0 +1,52 @@ +task; + $originalTask = $task->getOriginal(); + $attributeChanges = $task->getChanges(); + $attributeKeys = array_keys($task->getChanges()); + + $activityEntry = [ + 'type' => 'task_updated', + 'parent_id' => null, + 'user_id' => auth()->id(), + 'object_id' => $task->id, + 'object_type' => 'tasks', + 'data' => [ + 'before' => $this->getBeforeValues($originalTask, $attributeKeys), + 'after' => $this->getAfterValues($task->toArray(), $attributeKeys), + 'notes' => null, + ], + ]; + + Activity::create($activityEntry); + } + + private function getBeforeValues(array $originalTask, array $attributeKeys) + { + $beforeValues = []; + foreach ($attributeKeys as $attributeKey) { + $beforeValues[$attributeKey] = $originalTask[$attributeKey]; + } + + return $beforeValues; + } + + private function getAfterValues(array $task, array $attributeKeys) + { + $afterValues = []; + foreach ($attributeKeys as $attributeKey) { + $afterValues[$attributeKey] = $task[$attributeKey]; + } + + return $afterValues; + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index e62e20c..5fe772b 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -25,6 +25,7 @@ class AppServiceProvider extends ServiceProvider 'projects' => 'App\Entities\Projects\Project', 'issues' => 'App\Entities\Projects\Issue', 'jobs' => 'App\Entities\Projects\Job', + 'tasks' => 'App\Entities\Projects\Task', ]); Paginator::useBootstrap(); } diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index 907cff8..7f0ce06 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -13,8 +13,29 @@ class EventServiceProvider extends ServiceProvider * @var array */ protected $listen = [ - 'App\Events\SomeEvent' => [ - 'App\Listeners\EventListener', + 'App\Events\Projects\Created' => [ + 'App\Listeners\Projects\LogProjectCreationActivity', + ], + 'App\Events\Projects\Updated' => [ + 'App\Listeners\Projects\LogProjectUpdateActivity', + ], + 'App\Events\Jobs\Created' => [ + 'App\Listeners\Jobs\LogJobCreationActivity', + ], + 'App\Events\Jobs\Updated' => [ + 'App\Listeners\Jobs\LogJobUpdateActivity', + ], + 'App\Events\Jobs\Deleted' => [ + 'App\Listeners\Projects\LogProjectJobDeletionActivity', + ], + 'App\Events\Tasks\Created' => [ + 'App\Listeners\Tasks\LogTaskCreationActivity', + ], + 'App\Events\Tasks\Updated' => [ + 'App\Listeners\Tasks\LogTaskUpdateActivity', + ], + 'App\Events\Tasks\Deleted' => [ + 'App\Listeners\Jobs\LogJobTaskDeletionActivity', ], ]; diff --git a/database/migrations/2021_03_05_221708_create_user_activities_table.php b/database/migrations/2021_03_05_221708_create_user_activities_table.php new file mode 100644 index 0000000..9d040de --- /dev/null +++ b/database/migrations/2021_03_05_221708_create_user_activities_table.php @@ -0,0 +1,40 @@ +increments('id'); + $table->string('type'); + $table->unsignedInteger('parent_id')->nullable()->comment('Parent Activity ID'); + $table->unsignedInteger('user_id')->nullable(); + $table->unsignedInteger('object_id'); + $table->string('object_type', 60); + $table->text('data')->nullable(); + $table->timestamps(); + + $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); + $table->foreign('parent_id')->references('id')->on('user_activities')->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('user_activities'); + } +} diff --git a/resources/lang/de/activity.php b/resources/lang/de/activity.php new file mode 100644 index 0000000..6d4ab47 --- /dev/null +++ b/resources/lang/de/activity.php @@ -0,0 +1,18 @@ + [ + 'project_created' => 'Project created: :name new :user.', + 'project_updated' => 'Project data :name updated by :user.', + 'job_deleted' => 'Job deleted by :user.', + ], + 'jobs' => [ + 'job_created' => 'Job created: :name new :user.', + 'job_updated' => 'Job data :name updated by :user.', + 'task_deleted' => 'Task deleted by :user.', + ], + 'tasks' => [ + 'task_created' => 'Task created: :name new :user.', + 'task_updated' => 'Task data :name updated by :user.', + ], +]; diff --git a/resources/lang/de/job.php b/resources/lang/de/job.php index feb078a..a331a2b 100644 --- a/resources/lang/de/job.php +++ b/resources/lang/de/job.php @@ -47,6 +47,8 @@ return [ 'target_end_date' => 'Target End Date', 'actual_start_date' => 'Actual Start Date', 'actual_end_date' => 'Actual End Date', + 'updated_at' => 'Last Update', + 'position' => 'Priority', // Types 'main' => 'Haupt', diff --git a/resources/lang/de/project.php b/resources/lang/de/project.php index 07898c5..28ab2d7 100644 --- a/resources/lang/de/project.php +++ b/resources/lang/de/project.php @@ -59,6 +59,7 @@ return [ 'proposal_date' => 'Datum des Angebotes', 'project_value' => 'Projektwert', 'proposal_value' => 'Angebotswert', + 'updated_at' => 'Last Update', // Relations 'files' => 'Dokumentenliste', @@ -72,6 +73,7 @@ return [ 'status' => 'Projektstatus', 'payments' => 'Zahlungen', 'issues' => 'Issues', + 'activities' => 'Activities', // Statuses 'planned' => 'geplant', diff --git a/resources/lang/en/activity.php b/resources/lang/en/activity.php new file mode 100644 index 0000000..6d4ab47 --- /dev/null +++ b/resources/lang/en/activity.php @@ -0,0 +1,18 @@ + [ + 'project_created' => 'Project created: :name new :user.', + 'project_updated' => 'Project data :name updated by :user.', + 'job_deleted' => 'Job deleted by :user.', + ], + 'jobs' => [ + 'job_created' => 'Job created: :name new :user.', + 'job_updated' => 'Job data :name updated by :user.', + 'task_deleted' => 'Task deleted by :user.', + ], + 'tasks' => [ + 'task_created' => 'Task created: :name new :user.', + 'task_updated' => 'Task data :name updated by :user.', + ], +]; diff --git a/resources/lang/en/job.php b/resources/lang/en/job.php index 3415cab..ce51fd6 100644 --- a/resources/lang/en/job.php +++ b/resources/lang/en/job.php @@ -49,6 +49,8 @@ return [ 'target_end_date' => 'Target End Date', 'actual_start_date' => 'Actual Start Date', 'actual_end_date' => 'Actual End Date', + 'updated_at' => 'Last Update', + 'position' => 'Priority', // Types 'main' => 'Main', diff --git a/resources/lang/en/project.php b/resources/lang/en/project.php index c3a5c40..8b104f6 100644 --- a/resources/lang/en/project.php +++ b/resources/lang/en/project.php @@ -59,6 +59,7 @@ return [ 'proposal_date' => 'Proposal Date', 'project_value' => 'Project Value', 'proposal_value' => 'Proposal Value', + 'updated_at' => 'Last Update', // Relations 'files' => 'Document List', @@ -72,6 +73,7 @@ return [ 'status' => 'Project Status', 'payments' => 'Payments', 'issues' => 'Issues', + 'activities' => 'Activities', // Statuses 'planned' => 'Planned', diff --git a/resources/lang/id/activity.php b/resources/lang/id/activity.php new file mode 100644 index 0000000..aeff609 --- /dev/null +++ b/resources/lang/id/activity.php @@ -0,0 +1,18 @@ + [ + 'project_created' => 'Input project baru: :name oleh :user.', + 'project_updated' => 'Data project :name diubah oleh :user.', + 'job_deleted' => 'Job dihapus oleh :user.', + ], + 'jobs' => [ + 'job_created' => 'Input job baru: :name oleh :user.', + 'job_updated' => 'Data job :name diubah oleh :user.', + 'task_deleted' => 'Task dihapus oleh :user.', + ], + 'tasks' => [ + 'task_created' => 'Input task baru: :name oleh :user.', + 'task_updated' => 'Data task :name diubah oleh :user.', + ], +]; diff --git a/resources/lang/id/job.php b/resources/lang/id/job.php index 4835805..f6f9e2a 100644 --- a/resources/lang/id/job.php +++ b/resources/lang/id/job.php @@ -49,6 +49,8 @@ return [ 'target_end_date' => 'Target Tgl Selesai', 'actual_start_date' => 'Tgl Mulai Aktual', 'actual_end_date' => 'Tgl Selesai Aktual', + 'updated_at' => 'Waktu Update', + 'position' => 'Prioritas', // Types 'main' => 'Utama', diff --git a/resources/lang/id/project.php b/resources/lang/id/project.php index 2ea3b04..fcea474 100644 --- a/resources/lang/id/project.php +++ b/resources/lang/id/project.php @@ -59,6 +59,7 @@ return [ 'proposal_date' => 'Tanggal Proposal', 'project_value' => 'Nilai Project', 'proposal_value' => 'Nilai Proposal', + 'updated_at' => 'Waktu Update', // Relations 'files' => 'List Dokumen', @@ -72,6 +73,7 @@ return [ 'status' => 'Status Project', 'payments' => 'Pembayaran', 'issues' => 'Issue', + 'activities' => 'Aktifitas', // Statuses 'planned' => 'Rencana', diff --git a/resources/views/projects/activities/index.blade.php b/resources/views/projects/activities/index.blade.php new file mode 100755 index 0000000..ade6096 --- /dev/null +++ b/resources/views/projects/activities/index.blade.php @@ -0,0 +1,22 @@ +@extends('layouts.project') + +@section('subtitle', __('project.activities')) + +@section('content-project') + +
+
+ {{ $activities->links() }} + + {{ $activities->links() }} +
+
+ +@endsection diff --git a/resources/views/projects/partials/nav-tabs.blade.php b/resources/views/projects/partials/nav-tabs.blade.php index ed2ee19..47df5b1 100644 --- a/resources/views/projects/partials/nav-tabs.blade.php +++ b/resources/views/projects/partials/nav-tabs.blade.php @@ -3,6 +3,9 @@
  • {!! link_to_route('projects.show', __('project.detail'), $project) !!}
  • +
  • + {!! link_to_route('projects.activities.index', __('project.activities'), $project) !!} +
  • @can('view-jobs', $project)
  • {!! link_to_route('projects.jobs.index', __('project.jobs').' ('.$project->jobs->count().')', $project) !!} diff --git a/resources/views/users/activities/activity_list_item.blade.php b/resources/views/users/activities/activity_list_item.blade.php new file mode 100644 index 0000000..ad295f8 --- /dev/null +++ b/resources/views/users/activities/activity_list_item.blade.php @@ -0,0 +1,4 @@ +
  • + {{ $time }} + {{ $body }} +
  • diff --git a/resources/views/users/activities/jobs/job_created.blade.php b/resources/views/users/activities/jobs/job_created.blade.php new file mode 100644 index 0000000..421f0c6 --- /dev/null +++ b/resources/views/users/activities/jobs/job_created.blade.php @@ -0,0 +1,11 @@ +@component('users.activities.activity_list_item') +@slot('time') + {{ $activity->created_at }} +@endslot +@slot('body') + {!! __('activity.'.$activity->object_type.'.'.$activity->type, [ + 'user' => $activity->user->name, + 'name' => $activity->object->name, + ]) !!} +@endslot +@endcomponent diff --git a/resources/views/users/activities/jobs/job_updated.blade.php b/resources/views/users/activities/jobs/job_updated.blade.php new file mode 100644 index 0000000..59dbe2e --- /dev/null +++ b/resources/views/users/activities/jobs/job_updated.blade.php @@ -0,0 +1,28 @@ +@component('users.activities.activity_list_item') +@slot('time') + {{ $activity->created_at }} +@endslot +@slot('body') +

    + {!! __('activity.'.$activity->object_type.'.'.$activity->type, [ + 'user' => $activity->user->name, + 'name' => $activity->object->name, + ]) !!} +

    + @php + $data = $activity->data; + @endphp + @foreach ($data['before'] as $key => $value) + @php + if (in_array($key, ['price']) && !is_null($value)) { + $value = format_money($value); + } + $afterValue = $data['after'][$key] ?? null; + if (in_array($key, ['price']) && !is_null($afterValue)) { + $afterValue = format_money($afterValue); + } + @endphp +
    {{ __('job.'.$key) }}: {{ $value }} => {{ $afterValue }}
    + @endforeach +@endslot +@endcomponent diff --git a/resources/views/users/activities/jobs/task_deleted.blade.php b/resources/views/users/activities/jobs/task_deleted.blade.php new file mode 100644 index 0000000..b81aca5 --- /dev/null +++ b/resources/views/users/activities/jobs/task_deleted.blade.php @@ -0,0 +1,15 @@ +@component('users.activities.activity_list_item') +@slot('time') + {{ $activity->created_at }} +@endslot +@slot('body') +

    + {!! __('activity.'.$activity->object_type.'.'.$activity->type, [ + 'user' => $activity->user->name, + ]) !!} +

    +
    {{ __('task.name') }}: {{ $activity->data['name'] }}
    +
    {{ __('task.description') }}: {{ $activity->data['description'] }}
    +
    {{ __('task.progress') }}: {{ $activity->data['progress'] }} %
    +@endslot +@endcomponent diff --git a/resources/views/users/activities/projects/job_deleted.blade.php b/resources/views/users/activities/projects/job_deleted.blade.php new file mode 100644 index 0000000..57a1fa9 --- /dev/null +++ b/resources/views/users/activities/projects/job_deleted.blade.php @@ -0,0 +1,15 @@ +@component('users.activities.activity_list_item') +@slot('time') + {{ $activity->created_at }} +@endslot +@slot('body') +

    + {!! __('activity.'.$activity->object_type.'.'.$activity->type, [ + 'user' => $activity->user->name, + ]) !!} +

    +
    {{ __('job.name') }}: {{ $activity->data['name'] }}
    +
    {{ __('job.description') }}: {{ $activity->data['description'] }}
    +
    {{ __('job.price') }}: {{ format_money($activity->data['price']) }}
    +@endslot +@endcomponent diff --git a/resources/views/users/activities/projects/project_created.blade.php b/resources/views/users/activities/projects/project_created.blade.php new file mode 100644 index 0000000..421f0c6 --- /dev/null +++ b/resources/views/users/activities/projects/project_created.blade.php @@ -0,0 +1,11 @@ +@component('users.activities.activity_list_item') +@slot('time') + {{ $activity->created_at }} +@endslot +@slot('body') + {!! __('activity.'.$activity->object_type.'.'.$activity->type, [ + 'user' => $activity->user->name, + 'name' => $activity->object->name, + ]) !!} +@endslot +@endcomponent diff --git a/resources/views/users/activities/projects/project_updated.blade.php b/resources/views/users/activities/projects/project_updated.blade.php new file mode 100644 index 0000000..0a2d36b --- /dev/null +++ b/resources/views/users/activities/projects/project_updated.blade.php @@ -0,0 +1,22 @@ +@component('users.activities.activity_list_item') +@slot('time') + {{ $activity->created_at }} +@endslot +@slot('body') +

    + {!! __('activity.'.$activity->object_type.'.'.$activity->type, [ + 'user' => $activity->user->name, + 'name' => $activity->object->name, + ]) !!} +

    + @php + $data = $activity->data; + @endphp + @foreach ($data['before'] as $key => $value) + @php + $afterValue = $data['after'][$key] ?? null; + @endphp +
    {{ __('project.'.$key) }}: {{ $value }} => {{ $afterValue }}
    + @endforeach +@endslot +@endcomponent diff --git a/resources/views/users/activities/tasks/task_created.blade.php b/resources/views/users/activities/tasks/task_created.blade.php new file mode 100644 index 0000000..421f0c6 --- /dev/null +++ b/resources/views/users/activities/tasks/task_created.blade.php @@ -0,0 +1,11 @@ +@component('users.activities.activity_list_item') +@slot('time') + {{ $activity->created_at }} +@endslot +@slot('body') + {!! __('activity.'.$activity->object_type.'.'.$activity->type, [ + 'user' => $activity->user->name, + 'name' => $activity->object->name, + ]) !!} +@endslot +@endcomponent diff --git a/resources/views/users/activities/tasks/task_updated.blade.php b/resources/views/users/activities/tasks/task_updated.blade.php new file mode 100644 index 0000000..63b03db --- /dev/null +++ b/resources/views/users/activities/tasks/task_updated.blade.php @@ -0,0 +1,22 @@ +@component('users.activities.activity_list_item') +@slot('time') + {{ $activity->created_at }} +@endslot +@slot('body') +

    + {!! __('activity.'.$activity->object_type.'.'.$activity->type, [ + 'user' => $activity->user->name, + 'name' => $activity->object->name, + ]) !!} +

    + @php + $data = $activity->data; + @endphp + @foreach ($data['before'] as $key => $value) + @php + $afterValue = $data['after'][$key] ?? null; + @endphp +
    {{ __('job.'.$key) }}: {{ $value }} => {{ $afterValue }}
    + @endforeach +@endslot +@endcomponent diff --git a/routes/web/projects.php b/routes/web/projects.php index 45aefba..bc92298 100644 --- a/routes/web/projects.php +++ b/routes/web/projects.php @@ -26,6 +26,11 @@ Route::group(['middleware' => ['auth'], 'namespace' => 'Projects'], function () Route::get('projects/{project}/invoices', ['as' => 'projects.invoices', 'uses' => 'InvoicesController@index']); /* + * Project Activities Routes + */ + Route::get('projects/{project}/activities', ['as' => 'projects.activities.index', 'uses' => 'ActivityController@index']); + + /* * Project Jobs Routes */ Route::get('projects/{project}/jobs-export/{type?}', ['as' => 'projects.jobs-export', 'uses' => 'JobsController@jobsExport']); diff --git a/tests/Unit/Models/ActivityTest.php b/tests/Unit/Models/ActivityTest.php new file mode 100644 index 0000000..5187594 --- /dev/null +++ b/tests/Unit/Models/ActivityTest.php @@ -0,0 +1,250 @@ +adminUserSigningIn(); + $project = factory(Project::class)->create(); + + $this->seeInDatabase('user_activities', [ + 'type' => 'project_created', + 'parent_id' => null, + 'user_id' => $admin->id, + 'object_id' => $project->id, + 'object_type' => 'projects', + 'data' => null, + ]); + } + + /** @test */ + public function it_records_project_data_update_activities() + { + $admin = $this->adminUserSigningIn(); + $project = factory(Project::class)->create(['name' => 'New Project']); + + $project->name = 'Updated project'; + $project->save(); + + $this->seeInDatabase('user_activities', [ + 'type' => 'project_updated', + 'parent_id' => null, + 'user_id' => $admin->id, + 'object_id' => $project->id, + 'object_type' => 'projects', + 'data' => json_encode([ + 'before' => ['name' => 'New Project'], + 'after' => ['name' => 'Updated project'], + 'notes' => null, + ]), + ]); + } + + /** @test */ + public function it_records_job_creation_activities() + { + $admin = $this->adminUserSigningIn(); + $project = factory(Project::class)->create(); + $job = factory(Job::class)->create(['project_id' => $project->id]); + + $this->seeInDatabase('user_activities', [ + 'type' => 'job_created', + 'parent_id' => null, + 'user_id' => $admin->id, + 'object_id' => $job->id, + 'object_type' => 'jobs', + 'data' => null, + ]); + } + + /** @test */ + public function it_records_job_data_update_activities() + { + $admin = $this->adminUserSigningIn(); + $project = factory(Project::class)->create(); + $job = factory(Job::class)->create([ + 'name' => 'New Job', + 'project_id' => $project->id, + ]); + + $job->name = 'Updated job'; + $job->save(); + + $this->seeInDatabase('user_activities', [ + 'type' => 'job_updated', + 'parent_id' => null, + 'user_id' => $admin->id, + 'object_id' => $job->id, + 'object_type' => 'jobs', + 'data' => json_encode([ + 'before' => ['name' => 'New Job'], + 'after' => ['name' => 'Updated job'], + 'notes' => null, + ]), + ]); + } + + /** @test */ + public function it_records_job_deletion_activities() + { + $admin = $this->adminUserSigningIn(); + $project = factory(Project::class)->create(); + $job = factory(Job::class)->create(['project_id' => $project->id]); + $job->delete(); + + $this->seeInDatabase('user_activities', [ + 'type' => 'job_deleted', + 'parent_id' => null, + 'user_id' => $admin->id, + 'object_id' => $project->id, + 'object_type' => 'projects', + 'data' => json_encode([ + 'name' => $job->name, + 'description' => $job->description, + 'price' => $job->price, + ]), + ]); + } + + /** @test */ + public function it_records_task_creation_activities() + { + $admin = $this->adminUserSigningIn(); + $project = factory(Project::class)->create(); + $job = factory(Job::class)->create(['project_id' => $project->id]); + $task = factory(Task::class)->create(['job_id' => $job->id]); + + $this->seeInDatabase('user_activities', [ + 'type' => 'task_created', + 'parent_id' => null, + 'user_id' => $admin->id, + 'object_id' => $task->id, + 'object_type' => 'tasks', + 'data' => null, + ]); + } + + /** @test */ + public function it_records_task_data_update_activities() + { + $admin = $this->adminUserSigningIn(); + $project = factory(Project::class)->create(); + $job = factory(Job::class)->create(['project_id' => $project->id]); + $task = factory(Task::class)->create([ + 'name' => 'New Task', + 'job_id' => $job->id, + ]); + + $task->name = 'Updated task'; + $task->save(); + + $this->seeInDatabase('user_activities', [ + 'type' => 'task_updated', + 'parent_id' => null, + 'user_id' => $admin->id, + 'object_id' => $task->id, + 'object_type' => 'tasks', + 'data' => json_encode([ + 'before' => ['name' => 'New Task'], + 'after' => ['name' => 'Updated task'], + 'notes' => null, + ]), + ]); + } + + /** @test */ + public function it_records_task_progress_update_activities() + { + $admin = $this->adminUserSigningIn(); + $project = factory(Project::class)->create(); + $job = factory(Job::class)->create(['project_id' => $project->id]); + $task = factory(Task::class)->create([ + 'progress' => 20, + 'job_id' => $job->id, + ]); + + $task->progress = 40; + $task->save(); + + $this->seeInDatabase('user_activities', [ + 'type' => 'task_updated', + 'parent_id' => null, + 'user_id' => $admin->id, + 'object_id' => $task->id, + 'object_type' => 'tasks', + 'data' => json_encode([ + 'before' => ['progress' => 20], + 'after' => ['progress' => 40], + 'notes' => null, + ]), + ]); + } + + /** @test */ + public function it_records_task_deletion_activities() + { + $admin = $this->adminUserSigningIn(); + $project = factory(Project::class)->create(); + $job = factory(Job::class)->create(['project_id' => $project->id]); + $task = factory(Task::class)->create(['job_id' => $job->id]); + $task->delete(); + + $this->seeInDatabase('user_activities', [ + 'type' => 'task_deleted', + 'parent_id' => null, + 'user_id' => $admin->id, + 'object_id' => $job->id, + 'object_type' => 'jobs', + 'data' => json_encode([ + 'name' => $task->name, + 'description' => $task->description, + 'progress' => $task->progress, + ]), + ]); + } + + /** @test */ + public function an_activity_has_belongs_to_user_relation() + { + $project = factory(Project::class)->create(); + $activity = Activity::where('object_type', 'projects') + ->where('object_id', $project->id) + ->first(); + + $this->assertInstanceOf(User::class, $activity->user); + $this->assertEquals($activity->user_id, $activity->user->id); + } + + /** @test */ + public function an_activity_has_belongs_to_object_relation() + { + $project = factory(Project::class)->create(); + $job = factory(Job::class)->create(['project_id' => $project->id]); + $task = factory(Task::class)->create(['job_id' => $job->id]); + + $projectActivity = Activity::where('object_type', 'projects')->first(); + $this->assertInstanceOf(Project::class, $projectActivity->object); + $this->assertEquals($projectActivity->object_id, $projectActivity->object->id); + + $jobActivity = Activity::where('object_type', 'jobs')->first(); + $this->assertInstanceOf(Job::class, $jobActivity->object); + $this->assertEquals($jobActivity->object_id, $jobActivity->object->id); + + $taskActivity = Activity::where('object_type', 'tasks')->first(); + $this->assertInstanceOf(Task::class, $taskActivity->object); + $this->assertEquals($taskActivity->object_id, $taskActivity->object->id); + } +}