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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +170 -198
app.py CHANGED
@@ -32,11 +32,10 @@ app.secret_key = os.urandom(24)
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():
@@ -183,174 +182,144 @@ 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@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
  }
216
  .container {
217
- max-width: 650px;
218
  margin: 0 auto;
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>
351
  <div class="container">
352
  <header class="header">
353
- <div class="logo">DRUZHBA</div>
354
  <p id="greeting" class="welcome-text">Добро пожаловать!</p>
355
  </header>
356
 
@@ -358,6 +327,11 @@ TEMPLATE = """
358
  <p class="bonus-label">Ваши бонусы</p>
359
  <p class="bonus-amount">{{ "%.2f"|format(user.bonuses|float) }}</p>
360
  </section>
 
 
 
 
 
361
 
362
  <section class="history-section">
363
  <h2 class="history-title">История операций</h2>
@@ -386,9 +360,13 @@ TEMPLATE = """
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,10 +390,7 @@ TEMPLATE = """
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())
@@ -423,10 +398,12 @@ TEMPLATE = """
423
  if (data.status === 'ok' && data.verified && data.user_id) {
424
  window.location.replace('/?user_id_for_test=' + data.user_id);
425
  } else {
 
426
  document.body.style.visibility = 'visible';
427
  }
428
  })
429
  .catch(error => {
 
430
  document.body.style.visibility = 'visible';
431
  });
432
  } else {
@@ -437,9 +414,9 @@ TEMPLATE = """
437
  const greetingElement = document.getElementById('greeting');
438
  if (user) {
439
  const name = user.first_name || user.username || 'Гость';
440
- greetingElement.textContent = `Добро пожаловать, ${name}! 👋`;
441
  } else {
442
- greetingElement.textContent = `Добро пожаловать, {{ user.first_name or 'Гость' }}! 👋`;
443
  }
444
  }
445
 
@@ -467,60 +444,55 @@ ADMIN_TEMPLATE = """
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,7 +507,7 @@ ADMIN_TEMPLATE = """
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,15 +521,12 @@ ADMIN_TEMPLATE = """
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">
@@ -618,8 +587,8 @@ ADMIN_TEMPLATE = """
618
  <h2>Добавить нового клиента</h2>
619
  </div>
620
  <div class="form-group" style="margin-bottom: 1rem;">
621
- <label for="newClientName">Имя</label>
622
- <input type="text" id="newClientName" placeholder="Иван Иванов">
623
  </div>
624
  <div class="form-group" style="margin-bottom: 1.5rem;">
625
  <label for="newClientPhone">Номер телефона (уникальный)</label>
@@ -654,7 +623,7 @@ ADMIN_TEMPLATE = """
654
  currentUserData = userData;
655
  document.getElementById('modalUserId').value = userData.id;
656
  document.getElementById('modalUserName').textContent = `${userData.first_name || ''} ${userData.last_name || ''}`;
657
- document.getElementById('modalUserUsername').textContent = `@${userData.username || 'N/A'}`;
658
  document.getElementById('purchaseAmount').value = '';
659
  document.getElementById('deductAmount').value = '';
660
  document.getElementById('modalStatus').textContent = '';
@@ -682,18 +651,18 @@ ADMIN_TEMPLATE = """
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
  }
@@ -757,11 +726,11 @@ ADMIN_TEMPLATE = """
757
  statusEl.textContent = 'Сохранение...';
758
 
759
  const payload = {
760
- name: document.getElementById('newClientName').value.trim(),
761
  phone_number: document.getElementById('newClientPhone').value.trim(),
762
  };
763
 
764
- if (!payload.name || !payload.phone_number) {
765
  statusEl.style.color = 'var(--admin-danger)';
766
  statusEl.textContent = 'Имя и номер телефона обязательны.';
767
  return;
@@ -809,8 +778,9 @@ def index():
809
 
810
  if user_id_str and user_id_str in current_data:
811
  user_data = current_data[user_id_str]
 
812
  else:
813
- user_data = { "bonuses": 0, "history": [] }
814
 
815
  return render_template_string(TEMPLATE, user=user_data)
816
 
@@ -834,15 +804,19 @@ def verify_data():
834
  user_info_dict = {}
835
 
836
  if is_valid:
837
- user_id = user_info_dict.get('id')
838
- if user_id:
839
  now = datetime.now()
840
- user_id_str = str(user_id)
841
-
842
  all_data = load_visitor_data()
843
 
844
- if user_id_str in all_data:
845
- user_entry = all_data[user_id_str]
 
 
 
 
 
 
846
  user_entry.update({
847
  'first_name': user_info_dict.get('first_name'),
848
  'last_name': user_info_dict.get('last_name'),
@@ -852,25 +826,29 @@ def verify_data():
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'),
862
  'photo_url': user_info_dict.get('photo_url'),
863
  'language_code': user_info_dict.get('language_code'),
864
  'is_premium': user_info_dict.get('is_premium', False),
 
865
  'visited_at': now.timestamp(),
866
  'visited_at_str': now.strftime('%Y-%m-%d %H:%M:%S'),
867
  'bonuses': 0,
868
  'history': []
869
  }
 
870
 
871
- save_visitor_data({user_id_str: user_entry})
872
 
873
- return jsonify({"status": "ok", "verified": True, "user_id": user_id_str})
874
  else:
875
  return jsonify({"status": "error", "verified": True, "message": "User ID not found in parsed data"}), 400
876
  else:
@@ -883,18 +861,11 @@ def verify_data():
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'])
@@ -902,36 +873,37 @@ def add_client():
902
  try:
903
  data = request.get_json()
904
  phone_number = data.get('phone_number')
905
- name = data.get('name')
906
 
907
- if not phone_number or not name:
908
  return jsonify({"status": "error", "message": "Имя и номер телефона обязательны."}), 400
909
 
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',
927
  'is_premium': False,
 
928
  'visited_at': now.timestamp(),
929
  'visited_at_str': now.strftime('%Y-%m-%d %H:%M:%S'),
930
  'bonuses': 0,
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
 
 
32
  _data_lock = threading.Lock()
33
  visitor_data_cache = {}
34
 
35
+ def generate_unique_id(all_data):
 
36
  while True:
37
  new_id = str(random.randint(10000, 99999))
38
+ if new_id not in all_data:
39
  return new_id
40
 
41
  def download_data_from_hf():
 
182
  <script src="https://telegram.org/js/telegram-web-app.js"></script>
183
  <link rel="preconnect" href="https://fonts.googleapis.com">
184
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
185
+ <link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&display=swap" rel="stylesheet">
186
  <style>
187
  :root {
188
+ --brand-yellow: #FFC107;
189
+ --brand-black: #101010;
190
+ --card-bg: #1c1c1e;
191
+ --text-color: #ffffff;
192
+ --text-secondary-color: #a0a0a0;
193
+ --border-radius: 16px;
194
+ --padding-m: 16px;
195
+ --padding-l: 24px;
196
+ --font-family: 'Manrope', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
197
+ --shadow-color: rgba(255, 193, 7, 0.15);
198
+ --shadow-glow: 0 0 35px var(--shadow-color);
 
 
199
  }
200
  * { box-sizing: border-box; margin: 0; padding: 0; }
201
  html, body {
202
+ background-color: var(--brand-black);
203
  font-family: var(--font-family);
204
  color: var(--text-color);
205
  padding: var(--padding-m);
206
  overscroll-behavior-y: none;
207
  -webkit-font-smoothing: antialiased;
208
  -moz-osx-font-smoothing: grayscale;
 
209
  visibility: hidden;
210
  min-height: 100vh;
211
  }
212
  .container {
213
+ max-width: 600px;
214
  margin: 0 auto;
215
  display: flex;
216
  flex-direction: column;
217
+ gap: var(--padding-l);
 
218
  }
219
  .header {
220
+ text-align: left;
221
  padding: var(--padding-m) 0;
 
 
 
 
 
222
  }
223
  .logo {
224
  font-size: 2.5em;
225
  font-weight: 800;
226
+ color: var(--text-color);
227
+ letter-spacing: -1px;
 
228
  }
229
+ .logo span { color: var(--brand-yellow); }
230
  .welcome-text {
231
+ font-size: 1em;
232
+ color: var(--text-secondary-color);
233
+ margin-top: 4px;
 
234
  }
235
  .bonus-card {
236
+ background: linear-gradient(145deg, #2a2a2a, #1c1c1c);
237
+ border-radius: calc(var(--border-radius) + 8px);
238
  padding: var(--padding-l);
239
  text-align: center;
240
+ box-shadow: var(--shadow-glow);
241
+ border: 1px solid rgba(255, 193, 7, 0.2);
242
  position: relative;
243
+ overflow: hidden;
 
 
 
 
 
 
 
 
 
 
 
 
 
244
  }
245
  .bonus-label {
246
+ font-size: 1.1em;
247
  font-weight: 500;
248
+ color: var(--text-secondary-color);
249
  margin-bottom: 12px;
250
  }
251
  .bonus-amount {
252
+ font-size: 4em;
253
  font-weight: 800;
254
  color: var(--brand-yellow);
255
+ letter-spacing: -2px;
256
  line-height: 1;
 
257
  }
258
+ .client-id-card {
259
+ background-color: var(--card-bg);
260
+ border-radius: var(--border-radius);
261
+ padding: var(--padding-m);
262
+ display: flex;
263
+ justify-content: space-between;
264
+ align-items: center;
265
+ }
266
+ .client-id-label {
267
+ font-weight: 500;
268
+ color: var(--text-secondary-color);
269
  }
270
+ .client-id-value {
271
+ font-size: 1.3em;
272
+ font-weight: 700;
273
+ color: var(--brand-yellow);
274
+ letter-spacing: 2px;
275
+ background-color: rgba(255,193,7,0.1);
276
+ padding: 4px 10px;
277
+ border-radius: 8px;
278
+ }
279
+ .history-section {
280
+ background-color: var(--card-bg);
281
+ border-radius: var(--border-radius);
282
+ padding: var(--padding-l);
283
  }
284
  .history-title {
285
+ font-size: 1.4em;
286
  font-weight: 700;
287
+ margin-bottom: var(--padding-m);
288
+ padding-bottom: var(--padding-m);
289
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
 
290
  }
291
  .history-list {
292
  list-style: none;
293
  padding: 0;
294
  margin: 0;
295
+ max-height: 35vh;
296
  overflow-y: auto;
297
  }
298
  .history-item {
299
  display: flex;
300
  justify-content: space-between;
301
  align-items: center;
302
+ padding: 14px 4px;
303
+ border-bottom: 1px solid rgba(255, 255, 255, 0.05);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
304
  }
305
+ .history-item:last-child { border-bottom: none; }
306
+ .history-details { display: flex; flex-direction: column; }
307
+ .history-description { font-size: 1em; font-weight: 500; }
308
+ .history-date { font-size: 0.8em; color: var(--text-secondary-color); margin-top: 4px; }
309
+ .history-amount { font-size: 1.1em; font-weight: 700; }
310
+ .history-amount.accrual { color: #4CAF50; }
311
+ .history-amount.deduction { color: #F44336; }
312
  .no-history {
313
  text-align: center;
314
+ color: var(--text-secondary-color);
315
  padding: 2rem 0;
 
 
316
  }
317
  </style>
318
  </head>
319
  <body>
320
  <div class="container">
321
  <header class="header">
322
+ <div class="logo">DRUZHBA<span>.</span></div>
323
  <p id="greeting" class="welcome-text">Добро пожаловать!</p>
324
  </header>
325
 
 
327
  <p class="bonus-label">Ваши бонусы</p>
328
  <p class="bonus-amount">{{ "%.2f"|format(user.bonuses|float) }}</p>
329
  </section>
330
+
331
+ <section class="client-id-card">
332
+ <p class="client-id-label">Ваш ID клиента</p>
333
+ <p class="client-id-value">{{ user.id }}</p>
334
+ </section>
335
 
336
  <section class="history-section">
337
  <h2 class="history-title">История операций</h2>
 
360
 
361
  function applyTheme(themeParams) {
362
  const root = document.documentElement;
363
+ const isDark = themeParams.bg_color ? (parseInt(themeParams.bg_color.substring(1, 3), 16) < 128) : true;
364
+
365
+ root.style.setProperty('--brand-black', themeParams.bg_color || '#101010');
366
+ root.style.setProperty('--text-color', themeParams.text_color || '#ffffff');
367
+ root.style.setProperty('--text-secondary-color', themeParams.hint_color || '#a0a0a0');
368
+ root.style.setProperty('--brand-yellow', themeParams.button_color || '#FFC107');
369
+ root.style.setProperty('--card-bg', themeParams.secondary_bg_color || (isDark ? '#1c1c1e' : '#f1f1f1'));
370
  }
371
 
372
  function setupTelegram() {
 
390
  if (!userIdForTest) {
391
  fetch('/verify', {
392
  method: 'POST',
393
+ headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
 
 
 
394
  body: JSON.stringify({ initData: tg.initData }),
395
  })
396
  .then(response => response.json())
 
398
  if (data.status === 'ok' && data.verified && data.user_id) {
399
  window.location.replace('/?user_id_for_test=' + data.user_id);
400
  } else {
401
+ console.warn('Backend verification failed:', data.message);
402
  document.body.style.visibility = 'visible';
403
  }
404
  })
405
  .catch(error => {
406
+ console.error('Error sending initData for verification:', error);
407
  document.body.style.visibility = 'visible';
408
  });
409
  } else {
 
414
  const greetingElement = document.getElementById('greeting');
415
  if (user) {
416
  const name = user.first_name || user.username || 'Гость';
417
+ greetingElement.textContent = `Привет, ${name}! 👋`;
418
  } else {
419
+ greetingElement.textContent = `Привет, {{ user.first_name or 'Гость' }}! 👋`;
420
  }
421
  }
422
 
 
444
  <title>Druzhba Admin</title>
445
  <link rel="preconnect" href="https://fonts.googleapis.com">
446
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
447
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
448
  <style>
449
  :root {
450
+ --admin-bg: #f8f9fa;
451
+ --admin-text: #212529;
452
+ --admin-card-bg: #ffffff;
453
+ --admin-border: #dee2e6;
454
+ --admin-shadow: rgba(0, 0, 0, 0.05);
455
  --admin-primary: #FFC107;
456
  --admin-primary-dark: #e0a800;
457
+ --admin-secondary: #6c757d;
458
+ --admin-success: #198754;
459
  --admin-danger: #dc3545;
460
+ --border-radius: 12px;
461
  --padding: 1.5rem;
462
  --font-family: 'Inter', sans-serif;
463
  }
464
  body { font-family: var(--font-family); background-color: var(--admin-bg); color: var(--admin-text); margin: 0; padding: var(--padding); line-height: 1.6; }
465
+ .container { max-width: 1200px; margin: 0 auto; }
466
+ h1 { text-align: center; color: var(--admin-secondary); margin-bottom: var(--padding); font-weight: 600; }
467
+ .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 15px var(--admin-shadow); border: 1px solid var(--admin-border); margin-bottom: var(--padding); }
468
+ .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; min-width: 250px; }
469
+ .btn { padding: 12px 20px; font-size: 1em; border: none; border-radius: 8px; font-weight: 600; cursor: pointer; transition: background-color 0.2s ease; }
 
470
  .btn-primary { background-color: var(--admin-primary); color: #000; }
471
+ .btn-primary:hover { background-color: var(--admin-primary-dark); }
472
+ .user-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: var(--padding); margin-top: var(--padding); }
473
+ .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; }
474
+ .user-card:hover { transform: translateY(-5px); box-shadow: 0 8px 25px rgba(0, 0, 0, 0.08); }
475
  .user-info { display: flex; align-items: center; gap: 1rem; margin-bottom: 1rem; }
476
+ .user-info img { width: 60px; height: 60px; border-radius: 50%; object-fit: cover; border: 3px solid var(--admin-border); background-color: #eee; }
477
+ .user-details .name { font-weight: 600; font-size: 1.2em; }
478
+ .user-details .username { color: var(--admin-secondary); font-size: 0.9em; }
479
+ .user-bonuses { text-align: center; margin-bottom: 1rem; }
480
+ .user-bonuses .label { font-size: 0.9em; color: var(--admin-secondary); }
481
+ .user-bonuses .amount { font-size: 1.8em; font-weight: 700; color: var(--admin-primary-dark); }
482
+ .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; }
 
483
  .user-actions .btn-manage:hover { background-color: var(--admin-primary-dark); }
484
  .no-users { text-align: center; color: var(--admin-secondary); margin-top: 2rem; font-size: 1.1em; }
485
+ .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); }
486
+ .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); }
487
+ .modal-close { color: #aaa; position: absolute; top: 15px; right: 25px; font-size: 28px; font-weight: bold; cursor: pointer; }
 
 
 
488
  .modal-header { padding-bottom: 1rem; margin-bottom: 1.5rem; border-bottom: 1px solid var(--admin-border); }
489
  .modal-header h2 { margin: 0; font-size: 1.5rem; }
490
  .modal-header .username { font-size: 1rem; color: var(--admin-secondary); }
491
  .form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; align-items: center; margin-bottom: 1.5rem; }
492
  .form-group { display: flex; flex-direction: column; }
493
+ .form-group label { margin-bottom: 0.5rem; font-weight: 500; font-size: 0.9em; }
494
+ .form-group input { padding: 10px; font-size: 1rem; border: 1px solid var(--admin-border); border-radius: 8px; width: 100%; box-sizing: border-box; }
495
+ .calculation-summary { background: #f0f0f0; padding: 1rem; border-radius: 8px; margin-bottom: 1.5rem; }
496
  .summary-item { display: flex; justify-content: space-between; margin-bottom: 0.5rem; font-size: 0.95em; }
497
  .summary-item strong { font-weight: 600; }
498
  .history-container { margin-top: 1.5rem; }
 
507
  .modal-footer { margin-top: 1.5rem; display: flex; justify-content: flex-end; align-items: center; gap: 1rem;}
508
  .modal-footer button { padding: 12px 25px; font-size: 1.1em; border-radius: 8px; border: none; font-weight: 600; cursor: pointer; }
509
  .btn-submit { background-color: var(--admin-success); color: white; }
510
+ .status-message { text-align: center; font-weight: 500; flex-grow: 1; text-align: left; }
511
  </style>
512
  </head>
513
  <body>
 
521
  {% if users %}
522
  <div class="user-grid" id="userGrid">
523
  {% for user in users|sort(attribute='visited_at', reverse=true) %}
524
+ <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.phone_number|lower if user.phone_number }}">
525
  <div class="user-info">
526
+ <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">
527
  <div class="user-details">
528
  <div class="name">{{ user.first_name or '' }} {{ user.last_name or '' }}</div>
529
+ <div class="username">@{{ user.username if user.username else user.phone_number }} | ID: {{ user.id }}</div>
 
 
 
530
  </div>
531
  </div>
532
  <div class="user-bonuses">
 
587
  <h2>Добавить нового клиента</h2>
588
  </div>
589
  <div class="form-group" style="margin-bottom: 1rem;">
590
+ <label for="newClientFirstName">Имя</label>
591
+ <input type="text" id="newClientFirstName" placeholder="Иван">
592
  </div>
593
  <div class="form-group" style="margin-bottom: 1.5rem;">
594
  <label for="newClientPhone">Номер телефона (уникальный)</label>
 
623
  currentUserData = userData;
624
  document.getElementById('modalUserId').value = userData.id;
625
  document.getElementById('modalUserName').textContent = `${userData.first_name || ''} ${userData.last_name || ''}`;
626
+ document.getElementById('modalUserUsername').textContent = `@${userData.username || userData.phone_number} | ID: ${userData.id}`;
627
  document.getElementById('purchaseAmount').value = '';
628
  document.getElementById('deductAmount').value = '';
629
  document.getElementById('modalStatus').textContent = '';
 
651
  }
652
 
653
  calculateBonuses();
654
+ transactionModal.style.display = 'block';
655
  }
656
 
657
  function openAddClientModal() {
658
+ document.getElementById('newClientFirstName').value = '';
659
  document.getElementById('newClientPhone').value = '';
660
  document.getElementById('addClientStatus').textContent = '';
661
+ addClientModal.style.display = 'block';
662
  }
663
 
664
  function closeModal(modalId) {
665
+ document.getElementById(modalId).style.display = 'none';
666
  if (modalId === 'transactionModal') {
667
  currentUserData = null;
668
  }
 
726
  statusEl.textContent = 'Сохранение...';
727
 
728
  const payload = {
729
+ first_name: document.getElementById('newClientFirstName').value.trim(),
730
  phone_number: document.getElementById('newClientPhone').value.trim(),
731
  };
732
 
733
+ if (!payload.first_name || !payload.phone_number) {
734
  statusEl.style.color = 'var(--admin-danger)';
735
  statusEl.textContent = 'Имя и номер телефона обязательны.';
736
  return;
 
778
 
779
  if user_id_str and user_id_str in current_data:
780
  user_data = current_data[user_id_str]
781
+ user_data['id'] = user_id_str
782
  else:
783
+ user_data = { "id": "N/A", "bonuses": 0, "history": [] }
784
 
785
  return render_template_string(TEMPLATE, user=user_data)
786
 
 
804
  user_info_dict = {}
805
 
806
  if is_valid:
807
+ tg_user_id = user_info_dict.get('id')
808
+ if tg_user_id:
809
  now = datetime.now()
 
 
810
  all_data = load_visitor_data()
811
 
812
+ existing_user_key = None
813
+ for key, user_data_item in all_data.items():
814
+ if str(user_data_item.get('telegram_id')) == str(tg_user_id):
815
+ existing_user_key = key
816
+ break
817
+
818
+ if existing_user_key:
819
+ user_entry = all_data[existing_user_key]
820
  user_entry.update({
821
  'first_name': user_info_dict.get('first_name'),
822
  'last_name': user_info_dict.get('last_name'),
 
826
  'visited_at': now.timestamp(),
827
  'visited_at_str': now.strftime('%Y-%m-%d %H:%M:%S')
828
  })
829
+ user_id_to_save = existing_user_key
830
  else:
831
+ new_user_id = generate_unique_id(all_data)
832
  user_entry = {
833
+ 'id': new_user_id,
834
+ 'telegram_id': tg_user_id,
835
  'first_name': user_info_dict.get('first_name'),
836
  'last_name': user_info_dict.get('last_name'),
837
  'username': user_info_dict.get('username'),
838
  'photo_url': user_info_dict.get('photo_url'),
839
  'language_code': user_info_dict.get('language_code'),
840
  'is_premium': user_info_dict.get('is_premium', False),
841
+ 'phone_number': None,
842
  'visited_at': now.timestamp(),
843
  'visited_at_str': now.strftime('%Y-%m-%d %H:%M:%S'),
844
  'bonuses': 0,
845
  'history': []
846
  }
847
+ user_id_to_save = new_user_id
848
 
849
+ save_visitor_data({user_id_to_save: user_entry})
850
 
851
+ return jsonify({"status": "ok", "verified": True, "user_id": user_id_to_save})
852
  else:
853
  return jsonify({"status": "error", "verified": True, "message": "User ID not found in parsed data"}), 400
854
  else:
 
861
 
862
  @app.route('/admin')
863
  def admin_panel():
864
+ current_data = load_visitor_data()
865
+ users_list = []
866
+ for user_id, user_data in current_data.items():
867
+ user_data['id'] = user_id
868
+ users_list.append(user_data)
 
 
 
 
 
 
 
869
  return render_template_string(ADMIN_TEMPLATE, users=users_list)
870
 
871
  @app.route('/admin/add_client', methods=['POST'])
 
873
  try:
874
  data = request.get_json()
875
  phone_number = data.get('phone_number')
876
+ first_name = data.get('first_name')
877
 
878
+ if not phone_number or not first_name:
879
  return jsonify({"status": "error", "message": "Имя и номер телефона обязательны."}), 400
880
 
881
  all_data = load_visitor_data()
882
 
883
  for user in all_data.values():
884
+ if user.get('phone_number') == phone_number:
885
  return jsonify({"status": "error", "message": "Клиент с таким номером телефона уже существует."}), 409
886
 
887
  now = datetime.now()
888
+ new_id = generate_unique_id(all_data)
889
 
 
890
  new_client = {
891
+ 'id': new_id,
892
+ 'telegram_id': None,
893
+ 'first_name': first_name,
894
+ 'last_name': None,
895
+ 'username': None,
896
  'photo_url': None,
897
  'language_code': 'ru',
898
  'is_premium': False,
899
+ 'phone_number': phone_number,
900
  'visited_at': now.timestamp(),
901
  'visited_at_str': now.strftime('%Y-%m-%d %H:%M:%S'),
902
  'bonuses': 0,
903
  'history': []
904
  }
905
 
906
+ save_visitor_data({new_id: new_client})
907
 
908
  return jsonify({"status": "ok", "message": "Client added successfully"}), 201
909