Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
|
@@ -150,25 +150,40 @@ def load_data():
|
|
| 150 |
if 'categories' not in data: data['categories'] = []
|
| 151 |
if 'orders' not in data: data['orders'] = {}
|
| 152 |
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 156 |
|
|
|
|
| 157 |
for product in data['products']:
|
| 158 |
if 'subcategory' not in product:
|
| 159 |
-
product['subcategory'] =
|
|
|
|
| 160 |
if 'prices' not in product or not isinstance(product['prices'], list):
|
| 161 |
-
if 'price' in product:
|
| 162 |
product['prices'] = [{'type': 'шт', 'value': product.pop('price')}]
|
| 163 |
else:
|
| 164 |
product['prices'] = []
|
|
|
|
| 165 |
product['prices'] = [p for p in product['prices'] if isinstance(p, dict) and 'type' in p and 'value' in p]
|
| 166 |
for p in product['prices']:
|
| 167 |
try:
|
| 168 |
p['value'] = round(float(p['value']), 2)
|
| 169 |
except (ValueError, TypeError):
|
| 170 |
p['value'] = 0.0
|
| 171 |
-
if not product['prices']:
|
| 172 |
product['prices'] = [{'type': 'шт', 'value': 0.0}]
|
| 173 |
|
| 174 |
return data
|
|
@@ -182,6 +197,7 @@ def load_data():
|
|
| 182 |
with open(DATA_FILE, 'r', encoding='utf-8') as file:
|
| 183 |
data = json.load(file)
|
| 184 |
logging.info(f"Data loaded successfully from {DATA_FILE} after download.")
|
|
|
|
| 185 |
if not isinstance(data, dict):
|
| 186 |
logging.error(f"Downloaded {DATA_FILE} is not a dictionary. Using default.")
|
| 187 |
return default_data
|
|
@@ -189,15 +205,26 @@ def load_data():
|
|
| 189 |
if 'categories' not in data: data['categories'] = []
|
| 190 |
if 'orders' not in data: data['orders'] = {}
|
| 191 |
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 195 |
|
| 196 |
for product in data['products']:
|
| 197 |
if 'subcategory' not in product:
|
| 198 |
-
product['subcategory'] =
|
|
|
|
| 199 |
if 'prices' not in product or not isinstance(product['prices'], list):
|
| 200 |
-
if 'price' in product:
|
| 201 |
product['prices'] = [{'type': 'шт', 'value': product.pop('price')}]
|
| 202 |
else:
|
| 203 |
product['prices'] = []
|
|
@@ -266,14 +293,13 @@ CATALOG_TEMPLATE = '''
|
|
| 266 |
.header { display: flex; justify-content: space-between; align-items: center; padding: 15px 0; border-bottom: 1px solid #e0e0e0; }
|
| 267 |
.header h1 { font-size: 1.8rem; font-weight: 600; color: #e3a84f; }
|
| 268 |
.store-address { padding: 15px; text-align: center; background-color: #f9f9f9; margin: 20px 0; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.03); font-size: 1rem; color: #666; }
|
| 269 |
-
.filters-
|
| 270 |
-
.filters-container, .sub-filters-container { display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; }
|
| 271 |
-
.sub-filters-container { margin-top: 15px; padding-top: 15px; border-top: 1px dashed #e0e0e0;}
|
| 272 |
.search-container { margin: 20px 0; text-align: center; }
|
| 273 |
#search-input { width: 90%; max-width: 600px; padding: 12px 18px; font-size: 1rem; border: 1px solid #e0e0e0; border-radius: 25px; outline: none; box-shadow: 0 2px 5px rgba(0,0,0,0.03); transition: all 0.3s ease; }
|
| 274 |
#search-input:focus { border-color: #e3a84f; box-shadow: 0 0 0 3px rgba(227, 168, 79, 0.15); }
|
| 275 |
-
.category-filter
|
| 276 |
-
.category-filter.
|
|
|
|
| 277 |
.products-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 20px; padding: 10px; }
|
| 278 |
@media (min-width: 600px) { .products-grid { grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); } }
|
| 279 |
@media (min-width: 900px) { .products-grid { grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); } }
|
|
@@ -340,14 +366,14 @@ CATALOG_TEMPLATE = '''
|
|
| 340 |
|
| 341 |
<div class="store-address">Наш адрес: {{ store_address }}</div>
|
| 342 |
|
| 343 |
-
<div class="filters-
|
| 344 |
-
<
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
|
|
|
| 348 |
{% endfor %}
|
| 349 |
-
|
| 350 |
-
<div class="sub-filters-container" id="sub-filters-container"></div>
|
| 351 |
</div>
|
| 352 |
|
| 353 |
<div class="search-container">
|
|
@@ -360,7 +386,7 @@ CATALOG_TEMPLATE = '''
|
|
| 360 |
data-name="{{ product['name']|lower }}"
|
| 361 |
data-description="{{ product.get('description', '')|lower }}"
|
| 362 |
data-category="{{ product.get('category', 'Без категории') }}"
|
| 363 |
-
data-subcategory="{{ product.get('subcategory', '
|
| 364 |
{% if product.get('is_top', False) %}
|
| 365 |
<span class="top-product-indicator"><i class="fas fa-star"></i> Топ</span>
|
| 366 |
{% endif %}
|
|
@@ -384,7 +410,7 @@ CATALOG_TEMPLATE = '''
|
|
| 384 |
<span class="price-item">Нет цены</span>
|
| 385 |
{% endif %}
|
| 386 |
</div>
|
| 387 |
-
<p class="product-description">{{ product.get('
|
| 388 |
</div>
|
| 389 |
<div class="product-actions">
|
| 390 |
<button class="product-button" onclick="openModal({{ loop.index0 }})">Подробнее</button>
|
|
@@ -453,13 +479,11 @@ CATALOG_TEMPLATE = '''
|
|
| 453 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/Swiper/10.2.0/swiper-bundle.min.js"></script>
|
| 454 |
<script>
|
| 455 |
const products = {{ products|tojson }};
|
| 456 |
-
const categoriesData = {{ categories|tojson }};
|
| 457 |
const repoId = '{{ repo_id }}';
|
| 458 |
const currencyCode = '{{ currency_code }}';
|
| 459 |
let selectedProductIndex = null;
|
| 460 |
let cart = JSON.parse(localStorage.getItem('mekaCart') || '[]');
|
| 461 |
-
|
| 462 |
-
let activeSubcategory = 'all';
|
| 463 |
|
| 464 |
function openModal(index) {
|
| 465 |
loadProductDetails(index);
|
|
@@ -556,7 +580,7 @@ CATALOG_TEMPLATE = '''
|
|
| 556 |
const option = document.createElement('option');
|
| 557 |
option.value = price_item.type.trim();
|
| 558 |
option.text = `${price_item.value.toFixed(2)} ${currencyCode} / ${price_item.type.trim()}`;
|
| 559 |
-
option.dataset.priceValue = price_item.value;
|
| 560 |
priceTypeSelect.appendChild(option);
|
| 561 |
});
|
| 562 |
priceTypeSelect.style.display = 'block';
|
|
@@ -565,7 +589,7 @@ CATALOG_TEMPLATE = '''
|
|
| 565 |
priceTypeSelect.style.display = 'none';
|
| 566 |
if(priceTypeLabel) priceTypeLabel.style.display = 'none';
|
| 567 |
alert("Для этого товара нет доступных цен.");
|
| 568 |
-
return;
|
| 569 |
}
|
| 570 |
|
| 571 |
|
|
@@ -586,7 +610,7 @@ CATALOG_TEMPLATE = '''
|
|
| 586 |
const color = colorSelect.style.display !== 'none' && colorSelect.value ? colorSelect.value : 'N/A';
|
| 587 |
const priceTypeSelect = document.getElementById('priceTypeSelect');
|
| 588 |
const selectedPriceOption = priceTypeSelect.options[priceTypeSelect.selectedIndex];
|
| 589 |
-
const priceType = selectedPriceOption ? selectedPriceOption.value : 'шт';
|
| 590 |
const priceValue = selectedPriceOption ? parseFloat(selectedPriceOption.dataset.priceValue) : null;
|
| 591 |
|
| 592 |
|
|
@@ -607,7 +631,7 @@ CATALOG_TEMPLATE = '''
|
|
| 607 |
return;
|
| 608 |
}
|
| 609 |
|
| 610 |
-
const cartItemId = `${product.name}-${priceType}-${color}`;
|
| 611 |
const existingItemIndex = cart.findIndex(item => item.id === cartItemId);
|
| 612 |
|
| 613 |
if (existingItemIndex > -1) {
|
|
@@ -616,8 +640,8 @@ CATALOG_TEMPLATE = '''
|
|
| 616 |
cart.push({
|
| 617 |
id: cartItemId,
|
| 618 |
name: product.name,
|
| 619 |
-
price_type: priceType,
|
| 620 |
-
price_value: priceValue,
|
| 621 |
photo: product.photos && product.photos.length > 0 ? product.photos[0] : null,
|
| 622 |
quantity: quantity,
|
| 623 |
color: color
|
|
@@ -659,13 +683,13 @@ CATALOG_TEMPLATE = '''
|
|
| 659 |
cartTotalElement.textContent = '0.00';
|
| 660 |
} else {
|
| 661 |
cartContent.innerHTML = cart.map(item => {
|
| 662 |
-
const itemTotal = item.price_value * item.quantity;
|
| 663 |
total += itemTotal;
|
| 664 |
const photoUrl = item.photo
|
| 665 |
? `https://huggingface.co/datasets/${repoId}/resolve/main/photos/${item.photo}`
|
| 666 |
: 'https://via.placeholder.com/60x60.png?text=N/A';
|
| 667 |
const colorText = item.color && item.color !== 'N/A' ? ` (Цвет: ${item.color})` : '';
|
| 668 |
-
const priceTypeText = item.price_type ? `/${item.price_type}` : '';
|
| 669 |
|
| 670 |
return `
|
| 671 |
<div class="cart-item">
|
|
@@ -751,6 +775,11 @@ CATALOG_TEMPLATE = '''
|
|
| 751 |
|
| 752 |
function filterProducts() {
|
| 753 |
const searchTerm = document.getElementById('search-input').value.toLowerCase().trim();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 754 |
const grid = document.getElementById('products-grid');
|
| 755 |
let visibleProducts = 0;
|
| 756 |
|
|
@@ -760,14 +789,24 @@ CATALOG_TEMPLATE = '''
|
|
| 760 |
document.querySelectorAll('.products-grid .product').forEach(productElement => {
|
| 761 |
const name = productElement.getAttribute('data-name');
|
| 762 |
const description = productElement.getAttribute('data-description');
|
| 763 |
-
const
|
| 764 |
-
const
|
| 765 |
|
| 766 |
const matchesSearch = !searchTerm || name.includes(searchTerm) || description.includes(searchTerm);
|
| 767 |
-
|
| 768 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 769 |
|
| 770 |
-
if (matchesSearch &&
|
| 771 |
productElement.style.display = 'flex';
|
| 772 |
visibleProducts++;
|
| 773 |
} else {
|
|
@@ -787,38 +826,6 @@ CATALOG_TEMPLATE = '''
|
|
| 787 |
grid.appendChild(p);
|
| 788 |
}
|
| 789 |
}
|
| 790 |
-
|
| 791 |
-
function renderSubcategories() {
|
| 792 |
-
const subFiltersContainer = document.getElementById('sub-filters-container');
|
| 793 |
-
subFiltersContainer.innerHTML = '';
|
| 794 |
-
|
| 795 |
-
if (activeCategory === 'all') {
|
| 796 |
-
subFiltersContainer.style.display = 'none';
|
| 797 |
-
return;
|
| 798 |
-
}
|
| 799 |
-
|
| 800 |
-
const categoryData = categoriesData.find(c => c.name === activeCategory);
|
| 801 |
-
if (categoryData && categoryData.subcategories && categoryData.subcategories.length > 0) {
|
| 802 |
-
subFiltersContainer.style.display = 'flex';
|
| 803 |
-
|
| 804 |
-
let buttonsHTML = `<button class="subcategory-filter active" data-subcategory="all">Все в "${activeCategory}"</button>`;
|
| 805 |
-
categoryData.subcategories.forEach(sub => {
|
| 806 |
-
buttonsHTML += `<button class="subcategory-filter" data-subcategory="${sub.toLowerCase()}">${sub}</button>`;
|
| 807 |
-
});
|
| 808 |
-
subFiltersContainer.innerHTML = buttonsHTML;
|
| 809 |
-
|
| 810 |
-
subFiltersContainer.querySelectorAll('.subcategory-filter').forEach(subFilter => {
|
| 811 |
-
subFilter.addEventListener('click', function() {
|
| 812 |
-
subFiltersContainer.querySelectorAll('.subcategory-filter').forEach(f => f.classList.remove('active'));
|
| 813 |
-
this.classList.add('active');
|
| 814 |
-
activeSubcategory = this.dataset.subcategory;
|
| 815 |
-
filterProducts();
|
| 816 |
-
});
|
| 817 |
-
});
|
| 818 |
-
} else {
|
| 819 |
-
subFiltersContainer.style.display = 'none';
|
| 820 |
-
}
|
| 821 |
-
}
|
| 822 |
|
| 823 |
function setupFilters() {
|
| 824 |
const searchInput = document.getElementById('search-input');
|
|
@@ -830,9 +837,6 @@ CATALOG_TEMPLATE = '''
|
|
| 830 |
filter.addEventListener('click', function() {
|
| 831 |
categoryFilters.forEach(f => f.classList.remove('active'));
|
| 832 |
this.classList.add('active');
|
| 833 |
-
activeCategory = this.dataset.category;
|
| 834 |
-
activeSubcategory = 'all';
|
| 835 |
-
renderSubcategories();
|
| 836 |
filterProducts();
|
| 837 |
});
|
| 838 |
});
|
|
@@ -922,7 +926,10 @@ PRODUCT_DETAIL_TEMPLATE = '''
|
|
| 922 |
</div>
|
| 923 |
|
| 924 |
<div style="margin-top: 20px; font-size: 1rem; line-height: 1.7; color: #333;">
|
| 925 |
-
<p><strong>Категория:</strong> {{ product.get('category', 'Без категории') }}
|
|
|
|
|
|
|
|
|
|
| 926 |
<p style="font-size: 1.2rem; font-weight: bold; color: #c89345; margin-bottom: 10px;"><strong>Цена:</strong></p>
|
| 927 |
<div class="price-list" style="text-align: left;">
|
| 928 |
{% if product.get('prices') %}
|
|
@@ -942,8 +949,9 @@ PRODUCT_DETAIL_TEMPLATE = '''
|
|
| 942 |
</div>
|
| 943 |
</div>
|
| 944 |
<style>
|
|
|
|
| 945 |
#productModal .swiper-button-next, #productModal .swiper-button-prev {
|
| 946 |
-
color: #e3a84f;
|
| 947 |
}
|
| 948 |
</style>
|
| 949 |
'''
|
|
@@ -1028,7 +1036,7 @@ ORDER_TEMPLATE = '''
|
|
| 1028 |
function sendOrderViaWhatsApp() {
|
| 1029 |
const orderId = '{{ order.id }}';
|
| 1030 |
const orderUrl = `{{ request.url }}`;
|
| 1031 |
-
const whatsappNumber = "{{ whatsapp_number }}";
|
| 1032 |
|
| 1033 |
let message = `Здравствуйте! Хочу подтвердить свой заказ на сайте "Мир праздника":%0A%0A`;
|
| 1034 |
message += `*Номер заказа:* ${orderId}%0A`;
|
|
@@ -1067,7 +1075,6 @@ ADMIN_TEMPLATE = '''
|
|
| 1067 |
h1 { font-size: 1.8rem; }
|
| 1068 |
h2 { font-size: 1.5rem; margin-top: 30px; display: flex; align-items: center; gap: 8px; }
|
| 1069 |
h3 { font-size: 1.2rem; color: #c89345; margin-top: 20px; }
|
| 1070 |
-
h4 { font-size: 1.1rem; color: #333; margin-top: 20px; }
|
| 1071 |
.section { margin-bottom: 30px; padding: 20px; background-color: #f9f9f9; border: 1px solid #e0e0e0; border-radius: 8px; }
|
| 1072 |
form { margin-bottom: 20px; }
|
| 1073 |
label { font-weight: 500; margin-top: 10px; display: block; color: #666; font-size: 0.9rem;}
|
|
@@ -1092,8 +1099,8 @@ ADMIN_TEMPLATE = '''
|
|
| 1092 |
.item strong { color: #333; }
|
| 1093 |
.item .description { font-size: 0.85rem; color: #999; max-height: 60px; overflow: hidden; text-overflow: ellipsis; }
|
| 1094 |
.item-actions { margin-top: 15px; display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }
|
| 1095 |
-
.item-actions button:not(.delete-button) { background-color: #e3a84f; }
|
| 1096 |
-
.item-actions button:not(.delete-button):hover { background-color: #c89345; }
|
| 1097 |
.edit-form-container { margin-top: 15px; padding: 20px; background: #fffcf5; border: 1px dashed #e0e0e0; border-radius: 6px; display: none; }
|
| 1098 |
details { background-color: #f9f9f9; border: 1px solid #e0e0e0; border-radius: 8px; margin-bottom: 20px; }
|
| 1099 |
details > summary { cursor: pointer; font-weight: 600; color: #c89345; display: block; padding: 15px; border-bottom: 1px solid #e0e0e0; list-style: none; position: relative; }
|
|
@@ -1103,9 +1110,10 @@ ADMIN_TEMPLATE = '''
|
|
| 1103 |
details .form-content { padding: 20px; }
|
| 1104 |
.color-input-group, .price-input-group { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
|
| 1105 |
.color-input-group input, .price-input-group input { flex-grow: 1; margin: 0; }
|
| 1106 |
-
.price-input-group input[type="text"] { width: 100px; flex-grow: 0; }
|
| 1107 |
-
.price-input-group input[type="number"] { flex-grow: 1; }
|
| 1108 |
-
.price-input-group label { margin-top: 0; width: auto; display: inline-block; font-weight: normal; color: #333;}
|
|
|
|
| 1109 |
.remove-color-btn, .remove-price-btn { background-color: #dc3545; padding: 6px 10px; font-size: 0.8rem; margin-top: 0; line-height: 1; }
|
| 1110 |
.remove-color-btn:hover, .remove-price-btn:hover { background-color: #c82333; }
|
| 1111 |
.add-color-btn, .add-price-btn { background-color: #f0c38b; color: #c89345; border: 1px solid #e0e0e0; }
|
|
@@ -1124,10 +1132,9 @@ ADMIN_TEMPLATE = '''
|
|
| 1124 |
.status-indicator.in-stock { background-color: #d4edda; color: #155724; }
|
| 1125 |
.status-indicator.out-of-stock { background-color: #f8d7da; color: #721c24; }
|
| 1126 |
.status-indicator.top-product { background-color: #fff3cd; color: #856404; margin-left: 5px;}
|
| 1127 |
-
|
| 1128 |
-
|
| 1129 |
-
|
| 1130 |
-
.subcategory-item { background: #f0f0f0; padding: 5px 10px; border-radius: 4px; margin-bottom: 5px; display: flex; justify-content: space-between; align-items: center; font-size: 0.9rem; }
|
| 1131 |
</style>
|
| 1132 |
</head>
|
| 1133 |
<body>
|
|
@@ -1154,7 +1161,7 @@ ADMIN_TEMPLATE = '''
|
|
| 1154 |
<button type="submit" class="button" title="Загрузить локальные файлы на Hugging Face"><i class="fas fa-upload"></i> Загрузить БД</button>
|
| 1155 |
</form>
|
| 1156 |
<form method="POST" action="{{ url_for('force_download') }}" style="display: inline;" onsubmit="return confirm('Вы уверены, что хотите принудительно скачать данные с сервера? Это перезапишет ваши локальные файлы.');">
|
| 1157 |
-
<button type="submit" class="button download-hf-button" title="Скачать файлы
|
| 1158 |
</form>
|
| 1159 |
</div>
|
| 1160 |
<p style="font-size: 0.85rem; color: #999;">Резервное копирование происходит автоматически каждые 30 минут, а также после каждого сохранения данных. Используйте эти кнопки для немедленной синхронизации.</p>
|
|
@@ -1165,55 +1172,59 @@ ADMIN_TEMPLATE = '''
|
|
| 1165 |
<div class="section">
|
| 1166 |
<h2><i class="fas fa-tags"></i> Управление категориями</h2>
|
| 1167 |
<details>
|
| 1168 |
-
<summary><i class="fas fa-plus-circle"></i> Добавить
|
| 1169 |
<div class="form-content">
|
| 1170 |
<form method="POST">
|
| 1171 |
<input type="hidden" name="action" value="add_category">
|
| 1172 |
-
<label for="add_category_name">Название новой категории:</label>
|
| 1173 |
<input type="text" id="add_category_name" name="category_name" required>
|
| 1174 |
<button type="submit" class="add-button"><i class="fas fa-plus"></i> Добавить</button>
|
| 1175 |
</form>
|
| 1176 |
</div>
|
| 1177 |
</details>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1178 |
|
| 1179 |
<h3>Существующие категории:</h3>
|
| 1180 |
-
{% if
|
| 1181 |
<div class="item-list">
|
| 1182 |
-
{% for category in
|
| 1183 |
-
<div class="item">
|
| 1184 |
-
|
| 1185 |
-
|
| 1186 |
-
|
| 1187 |
-
|
| 1188 |
-
|
| 1189 |
-
|
| 1190 |
-
|
| 1191 |
-
|
| 1192 |
-
|
| 1193 |
-
|
| 1194 |
-
|
| 1195 |
-
|
| 1196 |
-
|
| 1197 |
-
|
| 1198 |
-
|
| 1199 |
-
|
| 1200 |
-
|
| 1201 |
-
|
| 1202 |
-
|
| 1203 |
-
<button type="submit" class="delete-button" style="padding: 3px 8px; font-size:0.7rem; margin:0;"><i class="fas fa-times"></i></button>
|
| 1204 |
-
</form>
|
| 1205 |
-
</li>
|
| 1206 |
-
{% endfor %}
|
| 1207 |
-
</ul>
|
| 1208 |
-
{% else %}
|
| 1209 |
-
<p style="font-size: 0.9rem; color: #999;">Подкатегорий нет.</p>
|
| 1210 |
-
{% endif %}
|
| 1211 |
-
<form method="POST" style="margin-top: 15px; display: flex; gap: 10px; align-items: center;">
|
| 1212 |
-
<input type="hidden" name="action" value="add_subcategory">
|
| 1213 |
-
<input type="hidden" name="parent_category" value="{{ category.name }}">
|
| 1214 |
-
<input type="text" name="subcategory_name" placeholder="Новая подкатегория" required style="margin:0; flex-grow: 1;">
|
| 1215 |
-
<button type="submit" class="add-button" style="margin:0; padding: 8px 12px;"><i class="fas fa-plus"></i></button>
|
| 1216 |
-
</form>
|
| 1217 |
</div>
|
| 1218 |
{% endfor %}
|
| 1219 |
</div>
|
|
@@ -1259,18 +1270,18 @@ ADMIN_TEMPLATE = '''
|
|
| 1259 |
<textarea id="add_description" name="description" rows="4"></textarea>
|
| 1260 |
|
| 1261 |
<label for="add_category">Категория:</label>
|
| 1262 |
-
<select id="add_category" name="category"
|
| 1263 |
<option value="Без категории">Без категории</option>
|
| 1264 |
-
{% for category in
|
| 1265 |
<option value="{{ category.name }}">{{ category.name }}</option>
|
| 1266 |
{% endfor %}
|
| 1267 |
</select>
|
| 1268 |
|
| 1269 |
<label for="add_subcategory">Подкатегория:</label>
|
| 1270 |
<select id="add_subcategory" name="subcategory">
|
| 1271 |
-
<option value="
|
| 1272 |
</select>
|
| 1273 |
-
|
| 1274 |
<label for="add_photos">Фотографии (до 10 шт.):</label>
|
| 1275 |
<input type="file" id="add_photos" name="photos" accept="image/*" multiple>
|
| 1276 |
<label>Цвета/Варианты (оставьте пустым, если нет):</label>
|
|
@@ -1323,7 +1334,8 @@ ADMIN_TEMPLATE = '''
|
|
| 1323 |
<span class="status-indicator top-product"><i class="fas fa-star"></i> Топ</span>
|
| 1324 |
{% endif %}
|
| 1325 |
</h3>
|
| 1326 |
-
<p><strong>Категория:</strong> {{ product.get('category', 'Без категории') }}
|
|
|
|
| 1327 |
<p><strong>Цены:</strong></p>
|
| 1328 |
<div class="admin-price-list">
|
| 1329 |
{% if product.get('prices') %}
|
|
@@ -1391,16 +1403,19 @@ ADMIN_TEMPLATE = '''
|
|
| 1391 |
<textarea name="description" rows="4">{{ product.get('description', '') }}</textarea>
|
| 1392 |
|
| 1393 |
<label>Категория:</label>
|
| 1394 |
-
<select
|
|
|
|
|
|
|
|
|
|
| 1395 |
<option value="Без категории" {% if product.get('category', 'Без категории') == 'Без категории' %}selected{% endif %}>Без категории</option>
|
| 1396 |
-
{% for category in
|
| 1397 |
<option value="{{ category.name }}" {% if product.get('category') == category.name %}selected{% endif %}>{{ category.name }}</option>
|
| 1398 |
{% endfor %}
|
| 1399 |
</select>
|
| 1400 |
-
|
| 1401 |
<label>Подкатегория:</label>
|
| 1402 |
-
<select id="
|
| 1403 |
-
|
| 1404 |
</select>
|
| 1405 |
|
| 1406 |
<label>Заменить фотографии (выберите новые файлы, до 10 шт.):</label>
|
|
@@ -1457,45 +1472,64 @@ ADMIN_TEMPLATE = '''
|
|
| 1457 |
</div>
|
| 1458 |
|
| 1459 |
<script>
|
| 1460 |
-
const categoriesData = {{
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1461 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1462 |
function toggleEditForm(formId) {
|
| 1463 |
const formContainer = document.getElementById(formId);
|
| 1464 |
if (formContainer) {
|
| 1465 |
-
|
| 1466 |
-
formContainer.style.display
|
| 1467 |
-
|
| 1468 |
-
const
|
|
|
|
| 1469 |
if (catSelect) {
|
| 1470 |
-
|
| 1471 |
-
|
|
|
|
|
|
|
| 1472 |
}
|
| 1473 |
}
|
| 1474 |
}
|
| 1475 |
}
|
| 1476 |
|
| 1477 |
-
function updateSubcategoryDropdown(formIdPrefix, selectedCategory, currentSubcategory = null) {
|
| 1478 |
-
const subcatSelect = document.getElementById(`${formIdPrefix}_subcategory`);
|
| 1479 |
-
if (!subcatSelect) return;
|
| 1480 |
-
|
| 1481 |
-
subcatSelect.innerHTML = '';
|
| 1482 |
-
|
| 1483 |
-
const categoryData = categoriesData.find(c => c.name === selectedCategory);
|
| 1484 |
-
|
| 1485 |
-
if (categoryData && categoryData.subcategories && categoryData.subcategories.length > 0) {
|
| 1486 |
-
subcatSelect.disabled = false;
|
| 1487 |
-
let optionsHtml = '<option value="none">-- Выберите подкатегорию --</option>';
|
| 1488 |
-
categoryData.subcategories.forEach(sub => {
|
| 1489 |
-
const isSelected = sub === currentSubcategory ? 'selected' : '';
|
| 1490 |
-
optionsHtml += `<option value="${sub}" ${isSelected}>${sub}</option>`;
|
| 1491 |
-
});
|
| 1492 |
-
subcatSelect.innerHTML = optionsHtml;
|
| 1493 |
-
} else {
|
| 1494 |
-
subcatSelect.innerHTML = '<option value="none">-- Нет подкатегорий --</option>';
|
| 1495 |
-
subcatSelect.disabled = true;
|
| 1496 |
-
}
|
| 1497 |
-
}
|
| 1498 |
-
|
| 1499 |
function addColorInput(containerId) {
|
| 1500 |
const container = document.getElementById(containerId);
|
| 1501 |
if (container) {
|
|
@@ -1562,16 +1596,27 @@ ADMIN_TEMPLATE = '''
|
|
| 1562 |
if(addPriceInputsDiv && addPriceInputsDiv.children.length === 0) {
|
| 1563 |
addPriceInput('add-price-inputs');
|
| 1564 |
}
|
| 1565 |
-
|
| 1566 |
-
|
| 1567 |
-
|
| 1568 |
-
|
| 1569 |
-
|
| 1570 |
-
|
| 1571 |
-
|
| 1572 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1573 |
});
|
| 1574 |
});
|
|
|
|
| 1575 |
</script>
|
| 1576 |
</body>
|
| 1577 |
</html>
|
|
@@ -1582,7 +1627,7 @@ ADMIN_TEMPLATE = '''
|
|
| 1582 |
def catalog():
|
| 1583 |
data = load_data()
|
| 1584 |
all_products = data.get('products', [])
|
| 1585 |
-
|
| 1586 |
|
| 1587 |
products_in_stock = [p for p in all_products if p.get('in_stock', True)]
|
| 1588 |
products_sorted = sorted(products_in_stock, key=lambda p: (not p.get('is_top', False), p.get('name', '').lower()))
|
|
@@ -1590,7 +1635,7 @@ def catalog():
|
|
| 1590 |
return render_template_string(
|
| 1591 |
CATALOG_TEMPLATE,
|
| 1592 |
products=products_sorted,
|
| 1593 |
-
|
| 1594 |
repo_id=REPO_ID,
|
| 1595 |
store_address=STORE_ADDRESS,
|
| 1596 |
currency_code=CURRENCY_CODE
|
|
@@ -1704,7 +1749,7 @@ def view_order(order_id):
|
|
| 1704 |
def admin():
|
| 1705 |
data = load_data()
|
| 1706 |
products = data.get('products', [])
|
| 1707 |
-
categories = data.get('categories', [])
|
| 1708 |
if 'orders' not in data or not isinstance(data.get('orders'), dict):
|
| 1709 |
data['orders'] = {}
|
| 1710 |
|
|
@@ -1717,75 +1762,80 @@ def admin():
|
|
| 1717 |
category_name = request.form.get('category_name', '').strip()
|
| 1718 |
if category_name and not any(c['name'] == category_name for c in categories):
|
| 1719 |
categories.append({'name': category_name, 'subcategories': []})
|
| 1720 |
-
data['categories'] =
|
| 1721 |
save_data(data)
|
| 1722 |
logging.info(f"Category '{category_name}' added.")
|
| 1723 |
-
flash(f"
|
| 1724 |
elif not category_name:
|
| 1725 |
logging.warning("Attempted to add empty category.")
|
| 1726 |
flash("Название категории не может быть пустым.", 'error')
|
| 1727 |
else:
|
| 1728 |
logging.warning(f"Category '{category_name}' already exists.")
|
| 1729 |
-
flash(f"
|
| 1730 |
|
| 1731 |
elif action == 'add_subcategory':
|
| 1732 |
-
parent_category_name = request.form.get('
|
| 1733 |
subcategory_name = request.form.get('subcategory_name', '').strip()
|
| 1734 |
parent_cat = next((c for c in categories if c['name'] == parent_category_name), None)
|
| 1735 |
|
| 1736 |
-
if parent_cat and subcategory_name
|
| 1737 |
-
parent_cat['subcategories']
|
| 1738 |
-
|
| 1739 |
-
|
| 1740 |
-
|
| 1741 |
-
|
| 1742 |
-
|
| 1743 |
-
elif not subcategory_name:
|
| 1744 |
-
flash("Название подкатегории не может быть пустым.", 'error')
|
| 1745 |
else:
|
| 1746 |
-
flash(
|
| 1747 |
|
| 1748 |
elif action == 'delete_category':
|
| 1749 |
category_to_delete = request.form.get('category_name')
|
| 1750 |
-
original_len = len(categories)
|
| 1751 |
-
categories = [c for c in categories if c['name'] != category_to_delete]
|
| 1752 |
-
if len(categories) < original_len:
|
| 1753 |
-
updated_count = 0
|
| 1754 |
-
for product in products:
|
| 1755 |
-
if product.get('category') == category_to_delete:
|
| 1756 |
-
product['category'] = 'Без категории'
|
| 1757 |
-
product['subcategory'] = None
|
| 1758 |
-
updated_count += 1
|
| 1759 |
-
data['categories'] = categories
|
| 1760 |
-
data['products'] = products
|
| 1761 |
-
save_data(data)
|
| 1762 |
-
logging.info(f"Category '{category_to_delete}' deleted. Updated products: {updated_count}.")
|
| 1763 |
-
flash(f"Категория '{category_to_delete}' удалена. {updated_count} товаров обновлено.", 'success')
|
| 1764 |
-
else:
|
| 1765 |
-
flash(f"Не удалось удалить категорию '{category_to_delete}'.", 'error')
|
| 1766 |
-
|
| 1767 |
-
elif action == 'delete_subcategory':
|
| 1768 |
-
parent_category_name = request.form.get('parent_category')
|
| 1769 |
subcategory_to_delete = request.form.get('subcategory_name')
|
| 1770 |
-
parent_cat = next((c for c in categories if c['name'] == parent_category_name), None)
|
| 1771 |
|
| 1772 |
-
if
|
| 1773 |
-
|
| 1774 |
-
|
| 1775 |
-
|
| 1776 |
-
|
| 1777 |
-
|
| 1778 |
-
|
| 1779 |
-
|
| 1780 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1781 |
else:
|
| 1782 |
-
flash(
|
|
|
|
| 1783 |
|
| 1784 |
elif action == 'add_product':
|
| 1785 |
name = request.form.get('name', '').strip()
|
| 1786 |
description = request.form.get('description', '').strip()
|
| 1787 |
category = request.form.get('category')
|
| 1788 |
-
subcategory = request.form.get('subcategory')
|
| 1789 |
photos_files = request.files.getlist('photos')
|
| 1790 |
colors = [c.strip() for c in request.form.getlist('colors') if c.strip()]
|
| 1791 |
in_stock = 'in_stock' in request.form
|
|
@@ -1799,7 +1849,7 @@ def admin():
|
|
| 1799 |
type_str = type_str.strip()
|
| 1800 |
value_str = value_str.strip().replace(',', '.')
|
| 1801 |
if not type_str or not value_str:
|
| 1802 |
-
continue
|
| 1803 |
|
| 1804 |
try:
|
| 1805 |
price_value = round(float(value_str), 2)
|
|
@@ -1809,12 +1859,18 @@ def admin():
|
|
| 1809 |
except ValueError:
|
| 1810 |
logging.warning(f"Skipping invalid price value '{value_str}' for type '{type_str}' during add product.")
|
| 1811 |
|
|
|
|
| 1812 |
if not name:
|
| 1813 |
flash("Название товара обязательно.", 'error')
|
| 1814 |
return redirect(url_for('admin'))
|
| 1815 |
if not valid_prices_exist:
|
| 1816 |
flash("Должен быть указан хотя бы один действительный вариант цены (Тип и Цена > 0).", 'error')
|
| 1817 |
return redirect(url_for('admin'))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1818 |
|
| 1819 |
|
| 1820 |
photos_list = []
|
|
@@ -1873,8 +1929,7 @@ def admin():
|
|
| 1873 |
|
| 1874 |
new_product = {
|
| 1875 |
'name': name, 'prices': prices, 'description': description,
|
| 1876 |
-
'category': category
|
| 1877 |
-
'subcategory': subcategory if subcategory and subcategory != 'none' else None,
|
| 1878 |
'photos': photos_list, 'colors': colors,
|
| 1879 |
'in_stock': in_stock, 'is_top': is_top
|
| 1880 |
}
|
|
@@ -1904,10 +1959,18 @@ def admin():
|
|
| 1904 |
|
| 1905 |
product_to_edit['name'] = request.form.get('name', product_to_edit['name']).strip()
|
| 1906 |
product_to_edit['description'] = request.form.get('description', product_to_edit.get('description', '')).strip()
|
| 1907 |
-
|
| 1908 |
-
|
| 1909 |
-
|
| 1910 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1911 |
product_to_edit['colors'] = [c.strip() for c in request.form.getlist('colors') if c.strip()]
|
| 1912 |
product_to_edit['in_stock'] = 'in_stock' in request.form
|
| 1913 |
product_to_edit['is_top'] = 'is_top' in request.form
|
|
@@ -1920,7 +1983,7 @@ def admin():
|
|
| 1920 |
type_str = type_str.strip()
|
| 1921 |
value_str = value_str.strip().replace(',', '.')
|
| 1922 |
if not type_str or not value_str:
|
| 1923 |
-
continue
|
| 1924 |
|
| 1925 |
try:
|
| 1926 |
price_value = round(float(value_str), 2)
|
|
@@ -2070,12 +2133,12 @@ def admin():
|
|
| 2070 |
|
| 2071 |
current_data = load_data()
|
| 2072 |
display_products = sorted(current_data.get('products', []), key=lambda p: p.get('name', '').lower())
|
| 2073 |
-
|
| 2074 |
|
| 2075 |
return render_template_string(
|
| 2076 |
ADMIN_TEMPLATE,
|
| 2077 |
products=display_products,
|
| 2078 |
-
|
| 2079 |
repo_id=REPO_ID,
|
| 2080 |
currency_code=CURRENCY_CODE
|
| 2081 |
)
|
|
|
|
| 150 |
if 'categories' not in data: data['categories'] = []
|
| 151 |
if 'orders' not in data: data['orders'] = {}
|
| 152 |
|
| 153 |
+
# 1. Ensure categories are in dictionary format with subcategories (Migration)
|
| 154 |
+
if 'categories' in data:
|
| 155 |
+
new_categories = []
|
| 156 |
+
for cat in data['categories']:
|
| 157 |
+
if isinstance(cat, str): # Old flat structure migration
|
| 158 |
+
new_categories.append({'name': cat, 'subcategories': []})
|
| 159 |
+
elif isinstance(cat, dict) and 'name' in cat:
|
| 160 |
+
if 'subcategories' not in cat or not isinstance(cat['subcategories'], list):
|
| 161 |
+
cat['subcategories'] = []
|
| 162 |
+
new_categories.append(cat)
|
| 163 |
+
else:
|
| 164 |
+
logging.warning(f"Skipping invalid category entry: {cat}")
|
| 165 |
+
data['categories'] = new_categories
|
| 166 |
+
else:
|
| 167 |
+
data['categories'] = []
|
| 168 |
|
| 169 |
+
# 2. Ensure products have 'prices' and 'subcategory' field
|
| 170 |
for product in data['products']:
|
| 171 |
if 'subcategory' not in product:
|
| 172 |
+
product['subcategory'] = 'Без подкатегории'
|
| 173 |
+
|
| 174 |
if 'prices' not in product or not isinstance(product['prices'], list):
|
| 175 |
+
if 'price' in product: # Convert old 'price' to new 'prices' structure
|
| 176 |
product['prices'] = [{'type': 'шт', 'value': product.pop('price')}]
|
| 177 |
else:
|
| 178 |
product['prices'] = []
|
| 179 |
+
|
| 180 |
product['prices'] = [p for p in product['prices'] if isinstance(p, dict) and 'type' in p and 'value' in p]
|
| 181 |
for p in product['prices']:
|
| 182 |
try:
|
| 183 |
p['value'] = round(float(p['value']), 2)
|
| 184 |
except (ValueError, TypeError):
|
| 185 |
p['value'] = 0.0
|
| 186 |
+
if not product['prices']:
|
| 187 |
product['prices'] = [{'type': 'шт', 'value': 0.0}]
|
| 188 |
|
| 189 |
return data
|
|
|
|
| 197 |
with open(DATA_FILE, 'r', encoding='utf-8') as file:
|
| 198 |
data = json.load(file)
|
| 199 |
logging.info(f"Data loaded successfully from {DATA_FILE} after download.")
|
| 200 |
+
|
| 201 |
if not isinstance(data, dict):
|
| 202 |
logging.error(f"Downloaded {DATA_FILE} is not a dictionary. Using default.")
|
| 203 |
return default_data
|
|
|
|
| 205 |
if 'categories' not in data: data['categories'] = []
|
| 206 |
if 'orders' not in data: data['orders'] = {}
|
| 207 |
|
| 208 |
+
# Apply same structure fixes after download
|
| 209 |
+
if 'categories' in data:
|
| 210 |
+
new_categories = []
|
| 211 |
+
for cat in data['categories']:
|
| 212 |
+
if isinstance(cat, str):
|
| 213 |
+
new_categories.append({'name': cat, 'subcategories': []})
|
| 214 |
+
elif isinstance(cat, dict) and 'name' in cat:
|
| 215 |
+
if 'subcategories' not in cat or not isinstance(cat['subcategories'], list):
|
| 216 |
+
cat['subcategories'] = []
|
| 217 |
+
new_categories.append(cat)
|
| 218 |
+
data['categories'] = new_categories
|
| 219 |
+
else:
|
| 220 |
+
data['categories'] = []
|
| 221 |
|
| 222 |
for product in data['products']:
|
| 223 |
if 'subcategory' not in product:
|
| 224 |
+
product['subcategory'] = 'Без подкатегории'
|
| 225 |
+
|
| 226 |
if 'prices' not in product or not isinstance(product['prices'], list):
|
| 227 |
+
if 'price' in product:
|
| 228 |
product['prices'] = [{'type': 'шт', 'value': product.pop('price')}]
|
| 229 |
else:
|
| 230 |
product['prices'] = []
|
|
|
|
| 293 |
.header { display: flex; justify-content: space-between; align-items: center; padding: 15px 0; border-bottom: 1px solid #e0e0e0; }
|
| 294 |
.header h1 { font-size: 1.8rem; font-weight: 600; color: #e3a84f; }
|
| 295 |
.store-address { padding: 15px; text-align: center; background-color: #f9f9f9; margin: 20px 0; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.03); font-size: 1rem; color: #666; }
|
| 296 |
+
.filters-container { margin: 20px 0; display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; }
|
|
|
|
|
|
|
| 297 |
.search-container { margin: 20px 0; text-align: center; }
|
| 298 |
#search-input { width: 90%; max-width: 600px; padding: 12px 18px; font-size: 1rem; border: 1px solid #e0e0e0; border-radius: 25px; outline: none; box-shadow: 0 2px 5px rgba(0,0,0,0.03); transition: all 0.3s ease; }
|
| 299 |
#search-input:focus { border-color: #e3a84f; box-shadow: 0 0 0 3px rgba(227, 168, 79, 0.15); }
|
| 300 |
+
.category-filter { padding: 8px 16px; border: 1px solid #e0e0e0; border-radius: 20px; background-color: #fff; cursor: pointer; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); font-size: 0.9rem; font-weight: 400; color: #c89345; white-space: nowrap;}
|
| 301 |
+
.category-filter.subcategory-filter { font-size: 0.8rem; padding: 6px 12px; }
|
| 302 |
+
.category-filter.active, .category-filter:hover { background-color: #e3a84f; color: white; border-color: #e3a84f; box-shadow: 0 2px 10px rgba(227, 168, 79, 0.2); }
|
| 303 |
.products-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 20px; padding: 10px; }
|
| 304 |
@media (min-width: 600px) { .products-grid { grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); } }
|
| 305 |
@media (min-width: 900px) { .products-grid { grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); } }
|
|
|
|
| 366 |
|
| 367 |
<div class="store-address">Наш адрес: {{ store_address }}</div>
|
| 368 |
|
| 369 |
+
<div class="filters-container">
|
| 370 |
+
<button class="category-filter active" data-category="all" data-subcategory="all">Все категории</button>
|
| 371 |
+
{% for category_obj in categories_data %}
|
| 372 |
+
<button class="category-filter" data-category="{{ category_obj.name }}" data-subcategory="all">{{ category_obj.name }}</button>
|
| 373 |
+
{% for subcategory in category_obj.subcategories %}
|
| 374 |
+
<button class="category-filter subcategory-filter" data-category="{{ category_obj.name }}" data-subcategory="{{ subcategory }}">{{ subcategory }}</button>
|
| 375 |
{% endfor %}
|
| 376 |
+
{% endfor %}
|
|
|
|
| 377 |
</div>
|
| 378 |
|
| 379 |
<div class="search-container">
|
|
|
|
| 386 |
data-name="{{ product['name']|lower }}"
|
| 387 |
data-description="{{ product.get('description', '')|lower }}"
|
| 388 |
data-category="{{ product.get('category', 'Без категории') }}"
|
| 389 |
+
data-subcategory="{{ product.get('subcategory', 'Без подкатегории') }}">
|
| 390 |
{% if product.get('is_top', False) %}
|
| 391 |
<span class="top-product-indicator"><i class="fas fa-star"></i> Топ</span>
|
| 392 |
{% endif %}
|
|
|
|
| 410 |
<span class="price-item">Нет цены</span>
|
| 411 |
{% endif %}
|
| 412 |
</div>
|
| 413 |
+
<p class="product-description">{{ product.get('category', 'Без категории') }}{% if product.get('subcategory', 'Без подкатегории') != 'Без подкатегории' %} / {{ product.get('subcategory') }}{% endif %}</p>
|
| 414 |
</div>
|
| 415 |
<div class="product-actions">
|
| 416 |
<button class="product-button" onclick="openModal({{ loop.index0 }})">Подробнее</button>
|
|
|
|
| 479 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/Swiper/10.2.0/swiper-bundle.min.js"></script>
|
| 480 |
<script>
|
| 481 |
const products = {{ products|tojson }};
|
|
|
|
| 482 |
const repoId = '{{ repo_id }}';
|
| 483 |
const currencyCode = '{{ currency_code }}';
|
| 484 |
let selectedProductIndex = null;
|
| 485 |
let cart = JSON.parse(localStorage.getItem('mekaCart') || '[]');
|
| 486 |
+
|
|
|
|
| 487 |
|
| 488 |
function openModal(index) {
|
| 489 |
loadProductDetails(index);
|
|
|
|
| 580 |
const option = document.createElement('option');
|
| 581 |
option.value = price_item.type.trim();
|
| 582 |
option.text = `${price_item.value.toFixed(2)} ${currencyCode} / ${price_item.type.trim()}`;
|
| 583 |
+
option.dataset.priceValue = price_item.value;
|
| 584 |
priceTypeSelect.appendChild(option);
|
| 585 |
});
|
| 586 |
priceTypeSelect.style.display = 'block';
|
|
|
|
| 589 |
priceTypeSelect.style.display = 'none';
|
| 590 |
if(priceTypeLabel) priceTypeLabel.style.display = 'none';
|
| 591 |
alert("Для этого товара нет доступных цен.");
|
| 592 |
+
return;
|
| 593 |
}
|
| 594 |
|
| 595 |
|
|
|
|
| 610 |
const color = colorSelect.style.display !== 'none' && colorSelect.value ? colorSelect.value : 'N/A';
|
| 611 |
const priceTypeSelect = document.getElementById('priceTypeSelect');
|
| 612 |
const selectedPriceOption = priceTypeSelect.options[priceTypeSelect.selectedIndex];
|
| 613 |
+
const priceType = selectedPriceOption ? selectedPriceOption.value : 'шт';
|
| 614 |
const priceValue = selectedPriceOption ? parseFloat(selectedPriceOption.dataset.priceValue) : null;
|
| 615 |
|
| 616 |
|
|
|
|
| 631 |
return;
|
| 632 |
}
|
| 633 |
|
| 634 |
+
const cartItemId = `${product.name}-${priceType}-${color}`;
|
| 635 |
const existingItemIndex = cart.findIndex(item => item.id === cartItemId);
|
| 636 |
|
| 637 |
if (existingItemIndex > -1) {
|
|
|
|
| 640 |
cart.push({
|
| 641 |
id: cartItemId,
|
| 642 |
name: product.name,
|
| 643 |
+
price_type: priceType,
|
| 644 |
+
price_value: priceValue,
|
| 645 |
photo: product.photos && product.photos.length > 0 ? product.photos[0] : null,
|
| 646 |
quantity: quantity,
|
| 647 |
color: color
|
|
|
|
| 683 |
cartTotalElement.textContent = '0.00';
|
| 684 |
} else {
|
| 685 |
cartContent.innerHTML = cart.map(item => {
|
| 686 |
+
const itemTotal = item.price_value * item.quantity;
|
| 687 |
total += itemTotal;
|
| 688 |
const photoUrl = item.photo
|
| 689 |
? `https://huggingface.co/datasets/${repoId}/resolve/main/photos/${item.photo}`
|
| 690 |
: 'https://via.placeholder.com/60x60.png?text=N/A';
|
| 691 |
const colorText = item.color && item.color !== 'N/A' ? ` (Цвет: ${item.color})` : '';
|
| 692 |
+
const priceTypeText = item.price_type ? `/${item.price_type}` : '';
|
| 693 |
|
| 694 |
return `
|
| 695 |
<div class="cart-item">
|
|
|
|
| 775 |
|
| 776 |
function filterProducts() {
|
| 777 |
const searchTerm = document.getElementById('search-input').value.toLowerCase().trim();
|
| 778 |
+
const activeCategoryButton = document.querySelector('.category-filter.active');
|
| 779 |
+
|
| 780 |
+
const activeCategory = activeCategoryButton ? activeCategoryButton.dataset.category : 'all';
|
| 781 |
+
const activeSubcategory = activeCategoryButton ? activeCategoryButton.dataset.subcategory : 'all';
|
| 782 |
+
|
| 783 |
const grid = document.getElementById('products-grid');
|
| 784 |
let visibleProducts = 0;
|
| 785 |
|
|
|
|
| 789 |
document.querySelectorAll('.products-grid .product').forEach(productElement => {
|
| 790 |
const name = productElement.getAttribute('data-name');
|
| 791 |
const description = productElement.getAttribute('data-description');
|
| 792 |
+
const productCategory = productElement.getAttribute('data-category');
|
| 793 |
+
const productSubcategory = productElement.getAttribute('data-subcategory');
|
| 794 |
|
| 795 |
const matchesSearch = !searchTerm || name.includes(searchTerm) || description.includes(searchTerm);
|
| 796 |
+
|
| 797 |
+
let matchesFilter = false;
|
| 798 |
+
|
| 799 |
+
if (activeCategory === 'all') {
|
| 800 |
+
matchesFilter = true;
|
| 801 |
+
} else if (productCategory === activeCategory) {
|
| 802 |
+
if (activeSubcategory === 'all') {
|
| 803 |
+
matchesFilter = true;
|
| 804 |
+
} else if (productSubcategory === activeSubcategory) {
|
| 805 |
+
matchesFilter = true;
|
| 806 |
+
}
|
| 807 |
+
}
|
| 808 |
|
| 809 |
+
if (matchesSearch && matchesFilter) {
|
| 810 |
productElement.style.display = 'flex';
|
| 811 |
visibleProducts++;
|
| 812 |
} else {
|
|
|
|
| 826 |
grid.appendChild(p);
|
| 827 |
}
|
| 828 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 829 |
|
| 830 |
function setupFilters() {
|
| 831 |
const searchInput = document.getElementById('search-input');
|
|
|
|
| 837 |
filter.addEventListener('click', function() {
|
| 838 |
categoryFilters.forEach(f => f.classList.remove('active'));
|
| 839 |
this.classList.add('active');
|
|
|
|
|
|
|
|
|
|
| 840 |
filterProducts();
|
| 841 |
});
|
| 842 |
});
|
|
|
|
| 926 |
</div>
|
| 927 |
|
| 928 |
<div style="margin-top: 20px; font-size: 1rem; line-height: 1.7; color: #333;">
|
| 929 |
+
<p><strong>Категория:</strong> {{ product.get('category', 'Без категории') }}</p>
|
| 930 |
+
{% if product.get('subcategory', 'Без подкатегории') != 'Без подкатегории' %}
|
| 931 |
+
<p><strong>Подкатегория:</strong> {{ product.get('subcategory') }}</p>
|
| 932 |
+
{% endif %}
|
| 933 |
<p style="font-size: 1.2rem; font-weight: bold; color: #c89345; margin-bottom: 10px;"><strong>Цена:</strong></p>
|
| 934 |
<div class="price-list" style="text-align: left;">
|
| 935 |
{% if product.get('prices') %}
|
|
|
|
| 949 |
</div>
|
| 950 |
</div>
|
| 951 |
<style>
|
| 952 |
+
/* Add Swiper Modal Specific Styles if needed */
|
| 953 |
#productModal .swiper-button-next, #productModal .swiper-button-prev {
|
| 954 |
+
color: #e3a84f; /* Ensure modal navigation buttons match new color */
|
| 955 |
}
|
| 956 |
</style>
|
| 957 |
'''
|
|
|
|
| 1036 |
function sendOrderViaWhatsApp() {
|
| 1037 |
const orderId = '{{ order.id }}';
|
| 1038 |
const orderUrl = `{{ request.url }}`;
|
| 1039 |
+
const whatsappNumber = "{{ whatsapp_number }}";
|
| 1040 |
|
| 1041 |
let message = `Здравствуйте! Хочу подтвердить свой заказ на сайте "Мир праздника":%0A%0A`;
|
| 1042 |
message += `*Номер заказа:* ${orderId}%0A`;
|
|
|
|
| 1075 |
h1 { font-size: 1.8rem; }
|
| 1076 |
h2 { font-size: 1.5rem; margin-top: 30px; display: flex; align-items: center; gap: 8px; }
|
| 1077 |
h3 { font-size: 1.2rem; color: #c89345; margin-top: 20px; }
|
|
|
|
| 1078 |
.section { margin-bottom: 30px; padding: 20px; background-color: #f9f9f9; border: 1px solid #e0e0e0; border-radius: 8px; }
|
| 1079 |
form { margin-bottom: 20px; }
|
| 1080 |
label { font-weight: 500; margin-top: 10px; display: block; color: #666; font-size: 0.9rem;}
|
|
|
|
| 1099 |
.item strong { color: #333; }
|
| 1100 |
.item .description { font-size: 0.85rem; color: #999; max-height: 60px; overflow: hidden; text-overflow: ellipsis; }
|
| 1101 |
.item-actions { margin-top: 15px; display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }
|
| 1102 |
+
.item-actions button:not(.delete-button) { background-color: #e3a84f; }
|
| 1103 |
+
.item-actions button:not(.delete-button):hover { background-color: #c89345; }
|
| 1104 |
.edit-form-container { margin-top: 15px; padding: 20px; background: #fffcf5; border: 1px dashed #e0e0e0; border-radius: 6px; display: none; }
|
| 1105 |
details { background-color: #f9f9f9; border: 1px solid #e0e0e0; border-radius: 8px; margin-bottom: 20px; }
|
| 1106 |
details > summary { cursor: pointer; font-weight: 600; color: #c89345; display: block; padding: 15px; border-bottom: 1px solid #e0e0e0; list-style: none; position: relative; }
|
|
|
|
| 1110 |
details .form-content { padding: 20px; }
|
| 1111 |
.color-input-group, .price-input-group { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
|
| 1112 |
.color-input-group input, .price-input-group input { flex-grow: 1; margin: 0; }
|
| 1113 |
+
.price-input-group input[type="text"] { width: 100px; flex-grow: 0; }
|
| 1114 |
+
.price-input-group input[type="number"] { flex-grow: 1; }
|
| 1115 |
+
.price-input-group label { margin-top: 0; width: auto; display: inline-block; font-weight: normal; color: #333;}
|
| 1116 |
+
|
| 1117 |
.remove-color-btn, .remove-price-btn { background-color: #dc3545; padding: 6px 10px; font-size: 0.8rem; margin-top: 0; line-height: 1; }
|
| 1118 |
.remove-color-btn:hover, .remove-price-btn:hover { background-color: #c82333; }
|
| 1119 |
.add-color-btn, .add-price-btn { background-color: #f0c38b; color: #c89345; border: 1px solid #e0e0e0; }
|
|
|
|
| 1132 |
.status-indicator.in-stock { background-color: #d4edda; color: #155724; }
|
| 1133 |
.status-indicator.out-of-stock { background-color: #f8d7da; color: #721c24; }
|
| 1134 |
.status-indicator.top-product { background-color: #fff3cd; color: #856404; margin-left: 5px;}
|
| 1135 |
+
.admin-price-list { margin-top: 5px; }
|
| 1136 |
+
.admin-price-item { font-size: 0.85rem; color: #333; display: block; margin-bottom: 3px;}
|
| 1137 |
+
.category-item { background: #fff; padding: 15px 20px; border-radius: 8px; box-shadow: 0 2px 5px rgba(0,0,0,0.03); border: 1px solid #f0f0f0; }
|
|
|
|
| 1138 |
</style>
|
| 1139 |
</head>
|
| 1140 |
<body>
|
|
|
|
| 1161 |
<button type="submit" class="button" title="Загрузить локальные файлы на Hugging Face"><i class="fas fa-upload"></i> Загрузить БД</button>
|
| 1162 |
</form>
|
| 1163 |
<form method="POST" action="{{ url_for('force_download') }}" style="display: inline;" onsubmit="return confirm('Вы уверены, что хотите принудительно скачать данные с сервера? Это перезапишет ваши локальные файлы.');">
|
| 1164 |
+
<button type="submit" class="button download-hf-button" title="Скачать файлы (перезапишет локальные)"><i class="fas fa-download"></i> Скачать БД</button>
|
| 1165 |
</form>
|
| 1166 |
</div>
|
| 1167 |
<p style="font-size: 0.85rem; color: #999;">Резервное копирование происходит автоматически каждые 30 минут, а также после каждого сохранения данных. Используйте эти кнопки для немедленной синхронизации.</p>
|
|
|
|
| 1172 |
<div class="section">
|
| 1173 |
<h2><i class="fas fa-tags"></i> Управление категориями</h2>
|
| 1174 |
<details>
|
| 1175 |
+
<summary><i class="fas fa-plus-circle"></i> Добавить главную категорию</summary>
|
| 1176 |
<div class="form-content">
|
| 1177 |
<form method="POST">
|
| 1178 |
<input type="hidden" name="action" value="add_category">
|
| 1179 |
+
<label for="add_category_name">Название новой главной категории:</label>
|
| 1180 |
<input type="text" id="add_category_name" name="category_name" required>
|
| 1181 |
<button type="submit" class="add-button"><i class="fas fa-plus"></i> Добавить</button>
|
| 1182 |
</form>
|
| 1183 |
</div>
|
| 1184 |
</details>
|
| 1185 |
+
|
| 1186 |
+
<details style="margin-top: 10px;">
|
| 1187 |
+
<summary><i class="fas fa-sitemap"></i> Добавить подкатегорию</summary>
|
| 1188 |
+
<div class="form-content">
|
| 1189 |
+
<form method="POST">
|
| 1190 |
+
<input type="hidden" name="action" value="add_subcategory">
|
| 1191 |
+
<label for="parent_category_name">Выберите главную категорию:</label>
|
| 1192 |
+
<select id="parent_category_name" name="parent_category_name" required>
|
| 1193 |
+
{% for category in categories_data %}
|
| 1194 |
+
<option value="{{ category.name }}">{{ category.name }}</option>
|
| 1195 |
+
{% endfor %}
|
| 1196 |
+
</select>
|
| 1197 |
+
<label for="add_subcategory_name">Название новой подкатегории:</label>
|
| 1198 |
+
<input type="text" id="add_subcategory_name" name="subcategory_name" required>
|
| 1199 |
+
<button type="submit" class="add-button"><i class="fas fa-plus"></i> Добавить подкатегорию</button>
|
| 1200 |
+
</form>
|
| 1201 |
+
</div>
|
| 1202 |
+
</details>
|
| 1203 |
|
| 1204 |
<h3>Существующие категории:</h3>
|
| 1205 |
+
{% if categories_data %}
|
| 1206 |
<div class="item-list">
|
| 1207 |
+
{% for category in categories_data %}
|
| 1208 |
+
<div class="category-item">
|
| 1209 |
+
<div style="display: flex; justify-content: space-between; align-items: center; background: #f0f0f0; padding: 5px 10px; border-radius: 4px; margin-bottom: 5px;">
|
| 1210 |
+
<span><strong>{{ category.name }}</strong> (Главная)</span>
|
| 1211 |
+
<form method="POST" style="margin: 0;" onsubmit="return confirm('Удалить главную категорию \'{{ category.name }}\'? Это также удалит все подкатегории и обновит товары.');">
|
| 1212 |
+
<input type="hidden" name="action" value="delete_category">
|
| 1213 |
+
<input type="hidden" name="category_name" value="{{ category.name }}">
|
| 1214 |
+
<button type="submit" class="delete-button" style="padding: 5px 10px; font-size: 0.8rem; margin: 0;"><i class="fas fa-trash-alt"></i></button>
|
| 1215 |
+
</form>
|
| 1216 |
+
</div>
|
| 1217 |
+
{% for subcategory in category.subcategories %}
|
| 1218 |
+
<div style="display: flex; justify-content: space-between; align-items: center; padding: 5px 10px; margin-left: 15px; border-left: 2px solid #e3a84f;">
|
| 1219 |
+
<span style="font-size: 0.9em;">— {{ subcategory }} (Подкатегория)</span>
|
| 1220 |
+
<form method="POST" style="margin: 0;" onsubmit="return confirm('Удалить подкатегорию \'{{ subcategory }}\' из \'{{ category.name }}\'?');">
|
| 1221 |
+
<input type="hidden" name="action" value="delete_category">
|
| 1222 |
+
<input type="hidden" name="category_name" value="{{ category.name }}">
|
| 1223 |
+
<input type="hidden" name="subcategory_name" value="{{ subcategory }}">
|
| 1224 |
+
<button type="submit" class="delete-button" style="padding: 3px 8px; font-size: 0.7rem; margin: 0;"><i class="fas fa-times"></i></button>
|
| 1225 |
+
</form>
|
| 1226 |
+
</div>
|
| 1227 |
+
{% endfor %}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1228 |
</div>
|
| 1229 |
{% endfor %}
|
| 1230 |
</div>
|
|
|
|
| 1270 |
<textarea id="add_description" name="description" rows="4"></textarea>
|
| 1271 |
|
| 1272 |
<label for="add_category">Категория:</label>
|
| 1273 |
+
<select id="add_category" name="category">
|
| 1274 |
<option value="Без категории">Без категории</option>
|
| 1275 |
+
{% for category in categories_data %}
|
| 1276 |
<option value="{{ category.name }}">{{ category.name }}</option>
|
| 1277 |
{% endfor %}
|
| 1278 |
</select>
|
| 1279 |
|
| 1280 |
<label for="add_subcategory">Подкатегория:</label>
|
| 1281 |
<select id="add_subcategory" name="subcategory">
|
| 1282 |
+
<option value="Без подкатегории">Без подкатегории</option>
|
| 1283 |
</select>
|
| 1284 |
+
|
| 1285 |
<label for="add_photos">Фотографии (до 10 шт.):</label>
|
| 1286 |
<input type="file" id="add_photos" name="photos" accept="image/*" multiple>
|
| 1287 |
<label>Цвета/Варианты (оставьте пустым, если нет):</label>
|
|
|
|
| 1334 |
<span class="status-indicator top-product"><i class="fas fa-star"></i> Топ</span>
|
| 1335 |
{% endif %}
|
| 1336 |
</h3>
|
| 1337 |
+
<p><strong>Категория:</strong> {{ product.get('category', 'Без категории') }}</p>
|
| 1338 |
+
<p><strong>Подкатегория:</strong> {{ product.get('subcategory', 'Без подкатегории') }}</p>
|
| 1339 |
<p><strong>Цены:</strong></p>
|
| 1340 |
<div class="admin-price-list">
|
| 1341 |
{% if product.get('prices') %}
|
|
|
|
| 1403 |
<textarea name="description" rows="4">{{ product.get('description', '') }}</textarea>
|
| 1404 |
|
| 1405 |
<label>Категория:</label>
|
| 1406 |
+
<select id="edit_category_{{ loop.index0 }}"
|
| 1407 |
+
name="category"
|
| 1408 |
+
data-initial-subcategory="{{ product.get('subcategory', 'Без подкатегории') }}"
|
| 1409 |
+
onchange="updateSubcategorySelect('edit_category_{{ loop.index0 }}', 'edit_subcategory_{{ loop.index0 }}')">
|
| 1410 |
<option value="Без категории" {% if product.get('category', 'Без категории') == 'Без категории' %}selected{% endif %}>Без категории</option>
|
| 1411 |
+
{% for category in categories_data %}
|
| 1412 |
<option value="{{ category.name }}" {% if product.get('category') == category.name %}selected{% endif %}>{{ category.name }}</option>
|
| 1413 |
{% endfor %}
|
| 1414 |
</select>
|
| 1415 |
+
|
| 1416 |
<label>Подкатегория:</label>
|
| 1417 |
+
<select id="edit_subcategory_{{ loop.index0 }}" name="subcategory">
|
| 1418 |
+
<!-- Options populated by JS -->
|
| 1419 |
</select>
|
| 1420 |
|
| 1421 |
<label>Заменить фотографии (выберите новые файлы, до 10 шт.):</label>
|
|
|
|
| 1472 |
</div>
|
| 1473 |
|
| 1474 |
<script>
|
| 1475 |
+
const categoriesData = {{ categories_data|tojson }};
|
| 1476 |
+
const categorySubcategoryMap = {};
|
| 1477 |
+
categoriesData.forEach(c => {
|
| 1478 |
+
categorySubcategoryMap[c.name] = c.subcategories;
|
| 1479 |
+
});
|
| 1480 |
|
| 1481 |
+
function updateSubcategorySelect(categorySelectId, subcategorySelectId) {
|
| 1482 |
+
const categorySelect = document.getElementById(categorySelectId);
|
| 1483 |
+
const subcategorySelect = document.getElementById(subcategorySelectId);
|
| 1484 |
+
if (!categorySelect || !subcategorySelect) return;
|
| 1485 |
+
|
| 1486 |
+
const selectedCategory = categorySelect.value;
|
| 1487 |
+
const subcategories = categorySubcategoryMap[selectedCategory] || [];
|
| 1488 |
+
|
| 1489 |
+
// Determine the initial subcategory value for edit forms
|
| 1490 |
+
let currentSubcategory = 'Без подкатегории';
|
| 1491 |
+
if (categorySelect.dataset.initialSubcategory) {
|
| 1492 |
+
currentSubcategory = categorySelect.dataset.initialSubcategory;
|
| 1493 |
+
// Clear the data attribute after using it once, so manual changes don't get overwritten
|
| 1494 |
+
delete categorySelect.dataset.initialSubcategory;
|
| 1495 |
+
}
|
| 1496 |
+
|
| 1497 |
+
// If the main category changed, reset the current subcategory selection check
|
| 1498 |
+
if (categorySelect.value !== selectedCategory) {
|
| 1499 |
+
currentSubcategory = 'Без подкатегории';
|
| 1500 |
+
}
|
| 1501 |
+
|
| 1502 |
+
subcategorySelect.innerHTML = '<option value="Без подкатегории">Без подкатегории</option>';
|
| 1503 |
+
|
| 1504 |
+
subcategories.forEach(sub => {
|
| 1505 |
+
const option = document.createElement('option');
|
| 1506 |
+
option.value = sub;
|
| 1507 |
+
option.textContent = sub;
|
| 1508 |
+
if (sub === currentSubcategory) {
|
| 1509 |
+
option.selected = true;
|
| 1510 |
+
}
|
| 1511 |
+
subcategorySelect.appendChild(option);
|
| 1512 |
+
});
|
| 1513 |
+
}
|
| 1514 |
+
|
| 1515 |
function toggleEditForm(formId) {
|
| 1516 |
const formContainer = document.getElementById(formId);
|
| 1517 |
if (formContainer) {
|
| 1518 |
+
formContainer.style.display = formContainer.style.display === 'none' || formContainer.style.display === '' ? 'block' : 'none';
|
| 1519 |
+
if (formContainer.style.display === 'block') {
|
| 1520 |
+
// Re-initialize subcategory select on opening the edit form
|
| 1521 |
+
const index = formId.split('-').pop();
|
| 1522 |
+
const catSelect = document.getElementById(`edit_category_${index}`);
|
| 1523 |
if (catSelect) {
|
| 1524 |
+
// Use the data attribute to restore the saved subcategory
|
| 1525 |
+
const initialSubcategory = catSelect.getAttribute('data-initial-subcategory');
|
| 1526 |
+
updateSubcategorySelect(`edit_category_${index}`, `edit_subcategory_${index}`, initialSubcategory);
|
| 1527 |
+
// Re-attach listener if not already done via onchange attribute
|
| 1528 |
}
|
| 1529 |
}
|
| 1530 |
}
|
| 1531 |
}
|
| 1532 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1533 |
function addColorInput(containerId) {
|
| 1534 |
const container = document.getElementById(containerId);
|
| 1535 |
if (container) {
|
|
|
|
| 1596 |
if(addPriceInputsDiv && addPriceInputsDiv.children.length === 0) {
|
| 1597 |
addPriceInput('add-price-inputs');
|
| 1598 |
}
|
| 1599 |
+
|
| 1600 |
+
// Setup for ADD form
|
| 1601 |
+
const addCatSelect = document.getElementById('add_category');
|
| 1602 |
+
if (addCatSelect) {
|
| 1603 |
+
updateSubcategorySelect('add_category', 'add_subcategory');
|
| 1604 |
+
addCatSelect.addEventListener('change', () => updateSubcategorySelect('add_category', 'add_subcategory'));
|
| 1605 |
+
}
|
| 1606 |
+
|
| 1607 |
+
// Initial setup for all EDIT forms (only run if the form is already open, otherwise done when opening via toggleEditForm)
|
| 1608 |
+
document.querySelectorAll('[id^="edit-form-"]').forEach(formContainer => {
|
| 1609 |
+
if (formContainer.style.display === 'block') {
|
| 1610 |
+
const index = formContainer.id.split('-').pop();
|
| 1611 |
+
const catSelect = document.getElementById(`edit_category_${index}`);
|
| 1612 |
+
if (catSelect) {
|
| 1613 |
+
const initialSubcategory = catSelect.getAttribute('data-initial-subcategory');
|
| 1614 |
+
updateSubcategorySelect(`edit_category_${index}`, `edit_subcategory_${index}`, initialSubcategory);
|
| 1615 |
+
}
|
| 1616 |
+
}
|
| 1617 |
});
|
| 1618 |
});
|
| 1619 |
+
|
| 1620 |
</script>
|
| 1621 |
</body>
|
| 1622 |
</html>
|
|
|
|
| 1627 |
def catalog():
|
| 1628 |
data = load_data()
|
| 1629 |
all_products = data.get('products', [])
|
| 1630 |
+
categories_data = data.get('categories', [])
|
| 1631 |
|
| 1632 |
products_in_stock = [p for p in all_products if p.get('in_stock', True)]
|
| 1633 |
products_sorted = sorted(products_in_stock, key=lambda p: (not p.get('is_top', False), p.get('name', '').lower()))
|
|
|
|
| 1635 |
return render_template_string(
|
| 1636 |
CATALOG_TEMPLATE,
|
| 1637 |
products=products_sorted,
|
| 1638 |
+
categories_data=categories_data,
|
| 1639 |
repo_id=REPO_ID,
|
| 1640 |
store_address=STORE_ADDRESS,
|
| 1641 |
currency_code=CURRENCY_CODE
|
|
|
|
| 1749 |
def admin():
|
| 1750 |
data = load_data()
|
| 1751 |
products = data.get('products', [])
|
| 1752 |
+
categories = data.get('categories', [])
|
| 1753 |
if 'orders' not in data or not isinstance(data.get('orders'), dict):
|
| 1754 |
data['orders'] = {}
|
| 1755 |
|
|
|
|
| 1762 |
category_name = request.form.get('category_name', '').strip()
|
| 1763 |
if category_name and not any(c['name'] == category_name for c in categories):
|
| 1764 |
categories.append({'name': category_name, 'subcategories': []})
|
| 1765 |
+
data['categories'] = categories
|
| 1766 |
save_data(data)
|
| 1767 |
logging.info(f"Category '{category_name}' added.")
|
| 1768 |
+
flash(f"Главная категория '{category_name}' успешно добавлена.", 'success')
|
| 1769 |
elif not category_name:
|
| 1770 |
logging.warning("Attempted to add empty category.")
|
| 1771 |
flash("Название категории не может быть пустым.", 'error')
|
| 1772 |
else:
|
| 1773 |
logging.warning(f"Category '{category_name}' already exists.")
|
| 1774 |
+
flash(f"Главная категория '{category_name}' уже существует.", 'error')
|
| 1775 |
|
| 1776 |
elif action == 'add_subcategory':
|
| 1777 |
+
parent_category_name = request.form.get('parent_category_name')
|
| 1778 |
subcategory_name = request.form.get('subcategory_name', '').strip()
|
| 1779 |
parent_cat = next((c for c in categories if c['name'] == parent_category_name), None)
|
| 1780 |
|
| 1781 |
+
if parent_cat and subcategory_name:
|
| 1782 |
+
if subcategory_name not in parent_cat['subcategories']:
|
| 1783 |
+
parent_cat['subcategories'].append(subcategory_name)
|
| 1784 |
+
save_data(data)
|
| 1785 |
+
flash(f"Подкатегория '{subcategory_name}' добавлена в '{parent_category_name}'.", 'success')
|
| 1786 |
+
else:
|
| 1787 |
+
flash(f"Подкатегория '{subcategory_name}' уже существует в этой категории.", 'error')
|
|
|
|
|
|
|
| 1788 |
else:
|
| 1789 |
+
flash("Неверное название категории или подкатегории.", 'error')
|
| 1790 |
|
| 1791 |
elif action == 'delete_category':
|
| 1792 |
category_to_delete = request.form.get('category_name')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1793 |
subcategory_to_delete = request.form.get('subcategory_name')
|
|
|
|
| 1794 |
|
| 1795 |
+
if subcategory_to_delete and category_to_delete:
|
| 1796 |
+
# Deleting a subcategory
|
| 1797 |
+
parent_cat = next((c for c in categories if c['name'] == category_to_delete), None)
|
| 1798 |
+
if parent_cat and subcategory_to_delete in parent_cat['subcategories']:
|
| 1799 |
+
parent_cat['subcategories'].remove(subcategory_to_delete)
|
| 1800 |
+
|
| 1801 |
+
# Update products whose category/subcategory matches
|
| 1802 |
+
updated_count = 0
|
| 1803 |
+
for product in products:
|
| 1804 |
+
if product.get('category') == category_to_delete and product.get('subcategory') == subcategory_to_delete:
|
| 1805 |
+
product['subcategory'] = 'Без подкатегории'
|
| 1806 |
+
updated_count += 1
|
| 1807 |
+
|
| 1808 |
+
save_data(data)
|
| 1809 |
+
flash(f"Подкатегория '{subcategory_to_delete}' из '{category_to_delete}' удалена. {updated_count} товаров обновлено.", 'success')
|
| 1810 |
+
else:
|
| 1811 |
+
flash(f"Не удалось найти подкатегорию '{subcategory_to_delete}' в '{category_to_delete}'.", 'error')
|
| 1812 |
+
elif category_to_delete and not subcategory_to_delete:
|
| 1813 |
+
# Deleting a main category
|
| 1814 |
+
if any(c['name'] == category_to_delete for c in categories):
|
| 1815 |
+
data['categories'] = [c for c in categories if c['name'] != category_to_delete]
|
| 1816 |
+
categories = data['categories']
|
| 1817 |
+
|
| 1818 |
+
updated_count = 0
|
| 1819 |
+
for product in products:
|
| 1820 |
+
if product.get('category') == category_to_delete:
|
| 1821 |
+
product['category'] = 'Без категории'
|
| 1822 |
+
product['subcategory'] = 'Без подкатегории'
|
| 1823 |
+
updated_count += 1
|
| 1824 |
+
|
| 1825 |
+
data['products'] = products
|
| 1826 |
+
save_data(data)
|
| 1827 |
+
flash(f"Главная категория '{category_to_delete}' удалена. {updated_count} товаров обновлено.", 'success')
|
| 1828 |
+
else:
|
| 1829 |
+
flash(f"Не удалось найти главную категорию '{category_to_delete}'.", 'error')
|
| 1830 |
else:
|
| 1831 |
+
flash("Неверное действие удаления категории/подкатегории.", 'error')
|
| 1832 |
+
|
| 1833 |
|
| 1834 |
elif action == 'add_product':
|
| 1835 |
name = request.form.get('name', '').strip()
|
| 1836 |
description = request.form.get('description', '').strip()
|
| 1837 |
category = request.form.get('category')
|
| 1838 |
+
subcategory = request.form.get('subcategory', 'Без подкатегории').strip()
|
| 1839 |
photos_files = request.files.getlist('photos')
|
| 1840 |
colors = [c.strip() for c in request.form.getlist('colors') if c.strip()]
|
| 1841 |
in_stock = 'in_stock' in request.form
|
|
|
|
| 1849 |
type_str = type_str.strip()
|
| 1850 |
value_str = value_str.strip().replace(',', '.')
|
| 1851 |
if not type_str or not value_str:
|
| 1852 |
+
continue
|
| 1853 |
|
| 1854 |
try:
|
| 1855 |
price_value = round(float(value_str), 2)
|
|
|
|
| 1859 |
except ValueError:
|
| 1860 |
logging.warning(f"Skipping invalid price value '{value_str}' for type '{type_str}' during add product.")
|
| 1861 |
|
| 1862 |
+
|
| 1863 |
if not name:
|
| 1864 |
flash("Название товара обязательно.", 'error')
|
| 1865 |
return redirect(url_for('admin'))
|
| 1866 |
if not valid_prices_exist:
|
| 1867 |
flash("Должен быть указан хотя бы один действительный вариант цены (Тип и Цена > 0).", 'error')
|
| 1868 |
return redirect(url_for('admin'))
|
| 1869 |
+
|
| 1870 |
+
if category != 'Без категории' and not any(c['name'] == category for c in categories):
|
| 1871 |
+
category = 'Без категории'
|
| 1872 |
+
subcategory = 'Без подкатегории'
|
| 1873 |
+
flash("Выбранная категория не найдена. Установлено 'Без категории'.", 'warning')
|
| 1874 |
|
| 1875 |
|
| 1876 |
photos_list = []
|
|
|
|
| 1929 |
|
| 1930 |
new_product = {
|
| 1931 |
'name': name, 'prices': prices, 'description': description,
|
| 1932 |
+
'category': category, 'subcategory': subcategory,
|
|
|
|
| 1933 |
'photos': photos_list, 'colors': colors,
|
| 1934 |
'in_stock': in_stock, 'is_top': is_top
|
| 1935 |
}
|
|
|
|
| 1959 |
|
| 1960 |
product_to_edit['name'] = request.form.get('name', product_to_edit['name']).strip()
|
| 1961 |
product_to_edit['description'] = request.form.get('description', product_to_edit.get('description', '')).strip()
|
| 1962 |
+
|
| 1963 |
+
new_category = request.form.get('category')
|
| 1964 |
+
new_subcategory = request.form.get('subcategory', 'Без подкатегории').strip()
|
| 1965 |
+
|
| 1966 |
+
if new_category != 'Без категории' and not any(c['name'] == new_category for c in categories):
|
| 1967 |
+
new_category = 'Без категории'
|
| 1968 |
+
new_subcategory = 'Без подкатегории'
|
| 1969 |
+
flash("Выбранная категория не найдена. Установлено 'Без категории' и 'Без подкатегории'.", 'warning')
|
| 1970 |
+
|
| 1971 |
+
product_to_edit['category'] = new_category
|
| 1972 |
+
product_to_edit['subcategory'] = new_subcategory
|
| 1973 |
+
|
| 1974 |
product_to_edit['colors'] = [c.strip() for c in request.form.getlist('colors') if c.strip()]
|
| 1975 |
product_to_edit['in_stock'] = 'in_stock' in request.form
|
| 1976 |
product_to_edit['is_top'] = 'is_top' in request.form
|
|
|
|
| 1983 |
type_str = type_str.strip()
|
| 1984 |
value_str = value_str.strip().replace(',', '.')
|
| 1985 |
if not type_str or not value_str:
|
| 1986 |
+
continue
|
| 1987 |
|
| 1988 |
try:
|
| 1989 |
price_value = round(float(value_str), 2)
|
|
|
|
| 2133 |
|
| 2134 |
current_data = load_data()
|
| 2135 |
display_products = sorted(current_data.get('products', []), key=lambda p: p.get('name', '').lower())
|
| 2136 |
+
display_categories_data = current_data.get('categories', [])
|
| 2137 |
|
| 2138 |
return render_template_string(
|
| 2139 |
ADMIN_TEMPLATE,
|
| 2140 |
products=display_products,
|
| 2141 |
+
categories_data=display_categories_data,
|
| 2142 |
repo_id=REPO_ID,
|
| 2143 |
currency_code=CURRENCY_CODE
|
| 2144 |
)
|