From 1a239502f2cf57fbdf2beab48d7a10efb0661e23 Mon Sep 17 00:00:00 2001 From: Ahmad Afandi Date: Sat, 15 Aug 2026 06:34:50 +0700 Subject: [PATCH 1/5] Penambahan fitur Logi Aktivitas --- .gitignore | 4 + .../Setting/LogAktivitasController.php | 17 ++ .../Setting/PengaturanDatabaseController.php | 87 ++++++--- .../Livewire/Pengaturan/LogAktivitasTable.php | 93 ++++++++++ app/Models/DataUmum.php | 9 +- app/Models/Komplain.php | 11 +- app/Models/Profil.php | 7 +- app/Models/SettingAplikasi.php | 14 +- app/Models/User.php | 12 +- app/Observers/DataUmumObserver.php | 60 ++++++ app/Observers/KomplainObserver.php | 60 ++++++ app/Observers/ProfilObserver.php | 60 ++++++ app/Observers/SettingAplikasiObserver.php | 60 ++++++ app/Observers/UserObserver.php | 53 +++++- app/Providers/AppServiceProvider.php | 43 ++++- app/Services/ActivityLogService.php | 99 ++++++++++ composer.json | 1 + composer.lock | 101 +++++++++- config/activitylog.php | 73 ++++++++ ...08_14_104239_create_activity_log_table.php | 23 +++ database/seeders/RoleSpatieSeeder.php | 4 +- public/css/admin-style.css | 48 +++++ .../layouts/dashboard_template.blade.php | 2 + .../pengaturan/log-aktivitas-table.blade.php | 161 ++++++++++++++++ .../setting/log-aktivitas/index.blade.php | 51 ++++++ .../vendor/laravel-log-viewer/index.blade.php | 4 + routes/web.php | 1 + .../Livewire/LogAktivitasTableTest.php | 84 +++++++++ .../Unit/Services/ActivityLogServiceTest.php | 173 ++++++++++++++++++ 29 files changed, 1355 insertions(+), 60 deletions(-) create mode 100644 app/Http/Controllers/Setting/LogAktivitasController.php create mode 100644 app/Http/Livewire/Pengaturan/LogAktivitasTable.php create mode 100644 app/Observers/DataUmumObserver.php create mode 100644 app/Observers/KomplainObserver.php create mode 100644 app/Observers/ProfilObserver.php create mode 100644 app/Observers/SettingAplikasiObserver.php create mode 100644 app/Services/ActivityLogService.php create mode 100644 config/activitylog.php create mode 100644 database/migrations/2026_08_14_104239_create_activity_log_table.php create mode 100644 resources/views/livewire/pengaturan/log-aktivitas-table.blade.php create mode 100644 resources/views/setting/log-aktivitas/index.blade.php create mode 100644 tests/Feature/Livewire/LogAktivitasTableTest.php create mode 100644 tests/Unit/Services/ActivityLogServiceTest.php diff --git a/.gitignore b/.gitignore index 309a4ae322..a4e724e526 100644 --- a/.gitignore +++ b/.gitignore @@ -71,3 +71,7 @@ graphify-out # Scribe (auto-generated, contains auth tokens) .scribe/ +specs/ +.specify/ + +AGENTS.md \ No newline at end of file diff --git a/app/Http/Controllers/Setting/LogAktivitasController.php b/app/Http/Controllers/Setting/LogAktivitasController.php new file mode 100644 index 0000000000..30e63c4cc2 --- /dev/null +++ b/app/Http/Controllers/Setting/LogAktivitasController.php @@ -0,0 +1,17 @@ + auth()->user() ? auth()->user()->name : 'Sistem', + 'exit_code' => $exitCode, + ]); return response()->json([ 'success' => false, @@ -88,10 +89,16 @@ public function createBackup() } Log::info('Ending backup process.'); + ActivityLogService::log('backup database', 'Proses backup database berhasil.', [ + 'user_name' => auth()->user() ? auth()->user()->name : 'Sistem', + ]); return response()->json(['success' => true, 'message' => 'Backup completed successfully']); } catch (\Exception $e) { Log::error('Backup process failed: ' . $e->getMessage(), ['exception' => $e]); + ActivityLogService::logFailed('backup database gagal', 'Proses backup database gagal: ' . $e->getMessage(), [ + 'user_name' => auth()->user() ? auth()->user()->name : 'Sistem', + ]); return response()->json(['success' => false, 'message' => 'Backup process failed', 'error' => $e->getMessage()], 500); } @@ -103,6 +110,11 @@ public function downloadBackup($file) $filePath = "{$this->destination}/{$file}"; if ($disk->exists($filePath)) { + ActivityLogService::log('unduh backup database', "Mengunduh file backup: {$file}", [ + 'user_name' => auth()->user() ? auth()->user()->name : 'Sistem', + 'file' => $file, + ]); + return $disk->download($filePath); } @@ -116,32 +128,17 @@ public function deleteBackup($file) if ($disk->exists($filePath)) { $disk->delete($filePath); + ActivityLogService::log('hapus backup database', "Menghapus file backup: {$file}", [ + 'user_name' => auth()->user() ? auth()->user()->name : 'Sistem', + 'file' => $file, + ]); + return redirect()->route('setting.pengaturan-database.backup')->with('success', 'Backup berhasil dihapus'); } return redirect()->route('setting.pengaturan-database.backup')->with('error', 'Backup tidak ditemukan'); } - /** - * Fungsi untuk format ukuran file - */ - private function formatSizeUnits($bytes) - { - if ($bytes >= 1073741824) { - return number_format($bytes / 1073741824, 2) . ' GB'; - } elseif ($bytes >= 1048576) { - return number_format($bytes / 1048576, 2) . ' MB'; - } elseif ($bytes >= 1024) { - return number_format($bytes / 1024, 2) . ' KB'; - } elseif ($bytes > 1) { - return $bytes . ' bytes'; - } elseif ($bytes == 1) { - return $bytes . ' byte'; - } else { - return '0 bytes'; - } - } - // RESTORE DATABASE public function restoreDatabase() @@ -152,7 +149,6 @@ public function restoreDatabase() return view('setting.pengaturan_database.table-restore', compact('page_title', 'page_description')); } - public function restoreBackup(Request $request) { $request->validate([ @@ -179,7 +175,7 @@ public function restoreBackup(Request $request) $filename = basename($path); $allowedChars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789._-'; - if (! preg_match("/^[" . $allowedChars . "]+$/", $filename)) { + if (! preg_match('/^[' . $allowedChars . ']+$/', $filename)) { return response()->json([ 'success' => false, 'message' => 'Nama file tidak valid. Hanya boleh mengandung huruf (a-z/A-Z), angka (0-9), titik (.), dan garis bawah (_).', @@ -209,12 +205,19 @@ public function restoreBackup(Request $request) Log::info('Restore from ZIP: ' . $finalPath); $result = $this->restoreFromZip($finalPath); + ActivityLogService::log('restore database', "Restore database berhasil. {$result['files_restored']} file asset dipulihkan.", [ + 'user_name' => auth()->user() ? auth()->user()->name : 'Sistem', + ]); + return response()->json([ 'success' => true, 'message' => "Restore berhasil. Database dan {$result['files_restored']} file asset telah dipulihkan.", ], 200); } catch (\Exception $e) { Log::error('Restore error: ' . $e->getMessage()); + ActivityLogService::logFailed('restore database gagal', 'Restore database gagal: ' . $e->getMessage(), [ + 'user_name' => auth()->user() ? auth()->user()->name : 'Sistem', + ]); return response()->json([ 'success' => false, @@ -226,6 +229,30 @@ public function restoreBackup(Request $request) } } + /** + * Fungsi untuk format ukuran file. + */ + private function formatSizeUnits($bytes) + { + if ($bytes >= 1073741824) { + return number_format($bytes / 1073741824, 2) . ' GB'; + } + if ($bytes >= 1048576) { + return number_format($bytes / 1048576, 2) . ' MB'; + } + if ($bytes >= 1024) { + return number_format($bytes / 1024, 2) . ' KB'; + } + if ($bytes > 1) { + return $bytes . ' bytes'; + } + if ($bytes == 1) { + return $bytes . ' byte'; + } + return '0 bytes'; + + } + private function runRestoreDatabase($sqlFilePath) { // Ambil konfigurasi database diff --git a/app/Http/Livewire/Pengaturan/LogAktivitasTable.php b/app/Http/Livewire/Pengaturan/LogAktivitasTable.php new file mode 100644 index 0000000000..b3999b79fb --- /dev/null +++ b/app/Http/Livewire/Pengaturan/LogAktivitasTable.php @@ -0,0 +1,93 @@ + ['except' => ''], + 'dateTo' => ['except' => ''], + 'userId' => ['except' => ''], + 'event' => ['except' => ''], + 'keyword' => ['except' => ''], + ]; + + public function mount(): void + { + $this->dateFrom = now()->subDays(30)->format('Y-m-d'); + $this->dateTo = now()->format('Y-m-d'); + } + + public function render() + { + $activities = app(ActivityLogService::class) + ->getFilteredActivities($this->dateFrom, $this->dateTo, $this->userId, $this->event, $this->keyword) + ->latest() + ->paginate(25); + + $users = \App\Models\User::orderBy('name')->get(); + $events = Activity::select('event')->distinct()->pluck('event'); + + return view('livewire.pengaturan.log-aktivitas-table', compact('activities', 'users', 'events')); + } + + public function showDetail(int $id): void + { + $this->selectedActivityId = $id; + $this->selectedActivity = Activity::with('causer')->findOrFail($id); + $this->showModal = true; + } + + public function closeDetail(): void + { + $this->selectedActivityId = null; + $this->selectedActivity = null; + $this->showModal = false; + } + + /** + * Cek apakah aktivitas merupakan kegagalan (failed login atau error). + */ + public function isActivityFailed($activity): bool + { + // Cek flag 'failed' di properties + if (isset($activity->properties['failed']) && $activity->properties['failed']) { + return true; + } + + // Cek nama event yang mengandung kata 'gagal' + if (in_array(strtolower($activity->event), ['login gagal', 'gagal'])) { + return true; + } + + return false; + } + + public function resetFilters(): void + { + $this->dateFrom = now()->subDays(30)->format('Y-m-d'); + $this->dateTo = now()->format('Y-m-d'); + $this->userId = ''; + $this->event = ''; + $this->keyword = ''; + $this->closeDetail(); + } +} diff --git a/app/Models/DataUmum.php b/app/Models/DataUmum.php index d06c3e0548..aa992ad6ef 100644 --- a/app/Models/DataUmum.php +++ b/app/Models/DataUmum.php @@ -31,12 +31,14 @@ namespace App\Models; -use Illuminate\Database\Eloquent\Model; +use App\Observers\DataUmumObserver; use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Database\Eloquent\Model; class DataUmum extends Model { use HasFactory; + // Attributes protected $table = 'das_data_umum'; @@ -114,4 +116,9 @@ public function setPathAttribute($value) $this->attributes['path'] = $value; } + + protected static function booted(): void + { + static::observe(DataUmumObserver::class); + } } diff --git a/app/Models/Komplain.php b/app/Models/Komplain.php index d0b9e19fa5..b96491ddbf 100644 --- a/app/Models/Komplain.php +++ b/app/Models/Komplain.php @@ -31,12 +31,14 @@ namespace App\Models; +use App\Observers\KomplainObserver; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; class Komplain extends Model { use HasFactory; + protected $table = 'das_komplain'; protected $fillable = [ @@ -76,12 +78,17 @@ public static function generateID() $id = mt_rand(100000, 999999); $pid = ''; - if (! Komplain::where('komplain_id', '=', $id)->exists()) { + if (! self::where('komplain_id', '=', $id)->exists()) { $pid = $id; } else { - self::generateID(); + return self::generateID(); } return $pid; } + + protected static function booted(): void + { + static::observe(KomplainObserver::class); + } } diff --git a/app/Models/Profil.php b/app/Models/Profil.php index 1efeb48090..ed14190359 100644 --- a/app/Models/Profil.php +++ b/app/Models/Profil.php @@ -31,8 +31,9 @@ namespace App\Models; -use Illuminate\Database\Eloquent\Model; +use App\Observers\ProfilObserver; use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Facades\Cache; class Profil extends Model @@ -93,9 +94,9 @@ public function strukturOrganisasi() // return $this->hasMany(Penduduk::class, 'kecamatan_id', 'kecamatan_id')->where('status_dasar', 1); // } - protected static function boot() + protected static function booted(): void { - parent::boot(); + static::observe(ProfilObserver::class); static::saved(function () { Cache::forget('profil'); diff --git a/app/Models/SettingAplikasi.php b/app/Models/SettingAplikasi.php index 79267e6a2a..e4e9cf6184 100644 --- a/app/Models/SettingAplikasi.php +++ b/app/Models/SettingAplikasi.php @@ -31,6 +31,7 @@ namespace App\Models; +use App\Observers\SettingAplikasiObserver; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Facades\Cache; @@ -38,6 +39,9 @@ class SettingAplikasi extends Model { use HasFactory; + + public $timestamps = false; + protected $table = 'das_setting'; protected $fillable = [ @@ -49,18 +53,12 @@ class SettingAplikasi extends Model 'option', ]; - public $timestamps = false; - - protected static function boot() + protected static function booted(): void { - parent::boot(); + static::observe(SettingAplikasiObserver::class); static::saved(function () { Cache::forget('setting'); }); - - static::updated(function () { - Cache::forget('setting'); - }); } } diff --git a/app/Models/User.php b/app/Models/User.php index 1357697313..6affd3b780 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -60,11 +60,6 @@ class User extends Authenticatable implements JWTSubject */ public const DEFAULT_PASSWORD = '12345678'; - protected static function booted(): void - { - static::observe(UserObserver::class); - } - /** * Field yang diizinkan untuk mass assignment. * @@ -155,7 +150,7 @@ public function scopeSuspend($query, $email) } /** - * Hash password + * Hash password. */ public function setPasswordAttribute($input) { @@ -200,4 +195,9 @@ public function passwordHistories() { return $this->hasMany(PasswordHistory::class); } + + protected static function booted(): void + { + static::observe(UserObserver::class); + } } diff --git a/app/Observers/DataUmumObserver.php b/app/Observers/DataUmumObserver.php new file mode 100644 index 0000000000..038bd1f9fd --- /dev/null +++ b/app/Observers/DataUmumObserver.php @@ -0,0 +1,60 @@ +wasChanged()) { + $changed = $dataUmum->getChanges(); + ActivityLogService::logAttributeChange( + 'insert data umum', + 'created', + auth()->id() ?? null, + $changed + ); + } + } + + public function updating(DataUmum $dataUmum): void + { + // About to update + } + + public function updated(DataUmum $dataUmum): void + { + if ($dataUmum->wasChanged()) { + $changed = $dataUmum->getChanges(); + ActivityLogService::logAttributeChange( + 'ubah data umum', + 'updated', + auth()->id() ?? null, + $changed + ); + } + } + + public function deleting(DataUmum $dataUmum): void + { + // About to delete + } + + public function deleted(DataUmum $dataUmum): void + { + ActivityLogService::logAttributeChange( + 'hapus data umum', + 'deleted', + auth()->id() ?? null, + $dataUmum->getOriginal() + ); + } +} \ No newline at end of file diff --git a/app/Observers/KomplainObserver.php b/app/Observers/KomplainObserver.php new file mode 100644 index 0000000000..2f46b24ff1 --- /dev/null +++ b/app/Observers/KomplainObserver.php @@ -0,0 +1,60 @@ +wasChanged()) { + $changed = $komplain->getChanges(); + ActivityLogService::logAttributeChange( + 'insert komplain', + 'created', + auth()->id() ?? null, + $changed + ); + } + } + + public function updating(Komplain $komplain): void + { + // About to update + } + + public function updated(Komplain $komplain): void + { + if ($komplain->wasChanged()) { + $changed = $komplain->getChanges(); + ActivityLogService::logAttributeChange( + 'ubah komplain', + 'updated', + auth()->id() ?? null, + $changed + ); + } + } + + public function deleting(Komplain $komplain): void + { + // About to delete + } + + public function deleted(Komplain $komplain): void + { + ActivityLogService::logAttributeChange( + 'hapus komplain', + 'deleted', + auth()->id() ?? null, + $komplain->getOriginal() + ); + } +} \ No newline at end of file diff --git a/app/Observers/ProfilObserver.php b/app/Observers/ProfilObserver.php new file mode 100644 index 0000000000..5192ceb050 --- /dev/null +++ b/app/Observers/ProfilObserver.php @@ -0,0 +1,60 @@ +wasChanged()) { + $changed = $profil->getChanges(); + ActivityLogService::logAttributeChange( + 'insert profil', + 'created', + auth()->id() ?? null, + $changed + ); + } + } + + public function updating(Profil $profil): void + { + // About to update + } + + public function updated(Profil $profil): void + { + if ($profil->wasChanged()) { + $changed = $profil->getChanges(); + ActivityLogService::logAttributeChange( + 'ubah profil', + 'updated', + auth()->id() ?? null, + $changed + ); + } + } + + public function deleting(Profil $profil): void + { + // About to delete + } + + public function deleted(Profil $profil): void + { + ActivityLogService::logAttributeChange( + 'hapus profil', + 'deleted', + auth()->id() ?? null, + $profil->getOriginal() + ); + } +} \ No newline at end of file diff --git a/app/Observers/SettingAplikasiObserver.php b/app/Observers/SettingAplikasiObserver.php new file mode 100644 index 0000000000..000469ffe4 --- /dev/null +++ b/app/Observers/SettingAplikasiObserver.php @@ -0,0 +1,60 @@ +wasChanged()) { + $changed = $setting->getChanges(); + ActivityLogService::logAttributeChange( + 'tambah pengaturan aplikasi', + 'created', + auth()->id() ?? null, + $changed + ); + } + } + + public function updating(SettingAplikasi $setting): void + { + // About to update + } + + public function updated(SettingAplikasi $setting): void + { + if ($setting->wasChanged()) { + $changed = $setting->getChanges(); + ActivityLogService::logAttributeChange( + 'ubah pengaturan aplikasi', + 'updated', + auth()->id() ?? null, + $changed + ); + } + } + + public function deleting(SettingAplikasi $setting): void + { + // About to delete + } + + public function deleted(SettingAplikasi $setting): void + { + ActivityLogService::logAttributeChange( + 'hapus pengaturan aplikasi', + 'deleted', + auth()->id() ?? null, + $setting->getOriginal() + ); + } +} diff --git a/app/Observers/UserObserver.php b/app/Observers/UserObserver.php index f80c06b1b1..3517d1d71c 100644 --- a/app/Observers/UserObserver.php +++ b/app/Observers/UserObserver.php @@ -3,10 +3,29 @@ namespace App\Observers; use App\Models\User; +use App\Services\ActivityLogService; use App\Services\PasswordHistoryService; class UserObserver { + public function creating(User $user): void + { + // New user creation + } + + public function created(User $user): void + { + if ($user->wasChanged()) { + $changed = $user->getChanges(); + ActivityLogService::logAttributeChange( + 'tambah pengguna', + 'created', + auth()->id() ?? null, + $changed + ); + } + } + public function saving(User $user): void { if ($user->isDirty('password') && $user->getOriginal('password')) { @@ -16,10 +35,40 @@ public function saving(User $user): void } } - public function saved(User $user): void + public function updating(User $user): void + { + // About to update + } + + public function updated(User $user): void { + if ($user->wasChanged()) { + $changed = $user->getChanges(); + ActivityLogService::logAttributeChange( + 'ubah pengguna', + 'updated', + auth()->id() ?? null, + $changed + ); + } + if ($user->wasChanged('password')) { app(PasswordHistoryService::class)->prune($user); } } -} + + public function deleting(User $user): void + { + // About to delete + } + + public function deleted(User $user): void + { + ActivityLogService::logAttributeChange( + 'suspend pengguna', + 'deleted', + auth()->id() ?? null, + $user->getOriginal() + ); + } +} \ No newline at end of file diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 253a57cdb4..e0421225d9 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -31,14 +31,19 @@ namespace App\Providers; +use App\Services\ActivityLogService; use App\Services\CacheService; use App\Support\Collection; +use Illuminate\Auth\Events\Failed; +use Illuminate\Auth\Events\Login; +use Illuminate\Auth\Events\Logout; +use Illuminate\Foundation\AliasLoader; use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Pagination\Paginator; -use Illuminate\Foundation\AliasLoader; use Illuminate\Support\Facades\Blade; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\Validator; @@ -96,6 +101,40 @@ public function boot(): void return true; }); } + + // Auto-log login + Event::listen(Login::class, function (Login $event) { + $user = $event->user; + $userId = $user ? $user->id : null; + ActivityLogService::log('login', "User: {$user->name} (ID: {$user->id})", [ + 'user_name' => $user ? $user->name : 'Sistem', + 'user_id' => $userId, + 'event' => 'login', + ]); + }); + + // Auto-log logout + Event::listen(Logout::class, function (Logout $event) { + $user = $event->user; + $userId = $user ? $user->id : null; + ActivityLogService::log('logout', "User: {$user->name} (ID: {$user->id})", [ + 'user_name' => $user ? $user->name : 'Sistem', + 'user_id' => $userId, + 'event' => 'logout', + ]); + }); + + // Auto-log failed login + Event::listen(Failed::class, function (Failed $event) { + $user = $event->user; + $userId = $user ? $user->id : null; + ActivityLogService::logFailed('login gagal', "User: {$user->name} (ID: {$user->id}) login gagal", [ + 'user_name' => $user ? $user->name : 'Sistem', + 'user_id' => $userId, + 'event' => 'login gagal', + 'attempted_username' => $event->username ?? null, + ], $userId); + }); } /** @@ -223,7 +262,7 @@ protected function file(): void */ protected function paginate(): void { - /** + /* * Paginate a standard Laravel Collection. * * @param int $perPage diff --git a/app/Services/ActivityLogService.php b/app/Services/ActivityLogService.php new file mode 100644 index 0000000000..783ec0ff87 --- /dev/null +++ b/app/Services/ActivityLogService.php @@ -0,0 +1,99 @@ +whereDate('created_at', '>=', $dateFrom); + } + + if ($dateTo) { + $query->whereDate('created_at', '<=', $dateTo); + } + + if ($userId) { + $query->where('causer_id', (int) $userId); + } + + if ($event) { + $query->where('event', $event); + } + + if ($keyword) { + $query->where(function ($q) use ($keyword) { + $q->where('description', 'like', "%{$keyword}%") + ->orWhere('properties->url_slug', 'like', "%{$keyword}%"); + }); + } + + return $query; + } + + public static function log(string $event, ?string $description = null, ?array $properties = []): Activity + { + $request = request(); + $properties = array_merge($properties ?? [], [ + 'ip_address' => $request ? $request->ip() : null, + 'user_agent' => $request ? $request->userAgent() : null, + 'url_slug' => $request ? $request->path() : null, + ]); + + return activity() + ->event($event) + ->withProperties($properties) + ->log($description ?? $event); + } + + public static function logFailed(string $event, ?string $description = null, ?array $properties = [], ?int $userId = null): Activity + { + $request = request(); + $properties = array_merge($properties ?? [], [ + 'ip_address' => $request ? $request->ip() : null, + 'user_agent' => $request ? $request->userAgent() : null, + 'url_slug' => $request ? $request->path() : null, + ]); + + // Ensure userId is set if not provided + if ($userId === null) { + $userId = $request ? ($request->user() ? $request->user()->id : null) : null; + } + + $properties['failed'] = true; + + return activity() + ->event($event) + ->withProperties($properties) + ->log($description ?? $event); + } + + public static function logAttributeChange(string $event, string $action, ?int $userId = null, array $changedAttributes = []): Activity + { + $request = request(); + $properties = [ + 'ip_address' => $request ? $request->ip() : null, + 'user_agent' => $request ? $request->userAgent() : null, + 'url_slug' => $request ? $request->path() : null, + 'action' => $action, + 'changed_attributes' => $changedAttributes, + ]; + + // Ensure userId is set if not provided + if ($userId === null) { + $userId = $request ? ($request->user() ? $request->user()->id : null) : null; + } + + $properties['user_id'] = $userId; + + return activity() + ->event($event) + ->withProperties($properties) + ->log($event); + } +} diff --git a/composer.json b/composer.json index 8977befef0..a6520fdfb2 100644 --- a/composer.json +++ b/composer.json @@ -31,6 +31,7 @@ "rap2hpoutre/laravel-log-viewer": "^3.0", "sentry/sentry-laravel": "^4.0", "simplepie/simplepie": "^1.0", + "spatie/laravel-activitylog": "^5.1", "spatie/laravel-backup": "^10.0", "spatie/laravel-fractal": "*", "spatie/laravel-html": "^3.2", diff --git a/composer.lock b/composer.lock index bd37c5fd43..a5b19f905e 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": "df56aba5c112793320edc63900fe707c", + "content-hash": "f5cc95c71f7062fe078645ed3419301c", "packages": [ { "name": "bensampo/laravel-enum", @@ -6270,6 +6270,99 @@ ], "time": "2026-02-21T21:10:25+00:00" }, + { + "name": "spatie/laravel-activitylog", + "version": "5.1.0", + "source": { + "type": "git", + "url": "https://github.com/spatie/laravel-activitylog.git", + "reference": "97c9f7e82033b18dc9db25de276f9b71e01449ea" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/laravel-activitylog/zipball/97c9f7e82033b18dc9db25de276f9b71e01449ea", + "reference": "97c9f7e82033b18dc9db25de276f9b71e01449ea", + "shasum": "" + }, + "require": { + "illuminate/config": "^13.0", + "illuminate/database": "^13.0", + "illuminate/support": "^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": "^11.0", + "pestphp/pest": "^5.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/laravel-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.1.0" + }, + "funding": [ + { + "url": "https://spatie.be/open-source/support-us", + "type": "custom" + }, + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2026-08-12T12:07:38+00:00" + }, { "name": "spatie/laravel-backup", "version": "10.3.1", @@ -16024,15 +16117,15 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": true, "prefer-lowest": false, "platform": { "php": "^8.4" }, - "platform-dev": [], + "platform-dev": {}, "platform-overrides": { "php": "8.4" }, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.9.0" } diff --git a/config/activitylog.php b/config/activitylog.php new file mode 100644 index 0000000000..27cd6aa06e --- /dev/null +++ b/config/activitylog.php @@ -0,0 +1,73 @@ + 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' => false, + + /* + * 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' => [], + + /* + * 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_08_14_104239_create_activity_log_table.php b/database/migrations/2026_08_14_104239_create_activity_log_table.php new file mode 100644 index 0000000000..5c17c24c31 --- /dev/null +++ b/database/migrations/2026_08_14_104239_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/database/seeders/RoleSpatieSeeder.php b/database/seeders/RoleSpatieSeeder.php index dab54988b5..fe95f39064 100644 --- a/database/seeders/RoleSpatieSeeder.php +++ b/database/seeders/RoleSpatieSeeder.php @@ -102,7 +102,7 @@ class RoleSpatieSeeder extends Seeder 'access.setting.aplikasi' => ['view', 'edit'], 'access.setting.info_sistem' => ['view'], 'access.setting.database' => ['view', 'export', 'import'], - 'access.setting.jenis_dokumen' => ['view', 'create', 'edit', 'delete'], + 'access.setting.jenis_dokumen' => ['view', 'create', 'edit', 'delete'], ]; /** @@ -180,7 +180,7 @@ public function run() 'access.setting.coa', 'access.setting.themes', 'access.setting.aplikasi', - 'access.setting.info_sistem', + 'access.setting.info_sistem', 'access.setting.database', 'access.setting.jenis_dokumen', ]; diff --git a/public/css/admin-style.css b/public/css/admin-style.css index a193a1927b..187c0bdd15 100644 --- a/public/css/admin-style.css +++ b/public/css/admin-style.css @@ -96,4 +96,52 @@ body.sidebar-collapse .sidebar-kabupaten { } .box-body .table-responsive .dropdown-menu { z-index: 1050; +} + +/* Simple centered modal for activity detail */ +.modal-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.5); + z-index: 1040; +} + +.modal-simple { + position: fixed; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + padding: 20px; + z-index: 1050; +} + +.modal-simple-content { + background: #fff; + border-radius: 6px; + width: 100%; + max-width: 800px; + max-height: calc(100vh - 40px); + overflow: hidden; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.35); +} + +.modal-simple-header { + padding: 15px; + border-bottom: 1px solid #e5e5e5; + display: flex; + align-items: center; + justify-content: space-between; +} + +.modal-simple-body { + padding: 15px; + overflow-y: auto; + max-height: calc(100vh - 160px); +} + +.modal-simple-footer { + padding: 15px; + border-top: 1px solid #e5e5e5; + text-align: right; } \ No newline at end of file diff --git a/resources/views/layouts/dashboard_template.blade.php b/resources/views/layouts/dashboard_template.blade.php index 7694fb1443..6749e7c957 100644 --- a/resources/views/layouts/dashboard_template.blade.php +++ b/resources/views/layouts/dashboard_template.blade.php @@ -21,6 +21,7 @@ + @livewireStyles @stack('css') @@ -102,6 +103,7 @@ + @livewireScripts @stack('scripts')