273 lines
10 KiB
PHP
273 lines
10 KiB
PHP
<?= $this->extend('layout/management_layout') ?>
|
|
<?= $this->section('content') ?>
|
|
|
|
<?php
|
|
helper('url');
|
|
$endpoint = $loginActivityEndpoint ?? site_url('api/login-activity');
|
|
$defaultPerPage = isset($defaultPerPage) && (int)$defaultPerPage > 0 ? (int)$defaultPerPage : 25;
|
|
?>
|
|
|
|
<div class="container-fluid"
|
|
data-login-activity-endpoint="<?= esc($endpoint) ?>"
|
|
data-default-per-page="<?= esc($defaultPerPage) ?>">
|
|
<div class="wrapper">
|
|
<div class="content"></div>
|
|
<h2 class="text-center mt-4 mb-3">Login Activity</h2>
|
|
<?= $this->include('partials/academic_filter') ?>
|
|
|
|
<div id="loginActivityAlert" class="alert alert-danger d-none" role="alert"></div>
|
|
|
|
<div class="d-flex justify-content-between align-items-center flex-wrap gap-3 mb-3">
|
|
<div>
|
|
<label for="perPageSelect" class="form-label mb-0 me-2">Rows per page:</label>
|
|
<select class="form-select d-inline-block w-auto" id="perPageSelect">
|
|
<option value="10">10</option>
|
|
<option value="25">25</option>
|
|
<option value="50">50</option>
|
|
<option value="100">100</option>
|
|
</select>
|
|
</div>
|
|
<div class="d-flex align-items-center gap-2">
|
|
<button type="button" class="btn btn-outline-secondary" id="prevPageBtn">Previous</button>
|
|
<span id="paginationInfo" class="text-muted small">Page 1 of 1</span>
|
|
<button type="button" class="btn btn-outline-secondary" id="nextPageBtn">Next</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="table-responsive">
|
|
<table id="loginActivityTable" class="table table-striped table-bordered align-middle mb-0">
|
|
<thead class="table-dark">
|
|
<tr>
|
|
<th>User ID</th>
|
|
<th>Email</th>
|
|
<th>Login Time</th>
|
|
<th>Logout Time</th>
|
|
<th>IP Address</th>
|
|
<th>User Agent</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="loginActivityBody">
|
|
<tr>
|
|
<td colspan="6" class="text-center text-muted">Loading login activity…</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<?= $this->endSection() ?>
|
|
|
|
<?= $this->section('scripts') ?>
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
const container = document.querySelector('[data-login-activity-endpoint]');
|
|
if (!container) {
|
|
return;
|
|
}
|
|
|
|
const endpoint = container.dataset.loginActivityEndpoint;
|
|
const alertBox = document.getElementById('loginActivityAlert');
|
|
const tableBody = document.getElementById('loginActivityBody');
|
|
const perPageSelect = document.getElementById('perPageSelect');
|
|
const paginationInfo = document.getElementById('paginationInfo');
|
|
const prevBtn = document.getElementById('prevPageBtn');
|
|
const nextBtn = document.getElementById('nextPageBtn');
|
|
const tableSelector = '#loginActivityTable';
|
|
|
|
let currentPage = 1;
|
|
let perPage = parseInt(container.dataset.defaultPerPage, 10) || 25;
|
|
let pageCount = 1;
|
|
|
|
const setPerPageSelect = () => {
|
|
const options = Array.from(perPageSelect.options);
|
|
const found = options.some((opt) => parseInt(opt.value, 10) === perPage);
|
|
if (!found) {
|
|
const option = document.createElement('option');
|
|
option.value = String(perPage);
|
|
option.textContent = perPage;
|
|
perPageSelect.appendChild(option);
|
|
}
|
|
perPageSelect.value = String(perPage);
|
|
};
|
|
|
|
setPerPageSelect();
|
|
|
|
const showError = (message) => {
|
|
if (!alertBox) {
|
|
return;
|
|
}
|
|
alertBox.textContent = message;
|
|
alertBox.classList.remove('d-none');
|
|
};
|
|
|
|
const clearError = () => {
|
|
if (!alertBox) {
|
|
return;
|
|
}
|
|
alertBox.textContent = '';
|
|
alertBox.classList.add('d-none');
|
|
};
|
|
|
|
const destroyDataTable = () => {
|
|
if (!window.jQuery || !window.jQuery.fn || !window.jQuery.fn.DataTable) {
|
|
return;
|
|
}
|
|
if (window.jQuery.fn.DataTable.isDataTable(tableSelector)) {
|
|
window.jQuery(tableSelector).DataTable().clear().destroy();
|
|
}
|
|
};
|
|
|
|
const initDataTable = () => {
|
|
if (!window.jQuery || !window.jQuery.fn || !window.jQuery.fn.DataTable) {
|
|
return;
|
|
}
|
|
window.jQuery(tableSelector).DataTable({
|
|
paging: false,
|
|
searching: false,
|
|
info: false,
|
|
order: [[2, 'desc']],
|
|
});
|
|
};
|
|
|
|
const formatDateTime = (value) => {
|
|
if (!value) {
|
|
return '—';
|
|
}
|
|
const date = new Date(value.replace(' ', 'T'));
|
|
if (Number.isNaN(date.valueOf())) {
|
|
return value;
|
|
}
|
|
const pad = (num) => String(num).padStart(2, '0');
|
|
const datePart = `${pad(date.getMonth() + 1)}-${pad(date.getDate())}-${date.getFullYear()}`;
|
|
const hasTime = /[T ]\d{2}:\d{2}/.test(String(value));
|
|
if (!hasTime) {
|
|
return datePart;
|
|
}
|
|
const timePart = `${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
|
return `${datePart} ${timePart}`;
|
|
};
|
|
|
|
const renderTable = (activities) => {
|
|
destroyDataTable();
|
|
tableBody.innerHTML = '';
|
|
|
|
if (!Array.isArray(activities) || activities.length === 0) {
|
|
const row = document.createElement('tr');
|
|
const cell = document.createElement('td');
|
|
cell.colSpan = 6;
|
|
cell.classList.add('text-center', 'text-muted');
|
|
cell.textContent = 'No login activities found.';
|
|
row.appendChild(cell);
|
|
tableBody.appendChild(row);
|
|
initDataTable();
|
|
return;
|
|
}
|
|
|
|
activities.forEach((activity) => {
|
|
const row = document.createElement('tr');
|
|
const cells = [
|
|
activity.user_id ?? '—',
|
|
activity.email ?? '—',
|
|
formatDateTime(activity.login_time ?? null),
|
|
formatDateTime(activity.logout_time ?? null),
|
|
activity.ip_address ?? '—',
|
|
activity.user_agent ?? '—',
|
|
];
|
|
|
|
cells.forEach((text) => {
|
|
const cell = document.createElement('td');
|
|
cell.textContent = text;
|
|
row.appendChild(cell);
|
|
});
|
|
|
|
tableBody.appendChild(row);
|
|
});
|
|
|
|
initDataTable();
|
|
};
|
|
|
|
const updatePaginationControls = () => {
|
|
paginationInfo.textContent = `Page ${currentPage} of ${pageCount}`;
|
|
prevBtn.disabled = currentPage <= 1;
|
|
nextBtn.disabled = currentPage >= pageCount;
|
|
};
|
|
|
|
const buildUrl = () => {
|
|
try {
|
|
const url = new URL(endpoint, window.location.origin);
|
|
url.searchParams.set('page', currentPage);
|
|
url.searchParams.set('per_page', perPage);
|
|
return url.toString();
|
|
} catch (error) {
|
|
const separator = endpoint.includes('?') ? '&' : '?';
|
|
return `${endpoint}${separator}page=${encodeURIComponent(currentPage)}&per_page=${encodeURIComponent(perPage)}`;
|
|
}
|
|
};
|
|
|
|
const fetchActivities = () => {
|
|
clearError();
|
|
const url = buildUrl();
|
|
|
|
return fetch(url, {
|
|
headers: {
|
|
'Accept': 'application/json',
|
|
},
|
|
credentials: 'same-origin',
|
|
})
|
|
.then((response) => {
|
|
if (!response.ok) {
|
|
throw new Error(`Request failed with status ${response.status}`);
|
|
}
|
|
return response.json();
|
|
})
|
|
.then((payload) => {
|
|
const activities = Array.isArray(payload?.activities) ? payload.activities : [];
|
|
const pagination = payload?.pagination ?? {};
|
|
|
|
currentPage = Number.isFinite(pagination.currentPage) ? pagination.currentPage : currentPage;
|
|
pageCount = Number.isFinite(pagination.pageCount) ? pagination.pageCount : pageCount;
|
|
|
|
renderTable(activities);
|
|
updatePaginationControls();
|
|
})
|
|
.catch((error) => {
|
|
showError(error.message || 'Unable to load login activity.');
|
|
destroyDataTable();
|
|
tableBody.innerHTML = '';
|
|
const row = document.createElement('tr');
|
|
const cell = document.createElement('td');
|
|
cell.colSpan = 6;
|
|
cell.classList.add('text-center', 'text-danger');
|
|
cell.textContent = 'Failed to load login activity.';
|
|
row.appendChild(cell);
|
|
tableBody.appendChild(row);
|
|
updatePaginationControls();
|
|
});
|
|
};
|
|
|
|
perPageSelect.addEventListener('change', () => {
|
|
perPage = parseInt(perPageSelect.value, 10) || 25;
|
|
currentPage = 1;
|
|
fetchActivities();
|
|
});
|
|
|
|
prevBtn.addEventListener('click', () => {
|
|
if (currentPage > 1) {
|
|
currentPage -= 1;
|
|
fetchActivities();
|
|
}
|
|
});
|
|
|
|
nextBtn.addEventListener('click', () => {
|
|
if (currentPage < pageCount) {
|
|
currentPage += 1;
|
|
fetchActivities();
|
|
}
|
|
});
|
|
|
|
fetchActivities();
|
|
});
|
|
</script>
|
|
<?= $this->endSection() ?>
|