Files
RFID_detectionGUI/static/js/app.js
T

1618 lines
59 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 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 ? `
<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) {
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 = `<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 === '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 = `<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)"><circle cx="9" cy="21" r="1"/><circle cx="20" cy="21" r="1"/><path d="M1 1h4l2.68 13.39a2 2 0 001.97 1.61h9.72a2 2 0 001.97-1.61L23 6H6"/></svg>`;
const FALLBACK_SVG_SERVICE = `<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="M12 22V12"/><path d="M5 12H2a10 10 0 0020 0h-3"/><path d="M12 12a4 4 0 000-8 4 4 0 000 8z"/><path d="M5 12V7"/><path d="M19 12V7"/></svg>`;
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 = `<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">${getTypeFallbackSvg(item.type)}</div>`
}</div>`;
const stockHtml = isService
? `<div class="inventory-card-stock service-label">Storitev</div>`
: `<div class="inventory-card-stock ${outOfStock ? 'stock-empty' : ''}">${outOfStock ? 'Ni zaloge' : item.stock + ' kos'}</div>`;
const priceHtml = isDual
? `<div class="inventory-card-price">€ ${item.price_member.toFixed(2)} / € ${item.price_non_member.toFixed(2)}</div>`
: `<div class="inventory-card-price">€ ${item.price.toFixed(2)}</div>`;
const addBtns = isDual
? `<div class="inventory-dual-btns">
<button class="btn btn-primary inventory-add-btn" onclick="addToCart(${item.id}, 'member')" ${outOfStock ? 'disabled' : ''}>+ Član</button>
<button class="btn inventory-add-btn inventory-add-btn-secondary" onclick="addToCart(${item.id}, 'non_member')" ${outOfStock ? 'disabled' : ''}>+ Nečlan</button>
</div>`
: `<button class="btn btn-primary inventory-add-btn" onclick="addToCart(${item.id})" ${outOfStock ? 'disabled' : ''}>+ Dodaj</button>`;
card.innerHTML = `
${imgHtml}
<div class="inventory-card-body">
<div class="inventory-card-name">${item.name}</div>
<div class="inventory-card-meta">
${priceHtml}
${stockHtml}
</div>
</div>
<div class="inventory-card-actions">
${addBtns}
${isAdmin ? `<div class="inventory-card-admin-actions">
<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>` : ''}
</div>
`;
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 = '<p style="color: var(--text-secondary); grid-column: 1/-1;">Ni artiklov.</p>';
} else {
products.forEach(item => gridProducts.appendChild(buildItemCard(item)));
}
gridServices.innerHTML = '';
if (services.length === 0) {
gridServices.innerHTML = '<p style="color: var(--text-secondary); grid-column: 1/-1;">Ni storitev.</p>';
} 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 = '<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';
const tierArg = item.tier ? `,'${item.tier}'` : '';
row.innerHTML = `
<div class="cart-item-name">${item.name}</div>
<div class="cart-item-controls">
<button class="cart-qty-btn" onclick="removeFromCart('${item.cartKey}')">−</button>
<span class="cart-qty">${item.qty}</span>
<button class="cart-qty-btn" onclick="addToCart(${item.id}${tierArg})">+</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 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 = `<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.seller || '-'}</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.seller || '-'}</strong></span>
</div>
<div class="rp-divider-dashed"></div>
<table class="rp-table">
<thead><tr><th>Artikel</th><th style="text-align:center;">Količina</th><th style="text-align:right;">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.seller || '-'}</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.seller || '-'}</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' : '';
}
}
}