diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -101,6 +101,8 @@ translations = { 'online_order': 'Онлайн', 'address_1_title': 'Адрес:', 'address_1_detail': 'Город Алматы, Рынок Олжа, ряд VIP, Бутик 7', + 'currency_usd': 'USD', + 'currency_kzt': 'KZT' }, 'kk': { 'site_title': "ManolisA - Каталог", @@ -169,6 +171,8 @@ translations = { 'online_order': 'Онлайн', 'address_1_title': 'Мекенжай:', 'address_1_detail': 'Алматы қаласы, Олжа базары, VIP қатары, 7 бутик', + 'currency_usd': 'USD', + 'currency_kzt': 'KZT' } } @@ -228,7 +232,7 @@ def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWN try: if file_name == DATA_FILE: with open(file_name, 'w', encoding='utf-8') as f: - json.dump({'products': [], 'categories': [], 'seasons': [], 'orders': {}, 'employees': [], 'settings': {'usd_kzt_rate': 450}}, f) + json.dump({'products': [], 'categories': [], 'orders': {}, 'employees': [], 'seasons': [], 'exchange_rate_kzt': 450.0}, f) logging.info(f"Created empty local file {file_name} because it was not found on HF.") except Exception as create_e: logging.error(f"Failed to create empty local file {file_name}: {create_e}") @@ -292,7 +296,7 @@ def periodic_backup(): def load_data(): - default_data = {'products': [], 'categories': [], 'seasons': [], 'orders': {}, 'employees': [], 'settings': {'usd_kzt_rate': 450}} + default_data = {'products': [], 'categories': [], 'orders': {}, 'employees': [], 'seasons': [], 'exchange_rate_kzt': 450.0} try: with open(DATA_FILE, 'r', encoding='utf-8') as file: data = json.load(file) @@ -302,10 +306,10 @@ def load_data(): raise FileNotFoundError if 'products' not in data: data['products'] = [] if 'categories' not in data: data['categories'] = [] - if 'seasons' not in data: data['seasons'] = [] if 'orders' not in data: data['orders'] = {} if 'employees' not in data: data['employees'] = [] - if 'settings' not in data: data['settings'] = {'usd_kzt_rate': 450} + if 'seasons' not in data: data['seasons'] = [] + if 'exchange_rate_kzt' not in data: data['exchange_rate_kzt'] = 450.0 return data except FileNotFoundError: logging.warning(f"Local file {DATA_FILE} not found. Attempting download from HF.") @@ -320,10 +324,10 @@ def load_data(): return default_data if 'products' not in data: data['products'] = [] if 'categories' not in data: data['categories'] = [] - if 'seasons' not in data: data['seasons'] = [] if 'orders' not in data: data['orders'] = {} if 'employees' not in data: data['employees'] = [] - if 'settings' not in data: data['settings'] = {'usd_kzt_rate': 450} + if 'seasons' not in data: data['seasons'] = [] + if 'exchange_rate_kzt' not in data: data['exchange_rate_kzt'] = 450.0 return data except FileNotFoundError: logging.error(f"File {DATA_FILE} still not found even after download reported success. Using default.") @@ -352,11 +356,10 @@ def save_data(data): return if 'products' not in data: data['products'] = [] if 'categories' not in data: data['categories'] = [] - if 'seasons' not in data: data['seasons'] = [] if 'orders' not in data: data['orders'] = {} if 'employees' not in data: data['employees'] = [] - if 'settings' not in data: data['settings'] = {'usd_kzt_rate': 450} - + if 'seasons' not in data: data['seasons'] = [] + if 'exchange_rate_kzt' not in data: data['exchange_rate_kzt'] = 450.0 with open(DATA_FILE, 'w', encoding='utf-8') as file: json.dump(data, file, ensure_ascii=False, indent=4) @@ -380,145 +383,135 @@ CATALOG_TEMPLATE = '''
-
-
-
+
+
- USD - KZT + +
RU KZ
-
+
{{ _('our_address') }} {{ store_address }}
- -
- -
-
- {% if seasons %} -
-
{{ _('season') }}
-
- - {% for season in seasons %} - - {% endfor %} -
+
+
+ + {% for season in seasons %} + + {% endfor %}
- {% endif %} - -
-
{{ _('category') }}
-
- - {% for category in categories %} - - {% endfor %} -
+
+ + {% for category in categories %} + + {% endfor %}
+
+ +
{% for product in products %} @@ -535,10 +528,9 @@ CATALOG_TEMPLATE = ''' {% endif %}
- {% set first_variant = product.get('variants', [])[0] if product.get('variants') else None %} - {% set first_photo = first_variant.photos[0] if first_variant and first_variant.photos else None %} - {% if first_photo %} - {{ product['name'] }} {% else %} @@ -546,23 +538,24 @@ CATALOG_TEMPLATE = ''' {% endif %}
-

{{ product['name'] }}

-
- - {{ "%.2f"|format(product['price']) }} USD - - {% if product.get('items_per_line', 0) > 1 %} - - {{ "%.2f"|format(product['price'] / product.get('items_per_line')) }} USD / {{ _('price_per_piece') }} +
+

{{ product['name'] }}

+
+ + {{ "%.2f"|format(product['price']) }} USD - ({{ product.get('items_per_line') }} {{ _('items_in_line') }}) - {% endif %} -
-
- {% set colors = product.get('variants', [])|map(attribute='color')|select('ne', '')|list %} - {% if colors %} - {{ colors|join(', ') }} - {% endif %} + + {% if product.get('items_per_line', 0) > 1 %} + {{ "%.2f"|format(product['price'] / product.get('items_per_line')) }} USD / {{ _('price_per_piece') }} + ({{ product.get('items_per_line') }} {{ _('items_in_line') }}) + {% endif %} + +
+
+ {% for color in product.get('colors', []) %} + {{ color.name }} + {% endfor %} +
@@ -608,7 +601,7 @@ CATALOG_TEMPLATE = '''
- {{ _('total') }} 0.00 + {{ _('total') }} 0.00 USD
`; }).join(''); - cartTotalElement.dataset.totalUsd = totalUSD.toFixed(2); + cartTotalElement.dataset.totalUsd = total.toFixed(2); } - + const employeeSelect = document.getElementById('employeeSelect'); if (employeeSelect) { employeeSelect.innerHTML = ``; @@ -890,13 +882,14 @@ CATALOG_TEMPLATE = ''' employeeSelect.appendChild(option); }); } - updateAllPrices(); + openModal('cartModal'); + updatePrices(); } function removeFromCart(itemId) { cart = cart.filter(item => item.id !== itemId); - localStorage.setItem('manolisACart', JSON.stringify(cart)); + localStorage.setItem('manolisaCart', JSON.stringify(cart)); openCartModal(); updateCartButton(); } @@ -904,7 +897,7 @@ CATALOG_TEMPLATE = ''' function clearCart() { if (confirm(t.confirm_clear_cart)) { cart = []; - localStorage.removeItem('manolisACart'); + localStorage.removeItem('manolisaCart'); openCartModal(); updateCartButton(); } @@ -916,7 +909,7 @@ CATALOG_TEMPLATE = ''' return; } const employee = document.getElementById('employeeSelect').value; - const orderData = { cart: cart, employee: employee, currency: currentCurrency, exchange_rate: USD_KZT_RATE }; + const orderData = { cart: cart, employee: employee, currency: currentCurrency, rate: exchangeRateKZT }; const formulateButton = document.querySelector('.formulate-order-button'); if (formulateButton) formulateButton.disabled = true; showNotification(t.formulating_order, 5000); @@ -933,7 +926,7 @@ CATALOG_TEMPLATE = ''' }) .then(data => { if (data.order_id) { - localStorage.removeItem('manolisACart'); + localStorage.removeItem('manolisaCart'); cart = []; updateCartButton(); closeModal('cartModal'); @@ -943,6 +936,7 @@ CATALOG_TEMPLATE = ''' } }) .catch(error => { + console.error('Order formulation error:', error); alert(`${t.order_creation_error} ${error.message}`); if (formulateButton) formulateButton.disabled = false; }); @@ -980,6 +974,11 @@ CATALOG_TEMPLATE = ''' p.className = 'no-results-message'; p.textContent = t.no_products_found; grid.appendChild(p); + } else if (products.length === 0 && !grid.querySelector('.no-results-message')) { + const p = document.createElement('p'); + p.className = 'no-results-message'; + p.textContent = t.no_products_added; + grid.appendChild(p); } } @@ -1008,11 +1007,13 @@ CATALOG_TEMPLATE = ''' if (index > -1) { favorites.splice(index, 1); buttonElement.classList.remove('favorited'); + buttonElement.innerHTML = ''; } else { favorites.push(productId); buttonElement.classList.add('favorited'); + buttonElement.innerHTML = ''; } - localStorage.setItem('manolisAFavorites', JSON.stringify(favorites)); + localStorage.setItem('manolisaFavorites', JSON.stringify(favorites)); } function updateFavoriteIcons() { @@ -1020,8 +1021,10 @@ CATALOG_TEMPLATE = ''' const productId = button.closest('.product').dataset.id; if (favorites.includes(productId)) { button.classList.add('favorited'); + button.innerHTML = ''; } else { button.classList.remove('favorited'); + button.innerHTML = ''; } }); } @@ -1030,7 +1033,9 @@ CATALOG_TEMPLATE = ''' const productIndex = products.findIndex(p => p.id === productId); if (productIndex > -1) { closeModal('favoritesModal'); - setTimeout(() => openModalByIndex(productIndex), 250); + setTimeout(() => { + openModalByIndex(productIndex); + }, 250); } } @@ -1043,10 +1048,9 @@ CATALOG_TEMPLATE = ''' } else { const favoriteProducts = products.filter(p => favorites.includes(p.id)); favoriteProducts.forEach(item => { - const first_variant = item.variants && item.variants[0]; - const photo = first_variant && first_variant.photos && first_variant.photos[0]; - const photoUrl = photo - ? `https://huggingface.co/datasets/${repoId}/resolve/main/photos/${photo}` + const firstColor = item.colors && item.colors.length > 0 ? item.colors[0] : null; + const photoUrl = firstColor && firstColor.photos.length > 0 + ? `https://huggingface.co/datasets/${repoId}/resolve/main/photos/${firstColor.photos[0]}` : 'https://via.placeholder.com/70x70.png?text=N/A'; const itemHtml = ` @@ -1054,17 +1058,17 @@ CATALOG_TEMPLATE = ''' ${item.name}
${item.name} -

${formatPrice(item.price, 'USD')}

+

${item.price.toFixed(2)} USD

- +
`; favoritesContent.innerHTML += itemHtml; }); - updateAllPrices(); } openModal('favoritesModal'); + updatePrices(); } function removeFromFavorites(productId, event) { @@ -1072,7 +1076,7 @@ CATALOG_TEMPLATE = ''' const index = favorites.indexOf(productId); if (index > -1) { favorites.splice(index, 1); - localStorage.setItem('manolisAFavorites', JSON.stringify(favorites)); + localStorage.setItem('manolisaFavorites', JSON.stringify(favorites)); openFavoritesModal(); updateFavoriteIcons(); } @@ -1093,25 +1097,73 @@ CATALOG_TEMPLATE = ''' }, duration); } + function updatePrices() { + document.querySelectorAll('[data-price-usd]').forEach(el => { + const priceUSD = parseFloat(el.dataset.priceUsd); + if (currentCurrency === 'KZT') { + const priceKZT = priceUSD * exchangeRateKZT; + el.textContent = `${priceKZT.toFixed(0)} KZT`; + } else { + el.textContent = `${priceUSD.toFixed(2)} USD`; + } + }); + + const cartTotalEl = document.getElementById('cartTotal'); + if(cartTotalEl) { + const totalUSD = parseFloat(cartTotalEl.dataset.totalUsd); + if (currentCurrency === 'KZT') { + cartTotalEl.textContent = `${(totalUSD * exchangeRateKZT).toFixed(0)} KZT`; + } else { + cartTotalEl.textContent = `${totalUSD.toFixed(2)} USD`; + } + } + } + + function setupCurrencySwitcher() { + const usdBtn = document.getElementById('currency-usd'); + const kztBtn = document.getElementById('currency-kzt'); + + if (currentCurrency === 'KZT') { + usdBtn.classList.remove('active'); + kztBtn.classList.add('active'); + } else { + usdBtn.classList.add('active'); + kztBtn.classList.remove('active'); + } + + usdBtn.addEventListener('click', () => { + currentCurrency = 'USD'; + localStorage.setItem('manolisaCurrency', 'USD'); + usdBtn.classList.add('active'); + kztBtn.classList.remove('active'); + updatePrices(); + }); + kztBtn.addEventListener('click', () => { + currentCurrency = 'KZT'; + localStorage.setItem('manolisaCurrency', 'KZT'); + kztBtn.classList.add('active'); + usdBtn.classList.remove('active'); + updatePrices(); + }); + } + document.addEventListener('DOMContentLoaded', () => { updateCartButton(); setupFilters(); updateFavoriteIcons(); - setCurrency(currentCurrency); + setupCurrencySwitcher(); + updatePrices(); - document.querySelectorAll('.currency-switcher a').forEach(a => { - a.addEventListener('click', (e) => { - e.preventDefault(); - setCurrency(a.dataset.currency); - }); - }); - - window.addEventListener('click', e => { - if (e.target.classList.contains('modal')) closeModal(e.target.id); + window.addEventListener('click', function(event) { + if (event.target.classList.contains('modal')) { + closeModal(event.target.id); + } }); - window.addEventListener('keydown', e => { - if (e.key === 'Escape') { - document.querySelectorAll('.modal[style*="display: block"]').forEach(modal => closeModal(modal.id)); + window.addEventListener('keydown', function(event) { + if (event.key === 'Escape') { + document.querySelectorAll('.modal[style*="display: block"]').forEach(modal => { + closeModal(modal.id); + }); } }); }); @@ -1121,24 +1173,18 @@ CATALOG_TEMPLATE = ''' ''' PRODUCT_DETAIL_TEMPLATE = ''' -
-

{{ product['name'] }}

-
+
+

{{ product['name'] }}

+
- {% set all_photos = [] %} - {% for variant in product.get('variants', []) %} - {% for photo in variant.photos %} - {% set _ = all_photos.append(photo) %} - {% endfor %} - {% endfor %} - - {% if all_photos %} - {% for photo in all_photos %} -
+ {% set first_color = product.get('colors', [])[0] if product.get('colors') else None %} + {% if first_color and first_color.get('photos') %} + {% for photo in first_color['photos'] %} +
{{ product['name'] }} - photo {{ loop.index }} + style="max-width: 100%; max-height: 400px; object-fit: contain; display: block; margin: auto; cursor: grab; border-radius: 8px;">
{% endfor %} @@ -1148,29 +1194,37 @@ PRODUCT_DETAIL_TEMPLATE = '''
{% endif %}
- {% if all_photos|length > 1 %} -
-
-
+ {% if product.get('colors') and product.get('colors')[0].get('photos')|length > 1 %} +
+
+
{% endif %}
+ + + +
+ {% for color in product.get('colors', []) %} +
{{ color.name }}
+ {% endfor %} +
-

{{ _('category') }} {{ product.get('category', _('no_category')) }}

-

{{ _('season') }} {{ product.get('season', _('no_season')) }}

-

- {{ _('price') }} {{ "%.2f"|format(product['price']) }} USD +

{{ _('category') }}: {{ product.get('category', _('no_category')) }}

+

{{ _('season') }}: {{ product.get('season', _('no_season')) }}

+

+ {{ _('price') }}: {{ "%.2f"|format(product['price']) }} USD

{% if product.get('items_per_line', 0) > 1 %} -

- {{ "%.2f"|format(product.price / product.get('items_per_line')) }} USD / {{ _('price_per_piece') }} ({{ product.get('items_per_line') }} {{ _('items_in_line') }}) +

+ {{ "%.2f"|format(product['price'] / product.get('items_per_line')) }} USD / {{ _('price_per_piece') }} ({{ product.get('items_per_line') }} {{ _('items_in_line') }})

{% endif %} -

{{ _('description') }}
{{ product.get('description', _('no_description'))|replace('\\n', '
')|safe }}

- {% set colors = product.get('variants', [])|map(attribute='color')|select('ne', '')|list %} - {% if colors %} -

{{ _('available_colors') }} {{ colors|join(', ') }}

- {% endif %} +

{{ _('description') }}:
{{ product.get('description', _('no_description'))|replace('\\n', '
')|safe }}

''' @@ -1186,38 +1240,38 @@ ORDER_TEMPLATE = ''' @@ -1231,26 +1285,22 @@ ORDER_TEMPLATE = '''

{{ _('products_in_order') }}

- {% set currency_symbol = '₸' if order.currency == 'KZT' else 'USD' %} {% for item in order.cart %} - {% set price_per_item = (item.price * order.exchange_rate) if order.currency == 'KZT' else item.price %} - {% set total_item_price = price_per_item * item.quantity %}
{{ item.name }}
{{ item.name }} {% if item.color != 'N/A' %}({{ item.color }}){% endif %} - {{ item.quantity }} × {{ "%.0f"|format(price_per_item) if currency_symbol == '₸' else "%.2f"|format(price_per_item) }} {{ currency_symbol }} + {{ item.quantity }} × {{ "%.2f"|format(item.price) }} {{ order.currency }}
- {{ "%.0f"|format(total_item_price) if currency_symbol == '₸' else "%.2f"|format(total_item_price) }} {{ currency_symbol }} + {{ "%.2f"|format(item.price * item.quantity) }} {{ order.currency }}
{% endfor %}
- {% set total_price_display = (order.total_price * order.exchange_rate) if order.currency == 'KZT' else order.total_price %} -

{{ _('total_to_pay') }}: {{ "%.0f"|format(total_price_display) if currency_symbol == '₸' else "%.2f"|format(total_price_display) }} {{ currency_symbol }}

+

{{ _('total_to_pay') }}: {{ "%.2f"|format(order.total_price) }} {{ order.currency }}

@@ -1281,7 +1331,7 @@ ORDER_TEMPLATE = ''' {% else %} -

{{ _('error') }}

+

{{ _('error') }}

{{ _('order_not_found') }}

{{ _('back_to_catalog') }} {% endif %} @@ -1297,74 +1347,81 @@ ADMIN_TEMPLATE = ''' Админ-панель - ManolisA - +
-
- -

Админ-панель ManolisA

-
- Перейти в каталог +

Админ-панель ManolisA

+ Перейти в каталог
- {% with messages = get_flashed_messages(with_categories=true) %} {% if messages %} {% for category, message in messages %} @@ -1374,23 +1431,27 @@ ADMIN_TEMPLATE = ''' {% endwith %}
-

Синхронизация и Настройки

-
- - - - -
+

Синхронизация

-
- + +
+
+

Курс валют

+
+ + + + +
+
+
@@ -1400,46 +1461,55 @@ ADMIN_TEMPLATE = '''
- - - + +
-

Существующие категории:

- {% for item in categories %} + {% if categories %} +
+ {% for category in categories %}
- {{ item }} -
- + {{ category }} + + +
- {% else %}

Категорий пока нет.

{% endfor %} + {% endfor %} +
+ {% endif %}
-
-

Сезоны

+
+ +
+
+

Сезоны

Добавить сезон
- - - + +
-

Существующие сезоны:

- {% for item in seasons %} + {% if seasons %} +
+ {% for season in seasons %}
- {{ item }} -
- + {{ season }} + + +
- {% else %}

Сезонов пока нет.

{% endfor %} + {% endfor %} +
+ {% endif %}
@@ -1451,32 +1521,35 @@ ADMIN_TEMPLATE = '''
- - - + +
-

Список сотрудников:

- {% for item in employees %} + {% if employees %} +
+ {% for employee in employees %}
- {{ item }} -
- + {{ employee }} + + +
- {% else %}

Сотрудников пока нет.

{% endfor %} + {% endfor %} +
+ {% endif %}
-

Управление товарами

+

Товары

Добавить новый товар
-
+ @@ -1487,65 +1560,82 @@ ADMIN_TEMPLATE = ''' - + - + -

Цвета / Варианты и их фото:

-
- - -
-
- - +

Цвета и Фотографии

+
+ +
+
+ + +
+
+ + +
+
+

Список товаров:

- {% if products %}
{% for product in products %}
- {% set thumb = product.get('variants', [])[0].photos[0] if product.get('variants') and product.variants[0].photos else None %} - {% if thumb %} - Фото + {% set first_color = product.get('colors', [])[0] if product.get('colors') else None %} + {% if first_color and first_color.get('photos') %} + + Фото + {% else %} Нет фото {% endif %}
-

{{ product.name }} +

+ {{ product['name'] }} {% if product.get('in_stock', True) %}В наличии{% else %}Нет{% endif %} {% if product.get('is_top', False) %} Топ{% endif %}

Категория: {{ product.get('category', 'Без категории') }}

Сезон: {{ product.get('season', 'Без сезона') }}

-

Цена: {{ "%.2f"|format(product.price) }} USD

+

Цена: {{ "%.2f"|format(product['price']) }} USD

Шт. в линейке: {{ product.get('items_per_line', 'N/A') }}

-

Цвета/Вар-ты: {{ (product.get('variants', [])|map(attribute='color')|list)|join(', ') }}

+

Цвета: {{ product.get('colors', [])|map(attribute='name')|join(', ') }}

-
-
- + + +
-
-

Редактирование: {{ product.name }}

- - - - - + + + + + + + + + + - -

Цвета / Варианты и фото:

-

Чтобы заменить фото для варианта, просто выберите новые файлы. Старые будут удалены. Чтобы удалить вариант, оставьте его название пустым.

-
- {% for variant in product.get('variants', []) %} -
-
Вариант {{loop.index}}
- - -
{% for photo in variant.photos %}{% endfor %}
- +

Цвета и Фотографии

+
+ {% for color_variant in product.get('colors', []) %} +
+
+ + +
+ + + +
+ {% for photo in color_variant.photos %} + + {% endfor %} +
+
{% endfor %}
- - -
-
- - + +
+ + +
+
+ + +
+
{% endfor %}
- {% else %}

Товаров пока нет.

{% endif %}
- @@ -1634,23 +1723,16 @@ def catalog(): categories = sorted(data.get('categories', [])) seasons = sorted(data.get('seasons', [])) employees = sorted(data.get('employees', [])) - settings = data.get('settings', {'usd_kzt_rate': 450}) + exchange_rate_kzt = data.get('exchange_rate_kzt', 450.0) needs_save = False for product in all_products: if 'id' not in product or not product['id']: product['id'] = str(uuid.uuid4()) needs_save = True - if 'variants' not in product: - product['variants'] = [{'color': c, 'photos': []} for c in product.get('colors', [])] - if product.get('photos'): - if product['variants']: - product['variants'][0]['photos'] = product['photos'] - else: - product['variants'].append({'color': 'Default', 'photos': product['photos']}) - needs_save = True - + if 'colors' not in product: product['colors'] = [] if needs_save: + data['products'] = all_products save_data(data) products_in_stock = [p for p in all_products if p.get('in_stock', True)] @@ -1662,7 +1744,7 @@ def catalog(): categories=categories, seasons=seasons, employees=employees, - settings=settings, + exchange_rate_kzt=exchange_rate_kzt, repo_id=REPO_ID, store_address=STORE_ADDRESS ) @@ -1686,6 +1768,7 @@ def product_detail(index): return render_template_string( PRODUCT_DETAIL_TEMPLATE, product=product, + index=index, repo_id=REPO_ID ) @@ -1694,41 +1777,38 @@ def create_order(): order_data = request.get_json() if not order_data or 'cart' not in order_data or not order_data['cart']: - return jsonify({"error": "Корзина пуста или не передана."}), 400 + return jsonify({"error": "Корзина пуста."}), 400 cart_items = order_data['cart'] employee_name = order_data.get('employee', 'Онлайн') currency = order_data.get('currency', 'USD') - exchange_rate = order_data.get('exchange_rate', 1) + rate = order_data.get('rate', 1.0) if currency == 'KZT' else 1.0 - total_price_usd = 0 + total_price = 0 processed_cart = [] for item in cart_items: try: - price = float(item['price']) + price_usd = float(item['price']) quantity = int(item['quantity']) - if price < 0 or quantity <= 0: raise ValueError("Invalid price or quantity") - total_price_usd += price * quantity + price_in_currency = price_usd * rate if currency == 'KZT' else price_usd + total_price += price_in_currency * quantity + + photo_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/photos/{item['photo']}" if item.get('photo') else "https://via.placeholder.com/70x70.png?text=N/A" + processed_cart.append({ - "name": item['name'], "price": price, "quantity": quantity, - "color": item.get('color', 'N/A'), "photo": item.get('photo'), - "items_per_line": item.get('items_per_line'), - "photo_url": f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/photos/{item['photo']}" if item.get('photo') else "https://via.placeholder.com/70x70.png?text=N/A" + "name": item['name'], "price": price_in_currency, "quantity": quantity, + "color": item.get('color', 'N/A'), "photo_url": photo_url }) - except (ValueError, TypeError): - return jsonify({"error": "Неверная цена или количество в товаре."}), 400 + except (ValueError, TypeError) as e: + return jsonify({"error": "Неверная цена или количество."}), 400 order_id = f"{datetime.now().strftime('%y%m%d%H%M')}-{uuid.uuid4().hex[:4]}" - + order_timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + new_order = { - "id": order_id, - "created_at": datetime.now().strftime('%Y-%m-%d %H:%M:%S'), - "cart": processed_cart, - "total_price": round(total_price_usd, 2), - "employee": employee_name, - "status": "new", - "currency": currency, - "exchange_rate": exchange_rate + "id": order_id, "created_at": order_timestamp, "cart": processed_cart, + "total_price": round(total_price, 2), "employee": employee_name, + "currency": currency, "status": "new" } try: @@ -1737,168 +1817,174 @@ def create_order(): save_data(data) return jsonify({"order_id": order_id}), 201 except Exception as e: - logging.error(f"Failed to save order {order_id}: {e}", exc_info=True) return jsonify({"error": "Ошибка сервера при сохранении заказа."}), 500 @app.route('/order/') def view_order(order_id): data = load_data() order = data.get('orders', {}).get(order_id) - if order and 'currency' not in order: - order['currency'] = 'USD' - order['exchange_rate'] = 1.0 - - return render_template_string(ORDER_TEMPLATE, order=order, repo_id=REPO_ID) + return render_template_string(ORDER_TEMPLATE, order=order) @app.route('/admin', methods=['GET', 'POST']) def admin(): data = load_data() - products = data.get('products', []) - categories = data.get('categories', []) - seasons = data.get('seasons', []) - employees = data.get('employees', []) - settings = data.get('settings', {'usd_kzt_rate': 450}) - + if request.method == 'POST': action = request.form.get('action') + logging.info(f"Admin action received: {action}") try: - if action == 'add_category': - name = request.form.get('category_name', '').strip() - if name and name not in categories: - data['categories'].append(name) - flash(f"Категория '{name}' добавлена.", 'success') - else: flash(f"Категория '{name}' уже существует или пуста.", 'error') - - elif action == 'delete_category': - name = request.form.get('category_name') - if name in data['categories']: - data['categories'].remove(name) - for p in data['products']: - if p.get('category') == name: p['category'] = 'Без категории' - flash(f"Категория '{name}' удалена.", 'success') - - elif action == 'add_season': - name = request.form.get('season_name', '').strip() - if name and name not in seasons: - data['seasons'].append(name) - flash(f"Сезон '{name}' добавлен.", 'success') - else: flash(f"Сезон '{name}' уже существует или пуст.", 'error') - - elif action == 'delete_season': - name = request.form.get('season_name') - if name in data['seasons']: - data['seasons'].remove(name) - for p in data['products']: - if p.get('season') == name: p['season'] = 'Без сезона' - flash(f"Сезон '{name}' удален.", 'success') - - elif action == 'add_employee': - name = request.form.get('employee_name', '').strip() - if name and name not in employees: - data['employees'].append(name) - flash(f"Сотрудник '{name}' добавлен.", 'success') - else: flash(f"Сотрудник '{name}' уже существует или пуст.", 'error') - - elif action == 'delete_employee': - name = request.form.get('employee_name') - if name in data['employees']: - data['employees'].remove(name) - flash(f"Сотрудник '{name}' удален.", 'success') + if action in ['add_category', 'delete_category']: + categories = data.get('categories', []) + if action == 'add_category': + name = request.form.get('category_name', '').strip() + if name and name not in categories: + categories.append(name) + flash(f"Категория '{name}' добавлена.", 'success') + elif action == 'delete_category': + name = request.form.get('category_name') + if name in categories: + categories.remove(name) + for p in data.get('products', []): + if p.get('category') == name: p['category'] = 'Без категории' + flash(f"Категория '{name}' удалена.", 'success') + data['categories'] = sorted(categories) + + elif action in ['add_season', 'delete_season']: + seasons = data.get('seasons', []) + if action == 'add_season': + name = request.form.get('season_name', '').strip() + if name and name not in seasons: + seasons.append(name) + flash(f"Сезон '{name}' добавлен.", 'success') + elif action == 'delete_season': + name = request.form.get('season_name') + if name in seasons: + seasons.remove(name) + for p in data.get('products', []): + if p.get('season') == name: p['season'] = 'Без сезона' + flash(f"Сезон '{name}' удален.", 'success') + data['seasons'] = sorted(seasons) + + elif action in ['add_employee', 'delete_employee']: + employees = data.get('employees', []) + if action == 'add_employee': + name = request.form.get('employee_name', '').strip() + if name and name not in employees: + employees.append(name) + flash(f"Сотрудник '{name}' добавлен.", 'success') + elif action == 'delete_employee': + name = request.form.get('employee_name') + if name in employees: + employees.remove(name) + flash(f"Сотрудник '{name}' удален.", 'success') + data['employees'] = sorted(employees) - elif action == 'update_settings': - rate = request.form.get('usd_kzt_rate') + elif action == 'update_rate': + rate = request.form.get('exchange_rate_kzt') try: - data['settings']['usd_kzt_rate'] = float(rate) - flash("Настройки сохранены.", 'success') + data['exchange_rate_kzt'] = float(rate) + flash('Курс валют обновлен.', 'success') except (ValueError, TypeError): - flash("Неверный формат курса.", 'error') - - elif action in ['add_product', 'edit_product']: - product_id = request.form.get('product_id') - - is_edit = action == 'edit_product' - if is_edit: - product_index = next((i for i, p in enumerate(products) if p.get('id') == product_id), -1) - if product_index == -1: - flash(f"Ошибка: товар с ID '{product_id}' не найден.", 'error') - return redirect(url_for('admin')) - product = products[product_index] - else: - product = {'id': str(uuid.uuid4())} - - product['name'] = request.form.get('name', '').strip() - product['price'] = round(float(request.form.get('price').replace(',', '.')), 2) - product['items_per_line'] = int(request.form.get('items_per_line', '1')) - product['description'] = request.form.get('description', '').strip() - product['category'] = request.form.get('category') - product['season'] = request.form.get('season') - product['in_stock'] = 'in_stock' in request.form - product['is_top'] = 'is_top' in request.form + flash('Неверный формат курса.', 'error') + elif action == 'add_product' or action == 'edit_product': + name = request.form.get('name', '').strip() + price = round(float(request.form.get('price', '0').replace(',', '.')), 2) + items_per_line = int(request.form.get('items_per_line', '1')) + + if not name or price <= 0 or items_per_line <= 0: + flash("Название, цена и кол-во в линейке обязательны.", 'error') + return redirect(url_for('admin')) + + product_data = { + 'name': name, 'price': price, + 'items_per_line': items_per_line, + 'description': request.form.get('description', '').strip(), + 'category': request.form.get('category', 'Без категории'), + 'season': request.form.get('season', 'Без сезона'), + 'in_stock': 'in_stock' in request.form, + 'is_top': 'is_top' in request.form, + 'colors': [] + } + api = HfApi() if HF_TOKEN_WRITE else None - new_variants = [] - variant_colors = request.form.getlist('variant_color') - for i, color in enumerate(variant_colors): - color = color.strip() - if not color: continue - - existing_photos_str = request.form.get(f'variant_existing_photos_{i}', '') - current_photos = existing_photos_str.split(',') if existing_photos_str else [] + uploads_dir = 'uploads_temp' + os.makedirs(uploads_dir, exist_ok=True) + + color_names = request.form.getlist('color_name') + color_photos_files = request.files.getlist('color_photos') + existing_photos_list = request.form.getlist('existing_photos') + + file_counter = 0 + for i, color_name in enumerate(color_names): + if not color_name.strip(): continue - new_photo_files = request.files.getlist(f'variant_photos_{i}') + variant_photos = [] + # Process new uploads for this color + current_color_files = [] + num_files_for_color = len(request.files.getlist(f'color_photos_{i}')) if f'color_photos_{i}' in request.files else 0 + if action == 'edit_product': # Special handling for edit form file naming + color_photos_files = request.files.getlist(f'color_photos_block_{i}') + else: + color_photos_files = request.files.getlist(f'color_photos_block_add_{i}') + + + if color_photos_files and any(f.filename for f in color_photos_files) and api: + for photo in color_photos_files: + if photo and photo.filename: + safe_name = secure_filename(name.replace(' ', '_'))[:30] + photo_filename = f"{safe_name}_{secure_filename(color_name)}_{datetime.now().strftime('%f')}{os.path.splitext(photo.filename)[1]}" + temp_path = os.path.join(uploads_dir, photo_filename) + photo.save(temp_path) + 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) + variant_photos.append(photo_filename) + os.remove(temp_path) - if new_photo_files and any(f.filename for f in new_photo_files): - if api and is_edit and current_photos: - try: - api.delete_files(repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE, - paths_in_repo=[f"photos/{p}" for p in current_photos]) + if action == 'edit_product': # Handle existing photos + existing_for_this_color = existing_photos_list[i].split(',') if i < len(existing_photos_list) and existing_photos_list[i] else [] + if not variant_photos: # No new photos uploaded, keep old ones + variant_photos.extend(existing_for_this_color) + elif api: # New photos uploaded, delete old ones + try: api.delete_files(repo_id=REPO_ID, paths_in_repo=[f"photos/{p}" for p in existing_for_this_color], repo_type="dataset", token=HF_TOKEN_WRITE) except Exception: pass - current_photos = [] - - uploaded_photos = [] - if api: - for photo in new_photo_files: - if photo and photo.filename: - safe_name = secure_filename(product['name'])[:50] - photo_filename = f"{safe_name}_{uuid.uuid4().hex[:8]}.{photo.filename.rsplit('.',1)[-1]}" - api.upload_file(path_or_fileobj=photo, path_in_repo=f"photos/{photo_filename}", - repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE) - uploaded_photos.append(photo_filename) - - new_variants.append({ - 'color': color, - 'photos': current_photos + uploaded_photos - }) - - product['variants'] = new_variants - - if is_edit: - data['products'][product_index] = product - flash(f"Товар '{product['name']}' обновлен.", 'success') - else: - data['products'].append(product) - flash(f"Товар '{product['name']}' добавлен.", 'success') + + product_data['colors'].append({'name': color_name.strip(), 'photos': variant_photos}) + + if action == 'add_product': + product_data['id'] = str(uuid.uuid4()) + data['products'].append(product_data) + flash(f"Товар '{name}' добавлен.", 'success') + else: # edit_product + product_id = request.form.get('product_id') + product_index = next((i for i, p in enumerate(data['products']) if p.get('id') == product_id), -1) + if product_index != -1: + product_data['id'] = product_id + data['products'][product_index] = product_data + flash(f"Товар '{name}' обновлен.", 'success') elif action == 'delete_product': product_id = request.form.get('product_id') - product_to_delete = next((p for p in products if p.get('id') == product_id), None) + product_to_delete = next((p for p in data['products'] if p.get('id') == product_id), None) if product_to_delete: - data['products'] = [p for p in products if p.get('id') != product_id] + data['products'].remove(product_to_delete) if HF_TOKEN_WRITE: api = HfApi() - photos_to_delete = [p for v in product_to_delete.get('variants', []) for p in v['photos']] - if photos_to_delete: - try: - api.delete_files(repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE, - paths_in_repo=[f"photos/{p}" for p in photos_to_delete]) - except Exception: pass - flash(f"Товар '{product_to_delete['name']}' удален.", 'success') - + for color_var in product_to_delete.get('colors', []): + photos = color_var.get('photos', []) + if photos: + try: api.delete_files(repo_id=REPO_ID, paths_in_repo=[f"photos/{p}" for p in photos], repo_type="dataset", token=HF_TOKEN_WRITE) + except Exception: pass + flash(f"Товар '{product_to_delete.get('name')}' удален.", 'success') + save_data(data) return redirect(url_for('admin')) + except Exception as e: - flash(f"Произошла ошибка: {e}", 'error') - return redirect(url_for('admin')) + logging.error(f"Admin action '{action}' error: {e}", exc_info=True) + flash(f"Произошла ошибка при выполнении: {e}", 'error') + return redirect(url_for('admin')) current_data = load_data() return render_template_string( @@ -1907,7 +1993,7 @@ def admin(): categories=sorted(current_data.get('categories', [])), seasons=sorted(current_data.get('seasons', [])), employees=sorted(current_data.get('employees', [])), - settings=current_data.get('settings'), + exchange_rate_kzt=current_data.get('exchange_rate_kzt', 450.0), repo_id=REPO_ID ) @@ -1915,7 +2001,7 @@ def admin(): def force_upload(): try: upload_db_to_hf() - flash("Данные успешно загружены на Hugging Face.", 'success') + flash("Данные успешно загружены.", 'success') except Exception as e: flash(f"Ошибка при загрузке: {e}", 'error') return redirect(url_for('admin')) @@ -1924,7 +2010,7 @@ def force_upload(): def force_download(): try: if download_db_from_hf(): - flash("Данные успешно скачаны. Локальные файлы обновлены.", 'success') + flash("Данные успешно скачаны.", 'success') else: flash("Не удалось скачать данные.", 'error') except Exception as e: @@ -1932,14 +2018,17 @@ def force_download(): return redirect(url_for('admin')) - if __name__ == '__main__': + logging.info("Application starting up...") download_db_from_hf() load_data() - + if HF_TOKEN_WRITE: - backup_thread = threading.Thread(target=periodic_backup, daemon=True) - backup_thread.start() + threading.Thread(target=periodic_backup, daemon=True).start() + logging.info("Periodic backup thread started.") + else: + logging.warning("Periodic backup will NOT run (HF_TOKEN for writing not set).") port = int(os.environ.get('PORT', 7860)) app.run(debug=False, host='0.0.0.0', port=port) +