recreate project
This commit is contained in:
@@ -0,0 +1,272 @@
|
||||
<?= $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() ?>
|
||||
Reference in New Issue
Block a user