Update app.py
Browse files
app.py
CHANGED
|
@@ -19,6 +19,7 @@ import zipfile
|
|
| 19 |
import tempfile
|
| 20 |
import pytz
|
| 21 |
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
|
|
|
| 22 |
|
| 23 |
app = Flask(__name__)
|
| 24 |
app.secret_key = os.getenv("FLASK_SECRET_KEY", "supersecretkey_folders_unique_tma")
|
|
@@ -391,6 +392,8 @@ def initialize_user_filesystem_tma(user_data, tma_user_id_str):
|
|
| 391 |
}
|
| 392 |
add_node(user_data['filesystem'], 'root', file_node)
|
| 393 |
del user_data['files']
|
|
|
|
|
|
|
| 394 |
|
| 395 |
@cache.memoize(timeout=300)
|
| 396 |
def load_data():
|
|
@@ -561,10 +564,13 @@ def auth_via_telegram():
|
|
| 561 |
user_info['created_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
| 562 |
user_info['filesystem'] = {"type": "folder", "id": "root", "name": "root", "children": []}
|
| 563 |
user_info['reminders'] = []
|
|
|
|
| 564 |
data['users'][tma_user_id_str] = user_info
|
| 565 |
initialize_user_filesystem_tma(data['users'][tma_user_id_str], tma_user_id_str)
|
| 566 |
else:
|
| 567 |
data['users'][tma_user_id_str].update(user_info)
|
|
|
|
|
|
|
| 568 |
|
| 569 |
try: save_data(data)
|
| 570 |
except Exception as e:
|
|
@@ -586,7 +592,9 @@ TMA_DASHBOARD_HTML_TEMPLATE = '''
|
|
| 586 |
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;800&display=swap" rel="stylesheet">
|
| 587 |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
|
| 588 |
<script src="https://telegram.org/js/telegram-web-app.js"></script>
|
| 589 |
-
<style>''' + BASE_STYLE + '''
|
|
|
|
|
|
|
| 590 |
<div class="app-header">
|
| 591 |
<div class="user-info">{{ display_name }}</div>
|
| 592 |
<div class="view-toggle">
|
|
@@ -684,6 +692,7 @@ TMA_DASHBOARD_HTML_TEMPLATE = '''
|
|
| 684 |
<div class="fab-option" id="fab-option-folder"><i class="fa-solid fa-folder-plus"></i><span>Папку</span></div>
|
| 685 |
<div class="fab-option" id="fab-option-todolist" onclick="openListEditorModal(null, 'todolist')"><i class="fa-solid fa-list-check"></i><span>Список дел</span></div>
|
| 686 |
<div class="fab-option" id="fab-option-shoppinglist" onclick="openListEditorModal(null, 'shoppinglist')"><i class="fa-solid fa-cart-shopping"></i><span>Покупки</span></div>
|
|
|
|
| 687 |
</div>
|
| 688 |
<form id="upload-form" method="POST" enctype="multipart/form-data" action="{{ url_for('tma_dashboard') }}" style="display:none;">
|
| 689 |
<input type="hidden" name="current_folder_id" value="{{ current_folder_id }}">
|
|
@@ -1536,7 +1545,6 @@ def batch_delete_tma():
|
|
| 1536 |
if node_type == 'folder':
|
| 1537 |
if node.get('children'): errors.append(f'Папка "{node_name}" не пуста.'); continue
|
| 1538 |
|
| 1539 |
-
# For all types that can be deleted
|
| 1540 |
if node_type in ['folder', 'note', 'todolist', 'shoppinglist', 'file']:
|
| 1541 |
if node_type == 'file':
|
| 1542 |
try:
|
|
@@ -2506,7 +2514,7 @@ def admin_delete_item(tma_user_id_str, item_id):
|
|
| 2506 |
api = HfApi()
|
| 2507 |
if hf_path: api.delete_file(path_in_repo=hf_path, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE)
|
| 2508 |
except hf_utils.EntryNotFoundError:
|
| 2509 |
-
pass
|
| 2510 |
except Exception as e:
|
| 2511 |
flash(f'Deletion error from remote storage: {e}', 'error')
|
| 2512 |
return redirect(url_for('admin_user_files', tma_user_id_str=tma_user_id_str, folder_id=current_folder_id))
|
|
@@ -2514,7 +2522,6 @@ def admin_delete_item(tma_user_id_str, item_id):
|
|
| 2514 |
flash('Folder is not empty.', 'error')
|
| 2515 |
return redirect(url_for('admin_user_files', tma_user_id_str=tma_user_id_str, folder_id=current_folder_id))
|
| 2516 |
|
| 2517 |
-
# If we are here, we can delete the node from filesystem
|
| 2518 |
if remove_node(user_data['filesystem'], item_id)[0]:
|
| 2519 |
try:
|
| 2520 |
save_data(data)
|
|
@@ -2547,6 +2554,275 @@ def admin_delete_reminder(tma_user_id_str, reminder_id):
|
|
| 2547 |
flash('Reminder not found.', 'error')
|
| 2548 |
return redirect(url_for('admin_user_reminders', tma_user_id_str=tma_user_id_str))
|
| 2549 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2550 |
if __name__ == '__main__':
|
| 2551 |
if not HF_TOKEN_WRITE: logging.warning("HF_TOKEN (write) is not set. Uploads/deletions will fail.")
|
| 2552 |
if not HF_TOKEN_READ: logging.warning("HF_TOKEN_READ is not set. Downloads/previews might fail.")
|
|
@@ -2566,4 +2842,3 @@ if __name__ == '__main__':
|
|
| 2566 |
threading.Thread(target=check_reminders, daemon=True).start()
|
| 2567 |
|
| 2568 |
app.run(debug=False, host='0.0.0.0', port=7860)
|
| 2569 |
-
|
|
|
|
| 19 |
import tempfile
|
| 20 |
import pytz
|
| 21 |
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
| 22 |
+
import re
|
| 23 |
|
| 24 |
app = Flask(__name__)
|
| 25 |
app.secret_key = os.getenv("FLASK_SECRET_KEY", "supersecretkey_folders_unique_tma")
|
|
|
|
| 392 |
}
|
| 393 |
add_node(user_data['filesystem'], 'root', file_node)
|
| 394 |
del user_data['files']
|
| 395 |
+
if 'business_profiles' not in user_data:
|
| 396 |
+
user_data['business_profiles'] = []
|
| 397 |
|
| 398 |
@cache.memoize(timeout=300)
|
| 399 |
def load_data():
|
|
|
|
| 564 |
user_info['created_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
| 565 |
user_info['filesystem'] = {"type": "folder", "id": "root", "name": "root", "children": []}
|
| 566 |
user_info['reminders'] = []
|
| 567 |
+
user_info['business_profiles'] = []
|
| 568 |
data['users'][tma_user_id_str] = user_info
|
| 569 |
initialize_user_filesystem_tma(data['users'][tma_user_id_str], tma_user_id_str)
|
| 570 |
else:
|
| 571 |
data['users'][tma_user_id_str].update(user_info)
|
| 572 |
+
if 'business_profiles' not in data['users'][tma_user_id_str]:
|
| 573 |
+
data['users'][tma_user_id_str]['business_profiles'] = []
|
| 574 |
|
| 575 |
try: save_data(data)
|
| 576 |
except Exception as e:
|
|
|
|
| 592 |
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;800&display=swap" rel="stylesheet">
|
| 593 |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
|
| 594 |
<script src="https://telegram.org/js/telegram-web-app.js"></script>
|
| 595 |
+
<style>''' + BASE_STYLE + '''
|
| 596 |
+
#fab-option-business i { color: #4caf50; }
|
| 597 |
+
</style></head><body>
|
| 598 |
<div class="app-header">
|
| 599 |
<div class="user-info">{{ display_name }}</div>
|
| 600 |
<div class="view-toggle">
|
|
|
|
| 692 |
<div class="fab-option" id="fab-option-folder"><i class="fa-solid fa-folder-plus"></i><span>Папку</span></div>
|
| 693 |
<div class="fab-option" id="fab-option-todolist" onclick="openListEditorModal(null, 'todolist')"><i class="fa-solid fa-list-check"></i><span>Список дел</span></div>
|
| 694 |
<div class="fab-option" id="fab-option-shoppinglist" onclick="openListEditorModal(null, 'shoppinglist')"><i class="fa-solid fa-cart-shopping"></i><span>Покупки</span></div>
|
| 695 |
+
<a href="{{ url_for('tma_business_profiles') }}" class="fab-option" id="fab-option-business"><i class="fa-solid fa-briefcase"></i><span>Бизнес</span></a>
|
| 696 |
</div>
|
| 697 |
<form id="upload-form" method="POST" enctype="multipart/form-data" action="{{ url_for('tma_dashboard') }}" style="display:none;">
|
| 698 |
<input type="hidden" name="current_folder_id" value="{{ current_folder_id }}">
|
|
|
|
| 1545 |
if node_type == 'folder':
|
| 1546 |
if node.get('children'): errors.append(f'Папка "{node_name}" не пуста.'); continue
|
| 1547 |
|
|
|
|
| 1548 |
if node_type in ['folder', 'note', 'todolist', 'shoppinglist', 'file']:
|
| 1549 |
if node_type == 'file':
|
| 1550 |
try:
|
|
|
|
| 2514 |
api = HfApi()
|
| 2515 |
if hf_path: api.delete_file(path_in_repo=hf_path, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE)
|
| 2516 |
except hf_utils.EntryNotFoundError:
|
| 2517 |
+
pass
|
| 2518 |
except Exception as e:
|
| 2519 |
flash(f'Deletion error from remote storage: {e}', 'error')
|
| 2520 |
return redirect(url_for('admin_user_files', tma_user_id_str=tma_user_id_str, folder_id=current_folder_id))
|
|
|
|
| 2522 |
flash('Folder is not empty.', 'error')
|
| 2523 |
return redirect(url_for('admin_user_files', tma_user_id_str=tma_user_id_str, folder_id=current_folder_id))
|
| 2524 |
|
|
|
|
| 2525 |
if remove_node(user_data['filesystem'], item_id)[0]:
|
| 2526 |
try:
|
| 2527 |
save_data(data)
|
|
|
|
| 2554 |
flash('Reminder not found.', 'error')
|
| 2555 |
return redirect(url_for('admin_user_reminders', tma_user_id_str=tma_user_id_str))
|
| 2556 |
|
| 2557 |
+
def find_business_profile_by_login(login):
|
| 2558 |
+
data = load_data()
|
| 2559 |
+
for user_id, user_data in data.get('users', {}).items():
|
| 2560 |
+
for profile in user_data.get('business_profiles', []):
|
| 2561 |
+
if profile.get('login') == login:
|
| 2562 |
+
return profile, user_data
|
| 2563 |
+
return None, None
|
| 2564 |
+
|
| 2565 |
+
def is_business_login_unique(login, user_id, profile_id=None):
|
| 2566 |
+
data = load_data()
|
| 2567 |
+
for uid, udata in data.get('users', {}).items():
|
| 2568 |
+
for profile in udata.get('business_profiles', []):
|
| 2569 |
+
if profile.get('login') == login:
|
| 2570 |
+
if uid == user_id and profile.get('id') == profile_id:
|
| 2571 |
+
continue
|
| 2572 |
+
return False
|
| 2573 |
+
return True
|
| 2574 |
+
|
| 2575 |
+
BUSINESS_PROFILES_LIST_HTML = '''
|
| 2576 |
+
<!DOCTYPE html><html lang="ru"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
| 2577 |
+
<title>Бизнес-страницы</title>
|
| 2578 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
|
| 2579 |
+
<script src="https://telegram.org/js/telegram-web-app.js"></script>
|
| 2580 |
+
<style>''' + BASE_STYLE + '''
|
| 2581 |
+
.profile-list-item { background: var(--card-bg-dark); border-radius: 12px; margin-bottom: 10px; padding: 15px; display: flex; justify-content: space-between; align-items: center; }
|
| 2582 |
+
.profile-details { text-align: left; }
|
| 2583 |
+
.profile-details a { color: var(--text-dark); text-decoration: none; font-weight: 600; }
|
| 2584 |
+
.profile-details small { color: var(--text-muted); display: block; }
|
| 2585 |
+
.profile-actions { display: flex; gap: 10px; }
|
| 2586 |
+
</style></head><body>
|
| 2587 |
+
<div class="app-header">
|
| 2588 |
+
<div class="user-info">{{ display_name }}</div>
|
| 2589 |
+
<a href="{{ url_for('tma_dashboard') }}" style="color: var(--text-muted); font-size: 1.2em; padding: 5px;"><i class="fa-solid fa-arrow-left"></i></a>
|
| 2590 |
+
</div>
|
| 2591 |
+
<div class="container">
|
| 2592 |
+
<h2>Мои бизнес-страницы</h2>
|
| 2593 |
+
{% with messages = get_flashed_messages(with_categories=true) %}{% if messages %}
|
| 2594 |
+
{% for category, message in messages %}<div class="flash {{ category }}">{{ message }}</div>{% endfor %}
|
| 2595 |
+
{% endif %}{% endwith %}
|
| 2596 |
+
<a href="{{ url_for('tma_create_business_profile') }}" class="btn" style="width: 100%; margin-bottom: 20px;">Создать новую страницу</a>
|
| 2597 |
+
<div class="profile-list">
|
| 2598 |
+
{% for profile in profiles %}
|
| 2599 |
+
<div class="profile-list-item">
|
| 2600 |
+
<div class="profile-details">
|
| 2601 |
+
<a href="{{ url_for('tma_manage_business_profile', profile_id=profile.id) }}"><strong>{{ profile.org_name }}</strong></a>
|
| 2602 |
+
<small>/biz/{{ profile.login }}</small>
|
| 2603 |
+
</div>
|
| 2604 |
+
<div class="profile-actions">
|
| 2605 |
+
<a href="{{ url_for('tma_manage_business_profile', profile_id=profile.id) }}" class="btn" style="padding: 8px 12px;"><i class="fa-solid fa-store"></i></a>
|
| 2606 |
+
</div>
|
| 2607 |
+
</div>
|
| 2608 |
+
{% else %}
|
| 2609 |
+
<p>У вас еще нет бизнес-страниц.</p>
|
| 2610 |
+
{% endfor %}
|
| 2611 |
+
</div>
|
| 2612 |
+
</div>
|
| 2613 |
+
<script>
|
| 2614 |
+
window.Telegram.WebApp.ready();
|
| 2615 |
+
window.Telegram.WebApp.BackButton.show();
|
| 2616 |
+
window.Telegram.WebApp.BackButton.onClick(() => { window.location.href = "{{ url_for('tma_dashboard') }}"; });
|
| 2617 |
+
</script>
|
| 2618 |
+
</body></html>
|
| 2619 |
+
'''
|
| 2620 |
+
|
| 2621 |
+
CREATE_EDIT_BUSINESS_PROFILE_HTML = '''
|
| 2622 |
+
<!DOCTYPE html><html lang="ru"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
| 2623 |
+
<title>{{ 'Редактировать' if profile else 'Создать' }} бизнес-страницу</title>
|
| 2624 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
|
| 2625 |
+
<script src="https://telegram.org/js/telegram-web-app.js"></script>
|
| 2626 |
+
<style>''' + BASE_STYLE + '''</style></head><body>
|
| 2627 |
+
<div class="app-header">
|
| 2628 |
+
<div class="user-info">{{ display_name }}</div>
|
| 2629 |
+
<a href="{{ url_for('tma_business_profiles') }}" style="color: var(--text-muted); font-size: 1.2em; padding: 5px;"><i class="fa-solid fa-arrow-left"></i></a>
|
| 2630 |
+
</div>
|
| 2631 |
+
<div class="container">
|
| 2632 |
+
<h2>{{ 'Редактировать' if profile else 'Создать' }} бизнес-страницу</h2>
|
| 2633 |
+
{% with messages = get_flashed_messages(with_categories=true) %}{% if messages %}
|
| 2634 |
+
{% for category, message in messages %}<div class="flash {{ category }}">{{ message }}</div>{% endfor %}
|
| 2635 |
+
{% endif %}{% endwith %}
|
| 2636 |
+
<form method="post" enctype="multipart/form-data">
|
| 2637 |
+
<label for="org_name">Название организации</label>
|
| 2638 |
+
<input type="text" name="org_name" value="{{ profile.org_name or '' }}" required>
|
| 2639 |
+
|
| 2640 |
+
<label for="login">Логин (URL)</label>
|
| 2641 |
+
<input type="text" name="login" value="{{ profile.login or '' }}" required pattern="[a-zA-Z0-9_.-]+" title="Только латинские буквы, цифры и символы _, -, .">
|
| 2642 |
+
|
| 2643 |
+
<label for="avatar">Аватар (необязательно)</label>
|
| 2644 |
+
<input type="file" name="avatar" accept="image/*">
|
| 2645 |
+
|
| 2646 |
+
<label for="currency">Валюта</label>
|
| 2647 |
+
<select name="currency" required>
|
| 2648 |
+
{% set currencies = {'тенге': 'KZT', 'рубль': 'RUB', 'кыргызский сом': 'KGS', 'узбекский сум': 'UZS', 'украинская гривна': 'UAH'} %}
|
| 2649 |
+
{% for name, code in currencies.items() %}
|
| 2650 |
+
<option value="{{ code }}" {% if profile and profile.currency == code %}selected{% endif %}>{{ name }}</option>
|
| 2651 |
+
{% endfor %}
|
| 2652 |
+
</select>
|
| 2653 |
+
|
| 2654 |
+
<label>
|
| 2655 |
+
<input type="checkbox" name="show_prices" value="true" {% if profile and profile.show_prices %}checked{% endif %} style="width: auto; margin-right: 10px;">
|
| 2656 |
+
Указывать цены
|
| 2657 |
+
</label>
|
| 2658 |
+
|
| 2659 |
+
<label for="order_destination">Куда будут приходить заказы?</label>
|
| 2660 |
+
<select name="order_destination" required>
|
| 2661 |
+
<option value="whatsapp" {% if profile and profile.order_destination == 'whatsapp' %}selected{% endif %}>WhatsApp</option>
|
| 2662 |
+
<option value="telegram" {% if profile and profile.order_destination == 'telegram' %}selected{% endif %}>Telegram</option>
|
| 2663 |
+
</select>
|
| 2664 |
+
|
| 2665 |
+
<label for="contact_info">Номер телефона или username</label>
|
| 2666 |
+
<input type="text" name="contact_info" value="{{ profile.contact_info or '' }}" placeholder="+7 (XXX) XXX-XX-XX или @username" required>
|
| 2667 |
+
|
| 2668 |
+
<button type="submit" class="btn" style="width: 100%; margin-top: 20px;">Сохранить</button>
|
| 2669 |
+
</form>
|
| 2670 |
+
</div>
|
| 2671 |
+
<script>
|
| 2672 |
+
window.Telegram.WebApp.ready();
|
| 2673 |
+
window.Telegram.WebApp.BackButton.show();
|
| 2674 |
+
window.Telegram.WebApp.BackButton.onClick(() => { window.location.href = "{{ url_for('tma_business_profiles') }}"; });
|
| 2675 |
+
</script>
|
| 2676 |
+
</body></html>
|
| 2677 |
+
'''
|
| 2678 |
+
|
| 2679 |
+
MANAGE_BUSINESS_PROFILE_HTML = '''
|
| 2680 |
+
<!DOCTYPE html><html lang="ru"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
| 2681 |
+
<title>Управление: {{ profile.org_name }}</title>
|
| 2682 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
|
| 2683 |
+
<script src="https://telegram.org/js/telegram-web-app.js"></script>
|
| 2684 |
+
<style>''' + BASE_STYLE + '''
|
| 2685 |
+
.profile-header { text-align: center; margin-bottom: 20px; }
|
| 2686 |
+
.profile-avatar { width: 100px; height: 100px; border-radius: 50%; object-fit: cover; margin-bottom: 10px; background: #333; }
|
| 2687 |
+
.public-link-bar { background: var(--card-bg-dark); padding: 10px; border-radius: 12px; display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
|
| 2688 |
+
.public-link-bar input { background: transparent; border: none; color: var(--text-muted); margin: 0; padding: 0; }
|
| 2689 |
+
.product-card { background: var(--card-bg-dark); border-radius: 12px; margin-bottom: 15px; padding: 15px; text-align: left; }
|
| 2690 |
+
.product-card img { max-width: 100%; border-radius: 8px; margin-bottom: 10px; }
|
| 2691 |
+
.product-actions { display: flex; gap: 10px; margin-top: 15px; }
|
| 2692 |
+
.add-product-form { background: var(--card-bg-dark); padding: 20px; border-radius: 12px; margin-top: 25px; }
|
| 2693 |
+
</style></head><body>
|
| 2694 |
+
<div class="app-header">
|
| 2695 |
+
<div class="user-info">{{ display_name }}</div>
|
| 2696 |
+
<a href="{{ url_for('tma_business_profiles') }}" style="color: var(--text-muted); font-size: 1.2em; padding: 5px;"><i class="fa-solid fa-arrow-left"></i></a>
|
| 2697 |
+
</div>
|
| 2698 |
+
<div class="container">
|
| 2699 |
+
{% with messages = get_flashed_messages(with_categories=true) %}{% if messages %}
|
| 2700 |
+
{% for category, message in messages %}<div class="flash {{ category }}">{{ message }}</div>{% endfor %}
|
| 2701 |
+
{% endif %}{% endwith %}
|
| 2702 |
+
<div class="profile-header">
|
| 2703 |
+
{% if profile.avatar_path %}
|
| 2704 |
+
<img src="{{ hf_file_url_jinja(profile.avatar_path) }}" class="profile-avatar">
|
| 2705 |
+
{% else %}
|
| 2706 |
+
<div class="profile-avatar" style="display: inline-flex; align-items: center; justify-content: center; font-size: 3em; color: var(--primary);">{{ profile.org_name[0] }}</div>
|
| 2707 |
+
{% endif %}
|
| 2708 |
+
<h2>{{ profile.org_name }}</h2>
|
| 2709 |
+
<a href="{{ url_for('tma_edit_business_profile', profile_id=profile.id) }}" class="btn" style="padding: 8px 15px; font-size: 0.9em;">Редактировать</a>
|
| 2710 |
+
</div>
|
| 2711 |
+
|
| 2712 |
+
<div class="public-link-bar">
|
| 2713 |
+
<input type="text" value="{{ url_for('public_business_page', login=profile.login, _external=True) }}" readonly>
|
| 2714 |
+
<button class="btn" onclick="copyToClipboard('{{ url_for('public_business_page', login=profile.login, _external=True) }}')" style="padding: 8px 12px;"><i class="fa-solid fa-copy"></i></button>
|
| 2715 |
+
</div>
|
| 2716 |
+
|
| 2717 |
+
<h3>Товары</h3>
|
| 2718 |
+
<div class="products-list">
|
| 2719 |
+
{% for product in profile.products %}
|
| 2720 |
+
<div class="product-card">
|
| 2721 |
+
{% if product.photo_path %}<img src="{{ hf_file_url_jinja(product.photo_path) }}">{% endif %}
|
| 2722 |
+
<h4>{{ product.name }}</h4>
|
| 2723 |
+
{% if profile.show_prices %}<p style="color: var(--secondary); font-weight: 600;">{{ "%.2f"|format(product.price|float) }} {{ profile.currency }}</p>{% endif %}
|
| 2724 |
+
<p style="white-space: pre-wrap; color: var(--text-muted);">{{ product.description }}</p>
|
| 2725 |
+
<div class="product-actions">
|
| 2726 |
+
<form method="post" action="{{ url_for('tma_delete_product', profile_id=profile.id, product_id=product.id) }}" onsubmit="return confirm('Удалить этот товар?');">
|
| 2727 |
+
<button type="submit" class="btn delete-btn" style="padding: 8px 15px;">Удалить</button>
|
| 2728 |
+
</form>
|
| 2729 |
+
</div>
|
| 2730 |
+
</div>
|
| 2731 |
+
{% else %}
|
| 2732 |
+
<p>Вы еще не добавили ни одного товара.</p>
|
| 2733 |
+
{% endfor %}
|
| 2734 |
+
</div>
|
| 2735 |
+
|
| 2736 |
+
<div class="add-product-form">
|
| 2737 |
+
<h4>Добавить новый товар</h4>
|
| 2738 |
+
<form method="post" action="{{ url_for('tma_add_product', profile_id=profile.id) }}" enctype="multipart/form-data">
|
| 2739 |
+
<label for="product_name">Название товара</label>
|
| 2740 |
+
<input type="text" name="product_name" required>
|
| 2741 |
+
|
| 2742 |
+
<label for="product_photo">Фото товара</label>
|
| 2743 |
+
<input type="file" name="product_photo" accept="image/*" required>
|
| 2744 |
+
|
| 2745 |
+
<label for="product_description">Описание</label>
|
| 2746 |
+
<textarea name="product_description" rows="4"></textarea>
|
| 2747 |
+
|
| 2748 |
+
{% if profile.show_prices %}
|
| 2749 |
+
<label for="product_price">Цена ({{ profile.currency }})</label>
|
| 2750 |
+
<input type="number" name="product_price" step="0.01" min="0">
|
| 2751 |
+
{% endif %}
|
| 2752 |
+
|
| 2753 |
+
<button type="submit" class="btn" style="width: 100%; margin-top: 15px;">Добавить товар</button>
|
| 2754 |
+
</form>
|
| 2755 |
+
</div>
|
| 2756 |
+
</div>
|
| 2757 |
+
<script>
|
| 2758 |
+
window.Telegram.WebApp.ready();
|
| 2759 |
+
window.Telegram.WebApp.BackButton.show();
|
| 2760 |
+
window.Telegram.WebApp.BackButton.onClick(() => { window.location.href = "{{ url_for('tma_business_profiles') }}"; });
|
| 2761 |
+
|
| 2762 |
+
function copyToClipboard(text) {
|
| 2763 |
+
navigator.clipboard.writeText(text).then(() => {
|
| 2764 |
+
window.Telegram.WebApp.HapticFeedback.notificationOccurred('success');
|
| 2765 |
+
window.Telegram.WebApp.showAlert('Ссылка скопирована!');
|
| 2766 |
+
}, () => {
|
| 2767 |
+
window.Telegram.WebApp.HapticFeedback.notificationOccurred('error');
|
| 2768 |
+
});
|
| 2769 |
+
}
|
| 2770 |
+
</script>
|
| 2771 |
+
</body></html>
|
| 2772 |
+
'''
|
| 2773 |
+
|
| 2774 |
+
PUBLIC_BUSINESS_PAGE_HTML = '''
|
| 2775 |
+
<!DOCTYPE html><html lang="ru"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 2776 |
+
<title>{{ profile.org_name }}</title>
|
| 2777 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
|
| 2778 |
+
<style>''' + BASE_STYLE + '''
|
| 2779 |
+
body { padding-bottom: 80px; }
|
| 2780 |
+
.public-header { padding: 20px; text-align: center; }
|
| 2781 |
+
.public-avatar { width: 120px; height: 120px; border-radius: 50%; object-fit: cover; margin-bottom: 15px; border: 3px solid var(--card-bg-dark); }
|
| 2782 |
+
.product-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 20px; }
|
| 2783 |
+
.product-public-card { background: var(--card-bg-dark); border-radius: 16px; overflow: hidden; text-align: left; }
|
| 2784 |
+
.product-public-card img { width: 100%; height: 200px; object-fit: cover; display: block; }
|
| 2785 |
+
.product-public-info { padding: 15px; }
|
| 2786 |
+
.product-public-info h4 { font-size: 1.2em; margin-bottom: 5px; }
|
| 2787 |
+
.product-public-info .price { font-size: 1.1em; font-weight: 600; color: var(--secondary); margin-bottom: 10px; }
|
| 2788 |
+
.product-public-info .desc { color: var(--text-muted); font-size: 0.9em; }
|
| 2789 |
+
.order-button-container { position: fixed; bottom: 0; left: 0; right: 0; padding: 15px; background: var(--glass-bg); backdrop-filter: blur(10px); text-align: center; }
|
| 2790 |
+
</style></head><body>
|
| 2791 |
+
<div class="public-header">
|
| 2792 |
+
{% if profile.avatar_path %}
|
| 2793 |
+
<img src="{{ hf_file_url_jinja(profile.avatar_path) }}" class="public-avatar">
|
| 2794 |
+
{% else %}
|
| 2795 |
+
<div class="public-avatar" style="display: inline-flex; align-items: center; justify-content: center; font-size: 4em; color: var(--primary);">{{ profile.org_name[0] }}</div>
|
| 2796 |
+
{% endif %}
|
| 2797 |
+
<h1>{{ profile.org_name }}</h1>
|
| 2798 |
+
</div>
|
| 2799 |
+
<div class="container" style="padding-top: 0;">
|
| 2800 |
+
<div class="product-grid">
|
| 2801 |
+
{% for product in profile.products %}
|
| 2802 |
+
<div class="product-public-card">
|
| 2803 |
+
{% if product.photo_path %}<img src="{{ hf_file_url_jinja(product.photo_path) }}">{% endif %}
|
| 2804 |
+
<div class="product-public-info">
|
| 2805 |
+
<h4>{{ product.name }}</h4>
|
| 2806 |
+
{% if profile.show_prices and product.price is not none %}
|
| 2807 |
+
<p class="price">{{ "%.2f"|format(product.price|float) }} {{ profile.currency }}</p>
|
| 2808 |
+
{% endif %}
|
| 2809 |
+
<p class="desc">{{ product.description }}</p>
|
| 2810 |
+
</div>
|
| 2811 |
+
</div>
|
| 2812 |
+
{% else %}
|
| 2813 |
+
<p style="grid-column: 1 / -1; text-align: center;">Товары скоро появятся.</p>
|
| 2814 |
+
{% endfor %}
|
| 2815 |
+
</div>
|
| 2816 |
+
</div>
|
| 2817 |
+
<div class="order-button-container">
|
| 2818 |
+
<a href="{{ order_link }}" target="_blank" class="btn" style="width: 100%; max-width: 400px; background: {{ '#25D366' if profile.order_destination == 'whatsapp' else '#0088cc' }};">
|
| 2819 |
+
<i class="fa-brands fa-{{ 'whatsapp' if profile.order_destination == 'whatsapp' else 'telegram' }}"></i>
|
| 2820 |
+
Заказать в {{ profile.order_destination.capitalize() }}
|
| 2821 |
+
</a>
|
| 2822 |
+
</div>
|
| 2823 |
+
</body></html>
|
| 2824 |
+
'''
|
| 2825 |
+
|
| 2826 |
if __name__ == '__main__':
|
| 2827 |
if not HF_TOKEN_WRITE: logging.warning("HF_TOKEN (write) is not set. Uploads/deletions will fail.")
|
| 2828 |
if not HF_TOKEN_READ: logging.warning("HF_TOKEN_READ is not set. Downloads/previews might fail.")
|
|
|
|
| 2842 |
threading.Thread(target=check_reminders, daemon=True).start()
|
| 2843 |
|
| 2844 |
app.run(debug=False, host='0.0.0.0', port=7860)
|
|
|