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..622a943af5
--- /dev/null
+++ b/app/Observers/SettingAplikasiObserver.php
@@ -0,0 +1,67 @@
+wasChanged()) {
+ $changed = $setting->getChanges();
+ ActivityLogService::logAttributeChange(
+ 'tambah pengaturan aplikasi',
+ 'created',
+ auth()->id() ?? null,
+ $changed,
+ subject: $setting,
+ causer: auth()->user()
+ );
+ }
+ }
+
+ 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,
+ subject: $setting,
+ causer: auth()->user()
+ );
+ }
+ }
+
+ 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(),
+ subject: $setting,
+ causer: auth()->user()
+ );
+ }
+}
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..a9f28e67ed 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: {$userId})", [
+ '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..cbfeeecc4e
--- /dev/null
+++ b/app/Services/ActivityLogService.php
@@ -0,0 +1,133 @@
+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
+ {
+ if (! self::isActivityLogAvailable()) {
+ return null;
+ }
+
+ $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
+ {
+ if (! self::isActivityLogAvailable()) {
+ return null;
+ }
+
+ $request = request();
+ $properties = array_merge($properties ?? [], [
+ 'ip_address' => $request ? $request->ip() : null,
+ 'user_agent' => $request ? $request->userAgent() : null,
+ 'url_slug' => $request ? $request->path() : null,
+ ]);
+
+ 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 = [], ?Model $subject = null, ?Model $causer = null): ?Activity
+ {
+ if (! self::isActivityLogAvailable()) {
+ return null;
+ }
+
+ $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,
+ ];
+
+ if ($userId === null) {
+ $userId = $request ? ($request->user() ? $request->user()->id : null) : null;
+ }
+
+ $properties['user_id'] = $userId;
+
+ $logger = activity()
+ ->event($event)
+ ->withProperties($properties);
+
+ if ($subject) {
+ $logger->on($subject);
+ }
+
+ if ($causer) {
+ $logger->causedBy($causer);
+ }
+
+ return $logger->log($event);
+ }
+
+ private static function isActivityLogAvailable(): bool
+ {
+ if (self::$activityLogAvailable !== null) {
+ return self::$activityLogAvailable;
+ }
+
+ self::$activityLogAvailable = Schema::hasTable('activity_log');
+
+ return self::$activityLogAvailable;
+ }
+}
diff --git a/catatan_rilis.md b/catatan_rilis.md
index f17a8c7472..5ee2f84821 100644
--- a/catatan_rilis.md
+++ b/catatan_rilis.md
@@ -5,6 +5,7 @@ Terimakasih [isi disini] yang telah berkontribusi langsung mengembangkan aplikas
#### FITUR
+1. [#1324](https://github.com/OpenSID/OpenDK/issues/1324) Penambahan fitur log aktivitas.
#### BUG
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..2fe00f3a3e 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",
@@ -2445,20 +2445,20 @@
},
{
"name": "laravel/framework",
- "version": "v13.24.0",
+ "version": "v13.25.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/framework.git",
- "reference": "6d481710375d2aa67656922ef760cdd2b18bcfe0"
+ "reference": "ed36fe882bd4eed4e6ff75343cbad8dbda03fdba"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/framework/zipball/6d481710375d2aa67656922ef760cdd2b18bcfe0",
- "reference": "6d481710375d2aa67656922ef760cdd2b18bcfe0",
+ "url": "https://api.github.com/repos/laravel/framework/zipball/ed36fe882bd4eed4e6ff75343cbad8dbda03fdba",
+ "reference": "ed36fe882bd4eed4e6ff75343cbad8dbda03fdba",
"shasum": ""
},
"require": {
- "brick/math": "^0.14.2 || ^0.15 || ^0.16 || ^0.17 || ^0.18",
+ "brick/math": "^0.14.2 || ^0.15 || ^0.16 || ^0.17 || ^0.18 || ^0.19",
"composer-runtime-api": "^2.2",
"doctrine/inflector": "^2.0.5",
"dragonmantank/cron-expression": "^3.4",
@@ -2668,7 +2668,7 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
- "time": "2026-08-04T15:54:59+00:00"
+ "time": "2026-08-11T13:56:22+00:00"
},
{
"name": "laravel/helpers",
@@ -2985,16 +2985,16 @@
},
{
"name": "league/commonmark",
- "version": "2.9.2",
+ "version": "2.10.0",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/commonmark.git",
- "reference": "72e9a87efcf41a8e83be3ed0866b69d77565cb12"
+ "reference": "d2d1aa8b35e072966c89bc0c66cf926e56767dc4"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/72e9a87efcf41a8e83be3ed0866b69d77565cb12",
- "reference": "72e9a87efcf41a8e83be3ed0866b69d77565cb12",
+ "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/d2d1aa8b35e072966c89bc0c66cf926e56767dc4",
+ "reference": "d2d1aa8b35e072966c89bc0c66cf926e56767dc4",
"shasum": ""
},
"require": {
@@ -3031,7 +3031,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-main": "2.10-dev"
+ "dev-main": "2.11-dev"
}
},
"autoload": {
@@ -3088,7 +3088,7 @@
"type": "tidelift"
}
],
- "time": "2026-08-11T00:58:45+00:00"
+ "time": "2026-08-11T16:06:25+00:00"
},
{
"name": "league/config",
@@ -3690,16 +3690,16 @@
},
{
"name": "maatwebsite/excel",
- "version": "3.1.69",
+ "version": "3.1.70",
"source": {
"type": "git",
"url": "https://github.com/SpartnerNL/Laravel-Excel.git",
- "reference": "ae5d65b7c9a2fac43bff4d44f796ac95d7a8e760"
+ "reference": "b5cafdfcf7ec63924e83303763be8fcae340f70b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/SpartnerNL/Laravel-Excel/zipball/ae5d65b7c9a2fac43bff4d44f796ac95d7a8e760",
- "reference": "ae5d65b7c9a2fac43bff4d44f796ac95d7a8e760",
+ "url": "https://api.github.com/repos/SpartnerNL/Laravel-Excel/zipball/b5cafdfcf7ec63924e83303763be8fcae340f70b",
+ "reference": "b5cafdfcf7ec63924e83303763be8fcae340f70b",
"shasum": ""
},
"require": {
@@ -3707,7 +3707,7 @@
"ext-json": "*",
"illuminate/support": "5.8.*||^6.0||^7.0||^8.0||^9.0||^10.0||^11.0||^12.0||^13.0",
"php": "^7.0||^8.0",
- "phpoffice/phpspreadsheet": "^1.30.4",
+ "phpoffice/phpspreadsheet": "^1.30.5",
"psr/simple-cache": "^1.0||^2.0||^3.0"
},
"require-dev": {
@@ -3755,7 +3755,7 @@
],
"support": {
"issues": "https://github.com/SpartnerNL/Laravel-Excel/issues",
- "source": "https://github.com/SpartnerNL/Laravel-Excel/tree/3.1.69"
+ "source": "https://github.com/SpartnerNL/Laravel-Excel/tree/3.1.70"
},
"funding": [
{
@@ -3767,7 +3767,7 @@
"type": "github"
}
],
- "time": "2026-04-30T20:03:58+00:00"
+ "time": "2026-08-13T09:11:54+00:00"
},
{
"name": "maennchen/zipstream-php",
@@ -4352,16 +4352,16 @@
},
{
"name": "nette/schema",
- "version": "v1.3.5",
+ "version": "v1.3.6",
"source": {
"type": "git",
"url": "https://github.com/nette/schema.git",
- "reference": "f0ab1a3cda782dbc5da270d28545236aa80c4002"
+ "reference": "c54350438cd6914616f790a49cb424605f421562"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/nette/schema/zipball/f0ab1a3cda782dbc5da270d28545236aa80c4002",
- "reference": "f0ab1a3cda782dbc5da270d28545236aa80c4002",
+ "url": "https://api.github.com/repos/nette/schema/zipball/c54350438cd6914616f790a49cb424605f421562",
+ "reference": "c54350438cd6914616f790a49cb424605f421562",
"shasum": ""
},
"require": {
@@ -4413,9 +4413,9 @@
],
"support": {
"issues": "https://github.com/nette/schema/issues",
- "source": "https://github.com/nette/schema/tree/v1.3.5"
+ "source": "https://github.com/nette/schema/tree/v1.3.6"
},
- "time": "2026-02-23T03:47:12+00:00"
+ "time": "2026-08-16T21:58:41+00:00"
},
{
"name": "nette/utils",
@@ -4567,23 +4567,23 @@
},
{
"name": "nunomaduro/collision",
- "version": "v8.9.5",
+ "version": "v8.9.4",
"source": {
"type": "git",
"url": "https://github.com/nunomaduro/collision.git",
- "reference": "fb53eacd509a1d303858e2d20cfebf2d630254ec"
+ "reference": "716af8f95a470e9094cfca09ed897b023be191a5"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/nunomaduro/collision/zipball/fb53eacd509a1d303858e2d20cfebf2d630254ec",
- "reference": "fb53eacd509a1d303858e2d20cfebf2d630254ec",
+ "url": "https://api.github.com/repos/nunomaduro/collision/zipball/716af8f95a470e9094cfca09ed897b023be191a5",
+ "reference": "716af8f95a470e9094cfca09ed897b023be191a5",
"shasum": ""
},
"require": {
"filp/whoops": "^2.18.4",
"nunomaduro/termwind": "^2.4.0",
"php": "^8.2.0",
- "symfony/console": "^7.4.14 || ^8.1.1"
+ "symfony/console": "^7.4.8 || ^8.0.8"
},
"conflict": {
"laravel/framework": "<11.48.0 || >=14.0.0",
@@ -4591,12 +4591,12 @@
},
"require-dev": {
"brianium/paratest": "^7.8.5",
- "larastan/larastan": "^3.10.0",
- "laravel/framework": "^11.48.0 || ^12.56.0 || ^13.20.0",
- "laravel/pint": "^1.29.3",
- "orchestra/testbench-core": "^9.12.0 || ^10.12.1 || ^11.3.5",
- "pestphp/pest": "^3.8.5 || ^4.7.5 || ^5.0.0",
- "sebastian/environment": "^7.2.1 || ^8.1.2 || ^9.3.2"
+ "larastan/larastan": "^3.9.6",
+ "laravel/framework": "^11.48.0 || ^12.56.0 || ^13.5.0",
+ "laravel/pint": "^1.29.1",
+ "orchestra/testbench-core": "^9.12.0 || ^10.12.1 || ^11.2.1",
+ "pestphp/pest": "^3.8.5 || ^4.4.3 || ^5.0.0",
+ "sebastian/environment": "^7.2.1 || ^8.0.4 || ^9.3.0"
},
"type": "library",
"extra": {
@@ -4659,7 +4659,7 @@
"type": "patreon"
}
],
- "time": "2026-07-15T19:09:14+00:00"
+ "time": "2026-04-21T14:04:20+00:00"
},
{
"name": "nunomaduro/termwind",
@@ -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",
@@ -7095,47 +7188,39 @@
},
{
"name": "symfony/console",
- "version": "v7.4.16",
+ "version": "v8.0.15",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
- "reference": "f4c69c9aed03abf933b294257d618bdd9b30a06d"
+ "reference": "7327288efcaf02a3838d2de0ae23915d64713e14"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/console/zipball/f4c69c9aed03abf933b294257d618bdd9b30a06d",
- "reference": "f4c69c9aed03abf933b294257d618bdd9b30a06d",
+ "url": "https://api.github.com/repos/symfony/console/zipball/7327288efcaf02a3838d2de0ae23915d64713e14",
+ "reference": "7327288efcaf02a3838d2de0ae23915d64713e14",
"shasum": ""
},
"require": {
- "php": ">=8.2",
- "symfony/deprecation-contracts": "^2.5|^3",
- "symfony/polyfill-mbstring": "~1.0",
+ "php": ">=8.4",
+ "symfony/polyfill-mbstring": "^1.0",
"symfony/service-contracts": "^2.5|^3",
- "symfony/string": "^7.2|^8.0"
- },
- "conflict": {
- "symfony/dependency-injection": "<6.4",
- "symfony/dotenv": "<6.4",
- "symfony/event-dispatcher": "<6.4",
- "symfony/lock": "<6.4",
- "symfony/process": "<6.4"
+ "symfony/string": "^7.4|^8.0"
},
"provide": {
"psr/log-implementation": "1.0|2.0|3.0"
},
"require-dev": {
"psr/log": "^1|^2|^3",
- "symfony/config": "^6.4|^7.0|^8.0",
- "symfony/dependency-injection": "^6.4|^7.0|^8.0",
- "symfony/event-dispatcher": "^6.4|^7.0|^8.0",
- "symfony/http-foundation": "^6.4|^7.0|^8.0",
- "symfony/http-kernel": "^6.4|^7.0|^8.0",
- "symfony/lock": "^6.4|^7.0|^8.0",
- "symfony/messenger": "^6.4|^7.0|^8.0",
- "symfony/process": "^6.4|^7.0|^8.0",
- "symfony/stopwatch": "^6.4|^7.0|^8.0",
- "symfony/var-dumper": "^6.4|^7.0|^8.0"
+ "symfony/config": "^7.4|^8.0",
+ "symfony/dependency-injection": "^7.4|^8.0",
+ "symfony/event-dispatcher": "^7.4|^8.0",
+ "symfony/http-foundation": "^7.4|^8.0",
+ "symfony/http-kernel": "^7.4|^8.0",
+ "symfony/lock": "^7.4|^8.0",
+ "symfony/messenger": "^7.4|^8.0",
+ "symfony/process": "^7.4|^8.0",
+ "symfony/stopwatch": "^7.4|^8.0",
+ "symfony/var-dumper": "^7.4|^8.0"
},
"type": "library",
"autoload": {
@@ -7169,7 +7254,7 @@
"terminal"
],
"support": {
- "source": "https://github.com/symfony/console/tree/v7.4.16"
+ "source": "https://github.com/symfony/console/tree/v8.0.15"
},
"funding": [
{
@@ -7189,7 +7274,7 @@
"type": "tidelift"
}
],
- "time": "2026-07-31T12:37:14+00:00"
+ "time": "2026-07-27T13:52:45+00:00"
},
{
"name": "symfony/css-selector",
@@ -9046,20 +9131,20 @@
},
{
"name": "symfony/process",
- "version": "v7.4.13",
+ "version": "v8.0.13",
"source": {
"type": "git",
"url": "https://github.com/symfony/process.git",
- "reference": "f5804be144caceb570f6747519999636b664f24c"
+ "reference": "4f23d9c7637ead9ed19f697fe93cb87fd9b379d4"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/process/zipball/f5804be144caceb570f6747519999636b664f24c",
- "reference": "f5804be144caceb570f6747519999636b664f24c",
+ "url": "https://api.github.com/repos/symfony/process/zipball/4f23d9c7637ead9ed19f697fe93cb87fd9b379d4",
+ "reference": "4f23d9c7637ead9ed19f697fe93cb87fd9b379d4",
"shasum": ""
},
"require": {
- "php": ">=8.2"
+ "php": ">=8.4"
},
"type": "library",
"autoload": {
@@ -9087,7 +9172,7 @@
"description": "Executes commands in sub-processes",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/process/tree/v7.4.13"
+ "source": "https://github.com/symfony/process/tree/v8.0.13"
},
"funding": [
{
@@ -9107,7 +9192,7 @@
"type": "tidelift"
}
],
- "time": "2026-05-23T16:05:06+00:00"
+ "time": "2026-05-23T18:05:53+00:00"
},
{
"name": "symfony/psr-http-message-bridge",
@@ -10504,16 +10589,16 @@
},
{
"name": "yajra/laravel-datatables-oracle",
- "version": "v13.1.6",
+ "version": "v13.2.0",
"source": {
"type": "git",
"url": "https://github.com/yajra/laravel-datatables.git",
- "reference": "662cbca5d7c7cecd1e21b123eafda0485f0f033d"
+ "reference": "1a3d674cdc0ef6a226c76f5ff0c3b6d8f3af15cc"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/yajra/laravel-datatables/zipball/662cbca5d7c7cecd1e21b123eafda0485f0f033d",
- "reference": "662cbca5d7c7cecd1e21b123eafda0485f0f033d",
+ "url": "https://api.github.com/repos/yajra/laravel-datatables/zipball/1a3d674cdc0ef6a226c76f5ff0c3b6d8f3af15cc",
+ "reference": "1a3d674cdc0ef6a226c76f5ff0c3b6d8f3af15cc",
"shasum": ""
},
"require": {
@@ -10581,7 +10666,7 @@
],
"support": {
"issues": "https://github.com/yajra/laravel-datatables/issues",
- "source": "https://github.com/yajra/laravel-datatables/tree/v13.1.6"
+ "source": "https://github.com/yajra/laravel-datatables/tree/v13.2.0"
},
"funding": [
{
@@ -10589,7 +10674,7 @@
"type": "github"
}
],
- "time": "2026-07-31T03:18:01+00:00"
+ "time": "2026-08-14T03:20:47+00:00"
}
],
"packages-dev": [
@@ -12237,19 +12322,21 @@
},
{
"name": "hamcrest/hamcrest-php",
- "version": "v2.1.1",
+ "version": "v3.0.0",
"source": {
"type": "git",
"url": "https://github.com/hamcrest/hamcrest-php.git",
- "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487"
+ "reference": "b61cd040da1a4925bc90a51c074f5297e7c0fa52"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/hamcrest/hamcrest-php/zipball/f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487",
- "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487",
+ "url": "https://api.github.com/repos/hamcrest/hamcrest-php/zipball/b61cd040da1a4925bc90a51c074f5297e7c0fa52",
+ "reference": "b61cd040da1a4925bc90a51c074f5297e7c0fa52",
"shasum": ""
},
"require": {
+ "ext-ctype": "*",
+ "ext-dom": "*",
"php": "^7.4|^8.0"
},
"replace": {
@@ -12258,13 +12345,15 @@
"kodova/hamcrest-php": "*"
},
"require-dev": {
+ "phpstan/phpstan": "^2.1",
+ "phpstan/phpstan-phpunit": "^2.0",
"phpunit/php-file-iterator": "^1.4 || ^2.0 || ^3.0",
"phpunit/phpunit": "^4.8.36 || ^5.7 || ^6.5 || ^7.0 || ^8.0 || ^9.0"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "2.1-dev"
+ "dev-master": "3.0-dev"
}
},
"autoload": {
@@ -12282,9 +12371,9 @@
],
"support": {
"issues": "https://github.com/hamcrest/hamcrest-php/issues",
- "source": "https://github.com/hamcrest/hamcrest-php/tree/v2.1.1"
+ "source": "https://github.com/hamcrest/hamcrest-php/tree/v3.0.0"
},
- "time": "2025-04-30T06:54:44+00:00"
+ "time": "2026-03-17T11:56:53+00:00"
},
{
"name": "justinrainbow/json-schema",
@@ -12717,29 +12806,28 @@
},
{
"name": "mockery/mockery",
- "version": "1.6.12",
+ "version": "1.6.13",
"source": {
"type": "git",
"url": "https://github.com/mockery/mockery.git",
- "reference": "1f4efdd7d3beafe9807b08156dfcb176d18f1699"
+ "reference": "9cb54414cdcd2ec5ca292e7ba19dba3a3444885d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/mockery/mockery/zipball/1f4efdd7d3beafe9807b08156dfcb176d18f1699",
- "reference": "1f4efdd7d3beafe9807b08156dfcb176d18f1699",
+ "url": "https://api.github.com/repos/mockery/mockery/zipball/9cb54414cdcd2ec5ca292e7ba19dba3a3444885d",
+ "reference": "9cb54414cdcd2ec5ca292e7ba19dba3a3444885d",
"shasum": ""
},
"require": {
- "hamcrest/hamcrest-php": "^2.0.1",
- "lib-pcre": ">=7.0",
+ "hamcrest/hamcrest-php": "^2.0 || ^3.0",
"php": ">=7.3"
},
"conflict": {
"phpunit/phpunit": "<8.0"
},
"require-dev": {
- "phpunit/phpunit": "^8.5 || ^9.6.17",
- "symplify/easy-coding-standard": "^12.1.14"
+ "phpunit/phpunit": "^9.6.36",
+ "symplify/easy-coding-standard": "^13.2.17"
},
"type": "library",
"autoload": {
@@ -12796,24 +12884,24 @@
"security": "https://github.com/mockery/mockery/security/advisories",
"source": "https://github.com/mockery/mockery"
},
- "time": "2024-05-16T03:13:13+00:00"
+ "time": "2026-08-15T03:07:32+00:00"
},
{
"name": "myclabs/deep-copy",
- "version": "1.13.4",
+ "version": "1.14.0",
"source": {
"type": "git",
"url": "https://github.com/myclabs/DeepCopy.git",
- "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a"
+ "reference": "8680aa248f8e07bc8fb43f56f0f5fc77a0c96aae"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a",
- "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a",
+ "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/8680aa248f8e07bc8fb43f56f0f5fc77a0c96aae",
+ "reference": "8680aa248f8e07bc8fb43f56f0f5fc77a0c96aae",
"shasum": ""
},
"require": {
- "php": "^7.1 || ^8.0"
+ "php": "^8.0"
},
"conflict": {
"doctrine/collections": "<1.6.8",
@@ -12848,55 +12936,55 @@
],
"support": {
"issues": "https://github.com/myclabs/DeepCopy/issues",
- "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4"
+ "source": "https://github.com/myclabs/DeepCopy/tree/1.14.0"
},
"funding": [
{
- "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy",
- "type": "tidelift"
+ "url": "https://github.com/mnapoli",
+ "type": "github"
}
],
- "time": "2025-08-01T08:46:24+00:00"
+ "time": "2026-08-11T10:17:44+00:00"
},
{
"name": "pestphp/pest",
- "version": "v4.7.8",
+ "version": "v4.7.0",
"source": {
"type": "git",
"url": "https://github.com/pestphp/pest.git",
- "reference": "5b2293f67adcf1b2320b33f521b94a692d18f360"
+ "reference": "2fc75cfcf03c041c804778fa894282234adc3c66"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/pestphp/pest/zipball/5b2293f67adcf1b2320b33f521b94a692d18f360",
- "reference": "5b2293f67adcf1b2320b33f521b94a692d18f360",
+ "url": "https://api.github.com/repos/pestphp/pest/zipball/2fc75cfcf03c041c804778fa894282234adc3c66",
+ "reference": "2fc75cfcf03c041c804778fa894282234adc3c66",
"shasum": ""
},
"require": {
"brianium/paratest": "^7.20.0",
"composer/xdebug-handler": "^3.0.5",
- "nunomaduro/collision": "^8.9.5",
+ "nunomaduro/collision": "^8.9.4",
"nunomaduro/termwind": "^2.4.0",
"pestphp/pest-plugin": "^4.0.0",
"pestphp/pest-plugin-arch": "^4.0.2",
"pestphp/pest-plugin-mutate": "^4.0.1",
"pestphp/pest-plugin-profanity": "^4.2.1",
"php": "^8.3.0",
- "phpunit/phpunit": "^12.5.33",
- "symfony/process": "^7.4.13|^8.1.0"
+ "phpunit/phpunit": "^12.5.24",
+ "symfony/process": "^7.4.8|^8.0.8"
},
"conflict": {
"filp/whoops": "<2.18.3",
- "phpunit/phpunit": ">12.5.33",
+ "phpunit/phpunit": ">12.5.24",
"sebastian/exporter": "<7.0.0",
"webmozart/assert": "<1.11.0"
},
"require-dev": {
- "mrpunyapal/peststan": "^0.2.12",
+ "mrpunyapal/peststan": "^0.2.9",
"pestphp/pest-dev-tools": "^4.1.0",
"pestphp/pest-plugin-browser": "^4.3.1",
"pestphp/pest-plugin-type-coverage": "^4.0.4",
- "psy/psysh": "^0.12.24"
+ "psy/psysh": "^0.12.22"
},
"bin": [
"bin/pest"
@@ -12923,6 +13011,7 @@
"Pest\\Plugins\\Verbose",
"Pest\\Plugins\\Version",
"Pest\\Plugins\\Shard",
+ "Pest\\Plugins\\Tia",
"Pest\\Plugins\\Parallel"
]
},
@@ -12962,7 +13051,7 @@
],
"support": {
"issues": "https://github.com/pestphp/pest/issues",
- "source": "https://github.com/pestphp/pest/tree/v4.7.8"
+ "source": "https://github.com/pestphp/pest/tree/v4.7.0"
},
"funding": [
{
@@ -12974,7 +13063,7 @@
"type": "github"
}
],
- "time": "2026-08-03T20:49:44+00:00"
+ "time": "2026-05-03T16:09:32+00:00"
},
{
"name": "pestphp/pest-plugin",
@@ -14261,43 +14350,43 @@
},
{
"name": "phpunit/phpunit",
- "version": "12.5.33",
+ "version": "12.5.24",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
- "reference": "b98e028a26c5c5ba7e4a54be96ccf35f2914d184"
+ "reference": "d75dd30597caa80e72fad2ef7904601a30ef1046"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/b98e028a26c5c5ba7e4a54be96ccf35f2914d184",
- "reference": "b98e028a26c5c5ba7e4a54be96ccf35f2914d184",
+ "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/d75dd30597caa80e72fad2ef7904601a30ef1046",
+ "reference": "d75dd30597caa80e72fad2ef7904601a30ef1046",
"shasum": ""
},
"require": {
"ext-dom": "*",
- "ext-filter": "*",
"ext-json": "*",
"ext-libxml": "*",
"ext-mbstring": "*",
+ "ext-xml": "*",
"ext-xmlwriter": "*",
"myclabs/deep-copy": "^1.13.4",
"phar-io/manifest": "^2.0.4",
"phar-io/version": "^3.2.1",
"php": ">=8.3",
- "phpunit/php-code-coverage": "^12.5.7",
+ "phpunit/php-code-coverage": "^12.5.6",
"phpunit/php-file-iterator": "^6.0.1",
"phpunit/php-invoker": "^6.0.0",
"phpunit/php-text-template": "^5.0.0",
"phpunit/php-timer": "^8.0.0",
- "sebastian/cli-parser": "^4.2.1",
- "sebastian/comparator": "^7.1.8",
+ "sebastian/cli-parser": "^4.2.0",
+ "sebastian/comparator": "^7.1.6",
"sebastian/diff": "^7.0.0",
- "sebastian/environment": "^8.1.2",
- "sebastian/exporter": "^7.0.3",
- "sebastian/global-state": "^8.0.3",
+ "sebastian/environment": "^8.1.0",
+ "sebastian/exporter": "^7.0.2",
+ "sebastian/global-state": "^8.0.2",
"sebastian/object-enumerator": "^7.0.0",
"sebastian/recursion-context": "^7.0.1",
- "sebastian/type": "^6.0.4",
+ "sebastian/type": "^6.0.3",
"sebastian/version": "^6.0.0",
"staabm/side-effects-detector": "^1.0.5"
},
@@ -14339,7 +14428,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
- "source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.33"
+ "source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.24"
},
"funding": [
{
@@ -14347,7 +14436,7 @@
"type": "other"
}
],
- "time": "2026-07-28T13:58:09+00:00"
+ "time": "2026-05-01T04:21:04+00:00"
},
{
"name": "psy/psysh",
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')