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'] }}](https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ photo }})
- {% 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 }}
- {{ "%.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 %}
-
- | {{ loop.index }} |
- {{ item.name }} |
- {{ item.quantity }} |
- {{ "%.2f"|format(item.price) }} |
- {{ "%.2f"|format(item.price * item.quantity) }} |
-
- {% endfor %}
-
-
-
- | ИТОГО: |
- {{ "%.2f"|format(order.total_price) }} |
-
-
-
-
-
-
-
-
×
-
-
![]()
-
-
-
-
-
-
- {% else %}
-
Ошибка
-
Заказ с таким ID не найден.
-
← Вернуться в каталог
- {% endif %}
-
-
-
-'''
-
-HISTORY_TEMPLATE = '''
-
-
-
-
-
- История Продаж - {{ settings.organization_name }}
-
-
-
-
-
-
-
Назад в админ-панель
-
История Продаж
-
-
-
-
-
-
-
-
- | ID Заказа |
- Дата |
- Сотрудник |
- Источник |
- Сумма |
- Действия |
-
-
-
- {% for order in orders %}
-
- | {{ 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 }} |
-
-
-
- |
-
- {% endfor %}
-
-
-
-
-
-
-
-'''
-
-POS_TEMPLATE = '''
-
-
-
-
-
- POS Касса - {{ settings.organization_name }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-'''
-
-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() %}
-
- | {{ emp }} |
- {{ data.count }} |
- {{ "%.2f"|format(data.revenue) }} {{ currency_code }} |
-
- {% endfor %}
-
-
-
Топ продаваемых товаров
-
- | Название товара | Продано шт. |
- {% for item in top_products %}
-
- | {{ item.name }} |
- {{ item.qty }} |
-
- {% endfor %}
-
-
-
-
-'''
-
-INVENTORY_TEMPLATE = '''
-
-
-
-
-
- Остатки - {{ settings.organization_name }}
-
-
-
-
-
-
-
Назад в админ-панель
-
Управление остатками
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- | Товар |
- Вариант |
- Остаток |
- Цена шт. |
- Действия |
-
-
-
- {% for item in items %}
-
- | {{ item.product_name }} |
- {{ item.tag_name }} |
- {{ item.stock }} |
- {{ item.price }} {{ currency_code }} |
-
-
-
-
- |
-
- {% endfor %}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-'''
-
-ADMIN_TEMPLATE = '''
-
-
-
-
-
- Админ-панель - {{ settings.organization_name }}
-
-
-
-
-
-
-
-
Сохранение данных...
-
Пожалуйста, подождите, идет обработка и загрузка.
-
-
-
-
- {% 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 categories %}
-
- {% for category in categories %}
-
- {{ category }}
-
-
- {% endfor %}
-
- {% else %}
-
Категорий пока нет.
- {% 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 %}
-
-
-
-
-
-
-
-
-'''
-
-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)