diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -14,7 +14,7 @@ import requests from io import BytesIO import uuid from functools import wraps -from urllib.parse import quote +from urllib.parse import quote, quote_plus import zipfile import tempfile import pytz @@ -27,7 +27,7 @@ DATA_FILE = 'cloudeng_data_tma.json' REPO_ID = "Eluza133/Z1e1u" HF_TOKEN_WRITE = os.getenv("HF_TOKEN") HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") or HF_TOKEN_WRITE -TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN", "6750208873:AAE2hvPlJ99dBdhGa_Brre0IIlUdOvXxHt4") +TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN", "6750208873:AAE2hvPlJ99dBdhGa_Brre0IIpUdOvXxHt4") ADMIN_TELEGRAM_ID = os.getenv("ADMIN_TELEGRAM_ID", "YOUR_ADMIN_TELEGRAM_USER_ID_HERE") ADMIN_USERNAME = os.getenv("ADMIN_USERNAME", "admin") ADMIN_PASSWORD = os.getenv("ADMIN_PASSWORD", "zeusadminpass") @@ -37,18 +37,6 @@ cache = Cache(app, config={'CACHE_TYPE': 'simple'}) logging.basicConfig(level=logging.INFO) save_data_lock = threading.Lock() -CURRENCIES = { - "KZT": "₸", - "RUB": "₽", - "KGS": "с", - "UZS": "сўм", - "UAH": "₴" -} - -def is_valid_login_slug(slug): - # Разрешены латинские буквы, цифры, дефисы и подчеркивания. Длина от 3 до 30. - return bool(re.match(r'^[a-z0-9_-]{3,30}$', slug)) - BASE_STYLE = ''' :root { --primary: #ff4d6d; --secondary: #00ddeb; --accent: #8b5cf6; @@ -57,8 +45,7 @@ BASE_STYLE = ''' --glass-bg: rgba(30, 30, 30, 0.7); --transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1); --delete-color: #ff4444; --folder-color: #ffc107; --selection-color: rgba(139, 92, 246, 0.3); --note-color: #6a5acd; --share-color: #4caf50; --archive-color: #78909c; - --todolist-color: #29b6f6; --shoppinglist-color: #ffa726; - --business-color: #00bcd4; + --todolist-color: #29b6f6; --shoppinglist-color: #ffa726; --business-color: #fd7e14; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } * { margin: 0; padding: 0; box-sizing: border-box; } @@ -68,14 +55,17 @@ body { font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Robo .app-header { position: fixed; top: 0; left: 0; right: 0; background: var(--glass-bg); backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); border-bottom: 1px solid rgba(255,255,255,0.1); z-index: 1000; padding: 10px 20px; display: flex; justify-content: space-between; align-items: center; } .user-info { font-weight: 600; } .view-toggle { display: flex; align-items: center; gap: 5px; } -.view-toggle button { background: none; border: none; color: var(--text-muted); font-size: 1.2em; padding: 5px; cursor: pointer; transition: var(--transition); } -.view-toggle button:hover, .view-toggle button.active { color: var(--primary); } -h2, h3, h4, h5 { color: var(--text-dark); } +.view-toggle button, .view-toggle a { background: none; border: none; color: var(--text-muted); font-size: 1.2em; padding: 5px; cursor: pointer; transition: var(--transition); text-decoration: none;} +.view-toggle button:hover, .view-toggle button.active, .view-toggle a:hover { color: var(--primary); } +h1, h2, h3, h4, h5 { color: var(--text-dark); } h2 { font-size: 1.3em; margin-bottom: 15px; margin-top: 15px; } .breadcrumbs { font-size: 1em; margin-bottom: 20px; white-space: nowrap; overflow-x: auto; -webkit-overflow-scrolling: touch; } .breadcrumbs a { color: var(--accent); text-decoration: none; } .breadcrumbs span { margin: 0 5px; color: var(--text-muted); } -input, select, textarea { width: 100%; padding: 14px; margin: 8px 0; border: 1px solid #333; border-radius: 12px; background: #2a2a2a; color: var(--text-dark); font-size: 1em; } +input, select, textarea, label { width: 100%; padding: 14px; margin: 8px 0; border: 1px solid #333; border-radius: 12px; background: #2a2a2a; color: var(--text-dark); font-size: 1em; } +label { padding: 0; margin: 0; border: none; background: none; } +.checkbox-label { display: flex; align-items: center; gap: 10px; width: auto; } +.checkbox-label input[type="checkbox"] { width: auto; margin: 0; } .btn { padding: 12px 24px; background: var(--primary); color: white; border: none; border-radius: 12px; cursor: pointer; font-size: 1em; font-weight: 600; transition: var(--transition); text-decoration: none; display: inline-block; text-align: center; } .btn:hover { filter: brightness(1.2); } .btn:active { transform: scale(0.98); } @@ -84,6 +74,7 @@ input, select, textarea { width: 100%; padding: 14px; margin: 8px 0; border: 1px .folder-btn { background: var(--folder-color); } .share-btn { background: var(--share-color); } .archive-btn { background: var(--archive-color); } +.business-btn { background: var(--business-color); } .flash { text-align: center; margin-bottom: 15px; padding: 12px; border-radius: 10px; background: rgba(0, 221, 235, 0.1); color: var(--secondary); } .flash.error { background: rgba(255, 68, 68, 0.1); color: var(--delete-color); } .file-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 15px; } @@ -97,7 +88,6 @@ input, select, textarea { width: 100%; padding: 14px; margin: 8px 0; border: 1px .item.note .item-preview { object-fit: contain; font-size: 3.5em; color: var(--note-color); } .item.todolist .item-preview { object-fit: contain; font-size: 3.5em; color: var(--todolist-color); } .item.shoppinglist .item-preview { object-fit: contain; font-size: 3.5em; color: var(--shoppinglist-color); } -.item.business .item-preview { object-fit: contain; font-size: 3.5em; color: var(--business-color); } .item-name { font-size: 0.9em; font-weight: 500; word-break: break-all; margin: 5px 0; flex-grow: 1; } .item-info { font-size: 0.75em; color: var(--text-muted); } .file-grid.list-view { display: flex; flex-direction: column; gap: 8px; } @@ -105,12 +95,11 @@ input, select, textarea { width: 100%; padding: 14px; margin: 8px 0; border: 1px .file-grid.list-view .item:hover { transform: translateY(0); } .file-grid.list-view .item-preview-wrapper { width: 45px; height: 45px; padding-top: 0; margin-bottom: 0; margin-right: 15px; flex-shrink: 0; } .file-grid.list-view .item.folder .item-preview, .file-grid.list-view .item.note .item-preview, -.file-grid.list-view .item.todolist .item-preview, .file-grid.list-view .item.shoppinglist .item-preview, -.file-grid.list-view .item.business .item-preview { font-size: 1.8em; } +.file-grid.list-view .item.todolist .item-preview, .file-grid.list-view .item.shoppinglist .item-preview { font-size: 1.8em; } .file-grid.list-view .item-name-info { flex-grow: 1; } .modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.85); z-index: 2000; justify-content: center; align-items: center; } .modal-content { display: flex; flex-direction: column; max-width: 95%; max-height: 95%; background: var(--card-bg-dark); padding: 10px; border-radius: 15px; overflow: hidden; position: relative; } -.modal-main-content { flex-grow: 1; overflow-y: auto; } +.modal-main-content { flex-grow: 1; overflow-y: auto; padding: 10px; } .modal-main-content img, .modal-main-content video, .modal-main-content iframe, .modal-main-content pre { max-width: 100%; max-height: 85vh; display: block; margin: auto; border-radius: 10px; } .modal-main-content iframe { width: 80vw; height: 85vh; border: none; } .modal-main-content pre { background: #121212; padding: 15px; border-radius: 8px; white-space: pre-wrap; word-wrap: break-word; max-height: 85vh; color: var(--text-dark); } @@ -158,6 +147,8 @@ input, select, textarea { width: 100%; padding: 14px; margin: 8px 0; border: 1px .public-list-item label { flex-grow: 1; cursor: pointer; } .public-list-item.purchased label { text-decoration: line-through; color: var(--text-muted); } .public-list-item .quantity { font-weight: bold; color: var(--secondary); background: #2a2a2a; padding: 2px 8px; border-radius: 6px; } +.form-group { margin-bottom: 15px; text-align: left; } +.form-group small { color: var(--text-muted); font-size: 0.8em; margin-top: 4px; display: block; } ''' PUBLIC_SHARE_PAGE_HTML = ''' @@ -308,390 +299,77 @@ body { padding-bottom: 30px; } ''' -BUSINESS_ADMIN_HTML_TEMPLATE = ''' - -{{ business.organization_name }} - Админ +PUBLIC_BUSINESS_PAGE_HTML = ''' + +{{ page.org_name }} - - - - -
-
{{ business.organization_name }} (Admin)
-
- Назад -
-
+
-{% with messages = get_flashed_messages(with_categories=true) %}{% if messages %} - {% for category, message in messages %}
{{ message }}
{% endfor %} -{% endif %}{% endwith %} - -
-
- Avatar -
-

{{ business.organization_name }}

-

Login: {{ business.login_slug }}

-
-
-
- - +
+ {% if page.avatar_path %} + Avatar + {% endif %} +

{{ page.org_name }}

-
- -

Товары ({{ business.products|length }})

- -
- {% for product in business.products %} -
-
+ {% if page.products %} +
+ {% for product in page.products %} +
{% if product.photo_path %} - Product Photo - {% else %} -
- {% endif %} -
-
-

{{ product.name }}

-

{{ product.description | truncate(60, True) }}

- {% if business.show_prices %} -
{{ currency_symbol }}{{ product.price }}
+ {{ product.name }} {% endif %} -
-
- - -
-
- {% endfor %} - {% if not business.products %}

Нет товаров в продаже.

{% endif %} -
-
- - - - - {% endfor %} @@ -1090,7 +769,7 @@ TMA_DASHBOARD_HTML_TEMPLATE = '''
Папку
Список дел
Покупки
-
Бизнес
+
Бизнес
- - -
''' @@ -1774,6 +1376,260 @@ ARCHIVED_LISTS_HTML = ''' ''' +TMA_MANAGE_BUSINESS_HTML = ''' + +Мои бизнес страницы + + + + + +
+
{{ display_name }}
+
+ +
+
+
+
+

Мои бизнес страницы

+ Создать +
+ {% with messages = get_flashed_messages(with_categories=true) %}{% if messages %} + {% for category, message in messages %}
{{ message }}
{% endfor %} + {% endif %}{% endwith %} +
+ {% for page in pages %} +
+
+ {% if page.avatar_path %} + + {% else %} +
+ {% endif %} +
+
+

{{ page.org_name }}

+

/business/{{ page.login }}

+
+ + + +
+ {% endfor %} + {% if not pages %} +

У вас еще нет бизнес страниц.

+ {% endif %} +
+
+ + +''' + +TMA_CREATE_EDIT_BUSINESS_FORM_HTML = ''' + +{{ 'Редактировать' if page else 'Создать' }} страницу + + + + + +
+
{{ display_name }}
+
+ +
+
+
+

{{ 'Редактировать' if page else 'Создать' }} бизнес страницу

+ {% with messages = get_flashed_messages(with_categories=true) %}{% if messages %} + {% for category, message in messages %}
{{ message }}
{% endfor %} + {% endif %}{% endwith %} +
+
+ + +
+
+ + + Только латинские буквы, цифры и символы (_, ., -). Это будет в ссылке: /business/логин +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + + Для WhatsApp: номер с кодом страны (e.g., +77001234567). Для Telegram: @username (без @). +
+ +
+ {% if page %} +
+ +
+ {% endif %} +
+ + +''' + +TMA_MANAGE_PRODUCTS_HTML = ''' + +Управление товарами + + + + + +
+
{{ page.org_name }}
+
+ +
+
+
+
+

Товары

+ +
+ {% with messages = get_flashed_messages(with_categories=true) %}{% if messages %} + {% for category, message in messages %}
{{ message }}
{% endfor %} + {% endif %}{% endwith %} +
+ {% for product in page.products %} +
+
+ {% if product.photo_path %} + + {% else %} +
+ {% endif %} +
+
+

{{ product.name }}

+ {% if page.show_prices %} +

{{ "%.2f"|format(product.price|float) }} {{ page.currency }}

+ {% endif %} +
+ +
+ +
+
+ {% endfor %} + {% if not page.products %} +

У вас еще нет товаров.

+ {% endif %} +
+
+ + + + + +''' + @app.route('/tma_dashboard', methods=['GET', 'POST']) def tma_dashboard(): if 'telegram_user_id' not in session: @@ -1798,7 +1654,7 @@ def tma_dashboard(): parent_folder_id = parent_folder.get('id', 'root') if parent_folder else 'root' items_in_folder = [item for item in current_folder.get('children', []) if not item.get('is_archived')] - items_in_folder = sorted(items_in_folder, key=lambda x: (x['type'] not in ['folder', 'note', 'todolist', 'shoppinglist', 'business'], x.get('name', x.get('original_filename', x.get('title', ''))).lower())) + items_in_folder = sorted(items_in_folder, key=lambda x: (x['type'] not in ['folder', 'note', 'todolist', 'shoppinglist'], x.get('name', x.get('original_filename', x.get('title', ''))).lower())) if request.method == 'POST': if not HF_TOKEN_WRITE: @@ -1851,7 +1707,7 @@ def tma_dashboard(): all_folders_for_move = get_all_folders(user_data['filesystem']) - return render_template_string(TMA_DASHBOARD_HTML_TEMPLATE, display_name=display_name, items=items_in_folder, current_folder_id=current_folder_id, current_folder=current_folder, parent_folder_id=parent_folder_id, breadcrumbs=breadcrumbs, hf_file_url_jinja=lambda path, download=False: f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{quote(path)}{'?download=true' if download else ''}", is_tma_user_admin_flag=is_admin_tma(), all_folders_for_move=all_folders_for_move, currencies=CURRENCIES) + return render_template_string(TMA_DASHBOARD_HTML_TEMPLATE, display_name=display_name, items=items_in_folder, current_folder_id=current_folder_id, current_folder=current_folder, parent_folder_id=parent_folder_id, breadcrumbs=breadcrumbs, hf_file_url_jinja=lambda path, download=False: f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{quote(path)}{'?download=true' if download else ''}", is_tma_user_admin_flag=is_admin_tma(), all_folders_for_move=all_folders_for_move) @app.route('/tma_archive') def tma_archive_view(): @@ -1888,311 +1744,6 @@ def create_folder_tma(): else: flash('Не удалось найти родительскую папку.', 'error') return redirect(url_for('tma_dashboard', folder_id=parent_folder_id)) -@app.route('/create_business_page_tma', methods=['POST']) -def create_business_page_tma(): - if 'telegram_user_id' not in session: - return redirect(url_for('tma_entry_page')) - tma_user_id = session['telegram_user_id'] - data = load_data() - user_data = data['users'].get(tma_user_id) - if not user_data: - return redirect(url_for('tma_entry_page')) - - parent_folder_id = request.form.get('parent_folder_id', 'root') - organization_name = request.form.get('organization_name', '').strip() - login_slug = request.form.get('login_slug', '').lower().strip() - currency = request.form.get('currency') - show_prices = 'show_prices' in request.form - contact_type = request.form.get('contact_type') - contact_value = request.form.get('contact_value', '').strip() - avatar_file = request.files.get('avatar_file') - now_str = datetime.now().strftime('%Y-%m-%d %H:%M') - - # Validation - if not all([organization_name, login_slug, currency, contact_type, contact_value]): - flash('Все поля, кроме аватара, обязательны!', 'error') - return redirect(url_for('tma_dashboard', folder_id=parent_folder_id)) - if not is_valid_login_slug(login_slug): - flash('Логин должен содержать только латинские буквы, цифры, дефисы или подчеркивания, от 3 до 30 символов.', 'error') - return redirect(url_for('tma_dashboard', folder_id=parent_folder_id)) - if login_slug in data['business_pages']: - flash('Логин уже занят!', 'error') - return redirect(url_for('tma_dashboard', folder_id=parent_folder_id)) - if currency not in CURRENCIES: - flash('Недопустимая валюта.', 'error') - return redirect(url_for('tma_dashboard', folder_id=parent_folder_id)) - - avatar_path = None - if avatar_file and avatar_file.filename and HF_TOKEN_WRITE: - file_id = uuid.uuid4().hex - original_filename = secure_filename(avatar_file.filename) - name_part, ext_part = os.path.splitext(original_filename) - unique_filename = f"avatar_{login_slug}_{uuid.uuid4().hex[:8]}{ext_part}" - hf_path = f"cloud_files/business/{login_slug}/{unique_filename}" - temp_path = os.path.join(UPLOAD_FOLDER, f"{file_id}_{unique_filename}") - try: - api = HfApi() - avatar_file.save(temp_path) - api.upload_file(path_or_fileobj=temp_path, path_in_repo=hf_path, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE) - avatar_path = hf_path - except Exception as e: - logging.error(f"Error uploading avatar: {e}") - flash('Ошибка загрузки аватара.', 'error') - finally: - if os.path.exists(temp_path): os.remove(temp_path) - elif avatar_file and avatar_file.filename: - flash('Загрузка невозможна: токен для записи не настроен.', 'error') - return redirect(url_for('tma_dashboard', folder_id=parent_folder_id)) - - - # Create Business Page data - business_id = uuid.uuid4().hex - business_data = { - "id": business_id, - "type": "business", - "name": organization_name, # name used in filesystem node for display - "login_slug": login_slug, - "owner_id": tma_user_id, - "organization_name": organization_name, - "avatar_path": avatar_path, - "currency": currency, - "show_prices": show_prices, - "contact_type": contact_type, - "contact_value": contact_value, - "created_at": now_str, - "products": [] - } - - # Add to main business_pages store - data['business_pages'][login_slug] = business_data - - # Add to user's filesystem - fs_node = { - 'type': 'business', - 'id': business_id, - 'name': organization_name, - 'login_slug': login_slug, - 'modified_date': now_str - } - - if add_node(user_data['filesystem'], parent_folder_id, fs_node): - try: save_data(data); flash(f'Бизнес-страница "{organization_name}" создана! Управлять') - except Exception: flash('Ошибка сохранения метаданных.', 'error') - else: flash('Не удалось найти родительскую папку.', 'error') - - return redirect(url_for('tma_dashboard', folder_id=parent_folder_id)) - -@app.route('/tma_business_admin/', methods=['GET', 'POST']) -def tma_business_admin(login_slug): - if 'telegram_user_id' not in session: return redirect(url_for('tma_entry_page')) - tma_user_id = session['telegram_user_id'] - data = load_data() - business = data['business_pages'].get(login_slug) - - if not business or business.get('owner_id') != tma_user_id: - flash('Бизнес-страница не найдена или доступ запрещен.', 'error') - return redirect(url_for('tma_dashboard')) - - # Handle Business Edit POST submission - if request.method == 'POST': - # This route should be updated to handle business editing for existing slugs, - # but for simplicity and to match the template, let's treat the form submission - # as a general update. Since the edit form in the template posts to the /create_business_page_tma - # route, we should handle the actual edit logic there based on a hidden field, - # or change the form action here. - # For now, relying on the logic in create_business_page_tma to handle both creation and simple updates. - # However, for a proper edit, we should check a hidden field for the *original* slug. - - # Since the template uses a dedicated business editor modal that posts to 'create_business_page_tma' - # with a hidden 'login_slug_original', let's assume POST to this route is only for future extensions. - # For now, simply redirect back after a GET. - - # If we were implementing the edit form submission here: - # 1. Get form data. - # 2. Validate. - # 3. Update 'business' object in 'data['business_pages']'. - # 4. Update the corresponding filesystem node name if changed. - # 5. Save data and flash success. - pass - - currency_symbol = CURRENCIES.get(business.get('currency', 'KZT'), '?') - - return render_template_string( - BUSINESS_ADMIN_HTML_TEMPLATE, - business=business, - currency_symbol=currency_symbol, - currencies=CURRENCIES, - hf_file_url_jinja=lambda path, download=False: f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{quote(path)}{'?download=true' if download else ''}" - ) - -@app.route('/tma_business_admin//add_product', methods=['POST']) -def add_product_tma(login_slug): - if 'telegram_user_id' not in session: return jsonify({'status': 'error', 'message': 'Не авторизован.'}), 401 - tma_user_id = session['telegram_user_id'] - data = load_data() - business = data['business_pages'].get(login_slug) - - if not business or business.get('owner_id') != tma_user_id: - return jsonify({'status': 'error', 'message': 'Бизнес-страница не найдена или нет доступа.'}), 404 - - product_id = request.form.get('product_id') - name = request.form.get('name', '').strip() - description = request.form.get('description', '').strip() - price = request.form.get('price', '').strip() - photo_file = request.files.get('photo_file') - - if not all([name, description, price]): - flash('Название, описание и цена обязательны.', 'error') - return redirect(url_for('tma_business_admin', login_slug=login_slug)) - - photo_path = None - product = None - if product_id: - product = next((p for p in business['products'] if p['id'] == product_id), None) - if product: - photo_path = product.get('photo_path') # Keep old photo path if no new file uploaded - - if photo_file and photo_file.filename and HF_TOKEN_WRITE: - file_id = uuid.uuid4().hex - original_filename = secure_filename(photo_file.filename) - name_part, ext_part = os.path.splitext(original_filename) - unique_filename = f"product_{product_id or uuid.uuid4().hex[:8]}_{uuid.uuid4().hex[:8]}{ext_part}" - hf_path = f"cloud_files/business/{login_slug}/products/{unique_filename}" - temp_path = os.path.join(UPLOAD_FOLDER, f"{file_id}_{unique_filename}") - try: - api = HfApi() - photo_file.save(temp_path) - api.upload_file(path_or_fileobj=temp_path, path_in_repo=hf_path, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE) - photo_path = hf_path - - # Delete old photo if it exists and a new one was uploaded - if product and product.get('photo_path') and product['photo_path'] != hf_path: - try: api.delete_file(path_in_repo=product['photo_path'], repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE) - except hf_utils.EntryNotFoundError: pass - except Exception as e: - logging.error(f"Error uploading product photo: {e}") - flash('Ошибка загрузки фото товара.', 'error') - return redirect(url_for('tma_business_admin', login_slug=login_slug)) - finally: - if os.path.exists(temp_path): os.remove(temp_path) - elif photo_file and photo_file.filename and not HF_TOKEN_WRITE: - flash('Загрузка невозможна: токен для записи не настроен.', 'error') - return redirect(url_for('tma_business_admin', login_slug=login_slug)) - - - if product: - # Update existing product - product['name'] = name - product['description'] = description - product['price'] = price - product['photo_path'] = photo_path - flash('Товар обновлен.', 'success') - else: - # Add new product - new_product = { - 'id': uuid.uuid4().hex, - 'name': name, - 'description': description, - 'price': price, - 'photo_path': photo_path - } - business['products'].append(new_product) - flash('Товар добавлен.', 'success') - - try: - save_data(data) - except Exception as e: - flash(f'Ошибка сохранения ��анных: {e}', 'error') - - return redirect(url_for('tma_business_admin', login_slug=login_slug)) - -@app.route('/tma_business_admin//delete_product', methods=['POST']) -def delete_product_tma(login_slug): - if 'telegram_user_id' not in session: return jsonify({'status': 'error', 'message': 'Не авторизован.'}), 401 - tma_user_id = session['telegram_user_id'] - data = load_data() - business = data['business_pages'].get(login_slug) - - if not business or business.get('owner_id') != tma_user_id: - return jsonify({'status': 'error', 'message': 'Бизнес-страница не найдена или нет доступа.'}), 404 - - product_id = request.json.get('product_id') - - product_to_delete = next((p for p in business['products'] if p['id'] == product_id), None) - - if not product_to_delete: - return jsonify({'status': 'error', 'message': 'Товар не найден.'}), 404 - - if product_to_delete.get('photo_path') and HF_TOKEN_WRITE: - try: - api = HfApi() - api.delete_file(path_in_repo=product_to_delete['photo_path'], repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE) - except hf_utils.EntryNotFoundError: pass - except Exception as e: - return jsonify({'status': 'error', 'message': f'Ошибка удаления фото с сервера: {e}'}), 500 - - business['products'] = [p for p in business['products'] if p['id'] != product_id] - - try: - save_data(data) - return jsonify({'status': 'success', 'message': 'Товар удален.'}) - except Exception as e: - return jsonify({'status': 'error', 'message': f'Ошибка сохранения данных: {e}'}), 500 - -@app.route('/tma_business_admin//product/') -def get_product_tma(login_slug, product_id): - if 'telegram_user_id' not in session: return jsonify({'status': 'error', 'message': 'Не авторизован.'}), 401 - tma_user_id = session['telegram_user_id'] - data = load_data() - business = data['business_pages'].get(login_slug) - - if not business or business.get('owner_id') != tma_user_id: - return jsonify({'status': 'error', 'message': 'Бизнес-страница не найдена или нет доступа.'}), 404 - - product = next((p for p in business['products'] if p['id'] == product_id), None) - if not product: - return jsonify({'status': 'error', 'message': 'Товар не найден.'}), 404 - - return jsonify({'status': 'success', 'product': product}) - -@app.route('/b/') -def shared_business_view(login_slug): - data = load_data() - business = data['business_pages'].get(login_slug) - - if not business: - return "Бизнес-страница не найдена.", 404 - - currency_symbol = CURRENCIES.get(business.get('currency', 'KZT'), '?') - - def generate_order_link(product): - contact_type = business.get('contact_type') - contact_value = business.get('contact_value') - org_name = business.get('organization_name') - - message = f"Здравствуйте, {org_name}! Хочу заказать товар: {product['name']}. " - if business.get('show_prices'): - message += f"Цена: {currency_symbol}{product['price']}. " - message += "Мой вопрос: [введите ваш вопрос]" - - if contact_type == 'whatsapp': - # Assumes contact_value is a phone number with country code (no +, spaces, dashes) - number = re.sub(r'[^0-9]', '', contact_value.lstrip('+')) - return f"https://wa.me/{number}?text={quote(message)}" - elif contact_type == 'telegram': - # Assumes contact_value is a username (@username) or ID - username = contact_value.lstrip('@') - return f"https://t.me/{username}?start={quote(f'order_{product['id']}_{login_slug}')}" - return "#" - - return render_template_string( - PUBLIC_BUSINESS_PAGE_HTML, - business=business, - currency_symbol=currency_symbol, - generate_order_link=generate_order_link, - hf_file_url_jinja=lambda path, download=False: f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{quote(path)}{'?download=true' if download else ''}" - ) - def get_item_node_for_user(item_id): if not (session.get('telegram_user_id') or session.get('admin_browser_logged_in')): return None @@ -2325,29 +1876,13 @@ def batch_delete_tma(): if node_type == 'folder': if node.get('children'): errors.append(f'Папка "{node_name}" не пуста.'); continue - if node_type in ['folder', 'note', 'todolist', 'shoppinglist', 'file', 'business']: + if node_type in ['folder', 'note', 'todolist', 'shoppinglist', 'file']: if node_type == 'file': try: if node.get('path') and HF_TOKEN_WRITE: api.delete_file(path_in_repo=node['path'], repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE) except hf_utils.EntryNotFoundError: pass except Exception as e: errors.append(f'Ошибка удаления "{node_name}" с сервера: {e}'); continue - if node_type == 'business': - login_slug = node.get('login_slug') - if login_slug and login_slug in data['business_pages']: - business = data['business_pages'][login_slug] - # Delete all product photos and avatar - paths_to_delete = [p.get('photo_path') for p in business.get('products', []) if p.get('photo_path')] - if business.get('avatar_path'): paths_to_delete.append(business['avatar_path']) - - for path in paths_to_delete: - try: - if path and HF_TOKEN_WRITE: api.delete_file(path_in_repo=path, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE) - except hf_utils.EntryNotFoundError: pass - except Exception as e: errors.append(f'Ошибка удаления фото "{node_name}" с сервера: {e}'); continue - - del data['business_pages'][login_slug] - if remove_node(user_data['filesystem'], item_id)[0]: success_count += 1 else: errors.append(f'Ошибка удаления "{node_name}" из базы.') @@ -2847,6 +2382,283 @@ def public_toggle_item(link_id, item_id): else: return jsonify({'status': 'error', 'message': 'Элемент в списке не найден.'}), 404 +@app.route('/tma_business') +def tma_manage_business_pages(): + if 'telegram_user_id' not in session: return redirect(url_for('tma_entry_page')) + tma_user_id = session['telegram_user_id'] + display_name = session.get('telegram_display_name', 'Пользователь') + data = load_data() + user_data = data['users'].get(tma_user_id) + if not user_data: return redirect(url_for('tma_entry_page')) + + owned_logins = user_data.get('owned_business_pages', []) + pages = [data['business_pages'][login] for login in owned_logins if login in data['business_pages']] + + return render_template_string(TMA_MANAGE_BUSINESS_HTML, display_name=display_name, pages=pages, hf_file_url_jinja=lambda path, download=False: f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{quote(path)}{'?download=true' if download else ''}") + +@app.route('/tma_business/create', methods=['GET', 'POST']) +def tma_create_business_page(): + if 'telegram_user_id' not in session: return redirect(url_for('tma_entry_page')) + tma_user_id = session['telegram_user_id'] + display_name = session.get('telegram_display_name', 'Пользователь') + + if request.method == 'POST': + data = load_data() + login = request.form.get('login', '').strip().lower() + + if not re.match(r'^[a-z0-9_.-]+$', login): + flash('Логин содержит недопустимые символы.', 'error') + return redirect(url_for('tma_create_business_page')) + if login in data['business_pages']: + flash('Этот логин уже занят.', 'error') + return redirect(url_for('tma_create_business_page')) + + new_page = { + 'owner_id': tma_user_id, + 'login': login, + 'org_name': request.form.get('org_name'), + 'currency': request.form.get('currency'), + 'show_prices': 'show_prices' in request.form, + 'order_destination': request.form.get('order_destination'), + 'contact_number': request.form.get('contact_number').replace('@', ''), + 'avatar_path': None, + 'products': [] + } + + avatar = request.files.get('avatar') + if avatar and avatar.filename: + if not HF_TOKEN_WRITE: + flash('Загрузка аватара невозможна: токен для записи не настроен.', 'error') + return redirect(url_for('tma_create_business_page')) + try: + api = HfApi() + unique_filename = f"avatar_{uuid.uuid4().hex[:8]}{os.path.splitext(secure_filename(avatar.filename))[1]}" + hf_path = f"business_pages/{login}/{unique_filename}" + api.upload_file(path_or_fileobj=BytesIO(avatar.read()), path_in_repo=hf_path, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE) + new_page['avatar_path'] = hf_path + except Exception as e: + flash(f'Ошибка загрузки аватара: {e}', 'error') + return redirect(url_for('tma_create_business_page')) + + data['business_pages'][login] = new_page + data['users'][tma_user_id].setdefault('owned_business_pages', []).append(login) + try: + save_data(data) + flash('Бизнес страница успешно создана!', 'success') + return redirect(url_for('tma_manage_business_pages')) + except Exception as e: + flash(f'Ошибка сохранения данных: {e}', 'error') + + return render_template_string(TMA_CREATE_EDIT_BUSINESS_FORM_HTML, display_name=display_name, page=None) + +@app.route('/tma_business/edit/', methods=['GET', 'POST']) +def tma_edit_business_page(login): + if 'telegram_user_id' not in session: return redirect(url_for('tma_entry_page')) + tma_user_id = session['telegram_user_id'] + display_name = session.get('telegram_display_name', 'Пользователь') + data = load_data() + page = data.get('business_pages', {}).get(login) + + if not page or page.get('owner_id') != tma_user_id: + flash('Страница не найдена или у вас нет доступа.', 'error') + return redirect(url_for('tma_manage_business_pages')) + + if request.method == 'POST': + page['org_name'] = request.form.get('org_name') + page['currency'] = request.form.get('currency') + page['show_prices'] = 'show_prices' in request.form + page['order_destination'] = request.form.get('order_destination') + page['contact_number'] = request.form.get('contact_number').replace('@', '') + + avatar = request.files.get('avatar') + if avatar and avatar.filename: + if not HF_TOKEN_WRITE: + flash('Загрузка аватара невозможна: токен для записи не настроен.', 'error') + else: + try: + api = HfApi() + if page.get('avatar_path'): + try: + api.delete_file(path_in_repo=page['avatar_path'], repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE) + except hf_utils.EntryNotFoundError: + pass + unique_filename = f"avatar_{uuid.uuid4().hex[:8]}{os.path.splitext(secure_filename(avatar.filename))[1]}" + hf_path = f"business_pages/{login}/{unique_filename}" + api.upload_file(path_or_fileobj=BytesIO(avatar.read()), path_in_repo=hf_path, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE) + page['avatar_path'] = hf_path + except Exception as e: + flash(f'Ошибка загрузки аватара: {e}', 'error') + try: + save_data(data) + flash('Изменения сохранены.', 'success') + return redirect(url_for('tma_manage_business_pages')) + except Exception as e: + flash(f'Ошибка сохранения данных: {e}', 'error') + + return render_template_string(TMA_CREATE_EDIT_BUSINESS_FORM_HTML, display_name=display_name, page=page) + +@app.route('/tma_business/delete/', methods=['POST']) +def tma_delete_business_page(login): + if 'telegram_user_id' not in session: return redirect(url_for('tma_entry_page')) + tma_user_id = session['telegram_user_id'] + data = load_data() + page = data.get('business_pages', {}).get(login) + + if not page or page.get('owner_id') != tma_user_id: + flash('Страница не найдена или у вас нет доступа.', 'error') + return redirect(url_for('tma_manage_business_pages')) + + if HF_TOKEN_WRITE: + api = HfApi() + try: + api.delete_folder(folder_path=f"business_pages/{login}", repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE) + except Exception as e: + logging.error(f"Could not delete business page folder from HF: {e}") + + del data['business_pages'][login] + if login in data['users'][tma_user_id].get('owned_business_pages', []): + data['users'][tma_user_id]['owned_business_pages'].remove(login) + + try: + save_data(data) + flash('Бизнес страница удалена.', 'success') + except Exception as e: + flash(f'Ошибка сохранения после удаления: {e}', 'error') + + return redirect(url_for('tma_manage_business_pages')) + +@app.route('/business/') +def public_business_page(login): + data = load_data() + page = data.get('business_pages', {}).get(login) + if not page: + return "Страница не найдена.", 404 + return render_template_string(PUBLIC_BUSINESS_PAGE_HTML, page=page, hf_file_url_jinja=lambda path, download=False: f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{quote(path)}{'?download=true' if download else ''}") + +@app.route('/tma_business/manage/') +def tma_manage_products(login): + if 'telegram_user_id' not in session: return redirect(url_for('tma_entry_page')) + tma_user_id = session['telegram_user_id'] + data = load_data() + page = data.get('business_pages', {}).get(login) + + if not page or page.get('owner_id') != tma_user_id: + flash('Страница не найдена или у вас нет доступа.', 'error') + return redirect(url_for('tma_manage_business_pages')) + + return render_template_string(TMA_MANAGE_PRODUCTS_HTML, page=page, hf_file_url_jinja=lambda path, download=False: f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{quote(path)}{'?download=true' if download else ''}") + +@app.route('/tma_business/product/add/', methods=['POST']) +def tma_add_product(login): + if 'telegram_user_id' not in session: return redirect(url_for('tma_entry_page')) + tma_user_id = session['telegram_user_id'] + data = load_data() + page = data.get('business_pages', {}).get(login) + + if not page or page.get('owner_id') != tma_user_id: + flash('Страница не найдена или у вас нет доступа.', 'error') + return redirect(url_for('tma_manage_business_pages')) + + new_product = { + 'id': uuid.uuid4().hex, + 'name': request.form.get('name'), + 'description': request.form.get('description', ''), + 'price': request.form.get('price', 0), + 'photo_path': None + } + + photo = request.files.get('photo') + if photo and photo.filename: + if not HF_TOKEN_WRITE: flash('Загрузка фото невозможна: токен не настроен.', 'error') + else: + try: + api = HfApi() + unique_filename = f"product_{new_product['id'][:8]}{os.path.splitext(secure_filename(photo.filename))[1]}" + hf_path = f"business_pages/{login}/{unique_filename}" + api.upload_file(path_or_fileobj=BytesIO(photo.read()), path_in_repo=hf_path, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE) + new_product['photo_path'] = hf_path + except Exception as e: + flash(f'Ошибка загрузки фото: {e}', 'error') + + page.setdefault('products', []).append(new_product) + try: + save_data(data) + flash('Товар добавлен.', 'success') + except Exception as e: + flash(f'Ошибка сохранения: {e}', 'error') + + return redirect(url_for('tma_manage_products', login=login)) + +@app.route('/tma_business/product/edit/', methods=['POST']) +def tma_edit_product(login): + if 'telegram_user_id' not in session: return redirect(url_for('tma_entry_page')) + tma_user_id = session['telegram_user_id'] + data = load_data() + page = data.get('business_pages', {}).get(login) + product_id = request.form.get('product_id') + + if not page or page.get('owner_id') != tma_user_id or not product_id: + flash('Ошибка доступа.', 'error'); return redirect(url_for('tma_manage_business_pages')) + + product_to_edit = next((p for p in page.get('products', []) if p['id'] == product_id), None) + if not product_to_edit: + flash('Товар не найден.', 'error'); return redirect(url_for('tma_manage_products', login=login)) + + product_to_edit['name'] = request.form.get('name') + product_to_edit['description'] = request.form.get('description', '') + product_to_edit['price'] = request.form.get('price', 0) + + photo = request.files.get('photo') + if photo and photo.filename: + if not HF_TOKEN_WRITE: flash('Загрузка фото невозможна: токен не настроен.', 'error') + else: + try: + api = HfApi() + if product_to_edit.get('photo_path'): + try: api.delete_file(path_in_repo=product_to_edit['photo_path'], repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE) + except hf_utils.EntryNotFoundError: pass + unique_filename = f"product_{product_id[:8]}{os.path.splitext(secure_filename(photo.filename))[1]}" + hf_path = f"business_pages/{login}/{unique_filename}" + api.upload_file(path_or_fileobj=BytesIO(photo.read()), path_in_repo=hf_path, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE) + product_to_edit['photo_path'] = hf_path + except Exception as e: + flash(f'Ошибка загрузки фото: {e}', 'error') + + try: + save_data(data) + flash('Товар обновлен.', 'success') + except Exception as e: + flash(f'Ошибка сохранения: {e}', 'error') + + return redirect(url_for('tma_manage_products', login=login)) + +@app.route('/tma_business/product/delete//', methods=['POST']) +def tma_delete_product(login, product_id): + if 'telegram_user_id' not in session: return redirect(url_for('tma_entry_page')) + tma_user_id = session['telegram_user_id'] + data = load_data() + page = data.get('business_pages', {}).get(login) + + if not page or page.get('owner_id') != tma_user_id: + flash('Ошибка доступа.', 'error'); return redirect(url_for('tma_manage_business_pages')) + + product_to_delete = next((p for p in page.get('products', []) if p['id'] == product_id), None) + if product_to_delete and product_to_delete.get('photo_path') and HF_TOKEN_WRITE: + try: + api = HfApi() + api.delete_file(path_in_repo=product_to_delete['photo_path'], repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE) + except Exception as e: + logging.error(f"Could not delete product photo from HF: {e}") + + page['products'] = [p for p in page.get('products', []) if p['id'] != product_id] + try: + save_data(data) + flash('Товар удален.', 'success') + except Exception as e: + flash(f'Ошибка сохранения: {e}', 'error') + + return redirect(url_for('tma_manage_products', login=login)) + ADMIN_LOGIN_HTML = ''' Admin Login @@ -2910,6 +2722,7 @@ ADMIN_PANEL_HTML = ''' Created: {{ user.get('created_at', 'N/A') }} Items: {{ user.get('item_count', 0) }} Reminders: {{ user.get('reminders', [])|length }} + Business Pages: {{ user.get('owned_business_pages', [])|length }}