Files
RFID_detectionGUI/static/js/app.js
T
2026-05-30 23:39:56 +02:00

1521 lines
54 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;
function fmtDate(iso) {
const [y, m, d] = iso.split('-');
return `${d}-${m}-${y.slice(2)}`;
}
// 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');
const addItemBtn = document.getElementById('addItemBtn');
const editBalanceBtn = document.getElementById('editBalanceBtn');
if (!adminBtn) return;
if (isAdmin) {
adminBtn.textContent = 'Odjava Admin';
adminBtn.onclick = adminLogout;
adminBtn.classList.add('active');
if (addBtn) addBtn.style.display = 'block';
if (addItemBtn) addItemBtn.style.display = 'inline-flex';
if (editBalanceBtn) editBalanceBtn.style.display = 'inline-flex';
} else {
adminBtn.textContent = 'Prijava Admin';
adminBtn.onclick = openLoginModal;
adminBtn.classList.remove('active');
if (addBtn) addBtn.style.display = 'none';
if (addItemBtn) addItemBtn.style.display = 'none';
if (editBalanceBtn) editBalanceBtn.style.display = 'none';
}
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();
loadStore();
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();
loadStore();
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 = fmtDate(timestamp.split('T')[0]);
const time = timestamp.split('T')[1].substring(0, 5);
// 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();
if (tabName === 'store') {
// Always return to shop view when switching to store tab
storeViewIsHistory = false;
document.getElementById('storeShopView').style.display = '';
document.getElementById('storeHistoryView').style.display = 'none';
document.getElementById('storeViewToggleBtn').textContent = 'Zgodovina plačil';
loadStore();
}
}
// 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');
const addItemModal = document.getElementById('addItemModal');
const editBalanceModal = document.getElementById('editBalanceModal');
if (event.target === editModal) closeModal();
if (event.target === addModal) closeAddParticipantModal();
if (event.target === loginModal) closeLoginModal();
if (event.target === addUnregisteredModal) closeAddUnregisteredModal();
if (event.target === addItemModal) closeAddItemModal();
if (event.target === editBalanceModal) closeEditBalanceModal();
const checkoutModal = document.getElementById('checkoutModal');
const monthlyModal = document.getElementById('monthlyModal');
if (event.target === checkoutModal) closeCheckoutModal();
if (event.target === monthlyModal) closeMonthlyOverview();
}
// ===== 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;
}
// ===== STORE =====
let cart = [];
let storeItems = [];
let currentBalance = 0;
let editingItemId = null;
async function loadStore() {
await Promise.all([loadStoreItems(), loadBalance()]);
}
async function loadStoreItems() {
try {
const response = await fetch('/api/store/items');
storeItems = await response.json();
renderInventory();
renderCart();
} catch (error) {
console.error('Error loading store items:', error);
}
}
async function loadBalance() {
try {
const response = await fetch('/api/store/balance');
const data = await response.json();
currentBalance = data.balance;
document.getElementById('storeBalanceDisplay').textContent = '€' + currentBalance.toFixed(2);
updateCartTotals();
} catch (error) {
console.error('Error loading balance:', error);
}
}
const ITEM_FALLBACK_SVG = `<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" style="color:var(--text-secondary)"><path d="M6 2L3 6v14a2 2 0 002 2h14a2 2 0 002-2V6l-3-4z"/><line x1="3" y1="6" x2="21" y2="6"/><path d="M16 10a4 4 0 01-8 0"/></svg>`;
function renderInventory() {
const grid = document.getElementById('inventoryGrid');
if (!grid) return;
if (storeItems.length === 0) {
grid.innerHTML = '<p style="color: var(--text-secondary); grid-column: 1/-1;">Ni artiklov v zalogi.</p>';
return;
}
grid.innerHTML = '';
storeItems.forEach(item => {
const card = document.createElement('div');
card.className = 'inventory-card' + (item.stock === 0 ? ' out-of-stock' : '');
const imgHtml = `<div class="inventory-card-img-wrap">${item.image
? `<img src="${item.image}" alt="${item.name}" class="inventory-card-img">`
: `<div class="inventory-card-img-placeholder">${ITEM_FALLBACK_SVG}</div>`
}</div>`;
card.innerHTML = `
${imgHtml}
<div class="inventory-card-body">
<div class="inventory-card-name">${item.name}</div>
<div class="inventory-card-price">€${item.price.toFixed(2)}</div>
<div class="inventory-card-stock ${item.stock === 0 ? 'stock-empty' : ''}">Zaloga: ${item.stock}</div>
</div>
<div class="inventory-card-actions">
<button class="btn btn-primary inventory-add-btn" onclick="addToCart(${item.id})" ${item.stock === 0 ? 'disabled' : ''}>+ Dodaj</button>
${isAdmin ? `<button class="action-btn edit-btn" onclick="openEditItemModal(${item.id})">Uredi</button>
<button class="action-btn delete-btn" onclick="deleteStoreItem(${item.id})">Izbriši</button>` : ''}
</div>
`;
grid.appendChild(card);
});
}
function addToCart(itemId) {
const item = storeItems.find(i => i.id === itemId);
if (!item || item.stock === 0) return;
const existing = cart.find(c => c.id === itemId);
const currentQty = existing ? existing.qty : 0;
if (currentQty >= item.stock) {
alert('Ni dovolj zaloge');
return;
}
if (existing) {
existing.qty++;
} else {
cart.push({ id: item.id, name: item.name, price: item.price, qty: 1 });
}
renderCart();
}
function removeFromCart(itemId) {
const idx = cart.findIndex(c => c.id === itemId);
if (idx === -1) return;
if (cart[idx].qty > 1) {
cart[idx].qty--;
} else {
cart.splice(idx, 1);
}
renderCart();
}
function clearCart() {
cart = [];
renderCart();
}
function renderCart() {
const container = document.getElementById('cartItems');
if (!container) return;
if (cart.length === 0) {
container.innerHTML = '<p style="color: var(--text-secondary); text-align: center; padding: 20px;">Košarica je prazna</p>';
updateCartTotals();
return;
}
container.innerHTML = '';
cart.forEach(item => {
const row = document.createElement('div');
row.className = 'cart-item';
row.innerHTML = `
<div class="cart-item-name">${item.name}</div>
<div class="cart-item-controls">
<button class="cart-qty-btn" onclick="removeFromCart(${item.id})">−</button>
<span class="cart-qty">${item.qty}</span>
<button class="cart-qty-btn" onclick="addToCart(${item.id})">+</button>
</div>
<div class="cart-item-subtotal">€${(item.price * item.qty).toFixed(2)}</div>
`;
container.appendChild(row);
});
updateCartTotals();
}
function updateCartTotals() {
const total = cart.reduce((sum, i) => sum + i.price * i.qty, 0);
const balanceAfter = currentBalance + total;
const totalEl = document.getElementById('cartTotal');
const balanceAfterEl = document.getElementById('cartBalanceAfter');
if (totalEl) totalEl.textContent = '€' + total.toFixed(2);
if (balanceAfterEl) {
balanceAfterEl.textContent = '€' + balanceAfter.toFixed(2);
balanceAfterEl.className = 'cart-balance-after';
}
}
function processPayment() {
if (cart.length === 0) {
alert('Košarica je prazna');
return;
}
const total = cart.reduce((sum, i) => sum + i.price * i.qty, 0);
const balanceAfter = currentBalance + total;
document.getElementById('checkoutFirstName').value = '';
document.getElementById('checkoutLastName').value = '';
document.getElementById('checkoutTotal').textContent = '€' + total.toFixed(2);
document.getElementById('checkoutBalanceAfter').textContent = '€' + balanceAfter.toFixed(2);
document.getElementById('checkoutModal').classList.add('show');
document.getElementById('checkoutFirstName').focus();
}
function closeCheckoutModal() {
document.getElementById('checkoutModal').classList.remove('show');
}
async function confirmCheckout() {
const firstName = document.getElementById('checkoutFirstName').value.trim();
const lastName = document.getElementById('checkoutLastName').value.trim();
if (!firstName || !lastName) {
alert('Prosimo, vnesite ime in priimek kupca');
return;
}
const buyer = firstName + ' ' + lastName;
const total = cart.reduce((sum, i) => sum + i.price * i.qty, 0);
const balanceAfter = currentBalance + total;
closeCheckoutModal();
try {
const cartItems = cart.map(cartItem => {
const storeItem = storeItems.find(i => i.id === cartItem.id);
return {
id: cartItem.id,
name: cartItem.name,
price: cartItem.price,
qty: cartItem.qty,
new_stock: storeItem ? storeItem.stock - cartItem.qty : 0,
image: storeItem ? storeItem.image : null
};
});
const response = await fetch('/api/store/payments', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
buyer,
items: cart.map(i => ({ name: i.name, qty: i.qty, price: i.price })),
cart_items: cartItems,
total,
balance_before: currentBalance,
balance_after: balanceAfter
})
});
const data = await response.json();
if (!data.success) { alert('Napaka: ' + data.message); return; }
const payment = {
buyer,
items: cart.map(i => ({ name: i.name, qty: i.qty, price: i.price })),
total,
balance_before: currentBalance,
balance_after: balanceAfter,
timestamp: new Date().toISOString()
};
cart = [];
await loadStore();
printReceipt(payment);
} catch (error) {
alert('Napaka: ' + error.message);
}
}
let currentItemImage = null;
function openAddItemModal() {
editingItemId = null;
currentItemImage = null;
document.getElementById('addItemModalTitle').textContent = 'Dodaj artikel';
document.getElementById('addItemSubmitBtn').textContent = 'Dodaj';
document.getElementById('itemName').value = '';
document.getElementById('itemPrice').value = '';
document.getElementById('itemStock').value = '';
document.getElementById('itemImageInput').value = '';
setImagePreview(null);
document.getElementById('addItemModal').classList.add('show');
document.getElementById('itemName').focus();
}
function openEditItemModal(itemId) {
const item = storeItems.find(i => i.id === itemId);
if (!item) return;
editingItemId = itemId;
currentItemImage = item.image || null;
document.getElementById('addItemModalTitle').textContent = 'Uredi artikel';
document.getElementById('addItemSubmitBtn').textContent = 'Shrani';
document.getElementById('itemName').value = item.name;
document.getElementById('itemPrice').value = item.price;
document.getElementById('itemStock').value = item.stock;
document.getElementById('itemImageInput').value = '';
setImagePreview(item.image || null);
document.getElementById('addItemModal').classList.add('show');
}
function closeAddItemModal() {
document.getElementById('addItemModal').classList.remove('show');
editingItemId = null;
currentItemImage = null;
}
function handleImageUpload(event) {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
currentItemImage = e.target.result;
setImagePreview(currentItemImage);
};
reader.readAsDataURL(file);
}
function setImagePreview(src) {
const preview = document.getElementById('imagePreview');
const placeholder = document.getElementById('imageUploadPlaceholder');
const removeBtn = document.getElementById('removeImageBtn');
if (src) {
preview.src = src;
preview.style.display = 'block';
placeholder.style.display = 'none';
removeBtn.style.display = 'inline-block';
} else {
preview.src = '';
preview.style.display = 'none';
placeholder.style.display = 'flex';
removeBtn.style.display = 'none';
}
}
function removeItemImage() {
currentItemImage = null;
document.getElementById('itemImageInput').value = '';
setImagePreview(null);
}
async function submitItem() {
const name = document.getElementById('itemName').value.trim();
const price = parseFloat(document.getElementById('itemPrice').value);
const stock = parseInt(document.getElementById('itemStock').value, 10);
if (!name) { alert('Vnesite ime artikla'); return; }
if (isNaN(price) || price < 0) { alert('Vnesite veljavno ceno'); return; }
if (isNaN(stock) || stock < 0) { alert('Vnesite veljavno zalogo'); return; }
try {
let response;
if (editingItemId) {
response = await fetch(`/api/store/items/${editingItemId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, price, stock, image: currentItemImage })
});
} else {
response = await fetch('/api/store/items', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, price, stock, image: currentItemImage })
});
}
const data = await response.json();
if (data.success) {
closeAddItemModal();
await loadStoreItems();
} else {
alert('Napaka: ' + data.message);
}
} catch (error) {
alert('Napaka: ' + error.message);
}
}
async function deleteStoreItem(itemId) {
if (!confirm('Izbriši artikel?')) return;
try {
const response = await fetch(`/api/store/items/${itemId}`, { method: 'DELETE' });
const data = await response.json();
if (data.success) {
cart = cart.filter(c => c.id !== itemId);
await loadStoreItems();
} else {
alert('Napaka: ' + data.message);
}
} catch (error) {
alert('Napaka: ' + error.message);
}
}
function openEditBalanceModal() {
document.getElementById('balanceInput').value = currentBalance.toFixed(2);
document.getElementById('editBalanceModal').classList.add('show');
document.getElementById('balanceInput').focus();
}
function closeEditBalanceModal() {
document.getElementById('editBalanceModal').classList.remove('show');
}
async function saveBalance() {
const amount = parseFloat(document.getElementById('balanceInput').value);
if (isNaN(amount)) { alert('Vnesite veljavno vrednost'); return; }
try {
const response = await fetch('/api/store/balance', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ balance: amount })
});
const data = await response.json();
if (data.success) {
closeEditBalanceModal();
await loadBalance();
} else {
alert('Napaka: ' + data.message);
}
} catch (error) {
alert('Napaka: ' + error.message);
}
}
// ===== PAYMENT HISTORY =====
let storeViewIsHistory = false;
function toggleStoreView() {
storeViewIsHistory = !storeViewIsHistory;
document.getElementById('storeShopView').style.display = storeViewIsHistory ? 'none' : '';
document.getElementById('storeHistoryView').style.display = storeViewIsHistory ? '' : 'none';
document.getElementById('storeViewToggleBtn').textContent = storeViewIsHistory ? '← Nazaj v trgovino' : 'Zgodovina plačil';
if (storeViewIsHistory) loadPaymentHistory();
}
function togglePaymentHistory() { toggleStoreView(); }
async function loadPaymentHistory() {
try {
const response = await fetch('/api/store/payments');
const payments = await response.json();
const tbody = document.getElementById('paymentHistoryTable');
if (!tbody) return;
const clearBtn = document.getElementById('clearHistoryBtn');
if (clearBtn) clearBtn.style.display = isAdmin ? 'inline-block' : 'none';
if (payments.length === 0) {
tbody.innerHTML = `<tr><td colspan="9" style="text-align:center;padding:20px;color:var(--text-secondary);">Ni zabeleženih plačil</td></tr>`;
return;
}
tbody.innerHTML = '';
[...payments].reverse().forEach(p => {
const ts = p.timestamp;
const date = fmtDate(ts.split('T')[0]);
const time = ts.split('T')[1].substring(0, 5);
const itemsSummary = p.items.map(i => `${i.name} ×${i.qty}`).join(', ');
const row = document.createElement('tr');
row.innerHTML = `
<td>${p.id}</td>
<td style="font-weight:600;">${p.buyer || '-'}</td>
<td>${date}</td>
<td>${time}</td>
<td style="max-width:200px;word-break:break-word;">${itemsSummary}</td>
<td style="font-weight:700;color:var(--accent-blue);">€${p.total.toFixed(2)}</td>
<td>€${p.balance_before.toFixed(2)}</td>
<td>€${p.balance_after.toFixed(2)}</td>
<td><button class="action-btn edit-btn" onclick="printReceipt(${JSON.stringify(p).replace(/"/g, '&quot;')})">🖨 Natisni</button></td>
`;
tbody.appendChild(row);
});
} catch (error) {
console.error('Error loading payment history:', error);
}
}
// ===== RECEIPT PRINT =====
const LOGO_URL = '/static/assets/logo.png';
function printReceipt(payment) {
const ts = payment.timestamp || new Date().toISOString();
const date = fmtDate(ts.split('T')[0]);
const time = ts.split('T')[1] ? ts.split('T')[1].substring(0, 5) : '';
const rows = payment.items.map(i =>
`<tr><td class="rp-td-name">${i.name}</td><td class="rp-td-qty">${i.qty}×</td><td class="rp-td-price">€${i.price.toFixed(2)}</td><td class="rp-td-sub">€${(i.price * i.qty).toFixed(2)}</td></tr>`
).join('');
const html = `
<div class="rp-page">
<div class="rp-logo-block">
<img src="${LOGO_URL}" alt="Logo" class="rp-logo" onerror="this.style.display='none'">
</div>
<div class="rp-divider"></div>
<div class="rp-title">POTRDILO O PLAČILU</div>
<div class="rp-meta">
<span>${date} ${time}</span>
<span>Prodajalec: <strong>${payment.buyer || '-'}</strong></span>
</div>
<div class="rp-divider-dashed"></div>
<table class="rp-table">
<thead><tr><th>Artikel</th><th>Kol.</th><th>Cena</th><th style="text-align:right;">Znesek</th></tr></thead>
<tbody>${rows}</tbody>
</table>
<div class="rp-divider"></div>
<div class="rp-total">
<span>SKUPAJ</span>
<span>€${payment.total.toFixed(2)}</span>
</div>
<div class="rp-divider-dashed"></div>
<div class="rp-footer">Hvala za nakup!</div>
</div>
`;
const area = document.getElementById('receiptPrintArea');
area.innerHTML = html;
area.style.display = 'block';
document.body.classList.add('printing-receipt');
window.print();
document.body.classList.remove('printing-receipt');
area.style.display = 'none';
}
// ===== MONTHLY OVERVIEW =====
let allPaymentsCache = [];
function openMonthlyOverview() {
const now = new Date();
const ym = now.toISOString().substring(0, 7);
document.getElementById('monthlyPickerInput').value = ym;
document.getElementById('monthlyModal').classList.add('show');
renderMonthlyOverview();
}
function closeMonthlyOverview() {
document.getElementById('monthlyModal').classList.remove('show');
}
async function renderMonthlyOverview() {
const picker = document.getElementById('monthlyPickerInput').value;
if (!picker) return;
try {
const response = await fetch('/api/store/payments');
allPaymentsCache = await response.json();
} catch (e) { return; }
const filtered = allPaymentsCache.filter(p => p.timestamp.startsWith(picker));
const content = document.getElementById('monthlyOverviewContent');
if (filtered.length === 0) {
content.innerHTML = '<p style="color:var(--text-secondary);padding:16px 0;">Ni plačil v izbranem mesecu.</p>';
return;
}
const grandTotal = filtered.reduce((s, p) => s + p.total, 0);
// Per-item breakdown
const itemTotals = {};
filtered.forEach(p => p.items.forEach(i => {
if (!itemTotals[i.name]) itemTotals[i.name] = { qty: 0, revenue: 0 };
itemTotals[i.name].qty += i.qty;
itemTotals[i.name].revenue += i.price * i.qty;
}));
const itemRows = Object.entries(itemTotals).map(([name, v]) =>
`<tr><td>${name}</td><td style="text-align:center;">${v.qty}</td><td style="text-align:right;font-weight:600;">€${v.revenue.toFixed(2)}</td></tr>`
).join('');
const txRows = [...filtered].reverse().map(p => {
const date = fmtDate(p.timestamp.split('T')[0]);
const time = p.timestamp.split('T')[1].substring(0, 5);
return `<tr><td>${date}</td><td>${time}</td><td style="font-weight:600;">${p.buyer || '-'}</td><td>${p.items.map(i => `${i.name} ×${i.qty}`).join(', ')}</td><td style="text-align:right;font-weight:700;color:var(--accent-blue);">€${p.total.toFixed(2)}</td></tr>`;
}).join('');
content.innerHTML = `
<div class="monthly-summary-total">Skupaj prihodek: <strong>€${grandTotal.toFixed(2)}</strong> (${filtered.length} transakcij)</div>
<h4 class="monthly-section-title">Po artiklih</h4>
<div class="table-container" style="margin-bottom:16px;">
<table><thead><tr><th>Artikel</th><th style="text-align:center;">Količina</th><th style="text-align:right;">Prihodek</th></tr></thead>
<tbody>${itemRows}</tbody></table>
</div>
<h4 class="monthly-section-title">Vse transakcije</h4>
<div class="table-container">
<table><thead><tr><th>Datum</th><th>Čas</th><th>Prodajalec</th><th>Artikli</th><th style="text-align:right;">Skupaj</th></tr></thead>
<tbody>${txRows}</tbody></table>
</div>
`;
}
function printMonthlyOverview() {
const picker = document.getElementById('monthlyPickerInput').value;
if (!picker || !allPaymentsCache.length) return;
const filtered = allPaymentsCache.filter(p => p.timestamp.startsWith(picker));
if (filtered.length === 0) { alert('Ni podatkov za izbrani mesec.'); return; }
const grandTotal = filtered.reduce((s, p) => s + p.total, 0);
const itemTotals = {};
filtered.forEach(p => p.items.forEach(i => {
if (!itemTotals[i.name]) itemTotals[i.name] = { qty: 0, revenue: 0 };
itemTotals[i.name].qty += i.qty;
itemTotals[i.name].revenue += i.price * i.qty;
}));
const itemRows = Object.entries(itemTotals).map(([name, v]) =>
`<tr><td>${name}</td><td style="text-align:center;">${v.qty}</td><td style="text-align:right;font-weight:600;">€${v.revenue.toFixed(2)}</td></tr>`
).join('');
const txRows = [...filtered].reverse().map(p => {
const date = fmtDate(p.timestamp.split('T')[0]);
const time = p.timestamp.split('T')[1].substring(0, 5);
const items = p.items.map(i => `${i.name} ×${i.qty} (€${(i.price*i.qty).toFixed(2)})`).join(', ');
return `<tr><td>${date}</td><td>${time}</td><td style="font-weight:600;">${p.buyer || '-'}</td><td>${items}</td><td style="text-align:right;font-weight:700;">€${p.total.toFixed(2)}</td></tr>`;
}).join('');
const [year, month] = picker.split('-');
const monthName = new Date(year, month - 1, 1).toLocaleDateString('sl-SI', { month: 'long', year: 'numeric' });
const printDate = fmtDate(new Date().toISOString().split('T')[0]);
const html = `
<div class="rp-page rp-monthly">
<div class="rp-logo-block">
<img src="${LOGO_URL}" alt="Logo" class="rp-logo" onerror="this.style.display='none'">
</div>
<div class="rp-divider"></div>
<div class="rp-title">MESEČNI PREGLED</div>
<div class="rp-meta"><span>${monthName}</span><span>Natisnjeno: ${printDate}</span></div>
<div class="rp-divider-dashed"></div>
<div class="rp-monthly-kpi">
<div class="rp-kpi-box"><div class="rp-kpi-label">Skupaj prihodek</div><div class="rp-kpi-value">€${grandTotal.toFixed(2)}</div></div>
<div class="rp-kpi-box"><div class="rp-kpi-label">Št. transakcij</div><div class="rp-kpi-value">${filtered.length}</div></div>
</div>
<div class="rp-section-title">Po artiklih</div>
<table class="rp-table">
<thead><tr><th>Artikel</th><th style="text-align:center;">Količina</th><th style="text-align:right;">Prihodek</th></tr></thead>
<tbody>${itemRows}</tbody>
</table>
<div class="rp-section-title" style="margin-top:16px;">Vse transakcije</div>
<table class="rp-table">
<thead><tr><th>Datum</th><th>Čas</th><th>Prodajalec</th><th>Artikli</th><th style="text-align:right;">Skupaj</th></tr></thead>
<tbody>${txRows}</tbody>
</table>
</div>
`;
const area = document.getElementById('receiptPrintArea');
area.innerHTML = html;
area.style.display = 'block';
document.body.classList.add('printing-receipt');
window.print();
document.body.classList.remove('printing-receipt');
area.style.display = 'none';
}
async function clearPaymentHistory() {
if (!confirm('Izbriši celotno zgodovino plačil?')) return;
try {
const response = await fetch('/api/store/payments', { method: 'DELETE' });
const data = await response.json();
if (data.success) loadPaymentHistory();
else alert('Napaka: ' + data.message);
} catch (error) {
alert('Napaka: ' + error.message);
}
}
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' : '';
}
}
}