Journaling improvements
This commit is contained in:
@@ -31,6 +31,7 @@
|
||||
<div class="dropdown-menu" id="adminDropdownMenu">
|
||||
<a href="#" onclick="window.location.href = '/atc'">🎛️ ATC View</a>
|
||||
<a href="#" onclick="window.location.href = '/reports'">📊 Reports</a>
|
||||
<a href="#" onclick="window.location.href = '/journal'">📔 Journal Log</a>
|
||||
<a href="#" onclick="openUserAircraftModal(); closeAdminDropdown()" id="user-aircraft-dropdown">✈️ User Aircraft</a>
|
||||
<a href="#" onclick="openUserManagementModal(); closeAdminDropdown()" id="user-management-dropdown" style="display: none;">👥 User Management</a>
|
||||
</div>
|
||||
|
||||
@@ -233,6 +233,7 @@
|
||||
<div class="dropdown-menu" id="adminDropdownMenu">
|
||||
<a href="#" onclick="window.location.href = '/admin'">🏠 Admin View</a>
|
||||
<a href="#" onclick="window.location.href = '/reports'">📊 Reports</a>
|
||||
<a href="#" onclick="window.location.href = '/journal'">📔 Journal Log</a>
|
||||
<a href="#" onclick="openUserAircraftModal(); closeAdminDropdown()" id="user-aircraft-dropdown">✈️ User Aircraft</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,752 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Journal - PPR System</title>
|
||||
<link rel="stylesheet" href="admin.css">
|
||||
<script src="config.js"></script>
|
||||
<script src="lookups.js"></script>
|
||||
<style>
|
||||
.journal-filters {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
padding: 1rem;
|
||||
background: #f5f5f5;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.journal-filters label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.journal-filters input,
|
||||
.journal-filters select {
|
||||
margin-top: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 3px;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.filter-buttons {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
grid-column: 1 / -1;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.filter-buttons button {
|
||||
padding: 0.5rem 1rem;
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
.btn-apply {
|
||||
background: #4CAF50;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-apply:hover {
|
||||
background: #45a049;
|
||||
}
|
||||
|
||||
.btn-reset {
|
||||
background: #757575;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-reset:hover {
|
||||
background: #616161;
|
||||
}
|
||||
|
||||
.journal-table {
|
||||
background: white;
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.journal-table table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.journal-table thead {
|
||||
background: #333;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.journal-table th {
|
||||
padding: 0.75rem;
|
||||
text-align: left;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.journal-table td {
|
||||
padding: 0.75rem;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.journal-table tbody tr:hover {
|
||||
background: #f9f9f9;
|
||||
}
|
||||
|
||||
.entity-type-badge {
|
||||
display: inline-block;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 3px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.entity-badge-ppr { background: #2196F3; }
|
||||
.entity-badge-local_flight { background: #FF9800; }
|
||||
.entity-badge-arrival { background: #4CAF50; }
|
||||
.entity-badge-departure { background: #9C27B0; }
|
||||
.entity-badge-overflight { background: #00BCD4; }
|
||||
.entity-badge-circuit { background: #FFC107; }
|
||||
.entity-badge-user { background: #795548; }
|
||||
|
||||
.entry-text {
|
||||
color: #555;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.entry-datetime {
|
||||
color: #888;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.entry-user {
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.entry-ip {
|
||||
color: #999;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.stats-row {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
flex: 1;
|
||||
min-width: 150px;
|
||||
padding: 1rem;
|
||||
background: white;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-card .stat-value {
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
color: #2196F3;
|
||||
}
|
||||
|
||||
.stat-card .stat-label {
|
||||
color: #888;
|
||||
font-size: 0.9rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
margin-top: 1rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.pagination button {
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: 1px solid #ddd;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
border-radius: 3px;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
.pagination button:hover {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.pagination button.active {
|
||||
background: #2196F3;
|
||||
color: white;
|
||||
border-color: #2196F3;
|
||||
}
|
||||
|
||||
.no-data {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
border: 4px solid #f3f3f3;
|
||||
border-top: 4px solid #2196F3;
|
||||
border-radius: 50%;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto 1rem;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.export-section {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.btn-export {
|
||||
background: #2196F3;
|
||||
color: white;
|
||||
padding: 0.5rem 1rem;
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.btn-export:hover {
|
||||
background: #1976D2;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="top-bar">
|
||||
<div class="title">
|
||||
<h1>📔 Journal Log</h1>
|
||||
</div>
|
||||
<div class="menu-buttons">
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-warning dropdown-toggle" id="adminDropdownBtn">
|
||||
⚙️ Menu
|
||||
</button>
|
||||
<div class="dropdown-menu" id="adminDropdownMenu">
|
||||
<a href="#" onclick="window.location.href = '/admin'">📋 Admin</a>
|
||||
<a href="#" onclick="window.location.href = '/atc'">🎛️ ATC View</a>
|
||||
<a href="#" onclick="window.location.href = '/reports'">📊 Reports</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="user-info">
|
||||
Logged in as: <span id="current-user">Loading...</span> |
|
||||
<a href="#" onclick="logout()" style="color: white;">Logout</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<!-- Filters -->
|
||||
<div class="journal-filters">
|
||||
<label>
|
||||
Date From:
|
||||
<input type="date" id="dateFrom">
|
||||
</label>
|
||||
<label>
|
||||
Date To:
|
||||
<input type="date" id="dateTo">
|
||||
</label>
|
||||
<label>
|
||||
Entity Type:
|
||||
<select id="entityType">
|
||||
<option value="">All Types</option>
|
||||
<option value="PPR">PPR</option>
|
||||
<option value="LOCAL_FLIGHT">Local Flight</option>
|
||||
<option value="ARRIVAL">Arrival</option>
|
||||
<option value="DEPARTURE">Departure</option>
|
||||
<option value="OVERFLIGHT">Overflight</option>
|
||||
<option value="CIRCUIT">Circuit</option>
|
||||
<option value="USER">User</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
User:
|
||||
<input type="text" id="filterUser" placeholder="e.g., john_doe">
|
||||
</label>
|
||||
<label>
|
||||
Entity ID:
|
||||
<input type="number" id="entityId" placeholder="Optional">
|
||||
</label>
|
||||
<label>
|
||||
Search Text:
|
||||
<input type="text" id="searchText" placeholder="Search in entries...">
|
||||
</label>
|
||||
<div class="filter-buttons">
|
||||
<button class="btn-apply" onclick="applyFilters()">🔍 Search</button>
|
||||
<button class="btn-reset" onclick="resetFilters()">↻ Reset</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Statistics -->
|
||||
<div class="stats-row">
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" id="totalEntries">0</div>
|
||||
<div class="stat-label">Total Entries</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" id="uniqueUsers">0</div>
|
||||
<div class="stat-label">Unique Users</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" id="dateRange">-</div>
|
||||
<div class="stat-label">Date Range</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Export -->
|
||||
<div class="export-section">
|
||||
<button class="btn-export" onclick="exportToCSV()">📥 Export as CSV</button>
|
||||
<button class="btn-export" onclick="exportToJSON()">📥 Export as JSON</button>
|
||||
</div>
|
||||
|
||||
<!-- Journal Table -->
|
||||
<div class="journal-table">
|
||||
<div id="journal-loading" class="loading" style="display: none;">
|
||||
<div class="spinner"></div>
|
||||
Loading journal entries...
|
||||
</div>
|
||||
|
||||
<div id="journal-content" style="display: none;">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date/Time</th>
|
||||
<th>Entity Type</th>
|
||||
<th>Entity ID</th>
|
||||
<th>User</th>
|
||||
<th>Entry</th>
|
||||
<th>IP Address</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="journal-table-body">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div id="journal-no-data" class="no-data" style="display: none;">
|
||||
<h3>No Journal Entries Found</h3>
|
||||
<p>Try adjusting your filters</p>
|
||||
</div>
|
||||
|
||||
<div id="journal-error" class="no-data" style="display: none; color: #d32f2f;">
|
||||
<h3>⚠️ Error Loading Journal</h3>
|
||||
<p id="error-message"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div id="pagination" class="pagination" style="display: none;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const API_BASE = window.PPR_CONFIG.apiBase;
|
||||
let allEntries = [];
|
||||
let filteredEntries = [];
|
||||
let currentPage = 1;
|
||||
const entriesPerPage = 50;
|
||||
let accessToken = null;
|
||||
let currentUser = null;
|
||||
|
||||
// Initialize
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initializeAuth();
|
||||
});
|
||||
|
||||
async function initializeAuth() {
|
||||
// Try to get cached token
|
||||
const cachedToken = localStorage.getItem('ppr_access_token');
|
||||
const cachedUser = localStorage.getItem('ppr_username');
|
||||
const tokenExpiry = localStorage.getItem('ppr_token_expiry');
|
||||
|
||||
if (cachedToken && cachedUser && tokenExpiry) {
|
||||
const now = new Date().getTime();
|
||||
if (now < parseInt(tokenExpiry)) {
|
||||
// Token is still valid
|
||||
accessToken = cachedToken;
|
||||
currentUser = cachedUser;
|
||||
document.getElementById('current-user').textContent = cachedUser;
|
||||
setDefaultDates();
|
||||
loadJournalEntries();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// No valid cached token, show error or redirect to login
|
||||
showError('Session expired or not authenticated. Please log in through the admin page.');
|
||||
}
|
||||
|
||||
function setDefaultDates() {
|
||||
const today = new Date();
|
||||
const thirtyDaysAgo = new Date(today);
|
||||
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
||||
|
||||
document.getElementById('dateFrom').valueAsDate = thirtyDaysAgo;
|
||||
document.getElementById('dateTo').valueAsDate = today;
|
||||
}
|
||||
|
||||
async function loadJournalEntries() {
|
||||
showLoading(true);
|
||||
|
||||
try {
|
||||
if (!accessToken) {
|
||||
showError('No authentication token available.');
|
||||
showLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Use the new search endpoint with default date range
|
||||
const dateFrom = document.getElementById('dateFrom').value;
|
||||
const dateTo = document.getElementById('dateTo').value;
|
||||
|
||||
const params = new URLSearchParams({
|
||||
date_from: dateFrom,
|
||||
date_to: dateTo,
|
||||
limit: 500
|
||||
});
|
||||
|
||||
const response = await fetch(`${API_BASE}/journal/search/all?${params}`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${accessToken}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.status === 401) {
|
||||
showError('Session expired. Please log in again through the admin page.');
|
||||
showLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load journal entries: ${response.statusText}`);
|
||||
}
|
||||
|
||||
allEntries = await response.json();
|
||||
|
||||
showLoading(false);
|
||||
updateStats();
|
||||
applyFilters();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading journal:', error);
|
||||
showError(error.message);
|
||||
showLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
function applyFilters() {
|
||||
showLoading(true);
|
||||
|
||||
if (!accessToken) {
|
||||
showError('No authentication token available. Please log in again.');
|
||||
showLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const dateFrom = document.getElementById('dateFrom').value;
|
||||
const dateTo = document.getElementById('dateTo').value;
|
||||
const entityType = document.getElementById('entityType').value;
|
||||
const filterUser = document.getElementById('filterUser').value;
|
||||
const entityId = document.getElementById('entityId').value;
|
||||
const searchText = document.getElementById('searchText').value.toLowerCase();
|
||||
|
||||
// Build API request with filters
|
||||
const params = new URLSearchParams();
|
||||
if (dateFrom) params.append('date_from', dateFrom);
|
||||
if (dateTo) params.append('date_to', dateTo);
|
||||
if (entityType) params.append('entity_type', entityType);
|
||||
if (entityId) params.append('entity_id', entityId);
|
||||
if (filterUser) params.append('user', filterUser);
|
||||
params.append('limit', 500);
|
||||
|
||||
fetch(`${API_BASE}/journal/search/all?${params}`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${accessToken}`
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
if (response.status === 401) {
|
||||
throw new Error('Session expired. Please log in again through the admin page.');
|
||||
}
|
||||
if (!response.ok) throw new Error(`Search failed: ${response.statusText}`);
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
filteredEntries = data;
|
||||
|
||||
// Apply client-side text search if any
|
||||
if (searchText) {
|
||||
filteredEntries = filteredEntries.filter(entry =>
|
||||
entry.entry.toLowerCase().includes(searchText)
|
||||
);
|
||||
}
|
||||
|
||||
currentPage = 1;
|
||||
displayEntries();
|
||||
updatePagination();
|
||||
showLoading(false);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error searching journal:', error);
|
||||
showError(error.message);
|
||||
showLoading(false);
|
||||
});
|
||||
}
|
||||
|
||||
function displayEntries() {
|
||||
const start = (currentPage - 1) * entriesPerPage;
|
||||
const end = start + entriesPerPage;
|
||||
const pageEntries = filteredEntries.slice(start, end);
|
||||
|
||||
if (pageEntries.length === 0) {
|
||||
document.getElementById('journal-table-body').innerHTML = '';
|
||||
document.getElementById('journal-content').style.display = 'none';
|
||||
document.getElementById('journal-no-data').style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
const tableBody = document.getElementById('journal-table-body');
|
||||
tableBody.innerHTML = pageEntries.map(entry => `
|
||||
<tr>
|
||||
<td class="entry-datetime">${formatDateTime(entry.entry_dt)}</td>
|
||||
<td><span class="entity-type-badge entity-badge-${entry.entity_type.toLowerCase()}">${entry.entity_type}</span></td>
|
||||
<td>${entry.entity_id}</td>
|
||||
<td class="entry-user">${entry.user}</td>
|
||||
<td class="entry-text">${escapeHtml(entry.entry)}</td>
|
||||
<td class="entry-ip">${entry.ip || '-'}</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
|
||||
document.getElementById('journal-content').style.display = 'block';
|
||||
document.getElementById('journal-no-data').style.display = 'none';
|
||||
document.getElementById('journal-error').style.display = 'none';
|
||||
}
|
||||
|
||||
function updatePagination() {
|
||||
const totalPages = Math.ceil(filteredEntries.length / entriesPerPage);
|
||||
|
||||
if (totalPages <= 1) {
|
||||
document.getElementById('pagination').style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
const paginationDiv = document.getElementById('pagination');
|
||||
paginationDiv.innerHTML = '';
|
||||
|
||||
if (currentPage > 1) {
|
||||
const prevBtn = document.createElement('button');
|
||||
prevBtn.textContent = '← Previous';
|
||||
prevBtn.onclick = () => {
|
||||
currentPage--;
|
||||
displayEntries();
|
||||
updatePagination();
|
||||
};
|
||||
paginationDiv.appendChild(prevBtn);
|
||||
}
|
||||
|
||||
// Show page numbers
|
||||
const startPage = Math.max(1, currentPage - 2);
|
||||
const endPage = Math.min(totalPages, currentPage + 2);
|
||||
|
||||
if (startPage > 1) {
|
||||
const firstBtn = document.createElement('button');
|
||||
firstBtn.textContent = '1';
|
||||
firstBtn.onclick = () => {
|
||||
currentPage = 1;
|
||||
displayEntries();
|
||||
updatePagination();
|
||||
};
|
||||
paginationDiv.appendChild(firstBtn);
|
||||
|
||||
if (startPage > 2) {
|
||||
const dots = document.createElement('span');
|
||||
dots.textContent = '...';
|
||||
dots.style.padding = '0.5rem 0.5rem';
|
||||
paginationDiv.appendChild(dots);
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = startPage; i <= endPage; i++) {
|
||||
const btn = document.createElement('button');
|
||||
btn.textContent = i;
|
||||
if (i === currentPage) btn.className = 'active';
|
||||
btn.onclick = () => {
|
||||
currentPage = i;
|
||||
displayEntries();
|
||||
updatePagination();
|
||||
};
|
||||
paginationDiv.appendChild(btn);
|
||||
}
|
||||
|
||||
if (endPage < totalPages) {
|
||||
if (endPage < totalPages - 1) {
|
||||
const dots = document.createElement('span');
|
||||
dots.textContent = '...';
|
||||
dots.style.padding = '0.5rem 0.5rem';
|
||||
paginationDiv.appendChild(dots);
|
||||
}
|
||||
|
||||
const lastBtn = document.createElement('button');
|
||||
lastBtn.textContent = totalPages;
|
||||
lastBtn.onclick = () => {
|
||||
currentPage = totalPages;
|
||||
displayEntries();
|
||||
updatePagination();
|
||||
};
|
||||
paginationDiv.appendChild(lastBtn);
|
||||
}
|
||||
|
||||
if (currentPage < totalPages) {
|
||||
const nextBtn = document.createElement('button');
|
||||
nextBtn.textContent = 'Next →';
|
||||
nextBtn.onclick = () => {
|
||||
currentPage++;
|
||||
displayEntries();
|
||||
updatePagination();
|
||||
};
|
||||
paginationDiv.appendChild(nextBtn);
|
||||
}
|
||||
|
||||
paginationDiv.style.display = 'flex';
|
||||
}
|
||||
|
||||
function updateStats() {
|
||||
document.getElementById('totalEntries').textContent = allEntries.length;
|
||||
|
||||
const uniqueUsers = new Set(allEntries.map(e => e.user)).size;
|
||||
document.getElementById('uniqueUsers').textContent = uniqueUsers;
|
||||
|
||||
if (allEntries.length > 0) {
|
||||
const dates = allEntries
|
||||
.map(e => new Date(e.entry_dt))
|
||||
.sort((a, b) => a - b);
|
||||
const earliest = formatDate(dates[0]);
|
||||
const latest = formatDate(dates[dates.length - 1]);
|
||||
document.getElementById('dateRange').textContent = `${earliest} to ${latest}`;
|
||||
}
|
||||
}
|
||||
|
||||
function resetFilters() {
|
||||
setDefaultDates();
|
||||
document.getElementById('entityType').value = '';
|
||||
document.getElementById('filterUser').value = '';
|
||||
document.getElementById('entityId').value = '';
|
||||
document.getElementById('searchText').value = '';
|
||||
applyFilters();
|
||||
}
|
||||
|
||||
function showLoading(show) {
|
||||
document.getElementById('journal-loading').style.display = show ? 'block' : 'none';
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
document.getElementById('error-message').textContent = message;
|
||||
document.getElementById('journal-error').style.display = 'block';
|
||||
document.getElementById('journal-content').style.display = 'none';
|
||||
document.getElementById('journal-no-data').style.display = 'none';
|
||||
}
|
||||
|
||||
function exportToCSV() {
|
||||
let csv = 'Date/Time,Entity Type,Entity ID,User,Entry,IP Address\n';
|
||||
|
||||
filteredEntries.forEach(entry => {
|
||||
const row = [
|
||||
formatDateTime(entry.entry_dt),
|
||||
entry.entity_type,
|
||||
entry.entity_id,
|
||||
entry.user,
|
||||
`"${entry.entry.replace(/"/g, '""')}"`,
|
||||
entry.ip || ''
|
||||
];
|
||||
csv += row.join(',') + '\n';
|
||||
});
|
||||
|
||||
downloadFile(csv, 'journal_export.csv', 'text/csv');
|
||||
}
|
||||
|
||||
function exportToJSON() {
|
||||
const json = JSON.stringify(filteredEntries, null, 2);
|
||||
downloadFile(json, 'journal_export.json', 'application/json');
|
||||
}
|
||||
|
||||
function downloadFile(content, filename, type) {
|
||||
const blob = new Blob([content], { type });
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(a);
|
||||
}
|
||||
|
||||
function formatDateTime(dateString) {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleString('en-US', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
});
|
||||
}
|
||||
|
||||
function formatDate(date) {
|
||||
return date.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: '2-digit'
|
||||
});
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
const map = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": '''
|
||||
};
|
||||
return text.replace(/[&<>"']/g, m => map[m]);
|
||||
}
|
||||
|
||||
function logout() {
|
||||
localStorage.removeItem('ppr_access_token');
|
||||
localStorage.removeItem('ppr_username');
|
||||
localStorage.removeItem('ppr_token_expiry');
|
||||
window.location.href = '/admin';
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -365,6 +365,9 @@
|
||||
<button class="btn btn-secondary" onclick="window.location.href='admin'">
|
||||
← Back to Admin
|
||||
</button>
|
||||
<button class="btn btn-secondary" onclick="window.location.href='journal'">
|
||||
📔 Journal Log
|
||||
</button>
|
||||
</div>
|
||||
<div class="title">
|
||||
<h1 id="tower-title">📊 PPR Reports</h1>
|
||||
|
||||
Reference in New Issue
Block a user