Menambahkan dokumenLogs untuk modul adminKredit. Masih diperlukan fixing lebih lanjut agar adminKredit bisa mengakses menu tersebut melalui sidebar.

AuditLogsController: Menambahkan function untuk datatables dan index baru dengan logic query yang berbeda dari datatable yang default

new page: adminkredit.blade

web: menambahkan rute baru untuk page audit trail dokumen ADK
This commit is contained in:
Genex1s
2025-12-24 17:09:46 +07:00
parent db364c5877
commit 9da61e862a
4 changed files with 467 additions and 4 deletions

View File

@@ -37,6 +37,104 @@
return view('logs::audit');
}
public function indexAdminKredit()
{
// Check if the authenticated user has the required permission to view audit logs
if (is_null($this->user) || !$this->user->can('audit-logs.read')) {
abort(403, 'Sorry! You are not allowed to view audit logs.');
}
return view('logs::adminkredit');
}
public function datatableAdminKredit(Request $request)
{
// Check if the authenticated user has the required permission to view audit logs
if (is_null($this->user) || !$this->user->can('audit-logs.read')) {
abort(403, 'Sorry! You are not allowed to view audit logs.');
}
// Retrieve data from the database
$query = Activity::query()
->where('log_name','ADK')
->where('subject_type', 'LIKE', '%Dokumen%');
// Apply search filter if provided
if ($request->has('search') && !empty($request->get('search'))) {
$search = $request->get('search');
$query->where(function ($q) use ($search) {
$q->where('log_name', 'LIKE', "%$search%")
->orWhere('description', 'LIKE', "%$search%")
->orWhere('subject_id', 'LIKE', "%$search%")
->orWhere('subject_type', 'LIKE', "%$search%")
->orWhere('causer_id', 'LIKE', "%$search%")
->orWhere('properties', 'LIKE', "%$search%");
});
}
// Apply sorting if provided
if ($request->has('sortOrder') && !empty($request->get('sortOrder'))) {
$order = $request->get('sortOrder');
$column = $request->get('sortField');
$query->orderBy($column, $order);
} else {
// Default sorting by created_at descending
$query->orderBy('created_at', 'desc');
}
// Get the total count of records before pagination
$totalRecords = Activity::count();
// Get the filtered count before pagination
$filteredRecords = $query->count();
// Apply pagination if provided
if ($request->has('page') && $request->has('size')) {
$page = $request->get('page');
$size = $request->get('size');
$offset = ($page - 1) * $size; // Calculate the offset
$query->skip($offset)->take($size);
}
// Get the data for the current page
$data = $query->get();
// Map causer_id to creator name
$data = $data->map(function ($item) {
// Create a new property for the creator's name
if ($item->causer_id && $item->causer_type === 'Modules\\Usermanagement\\Models\\User') {
// Try to find the user
$user = User::find($item->causer_id);
if ($user) {
$item->creator_name = $user->name;
} else {
$item->creator_name = 'Unknown User';
}
} else {
$item->creator_name = 'System';
}
return $item;
});
// Calculate the page count
$pageCount = ceil($filteredRecords / ($request->get('size') ?: 1));
// Calculate the current page number
$currentPage = $request->get('page') ?: 1;
// Return the response data as a JSON object
return response()->json([
'draw' => $request->get('draw'),
'recordsTotal' => $totalRecords,
'recordsFiltered' => $filteredRecords,
'pageCount' => $pageCount,
'page' => $currentPage,
'totalCount' => $filteredRecords,
'data' => $data,
]);
}
public function datatable(Request $request)
{
// Check if the authenticated user has the required permission to view audit logs

View File

@@ -43,6 +43,18 @@
"roles": [
"administrator"
]
},
{
"title": "Dokumen ADK Logs",
"path": "logs.dokumen",
"classes": "",
"attributes": [],
"permission": "",
"roles": [
"administrator",
"adminkredit"
]
}
]
}

View File

@@ -0,0 +1,351 @@
@extends('layouts.main')
@section('breadcrumbs')
{{ Breadcrumbs::render('logs.audit') }}
@endsection
@section('content')
<div class="grid">
<div class="card card-grid min-w-full" data-datatable="false" data-datatable-page-size="10"
data-datatable-state-save="false" id="audit-logs-table" data-api-url="{{ route('logs.audit.datatablesAdminKredit') }}">
<div class="card-header py-5 flex-wrap">
<h3 class="card-title">
Audit Logs
</h3>
<div class="flex flex-wrap gap-2 lg:gap-5">
<div class="flex">
<label class="input input-sm"> <i class="ki-filled ki-magnifier"> </i>
<input placeholder="Search Logs" id="search" type="text" value="">
</label>
</div>
</div>
</div>
<div class="card-body">
<div class="scrollable-x-auto">
<table class="table table-auto table-border align-middle text-gray-700 font-medium text-sm"
data-datatable-table="true">
<thead>
<tr>
<th class="min-w-[100px]" data-datatable-column="log_name">
<span class="sort"> <span class="sort-label"> Log Type </span>
<span class="sort-icon"> </span> </span>
</th>
<th class="min-w-[100px]" data-datatable-column="subject_type">
<span class="sort"> <span class="sort-label"> Subject Type </span>
<span class="sort-icon"> </span> </span>
</th>
<th class="min-w-[150px]" data-datatable-column="description">
<span class="sort"> <span class="sort-label"> Tipe Dokumen </span>
<span class="sort-icon"> </span> </span>
</th>
{{-- <th class="min-w-[150px]" data-datatable-column="properties">
<span class="sort"> <span class="sort-label"> Properties </span>
<span class="sort-icon"> </span> </span>
</th> --}}
<th class="min-w-[300px]" data-datatable-column="changes">
<span class="sort"> <span class="sort-label"> Perubahan </span>
<span class="sort-icon"> </span> </span>
</th>
<th class="min-w-[150px]" data-datatable-column="causer_type">
<span class="sort"> <span class="sort-label"> User Role </span>
<span class="sort-icon"> </span> </span>
</th>
<th class="min-w-[100px]" data-datatable-column="causer_id">
<span class="sort"> <span class="sort-label"> Username </span>
<span class="sort-icon"> </span> </span>
</th>
<th class="min-w-[100px]" data-datatable-column="created_at">
<span class="sort"> <span class="sort-label"> Date/Time </span>
<span class="sort-icon"> </span> </span>
</th>
</tr>
</thead>
</table>
</div>
<div
class="card-footer justify-center md:justify-between flex-col md:flex-row gap-3 text-gray-600 text-2sm font-medium">
<div class="flex items-center gap-2">
Show
<select class="select select-sm w-16" data-datatable-size="true" name="perpage"> </select> per page
</div>
<div class="flex items-center gap-4">
<span data-datatable-info="true"> </span>
<div class="pagination" data-datatable-pagination="true">
</div>
</div>
</div>
</div>
</div>
</div>
@endsection
@push('scripts')
<script type="module">
const element = document.querySelector('#audit-logs-table');
const searchInput = document.getElementById('search');
const apiUrl = element.getAttribute('data-api-url');
const dataTableOptions = {
apiEndpoint: apiUrl,
pageSize: 10,
columns: {
log_name: {
title: 'Log Type',
render: (item, data) => {
return `<span class="badge badge-light-primary">${data.log_name || 'N/A'}</span>`;
}
},
description: {
title: 'Description',
},
subject_type: {
title: 'Subject Type',
render: (item, data) => {
if (!data.subject_type) return 'N/A';
return data.subject_type.split('\\').pop();
}
},
// properties:{
// title: 'Properties',
// render: (item, data) => {
// if (!data.properties) return 'N/A';
// // Generate a unique ID for this property
// const propertyId = `property-${data.id || Math.random().toString(36).substr(2, 9)}`;
// // Create a shortened preview (first 50 characters)
// const preview = JSON.stringify(data.properties).substring(0, 50) + (JSON.stringify(data.properties).length > 50 ? '...' : '');
// // Return HTML with expand/collapse functionality using Tailwind classes
// return `
// <div class="relative w-full">
// <div class="flex justify-between items-center w-full">
// <div class="max-w-[calc(100%-50px)] whitespace-nowrap overflow-hidden text-ellipsis" id="preview-${propertyId}">${preview}</div>
// <div class="hidden max-h-[300px] overflow-y-auto bg-gray-100 p-2.5 rounded mt-1.5 w-full" id="full-${propertyId}">
// <pre class="m-0 whitespace-pre-wrap break-words">${JSON.stringify(data.properties, null, 2)}</pre>
// </div>
// <div class="flex items-center ml-2.5">
// <button type="button" class="btn btn-sm btn-outline btn-icon btn-info expand-property"
// data-property-id="${propertyId}" id="expand-${propertyId}">
// <i class="ki-duotone ki-arrow-down fs-7"></i>
// </button>
// <button type="button" class="btn btn-sm btn-outline btn-icon btn-info collapse-property hidden"
// data-property-id="${propertyId}" id="collapse-${propertyId}">
// <i class="ki-duotone ki-arrow-up fs-7"></i>
// </button>
// </div>
// </div>
// </div>
// `;
// }
// },
changes: {
title: 'Changes',
render: (item, data) => {
// Check if properties exists and contains old/attributes
if (!data.properties) return 'N/A';
// Parse properties if it's a string, otherwise use it directly
let properties = data.properties;
if (typeof properties === 'string') {
try {
properties = JSON.parse(properties);
} catch (e) {
return 'N/A';
}
}
// The old and attributes are inside properties object
const oldData = properties.old || {};
const newData = properties.attributes || {};
// Check if both objects are empty
if (Object.keys(oldData).length === 0 && Object.keys(newData).length === 0) {
return 'N/A';
}
// Fields to exclude from changes display
const excludedFields = ['updated_at', 'file_path', 'file_name', 'file_size', 'file_type'];
// Compute only the changed fields (excluding specific fields)
const diffs = {};
Object.keys(newData).forEach(key => {
if (!excludedFields.includes(key) && JSON.stringify(oldData[key]) !== JSON.stringify(newData[key])) {
diffs[key] = { old: oldData[key], new: newData[key] };
}
});
if (Object.keys(diffs).length === 0) return 'No changes';
// Generate unique ID for expand/collapse
const changeId = `change-${data.id || Math.random().toString(36).substr(2, 9)}`;
// Short preview showing count of changes
const changeCount = Object.keys(diffs).length;
const firstKey = Object.keys(diffs)[0];
// const preview = `<span class="font-medium text-blue-700">${changeCount}</span> field${changeCount > 1 ? 's' : ''} changed: <span class="font-semibold">"${firstKey}"</span>${changeCount > 1 ? ', ...' : ''}`;
const preview = `<span class="font-medium text-blue-700">${changeCount}</span> data diubah: <span class="font-semibold">"${firstKey}"</span>${changeCount > 1 ? ', ...' : ''}`;
// Build Before/After HTML with better styling
let fullHtml = '<div class="grid grid-cols-2 gap-4">';
// Before column
fullHtml += '<div class="bg-red-50 dark:bg-red-900/20 rounded-lg p-3">';
fullHtml += '<div class="flex items-center gap-2 mb-3">';
fullHtml += '<svg class="w-3 h-3 text-red-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">';
fullHtml += '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>';
fullHtml += '</svg>';
fullHtml += '<strong class="text-red-700 dark:text-red-400 text-base">Before</strong>';
fullHtml += '</div>';
fullHtml += '<div class="space-y-2">';
Object.keys(diffs).forEach(key => {
fullHtml += `
<div class="bg-white dark:bg-gray-800 rounded p-2 border-l-4 border-red-500">
<div class="font-semibold text-sm text-gray-700 dark:text-gray-300 mb-1">${key}</div>
<pre class="m-0 whitespace-pre-wrap break-words text-sm text-gray-600 dark:text-gray-400">${JSON.stringify(diffs[key].old, null, 2)}</pre>
</div>
`;
});
fullHtml += '</div></div>';
// After column
fullHtml += '<div class="bg-green-50 dark:bg-green-900/20 rounded-lg p-3">';
fullHtml += '<div class="flex items-center gap-2 mb-3">';
fullHtml += '<svg class="w-3 h-3 text-green-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">';
fullHtml += '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>';
fullHtml += '</svg>';
fullHtml += '<strong class="text-green-700 dark:text-green-400 text-base">After</strong>';
fullHtml += '</div>';
fullHtml += '<div class="space-y-2">';
Object.keys(diffs).forEach(key => {
fullHtml += `
<div class="bg-white dark:bg-gray-800 rounded p-2 border-l-4 border-green-500">
<div class="font-semibold text-sm text-gray-700 dark:text-gray-300 mb-1">${key}</div>
<pre class="m-0 whitespace-pre-wrap break-words text-sm text-gray-600 dark:text-gray-400">${JSON.stringify(diffs[key].new, null, 2)}</pre>
</div>
`;
});
fullHtml += '</div></div>';
fullHtml += '</div>';
// Return the collapsible block with improved styling
return `
<div class="relative w-full">
<div class="flex justify-between items-center w-full gap-3">
<div class="flex-1 min-w-0 text-sm" id="preview-${changeId}">
${preview}
</div>
<div class="flex-shrink-0">
<button type="button" class="btn btn-sm btn-outline btn-icon btn-info expand-change"
data-change-id="${changeId}" id="expand-${changeId}">
<i class="ki-duotone ki-arrow-down fs-7"></i>
</button>
<button type="button" class="btn btn-sm btn-outline btn-icon btn-info collapse-change hidden"
data-change-id="${changeId}" id="collapse-${changeId}">
<i class="ki-duotone ki-arrow-up fs-7"></i>
</button>
</div>
</div>
<div class="hidden max-h-[400px] overflow-y-auto rounded-lg mt-3 shadow-sm border border-gray-200 dark:border-gray-700" id="full-${changeId}">
${fullHtml}
</div>
</div>
`;
}
},
causer_type: {
title: 'Causer Type',
render: (item, data) => {
if (!data.causer_type) return 'System';
return data.causer_type.split('\\').pop();
}
},
causer_id: {
title: 'Causer ID',
render: (item, data) => {
return data.creator_name || 'System';
}
},
created_at: {
title: 'Date/Time',
render: (item, data) => {
const date = new Date(data.created_at);
return window.formatTanggalWaktuIndonesia(date)
}
}
},
};
let dataTable = new KTDataTable(element, dataTableOptions);
// Add event delegation for expand/collapse buttons
document.querySelector('#audit-logs-table').addEventListener('click', function(e) {
// Handle expand button click
if (e.target.closest('.expand-property')) {
const button = e.target.closest('.expand-property');
const propertyId = button.getAttribute('data-property-id');
// Show full property and hide preview
document.getElementById(`preview-${propertyId}`).style.display = 'none';
document.getElementById(`full-${propertyId}`).style.display = 'block';
// Toggle buttons
document.getElementById(`expand-${propertyId}`).style.display = 'none';
document.getElementById(`collapse-${propertyId}`).style.display = 'inline-flex';
}
// Handle collapse button click
if (e.target.closest('.collapse-property')) {
const button = e.target.closest('.collapse-property');
const propertyId = button.getAttribute('data-property-id');
// Hide full property and show preview
document.getElementById(`preview-${propertyId}`).style.display = 'block';
document.getElementById(`full-${propertyId}`).style.display = 'none';
// Toggle buttons
document.getElementById(`expand-${propertyId}`).style.display = 'inline-flex';
document.getElementById(`collapse-${propertyId}`).style.display = 'none';
}
if (e.target.closest('.expand-change')) {
const button = e.target.closest('.expand-change');
const changeId = button.getAttribute('data-change-id');
// Show full change and hide preview
document.getElementById(`preview-${changeId}`).style.display = 'none';
document.getElementById(`full-${changeId}`).style.display = 'block';
// Toggle buttons
document.getElementById(`expand-${changeId}`).style.display = 'none';
document.getElementById(`collapse-${changeId}`).style.display = 'inline-flex';
}
if (e.target.closest('.collapse-change')) {
const button = e.target.closest('.collapse-change');
const changeId = button.getAttribute('data-change-id');
// Hide full change and show preview
document.getElementById(`preview-${changeId}`).style.display = 'block';
document.getElementById(`full-${changeId}`).style.display = 'none';
// Toggle buttons
document.getElementById(`expand-${changeId}`).style.display = 'inline-flex';
document.getElementById(`collapse-${changeId}`).style.display = 'none';
}
});
// Custom search functionality
searchInput.addEventListener('input', function() {
const searchValue = this.value.trim();
// Reset to page 1 when searching and then perform search
dataTable.goPage(1);
dataTable.search(searchValue, true);
});
window.dataTable = dataTable;
</script>
@endpush

View File

@@ -18,8 +18,10 @@ use Illuminate\Support\Facades\Route;
Route::group([], function () {
Route::name('logs.')->prefix('logs')->group(function () {
Route::get('dokumen', [AuditLogsController::class, 'indexAdminKredit'])->name('dokumen.index');
Route::name('audit.')->prefix('audit')->group(function () {
Route::get('datatables', [AuditLogsController::class, 'datatable'])->name('datatables');
Route::get('datatablesAdminKredit', [AuditLogsController::class, 'datatableAdminKredit'])->name('datatablesAdminKredit');
});
Route::resource('audit', AuditLogsController::class)->only(['index', 'delete']);