chartboard/app/Views/audit/index.php
2026-03-30 09:51:33 +05:30

192 lines
9.2 KiB
PHP

<?= $this->extend('layouts/main') ?>
<?= $this->section('content') ?>
<header class="cb-page-header cb-page-header--center">
<div>
<h1 class="cb-page-title">Audit log</h1>
<p class="cb-page-subtitle mb-0">Workspace and app activity with filters.</p>
</div>
</header>
<div class="cb-page-body">
<?php if (session()->getFlashdata('error')): ?>
<div class="alert alert-danger"><?= esc(session()->getFlashdata('error')) ?></div>
<?php endif; ?>
<div class="cb-form-shell">
<form method="get" action="<?= base_url('audit') ?>" class="row g-3 align-items-end">
<?php if ($isSuperadmin): ?>
<div class="col-12 col-sm-6 col-lg-2">
<label class="form-label" for="audit-scope">Scope</label>
<select id="audit-scope" name="scope" class="form-select">
<option value="active" <?= $scope === 'active' ? 'selected' : '' ?>>Active workspace</option>
<option value="global" <?= $scope === 'global' ? 'selected' : '' ?>>Global (no workspace)</option>
<option value="all" <?= $scope === 'all' ? 'selected' : '' ?>>All workspaces</option>
</select>
</div>
<?php endif; ?>
<div class="col-12 col-sm-6 col-lg-2">
<label class="form-label" for="audit-user">User</label>
<select id="audit-user" name="user_id" class="form-select">
<option value="0">Any user</option>
<?php foreach ($memberUsers as $u): ?>
<option value="<?= (int) $u['id'] ?>" <?= (int) ($filters['user_id'] ?? 0) === (int) $u['id'] ? 'selected' : '' ?>>
<?= esc($u['name'] ?: $u['email']) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-12 col-md-6 col-lg-2">
<label class="form-label" for="audit-action">Action contains</label>
<input id="audit-action" type="text" name="action" class="form-control" value="<?= esc($filters['action'] ?? '') ?>" placeholder="e.g. chart.">
</div>
<div class="col-6 col-md-3 col-lg-2">
<label class="form-label" for="audit-from">From</label>
<input id="audit-from" type="date" name="date_from" class="form-control" value="<?= esc($filters['date_from'] ?? '') ?>">
</div>
<div class="col-6 col-md-3 col-lg-2">
<label class="form-label" for="audit-to">To</label>
<input id="audit-to" type="date" name="date_to" class="form-control" value="<?= esc($filters['date_to'] ?? '') ?>">
</div>
<div class="col-12 col-lg-auto d-flex flex-wrap gap-2 align-self-end">
<button type="submit" class="btn btn-primary">Apply</button>
<a class="btn btn-outline-secondary" href="<?= base_url('audit') ?>">Reset</a>
</div>
</form>
</div>
<div class="cb-table-shell">
<div class="table-responsive">
<table class="table cb-data-table align-middle">
<thead>
<tr>
<th>When</th>
<th>User</th>
<th>Action</th>
<th>Resource</th>
<th>IP</th>
<th class="text-end">Actions</th>
</tr>
</thead>
<tbody>
<?php if ($logs === []): ?>
<tr>
<td colspan="6" class="text-center text-muted py-5">No audit entries match your filters.</td>
</tr>
<?php endif; ?>
<?php foreach ($logs as $log): ?>
<tr>
<td class="text-nowrap small text-muted"><?= esc($log['created_at'] ?? '') ?></td>
<td>
<div class="cb-cell-name"><?= esc($log['user_name'] ?? '—') ?></div>
<?php if (! empty($log['user_email'])): ?>
<div class="cb-cell-sub"><?= esc($log['user_email']) ?></div>
<?php endif; ?>
</td>
<td><span class="cb-audit-action-code"><?= esc($log['action'] ?? '') ?></span></td>
<td class="small">
<?php if (! empty($log['resource_type'])): ?>
<?= esc($log['resource_type']) ?>
<?php if ($log['resource_id'] !== null && $log['resource_id'] !== ''): ?>
#<?= (int) $log['resource_id'] ?>
<?php endif; ?>
<?php else: ?>
<?php endif; ?>
</td>
<td class="small text-muted"><?= esc($log['ip_address'] ?? '—') ?></td>
<td class="text-end">
<div class="cb-table-actions">
<button type="button" class="cb-action-btn cb-action-btn--ghost" data-audit-detail="<?= (int) $log['id'] ?>">Details</button>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<div class="cb-pager-wrap">
<?= $pager->links('default', 'cb_full') ?>
</div>
</div>
</div>
<div class="modal fade" id="auditDetailModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title h6 mb-0">Audit detail</h2>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<dl class="row small mb-3" id="auditDetailMeta"></dl>
<div class="row g-2">
<div class="col-md-6">
<div class="fw-semibold small mb-1">Previous value</div>
<pre class="bg-light border rounded p-2 small mb-0" style="max-height:240px;overflow:auto;" id="auditOldJson"></pre>
</div>
<div class="col-md-6">
<div class="fw-semibold small mb-1">New value</div>
<pre class="bg-light border rounded p-2 small mb-0" style="max-height:240px;overflow:auto;" id="auditNewJson"></pre>
</div>
</div>
</div>
</div>
</div>
</div>
<?= $this->endSection() ?>
<?= $this->section('scripts') ?>
<script>
document.addEventListener('DOMContentLoaded', function () {
const modalEl = document.getElementById('auditDetailModal');
const metaEl = document.getElementById('auditDetailMeta');
const oldEl = document.getElementById('auditOldJson');
const newEl = document.getElementById('auditNewJson');
const modal = modalEl ? new bootstrap.Modal(modalEl) : null;
const detailUrlBase = <?= json_encode(rtrim(base_url('audit/detail/'), '/') . '/') ?>;
document.querySelectorAll('[data-audit-detail]').forEach(function (btn) {
btn.addEventListener('click', async function () {
const id = btn.getAttribute('data-audit-detail');
if (!id || !modal) return;
oldEl.textContent = '…';
newEl.textContent = '…';
metaEl.innerHTML = '';
modal.show();
try {
const res = await fetch(detailUrlBase + id, {
headers: { 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }
});
const data = await res.json();
if (!data.success) {
oldEl.textContent = data.message || 'Could not load detail.';
newEl.textContent = '';
return;
}
const l = data.log;
metaEl.innerHTML =
'<dt class="col-sm-3">Action</dt><dd class="col-sm-9"><code>' + escapeHtml(l.action) + '</code></dd>' +
'<dt class="col-sm-3">Resource</dt><dd class="col-sm-9">' + escapeHtml((l.resource_type || '—') + (l.resource_id != null ? ' #' + l.resource_id : '')) + '</dd>' +
'<dt class="col-sm-3">User</dt><dd class="col-sm-9">' + escapeHtml((l.user_name || '—') + (l.user_email ? ' · ' + l.user_email : '')) + '</dd>' +
'<dt class="col-sm-3">Time</dt><dd class="col-sm-9">' + escapeHtml(l.created_at) + '</dd>' +
'<dt class="col-sm-3">IP / UA</dt><dd class="col-sm-9">' + escapeHtml((l.ip_address || '—') + (l.user_agent ? ' · ' + l.user_agent.substring(0, 120) : '')) + '</dd>';
oldEl.textContent = l.old_value_json || '(empty)';
newEl.textContent = l.new_value_json || '(empty)';
} catch (e) {
oldEl.textContent = 'Request failed.';
newEl.textContent = '';
}
});
});
function escapeHtml(s) {
if (!s) return '';
const d = document.createElement('div');
d.textContent = s;
return d.innerHTML;
}
});
</script>
<?= $this->endSection() ?>