Files
RFID_detectionGUI/static/js/app.js
T

875 lines
29 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
let editingParticipantId = null;
let isAdmin = false;
// Initialize on page load
document.addEventListener('DOMContentLoaded', function() {
checkAdminStatus();
loadParticipants();
// Show today's entries by default
filterByToday();
updateLastScanResult();
// Auto-refresh logs every 5 seconds
setInterval(refreshLogs, 5000);
setInterval(loadParticipants, 5000);
setInterval(updateLastScanResult, 1000);
setInterval(checkAdminStatus, 10000);
});
// ===== AUTHENTICATION =====
async function checkAdminStatus() {
try {
const response = await fetch('/api/check-admin');
const data = await response.json();
isAdmin = data.is_admin;
updateAdminUI();
} catch (error) {
console.error('Error checking admin status:', error);
}
}
function updateAdminUI() {
const adminBtn = document.getElementById('adminToggleBtn');
const addBtn = document.querySelector('[onclick="openAddParticipantModal()"]');
const addUnregisteredBtn = document.getElementById('addUnregisteredBtn');
if (!adminBtn) return; // Exit if button doesn't exist yet
if (isAdmin) {
adminBtn.textContent = 'Odjava Admin';
adminBtn.onclick = adminLogout;
adminBtn.classList.add('active');
if (addBtn) addBtn.style.display = 'block';
} else {
adminBtn.textContent = 'Prijava Admin';
adminBtn.onclick = openLoginModal;
adminBtn.classList.remove('active');
if (addBtn) addBtn.style.display = 'none';
}
// Show unregistered button to both admin and non-admin users
if (addUnregisteredBtn) addUnregisteredBtn.style.display = 'block';
updateUIDVisibility();
}
async function adminLogin() {
const password = document.getElementById('loginPassword').value;
if (!password) {
alert('Prosimo, vnesite geslo');
return;
}
try {
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password })
});
const data = await response.json();
if (data.success) {
isAdmin = true;
closeLoginModal();
updateAdminUI();
loadParticipants();
alert('Uspešna prijava!');
} else {
alert('Napaka: ' + data.message);
}
} catch (error) {
alert('Napaka: ' + error.message);
}
}
async function adminLogout() {
try {
const response = await fetch('/api/logout', {
method: 'POST'
});
if (response.ok) {
isAdmin = false;
updateAdminUI();
loadParticipants();
alert('Odjavljen');
}
} catch (error) {
console.error('Logout error:', error);
}
}
function openLoginModal() {
document.getElementById('loginModal').classList.add('show');
document.getElementById('loginPassword').focus();
}
function closeLoginModal() {
document.getElementById('loginModal').classList.remove('show');
document.getElementById('loginPassword').value = '';
}
function handleLoginKeypress(event) {
if (event.key === 'Enter') {
adminLogin();
}
}
// ===== PARTICIPANT MANAGEMENT =====
async function loadParticipants() {
try {
const response = await fetch('/api/participants');
const participants = await response.json();
const tbody = document.getElementById('participantsTable');
tbody.innerHTML = '';
participants.forEach(p => {
const row = document.createElement('tr');
const dateAdded = p.date_added ? p.date_added.split('T')[0] : '-';
const expiration = p.expiration_date ? p.expiration_date.split('T')[0] : '-';
// Check if card is expired
let isExpired = false;
if (expiration !== '-') {
const expirationDate = new Date(expiration);
const today = new Date();
today.setHours(0, 0, 0, 0);
isExpired = expirationDate < today;
}
const keyUID = p.key_uid ? p.key_uid : '-';
const actionButtons = isAdmin ? `
<button class="action-btn edit-btn" onclick="openEditModal(${p.id}, '${p.name}', '${p.card_uid || ''}', '${p.key_uid || ''}', '${expiration}')">Uredi</button>
<button class="action-btn delete-btn" onclick="deleteParticipant(${p.id})">Izbriši</button>
` : '';
row.innerHTML = `
<td>${p.id}</td>
<td>${p.name}</td>
<td>${p.card_uid || '-'}</td>
<td>${keyUID}</td>
<td>${expiration}</td>
<td>${dateAdded}</td>
<td>${actionButtons || '-'}</td>
`;
// Add expired class if card is expired
if (isExpired) {
row.classList.add('expired');
}
tbody.appendChild(row);
});
// Update participant filter dropdown
updateParticipantFilter(participants);
updateUIDVisibility();
} catch (error) {
console.error('Error loading participants:', error);
}
}
function updateParticipantFilter(participants) {
const filterSelect = document.getElementById('participantFilter');
const currentValue = filterSelect.value;
// Clear existing options except the first one
filterSelect.innerHTML = '<option value="">Vsi udeleženci</option>';
// Sort participants by name and add to dropdown
participants
.sort((a, b) => a.name.localeCompare(b.name))
.forEach(p => {
const option = document.createElement('option');
option.value = p.name;
option.textContent = p.name;
filterSelect.appendChild(option);
});
// Restore previous selection if it still exists
filterSelect.value = currentValue;
}
async function addParticipant() {
const name = document.getElementById('inputName').value.trim();
const cardUID = document.getElementById('inputCardUID').value.trim();
const keyUID = document.getElementById('inputKeyUID').value.trim();
const expiration = document.getElementById('inputExpiration').value;
if (!name) {
alert('Prosimo, izpolnite polje za ime');
return;
}
if (!cardUID && !keyUID) {
alert('Prosimo, vnesite vsaj UID kartice ali UID ključa');
return;
}
try {
const response = await fetch('/api/participants', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: name,
card_uid: cardUID || null,
key_uid: keyUID || null,
expiration_date: expiration || null
})
});
if (!response.ok) {
const errorData = await response.json();
alert('Napaka: ' + (errorData.message || 'Unknown error'));
return;
}
const data = await response.json();
if (data.success) {
alert('Udeleženec je bil uspešno dodan');
closeAddParticipantModal();
loadParticipants();
} else {
alert('Napaka: ' + data.message);
}
} catch (error) {
alert('Napaka: ' + error.message);
}
}
function openEditModal(id, name, cardUID, keyUID, expiration) {
editingParticipantId = id;
document.getElementById('editName').value = name;
document.getElementById('editCardUID').value = cardUID;
document.getElementById('editKeyUID').value = keyUID;
document.getElementById('editExpiration').value = expiration !== '-' ? expiration : '';
document.getElementById('editModal').classList.add('show');
}
function openAddParticipantModal() {
document.getElementById('addParticipantModal').classList.add('show');
}
function closeAddParticipantModal() {
document.getElementById('addParticipantModal').classList.remove('show');
document.getElementById('inputName').value = '';
document.getElementById('inputCardUID').value = '';
document.getElementById('inputKeyUID').value = '';
document.getElementById('inputExpiration').value = '';
}
function openAddUnregisteredModal() {
document.getElementById('addUnregisteredModal').classList.add('show');
document.getElementById('inputUnregisteredName').focus();
}
function closeAddUnregisteredModal() {
document.getElementById('addUnregisteredModal').classList.remove('show');
document.getElementById('inputUnregisteredName').value = '';
}
async function addUnregisteredUser() {
const name = document.getElementById('inputUnregisteredName').value.trim();
if (!name) {
alert('Prosimo, izpolnite polje za ime');
return;
}
try {
const response = await fetch('/api/logs/unregistered', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: name
})
});
const data = await response.json();
if (data.success) {
alert('Neregistrirani udeleženec je bil uspešno dodan');
closeAddUnregisteredModal();
refreshLogs();
} else {
alert('Napaka: ' + data.message);
}
} catch (error) {
alert('Napaka: ' + error.message);
}
}
function closeModal() {
document.getElementById('editModal').classList.remove('show');
editingParticipantId = null;
}
async function saveChanges() {
if (!editingParticipantId) return;
const name = document.getElementById('editName').value.trim();
const cardUID = document.getElementById('editCardUID').value.trim();
const keyUID = document.getElementById('editKeyUID').value.trim();
const expiration = document.getElementById('editExpiration').value;
if (!name) {
alert('Prosimo, izpolnite polje za ime');
return;
}
if (!cardUID && !keyUID) {
alert('Prosimo, vnesite vsaj UID kartice ali UID ključa');
return;
}
try {
const response = await fetch(`/api/participants/${editingParticipantId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: name,
card_uid: cardUID,
key_uid: keyUID,
expiration_date: expiration || null
})
});
if (!response.ok) {
const errorData = await response.json();
alert('Napaka: ' + (errorData.message || 'Unknown error'));
return;
}
const data = await response.json();
if (data.success) {
alert('Udeleženec je bil uspešno posodobljen');
closeModal();
loadParticipants();
} else {
alert('Napaka: ' + data.message);
}
} catch (error) {
alert('Napaka: ' + error.message);
}
}
async function deleteParticipant(id) {
if (!confirm('Ali ste prepričani, da želite izbrisati tega udeleženca?')) return;
try {
const response = await fetch(`/api/participants/${id}`, {
method: 'DELETE'
});
const data = await response.json();
if (data.success) {
alert('Udeleženec je bil uspešno izbrisan');
loadParticipants();
} else {
alert('Napaka: ' + data.message);
}
} catch (error) {
alert('Napaka: ' + error.message);
}
}
// ===== LOGS MANAGEMENT =====
async function refreshLogs() {
try {
const filter = document.getElementById('filterSelect').value || 'All';
const response = await fetch(`/api/logs?filter=${filter}`);
const logs = await response.json();
// Apply client-side filtering
const filteredLogs = applyLogFilters(logs);
const tbody = document.getElementById('logsTable');
tbody.innerHTML = '';
if (filteredLogs.length === 0) {
tbody.innerHTML = `<tr><td colspan="8" style="text-align: center; padding: 20px; color: var(--text-secondary);">Ni najdenih dnevnikov, ki bi ustrezali filtrom</td></tr>`;
return;
}
// Count occurrences of each name to detect duplicates
const nameCounts = {};
filteredLogs.forEach(log => {
nameCounts[log.name] = (nameCounts[log.name] || 0) + 1;
});
// Count registered vs unregistered users
let registeredCount = 0;
let unregisteredCount = 0;
filteredLogs.forEach(log => {
const row = document.createElement('tr');
const timestamp = log.timestamp;
const date = timestamp.split('T')[0];
const time = timestamp.split('T')[1].substring(0, 8);
// Determine status class
let statusClass = 'pass';
if (log.scan_result === 'FAIL') {
statusClass = 'fail';
} else if (log.scan_result === 'MANUAL') {
statusClass = 'manual';
}
// Check if this is an unregistered duplicate
const isDuplicate = nameCounts[log.name] > 1;
const actionButtons = isAdmin ? `
<button class="action-btn delete-btn" onclick="deleteLog(${log.id})">Izbriši</button>
` : '';
row.innerHTML = `
<td>${log.id}</td>
<td>${log.name}</td>
<td>${log.card_uid || '-'}</td>
<td>${log.key_uid || '-'}</td>
<td><span class="${statusClass}">${log.scan_result}</span></td>
<td>${date}</td>
<td>${time}</td>
<td>${actionButtons}</td>
`;
// Add duplicate warning class if this is a duplicate unregistered user
if (isDuplicate && log.scan_result === 'MANUAL') {
row.classList.add('duplicate-warning');
}
// Count registered vs unregistered
if (log.scan_result === 'MANUAL') {
unregisteredCount++;
} else if (log.scan_result === 'PASS') {
registeredCount++;
}
tbody.appendChild(row);
});
// Update stats display
document.getElementById('statRegistered').textContent = registeredCount;
document.getElementById('statUnregistered').textContent = unregisteredCount;
updateUIDVisibility();
} catch (error) {
console.error('Error loading logs:', error);
}
}
function applyLogFilters(logs) {
const participantFilter = document.getElementById('participantFilter').value;
const startDateFilter = document.getElementById('startDateFilter').value;
const endDateFilter = document.getElementById('endDateFilter').value;
return logs.filter(log => {
// Filter by participant
if (participantFilter && log.name !== participantFilter) {
return false;
}
// Filter by date range
if (startDateFilter || endDateFilter) {
const logDate = log.timestamp.split('T')[0];
if (startDateFilter && logDate < startDateFilter) {
return false;
}
if (endDateFilter && logDate > endDateFilter) {
return false;
}
}
return true;
});
}
async function deleteLog(logId) {
if (!confirm('Ali ste prepričani, da želite izbrisati ta dnevnik?')) return;
try {
const response = await fetch(`/api/logs/${logId}`, {
method: 'DELETE'
});
const data = await response.json();
if (data.success) {
alert('Dnevnik je bil uspešno izbrisan');
refreshLogs();
} else {
alert('Napaka: ' + data.message);
}
} catch (error) {
alert('Napaka: ' + error.message);
}
}
function resetFilters() {
document.getElementById('filterSelect').value = 'All';
document.getElementById('participantFilter').value = '';
document.getElementById('startDateFilter').value = '';
document.getElementById('endDateFilter').value = '';
// Remove active class from all quick filter buttons
document.querySelectorAll('.btn-quick-filter').forEach(btn => {
btn.classList.remove('active');
});
refreshLogs();
}
function getDateString(date) {
return date.toISOString().split('T')[0];
}
function filterByToday() {
const today = new Date();
document.getElementById('startDateFilter').value = getDateString(today);
document.getElementById('endDateFilter').value = getDateString(today);
updateQuickFilterButtons('today');
refreshLogs();
}
function filterByLastWeek() {
const today = new Date();
const lastWeek = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000);
document.getElementById('startDateFilter').value = getDateString(lastWeek);
document.getElementById('endDateFilter').value = getDateString(today);
updateQuickFilterButtons('week');
refreshLogs();
}
function filterByLastMonth() {
const today = new Date();
const lastMonth = new Date(today.getTime() - 30 * 24 * 60 * 60 * 1000);
document.getElementById('startDateFilter').value = getDateString(lastMonth);
document.getElementById('endDateFilter').value = getDateString(today);
updateQuickFilterButtons('month');
refreshLogs();
}
function filterByLastYear() {
const today = new Date();
const lastYear = new Date(today.getTime() - 365 * 24 * 60 * 60 * 1000);
document.getElementById('startDateFilter').value = getDateString(lastYear);
document.getElementById('endDateFilter').value = getDateString(today);
updateQuickFilterButtons('year');
refreshLogs();
}
function updateQuickFilterButtons(activeFilter) {
const buttons = document.querySelectorAll('.btn-quick-filter');
buttons.forEach(btn => btn.classList.remove('active'));
if (activeFilter === 'today') buttons[0].classList.add('active');
else if (activeFilter === 'week') buttons[1].classList.add('active');
else if (activeFilter === 'month') buttons[2].classList.add('active');
else if (activeFilter === 'year') buttons[3].classList.add('active');
}
async function exportLogs() {
try {
// Get current filter values
const filter = document.getElementById('filterSelect').value;
const participant = document.getElementById('participantFilter').value;
const startDate = document.getElementById('startDateFilter').value;
const endDate = document.getElementById('endDateFilter').value;
// Build query string with filters
const params = new URLSearchParams({
filter: filter,
participant: participant,
startDate: startDate,
endDate: endDate
});
const response = await fetch('/api/export?' + params.toString());
const result = await response.json();
if (!result.success) {
alert('Napaka pri izvozу: ' + result.message);
return;
}
// Decode base64 file data
const binaryString = atob(result.data);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
// Create blob and trigger download with file dialog
const blob = new Blob([bytes], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
// Try to use pywebview if available (for desktop app)
if (window.pywebview && window.pywebview.api) {
try {
const success = await window.pywebview.api.save_file(result.filename, result.data);
if (success) {
alert('Datoteka je bila uspešno izvožena');
return;
} else {
console.log('PyWebView save was cancelled, falling back to browser download');
}
} catch (error) {
console.log('PyWebView save failed, falling back to browser download', error);
}
}
// Fallback to browser download
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = result.filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
alert('Datoteka je bila uspešno izvožena');
} catch (error) {
alert('Napaka: ' + error.message);
}
}
// ===== PRINT LOGS =====
function printLogs() {
const start = document.getElementById('startDateFilter').value;
const end = document.getElementById('endDateFilter').value;
const dateRange = document.getElementById('print-date-range');
if (start && end) {
dateRange.textContent = `Obdobje: ${start} – ${end}`;
} else if (start) {
dateRange.textContent = `Od: ${start}`;
} else if (end) {
dateRange.textContent = `Do: ${end}`;
} else {
dateRange.textContent = `Natisnjeno: ${new Date().toLocaleDateString('sl-SI')}`;
}
window.print();
}
// ===== DOCUMENTS =====
async function loadDocuments() {
try {
const response = await fetch('/api/documents');
const files = await response.json();
const grid = document.getElementById('documentsGrid');
if (files.length === 0) {
grid.innerHTML = `<p style="color: var(--text-secondary);">Ni dokumentov. Dodajte PDF datoteke v mapo static/PDF.</p>`;
return;
}
grid.innerHTML = '';
files.forEach(filename => {
const card = document.createElement('div');
card.className = 'document-card';
const displayName = filename.replace(/\.pdf$/i, '').replace(/_/g, ' ');
card.innerHTML = `
<div class="document-icon">
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
<polyline points="14 2 14 8 20 8"></polyline>
<line x1="9" y1="13" x2="15" y2="13"></line>
<line x1="9" y1="17" x2="15" y2="17"></line>
</svg>
</div>
<div class="document-name">${displayName}</div>
<button class="btn btn-primary document-print-btn" onclick="printDocument('${filename}')">Natisni</button>
`;
grid.appendChild(card);
});
} catch (error) {
console.error('Error loading documents:', error);
}
}
function printDocument(filename) {
const url = `/static/PDF/${encodeURIComponent(filename)}`;
let iframe = document.getElementById('print-iframe');
if (!iframe) {
iframe = document.createElement('iframe');
iframe.id = 'print-iframe';
iframe.style.cssText = 'display:none;position:fixed;top:0;left:0;width:0;height:0;';
document.body.appendChild(iframe);
}
iframe.onload = () => iframe.contentWindow.print();
iframe.src = url;
}
// ===== TAB SWITCHING =====
function switchTab(tabName, event) {
// Hide all tabs
document.querySelectorAll('.tab-panel').forEach(tab => {
tab.classList.remove('active');
});
// Remove active class from all buttons
document.querySelectorAll('.tab-nav-btn').forEach(btn => {
btn.classList.remove('active');
});
// Show selected tab
document.getElementById(tabName + '-tab').classList.add('active');
// Add active class to clicked button
event.target.classList.add('active');
// Load tab-specific data
if (tabName === 'documents') loadDocuments();
}
// Close modal when clicking outside
window.onclick = function(event) {
const editModal = document.getElementById('editModal');
const addModal = document.getElementById('addParticipantModal');
const loginModal = document.getElementById('loginModal');
const addUnregisteredModal = document.getElementById('addUnregisteredModal');
if (event.target === editModal) {
closeModal();
}
if (event.target === addModal) {
closeAddParticipantModal();
}
if (event.target === loginModal) {
closeLoginModal();
}
if (event.target === addUnregisteredModal) {
closeAddUnregisteredModal();
}
}
// ===== LAST SCAN RESULT =====
let lastScannedUID = '-';
let lastScanTimestamp = null;
async function updateLastScanResult() {
try {
const response = await fetch('/api/last-scan');
const result = await response.json();
const resultCard = document.getElementById('resultCard');
const uidDiv = resultCard.querySelector('.result-uid');
const nameDiv = resultCard.querySelector('.result-name');
const statusDiv = resultCard.querySelector('.result-status');
uidDiv.textContent = result.uid;
nameDiv.textContent = result.name;
statusDiv.textContent = result.status;
lastScannedUID = result.uid;
// If a new scan just happened, immediately refresh the logs table
if (result.timestamp && result.timestamp !== lastScanTimestamp) {
lastScanTimestamp = result.timestamp;
refreshLogs();
}
// Update card styling based on result
resultCard.className = 'result-card';
if (result.status === 'PASS') {
resultCard.classList.add('pass');
} else if (result.status === 'FAIL') {
resultCard.classList.add('fail');
}
} catch (error) {
console.error('Error updating last scan result:', error);
}
}
function copyUID(evt) {
if (lastScannedUID === '-') {
alert('Ni skeniranega UID-ja za kopiranje');
return;
}
const copyBtn = evt.currentTarget;
// Try using clipboard API first
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(lastScannedUID).then(() => {
showCopySuccess(copyBtn);
}).catch(err => {
console.error('Clipboard API failed:', err);
fallbackCopy(lastScannedUID, copyBtn);
});
} else {
// Fallback for older browsers
fallbackCopy(lastScannedUID, copyBtn);
}
}
function fallbackCopy(text, copyBtn) {
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.position = 'fixed';
textarea.style.opacity = '0';
document.body.appendChild(textarea);
textarea.select();
try {
document.execCommand('copy');
showCopySuccess(copyBtn);
} catch (err) {
console.error('Fallback copy failed:', err);
alert('Napaka pri kopiranju UID-ja');
}
document.body.removeChild(textarea);
}
function showCopySuccess(copyBtn) {
copyBtn.style.color = 'var(--accent-green)';
setTimeout(() => {
copyBtn.style.color = '';
}, 1500);
}
// ===== UID VISIBILITY MANAGEMENT =====
function updateUIDVisibility() {
const display = isAdmin ? '' : 'none';
// Participants table columns
const cardUIDHeader = document.getElementById('card-uid-header');
const keyUIDHeader = document.getElementById('key-uid-header');
if (cardUIDHeader) cardUIDHeader.style.display = display;
if (keyUIDHeader) keyUIDHeader.style.display = display;
// Logs table columns
const logsCardUIDHeader = document.getElementById('logs-card-uid-header');
const logsKeyUIDHeader = document.getElementById('logs-key-uid-header');
const logsActionsHeader = document.getElementById('logs-actions-header');
if (logsCardUIDHeader) logsCardUIDHeader.style.display = display;
if (logsKeyUIDHeader) logsKeyUIDHeader.style.display = display;
if (logsActionsHeader) logsActionsHeader.style.display = display;
// Hide/show corresponding cells in all table rows
hideTableColumn('participantsTable', 2, !isAdmin); // Card UID column
hideTableColumn('participantsTable', 3, !isAdmin); // Key UID column
hideTableColumn('logsTable', 2, !isAdmin); // Card UID column
hideTableColumn('logsTable', 3, !isAdmin); // Key UID column
hideTableColumn('logsTable', 7, !isAdmin); // Actions column
// Last scan result UID
const scanUIDContainer = document.getElementById('scan-result-uid-container');
if (scanUIDContainer) scanUIDContainer.style.display = display;
}
function hideTableColumn(tableId, columnIndex, hide) {
const table = document.getElementById(tableId);
if (!table) return;
const rows = table.getElementsByTagName('tr');
for (let row of rows) {
const cell = row.cells[columnIndex];
if (cell) {
cell.style.display = hide ? 'none' : '';
}
}
}