Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,6 +1,4 @@
|
|
| 1 |
|
| 2 |
-
|
| 3 |
-
|
| 4 |
from flask import Flask, render_template_string, request, redirect, url_for, send_file, flash, jsonify, session
|
| 5 |
import json
|
| 6 |
import os
|
|
@@ -94,6 +92,17 @@ translations = {
|
|
| 94 |
'whatsapp_contact_me': "Пожалуйста, свяжитесь со мной для уточнения деталей оплаты и доставки.",
|
| 95 |
'error': "Ошибка",
|
| 96 |
'order_not_found': "Заказ с таким ID не найден.",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
},
|
| 98 |
'kk': {
|
| 99 |
'site_title': "dalarssi - Каталог",
|
|
@@ -150,6 +159,17 @@ translations = {
|
|
| 150 |
'whatsapp_contact_me': "Төлем және жеткізу мәліметтерін нақтылау үшін менімен хабарласыңыз.",
|
| 151 |
'error': "Қате",
|
| 152 |
'order_not_found': "Бұл ID-мен тапсырыс табылмады.",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 153 |
}
|
| 154 |
}
|
| 155 |
|
|
@@ -209,7 +229,7 @@ def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWN
|
|
| 209 |
try:
|
| 210 |
if file_name == DATA_FILE:
|
| 211 |
with open(file_name, 'w', encoding='utf-8') as f:
|
| 212 |
-
json.dump({'products': [], 'categories': [], 'orders': {}}, f)
|
| 213 |
logging.info(f"Created empty local file {file_name} because it was not found on HF.")
|
| 214 |
except Exception as create_e:
|
| 215 |
logging.error(f"Failed to create empty local file {file_name}: {create_e}")
|
|
@@ -273,7 +293,7 @@ def periodic_backup():
|
|
| 273 |
|
| 274 |
|
| 275 |
def load_data():
|
| 276 |
-
default_data = {'products': [], 'categories': [], 'orders': {}}
|
| 277 |
try:
|
| 278 |
with open(DATA_FILE, 'r', encoding='utf-8') as file:
|
| 279 |
data = json.load(file)
|
|
@@ -284,6 +304,7 @@ def load_data():
|
|
| 284 |
if 'products' not in data: data['products'] = []
|
| 285 |
if 'categories' not in data: data['categories'] = []
|
| 286 |
if 'orders' not in data: data['orders'] = {}
|
|
|
|
| 287 |
return data
|
| 288 |
except FileNotFoundError:
|
| 289 |
logging.warning(f"Local file {DATA_FILE} not found. Attempting download from HF.")
|
|
@@ -299,6 +320,7 @@ def load_data():
|
|
| 299 |
if 'products' not in data: data['products'] = []
|
| 300 |
if 'categories' not in data: data['categories'] = []
|
| 301 |
if 'orders' not in data: data['orders'] = {}
|
|
|
|
| 302 |
return data
|
| 303 |
except FileNotFoundError:
|
| 304 |
logging.error(f"File {DATA_FILE} still not found even after download reported success. Using default.")
|
|
@@ -328,6 +350,7 @@ def save_data(data):
|
|
| 328 |
if 'products' not in data: data['products'] = []
|
| 329 |
if 'categories' not in data: data['categories'] = []
|
| 330 |
if 'orders' not in data: data['orders'] = {}
|
|
|
|
| 331 |
|
| 332 |
with open(DATA_FILE, 'w', encoding='utf-8') as file:
|
| 333 |
json.dump(data, file, ensure_ascii=False, indent=4)
|
|
@@ -359,7 +382,7 @@ CATALOG_TEMPLATE = '''
|
|
| 359 |
--text-muted: #a09a9a;
|
| 360 |
}
|
| 361 |
* { margin: 0; padding: 0; box-sizing: border-box; }
|
| 362 |
-
body { font-family: 'Montserrat', sans-serif; background: var(--bg-color); color: var(--text-color); line-height: 1.6; }
|
| 363 |
.container { max-width: 1400px; margin: 0 auto; padding: 20px; }
|
| 364 |
.header { display: flex; justify-content: space-between; align-items: center; padding: 20px 0; border-bottom: 1px solid var(--secondary-accent); }
|
| 365 |
.header h1 { font-size: 1.8rem; font-weight: 700; color: var(--primary-accent); letter-spacing: 1px; }
|
|
@@ -373,9 +396,8 @@ CATALOG_TEMPLATE = '''
|
|
| 373 |
#search-input:focus { border-color: var(--primary-accent); box-shadow: 0 0 0 4px rgba(217, 158, 203, 0.2); }
|
| 374 |
.category-filter { padding: 10px 20px; border: 1px solid var(--secondary-accent); border-radius: 30px; background-color: transparent; cursor: pointer; transition: all 0.3s ease; font-size: 0.9rem; font-weight: 600; color: var(--text-muted); }
|
| 375 |
.category-filter.active, .category-filter:hover { background-color: var(--primary-accent); color: var(--bg-color); border-color: var(--primary-accent); }
|
| 376 |
-
.products-grid { display: grid; grid-template-columns: repeat(
|
| 377 |
-
@media (min-width:
|
| 378 |
-
@media (min-width: 900px) { .products-grid { grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); } }
|
| 379 |
.product { background: var(--surface-color); border-radius: 15px; padding: 0; box-shadow: 0 5px 20px rgba(0, 0, 0, 0.3); transition: transform 0.3s ease, box-shadow 0.3s ease; overflow: hidden; display: flex; flex-direction: column; justify-content: space-between; height: 100%; border: 1px solid var(--secondary-accent); position: relative; }
|
| 380 |
.product:hover { transform: translateY(-8px); box-shadow: 0 8px 25px rgba(0, 0, 0, 0.5); }
|
| 381 |
.product-image { width: 100%; aspect-ratio: 1 / 1.1; background-color: #fff; overflow: hidden; display: flex; justify-content: center; align-items: center; }
|
|
@@ -393,7 +415,7 @@ CATALOG_TEMPLATE = '''
|
|
| 393 |
.product-button i { margin-right: 8px; }
|
| 394 |
.details-button { background-color: transparent; border: 2px solid var(--primary-accent); color: var(--primary-accent); }
|
| 395 |
.details-button:hover { background-color: var(--primary-accent); color: var(--bg-color); }
|
| 396 |
-
#cart-button { position: fixed; bottom:
|
| 397 |
#cart-button:hover { transform: scale(1.1); }
|
| 398 |
#cart-button span { position: absolute; top: 0px; right: 0px; background-color: var(--secondary-accent); color: var(--text-color); border-radius: 50%; padding: 3px 7px; font-size: 0.8rem; font-weight: bold; }
|
| 399 |
.modal { display: none; position: fixed; z-index: 1001; left: 0; top: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.7); backdrop-filter: blur(8px); overflow-y: auto; }
|
|
@@ -410,7 +432,7 @@ CATALOG_TEMPLATE = '''
|
|
| 410 |
.cart-item-total { font-weight: bold; text-align: right; font-size: 1.1rem; color: var(--primary-accent); }
|
| 411 |
.cart-item-remove { background:none; border:none; color:#c04c4c; cursor:pointer; font-size: 1.5rem; transition: color 0.3s; }
|
| 412 |
.cart-item-remove:hover { color: #a03c3c; }
|
| 413 |
-
.quantity-input, .color-select { width: 100%; max-width: 200px; padding: 12px; border: 1px solid var(--secondary-accent); border-radius: 8px; font-size: 1rem; margin: 10px 0; box-sizing: border-box; background-color: var(--bg-color); color: var(--text-color); }
|
| 414 |
.cart-summary { margin-top: 25px; text-align: right; border-top: 1px solid var(--secondary-accent); padding-top: 20px; }
|
| 415 |
.cart-summary strong { font-size: 1.4rem; color: var(--primary-accent); }
|
| 416 |
.cart-actions { margin-top: 30px; display: flex; justify-content: space-between; gap: 15px; flex-wrap: wrap; }
|
|
@@ -421,6 +443,14 @@ CATALOG_TEMPLATE = '''
|
|
| 421 |
.notification.show { opacity: 1;}
|
| 422 |
.no-results-message { grid-column: 1 / -1; text-align: center; padding: 50px; font-size: 1.2rem; color: var(--text-muted); }
|
| 423 |
.top-product-indicator { position: absolute; top: 12px; right: 12px; background-color: rgba(217, 158, 203, 0.9); color: var(--bg-color); padding: 4px 10px; font-size: 0.8rem; border-radius: 20px; font-weight: bold; z-index: 10; backdrop-filter: blur(3px); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 424 |
</style>
|
| 425 |
</head>
|
| 426 |
<body>
|
|
@@ -452,9 +482,11 @@ CATALOG_TEMPLATE = '''
|
|
| 452 |
<div class="products-grid" id="products-grid">
|
| 453 |
{% for product in products %}
|
| 454 |
<div class="product"
|
|
|
|
| 455 |
data-name="{{ product['name']|lower }}"
|
| 456 |
data-description="{{ product.get('description', '')|lower }}"
|
| 457 |
data-category="{{ product.get('category', 'Без категории') }}">
|
|
|
|
| 458 |
{% if product.get('is_top', False) %}
|
| 459 |
<span class="top-product-indicator"><i class="fas fa-star"></i> {{ _('top_product') }}</span>
|
| 460 |
{% endif %}
|
|
@@ -480,7 +512,7 @@ CATALOG_TEMPLATE = '''
|
|
| 480 |
</div>
|
| 481 |
</div>
|
| 482 |
<div class="product-actions">
|
| 483 |
-
<button class="product-button details-button" onclick="
|
| 484 |
<button class="product-button add-to-cart" onclick="openQuantityModal({{ loop.index0 }})">
|
| 485 |
<i class="fas fa-cart-plus"></i> {{ _('add_to_cart') }}
|
| 486 |
</button>
|
|
@@ -517,6 +549,10 @@ CATALOG_TEMPLATE = '''
|
|
| 517 |
<span class="close" onclick="closeModal('cartModal')" aria-label="{{ _('close') }}">×</span>
|
| 518 |
<h2><i class="fas fa-shopping-cart"></i> {{ _('your_cart') }}</h2>
|
| 519 |
<div id="cartContent"><p style="text-align: center; padding: 20px;">{{ _('cart_is_empty') }}</p></div>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 520 |
<div class="cart-summary">
|
| 521 |
<strong>{{ _('total') }} <span id="cartTotal">0.00</span> {{ currency_code }}</strong>
|
| 522 |
</div>
|
|
@@ -530,32 +566,72 @@ CATALOG_TEMPLATE = '''
|
|
| 530 |
</div>
|
| 531 |
</div>
|
| 532 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 533 |
|
| 534 |
<button id="cart-button" onclick="openCartModal()" aria-label="{{ _('open_cart') }}">
|
| 535 |
<i class="fas fa-shopping-cart"></i>
|
| 536 |
<span id="cart-count">0</span>
|
| 537 |
</button>
|
| 538 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 539 |
<div id="notification-placeholder"></div>
|
| 540 |
|
| 541 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/Swiper/10.2.0/swiper-bundle.min.js"></script>
|
| 542 |
<script>
|
| 543 |
const products = {{ products|tojson }};
|
|
|
|
| 544 |
const repoId = '{{ repo_id }}';
|
| 545 |
const currencyCode = '{{ currency_code }}';
|
| 546 |
const t = {{ translations[lang]|tojson|safe }};
|
| 547 |
let selectedProductIndex = null;
|
| 548 |
let cart = JSON.parse(localStorage.getItem('dalarssiCart') || '[]');
|
|
|
|
| 549 |
|
| 550 |
-
function openModal(
|
| 551 |
-
|
| 552 |
-
const modal = document.getElementById('productModal');
|
| 553 |
if (modal) {
|
| 554 |
modal.style.display = "block";
|
| 555 |
document.body.style.overflow = 'hidden';
|
| 556 |
}
|
| 557 |
}
|
| 558 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 559 |
function closeModal(modalId) {
|
| 560 |
const modal = document.getElementById(modalId);
|
| 561 |
if (modal) {
|
|
@@ -629,11 +705,7 @@ CATALOG_TEMPLATE = '''
|
|
| 629 |
}
|
| 630 |
|
| 631 |
document.getElementById('quantityInput').value = 1;
|
| 632 |
-
|
| 633 |
-
if(modal) {
|
| 634 |
-
modal.style.display = 'block';
|
| 635 |
-
document.body.style.overflow = 'hidden';
|
| 636 |
-
}
|
| 637 |
}
|
| 638 |
|
| 639 |
function confirmAddToCart() {
|
|
@@ -656,14 +728,14 @@ CATALOG_TEMPLATE = '''
|
|
| 656 |
return;
|
| 657 |
}
|
| 658 |
|
| 659 |
-
const cartItemId = `${product.
|
| 660 |
const existingItemIndex = cart.findIndex(item => item.id === cartItemId);
|
| 661 |
|
| 662 |
if (existingItemIndex > -1) {
|
| 663 |
cart[existingItemIndex].quantity += quantity;
|
| 664 |
} else {
|
| 665 |
cart.push({
|
| 666 |
-
id: cartItemId, name: product.name, price: product.price,
|
| 667 |
photo: product.photos && product.photos.length > 0 ? product.photos[0] : null,
|
| 668 |
quantity: quantity, color: color, items_per_line: product.items_per_line
|
| 669 |
});
|
|
@@ -723,11 +795,19 @@ CATALOG_TEMPLATE = '''
|
|
| 723 |
}).join('');
|
| 724 |
cartTotalElement.textContent = total.toFixed(2);
|
| 725 |
}
|
| 726 |
-
|
| 727 |
-
|
| 728 |
-
|
| 729 |
-
|
| 730 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 731 |
}
|
| 732 |
|
| 733 |
function removeFromCart(itemId) {
|
|
@@ -751,7 +831,8 @@ CATALOG_TEMPLATE = '''
|
|
| 751 |
alert(t.cart_empty_error);
|
| 752 |
return;
|
| 753 |
}
|
| 754 |
-
const
|
|
|
|
| 755 |
const formulateButton = document.querySelector('.formulate-order-button');
|
| 756 |
if (formulateButton) formulateButton.disabled = true;
|
| 757 |
showNotification(t.formulating_order, 5000);
|
|
@@ -784,7 +865,6 @@ CATALOG_TEMPLATE = '''
|
|
| 784 |
});
|
| 785 |
}
|
| 786 |
|
| 787 |
-
|
| 788 |
function filterProducts() {
|
| 789 |
const searchTerm = document.getElementById('search-input').value.toLowerCase().trim();
|
| 790 |
const activeCategoryButton = document.querySelector('.category-filter.active');
|
|
@@ -836,6 +916,73 @@ CATALOG_TEMPLATE = '''
|
|
| 836 |
});
|
| 837 |
filterProducts();
|
| 838 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 839 |
|
| 840 |
function showNotification(message, duration = 3000) {
|
| 841 |
const placeholder = document.getElementById('notification-placeholder');
|
|
@@ -855,6 +1002,7 @@ CATALOG_TEMPLATE = '''
|
|
| 855 |
document.addEventListener('DOMContentLoaded', () => {
|
| 856 |
updateCartButton();
|
| 857 |
setupFilters();
|
|
|
|
| 858 |
window.addEventListener('click', function(event) {
|
| 859 |
if (event.target.classList.contains('modal')) {
|
| 860 |
closeModal(event.target.id);
|
|
@@ -969,6 +1117,9 @@ ORDER_TEMPLATE = '''
|
|
| 969 |
{% if order %}
|
| 970 |
<h1><i class="fas fa-receipt"></i> {{ _('your_order') }}{{ order.id }}</h1>
|
| 971 |
<p class="order-meta">{{ _('creation_date') }}: {{ order.created_at }}</p>
|
|
|
|
|
|
|
|
|
|
| 972 |
|
| 973 |
<h2><i class="fas fa-shopping-bag"></i> {{ _('products_in_order') }}</h2>
|
| 974 |
<div id="orderItems">
|
|
@@ -1169,9 +1320,36 @@ ADMIN_TEMPLATE = '''
|
|
| 1169 |
|
| 1170 |
<div class="flex-item">
|
| 1171 |
<div class="section">
|
| 1172 |
-
<h2><i class="fas fa-
|
| 1173 |
-
|
| 1174 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1175 |
</div>
|
| 1176 |
</div>
|
| 1177 |
</div>
|
|
@@ -1395,6 +1573,16 @@ def catalog():
|
|
| 1395 |
data = load_data()
|
| 1396 |
all_products = data.get('products', [])
|
| 1397 |
categories = sorted(data.get('categories', []))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1398 |
|
| 1399 |
products_in_stock = [p for p in all_products if p.get('in_stock', True)]
|
| 1400 |
products_sorted = sorted(products_in_stock, key=lambda p: (not p.get('is_top', False), p.get('name', '').lower()))
|
|
@@ -1403,6 +1591,7 @@ def catalog():
|
|
| 1403 |
CATALOG_TEMPLATE,
|
| 1404 |
products=products_sorted,
|
| 1405 |
categories=categories,
|
|
|
|
| 1406 |
repo_id=REPO_ID,
|
| 1407 |
store_address=STORE_ADDRESS,
|
| 1408 |
currency_code=CURRENCY_CODE
|
|
@@ -1410,7 +1599,6 @@ def catalog():
|
|
| 1410 |
|
| 1411 |
@app.route('/product/<int:index>')
|
| 1412 |
def product_detail(index):
|
| 1413 |
-
# Set language from query param if provided, for fetch requests
|
| 1414 |
lang = request.args.get('lang', 'ru')
|
| 1415 |
session['lang'] = lang if lang in translations else 'ru'
|
| 1416 |
|
|
@@ -1441,6 +1629,7 @@ def create_order():
|
|
| 1441 |
return jsonify({"error": "Корзина пуста или не передана."}), 400
|
| 1442 |
|
| 1443 |
cart_items = order_data['cart']
|
|
|
|
| 1444 |
|
| 1445 |
total_price = 0
|
| 1446 |
processed_cart = []
|
|
@@ -1475,7 +1664,7 @@ def create_order():
|
|
| 1475 |
"created_at": order_timestamp,
|
| 1476 |
"cart": processed_cart,
|
| 1477 |
"total_price": round(total_price, 2),
|
| 1478 |
-
"
|
| 1479 |
"status": "new"
|
| 1480 |
}
|
| 1481 |
|
|
@@ -1486,7 +1675,7 @@ def create_order():
|
|
| 1486 |
|
| 1487 |
data['orders'][order_id] = new_order
|
| 1488 |
save_data(data)
|
| 1489 |
-
logging.info(f"Order {order_id} created successfully
|
| 1490 |
return jsonify({"order_id": order_id}), 201
|
| 1491 |
|
| 1492 |
except Exception as e:
|
|
@@ -1513,6 +1702,7 @@ def admin():
|
|
| 1513 |
data = load_data()
|
| 1514 |
products = data.get('products', [])
|
| 1515 |
categories = data.get('categories', [])
|
|
|
|
| 1516 |
|
| 1517 |
needs_save = False
|
| 1518 |
for product in products:
|
|
@@ -1524,10 +1714,6 @@ def admin():
|
|
| 1524 |
data['products'] = products
|
| 1525 |
save_data(data)
|
| 1526 |
|
| 1527 |
-
|
| 1528 |
-
if 'orders' not in data or not isinstance(data.get('orders'), dict):
|
| 1529 |
-
data['orders'] = {}
|
| 1530 |
-
|
| 1531 |
if request.method == 'POST':
|
| 1532 |
action = request.form.get('action')
|
| 1533 |
logging.info(f"Admin action received: {action}")
|
|
@@ -1565,6 +1751,28 @@ def admin():
|
|
| 1565 |
else:
|
| 1566 |
logging.warning(f"Attempted to delete non-existent or empty category: {category_to_delete}")
|
| 1567 |
flash(f"Не удалось удалить категорию '{category_to_delete}'.", 'error')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1568 |
|
| 1569 |
elif action == 'add_product':
|
| 1570 |
name = request.form.get('name', '').strip()
|
|
@@ -1790,11 +1998,13 @@ def admin():
|
|
| 1790 |
current_data = load_data()
|
| 1791 |
display_products = sorted(current_data.get('products', []), key=lambda p: p.get('name', '').lower())
|
| 1792 |
display_categories = sorted(current_data.get('categories', []))
|
|
|
|
| 1793 |
|
| 1794 |
return render_template_string(
|
| 1795 |
ADMIN_TEMPLATE,
|
| 1796 |
products=display_products,
|
| 1797 |
categories=display_categories,
|
|
|
|
| 1798 |
repo_id=REPO_ID,
|
| 1799 |
currency_code=CURRENCY_CODE
|
| 1800 |
)
|
|
|
|
| 1 |
|
|
|
|
|
|
|
| 2 |
from flask import Flask, render_template_string, request, redirect, url_for, send_file, flash, jsonify, session
|
| 3 |
import json
|
| 4 |
import os
|
|
|
|
| 92 |
'whatsapp_contact_me': "Пожалуйста, свяжитесь со мной для уточнения деталей оплаты и доставки.",
|
| 93 |
'error': "Ошибка",
|
| 94 |
'order_not_found': "Заказ с таким ID не найден.",
|
| 95 |
+
'your_favorites': 'Избранное',
|
| 96 |
+
'favorites_is_empty': 'В избранном пока ничего нет.',
|
| 97 |
+
'addresses': 'Адреса',
|
| 98 |
+
'whatsapp': 'WhatsApp',
|
| 99 |
+
'favorites': 'Избранное',
|
| 100 |
+
'employee': 'Сотрудник:',
|
| 101 |
+
'online_order': 'Онлайн',
|
| 102 |
+
'address_1_title': 'Адрес 1:',
|
| 103 |
+
'address_1_detail': 'рынок Олжа, VIP ряд, 109 бутик',
|
| 104 |
+
'address_2_title': 'Адрес 2:',
|
| 105 |
+
'address_2_detail': 'рынок Олжа, VIP ряд, 56 бутик',
|
| 106 |
},
|
| 107 |
'kk': {
|
| 108 |
'site_title': "dalarssi - Каталог",
|
|
|
|
| 159 |
'whatsapp_contact_me': "Төлем және жеткізу мәліметтерін нақтылау үшін менімен хабарласыңыз.",
|
| 160 |
'error': "Қате",
|
| 161 |
'order_not_found': "Бұл ID-мен тапсырыс табылмады.",
|
| 162 |
+
'your_favorites': 'Таңдаулылар',
|
| 163 |
+
'favorites_is_empty': 'Таңдаулыларда әзірше ештеңе жоқ.',
|
| 164 |
+
'addresses': 'Мекенжайлар',
|
| 165 |
+
'whatsapp': 'WhatsApp',
|
| 166 |
+
'favorites': 'Таңдаулылар',
|
| 167 |
+
'employee': 'Қызметкер:',
|
| 168 |
+
'online_order': 'Онлайн',
|
| 169 |
+
'address_1_title': 'Мекенжай 1:',
|
| 170 |
+
'address_1_detail': 'Олжа базары, VIP қатары, 109 бутик',
|
| 171 |
+
'address_2_title': 'Мекенжай 2:',
|
| 172 |
+
'address_2_detail': 'Олжа базары, VIP қатары, 56 бутик',
|
| 173 |
}
|
| 174 |
}
|
| 175 |
|
|
|
|
| 229 |
try:
|
| 230 |
if file_name == DATA_FILE:
|
| 231 |
with open(file_name, 'w', encoding='utf-8') as f:
|
| 232 |
+
json.dump({'products': [], 'categories': [], 'orders': {}, 'employees': []}, f)
|
| 233 |
logging.info(f"Created empty local file {file_name} because it was not found on HF.")
|
| 234 |
except Exception as create_e:
|
| 235 |
logging.error(f"Failed to create empty local file {file_name}: {create_e}")
|
|
|
|
| 293 |
|
| 294 |
|
| 295 |
def load_data():
|
| 296 |
+
default_data = {'products': [], 'categories': [], 'orders': {}, 'employees': []}
|
| 297 |
try:
|
| 298 |
with open(DATA_FILE, 'r', encoding='utf-8') as file:
|
| 299 |
data = json.load(file)
|
|
|
|
| 304 |
if 'products' not in data: data['products'] = []
|
| 305 |
if 'categories' not in data: data['categories'] = []
|
| 306 |
if 'orders' not in data: data['orders'] = {}
|
| 307 |
+
if 'employees' not in data: data['employees'] = []
|
| 308 |
return data
|
| 309 |
except FileNotFoundError:
|
| 310 |
logging.warning(f"Local file {DATA_FILE} not found. Attempting download from HF.")
|
|
|
|
| 320 |
if 'products' not in data: data['products'] = []
|
| 321 |
if 'categories' not in data: data['categories'] = []
|
| 322 |
if 'orders' not in data: data['orders'] = {}
|
| 323 |
+
if 'employees' not in data: data['employees'] = []
|
| 324 |
return data
|
| 325 |
except FileNotFoundError:
|
| 326 |
logging.error(f"File {DATA_FILE} still not found even after download reported success. Using default.")
|
|
|
|
| 350 |
if 'products' not in data: data['products'] = []
|
| 351 |
if 'categories' not in data: data['categories'] = []
|
| 352 |
if 'orders' not in data: data['orders'] = {}
|
| 353 |
+
if 'employees' not in data: data['employees'] = []
|
| 354 |
|
| 355 |
with open(DATA_FILE, 'w', encoding='utf-8') as file:
|
| 356 |
json.dump(data, file, ensure_ascii=False, indent=4)
|
|
|
|
| 382 |
--text-muted: #a09a9a;
|
| 383 |
}
|
| 384 |
* { margin: 0; padding: 0; box-sizing: border-box; }
|
| 385 |
+
body { font-family: 'Montserrat', sans-serif; background: var(--bg-color); color: var(--text-color); line-height: 1.6; padding-bottom: 80px; }
|
| 386 |
.container { max-width: 1400px; margin: 0 auto; padding: 20px; }
|
| 387 |
.header { display: flex; justify-content: space-between; align-items: center; padding: 20px 0; border-bottom: 1px solid var(--secondary-accent); }
|
| 388 |
.header h1 { font-size: 1.8rem; font-weight: 700; color: var(--primary-accent); letter-spacing: 1px; }
|
|
|
|
| 396 |
#search-input:focus { border-color: var(--primary-accent); box-shadow: 0 0 0 4px rgba(217, 158, 203, 0.2); }
|
| 397 |
.category-filter { padding: 10px 20px; border: 1px solid var(--secondary-accent); border-radius: 30px; background-color: transparent; cursor: pointer; transition: all 0.3s ease; font-size: 0.9rem; font-weight: 600; color: var(--text-muted); }
|
| 398 |
.category-filter.active, .category-filter:hover { background-color: var(--primary-accent); color: var(--bg-color); border-color: var(--primary-accent); }
|
| 399 |
+
.products-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 20px; padding: 10px; }
|
| 400 |
+
@media (min-width: 768px) { .products-grid { grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); } }
|
|
|
|
| 401 |
.product { background: var(--surface-color); border-radius: 15px; padding: 0; box-shadow: 0 5px 20px rgba(0, 0, 0, 0.3); transition: transform 0.3s ease, box-shadow 0.3s ease; overflow: hidden; display: flex; flex-direction: column; justify-content: space-between; height: 100%; border: 1px solid var(--secondary-accent); position: relative; }
|
| 402 |
.product:hover { transform: translateY(-8px); box-shadow: 0 8px 25px rgba(0, 0, 0, 0.5); }
|
| 403 |
.product-image { width: 100%; aspect-ratio: 1 / 1.1; background-color: #fff; overflow: hidden; display: flex; justify-content: center; align-items: center; }
|
|
|
|
| 415 |
.product-button i { margin-right: 8px; }
|
| 416 |
.details-button { background-color: transparent; border: 2px solid var(--primary-accent); color: var(--primary-accent); }
|
| 417 |
.details-button:hover { background-color: var(--primary-accent); color: var(--bg-color); }
|
| 418 |
+
#cart-button { position: fixed; bottom: 90px; right: 30px; background-color: var(--primary-accent); color: var(--bg-color); border: none; border-radius: 50%; width: 60px; height: 60px; font-size: 1.6rem; cursor: pointer; display: none; align-items: center; justify-content: center; box-shadow: 0 5px 20px rgba(217, 158, 203, 0.3); transition: all 0.3s ease; z-index: 1000; }
|
| 419 |
#cart-button:hover { transform: scale(1.1); }
|
| 420 |
#cart-button span { position: absolute; top: 0px; right: 0px; background-color: var(--secondary-accent); color: var(--text-color); border-radius: 50%; padding: 3px 7px; font-size: 0.8rem; font-weight: bold; }
|
| 421 |
.modal { display: none; position: fixed; z-index: 1001; left: 0; top: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.7); backdrop-filter: blur(8px); overflow-y: auto; }
|
|
|
|
| 432 |
.cart-item-total { font-weight: bold; text-align: right; font-size: 1.1rem; color: var(--primary-accent); }
|
| 433 |
.cart-item-remove { background:none; border:none; color:#c04c4c; cursor:pointer; font-size: 1.5rem; transition: color 0.3s; }
|
| 434 |
.cart-item-remove:hover { color: #a03c3c; }
|
| 435 |
+
.quantity-input, .color-select, .employee-select { width: 100%; max-width: 200px; padding: 12px; border: 1px solid var(--secondary-accent); border-radius: 8px; font-size: 1rem; margin: 10px 0; box-sizing: border-box; background-color: var(--bg-color); color: var(--text-color); }
|
| 436 |
.cart-summary { margin-top: 25px; text-align: right; border-top: 1px solid var(--secondary-accent); padding-top: 20px; }
|
| 437 |
.cart-summary strong { font-size: 1.4rem; color: var(--primary-accent); }
|
| 438 |
.cart-actions { margin-top: 30px; display: flex; justify-content: space-between; gap: 15px; flex-wrap: wrap; }
|
|
|
|
| 443 |
.notification.show { opacity: 1;}
|
| 444 |
.no-results-message { grid-column: 1 / -1; text-align: center; padding: 50px; font-size: 1.2rem; color: var(--text-muted); }
|
| 445 |
.top-product-indicator { position: absolute; top: 12px; right: 12px; background-color: rgba(217, 158, 203, 0.9); color: var(--bg-color); padding: 4px 10px; font-size: 0.8rem; border-radius: 20px; font-weight: bold; z-index: 10; backdrop-filter: blur(3px); }
|
| 446 |
+
.favorite-button { position: absolute; top: 12px; left: 12px; background: none; border: none; color: var(--text-color); font-size: 1.5rem; cursor: pointer; z-index: 10; padding: 5px; line-height: 1; transition: color 0.3s, transform 0.3s; text-shadow: 0 0 5px rgba(0,0,0,0.5); }
|
| 447 |
+
.favorite-button.favorited { color: #e91e63; transform: scale(1.1); }
|
| 448 |
+
.favorite-button:hover { transform: scale(1.2); }
|
| 449 |
+
.bottom-nav { position: fixed; bottom: 0; left: 0; right: 0; background-color: var(--surface-color); display: flex; justify-content: space-around; align-items: center; padding: 10px 0; box-shadow: 0 -3px 15px rgba(0,0,0,0.3); z-index: 999; border-top: 1px solid var(--secondary-accent); }
|
| 450 |
+
.nav-button { background: none; border: none; color: var(--text-muted); cursor: pointer; text-align: center; font-size: 0.8rem; transition: color 0.3s; display: flex; flex-direction: column; align-items: center; gap: 4px; padding: 5px 10px; }
|
| 451 |
+
.nav-button:hover, .nav-button.active { color: var(--primary-accent); }
|
| 452 |
+
.nav-button i { font-size: 1.5rem; }
|
| 453 |
+
.address-list p { margin-bottom: 10px; }
|
| 454 |
</style>
|
| 455 |
</head>
|
| 456 |
<body>
|
|
|
|
| 482 |
<div class="products-grid" id="products-grid">
|
| 483 |
{% for product in products %}
|
| 484 |
<div class="product"
|
| 485 |
+
data-id="{{ product.id }}"
|
| 486 |
data-name="{{ product['name']|lower }}"
|
| 487 |
data-description="{{ product.get('description', '')|lower }}"
|
| 488 |
data-category="{{ product.get('category', 'Без категории') }}">
|
| 489 |
+
<button class="favorite-button" onclick="toggleFavorite('{{ product.id }}', this)"><i class="far fa-heart"></i></button>
|
| 490 |
{% if product.get('is_top', False) %}
|
| 491 |
<span class="top-product-indicator"><i class="fas fa-star"></i> {{ _('top_product') }}</span>
|
| 492 |
{% endif %}
|
|
|
|
| 512 |
</div>
|
| 513 |
</div>
|
| 514 |
<div class="product-actions">
|
| 515 |
+
<button class="product-button details-button" onclick="openModalByIndex({{ loop.index0 }})"><i class="fas fa-info-circle"></i> {{ _('details') }}</button>
|
| 516 |
<button class="product-button add-to-cart" onclick="openQuantityModal({{ loop.index0 }})">
|
| 517 |
<i class="fas fa-cart-plus"></i> {{ _('add_to_cart') }}
|
| 518 |
</button>
|
|
|
|
| 549 |
<span class="close" onclick="closeModal('cartModal')" aria-label="{{ _('close') }}">×</span>
|
| 550 |
<h2><i class="fas fa-shopping-cart"></i> {{ _('your_cart') }}</h2>
|
| 551 |
<div id="cartContent"><p style="text-align: center; padding: 20px;">{{ _('cart_is_empty') }}</p></div>
|
| 552 |
+
<div style="margin-top: 15px;">
|
| 553 |
+
<label for="employeeSelect">{{ _('employee') }}</label>
|
| 554 |
+
<select id="employeeSelect" class="employee-select"></select>
|
| 555 |
+
</div>
|
| 556 |
<div class="cart-summary">
|
| 557 |
<strong>{{ _('total') }} <span id="cartTotal">0.00</span> {{ currency_code }}</strong>
|
| 558 |
</div>
|
|
|
|
| 566 |
</div>
|
| 567 |
</div>
|
| 568 |
</div>
|
| 569 |
+
|
| 570 |
+
<div id="favoritesModal" class="modal">
|
| 571 |
+
<div class="modal-content">
|
| 572 |
+
<span class="close" onclick="closeModal('favoritesModal')" aria-label="{{ _('close') }}">×</span>
|
| 573 |
+
<h2><i class="fas fa-heart"></i> {{ _('your_favorites') }}</h2>
|
| 574 |
+
<div id="favoritesContent"><p style="text-align: center; padding: 20px;">{{ _('favorites_is_empty') }}</p></div>
|
| 575 |
+
</div>
|
| 576 |
+
</div>
|
| 577 |
+
|
| 578 |
+
<div id="addressModal" class="modal">
|
| 579 |
+
<div class="modal-content">
|
| 580 |
+
<span class="close" onclick="closeModal('addressModal')" aria-label="{{ _('close') }}">×</span>
|
| 581 |
+
<h2><i class="fas fa-map-marker-alt"></i> {{ _('addresses') }}</h2>
|
| 582 |
+
<div class="address-list">
|
| 583 |
+
<p><strong>{{ _('address_1_title') }}</strong> {{ _('address_1_detail') }}</p>
|
| 584 |
+
<p><strong>{{ _('address_2_title') }}</strong> {{ _('address_2_detail') }}</p>
|
| 585 |
+
</div>
|
| 586 |
+
</div>
|
| 587 |
+
</div>
|
| 588 |
|
| 589 |
<button id="cart-button" onclick="openCartModal()" aria-label="{{ _('open_cart') }}">
|
| 590 |
<i class="fas fa-shopping-cart"></i>
|
| 591 |
<span id="cart-count">0</span>
|
| 592 |
</button>
|
| 593 |
|
| 594 |
+
<div class="bottom-nav">
|
| 595 |
+
<a href="https://api.whatsapp.com/send?phone=+77073479416" target="_blank" class="nav-button">
|
| 596 |
+
<i class="fab fa-whatsapp"></i>
|
| 597 |
+
<span>{{ _('whatsapp') }}</span>
|
| 598 |
+
</a>
|
| 599 |
+
<button class="nav-button" onclick="openModal('addressModal')">
|
| 600 |
+
<i class="fas fa-map-marker-alt"></i>
|
| 601 |
+
<span>{{ _('addresses') }}</span>
|
| 602 |
+
</button>
|
| 603 |
+
<button class="nav-button" onclick="openFavoritesModal()">
|
| 604 |
+
<i class="fas fa-heart"></i>
|
| 605 |
+
<span>{{ _('favorites') }}</span>
|
| 606 |
+
</button>
|
| 607 |
+
</div>
|
| 608 |
+
|
| 609 |
<div id="notification-placeholder"></div>
|
| 610 |
|
| 611 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/Swiper/10.2.0/swiper-bundle.min.js"></script>
|
| 612 |
<script>
|
| 613 |
const products = {{ products|tojson }};
|
| 614 |
+
const employees = {{ employees|tojson }};
|
| 615 |
const repoId = '{{ repo_id }}';
|
| 616 |
const currencyCode = '{{ currency_code }}';
|
| 617 |
const t = {{ translations[lang]|tojson|safe }};
|
| 618 |
let selectedProductIndex = null;
|
| 619 |
let cart = JSON.parse(localStorage.getItem('dalarssiCart') || '[]');
|
| 620 |
+
let favorites = JSON.parse(localStorage.getItem('dalarssiFavorites') || '[]');
|
| 621 |
|
| 622 |
+
function openModal(modalId) {
|
| 623 |
+
const modal = document.getElementById(modalId);
|
|
|
|
| 624 |
if (modal) {
|
| 625 |
modal.style.display = "block";
|
| 626 |
document.body.style.overflow = 'hidden';
|
| 627 |
}
|
| 628 |
}
|
| 629 |
|
| 630 |
+
function openModalByIndex(index) {
|
| 631 |
+
loadProductDetails(index);
|
| 632 |
+
openModal('productModal');
|
| 633 |
+
}
|
| 634 |
+
|
| 635 |
function closeModal(modalId) {
|
| 636 |
const modal = document.getElementById(modalId);
|
| 637 |
if (modal) {
|
|
|
|
| 705 |
}
|
| 706 |
|
| 707 |
document.getElementById('quantityInput').value = 1;
|
| 708 |
+
openModal('quantityModal');
|
|
|
|
|
|
|
|
|
|
|
|
|
| 709 |
}
|
| 710 |
|
| 711 |
function confirmAddToCart() {
|
|
|
|
| 728 |
return;
|
| 729 |
}
|
| 730 |
|
| 731 |
+
const cartItemId = `${product.id}-${color}`;
|
| 732 |
const existingItemIndex = cart.findIndex(item => item.id === cartItemId);
|
| 733 |
|
| 734 |
if (existingItemIndex > -1) {
|
| 735 |
cart[existingItemIndex].quantity += quantity;
|
| 736 |
} else {
|
| 737 |
cart.push({
|
| 738 |
+
id: cartItemId, productId: product.id, name: product.name, price: product.price,
|
| 739 |
photo: product.photos && product.photos.length > 0 ? product.photos[0] : null,
|
| 740 |
quantity: quantity, color: color, items_per_line: product.items_per_line
|
| 741 |
});
|
|
|
|
| 795 |
}).join('');
|
| 796 |
cartTotalElement.textContent = total.toFixed(2);
|
| 797 |
}
|
| 798 |
+
|
| 799 |
+
const employeeSelect = document.getElementById('employeeSelect');
|
| 800 |
+
if (employeeSelect) {
|
| 801 |
+
employeeSelect.innerHTML = `<option value="${t.online_order}">${t.online_order}</option>`;
|
| 802 |
+
employees.forEach(emp => {
|
| 803 |
+
const option = document.createElement('option');
|
| 804 |
+
option.value = emp;
|
| 805 |
+
option.text = emp;
|
| 806 |
+
employeeSelect.appendChild(option);
|
| 807 |
+
});
|
| 808 |
+
}
|
| 809 |
+
|
| 810 |
+
openModal('cartModal');
|
| 811 |
}
|
| 812 |
|
| 813 |
function removeFromCart(itemId) {
|
|
|
|
| 831 |
alert(t.cart_empty_error);
|
| 832 |
return;
|
| 833 |
}
|
| 834 |
+
const employee = document.getElementById('employeeSelect').value;
|
| 835 |
+
const orderData = { cart: cart, employee: employee };
|
| 836 |
const formulateButton = document.querySelector('.formulate-order-button');
|
| 837 |
if (formulateButton) formulateButton.disabled = true;
|
| 838 |
showNotification(t.formulating_order, 5000);
|
|
|
|
| 865 |
});
|
| 866 |
}
|
| 867 |
|
|
|
|
| 868 |
function filterProducts() {
|
| 869 |
const searchTerm = document.getElementById('search-input').value.toLowerCase().trim();
|
| 870 |
const activeCategoryButton = document.querySelector('.category-filter.active');
|
|
|
|
| 916 |
});
|
| 917 |
filterProducts();
|
| 918 |
}
|
| 919 |
+
|
| 920 |
+
function toggleFavorite(productId, buttonElement) {
|
| 921 |
+
const index = favorites.indexOf(productId);
|
| 922 |
+
if (index > -1) {
|
| 923 |
+
favorites.splice(index, 1);
|
| 924 |
+
buttonElement.classList.remove('favorited');
|
| 925 |
+
buttonElement.innerHTML = '<i class="far fa-heart"></i>';
|
| 926 |
+
} else {
|
| 927 |
+
favorites.push(productId);
|
| 928 |
+
buttonElement.classList.add('favorited');
|
| 929 |
+
buttonElement.innerHTML = '<i class="fas fa-heart"></i>';
|
| 930 |
+
}
|
| 931 |
+
localStorage.setItem('dalarssiFavorites', JSON.stringify(favorites));
|
| 932 |
+
}
|
| 933 |
+
|
| 934 |
+
function updateFavoriteIcons() {
|
| 935 |
+
document.querySelectorAll('.favorite-button').forEach(button => {
|
| 936 |
+
const productId = button.closest('.product').dataset.id;
|
| 937 |
+
if (favorites.includes(productId)) {
|
| 938 |
+
button.classList.add('favorited');
|
| 939 |
+
button.innerHTML = '<i class="fas fa-heart"></i>';
|
| 940 |
+
} else {
|
| 941 |
+
button.classList.remove('favorited');
|
| 942 |
+
button.innerHTML = '<i class="far fa-heart"></i>';
|
| 943 |
+
}
|
| 944 |
+
});
|
| 945 |
+
}
|
| 946 |
+
|
| 947 |
+
function openFavoritesModal() {
|
| 948 |
+
const favoritesContent = document.getElementById('favoritesContent');
|
| 949 |
+
favoritesContent.innerHTML = '';
|
| 950 |
+
|
| 951 |
+
if (favorites.length === 0) {
|
| 952 |
+
favoritesContent.innerHTML = `<p style="text-align: center; padding: 20px;">${t.favorites_is_empty}</p>`;
|
| 953 |
+
} else {
|
| 954 |
+
const favoriteProducts = products.filter(p => favorites.includes(p.id));
|
| 955 |
+
favoriteProducts.forEach(item => {
|
| 956 |
+
const photoUrl = item.photos && item.photos.length > 0
|
| 957 |
+
? `https://huggingface.co/datasets/${repoId}/resolve/main/photos/${item.photos[0]}`
|
| 958 |
+
: 'https://via.placeholder.com/70x70.png?text=N/A';
|
| 959 |
+
|
| 960 |
+
const itemHtml = `
|
| 961 |
+
<div class="cart-item">
|
| 962 |
+
<img src="${photoUrl}" alt="${item.name}">
|
| 963 |
+
<div class="cart-item-details">
|
| 964 |
+
<strong>${item.name}</strong>
|
| 965 |
+
<p class="cart-item-price">${item.price.toFixed(2)} ${currencyCode}</p>
|
| 966 |
+
</div>
|
| 967 |
+
<span class="cart-item-total"></span>
|
| 968 |
+
<button class="cart-item-remove" onclick="removeFromFavorites('${item.id}')" title="Удалить из избранного">×</button>
|
| 969 |
+
</div>`;
|
| 970 |
+
favoritesContent.innerHTML += itemHtml;
|
| 971 |
+
});
|
| 972 |
+
}
|
| 973 |
+
|
| 974 |
+
openModal('favoritesModal');
|
| 975 |
+
}
|
| 976 |
+
|
| 977 |
+
function removeFromFavorites(productId) {
|
| 978 |
+
const index = favorites.indexOf(productId);
|
| 979 |
+
if (index > -1) {
|
| 980 |
+
favorites.splice(index, 1);
|
| 981 |
+
localStorage.setItem('dalarssiFavorites', JSON.stringify(favorites));
|
| 982 |
+
openFavoritesModal();
|
| 983 |
+
updateFavoriteIcons();
|
| 984 |
+
}
|
| 985 |
+
}
|
| 986 |
|
| 987 |
function showNotification(message, duration = 3000) {
|
| 988 |
const placeholder = document.getElementById('notification-placeholder');
|
|
|
|
| 1002 |
document.addEventListener('DOMContentLoaded', () => {
|
| 1003 |
updateCartButton();
|
| 1004 |
setupFilters();
|
| 1005 |
+
updateFavoriteIcons();
|
| 1006 |
window.addEventListener('click', function(event) {
|
| 1007 |
if (event.target.classList.contains('modal')) {
|
| 1008 |
closeModal(event.target.id);
|
|
|
|
| 1117 |
{% if order %}
|
| 1118 |
<h1><i class="fas fa-receipt"></i> {{ _('your_order') }}{{ order.id }}</h1>
|
| 1119 |
<p class="order-meta">{{ _('creation_date') }}: {{ order.created_at }}</p>
|
| 1120 |
+
{% if order.employee %}
|
| 1121 |
+
<p class="order-meta">{{ _('employee') }} {{ order.employee }}</p>
|
| 1122 |
+
{% endif %}
|
| 1123 |
|
| 1124 |
<h2><i class="fas fa-shopping-bag"></i> {{ _('products_in_order') }}</h2>
|
| 1125 |
<div id="orderItems">
|
|
|
|
| 1320 |
|
| 1321 |
<div class="flex-item">
|
| 1322 |
<div class="section">
|
| 1323 |
+
<h2><i class="fas fa-users"></i> Управление сотрудниками</h2>
|
| 1324 |
+
<details>
|
| 1325 |
+
<summary><i class="fas fa-user-plus"></i> Добавить нового сотрудника</summary>
|
| 1326 |
+
<div class="form-content">
|
| 1327 |
+
<form method="POST">
|
| 1328 |
+
<input type="hidden" name="action" value="add_employee">
|
| 1329 |
+
<label for="add_employee_name">Имя сотрудника:</label>
|
| 1330 |
+
<input type="text" id="add_employee_name" name="employee_name" required>
|
| 1331 |
+
<button type="submit" class="add-button"><i class="fas fa-plus"></i> Добавить</button>
|
| 1332 |
+
</form>
|
| 1333 |
+
</div>
|
| 1334 |
+
</details>
|
| 1335 |
+
|
| 1336 |
+
<h3>Список сотрудников:</h3>
|
| 1337 |
+
{% if employees %}
|
| 1338 |
+
<div class="item-list">
|
| 1339 |
+
{% for employee in employees %}
|
| 1340 |
+
<div class="item" style="display: flex; justify-content: space-between; align-items: center;">
|
| 1341 |
+
<span>{{ employee }}</span>
|
| 1342 |
+
<form method="POST" style="margin: 0;" onsubmit="return confirm('Вы уверены, что хотите удалить сотрудника \'{{ employee }}\'?');">
|
| 1343 |
+
<input type="hidden" name="action" value="delete_employee">
|
| 1344 |
+
<input type="hidden" name="employee_name" value="{{ employee }}">
|
| 1345 |
+
<button type="submit" class="delete-button" style="padding: 5px 10px; font-size: 0.8rem; margin: 0;"><i class="fas fa-trash-alt"></i></button>
|
| 1346 |
+
</form>
|
| 1347 |
+
</div>
|
| 1348 |
+
{% endfor %}
|
| 1349 |
+
</div>
|
| 1350 |
+
{% else %}
|
| 1351 |
+
<p>Сотрудников пока нет.</p>
|
| 1352 |
+
{% endif %}
|
| 1353 |
</div>
|
| 1354 |
</div>
|
| 1355 |
</div>
|
|
|
|
| 1573 |
data = load_data()
|
| 1574 |
all_products = data.get('products', [])
|
| 1575 |
categories = sorted(data.get('categories', []))
|
| 1576 |
+
employees = sorted(data.get('employees', []))
|
| 1577 |
+
|
| 1578 |
+
needs_save = False
|
| 1579 |
+
for product in all_products:
|
| 1580 |
+
if 'id' not in product or not product['id']:
|
| 1581 |
+
product['id'] = str(uuid.uuid4())
|
| 1582 |
+
needs_save = True
|
| 1583 |
+
if needs_save:
|
| 1584 |
+
data['products'] = all_products
|
| 1585 |
+
save_data(data)
|
| 1586 |
|
| 1587 |
products_in_stock = [p for p in all_products if p.get('in_stock', True)]
|
| 1588 |
products_sorted = sorted(products_in_stock, key=lambda p: (not p.get('is_top', False), p.get('name', '').lower()))
|
|
|
|
| 1591 |
CATALOG_TEMPLATE,
|
| 1592 |
products=products_sorted,
|
| 1593 |
categories=categories,
|
| 1594 |
+
employees=employees,
|
| 1595 |
repo_id=REPO_ID,
|
| 1596 |
store_address=STORE_ADDRESS,
|
| 1597 |
currency_code=CURRENCY_CODE
|
|
|
|
| 1599 |
|
| 1600 |
@app.route('/product/<int:index>')
|
| 1601 |
def product_detail(index):
|
|
|
|
| 1602 |
lang = request.args.get('lang', 'ru')
|
| 1603 |
session['lang'] = lang if lang in translations else 'ru'
|
| 1604 |
|
|
|
|
| 1629 |
return jsonify({"error": "Корзина пуста или не передана."}), 400
|
| 1630 |
|
| 1631 |
cart_items = order_data['cart']
|
| 1632 |
+
employee_name = order_data.get('employee', 'Онлайн')
|
| 1633 |
|
| 1634 |
total_price = 0
|
| 1635 |
processed_cart = []
|
|
|
|
| 1664 |
"created_at": order_timestamp,
|
| 1665 |
"cart": processed_cart,
|
| 1666 |
"total_price": round(total_price, 2),
|
| 1667 |
+
"employee": employee_name,
|
| 1668 |
"status": "new"
|
| 1669 |
}
|
| 1670 |
|
|
|
|
| 1675 |
|
| 1676 |
data['orders'][order_id] = new_order
|
| 1677 |
save_data(data)
|
| 1678 |
+
logging.info(f"Order {order_id} created successfully by {employee_name}.")
|
| 1679 |
return jsonify({"order_id": order_id}), 201
|
| 1680 |
|
| 1681 |
except Exception as e:
|
|
|
|
| 1702 |
data = load_data()
|
| 1703 |
products = data.get('products', [])
|
| 1704 |
categories = data.get('categories', [])
|
| 1705 |
+
employees = data.get('employees', [])
|
| 1706 |
|
| 1707 |
needs_save = False
|
| 1708 |
for product in products:
|
|
|
|
| 1714 |
data['products'] = products
|
| 1715 |
save_data(data)
|
| 1716 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1717 |
if request.method == 'POST':
|
| 1718 |
action = request.form.get('action')
|
| 1719 |
logging.info(f"Admin action received: {action}")
|
|
|
|
| 1751 |
else:
|
| 1752 |
logging.warning(f"Attempted to delete non-existent or empty category: {category_to_delete}")
|
| 1753 |
flash(f"Не удалось удалить категорию '{category_to_delete}'.", 'error')
|
| 1754 |
+
|
| 1755 |
+
elif action == 'add_employee':
|
| 1756 |
+
employee_name = request.form.get('employee_name', '').strip()
|
| 1757 |
+
if employee_name and employee_name not in employees:
|
| 1758 |
+
employees.append(employee_name)
|
| 1759 |
+
data['employees'] = employees
|
| 1760 |
+
save_data(data)
|
| 1761 |
+
flash(f"Сотрудник '{employee_name}' успешно добавлен.", 'success')
|
| 1762 |
+
elif not employee_name:
|
| 1763 |
+
flash("Имя сотрудника не может быть пустым.", 'error')
|
| 1764 |
+
else:
|
| 1765 |
+
flash(f"Сотрудник '{employee_name}' уже существует.", 'error')
|
| 1766 |
+
|
| 1767 |
+
elif action == 'delete_employee':
|
| 1768 |
+
employee_to_delete = request.form.get('employee_name')
|
| 1769 |
+
if employee_to_delete and employee_to_delete in employees:
|
| 1770 |
+
employees.remove(employee_to_delete)
|
| 1771 |
+
data['employees'] = employees
|
| 1772 |
+
save_data(data)
|
| 1773 |
+
flash(f"Сотрудник '{employee_to_delete}' удален.", 'success')
|
| 1774 |
+
else:
|
| 1775 |
+
flash(f"Не удалось удалить сотрудника '{employee_to_delete}'.", 'error')
|
| 1776 |
|
| 1777 |
elif action == 'add_product':
|
| 1778 |
name = request.form.get('name', '').strip()
|
|
|
|
| 1998 |
current_data = load_data()
|
| 1999 |
display_products = sorted(current_data.get('products', []), key=lambda p: p.get('name', '').lower())
|
| 2000 |
display_categories = sorted(current_data.get('categories', []))
|
| 2001 |
+
display_employees = sorted(current_data.get('employees', []))
|
| 2002 |
|
| 2003 |
return render_template_string(
|
| 2004 |
ADMIN_TEMPLATE,
|
| 2005 |
products=display_products,
|
| 2006 |
categories=display_categories,
|
| 2007 |
+
employees=display_employees,
|
| 2008 |
repo_id=REPO_ID,
|
| 2009 |
currency_code=CURRENCY_CODE
|
| 2010 |
)
|