Kgshop commited on
Commit
57a6639
·
verified ·
1 Parent(s): 793bf62

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +173 -127
app.py CHANGED
@@ -32,6 +32,13 @@ app.secret_key = os.urandom(24)
32
  _data_lock = threading.Lock()
33
  visitor_data_cache = {}
34
 
 
 
 
 
 
 
 
35
  def download_data_from_hf():
36
  global visitor_data_cache
37
  if not HF_TOKEN_READ:
@@ -166,13 +173,6 @@ def verify_telegram_data(init_data_str):
166
  logging.error(f"Error verifying Telegram data: {e}")
167
  return None, False
168
 
169
- def generate_unique_bonus_id(all_data):
170
- existing_ids = {user.get('bonus_id') for user in all_data.values() if 'bonus_id' in user}
171
- while True:
172
- new_id = str(random.randint(10000, 99999))
173
- if new_id not in existing_ids:
174
- return new_id
175
-
176
  TEMPLATE = """
177
  <!DOCTYPE html>
178
  <html lang="ru">
@@ -183,41 +183,33 @@ TEMPLATE = """
183
  <script src="https://telegram.org/js/telegram-web-app.js"></script>
184
  <link rel="preconnect" href="https://fonts.googleapis.com">
185
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
186
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
187
  <style>
188
  :root {
189
- --brand-yellow: #FFC107;
190
- --brand-yellow-glow: rgba(255, 193, 7, 0.5);
191
- --brand-black: #0d0d0d;
192
- --card-bg: #1a1a1a;
193
- --card-bg-gradient: radial-gradient(circle, #2a2a2a 0%, #1a1a1a 100%);
194
- --text-color: #f5f5f7;
195
- --text-secondary-color: #888888;
 
 
196
  --border-radius-l: 24px;
197
- --padding-m: 20px;
198
- --padding-l: 30px;
199
  --font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
200
- --shadow-color: rgba(0, 0, 0, 0.6);
201
- --shadow-glow: 0 0 40px var(--brand-yellow-glow);
202
- }
203
- @keyframes fadeIn {
204
- from { opacity: 0; transform: translateY(20px); }
205
- to { opacity: 1; transform: translateY(0); }
206
- }
207
- @keyframes pulseGlow {
208
- 0% { text-shadow: 0 0 4px var(--brand-yellow-glow), 0 0 10px var(--brand-yellow-glow); }
209
- 50% { text-shadow: 0 0 10px var(--brand-yellow-glow), 0 0 30px var(--brand-yellow-glow); }
210
- 100% { text-shadow: 0 0 4px var(--brand-yellow-glow), 0 0 10px var(--brand-yellow-glow); }
211
  }
212
  * { box-sizing: border-box; margin: 0; padding: 0; }
213
  html, body {
214
- background-color: var(--brand-black);
215
  font-family: var(--font-family);
216
  color: var(--text-color);
217
  padding: var(--padding-m);
218
  overscroll-behavior-y: none;
219
  -webkit-font-smoothing: antialiased;
220
  -moz-osx-font-smoothing: grayscale;
 
221
  visibility: hidden;
222
  min-height: 100vh;
223
  }
@@ -227,94 +219,132 @@ TEMPLATE = """
227
  display: flex;
228
  flex-direction: column;
229
  gap: 2rem;
230
- animation: fadeIn 0.8s ease-out forwards;
231
  }
232
  .header {
233
  text-align: center;
234
  padding: var(--padding-m) 0;
235
- animation-delay: 0.1s;
 
 
 
 
236
  }
237
  .logo {
238
  font-size: 2.5em;
239
  font-weight: 800;
240
  color: var(--brand-yellow);
241
  letter-spacing: 2px;
242
- text-transform: uppercase;
243
  }
244
  .welcome-text {
245
  font-size: 1.1em;
246
- color: var(--text-secondary-color);
247
- margin-top: 10px;
 
248
  }
249
  .bonus-card {
250
- background: var(--card-bg-gradient);
251
  border-radius: var(--border-radius-l);
252
  padding: var(--padding-l);
253
  text-align: center;
254
- border: 1px solid rgba(255, 193, 7, 0.2);
255
- box-shadow: 0 10px 30px var(--shadow-color);
256
- transition: transform 0.3s ease, box-shadow 0.3s ease;
257
- animation-delay: 0.2s;
 
 
 
 
 
 
 
258
  }
259
- .bonus-card:hover {
260
- transform: translateY(-8px) scale(1.02);
261
- box-shadow: 0 15px 40px var(--shadow-color), var(--shadow-glow);
 
262
  }
263
  .bonus-label {
264
- font-size: 1.3em;
265
  font-weight: 500;
266
- color: var(--text-secondary-color);
267
- margin-bottom: 15px;
268
- text-transform: uppercase;
269
- letter-spacing: 1px;
270
  }
271
  .bonus-amount {
272
  font-size: 4.5em;
273
  font-weight: 800;
274
  color: var(--brand-yellow);
275
  line-height: 1;
276
- animation: pulseGlow 3s infinite ease-in-out;
277
  }
278
  .history-section {
279
- background-color: var(--card-bg);
280
- border-radius: var(--border-radius-l);
281
- padding: var(--padding-l);
282
- border: 1px solid rgba(255, 255, 255, 0.08);
283
- animation-delay: 0.3s;
284
  }
285
  .history-title {
286
- font-size: 1.6em;
287
  font-weight: 700;
288
- margin-bottom: var(--padding-m);
289
- padding-bottom: var(--padding-m);
290
- border-bottom: 1px solid rgba(255, 255, 255, 0.1);
 
291
  }
292
  .history-list {
293
  list-style: none;
294
  padding: 0;
295
  margin: 0;
296
- max-height: 400px;
297
  overflow-y: auto;
298
  }
299
  .history-item {
300
  display: flex;
301
  justify-content: space-between;
302
  align-items: center;
303
- padding: 18px 8px;
304
- border-bottom: 1px solid rgba(255, 255, 255, 0.05);
305
- transition: background-color 0.2s ease;
 
 
 
306
  }
307
  .history-item:hover {
308
- background-color: rgba(255, 255, 255, 0.03);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
309
  }
310
- .history-item:last-child { border-bottom: none; }
311
- .history-details { display: flex; flex-direction: column; }
312
- .history-description { font-size: 1.05em; font-weight: 500; }
313
- .history-date { font-size: 0.85em; color: var(--text-secondary-color); margin-top: 5px; }
314
- .history-amount { font-size: 1.3em; font-weight: 700; }
315
- .history-amount.accrual { color: #28a745; }
316
- .history-amount.deduction { color: #dc3545; }
317
- .no-history { text-align: center; color: var(--text-secondary-color); padding: 3rem 0; font-size: 1.1em; }
318
  </style>
319
  </head>
320
  <body>
@@ -356,11 +386,9 @@ TEMPLATE = """
356
 
357
  function applyTheme(themeParams) {
358
  const root = document.documentElement;
359
- if (themeParams.bg_color) root.style.setProperty('--brand-black', themeParams.bg_color);
360
  if (themeParams.text_color) root.style.setProperty('--text-color', themeParams.text_color);
361
- if (themeParams.hint_color) root.style.setProperty('--text-secondary-color', themeParams.hint_color);
362
- if (themeParams.button_color) root.style.setProperty('--brand-yellow', themeParams.button_color);
363
- if (themeParams.secondary_bg_color) root.style.setProperty('--card-bg', themeParams.secondary_bg_color);
364
  }
365
 
366
  function setupTelegram() {
@@ -384,7 +412,10 @@ TEMPLATE = """
384
  if (!userIdForTest) {
385
  fetch('/verify', {
386
  method: 'POST',
387
- headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
 
 
 
388
  body: JSON.stringify({ initData: tg.initData }),
389
  })
390
  .then(response => response.json())
@@ -436,57 +467,60 @@ ADMIN_TEMPLATE = """
436
  <title>Druzhba Admin</title>
437
  <link rel="preconnect" href="https://fonts.googleapis.com">
438
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
439
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
440
  <style>
441
  :root {
442
- --admin-bg: #f8f9fa;
443
- --admin-text: #212529;
444
- --admin-card-bg: #ffffff;
445
- --admin-border: #dee2e6;
446
- --admin-shadow: rgba(0, 0, 0, 0.05);
447
  --admin-primary: #FFC107;
448
  --admin-primary-dark: #e0a800;
449
- --admin-secondary: #6c757d;
450
- --admin-success: #198754;
451
  --admin-danger: #dc3545;
452
- --border-radius: 12px;
453
  --padding: 1.5rem;
454
  --font-family: 'Inter', sans-serif;
455
  }
456
  body { font-family: var(--font-family); background-color: var(--admin-bg); color: var(--admin-text); margin: 0; padding: var(--padding); line-height: 1.6; }
457
- .container { max-width: 1200px; margin: 0 auto; }
458
- h1 { text-align: center; color: var(--admin-secondary); margin-bottom: var(--padding); font-weight: 600; }
459
- .controls-bar { display: flex; gap: 1rem; align-items: center; background: var(--admin-card-bg); padding: var(--padding); border-radius: var(--border-radius); box-shadow: 0 4px 15px var(--admin-shadow); border: 1px solid var(--admin-border); margin-bottom: var(--padding); }
460
- .controls-bar input[type="text"] { flex-grow: 1; padding: 12px 15px; font-size: 1.1em; border-radius: 8px; border: 1px solid var(--admin-border); box-sizing: border-box; }
461
- .btn { padding: 12px 20px; font-size: 1em; border: none; border-radius: 8px; font-weight: 600; cursor: pointer; transition: background-color 0.2s ease, transform 0.2s ease; }
462
- .btn:hover { transform: translateY(-2px); }
463
  .btn-primary { background-color: var(--admin-primary); color: #000; }
464
- .btn-primary:hover { background-color: var(--admin-primary-dark); }
465
- .user-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: var(--padding); margin-top: var(--padding); }
466
- .user-card { background-color: var(--admin-card-bg); border-radius: var(--border-radius); padding: var(--padding); box-shadow: 0 4px 15px var(--admin-shadow); border: 1px solid var(--admin-border); display: flex; flex-direction: column; transition: transform 0.2s ease, box-shadow 0.2s ease; }
467
- .user-card:hover { transform: translateY(-5px); box-shadow: 0 8px 25px rgba(0, 0, 0, 0.08); }
468
  .user-info { display: flex; align-items: center; gap: 1rem; margin-bottom: 1rem; }
469
- .user-info img { width: 60px; height: 60px; border-radius: 50%; object-fit: cover; border: 3px solid var(--admin-border); background-color: #eee; }
470
- .user-details .name { font-weight: 600; font-size: 1.2em; }
471
- .user-details .username { color: var(--admin-secondary); font-size: 0.95em; }
472
- .user-details .bonus-id { color: var(--admin-primary-dark); font-size: 0.9em; font-weight: 500; margin-top: 4px; background: #fff8e1; padding: 2px 6px; border-radius: 4px; display: inline-block;}
473
- .user-bonuses { text-align: center; margin-bottom: 1rem; }
474
- .user-bonuses .label { font-size: 0.9em; color: var(--admin-secondary); }
475
- .user-bonuses .amount { font-size: 1.8em; font-weight: 700; color: var(--admin-primary-dark); }
476
- .user-actions .btn-manage { display: block; width: 100%; padding: 10px; background-color: var(--admin-primary); color: #000; border: none; border-radius: 8px; font-weight: 600; cursor: pointer; transition: background-color 0.2s; }
477
  .user-actions .btn-manage:hover { background-color: var(--admin-primary-dark); }
478
  .no-users { text-align: center; color: var(--admin-secondary); margin-top: 2rem; font-size: 1.1em; }
479
- .modal { display: none; position: fixed; z-index: 1001; left: 0; top: 0; width: 100%; height: 100%; overflow: auto; background-color: rgba(0,0,0,0.5); backdrop-filter: blur(5px); }
480
- .modal-content { background-color: var(--admin-bg); margin: 10% auto; padding: var(--padding); border: 1px solid var(--admin-border); width: 90%; max-width: 600px; border-radius: var(--border-radius); position: relative; box-shadow: 0 8px 30px rgba(0,0,0,0.15); }
481
- .modal-close { color: #aaa; position: absolute; top: 15px; right: 25px; font-size: 28px; font-weight: bold; cursor: pointer; }
 
 
 
482
  .modal-header { padding-bottom: 1rem; margin-bottom: 1.5rem; border-bottom: 1px solid var(--admin-border); }
483
  .modal-header h2 { margin: 0; font-size: 1.5rem; }
484
  .modal-header .username { font-size: 1rem; color: var(--admin-secondary); }
485
  .form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; align-items: center; margin-bottom: 1.5rem; }
486
  .form-group { display: flex; flex-direction: column; }
487
- .form-group label { margin-bottom: 0.5rem; font-weight: 500; font-size: 0.9em; }
488
- .form-group input { padding: 10px; font-size: 1rem; border: 1px solid var(--admin-border); border-radius: 8px; width: 100%; box-sizing: border-box; }
489
- .calculation-summary { background: #f0f0f0; padding: 1rem; border-radius: 8px; margin-bottom: 1.5rem; }
490
  .summary-item { display: flex; justify-content: space-between; margin-bottom: 0.5rem; font-size: 0.95em; }
491
  .summary-item strong { font-weight: 600; }
492
  .history-container { margin-top: 1.5rem; }
@@ -501,7 +535,7 @@ ADMIN_TEMPLATE = """
501
  .modal-footer { margin-top: 1.5rem; display: flex; justify-content: flex-end; align-items: center; gap: 1rem;}
502
  .modal-footer button { padding: 12px 25px; font-size: 1.1em; border-radius: 8px; border: none; font-weight: 600; cursor: pointer; }
503
  .btn-submit { background-color: var(--admin-success); color: white; }
504
- .status-message { text-align: center; font-weight: 500; flex-grow: 1; text-align: left; }
505
  </style>
506
  </head>
507
  <body>
@@ -515,13 +549,15 @@ ADMIN_TEMPLATE = """
515
  {% if users %}
516
  <div class="user-grid" id="userGrid">
517
  {% for user in users|sort(attribute='visited_at', reverse=true) %}
518
- <div class="user-card" data-user-id="{{ user.id }}" data-search-term="{{ user.first_name|lower }} {{ user.last_name|lower }} {{ user.username|lower }} {{ user.id }} {{ user.bonus_id }}">
519
  <div class="user-info">
520
- <img src="{{ user.photo_url if user.photo_url else 'data:image/svg+xml;charset=UTF-8,%3csvg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%270 0 100 100%27%3e%3crect width=%27100%27 height=%27100%27 fill=%27%23e9ecef%27/%3e%3ctext x=%2750%25%27 y=%2755%25%27 dominant-baseline=%27middle%27 text-anchor=%27middle%27 font-size=%2745%27 font-family=%27sans-serif%27 fill=%27%23adb5bd%27%3e?%3c/text%3e%3c/svg%3e' }}" alt="User Avatar">
521
  <div class="user-details">
522
  <div class="name">{{ user.first_name or '' }} {{ user.last_name or '' }}</div>
523
- <div class="username">@{{ user.username or 'N/A' }}</div>
524
- <div class="bonus-id">ID: {{ user.bonus_id or 'N/A' }}</div>
 
 
525
  </div>
526
  </div>
527
  <div class="user-bonuses">
@@ -646,18 +682,18 @@ ADMIN_TEMPLATE = """
646
  }
647
 
648
  calculateBonuses();
649
- transactionModal.style.display = 'block';
650
  }
651
 
652
  function openAddClientModal() {
653
  document.getElementById('newClientName').value = '';
654
  document.getElementById('newClientPhone').value = '';
655
  document.getElementById('addClientStatus').textContent = '';
656
- addClientModal.style.display = 'block';
657
  }
658
 
659
  function closeModal(modalId) {
660
- document.getElementById(modalId).style.display = 'none';
661
  if (modalId === 'transactionModal') {
662
  currentUserData = null;
663
  }
@@ -816,12 +852,10 @@ def verify_data():
816
  'visited_at': now.timestamp(),
817
  'visited_at_str': now.strftime('%Y-%m-%d %H:%M:%S')
818
  })
819
- if 'bonus_id' not in user_entry or not user_entry['bonus_id']:
820
- user_entry['bonus_id'] = generate_unique_bonus_id(all_data)
821
  else:
822
  user_entry = {
823
  'id': user_id,
824
- 'bonus_id': generate_unique_bonus_id(all_data),
825
  'first_name': user_info_dict.get('first_name'),
826
  'last_name': user_info_dict.get('last_name'),
827
  'username': user_info_dict.get('username'),
@@ -849,8 +883,18 @@ def verify_data():
849
 
850
  @app.route('/admin')
851
  def admin_panel():
852
- current_data = load_visitor_data()
853
- users_list = list(current_data.values())
 
 
 
 
 
 
 
 
 
 
854
  return render_template_string(ADMIN_TEMPLATE, users=users_list)
855
 
856
  @app.route('/admin/add_client', methods=['POST'])
@@ -866,15 +910,17 @@ def add_client():
866
  all_data = load_visitor_data()
867
 
868
  for user in all_data.values():
869
- if user.get('id') == phone_number or user.get('username') == phone_number:
870
- return jsonify({"status": "error", "message": "Клиент с таким номером телефона уже существует."}), 409
871
 
872
  now = datetime.now()
 
 
873
  new_client = {
874
- 'id': phone_number,
875
- 'bonus_id': generate_unique_bonus_id(all_data),
876
  'first_name': name,
877
- 'last_name': "",
878
  'username': phone_number,
879
  'photo_url': None,
880
  'language_code': 'ru',
@@ -885,7 +931,7 @@ def add_client():
885
  'history': []
886
  }
887
 
888
- save_visitor_data({phone_number: new_client})
889
 
890
  return jsonify({"status": "ok", "message": "Client added successfully"}), 201
891
 
 
32
  _data_lock = threading.Lock()
33
  visitor_data_cache = {}
34
 
35
+ def generate_unique_client_id(all_data):
36
+ existing_client_ids = {user.get('client_id') for user in all_data.values() if user.get('client_id')}
37
+ while True:
38
+ new_id = str(random.randint(10000, 99999))
39
+ if new_id not in existing_client_ids:
40
+ return new_id
41
+
42
  def download_data_from_hf():
43
  global visitor_data_cache
44
  if not HF_TOKEN_READ:
 
173
  logging.error(f"Error verifying Telegram data: {e}")
174
  return None, False
175
 
 
 
 
 
 
 
 
176
  TEMPLATE = """
177
  <!DOCTYPE html>
178
  <html lang="ru">
 
183
  <script src="https://telegram.org/js/telegram-web-app.js"></script>
184
  <link rel="preconnect" href="https://fonts.googleapis.com">
185
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
186
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700;800&display=swap" rel="stylesheet">
187
  <style>
188
  :root {
189
+ --bg-color: #0d0d0d;
190
+ --text-color: #f0f0f0;
191
+ --brand-yellow: #ffd700;
192
+ --card-bg-start: #2a2a2a;
193
+ --card-bg-end: #1c1c1c;
194
+ --secondary-text: #a0a0a0;
195
+ --shadow-color: rgba(255, 215, 0, 0.25);
196
+ --success-color: #4CAF50;
197
+ --danger-color: #F44336;
198
  --border-radius-l: 24px;
199
+ --padding-m: 1.5rem;
200
+ --padding-l: 2rem;
201
  --font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
 
 
 
 
 
 
 
 
 
 
 
202
  }
203
  * { box-sizing: border-box; margin: 0; padding: 0; }
204
  html, body {
205
+ background-color: var(--bg-color);
206
  font-family: var(--font-family);
207
  color: var(--text-color);
208
  padding: var(--padding-m);
209
  overscroll-behavior-y: none;
210
  -webkit-font-smoothing: antialiased;
211
  -moz-osx-font-smoothing: grayscale;
212
+ text-rendering: optimizeLegibility;
213
  visibility: hidden;
214
  min-height: 100vh;
215
  }
 
219
  display: flex;
220
  flex-direction: column;
221
  gap: 2rem;
222
+ perspective: 1500px;
223
  }
224
  .header {
225
  text-align: center;
226
  padding: var(--padding-m) 0;
227
+ animation: fadeInDown 0.8s ease-out both;
228
+ }
229
+ @keyframes fadeInDown {
230
+ from { opacity: 0; transform: translateY(-20px); }
231
+ to { opacity: 1; transform: translateY(0); }
232
  }
233
  .logo {
234
  font-size: 2.5em;
235
  font-weight: 800;
236
  color: var(--brand-yellow);
237
  letter-spacing: 2px;
238
+ text-shadow: 0 0 10px var(--shadow-color);
239
  }
240
  .welcome-text {
241
  font-size: 1.1em;
242
+ color: var(--secondary-text);
243
+ margin-top: 8px;
244
+ font-weight: 500;
245
  }
246
  .bonus-card {
247
+ background: linear-gradient(145deg, var(--card-bg-start), var(--card-bg-end));
248
  border-radius: var(--border-radius-l);
249
  padding: var(--padding-l);
250
  text-align: center;
251
+ border: 1px solid rgba(255, 215, 0, 0.3);
252
+ position: relative;
253
+ transform-style: preserve-3d;
254
+ transform: rotateY(-10deg) rotateX(15deg);
255
+ transition: transform 0.6s cubic-bezier(0.23, 1, 0.32, 1);
256
+ box-shadow: 0 25px 50px -12px rgba(0,0,0,0.5), 0 0 15px var(--shadow-color);
257
+ animation: pulse-glow 5s infinite ease-in-out, fadeInUp 1s 0.2s ease-out both;
258
+ }
259
+ @keyframes fadeInUp {
260
+ from { opacity: 0; transform: translateY(40px) rotateY(-10deg) rotateX(15deg); }
261
+ to { opacity: 1; transform: translateY(0) rotateY(-10deg) rotateX(15deg); }
262
  }
263
+ @keyframes pulse-glow {
264
+ 0% { box-shadow: 0 25px 50px -12px rgba(0,0,0,0.5), 0 0 15px var(--shadow-color); }
265
+ 50% { box-shadow: 0 25px 50px -12px rgba(0,0,0,0.4), 0 0 30px rgba(255, 215, 0, 0.4); }
266
+ 100% { box-shadow: 0 25px 50px -12px rgba(0,0,0,0.5), 0 0 15px var(--shadow-color); }
267
  }
268
  .bonus-label {
269
+ font-size: 1.2em;
270
  font-weight: 500;
271
+ color: var(--secondary-text);
272
+ margin-bottom: 12px;
 
 
273
  }
274
  .bonus-amount {
275
  font-size: 4.5em;
276
  font-weight: 800;
277
  color: var(--brand-yellow);
278
  line-height: 1;
279
+ text-shadow: 0 2px 15px rgba(0,0,0,0.5);
280
  }
281
  .history-section {
282
+ animation: fadeInUpList 1s 0.4s ease-out both;
283
+ }
284
+ @keyframes fadeInUpList {
285
+ from { opacity: 0; transform: translateY(40px); }
286
+ to { opacity: 1; transform: translateY(0); }
287
  }
288
  .history-title {
289
+ font-size: 1.8em;
290
  font-weight: 700;
291
+ text-align: center;
292
+ margin-bottom: 1.5rem;
293
+ padding-bottom: 1rem;
294
+ border-bottom: 2px solid var(--brand-yellow);
295
  }
296
  .history-list {
297
  list-style: none;
298
  padding: 0;
299
  margin: 0;
300
+ max-height: 40vh;
301
  overflow-y: auto;
302
  }
303
  .history-item {
304
  display: flex;
305
  justify-content: space-between;
306
  align-items: center;
307
+ background: rgba(255,255,255,0.03);
308
+ border-radius: 14px;
309
+ padding: 1rem 1.2rem;
310
+ margin-bottom: 1rem;
311
+ border: 1px solid rgba(255, 255, 255, 0.05);
312
+ transition: background-color 0.3s, transform 0.2s;
313
  }
314
  .history-item:hover {
315
+ background: rgba(255,255,255,0.07);
316
+ transform: scale(1.02);
317
+ }
318
+ .history-details {
319
+ display: flex;
320
+ flex-direction: column;
321
+ }
322
+ .history-description {
323
+ font-size: 1em;
324
+ font-weight: 500;
325
+ }
326
+ .history-date {
327
+ font-size: 0.85em;
328
+ color: var(--secondary-text);
329
+ margin-top: 4px;
330
+ }
331
+ .history-amount {
332
+ font-size: 1.2em;
333
+ font-weight: 700;
334
+ }
335
+ .history-amount.accrual {
336
+ color: var(--success-color);
337
+ }
338
+ .history-amount.deduction {
339
+ color: var(--danger-color);
340
+ }
341
+ .no-history {
342
+ text-align: center;
343
+ color: var(--secondary-text);
344
+ padding: 2rem 0;
345
+ background: rgba(255,255,255,0.03);
346
+ border-radius: 14px;
347
  }
 
 
 
 
 
 
 
 
348
  </style>
349
  </head>
350
  <body>
 
386
 
387
  function applyTheme(themeParams) {
388
  const root = document.documentElement;
389
+ if (themeParams.bg_color) root.style.setProperty('--bg-color', themeParams.bg_color);
390
  if (themeParams.text_color) root.style.setProperty('--text-color', themeParams.text_color);
391
+ if (themeParams.hint_color) root.style.setProperty('--secondary-text', themeParams.hint_color);
 
 
392
  }
393
 
394
  function setupTelegram() {
 
412
  if (!userIdForTest) {
413
  fetch('/verify', {
414
  method: 'POST',
415
+ headers: {
416
+ 'Content-Type': 'application/json',
417
+ 'Accept': 'application/json'
418
+ },
419
  body: JSON.stringify({ initData: tg.initData }),
420
  })
421
  .then(response => response.json())
 
467
  <title>Druzhba Admin</title>
468
  <link rel="preconnect" href="https://fonts.googleapis.com">
469
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
470
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
471
  <style>
472
  :root {
473
+ --admin-bg: #1a1a1a;
474
+ --admin-text: #e0e0e0;
475
+ --admin-card-bg: #2c2c2c;
476
+ --admin-border: #444;
477
+ --admin-shadow: rgba(0, 0, 0, 0.3);
478
  --admin-primary: #FFC107;
479
  --admin-primary-dark: #e0a800;
480
+ --admin-secondary: #888;
481
+ --admin-success: #28a745;
482
  --admin-danger: #dc3545;
483
+ --border-radius: 16px;
484
  --padding: 1.5rem;
485
  --font-family: 'Inter', sans-serif;
486
  }
487
  body { font-family: var(--font-family); background-color: var(--admin-bg); color: var(--admin-text); margin: 0; padding: var(--padding); line-height: 1.6; }
488
+ .container { max-width: 1400px; margin: 0 auto; padding: 0 1rem; }
489
+ h1 { text-align: center; color: var(--admin-text); margin-bottom: var(--padding); font-weight: 700; letter-spacing: 1px; }
490
+ .controls-bar { display: flex; flex-wrap: wrap; gap: 1rem; align-items: center; background: var(--admin-card-bg); padding: var(--padding); border-radius: var(--border-radius); box-shadow: 0 4px 25px var(--admin-shadow); border: 1px solid var(--admin-border); margin-bottom: var(--padding); }
491
+ .controls-bar input[type="text"] { flex-grow: 1; min-width: 250px; padding: 14px 18px; font-size: 1.1em; border-radius: 10px; border: 1px solid var(--admin-border); background-color: #333; color: var(--admin-text); transition: all 0.2s ease; }
492
+ .controls-bar input[type="text"]:focus { border-color: var(--admin-primary); box-shadow: 0 0 0 3px rgba(255, 193, 7, 0.2); outline: none; }
493
+ .btn { padding: 14px 24px; font-size: 1em; border: none; border-radius: 10px; font-weight: 600; cursor: pointer; transition: all 0.2s ease; }
494
  .btn-primary { background-color: var(--admin-primary); color: #000; }
495
+ .btn-primary:hover { background-color: var(--admin-primary-dark); transform: translateY(-2px); }
496
+ .user-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); gap: var(--padding); margin-top: var(--padding); }
497
+ .user-card { background: linear-gradient(145deg, #333, #252525); border-radius: var(--border-radius); padding: var(--padding); box-shadow: 0 8px 30px var(--admin-shadow); border: 1px solid var(--admin-border); display: flex; flex-direction: column; transition: transform 0.2s ease, box-shadow 0.2s ease; }
498
+ .user-card:hover { transform: translateY(-5px); box-shadow: 0 12px 40px rgba(0, 0, 0, 0.4); }
499
  .user-info { display: flex; align-items: center; gap: 1rem; margin-bottom: 1rem; }
500
+ .user-info img { width: 60px; height: 60px; border-radius: 50%; object-fit: cover; border: 3px solid var(--admin-border); background-color: #444; }
501
+ .user-details .name { font-weight: 600; font-size: 1.2em; color: var(--admin-text); }
502
+ .user-details .username-line { display: flex; align-items: center; gap: 0.75rem; color: var(--admin-secondary); font-size: 0.95em; }
503
+ .user-details .client-id { background-color: rgba(255, 193, 7, 0.1); color: var(--admin-primary); padding: 2px 8px; border-radius: 6px; font-weight: 600; font-size: 0.9em; }
504
+ .user-bonuses { text-align: center; margin-bottom: 1.5rem; background: rgba(0,0,0,0.2); padding: 1rem; border-radius: 12px; }
505
+ .user-bonuses .label { font-size: 0.9em; color: var(--admin-secondary); text-transform: uppercase; letter-spacing: 0.5px; }
506
+ .user-bonuses .amount { font-size: 2.2em; font-weight: 700; color: var(--admin-primary); }
507
+ .user-actions .btn-manage { display: block; width: 100%; padding: 12px; background-color: var(--admin-primary); color: #000; border: none; border-radius: 10px; font-weight: 600; cursor: pointer; transition: background-color 0.2s; }
508
  .user-actions .btn-manage:hover { background-color: var(--admin-primary-dark); }
509
  .no-users { text-align: center; color: var(--admin-secondary); margin-top: 2rem; font-size: 1.1em; }
510
+ .modal { opacity: 0; visibility: hidden; position: fixed; z-index: 1001; left: 0; top: 0; width: 100%; height: 100%; overflow: auto; background-color: rgba(0,0,0,0.7); backdrop-filter: blur(8px); transition: opacity 0.3s ease, visibility 0s 0.3s; }
511
+ .modal.is-visible { opacity: 1; visibility: visible; transition: opacity 0.3s ease; }
512
+ .modal-content { background-color: var(--admin-card-bg); margin: 10% auto; padding: var(--padding); border: 1px solid var(--admin-border); width: 90%; max-width: 600px; border-radius: var(--border-radius); position: relative; box-shadow: 0 8px 50px rgba(0,0,0,0.3); transform: translateY(20px) scale(0.98); transition: transform 0.3s ease; }
513
+ .modal.is-visible .modal-content { transform: translateY(0) scale(1); }
514
+ .modal-close { color: #aaa; position: absolute; top: 15px; right: 25px; font-size: 28px; font-weight: bold; cursor: pointer; transition: color 0.2s; }
515
+ .modal-close:hover { color: var(--admin-text); }
516
  .modal-header { padding-bottom: 1rem; margin-bottom: 1.5rem; border-bottom: 1px solid var(--admin-border); }
517
  .modal-header h2 { margin: 0; font-size: 1.5rem; }
518
  .modal-header .username { font-size: 1rem; color: var(--admin-secondary); }
519
  .form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; align-items: center; margin-bottom: 1.5rem; }
520
  .form-group { display: flex; flex-direction: column; }
521
+ .form-group label { margin-bottom: 0.5rem; font-weight: 500; font-size: 0.9em; color: var(--admin-secondary); }
522
+ .form-group input { padding: 12px; font-size: 1rem; border: 1px solid var(--admin-border); border-radius: 8px; width: 100%; box-sizing: border-box; background-color: #333; color: var(--admin-text); }
523
+ .calculation-summary { background: rgba(0,0,0,0.2); padding: 1rem; border-radius: 10px; margin-bottom: 1.5rem; }
524
  .summary-item { display: flex; justify-content: space-between; margin-bottom: 0.5rem; font-size: 0.95em; }
525
  .summary-item strong { font-weight: 600; }
526
  .history-container { margin-top: 1.5rem; }
 
535
  .modal-footer { margin-top: 1.5rem; display: flex; justify-content: flex-end; align-items: center; gap: 1rem;}
536
  .modal-footer button { padding: 12px 25px; font-size: 1.1em; border-radius: 8px; border: none; font-weight: 600; cursor: pointer; }
537
  .btn-submit { background-color: var(--admin-success); color: white; }
538
+ .status-message { font-weight: 500; flex-grow: 1; text-align: left; }
539
  </style>
540
  </head>
541
  <body>
 
549
  {% if users %}
550
  <div class="user-grid" id="userGrid">
551
  {% for user in users|sort(attribute='visited_at', reverse=true) %}
552
+ <div class="user-card" data-user-id="{{ user.id }}" data-search-term="{{ user.first_name|lower }} {{ user.last_name|lower }} {{ user.username|lower }} {{ user.id }} {{ user.client_id }}">
553
  <div class="user-info">
554
+ <img src="{{ user.photo_url if user.photo_url else 'data:image/svg+xml;charset=UTF-8,%3csvg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%270 0 100 100%27%3e%3crect width=%27100%27 height=%27100%27 fill=%27%23444%27/%3e%3ctext x=%2750%25%27 y=%2755%25%27 dominant-baseline=%27middle%27 text-anchor=%27middle%27 font-size=%2745%27 font-family=%27sans-serif%27 fill=%27%23888%27%3e?%3c/text%3e%3c/svg%3e' }}" alt="User Avatar">
555
  <div class="user-details">
556
  <div class="name">{{ user.first_name or '' }} {{ user.last_name or '' }}</div>
557
+ <div class="username-line">
558
+ <span class="username">@{{ user.username or 'N/A' }}</span>
559
+ <span class="client-id">ID: {{ user.client_id }}</span>
560
+ </div>
561
  </div>
562
  </div>
563
  <div class="user-bonuses">
 
682
  }
683
 
684
  calculateBonuses();
685
+ transactionModal.classList.add('is-visible');
686
  }
687
 
688
  function openAddClientModal() {
689
  document.getElementById('newClientName').value = '';
690
  document.getElementById('newClientPhone').value = '';
691
  document.getElementById('addClientStatus').textContent = '';
692
+ addClientModal.classList.add('is-visible');
693
  }
694
 
695
  function closeModal(modalId) {
696
+ document.getElementById(modalId).classList.remove('is-visible');
697
  if (modalId === 'transactionModal') {
698
  currentUserData = null;
699
  }
 
852
  'visited_at': now.timestamp(),
853
  'visited_at_str': now.strftime('%Y-%m-%d %H:%M:%S')
854
  })
 
 
855
  else:
856
  user_entry = {
857
  'id': user_id,
858
+ 'client_id': generate_unique_client_id(all_data),
859
  'first_name': user_info_dict.get('first_name'),
860
  'last_name': user_info_dict.get('last_name'),
861
  'username': user_info_dict.get('username'),
 
883
 
884
  @app.route('/admin')
885
  def admin_panel():
886
+ all_data = load_visitor_data()
887
+ data_updated = False
888
+
889
+ for user_data in all_data.values():
890
+ if 'client_id' not in user_data or not user_data.get('client_id'):
891
+ user_data['client_id'] = generate_unique_client_id(all_data)
892
+ data_updated = True
893
+
894
+ if data_updated:
895
+ save_visitor_data(all_data)
896
+
897
+ users_list = list(all_data.values())
898
  return render_template_string(ADMIN_TEMPLATE, users=users_list)
899
 
900
  @app.route('/admin/add_client', methods=['POST'])
 
910
  all_data = load_visitor_data()
911
 
912
  for user in all_data.values():
913
+ if str(user.get('id')) == phone_number or user.get('username') == phone_number:
914
+ return jsonify({"status": "error", "message": "Клиент с таким номером телефона уже существует."}), 409
915
 
916
  now = datetime.now()
917
+
918
+ new_client_id = phone_number
919
  new_client = {
920
+ 'id': new_client_id,
921
+ 'client_id': generate_unique_client_id(all_data),
922
  'first_name': name,
923
+ 'last_name': '',
924
  'username': phone_number,
925
  'photo_url': None,
926
  'language_code': 'ru',
 
931
  'history': []
932
  }
933
 
934
+ save_visitor_data({new_client_id: new_client})
935
 
936
  return jsonify({"status": "ok", "message": "Client added successfully"}), 201
937