diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -101,8 +101,6 @@ translations = { 'online_order': 'Онлайн', 'address_1_title': 'Адрес:', 'address_1_detail': 'Город Алматы, Рынок Олжа, ряд VIP, Бутик 7', - 'currency_usd': 'USD', - 'currency_kzt': 'KZT', }, 'kk': { 'site_title': "ManolisA - Каталог", @@ -171,8 +169,6 @@ translations = { 'online_order': 'Онлайн', 'address_1_title': 'Мекенжай:', 'address_1_detail': 'Алматы қаласы, Олжа базары, VIP қатары, 7 бутик', - 'currency_usd': 'USD', - 'currency_kzt': 'KZT', } } @@ -232,7 +228,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_to_kzt_rate': 450.0}}, f) + json.dump({'products': [], 'categories': [], 'seasons': [], 'orders': {}, 'employees': [], 'settings': {'usd_kzt_rate': 450}}, 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}") @@ -296,7 +292,7 @@ def periodic_backup(): def load_data(): - default_data = {'products': [], 'categories': [], 'seasons': [], 'orders': {}, 'employees': [], 'settings': {'usd_to_kzt_rate': 450.0}} + default_data = {'products': [], 'categories': [], 'seasons': [], 'orders': {}, 'employees': [], 'settings': {'usd_kzt_rate': 450}} try: with open(DATA_FILE, 'r', encoding='utf-8') as file: data = json.load(file) @@ -309,7 +305,7 @@ def load_data(): 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_to_kzt_rate': 450.0} + if 'settings' not in data: data['settings'] = {'usd_kzt_rate': 450} return data except FileNotFoundError: logging.warning(f"Local file {DATA_FILE} not found. Attempting download from HF.") @@ -327,7 +323,7 @@ def load_data(): 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_to_kzt_rate': 450.0} + if 'settings' not in data: data['settings'] = {'usd_kzt_rate': 450} return data except FileNotFoundError: logging.error(f"File {DATA_FILE} still not found even after download reported success. Using default.") @@ -359,7 +355,8 @@ def save_data(data): 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_to_kzt_rate': 450.0} + if 'settings' not in data: data['settings'] = {'usd_kzt_rate': 450} + with open(DATA_FILE, 'w', encoding='utf-8') as file: json.dump(data, file, ensure_ascii=False, indent=4) @@ -377,136 +374,151 @@ CATALOG_TEMPLATE = ''' {{ _('site_title') }} + - +
-
-
+
- - + USD + KZT
RU KZ
-
+
{{ _('our_address') }} {{ store_address }}
+ +
+ +
-
-
- {{ _('category') }} - - {% for category in categories %} - - {% endfor %} +
+ {% if seasons %} +
+
{{ _('season') }}
+
+ + {% for season in seasons %} + + {% endfor %} +
-
- {{ _('season') }} - - {% for season in seasons %} - - {% endfor %} + {% endif %} + +
+
{{ _('category') }}
+
+ + {% for category in categories %} + + {% endfor %} +
-
- -
{% for product in products %} @@ -525,22 +537,32 @@ CATALOG_TEMPLATE = '''
{% 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 %} - {{ product['name'] }} + {% if first_photo %} + {{ product['name'] }} + {% else %} + No Image + {% 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.get('items_per_line') }} {{ _('items_in_line') }}) - {% 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.get('items_per_line') }} {{ _('items_in_line') }}) + {% endif %} +
+
+ {% set colors = product.get('variants', [])|map(attribute='color')|select('ne', '')|list %} + {% if colors %} + {{ colors|join(', ') }} + {% endif %}
@@ -586,7 +608,7 @@ CATALOG_TEMPLATE = '''
- {{ _('total') }} 0.00 USD + {{ _('total') }} 0.00
`; }).join(''); - cartTotalElement.textContent = formatPrice(total, currentCurrency); + cartTotalElement.dataset.totalUsd = totalUSD.toFixed(2); } - + const employeeSelect = document.getElementById('employeeSelect'); if (employeeSelect) { employeeSelect.innerHTML = ``; @@ -923,13 +890,13 @@ CATALOG_TEMPLATE = ''' employeeSelect.appendChild(option); }); } - + updateAllPrices(); openModal('cartModal'); } function removeFromCart(itemId) { cart = cart.filter(item => item.id !== itemId); - localStorage.setItem('manolisaCart', JSON.stringify(cart)); + localStorage.setItem('manolisACart', JSON.stringify(cart)); openCartModal(); updateCartButton(); } @@ -937,7 +904,7 @@ CATALOG_TEMPLATE = ''' function clearCart() { if (confirm(t.confirm_clear_cart)) { cart = []; - localStorage.removeItem('manolisaCart'); + localStorage.removeItem('manolisACart'); openCartModal(); updateCartButton(); } @@ -949,7 +916,7 @@ CATALOG_TEMPLATE = ''' return; } const employee = document.getElementById('employeeSelect').value; - const orderData = { cart: cart, employee: employee, currency: currentCurrency, rate: exchangeRate }; + const orderData = { cart: cart, employee: employee, currency: currentCurrency, exchange_rate: USD_KZT_RATE }; const formulateButton = document.querySelector('.formulate-order-button'); if (formulateButton) formulateButton.disabled = true; showNotification(t.formulating_order, 5000); @@ -966,7 +933,7 @@ CATALOG_TEMPLATE = ''' }) .then(data => { if (data.order_id) { - localStorage.removeItem('manolisaCart'); + localStorage.removeItem('manolisACart'); cart = []; updateCartButton(); closeModal('cartModal'); @@ -976,7 +943,6 @@ CATALOG_TEMPLATE = ''' } }) .catch(error => { - console.error('Order formulation error:', error); alert(`${t.order_creation_error} ${error.message}`); if (formulateButton) formulateButton.disabled = false; }); @@ -1014,18 +980,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); } } function setupFilters() { - const searchInput = document.getElementById('search-input'); - if(searchInput) searchInput.addEventListener('input', filterProducts); - + document.getElementById('search-input').addEventListener('input', filterProducts); document.querySelectorAll('.category-filter').forEach(filter => { filter.addEventListener('click', function() { document.querySelectorAll('.category-filter').forEach(f => f.classList.remove('active')); @@ -1053,7 +1012,7 @@ CATALOG_TEMPLATE = ''' favorites.push(productId); buttonElement.classList.add('favorited'); } - localStorage.setItem('manolisaFavorites', JSON.stringify(favorites)); + localStorage.setItem('manolisAFavorites', JSON.stringify(favorites)); } function updateFavoriteIcons() { @@ -1071,9 +1030,7 @@ CATALOG_TEMPLATE = ''' const productIndex = products.findIndex(p => p.id === productId); if (productIndex > -1) { closeModal('favoritesModal'); - setTimeout(() => { - openModalByIndex(productIndex); - }, 250); + setTimeout(() => openModalByIndex(productIndex), 250); } } @@ -1086,9 +1043,10 @@ CATALOG_TEMPLATE = ''' } else { const favoriteProducts = products.filter(p => favorites.includes(p.id)); favoriteProducts.forEach(item => { - const first_variant = item.variants && item.variants.length > 0 ? item.variants[0] : null; - const photoUrl = first_variant && first_variant.photos && first_variant.photos.length > 0 - ? `https://huggingface.co/datasets/${repoId}/resolve/main/photos/${first_variant.photos[0]}` + 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}` : 'https://via.placeholder.com/70x70.png?text=N/A'; const itemHtml = ` @@ -1096,13 +1054,14 @@ CATALOG_TEMPLATE = ''' ${item.name}
${item.name} -

${formatPrice(item.price, currentCurrency)}

+

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

- +
`; favoritesContent.innerHTML += itemHtml; }); + updateAllPrices(); } openModal('favoritesModal'); @@ -1113,7 +1072,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(); } @@ -1135,20 +1094,24 @@ CATALOG_TEMPLATE = ''' } document.addEventListener('DOMContentLoaded', () => { - setCurrency(currentCurrency); updateCartButton(); setupFilters(); updateFavoriteIcons(); - window.addEventListener('click', function(event) { - if (event.target.classList.contains('modal')) { - closeModal(event.target.id); - } + setCurrency(currentCurrency); + + 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('keydown', function(event) { - if (event.key === 'Escape') { - document.querySelectorAll('.modal[style*="display: block"]').forEach(modal => { - closeModal(modal.id); - }); + window.addEventListener('keydown', e => { + if (e.key === 'Escape') { + document.querySelectorAll('.modal[style*="display: block"]').forEach(modal => closeModal(modal.id)); } }); }); @@ -1159,43 +1122,55 @@ 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 %} +
+
+ {{ product['name'] }} - photo {{ loop.index }} +
+
+ {% endfor %} + {% else %} +
+ No image available +
+ {% endif %} +
+ {% if all_photos|length > 1 %} +
+
+
+ {% endif %}
-
-

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

-

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

-
- -

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

{{ _('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')) }} / {{ _('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 %} - - {% set variants = product.get('variants', []) %} - {% if variants %} -
-

{{ _('available_colors') }}

-
- {% for variant in variants %} - - {% endfor %} -
-
- {% 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 %}
''' @@ -1207,42 +1182,42 @@ ORDER_TEMPLATE = ''' {{ _('order_title') }}{{ order.id }} - ManolisA - + @@ -1256,22 +1231,26 @@ 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 }} × {{ "%.2f"|format(item.price) }} {{ order.currency }} + {{ item.quantity }} × {{ "%.0f"|format(price_per_item) if currency_symbol == '₸' else "%.2f"|format(price_per_item) }} {{ currency_symbol }}
- {{ "%.2f"|format(item.price * item.quantity) }} {{ order.currency }} + {{ "%.0f"|format(total_item_price) if currency_symbol == '₸' else "%.2f"|format(total_item_price) }} {{ currency_symbol }}
{% endfor %}
-

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

+ {% 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 }}

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

{{ _('error') }}

+

{{ _('error') }}

{{ _('order_not_found') }}

{{ _('back_to_catalog') }} {% endif %} @@ -1318,66 +1297,74 @@ ADMIN_TEMPLATE = ''' Админ-панель - ManolisA - +
+ {% with messages = get_flashed_messages(with_categories=true) %} {% if messages %} {% for category, message in messages %} @@ -1387,26 +1374,21 @@ ADMIN_TEMPLATE = ''' {% endwith %}
-

Синхронизация с Датацентром

+

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

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

Резервное копирование происходит автоматически каждые 30 минут, а также после каждого сохранения данных. Используйте эти кнопки для немедленной синхронизации.

-
- -
-

Общие настройки

-
- - - - -
@@ -1424,24 +1406,19 @@ ADMIN_TEMPLATE = '''
-
- {% for category in categories %} +

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

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

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

{% endfor %}
-
- -
-

Сезоны

+

Сезоны

Добавить сезон
@@ -1453,18 +1430,16 @@ ADMIN_TEMPLATE = '''
-
- {% for season in seasons %} +

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

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

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

{% endfor %}
@@ -1482,18 +1457,16 @@ ADMIN_TEMPLATE = '''
-
- {% for employee in employees %} +

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

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

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

{% endfor %}
@@ -1503,9 +1476,9 @@ ADMIN_TEMPLATE = '''
Добавить новый товар
-
+ - + @@ -1514,31 +1487,18 @@ ADMIN_TEMPLATE = ''' - + - + -

Цветовые варианты

+

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

- -
+ + +
+
-
- - -
-
- - -
-
- +
@@ -1550,99 +1510,79 @@ ADMIN_TEMPLATE = '''
- {% set first_photo = product.variants[0].photos[0] if product.variants and product.variants[0].photos else None %} - Фото -
+ {% set thumb = product.get('variants', [])[0].photos[0] if product.get('variants') and product.variants[0].photos else None %} + {% if thumb %} + Фото + {% else %} + Нет фото + {% endif %} +
-

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

{{ 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 | В линейке: {{ product.get('items_per_line', 'N/A') }} шт.

- {% if product.variants %} -

Варианты: {{ product.variants|map(attribute='color')|join(', ') }}

- {% endif %} +

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

+

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

+

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

+

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

+

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

-
- - + +
-

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

+

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

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

Цветовые варианты

+

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

+

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

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

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

- {% endif %} + {% else %}

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

{% endif %} - @@ -1684,25 +1634,23 @@ def catalog(): categories = sorted(data.get('categories', [])) seasons = sorted(data.get('seasons', [])) employees = sorted(data.get('employees', [])) - settings = data.get('settings', {'usd_to_kzt_rate': 450.0}) + settings = data.get('settings', {'usd_kzt_rate': 450}) 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 - # Data migration for old structure - if 'colors' in product or 'photos' in product: - product['variants'] = [{ - 'color': 'Основной', - 'photos': product.get('photos', []) - }] - if 'colors' in product: del product['colors'] - if 'photos' in product: del product['photos'] + 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 needs_save: - data['products'] = all_products save_data(data) products_in_stock = [p for p in all_products if p.get('in_stock', True)] @@ -1751,43 +1699,36 @@ def create_order(): cart_items = order_data['cart'] employee_name = order_data.get('employee', 'Онлайн') currency = order_data.get('currency', 'USD') - rate = order_data.get('rate', 1) + exchange_rate = order_data.get('exchange_rate', 1) total_price_usd = 0 processed_cart = [] for item in cart_items: try: - price_usd = float(item['price']) + price = float(item['price']) quantity = int(item['quantity']) - if price_usd < 0 or quantity <= 0: raise ValueError - - total_price_usd += price_usd * quantity - - price_in_currency = price_usd * (rate if currency == 'KZT' else 1) - + if price < 0 or quantity <= 0: raise ValueError("Invalid price or quantity") + total_price_usd += price * quantity processed_cart.append({ - "name": item['name'], - "price": round(price_in_currency, 2), - "quantity": quantity, - "color": item.get('color', 'N/A'), - "photo": item.get('photo'), + "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" }) - except (ValueError, TypeError) as e: + except (ValueError, TypeError): return jsonify({"error": "Неверная цена или количество в товаре."}), 400 - order_id = f"{datetime.now().strftime('%Y%m%d%H%M%S')}-{uuid.uuid4().hex[:6]}" - total_in_currency = total_price_usd * (rate if currency == 'KZT' else 1) + order_id = f"{datetime.now().strftime('%y%m%d%H%M')}-{uuid.uuid4().hex[:4]}" new_order = { "id": order_id, "created_at": datetime.now().strftime('%Y-%m-%d %H:%M:%S'), "cart": processed_cart, - "total_price": round(total_in_currency, 2), - "currency": currency, + "total_price": round(total_price_usd, 2), "employee": employee_name, - "status": "new" + "status": "new", + "currency": currency, + "exchange_rate": exchange_rate } try: @@ -1796,166 +1737,166 @@ 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) @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': - category_name = request.form.get('category_name', '').strip() - if category_name and category_name not in data['categories']: - data['categories'].append(category_name) - flash(f"Категория '{category_name}' добавлена.", 'success') - else: - flash("Ошибка: Пустое имя или категория уже существует.", 'error') + 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': - category_name = request.form.get('category_name') - if category_name in data['categories']: - data['categories'].remove(category_name) + name = request.form.get('category_name') + if name in data['categories']: + data['categories'].remove(name) for p in data['products']: - if p.get('category') == category_name: p['category'] = 'Без категории' - flash(f"Категория '{category_name}' удалена.", 'success') - + if p.get('category') == name: p['category'] = 'Без категории' + flash(f"Категория '{name}' удалена.", 'success') + elif action == 'add_season': - season_name = request.form.get('season_name', '').strip() - if season_name and season_name not in data['seasons']: - data['seasons'].append(season_name) - flash(f"Сезон '{season_name}' добавлен.", 'success') - else: - flash("Ошибка: Пустое имя или сезон уже существует.", 'error') + 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': - season_name = request.form.get('season_name') - if season_name in data['seasons']: - data['seasons'].remove(season_name) + name = request.form.get('season_name') + if name in data['seasons']: + data['seasons'].remove(name) for p in data['products']: - if p.get('season') == season_name: p['season'] = 'Без сезона' - flash(f"Сезон '{season_name}' удален.", 'success') + if p.get('season') == name: p['season'] = 'Без сезона' + flash(f"Сезон '{name}' удален.", 'success') elif action == 'add_employee': - employee_name = request.form.get('employee_name', '').strip() - if employee_name and employee_name not in data['employees']: - data['employees'].append(employee_name) - flash(f"Сотрудник '{employee_name}' добавлен.", 'success') - else: - flash("Ошибка: Пустое имя или сотрудник уже существует.", 'error') + 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': - employee_name = request.form.get('employee_name') - if employee_name in data['employees']: - data['employees'].remove(employee_name) - flash(f"Сотрудник '{employee_name}' удален.", 'success') + name = request.form.get('employee_name') + if name in data['employees']: + data['employees'].remove(name) + flash(f"Сотрудник '{name}' удален.", 'success') - elif action == 'save_settings': - rate_str = request.form.get('usd_to_kzt_rate', '450.0').replace(',', '.') + elif action == 'update_settings': + rate = request.form.get('usd_kzt_rate') try: - data['settings']['usd_to_kzt_rate'] = float(rate_str) + data['settings']['usd_kzt_rate'] = float(rate) flash("Настройки сохранены.", 'success') except (ValueError, TypeError): flash("Неверный формат курса.", 'error') - + elif action in ['add_product', 'edit_product']: - api = HfApi() if HF_TOKEN_WRITE else None - uploads_dir = 'uploads_temp' - os.makedirs(uploads_dir, exist_ok=True) - - 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')) + 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 + + api = HfApi() if HF_TOKEN_WRITE else None new_variants = [] variant_colors = request.form.getlist('variant_color') - variant_photos_files = request.files.getlist('variant_photos') - for i, color in enumerate(variant_colors): - photos_list = [] - # For edit product, get existing photos - if action == 'edit_product': - product_id = request.form.get('product_id') - product_to_edit = next((p for p in data['products'] if p.get('id') == product_id), None) - if product_to_edit and i < len(product_to_edit.get('variants', [])): - photos_list = product_to_edit['variants'][i].get('photos', []) + 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 [] + + new_photo_files = request.files.getlist(f'variant_photos_{i}') - # If new photos are uploaded for this variant - if variant_photos_files and i < len(variant_photos_files) and variant_photos_files[i].filename: - # Delete old photos if replacing - if photos_list and api: + 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, paths_in_repo=[f"photos/{p}" for p in photos_list], repo_type="dataset", token=HF_TOKEN_WRITE) - except Exception as e: - logging.error(f"Error deleting old photos {photos_list}: {e}") - photos_list = [] # Reset list for new photos - - # Process new photos (this is simplified, would need to handle multiple files per variant properly) - # The current request.files.getlist('variant_photos') puts all files in one list. - # A better approach would be name="variant_photos_{i}" - photo = variant_photos_files[i] # This part is a simplification. - safe_name = secure_filename(name.replace(' ', '_'))[:50] - photo_filename = f"{safe_name}_{color}_{datetime.now().strftime('%Y%m%d%H%M%S%f')}.jpg" - temp_path = os.path.join(uploads_dir, photo_filename) - photo.save(temp_path) - if api: - 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) - photos_list.append(photo_filename) - os.remove(temp_path) - - if color or photos_list: - new_variants.append({'color': color.strip(), 'photos': photos_list}) + 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]) + 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_data = { - 'name': name, 'price': price, - 'description': request.form.get('description', '').strip(), - 'items_per_line': items_per_line, - 'category': request.form.get('category'), - 'season': request.form.get('season'), - 'in_stock': 'in_stock' in request.form, - 'is_top': 'is_top' in request.form, - 'variants': new_variants - } - - 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') - + 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') + elif action == 'delete_product': product_id = request.form.get('product_id') - product_to_delete = next((p for p in data['products'] if p.get('id') == product_id), None) + product_to_delete = next((p for p in 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] if HF_TOKEN_WRITE: api = HfApi() - for variant in product_to_delete.get('variants', []): - if variant.get('photos'): - try: - api.delete_files(repo_id=REPO_ID, paths_in_repo=[f"photos/{p}" for p in variant['photos']], repo_type="dataset", token=HF_TOKEN_WRITE) - except Exception as e: - logging.error(f"Error deleting photos for {product_to_delete['name']}: {e}") - data['products'] = [p for p in data['products'] if p.get('id') != product_id] + 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') save_data(data) return redirect(url_for('admin')) except Exception as e: - logging.error(f"Error processing admin action '{action}': {e}", exc_info=True) flash(f"Произошла ошибка: {e}", 'error') return redirect(url_for('admin')) @@ -1966,47 +1907,39 @@ 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', {'usd_to_kzt_rate': 450.0}), + settings=current_data.get('settings'), repo_id=REPO_ID ) @app.route('/force_upload', methods=['POST']) def force_upload(): - logging.info("Forcing upload to Hugging Face...") try: upload_db_to_hf() flash("Данные успешно загружены на Hugging Face.", 'success') except Exception as e: - flash(f"Ошибка при загрузке на Hugging Face: {e}", 'error') + flash(f"Ошибка при загрузке: {e}", 'error') return redirect(url_for('admin')) @app.route('/force_download', methods=['POST']) def force_download(): - logging.info("Forcing download from Hugging Face...") try: if download_db_from_hf(): flash("Данные успешно скачаны. Локальные файлы обновлены.", 'success') - load_data() else: flash("Не удалось скачать данные.", 'error') except Exception as e: - flash(f"Ошибка при скачивании с Hugging Face: {e}", 'error') + flash(f"Ошибка при скачивании: {e}", 'error') return redirect(url_for('admin')) + + if __name__ == '__main__': - logging.info("Application starting up. Performing initial data load/download...") download_db_from_hf() load_data() - logging.info("Initial data load complete.") if HF_TOKEN_WRITE: backup_thread = threading.Thread(target=periodic_backup, daemon=True) backup_thread.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)) - logging.info(f"Starting Flask app on host 0.0.0.0 and port {port}") app.run(debug=False, host='0.0.0.0', port=port) -