Browse Source

Merge pull request #72 from nafiesl/user_activities

Add User Activity Logs on a Project
master
Nafies Luthfi 5 years ago
committed by GitHub
parent
commit
dfdd05d69a
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 11
      app/Entities/Projects/Job.php
  2. 9
      app/Entities/Projects/Project.php
  3. 11
      app/Entities/Projects/Task.php
  4. 24
      app/Entities/Users/Activity.php
  5. 15
      app/Events/Jobs/Created.php
  6. 15
      app/Events/Jobs/Deleted.php
  7. 15
      app/Events/Jobs/Updated.php
  8. 15
      app/Events/Projects/Created.php
  9. 15
      app/Events/Projects/Updated.php
  10. 15
      app/Events/Tasks/Created.php
  11. 15
      app/Events/Tasks/Deleted.php
  12. 15
      app/Events/Tasks/Updated.php
  13. 34
      app/Http/Controllers/Projects/ActivityController.php
  14. 25
      app/Listeners/Jobs/LogJobCreationActivity.php
  15. 30
      app/Listeners/Jobs/LogJobTaskDeletionActivity.php
  16. 52
      app/Listeners/Jobs/LogJobUpdateActivity.php
  17. 24
      app/Listeners/Projects/LogProjectCreationActivity.php
  18. 30
      app/Listeners/Projects/LogProjectJobDeletionActivity.php
  19. 52
      app/Listeners/Projects/LogProjectUpdateActivity.php
  20. 25
      app/Listeners/Tasks/LogTaskCreationActivity.php
  21. 52
      app/Listeners/Tasks/LogTaskUpdateActivity.php
  22. 1
      app/Providers/AppServiceProvider.php
  23. 25
      app/Providers/EventServiceProvider.php
  24. 40
      database/migrations/2021_03_05_221708_create_user_activities_table.php
  25. 18
      resources/lang/de/activity.php
  26. 2
      resources/lang/de/job.php
  27. 2
      resources/lang/de/project.php
  28. 18
      resources/lang/en/activity.php
  29. 2
      resources/lang/en/job.php
  30. 2
      resources/lang/en/project.php
  31. 18
      resources/lang/id/activity.php
  32. 2
      resources/lang/id/job.php
  33. 2
      resources/lang/id/project.php
  34. 22
      resources/views/projects/activities/index.blade.php
  35. 3
      resources/views/projects/partials/nav-tabs.blade.php
  36. 4
      resources/views/users/activities/activity_list_item.blade.php
  37. 11
      resources/views/users/activities/jobs/job_created.blade.php
  38. 28
      resources/views/users/activities/jobs/job_updated.blade.php
  39. 15
      resources/views/users/activities/jobs/task_deleted.blade.php
  40. 15
      resources/views/users/activities/projects/job_deleted.blade.php
  41. 11
      resources/views/users/activities/projects/project_created.blade.php
  42. 22
      resources/views/users/activities/projects/project_updated.blade.php
  43. 11
      resources/views/users/activities/tasks/task_created.blade.php
  44. 22
      resources/views/users/activities/tasks/task_updated.blade.php
  45. 5
      routes/web/projects.php
  46. 250
      tests/Unit/Models/ActivityTest.php

11
app/Entities/Projects/Job.php

@ -17,6 +17,17 @@ class Job extends Model
use PresentableTrait; 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 * @var \App\Entities\Projects\JobPresenter
*/ */
protected $presenter = JobPresenter::class; protected $presenter = JobPresenter::class;

9
app/Entities/Projects/Project.php

@ -18,6 +18,15 @@ use Laracasts\Presenter\PresentableTrait;
class Project extends Model class Project extends Model
{ {
use PresentableTrait; 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 * @var \App\Entities\Projects\ProjectPresenter

11
app/Entities/Projects/Task.php

@ -6,6 +6,17 @@ use Illuminate\Database\Eloquent\Model;
class Task extends 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 $guarded = ['id', 'created_at', 'updated_at'];
protected $touches = ['job']; protected $touches = ['job'];

24
app/Entities/Users/Activity.php

@ -0,0 +1,24 @@
<?php
namespace App\Entities\Users;
use Illuminate\Database\Eloquent\Model;
class Activity extends Model
{
protected $table = 'user_activities';
protected $fillable = ['type', 'parent_id', 'user_id', 'object_id', 'object_type', 'data'];
protected $casts = ['data' => 'array'];
public function user()
{
return $this->belongsTo(User::class)->withDefault(['name' => 'n/a']);
}
public function object()
{
return $this->morphTo();
}
}

15
app/Events/Jobs/Created.php

@ -0,0 +1,15 @@
<?php
namespace App\Events\Jobs;
use App\Entities\Projects\Job;
class Created
{
public $job;
public function __construct(Job $job)
{
$this->job = $job;
}
}

15
app/Events/Jobs/Deleted.php

@ -0,0 +1,15 @@
<?php
namespace App\Events\Jobs;
use App\Entities\Projects\Job;
class Deleted
{
public $job;
public function __construct(Job $job)
{
$this->job = $job;
}
}

15
app/Events/Jobs/Updated.php

@ -0,0 +1,15 @@
<?php
namespace App\Events\Jobs;
use App\Entities\Projects\Job;
class Updated
{
public $job;
public function __construct(Job $job)
{
$this->job = $job;
}
}

15
app/Events/Projects/Created.php

@ -0,0 +1,15 @@
<?php
namespace App\Events\Projects;
use App\Entities\Projects\Project;
class Created
{
public $project;
public function __construct(Project $project)
{
$this->project = $project;
}
}

15
app/Events/Projects/Updated.php

@ -0,0 +1,15 @@
<?php
namespace App\Events\Projects;
use App\Entities\Projects\Project;
class Updated
{
public $project;
public function __construct(Project $project)
{
$this->project = $project;
}
}

15
app/Events/Tasks/Created.php

@ -0,0 +1,15 @@
<?php
namespace App\Events\Tasks;
use App\Entities\Projects\Task;
class Created
{
public $task;
public function __construct(Task $task)
{
$this->task = $task;
}
}

15
app/Events/Tasks/Deleted.php

@ -0,0 +1,15 @@
<?php
namespace App\Events\Tasks;
use App\Entities\Projects\Task;
class Deleted
{
public $task;
public function __construct(Task $task)
{
$this->task = $task;
}
}

15
app/Events/Tasks/Updated.php

@ -0,0 +1,15 @@
<?php
namespace App\Events\Tasks;
use App\Entities\Projects\Task;
class Updated
{
public $task;
public function __construct(Task $task)
{
$this->task = $task;
}
}

34
app/Http/Controllers/Projects/ActivityController.php

@ -0,0 +1,34 @@
<?php
namespace App\Http\Controllers\Projects;
use App\Entities\Projects\Project;
use App\Entities\Users\Activity;
use App\Http\Controllers\Controller;
class ActivityController extends Controller
{
public function index(Project $project)
{
$activityQuery = Activity::query();
$activityQuery->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'));
}
}

25
app/Listeners/Jobs/LogJobCreationActivity.php

@ -0,0 +1,25 @@
<?php
namespace App\Listeners\Jobs;
use App\Entities\Users\Activity;
use App\Events\Jobs\Created;
class LogJobCreationActivity
{
public function handle(Created $event)
{
$job = $event->job;
$activityEntry = [
'type' => 'job_created',
'parent_id' => null,
'user_id' => auth()->id(),
'object_id' => $job->id,
'object_type' => 'jobs',
'data' => null,
];
Activity::create($activityEntry);
}
}

30
app/Listeners/Jobs/LogJobTaskDeletionActivity.php

@ -0,0 +1,30 @@
<?php
namespace App\Listeners\Jobs;
use App\Entities\Users\Activity;
use App\Events\Tasks\Deleted;
class LogJobTaskDeletionActivity
{
public function handle(Deleted $event)
{
$task = $event->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);
}
}

52
app/Listeners/Jobs/LogJobUpdateActivity.php

@ -0,0 +1,52 @@
<?php
namespace App\Listeners\Jobs;
use App\Entities\Users\Activity;
use App\Events\Jobs\Updated;
class LogJobUpdateActivity
{
public function handle(Updated $event)
{
$job = $event->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;
}
}

24
app/Listeners/Projects/LogProjectCreationActivity.php

@ -0,0 +1,24 @@
<?php
namespace App\Listeners\Projects;
use App\Entities\Users\Activity;
use App\Events\Projects\Created;
class LogProjectCreationActivity
{
public function handle(Created $event)
{
$project = $event->project;
$activityEntry = [
'type' => 'project_created',
'parent_id' => null,
'user_id' => auth()->id(),
'object_id' => $project->id,
'object_type' => 'projects',
];
Activity::create($activityEntry);
}
}

30
app/Listeners/Projects/LogProjectJobDeletionActivity.php

@ -0,0 +1,30 @@
<?php
namespace App\Listeners\Projects;
use App\Entities\Users\Activity;
use App\Events\Jobs\Deleted;
class LogProjectJobDeletionActivity
{
public function handle(Deleted $event)
{
$job = $event->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);
}
}

52
app/Listeners/Projects/LogProjectUpdateActivity.php

@ -0,0 +1,52 @@
<?php
namespace App\Listeners\Projects;
use App\Entities\Users\Activity;
use App\Events\Projects\Updated;
class LogProjectUpdateActivity
{
public function handle(Updated $event)
{
$project = $event->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;
}
}

25
app/Listeners/Tasks/LogTaskCreationActivity.php

@ -0,0 +1,25 @@
<?php
namespace App\Listeners\Tasks;
use App\Entities\Users\Activity;
use App\Events\Tasks\Created;
class LogTaskCreationActivity
{
public function handle(Created $event)
{
$task = $event->task;
$activityEntry = [
'type' => 'task_created',
'parent_id' => null,
'user_id' => auth()->id(),
'object_id' => $task->id,
'object_type' => 'tasks',
'data' => null,
];
Activity::create($activityEntry);
}
}

52
app/Listeners/Tasks/LogTaskUpdateActivity.php

@ -0,0 +1,52 @@
<?php
namespace App\Listeners\Tasks;
use App\Entities\Users\Activity;
use App\Events\Tasks\Updated;
class LogTaskUpdateActivity
{
public function handle(Updated $event)
{
$task = $event->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;
}
}

1
app/Providers/AppServiceProvider.php

@ -25,6 +25,7 @@ class AppServiceProvider extends ServiceProvider
'projects' => 'App\Entities\Projects\Project', 'projects' => 'App\Entities\Projects\Project',
'issues' => 'App\Entities\Projects\Issue', 'issues' => 'App\Entities\Projects\Issue',
'jobs' => 'App\Entities\Projects\Job', 'jobs' => 'App\Entities\Projects\Job',
'tasks' => 'App\Entities\Projects\Task',
]); ]);
Paginator::useBootstrap(); Paginator::useBootstrap();
} }

25
app/Providers/EventServiceProvider.php

@ -13,8 +13,29 @@ class EventServiceProvider extends ServiceProvider
* @var array * @var array
*/ */
protected $listen = [ 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',
], ],
]; ];

40
database/migrations/2021_03_05_221708_create_user_activities_table.php

@ -0,0 +1,40 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateUserActivitiesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('user_activities', function (Blueprint $table) {
$table->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');
}
}

18
resources/lang/de/activity.php

@ -0,0 +1,18 @@
<?php
return [
'projects' => [
'project_created' => 'Project created: <strong>:name</strong> new <strong>:user</strong>.',
'project_updated' => 'Project data <strong>:name</strong> updated by <strong>:user</strong>.',
'job_deleted' => 'Job deleted by <strong>:user</strong>.',
],
'jobs' => [
'job_created' => 'Job created: <strong>:name</strong> new <strong>:user</strong>.',
'job_updated' => 'Job data <strong>:name</strong> updated by <strong>:user</strong>.',
'task_deleted' => 'Task deleted by <strong>:user</strong>.',
],
'tasks' => [
'task_created' => 'Task created: <strong>:name</strong> new <strong>:user</strong>.',
'task_updated' => 'Task data <strong>:name</strong> updated by <strong>:user</strong>.',
],
];

2
resources/lang/de/job.php

@ -47,6 +47,8 @@ return [
'target_end_date' => 'Target End Date', 'target_end_date' => 'Target End Date',
'actual_start_date' => 'Actual Start Date', 'actual_start_date' => 'Actual Start Date',
'actual_end_date' => 'Actual End Date', 'actual_end_date' => 'Actual End Date',
'updated_at' => 'Last Update',
'position' => 'Priority',
// Types // Types
'main' => 'Haupt', 'main' => 'Haupt',

2
resources/lang/de/project.php

@ -59,6 +59,7 @@ return [
'proposal_date' => 'Datum des Angebotes', 'proposal_date' => 'Datum des Angebotes',
'project_value' => 'Projektwert', 'project_value' => 'Projektwert',
'proposal_value' => 'Angebotswert', 'proposal_value' => 'Angebotswert',
'updated_at' => 'Last Update',
// Relations // Relations
'files' => 'Dokumentenliste', 'files' => 'Dokumentenliste',
@ -72,6 +73,7 @@ return [
'status' => 'Projektstatus', 'status' => 'Projektstatus',
'payments' => 'Zahlungen', 'payments' => 'Zahlungen',
'issues' => 'Issues', 'issues' => 'Issues',
'activities' => 'Activities',
// Statuses // Statuses
'planned' => 'geplant', 'planned' => 'geplant',

18
resources/lang/en/activity.php

@ -0,0 +1,18 @@
<?php
return [
'projects' => [
'project_created' => 'Project created: <strong>:name</strong> new <strong>:user</strong>.',
'project_updated' => 'Project data <strong>:name</strong> updated by <strong>:user</strong>.',
'job_deleted' => 'Job deleted by <strong>:user</strong>.',
],
'jobs' => [
'job_created' => 'Job created: <strong>:name</strong> new <strong>:user</strong>.',
'job_updated' => 'Job data <strong>:name</strong> updated by <strong>:user</strong>.',
'task_deleted' => 'Task deleted by <strong>:user</strong>.',
],
'tasks' => [
'task_created' => 'Task created: <strong>:name</strong> new <strong>:user</strong>.',
'task_updated' => 'Task data <strong>:name</strong> updated by <strong>:user</strong>.',
],
];

2
resources/lang/en/job.php

@ -49,6 +49,8 @@ return [
'target_end_date' => 'Target End Date', 'target_end_date' => 'Target End Date',
'actual_start_date' => 'Actual Start Date', 'actual_start_date' => 'Actual Start Date',
'actual_end_date' => 'Actual End Date', 'actual_end_date' => 'Actual End Date',
'updated_at' => 'Last Update',
'position' => 'Priority',
// Types // Types
'main' => 'Main', 'main' => 'Main',

2
resources/lang/en/project.php

@ -59,6 +59,7 @@ return [
'proposal_date' => 'Proposal Date', 'proposal_date' => 'Proposal Date',
'project_value' => 'Project Value', 'project_value' => 'Project Value',
'proposal_value' => 'Proposal Value', 'proposal_value' => 'Proposal Value',
'updated_at' => 'Last Update',
// Relations // Relations
'files' => 'Document List', 'files' => 'Document List',
@ -72,6 +73,7 @@ return [
'status' => 'Project Status', 'status' => 'Project Status',
'payments' => 'Payments', 'payments' => 'Payments',
'issues' => 'Issues', 'issues' => 'Issues',
'activities' => 'Activities',
// Statuses // Statuses
'planned' => 'Planned', 'planned' => 'Planned',

18
resources/lang/id/activity.php

@ -0,0 +1,18 @@
<?php
return [
'projects' => [
'project_created' => 'Input project baru: <strong>:name</strong> oleh <strong>:user</strong>.',
'project_updated' => 'Data project <strong>:name</strong> diubah oleh <strong>:user</strong>.',
'job_deleted' => 'Job dihapus oleh <strong>:user</strong>.',
],
'jobs' => [
'job_created' => 'Input job baru: <strong>:name</strong> oleh <strong>:user</strong>.',
'job_updated' => 'Data job <strong>:name</strong> diubah oleh <strong>:user</strong>.',
'task_deleted' => 'Task dihapus oleh <strong>:user</strong>.',
],
'tasks' => [
'task_created' => 'Input task baru: <strong>:name</strong> oleh <strong>:user</strong>.',
'task_updated' => 'Data task <strong>:name</strong> diubah oleh <strong>:user</strong>.',
],
];

2
resources/lang/id/job.php

@ -49,6 +49,8 @@ return [
'target_end_date' => 'Target Tgl Selesai', 'target_end_date' => 'Target Tgl Selesai',
'actual_start_date' => 'Tgl Mulai Aktual', 'actual_start_date' => 'Tgl Mulai Aktual',
'actual_end_date' => 'Tgl Selesai Aktual', 'actual_end_date' => 'Tgl Selesai Aktual',
'updated_at' => 'Waktu Update',
'position' => 'Prioritas',
// Types // Types
'main' => 'Utama', 'main' => 'Utama',

2
resources/lang/id/project.php

@ -59,6 +59,7 @@ return [
'proposal_date' => 'Tanggal Proposal', 'proposal_date' => 'Tanggal Proposal',
'project_value' => 'Nilai Project', 'project_value' => 'Nilai Project',
'proposal_value' => 'Nilai Proposal', 'proposal_value' => 'Nilai Proposal',
'updated_at' => 'Waktu Update',
// Relations // Relations
'files' => 'List Dokumen', 'files' => 'List Dokumen',
@ -72,6 +73,7 @@ return [
'status' => 'Status Project', 'status' => 'Status Project',
'payments' => 'Pembayaran', 'payments' => 'Pembayaran',
'issues' => 'Issue', 'issues' => 'Issue',
'activities' => 'Aktifitas',
// Statuses // Statuses
'planned' => 'Rencana', 'planned' => 'Rencana',

22
resources/views/projects/activities/index.blade.php

@ -0,0 +1,22 @@
@extends('layouts.project')
@section('subtitle', __('project.activities'))
@section('content-project')
<div class="row">
<div class="col-md-8 col-md-offset-2">
{{ $activities->links() }}
<ul class="list-group">
@foreach($activities as $activity)
@includeWhen(
view()->exists('users.activities.'.$activity->object_type.'.'.$activity->type),
'users.activities.'.$activity->object_type.'.'.$activity->type
)
@endforeach
</ul>
{{ $activities->links() }}
</div>
</div>
@endsection

3
resources/views/projects/partials/nav-tabs.blade.php

@ -3,6 +3,9 @@
<li class="{{ Request::segment(3) == null ? 'active' : '' }}"> <li class="{{ Request::segment(3) == null ? 'active' : '' }}">
{!! link_to_route('projects.show', __('project.detail'), $project) !!} {!! link_to_route('projects.show', __('project.detail'), $project) !!}
</li> </li>
<li class="{{ Request::segment(3) == 'activities' ? 'active' : '' }}">
{!! link_to_route('projects.activities.index', __('project.activities'), $project) !!}
</li>
@can('view-jobs', $project) @can('view-jobs', $project)
<li class="{{ Request::segment(3) == 'jobs' ? 'active' : '' }}"> <li class="{{ Request::segment(3) == 'jobs' ? 'active' : '' }}">
{!! link_to_route('projects.jobs.index', __('project.jobs').' ('.$project->jobs->count().')', $project) !!} {!! link_to_route('projects.jobs.index', __('project.jobs').' ('.$project->jobs->count().')', $project) !!}

4
resources/views/users/activities/activity_list_item.blade.php

@ -0,0 +1,4 @@
<li class="list-group-item">
<span class="label label-info pull-right">{{ $time }}</span>
{{ $body }}
</li>

11
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

28
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')
<p>
{!! __('activity.'.$activity->object_type.'.'.$activity->type, [
'user' => $activity->user->name,
'name' => $activity->object->name,
]) !!}
</p>
@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
<div>{{ __('job.'.$key) }}: {{ $value }} => {{ $afterValue }}</div>
@endforeach
@endslot
@endcomponent

15
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')
<p>
{!! __('activity.'.$activity->object_type.'.'.$activity->type, [
'user' => $activity->user->name,
]) !!}
</p>
<div>{{ __('task.name') }}: {{ $activity->data['name'] }}</div>
<div>{{ __('task.description') }}: {{ $activity->data['description'] }}</div>
<div>{{ __('task.progress') }}: {{ $activity->data['progress'] }} %</div>
@endslot
@endcomponent

15
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')
<p>
{!! __('activity.'.$activity->object_type.'.'.$activity->type, [
'user' => $activity->user->name,
]) !!}
</p>
<div>{{ __('job.name') }}: {{ $activity->data['name'] }}</div>
<div>{{ __('job.description') }}: {{ $activity->data['description'] }}</div>
<div>{{ __('job.price') }}: {{ format_money($activity->data['price']) }}</div>
@endslot
@endcomponent

11
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

22
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')
<p>
{!! __('activity.'.$activity->object_type.'.'.$activity->type, [
'user' => $activity->user->name,
'name' => $activity->object->name,
]) !!}
</p>
@php
$data = $activity->data;
@endphp
@foreach ($data['before'] as $key => $value)
@php
$afterValue = $data['after'][$key] ?? null;
@endphp
<div>{{ __('project.'.$key) }}: {{ $value }} => {{ $afterValue }}</div>
@endforeach
@endslot
@endcomponent

11
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

22
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')
<p>
{!! __('activity.'.$activity->object_type.'.'.$activity->type, [
'user' => $activity->user->name,
'name' => $activity->object->name,
]) !!}
</p>
@php
$data = $activity->data;
@endphp
@foreach ($data['before'] as $key => $value)
@php
$afterValue = $data['after'][$key] ?? null;
@endphp
<div>{{ __('job.'.$key) }}: {{ $value }} => {{ $afterValue }}</div>
@endforeach
@endslot
@endcomponent

5
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']); 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 * Project Jobs Routes
*/ */
Route::get('projects/{project}/jobs-export/{type?}', ['as' => 'projects.jobs-export', 'uses' => 'JobsController@jobsExport']); Route::get('projects/{project}/jobs-export/{type?}', ['as' => 'projects.jobs-export', 'uses' => 'JobsController@jobsExport']);

250
tests/Unit/Models/ActivityTest.php

@ -0,0 +1,250 @@
<?php
namespace Tests\Unit\Models;
use App\Entities\Projects\Job;
use App\Entities\Projects\Project;
use App\Entities\Projects\Task;
use App\Entities\Users\Activity;
use App\Entities\Users\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class ActivityTest extends TestCase
{
use RefreshDatabase;
/** @test */
public function it_creates_project_creation_activities()
{
$admin = $this->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);
}
}
Loading…
Cancel
Save