Kgshop commited on
Commit
ee5e2de
·
verified ·
1 Parent(s): 8e50a31

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +242 -77
app.py CHANGED
@@ -21,7 +21,7 @@ DATA_FILE = 'data.json'
21
 
22
  SYNC_FILES = [DATA_FILE]
23
 
24
- REPO_ID = "Kgshop/prazdnik" # Assuming this repo is correct for "Мир праздника"
25
  HF_TOKEN_WRITE = os.getenv("HF_TOKEN")
26
  HF_TOKEN_READ = os.getenv("HF_TOKEN_READ")
27
 
@@ -30,7 +30,7 @@ STORE_ADDRESS = "Рынок Джунхай , 6 проход , 49А контей
30
  CURRENCY_CODE = 'KGS'
31
  CURRENCY_NAME = 'Кыргызский сом'
32
 
33
- WHATSAPP_NUMBER = "996702888188" # Updated WhatsApp number
34
 
35
  DOWNLOAD_RETRIES = 3
36
  DOWNLOAD_DELAY = 5
@@ -149,21 +149,26 @@ def load_data():
149
  if 'products' not in data: data['products'] = []
150
  if 'categories' not in data: data['categories'] = []
151
  if 'orders' not in data: data['orders'] = {}
152
- # Ensure products have 'prices' field for consistency
 
 
 
 
153
  for product in data['products']:
 
 
154
  if 'prices' not in product or not isinstance(product['prices'], list):
155
- if 'price' in product: # Convert old 'price' to new 'prices' structure
156
  product['prices'] = [{'type': 'шт', 'value': product.pop('price')}]
157
  else:
158
  product['prices'] = []
159
- # Ensure prices have type and value and value is float
160
  product['prices'] = [p for p in product['prices'] if isinstance(p, dict) and 'type' in p and 'value' in p]
161
  for p in product['prices']:
162
  try:
163
  p['value'] = round(float(p['value']), 2)
164
  except (ValueError, TypeError):
165
- p['value'] = 0.0 # Default invalid price to 0
166
- if not product['prices']: # Ensure at least one price structure if no valid ones exist
167
  product['prices'] = [{'type': 'шт', 'value': 0.0}]
168
 
169
  return data
@@ -184,10 +189,15 @@ def load_data():
184
  if 'categories' not in data: data['categories'] = []
185
  if 'orders' not in data: data['orders'] = {}
186
 
187
- # Ensure products have 'prices' field for consistency after download
 
 
 
188
  for product in data['products']:
 
 
189
  if 'prices' not in product or not isinstance(product['prices'], list):
190
- if 'price' in product: # Convert old 'price' to new 'prices' structure
191
  product['prices'] = [{'type': 'шт', 'value': product.pop('price')}]
192
  else:
193
  product['prices'] = []
@@ -256,12 +266,14 @@ CATALOG_TEMPLATE = '''
256
  .header { display: flex; justify-content: space-between; align-items: center; padding: 15px 0; border-bottom: 1px solid #e0e0e0; }
257
  .header h1 { font-size: 1.8rem; font-weight: 600; color: #e3a84f; }
258
  .store-address { padding: 15px; text-align: center; background-color: #f9f9f9; margin: 20px 0; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.03); font-size: 1rem; color: #666; }
259
- .filters-container { margin: 20px 0; display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; }
 
 
260
  .search-container { margin: 20px 0; text-align: center; }
261
  #search-input { width: 90%; max-width: 600px; padding: 12px 18px; font-size: 1rem; border: 1px solid #e0e0e0; border-radius: 25px; outline: none; box-shadow: 0 2px 5px rgba(0,0,0,0.03); transition: all 0.3s ease; }
262
  #search-input:focus { border-color: #e3a84f; box-shadow: 0 0 0 3px rgba(227, 168, 79, 0.15); }
263
- .category-filter { padding: 8px 16px; border: 1px solid #e0e0e0; border-radius: 20px; background-color: #fff; cursor: pointer; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); font-size: 0.9rem; font-weight: 400; color: #c89345; }
264
- .category-filter.active, .category-filter:hover { background-color: #e3a84f; color: white; border-color: #e3a84f; box-shadow: 0 2px 10px rgba(227, 168, 79, 0.2); }
265
  .products-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 20px; padding: 10px; }
266
  @media (min-width: 600px) { .products-grid { grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); } }
267
  @media (min-width: 900px) { .products-grid { grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); } }
@@ -328,11 +340,14 @@ CATALOG_TEMPLATE = '''
328
 
329
  <div class="store-address">Наш адрес: {{ store_address }}</div>
330
 
331
- <div class="filters-container">
332
- <button class="category-filter active" data-category="all">Все категории</button>
333
- {% for category in categories %}
334
- <button class="category-filter" data-category="{{ category }}">{{ category }}</button>
335
- {% endfor %}
 
 
 
336
  </div>
337
 
338
  <div class="search-container">
@@ -344,7 +359,8 @@ CATALOG_TEMPLATE = '''
344
  <div class="product"
345
  data-name="{{ product['name']|lower }}"
346
  data-description="{{ product.get('description', '')|lower }}"
347
- data-category="{{ product.get('category', 'Без категории') }}">
 
348
  {% if product.get('is_top', False) %}
349
  <span class="top-product-indicator"><i class="fas fa-star"></i> Топ</span>
350
  {% endif %}
@@ -437,11 +453,13 @@ CATALOG_TEMPLATE = '''
437
  <script src="https://cdnjs.cloudflare.com/ajax/libs/Swiper/10.2.0/swiper-bundle.min.js"></script>
438
  <script>
439
  const products = {{ products|tojson }};
 
440
  const repoId = '{{ repo_id }}';
441
  const currencyCode = '{{ currency_code }}';
442
  let selectedProductIndex = null;
443
  let cart = JSON.parse(localStorage.getItem('mekaCart') || '[]');
444
-
 
445
 
446
  function openModal(index) {
447
  loadProductDetails(index);
@@ -547,7 +565,7 @@ CATALOG_TEMPLATE = '''
547
  priceTypeSelect.style.display = 'none';
548
  if(priceTypeLabel) priceTypeLabel.style.display = 'none';
549
  alert("Для этого товара нет доступных цен.");
550
- return; // Cannot add to cart if no price is available
551
  }
552
 
553
 
@@ -568,7 +586,7 @@ CATALOG_TEMPLATE = '''
568
  const color = colorSelect.style.display !== 'none' && colorSelect.value ? colorSelect.value : 'N/A';
569
  const priceTypeSelect = document.getElementById('priceTypeSelect');
570
  const selectedPriceOption = priceTypeSelect.options[priceTypeSelect.selectedIndex];
571
- const priceType = selectedPriceOption ? selectedPriceOption.value : 'шт'; // Default to 'шт' if no price type selected
572
  const priceValue = selectedPriceOption ? parseFloat(selectedPriceOption.dataset.priceValue) : null;
573
 
574
 
@@ -589,7 +607,7 @@ CATALOG_TEMPLATE = '''
589
  return;
590
  }
591
 
592
- const cartItemId = `${product.name}-${priceType}-${color}`; // Unique ID includes price type and color
593
  const existingItemIndex = cart.findIndex(item => item.id === cartItemId);
594
 
595
  if (existingItemIndex > -1) {
@@ -598,8 +616,8 @@ CATALOG_TEMPLATE = '''
598
  cart.push({
599
  id: cartItemId,
600
  name: product.name,
601
- price_type: priceType, // Store the selected price type
602
- price_value: priceValue, // Store the actual price value
603
  photo: product.photos && product.photos.length > 0 ? product.photos[0] : null,
604
  quantity: quantity,
605
  color: color
@@ -641,13 +659,13 @@ CATALOG_TEMPLATE = '''
641
  cartTotalElement.textContent = '0.00';
642
  } else {
643
  cartContent.innerHTML = cart.map(item => {
644
- const itemTotal = item.price_value * item.quantity; // Use price_value
645
  total += itemTotal;
646
  const photoUrl = item.photo
647
  ? `https://huggingface.co/datasets/${repoId}/resolve/main/photos/${item.photo}`
648
  : 'https://via.placeholder.com/60x60.png?text=N/A';
649
  const colorText = item.color && item.color !== 'N/A' ? ` (Цвет: ${item.color})` : '';
650
- const priceTypeText = item.price_type ? `/${item.price_type}` : ''; // Show price type
651
 
652
  return `
653
  <div class="cart-item">
@@ -733,8 +751,6 @@ CATALOG_TEMPLATE = '''
733
 
734
  function filterProducts() {
735
  const searchTerm = document.getElementById('search-input').value.toLowerCase().trim();
736
- const activeCategoryButton = document.querySelector('.category-filter.active');
737
- const activeCategory = activeCategoryButton ? activeCategoryButton.dataset.category : 'all';
738
  const grid = document.getElementById('products-grid');
739
  let visibleProducts = 0;
740
 
@@ -745,11 +761,13 @@ CATALOG_TEMPLATE = '''
745
  const name = productElement.getAttribute('data-name');
746
  const description = productElement.getAttribute('data-description');
747
  const category = productElement.getAttribute('data-category');
 
748
 
749
  const matchesSearch = !searchTerm || name.includes(searchTerm) || description.includes(searchTerm);
750
  const matchesCategory = activeCategory === 'all' || category === activeCategory;
 
751
 
752
- if (matchesSearch && matchesCategory) {
753
  productElement.style.display = 'flex';
754
  visibleProducts++;
755
  } else {
@@ -769,6 +787,38 @@ CATALOG_TEMPLATE = '''
769
  grid.appendChild(p);
770
  }
771
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
772
 
773
  function setupFilters() {
774
  const searchInput = document.getElementById('search-input');
@@ -780,6 +830,9 @@ CATALOG_TEMPLATE = '''
780
  filter.addEventListener('click', function() {
781
  categoryFilters.forEach(f => f.classList.remove('active'));
782
  this.classList.add('active');
 
 
 
783
  filterProducts();
784
  });
785
  });
@@ -869,7 +922,7 @@ PRODUCT_DETAIL_TEMPLATE = '''
869
  </div>
870
 
871
  <div style="margin-top: 20px; font-size: 1rem; line-height: 1.7; color: #333;">
872
- <p><strong>Категория:</strong> {{ product.get('category', 'Без категории') }}</p>
873
  <p style="font-size: 1.2rem; font-weight: bold; color: #c89345; margin-bottom: 10px;"><strong>Цена:</strong></p>
874
  <div class="price-list" style="text-align: left;">
875
  {% if product.get('prices') %}
@@ -889,9 +942,8 @@ PRODUCT_DETAIL_TEMPLATE = '''
889
  </div>
890
  </div>
891
  <style>
892
- /* Add Swiper Modal Specific Styles if needed */
893
  #productModal .swiper-button-next, #productModal .swiper-button-prev {
894
- color: #e3a84f; /* Ensure modal navigation buttons match new color */
895
  }
896
  </style>
897
  '''
@@ -976,7 +1028,7 @@ ORDER_TEMPLATE = '''
976
  function sendOrderViaWhatsApp() {
977
  const orderId = '{{ order.id }}';
978
  const orderUrl = `{{ request.url }}`;
979
- const whatsappNumber = "{{ whatsapp_number }}"; // Use variable
980
 
981
  let message = `Здравствуйте! Хочу подтвердить свой заказ на сайте "Мир праздника":%0A%0A`;
982
  message += `*Номер заказа:* ${orderId}%0A`;
@@ -1015,6 +1067,7 @@ ADMIN_TEMPLATE = '''
1015
  h1 { font-size: 1.8rem; }
1016
  h2 { font-size: 1.5rem; margin-top: 30px; display: flex; align-items: center; gap: 8px; }
1017
  h3 { font-size: 1.2rem; color: #c89345; margin-top: 20px; }
 
1018
  .section { margin-bottom: 30px; padding: 20px; background-color: #f9f9f9; border: 1px solid #e0e0e0; border-radius: 8px; }
1019
  form { margin-bottom: 20px; }
1020
  label { font-weight: 500; margin-top: 10px; display: block; color: #666; font-size: 0.9rem;}
@@ -1039,8 +1092,8 @@ ADMIN_TEMPLATE = '''
1039
  .item strong { color: #333; }
1040
  .item .description { font-size: 0.85rem; color: #999; max-height: 60px; overflow: hidden; text-overflow: ellipsis; }
1041
  .item-actions { margin-top: 15px; display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }
1042
- .item-actions button:not(.delete-button) { background-color: #e3a84f; } /* Use primary for non-delete */
1043
- .item-actions button:not(.delete-button):hover { background-color: #c89345; } /* Use darker primary for hover */
1044
  .edit-form-container { margin-top: 15px; padding: 20px; background: #fffcf5; border: 1px dashed #e0e0e0; border-radius: 6px; display: none; }
1045
  details { background-color: #f9f9f9; border: 1px solid #e0e0e0; border-radius: 8px; margin-bottom: 20px; }
1046
  details > summary { cursor: pointer; font-weight: 600; color: #c89345; display: block; padding: 15px; border-bottom: 1px solid #e0e0e0; list-style: none; position: relative; }
@@ -1050,10 +1103,9 @@ ADMIN_TEMPLATE = '''
1050
  details .form-content { padding: 20px; }
1051
  .color-input-group, .price-input-group { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
1052
  .color-input-group input, .price-input-group input { flex-grow: 1; margin: 0; }
1053
- .price-input-group input[type="text"] { width: 100px; flex-grow: 0; } /* Type input fixed width */
1054
- .price-input-group input[type="number"] { flex-grow: 1; } /* Value input takes rest */
1055
- .price-input-group label { margin-top: 0; width: auto; display: inline-block; font-weight: normal; color: #333;} /* Labels within group */
1056
-
1057
  .remove-color-btn, .remove-price-btn { background-color: #dc3545; padding: 6px 10px; font-size: 0.8rem; margin-top: 0; line-height: 1; }
1058
  .remove-color-btn:hover, .remove-price-btn:hover { background-color: #c82333; }
1059
  .add-color-btn, .add-price-btn { background-color: #f0c38b; color: #c89345; border: 1px solid #e0e0e0; }
@@ -1072,8 +1124,10 @@ ADMIN_TEMPLATE = '''
1072
  .status-indicator.in-stock { background-color: #d4edda; color: #155724; }
1073
  .status-indicator.out-of-stock { background-color: #f8d7da; color: #721c24; }
1074
  .status-indicator.top-product { background-color: #fff3cd; color: #856404; margin-left: 5px;}
1075
- .admin-price-list { margin-top: 5px; }
1076
- .admin-price-item { font-size: 0.85rem; color: #333; display: block; margin-bottom: 3px;}
 
 
1077
  </style>
1078
  </head>
1079
  <body>
@@ -1111,7 +1165,7 @@ ADMIN_TEMPLATE = '''
1111
  <div class="section">
1112
  <h2><i class="fas fa-tags"></i> Управление категориями</h2>
1113
  <details>
1114
- <summary><i class="fas fa-plus-circle"></i> Добавить новую категорию</summary>
1115
  <div class="form-content">
1116
  <form method="POST">
1117
  <input type="hidden" name="action" value="add_category">
@@ -1126,13 +1180,40 @@ ADMIN_TEMPLATE = '''
1126
  {% if categories %}
1127
  <div class="item-list">
1128
  {% for category in categories %}
1129
- <div class="item" style="display: flex; justify-content: space-between; align-items: center;">
1130
- <span>{{ category }}</span>
1131
- <form method="POST" style="margin: 0;" onsubmit="return confirm('Вы уверены, что хотите удалить категорию \'{{ category }}\'? Товары этой категории будут помечены как \'Без категории\'.');">
1132
- <input type="hidden" name="action" value="delete_category">
1133
- <input type="hidden" name="category_name" value="{{ category }}">
1134
- <button type="submit" class="delete-button" style="padding: 5px 10px; font-size: 0.8rem; margin: 0;"><i class="fas fa-trash-alt"></i></button>
1135
- </form>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1136
  </div>
1137
  {% endfor %}
1138
  </div>
@@ -1176,13 +1257,20 @@ ADMIN_TEMPLATE = '''
1176
 
1177
  <label for="add_description" style="margin-top: 15px;">Описание:</label>
1178
  <textarea id="add_description" name="description" rows="4"></textarea>
 
1179
  <label for="add_category">Категория:</label>
1180
- <select id="add_category" name="category">
1181
  <option value="Без категории">Без категории</option>
1182
  {% for category in categories %}
1183
- <option value="{{ category }}">{{ category }}</option>
1184
  {% endfor %}
1185
  </select>
 
 
 
 
 
 
1186
  <label for="add_photos">Фотографии (до 10 шт.):</label>
1187
  <input type="file" id="add_photos" name="photos" accept="image/*" multiple>
1188
  <label>Цвета/Варианты (оставьте пустым, если нет):</label>
@@ -1235,7 +1323,7 @@ ADMIN_TEMPLATE = '''
1235
  <span class="status-indicator top-product"><i class="fas fa-star"></i> Топ</span>
1236
  {% endif %}
1237
  </h3>
1238
- <p><strong>Категория:</strong> {{ product.get('category', 'Без категории') }}</p>
1239
  <p><strong>Цены:</strong></p>
1240
  <div class="admin-price-list">
1241
  {% if product.get('prices') %}
@@ -1301,13 +1389,20 @@ ADMIN_TEMPLATE = '''
1301
 
1302
  <label style="margin-top: 15px;">Описание:</label>
1303
  <textarea name="description" rows="4">{{ product.get('description', '') }}</textarea>
 
1304
  <label>Категория:</label>
1305
- <select name="category">
1306
  <option value="Без категории" {% if product.get('category', 'Без категории') == 'Без категории' %}selected{% endif %}>Без категории</option>
1307
  {% for category in categories %}
1308
- <option value="{{ category }}" {% if product.get('category') == category %}selected{% endif %}>{{ category }}</option>
1309
  {% endfor %}
1310
  </select>
 
 
 
 
 
 
1311
  <label>Заменить фотографии (выберите новые файлы, до 10 шт.):</label>
1312
  <input type="file" name="photos" accept="image/*" multiple>
1313
  {% if product.get('photos') %}
@@ -1362,10 +1457,42 @@ ADMIN_TEMPLATE = '''
1362
  </div>
1363
 
1364
  <script>
 
 
1365
  function toggleEditForm(formId) {
1366
  const formContainer = document.getElementById(formId);
1367
  if (formContainer) {
1368
- formContainer.style.display = formContainer.style.display === 'none' || formContainer.style.display === '' ? 'block' : 'none';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1369
  }
1370
  }
1371
 
@@ -1391,10 +1518,6 @@ ADMIN_TEMPLATE = '''
1391
  if (group) {
1392
  const container = group.parentNode;
1393
  group.remove();
1394
- // Optional: Add a placeholder if the last input is removed
1395
- // if (container && container.children.length === 0) {
1396
- // addColorInput(container.id);
1397
- // }
1398
  } else {
1399
  console.warn("Could not find parent .color-input-group for remove button");
1400
  }
@@ -1424,7 +1547,6 @@ ADMIN_TEMPLATE = '''
1424
  const group = button.closest('.price-input-group');
1425
  if (group) {
1426
  const container = group.parentNode;
1427
- // Prevent removing the last price input
1428
  if (container.children.length <= 1) {
1429
  alert("Должен быть указан хотя бы один вариант цены.");
1430
  return;
@@ -1435,14 +1557,21 @@ ADMIN_TEMPLATE = '''
1435
  }
1436
  }
1437
 
1438
- // Initial add price input if none exist on load for Add form
1439
  document.addEventListener('DOMContentLoaded', () => {
1440
  const addPriceInputsDiv = document.getElementById('add-price-inputs');
1441
  if(addPriceInputsDiv && addPriceInputsDiv.children.length === 0) {
1442
  addPriceInput('add-price-inputs');
1443
  }
1444
- });
1445
 
 
 
 
 
 
 
 
 
 
1446
  </script>
1447
  </body>
1448
  </html>
@@ -1453,7 +1582,7 @@ ADMIN_TEMPLATE = '''
1453
  def catalog():
1454
  data = load_data()
1455
  all_products = data.get('products', [])
1456
- categories = sorted(data.get('categories', []))
1457
 
1458
  products_in_stock = [p for p in all_products if p.get('in_stock', True)]
1459
  products_sorted = sorted(products_in_stock, key=lambda p: (not p.get('is_top', False), p.get('name', '').lower()))
@@ -1500,7 +1629,6 @@ def create_order():
1500
  total_price = 0
1501
  processed_cart = []
1502
  for item in cart_items:
1503
- # Expect price_value, price_type, quantity, name, color, photo
1504
  if not all(k in item for k in ('name', 'price_value', 'quantity', 'price_type')):
1505
  logging.error(f"Invalid cart item structure received: {item}")
1506
  return jsonify({"error": "Неверный формат товара в корзине."}), 400
@@ -1587,9 +1715,9 @@ def admin():
1587
  try:
1588
  if action == 'add_category':
1589
  category_name = request.form.get('category_name', '').strip()
1590
- if category_name and category_name not in categories:
1591
- categories.append(category_name)
1592
- data['categories'] = categories
1593
  save_data(data)
1594
  logging.info(f"Category '{category_name}' added.")
1595
  flash(f"Категория '{category_name}' успешно добавлена.", 'success')
@@ -1600,14 +1728,33 @@ def admin():
1600
  logging.warning(f"Category '{category_name}' already exists.")
1601
  flash(f"Категория '{category_name}' уже существует.", 'error')
1602
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1603
  elif action == 'delete_category':
1604
  category_to_delete = request.form.get('category_name')
1605
- if category_to_delete and category_to_delete in categories:
1606
- categories.remove(category_to_delete)
 
1607
  updated_count = 0
1608
  for product in products:
1609
  if product.get('category') == category_to_delete:
1610
  product['category'] = 'Без категории'
 
1611
  updated_count += 1
1612
  data['categories'] = categories
1613
  data['products'] = products
@@ -1615,13 +1762,30 @@ def admin():
1615
  logging.info(f"Category '{category_to_delete}' deleted. Updated products: {updated_count}.")
1616
  flash(f"Категория '{category_to_delete}' удалена. {updated_count} товаров обновлено.", 'success')
1617
  else:
1618
- logging.warning(f"Attempted to delete non-existent or empty category: {category_to_delete}")
1619
  flash(f"Не удалось удалить категорию '{category_to_delete}'.", 'error')
1620
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1621
  elif action == 'add_product':
1622
  name = request.form.get('name', '').strip()
1623
  description = request.form.get('description', '').strip()
1624
  category = request.form.get('category')
 
1625
  photos_files = request.files.getlist('photos')
1626
  colors = [c.strip() for c in request.form.getlist('colors') if c.strip()]
1627
  in_stock = 'in_stock' in request.form
@@ -1635,7 +1799,7 @@ def admin():
1635
  type_str = type_str.strip()
1636
  value_str = value_str.strip().replace(',', '.')
1637
  if not type_str or not value_str:
1638
- continue # Skip empty pairs
1639
 
1640
  try:
1641
  price_value = round(float(value_str), 2)
@@ -1645,7 +1809,6 @@ def admin():
1645
  except ValueError:
1646
  logging.warning(f"Skipping invalid price value '{value_str}' for type '{type_str}' during add product.")
1647
 
1648
-
1649
  if not name:
1650
  flash("Название товара обязательно.", 'error')
1651
  return redirect(url_for('admin'))
@@ -1710,7 +1873,8 @@ def admin():
1710
 
1711
  new_product = {
1712
  'name': name, 'prices': prices, 'description': description,
1713
- 'category': category if category in categories else 'Без категории',
 
1714
  'photos': photos_list, 'colors': colors,
1715
  'in_stock': in_stock, 'is_top': is_top
1716
  }
@@ -1741,7 +1905,9 @@ def admin():
1741
  product_to_edit['name'] = request.form.get('name', product_to_edit['name']).strip()
1742
  product_to_edit['description'] = request.form.get('description', product_to_edit.get('description', '')).strip()
1743
  category = request.form.get('category')
1744
- product_to_edit['category'] = category if category in categories else 'Без категории'
 
 
1745
  product_to_edit['colors'] = [c.strip() for c in request.form.getlist('colors') if c.strip()]
1746
  product_to_edit['in_stock'] = 'in_stock' in request.form
1747
  product_to_edit['is_top'] = 'is_top' in request.form
@@ -1754,7 +1920,7 @@ def admin():
1754
  type_str = type_str.strip()
1755
  value_str = value_str.strip().replace(',', '.')
1756
  if not type_str or not value_str:
1757
- continue # Skip empty pairs
1758
 
1759
  try:
1760
  price_value = round(float(value_str), 2)
@@ -1904,7 +2070,7 @@ def admin():
1904
 
1905
  current_data = load_data()
1906
  display_products = sorted(current_data.get('products', []), key=lambda p: p.get('name', '').lower())
1907
- display_categories = sorted(current_data.get('categories', []))
1908
 
1909
  return render_template_string(
1910
  ADMIN_TEMPLATE,
@@ -1956,4 +2122,3 @@ if __name__ == '__main__':
1956
  port = int(os.environ.get('PORT', 7860))
1957
  logging.info(f"Starting Flask app on host 0.0.0.0 and port {port}")
1958
  app.run(debug=False, host='0.0.0.0', port=port)
1959
-
 
21
 
22
  SYNC_FILES = [DATA_FILE]
23
 
24
+ REPO_ID = "Kgshop/prazdnik"
25
  HF_TOKEN_WRITE = os.getenv("HF_TOKEN")
26
  HF_TOKEN_READ = os.getenv("HF_TOKEN_READ")
27
 
 
30
  CURRENCY_CODE = 'KGS'
31
  CURRENCY_NAME = 'Кыргызский сом'
32
 
33
+ WHATSAPP_NUMBER = "996702888188"
34
 
35
  DOWNLOAD_RETRIES = 3
36
  DOWNLOAD_DELAY = 5
 
149
  if 'products' not in data: data['products'] = []
150
  if 'categories' not in data: data['categories'] = []
151
  if 'orders' not in data: data['orders'] = {}
152
+
153
+ if 'categories' in data and data['categories'] and isinstance(data['categories'][0], str):
154
+ logging.info("Old category format detected. Migrating to new structure.")
155
+ data['categories'] = [{'name': c, 'subcategories': []} for c in data['categories']]
156
+
157
  for product in data['products']:
158
+ if 'subcategory' not in product:
159
+ product['subcategory'] = None
160
  if 'prices' not in product or not isinstance(product['prices'], list):
161
+ if 'price' in product:
162
  product['prices'] = [{'type': 'шт', 'value': product.pop('price')}]
163
  else:
164
  product['prices'] = []
 
165
  product['prices'] = [p for p in product['prices'] if isinstance(p, dict) and 'type' in p and 'value' in p]
166
  for p in product['prices']:
167
  try:
168
  p['value'] = round(float(p['value']), 2)
169
  except (ValueError, TypeError):
170
+ p['value'] = 0.0
171
+ if not product['prices']:
172
  product['prices'] = [{'type': 'шт', 'value': 0.0}]
173
 
174
  return data
 
189
  if 'categories' not in data: data['categories'] = []
190
  if 'orders' not in data: data['orders'] = {}
191
 
192
+ if 'categories' in data and data['categories'] and isinstance(data['categories'][0], str):
193
+ logging.info("Old category format detected after download. Migrating to new structure.")
194
+ data['categories'] = [{'name': c, 'subcategories': []} for c in data['categories']]
195
+
196
  for product in data['products']:
197
+ if 'subcategory' not in product:
198
+ product['subcategory'] = None
199
  if 'prices' not in product or not isinstance(product['prices'], list):
200
+ if 'price' in product:
201
  product['prices'] = [{'type': 'шт', 'value': product.pop('price')}]
202
  else:
203
  product['prices'] = []
 
266
  .header { display: flex; justify-content: space-between; align-items: center; padding: 15px 0; border-bottom: 1px solid #e0e0e0; }
267
  .header h1 { font-size: 1.8rem; font-weight: 600; color: #e3a84f; }
268
  .store-address { padding: 15px; text-align: center; background-color: #f9f9f9; margin: 20px 0; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.03); font-size: 1rem; color: #666; }
269
+ .filters-wrapper { margin: 20px 0; }
270
+ .filters-container, .sub-filters-container { display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; }
271
+ .sub-filters-container { margin-top: 15px; padding-top: 15px; border-top: 1px dashed #e0e0e0;}
272
  .search-container { margin: 20px 0; text-align: center; }
273
  #search-input { width: 90%; max-width: 600px; padding: 12px 18px; font-size: 1rem; border: 1px solid #e0e0e0; border-radius: 25px; outline: none; box-shadow: 0 2px 5px rgba(0,0,0,0.03); transition: all 0.3s ease; }
274
  #search-input:focus { border-color: #e3a84f; box-shadow: 0 0 0 3px rgba(227, 168, 79, 0.15); }
275
+ .category-filter, .subcategory-filter { padding: 8px 16px; border: 1px solid #e0e0e0; border-radius: 20px; background-color: #fff; cursor: pointer; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); font-size: 0.9rem; font-weight: 400; color: #c89345; }
276
+ .category-filter.active, .category-filter:hover, .subcategory-filter.active, .subcategory-filter:hover { background-color: #e3a84f; color: white; border-color: #e3a84f; box-shadow: 0 2px 10px rgba(227, 168, 79, 0.2); }
277
  .products-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 20px; padding: 10px; }
278
  @media (min-width: 600px) { .products-grid { grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); } }
279
  @media (min-width: 900px) { .products-grid { grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); } }
 
340
 
341
  <div class="store-address">Наш адрес: {{ store_address }}</div>
342
 
343
+ <div class="filters-wrapper">
344
+ <div class="filters-container">
345
+ <button class="category-filter active" data-category="all">Все категории</button>
346
+ {% for category in categories %}
347
+ <button class="category-filter" data-category="{{ category.name }}">{{ category.name }}</button>
348
+ {% endfor %}
349
+ </div>
350
+ <div class="sub-filters-container" id="sub-filters-container"></div>
351
  </div>
352
 
353
  <div class="search-container">
 
359
  <div class="product"
360
  data-name="{{ product['name']|lower }}"
361
  data-description="{{ product.get('description', '')|lower }}"
362
+ data-category="{{ product.get('category', 'Без категории') }}"
363
+ data-subcategory="{{ product.get('subcategory', '')|lower if product.get('subcategory') else '' }}">
364
  {% if product.get('is_top', False) %}
365
  <span class="top-product-indicator"><i class="fas fa-star"></i> Топ</span>
366
  {% endif %}
 
453
  <script src="https://cdnjs.cloudflare.com/ajax/libs/Swiper/10.2.0/swiper-bundle.min.js"></script>
454
  <script>
455
  const products = {{ products|tojson }};
456
+ const categoriesData = {{ categories|tojson }};
457
  const repoId = '{{ repo_id }}';
458
  const currencyCode = '{{ currency_code }}';
459
  let selectedProductIndex = null;
460
  let cart = JSON.parse(localStorage.getItem('mekaCart') || '[]');
461
+ let activeCategory = 'all';
462
+ let activeSubcategory = 'all';
463
 
464
  function openModal(index) {
465
  loadProductDetails(index);
 
565
  priceTypeSelect.style.display = 'none';
566
  if(priceTypeLabel) priceTypeLabel.style.display = 'none';
567
  alert("Для этого товара нет доступных цен.");
568
+ return;
569
  }
570
 
571
 
 
586
  const color = colorSelect.style.display !== 'none' && colorSelect.value ? colorSelect.value : 'N/A';
587
  const priceTypeSelect = document.getElementById('priceTypeSelect');
588
  const selectedPriceOption = priceTypeSelect.options[priceTypeSelect.selectedIndex];
589
+ const priceType = selectedPriceOption ? selectedPriceOption.value : 'шт';
590
  const priceValue = selectedPriceOption ? parseFloat(selectedPriceOption.dataset.priceValue) : null;
591
 
592
 
 
607
  return;
608
  }
609
 
610
+ const cartItemId = `${product.name}-${priceType}-${color}`;
611
  const existingItemIndex = cart.findIndex(item => item.id === cartItemId);
612
 
613
  if (existingItemIndex > -1) {
 
616
  cart.push({
617
  id: cartItemId,
618
  name: product.name,
619
+ price_type: priceType,
620
+ price_value: priceValue,
621
  photo: product.photos && product.photos.length > 0 ? product.photos[0] : null,
622
  quantity: quantity,
623
  color: color
 
659
  cartTotalElement.textContent = '0.00';
660
  } else {
661
  cartContent.innerHTML = cart.map(item => {
662
+ const itemTotal = item.price_value * item.quantity;
663
  total += itemTotal;
664
  const photoUrl = item.photo
665
  ? `https://huggingface.co/datasets/${repoId}/resolve/main/photos/${item.photo}`
666
  : 'https://via.placeholder.com/60x60.png?text=N/A';
667
  const colorText = item.color && item.color !== 'N/A' ? ` (Цвет: ${item.color})` : '';
668
+ const priceTypeText = item.price_type ? `/${item.price_type}` : '';
669
 
670
  return `
671
  <div class="cart-item">
 
751
 
752
  function filterProducts() {
753
  const searchTerm = document.getElementById('search-input').value.toLowerCase().trim();
 
 
754
  const grid = document.getElementById('products-grid');
755
  let visibleProducts = 0;
756
 
 
761
  const name = productElement.getAttribute('data-name');
762
  const description = productElement.getAttribute('data-description');
763
  const category = productElement.getAttribute('data-category');
764
+ const subcategory = productElement.getAttribute('data-subcategory');
765
 
766
  const matchesSearch = !searchTerm || name.includes(searchTerm) || description.includes(searchTerm);
767
  const matchesCategory = activeCategory === 'all' || category === activeCategory;
768
+ const matchesSubcategory = activeSubcategory === 'all' || subcategory === activeSubcategory;
769
 
770
+ if (matchesSearch && matchesCategory && matchesSubcategory) {
771
  productElement.style.display = 'flex';
772
  visibleProducts++;
773
  } else {
 
787
  grid.appendChild(p);
788
  }
789
  }
790
+
791
+ function renderSubcategories() {
792
+ const subFiltersContainer = document.getElementById('sub-filters-container');
793
+ subFiltersContainer.innerHTML = '';
794
+
795
+ if (activeCategory === 'all') {
796
+ subFiltersContainer.style.display = 'none';
797
+ return;
798
+ }
799
+
800
+ const categoryData = categoriesData.find(c => c.name === activeCategory);
801
+ if (categoryData && categoryData.subcategories && categoryData.subcategories.length > 0) {
802
+ subFiltersContainer.style.display = 'flex';
803
+
804
+ let buttonsHTML = `<button class="subcategory-filter active" data-subcategory="all">Все в "${activeCategory}"</button>`;
805
+ categoryData.subcategories.forEach(sub => {
806
+ buttonsHTML += `<button class="subcategory-filter" data-subcategory="${sub.toLowerCase()}">${sub}</button>`;
807
+ });
808
+ subFiltersContainer.innerHTML = buttonsHTML;
809
+
810
+ subFiltersContainer.querySelectorAll('.subcategory-filter').forEach(subFilter => {
811
+ subFilter.addEventListener('click', function() {
812
+ subFiltersContainer.querySelectorAll('.subcategory-filter').forEach(f => f.classList.remove('active'));
813
+ this.classList.add('active');
814
+ activeSubcategory = this.dataset.subcategory;
815
+ filterProducts();
816
+ });
817
+ });
818
+ } else {
819
+ subFiltersContainer.style.display = 'none';
820
+ }
821
+ }
822
 
823
  function setupFilters() {
824
  const searchInput = document.getElementById('search-input');
 
830
  filter.addEventListener('click', function() {
831
  categoryFilters.forEach(f => f.classList.remove('active'));
832
  this.classList.add('active');
833
+ activeCategory = this.dataset.category;
834
+ activeSubcategory = 'all';
835
+ renderSubcategories();
836
  filterProducts();
837
  });
838
  });
 
922
  </div>
923
 
924
  <div style="margin-top: 20px; font-size: 1rem; line-height: 1.7; color: #333;">
925
+ <p><strong>Категория:</strong> {{ product.get('category', 'Без категории') }}{% if product.get('subcategory') %} / {{ product.get('subcategory') }}{% endif %}</p>
926
  <p style="font-size: 1.2rem; font-weight: bold; color: #c89345; margin-bottom: 10px;"><strong>Цена:</strong></p>
927
  <div class="price-list" style="text-align: left;">
928
  {% if product.get('prices') %}
 
942
  </div>
943
  </div>
944
  <style>
 
945
  #productModal .swiper-button-next, #productModal .swiper-button-prev {
946
+ color: #e3a84f;
947
  }
948
  </style>
949
  '''
 
1028
  function sendOrderViaWhatsApp() {
1029
  const orderId = '{{ order.id }}';
1030
  const orderUrl = `{{ request.url }}`;
1031
+ const whatsappNumber = "{{ whatsapp_number }}";
1032
 
1033
  let message = `Здравствуйте! Хочу подтвердить свой заказ на сайте "Мир праздника":%0A%0A`;
1034
  message += `*Номер заказа:* ${orderId}%0A`;
 
1067
  h1 { font-size: 1.8rem; }
1068
  h2 { font-size: 1.5rem; margin-top: 30px; display: flex; align-items: center; gap: 8px; }
1069
  h3 { font-size: 1.2rem; color: #c89345; margin-top: 20px; }
1070
+ h4 { font-size: 1.1rem; color: #333; margin-top: 20px; }
1071
  .section { margin-bottom: 30px; padding: 20px; background-color: #f9f9f9; border: 1px solid #e0e0e0; border-radius: 8px; }
1072
  form { margin-bottom: 20px; }
1073
  label { font-weight: 500; margin-top: 10px; display: block; color: #666; font-size: 0.9rem;}
 
1092
  .item strong { color: #333; }
1093
  .item .description { font-size: 0.85rem; color: #999; max-height: 60px; overflow: hidden; text-overflow: ellipsis; }
1094
  .item-actions { margin-top: 15px; display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }
1095
+ .item-actions button:not(.delete-button) { background-color: #e3a84f; }
1096
+ .item-actions button:not(.delete-button):hover { background-color: #c89345; }
1097
  .edit-form-container { margin-top: 15px; padding: 20px; background: #fffcf5; border: 1px dashed #e0e0e0; border-radius: 6px; display: none; }
1098
  details { background-color: #f9f9f9; border: 1px solid #e0e0e0; border-radius: 8px; margin-bottom: 20px; }
1099
  details > summary { cursor: pointer; font-weight: 600; color: #c89345; display: block; padding: 15px; border-bottom: 1px solid #e0e0e0; list-style: none; position: relative; }
 
1103
  details .form-content { padding: 20px; }
1104
  .color-input-group, .price-input-group { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
1105
  .color-input-group input, .price-input-group input { flex-grow: 1; margin: 0; }
1106
+ .price-input-group input[type="text"] { width: 100px; flex-grow: 0; }
1107
+ .price-input-group input[type="number"] { flex-grow: 1; }
1108
+ .price-input-group label { margin-top: 0; width: auto; display: inline-block; font-weight: normal; color: #333;}
 
1109
  .remove-color-btn, .remove-price-btn { background-color: #dc3545; padding: 6px 10px; font-size: 0.8rem; margin-top: 0; line-height: 1; }
1110
  .remove-color-btn:hover, .remove-price-btn:hover { background-color: #c82333; }
1111
  .add-color-btn, .add-price-btn { background-color: #f0c38b; color: #c89345; border: 1px solid #e0e0e0; }
 
1124
  .status-indicator.in-stock { background-color: #d4edda; color: #155724; }
1125
  .status-indicator.out-of-stock { background-color: #f8d7da; color: #721c24; }
1126
  .status-indicator.top-product { background-color: #fff3cd; color: #856404; margin-left: 5px;}
1127
+ .admin-price-list { margin-top: 5px; }
1128
+ .admin-price-item { font-size: 0.85rem; color: #333; display: block; margin-bottom: 3px;}
1129
+ .subcategory-list { list-style-type: none; padding-left: 20px; margin-top: 10px; }
1130
+ .subcategory-item { background: #f0f0f0; padding: 5px 10px; border-radius: 4px; margin-bottom: 5px; display: flex; justify-content: space-between; align-items: center; font-size: 0.9rem; }
1131
  </style>
1132
  </head>
1133
  <body>
 
1165
  <div class="section">
1166
  <h2><i class="fas fa-tags"></i> Управление категориями</h2>
1167
  <details>
1168
+ <summary><i class="fas fa-plus-circle"></i> Добавить новую основную категорию</summary>
1169
  <div class="form-content">
1170
  <form method="POST">
1171
  <input type="hidden" name="action" value="add_category">
 
1180
  {% if categories %}
1181
  <div class="item-list">
1182
  {% for category in categories %}
1183
+ <div class="item">
1184
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
1185
+ <strong>{{ category.name }}</strong>
1186
+ <form method="POST" style="margin: 0;" onsubmit="return confirm('Вы уверены, что хотите удалить категорию \'{{ category.name }}\' и все ее подкатегории?');">
1187
+ <input type="hidden" name="action" value="delete_category">
1188
+ <input type="hidden" name="category_name" value="{{ category.name }}">
1189
+ <button type="submit" class="delete-button" style="padding: 5px 10px; font-size: 0.8rem; margin: 0;"><i class="fas fa-trash-alt"></i></button>
1190
+ </form>
1191
+ </div>
1192
+ <hr style="border: 0; border-top: 1px solid #f0f0f0; margin: 10px 0;">
1193
+ <h4>Подкатегории:</h4>
1194
+ {% if category.subcategories %}
1195
+ <ul class="subcategory-list">
1196
+ {% for sub in category.subcategories %}
1197
+ <li class="subcategory-item">
1198
+ <span>{{ sub }}</span>
1199
+ <form method="POST" style="margin:0;" onsubmit="return confirm('Удалить подкатегорию \'{{ sub }}\'?');">
1200
+ <input type="hidden" name="action" value="delete_subcategory">
1201
+ <input type="hidden" name="parent_category" value="{{ category.name }}">
1202
+ <input type="hidden" name="subcategory_name" value="{{ sub }}">
1203
+ <button type="submit" class="delete-button" style="padding: 3px 8px; font-size:0.7rem; margin:0;"><i class="fas fa-times"></i></button>
1204
+ </form>
1205
+ </li>
1206
+ {% endfor %}
1207
+ </ul>
1208
+ {% else %}
1209
+ <p style="font-size: 0.9rem; color: #999;">Подкатегорий нет.</p>
1210
+ {% endif %}
1211
+ <form method="POST" style="margin-top: 15px; display: flex; gap: 10px; align-items: center;">
1212
+ <input type="hidden" name="action" value="add_subcategory">
1213
+ <input type="hidden" name="parent_category" value="{{ category.name }}">
1214
+ <input type="text" name="subcategory_name" placeholder="Новая подкатегория" required style="margin:0; flex-grow: 1;">
1215
+ <button type="submit" class="add-button" style="margin:0; padding: 8px 12px;"><i class="fas fa-plus"></i></button>
1216
+ </form>
1217
  </div>
1218
  {% endfor %}
1219
  </div>
 
1257
 
1258
  <label for="add_description" style="margin-top: 15px;">Описание:</label>
1259
  <textarea id="add_description" name="description" rows="4"></textarea>
1260
+
1261
  <label for="add_category">Категория:</label>
1262
+ <select id="add_category" name="category" onchange="updateSubcategoryDropdown('add', this.value)">
1263
  <option value="Без категории">Без категории</option>
1264
  {% for category in categories %}
1265
+ <option value="{{ category.name }}">{{ category.name }}</option>
1266
  {% endfor %}
1267
  </select>
1268
+
1269
+ <label for="add_subcategory">Подкатегория:</label>
1270
+ <select id="add_subcategory" name="subcategory">
1271
+ <option value="none">-- Сначала выберите категорию --</option>
1272
+ </select>
1273
+
1274
  <label for="add_photos">Фотографии (до 10 шт.):</label>
1275
  <input type="file" id="add_photos" name="photos" accept="image/*" multiple>
1276
  <label>Цвета/Варианты (оставьте пустым, если нет):</label>
 
1323
  <span class="status-indicator top-product"><i class="fas fa-star"></i> Топ</span>
1324
  {% endif %}
1325
  </h3>
1326
+ <p><strong>Категория:</strong> {{ product.get('category', 'Без категории') }}{% if product.get('subcategory') %} / {{ product.get('subcategory') }}{% endif %}</p>
1327
  <p><strong>Цены:</strong></p>
1328
  <div class="admin-price-list">
1329
  {% if product.get('prices') %}
 
1389
 
1390
  <label style="margin-top: 15px;">Описание:</label>
1391
  <textarea name="description" rows="4">{{ product.get('description', '') }}</textarea>
1392
+
1393
  <label>Категория:</label>
1394
+ <select name="category" onchange="updateSubcategoryDropdown('edit_{{ loop.index0 }}', this.value, '{{ product.get('subcategory') }}')">
1395
  <option value="Без категории" {% if product.get('category', 'Без категории') == 'Без категории' %}selected{% endif %}>Без категории</option>
1396
  {% for category in categories %}
1397
+ <option value="{{ category.name }}" {% if product.get('category') == category.name %}selected{% endif %}>{{ category.name }}</option>
1398
  {% endfor %}
1399
  </select>
1400
+
1401
+ <label>Подкатегория:</label>
1402
+ <select id="edit_{{ loop.index0 }}_subcategory" name="subcategory">
1403
+ <option value="none">-- Сначала выберите категорию --</option>
1404
+ </select>
1405
+
1406
  <label>Заменить фотографии (выберите новые файлы, до 10 шт.):</label>
1407
  <input type="file" name="photos" accept="image/*" multiple>
1408
  {% if product.get('photos') %}
 
1457
  </div>
1458
 
1459
  <script>
1460
+ const categoriesData = {{ categories|tojson }};
1461
+
1462
  function toggleEditForm(formId) {
1463
  const formContainer = document.getElementById(formId);
1464
  if (formContainer) {
1465
+ const isDisplayed = formContainer.style.display === 'block';
1466
+ formContainer.style.display = isDisplayed ? 'none' : 'block';
1467
+ if (!isDisplayed) {
1468
+ const catSelect = formContainer.querySelector('select[name="category"]');
1469
+ if (catSelect) {
1470
+ const currentProductSub = catSelect.getAttribute('data-current-subcategory');
1471
+ updateSubcategoryDropdown(catSelect.onchange.toString().match(/'(edit_[\d]+)'/)[1], catSelect.value, currentProductSub);
1472
+ }
1473
+ }
1474
+ }
1475
+ }
1476
+
1477
+ function updateSubcategoryDropdown(formIdPrefix, selectedCategory, currentSubcategory = null) {
1478
+ const subcatSelect = document.getElementById(`${formIdPrefix}_subcategory`);
1479
+ if (!subcatSelect) return;
1480
+
1481
+ subcatSelect.innerHTML = '';
1482
+
1483
+ const categoryData = categoriesData.find(c => c.name === selectedCategory);
1484
+
1485
+ if (categoryData && categoryData.subcategories && categoryData.subcategories.length > 0) {
1486
+ subcatSelect.disabled = false;
1487
+ let optionsHtml = '<option value="none">-- Выберите подкатегорию --</option>';
1488
+ categoryData.subcategories.forEach(sub => {
1489
+ const isSelected = sub === currentSubcategory ? 'selected' : '';
1490
+ optionsHtml += `<option value="${sub}" ${isSelected}>${sub}</option>`;
1491
+ });
1492
+ subcatSelect.innerHTML = optionsHtml;
1493
+ } else {
1494
+ subcatSelect.innerHTML = '<option value="none">-- Нет подкатегорий --</option>';
1495
+ subcatSelect.disabled = true;
1496
  }
1497
  }
1498
 
 
1518
  if (group) {
1519
  const container = group.parentNode;
1520
  group.remove();
 
 
 
 
1521
  } else {
1522
  console.warn("Could not find parent .color-input-group for remove button");
1523
  }
 
1547
  const group = button.closest('.price-input-group');
1548
  if (group) {
1549
  const container = group.parentNode;
 
1550
  if (container.children.length <= 1) {
1551
  alert("Должен быть указан хотя бы один вариант цены.");
1552
  return;
 
1557
  }
1558
  }
1559
 
 
1560
  document.addEventListener('DOMContentLoaded', () => {
1561
  const addPriceInputsDiv = document.getElementById('add-price-inputs');
1562
  if(addPriceInputsDiv && addPriceInputsDiv.children.length === 0) {
1563
  addPriceInput('add-price-inputs');
1564
  }
 
1565
 
1566
+ document.querySelectorAll('.edit-form-container').forEach((formContainer, index) => {
1567
+ const catSelect = formContainer.querySelector('select[name="category"]');
1568
+ const productSubcategory = "{{ products[" + index + "].get('subcategory', '') }}";
1569
+ if (catSelect) {
1570
+ const formIdPrefix = `edit_${index}`;
1571
+ updateSubcategoryDropdown(formIdPrefix, catSelect.value, productSubcategory);
1572
+ }
1573
+ });
1574
+ });
1575
  </script>
1576
  </body>
1577
  </html>
 
1582
  def catalog():
1583
  data = load_data()
1584
  all_products = data.get('products', [])
1585
+ categories = sorted(data.get('categories', []), key=lambda x: x['name'])
1586
 
1587
  products_in_stock = [p for p in all_products if p.get('in_stock', True)]
1588
  products_sorted = sorted(products_in_stock, key=lambda p: (not p.get('is_top', False), p.get('name', '').lower()))
 
1629
  total_price = 0
1630
  processed_cart = []
1631
  for item in cart_items:
 
1632
  if not all(k in item for k in ('name', 'price_value', 'quantity', 'price_type')):
1633
  logging.error(f"Invalid cart item structure received: {item}")
1634
  return jsonify({"error": "Неверный формат товара в корзине."}), 400
 
1715
  try:
1716
  if action == 'add_category':
1717
  category_name = request.form.get('category_name', '').strip()
1718
+ if category_name and not any(c['name'] == category_name for c in categories):
1719
+ categories.append({'name': category_name, 'subcategories': []})
1720
+ data['categories'] = sorted(categories, key=lambda x: x['name'])
1721
  save_data(data)
1722
  logging.info(f"Category '{category_name}' added.")
1723
  flash(f"Категория '{category_name}' успешно добавлена.", 'success')
 
1728
  logging.warning(f"Category '{category_name}' already exists.")
1729
  flash(f"Категория '{category_name}' уже существует.", 'error')
1730
 
1731
+ elif action == 'add_subcategory':
1732
+ parent_category_name = request.form.get('parent_category')
1733
+ subcategory_name = request.form.get('subcategory_name', '').strip()
1734
+ parent_cat = next((c for c in categories if c['name'] == parent_category_name), None)
1735
+
1736
+ if parent_cat and subcategory_name and subcategory_name not in parent_cat['subcategories']:
1737
+ parent_cat['subcategories'].append(subcategory_name)
1738
+ parent_cat['subcategories'].sort()
1739
+ save_data(data)
1740
+ flash(f"Подкатегория '{subcategory_name}' добавлена в '{parent_category_name}'.", 'success')
1741
+ elif not parent_cat:
1742
+ flash(f"Родительская категория '{parent_category_name}' не найдена.", 'error')
1743
+ elif not subcategory_name:
1744
+ flash("Название подкатегории не может быть пустым.", 'error')
1745
+ else:
1746
+ flash(f"Подкатегория '{subcategory_name}' уже существует в '{parent_category_name}'.", 'error')
1747
+
1748
  elif action == 'delete_category':
1749
  category_to_delete = request.form.get('category_name')
1750
+ original_len = len(categories)
1751
+ categories = [c for c in categories if c['name'] != category_to_delete]
1752
+ if len(categories) < original_len:
1753
  updated_count = 0
1754
  for product in products:
1755
  if product.get('category') == category_to_delete:
1756
  product['category'] = 'Без категории'
1757
+ product['subcategory'] = None
1758
  updated_count += 1
1759
  data['categories'] = categories
1760
  data['products'] = products
 
1762
  logging.info(f"Category '{category_to_delete}' deleted. Updated products: {updated_count}.")
1763
  flash(f"Категория '{category_to_delete}' удалена. {updated_count} товаров обновлено.", 'success')
1764
  else:
 
1765
  flash(f"Не удалось удалить категорию '{category_to_delete}'.", 'error')
1766
 
1767
+ elif action == 'delete_subcategory':
1768
+ parent_category_name = request.form.get('parent_category')
1769
+ subcategory_to_delete = request.form.get('subcategory_name')
1770
+ parent_cat = next((c for c in categories if c['name'] == parent_category_name), None)
1771
+
1772
+ if parent_cat and subcategory_to_delete in parent_cat['subcategories']:
1773
+ parent_cat['subcategories'].remove(subcategory_to_delete)
1774
+ updated_count = 0
1775
+ for product in products:
1776
+ if product.get('category') == parent_category_name and product.get('subcategory') == subcategory_to_delete:
1777
+ product['subcategory'] = None
1778
+ updated_count += 1
1779
+ save_data(data)
1780
+ flash(f"Подкатегория '{subcategory_to_delete}' удалена. {updated_count} товаров обновлено.", 'success')
1781
+ else:
1782
+ flash(f"Не удалось удалить подкатегорию '{subcategory_to_delete}'.", 'error')
1783
+
1784
  elif action == 'add_product':
1785
  name = request.form.get('name', '').strip()
1786
  description = request.form.get('description', '').strip()
1787
  category = request.form.get('category')
1788
+ subcategory = request.form.get('subcategory')
1789
  photos_files = request.files.getlist('photos')
1790
  colors = [c.strip() for c in request.form.getlist('colors') if c.strip()]
1791
  in_stock = 'in_stock' in request.form
 
1799
  type_str = type_str.strip()
1800
  value_str = value_str.strip().replace(',', '.')
1801
  if not type_str or not value_str:
1802
+ continue
1803
 
1804
  try:
1805
  price_value = round(float(value_str), 2)
 
1809
  except ValueError:
1810
  logging.warning(f"Skipping invalid price value '{value_str}' for type '{type_str}' during add product.")
1811
 
 
1812
  if not name:
1813
  flash("Название товара обязательно.", 'error')
1814
  return redirect(url_for('admin'))
 
1873
 
1874
  new_product = {
1875
  'name': name, 'prices': prices, 'description': description,
1876
+ 'category': category if any(c['name'] == category for c in categories) else 'Без категории',
1877
+ 'subcategory': subcategory if subcategory and subcategory != 'none' else None,
1878
  'photos': photos_list, 'colors': colors,
1879
  'in_stock': in_stock, 'is_top': is_top
1880
  }
 
1905
  product_to_edit['name'] = request.form.get('name', product_to_edit['name']).strip()
1906
  product_to_edit['description'] = request.form.get('description', product_to_edit.get('description', '')).strip()
1907
  category = request.form.get('category')
1908
+ subcategory = request.form.get('subcategory')
1909
+ product_to_edit['category'] = category if any(c['name'] == category for c in categories) else 'Без категории'
1910
+ product_to_edit['subcategory'] = subcategory if subcategory and subcategory != 'none' else None
1911
  product_to_edit['colors'] = [c.strip() for c in request.form.getlist('colors') if c.strip()]
1912
  product_to_edit['in_stock'] = 'in_stock' in request.form
1913
  product_to_edit['is_top'] = 'is_top' in request.form
 
1920
  type_str = type_str.strip()
1921
  value_str = value_str.strip().replace(',', '.')
1922
  if not type_str or not value_str:
1923
+ continue
1924
 
1925
  try:
1926
  price_value = round(float(value_str), 2)
 
2070
 
2071
  current_data = load_data()
2072
  display_products = sorted(current_data.get('products', []), key=lambda p: p.get('name', '').lower())
2073
+ display_categories = sorted(current_data.get('categories', []), key=lambda x: x.get('name', ''))
2074
 
2075
  return render_template_string(
2076
  ADMIN_TEMPLATE,
 
2122
  port = int(os.environ.get('PORT', 7860))
2123
  logging.info(f"Starting Flask app on host 0.0.0.0 and port {port}")
2124
  app.run(debug=False, host='0.0.0.0', port=port)