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 @@