366 lines
18 KiB
PHP
366 lines
18 KiB
PHP
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>Activity Log</title>
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
|
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet">
|
|
<link rel="stylesheet" href="<?= base_url('css/app.css') ?>">
|
|
<link rel="stylesheet" href="<?= base_url('css/dashboard.css') ?>">
|
|
<link rel="stylesheet" href="<?= base_url('css/activity-log.css') ?>">
|
|
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.7/css/dataTables.bootstrap5.min.css">
|
|
<link rel="stylesheet" href="https://cdn.datatables.net/buttons/2.4.2/css/buttons.bootstrap5.min.css">
|
|
</head>
|
|
<body class="app-body overview-layout">
|
|
|
|
<aside class="ov-sidebar" id="sidebar">
|
|
<div class="ov-brand"><h1><i class="bi bi-hospital me-1"></i> DoctGuide</h1><span>Control Panel</span></div>
|
|
<nav class="ov-nav">
|
|
<div class="ov-nav__section">Main</div>
|
|
<a href="<?= base_url('admin/dashboard') ?>" class="ov-nav__link"><i class="bi bi-speedometer2"></i> Dashboard</a>
|
|
<div class="ov-nav__section">Manage</div>
|
|
<div class="ov-nav__dropdown">
|
|
<a href="#" class="ov-nav__link d-flex justify-content-between align-items-center" onclick="toggleNavDropdown(event, this)">
|
|
<span><i class="bi bi-person-badge"></i> Doctors</span>
|
|
<i class="bi bi-chevron-down dropdown-icon"></i>
|
|
</a>
|
|
<div class="ov-dropdown-menu">
|
|
<a href="<?= base_url('admin/doctors') ?>" class="ov-nav__sublink">Doctor List</a>
|
|
<a href="<?= base_url('admin/doctors/add') ?>" class="ov-nav__sublink">Add Doctor</a>
|
|
</div>
|
|
</div>
|
|
<div class="ov-nav__dropdown">
|
|
<a href="#" class="ov-nav__link d-flex justify-content-between align-items-center" onclick="toggleNavDropdown(event, this)">
|
|
<span><i class="bi bi-people"></i> Patients</span>
|
|
<i class="bi bi-chevron-down dropdown-icon"></i>
|
|
</a>
|
|
<div class="ov-dropdown-menu">
|
|
<a href="<?= base_url('admin/patients') ?>" class="ov-nav__sublink">Patient List</a>
|
|
<a href="<?= base_url('admin/patients/add') ?>" class="ov-nav__sublink">Add Patient</a>
|
|
</div>
|
|
</div>
|
|
<a href="<?= base_url('admin/appointments') ?>" class="ov-nav__link"><i class="bi bi-calendar2-check"></i> Appointments</a>
|
|
<a href="<?= base_url('admin/activity-log') ?>" class="ov-nav__link active"><i class="bi bi-clipboard-data"></i> Activity Log</a>
|
|
</nav>
|
|
<div class="ov-sidebar__footer"><a href="<?= base_url('logout') ?>"><i class="bi bi-box-arrow-left"></i> Logout</a></div>
|
|
</aside>
|
|
|
|
<div class="ov-main" id="mainContent">
|
|
<header class="ov-topbar">
|
|
<div class="d-flex align-items-center">
|
|
<button class="ov-toggle-btn" onclick="toggleSidebar()" title="Toggle Sidebar"><i class="bi bi-list" id="toggleIcon"></i></button>
|
|
<p class="ov-topbar__title mb-0">Activity Log</p>
|
|
</div>
|
|
</header>
|
|
|
|
<main class="ov-content">
|
|
<div class="row mb-4">
|
|
<div class="col-md-4">
|
|
<div class="stat-card">
|
|
<p class="stat-value"><?= $summary['total_actions'] ?? 0 ?></p>
|
|
<p class="stat-label">Total Actions</p>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<div class="stat-card orange">
|
|
<p class="stat-value"><?= $summary['active_users'] ?? 0 ?></p>
|
|
<p class="stat-label">Activity Users</p>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<div class="stat-card blue">
|
|
<p class="stat-value"><?= $summary['activity_roles'] ?? 0 ?></p>
|
|
<p class="stat-label">Active Roles</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="ov-panel mb-4">
|
|
<div class="ov-panel__header d-flex justify-content-between align-items-center">
|
|
<h2 class="ov-panel__title">Advance Filter</h2>
|
|
<div class="d-flex gap-2">
|
|
<a href="<?= base_url('admin/activity/analytics') ?>" class="btn btn-outline-primary px-3">
|
|
<i class="bi bi-graph-up me-1"></i> Analytics
|
|
</a>
|
|
</div>
|
|
</div>
|
|
<div class="ov-panel__body">
|
|
<form method="get" action="<?= base_url('admin/activity-log') ?>" class="row g-3">
|
|
<?php
|
|
$selectedActions = [];
|
|
if (! empty($filters['action'])) {
|
|
$selectedActions = is_array($filters['action']) ? array_filter($filters['action']) : [$filters['action']];
|
|
}
|
|
$selectedActions = array_values(array_filter($selectedActions));
|
|
|
|
$selectedRoles = [];
|
|
if (! empty($filters['role'])) {
|
|
$selectedRoles = is_array($filters['role']) ? array_filter($filters['role']) : [$filters['role']];
|
|
}
|
|
$selectedRoles = array_values(array_filter($selectedRoles));
|
|
?>
|
|
<div class="col-md-6">
|
|
<label class="form-label" for="actionType">Action Type</label>
|
|
<div class="multi-select-arrow-wrap">
|
|
<select name="action[]" id="actionType" class="form-select select2" multiple>
|
|
<option value="login" <?= in_array('login', $selectedActions, true) ? 'selected' : '' ?>>Login</option>
|
|
<option value="logout" <?= in_array('logout', $selectedActions, true) ? 'selected' : '' ?>>Logout</option>
|
|
<?php if (! empty($availableActions)): ?>
|
|
<?php foreach ($availableActions as $actionOption): ?>
|
|
<?php if ($actionOption !== 'login' && $actionOption !== 'logout'): ?>
|
|
<option value="<?= esc($actionOption) ?>" <?= in_array($actionOption, $selectedActions, true) ? 'selected' : '' ?>><?= esc(ucfirst($actionOption)) ?></option>
|
|
<?php endif; ?>
|
|
<?php endforeach; ?>
|
|
<?php endif; ?>
|
|
</select>
|
|
<i class="bi bi-chevron-down select2-arrow-hint" aria-hidden="true"></i>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label" for="roleType">Role Type</label>
|
|
<div class="multi-select-arrow-wrap">
|
|
<select name="role[]" id="roleType" class="form-select select2" multiple>
|
|
<option value="admin" <?= in_array('admin', $selectedRoles, true) ? 'selected' : '' ?>>Admin</option>
|
|
<option value="doctor" <?= in_array('doctor', $selectedRoles, true) ? 'selected' : '' ?>>Doctor</option>
|
|
<option value="patient" <?= in_array('patient', $selectedRoles, true) ? 'selected' : '' ?>>Patient</option>
|
|
</select>
|
|
<i class="bi bi-chevron-down select2-arrow-hint" aria-hidden="true"></i>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label" for="date_from">From</label>
|
|
<input type="date" class="form-control" id="date_from" name="date_from" value="<?= esc($filters['date_from'] ?? '') ?>">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label" for="date_to">To</label>
|
|
<input type="date" class="form-control" id="date_to" name="date_to" value="<?= esc($filters['date_to'] ?? '') ?>">
|
|
</div>
|
|
<div class="col-12">
|
|
<div class="d-flex gap-2">
|
|
<!-- <button type="submit" class="btn btn-outline-success">
|
|
Filter
|
|
</button> -->
|
|
<a href="<?= base_url('admin/activity-log') ?>" class="btn btn-outline-secondary">
|
|
Clear Filters
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="ov-panel">
|
|
<div class="ov-panel__header">
|
|
<h2 class="ov-panel__title">Activity Log Entries</h2>
|
|
<div class="d-flex flex-wrap gap-2 align-items-center">
|
|
<div class="dropdown">
|
|
<button class="btn btn-outline-secondary btn-sm dropdown-toggle" type="button" data-bs-toggle="dropdown">
|
|
<i class="bi bi-three-dots-vertical"></i>
|
|
</button>
|
|
<ul class="dropdown-menu dropdown-menu-end">
|
|
<li><a class="dropdown-item" href="#" onclick="exportTable('csv'); return false;"><i class="bi bi-file-earmark-text me-2"></i> CSV</a></li>
|
|
<li><a class="dropdown-item" href="#" onclick="exportTable('excel'); return false;"><i class="bi bi-file-earmark-excel me-2"></i> Excel</a></li>
|
|
<li><a class="dropdown-item" href="#" onclick="exportTable('pdf'); return false;"><i class="bi bi-file-earmark-pdf me-2"></i> PDF</a></li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="p-0">
|
|
<table id="activityTable" class="table table-hover" style="width:100%">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th class="text-nowrap">Timestamp</th>
|
|
<th>User</th>
|
|
<th>Role</th>
|
|
<th>Action</th>
|
|
<th>Description</th>
|
|
<th>IP</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php if (! empty($logs)): ?>
|
|
<?php foreach ($logs as $log): ?>
|
|
<tr>
|
|
<td class="ps-3 text-nowrap"><?= esc($log['created_at']) ?></td>
|
|
<td>
|
|
<div class="fw-medium"><?= esc(trim((string) ($log['actor_name'] ?? ''))) !== '' ? esc(trim((string) $log['actor_name'])) : 'System' ?></div>
|
|
<div class="text-muted small"><?= esc($log['actor_email'] ?? '') ?></div>
|
|
</td>
|
|
<td><?= esc($log['actor_role'] ?? '-') ?></td>
|
|
<td><span class="badge bg-secondary"><?= esc($log['action']) ?></span></td>
|
|
<td><?= esc($log['description']) ?></td>
|
|
<td class="text-nowrap"><?= esc($log['ip_address'] ?? '-') ?></td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
<?php endif; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
|
|
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
|
|
<script src="https://cdn.datatables.net/1.13.7/js/jquery.dataTables.min.js"></script>
|
|
<script src="https://cdn.datatables.net/1.13.7/js/dataTables.bootstrap5.min.js"></script>
|
|
<script src="https://cdn.datatables.net/buttons/2.4.2/js/dataTables.buttons.min.js"></script>
|
|
<script src="https://cdn.datatables.net/buttons/2.4.2/js/buttons.bootstrap5.min.js"></script>
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.1.53/pdfmake.min.js"></script>
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.1.53/vfs_fonts.js"></script>
|
|
<script src="https://cdn.datatables.net/buttons/2.4.2/js/buttons.html5.min.js"></script>
|
|
<script src="https://cdn.datatables.net/buttons/2.4.2/js/buttons.print.min.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
|
<script>
|
|
let activityTable = null;
|
|
|
|
$(document).ready(function() {
|
|
activityTable = $('#activityTable').DataTable({
|
|
pageLength: 25,
|
|
lengthMenu: [[10, 25, 50, -1], [10, 25, 50, "All"]],
|
|
order: [[0, 'desc']],
|
|
dom: '<"row px-4 py-3"<"col-md-6 d-flex align-items-center"l><"col-md-6 d-flex justify-content-end align-items-center"f>>' +
|
|
'rt' +
|
|
'<"row px-4 py-3"<"col-md-5 d-flex align-items-center"i><"col-md-7 d-flex justify-content-end"p>>',
|
|
buttons: [
|
|
{
|
|
extend: 'csvHtml5',
|
|
className: 'buttons-csv',
|
|
title: 'Activity Log',
|
|
filename: 'activity_log_' + new Date().toISOString().split('T')[0],
|
|
exportOptions: { columns: [0, 1, 2, 3, 4, 6] }
|
|
},
|
|
{
|
|
extend: 'excelHtml5',
|
|
className: 'buttons-excel',
|
|
title: 'Activity Log',
|
|
filename: 'activity_log_' + new Date().toISOString().split('T')[0],
|
|
exportOptions: { columns: [0, 1, 2, 3, 4, 5, 6] }
|
|
},
|
|
{
|
|
extend: 'pdfHtml5',
|
|
className: 'buttons-pdf',
|
|
title: 'Activity Log',
|
|
filename: 'activity_log_' + new Date().toISOString().split('T')[0],
|
|
orientation: 'landscape',
|
|
pageSize: 'A4',
|
|
exportOptions: { columns: [0, 1, 2, 3, 4, 6] }
|
|
}
|
|
],
|
|
language: {
|
|
search: 'Search logs:',
|
|
lengthMenu: 'Show _MENU_ logs',
|
|
info: 'Showing _START_ to _END_ of _TOTAL_ logs',
|
|
paginate: {
|
|
first: 'First',
|
|
last: 'Last',
|
|
next: 'Next',
|
|
previous: 'Previous'
|
|
},
|
|
emptyTable: 'No logs found',
|
|
zeroRecords: 'No matching logs found'
|
|
}
|
|
});
|
|
});
|
|
|
|
function exportTable(format) {
|
|
if (!activityTable) return;
|
|
if (format === 'csv') {
|
|
activityTable.button('.buttons-csv').trigger();
|
|
} else if (format === 'excel') {
|
|
activityTable.button('.buttons-excel').trigger();
|
|
} else if (format === 'pdf') {
|
|
activityTable.button('.buttons-pdf').trigger();
|
|
}
|
|
}
|
|
|
|
$(function () {
|
|
$('.select2').select2({
|
|
placeholder: 'Select options',
|
|
closeOnSelect: false,
|
|
width: '100%',
|
|
allowClear: false,
|
|
dropdownParent: $('body')
|
|
});
|
|
|
|
$('.select2').on('select2:open', function () {
|
|
const $wrapper = $(this).closest('.multi-select-arrow-wrap');
|
|
$wrapper.find('.select2-arrow-hint').removeClass('bi-chevron-down').addClass('bi-chevron-up');
|
|
});
|
|
|
|
$('.select2').on('select2:close', function () {
|
|
const $wrapper = $(this).closest('.multi-select-arrow-wrap');
|
|
$wrapper.find('.select2-arrow-hint').removeClass('bi-chevron-up').addClass('bi-chevron-down');
|
|
});
|
|
|
|
$('.select2-arrow-hint').on('mousedown', function (event) {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
|
|
const $select = $(this).siblings('select');
|
|
if ($select.data('select2').isOpen()) {
|
|
$select.select2('close');
|
|
} else {
|
|
$select.select2('open');
|
|
}
|
|
});
|
|
let filterTimeout; //debouncer
|
|
|
|
$('#actionType, #roleType').on('change', function () {
|
|
clearTimeout(filterTimeout);
|
|
filterTimeout = setTimeout(() => {
|
|
applyFilters();
|
|
}, 300);
|
|
});
|
|
|
|
$('input[type="date"]').on('change', function () {
|
|
clearTimeout(filterTimeout);
|
|
filterTimeout = setTimeout(() => {
|
|
applyFilters();
|
|
}, 300);
|
|
});
|
|
function applyFilters() {
|
|
let formData = $('form').serialize();
|
|
|
|
$.ajax({
|
|
url: $('form').attr('action'),
|
|
type: 'GET',
|
|
data: formData,
|
|
success: function (response) {
|
|
// Extract table body only
|
|
let newTbody = $(response).find('#activityTable tbody').html();
|
|
|
|
$('#activityTable tbody').html(newTbody);
|
|
activityTable.destroy();
|
|
|
|
$('#activityTable tbody').html(newTbody);
|
|
|
|
activityTable = $('#activityTable').DataTable({
|
|
pageLength: 25,
|
|
order: [[0, 'desc']]
|
|
});
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
function toggleSidebar() {
|
|
const sidebar = document.getElementById('sidebar');
|
|
const main = document.getElementById('mainContent');
|
|
const icon = document.getElementById('toggleIcon');
|
|
sidebar.classList.toggle('collapsed');
|
|
main.classList.toggle('expanded');
|
|
icon.className = sidebar.classList.contains('collapsed') ? 'bi bi-layout-sidebar' : 'bi bi-list';
|
|
}
|
|
|
|
function toggleNavDropdown(event, element) {
|
|
event.preventDefault();
|
|
element.parentElement.classList.toggle('active');
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|