Spaces:
Running
Running
Update app.py
Browse files
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 |
-
|
| 143 |
-
|
| 144 |
-
data = json.load(file)
|
| 145 |
-
logging.info(f"Local data loaded successfully from {DATA_FILE}")
|
| 146 |
if not isinstance(data, dict):
|
| 147 |
-
|
| 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,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({{
|
| 449 |
-
<button class="product-button add-to-cart" onclick="openQuantityModal({{
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 517 |
let cart = JSON.parse(localStorage.getItem('mekaCart') || '[]');
|
| 518 |
|
| 519 |
|
| 520 |
-
function openModal(
|
| 521 |
-
loadProductDetails(
|
| 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(
|
| 541 |
const modalContent = document.getElementById('modalContent');
|
| 542 |
if (!modalContent) return;
|
| 543 |
modalContent.innerHTML = '<p style="text-align:center; padding: 40px;">Загрузка...</p>';
|
| 544 |
-
fetch('/product/' +
|
| 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(
|
| 576 |
-
|
| 577 |
-
const product =
|
| 578 |
if (!product) {
|
| 579 |
-
console.error("Product not found for
|
| 580 |
alert("Ошибка: товар не найден.");
|
| 581 |
return;
|
| 582 |
}
|
|
@@ -634,7 +609,7 @@ CATALOG_TEMPLATE = '''
|
|
| 634 |
}
|
| 635 |
|
| 636 |
function confirmAddToCart() {
|
| 637 |
-
if (
|
| 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 =
|
| 661 |
if (!product) {
|
| 662 |
alert("Ошибка добавления: товар не найден.");
|
| 663 |
return;
|
| 664 |
}
|
| 665 |
|
| 666 |
-
const cartItemId = `${product.
|
| 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-{{
|
| 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="
|
| 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-{{
|
| 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="
|
| 1436 |
<label>Название *:</label>
|
| 1437 |
<input type="text" name="name" value="{{ product['name'] }}" required>
|
| 1438 |
|
| 1439 |
<label>Цены (минимум одна) *:</label>
|
| 1440 |
-
<div id="edit-price-inputs-{{
|
| 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-{{
|
| 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_{{
|
| 1471 |
name="category"
|
| 1472 |
data-initial-subcategory="{{ product.get('subcategory', 'Без подкатегории') }}"
|
| 1473 |
-
onchange="updateSubcategorySelect('edit_category_{{
|
| 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_{{
|
| 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-{{
|
| 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-{{
|
| 1515 |
<br>
|
| 1516 |
<div style="margin-top: 15px;">
|
| 1517 |
-
<input type="checkbox" id="edit_in_stock_{{
|
| 1518 |
-
<label for="edit_in_stock_{{
|
| 1519 |
</div>
|
| 1520 |
<div style="margin-top: 5px;">
|
| 1521 |
-
<input type="checkbox" id="edit_is_top_{{
|
| 1522 |
-
<label for="edit_is_top_{{
|
| 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 (
|
|
|
|
|
|
|
| 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
|
| 1582 |
-
const catSelect = document.getElementById(`edit_category_${
|
| 1583 |
if (catSelect) {
|
| 1584 |
const initialSubcategory = catSelect.getAttribute('data-initial-subcategory');
|
| 1585 |
-
updateSubcategorySelect(`edit_category_${
|
| 1586 |
}
|
| 1587 |
}
|
| 1588 |
}
|
|
@@ -1661,15 +1642,15 @@ ADMIN_TEMPLATE = '''
|
|
| 1661 |
addCatSelect.addEventListener('change', () => updateSubcategorySelect('add_category', 'add_subcategory'));
|
| 1662 |
}
|
| 1663 |
|
| 1664 |
-
document.querySelectorAll('[id^="
|
| 1665 |
-
|
| 1666 |
-
|
| 1667 |
-
|
| 1668 |
-
|
| 1669 |
-
|
| 1670 |
-
|
| 1671 |
-
}
|
| 1672 |
}
|
|
|
|
| 1673 |
});
|
| 1674 |
});
|
| 1675 |
|
|
@@ -1697,17 +1678,15 @@ def catalog():
|
|
| 1697 |
currency_code=CURRENCY_CODE
|
| 1698 |
)
|
| 1699 |
|
| 1700 |
-
@app.route('/product/<
|
| 1701 |
-
def product_detail(
|
| 1702 |
data = load_data()
|
| 1703 |
all_products = data.get('products', [])
|
| 1704 |
-
|
| 1705 |
-
|
| 1706 |
|
| 1707 |
-
|
| 1708 |
-
product
|
| 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 |
-
|
| 1734 |
-
|
|
|
|
| 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 |
-
|
| 2000 |
-
|
| 2001 |
-
|
| 2002 |
-
return redirect(url_for('admin'))
|
| 2003 |
|
| 2004 |
-
|
| 2005 |
-
|
| 2006 |
-
|
| 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[
|
| 2133 |
data['products'] = products
|
| 2134 |
save_data(data)
|
| 2135 |
-
logging.info(f"Product '{original_name}' (
|
| 2136 |
flash(f"Товар '{product_to_edit['name']}' успешно обновлен.", 'success')
|
| 2137 |
|
| 2138 |
|
| 2139 |
elif action == 'delete_product':
|
| 2140 |
-
|
| 2141 |
-
|
| 2142 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2143 |
return redirect(url_for('admin'))
|
| 2144 |
-
|
| 2145 |
-
|
| 2146 |
-
|
| 2147 |
-
|
| 2148 |
-
|
| 2149 |
-
|
| 2150 |
-
|
| 2151 |
-
|
| 2152 |
-
|
| 2153 |
-
|
| 2154 |
-
|
| 2155 |
-
|
| 2156 |
-
|
| 2157 |
-
|
| 2158 |
-
|
| 2159 |
-
|
| 2160 |
-
|
| 2161 |
-
|
| 2162 |
-
|
| 2163 |
-
|
| 2164 |
-
|
| 2165 |
-
|
| 2166 |
-
|
| 2167 |
-
|
| 2168 |
-
|
| 2169 |
-
|
| 2170 |
-
|
| 2171 |
-
|
| 2172 |
-
|
| 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}")
|