Shop update

This commit is contained in:
2026-05-30 18:46:43 +02:00
parent c2797ee401
commit 44c45f5410
8 changed files with 1428 additions and 17 deletions
+375
View File
@@ -1220,6 +1220,381 @@ table tbody tr.duplicate-warning:hover {
font-size: 13px;
}
/* ===== STORE ===== */
.store-balance-display {
display: flex;
align-items: center;
gap: 10px;
}
.store-balance-label {
font-size: 13px;
font-weight: 600;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.3px;
}
.store-balance-value {
font-size: 20px;
font-weight: 700;
color: var(--accent-blue);
min-width: 80px;
}
.store-tab-scroll {
display: flex;
flex-direction: column;
gap: 0;
min-height: 0;
}
.store-layout {
display: grid;
grid-template-columns: 1fr 340px;
align-items: stretch;
gap: 20px;
flex: 1;
min-height: 0;
}
.store-inventory,
.store-cart {
display: flex;
flex-direction: column;
gap: 12px;
overflow: hidden;
}
.store-inventory {
overflow-y: auto;
}
.store-section-header {
display: flex;
justify-content: space-between;
align-items: center;
flex-shrink: 0;
}
.store-section-header h3 {
font-size: 13px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--accent-blue);
border-bottom: 2px solid var(--accent-blue);
padding-bottom: 6px;
flex: 1;
margin-right: 12px;
}
.store-section-header .btn {
width: auto;
margin: 0;
padding: 8px 14px;
font-size: 12px;
white-space: nowrap;
}
.inventory-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 12px;
overflow-y: auto;
padding-bottom: 4px;
}
.inventory-card {
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: 8px;
overflow: hidden;
display: flex;
flex-direction: column;
transition: box-shadow 0.15s ease;
}
.inventory-card:hover {
box-shadow: var(--shadow);
}
.inventory-card.out-of-stock {
opacity: 0.55;
}
.inventory-card-img {
width: 100%;
height: 120px;
object-fit: cover;
display: block;
background: var(--bg-quaternary);
}
.inventory-card-img-placeholder {
width: 100%;
height: 120px;
display: flex;
align-items: center;
justify-content: center;
background: var(--bg-quaternary);
border-bottom: 1px solid var(--border-color);
}
.inventory-card-body {
padding: 10px 12px 6px;
display: flex;
flex-direction: column;
gap: 4px;
flex: 1;
}
.inventory-card-name {
font-size: 13px;
font-weight: 600;
color: var(--text-primary);
word-break: break-word;
line-height: 1.3;
}
.inventory-card-price {
font-size: 17px;
font-weight: 700;
color: var(--accent-blue);
}
.inventory-card-stock {
font-size: 11px;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.3px;
}
.inventory-card-stock.stock-empty {
color: var(--accent-red);
font-weight: 600;
}
.inventory-card-actions {
display: flex;
flex-wrap: wrap;
gap: 6px;
padding: 8px 12px 12px;
}
.inventory-add-btn {
width: 100%;
margin: 0;
padding: 8px;
font-size: 12px;
}
.inventory-add-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.inventory-card-actions .action-btn {
flex: 1;
margin: 0;
padding: 6px 8px;
font-size: 11px;
text-align: center;
}
/* Image upload */
.image-upload-area {
border: 2px dashed var(--border-color);
border-radius: 8px;
cursor: pointer;
overflow: hidden;
min-height: 100px;
display: flex;
align-items: center;
justify-content: center;
transition: border-color 0.2s ease;
background: var(--bg-tertiary);
}
.image-upload-area:hover {
border-color: var(--accent-blue);
}
.image-upload-placeholder {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
padding: 20px;
color: var(--text-secondary);
font-size: 12px;
font-weight: 500;
text-align: center;
}
.btn-remove-image {
margin-top: 6px;
padding: 4px 10px;
background: var(--accent-red);
color: #fff;
border: none;
border-radius: 4px;
font-size: 11px;
font-weight: 600;
cursor: pointer;
text-transform: uppercase;
letter-spacing: 0.3px;
}
.btn-remove-image:hover {
background: var(--accent-red-hover);
}
/* Payment history panel */
.payment-history-panel {
border-top: 2px solid var(--border-color);
padding-top: 16px;
flex-shrink: 0;
max-height: 320px;
overflow-y: auto;
}
/* Cart */
.store-cart {
background: var(--bg-quaternary);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 16px;
overflow: hidden;
align-self: stretch;
}
.cart-items {
flex: 1;
overflow-y: auto;
min-height: 0;
}
.cart-item {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 0;
border-bottom: 1px solid var(--border-color);
}
.cart-item-name {
flex: 1;
font-size: 13px;
font-weight: 500;
color: var(--text-primary);
word-break: break-word;
}
.cart-item-controls {
display: flex;
align-items: center;
gap: 6px;
flex-shrink: 0;
}
.cart-qty-btn {
width: 26px;
height: 26px;
border: 1px solid var(--border-color);
border-radius: 4px;
background: var(--bg-secondary);
color: var(--text-primary);
font-size: 16px;
font-weight: 600;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.15s ease;
line-height: 1;
padding: 0;
}
.cart-qty-btn:hover {
background: var(--accent-blue);
color: #fff;
border-color: var(--accent-blue);
}
.cart-qty {
min-width: 22px;
text-align: center;
font-size: 13px;
font-weight: 600;
}
.cart-item-subtotal {
font-size: 13px;
font-weight: 700;
color: var(--accent-blue);
min-width: 52px;
text-align: right;
flex-shrink: 0;
}
.cart-total {
border-top: 2px solid var(--border-color);
padding-top: 12px;
margin-top: 4px;
display: flex;
flex-direction: column;
gap: 6px;
flex-shrink: 0;
}
.cart-total-row {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 14px;
font-weight: 600;
color: var(--text-primary);
}
.cart-balance-row {
font-size: 12px;
color: var(--text-secondary);
font-weight: 500;
}
.cart-total-amount {
font-size: 20px;
font-weight: 700;
color: var(--accent-blue);
}
.cart-balance-after {
font-weight: 600;
color: var(--accent-green);
}
.cart-balance-after.negative {
color: var(--accent-red);
}
.cart-pay-btn {
margin: 8px 0 0 0;
width: 100%;
padding: 12px;
font-size: 14px;
}
@media (max-width: 900px) {
.store-layout {
grid-template-columns: 1fr;
}
.store-cart {
min-height: 300px;
}
}
/* Print Styles */
@media print {
body {
+428 -17
View File
@@ -32,21 +32,26 @@ 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; // Exit if button doesn't exist yet
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';
}
// Show unregistered button to both admin and non-admin users
if (addUnregisteredBtn) addUnregisteredBtn.style.display = 'block';
updateUIDVisibility();
}
@@ -73,6 +78,7 @@ async function adminLogin() {
closeLoginModal();
updateAdminUI();
loadParticipants();
loadStore();
alert('Uspešna prijava!');
} else {
alert('Napaka: ' + data.message);
@@ -92,6 +98,7 @@ async function adminLogout() {
isAdmin = false;
updateAdminUI();
loadParticipants();
loadStore();
alert('Odjavljen');
}
} catch (error) {
@@ -721,6 +728,7 @@ function switchTab(tabName, event) {
// Load tab-specific data
if (tabName === 'documents') loadDocuments();
if (tabName === 'store') loadStore();
}
// Close modal when clicking outside
@@ -729,22 +737,15 @@ window.onclick = function(event) {
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 === 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();
}
// ===== LAST SCAN RESULT =====
@@ -860,6 +861,416 @@ function updateUIDVisibility() {
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 = item.image
? `<img src="${item.image}" alt="${item.name}" class="inventory-card-img">`
: `<div class="inventory-card-img-placeholder">${ITEM_FALLBACK_SVG}</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';
}
}
async 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;
if (!confirm(`Skupaj: €${total.toFixed(2)}\nStanje po plačilu: €${balanceAfter.toFixed(2)}\n\nPotrdite plačilo?`)) return;
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({
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; }
cart = [];
await loadStore();
alert('Plačilo uspešno!');
} 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 paymentHistoryVisible = false;
function togglePaymentHistory() {
paymentHistoryVisible = !paymentHistoryVisible;
const panel = document.getElementById('paymentHistoryPanel');
panel.style.display = paymentHistoryVisible ? 'block' : 'none';
if (paymentHistoryVisible) loadPaymentHistory();
}
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="7" 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 = ts.split('T')[0];
const time = ts.split('T')[1].substring(0, 8);
const itemsSummary = p.items.map(i => `${i.name} ×${i.qty}`).join(', ');
const row = document.createElement('tr');
row.innerHTML = `
<td>${p.id}</td>
<td>${date}</td>
<td>${time}</td>
<td style="max-width:220px;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>
`;
tbody.appendChild(row);
});
} catch (error) {
console.error('Error loading payment history:', error);
}
}
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;