Kgshop commited on
Commit
baae1a3
·
verified ·
1 Parent(s): d3908cc

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +420 -267
app.py CHANGED
@@ -1,3 +1,4 @@
 
1
  import os
2
  import io
3
  import base64
@@ -241,6 +242,12 @@ def get_env_data(env_id):
241
  if 'product_id' not in product:
242
  product['product_id'] = uuid4().hex
243
  products_changed = True
 
 
 
 
 
 
244
 
245
  if products_changed or settings_changed:
246
  save_env_data(env_id, env_data)
@@ -339,7 +346,27 @@ def generate_chat_response(message, chat_history_from_client, env_id):
339
  for p in products:
340
  if p.get('in_stock', True):
341
  price_display = f"{p.get('price', 0):.2f}".replace('.00', '')
342
- product_info_list.append(f"- [ID_ТОВАРА: {p.get('product_id', 'N/A')} Название: {p.get('name', 'Без названия')}], Категория: {p.get('category', 'Без категории')}, Цена: {price_display} {currency_code}, Описание: {p.get('description', '')[:100]}...")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
343
  product_list_str = "\n".join(product_info_list) if product_info_list else "В данный момент нет товаров в наличии."
344
 
345
  category_list_str = ", ".join(categories) if categories else "Категорий пока нет."
@@ -358,22 +385,26 @@ def generate_chat_response(message, chat_history_from_client, env_id):
358
 
359
 
360
  system_instruction_content = (
361
- f"Ты - доброжелательный и очень полезный виртуальный консультант по имени {chat_name} для магазина {org_name}. "
362
- "Твоя задача - помогать пользователям находить товары, отвечать на вопросы о них, предлагать варианты, а также предоставлять информацию о магазине. "
363
- "Всегда будь вежлив, информативен и стремись решить проблему пользователя. "
364
- "Никогда не выдумывай товары или категории, которых нет в предоставленных списках. "
365
- "Когда ты предлагаешь товар, всегда указывай его название и ID, используя *точный формат*: [ID_ТОВАРА: <product_id> Название: <product_name>]. Это *очень важно* для клиента. "
366
- "Если пользователь ищет товар или категорию, предлагай несколько наиболее подходящих вариантов или перечисляй доступные из этой категории.\n\n"
 
 
 
 
367
  f"Список доступных категорий: {category_list_str}.\n\n"
368
- f"Список доступных товаров в магазине:\n"
369
  f"{product_list_str}"
370
  f"{org_info_str}\n\n"
371
- "Если пользователь спрашивает про товары или категории, которых нет в списках, вежливо сообщи, что таких товаров/категорий нет и предложи что-то из имеющихся, или перечисли доступные категории. "
372
- "Если вопрос касается общей информации о магазине (например, 'о нас', 'доставка', 'возврат', 'контакты'), используй данные из блока 'Информация о магазине'. "
373
- "Старайся быть кратким, но информативным. Используй эмодзи для дружелюбности. "
374
- "Избегай упоминания Hugging Face или Hugging Face Hub."
375
  )
376
 
 
377
  generated_text = ""
378
  response = None
379
 
@@ -815,6 +846,7 @@ CATALOG_TEMPLATE = '''
815
  .cart-item img { width: 60px; height: 60px; object-fit: cover; border-radius: 8px; }
816
  .cart-item-details { grid-column: 2; }
817
  .cart-item-details strong { display: block; margin-bottom: 5px; font-size: 1rem; color: var(--text-dark);}
 
818
  .cart-item-price { font-size: 0.9rem; color: #666; }
819
  .dark-theme .cart-item-price { color: #ccc; }
820
  .cart-item-quantity { display: flex; align-items: center; gap: 8px; grid-column: 3;}
@@ -823,9 +855,9 @@ CATALOG_TEMPLATE = '''
823
  .cart-item-total { font-weight: bold; text-align: right; grid-column: 4; font-size: 1rem; color: var(--bg-medium);}
824
  .cart-item-remove { grid-column: 5; background:none; border:none; color: var(--danger); cursor:pointer; font-size: 1.3em; padding: 5px; line-height: 1; }
825
  .cart-item-remove:hover { color: var(--danger-hover); }
826
- .quantity-input, .color-select { width: 100%; max-width: 180px; padding: 10px; border: 1px solid #e0e0e0; border-radius: 8px; font-size: 1rem; margin: 10px 0; box-sizing: border-box; }
827
- .dark-theme .quantity-input, .dark-theme .color-select { background-color: #333; color: #fff; border-color: #555; }
828
- .quantity-input:focus, .color-select:focus { border-color: var(--accent); outline: none; box-shadow: 0 0 0 2px rgba(72, 209, 204, 0.2); }
829
  .cart-summary { margin-top: 20px; text-align: right; border-top: 1px solid #e0e0e0; padding-top: 15px; }
830
  .dark-theme .cart-summary { border-top-color: #444; }
831
  .cart-summary strong { font-size: 1.2rem; color: var(--bg-medium);}
@@ -917,11 +949,10 @@ CATALOG_TEMPLATE = '''
917
  <div id="quantityModal" class="modal">
918
  <div class="modal-content">
919
  <span class="close" onclick="closeModal('quantityModal')" aria-label="Закрыть">×</span>
920
- <h2>Укажите количество и цвет</h2>
 
921
  <label for="quantityInput">Количество:</label>
922
  <input type="number" id="quantityInput" class="quantity-input" min="1" value="1">
923
- <label for="colorSelect">Цвет/Вариант:</label>
924
- <select id="colorSelect" class="color-select"></select>
925
  <button class="product-button formulate-order-button" style="width:100%;" onclick="confirmAddToCart()"><i class="fas fa-check"></i> Добавить в корзину</button>
926
  </div>
927
  </div>
@@ -1013,12 +1044,37 @@ CATALOG_TEMPLATE = '''
1013
  .then(data => {
1014
  modalContent.innerHTML = data;
1015
  initializeSwiper();
 
1016
  })
1017
  .catch(error => {
1018
  console.error('Ошибка загрузки деталей продукта:', error);
1019
  modalContent.innerHTML = `<p style="color: var(--danger); text-align:center; padding: 40px;">Не удалось загрузить информацию о товаре. ${error.message}</p>`;
1020
  });
1021
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1022
 
1023
  function initializeSwiper() {
1024
  const swiperContainer = document.querySelector('#productModal .swiper-container');
@@ -1040,27 +1096,25 @@ CATALOG_TEMPLATE = '''
1040
  selectedProductId = productId;
1041
  const product = getProductById(productId);
1042
  if (!product) {
1043
- console.error("Product not found for ID:", productId);
1044
  alert("Ошибка: товар не найден.");
1045
  return;
1046
  }
1047
- const colorSelect = document.getElementById('colorSelect');
1048
- const colorLabel = document.querySelector('label[for="colorSelect"]');
1049
- colorSelect.innerHTML = '';
1050
- const validColors = product.colors ? product.colors.filter(c => c && c.trim() !== "") : [];
1051
- if (validColors.length > 0) {
1052
- validColors.forEach(color => {
1053
- const option = document.createElement('option');
1054
- option.value = color.trim();
1055
- option.text = color.trim();
1056
- colorSelect.appendChild(option);
1057
- });
1058
- colorSelect.style.display = 'block';
1059
- if(colorLabel) colorLabel.style.display = 'block';
1060
- } else {
1061
- colorSelect.style.display = 'none';
1062
- if(colorLabel) colorLabel.style.display = 'none';
1063
  }
 
 
 
 
 
 
 
1064
  document.getElementById('quantityInput').value = 1;
1065
  const modal = document.getElementById('quantityModal');
1066
  if(modal) {
@@ -1073,8 +1127,13 @@ CATALOG_TEMPLATE = '''
1073
  if (selectedProductId === null) return;
1074
  const quantityInput = document.getElementById('quantityInput');
1075
  const quantity = parseInt(quantityInput.value);
1076
- const colorSelect = document.getElementById('colorSelect');
1077
- const color = colorSelect.style.display !== 'none' && colorSelect.value ? colorSelect.value : 'N/A';
 
 
 
 
 
1078
  if (isNaN(quantity) || quantity <= 0) {
1079
  alert("Пожалуйста, укажите корректное количество (больше 0).");
1080
  quantityInput.focus();
@@ -1085,7 +1144,14 @@ CATALOG_TEMPLATE = '''
1085
  alert("Ошибка добавления: товар не найден.");
1086
  return;
1087
  }
1088
- const cartItemId = `${product.product_id}-${color}`;
 
 
 
 
 
 
 
1089
  const existingItemIndex = cart.findIndex(item => item.id === cartItemId);
1090
  if (existingItemIndex > -1) {
1091
  cart[existingItemIndex].quantity += quantity;
@@ -1094,10 +1160,11 @@ CATALOG_TEMPLATE = '''
1094
  id: cartItemId,
1095
  product_id: product.product_id,
1096
  name: product.name,
1097
- price: product.price,
1098
  photo: product.photos && product.photos.length > 0 ? product.photos[0] : null,
1099
  quantity: quantity,
1100
- color: color
 
1101
  });
1102
  }
1103
  localStorage.setItem(`mekaCart_${envId}`, JSON.stringify(cart));
@@ -1136,12 +1203,17 @@ CATALOG_TEMPLATE = '''
1136
  const photoUrl = item.photo
1137
  ? `https://huggingface.co/datasets/${repoId}/resolve/main/photos/${item.photo}`
1138
  : 'https://via.placeholder.com/60x60.png?text=N/A';
1139
- const colorText = item.color !== 'N/A' ? ` (Цвет: ${item.color})` : '';
 
 
 
 
1140
  return `
1141
  <div class="cart-item">
1142
  <img src="${photoUrl}" alt="${item.name}">
1143
  <div class="cart-item-details">
1144
- <strong>${item.name}${colorText}</strong>
 
1145
  <p class="cart-item-price">${item.price.toFixed(2)} ${currencyCode}</p>
1146
  </div>
1147
  <div class="cart-item-quantity">
@@ -1482,8 +1554,8 @@ CHAT_TEMPLATE = '''
1482
  body.dark-theme .quantity-btn { background-color: #444; border-color: #555; color: #fff; }
1483
  .cart-item-total { font-weight: bold; text-align: right; font-size: 1rem; color: var(--bg-medium);}
1484
  .cart-item-remove { background:none; border:none; color: var(--danger); cursor:pointer; font-size: 1.3em; }
1485
- .quantity-input, .color-select { width: 100%; max-width: 180px; padding: 10px; border: 1px solid #e0e0e0; border-radius: 8px; font-size: 1rem; margin: 10px 0; }
1486
- body.dark-theme .quantity-input, body.dark-theme .color-select { background-color: #333; color: #fff; border-color: #555; }
1487
  .cart-summary { margin-top: 20px; text-align: right; border-top: 1px solid #e0e0e0; padding-top: 15px; }
1488
  body.dark-theme .cart-summary { border-top-color: #444; }
1489
  .cart-actions { margin-top: 25px; display: flex; justify-content: space-between; }
@@ -1535,9 +1607,10 @@ CHAT_TEMPLATE = '''
1535
  <div id="quantityModal" class="modal">
1536
  <div class="modal-content">
1537
  <span class="close" onclick="closeModal('quantityModal')">×</span>
1538
- <h2>Укажите количество и цвет</h2>
 
 
1539
  <input type="number" id="quantityInput" class="quantity-input" min="1" value="1">
1540
- <select id="colorSelect" class="color-select"></select>
1541
  <button class="product-button formulate-order-button" onclick="confirmAddToCart()">Добавить в корзину</button>
1542
  </div>
1543
  </div>
@@ -1609,37 +1682,50 @@ CHAT_TEMPLATE = '''
1609
  selectedProductId = productId;
1610
  const product = getProductById(productId);
1611
  if (!product) return;
1612
- const colorSelect = document.getElementById('colorSelect');
1613
- colorSelect.innerHTML = '';
1614
- const validColors = product.colors ? product.colors.filter(c => c && c.trim()) : [];
1615
- if (validColors.length > 0) {
1616
- validColors.forEach(color => {
1617
- const option = document.createElement('option');
1618
- option.value = option.text = color.trim();
1619
- colorSelect.appendChild(option);
1620
- });
1621
- colorSelect.style.display = 'block';
1622
- } else {
1623
- colorSelect.style.display = 'none';
 
 
1624
  }
1625
  document.getElementById('quantityInput').value = 1;
1626
  document.getElementById('quantityModal').style.display = "block";
1627
  }
1628
  function confirmAddToCart() {
1629
  const quantity = parseInt(document.getElementById('quantityInput').value);
1630
- const colorSelect = document.getElementById('colorSelect');
1631
- const color = colorSelect.style.display !== 'none' ? colorSelect.value : 'N/A';
 
 
 
 
1632
  if (isNaN(quantity) || quantity <= 0) {
1633
  alert("Укажите корректное количество.");
1634
  return;
1635
  }
1636
  const product = getProductById(selectedProductId);
1637
- const cartItemId = `${product.product_id}-${color}`;
 
 
 
 
 
 
 
1638
  const existingItem = cart.find(item => item.id === cartItemId);
1639
  if (existingItem) {
1640
  existingItem.quantity += quantity;
1641
  } else {
1642
- cart.push({ id: cartItemId, product_id: product.product_id, name: product.name, price: product.price, photo: product.photos ? product.photos[0] : null, quantity, color });
1643
  }
1644
  localStorage.setItem(`mekaCart_${envId}`, JSON.stringify(cart));
1645
  closeModal('quantityModal');
@@ -1670,9 +1756,12 @@ CHAT_TEMPLATE = '''
1670
  cartContent.innerHTML = cart.map(item => {
1671
  const itemTotal = item.price * item.quantity;
1672
  total += itemTotal;
 
 
 
1673
  return `<div class="cart-item">
1674
  <img src="${item.photo ? `https://huggingface.co/datasets/${repoId}/resolve/main/photos/${item.photo}` : ''}" alt="${item.name}">
1675
- <div><strong>${item.name} ${item.color !== 'N/A' ? `(${item.color})` : ''}</strong><p>${item.price.toFixed(2)} ${currencyCode}</p></div>
1676
  <div class="cart-item-quantity"><button class="quantity-btn" onclick="decrementCartItem('${item.id}')">-</button><span>${item.quantity}</span><button class="quantity-btn" onclick="incrementCartItem('${item.id}')">+</button></div>
1677
  <span class="cart-item-total">${itemTotal.toFixed(2)}</span>
1678
  <button class="cart-item-remove" onclick="removeFromCart('${item.id}')"><i class="fas fa-trash-alt"></i></button>
@@ -1842,7 +1931,7 @@ CHAT_TEMPLATE = '''
1842
  '''
1843
 
1844
  PRODUCT_DETAIL_TEMPLATE = '''
1845
- <div style="padding: 10px;">
1846
  <h2 style="font-size: 1.6rem; font-weight: 600; margin-bottom: 15px; text-align: center; color: #135D66;">{{ product['name'] }}</h2>
1847
  <div class="swiper-container" style="max-width: 450px; margin: 0 auto 20px; border-radius: 10px; overflow: hidden; background-color: #fff; border: 1px solid #e0e0e0;">
1848
  <div class="swiper-wrapper">
@@ -1868,9 +1957,32 @@ PRODUCT_DETAIL_TEMPLATE = '''
1868
  <div class="swiper-button-prev" style="color: #135D66;"></div>
1869
  {% endif %}
1870
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1871
 
1872
  <div style="text-align:center; margin-top:20px; padding: 0 10px;">
1873
- <p style="font-size: 1.5rem; font-weight: bold; color: #135D66; margin-bottom: 15px;"><strong>Цена:</strong> {{ "%.0f"|format(product.price) }} {{ currency_code }}</p>
1874
  <button class="product-button formulate-order-button" style="padding: 12px 30px; width: 100%; max-width: 300px;" onclick="closeModal('productModal'); openQuantityModalById('{{ product.get('product_id', '') }}')">
1875
  <i class="fas fa-cart-plus"></i> В корзину
1876
  </button>
@@ -1879,12 +1991,12 @@ PRODUCT_DETAIL_TEMPLATE = '''
1879
  <div style="margin-top: 20px; font-size: 1rem; line-height: 1.7; color: #333; padding: 0 10px;">
1880
  <p><strong>Категория:</strong> {{ product.get('category', 'Без категории') }}</p>
1881
  <p><strong>Описание:</strong><br> {{ product.get('description', 'Описание отсутствует.')|replace('\\n', '<br>')|safe }}</p>
1882
- {% set colors = product.get('colors', []) %}
1883
- {% if colors and colors|select('ne', '')|list|length > 0 %}
1884
- <p><strong>Доступные цвета/варианты:</strong> {{ colors|select('ne', '')|join(', ') }}</p>
1885
- {% endif %}
1886
  </div>
1887
  </div>
 
 
 
 
1888
  '''
1889
 
1890
  ORDER_TEMPLATE = '''
@@ -1970,11 +2082,17 @@ ORDER_TEMPLATE = '''
1970
  container.innerHTML = '<p style="text-align:center; padding: 20px;">Заказ пуст.</p>';
1971
  document.querySelector('.actions .button').disabled = true;
1972
  } else {
1973
- container.innerHTML = order.cart.map((item, index) => `
 
 
 
 
 
1974
  <div class="order-item">
1975
  <img src="${item.photo_url}" alt="${item.name}">
1976
  <div class="item-details">
1977
- <strong>${item.name} ${item.color !== 'N/A' ? `(${item.color})` : ''}</strong>
 
1978
  <span>${item.price.toFixed(2)} {{ currency_code }}</span>
1979
  </div>
1980
  <div class="item-quantity">
@@ -1986,7 +2104,7 @@ ORDER_TEMPLATE = '''
1986
  ${(item.price * item.quantity).toFixed(2)} {{ currency_code }}
1987
  </div>
1988
  </div>
1989
- `).join('');
1990
  document.querySelector('.actions .button').disabled = false;
1991
  }
1992
  updateOrderTotal();
@@ -2032,7 +2150,12 @@ ORDER_TEMPLATE = '''
2032
  message += `*Номер заказа:* ${orderId}%0A%0A`;
2033
 
2034
  order.cart.forEach(item => {
2035
- message += `*${item.name}* ${item.color !== 'N/A' ? `(${item.color})` : ''}%0A`;
 
 
 
 
 
2036
  message += ` - Количество: ${item.quantity}%0A`;
2037
  message += ` - Цена: ${item.price.toFixed(2)} {{ currency_code }}%0A`;
2038
  });
@@ -2116,12 +2239,12 @@ ADMIN_TEMPLATE = '''
2116
  details[open] > summary::after { transform: translateY(-50%) rotate(180deg); }
2117
  details[open] > summary { border-bottom: 1px solid #e0e0e0; }
2118
  details .form-content { padding: 20px; }
2119
- .color-input-group { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
2120
- .color-input-group input { flex-grow: 1; margin: 0; }
2121
- .remove-color-btn { background-color: var(--danger); color: white; padding: 6px 10px; font-size: 0.8rem; margin-top: 0; line-height: 1; }
2122
- .remove-color-btn:hover { background-color: var(--danger-hover); }
2123
- .add-color-btn { background-color: #B2DFDB; color: var(--bg-medium); border: 1px solid #e0e0e0; }
2124
- .add-color-btn:hover { background-color: var(--bg-medium); color: white; border-color: var(--bg-medium); }
2125
  .photo-preview img { max-width: 70px; max-height: 70px; border-radius: 5px; margin: 5px 5px 0 0; border: 1px solid #e0e0e0; object-fit: cover;}
2126
  .sync-buttons { display: flex; gap: 10px; margin-bottom: 20px; flex-wrap: wrap; }
2127
  .download-hf-button { background-color: #6c757d; color: white; }
@@ -2150,6 +2273,10 @@ ADMIN_TEMPLATE = '''
2150
  .chat-message.user .bubble { background-color: #dcf8c6; }
2151
  .chat-message.ai .bubble { background-color: #f1f1f1; }
2152
  .current-avatar { max-width: 60px; max-height: 60px; border-radius: 50%; vertical-align: middle; margin-left: 10px; border: 2px solid var(--bg-medium);}
 
 
 
 
2153
  </style>
2154
  </head>
2155
  <body>
@@ -2328,7 +2455,7 @@ ADMIN_TEMPLATE = '''
2328
  <input type="hidden" name="action" value="add_product">
2329
  <label for="add_name">Название товара *:</label>
2330
  <input type="text" id="add_name" name="name" required>
2331
- <label for="add_price">Цена ({{ currency_code }}) *:</label>
2332
  <input type="number" id="add_price" name="price" step="0.01" min="0" required>
2333
  <label for="add_photos">Фотографии (до 10 шт.):</label>
2334
  <input type="file" id="add_photos" name="photos" accept="image/*" multiple>
@@ -2350,13 +2477,27 @@ ADMIN_TEMPLATE = '''
2350
  {% endfor %}
2351
  </select>
2352
  <label>Цвета/Варианты (оставьте пустым, если нет):</label>
2353
- <div id="add-color-inputs">
2354
- <div class="color-input-group">
2355
  <input type="text" name="colors" placeholder="Например: Розовый">
2356
- <button type="button" class="remove-color-btn" onclick="removeColorInput(this)"><i class="fas fa-times"></i></button>
2357
  </div>
2358
  </div>
2359
- <button type="button" class="button add-color-btn" style="margin-top: 5px;" onclick="addColorInput('add-color-inputs')"><i class="fas fa-palette"></i> Добавить поле</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2360
  <br>
2361
  <div style="margin-top: 15px;">
2362
  <input type="checkbox" id="add_in_stock" name="in_stock" checked>
@@ -2403,7 +2544,10 @@ ADMIN_TEMPLATE = '''
2403
  <p><strong>Цена:</strong> {{ "%.2f"|format(product.price) }} {{ currency_code }}</p>
2404
  <p class="description" title="{{ product.get('description', '') }}"><strong>Описание:</strong> {{ product.get('description', 'N/A')[:150] }}{% if product.get('description', '')|length > 150 %}...{% endif %}</p>
2405
  {% set colors = product.get('colors', []) %}
 
2406
  <p><strong>Цвета/Вар-ты:</strong> {{ colors|select('ne', '')|join(', ') if colors|select('ne', '')|list|length > 0 else 'Нет' }}</p>
 
 
2407
  {% if product.get('photos') and product['photos']|length > 1 %}
2408
  <p style="font-size: 0.8rem; color: #999;">(Всего фото: {{ product['photos']|length }})</p>
2409
  {% endif %}
@@ -2456,25 +2600,50 @@ ADMIN_TEMPLATE = '''
2456
  {% endfor %}
2457
  </select>
2458
  <label>Цвета/Варианты:</label>
2459
- <div id="edit-color-inputs-{{ loop.index0 }}">
2460
  {% set current_colors = product.get('colors', []) %}
2461
  {% if current_colors and current_colors|select('ne', '')|list|length > 0 %}
2462
  {% for color in current_colors %}
2463
  {% if color.strip() %}
2464
- <div class="color-input-group">
2465
  <input type="text" name="colors" value="{{ color }}">
2466
- <button type="button" class="remove-color-btn" onclick="removeColorInput(this)"><i class="fas fa-times"></i></button>
2467
  </div>
2468
  {% endif %}
2469
  {% endfor %}
2470
  {% else %}
2471
- <div class="color-input-group">
2472
  <input type="text" name="colors" placeholder="Например: Цвет">
2473
- <button type="button" class="remove-color-btn" onclick="removeColorInput(this)"><i class="fas fa-times"></i></button>
2474
  </div>
2475
  {% endif %}
2476
  </div>
2477
- <button type="button" class="button add-color-btn" style="margin-top: 5px;" onclick="addColorInput('edit-color-inputs-{{ loop.index0 }}')"><i class="fas fa-palette"></i> Добавить поле</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2478
  <br>
2479
  <div style="margin-top: 15px;">
2480
  <input type="checkbox" id="edit_in_stock_{{ loop.index0 }}" name="in_stock" {% if product.get('in_stock', True) %}checked{% endif %}>
@@ -2505,48 +2674,107 @@ ADMIN_TEMPLATE = '''
2505
  </div>
2506
 
2507
  <script>
 
 
2508
  function toggleEditForm(formId) {
2509
  const formContainer = document.getElementById(formId);
2510
  if (formContainer) {
2511
- formContainer.style.display = formContainer.style.display === 'none' || formContainer.style.display === '' ? 'block' : 'none';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2512
  }
2513
  }
2514
 
2515
- function addColorInput(containerId) {
2516
  const container = document.getElementById(containerId);
2517
  if (container) {
2518
  const newInputGroup = document.createElement('div');
2519
- newInputGroup.className = 'color-input-group';
2520
  newInputGroup.innerHTML = `
2521
- <input type="text" name="colors" placeholder="Новый цвет/вариант">
2522
- <button type="button" class="remove-color-btn" onclick="removeColorInput(this)"><i class="fas fa-times"></i></button>
2523
  `;
2524
  container.appendChild(newInputGroup);
2525
- const newInput = newInputGroup.querySelector('input[name="colors"]');
2526
- if (newInput) {
2527
- newInput.focus();
2528
- }
2529
  }
2530
  }
2531
 
2532
- function removeColorInput(button) {
2533
- const group = button.closest('.color-input-group');
2534
  if (group) {
2535
  const container = group.parentNode;
2536
  group.remove();
2537
  if (container && container.children.length === 0) {
2538
- const placeholderGroup = document.createElement('div');
2539
- placeholderGroup.className = 'color-input-group';
2540
- placeholderGroup.innerHTML = `
2541
- <input type="text" name="colors" placeholder="Например: Цвет">
2542
- <button type="button" class="remove-color-btn" onclick="removeColorInput(this)"><i class="fas fa-times"></i></button>
2543
- `;
2544
- container.appendChild(placeholderGroup);
2545
  }
2546
- } else {
2547
- console.warn("Could not find parent .color-input-group for remove button");
2548
  }
2549
  }
 
 
 
 
 
 
 
 
 
 
 
 
2550
 
2551
  async function generateDescription(photoInputId, descriptionTextareaId, languageSelectId) {
2552
  const photoInput = document.getElementById(photoInputId);
@@ -2874,6 +3102,7 @@ def create_order(env_id):
2874
  "price": price,
2875
  "quantity": quantity,
2876
  "color": item.get('color', 'N/A'),
 
2877
  "photo": item.get('photo'),
2878
  "photo_url": f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/photos/{item['photo']}" if item.get('photo') else "https://via.placeholder.com/60x60.png?text=N/A"
2879
  })
@@ -3018,177 +3247,101 @@ def admin(env_id):
3018
  save_env_data(env_id, data)
3019
  flash("Настройки магазина и чата успешно обновлены.", 'success')
3020
 
3021
- elif action == 'add_product':
3022
- name = request.form.get('name', '').strip()
 
 
 
 
 
 
 
 
 
 
3023
  price_str = request.form.get('price', '').replace(',', '.')
3024
- description = request.form.get('description', '').strip()
3025
  category = request.form.get('category')
3026
- photos_files = request.files.getlist('photos')
3027
- colors = [c.strip() for c in request.form.getlist('colors') if c.strip()]
3028
- in_stock = 'in_stock' in request.form
3029
- is_top = 'is_top' in request.form
 
3030
 
3031
- if not name or not price_str:
3032
  flash("Название и цена товара обязательны.", 'error')
3033
  return redirect(url_for('admin', env_id=env_id))
3034
-
3035
  try:
3036
  price = round(float(price_str), 2)
3037
  if price < 0: price = 0
 
3038
  except ValueError:
3039
- flash("Неверный формат цены.", 'error')
3040
- return redirect(url_for('admin', env_id=env_id))
3041
-
3042
- photos_list = []
3043
- if photos_files and HF_TOKEN_WRITE:
3044
- uploads_dir = 'uploads_temp'
3045
- os.makedirs(uploads_dir, exist_ok=True)
3046
- api = HfApi()
3047
- photo_limit = 10
3048
- uploaded_count = 0
3049
- for photo in photos_files:
3050
- if uploaded_count >= photo_limit:
3051
- flash(f"Загружено только первые {photo_limit} фото.", "warning")
3052
- break
3053
- if photo and photo.filename:
3054
- try:
3055
- ext = os.path.splitext(photo.filename)[1].lower()
3056
- if ext not in ['.jpg', '.jpeg', '.png', '.gif', '.webp']:
3057
- flash(f"Файл {photo.filename} не является изображением и был пропущен.", "warning")
3058
- continue
3059
-
3060
- safe_name = secure_filename(name.replace(' ', '_'))[:50]
3061
- photo_filename = f"{safe_name}_{datetime.now().strftime('%Y%m%d%H%M%S%f')}{ext}"
3062
- temp_path = os.path.join(uploads_dir, photo_filename)
3063
- photo.save(temp_path)
3064
- api.upload_file(
3065
- path_or_fileobj=temp_path,
3066
- path_in_repo=f"photos/{photo_filename}",
3067
- repo_id=REPO_ID,
3068
- repo_type="dataset",
3069
- token=HF_TOKEN_WRITE,
3070
- commit_message=f"Add photo for product {name}"
3071
- )
3072
- photos_list.append(photo_filename)
3073
- os.remove(temp_path)
3074
- uploaded_count += 1
3075
- except Exception as e:
3076
- flash(f"Ошибка при загрузке фото {photo.filename}.", 'error')
3077
- if os.path.exists(temp_path):
3078
- try: os.remove(temp_path)
3079
- except OSError: pass
3080
- elif photo and not photo.filename:
3081
- pass
3082
- try:
3083
- if os.path.exists(uploads_dir) and not os.listdir(uploads_dir):
3084
- os.rmdir(uploads_dir)
3085
- except OSError as e:
3086
- pass
3087
- elif not HF_TOKEN_WRITE and photos_files and any(f.filename for f in photos_files):
3088
- flash("HF_TOKEN (write) не настроен. Фотографии не были загружены.", "warning")
3089
-
3090
-
3091
- new_product = {
3092
- 'product_id': uuid4().hex,
3093
- 'name': name, 'price': price, 'description': description,
3094
- 'category': category if category in categories else 'Без категории',
3095
- 'photos': photos_list, 'colors': colors,
3096
- 'in_stock': in_stock, 'is_top': is_top
3097
- }
3098
- products.append(new_product)
3099
- data['products'] = products
3100
- save_env_data(env_id, data)
3101
- flash(f"Товар '{name}' успешно добавлен.", 'success')
3102
-
3103
- elif action == 'edit_product':
3104
- product_id = request.form.get('product_id')
3105
- product_to_edit = next((p for p in products if p.get('product_id') == product_id), None)
3106
-
3107
- if product_to_edit is None:
3108
- flash(f"Ошибка редактирования: товар с ID '{product_id}' не найден.", 'error')
3109
- return redirect(url_for('admin', env_id=env_id))
3110
-
3111
- original_name = product_to_edit.get('name', 'N/A')
3112
-
3113
- product_to_edit['name'] = request.form.get('name', product_to_edit['name']).strip()
3114
- price_str = request.form.get('price', str(product_to_edit['price'])).replace(',', '.')
3115
- product_to_edit['description'] = request.form.get('description', product_to_edit.get('description', '')).strip()
3116
- category = request.form.get('category')
3117
- product_to_edit['category'] = category if category in categories else 'Без категории'
3118
- product_to_edit['colors'] = [c.strip() for c in request.form.getlist('colors') if c.strip()]
3119
- product_to_edit['in_stock'] = 'in_stock' in request.form
3120
- product_to_edit['is_top'] = 'is_top' in request.form
3121
 
3122
- try:
3123
- price = round(float(price_str), 2)
3124
- if price < 0: price = 0
3125
- product_to_edit['price'] = price
3126
- except ValueError:
3127
- flash(f"Неверный формат цены для товара '{original_name}'. Цена не изменена.", 'warning')
 
 
 
3128
 
3129
  photos_files = request.files.getlist('photos')
3130
- if photos_files and any(f.filename for f in photos_files) and HF_TOKEN_WRITE:
3131
- uploads_dir = 'uploads_temp'
3132
- os.makedirs(uploads_dir, exist_ok=True)
3133
- api = HfApi()
3134
- new_photos_list = []
3135
- photo_limit = 10
3136
- uploaded_count = 0
3137
- for photo in photos_files:
3138
- if uploaded_count >= photo_limit:
3139
- flash(f"Загружено только первые {photo_limit} фото.", "warning")
3140
- break
3141
- if photo and photo.filename:
3142
- try:
3143
- ext = os.path.splitext(photo.filename)[1].lower()
3144
- if ext not in ['.jpg', '.jpeg', '.png', '.gif', '.webp']:
3145
- flash(f"Файл {photo.filename} не является изображением и был пропущен.", "warning")
3146
- continue
3147
-
3148
- safe_name = secure_filename(product_to_edit['name'].replace(' ', '_'))[:50]
3149
- photo_filename = f"{safe_name}_{datetime.now().strftime('%Y%m%d%H%M%S%f')}{ext}"
3150
- temp_path = os.path.join(uploads_dir, photo_filename)
3151
- photo.save(temp_path)
3152
- api.upload_file(path_or_fileobj=temp_path, path_in_repo=f"photos/{photo_filename}",
3153
- repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE,
3154
- commit_message=f"Update photo for product {product_to_edit['name']}")
3155
- new_photos_list.append(photo_filename)
3156
- os.remove(temp_path)
3157
- uploaded_count += 1
3158
- except Exception as e:
3159
- flash(f"Ошибка при загрузке нового фото {photo.filename}.", 'error')
3160
- if os.path.exists(temp_path):
3161
- try: os.remove(temp_path)
3162
- except OSError: pass
3163
- try:
3164
- if os.path.exists(uploads_dir) and not os.listdir(uploads_dir):
3165
- os.rmdir(uploads_dir)
3166
- except OSError as e:
3167
- pass
3168
-
3169
- if new_photos_list:
3170
- old_photos = product_to_edit.get('photos', [])
3171
- if old_photos:
3172
  try:
3173
- api = HfApi()
3174
- api.delete_files(
3175
- repo_id=REPO_ID,
3176
- paths_in_repo=[f"photos/{p}" for p in old_photos],
3177
- repo_type="dataset",
3178
- token=HF_TOKEN_WRITE,
3179
- commit_message=f"Delete old photos for product {product_to_edit['name']}"
3180
- )
3181
- except Exception as e:
3182
- flash("Не удалось удалить старые фотографии с сервера. Новые фото загружены.", "warning")
3183
- product_to_edit['photos'] = new_photos_list
3184
- flash("Фотографии товара успешно обновлены.", "success")
3185
- elif uploaded_count == 0 and any(f.filename for f in photos_files):
3186
- flash("Не удалось загрузить новые фотографии (возможно, неверный формат).", "error")
3187
- elif not HF_TOKEN_WRITE and photos_files and any(f.filename for f in photos_files):
3188
- flash("HF_TOKEN (write) не настроен. Фотографии не были обновлены.", "warning")
3189
-
 
3190
  save_env_data(env_id, data)
3191
- flash(f"Товар '{product_to_edit['name']}' успешно обновлен.", 'success')
3192
 
3193
  elif action == 'delete_product':
3194
  product_id = request.form.get('product_id')
 
1
+
2
  import os
3
  import io
4
  import base64
 
242
  if 'product_id' not in product:
243
  product['product_id'] = uuid4().hex
244
  products_changed = True
245
+ if 'sizes' not in product:
246
+ product['sizes'] = []
247
+ products_changed = True
248
+ if 'variant_prices' not in product:
249
+ product['variant_prices'] = {}
250
+ products_changed = True
251
 
252
  if products_changed or settings_changed:
253
  save_env_data(env_id, env_data)
 
346
  for p in products:
347
  if p.get('in_stock', True):
348
  price_display = f"{p.get('price', 0):.2f}".replace('.00', '')
349
+
350
+ colors_str = f"Цвета: {', '.join(p.get('colors', []))}" if p.get('colors') else ""
351
+ sizes_str = f"Размеры: {', '.join(p.get('sizes', []))}" if p.get('sizes') else ""
352
+
353
+ options_str = ""
354
+ if colors_str or sizes_str:
355
+ options_str = f", Варианты: ({' '.join(filter(None, [colors_str, sizes_str]))})"
356
+
357
+ variant_prices_str = ""
358
+ if p.get('variant_prices'):
359
+ variant_prices_list = [f"{k.replace('-', ' ')} - {v} {currency_code}" for k, v in p['variant_prices'].items()]
360
+ if variant_prices_list:
361
+ variant_prices_str = f", Особые цены: [{'; '.join(variant_prices_list)}]"
362
+
363
+ product_info_list.append(
364
+ f"- [ID_ТОВАРА: {p.get('product_id', 'N/A')} Название: {p.get('name', 'Без названия')}], "
365
+ f"Категория: {p.get('category', 'Без категории')}, "
366
+ f"Базовая цена: {price_display} {currency_code}"
367
+ f"{options_str}{variant_prices_str}, "
368
+ f"Описание: {p.get('description', '')[:100]}..."
369
+ )
370
  product_list_str = "\n".join(product_info_list) if product_info_list else "В данный момент нет товаров в наличии."
371
 
372
  category_list_str = ", ".join(categories) if categories else "Категорий пока нет."
 
385
 
386
 
387
  system_instruction_content = (
388
+ f"Ты первоклассный виртуальный консультант-продажник по имени {chat_name} для магазина {org_name}. Твоя главная цель — не просто отвечать на вопросы, а продавать, используя все свои навыки. "
389
+ "Говори на любом языке, на котором к тебе обращается клиент. Будь энергичным, убедительным и проактивным. "
390
+ "Твоя задача помогать пользователям находить товары, мастерски отвечать на вопросы о них, предлагать лучшие варианты, создавать ценность и закрывать сделку. "
391
+ "Всегда будь вежлив, но настойчив. Твоя речь должна быть живой, с использованием эмодзи, чтобы располагать к себе клиента. "
392
+ "Никогда не выдумывай товары, категории или характеристики, которых нет в предоставленных списках. "
393
+ "Когда ты предлагаешь товар, всегда указывай его название и ID, используя *точный формат*: [ID_ТОВАРА: <product_id> Название: <product_name>]. Это *критически важно* для клиента. "
394
+ "Если пользователь спрашивает цену на конкретный вариант (цвет или размер), найди ее в 'Особые цены'. Если там нет, используй 'Базовая цена'. "
395
+ "Активно предлагай сопутствующие товары или более дорогие аналоги (апсейл). Например: 'Отличный выбор! К этому телефону идеально подойдут наши новые беспроводные наушники. Хотите взглянуть?'. "
396
+ "Создавай ощущение срочности: 'Эта модель сейчас очень популярна, осталось всего несколько штук!'. "
397
+ "Работай с возражениями: если клиент говорит 'дорого', расскажи о качестве, гарантии и уникальных особенностях товара.\n\n"
398
  f"Список доступных категорий: {category_list_str}.\n\n"
399
+ f"Список доступных товаров в магазине (используй эту информацию для ответов):\n"
400
  f"{product_list_str}"
401
  f"{org_info_str}\n\n"
402
+ "Если пользователь спрашивает про товары, которых нет, вежливо сообщи об этом и немедленно предложи лучшую альтернативу из имеющихся. "
403
+ "Если вопрос касается общей информации о магазине (доставка, возврат), используй данные из блока 'Информация о магазине' и сразу после ответа возвращай разговор к покупкам: 'Кстати, я могу помочь вам подобрать что-нибудь еще?'. "
404
+ "Твоя конечная цель довольный клиент, который совершил покупку. Действуй!"
 
405
  )
406
 
407
+
408
  generated_text = ""
409
  response = None
410
 
 
846
  .cart-item img { width: 60px; height: 60px; object-fit: cover; border-radius: 8px; }
847
  .cart-item-details { grid-column: 2; }
848
  .cart-item-details strong { display: block; margin-bottom: 5px; font-size: 1rem; color: var(--text-dark);}
849
+ .cart-item-details .variant-info { font-size: 0.85rem; color: #666; }
850
  .cart-item-price { font-size: 0.9rem; color: #666; }
851
  .dark-theme .cart-item-price { color: #ccc; }
852
  .cart-item-quantity { display: flex; align-items: center; gap: 8px; grid-column: 3;}
 
855
  .cart-item-total { font-weight: bold; text-align: right; grid-column: 4; font-size: 1rem; color: var(--bg-medium);}
856
  .cart-item-remove { grid-column: 5; background:none; border:none; color: var(--danger); cursor:pointer; font-size: 1.3em; padding: 5px; line-height: 1; }
857
  .cart-item-remove:hover { color: var(--danger-hover); }
858
+ .quantity-input, .options-select { width: 100%; max-width: 180px; padding: 10px; border: 1px solid #e0e0e0; border-radius: 8px; font-size: 1rem; margin: 10px 0; box-sizing: border-box; }
859
+ .dark-theme .quantity-input, .dark-theme .options-select { background-color: #333; color: #fff; border-color: #555; }
860
+ .quantity-input:focus, .options-select:focus { border-color: var(--accent); outline: none; box-shadow: 0 0 0 2px rgba(72, 209, 204, 0.2); }
861
  .cart-summary { margin-top: 20px; text-align: right; border-top: 1px solid #e0e0e0; padding-top: 15px; }
862
  .dark-theme .cart-summary { border-top-color: #444; }
863
  .cart-summary strong { font-size: 1.2rem; color: var(--bg-medium);}
 
949
  <div id="quantityModal" class="modal">
950
  <div class="modal-content">
951
  <span class="close" onclick="closeModal('quantityModal')" aria-label="Закрыть">×</span>
952
+ <h2>Укажите опции и количество</h2>
953
+ <div id="quantityModalOptions"></div>
954
  <label for="quantityInput">Количество:</label>
955
  <input type="number" id="quantityInput" class="quantity-input" min="1" value="1">
 
 
956
  <button class="product-button formulate-order-button" style="width:100%;" onclick="confirmAddToCart()"><i class="fas fa-check"></i> Добавить в корзину</button>
957
  </div>
958
  </div>
 
1044
  .then(data => {
1045
  modalContent.innerHTML = data;
1046
  initializeSwiper();
1047
+ attachOptionListeners();
1048
  })
1049
  .catch(error => {
1050
  console.error('Ошибка загрузки деталей продукта:', error);
1051
  modalContent.innerHTML = `<p style="color: var(--danger); text-align:center; padding: 40px;">Не удалось загрузить информацию о товаре. ${error.message}</p>`;
1052
  });
1053
  }
1054
+
1055
+ function attachOptionListeners() {
1056
+ const product = getProductById(selectedProductId);
1057
+ if (!product) return;
1058
+ const priceEl = document.getElementById('variantPrice');
1059
+ const colorSelect = document.getElementById('colorSelect');
1060
+ const sizeSelect = document.getElementById('sizeSelect');
1061
+
1062
+ const updatePrice = () => {
1063
+ if (!priceEl) return;
1064
+ let color = colorSelect ? colorSelect.value : null;
1065
+ let size = sizeSelect ? sizeSelect.value : null;
1066
+ let key = [color, size].filter(Boolean).join('-');
1067
+
1068
+ let currentPrice = product.price;
1069
+ if (key && product.variant_prices && product.variant_prices[key]) {
1070
+ currentPrice = product.variant_prices[key];
1071
+ }
1072
+ priceEl.textContent = `${parseFloat(currentPrice).toFixed(0)} ${currencyCode}`;
1073
+ };
1074
+
1075
+ if (colorSelect) colorSelect.addEventListener('change', updatePrice);
1076
+ if (sizeSelect) sizeSelect.addEventListener('change', updatePrice);
1077
+ }
1078
 
1079
  function initializeSwiper() {
1080
  const swiperContainer = document.querySelector('#productModal .swiper-container');
 
1096
  selectedProductId = productId;
1097
  const product = getProductById(productId);
1098
  if (!product) {
 
1099
  alert("Ошибка: товар не найден.");
1100
  return;
1101
  }
1102
+ const optionsContainer = document.getElementById('quantityModalOptions');
1103
+ optionsContainer.innerHTML = '';
1104
+
1105
+ if (product.colors && product.colors.length > 0) {
1106
+ let colorHtml = '<label for="qColorSelect">Цвет/Вариант:</label><select id="qColorSelect" class="options-select">';
1107
+ product.colors.forEach(c => colorHtml += `<option value="${c}">${c}</option>`);
1108
+ colorHtml += '</select>';
1109
+ optionsContainer.innerHTML += colorHtml;
 
 
 
 
 
 
 
 
1110
  }
1111
+ if (product.sizes && product.sizes.length > 0) {
1112
+ let sizeHtml = '<label for="qSizeSelect">Размер/Объем:</label><select id="qSizeSelect" class="options-select">';
1113
+ product.sizes.forEach(s => sizeHtml += `<option value="${s}">${s}</option>`);
1114
+ sizeHtml += '</select>';
1115
+ optionsContainer.innerHTML += sizeHtml;
1116
+ }
1117
+
1118
  document.getElementById('quantityInput').value = 1;
1119
  const modal = document.getElementById('quantityModal');
1120
  if(modal) {
 
1127
  if (selectedProductId === null) return;
1128
  const quantityInput = document.getElementById('quantityInput');
1129
  const quantity = parseInt(quantityInput.value);
1130
+
1131
+ const colorSelect = document.getElementById('qColorSelect');
1132
+ const sizeSelect = document.getElementById('qSizeSelect');
1133
+
1134
+ const color = colorSelect ? colorSelect.value : 'N/A';
1135
+ const size = sizeSelect ? sizeSelect.value : 'N/A';
1136
+
1137
  if (isNaN(quantity) || quantity <= 0) {
1138
  alert("Пожалуйста, укажите корректное количество (больше 0).");
1139
  quantityInput.focus();
 
1144
  alert("Ошибка добавления: товар не найден.");
1145
  return;
1146
  }
1147
+
1148
+ let price = product.price;
1149
+ let variantKey = [color, size].filter(v => v !== 'N/A').join('-');
1150
+ if (variantKey && product.variant_prices && product.variant_prices[variantKey]) {
1151
+ price = product.variant_prices[variantKey];
1152
+ }
1153
+
1154
+ const cartItemId = `${product.product_id}-${color}-${size}`;
1155
  const existingItemIndex = cart.findIndex(item => item.id === cartItemId);
1156
  if (existingItemIndex > -1) {
1157
  cart[existingItemIndex].quantity += quantity;
 
1160
  id: cartItemId,
1161
  product_id: product.product_id,
1162
  name: product.name,
1163
+ price: price,
1164
  photo: product.photos && product.photos.length > 0 ? product.photos[0] : null,
1165
  quantity: quantity,
1166
+ color: color,
1167
+ size: size
1168
  });
1169
  }
1170
  localStorage.setItem(`mekaCart_${envId}`, JSON.stringify(cart));
 
1203
  const photoUrl = item.photo
1204
  ? `https://huggingface.co/datasets/${repoId}/resolve/main/photos/${item.photo}`
1205
  : 'https://via.placeholder.com/60x60.png?text=N/A';
1206
+
1207
+ let variantInfo = [];
1208
+ if (item.color && item.color !== 'N/A') variantInfo.push(`Цвет: ${item.color}`);
1209
+ if (item.size && item.size !== 'N/A') variantInfo.push(`Размер: ${item.size}`);
1210
+
1211
  return `
1212
  <div class="cart-item">
1213
  <img src="${photoUrl}" alt="${item.name}">
1214
  <div class="cart-item-details">
1215
+ <strong>${item.name}</strong>
1216
+ <p class="variant-info">${variantInfo.join(', ')}</p>
1217
  <p class="cart-item-price">${item.price.toFixed(2)} ${currencyCode}</p>
1218
  </div>
1219
  <div class="cart-item-quantity">
 
1554
  body.dark-theme .quantity-btn { background-color: #444; border-color: #555; color: #fff; }
1555
  .cart-item-total { font-weight: bold; text-align: right; font-size: 1rem; color: var(--bg-medium);}
1556
  .cart-item-remove { background:none; border:none; color: var(--danger); cursor:pointer; font-size: 1.3em; }
1557
+ .quantity-input, .options-select { width: 100%; max-width: 180px; padding: 10px; border: 1px solid #e0e0e0; border-radius: 8px; font-size: 1rem; margin: 10px 0; }
1558
+ body.dark-theme .quantity-input, body.dark-theme .options-select { background-color: #333; color: #fff; border-color: #555; }
1559
  .cart-summary { margin-top: 20px; text-align: right; border-top: 1px solid #e0e0e0; padding-top: 15px; }
1560
  body.dark-theme .cart-summary { border-top-color: #444; }
1561
  .cart-actions { margin-top: 25px; display: flex; justify-content: space-between; }
 
1607
  <div id="quantityModal" class="modal">
1608
  <div class="modal-content">
1609
  <span class="close" onclick="closeModal('quantityModal')">×</span>
1610
+ <h2>Укажите опции и количество</h2>
1611
+ <div id="quantityModalOptions"></div>
1612
+ <label for="quantityInput">Количество:</label>
1613
  <input type="number" id="quantityInput" class="quantity-input" min="1" value="1">
 
1614
  <button class="product-button formulate-order-button" onclick="confirmAddToCart()">Добавить в корзину</button>
1615
  </div>
1616
  </div>
 
1682
  selectedProductId = productId;
1683
  const product = getProductById(productId);
1684
  if (!product) return;
1685
+ const optionsContainer = document.getElementById('quantityModalOptions');
1686
+ optionsContainer.innerHTML = '';
1687
+
1688
+ if (product.colors && product.colors.length > 0) {
1689
+ let colorHtml = '<label for="qColorSelect">Цвет/Вариант:</label><select id="qColorSelect" class="options-select">';
1690
+ product.colors.forEach(c => colorHtml += `<option value="${c}">${c}</option>`);
1691
+ colorHtml += '</select>';
1692
+ optionsContainer.innerHTML += colorHtml;
1693
+ }
1694
+ if (product.sizes && product.sizes.length > 0) {
1695
+ let sizeHtml = '<label for="qSizeSelect">Размер/Объем:</label><select id="qSizeSelect" class="options-select">';
1696
+ product.sizes.forEach(s => sizeHtml += `<option value="${s}">${s}</option>`);
1697
+ sizeHtml += '</select>';
1698
+ optionsContainer.innerHTML += sizeHtml;
1699
  }
1700
  document.getElementById('quantityInput').value = 1;
1701
  document.getElementById('quantityModal').style.display = "block";
1702
  }
1703
  function confirmAddToCart() {
1704
  const quantity = parseInt(document.getElementById('quantityInput').value);
1705
+ const colorSelect = document.getElementById('qColorSelect');
1706
+ const sizeSelect = document.getElementById('qSizeSelect');
1707
+
1708
+ const color = colorSelect ? colorSelect.value : 'N/A';
1709
+ const size = sizeSelect ? sizeSelect.value : 'N/A';
1710
+
1711
  if (isNaN(quantity) || quantity <= 0) {
1712
  alert("Укажите корректное количество.");
1713
  return;
1714
  }
1715
  const product = getProductById(selectedProductId);
1716
+
1717
+ let price = product.price;
1718
+ let variantKey = [color, size].filter(v => v !== 'N/A').join('-');
1719
+ if (variantKey && product.variant_prices && product.variant_prices[variantKey]) {
1720
+ price = product.variant_prices[variantKey];
1721
+ }
1722
+
1723
+ const cartItemId = `${product.product_id}-${color}-${size}`;
1724
  const existingItem = cart.find(item => item.id === cartItemId);
1725
  if (existingItem) {
1726
  existingItem.quantity += quantity;
1727
  } else {
1728
+ cart.push({ id: cartItemId, product_id: product.product_id, name: product.name, price: price, photo: product.photos ? product.photos[0] : null, quantity, color, size });
1729
  }
1730
  localStorage.setItem(`mekaCart_${envId}`, JSON.stringify(cart));
1731
  closeModal('quantityModal');
 
1756
  cartContent.innerHTML = cart.map(item => {
1757
  const itemTotal = item.price * item.quantity;
1758
  total += itemTotal;
1759
+ let variantInfo = [];
1760
+ if (item.color && item.color !== 'N/A') variantInfo.push(`Цвет: ${item.color}`);
1761
+ if (item.size && item.size !== 'N/A') variantInfo.push(`Размер: ${item.size}`);
1762
  return `<div class="cart-item">
1763
  <img src="${item.photo ? `https://huggingface.co/datasets/${repoId}/resolve/main/photos/${item.photo}` : ''}" alt="${item.name}">
1764
+ <div><strong>${item.name}</strong><p>${variantInfo.join(', ')}</p><p>${item.price.toFixed(2)} ${currencyCode}</p></div>
1765
  <div class="cart-item-quantity"><button class="quantity-btn" onclick="decrementCartItem('${item.id}')">-</button><span>${item.quantity}</span><button class="quantity-btn" onclick="incrementCartItem('${item.id}')">+</button></div>
1766
  <span class="cart-item-total">${itemTotal.toFixed(2)}</span>
1767
  <button class="cart-item-remove" onclick="removeFromCart('${item.id}')"><i class="fas fa-trash-alt"></i></button>
 
1931
  '''
1932
 
1933
  PRODUCT_DETAIL_TEMPLATE = '''
1934
+ <div style="padding: 10px;" data-product-id="{{ product.get('product_id') }}">
1935
  <h2 style="font-size: 1.6rem; font-weight: 600; margin-bottom: 15px; text-align: center; color: #135D66;">{{ product['name'] }}</h2>
1936
  <div class="swiper-container" style="max-width: 450px; margin: 0 auto 20px; border-radius: 10px; overflow: hidden; background-color: #fff; border: 1px solid #e0e0e0;">
1937
  <div class="swiper-wrapper">
 
1957
  <div class="swiper-button-prev" style="color: #135D66;"></div>
1958
  {% endif %}
1959
  </div>
1960
+
1961
+ <div style="margin-top: 20px; padding: 0 10px;">
1962
+ {% if product.get('colors') and product.colors|select('ne', '')|list|length > 0 %}
1963
+ <div style="margin-bottom: 15px;">
1964
+ <label for="colorSelect" style="display: block; margin-bottom: 5px; font-weight: 500;">Цвет/Вариант:</label>
1965
+ <select id="colorSelect" class="options-select">
1966
+ {% for color in product.colors|select('ne', '')|list %}
1967
+ <option value="{{ color }}">{{ color }}</option>
1968
+ {% endfor %}
1969
+ </select>
1970
+ </div>
1971
+ {% endif %}
1972
+ {% if product.get('sizes') and product.sizes|select('ne', '')|list|length > 0 %}
1973
+ <div style="margin-bottom: 15px;">
1974
+ <label for="sizeSelect" style="display: block; margin-bottom: 5px; font-weight: 500;">Размер/Объем:</label>
1975
+ <select id="sizeSelect" class="options-select">
1976
+ {% for size in product.sizes|select('ne', '')|list %}
1977
+ <option value="{{ size }}">{{ size }}</option>
1978
+ {% endfor %}
1979
+ </select>
1980
+ </div>
1981
+ {% endif %}
1982
+ </div>
1983
 
1984
  <div style="text-align:center; margin-top:20px; padding: 0 10px;">
1985
+ <p style="font-size: 1.5rem; font-weight: bold; color: #135D66; margin-bottom: 15px;"><strong>Цена:</strong> <span id="variantPrice">{{ "%.0f"|format(product.price) }} {{ currency_code }}</span></p>
1986
  <button class="product-button formulate-order-button" style="padding: 12px 30px; width: 100%; max-width: 300px;" onclick="closeModal('productModal'); openQuantityModalById('{{ product.get('product_id', '') }}')">
1987
  <i class="fas fa-cart-plus"></i> В корзину
1988
  </button>
 
1991
  <div style="margin-top: 20px; font-size: 1rem; line-height: 1.7; color: #333; padding: 0 10px;">
1992
  <p><strong>Категория:</strong> {{ product.get('category', 'Без категории') }}</p>
1993
  <p><strong>Описание:</strong><br> {{ product.get('description', 'Описание отсутствует.')|replace('\\n', '<br>')|safe }}</p>
 
 
 
 
1994
  </div>
1995
  </div>
1996
+ <script>
1997
+ selectedProductId = '{{ product.get("product_id") }}';
1998
+ attachOptionListeners();
1999
+ </script>
2000
  '''
2001
 
2002
  ORDER_TEMPLATE = '''
 
2082
  container.innerHTML = '<p style="text-align:center; padding: 20px;">Заказ пуст.</p>';
2083
  document.querySelector('.actions .button').disabled = true;
2084
  } else {
2085
+ container.innerHTML = order.cart.map((item, index) => {
2086
+ let variantInfo = [];
2087
+ if (item.color && item.color !== 'N/A') variantInfo.push(`Цвет: ${item.color}`);
2088
+ if (item.size && item.size !== 'N/A') variantInfo.push(`Размер: ${item.size}`);
2089
+
2090
+ return `
2091
  <div class="order-item">
2092
  <img src="${item.photo_url}" alt="${item.name}">
2093
  <div class="item-details">
2094
+ <strong>${item.name}</strong>
2095
+ <span>${variantInfo.join(', ')}</span>
2096
  <span>${item.price.toFixed(2)} {{ currency_code }}</span>
2097
  </div>
2098
  <div class="item-quantity">
 
2104
  ${(item.price * item.quantity).toFixed(2)} {{ currency_code }}
2105
  </div>
2106
  </div>
2107
+ `}).join('');
2108
  document.querySelector('.actions .button').disabled = false;
2109
  }
2110
  updateOrderTotal();
 
2150
  message += `*Номер заказа:* ${orderId}%0A%0A`;
2151
 
2152
  order.cart.forEach(item => {
2153
+ let variantInfo = [];
2154
+ if (item.color && item.color !== 'N/A') variantInfo.push(item.color);
2155
+ if (item.size && item.size !== 'N/A') variantInfo.push(item.size);
2156
+ let variantText = variantInfo.length > 0 ? ` (${variantInfo.join(', ')})` : '';
2157
+
2158
+ message += `*${item.name}*${variantText}%0A`;
2159
  message += ` - Количество: ${item.quantity}%0A`;
2160
  message += ` - Цена: ${item.price.toFixed(2)} {{ currency_code }}%0A`;
2161
  });
 
2239
  details[open] > summary::after { transform: translateY(-50%) rotate(180deg); }
2240
  details[open] > summary { border-bottom: 1px solid #e0e0e0; }
2241
  details .form-content { padding: 20px; }
2242
+ .option-input-group { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
2243
+ .option-input-group input { flex-grow: 1; margin: 0; }
2244
+ .remove-option-btn { background-color: var(--danger); color: white; padding: 6px 10px; font-size: 0.8rem; margin-top: 0; line-height: 1; }
2245
+ .remove-option-btn:hover { background-color: var(--danger-hover); }
2246
+ .add-option-btn { background-color: #B2DFDB; color: var(--bg-medium); border: 1px solid #e0e0e0; }
2247
+ .add-option-btn:hover { background-color: var(--bg-medium); color: white; border-color: var(--bg-medium); }
2248
  .photo-preview img { max-width: 70px; max-height: 70px; border-radius: 5px; margin: 5px 5px 0 0; border: 1px solid #e0e0e0; object-fit: cover;}
2249
  .sync-buttons { display: flex; gap: 10px; margin-bottom: 20px; flex-wrap: wrap; }
2250
  .download-hf-button { background-color: #6c757d; color: white; }
 
2273
  .chat-message.user .bubble { background-color: #dcf8c6; }
2274
  .chat-message.ai .bubble { background-color: #f1f1f1; }
2275
  .current-avatar { max-width: 60px; max-height: 60px; border-radius: 50%; vertical-align: middle; margin-left: 10px; border: 2px solid var(--bg-medium);}
2276
+ .variant-price-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 10px; background-color: #f7f9fa; padding: 10px; border-radius: 5px; margin-top: 10px; border: 1px solid #e0e0e0; }
2277
+ .variant-price-item { display: flex; align-items: center; gap: 8px; font-size: 0.9em; }
2278
+ .variant-price-item label { margin-top: 0; white-space: nowrap; }
2279
+ .variant-price-item input { margin-top: 0; }
2280
  </style>
2281
  </head>
2282
  <body>
 
2455
  <input type="hidden" name="action" value="add_product">
2456
  <label for="add_name">Название товара *:</label>
2457
  <input type="text" id="add_name" name="name" required>
2458
+ <label for="add_price">Базовая Цена ({{ currency_code }}) *:</label>
2459
  <input type="number" id="add_price" name="price" step="0.01" min="0" required>
2460
  <label for="add_photos">Фотографии (до 10 шт.):</label>
2461
  <input type="file" id="add_photos" name="photos" accept="image/*" multiple>
 
2477
  {% endfor %}
2478
  </select>
2479
  <label>Цвета/Варианты (оставьте пустым, если нет):</label>
2480
+ <div id="add-color-inputs" class="option-inputs" data-type="color">
2481
+ <div class="option-input-group">
2482
  <input type="text" name="colors" placeholder="Например: Розовый">
2483
+ <button type="button" class="remove-option-btn" onclick="removeOptionInput(this)"><i class="fas fa-times"></i></button>
2484
  </div>
2485
  </div>
2486
+ <button type="button" class="button add-option-btn" style="margin-top: 5px;" onclick="addOptionInput('add-color-inputs', 'colors')"><i class="fas fa-palette"></i> Добавить поле</button>
2487
+
2488
+ <label style="margin-top: 15px;">Размеры/Объем (оставьте пустым, если нет):</label>
2489
+ <div id="add-size-inputs" class="option-inputs" data-type="size">
2490
+ <div class="option-input-group">
2491
+ <input type="text" name="sizes" placeholder="Например: 42 или 50ml">
2492
+ <button type="button" class="remove-option-btn" onclick="removeOptionInput(this)"><i class="fas fa-times"></i></button>
2493
+ </div>
2494
+ </div>
2495
+ <button type="button" class="button add-option-btn" style="margin-top: 5px;" onclick="addOptionInput('add-size-inputs', 'sizes')"><i class="fas fa-ruler-combined"></i> Добавить поле</button>
2496
+
2497
+ <h3 style="margin-top: 20px;">Особые цены для вариантов</h3>
2498
+ <p style="font-size:0.85em; color:#666;">Здесь можно указать цену для конкретного сочетания цвета и размера. Если цена не указана, будет использоваться базовая.</p>
2499
+ <div id="add-variant-prices-container" class="variant-price-grid"></div>
2500
+
2501
  <br>
2502
  <div style="margin-top: 15px;">
2503
  <input type="checkbox" id="add_in_stock" name="in_stock" checked>
 
2544
  <p><strong>Цена:</strong> {{ "%.2f"|format(product.price) }} {{ currency_code }}</p>
2545
  <p class="description" title="{{ product.get('description', '') }}"><strong>Описание:</strong> {{ product.get('description', 'N/A')[:150] }}{% if product.get('description', '')|length > 150 %}...{% endif %}</p>
2546
  {% set colors = product.get('colors', []) %}
2547
+ {% set sizes = product.get('sizes', []) %}
2548
  <p><strong>Цвета/Вар-ты:</strong> {{ colors|select('ne', '')|join(', ') if colors|select('ne', '')|list|length > 0 else 'Нет' }}</p>
2549
+ <p><strong>Размеры/Объем:</strong> {{ sizes|select('ne', '')|join(', ') if sizes|select('ne', '')|list|length > 0 else 'Нет' }}</p>
2550
+
2551
  {% if product.get('photos') and product['photos']|length > 1 %}
2552
  <p style="font-size: 0.8rem; color: #999;">(Всего фото: {{ product['photos']|length }})</p>
2553
  {% endif %}
 
2600
  {% endfor %}
2601
  </select>
2602
  <label>Цвета/Варианты:</label>
2603
+ <div id="edit-color-inputs-{{ loop.index0 }}" class="option-inputs" data-type="color">
2604
  {% set current_colors = product.get('colors', []) %}
2605
  {% if current_colors and current_colors|select('ne', '')|list|length > 0 %}
2606
  {% for color in current_colors %}
2607
  {% if color.strip() %}
2608
+ <div class="option-input-group">
2609
  <input type="text" name="colors" value="{{ color }}">
2610
+ <button type="button" class="remove-option-btn" onclick="removeOptionInput(this)"><i class="fas fa-times"></i></button>
2611
  </div>
2612
  {% endif %}
2613
  {% endfor %}
2614
  {% else %}
2615
+ <div class="option-input-group">
2616
  <input type="text" name="colors" placeholder="Например: Цвет">
2617
+ <button type="button" class="remove-option-btn" onclick="removeOptionInput(this)"><i class="fas fa-times"></i></button>
2618
  </div>
2619
  {% endif %}
2620
  </div>
2621
+ <button type="button" class="button add-option-btn" style="margin-top: 5px;" onclick="addOptionInput('edit-color-inputs-{{ loop.index0 }}', 'colors')"><i class="fas fa-palette"></i> Добавить поле</button>
2622
+
2623
+ <label style="margin-top: 15px;">Размеры/Объем:</label>
2624
+ <div id="edit-size-inputs-{{ loop.index0 }}" class="option-inputs" data-type="size">
2625
+ {% set current_sizes = product.get('sizes', []) %}
2626
+ {% if current_sizes and current_sizes|select('ne', '')|list|length > 0 %}
2627
+ {% for size in current_sizes %}
2628
+ {% if size.strip() %}
2629
+ <div class="option-input-group">
2630
+ <input type="text" name="sizes" value="{{ size }}">
2631
+ <button type="button" class="remove-option-btn" onclick="removeOptionInput(this)"><i class="fas fa-times"></i></button>
2632
+ </div>
2633
+ {% endif %}
2634
+ {% endfor %}
2635
+ {% else %}
2636
+ <div class="option-input-group">
2637
+ <input type="text" name="sizes" placeholder="Например: L">
2638
+ <button type="button" class="remove-option-btn" onclick="removeOptionInput(this)"><i class="fas fa-times"></i></button>
2639
+ </div>
2640
+ {% endif %}
2641
+ </div>
2642
+ <button type="button" class="button add-option-btn" style="margin-top: 5px;" onclick="addOptionInput('edit-size-inputs-{{ loop.index0 }}', 'sizes')"><i class="fas fa-ruler-combined"></i> Добавить поле</button>
2643
+
2644
+ <h3 style="margin-top: 20px;">Особые цены для вариантов</h3>
2645
+ <div id="edit-variant-prices-container-{{ loop.index0 }}" class="variant-price-grid"></div>
2646
+
2647
  <br>
2648
  <div style="margin-top: 15px;">
2649
  <input type="checkbox" id="edit_in_stock_{{ loop.index0 }}" name="in_stock" {% if product.get('in_stock', True) %}checked{% endif %}>
 
2674
  </div>
2675
 
2676
  <script>
2677
+ const allProductsForAdmin = {{ products|tojson|safe }};
2678
+
2679
  function toggleEditForm(formId) {
2680
  const formContainer = document.getElementById(formId);
2681
  if (formContainer) {
2682
+ const isOpening = formContainer.style.display === 'none' || formContainer.style.display === '';
2683
+ formContainer.style.display = isOpening ? 'block' : 'none';
2684
+ if (isOpening) {
2685
+ const index = parseInt(formId.split('-').pop());
2686
+ const product = allProductsForAdmin[index];
2687
+ const priceContainer = document.getElementById(`edit-variant-prices-container-${index}`);
2688
+ const colorContainer = document.getElementById(`edit-color-inputs-${index}`);
2689
+ const sizeContainer = document.getElementById(`edit-size-inputs-${index}`);
2690
+
2691
+ const updateVariantPrices = () => generateVariantPriceInputs(colorContainer, sizeContainer, priceContainer, product.variant_prices || {});
2692
+
2693
+ colorContainer.addEventListener('input', updateVariantPrices);
2694
+ sizeContainer.addEventListener('input', updateVariantPrices);
2695
+ updateVariantPrices();
2696
+ }
2697
+ }
2698
+ }
2699
+
2700
+ function generateVariantPriceInputs(colorContainer, sizeContainer, priceContainer, existingPrices) {
2701
+ priceContainer.innerHTML = '';
2702
+ const colors = Array.from(colorContainer.querySelectorAll('input[name="colors"]')).map(i => i.value.trim()).filter(Boolean);
2703
+ const sizes = Array.from(sizeContainer.querySelectorAll('input[name="sizes"]')).map(i => i.value.trim()).filter(Boolean);
2704
+
2705
+ if (colors.length === 0 && sizes.length > 0) {
2706
+ sizes.forEach(size => {
2707
+ const key = size;
2708
+ const price = existingPrices[key] || '';
2709
+ priceContainer.innerHTML += `
2710
+ <div class="variant-price-item">
2711
+ <label for="variant_price_${key}">${size}:</label>
2712
+ <input type="number" step="0.01" name="variant_price_${key}" value="${price}" placeholder="Базовая цена">
2713
+ </div>`;
2714
+ });
2715
+ } else if (colors.length > 0 && sizes.length === 0) {
2716
+ colors.forEach(color => {
2717
+ const key = color;
2718
+ const price = existingPrices[key] || '';
2719
+ priceContainer.innerHTML += `
2720
+ <div class="variant-price-item">
2721
+ <label for="variant_price_${key}">${color}:</label>
2722
+ <input type="number" step="0.01" name="variant_price_${key}" value="${price}" placeholder="Базовая цена">
2723
+ </div>`;
2724
+ });
2725
+ } else if (colors.length > 0 && sizes.length > 0) {
2726
+ colors.forEach(color => {
2727
+ sizes.forEach(size => {
2728
+ const key = `${color}-${size}`;
2729
+ const price = existingPrices[key] || '';
2730
+ priceContainer.innerHTML += `
2731
+ <div class="variant-price-item">
2732
+ <label for="variant_price_${key}">${color} - ${size}:</label>
2733
+ <input type="number" step="0.01" name="variant_price_${key}" value="${price}" placeholder="Базовая цена">
2734
+ </div>`;
2735
+ });
2736
+ });
2737
  }
2738
  }
2739
 
2740
+ function addOptionInput(containerId, name) {
2741
  const container = document.getElementById(containerId);
2742
  if (container) {
2743
  const newInputGroup = document.createElement('div');
2744
+ newInputGroup.className = 'option-input-group';
2745
  newInputGroup.innerHTML = `
2746
+ <input type="text" name="${name}" placeholder="Новый вариант">
2747
+ <button type="button" class="remove-option-btn" onclick="removeOptionInput(this)"><i class="fas fa-times"></i></button>
2748
  `;
2749
  container.appendChild(newInputGroup);
2750
+ newInputGroup.querySelector(`input[name="${name}"]`).focus();
 
 
 
2751
  }
2752
  }
2753
 
2754
+ function removeOptionInput(button) {
2755
+ const group = button.closest('.option-input-group');
2756
  if (group) {
2757
  const container = group.parentNode;
2758
  group.remove();
2759
  if (container && container.children.length === 0) {
2760
+ const name = container.dataset.type === 'color' ? 'colors' : 'sizes';
2761
+ const placeholder = container.dataset.type === 'color' ? 'Например: Цвет' : 'Например: L';
2762
+ addOptionInput(container.id, name, placeholder);
 
 
 
 
2763
  }
 
 
2764
  }
2765
  }
2766
+
2767
+ document.addEventListener('DOMContentLoaded', () => {
2768
+ const addForm = document.getElementById('add-product-form');
2769
+ const addColorContainer = document.getElementById('add-color-inputs');
2770
+ const addSizeContainer = document.getElementById('add-size-inputs');
2771
+ const addPriceContainer = document.getElementById('add-variant-prices-container');
2772
+
2773
+ const updateAddFormPrices = () => generateVariantPriceInputs(addColorContainer, addSizeContainer, addPriceContainer, {});
2774
+ addColorContainer.addEventListener('input', updateAddFormPrices);
2775
+ addSizeContainer.addEventListener('input', updateAddFormPrices);
2776
+ });
2777
+
2778
 
2779
  async function generateDescription(photoInputId, descriptionTextareaId, languageSelectId) {
2780
  const photoInput = document.getElementById(photoInputId);
 
3102
  "price": price,
3103
  "quantity": quantity,
3104
  "color": item.get('color', 'N/A'),
3105
+ "size": item.get('size', 'N/A'),
3106
  "photo": item.get('photo'),
3107
  "photo_url": f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/photos/{item['photo']}" if item.get('photo') else "https://via.placeholder.com/60x60.png?text=N/A"
3108
  })
 
3247
  save_env_data(env_id, data)
3248
  flash("Настройки магазина и чата успешно обновлены.", 'success')
3249
 
3250
+ elif action == 'add_product' or action == 'edit_product':
3251
+ product_id = request.form.get('product_id')
3252
+ product_data = {}
3253
+ is_edit = action == 'edit_product'
3254
+
3255
+ if is_edit:
3256
+ product_data = next((p for p in products if p.get('product_id') == product_id), None)
3257
+ if not product_data:
3258
+ flash(f"Ошибка: товар с ID {product_id} не найден.", 'error')
3259
+ return redirect(url_for('admin', env_id=env_id))
3260
+
3261
+ product_data['name'] = request.form.get('name', '').strip()
3262
  price_str = request.form.get('price', '').replace(',', '.')
3263
+ product_data['description'] = request.form.get('description', '').strip()
3264
  category = request.form.get('category')
3265
+ product_data['category'] = category if category in categories else 'Без категории'
3266
+ product_data['colors'] = sorted(list(set(c.strip() for c in request.form.getlist('colors') if c.strip())))
3267
+ product_data['sizes'] = sorted(list(set(s.strip() for s in request.form.getlist('sizes') if s.strip())))
3268
+ product_data['in_stock'] = 'in_stock' in request.form
3269
+ product_data['is_top'] = 'is_top' in request.form
3270
 
3271
+ if not product_data['name'] or not price_str:
3272
  flash("Название и цена товара обязательны.", 'error')
3273
  return redirect(url_for('admin', env_id=env_id))
3274
+
3275
  try:
3276
  price = round(float(price_str), 2)
3277
  if price < 0: price = 0
3278
+ product_data['price'] = price
3279
  except ValueError:
3280
+ flash("Неверный формат цены.", 'error')
3281
+ return redirect(url_for('admin', env_id=env_id))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3282
 
3283
+ variant_prices = {}
3284
+ for key, value in request.form.items():
3285
+ if key.startswith('variant_price_') and value:
3286
+ variant_key = key.replace('variant_price_', '')
3287
+ try:
3288
+ variant_prices[variant_key] = round(float(value), 2)
3289
+ except ValueError:
3290
+ pass
3291
+ product_data['variant_prices'] = variant_prices
3292
 
3293
  photos_files = request.files.getlist('photos')
3294
+ if photos_files and any(f.filename for f in photos_files):
3295
+ if HF_TOKEN_WRITE:
3296
+ uploads_dir = 'uploads_temp'
3297
+ os.makedirs(uploads_dir, exist_ok=True)
3298
+ api = HfApi()
3299
+ new_photos_list = []
3300
+ photo_limit = 10
3301
+ uploaded_count = 0
3302
+ for photo in photos_files:
3303
+ if uploaded_count >= photo_limit:
3304
+ flash(f"Загружено только первые {photo_limit} фото.", "warning")
3305
+ break
3306
+ if photo and photo.filename:
3307
+ try:
3308
+ ext = os.path.splitext(photo.filename)[1].lower()
3309
+ if ext not in ['.jpg', '.jpeg', '.png', '.gif', '.webp']:
3310
+ flash(f"Файл {photo.filename} пропущен (не изображение).", "warning")
3311
+ continue
3312
+
3313
+ safe_name = secure_filename(product_data['name'].replace(' ', '_'))[:50]
3314
+ photo_filename = f"{safe_name}_{uuid4().hex[:8]}{ext}"
3315
+ temp_path = os.path.join(uploads_dir, photo_filename)
3316
+ photo.save(temp_path)
3317
+ api.upload_file(path_or_fileobj=temp_path, path_in_repo=f"photos/{photo_filename}", repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE)
3318
+ new_photos_list.append(photo_filename)
3319
+ os.remove(temp_path)
3320
+ uploaded_count += 1
3321
+ except Exception as e:
3322
+ flash(f"Ошибка при загрузке фото {photo.filename}: {e}", 'error')
3323
+ if new_photos_list and is_edit and product_data.get('photos'):
 
 
 
 
 
 
 
 
 
 
 
 
3324
  try:
3325
+ api.delete_files(repo_id=REPO_ID, paths_in_repo=[f"photos/{p}" for p in product_data['photos']], repo_type="dataset", token=HF_TOKEN_WRITE)
3326
+ except Exception: pass
3327
+ if new_photos_list:
3328
+ product_data['photos'] = new_photos_list
3329
+ else:
3330
+ flash("HF_TOKEN (write) не настроен. Фотографии не были загружены.", "warning")
3331
+
3332
+ if is_edit:
3333
+ product_index = next((i for i, p in enumerate(products) if p.get('product_id') == product_id), -1)
3334
+ if product_index != -1:
3335
+ products[product_index] = product_data
3336
+ flash(f"Товар '{product_data['name']}' успешно обновлен.", 'success')
3337
+ else:
3338
+ product_data['product_id'] = uuid4().hex
3339
+ products.append(product_data)
3340
+ flash(f"Товар '{product_data['name']}' успешно добавлен.", 'success')
3341
+
3342
+ data['products'] = products
3343
  save_env_data(env_id, data)
3344
+
3345
 
3346
  elif action == 'delete_product':
3347
  product_id = request.form.get('product_id')