Files
uldp-degree-mangement-system/views/admin/audit-log/index.ejs
2026-02-10 16:42:35 +07:00

699 lines
31 KiB
Plaintext

<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>
</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>
</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>
</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>
</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>
<!-- 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>
<% } %>
<% } 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>
<% } %>
</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>
</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;
}
.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>