Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,7 +1,5 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
|
| 3 |
import os
|
| 4 |
-
from flask import Flask, request, Response, render_template_string, jsonify, redirect, url_for
|
| 5 |
import hmac
|
| 6 |
import hashlib
|
| 7 |
import json
|
|
@@ -11,11 +9,13 @@ from datetime import datetime
|
|
| 11 |
import logging
|
| 12 |
import threading
|
| 13 |
import random
|
| 14 |
-
import pytz
|
| 15 |
-
import uuid
|
| 16 |
-
|
| 17 |
from huggingface_hub import HfApi, hf_hub_download
|
| 18 |
from huggingface_hub.utils import RepositoryNotFoundError
|
|
|
|
|
|
|
| 19 |
|
| 20 |
BOT_TOKEN = os.getenv("BOT_TOKEN", "7835463659:AAGNePbelZIAOeaglyQi1qulOqnjs4BGQn4")
|
| 21 |
HOST = '0.0.0.0'
|
|
@@ -27,19 +27,17 @@ HF_DATA_FILE_PATH = "data.json"
|
|
| 27 |
HF_TOKEN_WRITE = os.getenv("HF_TOKEN_WRITE")
|
| 28 |
HF_TOKEN_READ = os.getenv("HF_TOKEN_READ")
|
| 29 |
|
| 30 |
-
# Define Bishkek timezone
|
| 31 |
BISHKEK_TZ = pytz.timezone('Asia/Bishkek')
|
| 32 |
|
| 33 |
app = Flask(__name__)
|
| 34 |
-
logging.basicConfig(level=logging.
|
| 35 |
app.secret_key = os.urandom(24)
|
| 36 |
|
| 37 |
_data_lock = threading.Lock()
|
| 38 |
-
visitor_data_cache = {}
|
| 39 |
|
| 40 |
def generate_unique_id(all_data):
|
| 41 |
while True:
|
| 42 |
-
# Check against both client IDs and invoice IDs
|
| 43 |
new_id = str(random.randint(10000, 99999))
|
| 44 |
if new_id not in all_data:
|
| 45 |
return new_id
|
|
@@ -47,10 +45,8 @@ def generate_unique_id(all_data):
|
|
| 47 |
def download_data_from_hf():
|
| 48 |
global visitor_data_cache
|
| 49 |
if not HF_TOKEN_READ:
|
| 50 |
-
logging.warning("HF_TOKEN_READ not set. Skipping Hugging Face download.")
|
| 51 |
return False
|
| 52 |
try:
|
| 53 |
-
logging.info(f"Attempting to download {HF_DATA_FILE_PATH} from {REPO_ID}...")
|
| 54 |
hf_hub_download(
|
| 55 |
repo_id=REPO_ID,
|
| 56 |
filename=HF_DATA_FILE_PATH,
|
|
@@ -61,41 +57,33 @@ def download_data_from_hf():
|
|
| 61 |
force_download=True,
|
| 62 |
etag_timeout=10
|
| 63 |
)
|
| 64 |
-
logging.info("Data file successfully downloaded from Hugging Face.")
|
| 65 |
with _data_lock:
|
| 66 |
try:
|
| 67 |
with open(DATA_FILE, 'r', encoding='utf-8') as f:
|
| 68 |
visitor_data_cache = json.load(f)
|
| 69 |
-
|
| 70 |
-
except (FileNotFoundError, json.JSONDecodeError) as e:
|
| 71 |
-
logging.error(f"Error reading downloaded data file: {e}. Starting with empty cache.")
|
| 72 |
visitor_data_cache = {}
|
| 73 |
return True
|
| 74 |
except RepositoryNotFoundError:
|
| 75 |
-
|
| 76 |
-
except Exception
|
| 77 |
-
|
| 78 |
return False
|
| 79 |
|
| 80 |
def load_visitor_data():
|
| 81 |
global visitor_data_cache
|
| 82 |
with _data_lock:
|
| 83 |
-
if not visitor_data_cache:
|
| 84 |
try:
|
| 85 |
with open(DATA_FILE, 'r', encoding='utf-8') as f:
|
| 86 |
visitor_data_cache = json.load(f)
|
| 87 |
-
logging.info("Visitor data loaded from local JSON.")
|
| 88 |
except FileNotFoundError:
|
| 89 |
-
|
| 90 |
-
visitor_data_cache = {"organization_details": {}} # Initialize with empty org details
|
| 91 |
except json.JSONDecodeError:
|
| 92 |
-
logging.error(f"Error decoding {DATA_FILE}. Starting with empty data.")
|
| 93 |
visitor_data_cache = {"organization_details": {}}
|
| 94 |
-
except Exception
|
| 95 |
-
logging.error(f"Unexpected error loading visitor data: {e}")
|
| 96 |
visitor_data_cache = {"organization_details": {}}
|
| 97 |
|
| 98 |
-
# Ensure organization_details key exists
|
| 99 |
if "organization_details" not in visitor_data_cache:
|
| 100 |
visitor_data_cache["organization_details"] = {}
|
| 101 |
|
|
@@ -104,42 +92,16 @@ def load_visitor_data():
|
|
| 104 |
def save_visitor_data(data):
|
| 105 |
with _data_lock:
|
| 106 |
try:
|
| 107 |
-
# When `data` is a dictionary, update it directly.
|
| 108 |
-
# If `data` is a partial update for `visitor_data_cache`, merge it.
|
| 109 |
-
# For simplicity, this function now assumes `data` is the complete `visitor_data_cache`
|
| 110 |
-
# or a mergeable dictionary that should be applied to the cache before saving.
|
| 111 |
-
# Given current usage, it's typically `save_visitor_data({user_id: user_entry})`
|
| 112 |
-
# or `save_visitor_data({"organization_details": new_org_details})` etc.
|
| 113 |
-
# It should ideally update the global `visitor_data_cache` and then dump it.
|
| 114 |
-
|
| 115 |
-
# This line needs to be careful: if `data` is a single user, it overwrites.
|
| 116 |
-
# It's better to update specific parts of the cache or always pass the full cache.
|
| 117 |
-
# Let's adjust existing call sites to pass the full `all_data` after modification.
|
| 118 |
-
# For now, let's assume `data` is what needs to be *merged* into `visitor_data_cache`
|
| 119 |
-
# or `data` IS the new `visitor_data_cache`.
|
| 120 |
-
|
| 121 |
-
# A more robust approach for `save_visitor_data` would be:
|
| 122 |
-
# 1. Take a user_id and user_data to update a specific user
|
| 123 |
-
# 2. Take an org_details dict to update org details
|
| 124 |
-
# 3. Then, always dump the *entire* `visitor_data_cache`.
|
| 125 |
-
|
| 126 |
-
# Simpler change for existing code:
|
| 127 |
-
# Ensure `visitor_data_cache` is directly modified by operations,
|
| 128 |
-
# and `save_visitor_data` just dumps the current `visitor_data_cache`.
|
| 129 |
-
|
| 130 |
with open(DATA_FILE, 'w', encoding='utf-8') as f:
|
| 131 |
json.dump(visitor_data_cache, f, ensure_ascii=False, indent=4)
|
| 132 |
-
logging.info(f"Visitor data successfully saved to {DATA_FILE}.")
|
| 133 |
upload_data_to_hf_async()
|
| 134 |
-
except Exception
|
| 135 |
-
|
| 136 |
|
| 137 |
def upload_data_to_hf():
|
| 138 |
if not HF_TOKEN_WRITE:
|
| 139 |
-
logging.warning("HF_TOKEN_WRITE not set. Skipping Hugging Face upload.")
|
| 140 |
return
|
| 141 |
if not os.path.exists(DATA_FILE):
|
| 142 |
-
logging.warning(f"{DATA_FILE} does not exist. Skipping upload.")
|
| 143 |
return
|
| 144 |
|
| 145 |
try:
|
|
@@ -147,10 +109,8 @@ def upload_data_to_hf():
|
|
| 147 |
with _data_lock:
|
| 148 |
file_content_exists = os.path.getsize(DATA_FILE) > 0
|
| 149 |
if not file_content_exists:
|
| 150 |
-
logging.warning(f"{DATA_FILE} is empty. Skipping upload.")
|
| 151 |
return
|
| 152 |
|
| 153 |
-
logging.info(f"Attempting to upload {DATA_FILE} to {REPO_ID}/{HF_DATA_FILE_PATH}...")
|
| 154 |
api.upload_file(
|
| 155 |
path_or_fileobj=DATA_FILE,
|
| 156 |
path_in_repo=HF_DATA_FILE_PATH,
|
|
@@ -159,9 +119,8 @@ def upload_data_to_hf():
|
|
| 159 |
token=HF_TOKEN_WRITE,
|
| 160 |
commit_message=f"Update bonus data {datetime.now(BISHKEK_TZ).strftime('%Y-%m-%d %H:%M:%S')}"
|
| 161 |
)
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
logging.error(f"Error uploading data to Hugging Face: {e}")
|
| 165 |
|
| 166 |
def upload_data_to_hf_async():
|
| 167 |
upload_thread = threading.Thread(target=upload_data_to_hf, daemon=True)
|
|
@@ -169,11 +128,9 @@ def upload_data_to_hf_async():
|
|
| 169 |
|
| 170 |
def periodic_backup():
|
| 171 |
if not HF_TOKEN_WRITE:
|
| 172 |
-
logging.info("Periodic backup disabled: HF_TOKEN_WRITE not set.")
|
| 173 |
return
|
| 174 |
while True:
|
| 175 |
time.sleep(3600)
|
| 176 |
-
logging.info("Initiating periodic backup...")
|
| 177 |
upload_data_to_hf()
|
| 178 |
|
| 179 |
def verify_telegram_data(init_data_str):
|
|
@@ -196,13 +153,12 @@ def verify_telegram_data(init_data_str):
|
|
| 196 |
auth_date = int(parsed_data.get('auth_date', [0])[0])
|
| 197 |
current_time = int(time.time())
|
| 198 |
if current_time - auth_date > 86400:
|
| 199 |
-
|
| 200 |
return parsed_data, True
|
| 201 |
else:
|
| 202 |
-
|
| 203 |
return parsed_data, False
|
| 204 |
-
except Exception
|
| 205 |
-
logging.error(f"Error verifying Telegram data: {e}")
|
| 206 |
return None, False
|
| 207 |
|
| 208 |
TEMPLATE = """
|
|
@@ -255,7 +211,7 @@ TEMPLATE = """
|
|
| 255 |
.header {
|
| 256 |
text-align: left;
|
| 257 |
padding: var(--padding-m) 0;
|
| 258 |
-
margin-bottom: 0;
|
| 259 |
}
|
| 260 |
.logo {
|
| 261 |
font-size: 2.5em;
|
|
@@ -296,12 +252,12 @@ TEMPLATE = """
|
|
| 296 |
box-shadow: 0 2px 10px rgba(255,193,7,0.3);
|
| 297 |
}
|
| 298 |
.content-section {
|
| 299 |
-
display: none;
|
| 300 |
flex-direction: column;
|
| 301 |
gap: var(--padding-m);
|
| 302 |
}
|
| 303 |
.content-section.active {
|
| 304 |
-
display: flex;
|
| 305 |
}
|
| 306 |
.card-grid {
|
| 307 |
display: grid;
|
|
@@ -365,7 +321,7 @@ TEMPLATE = """
|
|
| 365 |
padding: 4px 10px;
|
| 366 |
border-radius: 8px;
|
| 367 |
}
|
| 368 |
-
.history-section, .invoices-section, .business-card-section {
|
| 369 |
background-color: var(--card-bg);
|
| 370 |
border-radius: var(--border-radius);
|
| 371 |
padding: var(--padding-l);
|
|
@@ -373,7 +329,7 @@ TEMPLATE = """
|
|
| 373 |
flex-direction: column;
|
| 374 |
gap: var(--padding-m);
|
| 375 |
}
|
| 376 |
-
.history-title, .invoices-title, .business-card-title {
|
| 377 |
font-size: 1.4em;
|
| 378 |
font-weight: 700;
|
| 379 |
padding-bottom: var(--padding-m);
|
|
@@ -407,26 +363,25 @@ TEMPLATE = """
|
|
| 407 |
padding: 2rem 0;
|
| 408 |
}
|
| 409 |
|
| 410 |
-
|
| 411 |
-
.business-card-item {
|
| 412 |
margin-bottom: 10px;
|
| 413 |
}
|
| 414 |
-
.business-card-label {
|
| 415 |
font-weight: 500;
|
| 416 |
color: var(--text-secondary-color);
|
| 417 |
margin-bottom: 4px;
|
| 418 |
}
|
| 419 |
-
.business-card-value {
|
| 420 |
font-size: 1.1em;
|
| 421 |
font-weight: 600;
|
| 422 |
color: var(--text-color);
|
| 423 |
}
|
| 424 |
-
.business-card-value a {
|
| 425 |
color: var(--brand-yellow);
|
| 426 |
text-decoration: none;
|
| 427 |
-
word-break: break-all;
|
| 428 |
}
|
| 429 |
-
.business-card-value a:hover {
|
| 430 |
text-decoration: underline;
|
| 431 |
}
|
| 432 |
.business-card-phone-list {
|
|
@@ -456,7 +411,6 @@ TEMPLATE = """
|
|
| 456 |
width: 20px;
|
| 457 |
}
|
| 458 |
|
| 459 |
-
/* Invoice Detail Modal */
|
| 460 |
.modal {
|
| 461 |
display: none;
|
| 462 |
position: fixed;
|
|
@@ -546,6 +500,64 @@ TEMPLATE = """
|
|
| 546 |
.invoice-total-display span:last-child {
|
| 547 |
color: var(--brand-yellow);
|
| 548 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 549 |
</style>
|
| 550 |
</head>
|
| 551 |
<body>
|
|
@@ -558,6 +570,7 @@ TEMPLATE = """
|
|
| 558 |
<nav class="nav-buttons">
|
| 559 |
<button class="nav-btn active" data-target="dashboard-section">Главная</button>
|
| 560 |
<button class="nav-btn" data-target="invoices-section">Накладные</button>
|
|
|
|
| 561 |
<button class="nav-btn" data-target="business-card-section">Визитка</button>
|
| 562 |
</nav>
|
| 563 |
|
|
@@ -627,6 +640,27 @@ TEMPLATE = """
|
|
| 627 |
</section>
|
| 628 |
</div>
|
| 629 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 630 |
<div id="business-card-section" class="content-section">
|
| 631 |
<section class="business-card-section">
|
| 632 |
<h2 class="business-card-title">Визитка организации</h2>
|
|
@@ -642,7 +676,7 @@ TEMPLATE = """
|
|
| 642 |
{% for phone in org_details.phone_numbers %}
|
| 643 |
<li class="business-card-phone-item">
|
| 644 |
<a href="tel:{{ phone }}">
|
| 645 |
-
<img src="data:image/svg+xml;base64,
|
| 646 |
{{ phone }}
|
| 647 |
</a>
|
| 648 |
</li>
|
|
@@ -661,7 +695,7 @@ TEMPLATE = """
|
|
| 661 |
<div class="business-card-value">
|
| 662 |
{% if org_details.whatsapp_link %}
|
| 663 |
<a href="{{ org_details.whatsapp_link }}" target="_blank">
|
| 664 |
-
<img src="data:image/svg+xml;base64,
|
| 665 |
{{ org_details.whatsapp_link }}
|
| 666 |
</a>
|
| 667 |
{% else %}
|
|
@@ -674,7 +708,7 @@ TEMPLATE = """
|
|
| 674 |
<div class="business-card-value">
|
| 675 |
{% if org_details.telegram_link %}
|
| 676 |
<a href="{{ org_details.telegram_link }}" target="_blank">
|
| 677 |
-
<img src="data:image/svg+xml;base64,
|
| 678 |
{{ org_details.telegram_link }}
|
| 679 |
</a>
|
| 680 |
{% else %}
|
|
@@ -689,13 +723,11 @@ TEMPLATE = """
|
|
| 689 |
</div>
|
| 690 |
</div>
|
| 691 |
|
| 692 |
-
<!-- Invoice Detail Modal -->
|
| 693 |
<div id="invoiceDetailModal" class="modal">
|
| 694 |
<div class="modal-content">
|
| 695 |
<span class="modal-close" onclick="closeModal('invoiceDetailModal')">×</span>
|
| 696 |
<h2 id="invoiceDetailTitle" class="modal-title"></h2>
|
| 697 |
<ul id="invoiceDetailList" class="invoice-detail-list">
|
| 698 |
-
<!-- Invoice items will be loaded here -->
|
| 699 |
</ul>
|
| 700 |
<div id="invoiceDetailTotal" class="invoice-total-display">
|
| 701 |
<span>Итого:</span>
|
|
@@ -704,8 +736,11 @@ TEMPLATE = """
|
|
| 704 |
</div>
|
| 705 |
</div>
|
| 706 |
|
|
|
|
|
|
|
| 707 |
<script>
|
| 708 |
const tg = window.Telegram.WebApp;
|
|
|
|
| 709 |
|
| 710 |
function applyTheme(themeParams) {
|
| 711 |
const root = document.documentElement;
|
|
@@ -720,7 +755,6 @@ TEMPLATE = """
|
|
| 720 |
|
| 721 |
function setupTelegram() {
|
| 722 |
if (!tg || !tg.initData) {
|
| 723 |
-
console.error("Telegram WebApp script not loaded or initData is missing.");
|
| 724 |
document.body.style.visibility = 'visible';
|
| 725 |
return;
|
| 726 |
}
|
|
@@ -747,12 +781,10 @@ TEMPLATE = """
|
|
| 747 |
if (data.status === 'ok' && data.verified && data.user_id) {
|
| 748 |
window.location.replace('/?user_id_for_test=' + data.user_id);
|
| 749 |
} else {
|
| 750 |
-
console.warn('Backend verification failed:', data.message);
|
| 751 |
document.body.style.visibility = 'visible';
|
| 752 |
}
|
| 753 |
})
|
| 754 |
-
.catch(
|
| 755 |
-
console.error('Error sending initData for verification:', error);
|
| 756 |
document.body.style.visibility = 'visible';
|
| 757 |
});
|
| 758 |
} else {
|
|
@@ -814,7 +846,6 @@ TEMPLATE = """
|
|
| 814 |
});
|
| 815 |
});
|
| 816 |
|
| 817 |
-
// Initial section display
|
| 818 |
showSection('dashboard-section');
|
| 819 |
});
|
| 820 |
|
|
@@ -828,6 +859,133 @@ TEMPLATE = """
|
|
| 828 |
}
|
| 829 |
}, 3000);
|
| 830 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 831 |
</script>
|
| 832 |
</body>
|
| 833 |
</html>
|
|
@@ -921,7 +1079,6 @@ ADMIN_TEMPLATE = """
|
|
| 921 |
.btn-submit { background-color: var(--admin-success); color: white; }
|
| 922 |
.status-message { text-align: center; font-weight: 500; flex-grow: 1; text-align: left; }
|
| 923 |
|
| 924 |
-
/* Tabs for Transaction Modal */
|
| 925 |
.tab-buttons {
|
| 926 |
display: flex;
|
| 927 |
margin-bottom: 1rem;
|
|
@@ -948,7 +1105,6 @@ ADMIN_TEMPLATE = """
|
|
| 948 |
display: block;
|
| 949 |
}
|
| 950 |
|
| 951 |
-
/* Invoice table */
|
| 952 |
.invoice-items-table {
|
| 953 |
width: 100%;
|
| 954 |
border-collapse: collapse;
|
|
@@ -1098,7 +1254,6 @@ ADMIN_TEMPLATE = """
|
|
| 1098 |
{% endif %}
|
| 1099 |
</div>
|
| 1100 |
|
| 1101 |
-
<!-- Transaction/Invoice Modal -->
|
| 1102 |
<div id="transactionModal" class="modal">
|
| 1103 |
<div class="modal-content">
|
| 1104 |
<span class="modal-close" onclick="closeModal('transactionModal')">×</span>
|
|
@@ -1180,7 +1335,6 @@ ADMIN_TEMPLATE = """
|
|
| 1180 |
</tr>
|
| 1181 |
</thead>
|
| 1182 |
<tbody>
|
| 1183 |
-
<!-- New invoice items will be added here -->
|
| 1184 |
</tbody>
|
| 1185 |
<tfoot>
|
| 1186 |
<tr>
|
|
@@ -1205,7 +1359,6 @@ ADMIN_TEMPLATE = """
|
|
| 1205 |
</div>
|
| 1206 |
</div>
|
| 1207 |
|
| 1208 |
-
<!-- Add Client Modal -->
|
| 1209 |
<div id="addClientModal" class="modal">
|
| 1210 |
<div class="modal-content">
|
| 1211 |
<span class="modal-close" onclick="closeModal('addClientModal')">×</span>
|
|
@@ -1227,7 +1380,6 @@ ADMIN_TEMPLATE = """
|
|
| 1227 |
</div>
|
| 1228 |
</div>
|
| 1229 |
|
| 1230 |
-
<!-- Organization Settings Modal -->
|
| 1231 |
<div id="orgSettingsModal" class="modal">
|
| 1232 |
<div class="modal-content">
|
| 1233 |
<span class="modal-close" onclick="closeModal('orgSettingsModal')">×</span>
|
|
@@ -1263,13 +1415,11 @@ ADMIN_TEMPLATE = """
|
|
| 1263 |
</div>
|
| 1264 |
</div>
|
| 1265 |
|
| 1266 |
-
<!-- Invoice Detail Modal (for Admin) -->
|
| 1267 |
<div id="adminInvoiceDetailModal" class="modal">
|
| 1268 |
<div class="modal-content">
|
| 1269 |
<span class="modal-close" onclick="closeModal('adminInvoiceDetailModal')">×</span>
|
| 1270 |
<h2 id="adminInvoiceDetailTitle" class="modal-title"></h2>
|
| 1271 |
<ul id="adminInvoiceDetailList" class="invoice-detail-list">
|
| 1272 |
-
<!-- Invoice items will be loaded here -->
|
| 1273 |
</ul>
|
| 1274 |
<div id="adminInvoiceDetailTotal" class="invoice-total-display">
|
| 1275 |
<span>Итого:</span>
|
|
@@ -1311,13 +1461,11 @@ ADMIN_TEMPLATE = """
|
|
| 1311 |
document.getElementById('modalStatus').textContent = '';
|
| 1312 |
document.getElementById('invoiceStatus').textContent = '';
|
| 1313 |
|
| 1314 |
-
// Reset new invoice items
|
| 1315 |
newInvoiceItems = [];
|
| 1316 |
renderNewInvoiceItems();
|
| 1317 |
|
| 1318 |
-
loadUserHistoryAndInvoices();
|
| 1319 |
|
| 1320 |
-
// Set default tab
|
| 1321 |
showTab('bonus-debt-tab');
|
| 1322 |
|
| 1323 |
transactionModal.style.display = 'block';
|
|
@@ -1340,8 +1488,8 @@ ADMIN_TEMPLATE = """
|
|
| 1340 |
sign = item.type === 'accrual' ? '+' : '-';
|
| 1341 |
amountClass = item.type === 'accrual' ? 'bonus-accrual' : 'bonus-deduction';
|
| 1342 |
amountText = `${sign}${parseFloat(item.amount).toFixed(2)}`;
|
| 1343 |
-
} else {
|
| 1344 |
-
sign = item.type === 'accrual' ? '+' : '-';
|
| 1345 |
amountClass = item.type === 'accrual' ? 'debt-accrual' : 'debt-payment';
|
| 1346 |
amountText = `${item.type === 'accrual' ? '+' : '-'}${parseFloat(item.amount).toFixed(2)}`;
|
| 1347 |
}
|
|
@@ -1358,7 +1506,6 @@ ADMIN_TEMPLATE = """
|
|
| 1358 |
historyList.innerHTML = '<li style="text-align:center; padding: 1rem; color: var(--admin-secondary);">Нет истории</li>';
|
| 1359 |
}
|
| 1360 |
|
| 1361 |
-
// Load invoices for invoice tab
|
| 1362 |
const modalInvoiceList = document.getElementById('modalInvoiceList');
|
| 1363 |
modalInvoiceList.innerHTML = '';
|
| 1364 |
const userInvoices = (currentUserData.invoices || []).sort((a, b) => new Date(b.date) - new Date(a.date));
|
|
@@ -1405,8 +1552,7 @@ ADMIN_TEMPLATE = """
|
|
| 1405 |
document.getElementById('orgStatus').textContent = '';
|
| 1406 |
orgSettingsModal.style.display = 'block';
|
| 1407 |
})
|
| 1408 |
-
.catch(
|
| 1409 |
-
console.error('Error fetching organization details:', error);
|
| 1410 |
document.getElementById('orgStatus').style.color = 'var(--admin-danger)';
|
| 1411 |
document.getElementById('orgStatus').textContent = 'Ошибка загрузки данных.';
|
| 1412 |
orgSettingsModal.style.display = 'block';
|
|
@@ -1605,7 +1751,7 @@ ADMIN_TEMPLATE = """
|
|
| 1605 |
function addNewInvoiceItemRow() {
|
| 1606 |
const tableBody = document.getElementById('newInvoiceItemsTable').getElementsByTagName('tbody')[0];
|
| 1607 |
const newRow = tableBody.insertRow();
|
| 1608 |
-
const rowIndex = tableBody.rows.length - 1;
|
| 1609 |
|
| 1610 |
newInvoiceItems.push({
|
| 1611 |
product_name: '',
|
|
@@ -1640,15 +1786,14 @@ ADMIN_TEMPLATE = """
|
|
| 1640 |
|
| 1641 |
function removeInvoiceItemRow(button, index) {
|
| 1642 |
const tableBody = document.getElementById('newInvoiceItemsTable').getElementsByTagName('tbody')[0];
|
| 1643 |
-
tableBody.deleteRow(button.parentNode.parentNode.rowIndex - 1);
|
| 1644 |
|
| 1645 |
-
// Re-index newInvoiceItems and update the oninput attributes
|
| 1646 |
newInvoiceItems.splice(index, 1);
|
| 1647 |
for (let i = 0; i < tableBody.rows.length; i++) {
|
| 1648 |
const row = tableBody.rows[i];
|
| 1649 |
row.querySelector('input[type="text"]').setAttribute('oninput', `updateInvoiceItem(${i}, 'product_name', this.value)`);
|
| 1650 |
-
row.querySelector('input[type="number
|
| 1651 |
-
row.querySelector('input[type="number
|
| 1652 |
row.querySelector('.action-btn').setAttribute('onclick', `removeInvoiceItemRow(this, ${i})`);
|
| 1653 |
}
|
| 1654 |
|
|
@@ -1757,7 +1902,7 @@ ADMIN_TEMPLATE = """
|
|
| 1757 |
});
|
| 1758 |
const result = await response.json();
|
| 1759 |
if (response.ok) {
|
| 1760 |
-
location.reload();
|
| 1761 |
} else {
|
| 1762 |
throw new Error(result.message || 'Не удалось удалить накладную.');
|
| 1763 |
}
|
|
@@ -1782,20 +1927,35 @@ ADMIN_TEMPLATE = """
|
|
| 1782 |
}
|
| 1783 |
}
|
| 1784 |
|
| 1785 |
-
// Initial row for new invoice
|
| 1786 |
document.addEventListener('DOMContentLoaded', () => {
|
| 1787 |
-
addNewInvoiceItemRow();
|
| 1788 |
});
|
| 1789 |
</script>
|
| 1790 |
</body>
|
| 1791 |
</html>
|
| 1792 |
"""
|
| 1793 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1794 |
@app.route('/')
|
| 1795 |
def index():
|
| 1796 |
user_id_str = request.args.get('user_id_for_test')
|
| 1797 |
|
| 1798 |
-
all_data = load_visitor_data()
|
| 1799 |
user_data = {}
|
| 1800 |
|
| 1801 |
if user_id_str and user_id_str in all_data:
|
|
@@ -1816,7 +1976,8 @@ def index():
|
|
| 1816 |
reverse=True
|
| 1817 |
)
|
| 1818 |
user_data['combined_history'] = combined_history
|
| 1819 |
-
user_data['invoices'] = user_data.get('invoices', [])
|
|
|
|
| 1820 |
else:
|
| 1821 |
user_data = {
|
| 1822 |
"id": "N/A",
|
|
@@ -1825,7 +1986,8 @@ def index():
|
|
| 1825 |
"history": [],
|
| 1826 |
"debt_history": [],
|
| 1827 |
"combined_history": [],
|
| 1828 |
-
"invoices": []
|
|
|
|
| 1829 |
}
|
| 1830 |
|
| 1831 |
org_details = all_data.get('organization_details', {})
|
|
@@ -1847,19 +2009,17 @@ def verify_data():
|
|
| 1847 |
try:
|
| 1848 |
user_json_str = unquote(user_data_parsed['user'][0])
|
| 1849 |
user_info_dict = json.loads(user_json_str)
|
| 1850 |
-
except Exception
|
| 1851 |
-
logging.error(f"Could not parse user JSON: {e}")
|
| 1852 |
user_info_dict = {}
|
| 1853 |
|
| 1854 |
if is_valid:
|
| 1855 |
tg_user_id = user_info_dict.get('id')
|
| 1856 |
if tg_user_id:
|
| 1857 |
now = datetime.now(BISHKEK_TZ)
|
| 1858 |
-
all_data = load_visitor_data()
|
| 1859 |
|
| 1860 |
existing_user_key = None
|
| 1861 |
for key, user_data_item in all_data.items():
|
| 1862 |
-
# Skip 'organization_details' when iterating through users
|
| 1863 |
if key == "organization_details":
|
| 1864 |
continue
|
| 1865 |
if str(user_data_item.get('telegram_id')) == str(tg_user_id):
|
|
@@ -1889,29 +2049,71 @@ def verify_data():
|
|
| 1889 |
'photo_url': user_info_dict.get('photo_url'),
|
| 1890 |
'language_code': user_info_dict.get('language_code'),
|
| 1891 |
'is_premium': user_info_dict.get('is_premium', False),
|
| 1892 |
-
'phone_number': None,
|
| 1893 |
'visited_at': now.timestamp(),
|
| 1894 |
'visited_at_str': now.strftime('%Y-%m-%d %H:%M:%S'),
|
| 1895 |
'bonuses': 0,
|
| 1896 |
'history': [],
|
| 1897 |
'debts': 0,
|
| 1898 |
'debt_history': [],
|
| 1899 |
-
'invoices': []
|
|
|
|
| 1900 |
}
|
| 1901 |
user_id_to_save = new_user_id
|
| 1902 |
|
| 1903 |
-
all_data[user_id_to_save] = user_entry
|
| 1904 |
-
save_visitor_data(all_data)
|
| 1905 |
|
| 1906 |
return jsonify({"status": "ok", "verified": True, "user_id": user_id_to_save})
|
| 1907 |
else:
|
| 1908 |
return jsonify({"status": "error", "verified": True, "message": "User ID not found in parsed data"}), 400
|
| 1909 |
else:
|
| 1910 |
-
logging.warning(f"Verification failed for user: {user_info_dict.get('id')}")
|
| 1911 |
return jsonify({"status": "error", "verified": False, "message": "Invalid data"}), 403
|
| 1912 |
|
| 1913 |
-
except Exception
|
| 1914 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1915 |
return jsonify({"status": "error", "message": "Internal server error"}), 500
|
| 1916 |
|
| 1917 |
@app.route('/admin')
|
|
@@ -1919,7 +2121,7 @@ def admin_panel():
|
|
| 1919 |
all_data = load_visitor_data()
|
| 1920 |
users_list = []
|
| 1921 |
for user_id, user_data in all_data.items():
|
| 1922 |
-
if user_id == "organization_details":
|
| 1923 |
continue
|
| 1924 |
user_data['id'] = user_id
|
| 1925 |
users_list.append(user_data)
|
|
@@ -1950,7 +2152,6 @@ def add_client():
|
|
| 1950 |
|
| 1951 |
all_data = load_visitor_data()
|
| 1952 |
|
| 1953 |
-
# Check for existing phone number, excluding the 'organization_details' key
|
| 1954 |
for key, user in all_data.items():
|
| 1955 |
if key == "organization_details":
|
| 1956 |
continue
|
|
@@ -1976,17 +2177,17 @@ def add_client():
|
|
| 1976 |
'history': [],
|
| 1977 |
'debts': 0,
|
| 1978 |
'debt_history': [],
|
| 1979 |
-
'invoices': []
|
|
|
|
| 1980 |
}
|
| 1981 |
|
| 1982 |
-
all_data[new_id] = new_client
|
| 1983 |
save_visitor_data(all_data)
|
| 1984 |
|
| 1985 |
return jsonify({"status": "ok", "message": "Client added successfully"}), 201
|
| 1986 |
|
| 1987 |
-
except Exception
|
| 1988 |
-
|
| 1989 |
-
return jsonify({"status": "error", "message": str(e)}), 500
|
| 1990 |
|
| 1991 |
|
| 1992 |
@app.route('/admin/add_transaction', methods=['POST'])
|
|
@@ -2019,7 +2220,6 @@ def add_transaction():
|
|
| 2019 |
if repay_debt_amount > user.get('debts', 0):
|
| 2020 |
return jsonify({"status": "error", "message": "Сумма погашения превышает текущий долг"}), 400
|
| 2021 |
|
| 2022 |
-
# Bonus operations
|
| 2023 |
accrual_amount = purchase_amount * 0.02
|
| 2024 |
user['bonuses'] = round(user.get('bonuses', 0) + accrual_amount - deduct_amount, 2)
|
| 2025 |
if 'history' not in user or not isinstance(user['history'], list):
|
|
@@ -2038,7 +2238,6 @@ def add_transaction():
|
|
| 2038 |
"date": now_iso, "date_str": now_str
|
| 2039 |
})
|
| 2040 |
|
| 2041 |
-
# Debt operations
|
| 2042 |
user['debts'] = round(user.get('debts', 0) + add_debt_amount - repay_debt_amount, 2)
|
| 2043 |
if 'debt_history' not in user or not isinstance(user['debt_history'], list):
|
| 2044 |
user['debt_history'] = []
|
|
@@ -2056,7 +2255,7 @@ def add_transaction():
|
|
| 2056 |
"date": now_iso, "date_str": now_str
|
| 2057 |
})
|
| 2058 |
|
| 2059 |
-
all_data[user_id_str] = user
|
| 2060 |
save_visitor_data(all_data)
|
| 2061 |
|
| 2062 |
return jsonify({
|
|
@@ -2064,9 +2263,8 @@ def add_transaction():
|
|
| 2064 |
"new_balance": user['bonuses'], "new_debt": user['debts']
|
| 2065 |
}), 200
|
| 2066 |
|
| 2067 |
-
except Exception
|
| 2068 |
-
|
| 2069 |
-
return jsonify({"status": "error", "message": str(e)}), 500
|
| 2070 |
|
| 2071 |
@app.route('/admin/add_invoice', methods=['POST'])
|
| 2072 |
def add_invoice():
|
|
@@ -2092,7 +2290,7 @@ def add_invoice():
|
|
| 2092 |
now_iso = now.isoformat()
|
| 2093 |
now_str = now.strftime('%Y-%m-%d %H:%M:%S')
|
| 2094 |
|
| 2095 |
-
invoice_id = str(uuid.uuid4().hex[:8]).upper()
|
| 2096 |
|
| 2097 |
processed_items = []
|
| 2098 |
for item in items:
|
|
@@ -2124,9 +2322,8 @@ def add_invoice():
|
|
| 2124 |
|
| 2125 |
return jsonify({"status": "ok", "message": "Invoice added successfully", "invoice_id": invoice_id}), 200
|
| 2126 |
|
| 2127 |
-
except Exception
|
| 2128 |
-
|
| 2129 |
-
return jsonify({"status": "error", "message": str(e)}), 500
|
| 2130 |
|
| 2131 |
@app.route('/admin/delete_invoice', methods=['POST'])
|
| 2132 |
def delete_invoice():
|
|
@@ -2159,9 +2356,8 @@ def delete_invoice():
|
|
| 2159 |
|
| 2160 |
return jsonify({"status": "ok", "message": "Invoice deleted successfully"}), 200
|
| 2161 |
|
| 2162 |
-
except Exception
|
| 2163 |
-
|
| 2164 |
-
return jsonify({"status": "error", "message": str(e)}), 500
|
| 2165 |
|
| 2166 |
|
| 2167 |
@app.route('/admin/delete_client', methods=['POST'])
|
|
@@ -2174,9 +2370,9 @@ def delete_client():
|
|
| 2174 |
return jsonify({"status": "error", "message": "User ID is required"}), 400
|
| 2175 |
|
| 2176 |
user_id_str = str(user_id)
|
| 2177 |
-
all_data = load_visitor_data()
|
| 2178 |
|
| 2179 |
-
with _data_lock:
|
| 2180 |
if user_id_str not in all_data or user_id_str == "organization_details":
|
| 2181 |
return jsonify({"status": "error", "message": "User not found"}), 404
|
| 2182 |
|
|
@@ -2184,23 +2380,19 @@ def delete_client():
|
|
| 2184 |
if user_to_delete.get('telegram_id') is not None:
|
| 2185 |
return jsonify({"status": "error", "message": "Cannot delete a Telegram-linked user"}), 403
|
| 2186 |
|
| 2187 |
-
del all_data[user_id_str]
|
| 2188 |
|
| 2189 |
try:
|
| 2190 |
-
# Save the modified all_data (which is visitor_data_cache)
|
| 2191 |
with open(DATA_FILE, 'w', encoding='utf-8') as f:
|
| 2192 |
json.dump(all_data, f, ensure_ascii=False, indent=4)
|
| 2193 |
-
logging.info(f"User {user_id_str} deleted. Data saved to {DATA_FILE}.")
|
| 2194 |
upload_data_to_hf_async()
|
| 2195 |
-
except Exception
|
| 2196 |
-
logging.error(f"Error saving data after deletion: {e}")
|
| 2197 |
return jsonify({"status": "error", "message": "Failed to save data after deletion"}), 500
|
| 2198 |
|
| 2199 |
return jsonify({"status": "ok", "message": "Client deleted successfully"}), 200
|
| 2200 |
|
| 2201 |
-
except Exception
|
| 2202 |
-
|
| 2203 |
-
return jsonify({"status": "error", "message": str(e)}), 500
|
| 2204 |
|
| 2205 |
@app.route('/admin/organization_details', methods=['GET'])
|
| 2206 |
def get_organization_details():
|
|
@@ -2208,9 +2400,8 @@ def get_organization_details():
|
|
| 2208 |
all_data = load_visitor_data()
|
| 2209 |
org_details = all_data.get('organization_details', {})
|
| 2210 |
return jsonify(org_details), 200
|
| 2211 |
-
except Exception
|
| 2212 |
-
|
| 2213 |
-
return jsonify({"status": "error", "message": str(e)}), 500
|
| 2214 |
|
| 2215 |
@app.route('/admin/organization_details', methods=['POST'])
|
| 2216 |
def save_organization_details():
|
|
@@ -2226,30 +2417,22 @@ def save_organization_details():
|
|
| 2226 |
|
| 2227 |
all_data = load_visitor_data()
|
| 2228 |
all_data['organization_details'] = new_org_details
|
| 2229 |
-
save_visitor_data(all_data)
|
| 2230 |
|
| 2231 |
return jsonify({"status": "ok", "message": "Organization details saved successfully"}), 200
|
| 2232 |
-
except Exception
|
| 2233 |
-
|
| 2234 |
-
return jsonify({"status": "error", "message": str(e)}), 500
|
| 2235 |
|
| 2236 |
if __name__ == '__main__':
|
| 2237 |
-
print("--- BONUS SYSTEM SERVER ---")
|
| 2238 |
-
print(f"Server starting on http://{HOST}:{PORT}")
|
| 2239 |
if not HF_TOKEN_READ or not HF_TOKEN_WRITE:
|
| 2240 |
-
|
| 2241 |
else:
|
| 2242 |
-
print("Attempting initial data download from Hugging Face...")
|
| 2243 |
download_data_from_hf()
|
| 2244 |
|
| 2245 |
-
load_visitor_data()
|
| 2246 |
|
| 2247 |
-
print("WARNING: The /admin route is NOT protected. Implement proper authentication for production.")
|
| 2248 |
-
|
| 2249 |
if HF_TOKEN_WRITE:
|
| 2250 |
backup_thread = threading.Thread(target=periodic_backup, daemon=True)
|
| 2251 |
backup_thread.start()
|
| 2252 |
-
print("Periodic backup thread started (every hour).")
|
| 2253 |
|
| 2254 |
-
print("--- Server Ready ---")
|
| 2255 |
app.run(host=HOST, port=PORT, debug=False)
|
|
|
|
|
|
|
| 1 |
|
| 2 |
import os
|
|
|
|
| 3 |
import hmac
|
| 4 |
import hashlib
|
| 5 |
import json
|
|
|
|
| 9 |
import logging
|
| 10 |
import threading
|
| 11 |
import random
|
| 12 |
+
import pytz
|
| 13 |
+
import uuid
|
| 14 |
+
from flask import Flask, request, Response, render_template_string, jsonify, redirect, url_for
|
| 15 |
from huggingface_hub import HfApi, hf_hub_download
|
| 16 |
from huggingface_hub.utils import RepositoryNotFoundError
|
| 17 |
+
from PIL import Image
|
| 18 |
+
import io
|
| 19 |
|
| 20 |
BOT_TOKEN = os.getenv("BOT_TOKEN", "7835463659:AAGNePbelZIAOeaglyQi1qulOqnjs4BGQn4")
|
| 21 |
HOST = '0.0.0.0'
|
|
|
|
| 27 |
HF_TOKEN_WRITE = os.getenv("HF_TOKEN_WRITE")
|
| 28 |
HF_TOKEN_READ = os.getenv("HF_TOKEN_READ")
|
| 29 |
|
|
|
|
| 30 |
BISHKEK_TZ = pytz.timezone('Asia/Bishkek')
|
| 31 |
|
| 32 |
app = Flask(__name__)
|
| 33 |
+
logging.basicConfig(level=logging.ERROR)
|
| 34 |
app.secret_key = os.urandom(24)
|
| 35 |
|
| 36 |
_data_lock = threading.Lock()
|
| 37 |
+
visitor_data_cache = {}
|
| 38 |
|
| 39 |
def generate_unique_id(all_data):
|
| 40 |
while True:
|
|
|
|
| 41 |
new_id = str(random.randint(10000, 99999))
|
| 42 |
if new_id not in all_data:
|
| 43 |
return new_id
|
|
|
|
| 45 |
def download_data_from_hf():
|
| 46 |
global visitor_data_cache
|
| 47 |
if not HF_TOKEN_READ:
|
|
|
|
| 48 |
return False
|
| 49 |
try:
|
|
|
|
| 50 |
hf_hub_download(
|
| 51 |
repo_id=REPO_ID,
|
| 52 |
filename=HF_DATA_FILE_PATH,
|
|
|
|
| 57 |
force_download=True,
|
| 58 |
etag_timeout=10
|
| 59 |
)
|
|
|
|
| 60 |
with _data_lock:
|
| 61 |
try:
|
| 62 |
with open(DATA_FILE, 'r', encoding='utf-8') as f:
|
| 63 |
visitor_data_cache = json.load(f)
|
| 64 |
+
except (FileNotFoundError, json.JSONDecodeError):
|
|
|
|
|
|
|
| 65 |
visitor_data_cache = {}
|
| 66 |
return True
|
| 67 |
except RepositoryNotFoundError:
|
| 68 |
+
pass
|
| 69 |
+
except Exception:
|
| 70 |
+
pass
|
| 71 |
return False
|
| 72 |
|
| 73 |
def load_visitor_data():
|
| 74 |
global visitor_data_cache
|
| 75 |
with _data_lock:
|
| 76 |
+
if not visitor_data_cache:
|
| 77 |
try:
|
| 78 |
with open(DATA_FILE, 'r', encoding='utf-8') as f:
|
| 79 |
visitor_data_cache = json.load(f)
|
|
|
|
| 80 |
except FileNotFoundError:
|
| 81 |
+
visitor_data_cache = {"organization_details": {}}
|
|
|
|
| 82 |
except json.JSONDecodeError:
|
|
|
|
| 83 |
visitor_data_cache = {"organization_details": {}}
|
| 84 |
+
except Exception:
|
|
|
|
| 85 |
visitor_data_cache = {"organization_details": {}}
|
| 86 |
|
|
|
|
| 87 |
if "organization_details" not in visitor_data_cache:
|
| 88 |
visitor_data_cache["organization_details"] = {}
|
| 89 |
|
|
|
|
| 92 |
def save_visitor_data(data):
|
| 93 |
with _data_lock:
|
| 94 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
with open(DATA_FILE, 'w', encoding='utf-8') as f:
|
| 96 |
json.dump(visitor_data_cache, f, ensure_ascii=False, indent=4)
|
|
|
|
| 97 |
upload_data_to_hf_async()
|
| 98 |
+
except Exception:
|
| 99 |
+
pass
|
| 100 |
|
| 101 |
def upload_data_to_hf():
|
| 102 |
if not HF_TOKEN_WRITE:
|
|
|
|
| 103 |
return
|
| 104 |
if not os.path.exists(DATA_FILE):
|
|
|
|
| 105 |
return
|
| 106 |
|
| 107 |
try:
|
|
|
|
| 109 |
with _data_lock:
|
| 110 |
file_content_exists = os.path.getsize(DATA_FILE) > 0
|
| 111 |
if not file_content_exists:
|
|
|
|
| 112 |
return
|
| 113 |
|
|
|
|
| 114 |
api.upload_file(
|
| 115 |
path_or_fileobj=DATA_FILE,
|
| 116 |
path_in_repo=HF_DATA_FILE_PATH,
|
|
|
|
| 119 |
token=HF_TOKEN_WRITE,
|
| 120 |
commit_message=f"Update bonus data {datetime.now(BISHKEK_TZ).strftime('%Y-%m-%d %H:%M:%S')}"
|
| 121 |
)
|
| 122 |
+
except Exception:
|
| 123 |
+
pass
|
|
|
|
| 124 |
|
| 125 |
def upload_data_to_hf_async():
|
| 126 |
upload_thread = threading.Thread(target=upload_data_to_hf, daemon=True)
|
|
|
|
| 128 |
|
| 129 |
def periodic_backup():
|
| 130 |
if not HF_TOKEN_WRITE:
|
|
|
|
| 131 |
return
|
| 132 |
while True:
|
| 133 |
time.sleep(3600)
|
|
|
|
| 134 |
upload_data_to_hf()
|
| 135 |
|
| 136 |
def verify_telegram_data(init_data_str):
|
|
|
|
| 153 |
auth_date = int(parsed_data.get('auth_date', [0])[0])
|
| 154 |
current_time = int(time.time())
|
| 155 |
if current_time - auth_date > 86400:
|
| 156 |
+
pass
|
| 157 |
return parsed_data, True
|
| 158 |
else:
|
| 159 |
+
pass
|
| 160 |
return parsed_data, False
|
| 161 |
+
except Exception:
|
|
|
|
| 162 |
return None, False
|
| 163 |
|
| 164 |
TEMPLATE = """
|
|
|
|
| 211 |
.header {
|
| 212 |
text-align: left;
|
| 213 |
padding: var(--padding-m) 0;
|
| 214 |
+
margin-bottom: 0;
|
| 215 |
}
|
| 216 |
.logo {
|
| 217 |
font-size: 2.5em;
|
|
|
|
| 252 |
box-shadow: 0 2px 10px rgba(255,193,7,0.3);
|
| 253 |
}
|
| 254 |
.content-section {
|
| 255 |
+
display: none;
|
| 256 |
flex-direction: column;
|
| 257 |
gap: var(--padding-m);
|
| 258 |
}
|
| 259 |
.content-section.active {
|
| 260 |
+
display: flex;
|
| 261 |
}
|
| 262 |
.card-grid {
|
| 263 |
display: grid;
|
|
|
|
| 321 |
padding: 4px 10px;
|
| 322 |
border-radius: 8px;
|
| 323 |
}
|
| 324 |
+
.history-section, .invoices-section, .business-card-section, .ton-wallet-section {
|
| 325 |
background-color: var(--card-bg);
|
| 326 |
border-radius: var(--border-radius);
|
| 327 |
padding: var(--padding-l);
|
|
|
|
| 329 |
flex-direction: column;
|
| 330 |
gap: var(--padding-m);
|
| 331 |
}
|
| 332 |
+
.history-title, .invoices-title, .business-card-title, .ton-wallet-title {
|
| 333 |
font-size: 1.4em;
|
| 334 |
font-weight: 700;
|
| 335 |
padding-bottom: var(--padding-m);
|
|
|
|
| 363 |
padding: 2rem 0;
|
| 364 |
}
|
| 365 |
|
| 366 |
+
.business-card-item, .ton-wallet-item {
|
|
|
|
| 367 |
margin-bottom: 10px;
|
| 368 |
}
|
| 369 |
+
.business-card-label, .ton-wallet-label {
|
| 370 |
font-weight: 500;
|
| 371 |
color: var(--text-secondary-color);
|
| 372 |
margin-bottom: 4px;
|
| 373 |
}
|
| 374 |
+
.business-card-value, .ton-wallet-value {
|
| 375 |
font-size: 1.1em;
|
| 376 |
font-weight: 600;
|
| 377 |
color: var(--text-color);
|
| 378 |
}
|
| 379 |
+
.business-card-value a, .ton-wallet-value a {
|
| 380 |
color: var(--brand-yellow);
|
| 381 |
text-decoration: none;
|
| 382 |
+
word-break: break-all;
|
| 383 |
}
|
| 384 |
+
.business-card-value a:hover, .ton-wallet-value a:hover {
|
| 385 |
text-decoration: underline;
|
| 386 |
}
|
| 387 |
.business-card-phone-list {
|
|
|
|
| 411 |
width: 20px;
|
| 412 |
}
|
| 413 |
|
|
|
|
| 414 |
.modal {
|
| 415 |
display: none;
|
| 416 |
position: fixed;
|
|
|
|
| 500 |
.invoice-total-display span:last-child {
|
| 501 |
color: var(--brand-yellow);
|
| 502 |
}
|
| 503 |
+
|
| 504 |
+
.ton-wallet-connect-btn, .ton-wallet-disconnect-btn {
|
| 505 |
+
background-color: var(--brand-yellow);
|
| 506 |
+
color: var(--brand-black);
|
| 507 |
+
padding: 12px 20px;
|
| 508 |
+
border: none;
|
| 509 |
+
border-radius: 12px;
|
| 510 |
+
font-family: var(--font-family);
|
| 511 |
+
font-weight: 700;
|
| 512 |
+
font-size: 1.1em;
|
| 513 |
+
cursor: pointer;
|
| 514 |
+
width: 100%;
|
| 515 |
+
box-shadow: 0 4px 15px rgba(255,193,7,0.4);
|
| 516 |
+
transition: background-color 0.2s, box-shadow 0.2s;
|
| 517 |
+
}
|
| 518 |
+
.ton-wallet-disconnect-btn {
|
| 519 |
+
background-color: var(--brand-red);
|
| 520 |
+
color: var(--text-color);
|
| 521 |
+
box-shadow: 0 4px 15px rgba(244,67,54,0.4);
|
| 522 |
+
margin-top: 10px;
|
| 523 |
+
}
|
| 524 |
+
.ton-wallet-connect-btn:hover { background-color: #e0a800; box-shadow: 0 6px 20px rgba(255,193,7,0.5); }
|
| 525 |
+
.ton-wallet-disconnect-btn:hover { background-color: #d32f2f; box-shadow: 0 6px 20px rgba(244,67,54,0.5); }
|
| 526 |
+
|
| 527 |
+
.ton-wallet-details {
|
| 528 |
+
background-color: #2a2a2a;
|
| 529 |
+
border-radius: var(--border-radius);
|
| 530 |
+
padding: var(--padding-m);
|
| 531 |
+
margin-top: var(--padding-m);
|
| 532 |
+
}
|
| 533 |
+
.ton-wallet-detail-item {
|
| 534 |
+
display: flex;
|
| 535 |
+
justify-content: space-between;
|
| 536 |
+
padding: 8px 0;
|
| 537 |
+
border-bottom: 1px solid rgba(255,255,255,0.05);
|
| 538 |
+
}
|
| 539 |
+
.ton-wallet-detail-item:last-child { border-bottom: none; }
|
| 540 |
+
.ton-wallet-label-small {
|
| 541 |
+
font-size: 0.9em;
|
| 542 |
+
color: var(--text-secondary-color);
|
| 543 |
+
}
|
| 544 |
+
.ton-wallet-value-small {
|
| 545 |
+
font-size: 1em;
|
| 546 |
+
font-weight: 600;
|
| 547 |
+
color: var(--text-color);
|
| 548 |
+
word-break: break-all;
|
| 549 |
+
text-align: right;
|
| 550 |
+
}
|
| 551 |
+
.ton-wallet-value-small.balance {
|
| 552 |
+
color: var(--brand-yellow);
|
| 553 |
+
font-size: 1.2em;
|
| 554 |
+
}
|
| 555 |
+
.ton-status-message {
|
| 556 |
+
text-align: center;
|
| 557 |
+
color: var(--text-secondary-color);
|
| 558 |
+
margin-top: 10px;
|
| 559 |
+
font-size: 0.9em;
|
| 560 |
+
}
|
| 561 |
</style>
|
| 562 |
</head>
|
| 563 |
<body>
|
|
|
|
| 570 |
<nav class="nav-buttons">
|
| 571 |
<button class="nav-btn active" data-target="dashboard-section">Главная</button>
|
| 572 |
<button class="nav-btn" data-target="invoices-section">Накладные</button>
|
| 573 |
+
<button class="nav-btn" data-target="ton-wallet-section">TON Кошелек</button>
|
| 574 |
<button class="nav-btn" data-target="business-card-section">Визитка</button>
|
| 575 |
</nav>
|
| 576 |
|
|
|
|
| 640 |
</section>
|
| 641 |
</div>
|
| 642 |
|
| 643 |
+
<div id="ton-wallet-section" class="content-section">
|
| 644 |
+
<section class="ton-wallet-section">
|
| 645 |
+
<h2 class="ton-wallet-title">TON Кошелек</h2>
|
| 646 |
+
<div id="tonWalletContent">
|
| 647 |
+
<p id="tonStatusMessage" class="ton-status-message">Подключение...</p>
|
| 648 |
+
<button id="connectTonWalletBtn" class="ton-wallet-connect-btn" style="display: none;">Подключить TON Кошелек</button>
|
| 649 |
+
<div id="walletDetails" class="ton-wallet-details" style="display: none;">
|
| 650 |
+
<div class="ton-wallet-detail-item">
|
| 651 |
+
<span class="ton-wallet-label-small">Адрес кошелька:</span>
|
| 652 |
+
<span id="walletAddress" class="ton-wallet-value-small"></span>
|
| 653 |
+
</div>
|
| 654 |
+
<div class="ton-wallet-detail-item">
|
| 655 |
+
<span class="ton-wallet-label-small">Баланс TON:</span>
|
| 656 |
+
<span id="walletBalance" class="ton-wallet-value-small balance"></span>
|
| 657 |
+
</div>
|
| 658 |
+
</div>
|
| 659 |
+
<button id="disconnectTonWalletBtn" class="ton-wallet-disconnect-btn" style="display: none;">Отключить кошелек</button>
|
| 660 |
+
</div>
|
| 661 |
+
</section>
|
| 662 |
+
</div>
|
| 663 |
+
|
| 664 |
<div id="business-card-section" class="content-section">
|
| 665 |
<section class="business-card-section">
|
| 666 |
<h2 class="business-card-title">Визитка организации</h2>
|
|
|
|
| 676 |
{% for phone in org_details.phone_numbers %}
|
| 677 |
<li class="business-card-phone-item">
|
| 678 |
<a href="tel:{{ phone }}">
|
| 679 |
+
<img src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSJjdXJyZW50Q29sb3IiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48cGF0aCBkPSJtMjIuMDIyIDEuOTY3LTMuMDkgMy41MjNhOC40OCA4LjQ4IDAgMCAwLTEzLjA5OSAwTDEuOTc4IDEuOTY3Yy0uOTc0LTEuMTA3LTIuNTI1LS40MS0yLjUxMiAxLjAzNXYxOC44NjhjLjAwMSAxLjQ0NSAxLjUyOCAyLjEzNiAyLjQ4OCAxLjAzN2wzLjA5LTMuNTIzYTEw.xNi0uNzc4LS4wNzIuOTc0LjI3NSAxLjc2OC41NzIgMi4zNzUuODcxLjMwNS4xNS41NzcuMzEuNzk0LjQ1MS4yMTQuMTQzLjMyNC4yMi4zMjQuMjIuMDg2LS4yNzguMjYzLS42MS40NzYtLjkxMy4wNjItLjA4OC4xMzQtLjE4NS4yMTUtLjI4OC41MTQtLjY2MiAxLjExLS44NzUgMS44MS0uNTMxLjY0NC4zMTEgMS4xNzcuOCAxLjczMiAxLjUyLjQ3NS42ODkuOTUgMS4zNzkgMS40MjUgMi4wNzcuMzYzLjU1LjY4IDEuMDcuOTU3IDEuNTUzLjY1MiAxLjE0OSAxLjEzOCAxLjgxMiAxLjQ1MiAyLjEyNi40NzYuNDg3Ljg2LjgyNyAxLjI0MyAxLjA5Ni43NDEuNTIzIDEuNDg1LjkzNCAyLjIzNSAxLjIxMS43NDcuMjc0IDEuNDU1LjQ4NCAyLjExMy42MzYuNTY3LjEzIDEuMTU0LjE4MiAxLjczLjE1LjcxNC0uMDM1IDEuNDEtLjI0MyAxLjk3LS41Ny41ODEtLjMzOCAxLjA1NS0uODI5IDEuMzk3LTEuMzU1LjExOS0uMTc1LjIwMi0uMzguMjUtLjU4My4wNDctLjE4OS4wNjctLjM4OS4wNi0uNTkzWiIvPjwvc3ZnPg==">
|
| 680 |
{{ phone }}
|
| 681 |
</a>
|
| 682 |
</li>
|
|
|
|
| 695 |
<div class="business-card-value">
|
| 696 |
{% if org_details.whatsapp_link %}
|
| 697 |
<a href="{{ org_details.whatsapp_link }}" target="_blank">
|
| 698 |
+
<img src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgZmlsbD0iI2ZmZiIgY2xhc3M9ImJpIGJpLXdoYXRzYXBwIiB2aWV3Qm94PSIwIDAgMTYgMTYiPjxwYXRoIGQ9Ik0xMy42NzYgMS40NTJBMTAuNTE1IDEw.wOTYtLjE1LjM3Mi0uMjg1LjcwMi0uNDgzLjMxLS40OC41MDktLjUxNS42NjktLjQ1Mi4xMDkuMDUxLjQxNy4yMTEuNDYzLjI2Ny4xNDEuMDgyLjI4NC4xNjEuNDQzLjIyNWExLjIyNyAxLjIyNyAwIDAgMCAuNzQ2LjAyNGwuMjg0LS4xMzVhMy45NjcgMy45NjcgMCAwIDAgLjY2Mi0uNjQzLjkwOC45MDggMCAwIDAgLjMwMi0uNjc4LjE5OC4xOTggMCAwIDAgMC0uMTU2LjgxNS44MTUgMCAwIDAgMC0uNDc3eiIvPjwvc3ZnPg==">
|
| 699 |
{{ org_details.whatsapp_link }}
|
| 700 |
</a>
|
| 701 |
{% else %}
|
|
|
|
| 708 |
<div class="business-card-value">
|
| 709 |
{% if org_details.telegram_link %}
|
| 710 |
<a href="{{ org_details.telegram_link }}" target="_blank">
|
| 711 |
+
<img src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgZmlsbD0iI2ZmZiIgY2xhc3M9ImJpIGJpLXQtZWxlZ3JhbSIgdmlld0JveD0iMCAwIDE2IDE2Ij48cGF0aCBkPSJNMTYuMDAyIDguNjkyYy4wMDItLjg3Ni0uNDQ1LTEuMzM1LTEuMzAzLTEuNjk0LS4zMjctLjEwNy0uNjk0LS4yMzctMS4wODgtLjM2Ny0xLjYzNi0uNTE2LTIuOTQzLTEuMzMtNC4xMy0yLjU0NS0uODkxLS44OC0xLjY0Ny0xLjcxLTIuMzc0LTIuNDE4LS40MTctLjM5My0uNTM3LS42Ni0uNTAyLS44NTEuMDExLS4wNjkuMDc1LS4yNTguMDgyLS4zNDkuMDY2LS44MjktLjMyLS45NzgtMS4xNS0uNzAyLS40ODcuMTU3LS45OTMuMzItMS41MTMuNDc1LS4yMS4wNjItLjM3OC4wOTktLjUuMTItLjU3LjA4OC0xLjE3NS4yNjQtMS43MDkuNDMzLS4zMy4xMTgtLjY3LjI1MS0xLjAyLjM4OC0uNzUuMjczLTEuNTA5LjU0Ni0yLjI3LjgwNi0uNzg1LjI2Mi0xLjQ3NC40NzUtMi4wNjQuNjQtLjUzNC4xNDYtLjkzNy4xNi0xLjIxMi0uMDctLjQ4NS0uNDA5LS45MS0uOTQ2LS40NzYtMS40NTEuMTU1LS4xNy4zNDctLjMyNi41NzYtLjQ2Ny4xNjktLjEwNS4zNjMtLjIwNy41ODItLjMxMy40NjktLjIyLjkyNi0uMzc0IDEuMzY4LS40NjkuNjMyLS4xMyAxLjI3MS0uMjE2IDEuOTItLjI3MS4yNzktLjAyNC41NTMtLjA0LjgyMy0uMDYuMDYxLS4xNzguMjUzLS41NTYuNTQ3LTEuMTUyLjY4MS0xLjM2NSAxLjQxNy0yLjE4MSAyLjE2Ni0yLjczMi42NzYtLjUwOCAxLjQwNy0uOTUzIDIuMjY2LTEuMjg5LjI2MS0uMTA5LjUyLS4xNDMuNzc4LS4wNzIuOTc0LjI3NSAxLjc2OC41NzIgMi4zNzUuODcxLjMwNS4xNS41NzcuMzEuNzk0LjQ1MS4yMTQuMTQzLjMyNC4yMi4zMjQuMjIuMDg2LS4yNzguMjYzLS42MS40NzYtLjkxMy4wNjItLjA4OC4xMzQtLjE4NS4yMTUtLjI4OC41MTQtLjY2MiAxLjExLS44NzUgMS44MS0uNTMxLjY0NC4zMTEgMS4xNzcuOCAxLjczMiAxLjUyLjQ3NS42ODkuOTUgMS4zNzkgMS40MjUgMi4wNzcuMzYzLjU1LjY4IDEuMDcuOTU3IDEuNTUzLjY1MiAxLjE0OSAxLjEzOCAxLjgxMiAxLjQ1MiAyLjEyNi40NzYuNDg3Ljg2LjgyNyAxLjI0MyAxLjA5Ni43NDEu5Lz48L3N2Zz4=">
|
| 712 |
{{ org_details.telegram_link }}
|
| 713 |
</a>
|
| 714 |
{% else %}
|
|
|
|
| 723 |
</div>
|
| 724 |
</div>
|
| 725 |
|
|
|
|
| 726 |
<div id="invoiceDetailModal" class="modal">
|
| 727 |
<div class="modal-content">
|
| 728 |
<span class="modal-close" onclick="closeModal('invoiceDetailModal')">×</span>
|
| 729 |
<h2 id="invoiceDetailTitle" class="modal-title"></h2>
|
| 730 |
<ul id="invoiceDetailList" class="invoice-detail-list">
|
|
|
|
| 731 |
</ul>
|
| 732 |
<div id="invoiceDetailTotal" class="invoice-total-display">
|
| 733 |
<span>Итого:</span>
|
|
|
|
| 736 |
</div>
|
| 737 |
</div>
|
| 738 |
|
| 739 |
+
<script src="https://unpkg.com/@tonconnect/sdk@2.0.0/dist/ton-connect-sdk.min.js"></script>
|
| 740 |
+
<script src="https://unpkg.com/tonweb@0.0.60/dist/tonweb.standalone.min.js"></script>
|
| 741 |
<script>
|
| 742 |
const tg = window.Telegram.WebApp;
|
| 743 |
+
let userIdForBackend = '{{ user.id }}';
|
| 744 |
|
| 745 |
function applyTheme(themeParams) {
|
| 746 |
const root = document.documentElement;
|
|
|
|
| 755 |
|
| 756 |
function setupTelegram() {
|
| 757 |
if (!tg || !tg.initData) {
|
|
|
|
| 758 |
document.body.style.visibility = 'visible';
|
| 759 |
return;
|
| 760 |
}
|
|
|
|
| 781 |
if (data.status === 'ok' && data.verified && data.user_id) {
|
| 782 |
window.location.replace('/?user_id_for_test=' + data.user_id);
|
| 783 |
} else {
|
|
|
|
| 784 |
document.body.style.visibility = 'visible';
|
| 785 |
}
|
| 786 |
})
|
| 787 |
+
.catch(() => {
|
|
|
|
| 788 |
document.body.style.visibility = 'visible';
|
| 789 |
});
|
| 790 |
} else {
|
|
|
|
| 846 |
});
|
| 847 |
});
|
| 848 |
|
|
|
|
| 849 |
showSection('dashboard-section');
|
| 850 |
});
|
| 851 |
|
|
|
|
| 859 |
}
|
| 860 |
}, 3000);
|
| 861 |
}
|
| 862 |
+
|
| 863 |
+
let tonConnector = null;
|
| 864 |
+
let currentTonAddress = '{{ user.ton_address or "" }}';
|
| 865 |
+
let tonWeb = null;
|
| 866 |
+
|
| 867 |
+
async function initializeTonConnect() {
|
| 868 |
+
try {
|
| 869 |
+
tonConnector = new TonConnect({
|
| 870 |
+
manifest: {
|
| 871 |
+
url: window.location.origin,
|
| 872 |
+
name: "Bonus System",
|
| 873 |
+
iconUrl: window.location.origin + "/static/ton_icon.png",
|
| 874 |
+
termsOfServiceUrl: window.location.origin + "/terms",
|
| 875 |
+
privacyPolicyUrl: window.location.origin + "/privacy"
|
| 876 |
+
}
|
| 877 |
+
});
|
| 878 |
+
|
| 879 |
+
tonWeb = new TonWeb(new TonWeb.HttpProvider('https://toncenter.com/api/v2/jsonRPC', {apiKey: 'YOUR_TONCENTER_API_KEY_HERE'}));
|
| 880 |
+
|
| 881 |
+
await tonConnector.restoreConnection();
|
| 882 |
+
|
| 883 |
+
tonConnector.onStatusChange(async walletInfo => {
|
| 884 |
+
if (walletInfo) {
|
| 885 |
+
currentTonAddress = walletInfo.account.address;
|
| 886 |
+
document.getElementById('walletAddress').textContent = currentTonAddress;
|
| 887 |
+
document.getElementById('connectTonWalletBtn').style.display = 'none';
|
| 888 |
+
document.getElementById('disconnectTonWalletBtn').style.display = 'block';
|
| 889 |
+
document.getElementById('walletDetails').style.display = 'block';
|
| 890 |
+
document.getElementById('tonStatusMessage').textContent = 'Кошелек подключен.';
|
| 891 |
+
await fetchTonBalance(currentTonAddress);
|
| 892 |
+
saveTonAddressToBackend(currentTonAddress);
|
| 893 |
+
} else {
|
| 894 |
+
currentTonAddress = '';
|
| 895 |
+
document.getElementById('walletAddress').textContent = 'Не подключен';
|
| 896 |
+
document.getElementById('walletBalance').textContent = '0 TON';
|
| 897 |
+
document.getElementById('connectTonWalletBtn').style.display = 'block';
|
| 898 |
+
document.getElementById('disconnectTonWalletBtn').style.display = 'none';
|
| 899 |
+
document.getElementById('walletDetails').style.display = 'none';
|
| 900 |
+
document.getElementById('tonStatusMessage').textContent = 'Кошелек не подключен.';
|
| 901 |
+
saveTonAddressToBackend('');
|
| 902 |
+
}
|
| 903 |
+
});
|
| 904 |
+
|
| 905 |
+
if (tonConnector.connected) {
|
| 906 |
+
document.getElementById('tonStatusMessage').textContent = 'Кошелек подключен.';
|
| 907 |
+
} else if (currentTonAddress) {
|
| 908 |
+
document.getElementById('walletAddress').textContent = currentTonAddress;
|
| 909 |
+
document.getElementById('walletDetails').style.display = 'block';
|
| 910 |
+
document.getElementById('connectTonWalletBtn').style.display = 'none';
|
| 911 |
+
document.getElementById('disconnectTonWalletBtn').style.display = 'block';
|
| 912 |
+
document.getElementById('tonStatusMessage').textContent = 'Загрузка данных кошелька...';
|
| 913 |
+
await fetchTonBalance(currentTonAddress);
|
| 914 |
+
} else {
|
| 915 |
+
document.getElementById('tonStatusMessage').textContent = 'Нажмите кнопку, чтобы подключить кошелек.';
|
| 916 |
+
document.getElementById('connectTonWalletBtn').style.display = 'block';
|
| 917 |
+
}
|
| 918 |
+
|
| 919 |
+
} catch (e) {
|
| 920 |
+
document.getElementById('tonStatusMessage').textContent = 'Ошибка инициализации Ton Connect: ' + e.message;
|
| 921 |
+
document.getElementById('connectTonWalletBtn').style.display = 'block';
|
| 922 |
+
}
|
| 923 |
+
}
|
| 924 |
+
|
| 925 |
+
async function connectTonWallet() {
|
| 926 |
+
if (!tonConnector) {
|
| 927 |
+
document.getElementById('tonStatusMessage').textContent = 'Ошибка: Ton Connect не инициализирован.';
|
| 928 |
+
return;
|
| 929 |
+
}
|
| 930 |
+
document.getElementById('tonStatusMessage').textContent = 'Ожидание подключения...';
|
| 931 |
+
try {
|
| 932 |
+
tonConnector.connect();
|
| 933 |
+
} catch (e) {
|
| 934 |
+
document.getElementById('tonStatusMessage').textContent = 'Ошибка подключения: ' + e.message;
|
| 935 |
+
}
|
| 936 |
+
}
|
| 937 |
+
|
| 938 |
+
async function disconnectTonWallet() {
|
| 939 |
+
if (!tonConnector) return;
|
| 940 |
+
document.getElementById('tonStatusMessage').textContent = 'Отключение...';
|
| 941 |
+
try {
|
| 942 |
+
await tonConnector.disconnect();
|
| 943 |
+
} catch (e) {
|
| 944 |
+
document.getElementById('tonStatusMessage').textContent = 'Ошибка отключения: ' + e.message;
|
| 945 |
+
}
|
| 946 |
+
}
|
| 947 |
+
|
| 948 |
+
async function fetchTonBalance(address) {
|
| 949 |
+
if (!tonWeb) return;
|
| 950 |
+
document.getElementById('walletBalance').textContent = 'Загрузка...';
|
| 951 |
+
try {
|
| 952 |
+
const wallet = new tonWeb.wallet.create({address: address});
|
| 953 |
+
const balanceNano = await tonWeb.getBalance(address);
|
| 954 |
+
const balanceTon = tonWeb.utils.fromNano(balanceNano);
|
| 955 |
+
document.getElementById('walletBalance').textContent = `${parseFloat(balanceTon).toFixed(4)} TON`;
|
| 956 |
+
} catch (e) {
|
| 957 |
+
document.getElementById('walletBalance').textContent = 'Ошибка';
|
| 958 |
+
document.getElementById('tonStatusMessage').textContent = 'Ошибка получения баланса: ' + e.message;
|
| 959 |
+
}
|
| 960 |
+
}
|
| 961 |
+
|
| 962 |
+
async function saveTonAddressToBackend(address) {
|
| 963 |
+
try {
|
| 964 |
+
const response = await fetch('/save_ton_address', {
|
| 965 |
+
method: 'POST',
|
| 966 |
+
headers: { 'Content-Type': 'application/json' },
|
| 967 |
+
body: JSON.stringify({
|
| 968 |
+
user_id: userIdForBackend,
|
| 969 |
+
ton_address: address,
|
| 970 |
+
initData: tg.initData
|
| 971 |
+
}),
|
| 972 |
+
});
|
| 973 |
+
const result = await response.json();
|
| 974 |
+
if (response.ok) {
|
| 975 |
+
console.log('TON address saved:', result.message);
|
| 976 |
+
} else {
|
| 977 |
+
console.error('Failed to save TON address:', result.message);
|
| 978 |
+
}
|
| 979 |
+
} catch (error) {
|
| 980 |
+
console.error('Error saving TON address:', error);
|
| 981 |
+
}
|
| 982 |
+
}
|
| 983 |
+
|
| 984 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 985 |
+
document.getElementById('connectTonWalletBtn').addEventListener('click', connectTonWallet);
|
| 986 |
+
document.getElementById('disconnectTonWalletBtn').addEventListener('click', disconnectTonWallet);
|
| 987 |
+
initializeTonConnect();
|
| 988 |
+
});
|
| 989 |
</script>
|
| 990 |
</body>
|
| 991 |
</html>
|
|
|
|
| 1079 |
.btn-submit { background-color: var(--admin-success); color: white; }
|
| 1080 |
.status-message { text-align: center; font-weight: 500; flex-grow: 1; text-align: left; }
|
| 1081 |
|
|
|
|
| 1082 |
.tab-buttons {
|
| 1083 |
display: flex;
|
| 1084 |
margin-bottom: 1rem;
|
|
|
|
| 1105 |
display: block;
|
| 1106 |
}
|
| 1107 |
|
|
|
|
| 1108 |
.invoice-items-table {
|
| 1109 |
width: 100%;
|
| 1110 |
border-collapse: collapse;
|
|
|
|
| 1254 |
{% endif %}
|
| 1255 |
</div>
|
| 1256 |
|
|
|
|
| 1257 |
<div id="transactionModal" class="modal">
|
| 1258 |
<div class="modal-content">
|
| 1259 |
<span class="modal-close" onclick="closeModal('transactionModal')">×</span>
|
|
|
|
| 1335 |
</tr>
|
| 1336 |
</thead>
|
| 1337 |
<tbody>
|
|
|
|
| 1338 |
</tbody>
|
| 1339 |
<tfoot>
|
| 1340 |
<tr>
|
|
|
|
| 1359 |
</div>
|
| 1360 |
</div>
|
| 1361 |
|
|
|
|
| 1362 |
<div id="addClientModal" class="modal">
|
| 1363 |
<div class="modal-content">
|
| 1364 |
<span class="modal-close" onclick="closeModal('addClientModal')">×</span>
|
|
|
|
| 1380 |
</div>
|
| 1381 |
</div>
|
| 1382 |
|
|
|
|
| 1383 |
<div id="orgSettingsModal" class="modal">
|
| 1384 |
<div class="modal-content">
|
| 1385 |
<span class="modal-close" onclick="closeModal('orgSettingsModal')">×</span>
|
|
|
|
| 1415 |
</div>
|
| 1416 |
</div>
|
| 1417 |
|
|
|
|
| 1418 |
<div id="adminInvoiceDetailModal" class="modal">
|
| 1419 |
<div class="modal-content">
|
| 1420 |
<span class="modal-close" onclick="closeModal('adminInvoiceDetailModal')">×</span>
|
| 1421 |
<h2 id="adminInvoiceDetailTitle" class="modal-title"></h2>
|
| 1422 |
<ul id="adminInvoiceDetailList" class="invoice-detail-list">
|
|
|
|
| 1423 |
</ul>
|
| 1424 |
<div id="adminInvoiceDetailTotal" class="invoice-total-display">
|
| 1425 |
<span>Итого:</span>
|
|
|
|
| 1461 |
document.getElementById('modalStatus').textContent = '';
|
| 1462 |
document.getElementById('invoiceStatus').textContent = '';
|
| 1463 |
|
|
|
|
| 1464 |
newInvoiceItems = [];
|
| 1465 |
renderNewInvoiceItems();
|
| 1466 |
|
| 1467 |
+
loadUserHistoryAndInvoices();
|
| 1468 |
|
|
|
|
| 1469 |
showTab('bonus-debt-tab');
|
| 1470 |
|
| 1471 |
transactionModal.style.display = 'block';
|
|
|
|
| 1488 |
sign = item.type === 'accrual' ? '+' : '-';
|
| 1489 |
amountClass = item.type === 'accrual' ? 'bonus-accrual' : 'bonus-deduction';
|
| 1490 |
amountText = `${sign}${parseFloat(item.amount).toFixed(2)}`;
|
| 1491 |
+
} else {
|
| 1492 |
+
sign = item.type === 'accrual' ? '+' : '-';
|
| 1493 |
amountClass = item.type === 'accrual' ? 'debt-accrual' : 'debt-payment';
|
| 1494 |
amountText = `${item.type === 'accrual' ? '+' : '-'}${parseFloat(item.amount).toFixed(2)}`;
|
| 1495 |
}
|
|
|
|
| 1506 |
historyList.innerHTML = '<li style="text-align:center; padding: 1rem; color: var(--admin-secondary);">Нет истории</li>';
|
| 1507 |
}
|
| 1508 |
|
|
|
|
| 1509 |
const modalInvoiceList = document.getElementById('modalInvoiceList');
|
| 1510 |
modalInvoiceList.innerHTML = '';
|
| 1511 |
const userInvoices = (currentUserData.invoices || []).sort((a, b) => new Date(b.date) - new Date(a.date));
|
|
|
|
| 1552 |
document.getElementById('orgStatus').textContent = '';
|
| 1553 |
orgSettingsModal.style.display = 'block';
|
| 1554 |
})
|
| 1555 |
+
.catch(() => {
|
|
|
|
| 1556 |
document.getElementById('orgStatus').style.color = 'var(--admin-danger)';
|
| 1557 |
document.getElementById('orgStatus').textContent = 'Ошибка загрузки данных.';
|
| 1558 |
orgSettingsModal.style.display = 'block';
|
|
|
|
| 1751 |
function addNewInvoiceItemRow() {
|
| 1752 |
const tableBody = document.getElementById('newInvoiceItemsTable').getElementsByTagName('tbody')[0];
|
| 1753 |
const newRow = tableBody.insertRow();
|
| 1754 |
+
const rowIndex = tableBody.rows.length - 1;
|
| 1755 |
|
| 1756 |
newInvoiceItems.push({
|
| 1757 |
product_name: '',
|
|
|
|
| 1786 |
|
| 1787 |
function removeInvoiceItemRow(button, index) {
|
| 1788 |
const tableBody = document.getElementById('newInvoiceItemsTable').getElementsByTagName('tbody')[0];
|
| 1789 |
+
tableBody.deleteRow(button.parentNode.parentNode.rowIndex - 1);
|
| 1790 |
|
|
|
|
| 1791 |
newInvoiceItems.splice(index, 1);
|
| 1792 |
for (let i = 0; i < tableBody.rows.length; i++) {
|
| 1793 |
const row = tableBody.rows[i];
|
| 1794 |
row.querySelector('input[type="text"]').setAttribute('oninput', `updateInvoiceItem(${i}, 'product_name', this.value)`);
|
| 1795 |
+
row.querySelector('input[type="number'][step="1"]').setAttribute('oninput', `updateInvoiceItem(${i}, 'quantity', parseFloat(this.value))`);
|
| 1796 |
+
row.querySelector('input[type="number'][step="0.01"]').setAttribute('oninput', `updateInvoiceItem(${i}, 'unit_price', parseFloat(this.value))`);
|
| 1797 |
row.querySelector('.action-btn').setAttribute('onclick', `removeInvoiceItemRow(this, ${i})`);
|
| 1798 |
}
|
| 1799 |
|
|
|
|
| 1902 |
});
|
| 1903 |
const result = await response.json();
|
| 1904 |
if (response.ok) {
|
| 1905 |
+
location.reload();
|
| 1906 |
} else {
|
| 1907 |
throw new Error(result.message || 'Не удалось удалить накладную.');
|
| 1908 |
}
|
|
|
|
| 1927 |
}
|
| 1928 |
}
|
| 1929 |
|
|
|
|
| 1930 |
document.addEventListener('DOMContentLoaded', () => {
|
| 1931 |
+
addNewInvoiceItemRow();
|
| 1932 |
});
|
| 1933 |
</script>
|
| 1934 |
</body>
|
| 1935 |
</html>
|
| 1936 |
"""
|
| 1937 |
|
| 1938 |
+
@app.route('/static/ton_icon.png')
|
| 1939 |
+
def serve_ton_icon():
|
| 1940 |
+
img = Image.new('RGBA', (100, 100), (0, 0, 0, 0))
|
| 1941 |
+
byte_arr = io.BytesIO()
|
| 1942 |
+
img.save(byte_arr, format='PNG')
|
| 1943 |
+
byte_arr.seek(0)
|
| 1944 |
+
return Response(byte_arr.getvalue(), mimetype='image/png')
|
| 1945 |
+
|
| 1946 |
+
@app.route('/terms')
|
| 1947 |
+
def terms():
|
| 1948 |
+
return "<h1>Terms of Service</h1><p>This is a placeholder for your terms of service.</p>"
|
| 1949 |
+
|
| 1950 |
+
@app.route('/privacy')
|
| 1951 |
+
def privacy():
|
| 1952 |
+
return "<h1>Privacy Policy</h1><p>This is a placeholder for your privacy policy.</p>"
|
| 1953 |
+
|
| 1954 |
@app.route('/')
|
| 1955 |
def index():
|
| 1956 |
user_id_str = request.args.get('user_id_for_test')
|
| 1957 |
|
| 1958 |
+
all_data = load_visitor_data()
|
| 1959 |
user_data = {}
|
| 1960 |
|
| 1961 |
if user_id_str and user_id_str in all_data:
|
|
|
|
| 1976 |
reverse=True
|
| 1977 |
)
|
| 1978 |
user_data['combined_history'] = combined_history
|
| 1979 |
+
user_data['invoices'] = user_data.get('invoices', [])
|
| 1980 |
+
user_data['ton_address'] = user_data.get('ton_address', '')
|
| 1981 |
else:
|
| 1982 |
user_data = {
|
| 1983 |
"id": "N/A",
|
|
|
|
| 1986 |
"history": [],
|
| 1987 |
"debt_history": [],
|
| 1988 |
"combined_history": [],
|
| 1989 |
+
"invoices": [],
|
| 1990 |
+
"ton_address": ""
|
| 1991 |
}
|
| 1992 |
|
| 1993 |
org_details = all_data.get('organization_details', {})
|
|
|
|
| 2009 |
try:
|
| 2010 |
user_json_str = unquote(user_data_parsed['user'][0])
|
| 2011 |
user_info_dict = json.loads(user_json_str)
|
| 2012 |
+
except Exception:
|
|
|
|
| 2013 |
user_info_dict = {}
|
| 2014 |
|
| 2015 |
if is_valid:
|
| 2016 |
tg_user_id = user_info_dict.get('id')
|
| 2017 |
if tg_user_id:
|
| 2018 |
now = datetime.now(BISHKEK_TZ)
|
| 2019 |
+
all_data = load_visitor_data()
|
| 2020 |
|
| 2021 |
existing_user_key = None
|
| 2022 |
for key, user_data_item in all_data.items():
|
|
|
|
| 2023 |
if key == "organization_details":
|
| 2024 |
continue
|
| 2025 |
if str(user_data_item.get('telegram_id')) == str(tg_user_id):
|
|
|
|
| 2049 |
'photo_url': user_info_dict.get('photo_url'),
|
| 2050 |
'language_code': user_info_dict.get('language_code'),
|
| 2051 |
'is_premium': user_info_dict.get('is_premium', False),
|
| 2052 |
+
'phone_number': None,
|
| 2053 |
'visited_at': now.timestamp(),
|
| 2054 |
'visited_at_str': now.strftime('%Y-%m-%d %H:%M:%S'),
|
| 2055 |
'bonuses': 0,
|
| 2056 |
'history': [],
|
| 2057 |
'debts': 0,
|
| 2058 |
'debt_history': [],
|
| 2059 |
+
'invoices': [],
|
| 2060 |
+
'ton_address': ""
|
| 2061 |
}
|
| 2062 |
user_id_to_save = new_user_id
|
| 2063 |
|
| 2064 |
+
all_data[user_id_to_save] = user_entry
|
| 2065 |
+
save_visitor_data(all_data)
|
| 2066 |
|
| 2067 |
return jsonify({"status": "ok", "verified": True, "user_id": user_id_to_save})
|
| 2068 |
else:
|
| 2069 |
return jsonify({"status": "error", "verified": True, "message": "User ID not found in parsed data"}), 400
|
| 2070 |
else:
|
|
|
|
| 2071 |
return jsonify({"status": "error", "verified": False, "message": "Invalid data"}), 403
|
| 2072 |
|
| 2073 |
+
except Exception:
|
| 2074 |
+
return jsonify({"status": "error", "message": "Internal server error"}), 500
|
| 2075 |
+
|
| 2076 |
+
@app.route('/save_ton_address', methods=['POST'])
|
| 2077 |
+
def save_ton_address():
|
| 2078 |
+
try:
|
| 2079 |
+
req_data = request.get_json()
|
| 2080 |
+
internal_user_id = req_data.get('user_id')
|
| 2081 |
+
ton_address = req_data.get('ton_address')
|
| 2082 |
+
init_data_str = req_data.get('initData')
|
| 2083 |
+
|
| 2084 |
+
if not internal_user_id:
|
| 2085 |
+
return jsonify({"status": "error", "message": "Internal User ID is required"}), 400
|
| 2086 |
+
|
| 2087 |
+
user_data_parsed, is_valid = verify_telegram_data(init_data_str)
|
| 2088 |
+
if not is_valid:
|
| 2089 |
+
return jsonify({"status": "error", "message": "Invalid Telegram InitData"}), 403
|
| 2090 |
+
|
| 2091 |
+
user_info_dict = {}
|
| 2092 |
+
if user_data_parsed and 'user' in user_data_parsed:
|
| 2093 |
+
try:
|
| 2094 |
+
user_json_str = unquote(user_data_parsed['user'][0])
|
| 2095 |
+
user_info_dict = json.loads(user_json_str)
|
| 2096 |
+
except Exception:
|
| 2097 |
+
user_info_dict = {}
|
| 2098 |
+
|
| 2099 |
+
telegram_user_id = user_info_dict.get('id')
|
| 2100 |
+
|
| 2101 |
+
all_data = load_visitor_data()
|
| 2102 |
+
user_entry = all_data.get(internal_user_id)
|
| 2103 |
+
|
| 2104 |
+
if not user_entry or internal_user_id == "organization_details":
|
| 2105 |
+
return jsonify({"status": "error", "message": "User not found"}), 404
|
| 2106 |
+
|
| 2107 |
+
if user_entry.get('telegram_id') is not None and str(user_entry.get('telegram_id')) != str(telegram_user_id):
|
| 2108 |
+
return jsonify({"status": "error", "message": "Telegram user ID mismatch or not authorized to update this account"}), 403
|
| 2109 |
+
|
| 2110 |
+
user_entry['ton_address'] = ton_address
|
| 2111 |
+
all_data[internal_user_id] = user_entry
|
| 2112 |
+
save_visitor_data(all_data)
|
| 2113 |
+
|
| 2114 |
+
return jsonify({"status": "ok", "message": "TON address updated successfully"}), 200
|
| 2115 |
+
|
| 2116 |
+
except Exception:
|
| 2117 |
return jsonify({"status": "error", "message": "Internal server error"}), 500
|
| 2118 |
|
| 2119 |
@app.route('/admin')
|
|
|
|
| 2121 |
all_data = load_visitor_data()
|
| 2122 |
users_list = []
|
| 2123 |
for user_id, user_data in all_data.items():
|
| 2124 |
+
if user_id == "organization_details":
|
| 2125 |
continue
|
| 2126 |
user_data['id'] = user_id
|
| 2127 |
users_list.append(user_data)
|
|
|
|
| 2152 |
|
| 2153 |
all_data = load_visitor_data()
|
| 2154 |
|
|
|
|
| 2155 |
for key, user in all_data.items():
|
| 2156 |
if key == "organization_details":
|
| 2157 |
continue
|
|
|
|
| 2177 |
'history': [],
|
| 2178 |
'debts': 0,
|
| 2179 |
'debt_history': [],
|
| 2180 |
+
'invoices': [],
|
| 2181 |
+
'ton_address': ""
|
| 2182 |
}
|
| 2183 |
|
| 2184 |
+
all_data[new_id] = new_client
|
| 2185 |
save_visitor_data(all_data)
|
| 2186 |
|
| 2187 |
return jsonify({"status": "ok", "message": "Client added successfully"}), 201
|
| 2188 |
|
| 2189 |
+
except Exception:
|
| 2190 |
+
return jsonify({"status": "error", "message": "Internal server error"}), 500
|
|
|
|
| 2191 |
|
| 2192 |
|
| 2193 |
@app.route('/admin/add_transaction', methods=['POST'])
|
|
|
|
| 2220 |
if repay_debt_amount > user.get('debts', 0):
|
| 2221 |
return jsonify({"status": "error", "message": "Сумма погашения превышает текущий долг"}), 400
|
| 2222 |
|
|
|
|
| 2223 |
accrual_amount = purchase_amount * 0.02
|
| 2224 |
user['bonuses'] = round(user.get('bonuses', 0) + accrual_amount - deduct_amount, 2)
|
| 2225 |
if 'history' not in user or not isinstance(user['history'], list):
|
|
|
|
| 2238 |
"date": now_iso, "date_str": now_str
|
| 2239 |
})
|
| 2240 |
|
|
|
|
| 2241 |
user['debts'] = round(user.get('debts', 0) + add_debt_amount - repay_debt_amount, 2)
|
| 2242 |
if 'debt_history' not in user or not isinstance(user['debt_history'], list):
|
| 2243 |
user['debt_history'] = []
|
|
|
|
| 2255 |
"date": now_iso, "date_str": now_str
|
| 2256 |
})
|
| 2257 |
|
| 2258 |
+
all_data[user_id_str] = user
|
| 2259 |
save_visitor_data(all_data)
|
| 2260 |
|
| 2261 |
return jsonify({
|
|
|
|
| 2263 |
"new_balance": user['bonuses'], "new_debt": user['debts']
|
| 2264 |
}), 200
|
| 2265 |
|
| 2266 |
+
except Exception:
|
| 2267 |
+
return jsonify({"status": "error", "message": "Internal server error"}), 500
|
|
|
|
| 2268 |
|
| 2269 |
@app.route('/admin/add_invoice', methods=['POST'])
|
| 2270 |
def add_invoice():
|
|
|
|
| 2290 |
now_iso = now.isoformat()
|
| 2291 |
now_str = now.strftime('%Y-%m-%d %H:%M:%S')
|
| 2292 |
|
| 2293 |
+
invoice_id = str(uuid.uuid4().hex[:8]).upper()
|
| 2294 |
|
| 2295 |
processed_items = []
|
| 2296 |
for item in items:
|
|
|
|
| 2322 |
|
| 2323 |
return jsonify({"status": "ok", "message": "Invoice added successfully", "invoice_id": invoice_id}), 200
|
| 2324 |
|
| 2325 |
+
except Exception:
|
| 2326 |
+
return jsonify({"status": "error", "message": "Internal server error"}), 500
|
|
|
|
| 2327 |
|
| 2328 |
@app.route('/admin/delete_invoice', methods=['POST'])
|
| 2329 |
def delete_invoice():
|
|
|
|
| 2356 |
|
| 2357 |
return jsonify({"status": "ok", "message": "Invoice deleted successfully"}), 200
|
| 2358 |
|
| 2359 |
+
except Exception:
|
| 2360 |
+
return jsonify({"status": "error", "message": "Internal server error"}), 500
|
|
|
|
| 2361 |
|
| 2362 |
|
| 2363 |
@app.route('/admin/delete_client', methods=['POST'])
|
|
|
|
| 2370 |
return jsonify({"status": "error", "message": "User ID is required"}), 400
|
| 2371 |
|
| 2372 |
user_id_str = str(user_id)
|
| 2373 |
+
all_data = load_visitor_data()
|
| 2374 |
|
| 2375 |
+
with _data_lock:
|
| 2376 |
if user_id_str not in all_data or user_id_str == "organization_details":
|
| 2377 |
return jsonify({"status": "error", "message": "User not found"}), 404
|
| 2378 |
|
|
|
|
| 2380 |
if user_to_delete.get('telegram_id') is not None:
|
| 2381 |
return jsonify({"status": "error", "message": "Cannot delete a Telegram-linked user"}), 403
|
| 2382 |
|
| 2383 |
+
del all_data[user_id_str]
|
| 2384 |
|
| 2385 |
try:
|
|
|
|
| 2386 |
with open(DATA_FILE, 'w', encoding='utf-8') as f:
|
| 2387 |
json.dump(all_data, f, ensure_ascii=False, indent=4)
|
|
|
|
| 2388 |
upload_data_to_hf_async()
|
| 2389 |
+
except Exception:
|
|
|
|
| 2390 |
return jsonify({"status": "error", "message": "Failed to save data after deletion"}), 500
|
| 2391 |
|
| 2392 |
return jsonify({"status": "ok", "message": "Client deleted successfully"}), 200
|
| 2393 |
|
| 2394 |
+
except Exception:
|
| 2395 |
+
return jsonify({"status": "error", "message": "Internal server error"}), 500
|
|
|
|
| 2396 |
|
| 2397 |
@app.route('/admin/organization_details', methods=['GET'])
|
| 2398 |
def get_organization_details():
|
|
|
|
| 2400 |
all_data = load_visitor_data()
|
| 2401 |
org_details = all_data.get('organization_details', {})
|
| 2402 |
return jsonify(org_details), 200
|
| 2403 |
+
except Exception:
|
| 2404 |
+
return jsonify({"status": "error", "message": "Internal server error"}), 500
|
|
|
|
| 2405 |
|
| 2406 |
@app.route('/admin/organization_details', methods=['POST'])
|
| 2407 |
def save_organization_details():
|
|
|
|
| 2417 |
|
| 2418 |
all_data = load_visitor_data()
|
| 2419 |
all_data['organization_details'] = new_org_details
|
| 2420 |
+
save_visitor_data(all_data)
|
| 2421 |
|
| 2422 |
return jsonify({"status": "ok", "message": "Organization details saved successfully"}), 200
|
| 2423 |
+
except Exception:
|
| 2424 |
+
return jsonify({"status": "error", "message": "Internal server error"}), 500
|
|
|
|
| 2425 |
|
| 2426 |
if __name__ == '__main__':
|
|
|
|
|
|
|
| 2427 |
if not HF_TOKEN_READ or not HF_TOKEN_WRITE:
|
| 2428 |
+
pass
|
| 2429 |
else:
|
|
|
|
| 2430 |
download_data_from_hf()
|
| 2431 |
|
| 2432 |
+
load_visitor_data()
|
| 2433 |
|
|
|
|
|
|
|
| 2434 |
if HF_TOKEN_WRITE:
|
| 2435 |
backup_thread = threading.Thread(target=periodic_backup, daemon=True)
|
| 2436 |
backup_thread.start()
|
|
|
|
| 2437 |
|
|
|
|
| 2438 |
app.run(host=HOST, port=PORT, debug=False)
|