Kgshop commited on
Commit
5e6c7b8
·
verified ·
1 Parent(s): 1cbdce9

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +130 -156
app.py CHANGED
@@ -1,4 +1,6 @@
1
 
 
 
2
  from flask import Flask, render_template_string, request, redirect, url_for, send_file, flash, jsonify
3
  import json
4
  import os
@@ -139,13 +141,11 @@ def periodic_backup():
139
 
140
  def load_data():
141
  default_data = {'products': [], 'categories': [], 'orders': {}}
142
- try:
143
- with open(DATA_FILE, 'r', encoding='utf-8') as file:
144
- data = json.load(file)
145
- logging.info(f"Local data loaded successfully from {DATA_FILE}")
146
  if not isinstance(data, dict):
147
- logging.warning(f"Local {DATA_FILE} is not a dictionary. Attempting download.")
148
- raise FileNotFoundError
149
  if 'products' not in data: data['products'] = []
150
  if 'categories' not in data: data['categories'] = []
151
  if 'orders' not in data: data['orders'] = {}
@@ -166,6 +166,9 @@ def load_data():
166
  data['categories'] = []
167
 
168
  for product in data['products']:
 
 
 
169
  if 'subcategory' not in product:
170
  product['subcategory'] = 'Без подкатегории'
171
 
@@ -185,6 +188,12 @@ def load_data():
185
  product['prices'] = [{'type': 'шт', 'value': 0.0}]
186
 
187
  return data
 
 
 
 
 
 
188
  except FileNotFoundError:
189
  logging.warning(f"Local file {DATA_FILE} not found. Attempting download from HF.")
190
  except json.JSONDecodeError:
@@ -195,46 +204,7 @@ def load_data():
195
  with open(DATA_FILE, 'r', encoding='utf-8') as file:
196
  data = json.load(file)
197
  logging.info(f"Data loaded successfully from {DATA_FILE} after download.")
198
-
199
- if not isinstance(data, dict):
200
- logging.error(f"Downloaded {DATA_FILE} is not a dictionary. Using default.")
201
- return default_data
202
- if 'products' not in data: data['products'] = []
203
- if 'categories' not in data: data['categories'] = []
204
- if 'orders' not in data: data['orders'] = {}
205
-
206
- if 'categories' in data:
207
- new_categories = []
208
- for cat in data['categories']:
209
- if isinstance(cat, str):
210
- new_categories.append({'name': cat, 'subcategories': []})
211
- elif isinstance(cat, dict) and 'name' in cat:
212
- if 'subcategories' not in cat or not isinstance(cat['subcategories'], list):
213
- cat['subcategories'] = []
214
- new_categories.append(cat)
215
- data['categories'] = new_categories
216
- else:
217
- data['categories'] = []
218
-
219
- for product in data['products']:
220
- if 'subcategory' not in product:
221
- product['subcategory'] = 'Без подкатегории'
222
-
223
- if 'prices' not in product or not isinstance(product['prices'], list):
224
- if 'price' in product:
225
- product['prices'] = [{'type': 'шт', 'value': product.pop('price')}]
226
- else:
227
- product['prices'] = []
228
- product['prices'] = [p for p in product['prices'] if isinstance(p, dict) and 'type' in p and 'value' in p]
229
- for p in product['prices']:
230
- try:
231
- p['value'] = round(float(p['value']), 2)
232
- except (ValueError, TypeError):
233
- p['value'] = 0.0
234
- if not product['prices']:
235
- product['prices'] = [{'type': 'шт', 'value': 0.0}]
236
-
237
- return data
238
  except FileNotFoundError:
239
  logging.error(f"File {DATA_FILE} still not found even after download reported success. Using default.")
240
  return default_data
@@ -415,6 +385,7 @@ CATALOG_TEMPLATE = '''
415
  <div class="products-grid" id="products-grid">
416
  {% for product in products %}
417
  <div class="product"
 
418
  data-name="{{ product['name']|lower }}"
419
  data-description="{{ product.get('description', '')|lower }}"
420
  data-category="{{ product.get('category', 'Без категории') }}"
@@ -445,8 +416,8 @@ CATALOG_TEMPLATE = '''
445
  <p class="product-description">{{ product.get('category', 'Без категории') }}{% if product.get('subcategory', 'Без подкатегории') != 'Без подкатегории' %} / {{ product.get('subcategory') }}{% endif %}</p>
446
  </div>
447
  <div class="product-actions">
448
- <button class="product-button" onclick="openModal({{ loop.index0 }})">Подробнее</button>
449
- <button class="product-button add-to-cart" onclick="openQuantityModal({{ loop.index0 }})">
450
  <i class="fas fa-cart-plus"></i> В корзину
451
  </button>
452
  </div>
@@ -513,12 +484,16 @@ CATALOG_TEMPLATE = '''
513
  const products = {{ products|tojson }};
514
  const repoId = '{{ repo_id }}';
515
  const currencyCode = '{{ currency_code }}';
516
- let selectedProductIndex = null;
 
 
 
 
517
  let cart = JSON.parse(localStorage.getItem('mekaCart') || '[]');
518
 
519
 
520
- function openModal(index) {
521
- loadProductDetails(index);
522
  const modal = document.getElementById('productModal');
523
  if (modal) {
524
  modal.style.display = "block";
@@ -537,11 +512,11 @@ CATALOG_TEMPLATE = '''
537
  }
538
  }
539
 
540
- function loadProductDetails(index) {
541
  const modalContent = document.getElementById('modalContent');
542
  if (!modalContent) return;
543
  modalContent.innerHTML = '<p style="text-align:center; padding: 40px;">Загрузка...</p>';
544
- fetch('/product/' + index)
545
  .then(response => {
546
  if (!response.ok) throw new Error(`Ошибка ${response.status}: ${response.statusText}`);
547
  return response.text();
@@ -572,11 +547,11 @@ CATALOG_TEMPLATE = '''
572
  }
573
  }
574
 
575
- function openQuantityModal(index) {
576
- selectedProductIndex = index;
577
- const product = products[index];
578
  if (!product) {
579
- console.error("Product not found for index:", index);
580
  alert("Ошибка: товар не найден.");
581
  return;
582
  }
@@ -634,7 +609,7 @@ CATALOG_TEMPLATE = '''
634
  }
635
 
636
  function confirmAddToCart() {
637
- if (selectedProductIndex === null) return;
638
 
639
  const quantityInput = document.getElementById('quantityInput');
640
  const quantity = parseInt(quantityInput.value);
@@ -657,13 +632,13 @@ CATALOG_TEMPLATE = '''
657
  return;
658
  }
659
 
660
- const product = products[selectedProductIndex];
661
  if (!product) {
662
  alert("Ошибка добавления: товар не найден.");
663
  return;
664
  }
665
 
666
- const cartItemId = `${product.name}-${priceType}-${color}`;
667
  const existingItemIndex = cart.findIndex(item => item.id === cartItemId);
668
 
669
  if (existingItemIndex > -1) {
@@ -671,6 +646,7 @@ CATALOG_TEMPLATE = '''
671
  } else {
672
  cart.push({
673
  id: cartItemId,
 
674
  name: product.name,
675
  price_type: priceType,
676
  price_value: priceValue,
@@ -1420,24 +1396,24 @@ ADMIN_TEMPLATE = '''
1420
  </div>
1421
 
1422
  <div class="item-actions">
1423
- <button type="button" class="button" onclick="toggleEditForm('edit-form-{{ loop.index0 }}')"><i class="fas fa-edit"></i> Редактировать</button>
1424
  <form method="POST" style="margin:0;" onsubmit="return confirm('Вы уверены, что хотите удалить товар \'{{ product['name'] }}\'?');">
1425
  <input type="hidden" name="action" value="delete_product">
1426
- <input type="hidden" name="index" value="{{ loop.index0 }}">
1427
  <button type="submit" class="delete-button"><i class="fas fa-trash-alt"></i> Удалить</button>
1428
  </form>
1429
  </div>
1430
 
1431
- <div id="edit-form-{{ loop.index0 }}" class="edit-form-container">
1432
  <h4><i class="fas fa-edit"></i> Редактирование: {{ product['name'] }}</h4>
1433
  <form method="POST" enctype="multipart/form-data">
1434
  <input type="hidden" name="action" value="edit_product">
1435
- <input type="hidden" name="index" value="{{ loop.index0 }}">
1436
  <label>Название *:</label>
1437
  <input type="text" name="name" value="{{ product['name'] }}" required>
1438
 
1439
  <label>Цены (минимум одна) *:</label>
1440
- <div id="edit-price-inputs-{{ loop.index0 }}">
1441
  {% set current_prices = product.get('prices', []) %}
1442
  {% if current_prices %}
1443
  {% for price_item in current_prices %}
@@ -1459,7 +1435,7 @@ ADMIN_TEMPLATE = '''
1459
  </div>
1460
  {% endif %}
1461
  </div>
1462
- <button type="button" class="button add-price-btn" style="margin-top: 5px;" onclick="addPriceInput('edit-price-inputs-{{ loop.index0 }}')"><i class="fas fa-dollar-sign"></i> Добавить поле для цены</button>
1463
  <br>
1464
 
1465
 
@@ -1467,10 +1443,10 @@ ADMIN_TEMPLATE = '''
1467
  <textarea name="description" rows="4">{{ product.get('description', '') }}</textarea>
1468
 
1469
  <label>Категория:</label>
1470
- <select id="edit_category_{{ loop.index0 }}"
1471
  name="category"
1472
  data-initial-subcategory="{{ product.get('subcategory', 'Без подкатегории') }}"
1473
- onchange="updateSubcategorySelect('edit_category_{{ loop.index0 }}', 'edit_subcategory_{{ loop.index0 }}')">
1474
  <option value="Без категории" {% if product.get('category', 'Без категории') == 'Без категории' %}selected{% endif %}>Без категории</option>
1475
  {% for category in categories_data %}
1476
  <option value="{{ category.name }}" {% if product.get('category') == category.name %}selected{% endif %}>{{ category.name }}</option>
@@ -1478,7 +1454,7 @@ ADMIN_TEMPLATE = '''
1478
  </select>
1479
 
1480
  <label>Подкатегория:</label>
1481
- <select id="edit_subcategory_{{ loop.index0 }}" name="subcategory">
1482
  <!-- Options populated by JS -->
1483
  </select>
1484
 
@@ -1493,7 +1469,7 @@ ADMIN_TEMPLATE = '''
1493
  </div>
1494
  {% endif %}
1495
  <label>Цвета/Варианты:</label>
1496
- <div id="edit-color-inputs-{{ loop.index0 }}">
1497
  {% set current_colors = product.get('colors', []) %}
1498
  {% if current_colors and current_colors|select('ne', '')|list|length > 0 %}
1499
  {% for color in current_colors %}
@@ -1511,15 +1487,15 @@ ADMIN_TEMPLATE = '''
1511
  </div>
1512
  {% endif %}
1513
  </div>
1514
- <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>
1515
  <br>
1516
  <div style="margin-top: 15px;">
1517
- <input type="checkbox" id="edit_in_stock_{{ loop.index0 }}" name="in_stock" {% if product.get('in_stock', True) %}checked{% endif %}>
1518
- <label for="edit_in_stock_{{ loop.index0 }}" class="inline-label">В наличии</label>
1519
  </div>
1520
  <div style="margin-top: 5px;">
1521
- <input type="checkbox" id="edit_is_top_{{ loop.index0 }}" name="is_top" {% if product.get('is_top', False) %}checked{% endif %}>
1522
- <label for="edit_is_top_{{ loop.index0 }}" class="inline-label">Топ товар</label>
1523
  </div>
1524
  <br>
1525
  <button type="submit" class="add-button" style="margin-top: 20px;"><i class="fas fa-save"></i> Сохранить изменения</button>
@@ -1542,7 +1518,7 @@ ADMIN_TEMPLATE = '''
1542
  categorySubcategoryMap[c.name] = c.subcategories;
1543
  });
1544
 
1545
- function updateSubcategorySelect(categorySelectId, subcategorySelectId) {
1546
  const categorySelect = document.getElementById(categorySelectId);
1547
  const subcategorySelect = document.getElementById(subcategorySelectId);
1548
  if (!categorySelect || !subcategorySelect) return;
@@ -1550,12 +1526,17 @@ ADMIN_TEMPLATE = '''
1550
  const selectedCategory = categorySelect.value;
1551
  const subcategories = categorySubcategoryMap[selectedCategory] || [];
1552
 
1553
- let currentSubcategory = 'Без подкатегории';
1554
- if (categorySelect.dataset.initialSubcategory) {
 
 
1555
  currentSubcategory = categorySelect.dataset.initialSubcategory;
1556
  delete categorySelect.dataset.initialSubcategory;
 
 
1557
  }
1558
 
 
1559
  if (categorySelect.value !== selectedCategory) {
1560
  currentSubcategory = 'Без подкатегории';
1561
  }
@@ -1578,11 +1559,11 @@ ADMIN_TEMPLATE = '''
1578
  if (formContainer) {
1579
  formContainer.style.display = formContainer.style.display === 'none' || formContainer.style.display === '' ? 'block' : 'none';
1580
  if (formContainer.style.display === 'block') {
1581
- const index = formId.split('-').pop();
1582
- const catSelect = document.getElementById(`edit_category_${index}`);
1583
  if (catSelect) {
1584
  const initialSubcategory = catSelect.getAttribute('data-initial-subcategory');
1585
- updateSubcategorySelect(`edit_category_${index}`, `edit_subcategory_${index}`, initialSubcategory);
1586
  }
1587
  }
1588
  }
@@ -1661,15 +1642,15 @@ ADMIN_TEMPLATE = '''
1661
  addCatSelect.addEventListener('change', () => updateSubcategorySelect('add_category', 'add_subcategory'));
1662
  }
1663
 
1664
- document.querySelectorAll('[id^="edit-form-"]').forEach(formContainer => {
1665
- if (formContainer.style.display === 'block') {
1666
- const index = formContainer.id.split('-').pop();
1667
- const catSelect = document.getElementById(`edit_category_${index}`);
1668
- if (catSelect) {
1669
- const initialSubcategory = catSelect.getAttribute('data-initial-subcategory');
1670
- updateSubcategorySelect(`edit_category_${index}`, `edit_subcategory_${index}`, initialSubcategory);
1671
- }
1672
  }
 
1673
  });
1674
  });
1675
 
@@ -1697,17 +1678,15 @@ def catalog():
1697
  currency_code=CURRENCY_CODE
1698
  )
1699
 
1700
- @app.route('/product/<int:index>')
1701
- def product_detail(index):
1702
  data = load_data()
1703
  all_products = data.get('products', [])
1704
- products_in_stock = [p for p in all_products if p.get('in_stock', True)]
1705
- products_sorted = sorted(products_in_stock, key=lambda p: (not p.get('is_top', False), p.get('name', '').lower()))
1706
 
1707
- try:
1708
- product = products_sorted[index]
1709
- except IndexError:
1710
- logging.warning(f"Attempted access to non-existent or out-of-stock product with index {index}")
1711
  return "Товар не найден или отсутствует в наличии.", 404
1712
 
1713
  return render_template_string(
@@ -1730,8 +1709,9 @@ def create_order():
1730
  total_price = 0
1731
  processed_cart = []
1732
  for item in cart_items:
1733
- if not all(k in item for k in ('name', 'price_value', 'quantity', 'price_type')):
1734
- logging.error(f"Invalid cart item structure received: {item}")
 
1735
  return jsonify({"error": "Неверный формат товара в корзине."}), 400
1736
  try:
1737
  price_value = float(item['price_value'])
@@ -1740,11 +1720,13 @@ def create_order():
1740
  name = item['name']
1741
  color = item.get('color', 'N/A')
1742
  photo = item.get('photo')
 
1743
 
1744
  if price_value < 0 or quantity <= 0:
1745
  raise ValueError("Invalid price or quantity")
1746
  total_price += price_value * quantity
1747
  processed_cart.append({
 
1748
  "name": name,
1749
  "price_type": price_type,
1750
  "price_value": round(price_value, 2),
@@ -1849,12 +1831,10 @@ def admin():
1849
  subcategory_to_delete = request.form.get('subcategory_name')
1850
 
1851
  if subcategory_to_delete and category_to_delete:
1852
- # Deleting a subcategory
1853
  parent_cat = next((c for c in categories if c['name'] == category_to_delete), None)
1854
  if parent_cat and subcategory_to_delete in parent_cat['subcategories']:
1855
  parent_cat['subcategories'].remove(subcategory_to_delete)
1856
 
1857
- # Update products whose category/subcategory matches
1858
  updated_count = 0
1859
  for product in products:
1860
  if product.get('category') == category_to_delete and product.get('subcategory') == subcategory_to_delete:
@@ -1866,7 +1846,6 @@ def admin():
1866
  else:
1867
  flash(f"Не удалось найти подкатегорию '{subcategory_to_delete}' в '{category_to_delete}'.", 'error')
1868
  elif category_to_delete and not subcategory_to_delete:
1869
- # Deleting a main category
1870
  if any(c['name'] == category_to_delete for c in categories):
1871
  data['categories'] = [c for c in categories if c['name'] != category_to_delete]
1872
  categories = data['categories']
@@ -1984,6 +1963,7 @@ def admin():
1984
 
1985
 
1986
  new_product = {
 
1987
  'name': name, 'prices': prices, 'description': description,
1988
  'category': category, 'subcategory': subcategory,
1989
  'photos': photos_list, 'colors': colors,
@@ -1992,27 +1972,22 @@ def admin():
1992
  products.append(new_product)
1993
  data['products'] = products
1994
  save_data(data)
1995
- logging.info(f"Product '{name}' added.")
1996
  flash(f"Товар '{name}' успешно добавлен.", 'success')
1997
 
1998
  elif action == 'edit_product':
1999
- index_str = request.form.get('index')
2000
- if index_str is None:
2001
- flash("Ошибка редактирования: индекс товара не передан.", 'error')
2002
- return redirect(url_for('admin'))
2003
 
2004
- try:
2005
- index = int(index_str)
2006
- if not (0 <= index < len(products)):
2007
- raise IndexError("Product index out of range")
2008
- product_to_edit = products[index]
2009
- original_name = product_to_edit.get('name', 'N/A')
2010
-
2011
- except (ValueError, IndexError):
2012
- flash(f"Ошибка редактирования: неверный индекс товара '{index_str}'.", 'error')
2013
- logging.error(f"Invalid index '{index_str}' for editing. Product list length: {len(products)}")
2014
  return redirect(url_for('admin'))
2015
 
 
 
 
2016
  product_to_edit['name'] = request.form.get('name', product_to_edit['name']).strip()
2017
  product_to_edit['description'] = request.form.get('description', product_to_edit.get('description', '')).strip()
2018
 
@@ -2129,52 +2104,51 @@ def admin():
2129
  elif not HF_TOKEN_WRITE and photos_files and any(f.filename for f in photos_files):
2130
  flash("HF_TOKEN (write) не настроен. Фотографии не были обновлены.", "warning")
2131
 
2132
- products[index] = product_to_edit
2133
  data['products'] = products
2134
  save_data(data)
2135
- logging.info(f"Product '{original_name}' (index {index}) updated to '{product_to_edit['name']}'.")
2136
  flash(f"Товар '{product_to_edit['name']}' успешно обновлен.", 'success')
2137
 
2138
 
2139
  elif action == 'delete_product':
2140
- index_str = request.form.get('index')
2141
- if index_str is None:
2142
- flash("Ошибка удаления: индекс товара не передан.", 'error')
 
 
 
 
2143
  return redirect(url_for('admin'))
2144
- try:
2145
- index = int(index_str)
2146
- if not (0 <= index < len(products)): raise IndexError("Product index out of range")
2147
- deleted_product = products.pop(index)
2148
- product_name = deleted_product.get('name', 'N/A')
2149
-
2150
- photos_to_delete = deleted_product.get('photos', [])
2151
- if photos_to_delete and HF_TOKEN_WRITE:
2152
- logging.info(f"Attempting to delete photos for product '{product_name}' from HF: {photos_to_delete}")
2153
- try:
2154
- api = HfApi()
2155
- api.delete_files(
2156
- repo_id=REPO_ID,
2157
- paths_in_repo=[f"photos/{p}" for p in photos_to_delete],
2158
- repo_type="dataset",
2159
- token=HF_TOKEN_WRITE,
2160
- commit_message=f"Delete photos for deleted product {product_name}"
2161
- )
2162
- logging.info(f"Photos for product '{product_name}' deleted from HF.")
2163
- except Exception as e:
2164
- logging.error(f"Error deleting photos {photos_to_delete} for product '{product_name}' from HF: {e}", exc_info=True)
2165
- flash(f"Не удалось удалить фото для товара '{product_name}' с сервера. Товар удален локально.", "warning")
2166
- elif photos_to_delete and not HF_TOKEN_WRITE:
2167
- logging.warning(f"HF_TOKEN (write) not set. Cannot delete photos {photos_to_delete} for deleted product '{product_name}'.")
2168
- flash(f"Товар '{product_name}' удален локально, но фото не удалены с сервера (токен не задан).", "warning")
2169
-
2170
-
2171
- data['products'] = products
2172
- save_data(data)
2173
- logging.info(f"Product '{product_name}' (original index {index}) deleted.")
2174
- flash(f"Товар '{product_name}' удален.", 'success')
2175
- except (ValueError, IndexError):
2176
- flash(f"Ошибка удаления: неверный индекс товара '{index_str}'.", 'error')
2177
- logging.error(f"Invalid index '{index_str}' for deletion. Product list length: {len(products)}")
2178
 
2179
  else:
2180
  logging.warning(f"Received unknown admin action: {action}")
 
1
 
2
+
3
+
4
  from flask import Flask, render_template_string, request, redirect, url_for, send_file, flash, jsonify
5
  import json
6
  import os
 
141
 
142
  def load_data():
143
  default_data = {'products': [], 'categories': [], 'orders': {}}
144
+
145
+ def process_data(data):
 
 
146
  if not isinstance(data, dict):
147
+ return default_data
148
+
149
  if 'products' not in data: data['products'] = []
150
  if 'categories' not in data: data['categories'] = []
151
  if 'orders' not in data: data['orders'] = {}
 
166
  data['categories'] = []
167
 
168
  for product in data['products']:
169
+ if 'id' not in product:
170
+ product['id'] = uuid.uuid4().hex # Assign stable ID if missing
171
+
172
  if 'subcategory' not in product:
173
  product['subcategory'] = 'Без подкатегории'
174
 
 
188
  product['prices'] = [{'type': 'шт', 'value': 0.0}]
189
 
190
  return data
191
+
192
+ try:
193
+ with open(DATA_FILE, 'r', encoding='utf-8') as file:
194
+ data = json.load(file)
195
+ logging.info(f"Local data loaded successfully from {DATA_FILE}")
196
+ return process_data(data)
197
  except FileNotFoundError:
198
  logging.warning(f"Local file {DATA_FILE} not found. Attempting download from HF.")
199
  except json.JSONDecodeError:
 
204
  with open(DATA_FILE, 'r', encoding='utf-8') as file:
205
  data = json.load(file)
206
  logging.info(f"Data loaded successfully from {DATA_FILE} after download.")
207
+ return process_data(data)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
208
  except FileNotFoundError:
209
  logging.error(f"File {DATA_FILE} still not found even after download reported success. Using default.")
210
  return default_data
 
385
  <div class="products-grid" id="products-grid">
386
  {% for product in products %}
387
  <div class="product"
388
+ data-product-id="{{ product['id'] }}"
389
  data-name="{{ product['name']|lower }}"
390
  data-description="{{ product.get('description', '')|lower }}"
391
  data-category="{{ product.get('category', 'Без категории') }}"
 
416
  <p class="product-description">{{ product.get('category', 'Без категории') }}{% if product.get('subcategory', 'Без подкатегории') != 'Без подкатегории' %} / {{ product.get('subcategory') }}{% endif %}</p>
417
  </div>
418
  <div class="product-actions">
419
+ <button class="product-button" onclick="openModal('{{ product.id }}')">Подробнее</button>
420
+ <button class="product-button add-to-cart" onclick="openQuantityModal('{{ product.id }}')">
421
  <i class="fas fa-cart-plus"></i> В корзину
422
  </button>
423
  </div>
 
484
  const products = {{ products|tojson }};
485
  const repoId = '{{ repo_id }}';
486
  const currencyCode = '{{ currency_code }}';
487
+
488
+ const productMap = {};
489
+ products.forEach(p => { productMap[p.id] = p; });
490
+
491
+ let selectedProductId = null;
492
  let cart = JSON.parse(localStorage.getItem('mekaCart') || '[]');
493
 
494
 
495
+ function openModal(productId) {
496
+ loadProductDetails(productId);
497
  const modal = document.getElementById('productModal');
498
  if (modal) {
499
  modal.style.display = "block";
 
512
  }
513
  }
514
 
515
+ function loadProductDetails(productId) {
516
  const modalContent = document.getElementById('modalContent');
517
  if (!modalContent) return;
518
  modalContent.innerHTML = '<p style="text-align:center; padding: 40px;">Загрузка...</p>';
519
+ fetch('/product/' + productId)
520
  .then(response => {
521
  if (!response.ok) throw new Error(`Ошибка ${response.status}: ${response.statusText}`);
522
  return response.text();
 
547
  }
548
  }
549
 
550
+ function openQuantityModal(productId) {
551
+ selectedProductId = productId;
552
+ const product = productMap[productId];
553
  if (!product) {
554
+ console.error("Product not found for ID:", productId);
555
  alert("Ошибка: товар не найден.");
556
  return;
557
  }
 
609
  }
610
 
611
  function confirmAddToCart() {
612
+ if (selectedProductId === null) return;
613
 
614
  const quantityInput = document.getElementById('quantityInput');
615
  const quantity = parseInt(quantityInput.value);
 
632
  return;
633
  }
634
 
635
+ const product = productMap[selectedProductId];
636
  if (!product) {
637
  alert("Ошибка добавления: товар не найден.");
638
  return;
639
  }
640
 
641
+ const cartItemId = `${product.id}-${priceType}-${color}`;
642
  const existingItemIndex = cart.findIndex(item => item.id === cartItemId);
643
 
644
  if (existingItemIndex > -1) {
 
646
  } else {
647
  cart.push({
648
  id: cartItemId,
649
+ product_id: product.id,
650
  name: product.name,
651
  price_type: priceType,
652
  price_value: priceValue,
 
1396
  </div>
1397
 
1398
  <div class="item-actions">
1399
+ <button type="button" class="button" onclick="toggleEditForm('edit-form-{{ product.id }}')"><i class="fas fa-edit"></i> Редактировать</button>
1400
  <form method="POST" style="margin:0;" onsubmit="return confirm('Вы уверены, что хотите удалить товар \'{{ product['name'] }}\'?');">
1401
  <input type="hidden" name="action" value="delete_product">
1402
+ <input type="hidden" name="product_id" value="{{ product.id }}">
1403
  <button type="submit" class="delete-button"><i class="fas fa-trash-alt"></i> Удалить</button>
1404
  </form>
1405
  </div>
1406
 
1407
+ <div id="edit-form-{{ product.id }}" class="edit-form-container">
1408
  <h4><i class="fas fa-edit"></i> Редактирование: {{ product['name'] }}</h4>
1409
  <form method="POST" enctype="multipart/form-data">
1410
  <input type="hidden" name="action" value="edit_product">
1411
+ <input type="hidden" name="product_id" value="{{ product.id }}">
1412
  <label>Название *:</label>
1413
  <input type="text" name="name" value="{{ product['name'] }}" required>
1414
 
1415
  <label>Цены (минимум одна) *:</label>
1416
+ <div id="edit-price-inputs-{{ product.id }}">
1417
  {% set current_prices = product.get('prices', []) %}
1418
  {% if current_prices %}
1419
  {% for price_item in current_prices %}
 
1435
  </div>
1436
  {% endif %}
1437
  </div>
1438
+ <button type="button" class="button add-price-btn" style="margin-top: 5px;" onclick="addPriceInput('edit-price-inputs-{{ product.id }}')"><i class="fas fa-dollar-sign"></i> Добавить поле для цены</button>
1439
  <br>
1440
 
1441
 
 
1443
  <textarea name="description" rows="4">{{ product.get('description', '') }}</textarea>
1444
 
1445
  <label>Категория:</label>
1446
+ <select id="edit_category_{{ product.id }}"
1447
  name="category"
1448
  data-initial-subcategory="{{ product.get('subcategory', 'Без подкатегории') }}"
1449
+ onchange="updateSubcategorySelect('edit_category_{{ product.id }}', 'edit_subcategory_{{ product.id }}')">
1450
  <option value="Без категории" {% if product.get('category', 'Без категории') == 'Без категории' %}selected{% endif %}>Без категории</option>
1451
  {% for category in categories_data %}
1452
  <option value="{{ category.name }}" {% if product.get('category') == category.name %}selected{% endif %}>{{ category.name }}</option>
 
1454
  </select>
1455
 
1456
  <label>Подкатегория:</label>
1457
+ <select id="edit_subcategory_{{ product.id }}" name="subcategory">
1458
  <!-- Options populated by JS -->
1459
  </select>
1460
 
 
1469
  </div>
1470
  {% endif %}
1471
  <label>Цвета/Варианты:</label>
1472
+ <div id="edit-color-inputs-{{ product.id }}">
1473
  {% set current_colors = product.get('colors', []) %}
1474
  {% if current_colors and current_colors|select('ne', '')|list|length > 0 %}
1475
  {% for color in current_colors %}
 
1487
  </div>
1488
  {% endif %}
1489
  </div>
1490
+ <button type="button" class="button add-color-btn" style="margin-top: 5px;" onclick="addColorInput('edit-color-inputs-{{ product.id }}')"><i class="fas fa-palette"></i> Добавить поле для цвета</button>
1491
  <br>
1492
  <div style="margin-top: 15px;">
1493
+ <input type="checkbox" id="edit_in_stock_{{ product.id }}" name="in_stock" {% if product.get('in_stock', True) %}checked{% endif %}>
1494
+ <label for="edit_in_stock_{{ product.id }}" class="inline-label">В наличии</label>
1495
  </div>
1496
  <div style="margin-top: 5px;">
1497
+ <input type="checkbox" id="edit_is_top_{{ product.id }}" name="is_top" {% if product.get('is_top', False) %}checked{% endif %}>
1498
+ <label for="edit_is_top_{{ product.id }}" class="inline-label">Топ товар</label>
1499
  </div>
1500
  <br>
1501
  <button type="submit" class="add-button" style="margin-top: 20px;"><i class="fas fa-save"></i> Сохранить изменения</button>
 
1518
  categorySubcategoryMap[c.name] = c.subcategories;
1519
  });
1520
 
1521
+ function updateSubcategorySelect(categorySelectId, subcategorySelectId, initialSubcategoryOverride = null) {
1522
  const categorySelect = document.getElementById(categorySelectId);
1523
  const subcategorySelect = document.getElementById(subcategorySelectId);
1524
  if (!categorySelect || !subcategorySelect) return;
 
1526
  const selectedCategory = categorySelect.value;
1527
  const subcategories = categorySubcategoryMap[selectedCategory] || [];
1528
 
1529
+ let currentSubcategory;
1530
+ if (initialSubcategoryOverride) {
1531
+ currentSubcategory = initialSubcategoryOverride;
1532
+ } else if (categorySelect.dataset.initialSubcategory) {
1533
  currentSubcategory = categorySelect.dataset.initialSubcategory;
1534
  delete categorySelect.dataset.initialSubcategory;
1535
+ } else {
1536
+ currentSubcategory = 'Без подкатегории';
1537
  }
1538
 
1539
+ // If the main category changed, reset subcategory selection preference
1540
  if (categorySelect.value !== selectedCategory) {
1541
  currentSubcategory = 'Без подкатегории';
1542
  }
 
1559
  if (formContainer) {
1560
  formContainer.style.display = formContainer.style.display === 'none' || formContainer.style.display === '' ? 'block' : 'none';
1561
  if (formContainer.style.display === 'block') {
1562
+ const productId = formId.split('-').pop();
1563
+ const catSelect = document.getElementById(`edit_category_${productId}`);
1564
  if (catSelect) {
1565
  const initialSubcategory = catSelect.getAttribute('data-initial-subcategory');
1566
+ updateSubcategorySelect(`edit_category_${productId}`, `edit_subcategory_${productId}`, initialSubcategory);
1567
  }
1568
  }
1569
  }
 
1642
  addCatSelect.addEventListener('change', () => updateSubcategorySelect('add_category', 'add_subcategory'));
1643
  }
1644
 
1645
+ document.querySelectorAll('select[id^="edit_category_"]').forEach(catSelect => {
1646
+ const productId = catSelect.id.split('_').pop();
1647
+ const formContainer = document.getElementById(`edit-form-${productId}`);
1648
+
1649
+ if (formContainer && formContainer.style.display === 'block') {
1650
+ const initialSubcategory = catSelect.getAttribute('data-initial-subcategory');
1651
+ updateSubcategorySelect(catSelect.id, `edit_subcategory_${productId}`, initialSubcategory);
 
1652
  }
1653
+ catSelect.addEventListener('change', () => updateSubcategorySelect(catSelect.id, `edit_subcategory_${productId}`));
1654
  });
1655
  });
1656
 
 
1678
  currency_code=CURRENCY_CODE
1679
  )
1680
 
1681
+ @app.route('/product/<product_id>')
1682
+ def product_detail(product_id):
1683
  data = load_data()
1684
  all_products = data.get('products', [])
1685
+
1686
+ product = next((p for p in all_products if p.get('id') == product_id), None)
1687
 
1688
+ if not product or not product.get('in_stock', True):
1689
+ logging.warning(f"Attempted access to non-existent or out-of-stock product with ID {product_id}")
 
 
1690
  return "Товар не найден или отсутствует в наличии.", 404
1691
 
1692
  return render_template_string(
 
1709
  total_price = 0
1710
  processed_cart = []
1711
  for item in cart_items:
1712
+ # Check required fields including product_id
1713
+ if not all(k in item for k in ('name', 'price_value', 'quantity', 'price_type', 'product_id')):
1714
+ logging.error(f"Invalid cart item structure received (missing required keys): {item}")
1715
  return jsonify({"error": "Неверный формат товара в корзине."}), 400
1716
  try:
1717
  price_value = float(item['price_value'])
 
1720
  name = item['name']
1721
  color = item.get('color', 'N/A')
1722
  photo = item.get('photo')
1723
+ product_id = item.get('product_id')
1724
 
1725
  if price_value < 0 or quantity <= 0:
1726
  raise ValueError("Invalid price or quantity")
1727
  total_price += price_value * quantity
1728
  processed_cart.append({
1729
+ "product_id": product_id,
1730
  "name": name,
1731
  "price_type": price_type,
1732
  "price_value": round(price_value, 2),
 
1831
  subcategory_to_delete = request.form.get('subcategory_name')
1832
 
1833
  if subcategory_to_delete and category_to_delete:
 
1834
  parent_cat = next((c for c in categories if c['name'] == category_to_delete), None)
1835
  if parent_cat and subcategory_to_delete in parent_cat['subcategories']:
1836
  parent_cat['subcategories'].remove(subcategory_to_delete)
1837
 
 
1838
  updated_count = 0
1839
  for product in products:
1840
  if product.get('category') == category_to_delete and product.get('subcategory') == subcategory_to_delete:
 
1846
  else:
1847
  flash(f"Не удалось найти подкатегорию '{subcategory_to_delete}' в '{category_to_delete}'.", 'error')
1848
  elif category_to_delete and not subcategory_to_delete:
 
1849
  if any(c['name'] == category_to_delete for c in categories):
1850
  data['categories'] = [c for c in categories if c['name'] != category_to_delete]
1851
  categories = data['categories']
 
1963
 
1964
 
1965
  new_product = {
1966
+ 'id': uuid.uuid4().hex, # Assigning stable ID here
1967
  'name': name, 'prices': prices, 'description': description,
1968
  'category': category, 'subcategory': subcategory,
1969
  'photos': photos_list, 'colors': colors,
 
1972
  products.append(new_product)
1973
  data['products'] = products
1974
  save_data(data)
1975
+ logging.info(f"Product '{name}' added with ID {new_product['id']}.")
1976
  flash(f"Товар '{name}' успешно добавлен.", 'success')
1977
 
1978
  elif action == 'edit_product':
1979
+ product_id = request.form.get('product_id')
1980
+
1981
+ product_index = next((i for i, p in enumerate(products) if p.get('id') == product_id), -1)
 
1982
 
1983
+ if product_index == -1:
1984
+ flash(f"Ошибка редактирования: товар с ID '{product_id}' не найден.", 'error')
1985
+ logging.error(f"Product with ID '{product_id}' not found for editing.")
 
 
 
 
 
 
 
1986
  return redirect(url_for('admin'))
1987
 
1988
+ product_to_edit = products[product_index]
1989
+ original_name = product_to_edit.get('name', 'N/A')
1990
+
1991
  product_to_edit['name'] = request.form.get('name', product_to_edit['name']).strip()
1992
  product_to_edit['description'] = request.form.get('description', product_to_edit.get('description', '')).strip()
1993
 
 
2104
  elif not HF_TOKEN_WRITE and photos_files and any(f.filename for f in photos_files):
2105
  flash("HF_TOKEN (write) не настроен. Фотографии не были обновлены.", "warning")
2106
 
2107
+ products[product_index] = product_to_edit
2108
  data['products'] = products
2109
  save_data(data)
2110
+ logging.info(f"Product '{original_name}' (ID {product_id}) updated to '{product_to_edit['name']}'.")
2111
  flash(f"Товар '{product_to_edit['name']}' успешно обновлен.", 'success')
2112
 
2113
 
2114
  elif action == 'delete_product':
2115
+ product_id = request.form.get('product_id')
2116
+
2117
+ product_index = next((i for i, p in enumerate(products) if p.get('id') == product_id), -1)
2118
+
2119
+ if product_index == -1:
2120
+ flash(f"Ошибка удаления: товар с ID '{product_id}' не найден.", 'error')
2121
+ logging.error(f"Product with ID '{product_id}' not found for deletion.")
2122
  return redirect(url_for('admin'))
2123
+
2124
+ deleted_product = products.pop(product_index)
2125
+ product_name = deleted_product.get('name', 'N/A')
2126
+
2127
+ photos_to_delete = deleted_product.get('photos', [])
2128
+ if photos_to_delete and HF_TOKEN_WRITE:
2129
+ logging.info(f"Attempting to delete photos for product '{product_name}' from HF: {photos_to_delete}")
2130
+ try:
2131
+ api = HfApi()
2132
+ api.delete_files(
2133
+ repo_id=REPO_ID,
2134
+ paths_in_repo=[f"photos/{p}" for p in photos_to_delete],
2135
+ repo_type="dataset",
2136
+ token=HF_TOKEN_WRITE,
2137
+ commit_message=f"Delete photos for deleted product {product_name}"
2138
+ )
2139
+ logging.info(f"Photos for product '{product_name}' deleted from HF.")
2140
+ except Exception as e:
2141
+ logging.error(f"Error deleting photos {photos_to_delete} for product '{product_name}' from HF: {e}", exc_info=True)
2142
+ flash(f"Не удалось удалить фото для товара '{product_name}' с сервера. Товар удален локально.", "warning")
2143
+ elif photos_to_delete and not HF_TOKEN_WRITE:
2144
+ logging.warning(f"HF_TOKEN (write) not set. Cannot delete photos {photos_to_delete} for deleted product '{product_name}'.")
2145
+ flash(f"Товар '{product_name}' удален локально, но фото не удалены с сервера (токен не задан).", "warning")
2146
+
2147
+
2148
+ data['products'] = products
2149
+ save_data(data)
2150
+ logging.info(f"Product '{product_name}' (ID {product_id}) deleted.")
2151
+ flash(f"Товар '{product_name}' удален.", 'success')
 
 
 
 
 
2152
 
2153
  else:
2154
  logging.warning(f"Received unknown admin action: {action}")