feat: add resources and view components

This commit is contained in:
2026-05-21 16:05:19 +07:00
parent 28a06315b8
commit b2d60e680d
249 changed files with 37379 additions and 0 deletions
@@ -0,0 +1,323 @@
<x-app-layout>
<div class="container-fluid" id="main-content">
<div class="row gx-3 gx-lg-4">
<div class="col-12">
<div class="card adminuiux-card">
<div class="card-body p-0">
{{-- Action Log Header --}}
<div class="p-4 pb-0">
<div class="d-flex justify-content-between align-items-center mb-3">
<div>
<h5 class="mb-0 fw-bold">{{ __('Action Log') }}</h5>
<small class="text-muted">
{{ __('Monitor all recorded user actions within the system, including authentication and data modifications.') }}
</small>
</div>
<div class="d-flex gap-2 align-items-center">
<button type="button" class="btn btn-outline-dark btn-sm rounded-pill px-3" id="export-csv-btn">
<i class="bi bi-file-earmark-excel me-1"></i> {{ __('Export CSV') }}
</button>
@can('manage action history')
<button type="button" class="btn btn-outline-danger btn-sm rounded-pill px-3" id="clear-activity-logs-btn">
<i class="bi bi-trash me-1"></i> {{ __('Clear All Logs') }}
</button>
@endcan
</div>
</div>
<div class="d-flex gap-2 mb-4">
<button type="button" class="btn btn-sm btn-dark rounded-pill px-3 filter-event" data-value="">
<i class="bi bi-list-ul me-1"></i> {{ __('All Logs') }}
</button>
<button type="button" class="btn btn-sm btn-outline-dark border border-dark rounded-pill px-3 filter-event" data-value="auth">
<i class="bi bi-shield-lock me-1"></i> {{ __('Authentication') }}
</button>
<button type="button" class="btn btn-sm btn-outline-dark border border-dark rounded-pill px-3 filter-event" data-value="data">
<i class="bi bi-database-gear me-1"></i> {{ __('Data Modification') }}
</button>
<button type="button" class="btn btn-sm btn-outline-dark border border-dark rounded-pill px-3 filter-event" data-value="system">
<i class="bi bi-cpu me-1"></i> {{ __('System') }}
</button>
</div>
<input type="hidden" id="filter-event-val" name="event" class="filter-extra" value="">
</div>
{{-- Action Log Table --}}
<div class="p-4">
<div class="table-responsive">
<table id="datatables" class="table table-hover table-bordered w-100 nowrap mb-0"
data-server-side="true" data-ajax-url="{{ route('action-logs') }}"
data-order='@json([[4, "desc"]])'>
<thead>
<tr>
<th class="text-wrap">{{ __('User') }}</th>
<th class="text-wrap">{{ __('Action') }}</th>
<th class="text-wrap">{{ __('Preview') }}</th>
<th class="text-wrap">{{ __('Module') }}</th>
<th class="text-wrap">{{ __('Executed At') }}</th>
<th class="text-wrap" data-hide="audit">{{ __('IP Address') }}</th>
<th class="text-wrap" data-hide="audit">{{ __('Device / Agent') }}</th>
<th class="text-wrap" data-hide="audit">{{ __('Properties') }}</th>
<th class="text-end text-wrap" data-orderable="false"
data-searchable="false">{{ __('Details') }}</th>
</tr>
<tr class="filter-row">
<th><input class="form-control form-control-sm" placeholder="{{ __('Search User') }}"></th>
<th><input class="form-control form-control-sm" placeholder="{{ __('Action') }}"></th>
<th><input class="form-control form-control-sm" placeholder="{{ __('Keyword') }}"></th>
<th><input class="form-control form-control-sm" placeholder="{{ __('Module') }}"></th>
<th><input type="date" class="form-control form-control-sm"></th>
<th><input class="form-control form-control-sm" placeholder="{{ __('IP') }}"></th>
<th><input class="form-control form-control-sm" placeholder="{{ __('Agent') }}"></th>
<th><input class="form-control form-control-sm" placeholder="{{ __('Props') }}"></th>
<th></th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
{{-- ADVANCED AUDIT MODAL --}}
<div class="modal fade" id="detailLogModal" tabindex="-1">
<div class="modal-dialog modal-lg modal-dialog-centered">
<div class="modal-content border-0 shadow-lg" style="border-radius: 1.5rem;">
<div class="modal-header border-0 p-4 pb-0">
<div>
<h5 class="modal-title fw-black tracking-tight" id="modal-event-label">Action Log Detail</h5>
<div id="modal-module-info" class="text-theme-1 small fw-bold text-uppercase"></div>
</div>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body p-4">
{{-- Initiator & Time --}}
<div class="row g-4 mb-4">
<div class="col-md-6">
<div class="p-3 bg-light rounded-4 h-100 shadow-sm border border-white">
<label class="extra-small text-uppercase fw-black text-secondary mb-2 d-block opacity-50">{{ __('Initiator') }}</label>
<div class="d-flex align-items-center gap-3">
<div class="avatar avatar-40 rounded-circle bg-white shadow-sm d-flex align-items-center justify-content-center text-theme-1">
<i class="bi bi-person-fill"></i>
</div>
<div>
<div id="modal-causer-name" class="fw-bold text-dark"></div>
<div id="modal-causer-email" class="text-secondary extra-small"></div>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="p-3 bg-light rounded-4 h-100 shadow-sm border border-white">
<label class="extra-small text-uppercase fw-black text-secondary mb-2 d-block opacity-50">{{ __('Timestamp & Origin') }}</label>
<div class="d-flex align-items-center gap-3">
<div class="avatar avatar-40 rounded-circle bg-white shadow-sm d-flex align-items-center justify-content-center text-info">
<i class="bi bi-clock-history"></i>
</div>
<div>
<div id="modal-time-info" class="fw-bold text-dark small"></div>
<div id="modal-network-info" class="text-secondary extra-small"></div>
</div>
</div>
</div>
</div>
</div>
{{-- Data Changes List (Professional Diff Style) --}}
<div class="mb-4">
<label class="extra-small text-uppercase fw-black text-secondary mb-3 d-block opacity-50 ms-1">{{ __('Data Modifications') }}</label>
<div id="modal-changes-list" class="overflow-hidden border border-light" style="border-radius: 1rem;">
<!-- Injected here -->
</div>
</div>
{{-- Technical Data --}}
<div>
<div class="d-flex justify-content-between align-items-center mb-2 px-1">
<label class="extra-small text-uppercase fw-black text-secondary mb-0 opacity-50">{{ __('Raw Metadata') }}</label>
<button class="btn btn-sm btn-link extra-small text-decoration-none fw-bold p-0" onclick="copyRawJson()">{{ __('COPY JSON') }}</button>
</div>
<pre id="modal-raw-json" class="bg-dark text-warning p-4 rounded-4 small mb-0 shadow-sm scroll-custom" style="white-space: pre-wrap; font-size: 11px; max-height: 250px; overflow-y: auto; font-family: 'Fira Code', 'JetBrains Mono', monospace; line-height: 1.5;"></pre>
</div>
</div>
<div class="modal-footer border-0 p-4 pt-0">
<button type="button" class="btn btn-dark w-100 py-3 rounded-pill fw-bold shadow-sm" data-bs-dismiss="modal">{{ __('Close Audit View') }}</button>
</div>
</div>
</div>
</div>
@push('scripts')
<script>
document.addEventListener("DOMContentLoaded", () => {
const modal = new bootstrap.Modal('#detailLogModal');
document.addEventListener("click", e => {
// Event category filter handler
const filterBtn = e.target.closest(".filter-event");
if (filterBtn) {
document.querySelectorAll(".filter-event").forEach(b => {
b.classList.remove("btn-dark");
b.classList.add("btn-outline-dark", "border", "border-dark");
});
filterBtn.classList.add("btn-dark");
filterBtn.classList.remove("btn-outline-dark", "border", "border-dark");
document.getElementById("filter-event-val").value = filterBtn.dataset.value;
window.reloadDataTable?.();
return;
}
const btn = e.target.closest(".btn-detail-log");
if (!btn) return;
const data = JSON.parse(btn.dataset.activity);
// Populating data
document.getElementById('modal-event-label').textContent = data.event.label;
document.getElementById('modal-causer-name').textContent = data.causer.name;
document.getElementById('modal-causer-email').textContent = data.causer.email;
document.getElementById('modal-module-info').textContent = data.subject.module + "" + data.subject.type;
document.getElementById('modal-time-info').textContent = data.meta.time;
document.getElementById('modal-network-info').textContent = "IP: " + data.meta.ip;
// Build Changes
const container = document.getElementById('modal-changes-list');
container.innerHTML = '';
if (data.changes.length > 0) {
data.changes.forEach(change => {
const row = document.createElement('div');
row.className = 'bg-white border-bottom p-3 transition-all hover-bg-light';
let diffContent = '';
if (change.old === 'NULL' && change.new !== 'NULL') {
// Creation / Initialization
diffContent = `
<div class="diff-box bg-success bg-opacity-10 border-success border-opacity-25 p-2 rounded small w-100">
<span class="text-success fw-bold me-2">+</span> ${change.new}
</div>`;
} else if (change.old !== null) {
// Update
diffContent = `
<div class="d-flex flex-column gap-2 w-100">
<div class="diff-box bg-danger bg-opacity-10 border-danger border-opacity-25 p-2 rounded small">
<span class="text-danger fw-bold me-2">-</span> ${change.old}
</div>
<div class="diff-box bg-success bg-opacity-10 border-success border-opacity-25 p-2 rounded small">
<span class="text-success fw-bold me-2">+</span> ${change.new}
</div>
</div>`;
} else {
// General
diffContent = `<div class="p-2 small text-dark fw-medium">${change.new}</div>`;
}
row.innerHTML = `
<div class="row align-items-start g-3">
<div class="col-md-3">
<span class="extra-small text-uppercase fw-black text-secondary d-block mt-1">${change.field}</span>
</div>
<div class="col-md-9">
${diffContent}
</div>
</div>`;
container.appendChild(row);
});
} else {
container.innerHTML = `
<div class="p-5 text-center bg-light bg-opacity-50">
<i class="bi bi-info-circle text-muted fs-2 mb-2 d-block"></i>
<div class="text-muted small fw-bold">No data modifications recorded for this event.</div>
<div class="extra-small text-secondary opacity-75">This usually happens for authentication or read events.</div>
</div>`;
}
document.getElementById('modal-raw-json').textContent = JSON.stringify(data.raw, null, 2);
modal.show();
});
// Export CSV Handler
const exportBtn = document.getElementById('export-csv-btn');
if (exportBtn) {
exportBtn.addEventListener('click', () => {
const event = document.getElementById('filter-event-val').value;
const search = document.querySelector('.dataTables_filter input')?.value || '';
let url = `{{ route('action-logs.export') }}?event=${event}&search=${encodeURIComponent(search)}`;
window.location.href = url;
toastr?.info('{{ __("Preparing your export, please wait...") }}');
});
}
// Clear Activity Logs Handler
const clearBtn = document.getElementById('clear-activity-logs-btn');
if (clearBtn) {
clearBtn.addEventListener('click', () => {
StandardSwal.fire({
title: 'Clear Global Action Logs?',
text: 'You are about to permanently erase all recorded system audit trails. This action cannot be undone.',
icon: 'warning',
showCancelButton: true,
customClass: {
confirmButton: 'btn-pill-danger',
cancelButton: 'btn-pill-cancel'
},
confirmButtonText: 'Yes, Clear All',
cancelButtonText: 'Cancel'
}).then((result) => {
if (result.isConfirmed) {
fetch('{{ route("action-logs.clear") }}', {
method: 'POST',
headers: {
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'Accept': 'application/json'
}
})
.then(res => res.json())
.then(data => {
if (data.success) {
window.reloadDataTable?.();
StandardSwal.fire({
icon: 'success',
title: 'Logs Cleared!',
text: data.message || 'System audit trails have been successfully removed.',
timer: 2000,
showConfirmButton: false,
timerProgressBar: true
});
} else {
StandardSwal.fire({ icon: 'error', title: 'Action Failed!', text: data.message });
}
});
}
});
});
}
});
function copyRawJson() {
const content = document.getElementById('modal-raw-json').textContent;
navigator.clipboard.writeText(content);
toastr?.info('{{ __("Audit JSON Copied to Clipboard") }}');
}
</script>
<style>
.fw-black { font-weight: 900; }
.tracking-tight { letter-spacing: -1px; }
.extra-small { font-size: 0.65rem; }
.hover-bg-light:hover { background-color: #f8fafc !important; }
.diff-box { border: 1px solid transparent; font-family: 'Fira Code', 'JetBrains Mono', monospace; word-break: break-all; }
.scroll-custom::-webkit-scrollbar { width: 6px; }
.scroll-custom::-webkit-scrollbar-thumb { background: #334155; border-radius: 10px; }
.bg-dark { background-color: #0f172a !important; }
</style>
@endpush
</x-app-layout>