from flask import Flask, render_template_string, request, redirect, url_for, send_file, jsonify import json import os import logging import threading import time from datetime import datetime, timezone, timedelta from huggingface_hub import HfApi, hf_hub_download from huggingface_hub.utils import RepositoryNotFoundError from werkzeug.utils import secure_filename import pandas as pd from io import BytesIO import requests from dotenv import load_dotenv load_dotenv() app = Flask(__name__) DATA_FILE = 'data_optomshop.json' ORDERS_FILE = 'orders_optomshop.json' REPO_ID = "Kgshop/optomshop" HF_TOKEN_WRITE = os.getenv("HF_TOKEN") HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") WHATSAPP_NUMBER = "996703103679" logging.basicConfig(level=logging.DEBUG) BISHKEK_TIMEZONE = timezone(timedelta(hours=6)) def load_data(): try: download_db_from_hf() with open(DATA_FILE, 'r', encoding='utf-8') as f: data = json.load(f) if not isinstance(data, dict) or 'products' not in data or 'categories' not in data: return {'products': [], 'categories': []} if data['categories'] and isinstance(data['categories'][0], str): data['categories'] = [{'name': cat, 'subcategories': []} for cat in data['categories']] return data except (FileNotFoundError, json.JSONDecodeError, RepositoryNotFoundError) as e: logging.warning(f"Problem loading data (using empty values): {e}") return {'products': [], 'categories': []} except Exception as e: logging.error(f"Unexpected error loading data: {e}") return {'products': [], 'categories': []} def save_data(data): try: with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump(data, f, ensure_ascii=False, indent=4) upload_db_to_hf() except Exception as e: logging.error(f"Error saving data: {e}") def load_orders(): try: if not os.path.exists(ORDERS_FILE): return [] with open(ORDERS_FILE, 'r', encoding='utf-8') as f: return json.load(f) except (FileNotFoundError, json.JSONDecodeError) as e: logging.warning(f"Problem loading orders: {e}") return [] except Exception as e: logging.error(f"Error loading orders: {e}") return [] def save_orders(orders): try: with open(ORDERS_FILE, 'w', encoding='utf-8') as f: json.dump(orders, f, ensure_ascii=False, indent=4) except Exception as e: logging.error(f"Error saving orders: {e}") def upload_db_to_hf(): try: if not HF_TOKEN_WRITE: raise ValueError("HF_TOKEN_WRITE not set.") api = HfApi() api.upload_file( path_or_fileobj=DATA_FILE, path_in_repo=DATA_FILE, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE, commit_message=f"Automatic backup {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" ) logging.info("Database uploaded to HF Hub.") except Exception as e: logging.error(f"Error uploading to HF Hub: {e}") def download_db_from_hf(): try: if not HF_TOKEN_READ: raise ValueError("HF_TOKEN_READ not set") hf_hub_download( repo_id=REPO_ID, filename=DATA_FILE, repo_type="dataset", token=HF_TOKEN_READ, local_dir=".", local_dir_use_symlinks=False ) logging.info("Database downloaded from HF Hub.") except RepositoryNotFoundError as e: logging.error(f"Repository not found: {e}") raise except Exception as e: logging.error(f"Error downloading from HF Hub: {e}") raise def periodic_backup(): while True: upload_db_to_hf() time.sleep(800) @app.route('/') def index(): return redirect(url_for('catalog')) @app.route('/catalog') def catalog(): data = load_data() return render_template_string(get_catalog_template(), products=data['products'], categories=data['categories'], repo_id=REPO_ID, current_category="all") @app.route('/catalog/') def category_catalog(category): data = load_data() filtered_products = [] if ':' in category: main_category, subcategory = category.split(':', 1) main_category = main_category.strip().lower() subcategory = subcategory.strip().lower() filtered_products = [ p for p in data['products'] if str(p.get('category', '')).strip().lower() == main_category and str(p.get('subcategory', '')).strip().lower() == subcategory ] else: category = category.strip().lower() filtered_products = [ p for p in data['products'] if str(p.get('category', '')).strip().lower() == category ] return render_template_string(get_catalog_template(), products=filtered_products, categories=data['categories'], repo_id=REPO_ID, current_category=category) def get_catalog_template(): return ''' Optomshop - мужская одежда, футболки оптом
Контакты

Группы

Все категории
{% for category in categories %}
{{ category['name'] }}
{% if category['subcategories'] %}
{% for subcat in category['subcategories'] %}
{{ subcat }}
{% endfor %}
Все в {{ category['name'] }}
{% endif %} {% endfor %}
{% for product in products %}
{% if product.get('photos') and product['photos']|length > 0 %}
{{ product['name'] }}
{% endif %}

{{ product['name'] }}

{{ product['price']|int }} сом

{{ product['description'][:50] }}{% if product['description']|length > 50 %}...{% endif %}

{% endfor %}
''' @app.route('/product/') def product_detail(index): data = load_data() products = data['products'] try: product = products[index] except IndexError: return "Продукт не найден", 404 detail_html = ''' {{ product['name'] }} - Optomshop

{{ product['name'] }}

{% if product.get('photos') and product['photos']|length > 0 %} {{ product['name'] }} {% else %} No Image {% endif %}
{% if product.get('photos') %} {% for photo in product['photos'] %} {{ product['name'] }} {% endfor %} {% endif %}

Артикул: {{ product.get('article', 'Не указан') }}

Доступные цвета: {{ product.get('colors', ['Нет цветов'])|join(', ') }}

Размеры: {{ product.get('size', 'Не указан') }}

Цена: {{ product['price']|int }} сом

Описание: {{ product['description'] }}

Назад к каталогу
''' return render_template_string(detail_html, product=product, repo_id=REPO_ID) @app.route('/save_order', methods=['POST']) def save_order_route(): try: order = request.get_json() orders = load_orders() orders.append(order) save_orders(orders) return jsonify({'success': True}) except Exception as e: logging.error(f"Error saving order: {e}") return jsonify({'success': False, 'error': str(e)}) @app.route('/admin', methods=['GET', 'POST']) def admin(): data = load_data() products = data['products'] categories = data['categories'] orders = load_orders() if request.method == 'POST': action = request.form.get('action') if action == 'add_category': category_name = request.form.get('category_name').strip().lower() if category_name and not any(c['name'].lower() == category_name for c in categories): categories.append({'name': category_name, 'subcategories': []}) save_data(data) return redirect(url_for('admin')) return "Ошибка: Категория уже существует или не указано название", 400 elif action == 'add_subcategory': category_index = int(request.form.get('category_index')) subcategory_name = request.form.get('subcategory_name').strip().lower() if subcategory_name and subcategory_name not in [sub.lower() for sub in categories[category_index]['subcategories']]: categories[category_index]['subcategories'].append(subcategory_name) save_data(data) return redirect(url_for('admin')) return "Ошибка: Подкатегория уже существует или не указано название", 400 elif action == 'delete_category': category_index = int(request.form.get('category_index')) deleted_category = categories.pop(category_index) category_name = deleted_category['name'].lower() for product in products: if product.get('category', '').lower() == category_name: if 'category' in product: del product['category'] if 'subcategory' in product: del product['subcategory'] save_data(data) return redirect(url_for('admin')) elif action == 'delete_subcategory': category_index = int(request.form.get('category_index')) subcategory_index = int(request.form.get('subcategory_index')) deleted_subcategory = categories[category_index]['subcategories'].pop(subcategory_index).lower() for product in products: if product.get('category', '').lower() == categories[category_index]['name'].lower() and product.get('subcategory', '').lower() == deleted_subcategory: if 'subcategory' in product: del product['subcategory'] save_data(data) return redirect(url_for('admin')) elif action == 'add': name = request.form.get('name') price = request.form.get('price') description = request.form.get('description') size = request.form.get('size') article = request.form.get('article') category = request.form.get('category').strip().lower() if request.form.get('category') else None subcategory = request.form.get('subcategory').strip().lower() if request.form.get('subcategory') else None photos_files = request.files.getlist('photos') colors = request.form.getlist('colors') photos_list = [] if photos_files: for photo in photos_files[:10]: if photo and photo.filename: photo_filename = secure_filename(photo.filename) if not photo_filename: return "Ошибка: Недопустимое имя файла", 400 uploads_dir = 'uploads' os.makedirs(uploads_dir, exist_ok=True) temp_path = os.path.join(uploads_dir, photo_filename) photo.save(temp_path) if not HF_TOKEN_WRITE: return "Ошибка: HF_TOKEN_WRITE не установлен", 500 try: api = HfApi() 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, commit_message=f"Добавлено фото: {name}" ) photos_list.append(photo_filename) except Exception as e: logging.error(f"Ошибка при загрузке фото: {e}") return f"Ошибка при загрузке фото: {e}", 500 finally: if os.path.exists(temp_path): os.remove(temp_path) if not name or not price or not description: return "Ошибка: Заполните все поля", 400 try: price = int(float(price.replace(',', '.'))) except ValueError: return "Ошибка: Неверный формат цены", 400 new_product = { 'name': name, 'price': price, 'description': description, 'size': size if size else 'Не указан', 'article': article if article else 'Не указан', 'category': category if category else None, 'subcategory': subcategory if subcategory and category else None, 'photos': photos_list, 'colors': colors if colors else [] } products.append(new_product) save_data(data) return redirect(url_for('admin')) elif action == 'edit': index = int(request.form.get('index')) name = request.form.get('name') price = request.form.get('price') description = request.form.get('description') size = request.form.get('size') article = request.form.get('article') category = request.form.get('category').strip().lower() if request.form.get('category') else None subcategory = request.form.get('subcategory').strip().lower() if request.form.get('subcategory') else None photos_files = request.files.getlist('photos') colors = request.form.getlist('colors') if photos_files and any(photo.filename for photo in photos_files): new_photos_list = [] for photo in photos_files[:10]: if photo and photo.filename: photo_filename = secure_filename(photo.filename) if not photo_filename: return "Ошибка: Недопустимое имя файла", 400 uploads_dir = 'uploads' os.makedirs(uploads_dir, exist_ok=True) temp_path = os.path.join(uploads_dir, photo_filename) photo.save(temp_path) if not HF_TOKEN_WRITE: return "Ошибка: HF_TOKEN_WRITE не установлен", 500 try: api = HfApi() 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, commit_message=f"Обновлено фото: {name}" ) new_photos_list.append(photo_filename) except Exception as e: logging.error(f"Ошибка при загрузке фото: {e}") return f"Ошибка при загрузке фото: {e}", 500 finally: if os.path.exists(temp_path): os.remove(temp_path) products[index]['photos'] = new_photos_list try: products[index]['price'] = int(float(price.replace(',', '.'))) except ValueError: return "Ошибка: Неверный формат цены", 400 products[index]['name'] = name products[index]['description'] = description products[index]['size'] = size if size else 'Не указан' products[index]['article'] = article if article else 'Не указан' products[index]['category'] = category if category else None products[index]['subcategory'] = subcategory if subcategory and category else None products[index]['colors'] = colors if colors else [] save_data(data) return redirect(url_for('admin')) elif action == 'delete': index = int(request.form.get('index')) del products[index] save_data(data) return redirect(url_for('admin')) elif action == 'download_order': order_index = int(request.form.get('order_index')) order = orders[order_index] sorted_items = sorted(order['items'], key=lambda item: (item['name'], item['color'])) df = pd.DataFrame({ 'Фото': [item.get('photo', '') for item in sorted_items], 'Наименование': [item['name'] for item in sorted_items], 'Количество': [item['quantity'] for item in sorted_items], 'Цвет': [item['color'] for item in sorted_items], 'Итоговая цена': [item['price'] * item['quantity'] for item in sorted_items] }) total_row = pd.DataFrame({ 'Фото': [''], 'Наименование': ['Итого'], 'Количество': [''], 'Цвет': [''], 'Итоговая цена': [order['total']] }) df = pd.concat([df, total_row], ignore_index=True) output = BytesIO() writer = pd.ExcelWriter(output, engine='xlsxwriter') df.to_excel(writer, sheet_name='Order', index=False) workbook = writer.book worksheet = writer.sheets['Order'] worksheet.set_column('A:A', 20) worksheet.set_column('B:B', 30) worksheet.set_column('C:C', 15) worksheet.set_column('D:D', 15) worksheet.set_column('E:E', 20) text_format = workbook.add_format({ 'border': 1, 'valign': 'vcenter', 'text_wrap': True }) for i, item in enumerate(sorted_items): worksheet.set_row(i + 1, 100) for col in range(1, 5): worksheet.write(i + 1, col, df.iloc[i, col], text_format) if item.get('photo'): image_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/photos/{item['photo']}" try: response = requests.get(image_url, timeout=10) response.raise_for_status() image_data = BytesIO(response.content) if len(image_data.getvalue()) > 0: worksheet.insert_image(i + 1, 0, image_url, { 'image_data': image_data, 'x_scale': 0.05, 'y_scale': 0.05, 'object_position': 2, 'x_offset': 5, 'y_offset': 5 }) else: logging.warning(f"Empty image data for {image_url}") except Exception as e: logging.error(f"Error with image {image_url}: {e}") writer.close() output.seek(0) timestamp = datetime.fromisoformat(order['timestamp'].replace('Z', '+00:00')).strftime('%Y%m%d_%H%M%S') filename = f"order_{timestamp}.xlsx" return send_file( output, as_attachment=True, download_name=filename, mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ) elif action == 'delete_order': order_index = int(request.form.get('order_index')) del orders[order_index] save_orders(orders) return redirect(url_for('admin')) return render_template_string(get_admin_template(), products=products, categories=categories, orders=orders, repo_id=REPO_ID) def get_admin_template(): return ''' Админ-панель - Optomshop

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

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

{% for category in categories %}
{{ category['name'] }}
{% for subcat in category['subcategories'] %}
{{ subcat }}
{% endfor %}
{% endfor %}

Добавить товар

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

{% for product in products %}

{{ product['name'] }}

Артикул: {{ product.get('article', 'Не указан') }}

Размеры: {{ product.get('size', 'Не указан') }}

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

Подкатегория: {{ product.get('subcategory', 'Не указана') }}

Цена: {{ product['price']|int }} сом

Описание: {{ product['description'] }}

Цвета: {{ product.get('colors', ['Нет цветов'])|join(', ') }}

{% if product.get('photos') and product['photos']|length > 0 %}
{% for photo in product['photos'] %} {{ product['name'] }} {% endfor %}
{% endif %}
Редактировать
{% for color in product.get('colors', []) %}
{% endfor %}
{% endfor %}

Список заказов

{% for order in orders %}

Дата: {{ order['timestamp'] }}

Итого: {{ order['total'] }} сом

{% for item in order['items'] %}

{{ item['name'] }} - {{ item['price'] }} сом × {{ item['quantity'] }} (Цвет: {{ item['color'] }})

{% endfor %}
{% endfor %}
''' @app.route('/backup', methods=['POST']) def backup(): upload_db_to_hf() return "Резервная копия создана.", 200 @app.route('/download', methods=['GET']) def download(): download_db_from_hf() return "База данных скачана.", 200 if __name__ == '__main__': backup_thread = threading.Thread(target=periodic_backup) backup_thread.daemon = True backup_thread.start() app.run(debug=True, host='0.0.0.0', port=7860)