Shop update added services and other shhhhhhh
This commit is contained in:
Binary file not shown.
@@ -381,16 +381,21 @@ def add_store_item():
|
|||||||
try:
|
try:
|
||||||
data = request.json
|
data = request.json
|
||||||
name = (data.get('name') or '').strip()
|
name = (data.get('name') or '').strip()
|
||||||
price = float(data.get('price', 0))
|
item_type = data.get('type', 'product')
|
||||||
stock = int(data.get('stock', 0))
|
stock = 0 if item_type == 'service' else int(data.get('stock', 0))
|
||||||
image = data.get('image') or None
|
image = data.get('image') or None
|
||||||
|
pm = data.get('price_member')
|
||||||
|
pnm = data.get('price_non_member')
|
||||||
|
price_member = float(pm) if pm is not None else None
|
||||||
|
price_non_member = float(pnm) if pnm is not None else None
|
||||||
|
price = price_member if price_member is not None else float(data.get('price', 0))
|
||||||
if not name:
|
if not name:
|
||||||
return jsonify({'success': False, 'message': 'Name required'}), 400
|
return jsonify({'success': False, 'message': 'Name required'}), 400
|
||||||
if price < 0:
|
if price < 0:
|
||||||
return jsonify({'success': False, 'message': 'Price cannot be negative'}), 400
|
return jsonify({'success': False, 'message': 'Price cannot be negative'}), 400
|
||||||
if stock < 0:
|
if item_type != 'service' and stock < 0:
|
||||||
return jsonify({'success': False, 'message': 'Stock cannot be negative'}), 400
|
return jsonify({'success': False, 'message': 'Stock cannot be negative'}), 400
|
||||||
if data_manager.add_store_item(name, price, stock, image):
|
if data_manager.add_store_item(name, price, stock, image, item_type, price_member, price_non_member):
|
||||||
return jsonify({'success': True})
|
return jsonify({'success': True})
|
||||||
return jsonify({'success': False, 'message': 'Error adding item'}), 400
|
return jsonify({'success': False, 'message': 'Error adding item'}), 400
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -402,12 +407,17 @@ def update_store_item(item_id):
|
|||||||
try:
|
try:
|
||||||
data = request.json
|
data = request.json
|
||||||
name = (data.get('name') or '').strip()
|
name = (data.get('name') or '').strip()
|
||||||
price = float(data.get('price', 0))
|
item_type = data.get('type', 'product')
|
||||||
stock = int(data.get('stock', 0))
|
stock = 0 if item_type == 'service' else int(data.get('stock', 0))
|
||||||
image = data.get('image') or None
|
image = data.get('image') or None
|
||||||
|
pm = data.get('price_member')
|
||||||
|
pnm = data.get('price_non_member')
|
||||||
|
price_member = float(pm) if pm is not None else None
|
||||||
|
price_non_member = float(pnm) if pnm is not None else None
|
||||||
|
price = price_member if price_member is not None else float(data.get('price', 0))
|
||||||
if not name:
|
if not name:
|
||||||
return jsonify({'success': False, 'message': 'Name required'}), 400
|
return jsonify({'success': False, 'message': 'Name required'}), 400
|
||||||
if data_manager.update_store_item(item_id, name, price, stock, image):
|
if data_manager.update_store_item(item_id, name, price, stock, image, item_type, price_member, price_non_member):
|
||||||
return jsonify({'success': True})
|
return jsonify({'success': True})
|
||||||
return jsonify({'success': False, 'message': 'Item not found'}), 404
|
return jsonify({'success': False, 'message': 'Item not found'}), 404
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -435,15 +445,16 @@ def log_payment():
|
|||||||
seller = (data.get('seller') or '').strip()
|
seller = (data.get('seller') or '').strip()
|
||||||
cart_items = data.get('cart_items', []) # [{id, name, price, qty, image}]
|
cart_items = data.get('cart_items', []) # [{id, name, price, qty, image}]
|
||||||
|
|
||||||
# Deduct stock for each sold item
|
# Deduct stock for each sold product (services have no stock)
|
||||||
for ci in cart_items:
|
for ci in cart_items:
|
||||||
item_id = ci.get('id')
|
item_id = ci.get('id')
|
||||||
new_stock = ci.get('new_stock')
|
new_stock = ci.get('new_stock')
|
||||||
|
item_type = ci.get('type', 'product')
|
||||||
name = ci.get('name', '')
|
name = ci.get('name', '')
|
||||||
price = ci.get('price', 0)
|
price = ci.get('price', 0)
|
||||||
image = ci.get('image')
|
image = ci.get('image')
|
||||||
if item_id is not None and new_stock is not None:
|
if item_id is not None and new_stock is not None and item_type != 'service':
|
||||||
data_manager.update_store_item(item_id, name, float(price), int(new_stock), image)
|
data_manager.update_store_item(item_id, name, float(price), int(new_stock), image, item_type)
|
||||||
|
|
||||||
# Update balance atomically with payment log
|
# Update balance atomically with payment log
|
||||||
data_manager.set_balance(balance_after)
|
data_manager.set_balance(balance_after)
|
||||||
|
|||||||
+107
-11
File diff suppressed because one or more lines are too long
+10
-3
@@ -264,12 +264,15 @@ class DataManager:
|
|||||||
print(f"Error setting balance: {e}")
|
print(f"Error setting balance: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def add_store_item(self, name: str, price: float, stock: int, image: str = None) -> bool:
|
def add_store_item(self, name: str, price: float, stock: int, image: str = None, item_type: str = 'product', price_member=None, price_non_member=None) -> bool:
|
||||||
try:
|
try:
|
||||||
store = self.load_store()
|
store = self.load_store()
|
||||||
items = store.get('items', [])
|
items = store.get('items', [])
|
||||||
new_id = max((i['id'] for i in items), default=0) + 1
|
new_id = max((i['id'] for i in items), default=0) + 1
|
||||||
items.append({'id': new_id, 'name': name, 'price': round(price, 2), 'stock': stock, 'image': image})
|
entry = {'id': new_id, 'name': name, 'price': round(price, 2), 'stock': stock, 'image': image, 'type': item_type,
|
||||||
|
'price_member': round(price_member, 2) if price_member is not None else None,
|
||||||
|
'price_non_member': round(price_non_member, 2) if price_non_member is not None else None}
|
||||||
|
items.append(entry)
|
||||||
store['items'] = items
|
store['items'] = items
|
||||||
self.save_store(store)
|
self.save_store(store)
|
||||||
return True
|
return True
|
||||||
@@ -277,7 +280,7 @@ class DataManager:
|
|||||||
print(f"Error adding item: {e}")
|
print(f"Error adding item: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def update_store_item(self, item_id: int, name: str, price: float, stock: int, image: str = None) -> bool:
|
def update_store_item(self, item_id: int, name: str, price: float, stock: int, image: str = None, item_type: str = None, price_member=None, price_non_member=None) -> bool:
|
||||||
try:
|
try:
|
||||||
store = self.load_store()
|
store = self.load_store()
|
||||||
items = store.get('items', [])
|
items = store.get('items', [])
|
||||||
@@ -287,6 +290,10 @@ class DataManager:
|
|||||||
item['price'] = round(price, 2)
|
item['price'] = round(price, 2)
|
||||||
item['stock'] = stock
|
item['stock'] = stock
|
||||||
item['image'] = image
|
item['image'] = image
|
||||||
|
if item_type is not None:
|
||||||
|
item['type'] = item_type
|
||||||
|
item['price_member'] = round(price_member, 2) if price_member is not None else None
|
||||||
|
item['price_non_member'] = round(price_non_member, 2) if price_non_member is not None else None
|
||||||
store['items'] = items
|
store['items'] = items
|
||||||
self.save_store(store)
|
self.save_store(store)
|
||||||
return True
|
return True
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1448,6 +1448,95 @@ table tbody tr.duplicate-warning:hover {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Inventory sections */
|
||||||
|
.inventory-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inventory-section-header {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.6px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
padding-bottom: 6px;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inventory-card-stock.service-label {
|
||||||
|
color: var(--accent-blue, #3b82f6);
|
||||||
|
background: rgba(59,130,246,0.1);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dual price buttons on inventory card */
|
||||||
|
.inventory-dual-btns {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inventory-dual-btns .inventory-add-btn {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 7px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inventory-add-btn-secondary {
|
||||||
|
background: var(--bg-secondary) !important;
|
||||||
|
color: var(--text-primary) !important;
|
||||||
|
border: 1px solid var(--border-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inventory-add-btn-secondary:hover:not(:disabled) {
|
||||||
|
background: var(--bg-tertiary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dual price checkbox label */
|
||||||
|
.dual-price-label {
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
margin-bottom: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dual-price-label input[type="checkbox"] {
|
||||||
|
width: 15px;
|
||||||
|
height: 15px;
|
||||||
|
margin: 0;
|
||||||
|
accent-color: var(--accent-blue);
|
||||||
|
cursor: pointer;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Item type toggle */
|
||||||
|
.item-type-toggle {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-type-btn {
|
||||||
|
flex: 1;
|
||||||
|
padding: 7px 12px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s, color 0.15s, border-color 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-type-btn.active {
|
||||||
|
background: var(--accent-blue, #3b82f6);
|
||||||
|
color: #fff;
|
||||||
|
border-color: var(--accent-blue, #3b82f6);
|
||||||
|
}
|
||||||
|
|
||||||
/* Image upload */
|
/* Image upload */
|
||||||
.image-upload-area {
|
.image-upload-area {
|
||||||
border: 2px dashed var(--border-color);
|
border: 2px dashed var(--border-color);
|
||||||
|
|||||||
+155
-65
@@ -917,54 +917,102 @@ async function loadBalance() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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>`;
|
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 renderInventory() {
|
function getTypeFallbackSvg(type) {
|
||||||
const grid = document.getElementById('inventoryGrid');
|
if (type === 'service') return FALLBACK_SVG_SERVICE;
|
||||||
if (!grid) return;
|
return FALLBACK_SVG_PRODUCT;
|
||||||
|
|
||||||
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-meta">
|
|
||||||
<div class="inventory-card-price">€ ${item.price.toFixed(2)}</div>
|
|
||||||
<div class="inventory-card-stock ${item.stock === 0 ? 'stock-empty' : ''}">${item.stock === 0 ? 'Ni zaloge' : item.stock + ' kos'}</div>
|
|
||||||
</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 ? `<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>
|
|
||||||
`;
|
|
||||||
grid.appendChild(card);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function addToCart(itemId) {
|
function buildItemCard(item) {
|
||||||
const item = storeItems.find(i => i.id === itemId);
|
const isService = item.type === 'service';
|
||||||
if (!item || item.stock === 0) return;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
const existing = cart.find(c => c.id === itemId);
|
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;
|
const currentQty = existing ? existing.qty : 0;
|
||||||
|
|
||||||
if (currentQty >= item.stock) {
|
if (!isService && currentQty >= item.stock) {
|
||||||
alert('Ni dovolj zaloge');
|
alert('Ni dovolj zaloge');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -972,13 +1020,14 @@ function addToCart(itemId) {
|
|||||||
if (existing) {
|
if (existing) {
|
||||||
existing.qty++;
|
existing.qty++;
|
||||||
} else {
|
} else {
|
||||||
cart.push({ id: item.id, name: item.name, price: item.price, qty: 1 });
|
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();
|
renderCart();
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeFromCart(itemId) {
|
function removeFromCart(cartKey) {
|
||||||
const idx = cart.findIndex(c => c.id === itemId);
|
const idx = cart.findIndex(c => c.cartKey === cartKey);
|
||||||
if (idx === -1) return;
|
if (idx === -1) return;
|
||||||
if (cart[idx].qty > 1) {
|
if (cart[idx].qty > 1) {
|
||||||
cart[idx].qty--;
|
cart[idx].qty--;
|
||||||
@@ -1007,12 +1056,13 @@ function renderCart() {
|
|||||||
cart.forEach(item => {
|
cart.forEach(item => {
|
||||||
const row = document.createElement('div');
|
const row = document.createElement('div');
|
||||||
row.className = 'cart-item';
|
row.className = 'cart-item';
|
||||||
|
const tierArg = item.tier ? `,'${item.tier}'` : '';
|
||||||
row.innerHTML = `
|
row.innerHTML = `
|
||||||
<div class="cart-item-name">${item.name}</div>
|
<div class="cart-item-name">${item.name}</div>
|
||||||
<div class="cart-item-controls">
|
<div class="cart-item-controls">
|
||||||
<button class="cart-qty-btn" onclick="removeFromCart(${item.id})">−</button>
|
<button class="cart-qty-btn" onclick="removeFromCart('${item.cartKey}')">−</button>
|
||||||
<span class="cart-qty">${item.qty}</span>
|
<span class="cart-qty">${item.qty}</span>
|
||||||
<button class="cart-qty-btn" onclick="addToCart(${item.id})">+</button>
|
<button class="cart-qty-btn" onclick="addToCart(${item.id}${tierArg})">+</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="cart-item-subtotal">€ ${(item.price * item.qty).toFixed(2)}</div>
|
<div class="cart-item-subtotal">€ ${(item.price * item.qty).toFixed(2)}</div>
|
||||||
`;
|
`;
|
||||||
@@ -1024,16 +1074,9 @@ function renderCart() {
|
|||||||
|
|
||||||
function updateCartTotals() {
|
function updateCartTotals() {
|
||||||
const total = cart.reduce((sum, i) => sum + i.price * i.qty, 0);
|
const total = cart.reduce((sum, i) => sum + i.price * i.qty, 0);
|
||||||
const balanceAfter = currentBalance + total;
|
|
||||||
|
|
||||||
const totalEl = document.getElementById('cartTotal');
|
const totalEl = document.getElementById('cartTotal');
|
||||||
const balanceAfterEl = document.getElementById('cartBalanceAfter');
|
|
||||||
|
|
||||||
if (totalEl) totalEl.textContent = '€ ' + total.toFixed(2);
|
if (totalEl) totalEl.textContent = '€ ' + total.toFixed(2);
|
||||||
if (balanceAfterEl) {
|
|
||||||
balanceAfterEl.textContent = '€ ' + balanceAfter.toFixed(2);
|
|
||||||
balanceAfterEl.className = 'cart-balance-after';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function processPayment() {
|
function processPayment() {
|
||||||
@@ -1046,7 +1089,6 @@ function processPayment() {
|
|||||||
|
|
||||||
document.getElementById('checkoutBuyer').value = '';
|
document.getElementById('checkoutBuyer').value = '';
|
||||||
document.getElementById('checkoutTotal').textContent = '€ ' + total.toFixed(2);
|
document.getElementById('checkoutTotal').textContent = '€ ' + total.toFixed(2);
|
||||||
document.getElementById('checkoutBalanceAfter').textContent = '€ ' + balanceAfter.toFixed(2);
|
|
||||||
document.getElementById('checkoutModal').classList.add('show');
|
document.getElementById('checkoutModal').classList.add('show');
|
||||||
document.getElementById('checkoutBuyer').focus();
|
document.getElementById('checkoutBuyer').focus();
|
||||||
}
|
}
|
||||||
@@ -1068,14 +1110,20 @@ async function confirmCheckout() {
|
|||||||
closeCheckoutModal();
|
closeCheckoutModal();
|
||||||
|
|
||||||
try {
|
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 cartItems = cart.map(cartItem => {
|
||||||
const storeItem = storeItems.find(i => i.id === cartItem.id);
|
const storeItem = storeItems.find(i => i.id === cartItem.id);
|
||||||
|
const isService = ((storeItem && storeItem.type) || cartItem.type) === 'service';
|
||||||
return {
|
return {
|
||||||
id: cartItem.id,
|
id: cartItem.id,
|
||||||
name: cartItem.name,
|
name: cartItem.name,
|
||||||
price: cartItem.price,
|
price: cartItem.price,
|
||||||
qty: cartItem.qty,
|
qty: cartItem.qty,
|
||||||
new_stock: storeItem ? storeItem.stock - cartItem.qty : 0,
|
type: isService ? 'service' : 'product',
|
||||||
|
new_stock: isService ? null : (storeItem ? storeItem.stock - qtySoldById[cartItem.id] : 0),
|
||||||
image: storeItem ? storeItem.image : null
|
image: storeItem ? storeItem.image : null
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -1115,6 +1163,21 @@ async function confirmCheckout() {
|
|||||||
|
|
||||||
let currentItemImage = null;
|
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() {
|
function openAddItemModal() {
|
||||||
editingItemId = null;
|
editingItemId = null;
|
||||||
currentItemImage = null;
|
currentItemImage = null;
|
||||||
@@ -1122,9 +1185,14 @@ function openAddItemModal() {
|
|||||||
document.getElementById('addItemSubmitBtn').textContent = 'Dodaj';
|
document.getElementById('addItemSubmitBtn').textContent = 'Dodaj';
|
||||||
document.getElementById('itemName').value = '';
|
document.getElementById('itemName').value = '';
|
||||||
document.getElementById('itemPrice').value = '';
|
document.getElementById('itemPrice').value = '';
|
||||||
|
document.getElementById('itemPriceMember').value = '';
|
||||||
|
document.getElementById('itemPriceNonMember').value = '';
|
||||||
|
document.getElementById('itemDualPrice').checked = false;
|
||||||
document.getElementById('itemStock').value = '';
|
document.getElementById('itemStock').value = '';
|
||||||
document.getElementById('itemImageInput').value = '';
|
document.getElementById('itemImageInput').value = '';
|
||||||
setImagePreview(null);
|
setImagePreview(null);
|
||||||
|
setItemType('product');
|
||||||
|
toggleDualPrice();
|
||||||
document.getElementById('addItemModal').classList.add('show');
|
document.getElementById('addItemModal').classList.add('show');
|
||||||
document.getElementById('itemName').focus();
|
document.getElementById('itemName').focus();
|
||||||
}
|
}
|
||||||
@@ -1134,13 +1202,19 @@ function openEditItemModal(itemId) {
|
|||||||
if (!item) return;
|
if (!item) return;
|
||||||
editingItemId = itemId;
|
editingItemId = itemId;
|
||||||
currentItemImage = item.image || null;
|
currentItemImage = item.image || null;
|
||||||
|
const hasDual = !!(item.price_member != null && item.price_non_member != null);
|
||||||
document.getElementById('addItemModalTitle').textContent = 'Uredi artikel';
|
document.getElementById('addItemModalTitle').textContent = 'Uredi artikel';
|
||||||
document.getElementById('addItemSubmitBtn').textContent = 'Shrani';
|
document.getElementById('addItemSubmitBtn').textContent = 'Shrani';
|
||||||
document.getElementById('itemName').value = item.name;
|
document.getElementById('itemName').value = item.name;
|
||||||
document.getElementById('itemPrice').value = item.price;
|
document.getElementById('itemPrice').value = hasDual ? '' : item.price;
|
||||||
document.getElementById('itemStock').value = item.stock;
|
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 = '';
|
document.getElementById('itemImageInput').value = '';
|
||||||
setImagePreview(item.image || null);
|
setImagePreview(item.image || null);
|
||||||
|
setItemType(item.type === 'service' ? 'service' : 'product');
|
||||||
|
toggleDualPrice();
|
||||||
document.getElementById('addItemModal').classList.add('show');
|
document.getElementById('addItemModal').classList.add('show');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1186,12 +1260,28 @@ function removeItemImage() {
|
|||||||
|
|
||||||
async function submitItem() {
|
async function submitItem() {
|
||||||
const name = document.getElementById('itemName').value.trim();
|
const name = document.getElementById('itemName').value.trim();
|
||||||
const price = parseFloat(document.getElementById('itemPrice').value);
|
const isService = currentItemType === 'service';
|
||||||
const stock = parseInt(document.getElementById('itemStock').value, 10);
|
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 (!name) { alert('Vnesite ime artikla'); return; }
|
||||||
if (isNaN(price) || price < 0) { alert('Vnesite veljavno ceno'); return; }
|
if (!isService && (isNaN(stock) || stock < 0)) { alert('Vnesite veljavno zalogo'); return; }
|
||||||
if (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 {
|
try {
|
||||||
let response;
|
let response;
|
||||||
@@ -1199,13 +1289,13 @@ async function submitItem() {
|
|||||||
response = await fetch(`/api/store/items/${editingItemId}`, {
|
response = await fetch(`/api/store/items/${editingItemId}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ name, price, stock, image: currentItemImage })
|
body: JSON.stringify(payload)
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
response = await fetch('/api/store/items', {
|
response = await fetch('/api/store/items', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ name, price, stock, image: currentItemImage })
|
body: JSON.stringify(payload)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+34
-12
@@ -175,8 +175,17 @@
|
|||||||
<div id="storeShopView" class="store-tab-scroll panel-content">
|
<div id="storeShopView" class="store-tab-scroll panel-content">
|
||||||
<div class="store-layout">
|
<div class="store-layout">
|
||||||
<div class="store-inventory">
|
<div class="store-inventory">
|
||||||
<div class="inventory-grid" id="inventoryGrid">
|
<div class="inventory-section" id="inventorySectionProducts">
|
||||||
<p style="color: var(--text-secondary);">Nalaganje...</p>
|
<div class="inventory-section-header">Artikli</div>
|
||||||
|
<div class="inventory-grid" id="inventoryGridProducts">
|
||||||
|
<p style="color: var(--text-secondary);">Nalaganje...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="inventory-section" id="inventorySectionServices">
|
||||||
|
<div class="inventory-section-header">Storitve</div>
|
||||||
|
<div class="inventory-grid" id="inventoryGridServices">
|
||||||
|
<p style="color: var(--text-secondary);">Nalaganje...</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="store-cart">
|
<div class="store-cart">
|
||||||
@@ -192,10 +201,7 @@
|
|||||||
<span>Skupaj:</span>
|
<span>Skupaj:</span>
|
||||||
<span id="cartTotal" class="cart-total-amount">€ 0.00</span>
|
<span id="cartTotal" class="cart-total-amount">€ 0.00</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="cart-total-row cart-balance-row">
|
|
||||||
<span>Stanje po plačilu:</span>
|
|
||||||
<span id="cartBalanceAfter" class="cart-balance-after">€ 0.00</span>
|
|
||||||
</div>
|
|
||||||
<button class="btn btn-primary cart-pay-btn" onclick="processPayment()">Plačaj</button>
|
<button class="btn btn-primary cart-pay-btn" onclick="processPayment()">Plačaj</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -234,6 +240,13 @@
|
|||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<span class="close" onclick="closeAddItemModal()">×</span>
|
<span class="close" onclick="closeAddItemModal()">×</span>
|
||||||
<h2 id="addItemModalTitle">Dodaj artikel</h2>
|
<h2 id="addItemModalTitle">Dodaj artikel</h2>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Vrsta:</label>
|
||||||
|
<div class="item-type-toggle">
|
||||||
|
<button type="button" id="typeProductBtn" class="item-type-btn active" onclick="setItemType('product')">Artikel</button>
|
||||||
|
<button type="button" id="typeServiceBtn" class="item-type-btn" onclick="setItemType('service')">Storitev</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Slika (neobvezno):</label>
|
<label>Slika (neobvezno):</label>
|
||||||
<div class="image-upload-area" id="imageUploadArea" onclick="document.getElementById('itemImageInput').click()">
|
<div class="image-upload-area" id="imageUploadArea" onclick="document.getElementById('itemImageInput').click()">
|
||||||
@@ -252,11 +265,23 @@
|
|||||||
<label>Ime artikla:</label>
|
<label>Ime artikla:</label>
|
||||||
<input type="text" id="itemName" placeholder="Ime artikla">
|
<input type="text" id="itemName" placeholder="Ime artikla">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group" style="margin-bottom:10px;">
|
||||||
|
<label class="dual-price-label">
|
||||||
|
<input type="checkbox" id="itemDualPrice" onchange="toggleDualPrice()">
|
||||||
|
Ločene cene (član / nečlan)
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" id="singlePriceGroup">
|
||||||
<label>Cena (€):</label>
|
<label>Cena (€):</label>
|
||||||
<input type="number" id="itemPrice" placeholder="0.00" min="0" step="0.01">
|
<input type="number" id="itemPrice" placeholder="0.00" min="0" step="0.01">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group" id="dualPriceGroup" style="display:none;">
|
||||||
|
<label>Cena za člana (€):</label>
|
||||||
|
<input type="number" id="itemPriceMember" placeholder="0.00" min="0" step="0.01">
|
||||||
|
<label style="margin-top:8px;">Cena za nečlana (€):</label>
|
||||||
|
<input type="number" id="itemPriceNonMember" placeholder="0.00" min="0" step="0.01">
|
||||||
|
</div>
|
||||||
|
<div class="form-group" id="stockFormGroup">
|
||||||
<label>Zaloga (kos):</label>
|
<label>Zaloga (kos):</label>
|
||||||
<input type="number" id="itemStock" placeholder="0" min="0" step="1">
|
<input type="number" id="itemStock" placeholder="0" min="0" step="1">
|
||||||
</div>
|
</div>
|
||||||
@@ -367,10 +392,7 @@
|
|||||||
<span>Skupaj za plačilo:</span>
|
<span>Skupaj za plačilo:</span>
|
||||||
<span id="checkoutTotal" class="checkout-total-val"></span>
|
<span id="checkoutTotal" class="checkout-total-val"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="checkout-summary-row">
|
|
||||||
<span>Stanje po plačilu:</span>
|
|
||||||
<span id="checkoutBalanceAfter" class="checkout-balance-val"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-primary" onclick="confirmCheckout()">Potrdi plačilo</button>
|
<button class="btn btn-primary" onclick="confirmCheckout()">Potrdi plačilo</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user