first commit

This commit is contained in:
2026-04-11 14:08:27 +07:00
parent e86e5d2c46
commit 6b7655aa16
389 changed files with 5387 additions and 60861 deletions

View File

@@ -1,699 +1,243 @@
<div class="container">
<div class="d-flex justify-content-between align-items-center mt-4 mb-4">
<div>
<h1 class="h3 mb-0" style="color: var(--primary-dark);">
<%= title %>
</h1>
<p class="text-muted mb-0">System activity and change tracking</p>
<!-- Page Header -->
<div class="page-title-area">
<div>
<h1>Audit Logs</h1>
<p class="subtitle">System activity and change tracking</p>
</div>
<button type="button" class="btn btn-outline-warning" onclick="openCleanupModal()">
<i class="fas fa-broom"></i> Cleanup Old Logs
</button>
</div>
<!-- Filters -->
<div class="card border-0 mb-3">
<div class="card-body" style="padding:1rem 1.25rem;">
<form method="GET" action="/admin/audit-logs">
<div class="row g-2 align-items-end">
<div class="col-md-2">
<label class="form-label">Model</label>
<select class="form-select" name="model">
<option value="">All Models</option>
<% uniqueModels.forEach(model => { %>
<option value="<%= model %>" <%= query.model === model ? 'selected' : '' %>><%= model %></option>
<% }); %>
</select>
</div>
<div class="d-flex gap-2">
<button type="button" class="btn btn-outline-warning" onclick="openCleanupModal()">
<i class="fas fa-broom me-2"></i>Cleanup Old Logs
</button>
<div class="col-md-2">
<label class="form-label">Action</label>
<select class="form-select" name="action">
<option value="">All Actions</option>
<% uniqueActions.forEach(action => { %>
<option value="<%= action %>" <%= query.action === action ? 'selected' : '' %>><%= action %></option>
<% }); %>
</select>
</div>
</div>
<!-- Filters -->
<div class="card border-0 shadow-sm mb-4">
<div class="card-body">
<form method="GET" action="/admin/audit-logs" class="row g-3">
<div class="col-md-2">
<label class="form-label">Model</label>
<select class="form-select" name="model">
<option value="">All Models</option>
<% uniqueModels.forEach(model => { %>
<option value="<%= model %>" <%= query.model === model ? 'selected' : '' %>>
<%= model %>
</option>
<% }); %>
</select>
</div>
<div class="col-md-2">
<label class="form-label">Action</label>
<select class="form-select" name="action">
<option value="">All Actions</option>
<% uniqueActions.forEach(action => { %>
<option value="<%= action %>" <%= query.action === action ? 'selected' : '' %>>
<%= action %>
</option>
<% }); %>
</select>
</div>
<div class="col-md-2">
<label class="form-label">User</label>
<select class="form-select" name="user">
<option value="">All Users</option>
<% users.forEach(user => { %>
<option value="<%= user._id %>" <%= query.user === user._id.toString() ? 'selected' : '' %>>
<%= user.username %>
</option>
<% }); %>
</select>
</div>
<div class="col-md-2">
<label class="form-label">From Date</label>
<input type="date" class="form-control" name="dateFrom" value="<%= query.dateFrom || '' %>">
</div>
<div class="col-md-2">
<label class="form-label">To Date</label>
<input type="date" class="form-control" name="dateTo" value="<%= query.dateTo || '' %>">
</div>
<div class="col-md-2">
<label class="form-label">Items per page</label>
<select class="form-select" name="limit">
<option value="5" <%= (query.limit && query.limit == '5') ? 'selected' : '' %>>5 items</option>
<option value="7" <%= (!query.limit || query.limit == '7') ? 'selected' : '' %>>7 items</option>
<option value="10" <%= (query.limit && query.limit == '10') ? 'selected' : '' %>>10 items</option>
<option value="15" <%= (query.limit && query.limit == '15') ? 'selected' : '' %>>15 items</option>
<option value="20" <%= (query.limit && query.limit == '20') ? 'selected' : '' %>>20 items</option>
</select>
</div>
<div class="col-md-2">
<label class="form-label">&nbsp;</label>
<div class="d-flex gap-1">
<button type="submit" class="btn flex-fill" style="background-color: var(--primary-color); color: white;">
<i class="fas fa-search me-1"></i>Filter
</button>
<a href="/admin/audit-logs" class="btn btn-outline-secondary">
<i class="fas fa-times"></i>
</a>
</div>
</div>
</form>
<div class="col-md-2">
<label class="form-label">User</label>
<select class="form-select" name="user">
<option value="">All Users</option>
<% users.forEach(u => { %>
<option value="<%= u._id %>" <%= query.user === u._id.toString() ? 'selected' : '' %>><%= u.username %></option>
<% }); %>
</select>
</div>
</div>
<div class="col-md-2">
<label class="form-label">From Date</label>
<input type="date" class="form-control" name="dateFrom" value="<%= query.dateFrom || '' %>">
</div>
<div class="col-md-2">
<label class="form-label">To Date</label>
<input type="date" class="form-control" name="dateTo" value="<%= query.dateTo || '' %>">
</div>
<div class="col-md-1">
<label class="form-label">Per page</label>
<select class="form-select" name="limit" onchange="this.form.submit()">
<option value="8" <%= (!query.limit || query.limit == '8') ? 'selected' : '' %>>8</option>
<option value="15" <%= query.limit == '15' ? 'selected' : '' %>>15</option>
<option value="25" <%= query.limit == '25' ? 'selected' : '' %>>25</option>
</select>
</div>
<div class="col-md-1 d-flex gap-1">
<button type="submit" class="btn btn-primary flex-fill"><i class="fas fa-search"></i></button>
<a href="/admin/audit-logs" class="btn btn-outline-secondary"><i class="fas fa-times"></i></a>
</div>
</div>
</form>
</div>
</div>
<!-- Audit Logs List -->
<div class="card border-0 shadow-sm">
<div class="card-body p-4">
<% if (auditLogs && auditLogs.length > 0) { %>
<!-- Summary Stats -->
<div class="row mb-4">
<div class="col-md-8">
<div class="d-flex justify-content-between align-items-center">
<span class="text-muted">
Showing <%= ((pagination.current - 1) * pagination.limit) + 1 %> -
<%= Math.min(pagination.current * pagination.limit, pagination.totalCount) %>
of <%= pagination.totalCount %> audit logs
<% if (pagination.totalCount > pagination.limit) { %>
<span class="badge bg-info ms-2">
<%= pagination.total %> pages
</span>
<% } %>
</span>
</div>
<!-- Table -->
<div class="card border-0">
<div class="card-header">
<h5 class="card-header-title"><i class="fas fa-shield-alt"></i> Activity Log</h5>
<% if (pagination) { %>
<span class="badge badge-soft-primary"><%= pagination.totalCount %> records</span>
<% } %>
</div>
<div class="card-body p-0">
<% if (auditLogs && auditLogs.length > 0) { %>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead>
<tr>
<th style="width:130px">Date / Time</th>
<th>Model</th>
<th>Action</th>
<th>User</th>
<th>Changes</th>
<th>IP Address</th>
<th style="width:80px">Details</th>
</tr>
</thead>
<tbody>
<% auditLogs.forEach(log => { %>
<tr>
<td>
<div style="font-size:0.8rem;font-weight:500"><%= new Date(log.createdAt).toLocaleDateString('en-GB') %></div>
<div style="font-size:0.75rem;color:var(--text-muted)"><%= new Date(log.createdAt).toLocaleTimeString() %></div>
</td>
<td><span class="badge badge-soft-primary"><%= log.model %></span></td>
<td>
<%
let badgeClass = 'badge-soft-primary';
if (log.action.includes('CREATE')) badgeClass = 'bg-soft-success';
else if (log.action.includes('UPDATE')) badgeClass = 'bg-soft-warning';
else if (log.action.includes('DELETE')) badgeClass = 'bg-soft-danger';
%>
<span class="badge <%= badgeClass %>"><%= log.action %></span>
</td>
<td>
<% if (log.performedBy) { %>
<div style="font-weight:500;font-size:0.8125rem"><%= log.performedBy.username %></div>
<div style="font-size:0.75rem;color:var(--text-muted)"><%= log.performedBy.email %></div>
<% } else { %>
<span style="color:var(--text-muted);font-size:0.8125rem">System</span>
<% } %>
</td>
<td>
<% if (log.changes && log.changes.length > 0) { %>
<span class="badge badge-soft-accent"><%= log.changes.length %> field<%= log.changes.length > 1 ? 's' : '' %></span>
<div class="mt-1">
<% log.changes.slice(0, 2).forEach(change => { %>
<div style="font-size:0.75rem;color:var(--text-muted)"><%= change.field %></div>
<% }); %>
<% if (log.changes.length > 2) { %>
<div style="font-size:0.75rem;color:var(--text-muted)">+<%= log.changes.length - 2 %> more</div>
<% } %>
</div>
<div class="col-md-4">
<div class="d-flex justify-content-end">
<div class="btn-group btn-group-sm me-3" role="group">
<input type="radio" class="btn-check" name="viewMode" id="tableView" autocomplete="off" checked>
<label class="btn btn-outline-secondary" for="tableView">
<i class="fas fa-table"></i> Table
</label>
<input type="radio" class="btn-check" name="viewMode" id="cardView" autocomplete="off">
<label class="btn btn-outline-secondary" for="cardView">
<i class="fas fa-th-large"></i> Cards
</label>
</div>
</div>
</div>
</div>
<% } else { %>
<span style="color:var(--text-muted)">—</span>
<% } %>
</td>
<td style="font-size:0.8rem;color:var(--text-muted)"><%= log.ipAddress %></td>
<td>
<a href="/admin/audit-logs/<%= log._id %>" class="btn btn-sm btn-outline-primary btn-icon" title="View">
<i class="fas fa-eye"></i>
</a>
</td>
</tr>
<% }); %>
</tbody>
</table>
</div>
<!-- Table View -->
<div id="tableViewContent" class="table-responsive">
<table class="table table-hover align-middle">
<thead>
<tr>
<th scope="col" style="width: 140px; color: var(--primary-dark);">Date/Time</th>
<th scope="col" style="color: var(--primary-dark);">Model</th>
<th scope="col" style="color: var(--primary-dark);">Action</th>
<th scope="col" style="color: var(--primary-dark);">User</th>
<th scope="col" style="color: var(--primary-dark);">Changes</th>
<th scope="col" style="color: var(--primary-dark);">IP Address</th>
<th scope="col" style="width: 120px; color: var(--primary-dark);">Actions</th>
</tr>
</thead>
<tbody>
<% auditLogs.forEach((log, index) => { %>
<tr>
<td>
<small class="text-muted">
<%= new Date(log.createdAt).toLocaleDateString() %><br>
<%= new Date(log.createdAt).toLocaleTimeString() %>
</small>
</td>
<td>
<span class="badge" style="background-color: var(--primary-color); color: white;">
<%= log.model %>
</span>
</td>
<td>
<%
let actionStyle = 'background-color: var(--primary-color); color: white;';
if (log.action.includes('CREATE')) actionStyle = 'background-color: #28a745; color: white;';
else if (log.action.includes('UPDATE')) actionStyle = 'background-color: #ffc107; color: #212529;';
else if (log.action.includes('DELETE')) actionStyle = 'background-color: #dc3545; color: white;';
%>
<span class="badge" style="<%= actionStyle %>">
<%= log.action %>
</span>
</td>
<td>
<% if (log.performedBy) { %>
<div>
<strong style="color: var(--primary-dark);"><%= log.performedBy.username %></strong><br>
<small class="text-muted"><%= log.performedBy.email %></small>
</div>
<% } else { %>
<span class="text-muted">System</span>
<% } %>
</td>
<td>
<% if (log.changes && log.changes.length > 0) { %>
<span class="badge" style="background-color: var(--primary-color); color: white;">
<%= log.changes.length %> field<%= log.changes.length > 1 ? 's' : '' %>
</span>
<div class="mt-1">
<% log.changes.slice(0, 3).forEach(change => { %>
<small class="text-muted d-block">
<strong><%= change.field %></strong>
</small>
<% }); %>
<% if (log.changes.length > 3) { %>
<small class="text-muted">
+<%= log.changes.length - 3 %> more...
</small>
<% } %>
</div>
<% } else { %>
<span class="text-muted">-</span>
<% } %>
</td>
<td>
<small class="text-muted">
<%= log.ipAddress %>
</small>
</td>
<td>
<div class="btn-group" role="group">
<a href="/admin/audit-logs/<%= log._id %>" class="btn btn-sm" style="background-color: var(--primary-color); color: white;">
<i class="fas fa-eye me-1"></i>View
</a>
</div>
</td>
</tr>
<% }); %>
</tbody>
</table>
</div>
<!-- Card View (Hidden by default) -->
<div id="cardViewContent" style="display: none;">
<% auditLogs.forEach((log, index) => { %>
<div class="audit-card">
<div class="card h-100 border-0 shadow-sm">
<div class="card-header d-flex justify-content-between align-items-center py-2">
<span class="badge" style="background-color: var(--primary-color); color: white; font-size: 0.7rem;"><%= log.model %></span>
<small class="text-muted" style="font-size: 0.7rem;">
<%= new Date(log.createdAt).toLocaleDateString() %>
</small>
</div>
<div class="card-body p-3">
<div class="d-flex justify-content-between align-items-center mb-2">
<%
let actionStyle = 'background-color: var(--primary-color); color: white;';
if (log.action.includes('CREATE')) actionStyle = 'background-color: #28a745; color: white;';
else if (log.action.includes('UPDATE')) actionStyle = 'background-color: #ffc107; color: #212529;';
else if (log.action.includes('DELETE')) actionStyle = 'background-color: #dc3545; color: white;';
%>
<span class="badge" style="<%= actionStyle %>; font-size: 0.65rem;">
<%= log.action %>
</span>
<small class="text-muted" style="font-size: 0.7rem;">
<%= new Date(log.createdAt).toLocaleTimeString() %>
</small>
</div>
<div class="mb-2">
<strong style="font-size: 0.8rem;">User:</strong>
<% if (log.performedBy) { %>
<div style="color: var(--primary-dark); font-size: 0.8rem;"><%= log.performedBy.username %></div>
<small class="text-muted" style="font-size: 0.7rem;"><%= log.performedBy.email %></small>
<% } else { %>
<span class="text-muted" style="font-size: 0.8rem;">System</span>
<% } %>
</div>
<% if (log.changes && log.changes.length > 0) { %>
<div class="mb-2">
<strong style="font-size: 0.8rem;">Changes:</strong>
<span class="badge ms-1" style="background-color: var(--primary-color); color: white; font-size: 0.65rem;">
<%= log.changes.length %> field<%= log.changes.length > 1 ? 's' : '' %>
</span>
<div class="mt-1">
<% log.changes.slice(0, 2).forEach(change => { %>
<small class="text-muted d-block" style="font-size: 0.7rem;">
• <%= change.field %>
</small>
<% }); %>
<% if (log.changes.length > 2) { %>
<small class="text-muted" style="font-size: 0.7rem;">
+<%= log.changes.length - 2 %> more...
</small>
<% } %>
</div>
</div>
<% } %>
<div class="mb-2">
<small class="text-muted" style="font-size: 0.7rem;">
IP: <%= log.ipAddress %>
</small>
</div>
</div>
<div class="card-footer p-2">
<a href="/admin/audit-logs/<%= log._id %>" class="btn btn-sm w-100" style="background-color: var(--primary-color); color: white; font-size: 0.8rem;">
<i class="fas fa-eye me-1"></i>View Details
</a>
</div>
</div>
</div>
<% }); %>
</div>
<!-- Pagination -->
<% if (pagination && pagination.total > 1) { %>
<nav aria-label="Audit log pagination" class="mt-4">
<ul class="pagination justify-content-center">
<% if (pagination.current > 1) { %>
<li class="page-item">
<a class="page-link" href="?page=<%= pagination.current - 1 %><%= Object.keys(query).map(key => key !== 'page' ? '&' + key + '=' + encodeURIComponent(query[key]) : '').join('') %>" style="color: var(--primary-dark);">Previous</a>
</li>
<% } %>
<% for (let i = 1; i <= pagination.total; i++) { %>
<% if (i === pagination.current) { %>
<li class="page-item active">
<span class="page-link" style="background-color: var(--primary-color); border-color: var(--primary-color);"><%= i %></span>
</li>
<% } else if (i === 1 || i === pagination.total || (i >= pagination.current - 2 && i <= pagination.current + 2)) { %>
<li class="page-item">
<a class="page-link" href="?page=<%= i %><%= Object.keys(query).map(key => key !== 'page' ? '&' + key + '=' + encodeURIComponent(query[key]) : '').join('') %>" style="color: var(--primary-dark);">
<%= i %>
</a>
</li>
<% } else if (i === pagination.current - 3 || i === pagination.current + 3) { %>
<li class="page-item disabled">
<span class="page-link">...</span>
</li>
<% } %>
<% } %>
<% if (pagination.current < pagination.total) { %>
<li class="page-item">
<a class="page-link" href="?page=<%= pagination.current + 1 %><%= Object.keys(query).map(key => key !== 'page' ? '&' + key + '=' + encodeURIComponent(query[key]) : '').join('') %>" style="color: var(--primary-dark);">Next</a>
</li>
<% } %>
</ul>
<!-- Pagination Info -->
<div class="text-center mt-2">
<small class="text-muted">
Page <%= pagination.current %> of <%= pagination.total %>
(showing <%= ((pagination.current - 1) * pagination.limit) + 1 %> -
<%= Math.min(pagination.current * pagination.limit, pagination.totalCount) %>
of <%= pagination.totalCount %> total items)
</small>
</div>
</nav>
<!-- Pagination -->
<% if (pagination && pagination.total > 1) { %>
<div class="d-flex align-items-center justify-content-between px-3 py-2" style="border-top:1px solid var(--border-light);">
<small style="color:var(--text-muted)">
Showing <%= ((pagination.current - 1) * pagination.limit) + 1 %><%= Math.min(pagination.current * pagination.limit, pagination.totalCount) %> of <%= pagination.totalCount %>
</small>
<nav>
<ul class="pagination pagination-sm mb-0" style="gap:2px;">
<% if (pagination.current > 1) { %>
<li class="page-item">
<a class="page-link" href="?page=<%= pagination.current - 1 %><%= Object.keys(query).map(k => k !== 'page' ? '&' + k + '=' + encodeURIComponent(query[k]) : '').join('') %>">
<i class="fas fa-chevron-left" style="font-size:0.7rem"></i>
</a>
</li>
<% } %>
<% for (let i = 1; i <= pagination.total; i++) { %>
<% if (i === pagination.current) { %>
<li class="page-item active">
<span class="page-link" style="background:var(--primary-color);border-color:var(--primary-color)"><%= i %></span>
</li>
<% } else if (i === 1 || i === pagination.total || (i >= pagination.current - 2 && i <= pagination.current + 2)) { %>
<li class="page-item">
<a class="page-link" href="?page=<%= i %><%= Object.keys(query).map(k => k !== 'page' ? '&' + k + '=' + encodeURIComponent(query[k]) : '').join('') %>"><%= i %></a>
</li>
<% } else if (i === pagination.current - 3 || i === pagination.current + 3) { %>
<li class="page-item disabled"><span class="page-link">…</span></li>
<% } %>
<% } else { %>
<div class="text-center py-5">
<i class="fas fa-clipboard-list text-muted mb-3" style="font-size: 3rem;"></i>
<h5 class="text-muted mb-3">No Audit Logs Found</h5>
<p class="text-muted">No audit logs match your current filters.</p>
<a href="/admin/audit-logs" class="btn" style="background-color: var(--primary-color); color: white;">
<i class="fas fa-refresh me-1"></i>Clear Filters
</a>
</div>
<% } %>
<% } %>
<% if (pagination.current < pagination.total) { %>
<li class="page-item">
<a class="page-link" href="?page=<%= pagination.current + 1 %><%= Object.keys(query).map(k => k !== 'page' ? '&' + k + '=' + encodeURIComponent(query[k]) : '').join('') %>">
<i class="fas fa-chevron-right" style="font-size:0.7rem"></i>
</a>
</li>
<% } %>
</ul>
</nav>
</div>
</div>
<% } %>
<% } else { %>
<div class="empty-state">
<div class="empty-state-icon"><i class="fas fa-shield-alt"></i></div>
<h5>No audit logs found</h5>
<p>No activity matches your current filters.</p>
<a href="/admin/audit-logs" class="btn btn-outline-primary">Clear Filters</a>
</div>
<% } %>
</div>
</div>
<!-- Custom Cleanup Modal -->
<div id="cleanupModal" class="custom-modal" style="display: none;">
<div class="custom-modal-overlay"></div>
<div class="custom-modal-content">
<div class="custom-modal-header">
<h5 class="custom-modal-title">
<i class="fas fa-broom me-2"></i>Cleanup Old Audit Logs
</h5>
<button type="button" class="custom-modal-close" onclick="closeCleanupModal()">
<i class="fas fa-times"></i>
</button>
</div>
<form method="POST" action="/admin/audit-logs/cleanup">
<div class="custom-modal-body">
<p>Delete audit logs older than the specified number of days.</p>
<div class="mb-3">
<label for="days" class="form-label">Keep logs for (days):</label>
<input type="number" class="form-control" id="days" name="days" value="90" min="1" max="365" required>
<div class="form-text">Recommended: 90 days for compliance</div>
</div>
<div class="alert alert-warning">
<i class="fas fa-exclamation-triangle me-2"></i>
<strong>Warning:</strong> This action cannot be undone. Deleted audit logs will be permanently removed.
</div>
</div>
<div class="custom-modal-footer">
<button type="button" class="btn btn-secondary" onclick="closeCleanupModal()">Cancel</button>
<button type="submit" class="btn btn-warning">
<i class="fas fa-broom me-1"></i>Cleanup Logs
</button>
</div>
</form>
<!-- Cleanup Modal -->
<div id="cleanupModal" style="display:none;position:fixed;inset:0;z-index:1050;">
<div style="position:absolute;inset:0;background:rgba(0,0,0,0.45);" onclick="closeCleanupModal()"></div>
<div style="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);background:#fff;border-radius:var(--border-radius-lg);box-shadow:var(--shadow-lg);width:90%;max-width:460px;overflow:hidden;">
<div style="padding:1rem 1.25rem;background:var(--warning-soft);border-bottom:1px solid var(--border-color);display:flex;align-items:center;justify-content:space-between;">
<span style="font-weight:600;color:var(--warning-color);display:flex;align-items:center;gap:0.5rem;">
<i class="fas fa-broom"></i> Cleanup Old Audit Logs
</span>
<button onclick="closeCleanupModal()" style="background:none;border:none;cursor:pointer;color:var(--warning-color);font-size:1.1rem;"><i class="fas fa-times"></i></button>
</div>
<form method="POST" action="/admin/audit-logs/cleanup">
<div style="padding:1.25rem;">
<p style="font-size:var(--font-size-sm);color:var(--text-muted);margin-bottom:1rem;">Delete audit logs older than the specified number of days.</p>
<div class="mb-3">
<label class="form-label">Keep logs for (days)</label>
<input type="number" class="form-control" name="days" value="90" min="1" max="365" required>
<div class="form-text">Recommended: 90 days for compliance</div>
</div>
<div class="alert d-flex gap-2 align-items-start" style="background:var(--warning-soft);border:1px solid rgba(217,119,6,0.2);border-radius:var(--border-radius-sm);padding:0.75rem;">
<i class="fas fa-exclamation-triangle" style="color:var(--warning-color);margin-top:2px;flex-shrink:0;"></i>
<span style="font-size:var(--font-size-sm);color:var(--warning-color);">This action cannot be undone. Deleted logs will be permanently removed.</span>
</div>
</div>
<div style="padding:0.875rem 1.25rem;border-top:1px solid var(--border-color);display:flex;gap:0.5rem;justify-content:flex-end;background:#fafbfc;">
<button type="button" class="btn btn-outline-secondary" onclick="closeCleanupModal()">Cancel</button>
<button type="submit" class="btn btn-warning"><i class="fas fa-broom"></i> Cleanup</button>
</div>
</form>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// View mode toggle
const tableViewRadio = document.getElementById('tableView');
const cardViewRadio = document.getElementById('cardView');
const tableViewContent = document.getElementById('tableViewContent');
const cardViewContent = document.getElementById('cardViewContent');
// Debug: Check if elements exist
console.log('Elements found:', {
tableViewRadio: !!tableViewRadio,
cardViewRadio: !!cardViewRadio,
tableViewContent: !!tableViewContent,
cardViewContent: !!cardViewContent
});
if (!tableViewRadio || !cardViewRadio || !tableViewContent || !cardViewContent) {
console.error('Some view toggle elements are missing!');
return;
}
tableViewRadio.addEventListener('change', function() {
if (this.checked) {
tableViewContent.classList.remove('hidden');
cardViewContent.classList.remove('active');
console.log('Table view activated');
}
});
cardViewRadio.addEventListener('change', function() {
if (this.checked) {
tableViewContent.classList.add('hidden');
cardViewContent.classList.add('active');
console.log('Card view activated with CSS Grid');
}
});
// Auto-submit form when changing items per page
const limitSelect = document.querySelector('select[name="limit"]');
if (limitSelect) {
limitSelect.addEventListener('change', function() {
// Reset to page 1 when changing limit
const pageInput = document.querySelector('input[name="page"]');
if (pageInput) {
pageInput.value = 1;
} else {
// Create hidden input for page
const hiddenPageInput = document.createElement('input');
hiddenPageInput.type = 'hidden';
hiddenPageInput.name = 'page';
hiddenPageInput.value = 1;
this.form.appendChild(hiddenPageInput);
}
// Submit the form
this.form.submit();
});
}
});
function openCleanupModal() {
document.getElementById('cleanupModal').style.display = 'block';
document.body.style.overflow = 'hidden';
}
function closeCleanupModal() {
document.getElementById('cleanupModal').style.display = 'none';
document.body.style.overflow = 'auto';
}
// Close modal when clicking overlay
document.addEventListener('click', function(e) {
if (e.target.classList.contains('custom-modal-overlay')) {
closeCleanupModal();
}
});
// Close modal with Escape key
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
closeCleanupModal();
}
});
function exportLogs() {
// Get current filter parameters
const params = new URLSearchParams(window.location.search);
params.set('export', 'csv');
// Create download link
const exportUrl = '/admin/audit-logs/export?' + params.toString();
window.open(exportUrl, '_blank');
}
</script>
<style>
.badge {
font-size: 0.75em;
}
.card-header {
background-color: #f8f9fa;
border-bottom: 1px solid #dee2e6;
}
.table th {
border-top: none;
font-weight: 600;
color: var(--primary-dark);
}
.btn-group-sm .btn {
font-size: 0.75rem;
}
.pagination .page-link {
color: var(--primary-dark) !important;
background-color: #fff !important;
border: 1px solid #dee2e6 !important;
padding: 0.5rem 0.75rem;
text-decoration: none;
position: relative;
color: var(--primary-color);
border-color: var(--border-color);
border-radius: var(--border-radius-sm) !important;
font-size: 0.8125rem;
padding: 0.3rem 0.6rem;
}
.pagination .page-link:hover { background: var(--primary-soft); border-color: var(--primary-color); }
.pagination .page-item.active .page-link { color: #fff; }
.pagination .page-item.disabled .page-link { color: var(--text-muted); }
</style>
.pagination .page-item.active .page-link {
background-color: var(--primary-color) !important;
border-color: var(--primary-color) !important;
color: #fff !important;
z-index: 3;
font-weight: 600;
}
.pagination .page-link:hover {
color: var(--primary-color) !important;
background-color: #f8f9fa !important;
border-color: var(--primary-color) !important;
text-decoration: none;
}
.pagination .page-link:focus {
color: var(--primary-color) !important;
background-color: #f8f9fa !important;
border-color: var(--primary-color) !important;
box-shadow: 0 0 0 0.2rem rgba(var(--primary-color-rgb), 0.25);
outline: 0;
}
.pagination .page-item.disabled .page-link {
color: #6c757d !important;
background-color: #fff !important;
border-color: #dee2e6 !important;
}
/* Ensure pagination text is always visible */
.pagination .page-link span,
.pagination .page-link {
display: inline-block;
line-height: 1.25;
}
/* Custom Modal Styles */
.custom-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1050;
}
.custom-modal-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
}
.custom-modal-content {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: white;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
width: 90%;
max-width: 500px;
max-height: 90vh;
overflow-y: auto;
}
.custom-modal-header {
padding: 1rem 1.5rem;
border-bottom: 1px solid #dee2e6;
background-color: #fff3cd;
border-radius: 8px 8px 0 0;
display: flex;
justify-content: between;
align-items: center;
}
.custom-modal-title {
margin: 0;
font-size: 1.1rem;
font-weight: 600;
color: #856404;
flex: 1;
}
.custom-modal-close {
background: none;
border: none;
font-size: 1.2rem;
color: #856404;
cursor: pointer;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: background-color 0.2s;
}
.custom-modal-close:hover {
background-color: rgba(133, 100, 4, 0.1);
}
.custom-modal-body {
padding: 1.5rem;
}
.custom-modal-footer {
padding: 1rem 1.5rem;
border-top: 1px solid #dee2e6;
background-color: #f8f9fa;
border-radius: 0 0 8px 8px;
display: flex;
justify-content: flex-end;
gap: 0.5rem;
}
/* Card View Improvements - Using CSS Grid for better control */
#cardViewContent {
display: none !important;
visibility: hidden !important;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 1.5rem;
padding: 0;
margin: 0;
}
#cardViewContent.active {
display: grid !important;
visibility: visible !important;
}
#cardViewContent .card {
transition: transform 0.2s, box-shadow 0.2s;
width: 100% !important;
margin: 0 !important;
}
#cardViewContent .card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1) !important;
}
#cardViewContent .card-header {
background-color: #f8f9fa !important;
border-bottom: 1px solid #dee2e6;
}
#cardViewContent .card-footer {
background-color: #f8f9fa !important;
border-top: 1px solid #dee2e6;
}
/* Table View */
#tableViewContent {
display: block;
}
#tableViewContent.hidden {
display: none !important;
}
/* Responsive grid adjustments */
@media (max-width: 1400px) {
#cardViewContent {
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)) !important;
}
}
@media (max-width: 1200px) {
#cardViewContent {
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)) !important;
}
}
@media (max-width: 992px) {
#cardViewContent {
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)) !important;
}
}
@media (max-width: 768px) {
#cardViewContent {
grid-template-columns: 1fr !important;
gap: 1rem !important;
}
.custom-modal-content {
width: 95%;
margin: 1rem;
}
}
</style>
<script>
function openCleanupModal() { document.getElementById('cleanupModal').style.display = 'block'; }
function closeCleanupModal() { document.getElementById('cleanupModal').style.display = 'none'; }
document.addEventListener('keydown', e => { if (e.key === 'Escape') closeCleanupModal(); });
</script>

View File

@@ -1,314 +1,161 @@
<div class="container">
<div class="d-flex justify-content-between align-items-center mt-4 mb-4">
<div>
<h1 class="h3 mb-0" style="color: var(--primary-dark);">
<%= title %>
</h1>
<p class="text-muted mb-0">Detailed audit log information</p>
</div>
<div class="d-flex gap-2">
<a href="/admin/audit-logs" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left me-2"></i>Back to Audit Logs
</a>
</div>
</div>
<div class="row">
<!-- Main Information -->
<div class="col-12">
<div class="card border-0 shadow-sm mb-4">
<div class="card-header">
<h5 class="card-title mb-0" style="color: var(--primary-dark);">
<i class="fas fa-info-circle me-2"></i>Audit Information
</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label fw-bold">Model:</label>
<span class="badge ms-2" style="background-color: var(--primary-color); color: white;"><%= auditLog.model %></span>
</div>
<div class="mb-3">
<label class="form-label fw-bold">Action:</label>
<%
let actionStyle = 'background-color: var(--primary-color); color: white;';
if (auditLog.action.includes('CREATE')) actionStyle = 'background-color: #28a745; color: white;';
else if (auditLog.action.includes('UPDATE')) actionStyle = 'background-color: #ffc107; color: #212529;';
else if (auditLog.action.includes('DELETE')) actionStyle = 'background-color: #dc3545; color: white;';
%>
<span class="badge ms-2" style="<%= actionStyle %>"><%= auditLog.action %></span>
</div>
<div class="mb-3">
<label class="form-label fw-bold">Document ID:</label>
<code class="ms-2" style="background-color: #f8f9fa; color: var(--primary-dark);"><%= auditLog.documentId %></code>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label fw-bold">Date & Time:</label>
<div class="ms-2">
<%= new Date(auditLog.createdAt).toLocaleDateString() %>
<%= new Date(auditLog.createdAt).toLocaleTimeString() %>
</div>
</div>
<div class="mb-3">
<label class="form-label fw-bold">User:</label>
<div class="ms-2">
<% if (auditLog.performedBy) { %>
<strong style="color: var(--primary-dark);"><%= auditLog.performedBy.username %></strong><br>
<small class="text-muted"><%= auditLog.performedBy.email %></small>
<% } else { %>
<span class="text-muted">System</span>
<% } %>
</div>
</div>
<div class="mb-3">
<label class="form-label fw-bold">IP Address:</label>
<code class="ms-2" style="background-color: #f8f9fa; color: var(--primary-dark);"><%= auditLog.ipAddress %></code>
</div>
</div>
</div>
<% if (auditLog.userAgent) { %>
<div class="mb-3">
<label class="form-label fw-bold">User Agent:</label>
<div class="ms-2">
<small class="text-muted font-monospace"><%= auditLog.userAgent %></small>
</div>
</div>
<% } %>
</div>
</div>
<!-- Changes Details -->
<% if (auditLog.changes && auditLog.changes.length > 0) { %>
<div class="card border-0 shadow-sm mb-4">
<div class="card-header">
<h5 class="card-title mb-0" style="color: var(--primary-dark);">
<i class="fas fa-edit me-2"></i>Field Changes
<span class="badge ms-2" style="background-color: var(--primary-color); color: white;"><%= auditLog.changes.length %></span>
</h5>
</div>
<div class="card-body">
<% if (user && (user.role === 'admin' || user.role === 'superadmin')) { %>
<div class="table-responsive">
<table class="table table-sm">
<thead>
<tr>
<th style="width: 200px; color: var(--primary-dark);">Field</th>
<th style="color: var(--primary-dark);">Before</th>
<th style="color: var(--primary-dark);">After</th>
</tr>
</thead>
<tbody>
<% auditLog.changes.forEach((change, index) => { %>
<tr>
<td>
<strong style="color: var(--primary-dark);"><%= change.field %></strong>
</td>
<td>
<div class="change-value before">
<% if (change.before === null || change.before === undefined) { %>
<span class="text-muted fst-italic">null</span>
<% } else if (typeof change.before === 'object') { %>
<pre class="mb-0"><%= JSON.stringify(change.before, null, 2) %></pre>
<% } else { %>
<%= change.before %>
<% } %>
</div>
</td>
<td>
<div class="change-value after">
<% if (change.after === null || change.after === undefined) { %>
<span class="text-muted fst-italic">null</span>
<% } else if (typeof change.after === 'object') { %>
<pre class="mb-0"><%= JSON.stringify(change.after, null, 2) %></pre>
<% } else { %>
<%= change.after %>
<% } %>
</div>
</td>
</tr>
<% }); %>
</tbody>
</table>
</div>
<% } else { %>
<div class="alert alert-info">
<i class="fas fa-info-circle me-2"></i>
<strong>Summary View:</strong> Detailed field values are restricted to administrators.
</div>
<div class="table-responsive">
<table class="table table-sm">
<thead>
<tr>
<th style="color: var(--primary-dark);">Field</th>
<th style="color: var(--primary-dark);">Status</th>
</tr>
</thead>
<tbody>
<% auditLog.changes.forEach((change, index) => { %>
<tr>
<td>
<strong style="color: var(--primary-dark);"><%= change.field %></strong>
</td>
<td>
<span class="badge bg-info">Modified</span>
</td>
</tr>
<% }); %>
</tbody>
</table>
</div>
<% } %>
</div>
</div>
<% } %>
<!-- Summary Stats -->
<div class="card border-0 shadow-sm">
<div class="card-header">
<h6 class="card-title mb-0" style="color: var(--primary-dark);">
<i class="fas fa-chart-bar me-2"></i>Summary
</h6>
</div>
<div class="card-body">
<div class="row text-center">
<div class="col-6">
<div class="border-end">
<div class="h5 mb-0" style="color: var(--primary-color);">
<%= auditLog.changes ? auditLog.changes.length : 0 %>
</div>
<small class="text-muted">Fields Changed</small>
</div>
</div>
<div class="col-6">
<div class="h5 mb-0" style="color: var(--primary-color);">
<%= new Date(auditLog.createdAt).toLocaleDateString() === new Date().toLocaleDateString() ? 'Today' : 'Past' %>
</div>
<small class="text-muted">Timing</small>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Raw Data (Admin Only - Collapsible) -->
<!-- <% if (user && (user.role === 'admin' || user.role === 'superadmin')) { %>
<div class="card border-0 shadow-sm mt-4">
<div class="card-header">
<h5 class="card-title mb-0">
<button class="btn btn-link p-0 text-decoration-none" type="button" data-bs-toggle="collapse" data-bs-target="#rawDataCollapse" aria-expanded="false" style="color: var(--primary-dark);">
<i class="fas fa-code me-2"></i>Raw Data <span class="badge bg-warning text-dark ms-2">Admin Only</span>
<i class="fas fa-chevron-down ms-2"></i>
</button>
</h5>
</div>
<div class="collapse" id="rawDataCollapse">
<div class="card-body">
<div class="alert alert-info">
<i class="fas fa-info-circle me-2"></i>
<strong>Security Notice:</strong> Raw data contains sensitive information and is only visible to administrators.
</div>
<div class="row">
<% if (auditLog.before) { %>
<div class="col-md-6">
<h6 class="text-danger">Before:</h6>
<pre class="p-3 rounded" style="background-color: #f8f9fa; border: 1px solid #dee2e6; max-height: 400px; overflow-y: auto;"><%= JSON.stringify(auditLog.before, null, 2) %></pre>
</div>
<% } %>
<% if (auditLog.after) { %>
<div class="col-md-6">
<h6 class="text-success">After:</h6>
<pre class="p-3 rounded" style="background-color: #f8f9fa; border: 1px solid #dee2e6; max-height: 400px; overflow-y: auto;"><%= JSON.stringify(auditLog.after, null, 2) %></pre>
</div>
<% } %>
</div>
<% if (!auditLog.before && !auditLog.after) { %>
<div class="text-center text-muted py-3">
<i class="fas fa-info-circle me-2"></i>No raw data available for this audit log.
</div>
<% } %>
</div>
</div>
</div>
<% } else { %>
<div class="card border-0 shadow-sm mt-4">
<div class="card-header">
<h5 class="card-title mb-0" style="color: var(--primary-dark);">
<i class="fas fa-lock me-2"></i>Raw Data
</h5>
</div>
<div class="card-body">
<div class="alert alert-warning">
<i class="fas fa-shield-alt me-2"></i>
<strong>Access Restricted:</strong> Raw data access is limited to administrators for security reasons.
</div>
<div class="text-center text-muted py-3">
<i class="fas fa-user-shield" style="font-size: 3rem; opacity: 0.3;"></i>
<p class="mt-3">Contact your administrator if you need access to detailed raw data.</p>
</div>
</div>
</div>
<% } %> -->
<!-- Page Header -->
<div class="page-title-area">
<div>
<h1>Audit Log Details</h1>
<p class="subtitle">Detailed activity record</p>
</div>
<a href="/admin/audit-logs" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left"></i> Back
</a>
</div>
<style>
.change-value {
max-width: 300px;
word-wrap: break-word;
overflow-wrap: break-word;
}
<div class="row g-3">
.change-value pre {
max-height: 200px;
overflow-y: auto;
font-size: 0.8rem;
background-color: #f8f9fa;
border: 1px solid #dee2e6;
color: var(--primary-dark);
}
<!-- Main Info -->
<div class="col-lg-8">
<div class="card border-0 mb-3">
<div class="card-header">
<h5 class="card-header-title"><i class="fas fa-info-circle"></i> Audit Information</h5>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<div class="mb-3">
<div class="form-label">Model</div>
<span class="badge badge-soft-primary" style="font-size:0.8rem"><%= auditLog.model %></span>
</div>
<div class="mb-3">
<div class="form-label">Action</div>
<%
let badgeClass = 'badge-soft-primary';
if (auditLog.action.includes('CREATE')) badgeClass = 'bg-soft-success';
else if (auditLog.action.includes('UPDATE')) badgeClass = 'bg-soft-warning';
else if (auditLog.action.includes('DELETE')) badgeClass = 'bg-soft-danger';
%>
<span class="badge <%= badgeClass %>" style="font-size:0.8rem"><%= auditLog.action %></span>
</div>
<div class="mb-3">
<div class="form-label">Document ID</div>
<code style="font-size:0.8rem;color:var(--primary-color)"><%= auditLog.documentId %></code>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<div class="form-label">Date & Time</div>
<div style="font-size:var(--font-size-sm)">
<%= new Date(auditLog.createdAt).toLocaleDateString('en-GB') %>
<span style="color:var(--text-muted)"><%= new Date(auditLog.createdAt).toLocaleTimeString() %></span>
</div>
</div>
<div class="mb-3">
<div class="form-label">Performed By</div>
<% if (auditLog.performedBy) { %>
<div style="font-weight:500;font-size:var(--font-size-sm)"><%= auditLog.performedBy.username %></div>
<div style="font-size:var(--font-size-xs);color:var(--text-muted)"><%= auditLog.performedBy.email %></div>
<% } else { %>
<span style="color:var(--text-muted);font-size:var(--font-size-sm)">System</span>
<% } %>
</div>
<div class="mb-3">
<div class="form-label">IP Address</div>
<code style="font-size:0.8rem;color:var(--text-muted)"><%= auditLog.ipAddress %></code>
</div>
</div>
<% if (auditLog.userAgent) { %>
<div class="col-12">
<div class="form-label">User Agent</div>
<div style="font-size:0.75rem;color:var(--text-muted);font-family:monospace;word-break:break-all"><%= auditLog.userAgent %></div>
</div>
<% } %>
</div>
</div>
</div>
.change-value.before {
background-color: #fff5f5;
border-left: 3px solid #dc3545;
padding-left: 8px;
}
<!-- Field Changes -->
<% if (auditLog.changes && auditLog.changes.length > 0) { %>
<div class="card border-0">
<div class="card-header">
<h5 class="card-header-title"><i class="fas fa-pen"></i> Field Changes</h5>
<span class="badge badge-soft-accent"><%= auditLog.changes.length %> field<%= auditLog.changes.length > 1 ? 's' : '' %></span>
</div>
<div class="card-body p-0">
<% if (user && (user.role === 'admin' || user.role === 'superadmin')) { %>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead>
<tr>
<th style="width:160px">Field</th>
<th>Before</th>
<th>After</th>
</tr>
</thead>
<tbody>
<% auditLog.changes.forEach(change => { %>
<tr>
<td style="font-weight:500;font-size:var(--font-size-sm)"><%= change.field %></td>
<td>
<div style="background:var(--danger-soft);border-left:3px solid var(--danger-color);padding:0.4rem 0.6rem;border-radius:0 var(--border-radius-sm) var(--border-radius-sm) 0;font-size:0.8rem;word-break:break-all;">
<% if (change.before === null || change.before === undefined) { %>
<em style="color:var(--text-muted)">null</em>
<% } else if (typeof change.before === 'object') { %>
<pre style="margin:0;font-size:0.75rem;max-height:100px;overflow:auto"><%= JSON.stringify(change.before, null, 2) %></pre>
<% } else { %>
<%= change.before %>
<% } %>
</div>
</td>
<td>
<div style="background:var(--success-soft);border-left:3px solid var(--success-color);padding:0.4rem 0.6rem;border-radius:0 var(--border-radius-sm) var(--border-radius-sm) 0;font-size:0.8rem;word-break:break-all;">
<% if (change.after === null || change.after === undefined) { %>
<em style="color:var(--text-muted)">null</em>
<% } else if (typeof change.after === 'object') { %>
<pre style="margin:0;font-size:0.75rem;max-height:100px;overflow:auto"><%= JSON.stringify(change.after, null, 2) %></pre>
<% } else { %>
<%= change.after %>
<% } %>
</div>
</td>
</tr>
<% }); %>
</tbody>
</table>
</div>
<% } else { %>
<div class="p-3">
<div class="alert d-flex gap-2 align-items-center" style="background:var(--info-soft);border:1px solid rgba(14,116,144,0.15);border-radius:var(--border-radius-sm);padding:0.75rem;">
<i class="fas fa-info-circle" style="color:var(--info-color)"></i>
<span style="font-size:var(--font-size-sm)">Detailed field values are restricted to administrators.</span>
</div>
<% auditLog.changes.forEach(change => { %>
<div style="display:flex;align-items:center;gap:0.5rem;padding:0.4rem 0;border-bottom:1px solid var(--border-light);">
<span style="font-weight:500;font-size:var(--font-size-sm)"><%= change.field %></span>
<span class="badge bg-soft-info">Modified</span>
</div>
<% }); %>
</div>
<% } %>
</div>
</div>
<% } %>
</div>
.change-value.after {
background-color: #f0fff4;
border-left: 3px solid #28a745;
padding-left: 8px;
}
<!-- Sidebar summary -->
<div class="col-lg-4">
<div class="card border-0">
<div class="card-header">
<h5 class="card-header-title"><i class="fas fa-chart-bar"></i> Summary</h5>
</div>
<div class="card-body d-flex flex-column gap-2">
<div class="stat-card primary">
<div class="stat-card-value"><%= auditLog.changes ? auditLog.changes.length : 0 %></div>
<div class="stat-card-label">Fields Changed</div>
</div>
<div class="stat-card accent">
<div class="stat-card-value" style="font-size:1.25rem"><%= new Date(auditLog.createdAt).toLocaleDateString('en-GB') === new Date().toLocaleDateString('en-GB') ? 'Today' : 'Past' %></div>
<div class="stat-card-label">Timing</div>
</div>
</div>
</div>
</div>
.font-monospace {
font-family: 'Courier New', Courier, monospace;
font-size: 0.8rem;
}
.btn-link {
color: var(--primary-dark) !important;
}
.btn-link:hover {
color: var(--primary-color) !important;
}
.table th {
border-top: none;
font-weight: 600;
color: var(--primary-dark);
}
.card-header {
background-color: #f8f9fa !important;
border-bottom: 1px solid #dee2e6;
}
.badge {
font-size: 0.75em;
}
</style>
</div>