Kgshop commited on
Commit
8153dc8
·
verified ·
1 Parent(s): f283625

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +248 -38
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(auto-fill, minmax(200px, 1fr)); gap: 25px; padding: 10px; }
377
- @media (min-width: 600px) { .products-grid { grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); } }
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: 30px; 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; }
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="openModal({{ loop.index0 }})"><i class="fas fa-info-circle"></i> {{ _('details') }}</button>
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(index) {
551
- loadProductDetails(index);
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
- const modal = document.getElementById('quantityModal');
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.name}-${color}`;
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
- const modal = document.getElementById('cartModal');
727
- if (modal) {
728
- modal.style.display = 'block';
729
- document.body.style.overflow = 'hidden';
730
- }
 
 
 
 
 
 
 
 
731
  }
732
 
733
  function removeFromCart(itemId) {
@@ -751,7 +831,8 @@ CATALOG_TEMPLATE = '''
751
  alert(t.cart_empty_error);
752
  return;
753
  }
754
- const orderData = { cart: cart };
 
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-info-circle"></i> Информация</h2>
1173
- <p>Управление пользователями отключено, так как сайт не требует входа.</p>
1174
- <p>Заказы создаются анонимно и должны быть подтверждены через WhatsApp.</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- "user_info": None,
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 (anonymously).")
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
  )