Kgshop commited on
Commit
06a614c
·
verified ·
1 Parent(s): eab79ff

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +271 -268
app.py CHANGED
@@ -54,12 +54,11 @@ STATUS_MAPS = {
54
 
55
  translations = {
56
  'ru': {
57
- 'page_title': "Esmira - Каталог тканей и одежды",
58
  'header_title': "Esmira",
59
  'our_addresses': "Наши адреса в г. Алматы",
60
  'search_placeholder': "Поиск по названию или описанию...",
61
  'all_filter': "Все",
62
- 'top_product': "Топ",
63
  'add_to_cart_button': "В корзину",
64
  'from_price': "от",
65
  'no_products_yet': "Товары пока не добавлены.",
@@ -68,7 +67,8 @@ translations = {
68
  'specify_details': "Укажите детали",
69
  'variant_label': "Вариант:",
70
  'size_label': "Размер:",
71
- 'quantity_label': "Количество (в метрах для тканей):",
 
72
  'confirm_add_to_cart': "Добавить в корзину",
73
  'your_cart': "Ваша корзина",
74
  'cart_is_empty': "Ваша корзина пуста.",
@@ -84,9 +84,6 @@ translations = {
84
  'add_to_cart_notification': "добавлен в корзину!",
85
  'no_products_found': "По вашему запросу товары не найдены.",
86
  'category': "Категория",
87
- 'product_type': "Тип товара",
88
- 'fabrics': "Ткани",
89
- 'clothing': "Одежда",
90
  'no_category': "Без категории",
91
  'available_variants': "Доступные варианты:",
92
  'available_sizes': "Доступные размеры:",
@@ -107,14 +104,17 @@ translations = {
107
  'whatsapp_confirm_message_2': "Номер заказа:",
108
  'whatsapp_confirm_message_3': "Ссылка на заказ:",
109
  'whatsapp_confirm_message_4': "Пожалуйста, свяжитесь со мной для уточнения деталей.",
 
 
 
 
110
  },
111
  'kz': {
112
- 'page_title': "Esmira - Маталар мен киімдер каталогы",
113
  'header_title': "Esmira",
114
  'our_addresses': "Алматы қаласындағы мекенжайларымыз",
115
  'search_placeholder': "Аты немесе сипаттамасы бойынша іздеу...",
116
  'all_filter': "Барлығы",
117
- 'top_product': "Топ",
118
  'add_to_cart_button': "Себетке салу",
119
  'from_price': "-ден бастап",
120
  'no_products_yet': "Тауарлар әлі қосылмаған.",
@@ -123,7 +123,8 @@ translations = {
123
  'specify_details': "Мәліметтерді көрсетіңіз",
124
  'variant_label': "Нұсқа:",
125
  'size_label': "Өлшем:",
126
- 'quantity_label': "Саны (маталар үшін метрмен):",
 
127
  'confirm_add_to_cart': "Себетке қосу",
128
  'your_cart': "Сіздің себетіңіз",
129
  'cart_is_empty': "Сіздің себетіңіз бос.",
@@ -139,9 +140,6 @@ translations = {
139
  'add_to_cart_notification': "себетке қосылды!",
140
  'no_products_found': "Сіздің сұранысыңыз бойынша тауарлар табылмады.",
141
  'category': "Санат",
142
- 'product_type': "Тауар түрі",
143
- 'fabrics': "Маталар",
144
- 'clothing': "Киім",
145
  'no_category': "Санатсыз",
146
  'available_variants': "Қолжетімді нұсқалар:",
147
  'available_sizes': "Қолжетімді өлшемдер:",
@@ -162,6 +160,10 @@ translations = {
162
  'whatsapp_confirm_message_2': "Тапсырыс нөмірі:",
163
  'whatsapp_confirm_message_3': "Тапсырысқа сілтеме:",
164
  'whatsapp_confirm_message_4': "Мәліметтерді нақтылау үшін менімен хабарласуыңызды сұраймын.",
 
 
 
 
165
  }
166
  }
167
 
@@ -208,7 +210,7 @@ def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWN
208
  try:
209
  if file_name == DATA_FILE:
210
  with open(file_name, 'w', encoding='utf-8') as f:
211
- json.dump({'products': [], 'categories': [], 'orders': {}, 'settings': {}}, f)
212
  logging.info(f"Created empty local file {file_name} because it was not found on HF.")
213
  except Exception as create_e:
214
  logging.error(f"Failed to create empty local file {file_name}: {create_e}")
@@ -272,7 +274,7 @@ def periodic_backup():
272
 
273
 
274
  def load_data():
275
- default_data = {'products': [], 'categories': [], 'orders': {}, 'settings': {'whatsapp_number': '77073363943'}}
276
  try:
277
  with open(DATA_FILE, 'r', encoding='utf-8') as file:
278
  data = json.load(file)
@@ -283,7 +285,7 @@ def load_data():
283
  if 'products' not in data: data['products'] = []
284
  if 'categories' not in data: data['categories'] = []
285
  if 'orders' not in data: data['orders'] = {}
286
- if 'settings' not in data: data['settings'] = {'whatsapp_number': '77073363943'}
287
  return data
288
  except FileNotFoundError:
289
  logging.warning(f"Local file {DATA_FILE} not found. Attempting download from HF.")
@@ -301,7 +303,7 @@ def load_data():
301
  if 'products' not in data: data['products'] = []
302
  if 'categories' not in data: data['categories'] = []
303
  if 'orders' not in data: data['orders'] = {}
304
- if 'settings' not in data: data['settings'] = {'whatsapp_number': '77073363943'}
305
  return data
306
  except FileNotFoundError:
307
  logging.error(f"File {DATA_FILE} still not found even after download reported success. Using default.")
@@ -331,7 +333,8 @@ def save_data(data):
331
  if 'products' not in data: data['products'] = []
332
  if 'categories' not in data: data['categories'] = []
333
  if 'orders' not in data: data['orders'] = {}
334
- if 'settings' not in data: data['settings'] = {}
 
335
 
336
  with open(DATA_FILE, 'w', encoding='utf-8') as file:
337
  json.dump(data, file, ensure_ascii=False, indent=4)
@@ -356,30 +359,30 @@ CATALOG_TEMPLATE = '''
356
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/Swiper/10.2.0/swiper-bundle.min.css">
357
  <style>
358
  :root {
359
- --primary-color: #B22222;
360
- --primary-dark: #8B0000;
361
- --surface-color: #2b2b2b;
362
- --background-color: #1c1c1c;
363
  --text-color: #F5F5F5;
364
- --text-color-muted: #a0a0a0;
365
- --border-color: #444;
366
- --success-color: #28a745;
367
  }
368
  * { margin: 0; padding: 0; box-sizing: border-box; }
369
- body {
370
- font-family: 'Georgia', serif;
371
- background: var(--background-color);
372
- color: var(--text-color);
373
- line-height: 1.6;
374
- transition: background-color 0.3s;
375
  }
376
  .container { max-width: 100%; margin: 0 auto; padding: 0; }
377
  .content-area { padding: 20px; }
378
 
379
- .header { display: flex; justify-content: space-between; align-items: center; padding: 15px 20px; background: rgba(28, 28, 28, 0.8); backdrop-filter: blur(10px); border-bottom: 1px solid var(--border-color); position: sticky; top: 0; z-index: 1000; }
380
  .logo-title-container { display: flex; align-items: center; gap: 15px; }
381
- .logo-title-container img { height: 45px; width: 45px; border-radius: 50%; object-fit: cover; }
382
- .header h1 { font-family: 'Cormorant Garamond', serif; font-size: 1.8rem; font-weight: 700; color: var(--text-color); }
383
 
384
  .lang-switcher { display: flex; gap: 5px; background-color: var(--surface-color); padding: 5px; border-radius: 50px; }
385
  .lang-switcher a { color: var(--text-color-muted); text-decoration: none; font-size: 0.9rem; padding: 5px 10px; border-radius: 50px; transition: all 0.3s; }
@@ -391,36 +394,36 @@ CATALOG_TEMPLATE = '''
391
 
392
  .search-container { padding: 0 20px 20px; }
393
  #search-input { width: 100%; padding: 12px 20px; font-size: 1rem; border: 1px solid var(--border-color); border-radius: 50px; outline: none; transition: all 0.3s; background-color: var(--surface-color); color: var(--text-color); }
394
- #search-input:focus { border-color: var(--primary-color); box-shadow: 0 0 0 4px rgba(178, 34, 34, 0.2); }
395
 
396
  .filters-wrapper { margin: 0 20px 20px; display: flex; flex-direction: column; gap: 15px; }
397
  .filters-container { display: flex; overflow-x: auto; gap: 10px; padding-bottom: 10px; scrollbar-width: none; -ms-overflow-style: none; }
398
  .filters-container::-webkit-scrollbar { display: none; }
399
  .filter-label { font-size: 0.9rem; color: var(--text-color-muted); margin-left: 5px; }
400
  .filter-btn { padding: 8px 18px; border: 1px solid var(--border-color); border-radius: 50px; background-color: transparent; cursor: pointer; transition: all 0.3s ease; font-size: 0.9rem; font-weight: 400; color: var(--text-color-muted); white-space: nowrap; }
401
- .filter-btn.active, .filter-btn:hover { background-color: var(--primary-color); color: #fff; border-color: var(--primary-color); font-weight: 500; box-shadow: 0 2px 8px rgba(178, 34, 34, 0.3); transform: translateY(-2px); }
402
 
403
  .products-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 15px; padding: 0 20px 120px; }
404
  @media (min-width: 600px) { .products-grid { grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); gap: 20px; } }
405
 
406
  .product { background: var(--surface-color); border-radius: 16px; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.4); transition: all 0.3s ease; overflow: hidden; display: flex; flex-direction: column; height: 100%; position: relative; border: 1px solid var(--border-color); }
407
- .product:hover { transform: translateY(-5px); box-shadow: 0 8px 25px rgba(178, 34, 34, 0.15); border-color: var(--primary-color); }
408
 
409
- .product-image { width: 100%; aspect-ratio: 1 / 1; background-color: #000; display: flex; justify-content: center; align-items: center; padding: 10px; }
410
  .product-image img { max-width: 100%; max-height: 100%; object-fit: contain; transition: transform 0.3s ease; }
411
  .product:hover .product-image img { transform: scale(1.05); }
412
  .product-info { padding: 15px; flex-grow: 1; display: flex; flex-direction: column; text-align: center; }
413
- .product h2 { font-family: 'Cormorant Garamond', serif; font-size: 1.2rem; font-weight: 600; margin: 0 0 8px 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--text-color); }
414
  .product-price { font-size: 1.2rem; color: var(--primary-color); font-weight: 700; margin: 5px 0; }
415
  .product-price .from-text { font-size: 0.8rem; color: var(--text-color-muted); font-weight: 400; }
416
  .product-description { display: none; }
417
  .product-actions { padding: 0 15px 15px; }
418
 
419
  .product-button { display: inline-flex; align-items: center; justify-content: center; width: 100%; padding: 10px; border: none; border-radius: 50px; background-color: var(--primary-color); color: #fff; font-size: 0.9rem; font-weight: 500; cursor: pointer; transition: all 0.3s ease; text-decoration: none; text-transform: uppercase; letter-spacing: 0.5px; }
420
- .product-button:hover { background-color: var(--primary-dark); box-shadow: 0 4px 10px rgba(178, 34, 34, 0.4); }
421
  .product-button i { margin-right: 8px; }
422
 
423
- .fab { position: fixed; background-color: var(--primary-color); color: #fff; border: none; border-radius: 50%; width: 55px; height: 55px; font-size: 1.5rem; cursor: pointer; display: flex; align-items: center; justify-content: center; box-shadow: 0 4px 15px rgba(178, 34, 34, 0.4); z-index: 1000; transition: transform 0.2s ease; }
424
  .fab:hover { transform: scale(1.1); }
425
  #cart-button { bottom: 20px; right: 20px; display: none; }
426
  #cart-button span { position: absolute; top: -2px; right: -2px; background-color: #dc3545; color: white; border-radius: 50%; padding: 3px 7px; font-size: 0.75rem; font-weight: bold; min-width: 22px; text-align: center; }
@@ -436,7 +439,7 @@ CATALOG_TEMPLATE = '''
436
 
437
  .cart-item { display: grid; grid-template-columns: 65px 1fr auto 25px; gap: 15px; align-items: center; padding: 15px 0; border-bottom: 1px solid var(--border-color); }
438
  .cart-item:last-child { border-bottom: none; }
439
- .cart-item img { width: 65px; height: 65px; object-fit: contain; border-radius: 8px; background-color: #000; padding: 5px; }
440
  .cart-item-details strong { display: block; margin-bottom: 4px; font-size: 1.05rem; }
441
  .cart-item-price { font-size: 0.9rem; color: var(--text-color-muted); }
442
  .cart-item-total { font-weight: bold; text-align: right; font-size: 1rem; color: var(--primary-color); }
@@ -455,7 +458,6 @@ CATALOG_TEMPLATE = '''
455
  .notification { position: fixed; bottom: 80px; left: 50%; transform: translateX(-50%); background-color: var(--primary-color); color: #fff; padding: 12px 25px; border-radius: 50px; box-shadow: 0 4px 15px rgba(0,0,0,0.5); z-index: 1002; opacity: 0; transition: all 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94); font-size: 0.95rem; font-weight: 500; }
456
  .notification.show { opacity: 1; bottom: 90px; }
457
  .no-results-message { grid-column: 1 / -1; text-align: center; padding: 50px; font-size: 1.2rem; color: var(--text-color-muted); }
458
- .top-product-indicator { position: absolute; top: 10px; right: 10px; background: linear-gradient(135deg, #B22222, #8B0000); color: #fff; padding: 3px 8px; font-size: 0.7rem; border-radius: 50px; font-weight: bold; z-index: 10; display: flex; align-items: center; gap: 4px; box-shadow: 0 1px 3px rgba(0,0,0,0.5); }
459
 
460
  #whatsapp-fab { bottom: 20px; left: 20px; background-color: #25D366; color: white; }
461
  </style>
@@ -486,12 +488,12 @@ CATALOG_TEMPLATE = '''
486
  </div>
487
 
488
  <div class="filters-wrapper">
489
- <div>
490
  <span class="filter-label">{{ _['product_type'] }}:</span>
491
  <div class="filters-container">
492
  <button class="filter-btn type-filter active" data-type="all">{{ _['all_filter'] }}</button>
493
- <button class="filter-btn type-filter" data-type="Ткани">{{ _['fabrics'] }}</button>
494
- <button class="filter-btn type-filter" data-type="Одежда">{{ _['clothing'] }}</button>
495
  </div>
496
  </div>
497
  <div>
@@ -512,10 +514,7 @@ CATALOG_TEMPLATE = '''
512
  data-name="{{ product['name']|lower }}"
513
  data-description="{{ product.get('description', '')|lower }}"
514
  data-category="{{ product.get('category', _['no_category']) }}"
515
- data-type="{{ product.get('type') }}">
516
- {% if product.get('is_top', False) %}
517
- <span class="top-product-indicator"><i class="fas fa-star fa-xs"></i> {{ _['top_product'] }}</span>
518
- {% endif %}
519
  <div class="product-image" onclick="openModal({{ loop.index0 }})" style="cursor: pointer;">
520
  {% if product.get('photos') and product['photos']|length > 0 %}
521
  <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ product['photos'][0] }}"
@@ -560,16 +559,18 @@ CATALOG_TEMPLATE = '''
560
  <span class="close" onclick="closeModal('quantityModal')" aria-label="Закрыть">&times;</span>
561
  <h2>{{ _['specify_details'] }}</h2>
562
 
563
- <label for="variantSelect">{{ _['variant_label'] }}</label>
564
- <select id="variantSelect" class="variant-select"></select>
 
 
565
 
566
- <div id="size-selection-area" style="display: none;">
567
  <label for="sizeSelect">{{ _['size_label'] }}</label>
568
  <select id="sizeSelect" class="size-select"></select>
569
  </div>
570
-
571
- <label for="quantityInput">{{ _['quantity_label'] }}</label>
572
- <input type="number" id="quantityInput" class="quantity-input" min="0.1" step="0.1" value="1">
573
 
574
  <button class="product-button" onclick="confirmAddToCart()"><i class="fas fa-check"></i> {{ _['confirm_add_to_cart'] }}</button>
575
  </div>
@@ -599,7 +600,9 @@ CATALOG_TEMPLATE = '''
599
  <span id="cart-count">0</span>
600
  </button>
601
 
602
- <a id="whatsapp-fab" class="fab" href="https://api.whatsapp.com/send?phone={{ settings.whatsapp_number }}" target="_blank" rel="noopener noreferrer"><i class="fab fa-whatsapp"></i></a>
 
 
603
 
604
  <div id="notification-placeholder"></div>
605
 
@@ -609,7 +612,7 @@ CATALOG_TEMPLATE = '''
609
  const repoId = '{{ repo_id }}';
610
  const currencyCode = '{{ currency_code }}';
611
  let selectedProductIndex = null;
612
- let cart = JSON.parse(localStorage.getItem('esmiraCart') || '[]');
613
  const langCode = '{{ lang_code }}';
614
  const translations = {{ _|tojson }};
615
 
@@ -651,16 +654,16 @@ CATALOG_TEMPLATE = '''
651
  autoplay: { delay: 4000, disableOnInteraction: true },
652
  });
653
  }
654
-
655
- function setupOptionSelect(selectId, options, textFormatter) {
656
  const select = document.getElementById(selectId);
657
  select.innerHTML = '';
658
- if (options && options.length > 0) {
659
- options.forEach((option, index) => {
660
- const opt = document.createElement('option');
661
- opt.value = index;
662
- opt.text = textFormatter(option);
663
- select.appendChild(opt);
664
  });
665
  select.parentElement.style.display = 'block';
666
  } else {
@@ -672,23 +675,26 @@ CATALOG_TEMPLATE = '''
672
  selectedProductIndex = index;
673
  const product = products[index];
674
  if (!product) return;
675
-
676
- setupOptionSelect('variantSelect', product.variants, v => `${v.name} - ${v.price.toFixed(2)} ${currencyCode}`);
677
 
678
- const sizeArea = document.getElementById('size-selection-area');
679
  const quantityInput = document.getElementById('quantityInput');
 
680
 
681
- if (product.type === 'Одежда') {
682
- setupOptionSelect('sizeSelect', product.sizes, s => s.name);
683
- sizeArea.style.display = 'block';
 
 
684
  quantityInput.step = "1";
685
  quantityInput.min = "1";
686
- quantityInput.value = 1;
 
687
  } else {
688
- sizeArea.style.display = 'none';
 
689
  quantityInput.step = "0.1";
690
  quantityInput.min = "0.1";
691
- quantityInput.value = 1;
 
692
  }
693
 
694
  document.getElementById('quantityModal').style.display = 'block';
@@ -698,38 +704,34 @@ CATALOG_TEMPLATE = '''
698
  function confirmAddToCart() {
699
  if (selectedProductIndex === null) return;
700
 
701
- const quantityInput = document.getElementById('quantityInput');
702
  const product = products[selectedProductIndex];
703
-
704
- const quantity = product.type === 'Ткани' ? parseFloat(quantityInput.value) : parseInt(quantityInput.value);
705
-
706
- if (isNaN(quantity) || quantity <= 0) {
707
- alert("Пожалуйста, укажите корректное количество.");
 
708
  return;
709
  }
710
 
711
- const variantIndex = document.getElementById('variantSelect').value;
712
  const selectedVariant = product.variants[variantIndex];
713
-
714
  let selectedSize = null;
715
- let sizeName = null;
716
- if (product.type === 'Одежда') {
717
- const sizeIndex = document.getElementById('sizeSelect').value;
718
- if(product.sizes && product.sizes.length > 0) {
719
- selectedSize = product.sizes[sizeIndex];
720
- sizeName = selectedSize.name;
721
- } else {
722
- alert("Ошибка: Размеры для этого товара не найдены.");
723
  return;
724
  }
 
 
725
  }
726
 
727
- if (!product || !selectedVariant) {
728
- alert("Ошибка: Варианты для этого товара не найдены.");
729
  return;
730
  }
731
 
732
- const cartItemId = `${product.id}-${selectedVariant.name}${sizeName ? '-' + sizeName : ''}`;
733
  const existingItem = cart.find(item => item.id === cartItemId);
734
 
735
  if (existingItem) {
@@ -741,13 +743,14 @@ CATALOG_TEMPLATE = '''
741
  name: product.name,
742
  price: selectedVariant.price,
743
  variantName: selectedVariant.name,
744
- sizeName: sizeName,
 
745
  photo: product.photos && product.photos.length > 0 ? product.photos[0] : null,
746
  quantity: quantity
747
  });
748
  }
749
 
750
- localStorage.setItem('esmiraCart', JSON.stringify(cart));
751
  closeModal('quantityModal');
752
  updateCartButton();
753
  showNotification(`${product.name} ${translations['add_to_cart_notification']}`);
@@ -780,18 +783,18 @@ CATALOG_TEMPLATE = '''
780
  total += itemTotal;
781
  const photoUrl = item.photo ? `https://huggingface.co/datasets/${repoId}/resolve/main/photos/${item.photo}` : 'https://via.placeholder.com/65x65.png?text=N/A';
782
 
783
- let details = `<p class="cart-item-price">${translations['cart_item_variant']}: ${item.variantName}</p>`;
784
  if (item.sizeName) {
785
- details += `<p class="cart-item-price">${translations['cart_item_size']}: ${item.sizeName}</p>`;
786
  }
787
- details += `<p class="cart-item-price">${item.price.toFixed(2)} ${currencyCode} × ${item.quantity}</p>`;
788
-
789
  return `
790
  <div class="cart-item">
791
  <img src="${photoUrl}" alt="${item.name}">
792
  <div class="cart-item-details">
793
  <strong>${item.name}</strong>
794
- ${details}
 
795
  </div>
796
  <span class="cart-item-total">${itemTotal.toFixed(2)}</span>
797
  <button class="cart-item-remove" onclick="removeFromCart('${item.id}')" title="${translations['remove_item_title']}">&times;</button>
@@ -805,7 +808,7 @@ CATALOG_TEMPLATE = '''
805
 
806
  function removeFromCart(itemId) {
807
  cart = cart.filter(item => item.id !== itemId);
808
- localStorage.setItem('esmiraCart', JSON.stringify(cart));
809
  openCartModal();
810
  updateCartButton();
811
  }
@@ -813,7 +816,7 @@ CATALOG_TEMPLATE = '''
813
  function clearCart() {
814
  if (confirm(translations['clear_cart_confirm'])) {
815
  cart = [];
816
- localStorage.removeItem('esmiraCart');
817
  openCartModal();
818
  updateCartButton();
819
  }
@@ -835,7 +838,7 @@ CATALOG_TEMPLATE = '''
835
  .then(response => response.json())
836
  .then(data => {
837
  if (data.order_id) {
838
- localStorage.removeItem('esmiraCart');
839
  cart = [];
840
  updateCartButton();
841
  closeModal('cartModal');
@@ -889,16 +892,10 @@ CATALOG_TEMPLATE = '''
889
 
890
  function setupFilters() {
891
  document.getElementById('search-input').addEventListener('input', filterProducts);
892
- document.querySelectorAll('.category-filter').forEach(filter => {
893
  filter.addEventListener('click', function() {
894
- document.querySelector('.category-filter.active').classList.remove('active');
895
- this.classList.add('active');
896
- filterProducts();
897
- });
898
- });
899
- document.querySelectorAll('.type-filter').forEach(filter => {
900
- filter.addEventListener('click', function() {
901
- document.querySelector('.type-filter.active').classList.remove('active');
902
  this.classList.add('active');
903
  filterProducts();
904
  });
@@ -949,7 +946,7 @@ PRODUCT_DETAIL_TEMPLATE = '''
949
  <div class="swiper-wrapper">
950
  {% if product.get('photos') and product['photos']|length > 0 %}
951
  {% for photo in product['photos'] %}
952
- <div class="swiper-slide" style="display: flex; justify-content: center; align-items: center; padding: 10px; background-color: #000;">
953
  <div class="swiper-zoom-container">
954
  <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ photo }}"
955
  alt="{{ product['name'] }} - фото {{ loop.index }}"
@@ -971,11 +968,13 @@ PRODUCT_DETAIL_TEMPLATE = '''
971
  </div>
972
 
973
  <div style="font-size: 1rem; line-height: 1.7; padding: 0 10px;">
974
- <p><strong>{{ _['product_type'] }}:</strong> {{ product.get('type') }}</p>
975
  <p><strong>{{ _['category'] }}:</strong> {{ product.get('category', _['no_category']) }}</p>
 
976
  {% if product.get('variants') and product.variants|length > 0 %}
977
  <p style="font-size: 1.4rem; font-weight: bold; color: var(--primary-color); margin: 15px 0;">
978
  {{ _['from_price'] }} {{ "%.2f"|format(product.variants|map(attribute='price')|min) }} {{ currency_code }}
 
979
  </p>
980
  <p><strong>{{ _['available_variants'] }}</strong></p>
981
  <ul style="list-style: none; padding-left: 0;">
@@ -987,16 +986,18 @@ PRODUCT_DETAIL_TEMPLATE = '''
987
  {% endfor %}
988
  </ul>
989
  {% endif %}
990
- {% if product.type == 'Одежда' and product.get('sizes') and product.sizes|length > 0 %}
991
- <p style="margin-top: 15px;"><strong>{{ _['available_sizes'] }}</strong></p>
992
- <ul style="list-style: none; padding-left: 0;">
993
- {% for size in product.sizes %}
994
- <li style="padding: 5px 0; border-bottom: 1px solid var(--border-color);">
995
- - {{ size.name }}
996
- </li>
997
- {% endfor %}
998
- </ul>
 
999
  {% endif %}
 
1000
  <p style="margin-top: 20px;"><strong>{{ _['description'] }}:</strong><br> {{ product.get('description', _['no_description'])|replace('\\n', '<br>')|safe }}</p>
1001
  </div>
1002
  <div style="padding: 20px 10px 10px; text-align: center;">
@@ -1020,13 +1021,13 @@ ORDER_TEMPLATE = '''
1020
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
1021
  <style>
1022
  :root {
1023
- --primary-color: #B22222;
1024
- --primary-dark: #8B0000;
1025
- --surface-color: #2b2b2b;
1026
- --background-color: #1c1c1c;
1027
  --text-color: #F5F5F5;
1028
- --text-color-muted: #a0a0a0;
1029
- --border-color: #444;
1030
  }
1031
  body { font-family: 'Georgia', serif; background: var(--background-color); color: var(--text-color); line-height: 1.6; padding: 15px; }
1032
  .container { max-width: 800px; margin: 20px auto; padding: 30px; background: var(--surface-color); border-radius: 16px; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.6); border: 1px solid var(--border-color); }
@@ -1036,14 +1037,14 @@ ORDER_TEMPLATE = '''
1036
  .order-meta { font-size: 0.9rem; color: var(--text-color-muted); margin-bottom: 20px; text-align: center; }
1037
  .order-item { display: grid; grid-template-columns: 60px 1fr auto; gap: 15px; align-items: center; padding: 15px 0; border-bottom: 1px solid var(--border-color); }
1038
  .order-item:last-child { border-bottom: none; }
1039
- .order-item img { width: 60px; height: 60px; object-fit: contain; border-radius: 8px; background-color: #000; padding: 5px; border: 1px solid #444;}
1040
  .item-details strong { display: block; margin-bottom: 4px; font-size: 1.05rem; color: var(--text-color);}
1041
  .item-details span { font-size: 0.9rem; color: var(--text-color-muted); display: block;}
1042
  .item-total { font-weight: bold; text-align: right; font-size: 1.1rem; color: var(--primary-color);}
1043
  .order-summary { margin-top: 30px; padding-top: 20px; border-top: 2px solid var(--primary-color); text-align: right; }
1044
  .order-summary p { margin-bottom: 10px; font-size: 1.1rem; }
1045
  .order-summary strong { font-size: 1.5rem; color: var(--primary-color); }
1046
- .customer-info { margin-top: 30px; background-color: rgba(178, 34, 34, 0.05); padding: 20px; border-radius: 12px; border: 1px solid var(--primary-color);}
1047
  .customer-info p { margin-bottom: 8px; font-size: 1rem; }
1048
  .customer-info strong { color: var(--text-color); }
1049
  .actions { margin-top: 30px; text-align: center; }
@@ -1100,7 +1101,7 @@ ORDER_TEMPLATE = '''
1100
  function sendOrderViaWhatsApp() {
1101
  const orderId = '{{ order.id }}';
1102
  const orderUrl = `{{ request.url }}`;
1103
- const whatsappNumber = "{{ whatsapp_number }}";
1104
 
1105
  let message = `{{ _['whatsapp_confirm_message_1'] }}%0A%0A`;
1106
  message += `*{{ _['whatsapp_confirm_message_2'] }}* ${orderId}%0A`;
@@ -1135,13 +1136,13 @@ ADMIN_TEMPLATE = '''
1135
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
1136
  <style>
1137
  :root {
1138
- --primary-color: #B22222;
1139
- --primary-dark: #8B0000;
1140
- --surface-color: #2b2b2b;
1141
- --background-color: #1c1c1c;
1142
  --text-color: #F5F5F5;
1143
- --text-color-muted: #a0a0a0;
1144
- --border-color: #444;
1145
  --success-bg: #113d21;
1146
  --success-text: #6ee791;
1147
  --error-bg: #4d0a0a;
@@ -1156,11 +1157,11 @@ ADMIN_TEMPLATE = '''
1156
  h1 { font-size: 2rem; }
1157
  h2 { font-size: 1.6rem; margin-top: 30px; display: flex; align-items: center; gap: 10px; padding-bottom: 8px; border-bottom: 1px solid var(--border-color);}
1158
  h3 { font-size: 1.2rem; color: var(--text-color); margin-top: 20px;}
1159
- .section { margin-bottom: 30px; padding: 20px; background-color: #151515; border: 1px solid var(--border-color); border-radius: 8px; }
1160
 
1161
  label { font-weight: 500; margin-top: 12px; display: block; color: var(--text-color); font-size: 0.9rem;}
1162
  input[type="text"], input[type="number"], input[type="password"], input[type="tel"], textarea, select { width: 100%; padding: 10px 12px; margin-top: 6px; border: 1px solid var(--border-color); border-radius: 6px; font-size: 0.95rem; box-sizing: border-box; transition: border-color 0.3s ease; background-color: var(--background-color); color: var(--text-color); }
1163
- input:focus, textarea:focus, select:focus { border-color: var(--primary-color); outline: none; box-shadow: 0 0 0 3px rgba(178, 34, 34, 0.2); }
1164
  textarea { min-height: 90px; resize: vertical; }
1165
  input[type="file"] { padding: 8px; background-color: #222; cursor: pointer; border: 1px solid var(--border-color); border-radius: 6px; }
1166
  input[type="checkbox"] { transform: scale(1.2); margin-right: 8px; vertical-align: middle; accent-color: var(--primary-color); }
@@ -1173,7 +1174,7 @@ ADMIN_TEMPLATE = '''
1173
  .item-list { display: grid; gap: 15px; }
1174
  .item { background: var(--surface-color); padding: 15px; border-radius: 8px; border: 1px solid var(--border-color); }
1175
  .item-actions { margin-top: 15px; display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }
1176
- .photo-preview img, .photo-edit-item img { width: 60px; height: 60px; border-radius: 6px; margin: 5px 5px 0 0; border: 1px solid var(--border-color); object-fit: cover;}
1177
  .photo-preview-edit { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 5px; }
1178
  .photo-edit-item { position: relative; }
1179
  .photo-edit-item input[type="checkbox"] { position: absolute; top: 0px; right: 0px; transform: scale(1.3); accent-color: #ff453a; cursor: pointer; }
@@ -1189,13 +1190,12 @@ ADMIN_TEMPLATE = '''
1189
  .message.error { background-color: var(--error-bg); color: var(--error-text); border-color: var(--error-text);}
1190
  .message.warning { background-color: var(--warning-bg); color: var(--warning-text); border-color: var(--warning-text); }
1191
  .status-indicator { display: inline-block; padding: 3px 9px; border-radius: 50px; font-size: 0.75rem; font-weight: 500; margin-left: 5px; vertical-align: middle; }
1192
- .status-indicator.top-product { background-color: #9c4221; color: #fff3c4; }
1193
  .status-indicator.new { background-color: #e65100; color: #ffe0b2; }
1194
  .status-indicator.accepted { background-color: #0277bd; color: #b3e5fc; }
1195
  .status-indicator.prepared { background-color: #2f855a; color: #c6f6d5; }
1196
  .status-indicator.shipped { background-color: #065f46; color: #a7f3d0; }
1197
 
1198
- details { background-color: #151515; border: 1px solid var(--border-color); border-radius: 8px; margin-bottom: 10px; }
1199
  details > summary { cursor: pointer; font-weight: 500; color: var(--text-color); display: flex; justify-content: space-between; align-items: center; padding: 15px 20px; list-style: none; position: relative; }
1200
  details[open] > summary { border-bottom: 1px solid var(--border-color); }
1201
  .order-details-content { padding: 15px 20px; }
@@ -1210,8 +1210,8 @@ ADMIN_TEMPLATE = '''
1210
  <div class="container">
1211
  <div class="header">
1212
  <div class="logo-title-container" style="display: flex; align-items: center; gap: 15px;">
1213
- <img src="https://huggingface.co/spaces/esmira-tkani/admin/resolve/main/Screenshot_20251225-134859.png" alt="Esmira Logo" style="height: 45px; width: 45px; border-radius: 50%;">
1214
- <h1><i class="fas fa-tools"></i> Админ-панель Esmira</h1>
1215
  </div>
1216
  <a href="{{ url_for('catalog', lang_code='ru') }}" class="button"><i class="fas fa-store"></i> Перейти в каталог</a>
1217
  </div>
@@ -1225,17 +1225,6 @@ ADMIN_TEMPLATE = '''
1225
  {% endif %}
1226
  {% endwith %}
1227
 
1228
- <div class="section">
1229
- <h2><i class="fas fa-cogs"></i> Настройки</h2>
1230
- <form method="POST">
1231
- <input type="hidden" name="action" value="update_settings">
1232
- <label for="whatsapp_number">Номер WhatsApp для заказов:</label>
1233
- <input type="tel" id="whatsapp_number" name="whatsapp_number" value="{{ settings.get('whatsapp_number', '') }}" placeholder="77073363943">
1234
- <button type="submit"><i class="fas fa-save"></i> Сохранить настройки</button>
1235
- </form>
1236
- </div>
1237
-
1238
-
1239
  <div class="section">
1240
  <h2><i class="fas fa-history"></i> История заказов</h2>
1241
  {% if orders %}
@@ -1276,6 +1265,23 @@ ADMIN_TEMPLATE = '''
1276
  </div>
1277
 
1278
  <div class="section">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1279
  <h2><i class="fas fa-tags"></i> Категории</h2>
1280
  <details>
1281
  <summary><i class="fas fa-plus-circle"></i> Добавить новую категорию</summary>
@@ -1305,27 +1311,25 @@ ADMIN_TEMPLATE = '''
1305
  {% else %}
1306
  <p>Категорий пока нет.</p>
1307
  {% endif %}
1308
- </div>
1309
-
1310
-
1311
  <div class="section">
1312
  <h2><i class="fas fa-box-open"></i> Товары</h2>
1313
  <details>
1314
  <summary><i class="fas fa-plus-circle"></i> Добавить новый товар</summary>
1315
  <div style="padding: 15px;">
1316
- <form method="POST" enctype="multipart/form-data" id="addProductForm">
1317
  <input type="hidden" name="action" value="add_product">
1318
  <label for="add_name">Название товара *:</label>
1319
  <input type="text" id="add_name" name="name" required>
1320
-
1321
- <label for="add_type">Тип товара *:</label>
1322
- <select id="add_type" name="type" required onchange="toggleSizeInputs('add')">
1323
- <option value="Ткани">Ткани</option>
1324
- <option value="Одежда">Одежда</option>
1325
- </select>
1326
-
1327
  <label for="add_description">Описание:</label>
1328
  <textarea id="add_description" name="description" rows="4"></textarea>
 
 
 
 
 
 
1329
 
1330
  <label for="add_category">Категория:</label>
1331
  <select id="add_category" name="category">
@@ -1346,24 +1350,20 @@ ADMIN_TEMPLATE = '''
1346
  <button type="button" class="remove-btn" onclick="removeInputGroup(this)"><i class="fas fa-times"></i></button>
1347
  </div>
1348
  </div>
1349
- <button type="button" class="button add-btn" onclick="addVariantInput('add-variants-container')"><i class="fas fa-plus"></i> Добавить вариант</button>
1350
-
1351
- <div id="add-sizes-section" style="display: none;">
1352
- <h4>Размеры *:</h4>
1353
- <div id="add-sizes-container">
1354
- <div class="input-group">
1355
- <input type="text" name="size_names" placeholder="Название размера (напр. S, M, 42)">
1356
- <button type="button" class="remove-btn" onclick="removeInputGroup(this, false)"><i class="fas fa-times"></i></button>
1357
- </div>
1358
- </div>
1359
- <button type="button" class="button add-btn" onclick="addSizeInput('add-sizes-container')"><i class="fas fa-plus"></i> Добавить размер</button>
1360
- </div>
1361
-
1362
- <div style="margin-top: 20px;">
1363
- <input type="checkbox" id="add_is_top" name="is_top">
1364
- <label for="add_is_top">Топ товар</label>
1365
  </div>
1366
- <br>
 
1367
  <button type="submit"><i class="fas fa-save"></i> Добавить товар</button>
1368
  </form>
1369
  </div>
@@ -1385,11 +1385,8 @@ ADMIN_TEMPLATE = '''
1385
  {% endif %}
1386
  </div>
1387
  <div style="flex-grow: 1;">
1388
- <h3 style="margin-top: 0; margin-bottom: 5px;">
1389
- {{ product['name'] }}
1390
- {% if product.get('is_top', False) %}<span class="status-indicator top-product"><i class="fas fa-star fa-xs"></i> Топ</span>{% endif %}
1391
- </h3>
1392
- <p style="font-size: 0.9rem; color: var(--text-color-muted);"><strong>Тип:</strong> {{ product.get('type') }}</p>
1393
  <p style="font-size: 0.9rem; color: var(--text-color-muted);"><strong>Категория:</strong> {{ product.get('category', 'Без категории') }}</p>
1394
  </div>
1395
  </div>
@@ -1410,16 +1407,15 @@ ADMIN_TEMPLATE = '''
1410
  <input type="hidden" name="product_id" value="{{ product.id }}">
1411
  <label>Название *:</label>
1412
  <input type="text" name="name" value="{{ product['name'] }}" required>
 
 
1413
 
1414
  <label>Тип товара *:</label>
1415
- <select name="type" required onchange="toggleSizeInputs('edit-{{ product.id }}')">
1416
- <option value="Ткани" {% if product.get('type') == 'Ткани' %}selected{% endif %}>Ткани</option>
1417
- <option value="Одежда" {% if product.get('type') == 'Одежда' %}selected{% endif %}>Одежда</option>
1418
  </select>
1419
 
1420
- <label>Описание:</label>
1421
- <textarea name="description" rows="4">{{ product.get('description', '') }}</textarea>
1422
-
1423
  <label>Категория:</label>
1424
  <select name="category">
1425
  <option value="Без категории" {% if product.get('category', 'Без категории') == 'Без категории' %}selected{% endif %}>Без категории</option>
@@ -1455,32 +1451,22 @@ ADMIN_TEMPLATE = '''
1455
  </div>
1456
  {% endfor %}
1457
  </div>
1458
- <button type="button" class="button add-btn" onclick="addVariantInput('edit-variants-container-{{ product.id }}')"><i class="fas fa-plus"></i> Добавить вариант</button>
1459
 
1460
- <div id="edit-{{ product.id }}-sizes-section" style="display: {% if product.type == 'Одежда' %}block{% else %}none{% endif %};">
1461
  <h4>Размеры *:</h4>
1462
  <div id="edit-sizes-container-{{ product.id }}">
1463
  {% for size in product.get('sizes', []) %}
1464
  <div class="input-group">
1465
- <input type="text" name="size_names" value="{{ size.name }}">
1466
- <button type="button" class="remove-btn" onclick="removeInputGroup(this, false)"><i class="fas fa-times"></i></button>
1467
  </div>
1468
  {% endfor %}
1469
- {% if product.get('sizes', []) | length == 0 %}
1470
- <div class="input-group">
1471
- <input type="text" name="size_names" placeholder="Название размера (напр. S, M, 42)">
1472
- <button type="button" class="remove-btn" onclick="removeInputGroup(this, false)"><i class="fas fa-times"></i></button>
1473
- </div>
1474
- {% endif %}
1475
  </div>
1476
- <button type="button" class="button add-btn" onclick="addSizeInput('edit-sizes-container-{{ product.id }}')"><i class="fas fa-plus"></i> Добавить размер</button>
1477
- </div>
1478
-
1479
- <div style="margin-top: 20px;">
1480
- <input type="checkbox" id="edit_is_top_{{ product.id }}" name="is_top" {% if product.get('is_top', False) %}checked{% endif %}>
1481
- <label for="edit_is_top_{{ product.id }}">Топ товар</label>
1482
  </div>
1483
- <br>
 
1484
  <button type="submit"><i class="fas fa-save"></i> Сохранить изменения</button>
1485
  </form>
1486
  </div>
@@ -1491,16 +1477,6 @@ ADMIN_TEMPLATE = '''
1491
  <p>Товаров пока нет.</p>
1492
  {% endif %}
1493
  </div>
1494
- <div class="section">
1495
- <h2><i class="fas fa-info-circle"></i> Инфо о магазине</h2>
1496
- <p><strong>Адреса в г. Алматы:</strong></p>
1497
- <ul style="padding-left: 20px;">
1498
- {% for address in store_addresses %}
1499
- <li>{{ address }}</li>
1500
- {% endfor %}
1501
- </ul>
1502
- <p style="margin-top: 10px;"><strong>Валюта:</strong> {{ currency_name }} ({{ currency_code }})</p>
1503
- </div>
1504
  </div>
1505
 
1506
  <script>
@@ -1508,48 +1484,70 @@ ADMIN_TEMPLATE = '''
1508
  const form = document.getElementById(formId);
1509
  form.style.display = form.style.display === 'none' ? 'block' : 'none';
1510
  }
1511
-
1512
- function toggleSizeInputs(formPrefix) {
1513
- const typeSelect = document.querySelector(`#${formPrefix}_type, #edit-form-${formPrefix.split('-')[1]} select[name='type']`);
1514
- const sizesSection = document.getElementById(`${formPrefix}-sizes-section`);
1515
- if (typeSelect && sizesSection) {
1516
- sizesSection.style.display = typeSelect.value === 'Одежда' ? 'block' : 'none';
1517
- }
1518
- }
1519
 
1520
-
1521
- function addVariantInput(containerId) {
1522
- const container = document.getElementById(containerId);
1523
- const newInputGroup = document.createElement('div');
1524
- newInputGroup.className = 'input-group';
1525
- newInputGroup.innerHTML = `
1526
- <input type="text" name="variant_names" placeholder="Название варианта" required>
1527
- <input type="number" name="variant_prices" step="0.01" min="0" placeholder="Цена" required>
1528
- <button type="button" class="remove-btn" onclick="removeInputGroup(this)"><i class="fas fa-times"></i></button>
1529
- `;
1530
- container.appendChild(newInputGroup);
1531
- }
1532
-
1533
- function addSizeInput(containerId) {
1534
  const container = document.getElementById(containerId);
1535
  const newInputGroup = document.createElement('div');
1536
  newInputGroup.className = 'input-group';
1537
- newInputGroup.innerHTML = `
1538
- <input type="text" name="size_names" placeholder="Название размера">
1539
- <button type="button" class="remove-btn" onclick="removeInputGroup(this, false)"><i class="fas fa-times"></i></button>
1540
- `;
 
 
 
 
 
 
 
 
1541
  container.appendChild(newInputGroup);
1542
  }
1543
 
1544
- function removeInputGroup(button, requireOne = true) {
1545
  const group = button.closest('.input-group');
1546
  const container = group.parentNode;
1547
- if (!requireOne || container.children.length > 1) {
1548
  group.remove();
1549
  } else {
1550
- alert("Должен быть хотя бы один вариант.");
1551
  }
1552
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1553
  </script>
1554
  </body>
1555
  </html>
@@ -1575,13 +1573,15 @@ def catalog(lang_code):
1575
  all_products = data.get('products', [])
1576
  categories = sorted(data.get('categories', []))
1577
  settings = data.get('settings', {})
 
 
1578
 
1579
  products_in_stock = [p for p in all_products if p.get('variants')]
1580
 
1581
  for p in products_in_stock:
1582
  p['variants'] = sorted(p.get('variants', []), key=lambda v: v.get('price', 0))
1583
 
1584
- products_sorted = sorted(products_in_stock, key=lambda p: (not p.get('is_top', False), p.get('name', '').lower()))
1585
 
1586
  return render_template_string(
1587
  CATALOG_TEMPLATE,
@@ -1592,7 +1592,7 @@ def catalog(lang_code):
1592
  currency_code=CURRENCY_CODE,
1593
  lang_code=g.lang_code,
1594
  _=g.translations,
1595
- settings=settings
1596
  )
1597
 
1598
  @app.route('/<lang_code>/product/<int:index>')
@@ -1604,7 +1604,7 @@ def product_detail(lang_code, index):
1604
  for p in products_in_stock:
1605
  p['variants'] = sorted(p.get('variants', []), key=lambda v: v.get('price', 0))
1606
 
1607
- products_sorted = sorted(products_in_stock, key=lambda p: (not p.get('is_top', False), p.get('name', '').lower()))
1608
 
1609
  try:
1610
  product = products_sorted[index]
@@ -1687,7 +1687,7 @@ def view_order(lang_code, order_id):
1687
  data = load_data()
1688
  order = data.get('orders', {}).get(order_id)
1689
  settings = data.get('settings', {})
1690
- whatsapp_number = settings.get('whatsapp_number', '77073363943')
1691
  status_map = STATUS_MAPS.get(lang_code, STATUS_MAPS['ru'])
1692
  return render_template_string(ORDER_TEMPLATE, order=order, status_map=status_map, currency_code=CURRENCY_CODE, lang_code=g.lang_code, _=g.translations, whatsapp_number=whatsapp_number)
1693
 
@@ -1695,7 +1695,8 @@ def view_order(lang_code, order_id):
1695
  def admin():
1696
  data = load_data()
1697
  if 'orders' not in data: data['orders'] = {}
1698
- if 'settings' not in data: data['settings'] = {}
 
1699
 
1700
  needs_save = False
1701
  for p in data.get('products', []):
@@ -1709,10 +1710,13 @@ def admin():
1709
  action = request.form.get('action')
1710
  try:
1711
  if action == 'update_settings':
1712
- whatsapp_number = request.form.get('whatsapp_number', '').strip().replace('+', '')
1713
- data['settings']['whatsapp_number'] = whatsapp_number
1714
- save_data(data)
1715
- flash('Настройки сохранены.', 'success')
 
 
 
1716
 
1717
  elif action == 'update_order_status':
1718
  order_id = request.form.get('order_id')
@@ -1747,10 +1751,8 @@ def admin():
1747
 
1748
  elif action == 'add_product' or action == 'edit_product':
1749
  name = request.form.get('name', '').strip()
1750
- product_type = request.form.get('type')
1751
-
1752
- if not name or not product_type:
1753
- flash("Название и тип товара обязательны.", 'error')
1754
  return redirect(url_for('admin'))
1755
 
1756
  variant_names = [v.strip() for v in request.form.getlist('variant_names') if v.strip()]
@@ -1770,22 +1772,23 @@ def admin():
1770
  flash("Неверный формат цены в вариантах.", 'error')
1771
  return redirect(url_for('admin'))
1772
 
 
1773
  sizes = []
1774
- if product_type == 'Одежда':
1775
  size_names = [s.strip() for s in request.form.getlist('size_names') if s.strip()]
1776
  if not size_names:
1777
  flash("Для одежды необходимо указать хотя бы один размер.", 'error')
1778
  return redirect(url_for('admin'))
1779
  sizes = [{'name': s_name} for s_name in size_names]
1780
 
 
1781
  product_data = {
1782
  'name': name,
1783
- 'type': product_type,
1784
  'description': request.form.get('description', '').strip(),
1785
  'category': request.form.get('category'),
1786
- 'is_top': 'is_top' in request.form,
1787
  'variants': variants,
1788
- 'sizes': sizes if product_type == 'Одежда' else []
1789
  }
1790
 
1791
  newly_uploaded_photos = []
 
54
 
55
  translations = {
56
  'ru': {
57
+ 'page_title': "Esmira - ткани и одежда оптом и в розницу",
58
  'header_title': "Esmira",
59
  'our_addresses': "Наши адреса в г. Алматы",
60
  'search_placeholder': "Поиск по названию или описанию...",
61
  'all_filter': "Все",
 
62
  'add_to_cart_button': "В корзину",
63
  'from_price': "от",
64
  'no_products_yet': "Товары пока не добавлены.",
 
67
  'specify_details': "Укажите детали",
68
  'variant_label': "Вариант:",
69
  'size_label': "Размер:",
70
+ 'quantity_label': "Количество:",
71
+ 'quantity_label_meter': "Количество (метры):",
72
  'confirm_add_to_cart': "Добавить в корзину",
73
  'your_cart': "Ваша корзина",
74
  'cart_is_empty': "Ваша корзина пуста.",
 
84
  'add_to_cart_notification': "добавлен в корзину!",
85
  'no_products_found': "По вашему запросу товары не найдены.",
86
  'category': "Категория",
 
 
 
87
  'no_category': "Без категории",
88
  'available_variants': "Доступные варианты:",
89
  'available_sizes': "Доступные размеры:",
 
104
  'whatsapp_confirm_message_2': "Номер заказа:",
105
  'whatsapp_confirm_message_3': "Ссылка на заказ:",
106
  'whatsapp_confirm_message_4': "Пожалуйста, свяжитесь со мной для уточнения деталей.",
107
+ 'product_type': "Тип товара",
108
+ 'tkaniny': "Ткани",
109
+ 'odezhda': "Одежда",
110
+ 'price_per_meter': "Цена за метр",
111
  },
112
  'kz': {
113
+ 'page_title': "Esmira - маталар мен киімдер көтерме және бөлшек саудада",
114
  'header_title': "Esmira",
115
  'our_addresses': "Алматы қаласындағы мекенжайларымыз",
116
  'search_placeholder': "Аты немесе сипаттамасы бойынша іздеу...",
117
  'all_filter': "Барлығы",
 
118
  'add_to_cart_button': "Себетке салу",
119
  'from_price': "-ден бастап",
120
  'no_products_yet': "Тауарлар әлі қосылмаған.",
 
123
  'specify_details': "Мәліметтерді көрсетіңіз",
124
  'variant_label': "Нұсқа:",
125
  'size_label': "Өлшем:",
126
+ 'quantity_label': "Саны:",
127
+ 'quantity_label_meter': "Саны (метр):",
128
  'confirm_add_to_cart': "Себетке қосу",
129
  'your_cart': "Сіздің себетіңіз",
130
  'cart_is_empty': "Сіздің себетіңіз бос.",
 
140
  'add_to_cart_notification': "себетке қосылды!",
141
  'no_products_found': "Сіздің сұранысыңыз бойынша тауарлар табылмады.",
142
  'category': "Санат",
 
 
 
143
  'no_category': "Санатсыз",
144
  'available_variants': "Қолжетімді нұсқалар:",
145
  'available_sizes': "Қолжетімді өлшемдер:",
 
160
  'whatsapp_confirm_message_2': "Тапсырыс нөмірі:",
161
  'whatsapp_confirm_message_3': "Тапсырысқа сілтеме:",
162
  'whatsapp_confirm_message_4': "Мәліметтерді нақтылау үшін менімен хабарласуыңызды сұраймын.",
163
+ 'product_type': "Тауар түрі",
164
+ 'tkaniny': "Маталар",
165
+ 'odezhda': "Киімдер",
166
+ 'price_per_meter': "Метр бағасы",
167
  }
168
  }
169
 
 
210
  try:
211
  if file_name == DATA_FILE:
212
  with open(file_name, 'w', encoding='utf-8') as f:
213
+ json.dump({'products': [], 'categories': [], 'orders': {}, 'settings': {'whatsapp_number': '+77073363943'}}, f)
214
  logging.info(f"Created empty local file {file_name} because it was not found on HF.")
215
  except Exception as create_e:
216
  logging.error(f"Failed to create empty local file {file_name}: {create_e}")
 
274
 
275
 
276
  def load_data():
277
+ default_data = {'products': [], 'categories': [], 'orders': {}, 'settings': {'whatsapp_number': '+77073363943'}}
278
  try:
279
  with open(DATA_FILE, 'r', encoding='utf-8') as file:
280
  data = json.load(file)
 
285
  if 'products' not in data: data['products'] = []
286
  if 'categories' not in data: data['categories'] = []
287
  if 'orders' not in data: data['orders'] = {}
288
+ if 'settings' not in data: data['settings'] = {'whatsapp_number': '+77073363943'}
289
  return data
290
  except FileNotFoundError:
291
  logging.warning(f"Local file {DATA_FILE} not found. Attempting download from HF.")
 
303
  if 'products' not in data: data['products'] = []
304
  if 'categories' not in data: data['categories'] = []
305
  if 'orders' not in data: data['orders'] = {}
306
+ if 'settings' not in data: data['settings'] = {'whatsapp_number': '+77073363943'}
307
  return data
308
  except FileNotFoundError:
309
  logging.error(f"File {DATA_FILE} still not found even after download reported success. Using default.")
 
333
  if 'products' not in data: data['products'] = []
334
  if 'categories' not in data: data['categories'] = []
335
  if 'orders' not in data: data['orders'] = {}
336
+ if 'settings' not in data: data['settings'] = {'whatsapp_number': '+77073363943'}
337
+
338
 
339
  with open(DATA_FILE, 'w', encoding='utf-8') as file:
340
  json.dump(data, file, ensure_ascii=False, indent=4)
 
359
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/Swiper/10.2.0/swiper-bundle.min.css">
360
  <style>
361
  :root {
362
+ --primary-color: #9A2A47;
363
+ --primary-dark: #7a2138;
364
+ --surface-color: #313131;
365
+ --background-color: #212121;
366
  --text-color: #F5F5F5;
367
+ --text-color-muted: #BDBDBD;
368
+ --border-color: #424242;
369
+ --accent-color: #E0E0E0;
370
  }
371
  * { margin: 0; padding: 0; box-sizing: border-box; }
372
+ body {
373
+ font-family: 'Georgia', serif;
374
+ background: var(--background-color);
375
+ color: var(--text-color);
376
+ line-height: 1.6;
377
+ transition: background-color 0.3s;
378
  }
379
  .container { max-width: 100%; margin: 0 auto; padding: 0; }
380
  .content-area { padding: 20px; }
381
 
382
+ .header { display: flex; justify-content: space-between; align-items: center; padding: 15px 20px; background: rgba(33, 33, 33, 0.8); backdrop-filter: blur(10px); border-bottom: 1px solid var(--border-color); position: sticky; top: 0; z-index: 1000; }
383
  .logo-title-container { display: flex; align-items: center; gap: 15px; }
384
+ .logo-title-container img { height: 45px; width: auto; object-fit: contain; }
385
+ .header h1 { font-family: 'Cormorant Garamond', serif; font-size: 1.8rem; font-weight: 700; color: var(--accent-color); }
386
 
387
  .lang-switcher { display: flex; gap: 5px; background-color: var(--surface-color); padding: 5px; border-radius: 50px; }
388
  .lang-switcher a { color: var(--text-color-muted); text-decoration: none; font-size: 0.9rem; padding: 5px 10px; border-radius: 50px; transition: all 0.3s; }
 
394
 
395
  .search-container { padding: 0 20px 20px; }
396
  #search-input { width: 100%; padding: 12px 20px; font-size: 1rem; border: 1px solid var(--border-color); border-radius: 50px; outline: none; transition: all 0.3s; background-color: var(--surface-color); color: var(--text-color); }
397
+ #search-input:focus { border-color: var(--primary-color); box-shadow: 0 0 0 4px rgba(154, 42, 71, 0.2); }
398
 
399
  .filters-wrapper { margin: 0 20px 20px; display: flex; flex-direction: column; gap: 15px; }
400
  .filters-container { display: flex; overflow-x: auto; gap: 10px; padding-bottom: 10px; scrollbar-width: none; -ms-overflow-style: none; }
401
  .filters-container::-webkit-scrollbar { display: none; }
402
  .filter-label { font-size: 0.9rem; color: var(--text-color-muted); margin-left: 5px; }
403
  .filter-btn { padding: 8px 18px; border: 1px solid var(--border-color); border-radius: 50px; background-color: transparent; cursor: pointer; transition: all 0.3s ease; font-size: 0.9rem; font-weight: 400; color: var(--text-color-muted); white-space: nowrap; }
404
+ .filter-btn.active, .filter-btn:hover { background-color: var(--primary-color); color: #fff; border-color: var(--primary-color); font-weight: 500; box-shadow: 0 2px 8px rgba(154, 42, 71, 0.3); transform: translateY(-2px); }
405
 
406
  .products-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 15px; padding: 0 20px 120px; }
407
  @media (min-width: 600px) { .products-grid { grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); gap: 20px; } }
408
 
409
  .product { background: var(--surface-color); border-radius: 16px; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.4); transition: all 0.3s ease; overflow: hidden; display: flex; flex-direction: column; height: 100%; position: relative; border: 1px solid var(--border-color); }
410
+ .product:hover { transform: translateY(-5px); box-shadow: 0 8px 25px rgba(154, 42, 71, 0.15); border-color: var(--primary-color); }
411
 
412
+ .product-image { width: 100%; aspect-ratio: 1 / 1; background-color: #fff; display: flex; justify-content: center; align-items: center; padding: 10px; }
413
  .product-image img { max-width: 100%; max-height: 100%; object-fit: contain; transition: transform 0.3s ease; }
414
  .product:hover .product-image img { transform: scale(1.05); }
415
  .product-info { padding: 15px; flex-grow: 1; display: flex; flex-direction: column; text-align: center; }
416
+ .product h2 { font-family: 'Cormorant Garamond', serif; font-size: 1.2rem; font-weight: 600; margin: 0 0 8px 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--accent-color); }
417
  .product-price { font-size: 1.2rem; color: var(--primary-color); font-weight: 700; margin: 5px 0; }
418
  .product-price .from-text { font-size: 0.8rem; color: var(--text-color-muted); font-weight: 400; }
419
  .product-description { display: none; }
420
  .product-actions { padding: 0 15px 15px; }
421
 
422
  .product-button { display: inline-flex; align-items: center; justify-content: center; width: 100%; padding: 10px; border: none; border-radius: 50px; background-color: var(--primary-color); color: #fff; font-size: 0.9rem; font-weight: 500; cursor: pointer; transition: all 0.3s ease; text-decoration: none; text-transform: uppercase; letter-spacing: 0.5px; }
423
+ .product-button:hover { background-color: var(--primary-dark); box-shadow: 0 4px 10px rgba(154, 42, 71, 0.4); }
424
  .product-button i { margin-right: 8px; }
425
 
426
+ .fab { position: fixed; background-color: var(--primary-color); color: #fff; border: none; border-radius: 50%; width: 55px; height: 55px; font-size: 1.5rem; cursor: pointer; display: flex; align-items: center; justify-content: center; box-shadow: 0 4px 15px rgba(154, 42, 71, 0.4); z-index: 1000; transition: transform 0.2s ease; }
427
  .fab:hover { transform: scale(1.1); }
428
  #cart-button { bottom: 20px; right: 20px; display: none; }
429
  #cart-button span { position: absolute; top: -2px; right: -2px; background-color: #dc3545; color: white; border-radius: 50%; padding: 3px 7px; font-size: 0.75rem; font-weight: bold; min-width: 22px; text-align: center; }
 
439
 
440
  .cart-item { display: grid; grid-template-columns: 65px 1fr auto 25px; gap: 15px; align-items: center; padding: 15px 0; border-bottom: 1px solid var(--border-color); }
441
  .cart-item:last-child { border-bottom: none; }
442
+ .cart-item img { width: 65px; height: 65px; object-fit: contain; border-radius: 8px; background-color: #fff; padding: 5px; }
443
  .cart-item-details strong { display: block; margin-bottom: 4px; font-size: 1.05rem; }
444
  .cart-item-price { font-size: 0.9rem; color: var(--text-color-muted); }
445
  .cart-item-total { font-weight: bold; text-align: right; font-size: 1rem; color: var(--primary-color); }
 
458
  .notification { position: fixed; bottom: 80px; left: 50%; transform: translateX(-50%); background-color: var(--primary-color); color: #fff; padding: 12px 25px; border-radius: 50px; box-shadow: 0 4px 15px rgba(0,0,0,0.5); z-index: 1002; opacity: 0; transition: all 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94); font-size: 0.95rem; font-weight: 500; }
459
  .notification.show { opacity: 1; bottom: 90px; }
460
  .no-results-message { grid-column: 1 / -1; text-align: center; padding: 50px; font-size: 1.2rem; color: var(--text-color-muted); }
 
461
 
462
  #whatsapp-fab { bottom: 20px; left: 20px; background-color: #25D366; color: white; }
463
  </style>
 
488
  </div>
489
 
490
  <div class="filters-wrapper">
491
+ <div>
492
  <span class="filter-label">{{ _['product_type'] }}:</span>
493
  <div class="filters-container">
494
  <button class="filter-btn type-filter active" data-type="all">{{ _['all_filter'] }}</button>
495
+ <button class="filter-btn type-filter" data-type="tkaniny">{{ _['tkaniny'] }}</button>
496
+ <button class="filter-btn type-filter" data-type="odezhda">{{ _['odezhda'] }}</button>
497
  </div>
498
  </div>
499
  <div>
 
514
  data-name="{{ product['name']|lower }}"
515
  data-description="{{ product.get('description', '')|lower }}"
516
  data-category="{{ product.get('category', _['no_category']) }}"
517
+ data-type="{{ product.get('product_type', 'tkaniny') }}">
 
 
 
518
  <div class="product-image" onclick="openModal({{ loop.index0 }})" style="cursor: pointer;">
519
  {% if product.get('photos') and product['photos']|length > 0 %}
520
  <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ product['photos'][0] }}"
 
559
  <span class="close" onclick="closeModal('quantityModal')" aria-label="Закрыть">&times;</span>
560
  <h2>{{ _['specify_details'] }}</h2>
561
 
562
+ <div id="variant-section">
563
+ <label for="variantSelect">{{ _['variant_label'] }}</label>
564
+ <select id="variantSelect" class="variant-select"></select>
565
+ </div>
566
 
567
+ <div id="size-section" style="display: none;">
568
  <label for="sizeSelect">{{ _['size_label'] }}</label>
569
  <select id="sizeSelect" class="size-select"></select>
570
  </div>
571
+
572
+ <label for="quantityInput" id="quantity-label">{{ _['quantity_label'] }}</label>
573
+ <input type="number" id="quantityInput" class="quantity-input" min="1" value="1" step="1">
574
 
575
  <button class="product-button" onclick="confirmAddToCart()"><i class="fas fa-check"></i> {{ _['confirm_add_to_cart'] }}</button>
576
  </div>
 
600
  <span id="cart-count">0</span>
601
  </button>
602
 
603
+ <a href="https://api.whatsapp.com/send?phone={{ whatsapp_number }}" target="_blank" rel="noopener noreferrer" id="whatsapp-fab" class="fab">
604
+ <i class="fab fa-whatsapp"></i>
605
+ </a>
606
 
607
  <div id="notification-placeholder"></div>
608
 
 
612
  const repoId = '{{ repo_id }}';
613
  const currencyCode = '{{ currency_code }}';
614
  let selectedProductIndex = null;
615
+ let cart = JSON.parse(localStorage.getItem('esmiracart') || '[]');
616
  const langCode = '{{ lang_code }}';
617
  const translations = {{ _|tojson }};
618
 
 
654
  autoplay: { delay: 4000, disableOnInteraction: true },
655
  });
656
  }
657
+
658
+ function setupSelect(selectId, items, displayFunc) {
659
  const select = document.getElementById(selectId);
660
  select.innerHTML = '';
661
+ if (items && items.length > 0) {
662
+ items.forEach((item, index) => {
663
+ const option = document.createElement('option');
664
+ option.value = index;
665
+ option.text = displayFunc(item);
666
+ select.appendChild(option);
667
  });
668
  select.parentElement.style.display = 'block';
669
  } else {
 
675
  selectedProductIndex = index;
676
  const product = products[index];
677
  if (!product) return;
 
 
678
 
 
679
  const quantityInput = document.getElementById('quantityInput');
680
+ const quantityLabel = document.getElementById('quantity-label');
681
 
682
+ setupSelect('variantSelect', product.variants, v => `${v.name} - ${v.price.toFixed(2)} ${currencyCode}`);
683
+
684
+ if(product.product_type === 'odezhda') {
685
+ setupSelect('sizeSelect', product.sizes, s => s.name);
686
+ quantityInput.type = "number";
687
  quantityInput.step = "1";
688
  quantityInput.min = "1";
689
+ quantityInput.value = "1";
690
+ quantityLabel.textContent = translations['quantity_label'];
691
  } else {
692
+ document.getElementById('size-section').style.display = 'none';
693
+ quantityInput.type = "number";
694
  quantityInput.step = "0.1";
695
  quantityInput.min = "0.1";
696
+ quantityInput.value = "1.0";
697
+ quantityLabel.textContent = translations['quantity_label_meter'];
698
  }
699
 
700
  document.getElementById('quantityModal').style.display = 'block';
 
704
  function confirmAddToCart() {
705
  if (selectedProductIndex === null) return;
706
 
 
707
  const product = products[selectedProductIndex];
708
+ const quantity = parseFloat(document.getElementById('quantityInput').value);
709
+ const variantIndex = document.getElementById('variantSelect').value;
710
+ const sizeIndex = document.getElementById('sizeSelect').value;
711
+
712
+ if (!product || !product.variants || product.variants.length === 0) {
713
+ alert("Ошибка: Варианты для этого товара не найдены.");
714
  return;
715
  }
716
 
 
717
  const selectedVariant = product.variants[variantIndex];
 
718
  let selectedSize = null;
719
+ let cartItemId = `${product.id}-${selectedVariant.name}`;
720
+
721
+ if (product.product_type === 'odezhda') {
722
+ if (!product.sizes || product.sizes.length === 0) {
723
+ alert("Ошибка: Размеры для этой одежды не найдены.");
 
 
 
724
  return;
725
  }
726
+ selectedSize = product.sizes[sizeIndex];
727
+ cartItemId += `-${selectedSize.name}`;
728
  }
729
 
730
+ if (isNaN(quantity) || quantity <= 0) {
731
+ alert("Пожалуйста, укажите корректное количество.");
732
  return;
733
  }
734
 
 
735
  const existingItem = cart.find(item => item.id === cartItemId);
736
 
737
  if (existingItem) {
 
743
  name: product.name,
744
  price: selectedVariant.price,
745
  variantName: selectedVariant.name,
746
+ sizeName: selectedSize ? selectedSize.name : null,
747
+ productType: product.product_type,
748
  photo: product.photos && product.photos.length > 0 ? product.photos[0] : null,
749
  quantity: quantity
750
  });
751
  }
752
 
753
+ localStorage.setItem('esmiracart', JSON.stringify(cart));
754
  closeModal('quantityModal');
755
  updateCartButton();
756
  showNotification(`${product.name} ${translations['add_to_cart_notification']}`);
 
783
  total += itemTotal;
784
  const photoUrl = item.photo ? `https://huggingface.co/datasets/${repoId}/resolve/main/photos/${item.photo}` : 'https://via.placeholder.com/65x65.png?text=N/A';
785
 
786
+ let detailsHtml = `<p class="cart-item-price">${translations['cart_item_variant']}: ${item.variantName}</p>`;
787
  if (item.sizeName) {
788
+ detailsHtml += `<p class="cart-item-price">${translations['cart_item_size']}: ${item.sizeName}</p>`;
789
  }
790
+
 
791
  return `
792
  <div class="cart-item">
793
  <img src="${photoUrl}" alt="${item.name}">
794
  <div class="cart-item-details">
795
  <strong>${item.name}</strong>
796
+ ${detailsHtml}
797
+ <p class="cart-item-price">${item.price.toFixed(2)} ${currencyCode} × ${item.quantity}</p>
798
  </div>
799
  <span class="cart-item-total">${itemTotal.toFixed(2)}</span>
800
  <button class="cart-item-remove" onclick="removeFromCart('${item.id}')" title="${translations['remove_item_title']}">&times;</button>
 
808
 
809
  function removeFromCart(itemId) {
810
  cart = cart.filter(item => item.id !== itemId);
811
+ localStorage.setItem('esmiracart', JSON.stringify(cart));
812
  openCartModal();
813
  updateCartButton();
814
  }
 
816
  function clearCart() {
817
  if (confirm(translations['clear_cart_confirm'])) {
818
  cart = [];
819
+ localStorage.removeItem('esmiracart');
820
  openCartModal();
821
  updateCartButton();
822
  }
 
838
  .then(response => response.json())
839
  .then(data => {
840
  if (data.order_id) {
841
+ localStorage.removeItem('esmiracart');
842
  cart = [];
843
  updateCartButton();
844
  closeModal('cartModal');
 
892
 
893
  function setupFilters() {
894
  document.getElementById('search-input').addEventListener('input', filterProducts);
895
+ document.querySelectorAll('.filter-btn').forEach(filter => {
896
  filter.addEventListener('click', function() {
897
+ const filterGroupClass = this.classList.contains('category-filter') ? '.category-filter.active' : '.type-filter.active';
898
+ document.querySelector(filterGroupClass).classList.remove('active');
 
 
 
 
 
 
899
  this.classList.add('active');
900
  filterProducts();
901
  });
 
946
  <div class="swiper-wrapper">
947
  {% if product.get('photos') and product['photos']|length > 0 %}
948
  {% for photo in product['photos'] %}
949
+ <div class="swiper-slide" style="display: flex; justify-content: center; align-items: center; padding: 10px; background-color: #fff;">
950
  <div class="swiper-zoom-container">
951
  <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ photo }}"
952
  alt="{{ product['name'] }} - фото {{ loop.index }}"
 
968
  </div>
969
 
970
  <div style="font-size: 1rem; line-height: 1.7; padding: 0 10px;">
971
+ <p><strong>{{ _['product_type'] }}:</strong> {{ _[product.get('product_type', 'tkaniny')] }}</p>
972
  <p><strong>{{ _['category'] }}:</strong> {{ product.get('category', _['no_category']) }}</p>
973
+
974
  {% if product.get('variants') and product.variants|length > 0 %}
975
  <p style="font-size: 1.4rem; font-weight: bold; color: var(--primary-color); margin: 15px 0;">
976
  {{ _['from_price'] }} {{ "%.2f"|format(product.variants|map(attribute='price')|min) }} {{ currency_code }}
977
+ {% if product.product_type == 'tkaniny' %} / {{ _['price_per_meter']|lower }} {% endif %}
978
  </p>
979
  <p><strong>{{ _['available_variants'] }}</strong></p>
980
  <ul style="list-style: none; padding-left: 0;">
 
986
  {% endfor %}
987
  </ul>
988
  {% endif %}
989
+
990
+ {% if product.product_type == 'odezhda' and product.get('sizes') and product.sizes|length > 0 %}
991
+ <p style="margin-top: 15px;"><strong>{{ _['available_sizes'] }}</strong></p>
992
+ <ul style="list-style: none; padding-left: 0;">
993
+ {% for size in product.sizes %}
994
+ <li style="padding: 5px 0; border-bottom: 1px solid var(--border-color);">
995
+ - {{ size.name }}
996
+ </li>
997
+ {% endfor %}
998
+ </ul>
999
  {% endif %}
1000
+
1001
  <p style="margin-top: 20px;"><strong>{{ _['description'] }}:</strong><br> {{ product.get('description', _['no_description'])|replace('\\n', '<br>')|safe }}</p>
1002
  </div>
1003
  <div style="padding: 20px 10px 10px; text-align: center;">
 
1021
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
1022
  <style>
1023
  :root {
1024
+ --primary-color: #9A2A47;
1025
+ --primary-dark: #7a2138;
1026
+ --surface-color: #313131;
1027
+ --background-color: #212121;
1028
  --text-color: #F5F5F5;
1029
+ --text-color-muted: #BDBDBD;
1030
+ --border-color: #424242;
1031
  }
1032
  body { font-family: 'Georgia', serif; background: var(--background-color); color: var(--text-color); line-height: 1.6; padding: 15px; }
1033
  .container { max-width: 800px; margin: 20px auto; padding: 30px; background: var(--surface-color); border-radius: 16px; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.6); border: 1px solid var(--border-color); }
 
1037
  .order-meta { font-size: 0.9rem; color: var(--text-color-muted); margin-bottom: 20px; text-align: center; }
1038
  .order-item { display: grid; grid-template-columns: 60px 1fr auto; gap: 15px; align-items: center; padding: 15px 0; border-bottom: 1px solid var(--border-color); }
1039
  .order-item:last-child { border-bottom: none; }
1040
+ .order-item img { width: 60px; height: 60px; object-fit: contain; border-radius: 8px; background-color: #fff; padding: 5px; border: 1px solid #444;}
1041
  .item-details strong { display: block; margin-bottom: 4px; font-size: 1.05rem; color: var(--text-color);}
1042
  .item-details span { font-size: 0.9rem; color: var(--text-color-muted); display: block;}
1043
  .item-total { font-weight: bold; text-align: right; font-size: 1.1rem; color: var(--primary-color);}
1044
  .order-summary { margin-top: 30px; padding-top: 20px; border-top: 2px solid var(--primary-color); text-align: right; }
1045
  .order-summary p { margin-bottom: 10px; font-size: 1.1rem; }
1046
  .order-summary strong { font-size: 1.5rem; color: var(--primary-color); }
1047
+ .customer-info { margin-top: 30px; background-color: rgba(154, 42, 71, 0.05); padding: 20px; border-radius: 12px; border: 1px solid var(--primary-color);}
1048
  .customer-info p { margin-bottom: 8px; font-size: 1rem; }
1049
  .customer-info strong { color: var(--text-color); }
1050
  .actions { margin-top: 30px; text-align: center; }
 
1101
  function sendOrderViaWhatsApp() {
1102
  const orderId = '{{ order.id }}';
1103
  const orderUrl = `{{ request.url }}`;
1104
+ const whatsappNumber = "{{ whatsapp_number }}".replace(/\D/g, '');
1105
 
1106
  let message = `{{ _['whatsapp_confirm_message_1'] }}%0A%0A`;
1107
  message += `*{{ _['whatsapp_confirm_message_2'] }}* ${orderId}%0A`;
 
1136
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
1137
  <style>
1138
  :root {
1139
+ --primary-color: #9A2A47;
1140
+ --primary-dark: #7a2138;
1141
+ --surface-color: #313131;
1142
+ --background-color: #212121;
1143
  --text-color: #F5F5F5;
1144
+ --text-color-muted: #BDBDBD;
1145
+ --border-color: #424242;
1146
  --success-bg: #113d21;
1147
  --success-text: #6ee791;
1148
  --error-bg: #4d0a0a;
 
1157
  h1 { font-size: 2rem; }
1158
  h2 { font-size: 1.6rem; margin-top: 30px; display: flex; align-items: center; gap: 10px; padding-bottom: 8px; border-bottom: 1px solid var(--border-color);}
1159
  h3 { font-size: 1.2rem; color: var(--text-color); margin-top: 20px;}
1160
+ .section { margin-bottom: 30px; padding: 20px; background-color: #292929; border: 1px solid var(--border-color); border-radius: 8px; }
1161
 
1162
  label { font-weight: 500; margin-top: 12px; display: block; color: var(--text-color); font-size: 0.9rem;}
1163
  input[type="text"], input[type="number"], input[type="password"], input[type="tel"], textarea, select { width: 100%; padding: 10px 12px; margin-top: 6px; border: 1px solid var(--border-color); border-radius: 6px; font-size: 0.95rem; box-sizing: border-box; transition: border-color 0.3s ease; background-color: var(--background-color); color: var(--text-color); }
1164
+ input:focus, textarea:focus, select:focus { border-color: var(--primary-color); outline: none; box-shadow: 0 0 0 3px rgba(154, 42, 71, 0.2); }
1165
  textarea { min-height: 90px; resize: vertical; }
1166
  input[type="file"] { padding: 8px; background-color: #222; cursor: pointer; border: 1px solid var(--border-color); border-radius: 6px; }
1167
  input[type="checkbox"] { transform: scale(1.2); margin-right: 8px; vertical-align: middle; accent-color: var(--primary-color); }
 
1174
  .item-list { display: grid; gap: 15px; }
1175
  .item { background: var(--surface-color); padding: 15px; border-radius: 8px; border: 1px solid var(--border-color); }
1176
  .item-actions { margin-top: 15px; display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }
1177
+ .photo-preview img, .photo-edit-item img { width: 60px; height: 60px; border-radius: 6px; margin: 5px 5px 0 0; border: 1px solid var(--border-color); object-fit: cover; background-color: #fff;}
1178
  .photo-preview-edit { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 5px; }
1179
  .photo-edit-item { position: relative; }
1180
  .photo-edit-item input[type="checkbox"] { position: absolute; top: 0px; right: 0px; transform: scale(1.3); accent-color: #ff453a; cursor: pointer; }
 
1190
  .message.error { background-color: var(--error-bg); color: var(--error-text); border-color: var(--error-text);}
1191
  .message.warning { background-color: var(--warning-bg); color: var(--warning-text); border-color: var(--warning-text); }
1192
  .status-indicator { display: inline-block; padding: 3px 9px; border-radius: 50px; font-size: 0.75rem; font-weight: 500; margin-left: 5px; vertical-align: middle; }
 
1193
  .status-indicator.new { background-color: #e65100; color: #ffe0b2; }
1194
  .status-indicator.accepted { background-color: #0277bd; color: #b3e5fc; }
1195
  .status-indicator.prepared { background-color: #2f855a; color: #c6f6d5; }
1196
  .status-indicator.shipped { background-color: #065f46; color: #a7f3d0; }
1197
 
1198
+ details { background-color: #292929; border: 1px solid var(--border-color); border-radius: 8px; margin-bottom: 10px; }
1199
  details > summary { cursor: pointer; font-weight: 500; color: var(--text-color); display: flex; justify-content: space-between; align-items: center; padding: 15px 20px; list-style: none; position: relative; }
1200
  details[open] > summary { border-bottom: 1px solid var(--border-color); }
1201
  .order-details-content { padding: 15px 20px; }
 
1210
  <div class="container">
1211
  <div class="header">
1212
  <div class="logo-title-container" style="display: flex; align-items: center; gap: 15px;">
1213
+ <img src="https://huggingface.co/spaces/esmira-tkani/admin/resolve/main/Screenshot_20251225-134859.png" alt="Esmira Logo" style="height: 45px; width: auto;">
1214
+ <h1><i class="fas fa-tools"></i> Админ-панель</h1>
1215
  </div>
1216
  <a href="{{ url_for('catalog', lang_code='ru') }}" class="button"><i class="fas fa-store"></i> Перейти в каталог</a>
1217
  </div>
 
1225
  {% endif %}
1226
  {% endwith %}
1227
 
 
 
 
 
 
 
 
 
 
 
 
1228
  <div class="section">
1229
  <h2><i class="fas fa-history"></i> История заказов</h2>
1230
  {% if orders %}
 
1265
  </div>
1266
 
1267
  <div class="section">
1268
+ <h2><i class="fas fa-cog"></i> Настройки магазина</h2>
1269
+ <form method="POST">
1270
+ <input type="hidden" name="action" value="update_settings">
1271
+ <label for="whatsapp_number">Номер WhatsApp для заказов:</label>
1272
+ <input type="tel" id="whatsapp_number" name="whatsapp_number" value="{{ settings.get('whatsapp_number', '') }}" required>
1273
+ <button type="submit"><i class="fas fa-save"></i> Сохранить настройки</button>
1274
+ </form>
1275
+ <p style="margin-top: 20px;"><strong>Текущие адреса в г. Алматы:</strong></p>
1276
+ <ul style="padding-left: 20px;">
1277
+ {% for address in store_addresses %}
1278
+ <li>{{ address }}</li>
1279
+ {% endfor %}
1280
+ </ul>
1281
+ <p style="margin-top: 10px;"><strong>Валюта:</strong> {{ currency_name }} ({{ currency_code }})</p>
1282
+ </div>
1283
+
1284
+ <div class="section">
1285
  <h2><i class="fas fa-tags"></i> Категории</h2>
1286
  <details>
1287
  <summary><i class="fas fa-plus-circle"></i> Добавить новую категорию</summary>
 
1311
  {% else %}
1312
  <p>Категорий пока нет.</p>
1313
  {% endif %}
1314
+ </div>
1315
+
 
1316
  <div class="section">
1317
  <h2><i class="fas fa-box-open"></i> Товары</h2>
1318
  <details>
1319
  <summary><i class="fas fa-plus-circle"></i> Добавить новый товар</summary>
1320
  <div style="padding: 15px;">
1321
+ <form method="POST" enctype="multipart/form-data" id="add-product-form">
1322
  <input type="hidden" name="action" value="add_product">
1323
  <label for="add_name">Название товара *:</label>
1324
  <input type="text" id="add_name" name="name" required>
 
 
 
 
 
 
 
1325
  <label for="add_description">Описание:</label>
1326
  <textarea id="add_description" name="description" rows="4"></textarea>
1327
+
1328
+ <label for="add_product_type">Тип товара *:</label>
1329
+ <select id="add_product_type" name="product_type" onchange="toggleProductFields('add')" required>
1330
+ <option value="tkaniny">Ткани</option>
1331
+ <option value="odezhda">Одежда</option>
1332
+ </select>
1333
 
1334
  <label for="add_category">Категория:</label>
1335
  <select id="add_category" name="category">
 
1350
  <button type="button" class="remove-btn" onclick="removeInputGroup(this)"><i class="fas fa-times"></i></button>
1351
  </div>
1352
  </div>
1353
+ <button type="button" class="button add-btn" onclick="addInputGroup('add-variants-container', 'variant')"><i class="fas fa-plus"></i> Добавить вариант</button>
1354
+
1355
+ <div id="add-sizes-section" style="display:none; margin-top:20px;">
1356
+ <h4>Размеры *:</h4>
1357
+ <div id="add-sizes-container">
1358
+ <div class="input-group">
1359
+ <input type="text" name="size_names" placeholder="Название размера (напр. S, 42-44)">
1360
+ <button type="button" class="remove-btn" onclick="removeInputGroup(this)"><i class="fas fa-times"></i></button>
1361
+ </div>
1362
+ </div>
1363
+ <button type="button" class="button add-btn" onclick="addInputGroup('add-sizes-container', 'size')"><i class="fas fa-plus"></i> Добавить размер</button>
 
 
 
 
 
1364
  </div>
1365
+
1366
+ <br><br>
1367
  <button type="submit"><i class="fas fa-save"></i> Добавить товар</button>
1368
  </form>
1369
  </div>
 
1385
  {% endif %}
1386
  </div>
1387
  <div style="flex-grow: 1;">
1388
+ <h3 style="margin-top: 0; margin-bottom: 5px;">{{ product['name'] }}</h3>
1389
+ <p style="font-size: 0.9rem; color: var(--text-color-muted);"><strong>Тип:</strong> {{ 'Ткани' if product.get('product_type') == 'tkaniny' else 'Одежда' }}</p>
 
 
 
1390
  <p style="font-size: 0.9rem; color: var(--text-color-muted);"><strong>Категория:</strong> {{ product.get('category', 'Без категории') }}</p>
1391
  </div>
1392
  </div>
 
1407
  <input type="hidden" name="product_id" value="{{ product.id }}">
1408
  <label>Название *:</label>
1409
  <input type="text" name="name" value="{{ product['name'] }}" required>
1410
+ <label>Описание:</label>
1411
+ <textarea name="description" rows="4">{{ product.get('description', '') }}</textarea>
1412
 
1413
  <label>Тип товара *:</label>
1414
+ <select name="product_type" onchange="toggleProductFields('edit-{{ product.id }}')" required>
1415
+ <option value="tkaniny" {% if product.get('product_type') == 'tkaniny' %}selected{% endif %}>Ткани</option>
1416
+ <option value="odezhda" {% if product.get('product_type') == 'odezhda' %}selected{% endif %}>Одежда</option>
1417
  </select>
1418
 
 
 
 
1419
  <label>Категория:</label>
1420
  <select name="category">
1421
  <option value="Без категории" {% if product.get('category', 'Без категории') == 'Без категории' %}selected{% endif %}>Без категории</option>
 
1451
  </div>
1452
  {% endfor %}
1453
  </div>
1454
+ <button type="button" class="button add-btn" onclick="addInputGroup('edit-variants-container-{{ product.id }}', 'variant')"><i class="fas fa-plus"></i> Добавить вариант</button>
1455
 
1456
+ <div id="edit-{{ product.id }}-sizes-section" style="margin-top:20px; display: {% if product.get('product_type') == 'odezhda' %}block{% else %}none{% endif %};">
1457
  <h4>Размеры *:</h4>
1458
  <div id="edit-sizes-container-{{ product.id }}">
1459
  {% for size in product.get('sizes', []) %}
1460
  <div class="input-group">
1461
+ <input type="text" name="size_names" value="{{ size.name }}" >
1462
+ <button type="button" class="remove-btn" onclick="removeInputGroup(this)"><i class="fas fa-times"></i></button>
1463
  </div>
1464
  {% endfor %}
 
 
 
 
 
 
1465
  </div>
1466
+ <button type="button" class="button add-btn" onclick="addInputGroup('edit-sizes-container-{{ product.id }}', 'size')"><i class="fas fa-plus"></i> Добавить размер</button>
 
 
 
 
 
1467
  </div>
1468
+
1469
+ <br><br>
1470
  <button type="submit"><i class="fas fa-save"></i> Сохранить изменения</button>
1471
  </form>
1472
  </div>
 
1477
  <p>Товаров пока нет.</p>
1478
  {% endif %}
1479
  </div>
 
 
 
 
 
 
 
 
 
 
1480
  </div>
1481
 
1482
  <script>
 
1484
  const form = document.getElementById(formId);
1485
  form.style.display = form.style.display === 'none' ? 'block' : 'none';
1486
  }
 
 
 
 
 
 
 
 
1487
 
1488
+ function addInputGroup(containerId, type) {
 
 
 
 
 
 
 
 
 
 
 
 
 
1489
  const container = document.getElementById(containerId);
1490
  const newInputGroup = document.createElement('div');
1491
  newInputGroup.className = 'input-group';
1492
+ if (type === 'variant') {
1493
+ newInputGroup.innerHTML = `
1494
+ <input type="text" name="variant_names" placeholder="Название варианта" required>
1495
+ <input type="number" name="variant_prices" step="0.01" min="0" placeholder="Цена" required>
1496
+ <button type="button" class="remove-btn" onclick="removeInputGroup(this)"><i class="fas fa-times"></i></button>
1497
+ `;
1498
+ } else { // size
1499
+ newInputGroup.innerHTML = `
1500
+ <input type="text" name="size_names" placeholder="Название размера">
1501
+ <button type="button" class="remove-btn" onclick="removeInputGroup(this)"><i class="fas fa-times"></i></button>
1502
+ `;
1503
+ }
1504
  container.appendChild(newInputGroup);
1505
  }
1506
 
1507
+ function removeInputGroup(button) {
1508
  const group = button.closest('.input-group');
1509
  const container = group.parentNode;
1510
+ if (container.children.length > 1) {
1511
  group.remove();
1512
  } else {
1513
+ alert("Должен быть хотя бы один элемент.");
1514
  }
1515
  }
1516
+
1517
+ function toggleProductFields(prefix) {
1518
+ const productType = document.querySelector(`[name="product_type"]`).value;
1519
+ const sizesSection = document.getElementById(`${prefix}-sizes-section`);
1520
+ const sizeInputs = sizesSection.querySelectorAll('input[name="size_names"]');
1521
+
1522
+ if (productType === 'odezhda') {
1523
+ sizesSection.style.display = 'block';
1524
+ if (sizesSection.querySelector('.input-group') === null) {
1525
+ addInputGroup(`${prefix}-sizes-container`, 'size')
1526
+ }
1527
+ sizeInputs.forEach(input => input.required = true);
1528
+ } else {
1529
+ sizesSection.style.display = 'none';
1530
+ sizeInputs.forEach(input => input.required = false);
1531
+ }
1532
+ }
1533
+ document.addEventListener('DOMContentLoaded', function() {
1534
+ document.querySelectorAll('form[id^="edit-form-"]').forEach(form => {
1535
+ const id = form.id.replace('edit-form-', '');
1536
+ const select = form.querySelector('select[name="product_type"]');
1537
+ if(select) {
1538
+ const productType = select.value;
1539
+ const sizesSection = document.getElementById(`edit-${id}-sizes-section`);
1540
+ const sizeInputs = sizesSection.querySelectorAll('input[name="size_names"]');
1541
+ if (productType === 'odezhda') {
1542
+ sizesSection.style.display = 'block';
1543
+ sizeInputs.forEach(input => input.required = true);
1544
+ } else {
1545
+ sizesSection.style.display = 'none';
1546
+ sizeInputs.forEach(input => input.required = false);
1547
+ }
1548
+ }
1549
+ });
1550
+ });
1551
  </script>
1552
  </body>
1553
  </html>
 
1573
  all_products = data.get('products', [])
1574
  categories = sorted(data.get('categories', []))
1575
  settings = data.get('settings', {})
1576
+ whatsapp_number = settings.get('whatsapp_number', '+77073363943').replace(' ', '').replace('+', '')
1577
+
1578
 
1579
  products_in_stock = [p for p in all_products if p.get('variants')]
1580
 
1581
  for p in products_in_stock:
1582
  p['variants'] = sorted(p.get('variants', []), key=lambda v: v.get('price', 0))
1583
 
1584
+ products_sorted = sorted(products_in_stock, key=lambda p: p.get('name', '').lower())
1585
 
1586
  return render_template_string(
1587
  CATALOG_TEMPLATE,
 
1592
  currency_code=CURRENCY_CODE,
1593
  lang_code=g.lang_code,
1594
  _=g.translations,
1595
+ whatsapp_number=whatsapp_number
1596
  )
1597
 
1598
  @app.route('/<lang_code>/product/<int:index>')
 
1604
  for p in products_in_stock:
1605
  p['variants'] = sorted(p.get('variants', []), key=lambda v: v.get('price', 0))
1606
 
1607
+ products_sorted = sorted(products_in_stock, key=lambda p: p.get('name', '').lower())
1608
 
1609
  try:
1610
  product = products_sorted[index]
 
1687
  data = load_data()
1688
  order = data.get('orders', {}).get(order_id)
1689
  settings = data.get('settings', {})
1690
+ whatsapp_number = settings.get('whatsapp_number', '+77073363943')
1691
  status_map = STATUS_MAPS.get(lang_code, STATUS_MAPS['ru'])
1692
  return render_template_string(ORDER_TEMPLATE, order=order, status_map=status_map, currency_code=CURRENCY_CODE, lang_code=g.lang_code, _=g.translations, whatsapp_number=whatsapp_number)
1693
 
 
1695
  def admin():
1696
  data = load_data()
1697
  if 'orders' not in data: data['orders'] = {}
1698
+ if 'settings' not in data: data['settings'] = {'whatsapp_number': '+77073363943'}
1699
+
1700
 
1701
  needs_save = False
1702
  for p in data.get('products', []):
 
1710
  action = request.form.get('action')
1711
  try:
1712
  if action == 'update_settings':
1713
+ new_whatsapp = request.form.get('whatsapp_number', '').strip()
1714
+ if new_whatsapp:
1715
+ data['settings']['whatsapp_number'] = new_whatsapp
1716
+ save_data(data)
1717
+ flash('Настройки успешно обновлены.', 'success')
1718
+ else:
1719
+ flash('Номер WhatsApp не может быть пустым.', 'error')
1720
 
1721
  elif action == 'update_order_status':
1722
  order_id = request.form.get('order_id')
 
1751
 
1752
  elif action == 'add_product' or action == 'edit_product':
1753
  name = request.form.get('name', '').strip()
1754
+ if not name:
1755
+ flash("Название товара обязательно.", 'error')
 
 
1756
  return redirect(url_for('admin'))
1757
 
1758
  variant_names = [v.strip() for v in request.form.getlist('variant_names') if v.strip()]
 
1772
  flash("Неверный формат цены в вариантах.", 'error')
1773
  return redirect(url_for('admin'))
1774
 
1775
+ product_type = request.form.get('product_type')
1776
  sizes = []
1777
+ if product_type == 'odezhda':
1778
  size_names = [s.strip() for s in request.form.getlist('size_names') if s.strip()]
1779
  if not size_names:
1780
  flash("Для одежды необходимо указать хотя бы один размер.", 'error')
1781
  return redirect(url_for('admin'))
1782
  sizes = [{'name': s_name} for s_name in size_names]
1783
 
1784
+
1785
  product_data = {
1786
  'name': name,
 
1787
  'description': request.form.get('description', '').strip(),
1788
  'category': request.form.get('category'),
1789
+ 'product_type': product_type,
1790
  'variants': variants,
1791
+ 'sizes': sizes
1792
  }
1793
 
1794
  newly_uploaded_photos = []