1521 lines
54 KiB
JavaScript
1521 lines
54 KiB
JavaScript
let editingParticipantId = null;
|
||
let isAdmin = false;
|
||
|
||
function fmtDate(iso) {
|
||
const [y, m, d] = iso.split('-');
|
||
return `${d}-${m}-${y.slice(2)}`;
|
||
}
|
||
|
||
// Initialize on page load
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
checkAdminStatus();
|
||
loadParticipants();
|
||
// Show today's entries by default
|
||
filterByToday();
|
||
updateLastScanResult();
|
||
|
||
// Auto-refresh logs every 5 seconds
|
||
setInterval(refreshLogs, 5000);
|
||
setInterval(loadParticipants, 5000);
|
||
setInterval(updateLastScanResult, 1000);
|
||
setInterval(checkAdminStatus, 10000);
|
||
});
|
||
|
||
// ===== AUTHENTICATION =====
|
||
async function checkAdminStatus() {
|
||
try {
|
||
const response = await fetch('/api/check-admin');
|
||
const data = await response.json();
|
||
isAdmin = data.is_admin;
|
||
updateAdminUI();
|
||
} catch (error) {
|
||
console.error('Error checking admin status:', error);
|
||
}
|
||
}
|
||
|
||
function updateAdminUI() {
|
||
const adminBtn = document.getElementById('adminToggleBtn');
|
||
const addBtn = document.querySelector('[onclick="openAddParticipantModal()"]');
|
||
const addUnregisteredBtn = document.getElementById('addUnregisteredBtn');
|
||
const addItemBtn = document.getElementById('addItemBtn');
|
||
const editBalanceBtn = document.getElementById('editBalanceBtn');
|
||
|
||
if (!adminBtn) return;
|
||
|
||
if (isAdmin) {
|
||
adminBtn.textContent = 'Odjava Admin';
|
||
adminBtn.onclick = adminLogout;
|
||
adminBtn.classList.add('active');
|
||
if (addBtn) addBtn.style.display = 'block';
|
||
if (addItemBtn) addItemBtn.style.display = 'inline-flex';
|
||
if (editBalanceBtn) editBalanceBtn.style.display = 'inline-flex';
|
||
} else {
|
||
adminBtn.textContent = 'Prijava Admin';
|
||
adminBtn.onclick = openLoginModal;
|
||
adminBtn.classList.remove('active');
|
||
if (addBtn) addBtn.style.display = 'none';
|
||
if (addItemBtn) addItemBtn.style.display = 'none';
|
||
if (editBalanceBtn) editBalanceBtn.style.display = 'none';
|
||
}
|
||
if (addUnregisteredBtn) addUnregisteredBtn.style.display = 'block';
|
||
updateUIDVisibility();
|
||
}
|
||
|
||
async function adminLogin() {
|
||
const password = document.getElementById('loginPassword').value;
|
||
|
||
if (!password) {
|
||
alert('Prosimo, vnesite geslo');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch('/api/login', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ password })
|
||
});
|
||
|
||
const data = await response.json();
|
||
|
||
if (data.success) {
|
||
isAdmin = true;
|
||
closeLoginModal();
|
||
updateAdminUI();
|
||
loadParticipants();
|
||
loadStore();
|
||
alert('Uspešna prijava!');
|
||
} else {
|
||
alert('Napaka: ' + data.message);
|
||
}
|
||
} catch (error) {
|
||
alert('Napaka: ' + error.message);
|
||
}
|
||
}
|
||
|
||
async function adminLogout() {
|
||
try {
|
||
const response = await fetch('/api/logout', {
|
||
method: 'POST'
|
||
});
|
||
|
||
if (response.ok) {
|
||
isAdmin = false;
|
||
updateAdminUI();
|
||
loadParticipants();
|
||
loadStore();
|
||
alert('Odjavljen');
|
||
}
|
||
} catch (error) {
|
||
console.error('Logout error:', error);
|
||
}
|
||
}
|
||
|
||
function openLoginModal() {
|
||
document.getElementById('loginModal').classList.add('show');
|
||
document.getElementById('loginPassword').focus();
|
||
}
|
||
|
||
function closeLoginModal() {
|
||
document.getElementById('loginModal').classList.remove('show');
|
||
document.getElementById('loginPassword').value = '';
|
||
}
|
||
|
||
function handleLoginKeypress(event) {
|
||
if (event.key === 'Enter') {
|
||
adminLogin();
|
||
}
|
||
}
|
||
|
||
// ===== PARTICIPANT MANAGEMENT =====
|
||
async function loadParticipants() {
|
||
try {
|
||
const response = await fetch('/api/participants');
|
||
const participants = await response.json();
|
||
|
||
const tbody = document.getElementById('participantsTable');
|
||
tbody.innerHTML = '';
|
||
|
||
participants.forEach(p => {
|
||
const row = document.createElement('tr');
|
||
const dateAdded = p.date_added ? p.date_added.split('T')[0] : '-';
|
||
const expiration = p.expiration_date ? p.expiration_date.split('T')[0] : '-';
|
||
|
||
// Check if card is expired
|
||
let isExpired = false;
|
||
if (expiration !== '-') {
|
||
const expirationDate = new Date(expiration);
|
||
const today = new Date();
|
||
today.setHours(0, 0, 0, 0);
|
||
isExpired = expirationDate < today;
|
||
}
|
||
|
||
const keyUID = p.key_uid ? p.key_uid : '-';
|
||
|
||
const actionButtons = isAdmin ? `
|
||
<button class="action-btn edit-btn" onclick="openEditModal(${p.id}, '${p.name}', '${p.card_uid || ''}', '${p.key_uid || ''}', '${expiration}')">Uredi</button>
|
||
<button class="action-btn delete-btn" onclick="deleteParticipant(${p.id})">Izbriši</button>
|
||
` : '';
|
||
|
||
row.innerHTML = `
|
||
<td>${p.id}</td>
|
||
<td>${p.name}</td>
|
||
<td>${p.card_uid || '-'}</td>
|
||
<td>${keyUID}</td>
|
||
<td>${expiration}</td>
|
||
<td>${dateAdded}</td>
|
||
<td>${actionButtons || '-'}</td>
|
||
`;
|
||
|
||
// Add expired class if card is expired
|
||
if (isExpired) {
|
||
row.classList.add('expired');
|
||
}
|
||
|
||
tbody.appendChild(row);
|
||
});
|
||
|
||
// Update participant filter dropdown
|
||
updateParticipantFilter(participants);
|
||
updateUIDVisibility();
|
||
} catch (error) {
|
||
console.error('Error loading participants:', error);
|
||
}
|
||
}
|
||
|
||
function updateParticipantFilter(participants) {
|
||
const filterSelect = document.getElementById('participantFilter');
|
||
const currentValue = filterSelect.value;
|
||
|
||
// Clear existing options except the first one
|
||
filterSelect.innerHTML = '<option value="">Vsi udeleženci</option>';
|
||
|
||
// Sort participants by name and add to dropdown
|
||
participants
|
||
.sort((a, b) => a.name.localeCompare(b.name))
|
||
.forEach(p => {
|
||
const option = document.createElement('option');
|
||
option.value = p.name;
|
||
option.textContent = p.name;
|
||
filterSelect.appendChild(option);
|
||
});
|
||
|
||
// Restore previous selection if it still exists
|
||
filterSelect.value = currentValue;
|
||
}
|
||
|
||
async function addParticipant() {
|
||
const name = document.getElementById('inputName').value.trim();
|
||
const cardUID = document.getElementById('inputCardUID').value.trim();
|
||
const keyUID = document.getElementById('inputKeyUID').value.trim();
|
||
const expiration = document.getElementById('inputExpiration').value;
|
||
|
||
if (!name) {
|
||
alert('Prosimo, izpolnite polje za ime');
|
||
return;
|
||
}
|
||
|
||
if (!cardUID && !keyUID) {
|
||
alert('Prosimo, vnesite vsaj UID kartice ali UID ključa');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch('/api/participants', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
name: name,
|
||
card_uid: cardUID || null,
|
||
key_uid: keyUID || null,
|
||
expiration_date: expiration || null
|
||
})
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const errorData = await response.json();
|
||
alert('Napaka: ' + (errorData.message || 'Unknown error'));
|
||
return;
|
||
}
|
||
|
||
const data = await response.json();
|
||
if (data.success) {
|
||
alert('Udeleženec je bil uspešno dodan');
|
||
closeAddParticipantModal();
|
||
loadParticipants();
|
||
} else {
|
||
alert('Napaka: ' + data.message);
|
||
}
|
||
} catch (error) {
|
||
alert('Napaka: ' + error.message);
|
||
}
|
||
}
|
||
|
||
function openEditModal(id, name, cardUID, keyUID, expiration) {
|
||
editingParticipantId = id;
|
||
document.getElementById('editName').value = name;
|
||
document.getElementById('editCardUID').value = cardUID;
|
||
document.getElementById('editKeyUID').value = keyUID;
|
||
document.getElementById('editExpiration').value = expiration !== '-' ? expiration : '';
|
||
document.getElementById('editModal').classList.add('show');
|
||
}
|
||
|
||
function openAddParticipantModal() {
|
||
document.getElementById('addParticipantModal').classList.add('show');
|
||
}
|
||
|
||
function closeAddParticipantModal() {
|
||
document.getElementById('addParticipantModal').classList.remove('show');
|
||
document.getElementById('inputName').value = '';
|
||
document.getElementById('inputCardUID').value = '';
|
||
document.getElementById('inputKeyUID').value = '';
|
||
document.getElementById('inputExpiration').value = '';
|
||
}
|
||
|
||
function openAddUnregisteredModal() {
|
||
document.getElementById('addUnregisteredModal').classList.add('show');
|
||
document.getElementById('inputUnregisteredName').focus();
|
||
}
|
||
|
||
function closeAddUnregisteredModal() {
|
||
document.getElementById('addUnregisteredModal').classList.remove('show');
|
||
document.getElementById('inputUnregisteredName').value = '';
|
||
}
|
||
|
||
async function addUnregisteredUser() {
|
||
const name = document.getElementById('inputUnregisteredName').value.trim();
|
||
|
||
if (!name) {
|
||
alert('Prosimo, izpolnite polje za ime');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch('/api/logs/unregistered', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
name: name
|
||
})
|
||
});
|
||
|
||
const data = await response.json();
|
||
if (data.success) {
|
||
alert('Neregistrirani udeleženec je bil uspešno dodan');
|
||
closeAddUnregisteredModal();
|
||
refreshLogs();
|
||
} else {
|
||
alert('Napaka: ' + data.message);
|
||
}
|
||
} catch (error) {
|
||
alert('Napaka: ' + error.message);
|
||
}
|
||
}
|
||
|
||
function closeModal() {
|
||
document.getElementById('editModal').classList.remove('show');
|
||
editingParticipantId = null;
|
||
}
|
||
|
||
async function saveChanges() {
|
||
if (!editingParticipantId) return;
|
||
|
||
const name = document.getElementById('editName').value.trim();
|
||
const cardUID = document.getElementById('editCardUID').value.trim();
|
||
const keyUID = document.getElementById('editKeyUID').value.trim();
|
||
const expiration = document.getElementById('editExpiration').value;
|
||
|
||
if (!name) {
|
||
alert('Prosimo, izpolnite polje za ime');
|
||
return;
|
||
}
|
||
|
||
if (!cardUID && !keyUID) {
|
||
alert('Prosimo, vnesite vsaj UID kartice ali UID ključa');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(`/api/participants/${editingParticipantId}`, {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
name: name,
|
||
card_uid: cardUID,
|
||
key_uid: keyUID,
|
||
expiration_date: expiration || null
|
||
})
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const errorData = await response.json();
|
||
alert('Napaka: ' + (errorData.message || 'Unknown error'));
|
||
return;
|
||
}
|
||
|
||
const data = await response.json();
|
||
if (data.success) {
|
||
alert('Udeleženec je bil uspešno posodobljen');
|
||
closeModal();
|
||
loadParticipants();
|
||
} else {
|
||
alert('Napaka: ' + data.message);
|
||
}
|
||
} catch (error) {
|
||
alert('Napaka: ' + error.message);
|
||
}
|
||
}
|
||
|
||
async function deleteParticipant(id) {
|
||
if (!confirm('Ali ste prepričani, da želite izbrisati tega udeleženca?')) return;
|
||
|
||
try {
|
||
const response = await fetch(`/api/participants/${id}`, {
|
||
method: 'DELETE'
|
||
});
|
||
|
||
const data = await response.json();
|
||
if (data.success) {
|
||
alert('Udeleženec je bil uspešno izbrisan');
|
||
loadParticipants();
|
||
} else {
|
||
alert('Napaka: ' + data.message);
|
||
}
|
||
} catch (error) {
|
||
alert('Napaka: ' + error.message);
|
||
}
|
||
}
|
||
|
||
// ===== LOGS MANAGEMENT =====
|
||
async function refreshLogs() {
|
||
try {
|
||
const filter = document.getElementById('filterSelect').value || 'All';
|
||
const response = await fetch(`/api/logs?filter=${filter}`);
|
||
const logs = await response.json();
|
||
|
||
// Apply client-side filtering
|
||
const filteredLogs = applyLogFilters(logs);
|
||
|
||
const tbody = document.getElementById('logsTable');
|
||
tbody.innerHTML = '';
|
||
|
||
if (filteredLogs.length === 0) {
|
||
tbody.innerHTML = `<tr><td colspan="8" style="text-align: center; padding: 20px; color: var(--text-secondary);">Ni najdenih dnevnikov, ki bi ustrezali filtrom</td></tr>`;
|
||
return;
|
||
}
|
||
|
||
// Count occurrences of each name to detect duplicates
|
||
const nameCounts = {};
|
||
filteredLogs.forEach(log => {
|
||
nameCounts[log.name] = (nameCounts[log.name] || 0) + 1;
|
||
});
|
||
|
||
// Count registered vs unregistered users
|
||
let registeredCount = 0;
|
||
let unregisteredCount = 0;
|
||
|
||
filteredLogs.forEach(log => {
|
||
const row = document.createElement('tr');
|
||
const timestamp = log.timestamp;
|
||
const date = fmtDate(timestamp.split('T')[0]);
|
||
const time = timestamp.split('T')[1].substring(0, 5);
|
||
|
||
// Determine status class
|
||
let statusClass = 'pass';
|
||
if (log.scan_result === 'FAIL') {
|
||
statusClass = 'fail';
|
||
} else if (log.scan_result === 'MANUAL') {
|
||
statusClass = 'manual';
|
||
}
|
||
|
||
// Check if this is an unregistered duplicate
|
||
const isDuplicate = nameCounts[log.name] > 1;
|
||
|
||
const actionButtons = isAdmin ? `
|
||
<button class="action-btn delete-btn" onclick="deleteLog(${log.id})">Izbriši</button>
|
||
` : '';
|
||
|
||
row.innerHTML = `
|
||
<td>${log.id}</td>
|
||
<td>${log.name}</td>
|
||
<td>${log.card_uid || '-'}</td>
|
||
<td>${log.key_uid || '-'}</td>
|
||
<td><span class="${statusClass}">${log.scan_result}</span></td>
|
||
<td>${date}</td>
|
||
<td>${time}</td>
|
||
<td>${actionButtons}</td>
|
||
`;
|
||
|
||
// Add duplicate warning class if this is a duplicate unregistered user
|
||
if (isDuplicate && log.scan_result === 'MANUAL') {
|
||
row.classList.add('duplicate-warning');
|
||
}
|
||
|
||
// Count registered vs unregistered
|
||
if (log.scan_result === 'MANUAL') {
|
||
unregisteredCount++;
|
||
} else if (log.scan_result === 'PASS') {
|
||
registeredCount++;
|
||
}
|
||
|
||
tbody.appendChild(row);
|
||
});
|
||
|
||
// Update stats display
|
||
document.getElementById('statRegistered').textContent = registeredCount;
|
||
document.getElementById('statUnregistered').textContent = unregisteredCount;
|
||
|
||
updateUIDVisibility();
|
||
} catch (error) {
|
||
console.error('Error loading logs:', error);
|
||
}
|
||
}
|
||
|
||
function applyLogFilters(logs) {
|
||
const participantFilter = document.getElementById('participantFilter').value;
|
||
const startDateFilter = document.getElementById('startDateFilter').value;
|
||
const endDateFilter = document.getElementById('endDateFilter').value;
|
||
|
||
return logs.filter(log => {
|
||
// Filter by participant
|
||
if (participantFilter && log.name !== participantFilter) {
|
||
return false;
|
||
}
|
||
|
||
// Filter by date range
|
||
if (startDateFilter || endDateFilter) {
|
||
const logDate = log.timestamp.split('T')[0];
|
||
|
||
if (startDateFilter && logDate < startDateFilter) {
|
||
return false;
|
||
}
|
||
|
||
if (endDateFilter && logDate > endDateFilter) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
return true;
|
||
});
|
||
}
|
||
|
||
async function deleteLog(logId) {
|
||
if (!confirm('Ali ste prepričani, da želite izbrisati ta dnevnik?')) return;
|
||
|
||
try {
|
||
const response = await fetch(`/api/logs/${logId}`, {
|
||
method: 'DELETE'
|
||
});
|
||
|
||
const data = await response.json();
|
||
if (data.success) {
|
||
alert('Dnevnik je bil uspešno izbrisan');
|
||
refreshLogs();
|
||
} else {
|
||
alert('Napaka: ' + data.message);
|
||
}
|
||
} catch (error) {
|
||
alert('Napaka: ' + error.message);
|
||
}
|
||
}
|
||
|
||
function resetFilters() {
|
||
document.getElementById('filterSelect').value = 'All';
|
||
document.getElementById('participantFilter').value = '';
|
||
document.getElementById('startDateFilter').value = '';
|
||
document.getElementById('endDateFilter').value = '';
|
||
// Remove active class from all quick filter buttons
|
||
document.querySelectorAll('.btn-quick-filter').forEach(btn => {
|
||
btn.classList.remove('active');
|
||
});
|
||
refreshLogs();
|
||
}
|
||
|
||
function getDateString(date) {
|
||
return date.toISOString().split('T')[0];
|
||
}
|
||
|
||
function filterByToday() {
|
||
const today = new Date();
|
||
document.getElementById('startDateFilter').value = getDateString(today);
|
||
document.getElementById('endDateFilter').value = getDateString(today);
|
||
updateQuickFilterButtons('today');
|
||
refreshLogs();
|
||
}
|
||
|
||
function filterByLastWeek() {
|
||
const today = new Date();
|
||
const lastWeek = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000);
|
||
document.getElementById('startDateFilter').value = getDateString(lastWeek);
|
||
document.getElementById('endDateFilter').value = getDateString(today);
|
||
updateQuickFilterButtons('week');
|
||
refreshLogs();
|
||
}
|
||
|
||
function filterByLastMonth() {
|
||
const today = new Date();
|
||
const lastMonth = new Date(today.getTime() - 30 * 24 * 60 * 60 * 1000);
|
||
document.getElementById('startDateFilter').value = getDateString(lastMonth);
|
||
document.getElementById('endDateFilter').value = getDateString(today);
|
||
updateQuickFilterButtons('month');
|
||
refreshLogs();
|
||
}
|
||
|
||
function filterByLastYear() {
|
||
const today = new Date();
|
||
const lastYear = new Date(today.getTime() - 365 * 24 * 60 * 60 * 1000);
|
||
document.getElementById('startDateFilter').value = getDateString(lastYear);
|
||
document.getElementById('endDateFilter').value = getDateString(today);
|
||
updateQuickFilterButtons('year');
|
||
refreshLogs();
|
||
}
|
||
|
||
function updateQuickFilterButtons(activeFilter) {
|
||
const buttons = document.querySelectorAll('.btn-quick-filter');
|
||
buttons.forEach(btn => btn.classList.remove('active'));
|
||
|
||
if (activeFilter === 'today') buttons[0].classList.add('active');
|
||
else if (activeFilter === 'week') buttons[1].classList.add('active');
|
||
else if (activeFilter === 'month') buttons[2].classList.add('active');
|
||
else if (activeFilter === 'year') buttons[3].classList.add('active');
|
||
}
|
||
|
||
async function exportLogs() {
|
||
try {
|
||
// Get current filter values
|
||
const filter = document.getElementById('filterSelect').value;
|
||
const participant = document.getElementById('participantFilter').value;
|
||
const startDate = document.getElementById('startDateFilter').value;
|
||
const endDate = document.getElementById('endDateFilter').value;
|
||
|
||
// Build query string with filters
|
||
const params = new URLSearchParams({
|
||
filter: filter,
|
||
participant: participant,
|
||
startDate: startDate,
|
||
endDate: endDate
|
||
});
|
||
|
||
const response = await fetch('/api/export?' + params.toString());
|
||
const result = await response.json();
|
||
|
||
if (!result.success) {
|
||
alert('Napaka pri izvozу: ' + result.message);
|
||
return;
|
||
}
|
||
|
||
// Decode base64 file data
|
||
const binaryString = atob(result.data);
|
||
const bytes = new Uint8Array(binaryString.length);
|
||
for (let i = 0; i < binaryString.length; i++) {
|
||
bytes[i] = binaryString.charCodeAt(i);
|
||
}
|
||
|
||
// Create blob and trigger download with file dialog
|
||
const blob = new Blob([bytes], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
|
||
|
||
// Try to use pywebview if available (for desktop app)
|
||
if (window.pywebview && window.pywebview.api) {
|
||
try {
|
||
const success = await window.pywebview.api.save_file(result.filename, result.data);
|
||
if (success) {
|
||
alert('Datoteka je bila uspešno izvožena');
|
||
return;
|
||
} else {
|
||
console.log('PyWebView save was cancelled, falling back to browser download');
|
||
}
|
||
} catch (error) {
|
||
console.log('PyWebView save failed, falling back to browser download', error);
|
||
}
|
||
}
|
||
|
||
// Fallback to browser download
|
||
const url = URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = result.filename;
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
document.body.removeChild(a);
|
||
URL.revokeObjectURL(url);
|
||
|
||
alert('Datoteka je bila uspešno izvožena');
|
||
} catch (error) {
|
||
alert('Napaka: ' + error.message);
|
||
}
|
||
}
|
||
|
||
// ===== PRINT LOGS =====
|
||
function printLogs() {
|
||
const start = document.getElementById('startDateFilter').value;
|
||
const end = document.getElementById('endDateFilter').value;
|
||
const dateRange = document.getElementById('print-date-range');
|
||
|
||
if (start && end) {
|
||
dateRange.textContent = `Obdobje: ${start} – ${end}`;
|
||
} else if (start) {
|
||
dateRange.textContent = `Od: ${start}`;
|
||
} else if (end) {
|
||
dateRange.textContent = `Do: ${end}`;
|
||
} else {
|
||
dateRange.textContent = `Natisnjeno: ${new Date().toLocaleDateString('sl-SI')}`;
|
||
}
|
||
|
||
window.print();
|
||
}
|
||
|
||
// ===== DOCUMENTS =====
|
||
async function loadDocuments() {
|
||
try {
|
||
const response = await fetch('/api/documents');
|
||
const files = await response.json();
|
||
const grid = document.getElementById('documentsGrid');
|
||
|
||
if (files.length === 0) {
|
||
grid.innerHTML = `<p style="color: var(--text-secondary);">Ni dokumentov. Dodajte PDF datoteke v mapo static/PDF.</p>`;
|
||
return;
|
||
}
|
||
|
||
grid.innerHTML = '';
|
||
files.forEach(filename => {
|
||
const card = document.createElement('div');
|
||
card.className = 'document-card';
|
||
const displayName = filename.replace(/\.pdf$/i, '').replace(/_/g, ' ');
|
||
card.innerHTML = `
|
||
<div class="document-icon">
|
||
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
||
<polyline points="14 2 14 8 20 8"></polyline>
|
||
<line x1="9" y1="13" x2="15" y2="13"></line>
|
||
<line x1="9" y1="17" x2="15" y2="17"></line>
|
||
</svg>
|
||
</div>
|
||
<div class="document-name">${displayName}</div>
|
||
<button class="btn btn-primary document-print-btn" onclick="printDocument('${filename}')">Natisni</button>
|
||
`;
|
||
grid.appendChild(card);
|
||
});
|
||
} catch (error) {
|
||
console.error('Error loading documents:', error);
|
||
}
|
||
}
|
||
|
||
function printDocument(filename) {
|
||
const url = `/static/PDF/${encodeURIComponent(filename)}`;
|
||
let iframe = document.getElementById('print-iframe');
|
||
if (!iframe) {
|
||
iframe = document.createElement('iframe');
|
||
iframe.id = 'print-iframe';
|
||
iframe.style.cssText = 'display:none;position:fixed;top:0;left:0;width:0;height:0;';
|
||
document.body.appendChild(iframe);
|
||
}
|
||
iframe.onload = () => iframe.contentWindow.print();
|
||
iframe.src = url;
|
||
}
|
||
|
||
// ===== TAB SWITCHING =====
|
||
function switchTab(tabName, event) {
|
||
// Hide all tabs
|
||
document.querySelectorAll('.tab-panel').forEach(tab => {
|
||
tab.classList.remove('active');
|
||
});
|
||
|
||
// Remove active class from all buttons
|
||
document.querySelectorAll('.tab-nav-btn').forEach(btn => {
|
||
btn.classList.remove('active');
|
||
});
|
||
|
||
// Show selected tab
|
||
document.getElementById(tabName + '-tab').classList.add('active');
|
||
|
||
// Add active class to clicked button
|
||
event.target.classList.add('active');
|
||
|
||
// Load tab-specific data
|
||
if (tabName === 'documents') loadDocuments();
|
||
if (tabName === 'store') {
|
||
// Always return to shop view when switching to store tab
|
||
storeViewIsHistory = false;
|
||
document.getElementById('storeShopView').style.display = '';
|
||
document.getElementById('storeHistoryView').style.display = 'none';
|
||
document.getElementById('storeViewToggleBtn').textContent = 'Zgodovina plačil';
|
||
loadStore();
|
||
}
|
||
}
|
||
|
||
// Close modal when clicking outside
|
||
window.onclick = function(event) {
|
||
const editModal = document.getElementById('editModal');
|
||
const addModal = document.getElementById('addParticipantModal');
|
||
const loginModal = document.getElementById('loginModal');
|
||
const addUnregisteredModal = document.getElementById('addUnregisteredModal');
|
||
const addItemModal = document.getElementById('addItemModal');
|
||
const editBalanceModal = document.getElementById('editBalanceModal');
|
||
|
||
if (event.target === editModal) closeModal();
|
||
if (event.target === addModal) closeAddParticipantModal();
|
||
if (event.target === loginModal) closeLoginModal();
|
||
if (event.target === addUnregisteredModal) closeAddUnregisteredModal();
|
||
if (event.target === addItemModal) closeAddItemModal();
|
||
if (event.target === editBalanceModal) closeEditBalanceModal();
|
||
const checkoutModal = document.getElementById('checkoutModal');
|
||
const monthlyModal = document.getElementById('monthlyModal');
|
||
if (event.target === checkoutModal) closeCheckoutModal();
|
||
if (event.target === monthlyModal) closeMonthlyOverview();
|
||
}
|
||
|
||
// ===== LAST SCAN RESULT =====
|
||
let lastScannedUID = '-';
|
||
let lastScanTimestamp = null;
|
||
|
||
async function updateLastScanResult() {
|
||
try {
|
||
const response = await fetch('/api/last-scan');
|
||
const result = await response.json();
|
||
|
||
const resultCard = document.getElementById('resultCard');
|
||
const uidDiv = resultCard.querySelector('.result-uid');
|
||
const nameDiv = resultCard.querySelector('.result-name');
|
||
const statusDiv = resultCard.querySelector('.result-status');
|
||
|
||
uidDiv.textContent = result.uid;
|
||
nameDiv.textContent = result.name;
|
||
statusDiv.textContent = result.status;
|
||
lastScannedUID = result.uid;
|
||
|
||
// If a new scan just happened, immediately refresh the logs table
|
||
if (result.timestamp && result.timestamp !== lastScanTimestamp) {
|
||
lastScanTimestamp = result.timestamp;
|
||
refreshLogs();
|
||
}
|
||
|
||
// Update card styling based on result
|
||
resultCard.className = 'result-card';
|
||
if (result.status === 'PASS') {
|
||
resultCard.classList.add('pass');
|
||
} else if (result.status === 'FAIL') {
|
||
resultCard.classList.add('fail');
|
||
}
|
||
} catch (error) {
|
||
console.error('Error updating last scan result:', error);
|
||
}
|
||
}
|
||
|
||
function copyUID(evt) {
|
||
if (lastScannedUID === '-') {
|
||
alert('Ni skeniranega UID-ja za kopiranje');
|
||
return;
|
||
}
|
||
|
||
const copyBtn = evt.currentTarget;
|
||
|
||
// Try using clipboard API first
|
||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||
navigator.clipboard.writeText(lastScannedUID).then(() => {
|
||
showCopySuccess(copyBtn);
|
||
}).catch(err => {
|
||
console.error('Clipboard API failed:', err);
|
||
fallbackCopy(lastScannedUID, copyBtn);
|
||
});
|
||
} else {
|
||
// Fallback for older browsers
|
||
fallbackCopy(lastScannedUID, copyBtn);
|
||
}
|
||
}
|
||
|
||
function fallbackCopy(text, copyBtn) {
|
||
const textarea = document.createElement('textarea');
|
||
textarea.value = text;
|
||
textarea.style.position = 'fixed';
|
||
textarea.style.opacity = '0';
|
||
document.body.appendChild(textarea);
|
||
textarea.select();
|
||
try {
|
||
document.execCommand('copy');
|
||
showCopySuccess(copyBtn);
|
||
} catch (err) {
|
||
console.error('Fallback copy failed:', err);
|
||
alert('Napaka pri kopiranju UID-ja');
|
||
}
|
||
document.body.removeChild(textarea);
|
||
}
|
||
|
||
function showCopySuccess(copyBtn) {
|
||
copyBtn.style.color = 'var(--accent-green)';
|
||
setTimeout(() => {
|
||
copyBtn.style.color = '';
|
||
}, 1500);
|
||
}
|
||
|
||
// ===== UID VISIBILITY MANAGEMENT =====
|
||
function updateUIDVisibility() {
|
||
const display = isAdmin ? '' : 'none';
|
||
|
||
// Participants table columns
|
||
const cardUIDHeader = document.getElementById('card-uid-header');
|
||
const keyUIDHeader = document.getElementById('key-uid-header');
|
||
if (cardUIDHeader) cardUIDHeader.style.display = display;
|
||
if (keyUIDHeader) keyUIDHeader.style.display = display;
|
||
|
||
// Logs table columns
|
||
const logsCardUIDHeader = document.getElementById('logs-card-uid-header');
|
||
const logsKeyUIDHeader = document.getElementById('logs-key-uid-header');
|
||
const logsActionsHeader = document.getElementById('logs-actions-header');
|
||
if (logsCardUIDHeader) logsCardUIDHeader.style.display = display;
|
||
if (logsKeyUIDHeader) logsKeyUIDHeader.style.display = display;
|
||
if (logsActionsHeader) logsActionsHeader.style.display = display;
|
||
|
||
// Hide/show corresponding cells in all table rows
|
||
hideTableColumn('participantsTable', 2, !isAdmin); // Card UID column
|
||
hideTableColumn('participantsTable', 3, !isAdmin); // Key UID column
|
||
hideTableColumn('logsTable', 2, !isAdmin); // Card UID column
|
||
hideTableColumn('logsTable', 3, !isAdmin); // Key UID column
|
||
hideTableColumn('logsTable', 7, !isAdmin); // Actions column
|
||
|
||
// Last scan result UID
|
||
const scanUIDContainer = document.getElementById('scan-result-uid-container');
|
||
if (scanUIDContainer) scanUIDContainer.style.display = display;
|
||
}
|
||
|
||
// ===== STORE =====
|
||
let cart = [];
|
||
let storeItems = [];
|
||
let currentBalance = 0;
|
||
let editingItemId = null;
|
||
|
||
async function loadStore() {
|
||
await Promise.all([loadStoreItems(), loadBalance()]);
|
||
}
|
||
|
||
async function loadStoreItems() {
|
||
try {
|
||
const response = await fetch('/api/store/items');
|
||
storeItems = await response.json();
|
||
renderInventory();
|
||
renderCart();
|
||
} catch (error) {
|
||
console.error('Error loading store items:', error);
|
||
}
|
||
}
|
||
|
||
async function loadBalance() {
|
||
try {
|
||
const response = await fetch('/api/store/balance');
|
||
const data = await response.json();
|
||
currentBalance = data.balance;
|
||
document.getElementById('storeBalanceDisplay').textContent = '€' + currentBalance.toFixed(2);
|
||
updateCartTotals();
|
||
} catch (error) {
|
||
console.error('Error loading balance:', error);
|
||
}
|
||
}
|
||
|
||
const ITEM_FALLBACK_SVG = `<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" style="color:var(--text-secondary)"><path d="M6 2L3 6v14a2 2 0 002 2h14a2 2 0 002-2V6l-3-4z"/><line x1="3" y1="6" x2="21" y2="6"/><path d="M16 10a4 4 0 01-8 0"/></svg>`;
|
||
|
||
function renderInventory() {
|
||
const grid = document.getElementById('inventoryGrid');
|
||
if (!grid) return;
|
||
|
||
if (storeItems.length === 0) {
|
||
grid.innerHTML = '<p style="color: var(--text-secondary); grid-column: 1/-1;">Ni artiklov v zalogi.</p>';
|
||
return;
|
||
}
|
||
|
||
grid.innerHTML = '';
|
||
storeItems.forEach(item => {
|
||
const card = document.createElement('div');
|
||
card.className = 'inventory-card' + (item.stock === 0 ? ' out-of-stock' : '');
|
||
const imgHtml = `<div class="inventory-card-img-wrap">${item.image
|
||
? `<img src="${item.image}" alt="${item.name}" class="inventory-card-img">`
|
||
: `<div class="inventory-card-img-placeholder">${ITEM_FALLBACK_SVG}</div>`
|
||
}</div>`;
|
||
card.innerHTML = `
|
||
${imgHtml}
|
||
<div class="inventory-card-body">
|
||
<div class="inventory-card-name">${item.name}</div>
|
||
<div class="inventory-card-price">€${item.price.toFixed(2)}</div>
|
||
<div class="inventory-card-stock ${item.stock === 0 ? 'stock-empty' : ''}">Zaloga: ${item.stock}</div>
|
||
</div>
|
||
<div class="inventory-card-actions">
|
||
<button class="btn btn-primary inventory-add-btn" onclick="addToCart(${item.id})" ${item.stock === 0 ? 'disabled' : ''}>+ Dodaj</button>
|
||
${isAdmin ? `<button class="action-btn edit-btn" onclick="openEditItemModal(${item.id})">Uredi</button>
|
||
<button class="action-btn delete-btn" onclick="deleteStoreItem(${item.id})">Izbriši</button>` : ''}
|
||
</div>
|
||
`;
|
||
grid.appendChild(card);
|
||
});
|
||
}
|
||
|
||
function addToCart(itemId) {
|
||
const item = storeItems.find(i => i.id === itemId);
|
||
if (!item || item.stock === 0) return;
|
||
|
||
const existing = cart.find(c => c.id === itemId);
|
||
const currentQty = existing ? existing.qty : 0;
|
||
|
||
if (currentQty >= item.stock) {
|
||
alert('Ni dovolj zaloge');
|
||
return;
|
||
}
|
||
|
||
if (existing) {
|
||
existing.qty++;
|
||
} else {
|
||
cart.push({ id: item.id, name: item.name, price: item.price, qty: 1 });
|
||
}
|
||
renderCart();
|
||
}
|
||
|
||
function removeFromCart(itemId) {
|
||
const idx = cart.findIndex(c => c.id === itemId);
|
||
if (idx === -1) return;
|
||
if (cart[idx].qty > 1) {
|
||
cart[idx].qty--;
|
||
} else {
|
||
cart.splice(idx, 1);
|
||
}
|
||
renderCart();
|
||
}
|
||
|
||
function clearCart() {
|
||
cart = [];
|
||
renderCart();
|
||
}
|
||
|
||
function renderCart() {
|
||
const container = document.getElementById('cartItems');
|
||
if (!container) return;
|
||
|
||
if (cart.length === 0) {
|
||
container.innerHTML = '<p style="color: var(--text-secondary); text-align: center; padding: 20px;">Košarica je prazna</p>';
|
||
updateCartTotals();
|
||
return;
|
||
}
|
||
|
||
container.innerHTML = '';
|
||
cart.forEach(item => {
|
||
const row = document.createElement('div');
|
||
row.className = 'cart-item';
|
||
row.innerHTML = `
|
||
<div class="cart-item-name">${item.name}</div>
|
||
<div class="cart-item-controls">
|
||
<button class="cart-qty-btn" onclick="removeFromCart(${item.id})">−</button>
|
||
<span class="cart-qty">${item.qty}</span>
|
||
<button class="cart-qty-btn" onclick="addToCart(${item.id})">+</button>
|
||
</div>
|
||
<div class="cart-item-subtotal">€${(item.price * item.qty).toFixed(2)}</div>
|
||
`;
|
||
container.appendChild(row);
|
||
});
|
||
|
||
updateCartTotals();
|
||
}
|
||
|
||
function updateCartTotals() {
|
||
const total = cart.reduce((sum, i) => sum + i.price * i.qty, 0);
|
||
const balanceAfter = currentBalance + total;
|
||
|
||
const totalEl = document.getElementById('cartTotal');
|
||
const balanceAfterEl = document.getElementById('cartBalanceAfter');
|
||
|
||
if (totalEl) totalEl.textContent = '€' + total.toFixed(2);
|
||
if (balanceAfterEl) {
|
||
balanceAfterEl.textContent = '€' + balanceAfter.toFixed(2);
|
||
balanceAfterEl.className = 'cart-balance-after';
|
||
}
|
||
}
|
||
|
||
function processPayment() {
|
||
if (cart.length === 0) {
|
||
alert('Košarica je prazna');
|
||
return;
|
||
}
|
||
const total = cart.reduce((sum, i) => sum + i.price * i.qty, 0);
|
||
const balanceAfter = currentBalance + total;
|
||
|
||
document.getElementById('checkoutFirstName').value = '';
|
||
document.getElementById('checkoutLastName').value = '';
|
||
document.getElementById('checkoutTotal').textContent = '€' + total.toFixed(2);
|
||
document.getElementById('checkoutBalanceAfter').textContent = '€' + balanceAfter.toFixed(2);
|
||
document.getElementById('checkoutModal').classList.add('show');
|
||
document.getElementById('checkoutFirstName').focus();
|
||
}
|
||
|
||
function closeCheckoutModal() {
|
||
document.getElementById('checkoutModal').classList.remove('show');
|
||
}
|
||
|
||
async function confirmCheckout() {
|
||
const firstName = document.getElementById('checkoutFirstName').value.trim();
|
||
const lastName = document.getElementById('checkoutLastName').value.trim();
|
||
|
||
if (!firstName || !lastName) {
|
||
alert('Prosimo, vnesite ime in priimek kupca');
|
||
return;
|
||
}
|
||
|
||
const buyer = firstName + ' ' + lastName;
|
||
const total = cart.reduce((sum, i) => sum + i.price * i.qty, 0);
|
||
const balanceAfter = currentBalance + total;
|
||
|
||
closeCheckoutModal();
|
||
|
||
try {
|
||
const cartItems = cart.map(cartItem => {
|
||
const storeItem = storeItems.find(i => i.id === cartItem.id);
|
||
return {
|
||
id: cartItem.id,
|
||
name: cartItem.name,
|
||
price: cartItem.price,
|
||
qty: cartItem.qty,
|
||
new_stock: storeItem ? storeItem.stock - cartItem.qty : 0,
|
||
image: storeItem ? storeItem.image : null
|
||
};
|
||
});
|
||
|
||
const response = await fetch('/api/store/payments', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
buyer,
|
||
items: cart.map(i => ({ name: i.name, qty: i.qty, price: i.price })),
|
||
cart_items: cartItems,
|
||
total,
|
||
balance_before: currentBalance,
|
||
balance_after: balanceAfter
|
||
})
|
||
});
|
||
|
||
const data = await response.json();
|
||
if (!data.success) { alert('Napaka: ' + data.message); return; }
|
||
|
||
const payment = {
|
||
buyer,
|
||
items: cart.map(i => ({ name: i.name, qty: i.qty, price: i.price })),
|
||
total,
|
||
balance_before: currentBalance,
|
||
balance_after: balanceAfter,
|
||
timestamp: new Date().toISOString()
|
||
};
|
||
|
||
cart = [];
|
||
await loadStore();
|
||
printReceipt(payment);
|
||
} catch (error) {
|
||
alert('Napaka: ' + error.message);
|
||
}
|
||
}
|
||
|
||
let currentItemImage = null;
|
||
|
||
function openAddItemModal() {
|
||
editingItemId = null;
|
||
currentItemImage = null;
|
||
document.getElementById('addItemModalTitle').textContent = 'Dodaj artikel';
|
||
document.getElementById('addItemSubmitBtn').textContent = 'Dodaj';
|
||
document.getElementById('itemName').value = '';
|
||
document.getElementById('itemPrice').value = '';
|
||
document.getElementById('itemStock').value = '';
|
||
document.getElementById('itemImageInput').value = '';
|
||
setImagePreview(null);
|
||
document.getElementById('addItemModal').classList.add('show');
|
||
document.getElementById('itemName').focus();
|
||
}
|
||
|
||
function openEditItemModal(itemId) {
|
||
const item = storeItems.find(i => i.id === itemId);
|
||
if (!item) return;
|
||
editingItemId = itemId;
|
||
currentItemImage = item.image || null;
|
||
document.getElementById('addItemModalTitle').textContent = 'Uredi artikel';
|
||
document.getElementById('addItemSubmitBtn').textContent = 'Shrani';
|
||
document.getElementById('itemName').value = item.name;
|
||
document.getElementById('itemPrice').value = item.price;
|
||
document.getElementById('itemStock').value = item.stock;
|
||
document.getElementById('itemImageInput').value = '';
|
||
setImagePreview(item.image || null);
|
||
document.getElementById('addItemModal').classList.add('show');
|
||
}
|
||
|
||
function closeAddItemModal() {
|
||
document.getElementById('addItemModal').classList.remove('show');
|
||
editingItemId = null;
|
||
currentItemImage = null;
|
||
}
|
||
|
||
function handleImageUpload(event) {
|
||
const file = event.target.files[0];
|
||
if (!file) return;
|
||
const reader = new FileReader();
|
||
reader.onload = (e) => {
|
||
currentItemImage = e.target.result;
|
||
setImagePreview(currentItemImage);
|
||
};
|
||
reader.readAsDataURL(file);
|
||
}
|
||
|
||
function setImagePreview(src) {
|
||
const preview = document.getElementById('imagePreview');
|
||
const placeholder = document.getElementById('imageUploadPlaceholder');
|
||
const removeBtn = document.getElementById('removeImageBtn');
|
||
if (src) {
|
||
preview.src = src;
|
||
preview.style.display = 'block';
|
||
placeholder.style.display = 'none';
|
||
removeBtn.style.display = 'inline-block';
|
||
} else {
|
||
preview.src = '';
|
||
preview.style.display = 'none';
|
||
placeholder.style.display = 'flex';
|
||
removeBtn.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
function removeItemImage() {
|
||
currentItemImage = null;
|
||
document.getElementById('itemImageInput').value = '';
|
||
setImagePreview(null);
|
||
}
|
||
|
||
async function submitItem() {
|
||
const name = document.getElementById('itemName').value.trim();
|
||
const price = parseFloat(document.getElementById('itemPrice').value);
|
||
const stock = parseInt(document.getElementById('itemStock').value, 10);
|
||
|
||
if (!name) { alert('Vnesite ime artikla'); return; }
|
||
if (isNaN(price) || price < 0) { alert('Vnesite veljavno ceno'); return; }
|
||
if (isNaN(stock) || stock < 0) { alert('Vnesite veljavno zalogo'); return; }
|
||
|
||
try {
|
||
let response;
|
||
if (editingItemId) {
|
||
response = await fetch(`/api/store/items/${editingItemId}`, {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ name, price, stock, image: currentItemImage })
|
||
});
|
||
} else {
|
||
response = await fetch('/api/store/items', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ name, price, stock, image: currentItemImage })
|
||
});
|
||
}
|
||
|
||
const data = await response.json();
|
||
if (data.success) {
|
||
closeAddItemModal();
|
||
await loadStoreItems();
|
||
} else {
|
||
alert('Napaka: ' + data.message);
|
||
}
|
||
} catch (error) {
|
||
alert('Napaka: ' + error.message);
|
||
}
|
||
}
|
||
|
||
async function deleteStoreItem(itemId) {
|
||
if (!confirm('Izbriši artikel?')) return;
|
||
try {
|
||
const response = await fetch(`/api/store/items/${itemId}`, { method: 'DELETE' });
|
||
const data = await response.json();
|
||
if (data.success) {
|
||
cart = cart.filter(c => c.id !== itemId);
|
||
await loadStoreItems();
|
||
} else {
|
||
alert('Napaka: ' + data.message);
|
||
}
|
||
} catch (error) {
|
||
alert('Napaka: ' + error.message);
|
||
}
|
||
}
|
||
|
||
function openEditBalanceModal() {
|
||
document.getElementById('balanceInput').value = currentBalance.toFixed(2);
|
||
document.getElementById('editBalanceModal').classList.add('show');
|
||
document.getElementById('balanceInput').focus();
|
||
}
|
||
|
||
function closeEditBalanceModal() {
|
||
document.getElementById('editBalanceModal').classList.remove('show');
|
||
}
|
||
|
||
async function saveBalance() {
|
||
const amount = parseFloat(document.getElementById('balanceInput').value);
|
||
if (isNaN(amount)) { alert('Vnesite veljavno vrednost'); return; }
|
||
|
||
try {
|
||
const response = await fetch('/api/store/balance', {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ balance: amount })
|
||
});
|
||
const data = await response.json();
|
||
if (data.success) {
|
||
closeEditBalanceModal();
|
||
await loadBalance();
|
||
} else {
|
||
alert('Napaka: ' + data.message);
|
||
}
|
||
} catch (error) {
|
||
alert('Napaka: ' + error.message);
|
||
}
|
||
}
|
||
|
||
// ===== PAYMENT HISTORY =====
|
||
let storeViewIsHistory = false;
|
||
|
||
function toggleStoreView() {
|
||
storeViewIsHistory = !storeViewIsHistory;
|
||
document.getElementById('storeShopView').style.display = storeViewIsHistory ? 'none' : '';
|
||
document.getElementById('storeHistoryView').style.display = storeViewIsHistory ? '' : 'none';
|
||
document.getElementById('storeViewToggleBtn').textContent = storeViewIsHistory ? '← Nazaj v trgovino' : 'Zgodovina plačil';
|
||
if (storeViewIsHistory) loadPaymentHistory();
|
||
}
|
||
|
||
function togglePaymentHistory() { toggleStoreView(); }
|
||
|
||
async function loadPaymentHistory() {
|
||
try {
|
||
const response = await fetch('/api/store/payments');
|
||
const payments = await response.json();
|
||
const tbody = document.getElementById('paymentHistoryTable');
|
||
if (!tbody) return;
|
||
|
||
const clearBtn = document.getElementById('clearHistoryBtn');
|
||
if (clearBtn) clearBtn.style.display = isAdmin ? 'inline-block' : 'none';
|
||
|
||
if (payments.length === 0) {
|
||
tbody.innerHTML = `<tr><td colspan="9" style="text-align:center;padding:20px;color:var(--text-secondary);">Ni zabeleženih plačil</td></tr>`;
|
||
return;
|
||
}
|
||
|
||
tbody.innerHTML = '';
|
||
[...payments].reverse().forEach(p => {
|
||
const ts = p.timestamp;
|
||
const date = fmtDate(ts.split('T')[0]);
|
||
const time = ts.split('T')[1].substring(0, 5);
|
||
const itemsSummary = p.items.map(i => `${i.name} ×${i.qty}`).join(', ');
|
||
const row = document.createElement('tr');
|
||
row.innerHTML = `
|
||
<td>${p.id}</td>
|
||
<td style="font-weight:600;">${p.buyer || '-'}</td>
|
||
<td>${date}</td>
|
||
<td>${time}</td>
|
||
<td style="max-width:200px;word-break:break-word;">${itemsSummary}</td>
|
||
<td style="font-weight:700;color:var(--accent-blue);">€${p.total.toFixed(2)}</td>
|
||
<td>€${p.balance_before.toFixed(2)}</td>
|
||
<td>€${p.balance_after.toFixed(2)}</td>
|
||
<td><button class="action-btn edit-btn" onclick="printReceipt(${JSON.stringify(p).replace(/"/g, '"')})">🖨 Natisni</button></td>
|
||
`;
|
||
tbody.appendChild(row);
|
||
});
|
||
} catch (error) {
|
||
console.error('Error loading payment history:', error);
|
||
}
|
||
}
|
||
|
||
// ===== RECEIPT PRINT =====
|
||
const LOGO_URL = '/static/assets/logo.png';
|
||
|
||
function printReceipt(payment) {
|
||
const ts = payment.timestamp || new Date().toISOString();
|
||
const date = fmtDate(ts.split('T')[0]);
|
||
const time = ts.split('T')[1] ? ts.split('T')[1].substring(0, 5) : '';
|
||
const rows = payment.items.map(i =>
|
||
`<tr><td class="rp-td-name">${i.name}</td><td class="rp-td-qty">${i.qty}×</td><td class="rp-td-price">€${i.price.toFixed(2)}</td><td class="rp-td-sub">€${(i.price * i.qty).toFixed(2)}</td></tr>`
|
||
).join('');
|
||
|
||
const html = `
|
||
<div class="rp-page">
|
||
<div class="rp-logo-block">
|
||
<img src="${LOGO_URL}" alt="Logo" class="rp-logo" onerror="this.style.display='none'">
|
||
</div>
|
||
<div class="rp-divider"></div>
|
||
<div class="rp-title">POTRDILO O PLAČILU</div>
|
||
<div class="rp-meta">
|
||
<span>${date} ${time}</span>
|
||
<span>Prodajalec: <strong>${payment.buyer || '-'}</strong></span>
|
||
</div>
|
||
<div class="rp-divider-dashed"></div>
|
||
<table class="rp-table">
|
||
<thead><tr><th>Artikel</th><th>Kol.</th><th>Cena</th><th style="text-align:right;">Znesek</th></tr></thead>
|
||
<tbody>${rows}</tbody>
|
||
</table>
|
||
<div class="rp-divider"></div>
|
||
<div class="rp-total">
|
||
<span>SKUPAJ</span>
|
||
<span>€${payment.total.toFixed(2)}</span>
|
||
</div>
|
||
<div class="rp-divider-dashed"></div>
|
||
<div class="rp-footer">Hvala za nakup!</div>
|
||
</div>
|
||
`;
|
||
|
||
const area = document.getElementById('receiptPrintArea');
|
||
area.innerHTML = html;
|
||
area.style.display = 'block';
|
||
document.body.classList.add('printing-receipt');
|
||
window.print();
|
||
document.body.classList.remove('printing-receipt');
|
||
area.style.display = 'none';
|
||
}
|
||
|
||
// ===== MONTHLY OVERVIEW =====
|
||
let allPaymentsCache = [];
|
||
|
||
function openMonthlyOverview() {
|
||
const now = new Date();
|
||
const ym = now.toISOString().substring(0, 7);
|
||
document.getElementById('monthlyPickerInput').value = ym;
|
||
document.getElementById('monthlyModal').classList.add('show');
|
||
renderMonthlyOverview();
|
||
}
|
||
|
||
function closeMonthlyOverview() {
|
||
document.getElementById('monthlyModal').classList.remove('show');
|
||
}
|
||
|
||
async function renderMonthlyOverview() {
|
||
const picker = document.getElementById('monthlyPickerInput').value;
|
||
if (!picker) return;
|
||
|
||
try {
|
||
const response = await fetch('/api/store/payments');
|
||
allPaymentsCache = await response.json();
|
||
} catch (e) { return; }
|
||
|
||
const filtered = allPaymentsCache.filter(p => p.timestamp.startsWith(picker));
|
||
const content = document.getElementById('monthlyOverviewContent');
|
||
|
||
if (filtered.length === 0) {
|
||
content.innerHTML = '<p style="color:var(--text-secondary);padding:16px 0;">Ni plačil v izbranem mesecu.</p>';
|
||
return;
|
||
}
|
||
|
||
const grandTotal = filtered.reduce((s, p) => s + p.total, 0);
|
||
|
||
// Per-item breakdown
|
||
const itemTotals = {};
|
||
filtered.forEach(p => p.items.forEach(i => {
|
||
if (!itemTotals[i.name]) itemTotals[i.name] = { qty: 0, revenue: 0 };
|
||
itemTotals[i.name].qty += i.qty;
|
||
itemTotals[i.name].revenue += i.price * i.qty;
|
||
}));
|
||
|
||
const itemRows = Object.entries(itemTotals).map(([name, v]) =>
|
||
`<tr><td>${name}</td><td style="text-align:center;">${v.qty}</td><td style="text-align:right;font-weight:600;">€${v.revenue.toFixed(2)}</td></tr>`
|
||
).join('');
|
||
|
||
const txRows = [...filtered].reverse().map(p => {
|
||
const date = fmtDate(p.timestamp.split('T')[0]);
|
||
const time = p.timestamp.split('T')[1].substring(0, 5);
|
||
return `<tr><td>${date}</td><td>${time}</td><td style="font-weight:600;">${p.buyer || '-'}</td><td>${p.items.map(i => `${i.name} ×${i.qty}`).join(', ')}</td><td style="text-align:right;font-weight:700;color:var(--accent-blue);">€${p.total.toFixed(2)}</td></tr>`;
|
||
}).join('');
|
||
|
||
content.innerHTML = `
|
||
<div class="monthly-summary-total">Skupaj prihodek: <strong>€${grandTotal.toFixed(2)}</strong> (${filtered.length} transakcij)</div>
|
||
<h4 class="monthly-section-title">Po artiklih</h4>
|
||
<div class="table-container" style="margin-bottom:16px;">
|
||
<table><thead><tr><th>Artikel</th><th style="text-align:center;">Količina</th><th style="text-align:right;">Prihodek</th></tr></thead>
|
||
<tbody>${itemRows}</tbody></table>
|
||
</div>
|
||
<h4 class="monthly-section-title">Vse transakcije</h4>
|
||
<div class="table-container">
|
||
<table><thead><tr><th>Datum</th><th>Čas</th><th>Prodajalec</th><th>Artikli</th><th style="text-align:right;">Skupaj</th></tr></thead>
|
||
<tbody>${txRows}</tbody></table>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function printMonthlyOverview() {
|
||
const picker = document.getElementById('monthlyPickerInput').value;
|
||
if (!picker || !allPaymentsCache.length) return;
|
||
|
||
const filtered = allPaymentsCache.filter(p => p.timestamp.startsWith(picker));
|
||
if (filtered.length === 0) { alert('Ni podatkov za izbrani mesec.'); return; }
|
||
|
||
const grandTotal = filtered.reduce((s, p) => s + p.total, 0);
|
||
|
||
const itemTotals = {};
|
||
filtered.forEach(p => p.items.forEach(i => {
|
||
if (!itemTotals[i.name]) itemTotals[i.name] = { qty: 0, revenue: 0 };
|
||
itemTotals[i.name].qty += i.qty;
|
||
itemTotals[i.name].revenue += i.price * i.qty;
|
||
}));
|
||
|
||
const itemRows = Object.entries(itemTotals).map(([name, v]) =>
|
||
`<tr><td>${name}</td><td style="text-align:center;">${v.qty}</td><td style="text-align:right;font-weight:600;">€${v.revenue.toFixed(2)}</td></tr>`
|
||
).join('');
|
||
|
||
const txRows = [...filtered].reverse().map(p => {
|
||
const date = fmtDate(p.timestamp.split('T')[0]);
|
||
const time = p.timestamp.split('T')[1].substring(0, 5);
|
||
const items = p.items.map(i => `${i.name} ×${i.qty} (€${(i.price*i.qty).toFixed(2)})`).join(', ');
|
||
return `<tr><td>${date}</td><td>${time}</td><td style="font-weight:600;">${p.buyer || '-'}</td><td>${items}</td><td style="text-align:right;font-weight:700;">€${p.total.toFixed(2)}</td></tr>`;
|
||
}).join('');
|
||
|
||
const [year, month] = picker.split('-');
|
||
const monthName = new Date(year, month - 1, 1).toLocaleDateString('sl-SI', { month: 'long', year: 'numeric' });
|
||
const printDate = fmtDate(new Date().toISOString().split('T')[0]);
|
||
|
||
const html = `
|
||
<div class="rp-page rp-monthly">
|
||
<div class="rp-logo-block">
|
||
<img src="${LOGO_URL}" alt="Logo" class="rp-logo" onerror="this.style.display='none'">
|
||
</div>
|
||
<div class="rp-divider"></div>
|
||
<div class="rp-title">MESEČNI PREGLED</div>
|
||
<div class="rp-meta"><span>${monthName}</span><span>Natisnjeno: ${printDate}</span></div>
|
||
<div class="rp-divider-dashed"></div>
|
||
<div class="rp-monthly-kpi">
|
||
<div class="rp-kpi-box"><div class="rp-kpi-label">Skupaj prihodek</div><div class="rp-kpi-value">€${grandTotal.toFixed(2)}</div></div>
|
||
<div class="rp-kpi-box"><div class="rp-kpi-label">Št. transakcij</div><div class="rp-kpi-value">${filtered.length}</div></div>
|
||
</div>
|
||
<div class="rp-section-title">Po artiklih</div>
|
||
<table class="rp-table">
|
||
<thead><tr><th>Artikel</th><th style="text-align:center;">Količina</th><th style="text-align:right;">Prihodek</th></tr></thead>
|
||
<tbody>${itemRows}</tbody>
|
||
</table>
|
||
<div class="rp-section-title" style="margin-top:16px;">Vse transakcije</div>
|
||
<table class="rp-table">
|
||
<thead><tr><th>Datum</th><th>Čas</th><th>Prodajalec</th><th>Artikli</th><th style="text-align:right;">Skupaj</th></tr></thead>
|
||
<tbody>${txRows}</tbody>
|
||
</table>
|
||
</div>
|
||
`;
|
||
|
||
const area = document.getElementById('receiptPrintArea');
|
||
area.innerHTML = html;
|
||
area.style.display = 'block';
|
||
document.body.classList.add('printing-receipt');
|
||
window.print();
|
||
document.body.classList.remove('printing-receipt');
|
||
area.style.display = 'none';
|
||
}
|
||
|
||
async function clearPaymentHistory() {
|
||
if (!confirm('Izbriši celotno zgodovino plačil?')) return;
|
||
try {
|
||
const response = await fetch('/api/store/payments', { method: 'DELETE' });
|
||
const data = await response.json();
|
||
if (data.success) loadPaymentHistory();
|
||
else alert('Napaka: ' + data.message);
|
||
} catch (error) {
|
||
alert('Napaka: ' + error.message);
|
||
}
|
||
}
|
||
|
||
function hideTableColumn(tableId, columnIndex, hide) {
|
||
const table = document.getElementById(tableId);
|
||
if (!table) return;
|
||
|
||
const rows = table.getElementsByTagName('tr');
|
||
for (let row of rows) {
|
||
const cell = row.cells[columnIndex];
|
||
if (cell) {
|
||
cell.style.display = hide ? 'none' : '';
|
||
}
|
||
}
|
||
}
|