feat: add activity log management module with database migrations, controller, and UI components
This commit is contained in:
parent
c48d2ecd18
commit
702b033a8b
33
app/Http/Controllers/Admin/System/ActivityLogController.php
Normal file
33
app/Http/Controllers/Admin/System/ActivityLogController.php
Normal 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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@ -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
95
composer.lock
generated
@ -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"
|
||||
}
|
||||
|
||||
@ -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'));
|
||||
}
|
||||
}
|
||||
@ -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');
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -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');
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -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() {
|
||||
|
||||
51
resources/js/pages/admin/system/activity-log/index.tsx
Normal file
51
resources/js/pages/admin/system/activity-log/index.tsx
Normal 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' },
|
||||
],
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
},
|
||||
];
|
||||
}
|
||||
@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user