Spaces:
Build error
Build error
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
|
|
| 1 |
import os
|
| 2 |
import io
|
| 3 |
import base64
|
|
@@ -241,6 +242,12 @@ def get_env_data(env_id):
|
|
| 241 |
if 'product_id' not in product:
|
| 242 |
product['product_id'] = uuid4().hex
|
| 243 |
products_changed = True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 244 |
|
| 245 |
if products_changed or settings_changed:
|
| 246 |
save_env_data(env_id, env_data)
|
|
@@ -339,7 +346,27 @@ def generate_chat_response(message, chat_history_from_client, env_id):
|
|
| 339 |
for p in products:
|
| 340 |
if p.get('in_stock', True):
|
| 341 |
price_display = f"{p.get('price', 0):.2f}".replace('.00', '')
|
| 342 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 343 |
product_list_str = "\n".join(product_info_list) if product_info_list else "В данный момент нет товаров в наличии."
|
| 344 |
|
| 345 |
category_list_str = ", ".join(categories) if categories else "Категорий пока нет."
|
|
@@ -358,22 +385,26 @@ def generate_chat_response(message, chat_history_from_client, env_id):
|
|
| 358 |
|
| 359 |
|
| 360 |
system_instruction_content = (
|
| 361 |
-
f"Ты
|
| 362 |
-
"
|
| 363 |
-
"
|
| 364 |
-
"
|
| 365 |
-
"
|
| 366 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 367 |
f"Список доступных категорий: {category_list_str}.\n\n"
|
| 368 |
-
f"Список доступных товаров в
|
| 369 |
f"{product_list_str}"
|
| 370 |
f"{org_info_str}\n\n"
|
| 371 |
-
"Если пользователь спрашивает про
|
| 372 |
-
"Если вопрос касается общей информации о магазине (
|
| 373 |
-
"
|
| 374 |
-
"Избегай упоминания Hugging Face или Hugging Face Hub."
|
| 375 |
)
|
| 376 |
|
|
|
|
| 377 |
generated_text = ""
|
| 378 |
response = None
|
| 379 |
|
|
@@ -815,6 +846,7 @@ CATALOG_TEMPLATE = '''
|
|
| 815 |
.cart-item img { width: 60px; height: 60px; object-fit: cover; border-radius: 8px; }
|
| 816 |
.cart-item-details { grid-column: 2; }
|
| 817 |
.cart-item-details strong { display: block; margin-bottom: 5px; font-size: 1rem; color: var(--text-dark);}
|
|
|
|
| 818 |
.cart-item-price { font-size: 0.9rem; color: #666; }
|
| 819 |
.dark-theme .cart-item-price { color: #ccc; }
|
| 820 |
.cart-item-quantity { display: flex; align-items: center; gap: 8px; grid-column: 3;}
|
|
@@ -823,9 +855,9 @@ CATALOG_TEMPLATE = '''
|
|
| 823 |
.cart-item-total { font-weight: bold; text-align: right; grid-column: 4; font-size: 1rem; color: var(--bg-medium);}
|
| 824 |
.cart-item-remove { grid-column: 5; background:none; border:none; color: var(--danger); cursor:pointer; font-size: 1.3em; padding: 5px; line-height: 1; }
|
| 825 |
.cart-item-remove:hover { color: var(--danger-hover); }
|
| 826 |
-
.quantity-input, .
|
| 827 |
-
.dark-theme .quantity-input, .dark-theme .
|
| 828 |
-
.quantity-input:focus, .
|
| 829 |
.cart-summary { margin-top: 20px; text-align: right; border-top: 1px solid #e0e0e0; padding-top: 15px; }
|
| 830 |
.dark-theme .cart-summary { border-top-color: #444; }
|
| 831 |
.cart-summary strong { font-size: 1.2rem; color: var(--bg-medium);}
|
|
@@ -917,11 +949,10 @@ CATALOG_TEMPLATE = '''
|
|
| 917 |
<div id="quantityModal" class="modal">
|
| 918 |
<div class="modal-content">
|
| 919 |
<span class="close" onclick="closeModal('quantityModal')" aria-label="Закрыть">×</span>
|
| 920 |
-
<h2>Укажите
|
|
|
|
| 921 |
<label for="quantityInput">Количество:</label>
|
| 922 |
<input type="number" id="quantityInput" class="quantity-input" min="1" value="1">
|
| 923 |
-
<label for="colorSelect">Цвет/Вариант:</label>
|
| 924 |
-
<select id="colorSelect" class="color-select"></select>
|
| 925 |
<button class="product-button formulate-order-button" style="width:100%;" onclick="confirmAddToCart()"><i class="fas fa-check"></i> Добавить в корзину</button>
|
| 926 |
</div>
|
| 927 |
</div>
|
|
@@ -1013,12 +1044,37 @@ CATALOG_TEMPLATE = '''
|
|
| 1013 |
.then(data => {
|
| 1014 |
modalContent.innerHTML = data;
|
| 1015 |
initializeSwiper();
|
|
|
|
| 1016 |
})
|
| 1017 |
.catch(error => {
|
| 1018 |
console.error('Ошибка загрузки деталей продукта:', error);
|
| 1019 |
modalContent.innerHTML = `<p style="color: var(--danger); text-align:center; padding: 40px;">Не удалось загрузить информацию о товаре. ${error.message}</p>`;
|
| 1020 |
});
|
| 1021 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1022 |
|
| 1023 |
function initializeSwiper() {
|
| 1024 |
const swiperContainer = document.querySelector('#productModal .swiper-container');
|
|
@@ -1040,27 +1096,25 @@ CATALOG_TEMPLATE = '''
|
|
| 1040 |
selectedProductId = productId;
|
| 1041 |
const product = getProductById(productId);
|
| 1042 |
if (!product) {
|
| 1043 |
-
console.error("Product not found for ID:", productId);
|
| 1044 |
alert("Ошибка: товар не найден.");
|
| 1045 |
return;
|
| 1046 |
}
|
| 1047 |
-
const
|
| 1048 |
-
|
| 1049 |
-
|
| 1050 |
-
|
| 1051 |
-
|
| 1052 |
-
|
| 1053 |
-
|
| 1054 |
-
|
| 1055 |
-
option.text = color.trim();
|
| 1056 |
-
colorSelect.appendChild(option);
|
| 1057 |
-
});
|
| 1058 |
-
colorSelect.style.display = 'block';
|
| 1059 |
-
if(colorLabel) colorLabel.style.display = 'block';
|
| 1060 |
-
} else {
|
| 1061 |
-
colorSelect.style.display = 'none';
|
| 1062 |
-
if(colorLabel) colorLabel.style.display = 'none';
|
| 1063 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1064 |
document.getElementById('quantityInput').value = 1;
|
| 1065 |
const modal = document.getElementById('quantityModal');
|
| 1066 |
if(modal) {
|
|
@@ -1073,8 +1127,13 @@ CATALOG_TEMPLATE = '''
|
|
| 1073 |
if (selectedProductId === null) return;
|
| 1074 |
const quantityInput = document.getElementById('quantityInput');
|
| 1075 |
const quantity = parseInt(quantityInput.value);
|
| 1076 |
-
|
| 1077 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1078 |
if (isNaN(quantity) || quantity <= 0) {
|
| 1079 |
alert("Пожалуйста, укажите корректное количество (больше 0).");
|
| 1080 |
quantityInput.focus();
|
|
@@ -1085,7 +1144,14 @@ CATALOG_TEMPLATE = '''
|
|
| 1085 |
alert("Ошибка добавления: товар не найден.");
|
| 1086 |
return;
|
| 1087 |
}
|
| 1088 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1089 |
const existingItemIndex = cart.findIndex(item => item.id === cartItemId);
|
| 1090 |
if (existingItemIndex > -1) {
|
| 1091 |
cart[existingItemIndex].quantity += quantity;
|
|
@@ -1094,10 +1160,11 @@ CATALOG_TEMPLATE = '''
|
|
| 1094 |
id: cartItemId,
|
| 1095 |
product_id: product.product_id,
|
| 1096 |
name: product.name,
|
| 1097 |
-
price:
|
| 1098 |
photo: product.photos && product.photos.length > 0 ? product.photos[0] : null,
|
| 1099 |
quantity: quantity,
|
| 1100 |
-
color: color
|
|
|
|
| 1101 |
});
|
| 1102 |
}
|
| 1103 |
localStorage.setItem(`mekaCart_${envId}`, JSON.stringify(cart));
|
|
@@ -1136,12 +1203,17 @@ CATALOG_TEMPLATE = '''
|
|
| 1136 |
const photoUrl = item.photo
|
| 1137 |
? `https://huggingface.co/datasets/${repoId}/resolve/main/photos/${item.photo}`
|
| 1138 |
: 'https://via.placeholder.com/60x60.png?text=N/A';
|
| 1139 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1140 |
return `
|
| 1141 |
<div class="cart-item">
|
| 1142 |
<img src="${photoUrl}" alt="${item.name}">
|
| 1143 |
<div class="cart-item-details">
|
| 1144 |
-
<strong>${item.name}
|
|
|
|
| 1145 |
<p class="cart-item-price">${item.price.toFixed(2)} ${currencyCode}</p>
|
| 1146 |
</div>
|
| 1147 |
<div class="cart-item-quantity">
|
|
@@ -1482,8 +1554,8 @@ CHAT_TEMPLATE = '''
|
|
| 1482 |
body.dark-theme .quantity-btn { background-color: #444; border-color: #555; color: #fff; }
|
| 1483 |
.cart-item-total { font-weight: bold; text-align: right; font-size: 1rem; color: var(--bg-medium);}
|
| 1484 |
.cart-item-remove { background:none; border:none; color: var(--danger); cursor:pointer; font-size: 1.3em; }
|
| 1485 |
-
.quantity-input, .
|
| 1486 |
-
body.dark-theme .quantity-input, body.dark-theme .
|
| 1487 |
.cart-summary { margin-top: 20px; text-align: right; border-top: 1px solid #e0e0e0; padding-top: 15px; }
|
| 1488 |
body.dark-theme .cart-summary { border-top-color: #444; }
|
| 1489 |
.cart-actions { margin-top: 25px; display: flex; justify-content: space-between; }
|
|
@@ -1535,9 +1607,10 @@ CHAT_TEMPLATE = '''
|
|
| 1535 |
<div id="quantityModal" class="modal">
|
| 1536 |
<div class="modal-content">
|
| 1537 |
<span class="close" onclick="closeModal('quantityModal')">×</span>
|
| 1538 |
-
<h2>Укажите
|
|
|
|
|
|
|
| 1539 |
<input type="number" id="quantityInput" class="quantity-input" min="1" value="1">
|
| 1540 |
-
<select id="colorSelect" class="color-select"></select>
|
| 1541 |
<button class="product-button formulate-order-button" onclick="confirmAddToCart()">Добавить в корзину</button>
|
| 1542 |
</div>
|
| 1543 |
</div>
|
|
@@ -1609,37 +1682,50 @@ CHAT_TEMPLATE = '''
|
|
| 1609 |
selectedProductId = productId;
|
| 1610 |
const product = getProductById(productId);
|
| 1611 |
if (!product) return;
|
| 1612 |
-
const
|
| 1613 |
-
|
| 1614 |
-
|
| 1615 |
-
if (
|
| 1616 |
-
|
| 1617 |
-
|
| 1618 |
-
|
| 1619 |
-
|
| 1620 |
-
|
| 1621 |
-
|
| 1622 |
-
|
| 1623 |
-
|
|
|
|
|
|
|
| 1624 |
}
|
| 1625 |
document.getElementById('quantityInput').value = 1;
|
| 1626 |
document.getElementById('quantityModal').style.display = "block";
|
| 1627 |
}
|
| 1628 |
function confirmAddToCart() {
|
| 1629 |
const quantity = parseInt(document.getElementById('quantityInput').value);
|
| 1630 |
-
const colorSelect = document.getElementById('
|
| 1631 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1632 |
if (isNaN(quantity) || quantity <= 0) {
|
| 1633 |
alert("Укажите корректное количество.");
|
| 1634 |
return;
|
| 1635 |
}
|
| 1636 |
const product = getProductById(selectedProductId);
|
| 1637 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1638 |
const existingItem = cart.find(item => item.id === cartItemId);
|
| 1639 |
if (existingItem) {
|
| 1640 |
existingItem.quantity += quantity;
|
| 1641 |
} else {
|
| 1642 |
-
cart.push({ id: cartItemId, product_id: product.product_id, name: product.name, price:
|
| 1643 |
}
|
| 1644 |
localStorage.setItem(`mekaCart_${envId}`, JSON.stringify(cart));
|
| 1645 |
closeModal('quantityModal');
|
|
@@ -1670,9 +1756,12 @@ CHAT_TEMPLATE = '''
|
|
| 1670 |
cartContent.innerHTML = cart.map(item => {
|
| 1671 |
const itemTotal = item.price * item.quantity;
|
| 1672 |
total += itemTotal;
|
|
|
|
|
|
|
|
|
|
| 1673 |
return `<div class="cart-item">
|
| 1674 |
<img src="${item.photo ? `https://huggingface.co/datasets/${repoId}/resolve/main/photos/${item.photo}` : ''}" alt="${item.name}">
|
| 1675 |
-
<div><strong>${item.name}
|
| 1676 |
<div class="cart-item-quantity"><button class="quantity-btn" onclick="decrementCartItem('${item.id}')">-</button><span>${item.quantity}</span><button class="quantity-btn" onclick="incrementCartItem('${item.id}')">+</button></div>
|
| 1677 |
<span class="cart-item-total">${itemTotal.toFixed(2)}</span>
|
| 1678 |
<button class="cart-item-remove" onclick="removeFromCart('${item.id}')"><i class="fas fa-trash-alt"></i></button>
|
|
@@ -1842,7 +1931,7 @@ CHAT_TEMPLATE = '''
|
|
| 1842 |
'''
|
| 1843 |
|
| 1844 |
PRODUCT_DETAIL_TEMPLATE = '''
|
| 1845 |
-
<div style="padding: 10px;">
|
| 1846 |
<h2 style="font-size: 1.6rem; font-weight: 600; margin-bottom: 15px; text-align: center; color: #135D66;">{{ product['name'] }}</h2>
|
| 1847 |
<div class="swiper-container" style="max-width: 450px; margin: 0 auto 20px; border-radius: 10px; overflow: hidden; background-color: #fff; border: 1px solid #e0e0e0;">
|
| 1848 |
<div class="swiper-wrapper">
|
|
@@ -1868,9 +1957,32 @@ PRODUCT_DETAIL_TEMPLATE = '''
|
|
| 1868 |
<div class="swiper-button-prev" style="color: #135D66;"></div>
|
| 1869 |
{% endif %}
|
| 1870 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1871 |
|
| 1872 |
<div style="text-align:center; margin-top:20px; padding: 0 10px;">
|
| 1873 |
-
<p style="font-size: 1.5rem; font-weight: bold; color: #135D66; margin-bottom: 15px;"><strong>Цена:</strong> {{ "%.0f"|format(product.price) }} {{ currency_code }}</p>
|
| 1874 |
<button class="product-button formulate-order-button" style="padding: 12px 30px; width: 100%; max-width: 300px;" onclick="closeModal('productModal'); openQuantityModalById('{{ product.get('product_id', '') }}')">
|
| 1875 |
<i class="fas fa-cart-plus"></i> В корзину
|
| 1876 |
</button>
|
|
@@ -1879,12 +1991,12 @@ PRODUCT_DETAIL_TEMPLATE = '''
|
|
| 1879 |
<div style="margin-top: 20px; font-size: 1rem; line-height: 1.7; color: #333; padding: 0 10px;">
|
| 1880 |
<p><strong>Категория:</strong> {{ product.get('category', 'Без категории') }}</p>
|
| 1881 |
<p><strong>Описание:</strong><br> {{ product.get('description', 'Описание отсутствует.')|replace('\\n', '<br>')|safe }}</p>
|
| 1882 |
-
{% set colors = product.get('colors', []) %}
|
| 1883 |
-
{% if colors and colors|select('ne', '')|list|length > 0 %}
|
| 1884 |
-
<p><strong>Доступные цвета/варианты:</strong> {{ colors|select('ne', '')|join(', ') }}</p>
|
| 1885 |
-
{% endif %}
|
| 1886 |
</div>
|
| 1887 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1888 |
'''
|
| 1889 |
|
| 1890 |
ORDER_TEMPLATE = '''
|
|
@@ -1970,11 +2082,17 @@ ORDER_TEMPLATE = '''
|
|
| 1970 |
container.innerHTML = '<p style="text-align:center; padding: 20px;">Заказ пуст.</p>';
|
| 1971 |
document.querySelector('.actions .button').disabled = true;
|
| 1972 |
} else {
|
| 1973 |
-
container.innerHTML = order.cart.map((item, index) =>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1974 |
<div class="order-item">
|
| 1975 |
<img src="${item.photo_url}" alt="${item.name}">
|
| 1976 |
<div class="item-details">
|
| 1977 |
-
<strong>${item.name}
|
|
|
|
| 1978 |
<span>${item.price.toFixed(2)} {{ currency_code }}</span>
|
| 1979 |
</div>
|
| 1980 |
<div class="item-quantity">
|
|
@@ -1986,7 +2104,7 @@ ORDER_TEMPLATE = '''
|
|
| 1986 |
${(item.price * item.quantity).toFixed(2)} {{ currency_code }}
|
| 1987 |
</div>
|
| 1988 |
</div>
|
| 1989 |
-
`).join('');
|
| 1990 |
document.querySelector('.actions .button').disabled = false;
|
| 1991 |
}
|
| 1992 |
updateOrderTotal();
|
|
@@ -2032,7 +2150,12 @@ ORDER_TEMPLATE = '''
|
|
| 2032 |
message += `*Номер заказа:* ${orderId}%0A%0A`;
|
| 2033 |
|
| 2034 |
order.cart.forEach(item => {
|
| 2035 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2036 |
message += ` - Количество: ${item.quantity}%0A`;
|
| 2037 |
message += ` - Цена: ${item.price.toFixed(2)} {{ currency_code }}%0A`;
|
| 2038 |
});
|
|
@@ -2116,12 +2239,12 @@ ADMIN_TEMPLATE = '''
|
|
| 2116 |
details[open] > summary::after { transform: translateY(-50%) rotate(180deg); }
|
| 2117 |
details[open] > summary { border-bottom: 1px solid #e0e0e0; }
|
| 2118 |
details .form-content { padding: 20px; }
|
| 2119 |
-
.
|
| 2120 |
-
.
|
| 2121 |
-
.remove-
|
| 2122 |
-
.remove-
|
| 2123 |
-
.add-
|
| 2124 |
-
.add-
|
| 2125 |
.photo-preview img { max-width: 70px; max-height: 70px; border-radius: 5px; margin: 5px 5px 0 0; border: 1px solid #e0e0e0; object-fit: cover;}
|
| 2126 |
.sync-buttons { display: flex; gap: 10px; margin-bottom: 20px; flex-wrap: wrap; }
|
| 2127 |
.download-hf-button { background-color: #6c757d; color: white; }
|
|
@@ -2150,6 +2273,10 @@ ADMIN_TEMPLATE = '''
|
|
| 2150 |
.chat-message.user .bubble { background-color: #dcf8c6; }
|
| 2151 |
.chat-message.ai .bubble { background-color: #f1f1f1; }
|
| 2152 |
.current-avatar { max-width: 60px; max-height: 60px; border-radius: 50%; vertical-align: middle; margin-left: 10px; border: 2px solid var(--bg-medium);}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2153 |
</style>
|
| 2154 |
</head>
|
| 2155 |
<body>
|
|
@@ -2328,7 +2455,7 @@ ADMIN_TEMPLATE = '''
|
|
| 2328 |
<input type="hidden" name="action" value="add_product">
|
| 2329 |
<label for="add_name">Название товара *:</label>
|
| 2330 |
<input type="text" id="add_name" name="name" required>
|
| 2331 |
-
<label for="add_price"
|
| 2332 |
<input type="number" id="add_price" name="price" step="0.01" min="0" required>
|
| 2333 |
<label for="add_photos">Фотографии (до 10 шт.):</label>
|
| 2334 |
<input type="file" id="add_photos" name="photos" accept="image/*" multiple>
|
|
@@ -2350,13 +2477,27 @@ ADMIN_TEMPLATE = '''
|
|
| 2350 |
{% endfor %}
|
| 2351 |
</select>
|
| 2352 |
<label>Цвета/Варианты (оставьте пустым, если нет):</label>
|
| 2353 |
-
<div id="add-color-inputs">
|
| 2354 |
-
<div class="
|
| 2355 |
<input type="text" name="colors" placeholder="Например: Розовый">
|
| 2356 |
-
<button type="button" class="remove-
|
| 2357 |
</div>
|
| 2358 |
</div>
|
| 2359 |
-
<button type="button" class="button add-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2360 |
<br>
|
| 2361 |
<div style="margin-top: 15px;">
|
| 2362 |
<input type="checkbox" id="add_in_stock" name="in_stock" checked>
|
|
@@ -2403,7 +2544,10 @@ ADMIN_TEMPLATE = '''
|
|
| 2403 |
<p><strong>Цена:</strong> {{ "%.2f"|format(product.price) }} {{ currency_code }}</p>
|
| 2404 |
<p class="description" title="{{ product.get('description', '') }}"><strong>Описание:</strong> {{ product.get('description', 'N/A')[:150] }}{% if product.get('description', '')|length > 150 %}...{% endif %}</p>
|
| 2405 |
{% set colors = product.get('colors', []) %}
|
|
|
|
| 2406 |
<p><strong>Цвета/Вар-ты:</strong> {{ colors|select('ne', '')|join(', ') if colors|select('ne', '')|list|length > 0 else 'Нет' }}</p>
|
|
|
|
|
|
|
| 2407 |
{% if product.get('photos') and product['photos']|length > 1 %}
|
| 2408 |
<p style="font-size: 0.8rem; color: #999;">(Всего фото: {{ product['photos']|length }})</p>
|
| 2409 |
{% endif %}
|
|
@@ -2456,25 +2600,50 @@ ADMIN_TEMPLATE = '''
|
|
| 2456 |
{% endfor %}
|
| 2457 |
</select>
|
| 2458 |
<label>Цвета/Варианты:</label>
|
| 2459 |
-
<div id="edit-color-inputs-{{ loop.index0 }}">
|
| 2460 |
{% set current_colors = product.get('colors', []) %}
|
| 2461 |
{% if current_colors and current_colors|select('ne', '')|list|length > 0 %}
|
| 2462 |
{% for color in current_colors %}
|
| 2463 |
{% if color.strip() %}
|
| 2464 |
-
<div class="
|
| 2465 |
<input type="text" name="colors" value="{{ color }}">
|
| 2466 |
-
<button type="button" class="remove-
|
| 2467 |
</div>
|
| 2468 |
{% endif %}
|
| 2469 |
{% endfor %}
|
| 2470 |
{% else %}
|
| 2471 |
-
<div class="
|
| 2472 |
<input type="text" name="colors" placeholder="Например: Цвет">
|
| 2473 |
-
<button type="button" class="remove-
|
| 2474 |
</div>
|
| 2475 |
{% endif %}
|
| 2476 |
</div>
|
| 2477 |
-
<button type="button" class="button add-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2478 |
<br>
|
| 2479 |
<div style="margin-top: 15px;">
|
| 2480 |
<input type="checkbox" id="edit_in_stock_{{ loop.index0 }}" name="in_stock" {% if product.get('in_stock', True) %}checked{% endif %}>
|
|
@@ -2505,48 +2674,107 @@ ADMIN_TEMPLATE = '''
|
|
| 2505 |
</div>
|
| 2506 |
|
| 2507 |
<script>
|
|
|
|
|
|
|
| 2508 |
function toggleEditForm(formId) {
|
| 2509 |
const formContainer = document.getElementById(formId);
|
| 2510 |
if (formContainer) {
|
| 2511 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2512 |
}
|
| 2513 |
}
|
| 2514 |
|
| 2515 |
-
function
|
| 2516 |
const container = document.getElementById(containerId);
|
| 2517 |
if (container) {
|
| 2518 |
const newInputGroup = document.createElement('div');
|
| 2519 |
-
newInputGroup.className = '
|
| 2520 |
newInputGroup.innerHTML = `
|
| 2521 |
-
<input type="text" name="
|
| 2522 |
-
<button type="button" class="remove-
|
| 2523 |
`;
|
| 2524 |
container.appendChild(newInputGroup);
|
| 2525 |
-
|
| 2526 |
-
if (newInput) {
|
| 2527 |
-
newInput.focus();
|
| 2528 |
-
}
|
| 2529 |
}
|
| 2530 |
}
|
| 2531 |
|
| 2532 |
-
function
|
| 2533 |
-
const group = button.closest('.
|
| 2534 |
if (group) {
|
| 2535 |
const container = group.parentNode;
|
| 2536 |
group.remove();
|
| 2537 |
if (container && container.children.length === 0) {
|
| 2538 |
-
|
| 2539 |
-
|
| 2540 |
-
|
| 2541 |
-
<input type="text" name="colors" placeholder="Например: Цвет">
|
| 2542 |
-
<button type="button" class="remove-color-btn" onclick="removeColorInput(this)"><i class="fas fa-times"></i></button>
|
| 2543 |
-
`;
|
| 2544 |
-
container.appendChild(placeholderGroup);
|
| 2545 |
}
|
| 2546 |
-
} else {
|
| 2547 |
-
console.warn("Could not find parent .color-input-group for remove button");
|
| 2548 |
}
|
| 2549 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2550 |
|
| 2551 |
async function generateDescription(photoInputId, descriptionTextareaId, languageSelectId) {
|
| 2552 |
const photoInput = document.getElementById(photoInputId);
|
|
@@ -2874,6 +3102,7 @@ def create_order(env_id):
|
|
| 2874 |
"price": price,
|
| 2875 |
"quantity": quantity,
|
| 2876 |
"color": item.get('color', 'N/A'),
|
|
|
|
| 2877 |
"photo": item.get('photo'),
|
| 2878 |
"photo_url": f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/photos/{item['photo']}" if item.get('photo') else "https://via.placeholder.com/60x60.png?text=N/A"
|
| 2879 |
})
|
|
@@ -3018,177 +3247,101 @@ def admin(env_id):
|
|
| 3018 |
save_env_data(env_id, data)
|
| 3019 |
flash("Настройки магазина и чата успешно обновлены.", 'success')
|
| 3020 |
|
| 3021 |
-
elif action == 'add_product':
|
| 3022 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3023 |
price_str = request.form.get('price', '').replace(',', '.')
|
| 3024 |
-
description = request.form.get('description', '').strip()
|
| 3025 |
category = request.form.get('category')
|
| 3026 |
-
|
| 3027 |
-
colors =
|
| 3028 |
-
|
| 3029 |
-
|
|
|
|
| 3030 |
|
| 3031 |
-
if not name or not price_str:
|
| 3032 |
flash("Название и цена товара обязательны.", 'error')
|
| 3033 |
return redirect(url_for('admin', env_id=env_id))
|
| 3034 |
-
|
| 3035 |
try:
|
| 3036 |
price = round(float(price_str), 2)
|
| 3037 |
if price < 0: price = 0
|
|
|
|
| 3038 |
except ValueError:
|
| 3039 |
-
|
| 3040 |
-
|
| 3041 |
-
|
| 3042 |
-
photos_list = []
|
| 3043 |
-
if photos_files and HF_TOKEN_WRITE:
|
| 3044 |
-
uploads_dir = 'uploads_temp'
|
| 3045 |
-
os.makedirs(uploads_dir, exist_ok=True)
|
| 3046 |
-
api = HfApi()
|
| 3047 |
-
photo_limit = 10
|
| 3048 |
-
uploaded_count = 0
|
| 3049 |
-
for photo in photos_files:
|
| 3050 |
-
if uploaded_count >= photo_limit:
|
| 3051 |
-
flash(f"Загружено только первые {photo_limit} фото.", "warning")
|
| 3052 |
-
break
|
| 3053 |
-
if photo and photo.filename:
|
| 3054 |
-
try:
|
| 3055 |
-
ext = os.path.splitext(photo.filename)[1].lower()
|
| 3056 |
-
if ext not in ['.jpg', '.jpeg', '.png', '.gif', '.webp']:
|
| 3057 |
-
flash(f"Файл {photo.filename} не является изображением и был пропущен.", "warning")
|
| 3058 |
-
continue
|
| 3059 |
-
|
| 3060 |
-
safe_name = secure_filename(name.replace(' ', '_'))[:50]
|
| 3061 |
-
photo_filename = f"{safe_name}_{datetime.now().strftime('%Y%m%d%H%M%S%f')}{ext}"
|
| 3062 |
-
temp_path = os.path.join(uploads_dir, photo_filename)
|
| 3063 |
-
photo.save(temp_path)
|
| 3064 |
-
api.upload_file(
|
| 3065 |
-
path_or_fileobj=temp_path,
|
| 3066 |
-
path_in_repo=f"photos/{photo_filename}",
|
| 3067 |
-
repo_id=REPO_ID,
|
| 3068 |
-
repo_type="dataset",
|
| 3069 |
-
token=HF_TOKEN_WRITE,
|
| 3070 |
-
commit_message=f"Add photo for product {name}"
|
| 3071 |
-
)
|
| 3072 |
-
photos_list.append(photo_filename)
|
| 3073 |
-
os.remove(temp_path)
|
| 3074 |
-
uploaded_count += 1
|
| 3075 |
-
except Exception as e:
|
| 3076 |
-
flash(f"Ошибка при загрузке фото {photo.filename}.", 'error')
|
| 3077 |
-
if os.path.exists(temp_path):
|
| 3078 |
-
try: os.remove(temp_path)
|
| 3079 |
-
except OSError: pass
|
| 3080 |
-
elif photo and not photo.filename:
|
| 3081 |
-
pass
|
| 3082 |
-
try:
|
| 3083 |
-
if os.path.exists(uploads_dir) and not os.listdir(uploads_dir):
|
| 3084 |
-
os.rmdir(uploads_dir)
|
| 3085 |
-
except OSError as e:
|
| 3086 |
-
pass
|
| 3087 |
-
elif not HF_TOKEN_WRITE and photos_files and any(f.filename for f in photos_files):
|
| 3088 |
-
flash("HF_TOKEN (write) не настроен. Фотографии не были загружены.", "warning")
|
| 3089 |
-
|
| 3090 |
-
|
| 3091 |
-
new_product = {
|
| 3092 |
-
'product_id': uuid4().hex,
|
| 3093 |
-
'name': name, 'price': price, 'description': description,
|
| 3094 |
-
'category': category if category in categories else 'Без категории',
|
| 3095 |
-
'photos': photos_list, 'colors': colors,
|
| 3096 |
-
'in_stock': in_stock, 'is_top': is_top
|
| 3097 |
-
}
|
| 3098 |
-
products.append(new_product)
|
| 3099 |
-
data['products'] = products
|
| 3100 |
-
save_env_data(env_id, data)
|
| 3101 |
-
flash(f"Товар '{name}' успешно добавлен.", 'success')
|
| 3102 |
-
|
| 3103 |
-
elif action == 'edit_product':
|
| 3104 |
-
product_id = request.form.get('product_id')
|
| 3105 |
-
product_to_edit = next((p for p in products if p.get('product_id') == product_id), None)
|
| 3106 |
-
|
| 3107 |
-
if product_to_edit is None:
|
| 3108 |
-
flash(f"Ошибка редактирования: товар с ID '{product_id}' не найден.", 'error')
|
| 3109 |
-
return redirect(url_for('admin', env_id=env_id))
|
| 3110 |
-
|
| 3111 |
-
original_name = product_to_edit.get('name', 'N/A')
|
| 3112 |
-
|
| 3113 |
-
product_to_edit['name'] = request.form.get('name', product_to_edit['name']).strip()
|
| 3114 |
-
price_str = request.form.get('price', str(product_to_edit['price'])).replace(',', '.')
|
| 3115 |
-
product_to_edit['description'] = request.form.get('description', product_to_edit.get('description', '')).strip()
|
| 3116 |
-
category = request.form.get('category')
|
| 3117 |
-
product_to_edit['category'] = category if category in categories else 'Без категории'
|
| 3118 |
-
product_to_edit['colors'] = [c.strip() for c in request.form.getlist('colors') if c.strip()]
|
| 3119 |
-
product_to_edit['in_stock'] = 'in_stock' in request.form
|
| 3120 |
-
product_to_edit['is_top'] = 'is_top' in request.form
|
| 3121 |
|
| 3122 |
-
|
| 3123 |
-
|
| 3124 |
-
if
|
| 3125 |
-
|
| 3126 |
-
|
| 3127 |
-
|
|
|
|
|
|
|
|
|
|
| 3128 |
|
| 3129 |
photos_files = request.files.getlist('photos')
|
| 3130 |
-
if photos_files and any(f.filename for f in photos_files)
|
| 3131 |
-
|
| 3132 |
-
|
| 3133 |
-
|
| 3134 |
-
|
| 3135 |
-
|
| 3136 |
-
|
| 3137 |
-
|
| 3138 |
-
|
| 3139 |
-
|
| 3140 |
-
|
| 3141 |
-
|
| 3142 |
-
|
| 3143 |
-
|
| 3144 |
-
|
| 3145 |
-
|
| 3146 |
-
|
| 3147 |
-
|
| 3148 |
-
|
| 3149 |
-
|
| 3150 |
-
|
| 3151 |
-
|
| 3152 |
-
|
| 3153 |
-
|
| 3154 |
-
|
| 3155 |
-
|
| 3156 |
-
|
| 3157 |
-
|
| 3158 |
-
|
| 3159 |
-
|
| 3160 |
-
if os.path.exists(temp_path):
|
| 3161 |
-
try: os.remove(temp_path)
|
| 3162 |
-
except OSError: pass
|
| 3163 |
-
try:
|
| 3164 |
-
if os.path.exists(uploads_dir) and not os.listdir(uploads_dir):
|
| 3165 |
-
os.rmdir(uploads_dir)
|
| 3166 |
-
except OSError as e:
|
| 3167 |
-
pass
|
| 3168 |
-
|
| 3169 |
-
if new_photos_list:
|
| 3170 |
-
old_photos = product_to_edit.get('photos', [])
|
| 3171 |
-
if old_photos:
|
| 3172 |
try:
|
| 3173 |
-
api =
|
| 3174 |
-
|
| 3175 |
-
|
| 3176 |
-
|
| 3177 |
-
|
| 3178 |
-
|
| 3179 |
-
|
| 3180 |
-
|
| 3181 |
-
|
| 3182 |
-
|
| 3183 |
-
|
| 3184 |
-
flash("
|
| 3185 |
-
|
| 3186 |
-
|
| 3187 |
-
|
| 3188 |
-
flash("
|
| 3189 |
-
|
|
|
|
| 3190 |
save_env_data(env_id, data)
|
| 3191 |
-
|
| 3192 |
|
| 3193 |
elif action == 'delete_product':
|
| 3194 |
product_id = request.form.get('product_id')
|
|
|
|
| 1 |
+
|
| 2 |
import os
|
| 3 |
import io
|
| 4 |
import base64
|
|
|
|
| 242 |
if 'product_id' not in product:
|
| 243 |
product['product_id'] = uuid4().hex
|
| 244 |
products_changed = True
|
| 245 |
+
if 'sizes' not in product:
|
| 246 |
+
product['sizes'] = []
|
| 247 |
+
products_changed = True
|
| 248 |
+
if 'variant_prices' not in product:
|
| 249 |
+
product['variant_prices'] = {}
|
| 250 |
+
products_changed = True
|
| 251 |
|
| 252 |
if products_changed or settings_changed:
|
| 253 |
save_env_data(env_id, env_data)
|
|
|
|
| 346 |
for p in products:
|
| 347 |
if p.get('in_stock', True):
|
| 348 |
price_display = f"{p.get('price', 0):.2f}".replace('.00', '')
|
| 349 |
+
|
| 350 |
+
colors_str = f"Цвета: {', '.join(p.get('colors', []))}" if p.get('colors') else ""
|
| 351 |
+
sizes_str = f"Размеры: {', '.join(p.get('sizes', []))}" if p.get('sizes') else ""
|
| 352 |
+
|
| 353 |
+
options_str = ""
|
| 354 |
+
if colors_str or sizes_str:
|
| 355 |
+
options_str = f", Варианты: ({' '.join(filter(None, [colors_str, sizes_str]))})"
|
| 356 |
+
|
| 357 |
+
variant_prices_str = ""
|
| 358 |
+
if p.get('variant_prices'):
|
| 359 |
+
variant_prices_list = [f"{k.replace('-', ' ')} - {v} {currency_code}" for k, v in p['variant_prices'].items()]
|
| 360 |
+
if variant_prices_list:
|
| 361 |
+
variant_prices_str = f", Особые цены: [{'; '.join(variant_prices_list)}]"
|
| 362 |
+
|
| 363 |
+
product_info_list.append(
|
| 364 |
+
f"- [ID_ТОВАРА: {p.get('product_id', 'N/A')} Название: {p.get('name', 'Без названия')}], "
|
| 365 |
+
f"Категория: {p.get('category', 'Без категории')}, "
|
| 366 |
+
f"Базовая цена: {price_display} {currency_code}"
|
| 367 |
+
f"{options_str}{variant_prices_str}, "
|
| 368 |
+
f"Описание: {p.get('description', '')[:100]}..."
|
| 369 |
+
)
|
| 370 |
product_list_str = "\n".join(product_info_list) if product_info_list else "В данный момент нет товаров в наличии."
|
| 371 |
|
| 372 |
category_list_str = ", ".join(categories) if categories else "Категорий пока нет."
|
|
|
|
| 385 |
|
| 386 |
|
| 387 |
system_instruction_content = (
|
| 388 |
+
f"Ты — первоклассный виртуальный консультант-продажник по имени {chat_name} для магазина {org_name}. Твоя главная цель — не просто отвечать на вопросы, а продавать, используя все свои навыки. "
|
| 389 |
+
"Говори на любом языке, на котором к тебе обращается клиент. Будь энергичным, убедительным и проактивным. "
|
| 390 |
+
"Твоя задача — помогать пользователям находить товары, мастерски отвечать на вопросы о них, предлагать лучшие варианты, создавать ценность и закрывать сделку. "
|
| 391 |
+
"Всегда будь вежлив, но настойчив. Твоя речь должна быть живой, с использованием эмодзи, чтобы располагать к себе клиента. "
|
| 392 |
+
"Никогда не выдумывай товары, категории или характеристики, которых нет в предоставленных списках. "
|
| 393 |
+
"Когда ты предлагаешь товар, всегда указывай его название и ID, используя *точный формат*: [ID_ТОВАРА: <product_id> Название: <product_name>]. Это *критически важно* для клиента. "
|
| 394 |
+
"Если пользователь спрашивает цену на конкретный вариант (цвет или размер), найди ее в 'Особые цены'. Если там нет, используй 'Базовая цена'. "
|
| 395 |
+
"Активно предлагай сопутствующие товары или более дорогие аналоги (апсейл). Например: 'Отличный выбор! К этому телефону идеально подойдут наши новые беспроводные наушники. Хотите взглянуть?'. "
|
| 396 |
+
"Создавай ощущение срочности: 'Эта модель сейчас очень популярна, осталось всего несколько штук!'. "
|
| 397 |
+
"Работай с возражениями: если клиент говорит 'дорого', расскажи о качестве, гарантии и уникальных особенностях товара.\n\n"
|
| 398 |
f"Список доступных категорий: {category_list_str}.\n\n"
|
| 399 |
+
f"Список доступных товаров в магазине (используй эту информацию для ответов):\n"
|
| 400 |
f"{product_list_str}"
|
| 401 |
f"{org_info_str}\n\n"
|
| 402 |
+
"Если пользователь спрашивает про товары, которых нет, вежливо сообщи об этом и немедленно предложи лучшую альтернативу из имеющихся. "
|
| 403 |
+
"Если вопрос касается общей информации о магазине (доставка, возврат), используй данные из блока 'Информация о магазине' и сразу после ответа возвращай разговор к покупкам: 'Кстати, я могу помочь вам подобрать что-нибудь еще?'. "
|
| 404 |
+
"Твоя конечная цель — довольный клиент, который совершил покупку. Действуй!"
|
|
|
|
| 405 |
)
|
| 406 |
|
| 407 |
+
|
| 408 |
generated_text = ""
|
| 409 |
response = None
|
| 410 |
|
|
|
|
| 846 |
.cart-item img { width: 60px; height: 60px; object-fit: cover; border-radius: 8px; }
|
| 847 |
.cart-item-details { grid-column: 2; }
|
| 848 |
.cart-item-details strong { display: block; margin-bottom: 5px; font-size: 1rem; color: var(--text-dark);}
|
| 849 |
+
.cart-item-details .variant-info { font-size: 0.85rem; color: #666; }
|
| 850 |
.cart-item-price { font-size: 0.9rem; color: #666; }
|
| 851 |
.dark-theme .cart-item-price { color: #ccc; }
|
| 852 |
.cart-item-quantity { display: flex; align-items: center; gap: 8px; grid-column: 3;}
|
|
|
|
| 855 |
.cart-item-total { font-weight: bold; text-align: right; grid-column: 4; font-size: 1rem; color: var(--bg-medium);}
|
| 856 |
.cart-item-remove { grid-column: 5; background:none; border:none; color: var(--danger); cursor:pointer; font-size: 1.3em; padding: 5px; line-height: 1; }
|
| 857 |
.cart-item-remove:hover { color: var(--danger-hover); }
|
| 858 |
+
.quantity-input, .options-select { width: 100%; max-width: 180px; padding: 10px; border: 1px solid #e0e0e0; border-radius: 8px; font-size: 1rem; margin: 10px 0; box-sizing: border-box; }
|
| 859 |
+
.dark-theme .quantity-input, .dark-theme .options-select { background-color: #333; color: #fff; border-color: #555; }
|
| 860 |
+
.quantity-input:focus, .options-select:focus { border-color: var(--accent); outline: none; box-shadow: 0 0 0 2px rgba(72, 209, 204, 0.2); }
|
| 861 |
.cart-summary { margin-top: 20px; text-align: right; border-top: 1px solid #e0e0e0; padding-top: 15px; }
|
| 862 |
.dark-theme .cart-summary { border-top-color: #444; }
|
| 863 |
.cart-summary strong { font-size: 1.2rem; color: var(--bg-medium);}
|
|
|
|
| 949 |
<div id="quantityModal" class="modal">
|
| 950 |
<div class="modal-content">
|
| 951 |
<span class="close" onclick="closeModal('quantityModal')" aria-label="Закрыть">×</span>
|
| 952 |
+
<h2>Укажите опции и количество</h2>
|
| 953 |
+
<div id="quantityModalOptions"></div>
|
| 954 |
<label for="quantityInput">Количество:</label>
|
| 955 |
<input type="number" id="quantityInput" class="quantity-input" min="1" value="1">
|
|
|
|
|
|
|
| 956 |
<button class="product-button formulate-order-button" style="width:100%;" onclick="confirmAddToCart()"><i class="fas fa-check"></i> Добавить в корзину</button>
|
| 957 |
</div>
|
| 958 |
</div>
|
|
|
|
| 1044 |
.then(data => {
|
| 1045 |
modalContent.innerHTML = data;
|
| 1046 |
initializeSwiper();
|
| 1047 |
+
attachOptionListeners();
|
| 1048 |
})
|
| 1049 |
.catch(error => {
|
| 1050 |
console.error('Ошибка загрузки деталей продукта:', error);
|
| 1051 |
modalContent.innerHTML = `<p style="color: var(--danger); text-align:center; padding: 40px;">Не удалось загрузить информацию о товаре. ${error.message}</p>`;
|
| 1052 |
});
|
| 1053 |
}
|
| 1054 |
+
|
| 1055 |
+
function attachOptionListeners() {
|
| 1056 |
+
const product = getProductById(selectedProductId);
|
| 1057 |
+
if (!product) return;
|
| 1058 |
+
const priceEl = document.getElementById('variantPrice');
|
| 1059 |
+
const colorSelect = document.getElementById('colorSelect');
|
| 1060 |
+
const sizeSelect = document.getElementById('sizeSelect');
|
| 1061 |
+
|
| 1062 |
+
const updatePrice = () => {
|
| 1063 |
+
if (!priceEl) return;
|
| 1064 |
+
let color = colorSelect ? colorSelect.value : null;
|
| 1065 |
+
let size = sizeSelect ? sizeSelect.value : null;
|
| 1066 |
+
let key = [color, size].filter(Boolean).join('-');
|
| 1067 |
+
|
| 1068 |
+
let currentPrice = product.price;
|
| 1069 |
+
if (key && product.variant_prices && product.variant_prices[key]) {
|
| 1070 |
+
currentPrice = product.variant_prices[key];
|
| 1071 |
+
}
|
| 1072 |
+
priceEl.textContent = `${parseFloat(currentPrice).toFixed(0)} ${currencyCode}`;
|
| 1073 |
+
};
|
| 1074 |
+
|
| 1075 |
+
if (colorSelect) colorSelect.addEventListener('change', updatePrice);
|
| 1076 |
+
if (sizeSelect) sizeSelect.addEventListener('change', updatePrice);
|
| 1077 |
+
}
|
| 1078 |
|
| 1079 |
function initializeSwiper() {
|
| 1080 |
const swiperContainer = document.querySelector('#productModal .swiper-container');
|
|
|
|
| 1096 |
selectedProductId = productId;
|
| 1097 |
const product = getProductById(productId);
|
| 1098 |
if (!product) {
|
|
|
|
| 1099 |
alert("Ошибка: товар не найден.");
|
| 1100 |
return;
|
| 1101 |
}
|
| 1102 |
+
const optionsContainer = document.getElementById('quantityModalOptions');
|
| 1103 |
+
optionsContainer.innerHTML = '';
|
| 1104 |
+
|
| 1105 |
+
if (product.colors && product.colors.length > 0) {
|
| 1106 |
+
let colorHtml = '<label for="qColorSelect">Цвет/Вариант:</label><select id="qColorSelect" class="options-select">';
|
| 1107 |
+
product.colors.forEach(c => colorHtml += `<option value="${c}">${c}</option>`);
|
| 1108 |
+
colorHtml += '</select>';
|
| 1109 |
+
optionsContainer.innerHTML += colorHtml;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1110 |
}
|
| 1111 |
+
if (product.sizes && product.sizes.length > 0) {
|
| 1112 |
+
let sizeHtml = '<label for="qSizeSelect">Размер/Объем:</label><select id="qSizeSelect" class="options-select">';
|
| 1113 |
+
product.sizes.forEach(s => sizeHtml += `<option value="${s}">${s}</option>`);
|
| 1114 |
+
sizeHtml += '</select>';
|
| 1115 |
+
optionsContainer.innerHTML += sizeHtml;
|
| 1116 |
+
}
|
| 1117 |
+
|
| 1118 |
document.getElementById('quantityInput').value = 1;
|
| 1119 |
const modal = document.getElementById('quantityModal');
|
| 1120 |
if(modal) {
|
|
|
|
| 1127 |
if (selectedProductId === null) return;
|
| 1128 |
const quantityInput = document.getElementById('quantityInput');
|
| 1129 |
const quantity = parseInt(quantityInput.value);
|
| 1130 |
+
|
| 1131 |
+
const colorSelect = document.getElementById('qColorSelect');
|
| 1132 |
+
const sizeSelect = document.getElementById('qSizeSelect');
|
| 1133 |
+
|
| 1134 |
+
const color = colorSelect ? colorSelect.value : 'N/A';
|
| 1135 |
+
const size = sizeSelect ? sizeSelect.value : 'N/A';
|
| 1136 |
+
|
| 1137 |
if (isNaN(quantity) || quantity <= 0) {
|
| 1138 |
alert("Пожалуйста, укажите корректное количество (больше 0).");
|
| 1139 |
quantityInput.focus();
|
|
|
|
| 1144 |
alert("Ошибка добавления: товар не найден.");
|
| 1145 |
return;
|
| 1146 |
}
|
| 1147 |
+
|
| 1148 |
+
let price = product.price;
|
| 1149 |
+
let variantKey = [color, size].filter(v => v !== 'N/A').join('-');
|
| 1150 |
+
if (variantKey && product.variant_prices && product.variant_prices[variantKey]) {
|
| 1151 |
+
price = product.variant_prices[variantKey];
|
| 1152 |
+
}
|
| 1153 |
+
|
| 1154 |
+
const cartItemId = `${product.product_id}-${color}-${size}`;
|
| 1155 |
const existingItemIndex = cart.findIndex(item => item.id === cartItemId);
|
| 1156 |
if (existingItemIndex > -1) {
|
| 1157 |
cart[existingItemIndex].quantity += quantity;
|
|
|
|
| 1160 |
id: cartItemId,
|
| 1161 |
product_id: product.product_id,
|
| 1162 |
name: product.name,
|
| 1163 |
+
price: price,
|
| 1164 |
photo: product.photos && product.photos.length > 0 ? product.photos[0] : null,
|
| 1165 |
quantity: quantity,
|
| 1166 |
+
color: color,
|
| 1167 |
+
size: size
|
| 1168 |
});
|
| 1169 |
}
|
| 1170 |
localStorage.setItem(`mekaCart_${envId}`, JSON.stringify(cart));
|
|
|
|
| 1203 |
const photoUrl = item.photo
|
| 1204 |
? `https://huggingface.co/datasets/${repoId}/resolve/main/photos/${item.photo}`
|
| 1205 |
: 'https://via.placeholder.com/60x60.png?text=N/A';
|
| 1206 |
+
|
| 1207 |
+
let variantInfo = [];
|
| 1208 |
+
if (item.color && item.color !== 'N/A') variantInfo.push(`Цвет: ${item.color}`);
|
| 1209 |
+
if (item.size && item.size !== 'N/A') variantInfo.push(`Размер: ${item.size}`);
|
| 1210 |
+
|
| 1211 |
return `
|
| 1212 |
<div class="cart-item">
|
| 1213 |
<img src="${photoUrl}" alt="${item.name}">
|
| 1214 |
<div class="cart-item-details">
|
| 1215 |
+
<strong>${item.name}</strong>
|
| 1216 |
+
<p class="variant-info">${variantInfo.join(', ')}</p>
|
| 1217 |
<p class="cart-item-price">${item.price.toFixed(2)} ${currencyCode}</p>
|
| 1218 |
</div>
|
| 1219 |
<div class="cart-item-quantity">
|
|
|
|
| 1554 |
body.dark-theme .quantity-btn { background-color: #444; border-color: #555; color: #fff; }
|
| 1555 |
.cart-item-total { font-weight: bold; text-align: right; font-size: 1rem; color: var(--bg-medium);}
|
| 1556 |
.cart-item-remove { background:none; border:none; color: var(--danger); cursor:pointer; font-size: 1.3em; }
|
| 1557 |
+
.quantity-input, .options-select { width: 100%; max-width: 180px; padding: 10px; border: 1px solid #e0e0e0; border-radius: 8px; font-size: 1rem; margin: 10px 0; }
|
| 1558 |
+
body.dark-theme .quantity-input, body.dark-theme .options-select { background-color: #333; color: #fff; border-color: #555; }
|
| 1559 |
.cart-summary { margin-top: 20px; text-align: right; border-top: 1px solid #e0e0e0; padding-top: 15px; }
|
| 1560 |
body.dark-theme .cart-summary { border-top-color: #444; }
|
| 1561 |
.cart-actions { margin-top: 25px; display: flex; justify-content: space-between; }
|
|
|
|
| 1607 |
<div id="quantityModal" class="modal">
|
| 1608 |
<div class="modal-content">
|
| 1609 |
<span class="close" onclick="closeModal('quantityModal')">×</span>
|
| 1610 |
+
<h2>Укажите опции и количество</h2>
|
| 1611 |
+
<div id="quantityModalOptions"></div>
|
| 1612 |
+
<label for="quantityInput">Количество:</label>
|
| 1613 |
<input type="number" id="quantityInput" class="quantity-input" min="1" value="1">
|
|
|
|
| 1614 |
<button class="product-button formulate-order-button" onclick="confirmAddToCart()">Добавить в корзину</button>
|
| 1615 |
</div>
|
| 1616 |
</div>
|
|
|
|
| 1682 |
selectedProductId = productId;
|
| 1683 |
const product = getProductById(productId);
|
| 1684 |
if (!product) return;
|
| 1685 |
+
const optionsContainer = document.getElementById('quantityModalOptions');
|
| 1686 |
+
optionsContainer.innerHTML = '';
|
| 1687 |
+
|
| 1688 |
+
if (product.colors && product.colors.length > 0) {
|
| 1689 |
+
let colorHtml = '<label for="qColorSelect">Цвет/Вариант:</label><select id="qColorSelect" class="options-select">';
|
| 1690 |
+
product.colors.forEach(c => colorHtml += `<option value="${c}">${c}</option>`);
|
| 1691 |
+
colorHtml += '</select>';
|
| 1692 |
+
optionsContainer.innerHTML += colorHtml;
|
| 1693 |
+
}
|
| 1694 |
+
if (product.sizes && product.sizes.length > 0) {
|
| 1695 |
+
let sizeHtml = '<label for="qSizeSelect">Размер/Объем:</label><select id="qSizeSelect" class="options-select">';
|
| 1696 |
+
product.sizes.forEach(s => sizeHtml += `<option value="${s}">${s}</option>`);
|
| 1697 |
+
sizeHtml += '</select>';
|
| 1698 |
+
optionsContainer.innerHTML += sizeHtml;
|
| 1699 |
}
|
| 1700 |
document.getElementById('quantityInput').value = 1;
|
| 1701 |
document.getElementById('quantityModal').style.display = "block";
|
| 1702 |
}
|
| 1703 |
function confirmAddToCart() {
|
| 1704 |
const quantity = parseInt(document.getElementById('quantityInput').value);
|
| 1705 |
+
const colorSelect = document.getElementById('qColorSelect');
|
| 1706 |
+
const sizeSelect = document.getElementById('qSizeSelect');
|
| 1707 |
+
|
| 1708 |
+
const color = colorSelect ? colorSelect.value : 'N/A';
|
| 1709 |
+
const size = sizeSelect ? sizeSelect.value : 'N/A';
|
| 1710 |
+
|
| 1711 |
if (isNaN(quantity) || quantity <= 0) {
|
| 1712 |
alert("Укажите корректное количество.");
|
| 1713 |
return;
|
| 1714 |
}
|
| 1715 |
const product = getProductById(selectedProductId);
|
| 1716 |
+
|
| 1717 |
+
let price = product.price;
|
| 1718 |
+
let variantKey = [color, size].filter(v => v !== 'N/A').join('-');
|
| 1719 |
+
if (variantKey && product.variant_prices && product.variant_prices[variantKey]) {
|
| 1720 |
+
price = product.variant_prices[variantKey];
|
| 1721 |
+
}
|
| 1722 |
+
|
| 1723 |
+
const cartItemId = `${product.product_id}-${color}-${size}`;
|
| 1724 |
const existingItem = cart.find(item => item.id === cartItemId);
|
| 1725 |
if (existingItem) {
|
| 1726 |
existingItem.quantity += quantity;
|
| 1727 |
} else {
|
| 1728 |
+
cart.push({ id: cartItemId, product_id: product.product_id, name: product.name, price: price, photo: product.photos ? product.photos[0] : null, quantity, color, size });
|
| 1729 |
}
|
| 1730 |
localStorage.setItem(`mekaCart_${envId}`, JSON.stringify(cart));
|
| 1731 |
closeModal('quantityModal');
|
|
|
|
| 1756 |
cartContent.innerHTML = cart.map(item => {
|
| 1757 |
const itemTotal = item.price * item.quantity;
|
| 1758 |
total += itemTotal;
|
| 1759 |
+
let variantInfo = [];
|
| 1760 |
+
if (item.color && item.color !== 'N/A') variantInfo.push(`Цвет: ${item.color}`);
|
| 1761 |
+
if (item.size && item.size !== 'N/A') variantInfo.push(`Размер: ${item.size}`);
|
| 1762 |
return `<div class="cart-item">
|
| 1763 |
<img src="${item.photo ? `https://huggingface.co/datasets/${repoId}/resolve/main/photos/${item.photo}` : ''}" alt="${item.name}">
|
| 1764 |
+
<div><strong>${item.name}</strong><p>${variantInfo.join(', ')}</p><p>${item.price.toFixed(2)} ${currencyCode}</p></div>
|
| 1765 |
<div class="cart-item-quantity"><button class="quantity-btn" onclick="decrementCartItem('${item.id}')">-</button><span>${item.quantity}</span><button class="quantity-btn" onclick="incrementCartItem('${item.id}')">+</button></div>
|
| 1766 |
<span class="cart-item-total">${itemTotal.toFixed(2)}</span>
|
| 1767 |
<button class="cart-item-remove" onclick="removeFromCart('${item.id}')"><i class="fas fa-trash-alt"></i></button>
|
|
|
|
| 1931 |
'''
|
| 1932 |
|
| 1933 |
PRODUCT_DETAIL_TEMPLATE = '''
|
| 1934 |
+
<div style="padding: 10px;" data-product-id="{{ product.get('product_id') }}">
|
| 1935 |
<h2 style="font-size: 1.6rem; font-weight: 600; margin-bottom: 15px; text-align: center; color: #135D66;">{{ product['name'] }}</h2>
|
| 1936 |
<div class="swiper-container" style="max-width: 450px; margin: 0 auto 20px; border-radius: 10px; overflow: hidden; background-color: #fff; border: 1px solid #e0e0e0;">
|
| 1937 |
<div class="swiper-wrapper">
|
|
|
|
| 1957 |
<div class="swiper-button-prev" style="color: #135D66;"></div>
|
| 1958 |
{% endif %}
|
| 1959 |
</div>
|
| 1960 |
+
|
| 1961 |
+
<div style="margin-top: 20px; padding: 0 10px;">
|
| 1962 |
+
{% if product.get('colors') and product.colors|select('ne', '')|list|length > 0 %}
|
| 1963 |
+
<div style="margin-bottom: 15px;">
|
| 1964 |
+
<label for="colorSelect" style="display: block; margin-bottom: 5px; font-weight: 500;">Цвет/Вариант:</label>
|
| 1965 |
+
<select id="colorSelect" class="options-select">
|
| 1966 |
+
{% for color in product.colors|select('ne', '')|list %}
|
| 1967 |
+
<option value="{{ color }}">{{ color }}</option>
|
| 1968 |
+
{% endfor %}
|
| 1969 |
+
</select>
|
| 1970 |
+
</div>
|
| 1971 |
+
{% endif %}
|
| 1972 |
+
{% if product.get('sizes') and product.sizes|select('ne', '')|list|length > 0 %}
|
| 1973 |
+
<div style="margin-bottom: 15px;">
|
| 1974 |
+
<label for="sizeSelect" style="display: block; margin-bottom: 5px; font-weight: 500;">Размер/Объем:</label>
|
| 1975 |
+
<select id="sizeSelect" class="options-select">
|
| 1976 |
+
{% for size in product.sizes|select('ne', '')|list %}
|
| 1977 |
+
<option value="{{ size }}">{{ size }}</option>
|
| 1978 |
+
{% endfor %}
|
| 1979 |
+
</select>
|
| 1980 |
+
</div>
|
| 1981 |
+
{% endif %}
|
| 1982 |
+
</div>
|
| 1983 |
|
| 1984 |
<div style="text-align:center; margin-top:20px; padding: 0 10px;">
|
| 1985 |
+
<p style="font-size: 1.5rem; font-weight: bold; color: #135D66; margin-bottom: 15px;"><strong>Цена:</strong> <span id="variantPrice">{{ "%.0f"|format(product.price) }} {{ currency_code }}</span></p>
|
| 1986 |
<button class="product-button formulate-order-button" style="padding: 12px 30px; width: 100%; max-width: 300px;" onclick="closeModal('productModal'); openQuantityModalById('{{ product.get('product_id', '') }}')">
|
| 1987 |
<i class="fas fa-cart-plus"></i> В корзину
|
| 1988 |
</button>
|
|
|
|
| 1991 |
<div style="margin-top: 20px; font-size: 1rem; line-height: 1.7; color: #333; padding: 0 10px;">
|
| 1992 |
<p><strong>Категория:</strong> {{ product.get('category', 'Без категории') }}</p>
|
| 1993 |
<p><strong>Описание:</strong><br> {{ product.get('description', 'Описание отсутствует.')|replace('\\n', '<br>')|safe }}</p>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1994 |
</div>
|
| 1995 |
</div>
|
| 1996 |
+
<script>
|
| 1997 |
+
selectedProductId = '{{ product.get("product_id") }}';
|
| 1998 |
+
attachOptionListeners();
|
| 1999 |
+
</script>
|
| 2000 |
'''
|
| 2001 |
|
| 2002 |
ORDER_TEMPLATE = '''
|
|
|
|
| 2082 |
container.innerHTML = '<p style="text-align:center; padding: 20px;">Заказ пуст.</p>';
|
| 2083 |
document.querySelector('.actions .button').disabled = true;
|
| 2084 |
} else {
|
| 2085 |
+
container.innerHTML = order.cart.map((item, index) => {
|
| 2086 |
+
let variantInfo = [];
|
| 2087 |
+
if (item.color && item.color !== 'N/A') variantInfo.push(`Цвет: ${item.color}`);
|
| 2088 |
+
if (item.size && item.size !== 'N/A') variantInfo.push(`Размер: ${item.size}`);
|
| 2089 |
+
|
| 2090 |
+
return `
|
| 2091 |
<div class="order-item">
|
| 2092 |
<img src="${item.photo_url}" alt="${item.name}">
|
| 2093 |
<div class="item-details">
|
| 2094 |
+
<strong>${item.name}</strong>
|
| 2095 |
+
<span>${variantInfo.join(', ')}</span>
|
| 2096 |
<span>${item.price.toFixed(2)} {{ currency_code }}</span>
|
| 2097 |
</div>
|
| 2098 |
<div class="item-quantity">
|
|
|
|
| 2104 |
${(item.price * item.quantity).toFixed(2)} {{ currency_code }}
|
| 2105 |
</div>
|
| 2106 |
</div>
|
| 2107 |
+
`}).join('');
|
| 2108 |
document.querySelector('.actions .button').disabled = false;
|
| 2109 |
}
|
| 2110 |
updateOrderTotal();
|
|
|
|
| 2150 |
message += `*Номер заказа:* ${orderId}%0A%0A`;
|
| 2151 |
|
| 2152 |
order.cart.forEach(item => {
|
| 2153 |
+
let variantInfo = [];
|
| 2154 |
+
if (item.color && item.color !== 'N/A') variantInfo.push(item.color);
|
| 2155 |
+
if (item.size && item.size !== 'N/A') variantInfo.push(item.size);
|
| 2156 |
+
let variantText = variantInfo.length > 0 ? ` (${variantInfo.join(', ')})` : '';
|
| 2157 |
+
|
| 2158 |
+
message += `*${item.name}*${variantText}%0A`;
|
| 2159 |
message += ` - Количество: ${item.quantity}%0A`;
|
| 2160 |
message += ` - Цена: ${item.price.toFixed(2)} {{ currency_code }}%0A`;
|
| 2161 |
});
|
|
|
|
| 2239 |
details[open] > summary::after { transform: translateY(-50%) rotate(180deg); }
|
| 2240 |
details[open] > summary { border-bottom: 1px solid #e0e0e0; }
|
| 2241 |
details .form-content { padding: 20px; }
|
| 2242 |
+
.option-input-group { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
|
| 2243 |
+
.option-input-group input { flex-grow: 1; margin: 0; }
|
| 2244 |
+
.remove-option-btn { background-color: var(--danger); color: white; padding: 6px 10px; font-size: 0.8rem; margin-top: 0; line-height: 1; }
|
| 2245 |
+
.remove-option-btn:hover { background-color: var(--danger-hover); }
|
| 2246 |
+
.add-option-btn { background-color: #B2DFDB; color: var(--bg-medium); border: 1px solid #e0e0e0; }
|
| 2247 |
+
.add-option-btn:hover { background-color: var(--bg-medium); color: white; border-color: var(--bg-medium); }
|
| 2248 |
.photo-preview img { max-width: 70px; max-height: 70px; border-radius: 5px; margin: 5px 5px 0 0; border: 1px solid #e0e0e0; object-fit: cover;}
|
| 2249 |
.sync-buttons { display: flex; gap: 10px; margin-bottom: 20px; flex-wrap: wrap; }
|
| 2250 |
.download-hf-button { background-color: #6c757d; color: white; }
|
|
|
|
| 2273 |
.chat-message.user .bubble { background-color: #dcf8c6; }
|
| 2274 |
.chat-message.ai .bubble { background-color: #f1f1f1; }
|
| 2275 |
.current-avatar { max-width: 60px; max-height: 60px; border-radius: 50%; vertical-align: middle; margin-left: 10px; border: 2px solid var(--bg-medium);}
|
| 2276 |
+
.variant-price-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 10px; background-color: #f7f9fa; padding: 10px; border-radius: 5px; margin-top: 10px; border: 1px solid #e0e0e0; }
|
| 2277 |
+
.variant-price-item { display: flex; align-items: center; gap: 8px; font-size: 0.9em; }
|
| 2278 |
+
.variant-price-item label { margin-top: 0; white-space: nowrap; }
|
| 2279 |
+
.variant-price-item input { margin-top: 0; }
|
| 2280 |
</style>
|
| 2281 |
</head>
|
| 2282 |
<body>
|
|
|
|
| 2455 |
<input type="hidden" name="action" value="add_product">
|
| 2456 |
<label for="add_name">Название товара *:</label>
|
| 2457 |
<input type="text" id="add_name" name="name" required>
|
| 2458 |
+
<label for="add_price">Базовая Цена ({{ currency_code }}) *:</label>
|
| 2459 |
<input type="number" id="add_price" name="price" step="0.01" min="0" required>
|
| 2460 |
<label for="add_photos">Фотографии (до 10 шт.):</label>
|
| 2461 |
<input type="file" id="add_photos" name="photos" accept="image/*" multiple>
|
|
|
|
| 2477 |
{% endfor %}
|
| 2478 |
</select>
|
| 2479 |
<label>Цвета/Варианты (оставьте пустым, если нет):</label>
|
| 2480 |
+
<div id="add-color-inputs" class="option-inputs" data-type="color">
|
| 2481 |
+
<div class="option-input-group">
|
| 2482 |
<input type="text" name="colors" placeholder="Например: Розовый">
|
| 2483 |
+
<button type="button" class="remove-option-btn" onclick="removeOptionInput(this)"><i class="fas fa-times"></i></button>
|
| 2484 |
</div>
|
| 2485 |
</div>
|
| 2486 |
+
<button type="button" class="button add-option-btn" style="margin-top: 5px;" onclick="addOptionInput('add-color-inputs', 'colors')"><i class="fas fa-palette"></i> Добавить поле</button>
|
| 2487 |
+
|
| 2488 |
+
<label style="margin-top: 15px;">Размеры/Объем (оставьте пустым, если нет):</label>
|
| 2489 |
+
<div id="add-size-inputs" class="option-inputs" data-type="size">
|
| 2490 |
+
<div class="option-input-group">
|
| 2491 |
+
<input type="text" name="sizes" placeholder="Например: 42 или 50ml">
|
| 2492 |
+
<button type="button" class="remove-option-btn" onclick="removeOptionInput(this)"><i class="fas fa-times"></i></button>
|
| 2493 |
+
</div>
|
| 2494 |
+
</div>
|
| 2495 |
+
<button type="button" class="button add-option-btn" style="margin-top: 5px;" onclick="addOptionInput('add-size-inputs', 'sizes')"><i class="fas fa-ruler-combined"></i> Добавить поле</button>
|
| 2496 |
+
|
| 2497 |
+
<h3 style="margin-top: 20px;">Особые цены для вариантов</h3>
|
| 2498 |
+
<p style="font-size:0.85em; color:#666;">Здесь можно указать цену для конкретного сочетания цвета и размера. Если цена не указана, будет использоваться базовая.</p>
|
| 2499 |
+
<div id="add-variant-prices-container" class="variant-price-grid"></div>
|
| 2500 |
+
|
| 2501 |
<br>
|
| 2502 |
<div style="margin-top: 15px;">
|
| 2503 |
<input type="checkbox" id="add_in_stock" name="in_stock" checked>
|
|
|
|
| 2544 |
<p><strong>Цена:</strong> {{ "%.2f"|format(product.price) }} {{ currency_code }}</p>
|
| 2545 |
<p class="description" title="{{ product.get('description', '') }}"><strong>Описание:</strong> {{ product.get('description', 'N/A')[:150] }}{% if product.get('description', '')|length > 150 %}...{% endif %}</p>
|
| 2546 |
{% set colors = product.get('colors', []) %}
|
| 2547 |
+
{% set sizes = product.get('sizes', []) %}
|
| 2548 |
<p><strong>Цвета/Вар-ты:</strong> {{ colors|select('ne', '')|join(', ') if colors|select('ne', '')|list|length > 0 else 'Нет' }}</p>
|
| 2549 |
+
<p><strong>Размеры/Объем:</strong> {{ sizes|select('ne', '')|join(', ') if sizes|select('ne', '')|list|length > 0 else 'Нет' }}</p>
|
| 2550 |
+
|
| 2551 |
{% if product.get('photos') and product['photos']|length > 1 %}
|
| 2552 |
<p style="font-size: 0.8rem; color: #999;">(Всего фото: {{ product['photos']|length }})</p>
|
| 2553 |
{% endif %}
|
|
|
|
| 2600 |
{% endfor %}
|
| 2601 |
</select>
|
| 2602 |
<label>Цвета/Варианты:</label>
|
| 2603 |
+
<div id="edit-color-inputs-{{ loop.index0 }}" class="option-inputs" data-type="color">
|
| 2604 |
{% set current_colors = product.get('colors', []) %}
|
| 2605 |
{% if current_colors and current_colors|select('ne', '')|list|length > 0 %}
|
| 2606 |
{% for color in current_colors %}
|
| 2607 |
{% if color.strip() %}
|
| 2608 |
+
<div class="option-input-group">
|
| 2609 |
<input type="text" name="colors" value="{{ color }}">
|
| 2610 |
+
<button type="button" class="remove-option-btn" onclick="removeOptionInput(this)"><i class="fas fa-times"></i></button>
|
| 2611 |
</div>
|
| 2612 |
{% endif %}
|
| 2613 |
{% endfor %}
|
| 2614 |
{% else %}
|
| 2615 |
+
<div class="option-input-group">
|
| 2616 |
<input type="text" name="colors" placeholder="Например: Цвет">
|
| 2617 |
+
<button type="button" class="remove-option-btn" onclick="removeOptionInput(this)"><i class="fas fa-times"></i></button>
|
| 2618 |
</div>
|
| 2619 |
{% endif %}
|
| 2620 |
</div>
|
| 2621 |
+
<button type="button" class="button add-option-btn" style="margin-top: 5px;" onclick="addOptionInput('edit-color-inputs-{{ loop.index0 }}', 'colors')"><i class="fas fa-palette"></i> Добавить поле</button>
|
| 2622 |
+
|
| 2623 |
+
<label style="margin-top: 15px;">Размеры/Объем:</label>
|
| 2624 |
+
<div id="edit-size-inputs-{{ loop.index0 }}" class="option-inputs" data-type="size">
|
| 2625 |
+
{% set current_sizes = product.get('sizes', []) %}
|
| 2626 |
+
{% if current_sizes and current_sizes|select('ne', '')|list|length > 0 %}
|
| 2627 |
+
{% for size in current_sizes %}
|
| 2628 |
+
{% if size.strip() %}
|
| 2629 |
+
<div class="option-input-group">
|
| 2630 |
+
<input type="text" name="sizes" value="{{ size }}">
|
| 2631 |
+
<button type="button" class="remove-option-btn" onclick="removeOptionInput(this)"><i class="fas fa-times"></i></button>
|
| 2632 |
+
</div>
|
| 2633 |
+
{% endif %}
|
| 2634 |
+
{% endfor %}
|
| 2635 |
+
{% else %}
|
| 2636 |
+
<div class="option-input-group">
|
| 2637 |
+
<input type="text" name="sizes" placeholder="Например: L">
|
| 2638 |
+
<button type="button" class="remove-option-btn" onclick="removeOptionInput(this)"><i class="fas fa-times"></i></button>
|
| 2639 |
+
</div>
|
| 2640 |
+
{% endif %}
|
| 2641 |
+
</div>
|
| 2642 |
+
<button type="button" class="button add-option-btn" style="margin-top: 5px;" onclick="addOptionInput('edit-size-inputs-{{ loop.index0 }}', 'sizes')"><i class="fas fa-ruler-combined"></i> Добавить поле</button>
|
| 2643 |
+
|
| 2644 |
+
<h3 style="margin-top: 20px;">Особые цены для вариантов</h3>
|
| 2645 |
+
<div id="edit-variant-prices-container-{{ loop.index0 }}" class="variant-price-grid"></div>
|
| 2646 |
+
|
| 2647 |
<br>
|
| 2648 |
<div style="margin-top: 15px;">
|
| 2649 |
<input type="checkbox" id="edit_in_stock_{{ loop.index0 }}" name="in_stock" {% if product.get('in_stock', True) %}checked{% endif %}>
|
|
|
|
| 2674 |
</div>
|
| 2675 |
|
| 2676 |
<script>
|
| 2677 |
+
const allProductsForAdmin = {{ products|tojson|safe }};
|
| 2678 |
+
|
| 2679 |
function toggleEditForm(formId) {
|
| 2680 |
const formContainer = document.getElementById(formId);
|
| 2681 |
if (formContainer) {
|
| 2682 |
+
const isOpening = formContainer.style.display === 'none' || formContainer.style.display === '';
|
| 2683 |
+
formContainer.style.display = isOpening ? 'block' : 'none';
|
| 2684 |
+
if (isOpening) {
|
| 2685 |
+
const index = parseInt(formId.split('-').pop());
|
| 2686 |
+
const product = allProductsForAdmin[index];
|
| 2687 |
+
const priceContainer = document.getElementById(`edit-variant-prices-container-${index}`);
|
| 2688 |
+
const colorContainer = document.getElementById(`edit-color-inputs-${index}`);
|
| 2689 |
+
const sizeContainer = document.getElementById(`edit-size-inputs-${index}`);
|
| 2690 |
+
|
| 2691 |
+
const updateVariantPrices = () => generateVariantPriceInputs(colorContainer, sizeContainer, priceContainer, product.variant_prices || {});
|
| 2692 |
+
|
| 2693 |
+
colorContainer.addEventListener('input', updateVariantPrices);
|
| 2694 |
+
sizeContainer.addEventListener('input', updateVariantPrices);
|
| 2695 |
+
updateVariantPrices();
|
| 2696 |
+
}
|
| 2697 |
+
}
|
| 2698 |
+
}
|
| 2699 |
+
|
| 2700 |
+
function generateVariantPriceInputs(colorContainer, sizeContainer, priceContainer, existingPrices) {
|
| 2701 |
+
priceContainer.innerHTML = '';
|
| 2702 |
+
const colors = Array.from(colorContainer.querySelectorAll('input[name="colors"]')).map(i => i.value.trim()).filter(Boolean);
|
| 2703 |
+
const sizes = Array.from(sizeContainer.querySelectorAll('input[name="sizes"]')).map(i => i.value.trim()).filter(Boolean);
|
| 2704 |
+
|
| 2705 |
+
if (colors.length === 0 && sizes.length > 0) {
|
| 2706 |
+
sizes.forEach(size => {
|
| 2707 |
+
const key = size;
|
| 2708 |
+
const price = existingPrices[key] || '';
|
| 2709 |
+
priceContainer.innerHTML += `
|
| 2710 |
+
<div class="variant-price-item">
|
| 2711 |
+
<label for="variant_price_${key}">${size}:</label>
|
| 2712 |
+
<input type="number" step="0.01" name="variant_price_${key}" value="${price}" placeholder="Базовая цена">
|
| 2713 |
+
</div>`;
|
| 2714 |
+
});
|
| 2715 |
+
} else if (colors.length > 0 && sizes.length === 0) {
|
| 2716 |
+
colors.forEach(color => {
|
| 2717 |
+
const key = color;
|
| 2718 |
+
const price = existingPrices[key] || '';
|
| 2719 |
+
priceContainer.innerHTML += `
|
| 2720 |
+
<div class="variant-price-item">
|
| 2721 |
+
<label for="variant_price_${key}">${color}:</label>
|
| 2722 |
+
<input type="number" step="0.01" name="variant_price_${key}" value="${price}" placeholder="Базовая цена">
|
| 2723 |
+
</div>`;
|
| 2724 |
+
});
|
| 2725 |
+
} else if (colors.length > 0 && sizes.length > 0) {
|
| 2726 |
+
colors.forEach(color => {
|
| 2727 |
+
sizes.forEach(size => {
|
| 2728 |
+
const key = `${color}-${size}`;
|
| 2729 |
+
const price = existingPrices[key] || '';
|
| 2730 |
+
priceContainer.innerHTML += `
|
| 2731 |
+
<div class="variant-price-item">
|
| 2732 |
+
<label for="variant_price_${key}">${color} - ${size}:</label>
|
| 2733 |
+
<input type="number" step="0.01" name="variant_price_${key}" value="${price}" placeholder="Базовая цена">
|
| 2734 |
+
</div>`;
|
| 2735 |
+
});
|
| 2736 |
+
});
|
| 2737 |
}
|
| 2738 |
}
|
| 2739 |
|
| 2740 |
+
function addOptionInput(containerId, name) {
|
| 2741 |
const container = document.getElementById(containerId);
|
| 2742 |
if (container) {
|
| 2743 |
const newInputGroup = document.createElement('div');
|
| 2744 |
+
newInputGroup.className = 'option-input-group';
|
| 2745 |
newInputGroup.innerHTML = `
|
| 2746 |
+
<input type="text" name="${name}" placeholder="Новый вариант">
|
| 2747 |
+
<button type="button" class="remove-option-btn" onclick="removeOptionInput(this)"><i class="fas fa-times"></i></button>
|
| 2748 |
`;
|
| 2749 |
container.appendChild(newInputGroup);
|
| 2750 |
+
newInputGroup.querySelector(`input[name="${name}"]`).focus();
|
|
|
|
|
|
|
|
|
|
| 2751 |
}
|
| 2752 |
}
|
| 2753 |
|
| 2754 |
+
function removeOptionInput(button) {
|
| 2755 |
+
const group = button.closest('.option-input-group');
|
| 2756 |
if (group) {
|
| 2757 |
const container = group.parentNode;
|
| 2758 |
group.remove();
|
| 2759 |
if (container && container.children.length === 0) {
|
| 2760 |
+
const name = container.dataset.type === 'color' ? 'colors' : 'sizes';
|
| 2761 |
+
const placeholder = container.dataset.type === 'color' ? 'Например: Цвет' : 'Например: L';
|
| 2762 |
+
addOptionInput(container.id, name, placeholder);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2763 |
}
|
|
|
|
|
|
|
| 2764 |
}
|
| 2765 |
}
|
| 2766 |
+
|
| 2767 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 2768 |
+
const addForm = document.getElementById('add-product-form');
|
| 2769 |
+
const addColorContainer = document.getElementById('add-color-inputs');
|
| 2770 |
+
const addSizeContainer = document.getElementById('add-size-inputs');
|
| 2771 |
+
const addPriceContainer = document.getElementById('add-variant-prices-container');
|
| 2772 |
+
|
| 2773 |
+
const updateAddFormPrices = () => generateVariantPriceInputs(addColorContainer, addSizeContainer, addPriceContainer, {});
|
| 2774 |
+
addColorContainer.addEventListener('input', updateAddFormPrices);
|
| 2775 |
+
addSizeContainer.addEventListener('input', updateAddFormPrices);
|
| 2776 |
+
});
|
| 2777 |
+
|
| 2778 |
|
| 2779 |
async function generateDescription(photoInputId, descriptionTextareaId, languageSelectId) {
|
| 2780 |
const photoInput = document.getElementById(photoInputId);
|
|
|
|
| 3102 |
"price": price,
|
| 3103 |
"quantity": quantity,
|
| 3104 |
"color": item.get('color', 'N/A'),
|
| 3105 |
+
"size": item.get('size', 'N/A'),
|
| 3106 |
"photo": item.get('photo'),
|
| 3107 |
"photo_url": f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/photos/{item['photo']}" if item.get('photo') else "https://via.placeholder.com/60x60.png?text=N/A"
|
| 3108 |
})
|
|
|
|
| 3247 |
save_env_data(env_id, data)
|
| 3248 |
flash("Настройки магазина и чата успешно обновлены.", 'success')
|
| 3249 |
|
| 3250 |
+
elif action == 'add_product' or action == 'edit_product':
|
| 3251 |
+
product_id = request.form.get('product_id')
|
| 3252 |
+
product_data = {}
|
| 3253 |
+
is_edit = action == 'edit_product'
|
| 3254 |
+
|
| 3255 |
+
if is_edit:
|
| 3256 |
+
product_data = next((p for p in products if p.get('product_id') == product_id), None)
|
| 3257 |
+
if not product_data:
|
| 3258 |
+
flash(f"Ошибка: товар с ID {product_id} не найден.", 'error')
|
| 3259 |
+
return redirect(url_for('admin', env_id=env_id))
|
| 3260 |
+
|
| 3261 |
+
product_data['name'] = request.form.get('name', '').strip()
|
| 3262 |
price_str = request.form.get('price', '').replace(',', '.')
|
| 3263 |
+
product_data['description'] = request.form.get('description', '').strip()
|
| 3264 |
category = request.form.get('category')
|
| 3265 |
+
product_data['category'] = category if category in categories else 'Без категории'
|
| 3266 |
+
product_data['colors'] = sorted(list(set(c.strip() for c in request.form.getlist('colors') if c.strip())))
|
| 3267 |
+
product_data['sizes'] = sorted(list(set(s.strip() for s in request.form.getlist('sizes') if s.strip())))
|
| 3268 |
+
product_data['in_stock'] = 'in_stock' in request.form
|
| 3269 |
+
product_data['is_top'] = 'is_top' in request.form
|
| 3270 |
|
| 3271 |
+
if not product_data['name'] or not price_str:
|
| 3272 |
flash("Название и цена товара обязательны.", 'error')
|
| 3273 |
return redirect(url_for('admin', env_id=env_id))
|
| 3274 |
+
|
| 3275 |
try:
|
| 3276 |
price = round(float(price_str), 2)
|
| 3277 |
if price < 0: price = 0
|
| 3278 |
+
product_data['price'] = price
|
| 3279 |
except ValueError:
|
| 3280 |
+
flash("Неверный формат цены.", 'error')
|
| 3281 |
+
return redirect(url_for('admin', env_id=env_id))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3282 |
|
| 3283 |
+
variant_prices = {}
|
| 3284 |
+
for key, value in request.form.items():
|
| 3285 |
+
if key.startswith('variant_price_') and value:
|
| 3286 |
+
variant_key = key.replace('variant_price_', '')
|
| 3287 |
+
try:
|
| 3288 |
+
variant_prices[variant_key] = round(float(value), 2)
|
| 3289 |
+
except ValueError:
|
| 3290 |
+
pass
|
| 3291 |
+
product_data['variant_prices'] = variant_prices
|
| 3292 |
|
| 3293 |
photos_files = request.files.getlist('photos')
|
| 3294 |
+
if photos_files and any(f.filename for f in photos_files):
|
| 3295 |
+
if HF_TOKEN_WRITE:
|
| 3296 |
+
uploads_dir = 'uploads_temp'
|
| 3297 |
+
os.makedirs(uploads_dir, exist_ok=True)
|
| 3298 |
+
api = HfApi()
|
| 3299 |
+
new_photos_list = []
|
| 3300 |
+
photo_limit = 10
|
| 3301 |
+
uploaded_count = 0
|
| 3302 |
+
for photo in photos_files:
|
| 3303 |
+
if uploaded_count >= photo_limit:
|
| 3304 |
+
flash(f"Загружено только первые {photo_limit} фото.", "warning")
|
| 3305 |
+
break
|
| 3306 |
+
if photo and photo.filename:
|
| 3307 |
+
try:
|
| 3308 |
+
ext = os.path.splitext(photo.filename)[1].lower()
|
| 3309 |
+
if ext not in ['.jpg', '.jpeg', '.png', '.gif', '.webp']:
|
| 3310 |
+
flash(f"Файл {photo.filename} пропущен (не изображение).", "warning")
|
| 3311 |
+
continue
|
| 3312 |
+
|
| 3313 |
+
safe_name = secure_filename(product_data['name'].replace(' ', '_'))[:50]
|
| 3314 |
+
photo_filename = f"{safe_name}_{uuid4().hex[:8]}{ext}"
|
| 3315 |
+
temp_path = os.path.join(uploads_dir, photo_filename)
|
| 3316 |
+
photo.save(temp_path)
|
| 3317 |
+
api.upload_file(path_or_fileobj=temp_path, path_in_repo=f"photos/{photo_filename}", repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE)
|
| 3318 |
+
new_photos_list.append(photo_filename)
|
| 3319 |
+
os.remove(temp_path)
|
| 3320 |
+
uploaded_count += 1
|
| 3321 |
+
except Exception as e:
|
| 3322 |
+
flash(f"Ошибка при загрузке фото {photo.filename}: {e}", 'error')
|
| 3323 |
+
if new_photos_list and is_edit and product_data.get('photos'):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3324 |
try:
|
| 3325 |
+
api.delete_files(repo_id=REPO_ID, paths_in_repo=[f"photos/{p}" for p in product_data['photos']], repo_type="dataset", token=HF_TOKEN_WRITE)
|
| 3326 |
+
except Exception: pass
|
| 3327 |
+
if new_photos_list:
|
| 3328 |
+
product_data['photos'] = new_photos_list
|
| 3329 |
+
else:
|
| 3330 |
+
flash("HF_TOKEN (write) не настроен. Фотографии не были загружены.", "warning")
|
| 3331 |
+
|
| 3332 |
+
if is_edit:
|
| 3333 |
+
product_index = next((i for i, p in enumerate(products) if p.get('product_id') == product_id), -1)
|
| 3334 |
+
if product_index != -1:
|
| 3335 |
+
products[product_index] = product_data
|
| 3336 |
+
flash(f"Товар '{product_data['name']}' успешно обновлен.", 'success')
|
| 3337 |
+
else:
|
| 3338 |
+
product_data['product_id'] = uuid4().hex
|
| 3339 |
+
products.append(product_data)
|
| 3340 |
+
flash(f"Товар '{product_data['name']}' успешно добавлен.", 'success')
|
| 3341 |
+
|
| 3342 |
+
data['products'] = products
|
| 3343 |
save_env_data(env_id, data)
|
| 3344 |
+
|
| 3345 |
|
| 3346 |
elif action == 'delete_product':
|
| 3347 |
product_id = request.form.get('product_id')
|