Update app.py
Browse files
app.py
CHANGED
|
@@ -30,22 +30,20 @@ def load_data():
|
|
| 30 |
data = json.load(file)
|
| 31 |
logging.info("Данные успешно загружены из JSON")
|
| 32 |
if not isinstance(data, dict) or 'products' not in data or 'categories' not in data:
|
| 33 |
-
return {'products': [], 'categories': []
|
| 34 |
-
if 'options' not in data:
|
| 35 |
-
data['options'] = []
|
| 36 |
return data
|
| 37 |
except FileNotFoundError:
|
| 38 |
logging.warning("Локальный файл базы данных не найден после скачивания.")
|
| 39 |
-
return {'products': [], 'categories': []
|
| 40 |
except json.JSONDecodeError:
|
| 41 |
logging.error("Ошибка: Невозможно декодировать JSON файл.")
|
| 42 |
-
return {'products': [], 'categories': []
|
| 43 |
except RepositoryNotFoundError:
|
| 44 |
logging.error("Репозиторий не найден. Создание локальной базы данных.")
|
| 45 |
-
return {'products': [], 'categories': []
|
| 46 |
except Exception as e:
|
| 47 |
logging.error(f"Произошла ошибка при загрузке данных: {e}")
|
| 48 |
-
return {'products': [], 'categories': []
|
| 49 |
|
| 50 |
def save_data(data):
|
| 51 |
try:
|
|
@@ -100,7 +98,6 @@ def menu():
|
|
| 100 |
data = load_data()
|
| 101 |
products = data['products']
|
| 102 |
categories = data['categories']
|
| 103 |
-
options = data['options']
|
| 104 |
|
| 105 |
menu_html = '''
|
| 106 |
<!DOCTYPE html>
|
|
@@ -496,7 +493,6 @@ def menu():
|
|
| 496 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/Swiper/10.2.0/swiper-bundle.min.js"></script>
|
| 497 |
<script>
|
| 498 |
const products = {{ products|tojson }};
|
| 499 |
-
const options = {{ options|tojson }};
|
| 500 |
let selectedProductIndex = null;
|
| 501 |
|
| 502 |
function toggleTheme() {
|
|
@@ -547,12 +543,13 @@ def menu():
|
|
| 547 |
selectedProductIndex = index;
|
| 548 |
const product = products[index];
|
| 549 |
const optionsList = document.getElementById('optionsList');
|
| 550 |
-
optionsList.innerHTML = options.
|
| 551 |
-
|
| 552 |
-
<
|
| 553 |
-
|
| 554 |
-
|
| 555 |
-
|
|
|
|
| 556 |
document.getElementById('optionsModal').style.display = 'block';
|
| 557 |
document.getElementById('quantityInput').value = 1;
|
| 558 |
}
|
|
@@ -572,7 +569,7 @@ def menu():
|
|
| 572 |
price: parseFloat(cb.dataset.price)
|
| 573 |
}));
|
| 574 |
|
| 575 |
-
const cartItemId = `${product.name}-${Date.now()}`;
|
| 576 |
cart.push({
|
| 577 |
id: cartItemId,
|
| 578 |
name: product.name,
|
|
@@ -678,7 +675,7 @@ def menu():
|
|
| 678 |
</body>
|
| 679 |
</html>
|
| 680 |
'''
|
| 681 |
-
return render_template_string(menu_html, products=products, categories=categories,
|
| 682 |
|
| 683 |
@app.route('/product/<int:index>')
|
| 684 |
def product_detail(index):
|
|
@@ -716,6 +713,13 @@ def product_detail(index):
|
|
| 716 |
<p><strong>Категория:</strong> {{ product.get('category', 'Без категории') }}</p>
|
| 717 |
<p><strong>Цена:</strong> {{ product['price'] }} с</p>
|
| 718 |
<p><strong>Описание:</strong> {{ product['description'] }}</p>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 719 |
</div>
|
| 720 |
'''
|
| 721 |
return render_template_string(detail_html, product=product, repo_id=REPO_ID)
|
|
@@ -725,7 +729,6 @@ def admin():
|
|
| 725 |
data = load_data()
|
| 726 |
products = data['products']
|
| 727 |
categories = data['categories']
|
| 728 |
-
options = data['options']
|
| 729 |
|
| 730 |
if request.method == 'POST':
|
| 731 |
action = request.form.get('action')
|
|
@@ -747,36 +750,16 @@ def admin():
|
|
| 747 |
save_data(data)
|
| 748 |
return redirect(url_for('admin'))
|
| 749 |
|
| 750 |
-
elif action == 'add_option':
|
| 751 |
-
option_name = request.form.get('option_name')
|
| 752 |
-
option_price = float(request.form.get('option_price', '0').replace(',', '.'))
|
| 753 |
-
if option_name and option_name not in [opt['name'] for opt in options]:
|
| 754 |
-
options.append({'name': option_name, 'price': option_price})
|
| 755 |
-
save_data(data)
|
| 756 |
-
return redirect(url_for('admin'))
|
| 757 |
-
return "Ошибка: Опция уже существует или не указано название", 400
|
| 758 |
-
|
| 759 |
-
elif action == 'delete_option':
|
| 760 |
-
option_index = int(request.form.get('option_index'))
|
| 761 |
-
del options[option_index]
|
| 762 |
-
save_data(data)
|
| 763 |
-
return redirect(url_for('admin'))
|
| 764 |
-
|
| 765 |
-
elif action == 'edit_option':
|
| 766 |
-
option_index = int(request.form.get('option_index'))
|
| 767 |
-
option_name = request.form.get('option_name')
|
| 768 |
-
option_price = float(request.form.get('option_price').replace(',', '.'))
|
| 769 |
-
options[option_index] = {'name': option_name, 'price': option_price}
|
| 770 |
-
save_data(data)
|
| 771 |
-
return redirect(url_for('admin'))
|
| 772 |
-
|
| 773 |
elif action == 'add':
|
| 774 |
name = request.form.get('name')
|
| 775 |
price = request.form.get('price')
|
| 776 |
description = request.form.get('description')
|
| 777 |
category = request.form.get('category')
|
| 778 |
photos_files = request.files.getlist('photos')
|
|
|
|
|
|
|
| 779 |
photos_list = []
|
|
|
|
| 780 |
|
| 781 |
if photos_files:
|
| 782 |
for photo in photos_files[:10]:
|
|
@@ -799,6 +782,13 @@ def admin():
|
|
| 799 |
if os.path.exists(temp_path):
|
| 800 |
os.remove(temp_path)
|
| 801 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 802 |
if not name or not price or not description:
|
| 803 |
return "Ошибка: Заполните все обязательные поля", 400
|
| 804 |
|
|
@@ -808,7 +798,8 @@ def admin():
|
|
| 808 |
'price': price,
|
| 809 |
'description': description,
|
| 810 |
'category': category if category in categories else 'Без категории',
|
| 811 |
-
'photos': photos_list
|
|
|
|
| 812 |
}
|
| 813 |
products.append(new_product)
|
| 814 |
save_data(data)
|
|
@@ -821,6 +812,8 @@ def admin():
|
|
| 821 |
description = request.form.get('description')
|
| 822 |
category = request.form.get('category')
|
| 823 |
photos_files = request.files.getlist('photos')
|
|
|
|
|
|
|
| 824 |
|
| 825 |
if photos_files and any(photo.filename for photo in photos_files):
|
| 826 |
new_photos_list = []
|
|
@@ -845,10 +838,19 @@ def admin():
|
|
| 845 |
os.remove(temp_path)
|
| 846 |
products[index]['photos'] = new_photos_list
|
| 847 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 848 |
products[index]['name'] = name
|
| 849 |
products[index]['price'] = float(price.replace(',', '.'))
|
| 850 |
products[index]['description'] = description
|
| 851 |
products[index]['category'] = category if category in categories else 'Без категории'
|
|
|
|
| 852 |
save_data(data)
|
| 853 |
return redirect(url_for('admin'))
|
| 854 |
|
|
@@ -949,11 +951,11 @@ def admin():
|
|
| 949 |
background-color: #dc2626;
|
| 950 |
box-shadow: 0 4px 15px rgba(220, 38, 38, 0.4);
|
| 951 |
}
|
| 952 |
-
.product-list, .category-list
|
| 953 |
display: grid;
|
| 954 |
gap: 20px;
|
| 955 |
}
|
| 956 |
-
.product-item, .category-item
|
| 957 |
background: #fff;
|
| 958 |
padding: 20px;
|
| 959 |
border-radius: 15px;
|
|
@@ -965,6 +967,17 @@ def admin():
|
|
| 965 |
background: #f7fafc;
|
| 966 |
border-radius: 10px;
|
| 967 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 968 |
</style>
|
| 969 |
</head>
|
| 970 |
<body>
|
|
@@ -991,6 +1004,14 @@ def admin():
|
|
| 991 |
</select>
|
| 992 |
<label>Фотографии (до 10):</label>
|
| 993 |
<input type="file" name="photos" accept="image/*" multiple>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 994 |
<button type="submit">Добавить блюдо</button>
|
| 995 |
</form>
|
| 996 |
|
|
@@ -1016,41 +1037,6 @@ def admin():
|
|
| 1016 |
{% endfor %}
|
| 1017 |
</div>
|
| 1018 |
|
| 1019 |
-
<h1>Управление опциями</h1>
|
| 1020 |
-
<form method="POST">
|
| 1021 |
-
<input type="hidden" name="action" value="add_option">
|
| 1022 |
-
<label>Название опции:</label>
|
| 1023 |
-
<input type="text" name="option_name" required>
|
| 1024 |
-
<label>Дополнительная стоимость:</label>
|
| 1025 |
-
<input type="number" name="option_price" step="0.01" value="0">
|
| 1026 |
-
<button type="submit">Добавить</button>
|
| 1027 |
-
</form>
|
| 1028 |
-
|
| 1029 |
-
<h2>Список опций</h2>
|
| 1030 |
-
<div class="options-list">
|
| 1031 |
-
{% for option in options %}
|
| 1032 |
-
<div class="option-item">
|
| 1033 |
-
<details>
|
| 1034 |
-
<summary>{{ option['name'] }} ({{ option['price'] }} с)</summary>
|
| 1035 |
-
<form method="POST" class="edit-form">
|
| 1036 |
-
<input type="hidden" name="action" value="edit_option">
|
| 1037 |
-
<input type="hidden" name="option_index" value="{{ loop.index0 }}">
|
| 1038 |
-
<label>Название:</label>
|
| 1039 |
-
<input type="text" name="option_name" value="{{ option['name'] }}" required>
|
| 1040 |
-
<label>Стоимость:</label>
|
| 1041 |
-
<input type="number" name="option_price" step="0.01" value="{{ option['price'] }}" required>
|
| 1042 |
-
<button type="submit">Сохранить</button>
|
| 1043 |
-
</form>
|
| 1044 |
-
</details>
|
| 1045 |
-
<form method="POST" style="display: inline;">
|
| 1046 |
-
<input type="hidden" name="action" value="delete_option">
|
| 1047 |
-
<input type="hidden" name="option_index" value="{{ loop.index0 }}">
|
| 1048 |
-
<button type="submit" class="delete-button">Удалить</button>
|
| 1049 |
-
</form>
|
| 1050 |
-
</div>
|
| 1051 |
-
{% endfor %}
|
| 1052 |
-
</div>
|
| 1053 |
-
|
| 1054 |
<h2>Управление базой данных</h2>
|
| 1055 |
<form method="POST" action="{{ url_for('backup') }}" style="display: inline;">
|
| 1056 |
<button type="submit">Создать копию</button>
|
|
@@ -1067,6 +1053,13 @@ def admin():
|
|
| 1067 |
<p><strong>Категория:</strong> {{ product.get('category', 'Без категории') }}</p>
|
| 1068 |
<p><strong>Цена:</strong> {{ product['price'] }} с</p>
|
| 1069 |
<p><strong>Описание:</strong> {{ product['description'] }}</p>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1070 |
{% if product.get('photos') and product['photos']|length > 0 %}
|
| 1071 |
<div style="display: flex; flex-wrap: wrap; gap: 10px;">
|
| 1072 |
{% for photo in product['photos'] %}
|
|
@@ -1096,6 +1089,16 @@ def admin():
|
|
| 1096 |
</select>
|
| 1097 |
<label>Фотографии (до 10):</label>
|
| 1098 |
<input type="file" name="photos" accept="image/*" multiple>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1099 |
<button type="submit">Сохранить</button>
|
| 1100 |
</form>
|
| 1101 |
</details>
|
|
@@ -1108,10 +1111,22 @@ def admin():
|
|
| 1108 |
{% endfor %}
|
| 1109 |
</div>
|
| 1110 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1111 |
</body>
|
| 1112 |
</html>
|
| 1113 |
'''
|
| 1114 |
-
return render_template_string(admin_html, products=products, categories=categories,
|
| 1115 |
|
| 1116 |
@app.route('/backup', methods=['POST'])
|
| 1117 |
def backup():
|
|
|
|
| 30 |
data = json.load(file)
|
| 31 |
logging.info("Данные успешно загружены из JSON")
|
| 32 |
if not isinstance(data, dict) or 'products' not in data or 'categories' not in data:
|
| 33 |
+
return {'products': [], 'categories': []}
|
|
|
|
|
|
|
| 34 |
return data
|
| 35 |
except FileNotFoundError:
|
| 36 |
logging.warning("Локальный файл базы данных не найден после скачивания.")
|
| 37 |
+
return {'products': [], 'categories': []}
|
| 38 |
except json.JSONDecodeError:
|
| 39 |
logging.error("Ошибка: Невозможно декодировать JSON файл.")
|
| 40 |
+
return {'products': [], 'categories': []}
|
| 41 |
except RepositoryNotFoundError:
|
| 42 |
logging.error("Репозиторий не найден. Создание локальной базы данных.")
|
| 43 |
+
return {'products': [], 'categories': []}
|
| 44 |
except Exception as e:
|
| 45 |
logging.error(f"Произошла ошибка при загрузке данных: {e}")
|
| 46 |
+
return {'products': [], 'categories': []}
|
| 47 |
|
| 48 |
def save_data(data):
|
| 49 |
try:
|
|
|
|
| 98 |
data = load_data()
|
| 99 |
products = data['products']
|
| 100 |
categories = data['categories']
|
|
|
|
| 101 |
|
| 102 |
menu_html = '''
|
| 103 |
<!DOCTYPE html>
|
|
|
|
| 493 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/Swiper/10.2.0/swiper-bundle.min.js"></script>
|
| 494 |
<script>
|
| 495 |
const products = {{ products|tojson }};
|
|
|
|
| 496 |
let selectedProductIndex = null;
|
| 497 |
|
| 498 |
function toggleTheme() {
|
|
|
|
| 543 |
selectedProductIndex = index;
|
| 544 |
const product = products[index];
|
| 545 |
const optionsList = document.getElementById('optionsList');
|
| 546 |
+
optionsList.innerHTML = (product.options && product.options.length > 0) ?
|
| 547 |
+
product.options.map(option => `
|
| 548 |
+
<label class="options-checkbox">
|
| 549 |
+
<input type="checkbox" class="option-checkbox" data-name="${option.name}" data-price="${option.price}">
|
| 550 |
+
${option.name} (+${option.price} с)
|
| 551 |
+
</label>
|
| 552 |
+
`).join('') : '<p>Нет дополнительных опций</p>';
|
| 553 |
document.getElementById('optionsModal').style.display = 'block';
|
| 554 |
document.getElementById('quantityInput').value = 1;
|
| 555 |
}
|
|
|
|
| 569 |
price: parseFloat(cb.dataset.price)
|
| 570 |
}));
|
| 571 |
|
| 572 |
+
const cartItemId = `${product.name}-${Date.now()}`;
|
| 573 |
cart.push({
|
| 574 |
id: cartItemId,
|
| 575 |
name: product.name,
|
|
|
|
| 675 |
</body>
|
| 676 |
</html>
|
| 677 |
'''
|
| 678 |
+
return render_template_string(menu_html, products=products, categories=categories, repo_id=REPO_ID)
|
| 679 |
|
| 680 |
@app.route('/product/<int:index>')
|
| 681 |
def product_detail(index):
|
|
|
|
| 713 |
<p><strong>Категория:</strong> {{ product.get('category', 'Без категории') }}</p>
|
| 714 |
<p><strong>Цена:</strong> {{ product['price'] }} с</p>
|
| 715 |
<p><strong>Описание:</strong> {{ product['description'] }}</p>
|
| 716 |
+
<p><strong>Дополнительные опции:</strong>
|
| 717 |
+
{% if product.get('options') and product['options']|length > 0 %}
|
| 718 |
+
{{ product['options']|map(attribute='name')|join(', ') }}
|
| 719 |
+
{% else %}
|
| 720 |
+
Нет опций
|
| 721 |
+
{% endif %}
|
| 722 |
+
</p>
|
| 723 |
</div>
|
| 724 |
'''
|
| 725 |
return render_template_string(detail_html, product=product, repo_id=REPO_ID)
|
|
|
|
| 729 |
data = load_data()
|
| 730 |
products = data['products']
|
| 731 |
categories = data['categories']
|
|
|
|
| 732 |
|
| 733 |
if request.method == 'POST':
|
| 734 |
action = request.form.get('action')
|
|
|
|
| 750 |
save_data(data)
|
| 751 |
return redirect(url_for('admin'))
|
| 752 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 753 |
elif action == 'add':
|
| 754 |
name = request.form.get('name')
|
| 755 |
price = request.form.get('price')
|
| 756 |
description = request.form.get('description')
|
| 757 |
category = request.form.get('category')
|
| 758 |
photos_files = request.files.getlist('photos')
|
| 759 |
+
option_names = request.form.getlist('option_names')
|
| 760 |
+
option_prices = request.form.getlist('option_prices')
|
| 761 |
photos_list = []
|
| 762 |
+
options_list = []
|
| 763 |
|
| 764 |
if photos_files:
|
| 765 |
for photo in photos_files[:10]:
|
|
|
|
| 782 |
if os.path.exists(temp_path):
|
| 783 |
os.remove(temp_path)
|
| 784 |
|
| 785 |
+
for opt_name, opt_price in zip(option_names, option_prices):
|
| 786 |
+
if opt_name and opt_price:
|
| 787 |
+
options_list.append({
|
| 788 |
+
'name': opt_name,
|
| 789 |
+
'price': float(opt_price.replace(',', '.'))
|
| 790 |
+
})
|
| 791 |
+
|
| 792 |
if not name or not price or not description:
|
| 793 |
return "Ошибка: Заполните все обязательные поля", 400
|
| 794 |
|
|
|
|
| 798 |
'price': price,
|
| 799 |
'description': description,
|
| 800 |
'category': category if category in categories else 'Без категории',
|
| 801 |
+
'photos': photos_list,
|
| 802 |
+
'options': options_list
|
| 803 |
}
|
| 804 |
products.append(new_product)
|
| 805 |
save_data(data)
|
|
|
|
| 812 |
description = request.form.get('description')
|
| 813 |
category = request.form.get('category')
|
| 814 |
photos_files = request.files.getlist('photos')
|
| 815 |
+
option_names = request.form.getlist('option_names')
|
| 816 |
+
option_prices = request.form.getlist('option_prices')
|
| 817 |
|
| 818 |
if photos_files and any(photo.filename for photo in photos_files):
|
| 819 |
new_photos_list = []
|
|
|
|
| 838 |
os.remove(temp_path)
|
| 839 |
products[index]['photos'] = new_photos_list
|
| 840 |
|
| 841 |
+
options_list = []
|
| 842 |
+
for opt_name, opt_price in zip(option_names, option_prices):
|
| 843 |
+
if opt_name and opt_price:
|
| 844 |
+
options_list.append({
|
| 845 |
+
'name': opt_name,
|
| 846 |
+
'price': float(opt_price.replace(',', '.'))
|
| 847 |
+
})
|
| 848 |
+
|
| 849 |
products[index]['name'] = name
|
| 850 |
products[index]['price'] = float(price.replace(',', '.'))
|
| 851 |
products[index]['description'] = description
|
| 852 |
products[index]['category'] = category if category in categories else 'Без категории'
|
| 853 |
+
products[index]['options'] = options_list
|
| 854 |
save_data(data)
|
| 855 |
return redirect(url_for('admin'))
|
| 856 |
|
|
|
|
| 951 |
background-color: #dc2626;
|
| 952 |
box-shadow: 0 4px 15px rgba(220, 38, 38, 0.4);
|
| 953 |
}
|
| 954 |
+
.product-list, .category-list {
|
| 955 |
display: grid;
|
| 956 |
gap: 20px;
|
| 957 |
}
|
| 958 |
+
.product-item, .category-item {
|
| 959 |
background: #fff;
|
| 960 |
padding: 20px;
|
| 961 |
border-radius: 15px;
|
|
|
|
| 967 |
background: #f7fafc;
|
| 968 |
border-radius: 10px;
|
| 969 |
}
|
| 970 |
+
.option-input-group {
|
| 971 |
+
display: flex;
|
| 972 |
+
gap: 10px;
|
| 973 |
+
margin-top: 5px;
|
| 974 |
+
}
|
| 975 |
+
.add-option-btn {
|
| 976 |
+
background-color: #10b981;
|
| 977 |
+
}
|
| 978 |
+
.add-option-btn:hover {
|
| 979 |
+
background-color: #059669;
|
| 980 |
+
}
|
| 981 |
</style>
|
| 982 |
</head>
|
| 983 |
<body>
|
|
|
|
| 1004 |
</select>
|
| 1005 |
<label>Фотографии (до 10):</label>
|
| 1006 |
<input type="file" name="photos" accept="image/*" multiple>
|
| 1007 |
+
<label>Дополнительные опции:</label>
|
| 1008 |
+
<div id="option-inputs">
|
| 1009 |
+
<div class="option-input-group">
|
| 1010 |
+
<input type="text" name="option_names" placeholder="Название опции">
|
| 1011 |
+
<input type="number" name="option_prices" step="0.01" value="0" placeholder="Цена">
|
| 1012 |
+
</div>
|
| 1013 |
+
</div>
|
| 1014 |
+
<button type="button" class="add-option-btn" onclick="addOptionInput()">Добавить опцию</button>
|
| 1015 |
<button type="submit">Добавить блюдо</button>
|
| 1016 |
</form>
|
| 1017 |
|
|
|
|
| 1037 |
{% endfor %}
|
| 1038 |
</div>
|
| 1039 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1040 |
<h2>Управление базой данных</h2>
|
| 1041 |
<form method="POST" action="{{ url_for('backup') }}" style="display: inline;">
|
| 1042 |
<button type="submit">Создать копию</button>
|
|
|
|
| 1053 |
<p><strong>Категория:</strong> {{ product.get('category', 'Без категории') }}</p>
|
| 1054 |
<p><strong>Цена:</strong> {{ product['price'] }} с</p>
|
| 1055 |
<p><strong>Описание:</strong> {{ product['description'] }}</p>
|
| 1056 |
+
<p><strong>Опции:</strong>
|
| 1057 |
+
{% if product.get('options') and product['options']|length > 0 %}
|
| 1058 |
+
{{ product['options']|map(attribute='name')|join(', ') }}
|
| 1059 |
+
{% else %}
|
| 1060 |
+
Нет опций
|
| 1061 |
+
{% endif %}
|
| 1062 |
+
</p>
|
| 1063 |
{% if product.get('photos') and product['photos']|length > 0 %}
|
| 1064 |
<div style="display: flex; flex-wrap: wrap; gap: 10px;">
|
| 1065 |
{% for photo in product['photos'] %}
|
|
|
|
| 1089 |
</select>
|
| 1090 |
<label>Фотографии (до 10):</label>
|
| 1091 |
<input type="file" name="photos" accept="image/*" multiple>
|
| 1092 |
+
<label>Дополнительные опции:</label>
|
| 1093 |
+
<div id="edit-option-inputs-{{ loop.index0 }}">
|
| 1094 |
+
{% for option in product.get('options', []) %}
|
| 1095 |
+
<div class="option-input-group">
|
| 1096 |
+
<input type="text" name="option_names" value="{{ option['name'] }}">
|
| 1097 |
+
<input type="number" name="option_prices" step="0.01" value="{{ option['price'] }}">
|
| 1098 |
+
</div>
|
| 1099 |
+
{% endfor %}
|
| 1100 |
+
</div>
|
| 1101 |
+
<button type="button" class="add-option-btn" onclick="addOptionInput('edit-option-inputs-{{ loop.index0 }}')">Добавить опцию</button>
|
| 1102 |
<button type="submit">Сохранить</button>
|
| 1103 |
</form>
|
| 1104 |
</details>
|
|
|
|
| 1111 |
{% endfor %}
|
| 1112 |
</div>
|
| 1113 |
</div>
|
| 1114 |
+
<script>
|
| 1115 |
+
function addOptionInput(containerId = 'option-inputs') {
|
| 1116 |
+
const container = document.getElementById(containerId);
|
| 1117 |
+
const newInput = document.createElement('div');
|
| 1118 |
+
newInput.className = 'option-input-group';
|
| 1119 |
+
newInput.innerHTML = `
|
| 1120 |
+
<input type="text" name="option_names" placeholder="Название опции">
|
| 1121 |
+
<input type="number" name="option_prices" step="0.01" value="0" placeholder="Цена">
|
| 1122 |
+
`;
|
| 1123 |
+
container.appendChild(newInput);
|
| 1124 |
+
}
|
| 1125 |
+
</script>
|
| 1126 |
</body>
|
| 1127 |
</html>
|
| 1128 |
'''
|
| 1129 |
+
return render_template_string(admin_html, products=products, categories=categories, repo_id=REPO_ID)
|
| 1130 |
|
| 1131 |
@app.route('/backup', methods=['POST'])
|
| 1132 |
def backup():
|