Kgshop commited on
Commit
0e018d0
·
verified ·
1 Parent(s): 96fde15

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +355 -172
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 # Import pytz for timezone handling
15
- import uuid # For generating unique invoice IDs
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.INFO)
35
  app.secret_key = os.urandom(24)
36
 
37
  _data_lock = threading.Lock()
38
- visitor_data_cache = {} # This will store all data, including organization details
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
- logging.info("Successfully loaded downloaded data into cache.")
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
- logging.error(f"Hugging Face repository '{REPO_ID}' not found. Cannot download data.")
76
- except Exception as e:
77
- logging.error(f"Error downloading data from Hugging Face: {e}")
78
  return False
79
 
80
  def load_visitor_data():
81
  global visitor_data_cache
82
  with _data_lock:
83
- if not visitor_data_cache: # Only load from file if cache is empty
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
- logging.warning(f"{DATA_FILE} not found locally. Starting with empty data.")
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 as e:
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 as e:
135
- logging.error(f"Error saving visitor data: {e}")
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
- logging.info("Bonus data successfully uploaded to Hugging Face.")
163
- except Exception as e:
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
- logging.warning(f"Telegram InitData is older than 24 hours (Auth Date: {auth_date}, Current: {current_time}).")
200
  return parsed_data, True
201
  else:
202
- logging.warning(f"Data verification failed. Calculated: {calculated_hash}, Received: {received_hash}")
203
  return parsed_data, False
204
- except Exception as e:
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; /* Adjusted for nav buttons */
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; /* Hidden by default */
300
  flex-direction: column;
301
  gap: var(--padding-m);
302
  }
303
  .content-section.active {
304
- display: flex; /* Shown when active */
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
- /* Business Card Styles */
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; /* For long URLs */
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,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSJjdXJyZW50Q29sb3IiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48cGF0aCBkPSJtMjIuMDIyIDEuOTY3LTMuMDkgMy41MjNhOC40OCA4LjQ4IDAgMCAwLTEzLjA5OSAwTDEuOTc4IDEuOTY3Yy0uOTc0LTEuMTA3LTIuNTI1LS40MS0yLjUxMiAxLjAzNXYxOC44NjhjLjAwMSAxLjQ0NSAxLjUyOCAyLjEzNiAyLjQ4OCAxLjAzN2wzLjA5LTMuNTIzYTEwLjE2IDEwLjE2IDAgMCAwIDEzLjA5OSAwbDMuMDkgMy41MjNjLjk3NCAxLjEwNyAyLjUyNS40MSAyLjUxMi0xLjAzN1YyLjAwMWMtLjAwMS0xLjQ0NS0xLjUyOC0yLjEzNi0yLjQ4OC0xLjAzN3oiLz48cGF0aCBkPSJtNy41IDIuOTc2YzEuMjI1LjA2MSAyLjQ1LjA5MSAzLjY3NS4wOTFzMi40NS0uMDMxIDMuNjc1LS4wOTFjLjY4Ny0uMDM0IDEuNDMzLjMyNSAxLjcwOS43NDdsMS40NzMgMS42NzhjLjQ5LjU1Ny44MzcgMS4yNjcuODk5IDIuMDgzLjA0MS41NTEuMDYyIDEuMS4wNjIgMS42NnYxLjUwNGMwIC43NzUtLjE3IDEuNTUzLS41MDggMi4yMzMtLjQ3Mi45MjUtMS4xMTcgMS43NDgtMS45MzUgMi4zODhsLS4xMzQuMTA1Yy0uNzU0LjU4My0xLjY0Ny45NzUtMi41NjIgMS4xODQtLjYwMi4xMzYtMS4wMDkuMjA4LTEuNzI1LjIwOC0xLjExNSAwLjc1NC0uMTUzIDMuNjgyLS4xNTMgNS41NDJzLjE1NCA0Ljc4Ni4xNTMgNS41NDJjLjAwMi0uNjU3LS4wNzgtMS4yNy0uMjItMS44MjQtLjE0OC0uNTU4LS40MTctMS4wODgtLjc2NC0xLjU1MS0uNjMyLS44NDUtMS40NDctMS41NTctMi40MTItMi4wNjItLjk2My0uNTA3LTEuOTk0LS44Mi0zLjA0Ni0uOTUyLS44MTMt.MTA0LTEuNTcxLS4xNDUtMi4zMzgtLjA5OS0uNjk0LjAxMS0xLjMzNy4xMDYtMS45MjQuMjg1LS41ODkuMTg0LTEuMTI2LjQyMS0xLjYwMS42OTMtLjQ3Ni4yNzMtLjkwNi41NzQtMS4yOTcuODktLjM4OC4zMTQtLjc0My42NDctMS4wNjcuOTk4LS4zMjYuMzUzLS42NDYuNzIyLS45NTkuMTA1OS0uMzEzLjMyOC0uNjIuNjUzLS45MjEuOTc1LS4yOTQuMzExLS41NzYuNjIzLS44NDQuOTMyLS4yNy4zMDktLjUyMy42MTctLjc1Ny45MTgtLjE5Ny4yNTQtLjM2MS40OTMtLjQ4MS43MjUtLjExOS4yMzItLjE4NS40NTItLjE5My41OTMtLjAwOS4xNDUtLjAxNC4yOTMtLjAxNi40NDF2MS43NmMwIC41NzYtLjE3NSAxLjEyMi0uNDQ3IDEuNTYtLjIyNy4zOTMtLjU2NS41OTktMS4wMTkuNTk5LTEuMTg2LS4wMDEtMS45OTYtMS4zOTctMi4yOTYtMi44NDItLjMyMy0xLjU1OC0uMzIzLTQuNTY5LS4zMjMtNi40MTFzLjAxNS00Ljg1NC4zMjMtNi40MTJjLjI5OS0xLjQ0NSAxLjExLTIuODQxIDIuMjk2LTIuODQyLjQ1NC4wMDcuNzgxLjI1OSAxLjAxOS42MDkuMjE1LjMzNC4zMjMuNzMuMzIzIDEuMTQ3di45NWMuMDMgMS4zMTQtLjAxNSAyLjYxLS4xNDcgMy44NzUtLjEwNiAxLjAzLS4yMzQgMi4wNDYtLjM1MSAyLjk5NmwuNTkzLS4zNzljLjMyNi0uMjA2LjY4Mi0uMzgxIDEuMDQ5LS41NzEuMzg2LS4xOTcgLjc5LS4zNjUgMS4xOTQtLjQ5NC40MDUtLjEyOS43ODctLjIzMyAxLjEyOC0uMjkwLjM0Mi0uMDU4LjYwNC0uMDc0Ljc4Mi0uMDQ3WiIvPjwvc3ZnPg==">
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,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgZmlsbD0iI2ZmZiIgY2xhc3M9ImJpIGJpLXdoYXRzYXBwIiB2aWV3Qm94PSIwIDAgMTYgMTYiPjxwYXRoIGQ9Ik0xMy42NzYgMS40NTJBMTAuNTE1IDEwLjUxNSAwIDAgMCA3Ljg4MyAwQzMuNTEgMCAuMDYzIDMuNDQuMDYzIDcuNjU0YzAgMS40MS4zNDcgMi44NjIgLjk2NiA0LjExOC4wNC4wNy4wNjYuMTc2LjA3My4yOTlsLjA2NSAxLjg5MmExLjIzMyAxLjIzMyAwIDAgMCAxLjU1MyAxLjQ1NmwxLjg4Ljc4Yy4xMzYuMDU2LjI3Ni4wOTYuNDI4LjE3MiAxLjI1Ni42NjggMi42MTQuOTgyIDQuMDQ4Ljk4MiA0LjM3MiAwIDcuOTEtMy40NCA3LjkxLTcuNjU0IDAtMi4wNDItLjg1LTMuOTY1LTIuMzQxLTUuMzUzWm0tMi42NzYgOS4yMzFhLjg4MS44ODEgMCAwIDEtMS4yMjUuMDM2bC0uNzk0LS40ODMtLjQ2NC4zNjYtLjY1MS0uNDktMS4xNjYgMS4xNjYtLjUzLS4zMTctLjA4NC41NTUtLjEwMi40NTktLjI1MS4xNjItLjU3LjU3LS41NTUuMDgyLS4zMjIuMDY5LS42Mi0uMTc3LS45ODQtLjE5LS45MDgtLjYxNy0uNDc4LS45MDktLjY3Ny0uNzUxLS40MzQtMS4xNzgtLjMyMi0xLjQ3MS0uMzIyLS42MDcgMC0uODguMDQyLS45NjYuMDg4LS40OTcuMjU3LS42MTEuNzMyLS42MTEgMS4xMzdhMS42MzkgMS42MzkgMCAwIDAgLjQ4NiAxLjY5Yy40MTYuNDE2Ljc2Mi43MjQuNzYyLjk1NSAwIC41NDIuMjgyIDEuMDQ2LjMwNCAxLjQxMS41ODYgMS4wNTEgMS43NzUgMS44NDcgMy4wNCAyLjAxOCAxLjMxMi4xNzcgMi4xMDYuMDk1IDIuNzYzLjA3Mi4wOTYtLjE1LjM3Mi0uMjg1LjcwMi0uNDgzLjMxLS40OC41MDktLjUxNS42NjktLjQ1Mi4xMDkuMDUxLjQxNy4yMTEuNDYzLjI2Ny4xNDEuMDgyLjI4NC4xNjEuNDQzLjIyNWExLjIyNyAxLjIyNyAwIDAgMCAuNzQ2LjAyNGwuMjg0LS4xMzVhMy45NjcgMy45NjcgMCAwIDAgLjY2Mi0uNjQzLjkwOC45MDggMCAwIDAgLjMwMi0uNjc4LjE5OC4xOTggMCAwIDAgMC0uMTU2LjgxNS44MTUgMCAwIDAgMC0uNDc3eiIvPjwvc3ZnPg==">
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,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgZmlsbD0iI2ZmZiIgY2xhc3M9ImJpIGJpLXQtZWxlZ3JhbSIgdmlld0JveD0iMCAwIDE2IDE2Ij48cGF0aCBkPSJNMTYuMDAyIDguNjkyYy4wMDItLjg3Ni0uNDQ1LTEuMzM1LTEuMzAzLTEuNjk0LS4zMjctLjEwNy0uNjk0LS4yMzctMS4wODgtLjM2Ny0xLjYzNi0uNTE2LTIuOTQzLTEuMzMtNC4xMy0yLjU0NS0uODkxLS44OC0xLjY0Ny0xLjcxLTIuMzc0LTIuNDE4LS40MTctLjM5My0uNTM3LS42Ni0uNTAyLS44NTEuMDExLS4wNjkuMDc1LS4yNTguMDgyLS4zNDkuMDY2LS44MjktLjMyLS45NzgtMS4xNS0uNzAyLS40ODcuMTU3LS45OTMuMzItMS41MTMuNDc1LS4yMS4wNjItLjM3OC4wOTktLjUuMTItLjU3LjA4OC0xLjE3NS4yNjQtMS43MDkuNDMzLS4zMy4xMTgtLjY3LjI1MS0xLjAyLjM4OC0uNzUuMjczLTEuNTA5LjU0Ni0yLjI3LjgwNi0uNzg1LjI2Mi0xLjQ3NC40NzUtMi4wNjQuNjQtLjUzNC4xNDYtLjkzNy4xNi0xLjIxMi0uMDctLjQ4NS0uNDA5LS45MS0uOTQ2LS40NzYtMS40NTEuMTU1LS4xNy4zNDctLjMyNi41NzYtLjQ2Ny4xNjktLjEwNS4zNjMtLjIwNy41ODItLjMxMy40NjktLjIyLjkyNi0uMzc0IDEuMzY4LS40NjkuNjMyLS4xMyAxLjI3MS0uMjE2IDEuOTItLjI3MS4yNzktLjAyNC41NTMtLjA0LjgyMy0uMDYuMDYxLS4xNzguMjUzLS41NTYuNTQ3LTEuMTUyLjY4MS0xLjM2NSAxLjQxNy0yLjE4MSAyLjE2Ni0yLjczMi42NzYtLjUwOCAxLjQwNy0uOTUzIDIuMjY2LTEuMjg5LjI2MS0uMTA5LjUyLS4xNDMuNzc4LS4wNzIuOTc0LjI3NSAxLjc2OC41NzIgMi4zNzUuODcxLjMwNS4xNS41NzcuMzEuNzk0LjQ1MS4yMTQuMTQzLjMyNC4yMi4zMjQuMjIuMDg2LS4yNzguMjYzLS42MS40NzYtLjkxMy4wNjItLjA4OC4xMzQtLjE4NS4yMTUtLjI4OC41MTQtLjY2MiAxLjExLS44NzUgMS44MS0uNTMxLjY0NC4zMTEgMS4xNzcuOCAxLjczMiAxLjUyLjQ3NS42ODkuOTUgMS4zNzkgMS40MjUgMi4wNzcuMzYzLjU1LjY4IDEuMDcuOTU3IDEuNTUzLjY1MiAxLjE0OSAxLjEzOCAxLjgxMiAxLjQ1MiAyLjEyNi40NzYuNDg3Ljg2LjgyNyAxLjI0MyAxLjA5Ni43NDEuNTIzIDEuNDg1LjkzNCAyLjIzNSAxLjIxMS43NDcuMjc0IDEuNDU1LjQ4NCAyLjExMy42MzYuNTY3LjEzIDEuMTU0LjE4MiAxLjczLjE1LjcxNC0uMDM1IDEuNDEtLjI0MyAxLjk3LS41Ny41ODEtLjMzOCAxLjA1NS0uODI5IDEuMzk3LTEuMzU1LjExOS0uMTc1LjIwMi0uMzguMjUtLjU4My4wNDctLjE4OS4wNjctLjM4OS4wNi0uNTkzWiIvPjwvc3ZnPg==">
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(error => {
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(); // Load history and invoices for current user
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 { // debt
1344
- sign = item.type === 'accrual' ? '+' : '-'; // 'accrual' for debt means debt increases (positive)
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(error => {
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; // Index for current item in newInvoiceItems
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); // rowIndex is 1-based, -1 for header
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"][step="1"]').setAttribute('oninput', `updateInvoiceItem(${i}, 'quantity', parseFloat(this.value))`);
1651
- row.querySelector('input[type="number"][step="0.01"]').setAttribute('oninput', `updateInvoiceItem(${i}, 'unit_price', parseFloat(this.value))`);
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(); // Reload the page to update the list
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(); // Add an empty row for new invoice input
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() # Load all data, including organization details
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', []) # Pass invoices to template
 
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 as e:
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() # Get current state of all 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, # No phone number from Telegram initData by default
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': [] # Initialize invoices list
 
1900
  }
1901
  user_id_to_save = new_user_id
1902
 
1903
- all_data[user_id_to_save] = user_entry # Update the global cache
1904
- save_visitor_data(all_data) # Save the entire cache
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 as e:
1914
- logging.exception("Error in /verify endpoint")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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": # Skip 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': [] # Initialize invoices for new manual client
 
1980
  }
1981
 
1982
- all_data[new_id] = new_client # Update the global cache
1983
  save_visitor_data(all_data)
1984
 
1985
  return jsonify({"status": "ok", "message": "Client added successfully"}), 201
1986
 
1987
- except Exception as e:
1988
- logging.exception("Error in /admin/add_client endpoint")
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 # Update the global cache
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 as e:
2068
- logging.exception("Error in /admin/add_transaction endpoint")
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() # Generate a short unique ID
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 as e:
2128
- logging.exception("Error in /admin/add_invoice endpoint")
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 as e:
2163
- logging.exception("Error in /admin/delete_invoice endpoint")
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() # Load current state
2178
 
2179
- with _data_lock: # Ensure thread-safe modification of cache
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] # Modify the loaded data
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 as e:
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 as e:
2202
- logging.exception("Error in /admin/delete_client endpoint")
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 as e:
2212
- logging.exception("Error getting organization details")
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) # Save the entire updated cache
2230
 
2231
  return jsonify({"status": "ok", "message": "Organization details saved successfully"}), 200
2232
- except Exception as e:
2233
- logging.exception("Error saving organization details")
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
- print("WARNING: Hugging Face token(s) not set. Backup/restore functionality will be limited.")
2241
  else:
2242
- print("Attempting initial data download from Hugging Face...")
2243
  download_data_from_hf()
2244
 
2245
- load_visitor_data() # Ensure data is loaded into cache at startup
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)