feat: add activity log management module with database migrations, controller, and UI components

This commit is contained in:
Yoga Pangestu 2026-04-20 23:53:52 +07:00
parent c48d2ecd18
commit 702b033a8b
10 changed files with 359 additions and 3 deletions

View File

@ -0,0 +1,33 @@
<?php
namespace App\Http\Controllers\Admin\System;
use App\Http\Controllers\Controller;
use Inertia\Inertia;
use Inertia\Response;
use Spatie\Activitylog\Models\Activity;
class ActivityLogController extends Controller
{
public function index(): Response
{
$activities = Activity::with('causer')
->latest()
->limit(500)
->get()
->map(fn ($activity) => [
'id' => $activity->id,
'description' => $activity->description,
'module' => $activity->log_name,
'subject_id' => $activity->subject_id,
'causer' => $activity->causer ? $activity->causer->name : 'Sistem',
'properties' => $activity->properties,
'created_at' => $activity->created_at->diffForHumans(),
'date' => $activity->created_at->format('d F Y H:i'),
]);
return Inertia::render('admin/system/activity-log/index', [
'activities' => $activities,
]);
}
}

View File

@ -15,6 +15,7 @@
"laravel/framework": "^13.0",
"laravel/tinker": "^3.0",
"laravel/wayfinder": "^0.1.14",
"spatie/laravel-activitylog": "^4.12",
"spatie/laravel-medialibrary": "^11.21",
"spatie/laravel-sluggable": "^3.8"
},

95
composer.lock generated
View File

@ -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": "b928ca95e5630c3434a47320633e6e52",
"content-hash": "449b4cbc173dadfe79027dc7d0baeb62",
"packages": [
{
"name": "bacon/bacon-qr-code",
@ -4054,6 +4054,97 @@
},
"time": "2025-11-26T10:57:19+00:00"
},
{
"name": "spatie/laravel-activitylog",
"version": "4.12.3",
"source": {
"type": "git",
"url": "https://github.com/spatie/laravel-activitylog.git",
"reference": "2a2024fcac05628b0d1bfdbb1b94dda8b0661dc0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/spatie/laravel-activitylog/zipball/2a2024fcac05628b0d1bfdbb1b94dda8b0661dc0",
"reference": "2a2024fcac05628b0d1bfdbb1b94dda8b0661dc0",
"shasum": ""
},
"require": {
"illuminate/config": "^8.0 || ^9.0 || ^10.0 || ^11.0 || ^12.0 || ^13.0",
"illuminate/database": "^8.69 || ^9.27 || ^10.0 || ^11.0 || ^12.0 || ^13.0",
"illuminate/support": "^8.0 || ^9.0 || ^10.0 || ^11.0 || ^12.0 || ^13.0",
"php": "^8.1",
"spatie/laravel-package-tools": "^1.6.3"
},
"require-dev": {
"ext-json": "*",
"orchestra/testbench": "^6.23 || ^7.0 || ^8.0 || ^9.6 || ^10.0 || ^11.0",
"pestphp/pest": "^1.20 || ^2.0 || ^3.0 || ^4.0"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Spatie\\Activitylog\\ActivitylogServiceProvider"
]
}
},
"autoload": {
"files": [
"src/helpers.php"
],
"psr-4": {
"Spatie\\Activitylog\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Freek Van der Herten",
"email": "freek@spatie.be",
"homepage": "https://spatie.be",
"role": "Developer"
},
{
"name": "Sebastian De Deyne",
"email": "sebastian@spatie.be",
"homepage": "https://spatie.be",
"role": "Developer"
},
{
"name": "Tom Witkowski",
"email": "dev.gummibeer@gmail.com",
"homepage": "https://gummibeer.de",
"role": "Developer"
}
],
"description": "A very simple activity logger to monitor the users of your website or application",
"homepage": "https://github.com/spatie/activitylog",
"keywords": [
"activity",
"laravel",
"log",
"spatie",
"user"
],
"support": {
"issues": "https://github.com/spatie/laravel-activitylog/issues",
"source": "https://github.com/spatie/laravel-activitylog/tree/4.12.3"
},
"funding": [
{
"url": "https://spatie.be/open-source/support-us",
"type": "custom"
},
{
"url": "https://github.com/spatie",
"type": "github"
}
],
"time": "2026-03-24T12:33:53+00:00"
},
{
"name": "spatie/laravel-medialibrary",
"version": "11.21.0",
@ -10356,5 +10447,5 @@
"php": "^8.3"
},
"platform-dev": {},
"plugin-api-version": "2.9.0"
"plugin-api-version": "2.6.0"
}

View File

@ -0,0 +1,27 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateActivityLogTable extends Migration
{
public function up()
{
Schema::connection(config('activitylog.database_connection'))->create(config('activitylog.table_name'), function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('log_name')->nullable();
$table->text('description');
$table->nullableMorphs('subject', 'subject');
$table->nullableMorphs('causer', 'causer');
$table->json('properties')->nullable();
$table->timestamps();
$table->index('log_name');
});
}
public function down()
{
Schema::connection(config('activitylog.database_connection'))->dropIfExists(config('activitylog.table_name'));
}
}

View File

@ -0,0 +1,22 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddEventColumnToActivityLogTable extends Migration
{
public function up()
{
Schema::connection(config('activitylog.database_connection'))->table(config('activitylog.table_name'), function (Blueprint $table) {
$table->string('event')->nullable()->after('subject_type');
});
}
public function down()
{
Schema::connection(config('activitylog.database_connection'))->table(config('activitylog.table_name'), function (Blueprint $table) {
$table->dropColumn('event');
});
}
}

View File

@ -0,0 +1,22 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddBatchUuidColumnToActivityLogTable extends Migration
{
public function up()
{
Schema::connection(config('activitylog.database_connection'))->table(config('activitylog.table_name'), function (Blueprint $table) {
$table->uuid('batch_uuid')->nullable()->after('properties');
});
}
public function down()
{
Schema::connection(config('activitylog.database_connection'))->table(config('activitylog.table_name'), function (Blueprint $table) {
$table->dropColumn('batch_uuid');
});
}
}

View File

@ -1,5 +1,5 @@
import { Link } from '@inertiajs/react';
import { Boxes, Currency, DollarSign, LayoutGrid, List, ScrollText, ShoppingCart, User, Wallet, WalletCardsIcon } from 'lucide-react';
import { Boxes, DollarSign, History, LayoutGrid, List, ScrollText, ShoppingCart, User, Wallet } from 'lucide-react';
import AppLogo from '@/components/app-logo';
import { NavMain } from '@/components/nav-main';
import {
@ -74,6 +74,11 @@ const systemNavItems: NavItem[] = [
href: system.logs.index().url,
icon: ScrollText,
},
{
title: 'Riwayat',
href: system.activityLogs.index().url,
icon: History,
},
];
export function AppSidebar() {

View File

@ -0,0 +1,51 @@
import { Head } from '@inertiajs/react';
import { Card, CardContent } from '@/components/ui/card';
import { DataTable } from '@/components/data-table';
import { useState } from 'react';
import { getColumns, Activity } from './partials/columns';
interface Props {
activities: Activity[];
}
export default function ActivityLogIndex({ activities }: Props) {
const columns = getColumns();
return (
<div className="flex flex-col gap-6 p-6">
<Head title="Riwayat Aktivitas" />
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold tracking-tight">Riwayat Aktivitas</h1>
</div>
</div>
<Card className="overflow-hidden border-none shadow-lg bg-card/50 backdrop-blur-sm p-5">
<CardContent className="p-0">
<DataTable
columns={columns}
data={activities}
filters={[
{
columnId: 'description',
title: 'Aksi',
options: [
{ label: 'Tambah', value: 'TAMBAH' },
{ label: 'Ubah', value: 'UBAH' },
{ label: 'Hapus', value: 'HAPUS' },
]
}
]}
/>
</CardContent>
</Card>
</div>
);
}
ActivityLogIndex.layout = {
breadcrumbs: [
{ title: 'Sistem' },
],
};

View File

@ -0,0 +1,102 @@
import { ColumnDef } from '@tanstack/react-table';
import { Badge } from '@/components/ui/badge';
export interface Activity {
id: number;
description: string;
module: string;
subject_id: number | string | null;
causer: string;
properties: any;
created_at: string;
date: string;
}
export function getColumns(): ColumnDef<Activity>[] {
return [
{
accessorKey: 'date',
header: 'Waktu',
cell: ({ row }) => (
<div className="flex flex-col">
<span className="font-medium text-sm">{row.original.date}</span>
<span className="text-[10px] text-muted-foreground">{row.original.created_at}</span>
</div>
)
},
{
accessorKey: 'description',
header: 'Aksi',
cell: ({ row }) => {
const label = row.original.description;
if (label === 'TAMBAH') {
return (
<Badge className="bg-green-50 text-green-700 dark:bg-green-950 dark:text-green-300 uppercase text-[10px]">
{label}
</Badge>
);
}
if (label === 'UBAH') {
return (
<Badge className="bg-blue-50 text-blue-700 dark:bg-blue-950 dark:text-blue-300 uppercase text-[10px]">
{label}
</Badge>
);
}
if (label === 'HAPUS') {
return (
<Badge className="bg-red-50 text-red-700 dark:bg-red-950 dark:text-red-300 uppercase text-[10px]">
{label}
</Badge>
);
}
return (
<Badge variant="outline" className="uppercase text-[10px]">
{label}
</Badge>
);
}
},
{
accessorKey: 'module',
header: 'Modul',
},
{
accessorKey: 'causer',
header: 'Oleh',
},
{
accessorKey: 'properties',
header: 'Detail Perubahan',
cell: ({ row }) => {
const attributes = row.original.properties?.attributes;
const old = row.original.properties?.old;
if (!attributes) return <span className="text-muted-foreground text-xs italic">-</span>;
return (
<div className="flex flex-col gap-1 py-1 max-w-[400px]">
{Object.entries(attributes).map(([key, value]) => {
return (
<div key={key} className="text-[11px] leading-tight flex flex-wrap items-center gap-1">
<span className="font-semibold text-indigo-600 dark:text-indigo-400">{key}: </span>
{old && old[key] !== undefined && String(old[key]) !== String(value) && (
<span className="text-red-500 line-through opacity-60 italic">
{String(old[key])}
</span>
)}
{old && old[key] !== undefined && String(old[key]) !== String(value) && <span></span>}
<span className="text-green-600 dark:text-green-400 font-medium">{String(value)}</span>
</div>
);
})}
</div>
);
}
},
];
}

View File

@ -1,10 +1,12 @@
<?php
use App\Http\Controllers\Admin\System\ActivityLogController;
use App\Http\Controllers\Admin\System\LogController;
use Illuminate\Support\Facades\Route;
Route::middleware(['auth'])->group(function () {
Route::prefix('admin/system')->group(function () {
Route::get('logs', [LogController::class, 'index'])->name('system.logs.index');
Route::get('activity-logs', [ActivityLogController::class, 'index'])->name('system.activity-logs.index');
});
});