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 participantsToolbar = document.getElementById('participantsToolbar'); 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 (participantsToolbar) participantsToolbar.style.display = ''; 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 (participantsToolbar) participantsToolbar.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 ? ` ` : ''; row.innerHTML = ` ${p.id} ${p.name} ${p.card_uid || '-'} ${keyUID} ${expiration} ${dateAdded} ${actionButtons || '-'} `; // 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 = ''; // 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 = `Ni najdenih dnevnikov, ki bi ustrezali filtrom`; 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 ? ` ` : ''; row.innerHTML = ` ${log.id} ${log.name} ${log.card_uid || '-'} ${log.key_uid || '-'} ${log.scan_result} ${date} ${time} ${actionButtons} `; // 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) { const y = date.getFullYear(); const m = String(date.getMonth() + 1).padStart(2, '0'); const d = String(date.getDate()).padStart(2, '0'); return `${y}-${m}-${d}`; } 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 = `

Ni dokumentov. Dodajte PDF datoteke v mapo static/PDF.

`; 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 = `
${displayName}
`; 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 === 'logs') filterByToday(); 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 FALLBACK_SVG_PRODUCT = ``; const FALLBACK_SVG_SERVICE = ``; function getTypeFallbackSvg(type) { if (type === 'service') return FALLBACK_SVG_SERVICE; return FALLBACK_SVG_PRODUCT; } function buildItemCard(item) { const isService = item.type === 'service'; const isDual = item.price_member != null && item.price_non_member != null; const outOfStock = !isService && item.stock === 0; const card = document.createElement('div'); card.className = 'inventory-card' + (outOfStock ? ' out-of-stock' : ''); const imgHtml = `
${item.image ? `${item.name}` : `
${getTypeFallbackSvg(item.type)}
` }
`; const stockHtml = isService ? `
Storitev
` : `
${outOfStock ? 'Ni zaloge' : item.stock + ' kos'}
`; const priceHtml = isDual ? `
€ ${item.price_member.toFixed(2)} / € ${item.price_non_member.toFixed(2)}
` : `
€ ${item.price.toFixed(2)}
`; const addBtns = isDual ? `
` : ``; card.innerHTML = ` ${imgHtml}
${item.name}
${priceHtml} ${stockHtml}
${addBtns} ${isAdmin ? `
` : ''}
`; return card; } function renderInventory() { const gridProducts = document.getElementById('inventoryGridProducts'); const gridServices = document.getElementById('inventoryGridServices'); const sectionProducts = document.getElementById('inventorySectionProducts'); const sectionServices = document.getElementById('inventorySectionServices'); if (!gridProducts || !gridServices) return; const products = storeItems.filter(i => !i.type || i.type === 'product'); const services = storeItems.filter(i => i.type === 'service'); gridProducts.innerHTML = ''; if (products.length === 0) { gridProducts.innerHTML = '

Ni artiklov.

'; } else { products.forEach(item => gridProducts.appendChild(buildItemCard(item))); } gridServices.innerHTML = ''; if (services.length === 0) { gridServices.innerHTML = '

Ni storitev.

'; } else { services.forEach(item => gridServices.appendChild(buildItemCard(item))); } sectionProducts.style.display = ''; sectionServices.style.display = ''; } function addToCart(itemId, tier) { const item = storeItems.find(i => i.id === itemId); if (!item) return; const isService = item.type === 'service'; if (!isService && item.stock === 0) return; const isDual = item.price_member != null && item.price_non_member != null; const usedTier = isDual ? (tier || 'member') : null; const price = isDual ? (usedTier === 'member' ? item.price_member : item.price_non_member) : item.price; // For dual-price items, each tier is a separate cart line const cartKey = isDual ? `${itemId}_${usedTier}` : `${itemId}`; const existing = cart.find(c => c.cartKey === cartKey); const currentQty = existing ? existing.qty : 0; if (!isService && currentQty >= item.stock) { alert('Ni dovolj zaloge'); return; } if (existing) { existing.qty++; } else { const label = isDual ? `${item.name} (${usedTier === 'member' ? 'član' : 'nečlan'})` : item.name; cart.push({ cartKey, id: item.id, name: label, price, qty: 1, type: item.type || 'product', tier: usedTier }); } renderCart(); } function removeFromCart(cartKey) { const idx = cart.findIndex(c => c.cartKey === cartKey); 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 = '

Košarica je prazna

'; updateCartTotals(); return; } container.innerHTML = ''; cart.forEach(item => { const row = document.createElement('div'); row.className = 'cart-item'; const tierArg = item.tier ? `,'${item.tier}'` : ''; row.innerHTML = `
${item.name}
${item.qty}
€ ${(item.price * item.qty).toFixed(2)}
`; container.appendChild(row); }); updateCartTotals(); } function updateCartTotals() { const total = cart.reduce((sum, i) => sum + i.price * i.qty, 0); const totalEl = document.getElementById('cartTotal'); if (totalEl) totalEl.textContent = '€ ' + total.toFixed(2); } 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('checkoutBuyer').value = ''; document.getElementById('checkoutTotal').textContent = '€ ' + total.toFixed(2); document.getElementById('checkoutModal').classList.add('show'); document.getElementById('checkoutBuyer').focus(); } function closeCheckoutModal() { document.getElementById('checkoutModal').classList.remove('show'); } async function confirmCheckout() { const seller = document.getElementById('checkoutBuyer').value.trim(); if (!seller) { alert('Prosimo, vnesite ime in priimek prodajalca'); return; } const total = cart.reduce((sum, i) => sum + i.price * i.qty, 0); const balanceAfter = currentBalance + total; closeCheckoutModal(); try { // Total qty sold per store item id (may have member+non_member lines) const qtySoldById = {}; cart.forEach(ci => { qtySoldById[ci.id] = (qtySoldById[ci.id] || 0) + ci.qty; }); const cartItems = cart.map(cartItem => { const storeItem = storeItems.find(i => i.id === cartItem.id); const isService = ((storeItem && storeItem.type) || cartItem.type) === 'service'; return { id: cartItem.id, name: cartItem.name, price: cartItem.price, qty: cartItem.qty, type: isService ? 'service' : 'product', new_stock: isService ? null : (storeItem ? storeItem.stock - qtySoldById[cartItem.id] : 0), image: storeItem ? storeItem.image : null }; }); const response = await fetch('/api/store/payments', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ seller, 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 = { seller, 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; let currentItemType = 'product'; function setItemType(type) { currentItemType = type; document.getElementById('typeProductBtn').classList.toggle('active', type === 'product'); document.getElementById('typeServiceBtn').classList.toggle('active', type === 'service'); document.getElementById('stockFormGroup').style.display = type === 'service' ? 'none' : ''; } function toggleDualPrice() { const dual = document.getElementById('itemDualPrice').checked; document.getElementById('singlePriceGroup').style.display = dual ? 'none' : ''; document.getElementById('dualPriceGroup').style.display = dual ? '' : 'none'; } 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('itemPriceMember').value = ''; document.getElementById('itemPriceNonMember').value = ''; document.getElementById('itemDualPrice').checked = false; document.getElementById('itemStock').value = ''; document.getElementById('itemImageInput').value = ''; setImagePreview(null); setItemType('product'); toggleDualPrice(); 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; const hasDual = !!(item.price_member != null && item.price_non_member != null); document.getElementById('addItemModalTitle').textContent = 'Uredi artikel'; document.getElementById('addItemSubmitBtn').textContent = 'Shrani'; document.getElementById('itemName').value = item.name; document.getElementById('itemPrice').value = hasDual ? '' : item.price; document.getElementById('itemPriceMember').value = hasDual ? item.price_member : ''; document.getElementById('itemPriceNonMember').value = hasDual ? item.price_non_member : ''; document.getElementById('itemDualPrice').checked = hasDual; document.getElementById('itemStock').value = item.stock || 0; document.getElementById('itemImageInput').value = ''; setImagePreview(item.image || null); setItemType(item.type === 'service' ? 'service' : 'product'); toggleDualPrice(); 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 isService = currentItemType === 'service'; const isDual = document.getElementById('itemDualPrice').checked; const stock = isService ? 0 : parseInt(document.getElementById('itemStock').value, 10); let price = null, price_member = null, price_non_member = null; if (isDual) { price_member = parseFloat(document.getElementById('itemPriceMember').value); price_non_member = parseFloat(document.getElementById('itemPriceNonMember').value); price = price_member; if (isNaN(price_member) || price_member < 0) { alert('Vnesite veljavno ceno za člana'); return; } if (isNaN(price_non_member) || price_non_member < 0) { alert('Vnesite veljavno ceno za nečlana'); return; } } else { price = parseFloat(document.getElementById('itemPrice').value); if (isNaN(price) || price < 0) { alert('Vnesite veljavno ceno'); return; } } if (!name) { alert('Vnesite ime artikla'); return; } if (!isService && (isNaN(stock) || stock < 0)) { alert('Vnesite veljavno zalogo'); return; } const payload = { name, price, stock, type: currentItemType, image: currentItemImage, price_member: isDual ? price_member : null, price_non_member: isDual ? price_non_member : null }; try { let response; if (editingItemId) { response = await fetch(`/api/store/items/${editingItemId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); } else { response = await fetch('/api/store/items', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); } 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 = `Ni zabeleženih plačil`; 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 = ` ${p.id} ${p.seller || '-'} ${date} ${time} ${itemsSummary} € ${p.total.toFixed(2)} € ${p.balance_before.toFixed(2)} € ${p.balance_after.toFixed(2)} `; 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 => `${i.name}${i.qty}×€ ${i.price.toFixed(2)}€ ${(i.price * i.qty).toFixed(2)}` ).join(''); const html = `
POTRDILO O PLAČILU
${date} ${time} Prodajalec: ${payment.seller || '-'}
${rows}
ArtikelKoličinaCenaZnesek
SKUPAJ € ${payment.total.toFixed(2)}
`; 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 = '

Ni plačil v izbranem mesecu.

'; 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]) => `${name}${v.qty}€ ${v.revenue.toFixed(2)}` ).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 `${date}${time}${p.seller || '-'}${p.items.map(i => `${i.name} ×${i.qty}`).join(', ')}€ ${p.total.toFixed(2)}`; }).join(''); content.innerHTML = `
Skupaj prihodek: € ${grandTotal.toFixed(2)} (${filtered.length} transakcij)

Po artiklih

${itemRows}
ArtikelKoličinaPrihodek

Vse transakcije

${txRows}
DatumČasProdajalecArtikliSkupaj
`; } 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]) => `${name}${v.qty}€ ${v.revenue.toFixed(2)}` ).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 `${date}${time}${p.seller || '-'}${items}€ ${p.total.toFixed(2)}`; }).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 = `
MESEČNI PREGLED
${monthName}Natisnjeno: ${printDate}
Skupaj prihodek
€ ${grandTotal.toFixed(2)}
Št. transakcij
${filtered.length}
Po artiklih
${itemRows}
ArtikelKoličinaPrihodek
Vse transakcije
${txRows}
DatumČasProdajalecArtikliSkupaj
`; 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' : ''; } } }