diff --git a/app/Http/Controllers/Web/Konfigurasi/LogAktivitasController.php b/app/Http/Controllers/Web/Konfigurasi/LogAktivitasController.php index fb1c90c..781ca83 100644 --- a/app/Http/Controllers/Web/Konfigurasi/LogAktivitasController.php +++ b/app/Http/Controllers/Web/Konfigurasi/LogAktivitasController.php @@ -4,10 +4,12 @@ namespace App\Http\Controllers\Web\Konfigurasi; use App\Http\Controllers\Controller; use App\Http\Requests\Konfigurasi\FilterLogAktivitasRequest; +use App\Models\Konfigurasi\LogAktivitas; use App\Models\Pertanian\Petani; use App\Models\User; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\Carbon; -use Spatie\Activitylog\Models\Activity; +use Illuminate\Support\Facades\DB; class LogAktivitasController extends Controller { @@ -38,21 +40,24 @@ class LogAktivitasController extends Controller { $filter = $request->validated(); - // Ambil log sesuai filter - $aktivitas = Activity::query() - ->with(['causer', 'subject']) + // Ambil log sesuai filter (tanpa COUNT total) + $aktivitas = LogAktivitas::query() + ->with('causer:id,name,role') ->when($filter['jenis'] ?? null, fn ($q, $jenis) => $q->where('log_name', $jenis)) ->when($filter['dari'] ?? null, fn ($q, $tgl) => $q->where('created_at', '>=', $tgl)) ->when($filter['sampai'] ?? null, fn ($q, $tgl) => $q->where('created_at', '<', Carbon::parse($tgl)->addDay())) ->when($filter['search'] ?? null, fn ($q, $cari) => $this->cari($q, '%' . mb_strtolower($cari) . '%')) ->latest('id') - ->paginate((int) ($filter['per_page'] ?? self::PER_PAGE[0])) + ->simplePaginate((int) ($filter['per_page'] ?? self::PER_PAGE[0])) ->withQueryString(); - // Nama petani untuk log tab petani (satu query) - $namaPetani = Petani::withoutGlobalScopes()->withTrashed() - ->whereIn('id', $aktivitas->getCollection()->pluck('properties.petani_id')->filter()->unique()) - ->pluck('nama', 'id'); + // Nama petani untuk log yang belum menyimpan nama (satu query) + $idPetani = $aktivitas->getCollection() + ->map(fn ($a) => $a->properties['petani_id'] ?? ($a->subject_type === Petani::class ? $a->subject_id : null)) + ->filter() + ->unique(); + + $namaPetani = $idPetani->isEmpty() ? collect() : Petani::withoutGlobalScopes()->whereIn('id', $idPetani)->pluck('nama', 'id'); return view('konfigurasi.log-aktivitas.index', [ 'aktivitas' => $aktivitas, @@ -62,15 +67,16 @@ class LogAktivitasController extends Controller ]); } - // Cari di aktivitas, nama pengguna, dan nama petani - private function cari($query, string $cari): void + // Cari di aktivitas, nama pengguna, dan nama petani (memakai index) + private function cari(Builder $query, string $cari): void { + $petani = Petani::withoutGlobalScopes()->select('id')->whereRaw('lower(nama) like ?', [$cari]); + $pengguna = User::withoutGlobalScopes()->select('id')->whereRaw('lower(name) like ?', [$cari]); + $query->where(fn ($q) => $q ->whereRaw('lower(description) like ?', [$cari]) - ->orWhereHasMorph('causer', [User::class], fn ($q) => $q->whereRaw('lower(name) like ?', [$cari])) - ->orWhereRaw( - "coalesce((properties->>'petani_id')::bigint, case when subject_type = ? then subject_id end) in (select id from petani where lower(nama) like ?)", - [Petani::class, $cari] - )); + ->orWhere(fn ($q) => $q->where('causer_type', User::class)->whereIn('causer_id', $pengguna)) + ->orWhere(fn ($q) => $q->where('subject_type', Petani::class)->whereIn('subject_id', $petani)) + ->orWhereIn(DB::raw("(properties->>'petani_id')::bigint"), $petani)); } } diff --git a/app/Http/Middleware/SimpanLogAktivitas.php b/app/Http/Middleware/SimpanLogAktivitas.php new file mode 100644 index 0000000..11c5787 --- /dev/null +++ b/app/Http/Middleware/SimpanLogAktivitas.php @@ -0,0 +1,26 @@ +buffer->mulai(); + + return $next($request); + } + + // Simpan log setelah respons terkirim; request gagal (5xx) tidak dicatat + public function terminate(Request $request, Response $response): void + { + $response->getStatusCode() >= 500 ? $this->buffer->buang() : $this->buffer->simpan(); + } +} diff --git a/app/Models/Concerns/CatatAktivitas.php b/app/Models/Concerns/CatatAktivitas.php index f046950..c5811f1 100644 --- a/app/Models/Concerns/CatatAktivitas.php +++ b/app/Models/Concerns/CatatAktivitas.php @@ -2,10 +2,10 @@ namespace App\Models\Concerns; -use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Str; -use Spatie\Activitylog\Models\Concerns\LogsActivity; -use Spatie\Activitylog\Support\LogOptions; +use Spatie\Activitylog\Contracts\Activity; +use Spatie\Activitylog\LogOptions; +use Spatie\Activitylog\Traits\LogsActivity; trait CatatAktivitas { @@ -36,15 +36,19 @@ trait CatatAktivitas ->logExcept([...self::$kolomTidakDicatat, ...$this->getHidden()]) ->dontLogIfAttributesChangedOnly(self::$kolomTidakDicatat) ->logOnlyDirty() - ->dontLogEmptyChanges() + ->dontSubmitEmptyLogs() ->setDescriptionForEvent(fn (string $event) => $label . ' ' . (self::$kataEvent[$event] ?? $event)); } - // Tandai petani terkait - public function beforeActivityLogged(Model $activity, string $event): void + // Simpan nama data & petani terkait di log (halaman log tak perlu memuat data asli) + public function tapActivity(Activity $activity, string $eventName): void { - if ($petaniId = $this->getAttribute('petani_id')) { - $activity->properties = $activity->properties->put('petani_id', (int) $petaniId); - } + $label = $this->getAttribute('nama') ?? $this->getAttribute('name'); + $petaniId = $this->getAttribute('petani_id'); + + $activity->properties = $activity->properties->merge(array_filter([ + 'label' => is_string($label) ? Str::limit($label, 100) : null, + 'petani_id' => $petaniId ? (int) $petaniId : null, + ])); } } diff --git a/app/Models/Konfigurasi/LogAktivitas.php b/app/Models/Konfigurasi/LogAktivitas.php new file mode 100644 index 0000000..a1c5828 --- /dev/null +++ b/app/Models/Konfigurasi/LogAktivitas.php @@ -0,0 +1,24 @@ +exists || ! $buffer->aktif()) { + return parent::save($options); + } + + $this->updateTimestamps(); + $buffer->tambah($this->getAttributes()); + + return true; + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 48f67f4..bacceab 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -15,7 +15,8 @@ use Dedoc\Scramble\Support\Generator\OpenApi; use Dedoc\Scramble\Support\Generator\SecurityScheme; use App\Models\Ekosistem\TabelBasisData; -use Spatie\Activitylog\Support\ActivityLogStatus; +use App\Services\Konfigurasi\BufferLogAktivitas; +use Spatie\Activitylog\ActivityLogStatus; class AppServiceProvider extends ServiceProvider { @@ -24,7 +25,7 @@ class AppServiceProvider extends ServiceProvider */ public function register(): void { - // + $this->app->scoped(BufferLogAktivitas::class); } /** diff --git a/app/Services/Konfigurasi/BufferLogAktivitas.php b/app/Services/Konfigurasi/BufferLogAktivitas.php new file mode 100644 index 0000000..0e86d2f --- /dev/null +++ b/app/Services/Konfigurasi/BufferLogAktivitas.php @@ -0,0 +1,49 @@ +aktif = true; + } + + public function aktif(): bool + { + return $this->aktif; + } + + private const KOLOM = [ + 'log_name', 'description', 'subject_type', 'subject_id', 'event', + 'causer_type', 'causer_id', 'properties', 'batch_uuid', 'created_at', 'updated_at', + ]; + + // Samakan kolom tiap baris supaya bisa di-insert sekaligus + public function tambah(array $baris): void + { + $this->antrean[] = array_merge(array_fill_keys(self::KOLOM, null), array_intersect_key($baris, array_flip(self::KOLOM))); + } + + // Simpan semua log dalam satu insert + public function simpan(): void + { + foreach (array_chunk($this->antrean, 500) as $potongan) { + LogAktivitas::query()->insert($potongan); + } + + $this->buang(); + } + + public function buang(): void + { + $this->antrean = []; + $this->aktif = false; + } +} diff --git a/bootstrap/app.php b/bootstrap/app.php index df1f9c4..28c9004 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -15,6 +15,7 @@ return Application::configure(basePath: dirname(__DIR__)) ) ->withMiddleware(function (Middleware $middleware): void { $middleware->append(\App\Http\Middleware\ResolveProgram::class); + $middleware->append(\App\Http\Middleware\SimpanLogAktivitas::class); $middleware->alias([ 'akses.petani' => \App\Http\Middleware\BatasiAksesPetani::class, diff --git a/composer.json b/composer.json index 1996193..0c0930c 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ "laravel/sanctum": "^4.0", "laravel/telescope": "^5.18", "laravel/tinker": "^2.10.1", - "spatie/laravel-activitylog": "^5.1" + "spatie/laravel-activitylog": "^4.10" }, "require-dev": { "fakerphp/faker": "^1.23", diff --git a/config/activitylog.php b/config/activitylog.php index 9c26fc0..8a14115 100644 --- a/config/activitylog.php +++ b/config/activitylog.php @@ -1,21 +1,17 @@ env('ACTIVITYLOG_ENABLED', true), + 'enabled' => env('ACTIVITY_LOGGER_ENABLED', true), /* - * When the clean command is executed, all recording activities older than + * When the clean-command is executed, all recording activities older than * the number of days specified here will be deleted. */ - 'clean_after_days' => 365, + 'delete_records_older_than_days' => 365, /* * If no log name is passed to the activity() helper @@ -30,44 +26,27 @@ return [ 'default_auth_driver' => null, /* - * If set to true, the subject relationship on activities - * will include soft deleted models. + * If set to true, the subject returns soft deleted models. */ - 'include_soft_deleted_subjects' => true, + 'subject_returns_soft_deleted_models' => 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, + 'activity_model' => \App\Models\Konfigurasi\LogAktivitas::class, /* - * These attributes will be excluded from logging for all models. - * Model-specific exclusions via logExcept() are merged with these. + * This is the name of the table that will be created by the migration and + * used by the Activity model shipped with this package. */ - 'default_except_attributes' => [], + 'table_name' => env('ACTIVITY_LOGGER_TABLE_NAME', 'activity_log'), /* - * 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. + * This is the database connection that will be used by the migration and + * the Activity model shipped with this package. In case it's not set + * Laravel's database.default will be used instead. */ - 'buffer' => [ - 'enabled' => env('ACTIVITYLOG_BUFFER_ENABLED', true), - ], - - /* - * 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, - ], + 'database_connection' => env('ACTIVITY_LOGGER_DB_CONNECTION'), ]; diff --git a/database/migrations/2026_11_05_152339_create_activity_log_table.php b/database/migrations/2026_11_05_152339_create_activity_log_table.php index 615d04d..2048bcc 100644 --- a/database/migrations/2026_11_05_152339_create_activity_log_table.php +++ b/database/migrations/2026_11_05_152339_create_activity_log_table.php @@ -2,6 +2,7 @@ use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Schema; return new class extends Migration @@ -15,11 +16,19 @@ return new class extends Migration $table->nullableMorphs('subject', 'subject'); $table->string('event')->nullable(); $table->nullableMorphs('causer', 'causer'); - $table->json('attribute_changes')->nullable(); $table->json('properties')->nullable(); + $table->uuid('batch_uuid')->nullable(); $table->timestamps(); $table->index('created_at'); }); + + // Index pencarian log per petani + DB::statement("create index activity_log_petani_id_index on activity_log (((properties->>'petani_id')::bigint))"); + } + + public function down(): void + { + Schema::dropIfExists('activity_log'); } }; diff --git a/resources/views/konfigurasi/log-aktivitas/index.blade.php b/resources/views/konfigurasi/log-aktivitas/index.blade.php index 746eb82..3778a68 100644 --- a/resources/views/konfigurasi/log-aktivitas/index.blade.php +++ b/resources/views/konfigurasi/log-aktivitas/index.blade.php @@ -2,7 +2,6 @@ @use('App\Services\Konfigurasi\FormatLogAktivitas', 'Format') @use('App\Models\Pertanian\Petani') -@use('Illuminate\Support\Str') @section('content')