diff --git "a/app.py" "b/app.py" deleted file mode 100644--- "a/app.py" +++ /dev/null @@ -1,4390 +0,0 @@ -import os -import io -import base64 -import json -import logging -import threading -import time -import math -from datetime import datetime, timedelta, timezone -from uuid import uuid4 -import random -import string -from flask import Flask, render_template_string, request, redirect, url_for, flash, jsonify, session -from PIL import Image -import google.generativeai as genai -import numpy as np -from huggingface_hub import HfApi, hf_hub_download -from huggingface_hub.utils import RepositoryNotFoundError, HfHubHTTPError -from werkzeug.utils import secure_filename -from dotenv import load_dotenv -import requests - -load_dotenv() - -db_lock = threading.RLock() - -app = Flask(__name__) -app.secret_key = 'your_unique_secret_key_gippo_312_shop_54321_no_login' -DATA_FILE = 'data.json' -SYNC_FILES =[DATA_FILE] -REPO_ID = "Kgshop/metas" -HF_TOKEN_WRITE = os.getenv("HF_TOKEN") -HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") -GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY") - -DOWNLOAD_RETRIES = 3 -DOWNLOAD_DELAY = 5 -ALMATY_TZ = timezone(timedelta(hours=6)) - -CURRENCIES = { - 'KGS': 'Кыргызский сом', - 'KZT': 'Казахстанский тенге', - 'UAH': 'Украинская гривна', - 'RUB': 'Российский рубль', - 'USD': 'Доллар США', - 'EUR': 'Евро' -} - -COLOR_SCHEMES = { - 'default': 'Бирюзовый (по умолч.)', - 'forest': 'Лесной зеленый', - 'ocean': 'Глубокий синий', - 'sunset': 'Закатный оранжевый', - 'lavender': 'Лавандовый', - 'vintage': 'Винтажный', - 'dark': 'Полночь (тёмная)', - 'cosmic': 'Космическая ночь', - 'minty': 'Свежая мята', - 'mocha': 'Кофейный мокко', - 'crimson': 'Багровый рассвет', - 'solar': 'Солнечная вспышка', - 'cyberpunk': 'Киберпанк неон', - 'neon': 'Неоновая вспышка', - 'pastel': 'Пастельный (светлый)', - 'emerald': 'Изумрудный город', - 'gold': 'Роскошное золото', - 'sakura': 'Цветение сакуры (светлый)', - 'arctic': 'Арктический лед (светлый)', - 'volcano': 'Магма', - 'monochrome_light': 'Классика (Светлая)', - 'monochrome_dark': 'Классика (Темная)', - 'nord': 'Скандинавский Норд', - 'dracula': 'Дракула (Темный)', - 'ruby': 'Глубокий Рубин', - 'sapphire': 'Королевский Сапфир', - 'amethyst': 'Аметистовый блеск' -} - -logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') - -def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWNLOAD_DELAY): - if not HF_TOKEN_READ and not HF_TOKEN_WRITE: - pass - token_to_use = HF_TOKEN_READ if HF_TOKEN_READ else HF_TOKEN_WRITE - files_to_download = [specific_file] if specific_file else SYNC_FILES - all_successful = True - for file_name in files_to_download: - success = False - for attempt in range(retries + 1): - try: - local_path = hf_hub_download( - repo_id=REPO_ID, - filename=file_name, - repo_type="dataset", - token=token_to_use, - local_dir=".", - local_dir_use_symlinks=False, - force_download=True, - resume_download=False - ) - success = True - break - except RepositoryNotFoundError: - return False - except HfHubHTTPError as e: - if e.response.status_code == 404: - if attempt == 0 and not os.path.exists(file_name): - try: - if file_name == DATA_FILE: - with open(file_name, 'w', encoding='utf-8') as f: - json.dump({}, f) - except Exception: - pass - success = False - break - except requests.exceptions.RequestException: - pass - except Exception: - pass - if attempt < retries: - time.sleep(delay) - if not success: - all_successful = False - return all_successful - -def upload_db_to_hf(specific_file=None): - if not HF_TOKEN_WRITE: - return - try: - api = HfApi() - files_to_upload = [specific_file] if specific_file else SYNC_FILES - for file_name in files_to_upload: - if os.path.exists(file_name): - try: - api.upload_file( - path_or_fileobj=file_name, - path_in_repo=file_name, - repo_id=REPO_ID, - repo_type="dataset", - token=HF_TOKEN_WRITE, - commit_message=f"Sync {file_name} {datetime.now(ALMATY_TZ).strftime('%Y-%m-%d %H:%M:%S')}" - ) - except Exception: - pass - except Exception: - pass - -def periodic_backup(): - backup_interval = 1800 - while True: - time.sleep(backup_interval) - upload_db_to_hf() - -def load_data(): - with db_lock: - try: - with open(DATA_FILE, 'r', encoding='utf-8') as f: - data = json.load(f) - if not isinstance(data, dict): - data = {} - except (FileNotFoundError, json.JSONDecodeError): - if download_db_from_hf(specific_file=DATA_FILE): - try: - with open(DATA_FILE, 'r', encoding='utf-8') as f: - data = json.load(f) - if not isinstance(data, dict): - data = {} - except (FileNotFoundError, json.JSONDecodeError): - data = {} - else: - data = {} - return data - -def save_data(data): - with db_lock: - try: - with open(DATA_FILE, 'w', encoding='utf-8') as file: - json.dump(data, file, ensure_ascii=False, indent=4) - except Exception: - pass - threading.Thread(target=upload_db_to_hf, kwargs={'specific_file': DATA_FILE}, daemon=True).start() - -def get_env_data(env_id): - with db_lock: - all_data = load_data() - default_organization_info = { - "about_us": "Мы — надежный партнер в мире уникальных товаров.", - "shipping": "Доставка осуществляется по всему Кыргызстану.", - "returns": "Возврат и обмен товара возможен в течение 14 дней.", - "contact": "Наш магазин находится по адресу: Рынок Кербен. Мы работаем ежедневно с 9:00 до 18:00." - } - default_settings = { - "organization_name": "Gippo312", - "whatsapp_number": "+996701202013", - "currency_code": "KGS", - "chat_name": "EVA", - "chat_avatar": None, - "color_scheme": "default", - "business_type": "retail", - "env_mode": "external", - "welcome_message_enabled": False, - "welcome_message_text": "Добро пожаловать в наш магазин!", - "inventory_tracking": False, - "admin_password_enabled": False, - "admin_password": "", - "checkout_fields_enabled": False, - "checkout_fields": {"name": False, "phone": False, "city": False, "address": False, "zip": False}, - "categories_as_lines": False - } - - env_data = all_data.get(env_id, {}) - if not env_data: - env_data = { - 'products':[], 'categories':[], 'orders': {}, 'employees':[], 'blocks':[], - 'organization_info': default_organization_info, - 'settings': default_settings, - 'inventory_history':[] - } - - if 'products' not in env_data: env_data['products'] =[] - if 'categories' not in env_data: env_data['categories'] =[] - if 'orders' not in env_data: env_data['orders'] = {} - if 'organization_info' not in env_data: env_data['organization_info'] = default_organization_info - if 'settings' not in env_data: env_data['settings'] = default_settings - if 'employees' not in env_data: env_data['employees'] =[] - if 'blocks' not in env_data: env_data['blocks'] =[] - if 'inventory_history' not in env_data: env_data['inventory_history'] =[] - - settings_changed = False - for key, value in default_settings.items(): - if key not in env_data['settings']: - env_data['settings'][key] = value - settings_changed = True - - products_changed = False - for product in env_data['products']: - if 'product_id' not in product: - product['product_id'] = uuid4().hex - products_changed = True - if 'views' not in product: - product['views'] = 0 - products_changed = True - if 'tags' not in product: - product['tags'] =[] - products_changed = True - else: - for tag in product['tags']: - if 'stock' not in tag: - tag['stock'] = 0 - products_changed = True - if 'stock_batches' not in tag: - tag['stock_batches'] = [{"qty": tag['stock'], "price": tag.get('price', 0), "box_price": tag.get('box_price', 0)}] - products_changed = True - - if products_changed or settings_changed: - all_data[env_id] = env_data - save_data(all_data) - - return env_data - -def save_env_data(env_id, env_data): - with db_lock: - all_data = load_data() - all_data[env_id] = env_data - save_data(all_data) - -def configure_gemini(): - if not GOOGLE_API_KEY: - return False - try: - genai.configure(api_key=GOOGLE_API_KEY) - return True - except Exception: - return False - -def generate_ai_description_from_image(image_data, language): - if not configure_gemini(): - raise ValueError("Google AI API не настроен.") - try: - if not image_data: - raise ValueError("Файл изображения не найден.") - image_stream = io.BytesIO(image_data) - image = Image.open(image_stream).convert('RGB') - except Exception: - raise ValueError("Не удалось обработать изображение.") - base_prompt = "Напиши большой и красивый, содержательный рекламный пост минимум на 1000 символов со смайликами и 25 тематических хэштегов с ключевыми словами разных вариантов, чтобы мои клиенты могли найти меня в поиске Instagram, Google и т.д. Пост пиши исключительно под товар, который на фото, без адресов и номеров телефона." - lang_suffix = "" - if language == "Русский": - lang_suffix = " Пиши на русском языке." - elif language == "Кыргызский": - lang_suffix = " Пиши на кыргызском языке." - elif language == "Казахский": - lang_suffix = " Пиши на казахском языке." - elif language == "Узбекский": - lang_suffix = " Пиши на узбекском языке." - final_prompt = f"{base_prompt}{lang_suffix}" - try: - model = genai.GenerativeModel('gemma-3-27b-it') - response = model.generate_content([final_prompt, image]) - if hasattr(response, 'text'): - return response.text - else: - if response.parts: - return "".join(part.text for part in response.parts if hasattr(part, 'text')) - else: - response.resolve() - return response.text - except Exception as e: - raise ValueError(f"Ошибка при генерации контента: {e}") - -LANDING_PAGE_TEMPLATE = ''' - - - - - - MetaStore - AI система для Вашего Бизнеса - - - - - - -''' - -LOGIN_TEMPLATE = ''' - - - - - - Вход в Админ-панель - - - - -
-

Вход

- {% with messages = get_flashed_messages(with_categories=true) %} - {% if messages %} - {% for category, message in messages %} -
{{ message }}
- {% endfor %} - {% endif %} - {% endwith %} -
- - -
-
- - -''' - -ADMHOSTO_TEMPLATE = ''' - - - - - - Главная Админ-панель - - - - - -
-

Управление Средами

- {% with messages = get_flashed_messages(with_categories=true) %} - {% if messages %} - {% for category, message in messages %} -
{{ message }}
- {% endfor %} - {% endif %} - {% endwith %} -
-
- - -
-
-
- -
-
-

Существующие среды

- {% if environments %} - - {% else %} -

Пока не создано ни одной среды.

- {% endif %} -
-
- - - -''' - -CATALOG_TEMPLATE = ''' - - - - - - {{ settings.organization_name }} - Каталог - - - - - - -
-
- -
- - -
-
- - {% if blocks %} -
- {% for block in blocks %} - {% if block.type == 'link' %} - {{ block.title }} - {% elif block.type == 'text' %} -
- {% if block.title %}

{{ block.title }}

{% endif %} -

{{ block.content|replace('\\n', '
')|safe }}

-
- {% endif %} - {% endfor %} -
- {% endif %} - -
-
-
- -
-
- - - -
- -
-
- - - - - -''' - -PRODUCT_DETAIL_TEMPLATE = ''' -
-

{{ product['name'] }}

-
-
- {% if product.get('photos') and product['photos']|length > 0 %} - {% for photo in product['photos'] %} - {% set photo_idx = loop.index0 %} -
-
- {{ product['name'] }} - {% for tag in product.tags %} - {% if tag.photo_index == photo_idx %} -
- {% endif %} - {% endfor %} -
-
- {% endfor %} - {% else %} -
- Изображение отсутствует -
- {% endif %} -
- {% if product.get('photos') and product['photos']|length > 1 %} -
-
-
- {% endif %} -
-
-
-

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

-

Описание:
{{ product.get('description', 'Описание отсутствует.')|replace('\\n', '
')|safe }}

-
-
-''' - -ORDER_TEMPLATE = ''' - - - - - - Заказ №{{ order.id }} - {{ settings.organization_name }} - - - - - -
- {% if order %} -

Ваш Заказ №{{ order.id }}

-

Дата создания: {{ order.created_at }}

- - {% if order.customer_data %} -
-

Данные клиента

- {% if order.customer_data.name %}

Имя: {{ order.customer_data.name }}

{% endif %} - {% if order.customer_data.phone %}

Телефон: {{ order.customer_data.phone }}

{% endif %} - {% if order.customer_data.city %}

Город: {{ order.customer_data.city }}

{% endif %} - {% if order.customer_data.address %}

Адрес: {{ order.customer_data.address }}

{% endif %} - {% if order.customer_data.zip %}

Индекс: {{ order.customer_data.zip }}

{% endif %} -
- {% endif %} - -

Товары в заказе

-
- {% for item in order.cart %} -
- {{ item.name }} -
- {{ item.name }} - {{ "%.2f"|format(item.price) }} {{ currency_code }} - {% if item.discount_applied %} - (Оптовая цена) - {% endif %} -
-
-
- - - - -
-
{{ "%.2f"|format(item.price * item.quantity) }} {{ currency_code }}
-
-
- {% endfor %} -
-

ИТОГО К ОПЛАТЕ: {{ "%.2f"|format(order.total_price) }} {{ currency_code }}

-
-

Статус заказа

- {% if order.employee_name %}

Ваш персональный менеджер: {{ order.employee_name }}

{% endif %} -

Пожалуйста, свяжитесь с нами по WhatsApp для подтверждения и уточнения деталей.

-
-
- - - -
- ← Вернуться в каталог - -
-

Заказ №{{ order.id }}

-

Дата: {{ order.created_at }}

- - - - - - - - - - - - {% for item in order.cart %} - - - - - - - - {% endfor %} - - - - - - - -
НаименованиеКол-воЦенаСумма
{{ loop.index }}{{ item.name }}{{ item.quantity }}{{ "%.2f"|format(item.price) }}{{ "%.2f"|format(item.price * item.quantity) }}
ИТОГО:{{ "%.2f"|format(order.total_price) }}
-
- - - - - {% else %} -

Ошибка

-

Заказ с таким ID не найден.

- ← Вернуться в каталог - {% endif %} -
- - -''' - -HISTORY_TEMPLATE = ''' - - - - - - История Продаж - {{ settings.organization_name }} - - - - - -
- Назад в админ-панель -

История Продаж

-
- - -
-
- - - - - - - - - - - - - {% for order in orders %} - - - - - - - - - {% endfor %} - -
ID Заказа Дата Сотрудник Источник Сумма Действия
{{ order.id }}{{ order.created_at }}{{ order.employee_name if order.employee_name else 'Прямой заказ' }}{{ 'Касса (POS)' if order.source == 'pos' else 'Каталог' }}{{ "%.2f"|format(order.total_price) }} {{ currency_code }} - -
- -
-
-
-
- - - -''' - -POS_TEMPLATE = ''' - - - - - - POS Касса - {{ settings.organization_name }} - - - - - -
- - -
- -
- -
- -
-
- Текущий заказ - -
-
-
- -
- Итого: - 0.00 {{ currency_code }} -
-
- - -
- -
-
- -
-
-
- Товар - -
-
-
-
- - - -
-
- -
-
- - - - -''' - -REPORTS_TEMPLATE = ''' - - - - - - Отчеты - {{ settings.organization_name }} - - - - - -
- Назад в админ-панель -

Отчеты (Режим 2 в 1)

- -
- - - - - -
- -
-
-

Всего заказов

-
{{ total_orders }}
-
-
-

Общая выручка

-
{{ "%.2f"|format(total_revenue) }} {{ currency_code }}
-
-
-

Заказы с кассы (POS)

-
{{ pos_orders }}
-
-
-

Онлайн заказы

-
{{ online_orders }}
-
-
- -

Продажи по сотрудникам

- - - {% for emp, data in emp_stats.items() %} - - - - - - {% endfor %} -
СотрудникКол-во заказовВыручка
{{ emp }}{{ data.count }}{{ "%.2f"|format(data.revenue) }} {{ currency_code }}
- -

Топ продаваемых товаров

- - - {% for item in top_products %} - - - - - {% endfor %} -
Название товараПродано шт.
{{ item.name }}{{ item.qty }}
-
- - -''' - -INVENTORY_TEMPLATE = ''' - - - - - - Остатки - {{ settings.organization_name }} - - - - - -
- Назад в админ-панель -

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

- -
- - -
- - - -
- - - - - - - - - - - - {% for item in items %} - - - - - - - - {% endfor %} - -
ТоварВариантОстатокЦена шт.Действия
{{ item.product_name }}{{ item.tag_name }}{{ item.stock }}{{ item.price }} {{ currency_code }} - - - -
-
-
- - - - - - - - - - -''' - -ADMIN_TEMPLATE = ''' - - - - - - Админ-панель - {{ settings.organization_name }} - - - - - -
-
-

Сохранение данных...

-

Пожалуйста, подождите, идет обработка и загрузка.

-
-
-
-
- Logo -

Админ-панель {{ settings.organization_name }} (Среда: {{ env_id }})

-
-
- История продаж - {% if settings.env_mode == '2in1' %} - - Остатки - {% if low_stock_count > 0 %}{{ low_stock_count }}{% endif %} - - Отчеты - {% endif %} - Каталог - {% if settings.admin_password_enabled %} - Выход - {% endif %} -
-
- - {% with messages = get_flashed_messages(with_categories=true) %} - {% if messages %} - {% for category, message in messages %} -
{{ message }}
- {% endfor %} - {% endif %} - {% endwith %} - -
-

Сотрудники (Менеджеры)

-
- Добавить сотрудника -
-
- - - - - - -
-
-
- {% if employees %} -
- {% for emp in employees %} -
-
{{ emp.name }}
{{ emp.whatsapp }}
-
- - {% if settings.env_mode == '2in1' %} - Ссылка на кассу - {% endif %} -
- - - -
-
-
- {% endfor %} -
- {% else %} -

Сотрудники не добавлены.

- {% endif %} -
- -
-

Блоки в каталоге (Ссылки и Инфо)

-
- Добавить блок -
-
- - - - - -
- - -
- - -
-
-
-
- {% if blocks %} - {% for block in blocks %} -
-
- {{ block.title }} ({{ 'Ссылка' if block.type == 'link' else 'Текст' }}) - {% if block.type == 'link' %}
{{ block.url }}{% endif %} -
-
-
- - - -
-
- - - -
-
- - - -
-
-
- {% endfor %} - {% else %} -

Блоки не добавлены.

- {% endif %} -
- -
- -
-
- Настройки магазина -
-
- - -
-

Доступ к админ-панели

- - - -
- - - - - - - - - - - - - Влияет на выбор упаковка/штучно при добавлении товара. - -
-

Данные клиента при заказе

- -
- - - - - -
-
- - - - {% if settings.chat_avatar %} -

Текущий аватар:

- {% endif %} - - - - -
-

Внешний вид каталога

- -
- - -
-
-
-
- -
-
-
-

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

-
- Добавить новую категорию -
-
- - - - -
-
-
-

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

- {% if categories %} -
- {% for category in categories %} -
- {{ category }} -
- - - -
-
- {% endfor %} -
- {% else %} -

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

- {% endif %} -
-
- -
-
-

Информация о магазине

-
- Развернуть/Свернуть -
-
- - - - - - -
-
-
-
-
-
- -
-

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

-
- Добавить новый товар -
-
- - - - - - - -
-

Отметки товаров на фото

-

Загрузите фото, выберите миниатюру и кликните по большому фото, чтобы отметить вариант товара.

-
- -
- -
-
-
-
- - - -
- - -
- - - - -
- -
-
- -
-
- -
-
-
- -

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

-
- - - {% if search_q %} - Сброс - {% endif %} -
- - {% if paginated_products %} -
- {% for product in paginated_products %} -
-
-
- {% if product.get('photos') %} - Фото - {% else %} - Нет фото - {% endif %} -
-
-

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

-

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

- {% set min_price = 0 %} - {% if product.tags %} - {% if settings.business_type == 'wholesale' %} - {% set prices = product.tags | map(attribute='box_price') | list %} - {% else %} - {% set prices = product.tags | map(attribute='price') | list %} - {% endif %} - {% if prices %} - {% set min_price = prices | min %} - {% endif %} - {% endif %} -

Цена от: {% if min_price > 0 %}{{ "%.2f"|format(min_price) }} {{ currency_code }}{% else %}Не указана{% endif %}

- {% if product.get('tags') %} -

Отметок: {{ product.tags|length }}

- {% endif %} -

Просмотров: {{ product.get('views', 0) }}

-

ID: {{ product.get('product_id', '') }}

-
-
- -
- -
- - - -
-
- -
-

Редактирование

-
- - - - - - - - -
-

Отметки товаров на фото

-
- -
- -
-
-
-
- - - - - - - -
- - -
-
- - -
-
- -
-
-
- {% endfor %} -
- - {% if total_pages > 1 %} - - {% endif %} - - {% else %} -

Товаров пока нет или по вашему запросу ничего не найдено.

- {% endif %} -
-
- -
-
- AI Аналитик - -
- -
- - - - -''' - -write_queue = [] -write_queue_lock = threading.Lock() -write_queue_event = threading.Event() - -def write_worker(): - while True: - write_queue_event.wait() - write_queue_event.clear() - while True: - with write_queue_lock: - if not write_queue: - break - task = write_queue.pop(0) - try: - task() - except Exception as e: - logging.error(f"Write worker error: {e}") - -write_worker_thread = threading.Thread(target=write_worker, daemon=True) -write_worker_thread.start() - -def enqueue_save(data_snapshot): - def task(): - with db_lock: - try: - with open(DATA_FILE, 'w', encoding='utf-8') as file: - json.dump(data_snapshot, file, ensure_ascii=False, indent=4) - except Exception as e: - logging.error(f"Enqueue save error: {e}") - threading.Thread(target=upload_db_to_hf, kwargs={'specific_file': DATA_FILE}, daemon=True).start() - with write_queue_lock: - write_queue.append(task) - write_queue_event.set() - -@app.route('/') -def index(): - return render_template_string(LANDING_PAGE_TEMPLATE) - -@app.route('/admhosto', methods=['GET']) -def admhosto(): - data = load_data() - environments_data = [] - for env_id, env_data in data.items(): - settings = env_data.get('settings', {}) - org_name = settings.get("organization_name", "Gippo312") - env_mode = settings.get("env_mode", "external") - environments_data.append({ - "id": env_id, - "org_name": org_name, - "mode": env_mode, - "pwd_enabled": settings.get("admin_password_enabled", False), - "password": settings.get("admin_password", "") - }) - environments_data.sort(key=lambda x: x['id']) - return render_template_string(ADMHOSTO_TEMPLATE, environments=environments_data) - -@app.route('/admhosto/create', methods=['POST']) -def create_environment(): - all_data = load_data() - env_mode = request.form.get('env_mode', 'external') - while True: - new_id = ''.join(random.choices(string.digits, k=6)) - if new_id not in all_data: - break - all_data[new_id] = { - 'products': [], 'categories': [], 'orders': {}, 'employees': [], 'blocks': [], - 'organization_info': { - "about_us": "Мы — Gippo312, ваш надежный партнер в мире уникальных товаров.", - "shipping": "Доставка осуществляется по всему Кыргызстану.", - "returns": "Возврат и обмен товара возможен в течение 14 дней.", - "contact": "Наш магазин находится по адресу: ... Связаться с нами можно по телефону ..." - }, - 'settings': { - "organization_name": "Gippo312", "whatsapp_number": "+996701202013", - "currency_code": "KGS", "chat_name": "EVA", "chat_avatar": None, - "color_scheme": "default", - "business_type": "retail", "env_mode": env_mode, "welcome_message_enabled": False, - "welcome_message_text": "Добро пожаловать в наш магазин!", - "inventory_tracking": False, "admin_password_enabled": False, "admin_password": "", - "checkout_fields_enabled": False, - "checkout_fields": {"name": False, "phone": False, "city": False, "address": False, "zip": False}, - "categories_as_lines": False - }, - 'inventory_history': [] - } - enqueue_save(all_data) - flash(f'Новая среда с ID {new_id} успешно создана.', 'success') - return redirect(url_for('admhosto')) - -@app.route('/admhosto/update_mode/', methods=['POST']) -def update_env_mode(env_id): - all_data = load_data() - if env_id in all_data: - new_mode = request.form.get('env_mode', 'external') - all_data[env_id]['settings']['env_mode'] = new_mode - enqueue_save(all_data) - flash(f'Режим среды {env_id} обновлен.', 'success') - else: - flash(f'Среда {env_id} не найдена.', 'error') - return redirect(url_for('admhosto')) - -@app.route('/admhosto/update_pwd/', methods=['POST']) -def update_env_pwd(env_id): - all_data = load_data() - if env_id in all_data: - pwd_enabled = 'pwd_enabled' in request.form - password = request.form.get('password', '').strip() - all_data[env_id]['settings']['admin_password_enabled'] = pwd_enabled - all_data[env_id]['settings']['admin_password'] = password - enqueue_save(all_data) - flash(f'Пароль для среды {env_id} обновлен.', 'success') - else: - flash(f'Среда {env_id} не найдена.', 'error') - return redirect(url_for('admhosto')) - -@app.route('/admhosto/delete/', methods=['POST']) -def delete_environment(env_id): - all_data = load_data() - if env_id in all_data: - del all_data[env_id] - enqueue_save(all_data) - flash(f'Среда {env_id} была удалена.', 'success') - else: - flash(f'Среда {env_id} н�� найдена.', 'error') - return redirect(url_for('admhosto')) - -@app.route('//login', methods=['GET', 'POST']) -def admin_login(env_id): - data = get_env_data(env_id) - settings = data.get('settings', {}) - if not settings.get('admin_password_enabled'): - return redirect(url_for('admin', env_id=env_id)) - if request.method == 'POST': - pwd = request.form.get('password', '') - if pwd == settings.get('admin_password', ''): - session[f'admin_auth_{env_id}'] = True - return redirect(url_for('admin', env_id=env_id)) - else: - flash('Неверный пароль', 'error') - return render_template_string(LOGIN_TEMPLATE, env_id=env_id) - -@app.route('//logout') -def admin_logout(env_id): - session.pop(f'admin_auth_{env_id}', None) - return redirect(url_for('admin_login', env_id=env_id)) - -def update_tag_price_from_batches(tag): - if 'stock_batches' in tag: - for batch in tag['stock_batches']: - if batch.get('qty', 0) > 0: - tag['price'] = batch.get('price', tag.get('price')) - tag['box_price'] = batch.get('box_price', tag.get('box_price')) - break - -@app.route('//catalog') -def catalog(env_id): - data = get_env_data(env_id) - all_products_raw = data.get('products', []) - settings = data.get('settings', {}) - blocks = data.get('blocks', []) - env_mode = settings.get('env_mode', 'external') - - product_categories = set(p.get('category', 'Без категории') for p in all_products_raw) - admin_categories = set(data.get('categories', [])) - all_cat_names = sorted(list(product_categories.union(admin_categories))) - - products_in_stock = [] - for p in all_products_raw: - if not p.get('in_stock', True): - continue - if env_mode == '2in1': - valid_tags = [t for t in p.get('tags', []) if t.get('stock', 0) > 0] - if not valid_tags and p.get('tags', []): - continue - p_copy = p.copy() - p_copy['tags'] = valid_tags - products_in_stock.append(p_copy) - else: - products_in_stock.append(p) - - products_sorted_for_js = sorted(products_in_stock, key=lambda p: (not p.get('is_top', False), p.get('name', '').lower())) - - products_by_category = {cat: [] for cat in all_cat_names} - for product in products_in_stock: - products_by_category[product.get('category', 'Без категории')].append(product) - ordered_categories = [cat for cat in all_cat_names if products_by_category.get(cat)] - - chat_avatar_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/avatars/{settings['chat_avatar']}" if settings.get('chat_avatar') else "https://huggingface.co/spaces/gippo312/admin/resolve/main/Picsart_25-11-04_12-02-21-390.png" - return render_template_string( - CATALOG_TEMPLATE, ordered_categories=ordered_categories, - products_json=json.dumps(products_sorted_for_js), repo_id=REPO_ID, - currency_code=settings.get('currency_code', 'KGS'), - settings=settings, chat_avatar_url=chat_avatar_url, env_id=env_id, blocks=blocks - ) - -@app.route('//pos') -def pos_page(env_id): - data = get_env_data(env_id) - settings = data.get('settings', {}) - if settings.get('env_mode') != '2in1': - return "POS доступен только в режиме '2 в 1'", 403 - emp_id = request.args.get('emp', '') - all_products_raw = data.get('products', []) - - products_in_stock = [] - for p in all_products_raw: - if not p.get('in_stock', True): - continue - valid_tags = [t for t in p.get('tags', []) if t.get('stock', 0) > 0] - if not valid_tags and p.get('tags', []): - continue - p_copy = p.copy() - p_copy['tags'] = valid_tags - products_in_stock.append(p_copy) - - return render_template_string(POS_TEMPLATE, products_json=json.dumps(products_in_stock), settings=settings, currency_code=settings.get('currency_code', 'KGS'), env_id=env_id, emp_id=emp_id, repo_id=REPO_ID) - -@app.route('//inventory') -def inventory_page(env_id): - data = get_env_data(env_id) - settings = data.get('settings', {}) - if settings.get('env_mode') != '2in1': - return "Остатки доступны только в режиме '2 в 1'", 403 - if settings.get('admin_password_enabled') and not session.get(f'admin_auth_{env_id}'): - return redirect(url_for('admin_login', env_id=env_id)) - - products = data.get('products', []) - items = [] - low_stock_count = 0 - - for p in products: - for t in p.get('tags', []): - stock = t.get('stock', 0) - is_low = stock <= 50 - if is_low: low_stock_count += 1 - items.append({ - 'product_id': p.get('product_id'), - 'product_name': p.get('name'), - 'tag_id': t.get('id'), - 'tag_name': t.get('name'), - 'stock': stock, - 'price': t.get('price', 0), - 'is_low': is_low - }) - items.sort(key=lambda x: x['product_name']) - - return render_template_string( - INVENTORY_TEMPLATE, - env_id=env_id, settings=settings, currency_code=settings.get('currency_code', 'KGS'), - items=items, low_stock_count=low_stock_count - ) - -@app.route('//inventory_action', methods=['POST']) -def inventory_action(env_id): - data = get_env_data(env_id) - settings = data.get('settings', {}) - if settings.get('env_mode') != '2in1': - return jsonify({"error": "Только для режима 2 в 1"}), 403 - - req = request.get_json() - p_id = req.get('product_id') - t_id = req.get('tag_id') - action = req.get('action') - qty = int(req.get('qty', 0)) - - if qty <= 0: - return jsonify({"error": "Количество должно быть больше 0"}), 400 - - products = data.get('products', []) - product = next((p for p in products if p.get('product_id') == p_id), None) - if not product: return jsonify({"error": "Товар не найден"}), 404 - - tag = next((t for t in product.get('tags', []) if t.get('id') == t_id), None) - if not tag: return jsonify({"error": "Вариант не найден"}), 404 - - if 'stock_batches' not in tag: - tag['stock_batches'] = [{"qty": tag.get('stock', 0), "price": tag.get('price', 0), "box_price": tag.get('box_price', 0)}] - - history_entry = { - 'id': uuid4().hex, - 'product_id': p_id, - 'tag_id': t_id, - 'type': action, - 'qty': qty, - 'timestamp': datetime.now(ALMATY_TZ).strftime('%Y-%m-%d %H:%M:%S'), - 'details': '' - } - - if action == 'add': - new_price = req.get('new_price') - new_box_price = req.get('new_box_price') - if new_price is not None: - new_price = float(new_price) - new_box_price = float(new_box_price) if new_box_price else new_price * tag.get('box_qty', 1) - tag['stock_batches'].append({'qty': qty, 'price': new_price, 'box_price': new_box_price}) - history_entry['details'] = f'Оприходование (новая цена: {new_price})' - else: - if tag['stock_batches']: - tag['stock_batches'][-1]['qty'] += qty - else: - tag['stock_batches'].append({'qty': qty, 'price': tag.get('price', 0), 'box_price': tag.get('box_price', 0)}) - history_entry['details'] = 'Оприходование (старая цена)' - tag['stock'] = tag.get('stock', 0) + qty - update_tag_price_from_batches(tag) - - elif action == 'write_off': - if tag.get('stock', 0) < qty: - return jsonify({"error": "Недостаточно остатков для списания"}), 400 - remaining_to_deduct = qty - for batch in tag['stock_batches']: - if batch['qty'] > 0: - if batch['qty'] >= remaining_to_deduct: - batch['qty'] -= remaining_to_deduct - remaining_to_deduct = 0 - break - else: - remaining_to_deduct -= batch['qty'] - batch['qty'] = 0 - tag['stock'] -= qty - history_entry['details'] = 'Ручное списание' - update_tag_price_from_batches(tag) - else: - return jsonify({"error": "Неизвестное действие"}), 400 - - if 'inventory_history' not in data: - data['inventory_history'] = [] - data['inventory_history'].append(history_entry) - - all_data = load_data() - all_data[env_id] = data - enqueue_save(all_data) - return jsonify({"success": True}) - -@app.route('//inventory_history//') -def inventory_history(env_id, p_id, t_id): - data = get_env_data(env_id) - history = data.get('inventory_history', []) - item_history = [h for h in history if h.get('product_id') == p_id and h.get('tag_id') == t_id] - item_history.sort(key=lambda x: x['timestamp'], reverse=True) - return jsonify(item_history) - -@app.route('//reports') -def reports_page(env_id): - data = get_env_data(env_id) - settings = data.get('settings', {}) - if settings.get('env_mode') != '2in1': - return "Отчеты доступны только в режиме '2 в 1'", 403 - if settings.get('admin_password_enabled') and not session.get(f'admin_auth_{env_id}'): - return redirect(url_for('admin_login', env_id=env_id)) - - now = datetime.now(ALMATY_TZ) - default_start = now.replace(day=1).strftime('%Y-%m-%d') - default_end = now.strftime('%Y-%m-%d') - start_date = request.args.get('start_date', default_start) - end_date = request.args.get('end_date', default_end) - - orders = data.get('orders', {}).values() - filtered_orders = [] - for o in orders: - created_at = o.get('created_at', '') - if created_at: - date_part = created_at.split(' ')[0] - if start_date <= date_part <= end_date: - filtered_orders.append(o) - - total_orders = len(filtered_orders) - total_revenue = sum(o.get('total_price', 0) for o in filtered_orders) - pos_orders = sum(1 for o in filtered_orders if o.get('source') == 'pos') - online_orders = total_orders - pos_orders - - emp_stats = {} - product_sales = {} - for o in filtered_orders: - emp = o.get('employee_name') or 'Прямой заказ' - if emp not in emp_stats: - emp_stats[emp] = {'count': 0, 'revenue': 0} - emp_stats[emp]['count'] += 1 - emp_stats[emp]['revenue'] += o.get('total_price', 0) - for item in o.get('cart', []): - name = item.get('name', 'Неизвестно') - qty = item.get('quantity', 0) - if name not in product_sales: - product_sales[name] = 0 - product_sales[name] += qty - - top_products = [{'name': k, 'qty': v} for k, v in product_sales.items()] - top_products.sort(key=lambda x: x['qty'], reverse=True) - - return render_template_string( - REPORTS_TEMPLATE, env_id=env_id, settings=settings, currency_code=settings.get('currency_code', 'KGS'), - total_orders=total_orders, total_revenue=total_revenue, pos_orders=pos_orders, online_orders=online_orders, - emp_stats=emp_stats, top_products=top_products[:20], start_date=start_date, end_date=end_date - ) - -@app.route('//track_view/', methods=['POST']) -def track_view(env_id, product_id): - data = get_env_data(env_id) - for p in data['products']: - if p.get('product_id') == product_id: - p['views'] = p.get('views', 0) + 1 - break - all_data = load_data() - all_data[env_id] = data - enqueue_save(all_data) - return jsonify({"status": "ok"}) - -@app.route('//product/') -def product_detail(env_id, product_id): - data = get_env_data(env_id) - all_products_raw = data.get('products', []) - settings = data.get('settings', {}) - env_mode = settings.get('env_mode', 'external') - - product = None - for p in all_products_raw: - if p.get('product_id') == product_id: - if not p.get('in_stock', True): - return "Товар не в наличии.", 404 - if env_mode == '2in1': - valid_tags = [t for t in p.get('tags', []) if t.get('stock', 0) > 0] - if not valid_tags and p.get('tags', []): - return "Товар закончился.", 404 - p_copy = p.copy() - p_copy['tags'] = valid_tags - product = p_copy - else: - product = p - break - - if not product: - return "Товар не найден.", 404 - - return render_template_string( - PRODUCT_DETAIL_TEMPLATE, product=product, repo_id=REPO_ID, - currency_code=settings.get('currency_code', 'KGS'), settings=settings, env_id=env_id - ) - -@app.route('//create_order', methods=['POST']) -def create_order(env_id): - order_data = request.get_json() - if not order_data or 'cart' not in order_data or not order_data['cart']: - return jsonify({"error": "Корзина пуста или не передана."}), 400 - - data = get_env_data(env_id) - settings = data.get('settings', {}) - products = data.get('products', []) - env_mode = settings.get('env_mode', 'external') - - cart_items = order_data['cart'] - customer_data = order_data.get('customer_data', {}) - emp_id = order_data.get('emp_id') - source = order_data.get('source', 'catalog') - emp_name = None - emp_whatsapp = None - - if emp_id: - employees = data.get('employees', []) - for emp in employees: - if emp.get('id') == emp_id: - emp_name = emp.get('name') - emp_whatsapp = emp.get('whatsapp') - break - - total_price = 0 - processed_cart = [] - order_timestamp = datetime.now(ALMATY_TZ).strftime('%Y-%m-%d %H:%M:%S') - - for item in cart_items: - if not all(k in item for k in ('name', 'quantity')): - return jsonify({"error": "Неверный формат товара в корзине."}), 400 - try: - quantity = int(item['quantity']) - if quantity <= 0: - raise ValueError("Invalid quantity") - - p_id = item.get('product_id') - c_color = item.get('color', 'N/A') - tx = item.get('tag_x') - ty = item.get('tag_y') - u_type = item.get('unit_type', 'piece') - - product_ref = next((p for p in products if p.get('product_id') == p_id), None) - if not product_ref: - return jsonify({"error": f"Товар {p_id} не найден."}), 400 - - tag_ref = None - if 'TAG_' in c_color: - tag_id = c_color.split('_VAR_')[0].replace('TAG_', '') - tag_ref = next((t for t in product_ref.get('tags', []) if t.get('id') == tag_id), None) - elif item.get('id') and len(item['id'].split('-')) >= 2: - tag_id = item['id'].split('-')[1] - tag_ref = next((t for t in product_ref.get('tags', []) if t.get('id') == tag_id), None) - - price = float(item.get('price', 0)) - discount_applied = False - - if tag_ref: - orig_price = float(tag_ref.get('price', 0)) - box_price = float(tag_ref.get('box_price', orig_price)) - box_qty = int(tag_ref.get('box_qty', 1)) - - if u_type == 'piece' and box_qty > 1 and quantity >= box_qty: - price = box_price / box_qty - discount_applied = True - elif u_type == 'box': - price = box_price - else: - price = orig_price - - if env_mode == '2in1': - deduction = quantity - if u_type == 'box': - deduction = quantity * box_qty - if tag_ref.get('stock', 0) < deduction: - return jsonify({"error": f"Недостаточно остатков для товара {item['name']}."}), 400 - - if 'stock_batches' not in tag_ref: - tag_ref['stock_batches'] = [{"qty": tag_ref.get('stock', 0), "price": tag_ref.get('price', 0), "box_price": tag_ref.get('box_price', 0)}] - - remaining_to_deduct = deduction - for batch in tag_ref['stock_batches']: - if batch['qty'] > 0: - if batch['qty'] >= remaining_to_deduct: - batch['qty'] -= remaining_to_deduct - remaining_to_deduct = 0 - break - else: - remaining_to_deduct -= batch['qty'] - batch['qty'] = 0 - tag_ref['stock'] -= deduction - update_tag_price_from_batches(tag_ref) - - if 'inventory_history' not in data: - data['inventory_history'] = [] - data['inventory_history'].append({ - 'id': uuid4().hex, - 'product_id': p_id, - 'tag_id': tag_ref['id'], - 'type': 'sale', - 'qty': deduction, - 'timestamp': order_timestamp, - 'details': f"Продажа ({source})" - }) - - processed_cart.append({ - "product_id": p_id, "name": item['name'], "price": price, "quantity": quantity, - "color": c_color, "photo": item.get('photo'), "tag_x": tx, "tag_y": ty, "unit_type": u_type, - "discount_applied": discount_applied, - "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" - }) - total_price += price * quantity - except (ValueError, TypeError) as e: - return jsonify({"error": "Неверная цена или количество в товаре."}), 400 - - order_id = f"{datetime.now(ALMATY_TZ).strftime('%Y%m%d%H%M%S')}-{uuid4().hex[:6]}" - new_order = { - "id": order_id, "created_at": order_timestamp, "cart": processed_cart, - "total_price": round(total_price, 2), "status": "new", - "employee_id": emp_id, "employee_name": emp_name, "employee_whatsapp": emp_whatsapp, - "customer_data": customer_data, "source": source - } - - try: - if 'orders' not in data or not isinstance(data.get('orders'), dict): - data['orders'] = {} - data['orders'][order_id] = new_order - data['products'] = products - all_data = load_data() - all_data[env_id] = data - enqueue_save(all_data) - return jsonify({"order_id": order_id}), 201 - except Exception: - return jsonify({"error": "Ошибка сервера при сохранении заказа."}), 500 - -@app.route('//update_order/', methods=['POST']) -def update_order(env_id, order_id): - data = get_env_data(env_id) - order = data.get('orders', {}).get(order_id) - if not order: - return jsonify({"error": "Заказ не найден."}), 404 - - req = request.get_json() - idx = req.get('index') - action = req.get('action') - - if idx is None or action not in ['inc', 'dec', 'set', 'remove']: - return jsonify({"error": "Некорректный запрос."}), 400 - - try: - idx = int(idx) - cart = order.get('cart', []) - if idx < 0 or idx >= len(cart): - return jsonify({"error": "Товар не найден."}), 404 - - if action == 'inc': - cart[idx]['quantity'] += 1 - elif action == 'dec': - cart[idx]['quantity'] -= 1 - if cart[idx]['quantity'] <= 0: - cart.pop(idx) - elif action == 'set': - val = int(req.get('value', 1)) - if val <= 0: - cart.pop(idx) - else: - cart[idx]['quantity'] = val - elif action == 'remove': - cart.pop(idx) - - total = sum(float(item['price']) * int(item['quantity']) for item in cart) - order['total_price'] = round(total, 2) - order['cart'] = cart - - all_data = load_data() - all_data[env_id] = data - enqueue_save(all_data) - return jsonify({"success": True}) - except Exception as e: - return jsonify({"error": str(e)}), 500 - -@app.route('//delete_order/', methods=['POST']) -def delete_order(env_id, order_id): - data = get_env_data(env_id) - if 'orders' in data and order_id in data['orders']: - del data['orders'][order_id] - all_data = load_data() - all_data[env_id] = data - enqueue_save(all_data) - flash("Заказ успешно удален.", "success") - else: - flash("Заказ не найден.", "error") - return redirect(url_for('history_page', env_id=env_id)) - -@app.route('//order/') -def view_order(env_id, order_id): - data = get_env_data(env_id) - order = data.get('orders', {}).get(order_id) - settings = data.get('settings', {}) - return render_template_string(ORDER_TEMPLATE, order=order, repo_id=REPO_ID, currency_code=settings.get('currency_code', 'KGS'), settings=settings, env_id=env_id) - -@app.route('//history') -def history_page(env_id): - data = get_env_data(env_id) - settings = data.get('settings', {}) - if settings.get('admin_password_enabled') and not session.get(f'admin_auth_{env_id}'): - return redirect(url_for('admin_login', env_id=env_id)) - orders = list(data.get('orders', {}).values()) - orders.sort(key=lambda x: x.get('created_at', ''), reverse=True) - employees = data.get('employees', []) - return render_template_string(HISTORY_TEMPLATE, orders=orders, employees=employees, settings=settings, currency_code=settings.get('currency_code', 'KGS'), env_id=env_id) - -@app.route('//admin_ai_chat', methods=['POST']) -def admin_ai_chat(env_id): - data = get_env_data(env_id) - settings = data.get('settings', {}) - if settings.get('admin_password_enabled') and not session.get(f'admin_auth_{env_id}'): - return jsonify({"text": "Доступ запрещен."}) - if not configure_gemini(): - return jsonify({"text": "AI не настроен."}) - - req = request.get_json() - message = req.get('message') - history = req.get('history', []) - - orders = data.get('orders', {}) - products = data.get('products', []) - - now = datetime.now(ALMATY_TZ) - current_month = now.strftime('%Y-%m') - - monthly_revenue = 0 - product_sales_counts = {} - - for o in orders.values(): - if o.get('created_at', '').startswith(current_month): - monthly_revenue += o.get('total_price', 0) - for item in o.get('cart', []): - pid = item.get('product_id') - product_sales_counts[pid] = product_sales_counts.get(pid, 0) + item.get('quantity', 0) - - sorted_views = sorted(products, key=lambda x: x.get('views', 0), reverse=True)[:5] - views_str_list = [f"[POST: {p['product_id']} Название: {p['name']}] (просмотров: {p.get('views', 0)})" for p in sorted_views if p.get('views', 0) > 0] - views_str = ", ".join(views_str_list) if views_str_list else "Нет просмотров" - - sorted_sales_pids = sorted(product_sales_counts.items(), key=lambda x: x[1], reverse=True)[:5] - sales_str_list = [] - for pid, qty in sorted_sales_pids: - p = next((x for x in products if x['product_id'] == pid), None) - if p: - sales_str_list.append(f"[POST: {pid} Название: {p['name']}] (продано: {qty} шт)") - sales_str = ", ".join(sales_str_list) if sales_str_list else "Пока нет продаж" - - currency = data['settings'].get('currency_code', 'KGS') - - sys_prompt = f"""Ты — умный AI-ассистент администратора магазина. -Текущее время (Алматы): {now.strftime('%Y-%m-%d %H:%M:%S')}. -Выручка за этот месяц: {monthly_revenue} {currency}. -Самые просматриваемые товары (Топ-5): {views_str}. -Самые продаваемые товары (Топ-5): {sales_str}. -Если упоминаешь товар, используй точный формат:[POST: Название: ]. -Помогай владельцу анализировать продажи и отвечать на бизнес-вопросы.""" - - try: - model = genai.GenerativeModel('gemma-4-26b-a4b-it') - messages = [{'role': 'user', 'parts': [{'text': sys_prompt}]}] - for h in history: - messages.append({'role': 'model' if h['role'] == 'ai' else 'user', 'parts': [{'text': h['text']}]}) - chat = model.start_chat(history=messages) - resp = chat.send_message(message) - return jsonify({'text': resp.text}) - except Exception as e: - return jsonify({'text': f"Ошибка AI: {str(e)}"}) - -@app.route('//admin', methods=['GET', 'POST']) -def admin(env_id): - data = get_env_data(env_id) - settings = data.get('settings', {}) - - if settings.get('admin_password_enabled') and not session.get(f'admin_auth_{env_id}'): - return redirect(url_for('admin_login', env_id=env_id)) - - products = data.get('products', []) - categories = data.get('categories', []) - organization_info = data.get('organization_info', {}) - employees = data.get('employees', []) - blocks = data.get('blocks', []) - - page = request.args.get('p', 1, type=int) - search_q = request.args.get('q', '').strip() - - if 'orders' not in data or not isinstance(data.get('orders'), dict): - data['orders'] = {} - - if request.method == 'POST': - action = request.form.get('action') - try: - if action == 'add_block': - b_type = request.form.get('block_type') - b_title = request.form.get('block_title', '').strip() - b_url = request.form.get('block_url', '').strip() - if b_url and not b_url.startswith(('http://', 'https://')): - b_url = 'https://' + b_url - b_content = request.form.get('block_content', '').strip() - blocks.append({'id': uuid4().hex[:8], 'type': b_type, 'title': b_title, 'url': b_url, 'content': b_content}) - data['blocks'] = blocks - all_data = load_data() - all_data[env_id] = data - enqueue_save(all_data) - flash("Блок добавлен.", "success") - elif action == 'delete_block': - b_id = request.form.get('block_id') - data['blocks'] = [b for b in blocks if b.get('id') != b_id] - all_data = load_data() - all_data[env_id] = data - enqueue_save(all_data) - flash("Блок удален.", "success") - elif action == 'move_block_up': - b_id = request.form.get('block_id') - idx = next((i for i, b in enumerate(blocks) if b.get('id') == b_id), -1) - if idx > 0: - blocks[idx], blocks[idx-1] = blocks[idx-1], blocks[idx] - data['blocks'] = blocks - all_data = load_data() - all_data[env_id] = data - enqueue_save(all_data) - flash("Блок перемещен выше.", "success") - elif action == 'move_block_down': - b_id = request.form.get('block_id') - idx = next((i for i, b in enumerate(blocks) if b.get('id') == b_id), -1) - if idx != -1 and idx < len(blocks) - 1: - blocks[idx], blocks[idx+1] = blocks[idx+1], blocks[idx] - data['blocks'] = blocks - all_data = load_data() - all_data[env_id] = data - enqueue_save(all_data) - flash("Блок перемещен ниже.", "success") - elif action == 'add_employee': - emp_name = request.form.get('emp_name', '').strip() - emp_whatsapp = request.form.get('emp_whatsapp', '').strip() - if emp_name and emp_whatsapp: - emp_id = uuid4().hex[:8] - employees.append({'id': emp_id, 'name': emp_name, 'whatsapp': emp_whatsapp}) - data['employees'] = employees - all_data = load_data() - all_data[env_id] = data - enqueue_save(all_data) - flash("Сотрудник добавлен.", "success") - elif action == 'delete_employee': - emp_id = request.form.get('emp_id') - employees = [e for e in employees if e.get('id') != emp_id] - data['employees'] = employees - all_data = load_data() - all_data[env_id] = data - enqueue_save(all_data) - flash("Сотрудник удален.", "success") - elif action == 'add_category': - category_name = request.form.get('category_name', '').strip() - if category_name and category_name not in categories: - categories.append(category_name) - data['categories'] = categories - all_data = load_data() - all_data[env_id] = data - enqueue_save(all_data) - flash(f"Категория '{category_name}' успешно добавлена.", 'success') - elif not category_name: - flash("Название категории не может быть пустым.", 'error') - else: - flash(f"Категория '{category_name}' уже существует.", 'error') - elif action == 'delete_category': - category_to_delete = request.form.get('category_name') - if category_to_delete and category_to_delete in categories: - categories.remove(category_to_delete) - updated_count = 0 - for product in products: - if product.get('category') == category_to_delete: - product['category'] = 'Без категории' - updated_count += 1 - data['categories'] = categories - data['products'] = products - all_data = load_data() - all_data[env_id] = data - enqueue_save(all_data) - flash(f"Категория '{category_to_delete}' удалена. {updated_count} товаров обновлено.", 'success') - else: - flash(f"Не удалось удалить категорию '{category_to_delete}'.", 'error') - elif action == 'update_org_info': - organization_info['about_us'] = request.form.get('about_us', '').strip() - organization_info['shipping'] = request.form.get('shipping', '').strip() - organization_info['returns'] = request.form.get('returns', '').strip() - organization_info['contact'] = request.form.get('contact', '').strip() - data['organization_info'] = organization_info - all_data = load_data() - all_data[env_id] = data - enqueue_save(all_data) - flash("Информация о магазине успешно обновлена.", 'success') - elif action == 'update_settings': - settings['admin_password_enabled'] = 'admin_password_enabled' in request.form - settings['admin_password'] = request.form.get('admin_password', '').strip() - settings['organization_name'] = request.form.get('organization_name', 'Gippo312').strip() - settings['whatsapp_number'] = request.form.get('whatsapp_number', '').strip() - settings['currency_code'] = request.form.get('currency_code', 'KGS') - settings['business_type'] = request.form.get('business_type', 'retail') - settings['color_scheme'] = request.form.get('color_scheme', 'default') - settings['checkout_fields_enabled'] = 'checkout_fields_enabled' in request.form - settings['checkout_fields'] = { - 'name': 'cf_name' in request.form, - 'phone': 'cf_phone' in request.form, - 'city': 'cf_city' in request.form, - 'address': 'cf_address' in request.form, - 'zip': 'cf_zip' in request.form - } - settings['categories_as_lines'] = 'categories_as_lines' in request.form - - avatar_file = request.files.get('chat_avatar') - if avatar_file and avatar_file.filename: - if HF_TOKEN_WRITE: - try: - api = HfApi() - old_avatar = settings.get('chat_avatar') - if old_avatar: - try: api.delete_files(repo_id=REPO_ID, paths_in_repo=[f"avatars/{old_avatar}"], repo_type="dataset", token=HF_TOKEN_WRITE) - except Exception: pass - ext = os.path.splitext(avatar_file.filename)[1].lower() - avatar_filename = f"avatar_{env_id}_{int(time.time())}{ext}" - uploads_dir = 'uploads_temp' - os.makedirs(uploads_dir, exist_ok=True) - temp_path = os.path.join(uploads_dir, avatar_filename) - avatar_file.save(temp_path) - api.upload_file(path_or_fileobj=temp_path, path_in_repo=f"avatars/{avatar_filename}", repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE) - settings['chat_avatar'] = avatar_filename - os.remove(temp_path) - flash("Аватар успешно обновлен.", 'success') - except Exception as e: - flash(f"Ошибка при загрузке аватара: {e}", 'error') - else: - flash("HF_TOKEN (write) н�� настроен. Аватар не был загружен.", "warning") - - data['settings'] = settings - all_data = load_data() - all_data[env_id] = data - enqueue_save(all_data) - flash("Настройки успешно обновлены.", 'success') - elif action == 'add_product' or action == 'edit_product': - product_id = request.form.get('product_id') - product_data = {} - is_edit = action == 'edit_product' - - if is_edit: - product_data = next((p for p in products if p.get('product_id') == product_id), None) - if not product_data: - flash(f"Ошибка: товар с ID {product_id} не найден.", 'error') - return redirect(url_for('admin', env_id=env_id, p=page, q=search_q)) - else: - product_data['views'] = 0 - - product_data['name'] = request.form.get('name', '').strip() - product_data['description'] = request.form.get('description', '').strip() - category = request.form.get('category') - product_data['category'] = category if category in categories else 'Без категории' - - tags_raw = request.form.get('tags_json', '[]') - try: - parsed_tags = json.loads(tags_raw) - for t in parsed_tags: - if 'stock_batches' not in t: - t['stock_batches'] = [{"qty": t.get('stock', 0), "price": t.get('price', 0), "box_price": t.get('box_price', 0)}] - product_data['tags'] = parsed_tags - except: - product_data['tags'] = [] - - product_data['in_stock'] = 'in_stock' in request.form - product_data['is_top'] = 'is_top' in request.form - - if not product_data['name']: - flash("Название товара обязательно.", 'error') - return redirect(url_for('admin', env_id=env_id, p=page, q=search_q)) - - photos_files = request.files.getlist('photos') - if photos_files and any(f.filename for f in photos_files): - if HF_TOKEN_WRITE: - uploads_dir = 'uploads_temp' - os.makedirs(uploads_dir, exist_ok=True) - api = HfApi() - new_photos_list = [] - photo_limit = 10 - uploaded_count = 0 - for photo in photos_files: - if uploaded_count >= photo_limit: break - if photo and photo.filename: - try: - ext = os.path.splitext(photo.filename)[1].lower() - if ext not in ['.jpg', '.jpeg', '.png', '.gif', '.webp']: continue - safe_name = secure_filename(product_data['name'].replace(' ', '_'))[:50] - photo_filename = f"{safe_name}_{uuid4().hex[:8]}{ext}" - 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) - new_photos_list.append(photo_filename) - os.remove(temp_path) - uploaded_count += 1 - except Exception as e: - flash(f"Ошибка при загрузке фото {photo.filename}: {e}", 'error') - if new_photos_list and is_edit and product_data.get('photos'): - try: 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) - except Exception: pass - if new_photos_list: - product_data['photos'] = new_photos_list - else: - flash("HF_TOKEN не настроен. Фотографии не загружены.", "warning") - - if is_edit: - product_index = next((i for i, p in enumerate(products) if p.get('product_id') == product_id), -1) - if product_index != -1: - products[product_index] = product_data - flash(f"Товар '{product_data['name']}' обновлен.", 'success') - else: - product_data['product_id'] = uuid4().hex - products.append(product_data) - flash(f"Товар '{product_data['name']}' добавлен.", 'success') - - data['products'] = products - all_data = load_data() - all_data[env_id] = data - enqueue_save(all_data) - elif action == 'delete_product': - product_id = request.form.get('product_id') - product_index = next((i for i, p in enumerate(products) if p.get('product_id') == product_id), -1) - if product_index == -1: - flash(f"Ошибка удаления: товар не найден.", 'error') - return redirect(url_for('admin', env_id=env_id, p=page, q=search_q)) - deleted_product = products.pop(product_index) - product_name = deleted_product.get('name', 'N/A') - photos_to_delete = deleted_product.get('photos', []) - if photos_to_delete and HF_TOKEN_WRITE: - try: - api = HfApi() - api.delete_files(repo_id=REPO_ID, paths_in_repo=[f"photos/{p}" for p in photos_to_delete], repo_type="dataset", token=HF_TOKEN_WRITE) - except Exception: pass - data['products'] = products - all_data = load_data() - all_data[env_id] = data - enqueue_save(all_data) - flash(f"Товар '{product_name}' удален.", 'success') - else: - flash(f"Неизвестное действие: {action}", 'warning') - return redirect(url_for('admin', env_id=env_id, p=page, q=search_q)) - except Exception as e: - flash(f"Ошибка при выполнении действия.", 'error') - return redirect(url_for('admin', env_id=env_id, p=page, q=search_q)) - - filtered_products = products - if search_q: - q_lower = search_q.lower() - filtered_products = [p for p in products if q_lower in p.get('name', '').lower() or q_lower in p.get('description', '').lower()] - - filtered_products = sorted(filtered_products, key=lambda p: p.get('name', '').lower()) - - PER_PAGE = 20 - total_items = len(filtered_products) - total_pages = math.ceil(total_items / PER_PAGE) if total_items > 0 else 1 - - if page < 1: page = 1 - if page > total_pages: page = total_pages - - start_idx = (page - 1) * PER_PAGE - end_idx = start_idx + PER_PAGE - paginated_products = filtered_products[start_idx:end_idx] - - display_categories = sorted(categories) - chat_avatar_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/avatars/{settings['chat_avatar']}" if settings.get('chat_avatar') else "https://huggingface.co/spaces/gippo312/admin/resolve/main/Picsart_25-11-04_12-02-21-390.png" - - low_stock_count = 0 - if settings.get('env_mode') == '2in1': - for p in products: - for t in p.get('tags', []): - if t.get('stock', 0) <= 50: - low_stock_count += 1 - - return render_template_string( - ADMIN_TEMPLATE, paginated_products=paginated_products, total_pages=total_pages, page=page, - search_q=search_q, categories=display_categories, - organization_info=organization_info, chats={}, settings=settings, employees=employees, - blocks=blocks, repo_id=REPO_ID, currency_code=settings.get('currency_code', 'KGS'), - chat_avatar_url=chat_avatar_url, currencies=CURRENCIES, color_schemes=COLOR_SCHEMES, - env_id=env_id, chat_status={}, low_stock_count=low_stock_count - ) - -@app.route('/generate_description_ai', methods=['POST']) -def handle_generate_description_ai(): - request_data = request.get_json() - base64_image = request_data.get('image') - language = request_data.get('language', 'Русский') - if not base64_image: return jsonify({"error": "Изображение не найдено в запросе."}), 400 - try: - image_data = base64.b64decode(base64_image) - result_text = generate_ai_description_from_image(image_data, language) - return jsonify({"text": result_text}) - except ValueError as ve: return jsonify({"error": str(ve)}), 400 - except Exception as e: return jsonify({"error": f"Внутренняя ошибка сервера: {e}"}), 500 - -if __name__ == '__main__': - configure_gemini() - download_db_from_hf() - load_data() - if HF_TOKEN_WRITE: - backup_thread = threading.Thread(target=periodic_backup, daemon=True) - backup_thread.start() - port = int(os.environ.get('PORT', 7860)) - app.run(debug=False, host='0.0.0.0', port=port)