Kgshop commited on
Commit
502f979
·
verified ·
1 Parent(s): 7ea0e3b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +177 -63
app.py CHANGED
@@ -365,14 +365,14 @@ TEMPLATE = """
365
  if (themeParams.button_text_color) root.style.setProperty('--tg-theme-button-text-color', themeParams.button_text_color);
366
  if (themeParams.secondary_bg_color) root.style.setProperty('--tg-theme-secondary-bg-color', themeParams.secondary_bg_color);
367
  }
368
-
369
  function setupTelegram() {
370
  if (!tg || !tg.initData) {
371
  console.error("Telegram WebApp script not loaded or initData is missing.");
372
  document.body.style.visibility = 'visible';
373
  return;
374
  }
375
-
376
  tg.ready();
377
  tg.expand();
378
 
@@ -381,38 +381,50 @@ TEMPLATE = """
381
  }
382
  tg.onEvent('themeChanged', () => applyTheme(tg.themeParams));
383
 
384
- fetch('/verify', {
385
- method: 'POST',
386
- headers: {
387
- 'Content-Type': 'application/json',
388
- 'Accept': 'application/json'
389
- },
390
- body: JSON.stringify({ initData: tg.initData }),
391
- })
392
- .then(response => response.json())
393
- .then(data => {
394
- if (data.status === 'ok' && data.verified) {
395
- console.log('Backend verification successful.');
396
- } else {
397
- console.warn('Backend verification failed:', data.message);
398
- }
399
- })
400
- .catch(error => console.error('Error sending initData for verification:', error));
 
 
 
 
 
 
 
 
 
 
 
 
401
 
402
  const user = tg.initDataUnsafe?.user;
403
  const greetingElement = document.getElementById('greeting');
404
  if (user) {
405
  const name = user.first_name || user.username || 'Гость';
406
  greetingElement.textContent = `Добро пожаловать, ${name}! 👋`;
 
 
407
  }
408
-
409
- document.body.style.visibility = 'visible';
410
  }
411
 
412
  if (window.Telegram && window.Telegram.WebApp) {
413
  setupTelegram();
414
  } else {
415
- window.addEventListener('load', setupTelegram);
416
  setTimeout(() => {
417
  if (document.body.style.visibility !== 'visible') {
418
  document.body.style.visibility = 'visible';
@@ -453,8 +465,11 @@ ADMIN_TEMPLATE = """
453
  body { font-family: var(--font-family); background-color: var(--admin-bg); color: var(--admin-text); margin: 0; padding: var(--padding); line-height: 1.6; }
454
  .container { max-width: 1200px; margin: 0 auto; }
455
  h1 { text-align: center; color: var(--admin-secondary); margin-bottom: var(--padding); font-weight: 600; }
456
- .controls-bar { 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); }
457
- .controls-bar input[type="text"] { width: 100%; padding: 12px 15px; font-size: 1.1em; border-radius: 8px; border: 1px solid var(--admin-border); box-sizing: border-box; }
 
 
 
458
  .user-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: var(--padding); margin-top: var(--padding); }
459
  .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; }
460
  .user-card:hover { transform: translateY(-5px); box-shadow: 0 8px 25px rgba(0, 0, 0, 0.08); }
@@ -465,10 +480,9 @@ ADMIN_TEMPLATE = """
465
  .user-bonuses { text-align: center; margin-bottom: 1rem; }
466
  .user-bonuses .label { font-size: 0.9em; color: var(--admin-secondary); }
467
  .user-bonuses .amount { font-size: 1.8em; font-weight: 700; color: var(--admin-primary-dark); }
468
- .user-actions .btn { 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; }
469
- .user-actions .btn:hover { background-color: var(--admin-primary-dark); }
470
  .no-users { text-align: center; color: var(--admin-secondary); margin-top: 2rem; font-size: 1.1em; }
471
-
472
  .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); }
473
  .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); }
474
  .modal-close { color: #aaa; position: absolute; top: 15px; right: 25px; font-size: 28px; font-weight: bold; cursor: pointer; }
@@ -478,7 +492,7 @@ ADMIN_TEMPLATE = """
478
  .form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; align-items: center; margin-bottom: 1.5rem; }
479
  .form-group { display: flex; flex-direction: column; }
480
  .form-group label { margin-bottom: 0.5rem; font-weight: 500; font-size: 0.9em; }
481
- .form-group input { padding: 10px; font-size: 1rem; border: 1px solid var(--admin-border); border-radius: 8px; }
482
  .calculation-summary { background: #f0f0f0; padding: 1rem; border-radius: 8px; margin-bottom: 1.5rem; }
483
  .summary-item { display: flex; justify-content: space-between; margin-bottom: 0.5rem; font-size: 0.95em; }
484
  .summary-item strong { font-weight: 600; }
@@ -491,16 +505,18 @@ ADMIN_TEMPLATE = """
491
  .history-item .date { font-size: 0.8em; color: var(--admin-secondary); }
492
  .history-item .amount.accrual { color: var(--admin-success); font-weight: 600; }
493
  .history-item .amount.deduction { color: var(--admin-danger); font-weight: 600; }
 
494
  .modal-footer button { padding: 12px 25px; font-size: 1.1em; border-radius: 8px; border: none; font-weight: 600; cursor: pointer; }
495
  .btn-submit { background-color: var(--admin-success); color: white; }
496
- .status-message { margin-top: 1rem; text-align: center; font-weight: 500; }
497
  </style>
498
  </head>
499
  <body>
500
  <div class="container">
501
  <h1>Панель администратора Druzhba</h1>
502
  <div class="controls-bar">
503
- <input type="text" id="searchInput" onkeyup="searchUsers()" placeholder="Поиск по имени, ID, username...">
 
504
  </div>
505
 
506
  {% if users %}
@@ -519,7 +535,7 @@ ADMIN_TEMPLATE = """
519
  <div class="amount">{{ "%.2f"|format(user.bonuses|float) }}</div>
520
  </div>
521
  <div class="user-actions">
522
- <button class="btn" onclick='openTransactionModal({{ user|tojson }})'>Управление бонусами</button>
523
  </div>
524
  </div>
525
  {% endfor %}
@@ -531,14 +547,12 @@ ADMIN_TEMPLATE = """
531
 
532
  <div id="transactionModal" class="modal">
533
  <div class="modal-content">
534
- <span class="modal-close" onclick="closeModal()">×</span>
535
  <div class="modal-header">
536
  <h2 id="modalUserName"></h2>
537
  <div id="modalUserUsername" class="username"></div>
538
  </div>
539
-
540
  <input type="hidden" id="modalUserId">
541
-
542
  <div class="form-row">
543
  <div class="form-group">
544
  <label for="purchaseAmount">Сумма покупки</label>
@@ -549,7 +563,6 @@ ADMIN_TEMPLATE = """
549
  <input type="number" id="deductAmount" placeholder="Например, 100" oninput="calculateBonuses()">
550
  </div>
551
  </div>
552
-
553
  <div class="calculation-summary">
554
  <div class="summary-item"><span>Текущий баланс:</span> <strong id="summaryCurrentBalance">0.00</strong></div>
555
  <div class="summary-item"><span>Будет начислено (2%):</span> <strong id="summaryAccrual">+0.00</strong></div>
@@ -557,22 +570,45 @@ ADMIN_TEMPLATE = """
557
  <hr>
558
  <div class="summary-item"><strong>Итоговый баланс:</strong> <strong id="summaryFinalBalance">0.00</strong></div>
559
  </div>
560
-
561
  <div class="history-container">
562
  <h3>История операций</h3>
563
  <ul id="modalHistoryList" class="history-list"></ul>
564
  </div>
565
-
566
  <div class="modal-footer">
567
- <button class="btn-submit" onclick="submitTransaction()">Провести операцию</button>
568
  <div id="modalStatus" class="status-message"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
569
  </div>
570
  </div>
571
  </div>
572
 
573
  <script>
574
- const modal = document.getElementById('transactionModal');
575
- const allUsers = {{ users|tojson }};
576
  let currentUserData = null;
577
 
578
  function searchUsers() {
@@ -620,31 +656,36 @@ ADMIN_TEMPLATE = """
620
  }
621
 
622
  calculateBonuses();
623
- modal.style.display = 'block';
624
  }
625
 
626
- function closeModal() {
627
- modal.style.display = 'none';
628
- currentUserData = null;
 
 
 
 
 
 
 
 
 
 
629
  }
630
 
631
  function calculateBonuses() {
632
  if (!currentUserData) return;
633
-
634
  const currentBalance = parseFloat(currentUserData.bonuses) || 0;
635
  const purchaseAmount = parseFloat(document.getElementById('purchaseAmount').value) || 0;
636
  const deductAmount = parseFloat(document.getElementById('deductAmount').value) || 0;
637
-
638
  const accrualAmount = purchaseAmount * 0.02;
639
-
640
  let finalDeductAmount = deductAmount;
641
  if (deductAmount > currentBalance) {
642
  finalDeductAmount = currentBalance;
643
- document.getElementById('deductAmount').value = finalDeductAmount;
644
  }
645
-
646
  const finalBalance = currentBalance + accrualAmount - finalDeductAmount;
647
-
648
  document.getElementById('summaryCurrentBalance').textContent = currentBalance.toFixed(2);
649
  document.getElementById('summaryAccrual').textContent = `+${accrualAmount.toFixed(2)}`;
650
  document.getElementById('summaryDeduction').textContent = `-${finalDeductAmount.toFixed(2)}`;
@@ -655,19 +696,16 @@ ADMIN_TEMPLATE = """
655
  const statusEl = document.getElementById('modalStatus');
656
  statusEl.style.color = 'var(--admin-secondary)';
657
  statusEl.textContent = 'Обработка...';
658
-
659
  const payload = {
660
  user_id: document.getElementById('modalUserId').value,
661
  purchase_amount: parseFloat(document.getElementById('purchaseAmount').value) || 0,
662
  deduct_amount: parseFloat(document.getElementById('deductAmount').value) || 0,
663
  };
664
-
665
  if (payload.purchase_amount <= 0 && payload.deduct_amount <= 0) {
666
  statusEl.style.color = 'var(--admin-danger)';
667
  statusEl.textContent = 'Введите сумму покупки или сумму для списания.';
668
  return;
669
  }
670
-
671
  try {
672
  const response = await fetch('/admin/add_transaction', {
673
  method: 'POST',
@@ -675,13 +713,47 @@ ADMIN_TEMPLATE = """
675
  body: JSON.stringify(payload)
676
  });
677
  const result = await response.json();
678
-
679
  if (response.ok) {
680
  statusEl.style.color = 'var(--admin-success)';
681
  statusEl.textContent = 'Операция успешно проведена!';
682
- setTimeout(() => {
683
- location.reload();
684
- }, 1500);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
685
  } else {
686
  throw new Error(result.message || 'Произошла ошибка');
687
  }
@@ -692,8 +764,11 @@ ADMIN_TEMPLATE = """
692
  }
693
 
694
  window.onclick = function(event) {
695
- if (event.target == modal) {
696
- closeModal();
 
 
 
697
  }
698
  }
699
  </script>
@@ -770,9 +845,9 @@ def verify_data():
770
 
771
  save_visitor_data({user_id_str: user_entry})
772
 
773
- return redirect(url_for('index', user_id_for_test=user_id_str))
774
-
775
- return jsonify({"status": "ok", "verified": True, "user": user_info_dict}), 200
776
  else:
777
  logging.warning(f"Verification failed for user: {user_info_dict.get('id')}")
778
  return jsonify({"status": "error", "verified": False, "message": "Invalid data"}), 403
@@ -781,13 +856,52 @@ def verify_data():
781
  logging.exception("Error in /verify endpoint")
782
  return jsonify({"status": "error", "message": "Internal server error"}), 500
783
 
784
-
785
  @app.route('/admin')
786
  def admin_panel():
787
  current_data = load_visitor_data()
788
  users_list = list(current_data.values())
789
  return render_template_string(ADMIN_TEMPLATE, users=users_list)
790
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
791
  @app.route('/admin/add_transaction', methods=['POST'])
792
  def add_transaction():
793
  try:
 
365
  if (themeParams.button_text_color) root.style.setProperty('--tg-theme-button-text-color', themeParams.button_text_color);
366
  if (themeParams.secondary_bg_color) root.style.setProperty('--tg-theme-secondary-bg-color', themeParams.secondary_bg_color);
367
  }
368
+
369
  function setupTelegram() {
370
  if (!tg || !tg.initData) {
371
  console.error("Telegram WebApp script not loaded or initData is missing.");
372
  document.body.style.visibility = 'visible';
373
  return;
374
  }
375
+
376
  tg.ready();
377
  tg.expand();
378
 
 
381
  }
382
  tg.onEvent('themeChanged', () => applyTheme(tg.themeParams));
383
 
384
+ const urlParams = new URLSearchParams(window.location.search);
385
+ const userIdForTest = urlParams.get('user_id_for_test');
386
+
387
+ if (!userIdForTest) {
388
+ fetch('/verify', {
389
+ method: 'POST',
390
+ headers: {
391
+ 'Content-Type': 'application/json',
392
+ 'Accept': 'application/json'
393
+ },
394
+ body: JSON.stringify({ initData: tg.initData }),
395
+ })
396
+ .then(response => response.json())
397
+ .then(data => {
398
+ if (data.status === 'ok' && data.verified && data.user_id) {
399
+ console.log('Backend verification successful. Reloading with user data.');
400
+ window.location.replace('/?user_id_for_test=' + data.user_id);
401
+ } else {
402
+ console.warn('Backend verification failed:', data.message);
403
+ document.body.style.visibility = 'visible';
404
+ }
405
+ })
406
+ .catch(error => {
407
+ console.error('Error sending initData for verification:', error);
408
+ document.body.style.visibility = 'visible';
409
+ });
410
+ } else {
411
+ document.body.style.visibility = 'visible';
412
+ }
413
 
414
  const user = tg.initDataUnsafe?.user;
415
  const greetingElement = document.getElementById('greeting');
416
  if (user) {
417
  const name = user.first_name || user.username || 'Гость';
418
  greetingElement.textContent = `Добро пожаловать, ${name}! 👋`;
419
+ } else {
420
+ greetingElement.textContent = `Добро пожаловать, {{ user.first_name or 'Гость' }}! 👋`;
421
  }
 
 
422
  }
423
 
424
  if (window.Telegram && window.Telegram.WebApp) {
425
  setupTelegram();
426
  } else {
427
+ window.addEventListener('load', setupTelegram, {once: true});
428
  setTimeout(() => {
429
  if (document.body.style.visibility !== 'visible') {
430
  document.body.style.visibility = 'visible';
 
465
  body { font-family: var(--font-family); background-color: var(--admin-bg); color: var(--admin-text); margin: 0; padding: var(--padding); line-height: 1.6; }
466
  .container { max-width: 1200px; margin: 0 auto; }
467
  h1 { text-align: center; color: var(--admin-secondary); margin-bottom: var(--padding); font-weight: 600; }
468
+ .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); }
469
+ .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; }
470
+ .btn { padding: 12px 20px; font-size: 1em; border: none; border-radius: 8px; font-weight: 600; cursor: pointer; transition: background-color 0.2s ease; }
471
+ .btn-primary { background-color: var(--admin-primary); color: #000; }
472
+ .btn-primary:hover { background-color: var(--admin-primary-dark); }
473
  .user-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: var(--padding); margin-top: var(--padding); }
474
  .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; }
475
  .user-card:hover { transform: translateY(-5px); box-shadow: 0 8px 25px rgba(0, 0, 0, 0.08); }
 
480
  .user-bonuses { text-align: center; margin-bottom: 1rem; }
481
  .user-bonuses .label { font-size: 0.9em; color: var(--admin-secondary); }
482
  .user-bonuses .amount { font-size: 1.8em; font-weight: 700; color: var(--admin-primary-dark); }
483
+ .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; }
484
+ .user-actions .btn-manage:hover { background-color: var(--admin-primary-dark); }
485
  .no-users { text-align: center; color: var(--admin-secondary); margin-top: 2rem; font-size: 1.1em; }
 
486
  .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); }
487
  .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); }
488
  .modal-close { color: #aaa; position: absolute; top: 15px; right: 25px; font-size: 28px; font-weight: bold; cursor: pointer; }
 
492
  .form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; align-items: center; margin-bottom: 1.5rem; }
493
  .form-group { display: flex; flex-direction: column; }
494
  .form-group label { margin-bottom: 0.5rem; font-weight: 500; font-size: 0.9em; }
495
+ .form-group input { padding: 10px; font-size: 1rem; border: 1px solid var(--admin-border); border-radius: 8px; width: 100%; box-sizing: border-box; }
496
  .calculation-summary { background: #f0f0f0; padding: 1rem; border-radius: 8px; margin-bottom: 1.5rem; }
497
  .summary-item { display: flex; justify-content: space-between; margin-bottom: 0.5rem; font-size: 0.95em; }
498
  .summary-item strong { font-weight: 600; }
 
505
  .history-item .date { font-size: 0.8em; color: var(--admin-secondary); }
506
  .history-item .amount.accrual { color: var(--admin-success); font-weight: 600; }
507
  .history-item .amount.deduction { color: var(--admin-danger); font-weight: 600; }
508
+ .modal-footer { margin-top: 1.5rem; display: flex; justify-content: flex-end; align-items: center; gap: 1rem;}
509
  .modal-footer button { padding: 12px 25px; font-size: 1.1em; border-radius: 8px; border: none; font-weight: 600; cursor: pointer; }
510
  .btn-submit { background-color: var(--admin-success); color: white; }
511
+ .status-message { text-align: center; font-weight: 500; flex-grow: 1; text-align: left; }
512
  </style>
513
  </head>
514
  <body>
515
  <div class="container">
516
  <h1>Панель администратора Druzhba</h1>
517
  <div class="controls-bar">
518
+ <input type="text" id="searchInput" onkeyup="searchUsers()" placeholder="Поиск по имени, ID, username, номеру...">
519
+ <button class="btn btn-primary" onclick="openAddClientModal()">Добавить клиента</button>
520
  </div>
521
 
522
  {% if users %}
 
535
  <div class="amount">{{ "%.2f"|format(user.bonuses|float) }}</div>
536
  </div>
537
  <div class="user-actions">
538
+ <button class="btn-manage" onclick='openTransactionModal({{ user|tojson }})'>Управление бонусами</button>
539
  </div>
540
  </div>
541
  {% endfor %}
 
547
 
548
  <div id="transactionModal" class="modal">
549
  <div class="modal-content">
550
+ <span class="modal-close" onclick="closeModal('transactionModal')">×</span>
551
  <div class="modal-header">
552
  <h2 id="modalUserName"></h2>
553
  <div id="modalUserUsername" class="username"></div>
554
  </div>
 
555
  <input type="hidden" id="modalUserId">
 
556
  <div class="form-row">
557
  <div class="form-group">
558
  <label for="purchaseAmount">Сумма покупки</label>
 
563
  <input type="number" id="deductAmount" placeholder="Например, 100" oninput="calculateBonuses()">
564
  </div>
565
  </div>
 
566
  <div class="calculation-summary">
567
  <div class="summary-item"><span>Текущий баланс:</span> <strong id="summaryCurrentBalance">0.00</strong></div>
568
  <div class="summary-item"><span>Будет начислено (2%):</span> <strong id="summaryAccrual">+0.00</strong></div>
 
570
  <hr>
571
  <div class="summary-item"><strong>Итоговый баланс:</strong> <strong id="summaryFinalBalance">0.00</strong></div>
572
  </div>
 
573
  <div class="history-container">
574
  <h3>История операций</h3>
575
  <ul id="modalHistoryList" class="history-list"></ul>
576
  </div>
 
577
  <div class="modal-footer">
 
578
  <div id="modalStatus" class="status-message"></div>
579
+ <button class="btn-submit" onclick="submitTransaction()">Провести операцию</button>
580
+ </div>
581
+ </div>
582
+ </div>
583
+
584
+ <div id="addClientModal" class="modal">
585
+ <div class="modal-content">
586
+ <span class="modal-close" onclick="closeModal('addClientModal')">×</span>
587
+ <div class="modal-header">
588
+ <h2>Добавить нового клиента</h2>
589
+ </div>
590
+ <div class="form-group" style="margin-bottom: 1rem;">
591
+ <label for="newClientFirstName">Имя</label>
592
+ <input type="text" id="newClientFirstName" placeholder="Иван">
593
+ </div>
594
+ <div class="form-group" style="margin-bottom: 1rem;">
595
+ <label for="newClientLastName">Фамилия</label>
596
+ <input type="text" id="newClientLastName" placeholder="Иванов">
597
+ </div>
598
+ <div class="form-group" style="margin-bottom: 1.5rem;">
599
+ <label for="newClientPhone">Номер телефона (уникальный)</label>
600
+ <input type="tel" id="newClientPhone" placeholder="+79001234567">
601
+ </div>
602
+ <div class="modal-footer">
603
+ <div id="addClientStatus" class="status-message"></div>
604
+ <button class="btn-submit" onclick="submitNewClient()">Сохранить клиента</button>
605
  </div>
606
  </div>
607
  </div>
608
 
609
  <script>
610
+ const transactionModal = document.getElementById('transactionModal');
611
+ const addClientModal = document.getElementById('addClientModal');
612
  let currentUserData = null;
613
 
614
  function searchUsers() {
 
656
  }
657
 
658
  calculateBonuses();
659
+ transactionModal.style.display = 'block';
660
  }
661
 
662
+ function openAddClientModal() {
663
+ document.getElementById('newClientFirstName').value = '';
664
+ document.getElementById('newClientLastName').value = '';
665
+ document.getElementById('newClientPhone').value = '';
666
+ document.getElementById('addClientStatus').textContent = '';
667
+ addClientModal.style.display = 'block';
668
+ }
669
+
670
+ function closeModal(modalId) {
671
+ document.getElementById(modalId).style.display = 'none';
672
+ if (modalId === 'transactionModal') {
673
+ currentUserData = null;
674
+ }
675
  }
676
 
677
  function calculateBonuses() {
678
  if (!currentUserData) return;
 
679
  const currentBalance = parseFloat(currentUserData.bonuses) || 0;
680
  const purchaseAmount = parseFloat(document.getElementById('purchaseAmount').value) || 0;
681
  const deductAmount = parseFloat(document.getElementById('deductAmount').value) || 0;
 
682
  const accrualAmount = purchaseAmount * 0.02;
 
683
  let finalDeductAmount = deductAmount;
684
  if (deductAmount > currentBalance) {
685
  finalDeductAmount = currentBalance;
686
+ document.getElementById('deductAmount').value = finalDeductAmount.toFixed(2);
687
  }
 
688
  const finalBalance = currentBalance + accrualAmount - finalDeductAmount;
 
689
  document.getElementById('summaryCurrentBalance').textContent = currentBalance.toFixed(2);
690
  document.getElementById('summaryAccrual').textContent = `+${accrualAmount.toFixed(2)}`;
691
  document.getElementById('summaryDeduction').textContent = `-${finalDeductAmount.toFixed(2)}`;
 
696
  const statusEl = document.getElementById('modalStatus');
697
  statusEl.style.color = 'var(--admin-secondary)';
698
  statusEl.textContent = 'Обработка...';
 
699
  const payload = {
700
  user_id: document.getElementById('modalUserId').value,
701
  purchase_amount: parseFloat(document.getElementById('purchaseAmount').value) || 0,
702
  deduct_amount: parseFloat(document.getElementById('deductAmount').value) || 0,
703
  };
 
704
  if (payload.purchase_amount <= 0 && payload.deduct_amount <= 0) {
705
  statusEl.style.color = 'var(--admin-danger)';
706
  statusEl.textContent = 'Введите сумму покупки или сумму для списания.';
707
  return;
708
  }
 
709
  try {
710
  const response = await fetch('/admin/add_transaction', {
711
  method: 'POST',
 
713
  body: JSON.stringify(payload)
714
  });
715
  const result = await response.json();
 
716
  if (response.ok) {
717
  statusEl.style.color = 'var(--admin-success)';
718
  statusEl.textContent = 'Операция успешно проведена!';
719
+ setTimeout(() => { location.reload(); }, 1500);
720
+ } else {
721
+ throw new Error(result.message || 'Произошла ошибка');
722
+ }
723
+ } catch (error) {
724
+ statusEl.style.color = 'var(--admin-danger)';
725
+ statusEl.textContent = `Ошибка: ${error.message}`;
726
+ }
727
+ }
728
+
729
+ async function submitNewClient() {
730
+ const statusEl = document.getElementById('addClientStatus');
731
+ statusEl.style.color = 'var(--admin-secondary)';
732
+ statusEl.textContent = 'Сохранение...';
733
+
734
+ const payload = {
735
+ first_name: document.getElementById('newClientFirstName').value.trim(),
736
+ last_name: document.getElementById('newClientLastName').value.trim(),
737
+ phone_number: document.getElementById('newClientPhone').value.trim(),
738
+ };
739
+
740
+ if (!payload.first_name || !payload.phone_number) {
741
+ statusEl.style.color = 'var(--admin-danger)';
742
+ statusEl.textContent = 'Имя и номер телефона обязательны.';
743
+ return;
744
+ }
745
+
746
+ try {
747
+ const response = await fetch('/admin/add_client', {
748
+ method: 'POST',
749
+ headers: { 'Content-Type': 'application/json' },
750
+ body: JSON.stringify(payload)
751
+ });
752
+ const result = await response.json();
753
+ if (response.ok) {
754
+ statusEl.style.color = 'var(--admin-success)';
755
+ statusEl.textContent = 'Клиент успешно добавлен!';
756
+ setTimeout(() => { location.reload(); }, 1500);
757
  } else {
758
  throw new Error(result.message || 'Произошла ошибка');
759
  }
 
764
  }
765
 
766
  window.onclick = function(event) {
767
+ if (event.target == transactionModal) {
768
+ closeModal('transactionModal');
769
+ }
770
+ if (event.target == addClientModal) {
771
+ closeModal('addClientModal');
772
  }
773
  }
774
  </script>
 
845
 
846
  save_visitor_data({user_id_str: user_entry})
847
 
848
+ return jsonify({"status": "ok", "verified": True, "user_id": user_id_str})
849
+ else:
850
+ return jsonify({"status": "error", "verified": True, "message": "User ID not found in parsed data"}), 400
851
  else:
852
  logging.warning(f"Verification failed for user: {user_info_dict.get('id')}")
853
  return jsonify({"status": "error", "verified": False, "message": "Invalid data"}), 403
 
856
  logging.exception("Error in /verify endpoint")
857
  return jsonify({"status": "error", "message": "Internal server error"}), 500
858
 
 
859
  @app.route('/admin')
860
  def admin_panel():
861
  current_data = load_visitor_data()
862
  users_list = list(current_data.values())
863
  return render_template_string(ADMIN_TEMPLATE, users=users_list)
864
 
865
+ @app.route('/admin/add_client', methods=['POST'])
866
+ def add_client():
867
+ try:
868
+ data = request.get_json()
869
+ phone_number = data.get('phone_number')
870
+ first_name = data.get('first_name')
871
+ last_name = data.get('last_name')
872
+
873
+ if not phone_number or not first_name:
874
+ return jsonify({"status": "error", "message": "Имя и номер телефона обязательны."}), 400
875
+
876
+ all_data = load_visitor_data()
877
+
878
+ if phone_number in all_data:
879
+ return jsonify({"status": "error", "message": "Клиент с таким номером телефона уже существует."}), 409
880
+
881
+ now = datetime.now()
882
+ new_client = {
883
+ 'id': phone_number,
884
+ 'first_name': first_name,
885
+ 'last_name': last_name,
886
+ 'username': phone_number,
887
+ 'photo_url': None,
888
+ 'language_code': 'ru',
889
+ 'is_premium': False,
890
+ 'visited_at': now.timestamp(),
891
+ 'visited_at_str': now.strftime('%Y-%m-%d %H:%M:%S'),
892
+ 'bonuses': 0,
893
+ 'history': []
894
+ }
895
+
896
+ save_visitor_data({phone_number: new_client})
897
+
898
+ return jsonify({"status": "ok", "message": "Client added successfully"}), 201
899
+
900
+ except Exception as e:
901
+ logging.exception("Error in /admin/add_client endpoint")
902
+ return jsonify({"status": "error", "message": str(e)}), 500
903
+
904
+
905
  @app.route('/admin/add_transaction', methods=['POST'])
906
  def add_transaction():
907
  try: