From 1d1f0e7941d0275d537f69945a295f79ebdb5283 Mon Sep 17 00:00:00 2001 From: Yoga Pangestu Date: Fri, 12 Jun 2026 11:10:36 +0700 Subject: [PATCH] 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. --- app/Enums/ActivityEventLabel.php | 45 ++++++ app/Enums/Permission.php | 5 + app/Enums/Role.php | 2 + .../Admin/System/ActivityLogController.php | 38 ++++++ app/Http/Controllers/Auth/LoginController.php | 9 +- .../Controllers/Auth/LogoutController.php | 7 + app/Models/Attendance.php | 2 + app/Models/CashAccount.php | 3 + app/Models/CashTransaction.php | 2 + app/Models/Category.php | 2 + .../Concerns/InteractsWithActivityLog.php | 37 +++++ app/Models/Customer.php | 2 + app/Models/Cutting.php | 2 + app/Models/CuttingMaterial.php | 3 + app/Models/CuttingResult.php | 3 + app/Models/Employee.php | 2 + app/Models/EmployeeAdvance.php | 2 + app/Models/Expense.php | 2 + app/Models/LeaveRequest.php | 2 + app/Models/Order.php | 2 + app/Models/OrderItem.php | 3 + app/Models/Payroll.php | 3 + app/Models/PayrollAdjustment.php | 3 + app/Models/PayrollPeriod.php | 3 + app/Models/Product.php | 2 + app/Models/ProductPrice.php | 3 + app/Models/ProductVariant.php | 2 + app/Models/Purchase.php | 2 + app/Models/PurchaseItem.php | 2 + app/Models/RawMaterial.php | 2 + app/Models/RawMaterialPrice.php | 2 + app/Models/Rejection.php | 3 + app/Models/Supplier.php | 2 + app/Models/SystemConfiguration.php | 2 + app/Models/User.php | 4 +- app/Models/UserProfile.php | 2 + app/Services/System/ActivityLogService.php | 128 ++++++++++++++++++ app/Support/ActivityLog/ModelLabel.php | 95 +++++++++++++ composer.json | 1 + composer.lock | 95 ++++++++++++- config/activitylog.php | 76 +++++++++++ ...06_12_035838_create_activity_log_table.php | 23 ++++ resources/js/components/AppSidebar.vue | 13 +- .../activity-logs/ActivityLogDetailModal.vue | 94 +++++++++++++ .../admin/system/activity-logs/columns.ts | 45 ++++++ .../activity-logs/data-table-actions.vue | 20 +++ .../admin/system/activity-logs/Index.vue | 120 ++++++++++++++++ resources/js/types/activity-log.ts | 45 ++++++ routes/web.php | 7 + 49 files changed, 969 insertions(+), 5 deletions(-) create mode 100644 app/Enums/ActivityEventLabel.php create mode 100644 app/Http/Controllers/Admin/System/ActivityLogController.php create mode 100644 app/Models/Concerns/InteractsWithActivityLog.php create mode 100644 app/Services/System/ActivityLogService.php create mode 100644 app/Support/ActivityLog/ModelLabel.php create mode 100644 config/activitylog.php create mode 100644 database/migrations/2026_06_12_035838_create_activity_log_table.php create mode 100644 resources/js/components/admin/system/activity-logs/ActivityLogDetailModal.vue create mode 100644 resources/js/components/admin/system/activity-logs/columns.ts create mode 100644 resources/js/components/admin/system/activity-logs/data-table-actions.vue create mode 100644 resources/js/pages/admin/system/activity-logs/Index.vue create mode 100644 resources/js/types/activity-log.ts diff --git a/app/Enums/ActivityEventLabel.php b/app/Enums/ActivityEventLabel.php new file mode 100644 index 0000000..24e8c44 --- /dev/null +++ b/app/Enums/ActivityEventLabel.php @@ -0,0 +1,45 @@ + '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 + */ + public static function selectOptions(): array + { + return collect(self::cases()) + ->map(fn (self $event) => [ + 'value' => $event->value, + 'label' => $event->label(), + ]) + ->all(); + } +} diff --git a/app/Enums/Permission.php b/app/Enums/Permission.php index 13a1ddf..5df3566 100644 --- a/app/Enums/Permission.php +++ b/app/Enums/Permission.php @@ -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', }; } diff --git a/app/Enums/Role.php b/app/Enums/Role.php index caf911a..ecb0d8f 100644 --- a/app/Enums/Role.php +++ b/app/Enums/Role.php @@ -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, diff --git a/app/Http/Controllers/Admin/System/ActivityLogController.php b/app/Http/Controllers/Admin/System/ActivityLogController.php new file mode 100644 index 0000000..c52a72b --- /dev/null +++ b/app/Http/Controllers/Admin/System/ActivityLogController.php @@ -0,0 +1,38 @@ +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(), + ]); + } +} diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php index d5025e7..9c4eec0 100644 --- a/app/Http/Controllers/Auth/LoginController.php +++ b/app/Http/Controllers/Auth/LoginController.php @@ -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')) diff --git a/app/Http/Controllers/Auth/LogoutController.php b/app/Http/Controllers/Auth/LogoutController.php index 998eeee..4422c71 100644 --- a/app/Http/Controllers/Auth/LogoutController.php +++ b/app/Http/Controllers/Auth/LogoutController.php @@ -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(); diff --git a/app/Models/Attendance.php b/app/Models/Attendance.php index eee4503..1f0e58d 100644 --- a/app/Models/Attendance.php +++ b/app/Models/Attendance.php @@ -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 { diff --git a/app/Models/CashAccount.php b/app/Models/CashAccount.php index a252f4c..6b4e3b8 100644 --- a/app/Models/CashAccount.php +++ b/app/Models/CashAccount.php @@ -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 [ diff --git a/app/Models/CashTransaction.php b/app/Models/CashTransaction.php index 1df08f1..ceacfcc 100644 --- a/app/Models/CashTransaction.php +++ b/app/Models/CashTransaction.php @@ -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 diff --git a/app/Models/Category.php b/app/Models/Category.php index 56cae0e..2414764 100644 --- a/app/Models/Category.php +++ b/app/Models/Category.php @@ -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 diff --git a/app/Models/Concerns/InteractsWithActivityLog.php b/app/Models/Concerns/InteractsWithActivityLog.php new file mode 100644 index 0000000..9507a69 --- /dev/null +++ b/app/Models/Concerns/InteractsWithActivityLog.php @@ -0,0 +1,37 @@ +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}", + }; + }); + } +} diff --git a/app/Models/Customer.php b/app/Models/Customer.php index 7b830c3..d94df35 100644 --- a/app/Models/Customer.php +++ b/app/Models/Customer.php @@ -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 diff --git a/app/Models/Cutting.php b/app/Models/Cutting.php index 55e96dd..ce353d1 100644 --- a/app/Models/Cutting.php +++ b/app/Models/Cutting.php @@ -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 diff --git a/app/Models/CuttingMaterial.php b/app/Models/CuttingMaterial.php index 63582ed..02f78fb 100644 --- a/app/Models/CuttingMaterial.php +++ b/app/Models/CuttingMaterial.php @@ -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 [ diff --git a/app/Models/CuttingResult.php b/app/Models/CuttingResult.php index 1f4edab..5405f92 100644 --- a/app/Models/CuttingResult.php +++ b/app/Models/CuttingResult.php @@ -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 [ diff --git a/app/Models/Employee.php b/app/Models/Employee.php index 49b36bf..8bc11ae 100644 --- a/app/Models/Employee.php +++ b/app/Models/Employee.php @@ -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 diff --git a/app/Models/EmployeeAdvance.php b/app/Models/EmployeeAdvance.php index 02ffa1c..34ceeac 100644 --- a/app/Models/EmployeeAdvance.php +++ b/app/Models/EmployeeAdvance.php @@ -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 { diff --git a/app/Models/Expense.php b/app/Models/Expense.php index fef60fc..2d397b7 100644 --- a/app/Models/Expense.php +++ b/app/Models/Expense.php @@ -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 diff --git a/app/Models/LeaveRequest.php b/app/Models/LeaveRequest.php index 9475e43..b599862 100644 --- a/app/Models/LeaveRequest.php +++ b/app/Models/LeaveRequest.php @@ -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 { diff --git a/app/Models/Order.php b/app/Models/Order.php index 80804eb..d3a4ebd 100644 --- a/app/Models/Order.php +++ b/app/Models/Order.php @@ -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 diff --git a/app/Models/OrderItem.php b/app/Models/OrderItem.php index e797e23..ac1bd48 100644 --- a/app/Models/OrderItem.php +++ b/app/Models/OrderItem.php @@ -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 [ diff --git a/app/Models/Payroll.php b/app/Models/Payroll.php index d036480..d48b1ac 100644 --- a/app/Models/Payroll.php +++ b/app/Models/Payroll.php @@ -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 [ diff --git a/app/Models/PayrollAdjustment.php b/app/Models/PayrollAdjustment.php index 60ff6f7..a7f9b3e 100644 --- a/app/Models/PayrollAdjustment.php +++ b/app/Models/PayrollAdjustment.php @@ -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 diff --git a/app/Models/PayrollPeriod.php b/app/Models/PayrollPeriod.php index 84cd7ca..21af811 100644 --- a/app/Models/PayrollPeriod.php +++ b/app/Models/PayrollPeriod.php @@ -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 [ diff --git a/app/Models/Product.php b/app/Models/Product.php index edca004..9ae9209 100644 --- a/app/Models/Product.php +++ b/app/Models/Product.php @@ -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 diff --git a/app/Models/ProductPrice.php b/app/Models/ProductPrice.php index ccb646e..3541650 100644 --- a/app/Models/ProductPrice.php +++ b/app/Models/ProductPrice.php @@ -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 [ diff --git a/app/Models/ProductVariant.php b/app/Models/ProductVariant.php index 3096536..dd53001 100644 --- a/app/Models/ProductVariant.php +++ b/app/Models/ProductVariant.php @@ -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 diff --git a/app/Models/Purchase.php b/app/Models/Purchase.php index c046202..d1acb0c 100644 --- a/app/Models/Purchase.php +++ b/app/Models/Purchase.php @@ -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 diff --git a/app/Models/PurchaseItem.php b/app/Models/PurchaseItem.php index a4da751..fba1825 100644 --- a/app/Models/PurchaseItem.php +++ b/app/Models/PurchaseItem.php @@ -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 diff --git a/app/Models/RawMaterial.php b/app/Models/RawMaterial.php index 159cdac..c053ce7 100644 --- a/app/Models/RawMaterial.php +++ b/app/Models/RawMaterial.php @@ -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 diff --git a/app/Models/RawMaterialPrice.php b/app/Models/RawMaterialPrice.php index 1c5ed04..00e97f8 100644 --- a/app/Models/RawMaterialPrice.php +++ b/app/Models/RawMaterialPrice.php @@ -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 diff --git a/app/Models/Rejection.php b/app/Models/Rejection.php index 618c367..30904d4 100644 --- a/app/Models/Rejection.php +++ b/app/Models/Rejection.php @@ -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(); diff --git a/app/Models/Supplier.php b/app/Models/Supplier.php index 37d1f92..570c4ea 100644 --- a/app/Models/Supplier.php +++ b/app/Models/Supplier.php @@ -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 diff --git a/app/Models/SystemConfiguration.php b/app/Models/SystemConfiguration.php index e8dfdad..a90ed6d 100644 --- a/app/Models/SystemConfiguration.php +++ b/app/Models/SystemConfiguration.php @@ -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 { diff --git a/app/Models/User.php b/app/Models/User.php index 8fc5481..0477acb 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -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 { diff --git a/app/Models/UserProfile.php b/app/Models/UserProfile.php index 1663189..a5f8ef9 100644 --- a/app/Models/UserProfile.php +++ b/app/Models/UserProfile.php @@ -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 diff --git a/app/Services/System/ActivityLogService.php b/app/Services/System/ActivityLogService.php new file mode 100644 index 0000000..7a448e8 --- /dev/null +++ b/app/Services/System/ActivityLogService.php @@ -0,0 +1,128 @@ +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 + * } + */ + 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 + */ + 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(); + } +} diff --git a/app/Support/ActivityLog/ModelLabel.php b/app/Support/ActivityLog/ModelLabel.php new file mode 100644 index 0000000..6c98234 --- /dev/null +++ b/app/Support/ActivityLog/ModelLabel.php @@ -0,0 +1,95 @@ + + */ + 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 + */ + public static function selectOptions(): array + { + return collect(self::LABELS) + ->map(fn (string $label, string $class) => [ + 'value' => $class, + 'label' => $label, + ]) + ->sortBy('label') + ->values() + ->all(); + } +} diff --git a/composer.json b/composer.json index 546ac6e..cb7e31c 100644 --- a/composer.json +++ b/composer.json @@ -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", diff --git a/composer.lock b/composer.lock index 42a7eac..35eede9 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "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", diff --git a/config/activitylog.php b/config/activitylog.php new file mode 100644 index 0000000..46da854 --- /dev/null +++ b/config/activitylog.php @@ -0,0 +1,76 @@ + 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, + ], +]; diff --git a/database/migrations/2026_06_12_035838_create_activity_log_table.php b/database/migrations/2026_06_12_035838_create_activity_log_table.php new file mode 100644 index 0000000..5c17c24 --- /dev/null +++ b/database/migrations/2026_06_12_035838_create_activity_log_table.php @@ -0,0 +1,23 @@ +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(); + }); + } +}; diff --git a/resources/js/components/AppSidebar.vue b/resources/js/components/AppSidebar.vue index b23a808..c85a150 100644 --- a/resources/js/components/AppSidebar.vue +++ b/resources/js/components/AppSidebar.vue @@ -1,6 +1,6 @@