Add activity logging features by integrating Spatie's laravel-activitylog package. Update Permission and Role enums to include activity log permissions. Implement activity logging in Login and Logout controllers. Enhance sidebar navigation to include activity logs section. Update models to interact with activity logs through InteractsWithActivityLog concern.
This commit is contained in:
parent
ec8b49be05
commit
1d1f0e7941
45
app/Enums/ActivityEventLabel.php
Normal file
45
app/Enums/ActivityEventLabel.php
Normal file
@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum ActivityEventLabel: string
|
||||
{
|
||||
case Created = 'created';
|
||||
case Updated = 'updated';
|
||||
case Deleted = 'deleted';
|
||||
case Login = 'login';
|
||||
case Logout = 'logout';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::Created => 'Dibuat',
|
||||
self::Updated => 'Diperbarui',
|
||||
self::Deleted => 'Dihapus',
|
||||
self::Login => 'Masuk',
|
||||
self::Logout => 'Keluar',
|
||||
};
|
||||
}
|
||||
|
||||
public static function labelFor(?string $event): string
|
||||
{
|
||||
if ($event === null || $event === '') {
|
||||
return '-';
|
||||
}
|
||||
|
||||
return self::tryFrom($event)?->label() ?? ucfirst($event);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array{value: string, label: string}>
|
||||
*/
|
||||
public static function selectOptions(): array
|
||||
{
|
||||
return collect(self::cases())
|
||||
->map(fn (self $event) => [
|
||||
'value' => $event->value,
|
||||
'label' => $event->label(),
|
||||
])
|
||||
->all();
|
||||
}
|
||||
}
|
||||
@ -101,6 +101,8 @@ enum Permission: string
|
||||
case SETTINGS_VIEW = 'settings.view';
|
||||
case SETTINGS_UPDATE = 'settings.update';
|
||||
|
||||
case ACTIVITY_LOGS_VIEW = 'activity-logs.view';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
@ -196,6 +198,8 @@ public function label(): string
|
||||
|
||||
self::SETTINGS_VIEW => 'Lihat Pengaturan Aplikasi',
|
||||
self::SETTINGS_UPDATE => 'Ubah Pengaturan Aplikasi',
|
||||
|
||||
self::ACTIVITY_LOGS_VIEW => 'Lihat Log Aktivitas',
|
||||
};
|
||||
}
|
||||
|
||||
@ -236,6 +240,7 @@ public function group(): string
|
||||
self::PAYROLL_VIEW, self::PAYROLL_PAY, self::PAYROLL_ADJUST,
|
||||
self::PAYROLL_CLOSE => 'Gaji',
|
||||
self::SETTINGS_VIEW, self::SETTINGS_UPDATE => 'Pengaturan Aplikasi',
|
||||
self::ACTIVITY_LOGS_VIEW => 'Log Aktivitas',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -109,6 +109,7 @@ public function permissions(): array
|
||||
Permission::PAYROLL_PAY,
|
||||
Permission::PAYROLL_ADJUST,
|
||||
Permission::PAYROLL_CLOSE,
|
||||
Permission::ACTIVITY_LOGS_VIEW,
|
||||
],
|
||||
self::ADMIN_TOKO => [
|
||||
Permission::DASHBOARD_VIEW,
|
||||
@ -171,6 +172,7 @@ public function permissions(): array
|
||||
Permission::PAYROLL_CLOSE,
|
||||
Permission::SETTINGS_VIEW,
|
||||
Permission::SETTINGS_UPDATE,
|
||||
Permission::ACTIVITY_LOGS_VIEW,
|
||||
],
|
||||
self::ADMIN_BAHAN_BAKU => [
|
||||
Permission::DASHBOARD_VIEW,
|
||||
|
||||
38
app/Http/Controllers/Admin/System/ActivityLogController.php
Normal file
38
app/Http/Controllers/Admin/System/ActivityLogController.php
Normal file
@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\System;
|
||||
|
||||
use App\Enums\ActivityEventLabel;
|
||||
use App\Http\Controllers\Concerns\ParsesDataTableQuery;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\System\ActivityLogService;
|
||||
use App\Support\ActivityLog\ModelLabel;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class ActivityLogController extends Controller
|
||||
{
|
||||
use ParsesDataTableQuery;
|
||||
|
||||
public function __construct(
|
||||
private readonly ActivityLogService $activityLogService,
|
||||
) {}
|
||||
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$tableQuery = $this->parseDataTableQuery($request);
|
||||
$event = $request->string('event')->toString();
|
||||
$subjectType = $request->string('subject_type')->toString();
|
||||
|
||||
return Inertia::render('admin/system/activity-logs/Index', [
|
||||
'activityLogs' => $this->activityLogService->paginateForIndex($tableQuery, $event, $subjectType),
|
||||
'filters' => $this->dataTableFilters($tableQuery, [
|
||||
'event' => $event,
|
||||
'subject_type' => $subjectType,
|
||||
]),
|
||||
'eventOptions' => ActivityEventLabel::selectOptions(),
|
||||
'subjectOptions' => ModelLabel::selectOptions(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@ -19,9 +19,16 @@ public function store(LoginRequest $request): RedirectResponse
|
||||
{
|
||||
$request->authenticate();
|
||||
|
||||
$user = $request->user();
|
||||
|
||||
activity()
|
||||
->causedBy($user)
|
||||
->event('login')
|
||||
->log('Berhasil masuk ke aplikasi');
|
||||
|
||||
$request->session()->regenerate();
|
||||
|
||||
$request->user()?->update(['last_login_at' => now()]);
|
||||
$user?->update(['last_login_at' => now()]);
|
||||
|
||||
return redirect()
|
||||
->intended(route('admin.dashboard'))
|
||||
|
||||
@ -11,6 +11,13 @@ class LogoutController extends Controller
|
||||
{
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
if ($user = $request->user()) {
|
||||
activity()
|
||||
->causedBy($user)
|
||||
->event('logout')
|
||||
->log('Keluar dari aplikasi');
|
||||
}
|
||||
|
||||
Auth::logout();
|
||||
|
||||
$request->session()->invalidate();
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Concerns\HasModuleMedia;
|
||||
use App\Models\Concerns\InteractsWithActivityLog;
|
||||
use App\Support\Media\MediaPresenter;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Attributes\Appends;
|
||||
@ -25,6 +26,7 @@
|
||||
class Attendance extends Model implements HasMedia
|
||||
{
|
||||
use HasModuleMedia;
|
||||
use InteractsWithActivityLog;
|
||||
|
||||
public static function mediaModuleName(): string
|
||||
{
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Concerns\InteractsWithActivityLog;
|
||||
use Illuminate\Database\Eloquent\Attributes\Appends;
|
||||
use Illuminate\Database\Eloquent\Attributes\Guarded;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
@ -12,6 +13,8 @@
|
||||
#[Appends(['balance_formatted'])]
|
||||
class CashAccount extends Model
|
||||
{
|
||||
use InteractsWithActivityLog;
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Concerns\HasModuleMedia;
|
||||
use App\Models\Concerns\InteractsWithActivityLog;
|
||||
use Illuminate\Database\Eloquent\Attributes\Appends;
|
||||
use Illuminate\Database\Eloquent\Attributes\Guarded;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
@ -24,6 +25,7 @@
|
||||
class CashTransaction extends Model implements HasMedia
|
||||
{
|
||||
use HasModuleMedia;
|
||||
use InteractsWithActivityLog;
|
||||
use SoftDeletes;
|
||||
|
||||
public static function mediaModuleName(): string
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Concerns\InteractsWithActivityLog;
|
||||
use Illuminate\Database\Eloquent\Attributes\Guarded;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
@ -12,6 +13,7 @@
|
||||
#[Sluggable(from: 'name', to: 'slug')]
|
||||
class Category extends Model
|
||||
{
|
||||
use InteractsWithActivityLog;
|
||||
use SoftDeletes;
|
||||
|
||||
public function products(): BelongsToMany
|
||||
|
||||
37
app/Models/Concerns/InteractsWithActivityLog.php
Normal file
37
app/Models/Concerns/InteractsWithActivityLog.php
Normal file
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Concerns;
|
||||
|
||||
use App\Support\ActivityLog\ModelLabel;
|
||||
use Spatie\Activitylog\Models\Concerns\LogsActivity;
|
||||
use Spatie\Activitylog\Support\LogOptions;
|
||||
|
||||
trait InteractsWithActivityLog
|
||||
{
|
||||
use LogsActivity;
|
||||
|
||||
public function getActivitylogOptions(): LogOptions
|
||||
{
|
||||
return LogOptions::defaults()
|
||||
->logUnguarded()
|
||||
->logOnlyDirty()
|
||||
->dontLogEmptyChanges()
|
||||
->logExcept([
|
||||
'password',
|
||||
'remember_token',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'deleted_at',
|
||||
])
|
||||
->setDescriptionForEvent(function (string $eventName): string {
|
||||
$label = ModelLabel::for(static::class);
|
||||
|
||||
return match ($eventName) {
|
||||
'created' => "{$label} dibuat",
|
||||
'updated' => "{$label} diperbarui",
|
||||
'deleted' => "{$label} dihapus",
|
||||
default => "{$label} {$eventName}",
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Concerns\InteractsWithActivityLog;
|
||||
use Illuminate\Database\Eloquent\Attributes\Guarded;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
@ -10,6 +11,7 @@
|
||||
#[Guarded(['id'])]
|
||||
class Customer extends Model
|
||||
{
|
||||
use InteractsWithActivityLog;
|
||||
use SoftDeletes;
|
||||
|
||||
public function orders(): HasMany
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
|
||||
use App\Enums\CuttingStatus;
|
||||
use App\Models\Concerns\HasRejection;
|
||||
use App\Models\Concerns\InteractsWithActivityLog;
|
||||
use Illuminate\Database\Eloquent\Attributes\Appends;
|
||||
use Illuminate\Database\Eloquent\Attributes\Guarded;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
@ -20,6 +21,7 @@
|
||||
class Cutting extends Model
|
||||
{
|
||||
use HasRejection;
|
||||
use InteractsWithActivityLog;
|
||||
use SoftDeletes;
|
||||
|
||||
protected function casts(): array
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Concerns\InteractsWithActivityLog;
|
||||
use Illuminate\Database\Eloquent\Attributes\Appends;
|
||||
use Illuminate\Database\Eloquent\Attributes\Guarded;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
@ -18,6 +19,8 @@
|
||||
])]
|
||||
class CuttingMaterial extends Model
|
||||
{
|
||||
use InteractsWithActivityLog;
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Concerns\InteractsWithActivityLog;
|
||||
use Illuminate\Database\Eloquent\Attributes\Guarded;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
@ -9,6 +10,8 @@
|
||||
#[Guarded(['id'])]
|
||||
class CuttingResult extends Model
|
||||
{
|
||||
use InteractsWithActivityLog;
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Enums\EmploymentStatus;
|
||||
use App\Models\Concerns\InteractsWithActivityLog;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Attributes\Appends;
|
||||
use Illuminate\Database\Eloquent\Attributes\Guarded;
|
||||
@ -18,6 +19,7 @@
|
||||
#[Appends(['base_salary_formatted', 'join_date_formatted', 'resign_date_formatted', 'join_date_input', 'employment_status_label'])]
|
||||
class Employee extends Model
|
||||
{
|
||||
use InteractsWithActivityLog;
|
||||
use SoftDeletes;
|
||||
|
||||
protected function casts(): array
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
use App\Enums\EmployeeAdvanceStatus;
|
||||
use App\Models\Concerns\HasModuleMedia;
|
||||
use App\Models\Concerns\HasRejection;
|
||||
use App\Models\Concerns\InteractsWithActivityLog;
|
||||
use Illuminate\Database\Eloquent\Attributes\Appends;
|
||||
use Illuminate\Database\Eloquent\Attributes\Guarded;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
@ -29,6 +30,7 @@ class EmployeeAdvance extends Model implements HasMedia
|
||||
{
|
||||
use HasModuleMedia;
|
||||
use HasRejection;
|
||||
use InteractsWithActivityLog;
|
||||
|
||||
public static function mediaModuleName(): string
|
||||
{
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Concerns\HasModuleMedia;
|
||||
use App\Models\Concerns\InteractsWithActivityLog;
|
||||
use Illuminate\Database\Eloquent\Attributes\Appends;
|
||||
use Illuminate\Database\Eloquent\Attributes\Guarded;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
@ -16,6 +17,7 @@
|
||||
class Expense extends Model implements HasMedia
|
||||
{
|
||||
use HasModuleMedia;
|
||||
use InteractsWithActivityLog;
|
||||
use SoftDeletes;
|
||||
|
||||
public static function mediaModuleName(): string
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
|
||||
use App\Enums\LeaveRequestStatus;
|
||||
use App\Models\Concerns\HasRejection;
|
||||
use App\Models\Concerns\InteractsWithActivityLog;
|
||||
use Illuminate\Database\Eloquent\Attributes\Appends;
|
||||
use Illuminate\Database\Eloquent\Attributes\Guarded;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
@ -26,6 +27,7 @@
|
||||
class LeaveRequest extends Model
|
||||
{
|
||||
use HasRejection;
|
||||
use InteractsWithActivityLog;
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
use App\Enums\OrderChannel;
|
||||
use App\Enums\OrderStatus;
|
||||
use App\Enums\PriceType;
|
||||
use App\Models\Concerns\InteractsWithActivityLog;
|
||||
use Illuminate\Database\Eloquent\Attributes\Appends;
|
||||
use Illuminate\Database\Eloquent\Attributes\Guarded;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
@ -26,6 +27,7 @@
|
||||
])]
|
||||
class Order extends Model
|
||||
{
|
||||
use InteractsWithActivityLog;
|
||||
use SoftDeletes;
|
||||
|
||||
protected function casts(): array
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Concerns\InteractsWithActivityLog;
|
||||
use Illuminate\Database\Eloquent\Attributes\Appends;
|
||||
use Illuminate\Database\Eloquent\Attributes\Guarded;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
@ -17,6 +18,8 @@
|
||||
])]
|
||||
class OrderItem extends Model
|
||||
{
|
||||
use InteractsWithActivityLog;
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
use App\Enums\EmployeeAdvanceStatus;
|
||||
use App\Enums\PayrollAdjustmentType;
|
||||
use App\Enums\PayrollStatus;
|
||||
use App\Models\Concerns\InteractsWithActivityLog;
|
||||
use Illuminate\Database\Eloquent\Attributes\Appends;
|
||||
use Illuminate\Database\Eloquent\Attributes\Guarded;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
@ -26,6 +27,8 @@
|
||||
])]
|
||||
class Payroll extends Model
|
||||
{
|
||||
use InteractsWithActivityLog;
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Enums\PayrollAdjustmentType;
|
||||
use App\Models\Concerns\InteractsWithActivityLog;
|
||||
use Illuminate\Database\Eloquent\Attributes\Appends;
|
||||
use Illuminate\Database\Eloquent\Attributes\Guarded;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
@ -13,6 +14,8 @@
|
||||
#[Appends(['amount_formatted', 'type_label', 'created_at_formatted', 'created_by_name'])]
|
||||
class PayrollAdjustment extends Model
|
||||
{
|
||||
use InteractsWithActivityLog;
|
||||
|
||||
public $timestamps = false;
|
||||
|
||||
protected function casts(): array
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Enums\PayrollPeriodStatus;
|
||||
use App\Models\Concerns\InteractsWithActivityLog;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Attributes\Appends;
|
||||
use Illuminate\Database\Eloquent\Attributes\Guarded;
|
||||
@ -15,6 +16,8 @@
|
||||
#[Appends(['period_label', 'status_label', 'closed_at_formatted'])]
|
||||
class PayrollPeriod extends Model
|
||||
{
|
||||
use InteractsWithActivityLog;
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Concerns\InteractsWithActivityLog;
|
||||
use Illuminate\Database\Eloquent\Attributes\Guarded;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
@ -13,6 +14,7 @@
|
||||
#[Sluggable(from: 'name', to: 'slug')]
|
||||
class Product extends Model
|
||||
{
|
||||
use InteractsWithActivityLog;
|
||||
use SoftDeletes;
|
||||
|
||||
protected function casts(): array
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Enums\PriceType;
|
||||
use App\Models\Concerns\InteractsWithActivityLog;
|
||||
use Illuminate\Database\Eloquent\Attributes\Appends;
|
||||
use Illuminate\Database\Eloquent\Attributes\Guarded;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
@ -13,6 +14,8 @@
|
||||
#[Appends(['price_formatted', 'price_input', 'type_label'])]
|
||||
class ProductPrice extends Model
|
||||
{
|
||||
use InteractsWithActivityLog;
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Concerns\HasModuleMedia;
|
||||
use App\Models\Concerns\InteractsWithActivityLog;
|
||||
use Illuminate\Database\Eloquent\Attributes\Guarded;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
@ -14,6 +15,7 @@
|
||||
class ProductVariant extends Model implements HasMedia
|
||||
{
|
||||
use HasModuleMedia;
|
||||
use InteractsWithActivityLog;
|
||||
use SoftDeletes;
|
||||
|
||||
public static function mediaModuleName(): string
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Concerns\HasModuleMedia;
|
||||
use App\Models\Concerns\InteractsWithActivityLog;
|
||||
use Illuminate\Database\Eloquent\Attributes\Appends;
|
||||
use Illuminate\Database\Eloquent\Attributes\Guarded;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
@ -22,6 +23,7 @@
|
||||
class Purchase extends Model implements HasMedia
|
||||
{
|
||||
use HasModuleMedia;
|
||||
use InteractsWithActivityLog;
|
||||
use SoftDeletes;
|
||||
|
||||
public static function mediaModuleName(): string
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Concerns\InteractsWithActivityLog;
|
||||
use Illuminate\Database\Eloquent\Attributes\Appends;
|
||||
use Illuminate\Database\Eloquent\Attributes\Guarded;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
@ -19,6 +20,7 @@
|
||||
])]
|
||||
class PurchaseItem extends Model
|
||||
{
|
||||
use InteractsWithActivityLog;
|
||||
use SoftDeletes;
|
||||
|
||||
protected function casts(): array
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Enums\RawMaterialUnit;
|
||||
use App\Models\Concerns\InteractsWithActivityLog;
|
||||
use Illuminate\Database\Eloquent\Attributes\Appends;
|
||||
use Illuminate\Database\Eloquent\Attributes\Guarded;
|
||||
use Illuminate\Database\Eloquent\Attributes\Scope;
|
||||
@ -16,6 +17,7 @@
|
||||
#[Appends(['unit_label', 'unit_abbreviation'])]
|
||||
class RawMaterial extends Model
|
||||
{
|
||||
use InteractsWithActivityLog;
|
||||
use SoftDeletes;
|
||||
|
||||
protected function casts(): array
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Concerns\HasModuleMedia;
|
||||
use App\Models\Concerns\InteractsWithActivityLog;
|
||||
use Illuminate\Database\Eloquent\Attributes\Appends;
|
||||
use Illuminate\Database\Eloquent\Attributes\Guarded;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
@ -17,6 +18,7 @@
|
||||
class RawMaterialPrice extends Model implements HasMedia
|
||||
{
|
||||
use HasModuleMedia;
|
||||
use InteractsWithActivityLog;
|
||||
use SoftDeletes;
|
||||
|
||||
public static function mediaModuleName(): string
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Concerns\InteractsWithActivityLog;
|
||||
use Illuminate\Database\Eloquent\Attributes\Guarded;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
@ -10,6 +11,8 @@
|
||||
#[Guarded(['id'])]
|
||||
class Rejection extends Model
|
||||
{
|
||||
use InteractsWithActivityLog;
|
||||
|
||||
public function rejectable(): MorphTo
|
||||
{
|
||||
return $this->morphTo();
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Concerns\InteractsWithActivityLog;
|
||||
use Illuminate\Database\Eloquent\Attributes\Guarded;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
@ -10,6 +11,7 @@
|
||||
#[Guarded(['id'])]
|
||||
class Supplier extends Model
|
||||
{
|
||||
use InteractsWithActivityLog;
|
||||
use SoftDeletes;
|
||||
|
||||
public function purchases(): HasMany
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Concerns\HasModuleMedia;
|
||||
use App\Models\Concerns\InteractsWithActivityLog;
|
||||
use Illuminate\Database\Eloquent\Attributes\Guarded;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Spatie\MediaLibrary\HasMedia;
|
||||
@ -11,6 +12,7 @@
|
||||
class SystemConfiguration extends Model implements HasMedia
|
||||
{
|
||||
use HasModuleMedia;
|
||||
use InteractsWithActivityLog;
|
||||
|
||||
public static function mediaModuleName(): string
|
||||
{
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Enums\Role;
|
||||
use App\Models\Concerns\InteractsWithActivityLog;
|
||||
use Illuminate\Database\Eloquent\Attributes\Appends;
|
||||
use Illuminate\Database\Eloquent\Attributes\Guarded;
|
||||
use Illuminate\Database\Eloquent\Attributes\Hidden;
|
||||
@ -15,6 +16,7 @@
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Spatie\Activitylog\Models\Concerns\CausesActivity;
|
||||
use Spatie\Permission\Traits\HasRoles;
|
||||
|
||||
#[Guarded(['id'])]
|
||||
@ -22,7 +24,7 @@
|
||||
#[Appends(['role_label'])]
|
||||
class User extends Authenticatable
|
||||
{
|
||||
use HasFactory, HasRoles, Notifiable, SoftDeletes;
|
||||
use CausesActivity, HasFactory, HasRoles, InteractsWithActivityLog, Notifiable, SoftDeletes;
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Enums\Gender;
|
||||
use App\Models\Concerns\InteractsWithActivityLog;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Attributes\Appends;
|
||||
use Illuminate\Database\Eloquent\Attributes\Guarded;
|
||||
@ -15,6 +16,7 @@
|
||||
#[Appends(['birth_date_formatted', 'birth_date_input', 'gender_label'])]
|
||||
class UserProfile extends Model
|
||||
{
|
||||
use InteractsWithActivityLog;
|
||||
use SoftDeletes;
|
||||
|
||||
protected function casts(): array
|
||||
|
||||
128
app/Services/System/ActivityLogService.php
Normal file
128
app/Services/System/ActivityLogService.php
Normal file
@ -0,0 +1,128 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\System;
|
||||
|
||||
use App\Enums\ActivityEventLabel;
|
||||
use App\Models\User;
|
||||
use App\Support\ActivityLog\ModelLabel;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Collection;
|
||||
use Spatie\Activitylog\Models\Activity;
|
||||
|
||||
class ActivityLogService
|
||||
{
|
||||
/**
|
||||
* @param array{search: string, sort: string, direction: 'asc'|'desc'} $tableQuery
|
||||
*/
|
||||
public function paginateForIndex(array $tableQuery, string $event = '', string $subjectType = ''): LengthAwarePaginator
|
||||
{
|
||||
$query = Activity::query()
|
||||
->with(['causer.profile'])
|
||||
->when($tableQuery['search'] !== '', function (Builder $query) use ($tableQuery): void {
|
||||
$search = $tableQuery['search'];
|
||||
$query->where(function (Builder $query) use ($search): void {
|
||||
$query->where('description', 'like', "%{$search}%")
|
||||
->orWhere('event', 'like', "%{$search}%")
|
||||
->orWhereHasMorph(
|
||||
'causer',
|
||||
[User::class],
|
||||
fn (Builder $query) => $query
|
||||
->where('username', 'like', "%{$search}%")
|
||||
->orWhereHas('profile', fn (Builder $query) => $query->where('full_name', 'like', "%{$search}%")),
|
||||
);
|
||||
});
|
||||
})
|
||||
->when($event !== '', fn (Builder $query) => $query->where('event', $event))
|
||||
->when($subjectType !== '', fn (Builder $query) => $query->where('subject_type', $subjectType));
|
||||
|
||||
$this->applySorting($query, $tableQuery['sort'], $tableQuery['direction']);
|
||||
|
||||
return $query
|
||||
->paginate(10)
|
||||
->withQueryString()
|
||||
->through(fn (Activity $activity) => $this->present($activity));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{
|
||||
* id: int,
|
||||
* description: string,
|
||||
* event: string|null,
|
||||
* event_label: string,
|
||||
* subject_type: string|null,
|
||||
* subject_label: string,
|
||||
* subject_id: int|null,
|
||||
* causer_name: string,
|
||||
* created_at: string|null,
|
||||
* created_at_formatted: string|null,
|
||||
* changes: list<array{field: string, old: mixed, new: mixed}>
|
||||
* }
|
||||
*/
|
||||
private function present(Activity $activity): array
|
||||
{
|
||||
return [
|
||||
'id' => $activity->id,
|
||||
'description' => $activity->description,
|
||||
'event' => $activity->event,
|
||||
'event_label' => ActivityEventLabel::labelFor($activity->event),
|
||||
'subject_type' => $activity->subject_type,
|
||||
'subject_label' => ModelLabel::for($activity->subject_type),
|
||||
'subject_id' => $activity->subject_id,
|
||||
'causer_name' => $this->resolveCauserName($activity->causer),
|
||||
'created_at' => $activity->created_at?->toIso8601String(),
|
||||
'created_at_formatted' => $activity->created_at?->translatedFormat('l, d F Y H:i'),
|
||||
'changes' => $this->formatChanges($activity->attribute_changes),
|
||||
];
|
||||
}
|
||||
|
||||
private function resolveCauserName(?Model $causer): string
|
||||
{
|
||||
if (! $causer instanceof User) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
return $causer->profile?->full_name ?? $causer->username ?? '-';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array{field: string, old: mixed, new: mixed}>
|
||||
*/
|
||||
private function formatChanges(?Collection $changes): array
|
||||
{
|
||||
if ($changes === null || $changes->isEmpty()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$attributes = $changes->get('attributes', []);
|
||||
$old = $changes->get('old', []);
|
||||
|
||||
if (! is_array($attributes)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$formatted = [];
|
||||
|
||||
foreach ($attributes as $field => $newValue) {
|
||||
$formatted[] = [
|
||||
'field' => (string) $field,
|
||||
'old' => is_array($old) ? ($old[$field] ?? null) : null,
|
||||
'new' => $newValue,
|
||||
];
|
||||
}
|
||||
|
||||
return $formatted;
|
||||
}
|
||||
|
||||
private function applySorting(Builder $query, string $sort, string $direction): void
|
||||
{
|
||||
if (in_array($sort, ['created_at', 'description', 'event'], true)) {
|
||||
$query->orderBy($sort, $direction);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$query->latest();
|
||||
}
|
||||
}
|
||||
95
app/Support/ActivityLog/ModelLabel.php
Normal file
95
app/Support/ActivityLog/ModelLabel.php
Normal file
@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support\ActivityLog;
|
||||
|
||||
use App\Models\Attendance;
|
||||
use App\Models\CashAccount;
|
||||
use App\Models\CashTransaction;
|
||||
use App\Models\Category;
|
||||
use App\Models\Customer;
|
||||
use App\Models\Cutting;
|
||||
use App\Models\CuttingMaterial;
|
||||
use App\Models\CuttingResult;
|
||||
use App\Models\Employee;
|
||||
use App\Models\EmployeeAdvance;
|
||||
use App\Models\Expense;
|
||||
use App\Models\LeaveRequest;
|
||||
use App\Models\Order;
|
||||
use App\Models\OrderItem;
|
||||
use App\Models\Payroll;
|
||||
use App\Models\PayrollAdjustment;
|
||||
use App\Models\PayrollPeriod;
|
||||
use App\Models\Product;
|
||||
use App\Models\ProductPrice;
|
||||
use App\Models\ProductVariant;
|
||||
use App\Models\Purchase;
|
||||
use App\Models\PurchaseItem;
|
||||
use App\Models\RawMaterial;
|
||||
use App\Models\RawMaterialPrice;
|
||||
use App\Models\Rejection;
|
||||
use App\Models\Supplier;
|
||||
use App\Models\SystemConfiguration;
|
||||
use App\Models\User;
|
||||
use App\Models\UserProfile;
|
||||
|
||||
class ModelLabel
|
||||
{
|
||||
/**
|
||||
* @var array<class-string, string>
|
||||
*/
|
||||
private const LABELS = [
|
||||
Attendance::class => 'Presensi',
|
||||
CashAccount::class => 'Akun Kas',
|
||||
CashTransaction::class => 'Transaksi Kas',
|
||||
Category::class => 'Kategori',
|
||||
Customer::class => 'Pelanggan',
|
||||
Cutting::class => 'Cutting',
|
||||
CuttingMaterial::class => 'Bahan Cutting',
|
||||
CuttingResult::class => 'Hasil Cutting',
|
||||
Employee::class => 'Pegawai',
|
||||
EmployeeAdvance::class => 'Kasbon',
|
||||
Expense::class => 'Pengeluaran',
|
||||
LeaveRequest::class => 'Pengajuan Cuti',
|
||||
Order::class => 'Pesanan',
|
||||
OrderItem::class => 'Item Pesanan',
|
||||
Payroll::class => 'Gaji',
|
||||
PayrollAdjustment::class => 'Penyesuaian Gaji',
|
||||
PayrollPeriod::class => 'Periode Gaji',
|
||||
Product::class => 'Produk',
|
||||
ProductPrice::class => 'Harga Produk',
|
||||
ProductVariant::class => 'Varian Produk',
|
||||
Purchase::class => 'Belanja',
|
||||
PurchaseItem::class => 'Item Belanja',
|
||||
RawMaterial::class => 'Bahan Baku',
|
||||
RawMaterialPrice::class => 'Harga Bahan Baku',
|
||||
Rejection::class => 'Penolakan',
|
||||
Supplier::class => 'Supplier',
|
||||
SystemConfiguration::class => 'Konfigurasi Sistem',
|
||||
User::class => 'Pengguna',
|
||||
UserProfile::class => 'Profil Pengguna',
|
||||
];
|
||||
|
||||
public static function for(?string $modelClass): string
|
||||
{
|
||||
if ($modelClass === null) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
return self::LABELS[$modelClass] ?? class_basename($modelClass);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array{value: string, label: string}>
|
||||
*/
|
||||
public static function selectOptions(): array
|
||||
{
|
||||
return collect(self::LABELS)
|
||||
->map(fn (string $label, string $class) => [
|
||||
'value' => $class,
|
||||
'label' => $label,
|
||||
])
|
||||
->sortBy('label')
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
}
|
||||
@ -14,6 +14,7 @@
|
||||
"laravel/framework": "^13.7",
|
||||
"laravel/tinker": "^3.0",
|
||||
"laravel/wayfinder": "^0.1.14",
|
||||
"spatie/laravel-activitylog": "^5.0",
|
||||
"spatie/laravel-medialibrary": "^11.23",
|
||||
"spatie/laravel-permission": "^8.0",
|
||||
"spatie/laravel-settings": "^3.9",
|
||||
|
||||
95
composer.lock
generated
95
composer.lock
generated
@ -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": "6e75752243def7f52763c43a3fb6eb14",
|
||||
"content-hash": "14f85c73d73d1a795e83a072d4d37995",
|
||||
"packages": [
|
||||
{
|
||||
"name": "brick/math",
|
||||
@ -3930,6 +3930,99 @@
|
||||
},
|
||||
"time": "2025-11-26T10:57:19+00:00"
|
||||
},
|
||||
{
|
||||
"name": "spatie/laravel-activitylog",
|
||||
"version": "5.0.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/spatie/laravel-activitylog.git",
|
||||
"reference": "0e00fe74fd071cc572a045459f6d4c9de33130bd"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/spatie/laravel-activitylog/zipball/0e00fe74fd071cc572a045459f6d4c9de33130bd",
|
||||
"reference": "0e00fe74fd071cc572a045459f6d4c9de33130bd",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"illuminate/config": "^12.0 || ^13.0",
|
||||
"illuminate/database": "^12.0 || ^13.0",
|
||||
"illuminate/support": "^12.0 || ^13.0",
|
||||
"php": "^8.4",
|
||||
"spatie/laravel-package-tools": "^1.6.3"
|
||||
},
|
||||
"require-dev": {
|
||||
"ext-json": "*",
|
||||
"larastan/larastan": "^3.0",
|
||||
"laravel/pint": "^1.29",
|
||||
"orchestra/testbench": "^10.0 || ^11.0",
|
||||
"pestphp/pest": "^4.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"providers": [
|
||||
"Spatie\\Activitylog\\ActivitylogServiceProvider"
|
||||
]
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
"src/helpers.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Spatie\\Activitylog\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Freek Van der Herten",
|
||||
"email": "freek@spatie.be",
|
||||
"homepage": "https://spatie.be",
|
||||
"role": "Developer"
|
||||
},
|
||||
{
|
||||
"name": "Sebastian De Deyne",
|
||||
"email": "sebastian@spatie.be",
|
||||
"homepage": "https://spatie.be",
|
||||
"role": "Developer"
|
||||
},
|
||||
{
|
||||
"name": "Tom Witkowski",
|
||||
"email": "dev.gummibeer@gmail.com",
|
||||
"homepage": "https://gummibeer.de",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "A very simple activity logger to monitor the users of your website or application",
|
||||
"homepage": "https://github.com/spatie/activitylog",
|
||||
"keywords": [
|
||||
"activity",
|
||||
"laravel",
|
||||
"log",
|
||||
"spatie",
|
||||
"user"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/spatie/laravel-activitylog/issues",
|
||||
"source": "https://github.com/spatie/laravel-activitylog/tree/5.0.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://spatie.be/open-source/support-us",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/spatie",
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2026-03-25T10:04:54+00:00"
|
||||
},
|
||||
{
|
||||
"name": "spatie/laravel-medialibrary",
|
||||
"version": "11.23.0",
|
||||
|
||||
76
config/activitylog.php
Normal file
76
config/activitylog.php
Normal file
@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
use Spatie\Activitylog\Actions\CleanActivityLogAction;
|
||||
use Spatie\Activitylog\Actions\LogActivityAction;
|
||||
use Spatie\Activitylog\Models\Activity;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
* If set to false, no activities will be saved to the database.
|
||||
*/
|
||||
'enabled' => env('ACTIVITYLOG_ENABLED', true),
|
||||
|
||||
/*
|
||||
* When the clean command is executed, all recording activities older than
|
||||
* the number of days specified here will be deleted.
|
||||
*/
|
||||
'clean_after_days' => 365,
|
||||
|
||||
/*
|
||||
* If no log name is passed to the activity() helper
|
||||
* we use this default log name.
|
||||
*/
|
||||
'default_log_name' => 'default',
|
||||
|
||||
/*
|
||||
* You can specify an auth driver here that gets user models.
|
||||
* If this is null we'll use the current Laravel auth driver.
|
||||
*/
|
||||
'default_auth_driver' => null,
|
||||
|
||||
/*
|
||||
* If set to true, the subject relationship on activities
|
||||
* will include soft deleted models.
|
||||
*/
|
||||
'include_soft_deleted_subjects' => true,
|
||||
|
||||
/*
|
||||
* This model will be used to log activity.
|
||||
* It should implement the Spatie\Activitylog\Contracts\Activity interface
|
||||
* and extend Illuminate\Database\Eloquent\Model.
|
||||
*/
|
||||
'activity_model' => Activity::class,
|
||||
|
||||
/*
|
||||
* These attributes will be excluded from logging for all models.
|
||||
* Model-specific exclusions via logExcept() are merged with these.
|
||||
*/
|
||||
'default_except_attributes' => [
|
||||
'password',
|
||||
'remember_token',
|
||||
],
|
||||
|
||||
/*
|
||||
* When enabled, activities are buffered in memory and inserted in a
|
||||
* single bulk query after the response has been sent to the client.
|
||||
* This can significantly reduce the number of database queries when
|
||||
* many activities are logged during a single request.
|
||||
*
|
||||
* Only enable this if your application logs a high volume of activities
|
||||
* per request. Buffered activities will not have an ID until the
|
||||
* buffer is flushed.
|
||||
*/
|
||||
'buffer' => [
|
||||
'enabled' => env('ACTIVITYLOG_BUFFER_ENABLED', false),
|
||||
],
|
||||
|
||||
/*
|
||||
* These action classes can be overridden to customize how activities
|
||||
* are logged and cleaned. Your custom classes must extend the originals.
|
||||
*/
|
||||
'actions' => [
|
||||
'log_activity' => LogActivityAction::class,
|
||||
'clean_log' => CleanActivityLogAction::class,
|
||||
],
|
||||
];
|
||||
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('activity_log', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('log_name')->nullable()->index();
|
||||
$table->text('description');
|
||||
$table->nullableMorphs('subject', 'subject');
|
||||
$table->string('event')->nullable();
|
||||
$table->nullableMorphs('causer', 'causer');
|
||||
$table->json('attribute_changes')->nullable();
|
||||
$table->json('properties')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { Link, usePage } from '@inertiajs/vue3';
|
||||
import { Banknote, CalendarDays, Clock, FolderTree, Layers, LayoutDashboard, Package, Receipt, Scissors, Settings2, ShoppingBag, ShoppingCart, User, UserCheck, Users, Wallet, WalletCards } from '@lucide/vue';
|
||||
import { Banknote, CalendarDays, Clock, FolderTree, History, Layers, LayoutDashboard, Package, Receipt, Scissors, Settings2, ShoppingBag, ShoppingCart, User, UserCheck, Users, Wallet, WalletCards } from '@lucide/vue';
|
||||
import { computed } from 'vue';
|
||||
import {
|
||||
Sidebar,
|
||||
@ -45,7 +45,8 @@ const showMasterMenu = computed(() => (
|
||||
|| can('suppliers.view') || can('customers.view')
|
||||
));
|
||||
const isSettingActive = computed(() => page.url.startsWith('/admin/system/setting'));
|
||||
const showSystemMenu = computed(() => can('settings.view'));
|
||||
const isActivityLogsActive = computed(() => page.url.startsWith('/admin/system/activity-logs'));
|
||||
const showSystemMenu = computed(() => can('settings.view') || can('activity-logs.view'));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -239,6 +240,14 @@ const showSystemMenu = computed(() => can('settings.view'));
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem v-if="can('activity-logs.view')">
|
||||
<SidebarMenuButton as-child tooltip="Log Aktivitas" :is-active="isActivityLogsActive">
|
||||
<Link href="/admin/system/activity-logs">
|
||||
<History />
|
||||
<span>Log Aktivitas</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
|
||||
@ -0,0 +1,94 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import type { ActivityLogListItem } from '@/types/activity-log';
|
||||
|
||||
const open = defineModel<boolean>('open', { default: false });
|
||||
|
||||
const props = defineProps<{
|
||||
activityLog?: ActivityLogListItem | null;
|
||||
}>();
|
||||
|
||||
const hasChanges = computed(() => (props.activityLog?.changes.length ?? 0) > 0);
|
||||
|
||||
function formatValue(value: unknown): string {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return '-';
|
||||
}
|
||||
|
||||
if (typeof value === 'boolean') {
|
||||
return value ? 'Ya' : 'Tidak';
|
||||
}
|
||||
|
||||
if (typeof value === 'object') {
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
|
||||
return String(value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog v-model:open="open">
|
||||
<DialogContent class="max-h-[85vh] overflow-y-auto sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Detail Log Aktivitas</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div v-if="activityLog" class="space-y-4 text-sm">
|
||||
<div class="grid gap-3 sm:grid-cols-2">
|
||||
<div>
|
||||
<p class="text-muted-foreground">Waktu</p>
|
||||
<p class="font-medium">{{ activityLog.created_at_formatted ?? '-' }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-muted-foreground">Pengguna</p>
|
||||
<p class="font-medium">{{ activityLog.causer_name }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-muted-foreground">Aksi</p>
|
||||
<p class="font-medium">{{ activityLog.event_label }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-muted-foreground">Modul</p>
|
||||
<p class="font-medium">{{ activityLog.subject_label }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-muted-foreground">Deskripsi</p>
|
||||
<p class="font-medium">{{ activityLog.description }}</p>
|
||||
</div>
|
||||
|
||||
<div v-if="hasChanges" class="space-y-2">
|
||||
<p class="font-medium">Perubahan Data</p>
|
||||
<div class="overflow-hidden rounded-md border">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-muted/50">
|
||||
<tr>
|
||||
<th class="px-3 py-2 text-left font-medium">Field</th>
|
||||
<th class="px-3 py-2 text-left font-medium">Sebelum</th>
|
||||
<th class="px-3 py-2 text-left font-medium">Sesudah</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="change in activityLog.changes" :key="change.field" class="border-t">
|
||||
<td class="px-3 py-2 align-top font-medium">{{ change.field }}</td>
|
||||
<td class="px-3 py-2 align-top text-muted-foreground">{{ formatValue(change.old) }}</td>
|
||||
<td class="px-3 py-2 align-top">{{ formatValue(change.new) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p v-else class="text-muted-foreground">Tidak ada perubahan data yang tercatat.</p>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</template>
|
||||
@ -0,0 +1,45 @@
|
||||
import type { ColumnDef } from '@tanstack/vue-table';
|
||||
import { h } from 'vue';
|
||||
import DataTableActions from '@/components/admin/system/activity-logs/data-table-actions.vue';
|
||||
import { DataTableColumnHeader } from '@/components/data-table';
|
||||
import type { ActivityLogListItem } from '@/types/activity-log';
|
||||
|
||||
export function createColumns(onView: (activityLog: ActivityLogListItem) => void): ColumnDef<ActivityLogListItem>[] {
|
||||
return [
|
||||
{
|
||||
accessorKey: 'created_at_formatted',
|
||||
enableSorting: true,
|
||||
header: () => h(DataTableColumnHeader, { title: 'Waktu', column: 'created_at' }),
|
||||
cell: ({ row }) => row.original.created_at_formatted ?? '-',
|
||||
},
|
||||
{
|
||||
accessorKey: 'causer_name',
|
||||
enableSorting: false,
|
||||
header: 'Pengguna',
|
||||
},
|
||||
{
|
||||
accessorKey: 'event_label',
|
||||
enableSorting: true,
|
||||
header: () => h(DataTableColumnHeader, { title: 'Aksi', column: 'event' }),
|
||||
},
|
||||
{
|
||||
accessorKey: 'subject_label',
|
||||
enableSorting: false,
|
||||
header: 'Modul',
|
||||
},
|
||||
{
|
||||
accessorKey: 'description',
|
||||
enableSorting: true,
|
||||
header: () => h(DataTableColumnHeader, { title: 'Deskripsi', column: 'description' }),
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
cell: ({ row }) => h(DataTableActions, {
|
||||
activityLog: row.original,
|
||||
onView: () => onView(row.original),
|
||||
}),
|
||||
},
|
||||
];
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import { Eye } from '@lucide/vue';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import type { ActivityLogListItem } from '@/types/activity-log';
|
||||
|
||||
defineProps<{
|
||||
activityLog: ActivityLogListItem;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
view: [];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Button variant="ghost" size="icon-sm" @click="emit('view')">
|
||||
<Eye class="size-4" />
|
||||
<span class="sr-only">Lihat detail</span>
|
||||
</Button>
|
||||
</template>
|
||||
120
resources/js/pages/admin/system/activity-logs/Index.vue
Normal file
120
resources/js/pages/admin/system/activity-logs/Index.vue
Normal file
@ -0,0 +1,120 @@
|
||||
<script setup lang="ts">
|
||||
import { Head } from '@inertiajs/vue3';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import ActivityLogDetailModal from '@/components/admin/system/activity-logs/ActivityLogDetailModal.vue';
|
||||
import { createColumns } from '@/components/admin/system/activity-logs/columns';
|
||||
import { DataTable } from '@/components/data-table';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { useDataTableQuery, useDataTableQuerySync } from '@/composables/useDataTableQuery';
|
||||
import AdminLayout from '@/layouts/AdminLayout.vue';
|
||||
import type { ActivityLogListItem, PaginatedActivityLogs, SelectOption } from '@/types/activity-log';
|
||||
import type { DataTableSort } from '@/types/data-table';
|
||||
|
||||
const props = defineProps<{
|
||||
activityLogs: PaginatedActivityLogs;
|
||||
filters: {
|
||||
search: string;
|
||||
sort?: string;
|
||||
direction?: 'asc' | 'desc';
|
||||
event?: string;
|
||||
subject_type?: string;
|
||||
};
|
||||
eventOptions: SelectOption[];
|
||||
subjectOptions: SelectOption[];
|
||||
}>();
|
||||
|
||||
const search = ref(props.filters.search ?? '');
|
||||
const detailModalOpen = ref(false);
|
||||
const selectedActivityLog = ref<ActivityLogListItem | null>(null);
|
||||
|
||||
const { query, setSearch, setSort, setFilter, resetFilters, syncFromServer } = useDataTableQuery({
|
||||
url: '/admin/system/activity-logs',
|
||||
initial: { ...props.filters },
|
||||
filterKeys: ['event', 'subject_type'],
|
||||
});
|
||||
|
||||
useDataTableQuerySync(() => props.filters, syncFromServer);
|
||||
|
||||
const columns = computed(() => createColumns(openDetailModal));
|
||||
|
||||
const filterDefs = computed(() => [
|
||||
{
|
||||
key: 'event',
|
||||
label: 'Aksi',
|
||||
type: 'select' as const,
|
||||
options: props.eventOptions,
|
||||
},
|
||||
{
|
||||
key: 'subject_type',
|
||||
label: 'Modul',
|
||||
type: 'select' as const,
|
||||
options: props.subjectOptions,
|
||||
},
|
||||
]);
|
||||
|
||||
const filterValues = computed(() => ({
|
||||
event: query.value.event ?? '',
|
||||
subject_type: query.value.subject_type ?? '',
|
||||
}));
|
||||
|
||||
const currentSort = computed<DataTableSort | null>(() => {
|
||||
if (!query.value.sort || !query.value.direction) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
column: query.value.sort,
|
||||
direction: query.value.direction,
|
||||
};
|
||||
});
|
||||
|
||||
const pagination = computed(() => ({
|
||||
currentPage: props.activityLogs.current_page,
|
||||
perPage: props.activityLogs.per_page,
|
||||
lastPage: props.activityLogs.last_page,
|
||||
total: props.activityLogs.total,
|
||||
}));
|
||||
|
||||
function openDetailModal(activityLog: ActivityLogListItem) {
|
||||
selectedActivityLog.value = activityLog;
|
||||
detailModalOpen.value = true;
|
||||
}
|
||||
|
||||
watch(search, (value) => {
|
||||
setSearch(value);
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.filters.search,
|
||||
(value) => {
|
||||
search.value = value ?? '';
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
<Head title="Log Aktivitas" />
|
||||
|
||||
<AdminLayout>
|
||||
<div class="space-y-1">
|
||||
<h2 class="text-2xl font-bold tracking-tight">
|
||||
Log Aktivitas
|
||||
</h2>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
Riwayat semua aksi pengguna untuk keperluan audit.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Card class="min-w-0">
|
||||
<CardContent class="min-w-0">
|
||||
<DataTable v-model:search="search" :columns="columns" :data="activityLogs.data"
|
||||
:pagination="pagination" :pagination-links="activityLogs.links" :sort="currentSort"
|
||||
:filter-defs="filterDefs" :filter-values="filterValues" search-placeholder="Cari log aktivitas..."
|
||||
@sort-change="setSort" @filter-change="setFilter" @filters-reset="resetFilters" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<ActivityLogDetailModal v-model:open="detailModalOpen" :activity-log="selectedActivityLog" />
|
||||
</AdminLayout>
|
||||
</template>
|
||||
45
resources/js/types/activity-log.ts
Normal file
45
resources/js/types/activity-log.ts
Normal file
@ -0,0 +1,45 @@
|
||||
export type ActivityLogChange = {
|
||||
field: string;
|
||||
old: unknown;
|
||||
new: unknown;
|
||||
};
|
||||
|
||||
export type ActivityLogListItem = {
|
||||
id: number;
|
||||
description: string;
|
||||
event: string | null;
|
||||
event_label: string;
|
||||
subject_type: string | null;
|
||||
subject_label: string;
|
||||
subject_id: number | null;
|
||||
causer_name: string;
|
||||
created_at: string | null;
|
||||
created_at_formatted: string | null;
|
||||
changes: ActivityLogChange[];
|
||||
};
|
||||
|
||||
export type ActivityLogFilters = {
|
||||
search: string;
|
||||
sort?: string;
|
||||
direction?: 'asc' | 'desc' | null;
|
||||
event?: string;
|
||||
subject_type?: string;
|
||||
};
|
||||
|
||||
export type PaginatedActivityLogs = {
|
||||
data: ActivityLogListItem[];
|
||||
current_page: number;
|
||||
last_page: number;
|
||||
per_page: number;
|
||||
total: number;
|
||||
links: Array<{
|
||||
url: string | null;
|
||||
label: string;
|
||||
active: boolean;
|
||||
}>;
|
||||
};
|
||||
|
||||
export type SelectOption = {
|
||||
value: string;
|
||||
label: string;
|
||||
};
|
||||
@ -21,6 +21,7 @@
|
||||
use App\Http\Controllers\Admin\Master\ProductController;
|
||||
use App\Http\Controllers\Admin\Master\RawMaterialController;
|
||||
use App\Http\Controllers\Admin\Master\SupplierController;
|
||||
use App\Http\Controllers\Admin\System\ActivityLogController;
|
||||
use App\Http\Controllers\Admin\System\SettingController;
|
||||
use App\Http\Controllers\Auth\LoginController;
|
||||
use App\Http\Controllers\Auth\LogoutController;
|
||||
@ -244,6 +245,12 @@
|
||||
});
|
||||
|
||||
Route::prefix('system')->name('system.')->group(function () {
|
||||
Route::prefix('activity-logs')->name('activity-logs.')
|
||||
->middleware('permission:'.Permission::ACTIVITY_LOGS_VIEW->value)
|
||||
->group(function () {
|
||||
Route::get('/', [ActivityLogController::class, 'index'])->name('index');
|
||||
});
|
||||
|
||||
Route::prefix('setting')->name('setting.')
|
||||
->middleware('permission:'.Permission::SETTINGS_VIEW->value)
|
||||
->group(function () {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user