Shop update
This commit is contained in:
Binary file not shown.
Binary file not shown.
@@ -369,6 +369,113 @@ def export_logs():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({'success': False, 'message': str(e)}), 500
|
return jsonify({'success': False, 'message': str(e)}), 500
|
||||||
|
|
||||||
|
# ===== STORE ROUTES =====
|
||||||
|
|
||||||
|
@app.route('/api/store/items', methods=['GET'])
|
||||||
|
def get_store_items():
|
||||||
|
return jsonify(data_manager.get_store_items())
|
||||||
|
|
||||||
|
@app.route('/api/store/items', methods=['POST'])
|
||||||
|
@require_admin
|
||||||
|
def add_store_item():
|
||||||
|
try:
|
||||||
|
data = request.json
|
||||||
|
name = (data.get('name') or '').strip()
|
||||||
|
price = float(data.get('price', 0))
|
||||||
|
stock = int(data.get('stock', 0))
|
||||||
|
image = data.get('image') or None
|
||||||
|
if not name:
|
||||||
|
return jsonify({'success': False, 'message': 'Name required'}), 400
|
||||||
|
if price < 0:
|
||||||
|
return jsonify({'success': False, 'message': 'Price cannot be negative'}), 400
|
||||||
|
if stock < 0:
|
||||||
|
return jsonify({'success': False, 'message': 'Stock cannot be negative'}), 400
|
||||||
|
if data_manager.add_store_item(name, price, stock, image):
|
||||||
|
return jsonify({'success': True})
|
||||||
|
return jsonify({'success': False, 'message': 'Error adding item'}), 400
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'success': False, 'message': str(e)}), 400
|
||||||
|
|
||||||
|
@app.route('/api/store/items/<int:item_id>', methods=['PUT'])
|
||||||
|
@require_admin
|
||||||
|
def update_store_item(item_id):
|
||||||
|
try:
|
||||||
|
data = request.json
|
||||||
|
name = (data.get('name') or '').strip()
|
||||||
|
price = float(data.get('price', 0))
|
||||||
|
stock = int(data.get('stock', 0))
|
||||||
|
image = data.get('image') or None
|
||||||
|
if not name:
|
||||||
|
return jsonify({'success': False, 'message': 'Name required'}), 400
|
||||||
|
if data_manager.update_store_item(item_id, name, price, stock, image):
|
||||||
|
return jsonify({'success': True})
|
||||||
|
return jsonify({'success': False, 'message': 'Item not found'}), 404
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'success': False, 'message': str(e)}), 400
|
||||||
|
|
||||||
|
@app.route('/api/store/items/<int:item_id>', methods=['DELETE'])
|
||||||
|
@require_admin
|
||||||
|
def delete_store_item(item_id):
|
||||||
|
if data_manager.delete_store_item(item_id):
|
||||||
|
return jsonify({'success': True})
|
||||||
|
return jsonify({'success': False, 'message': 'Item not found'}), 404
|
||||||
|
|
||||||
|
@app.route('/api/store/payments', methods=['GET'])
|
||||||
|
def get_payments():
|
||||||
|
return jsonify(data_manager.get_payments())
|
||||||
|
|
||||||
|
@app.route('/api/store/payments', methods=['POST'])
|
||||||
|
def log_payment():
|
||||||
|
try:
|
||||||
|
data = request.json
|
||||||
|
items = data.get('items', [])
|
||||||
|
total = float(data.get('total', 0))
|
||||||
|
balance_before = float(data.get('balance_before', 0))
|
||||||
|
balance_after = float(data.get('balance_after', 0))
|
||||||
|
cart_items = data.get('cart_items', []) # [{id, name, price, qty, image}]
|
||||||
|
|
||||||
|
# Deduct stock for each sold item
|
||||||
|
for ci in cart_items:
|
||||||
|
item_id = ci.get('id')
|
||||||
|
new_stock = ci.get('new_stock')
|
||||||
|
name = ci.get('name', '')
|
||||||
|
price = ci.get('price', 0)
|
||||||
|
image = ci.get('image')
|
||||||
|
if item_id is not None and new_stock is not None:
|
||||||
|
data_manager.update_store_item(item_id, name, float(price), int(new_stock), image)
|
||||||
|
|
||||||
|
# Update balance atomically with payment log
|
||||||
|
data_manager.set_balance(balance_after)
|
||||||
|
|
||||||
|
if data_manager.log_payment(items, total, balance_before, balance_after):
|
||||||
|
return jsonify({'success': True})
|
||||||
|
return jsonify({'success': False, 'message': 'Error logging payment'}), 400
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'success': False, 'message': str(e)}), 400
|
||||||
|
|
||||||
|
@app.route('/api/store/payments', methods=['DELETE'])
|
||||||
|
@require_admin
|
||||||
|
def clear_payments():
|
||||||
|
if data_manager.clear_payments():
|
||||||
|
return jsonify({'success': True})
|
||||||
|
return jsonify({'success': False, 'message': 'Error clearing payments'}), 400
|
||||||
|
|
||||||
|
@app.route('/api/store/balance', methods=['GET'])
|
||||||
|
def get_balance():
|
||||||
|
return jsonify({'balance': data_manager.get_balance()})
|
||||||
|
|
||||||
|
@app.route('/api/store/balance', methods=['PUT'])
|
||||||
|
@require_admin
|
||||||
|
def set_balance():
|
||||||
|
try:
|
||||||
|
data = request.json
|
||||||
|
amount = float(data.get('balance', 0))
|
||||||
|
if data_manager.set_balance(amount):
|
||||||
|
return jsonify({'success': True, 'balance': round(amount, 2)})
|
||||||
|
return jsonify({'success': False, 'message': 'Error setting balance'}), 400
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'success': False, 'message': str(e)}), 400
|
||||||
|
|
||||||
@app.route('/api/stats', methods=['GET'])
|
@app.route('/api/stats', methods=['GET'])
|
||||||
def get_stats():
|
def get_stats():
|
||||||
"""Get statistics"""
|
"""Get statistics"""
|
||||||
|
|||||||
+280
File diff suppressed because one or more lines are too long
+112
@@ -230,6 +230,118 @@ class DataManager:
|
|||||||
print(f"Error logging unregistered user: {e}")
|
print(f"Error logging unregistered user: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# ===== STORE =====
|
||||||
|
|
||||||
|
def load_store(self) -> Dict:
|
||||||
|
try:
|
||||||
|
store_file = self.participants_file.replace('participants.json', 'store.json')
|
||||||
|
with open(store_file, 'r') as f:
|
||||||
|
return json.load(f)
|
||||||
|
except Exception:
|
||||||
|
return {'balance': 0.0, 'items': [], 'payments': []}
|
||||||
|
|
||||||
|
def save_store(self, store: Dict):
|
||||||
|
try:
|
||||||
|
store_file = self.participants_file.replace('participants.json', 'store.json')
|
||||||
|
with open(store_file, 'w') as f:
|
||||||
|
json.dump(store, f, indent=2)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error saving store: {e}")
|
||||||
|
|
||||||
|
def get_store_items(self) -> List[Dict]:
|
||||||
|
return self.load_store().get('items', [])
|
||||||
|
|
||||||
|
def get_balance(self) -> float:
|
||||||
|
return self.load_store().get('balance', 0.0)
|
||||||
|
|
||||||
|
def set_balance(self, amount: float) -> bool:
|
||||||
|
try:
|
||||||
|
store = self.load_store()
|
||||||
|
store['balance'] = round(amount, 2)
|
||||||
|
self.save_store(store)
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error setting balance: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def add_store_item(self, name: str, price: float, stock: int, image: str = None) -> bool:
|
||||||
|
try:
|
||||||
|
store = self.load_store()
|
||||||
|
items = store.get('items', [])
|
||||||
|
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})
|
||||||
|
store['items'] = items
|
||||||
|
self.save_store(store)
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error adding item: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def update_store_item(self, item_id: int, name: str, price: float, stock: int, image: str = None) -> bool:
|
||||||
|
try:
|
||||||
|
store = self.load_store()
|
||||||
|
items = store.get('items', [])
|
||||||
|
for item in items:
|
||||||
|
if item['id'] == item_id:
|
||||||
|
item['name'] = name
|
||||||
|
item['price'] = round(price, 2)
|
||||||
|
item['stock'] = stock
|
||||||
|
item['image'] = image
|
||||||
|
store['items'] = items
|
||||||
|
self.save_store(store)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error updating item: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def delete_store_item(self, item_id: int) -> bool:
|
||||||
|
try:
|
||||||
|
store = self.load_store()
|
||||||
|
items = store.get('items', [])
|
||||||
|
new_items = [i for i in items if i['id'] != item_id]
|
||||||
|
if len(new_items) == len(items):
|
||||||
|
return False
|
||||||
|
store['items'] = new_items
|
||||||
|
self.save_store(store)
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error deleting item: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def log_payment(self, items: list, total: float, balance_before: float, balance_after: float) -> bool:
|
||||||
|
try:
|
||||||
|
store = self.load_store()
|
||||||
|
payments = store.get('payments', [])
|
||||||
|
new_id = max((p['id'] for p in payments), default=0) + 1
|
||||||
|
payments.append({
|
||||||
|
'id': new_id,
|
||||||
|
'timestamp': datetime.now().isoformat(),
|
||||||
|
'items': items,
|
||||||
|
'total': round(total, 2),
|
||||||
|
'balance_before': round(balance_before, 2),
|
||||||
|
'balance_after': round(balance_after, 2)
|
||||||
|
})
|
||||||
|
store['payments'] = payments
|
||||||
|
self.save_store(store)
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error logging payment: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_payments(self) -> list:
|
||||||
|
return self.load_store().get('payments', [])
|
||||||
|
|
||||||
|
def clear_payments(self) -> bool:
|
||||||
|
try:
|
||||||
|
store = self.load_store()
|
||||||
|
store['payments'] = []
|
||||||
|
self.save_store(store)
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error clearing payments: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
def delete_log(self, log_id: int) -> bool:
|
def delete_log(self, log_id: int) -> bool:
|
||||||
"""Delete a log entry by ID (admin only)"""
|
"""Delete a log entry by ID (admin only)"""
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -1220,6 +1220,381 @@ table tbody tr.duplicate-warning:hover {
|
|||||||
font-size: 13px;
|
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 */
|
/* Print Styles */
|
||||||
@media print {
|
@media print {
|
||||||
body {
|
body {
|
||||||
|
|||||||
+428
-17
@@ -32,21 +32,26 @@ function updateAdminUI() {
|
|||||||
const adminBtn = document.getElementById('adminToggleBtn');
|
const adminBtn = document.getElementById('adminToggleBtn');
|
||||||
const addBtn = document.querySelector('[onclick="openAddParticipantModal()"]');
|
const addBtn = document.querySelector('[onclick="openAddParticipantModal()"]');
|
||||||
const addUnregisteredBtn = document.getElementById('addUnregisteredBtn');
|
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) {
|
if (isAdmin) {
|
||||||
adminBtn.textContent = 'Odjava Admin';
|
adminBtn.textContent = 'Odjava Admin';
|
||||||
adminBtn.onclick = adminLogout;
|
adminBtn.onclick = adminLogout;
|
||||||
adminBtn.classList.add('active');
|
adminBtn.classList.add('active');
|
||||||
if (addBtn) addBtn.style.display = 'block';
|
if (addBtn) addBtn.style.display = 'block';
|
||||||
|
if (addItemBtn) addItemBtn.style.display = 'inline-flex';
|
||||||
|
if (editBalanceBtn) editBalanceBtn.style.display = 'inline-flex';
|
||||||
} else {
|
} else {
|
||||||
adminBtn.textContent = 'Prijava Admin';
|
adminBtn.textContent = 'Prijava Admin';
|
||||||
adminBtn.onclick = openLoginModal;
|
adminBtn.onclick = openLoginModal;
|
||||||
adminBtn.classList.remove('active');
|
adminBtn.classList.remove('active');
|
||||||
if (addBtn) addBtn.style.display = 'none';
|
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';
|
if (addUnregisteredBtn) addUnregisteredBtn.style.display = 'block';
|
||||||
updateUIDVisibility();
|
updateUIDVisibility();
|
||||||
}
|
}
|
||||||
@@ -73,6 +78,7 @@ async function adminLogin() {
|
|||||||
closeLoginModal();
|
closeLoginModal();
|
||||||
updateAdminUI();
|
updateAdminUI();
|
||||||
loadParticipants();
|
loadParticipants();
|
||||||
|
loadStore();
|
||||||
alert('Uspešna prijava!');
|
alert('Uspešna prijava!');
|
||||||
} else {
|
} else {
|
||||||
alert('Napaka: ' + data.message);
|
alert('Napaka: ' + data.message);
|
||||||
@@ -92,6 +98,7 @@ async function adminLogout() {
|
|||||||
isAdmin = false;
|
isAdmin = false;
|
||||||
updateAdminUI();
|
updateAdminUI();
|
||||||
loadParticipants();
|
loadParticipants();
|
||||||
|
loadStore();
|
||||||
alert('Odjavljen');
|
alert('Odjavljen');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -721,6 +728,7 @@ function switchTab(tabName, event) {
|
|||||||
|
|
||||||
// Load tab-specific data
|
// Load tab-specific data
|
||||||
if (tabName === 'documents') loadDocuments();
|
if (tabName === 'documents') loadDocuments();
|
||||||
|
if (tabName === 'store') loadStore();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close modal when clicking outside
|
// Close modal when clicking outside
|
||||||
@@ -729,22 +737,15 @@ window.onclick = function(event) {
|
|||||||
const addModal = document.getElementById('addParticipantModal');
|
const addModal = document.getElementById('addParticipantModal');
|
||||||
const loginModal = document.getElementById('loginModal');
|
const loginModal = document.getElementById('loginModal');
|
||||||
const addUnregisteredModal = document.getElementById('addUnregisteredModal');
|
const addUnregisteredModal = document.getElementById('addUnregisteredModal');
|
||||||
|
const addItemModal = document.getElementById('addItemModal');
|
||||||
|
const editBalanceModal = document.getElementById('editBalanceModal');
|
||||||
|
|
||||||
if (event.target === editModal) {
|
if (event.target === editModal) closeModal();
|
||||||
closeModal();
|
if (event.target === addModal) closeAddParticipantModal();
|
||||||
}
|
if (event.target === loginModal) closeLoginModal();
|
||||||
|
if (event.target === addUnregisteredModal) closeAddUnregisteredModal();
|
||||||
if (event.target === addModal) {
|
if (event.target === addItemModal) closeAddItemModal();
|
||||||
closeAddParticipantModal();
|
if (event.target === editBalanceModal) closeEditBalanceModal();
|
||||||
}
|
|
||||||
|
|
||||||
if (event.target === loginModal) {
|
|
||||||
closeLoginModal();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.target === addUnregisteredModal) {
|
|
||||||
closeAddUnregisteredModal();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== LAST SCAN RESULT =====
|
// ===== LAST SCAN RESULT =====
|
||||||
@@ -860,6 +861,416 @@ function updateUIDVisibility() {
|
|||||||
if (scanUIDContainer) scanUIDContainer.style.display = display;
|
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) {
|
function hideTableColumn(tableId, columnIndex, hide) {
|
||||||
const table = document.getElementById(tableId);
|
const table = document.getElementById(tableId);
|
||||||
if (!table) return;
|
if (!table) return;
|
||||||
|
|||||||
@@ -55,6 +55,7 @@
|
|||||||
<button class="tab-nav-btn active" onclick="switchTab('logs', event)">Dnevnik skeniranja</button>
|
<button class="tab-nav-btn active" onclick="switchTab('logs', event)">Dnevnik skeniranja</button>
|
||||||
<button class="tab-nav-btn" onclick="switchTab('participants', event)">Člani</button>
|
<button class="tab-nav-btn" onclick="switchTab('participants', event)">Člani</button>
|
||||||
<button class="tab-nav-btn" onclick="switchTab('documents', event)">Dokumenti</button>
|
<button class="tab-nav-btn" onclick="switchTab('documents', event)">Dokumenti</button>
|
||||||
|
<button class="tab-nav-btn" onclick="switchTab('store', event)">Trgovina</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Participants Tab -->
|
<!-- Participants Tab -->
|
||||||
@@ -162,6 +163,131 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Store Tab -->
|
||||||
|
<div id="store-tab" class="tab-panel">
|
||||||
|
<div class="panel-header">
|
||||||
|
<h2>Trgovina</h2>
|
||||||
|
<div class="header-actions">
|
||||||
|
<div class="store-balance-display">
|
||||||
|
<span class="store-balance-label">Stanje:</span>
|
||||||
|
<span id="storeBalanceDisplay" class="store-balance-value">€0.00</span>
|
||||||
|
<button id="editBalanceBtn" class="btn btn-primary" onclick="openEditBalanceModal()" style="display:none;">Uredi stanje</button>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary" onclick="togglePaymentHistory()">Zgodovina plačil</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="store-tab-scroll panel-content">
|
||||||
|
<div class="store-layout">
|
||||||
|
<!-- Inventory -->
|
||||||
|
<div class="store-inventory">
|
||||||
|
<div class="store-section-header">
|
||||||
|
<h3>Zaloga</h3>
|
||||||
|
<button id="addItemBtn" class="btn btn-primary" onclick="openAddItemModal()" style="display:none;">+ Dodaj artikel</button>
|
||||||
|
</div>
|
||||||
|
<div class="inventory-grid" id="inventoryGrid">
|
||||||
|
<p style="color: var(--text-secondary);">Nalaganje...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Cart -->
|
||||||
|
<div class="store-cart">
|
||||||
|
<div class="store-section-header">
|
||||||
|
<h3>Košarica</h3>
|
||||||
|
<button class="btn btn-primary" onclick="clearCart()">Počisti</button>
|
||||||
|
</div>
|
||||||
|
<div class="cart-items" id="cartItems">
|
||||||
|
<p style="color: var(--text-secondary); text-align: center; padding: 20px;">Košarica je prazna</p>
|
||||||
|
</div>
|
||||||
|
<div class="cart-total">
|
||||||
|
<div class="cart-total-row">
|
||||||
|
<span>Skupaj:</span>
|
||||||
|
<span id="cartTotal" class="cart-total-amount">€0.00</span>
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div><!-- end store-layout -->
|
||||||
|
|
||||||
|
<!-- Payment History Panel -->
|
||||||
|
<div id="paymentHistoryPanel" class="payment-history-panel" style="display:none;">
|
||||||
|
<div class="store-section-header" style="padding: 0 16px 12px;">
|
||||||
|
<h3>Zgodovina plačil</h3>
|
||||||
|
<button id="clearHistoryBtn" class="action-btn delete-btn" onclick="clearPaymentHistory()" style="display:none;">Izbriši zgodovino</button>
|
||||||
|
</div>
|
||||||
|
<div class="table-container" style="margin: 0 16px 16px;">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>#</th>
|
||||||
|
<th>Datum</th>
|
||||||
|
<th>Čas</th>
|
||||||
|
<th>Artikli</th>
|
||||||
|
<th>Skupaj</th>
|
||||||
|
<th>Stanje pred</th>
|
||||||
|
<th>Stanje po</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="paymentHistoryTable">
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Add Item Modal -->
|
||||||
|
<div id="addItemModal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<span class="close" onclick="closeAddItemModal()">×</span>
|
||||||
|
<h2 id="addItemModalTitle">Dodaj artikel</h2>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Slika (neobvezno):</label>
|
||||||
|
<div class="image-upload-area" id="imageUploadArea" onclick="document.getElementById('itemImageInput').click()">
|
||||||
|
<div id="imagePreviewWrapper">
|
||||||
|
<div class="image-upload-placeholder" id="imageUploadPlaceholder">
|
||||||
|
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
|
||||||
|
<span>Kliknite za nalaganje slike</span>
|
||||||
|
</div>
|
||||||
|
<img id="imagePreview" src="" alt="" style="display:none; max-height:140px; border-radius:6px; object-fit:contain;">
|
||||||
|
</div>
|
||||||
|
<input type="file" id="itemImageInput" accept="image/*" style="display:none" onchange="handleImageUpload(event)">
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn-remove-image" id="removeImageBtn" onclick="removeItemImage()" style="display:none;">Odstrani sliko</button>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Ime artikla:</label>
|
||||||
|
<input type="text" id="itemName" placeholder="Ime artikla">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Cena (€):</label>
|
||||||
|
<input type="number" id="itemPrice" placeholder="0.00" min="0" step="0.01">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Zaloga (kos):</label>
|
||||||
|
<input type="number" id="itemStock" placeholder="0" min="0" step="1">
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary" id="addItemSubmitBtn" onclick="submitItem()">Dodaj</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Edit Balance Modal -->
|
||||||
|
<div id="editBalanceModal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<span class="close" onclick="closeEditBalanceModal()">×</span>
|
||||||
|
<h2>Uredi stanje</h2>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Novo stanje (€):</label>
|
||||||
|
<input type="number" id="balanceInput" placeholder="0.00" step="0.01">
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary" onclick="saveBalance()">Shrani</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user