Kgshop commited on
Commit
fdd1b2c
·
verified ·
1 Parent(s): d6a8eff

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +553 -405
app.py CHANGED
@@ -7,7 +7,7 @@ import hashlib
7
  import json
8
  from urllib.parse import unquote, parse_qs, quote
9
  import time
10
- from datetime import datetime
11
  import logging
12
  import threading
13
  import random
@@ -31,12 +31,17 @@ app.secret_key = os.urandom(24)
31
  _data_lock = threading.Lock()
32
  visitor_data_cache = {}
33
 
 
 
34
  def generate_unique_id(all_data):
35
  while True:
36
  new_id = str(random.randint(10000, 99999))
37
  if new_id not in all_data:
38
  return new_id
39
 
 
 
 
40
  def download_data_from_hf():
41
  global visitor_data_cache
42
  if not HF_TOKEN_READ:
@@ -123,7 +128,7 @@ def upload_data_to_hf():
123
  repo_id=REPO_ID,
124
  repo_type="dataset",
125
  token=HF_TOKEN_WRITE,
126
- commit_message=f"Update bonus data {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
127
  )
128
  logging.info("Bonus data successfully uploaded to Hugging Face.")
129
  except Exception as e:
@@ -198,6 +203,7 @@ TEMPLATE = """
198
  --shadow-glow: 0 0 35px var(--shadow-color);
199
  --shadow-color-red: rgba(244, 67, 54, 0.15);
200
  --shadow-glow-red: 0 0 35px var(--shadow-color-red);
 
201
  }
202
  * { box-sizing: border-box; margin: 0; padding: 0; }
203
  html, body {
@@ -218,27 +224,11 @@ TEMPLATE = """
218
  flex-direction: column;
219
  gap: var(--padding-m);
220
  }
221
- .header {
222
- text-align: left;
223
- padding: var(--padding-m) 0;
224
- }
225
- .logo {
226
- font-size: 2.5em;
227
- font-weight: 800;
228
- color: var(--text-color);
229
- letter-spacing: -1px;
230
- }
231
  .logo span { color: var(--brand-yellow); }
232
- .welcome-text {
233
- font-size: 1em;
234
- color: var(--text-secondary-color);
235
- margin-top: 4px;
236
- }
237
- .card-grid {
238
- display: grid;
239
- grid-template-columns: 1fr 1fr;
240
- gap: var(--padding-m);
241
- }
242
  .bonus-card, .debt-card {
243
  background: linear-gradient(145deg, #2a2a2a, #1c1c1c);
244
  border-radius: calc(var(--border-radius) + 8px);
@@ -247,93 +237,51 @@ TEMPLATE = """
247
  position: relative;
248
  overflow: hidden;
249
  }
250
- .bonus-card {
251
- box-shadow: var(--shadow-glow);
252
- border: 1px solid rgba(255, 193, 7, 0.2);
253
- }
254
- .debt-card {
255
- box-shadow: var(--shadow-glow-red);
256
- border: 1px solid rgba(244, 67, 54, 0.2);
257
- }
258
- .card-label {
259
- font-size: 1.1em;
260
- font-weight: 500;
261
- color: var(--text-secondary-color);
262
- margin-bottom: 12px;
263
- }
264
- .bonus-amount {
265
- font-size: 3em;
266
- font-weight: 800;
267
- color: var(--brand-yellow);
268
- letter-spacing: -2px;
269
- line-height: 1;
270
- }
271
- .debt-amount {
272
- font-size: 3em;
273
- font-weight: 800;
274
- color: var(--brand-red);
275
- letter-spacing: -2px;
276
- line-height: 1;
277
- }
278
- .client-id-card {
279
- background-color: var(--card-bg);
280
- border-radius: var(--border-radius);
281
- padding: var(--padding-m);
282
- display: flex;
283
- justify-content: space-between;
284
- align-items: center;
285
- }
286
- .client-id-label {
287
- font-weight: 500;
288
- color: var(--text-secondary-color);
289
- }
290
- .client-id-value {
291
- font-size: 1.3em;
292
- font-weight: 700;
293
- color: var(--brand-yellow);
294
- letter-spacing: 2px;
295
- background-color: rgba(255,193,7,0.1);
296
- padding: 4px 10px;
297
- border-radius: 8px;
298
- }
299
- .history-section {
300
- background-color: var(--card-bg);
301
- border-radius: var(--border-radius);
302
- padding: var(--padding-l);
303
- }
304
- .history-title {
305
- font-size: 1.4em;
306
- font-weight: 700;
307
- margin-bottom: var(--padding-m);
308
- padding-bottom: var(--padding-m);
309
- border-bottom: 1px solid rgba(255, 255, 255, 0.1);
310
- }
311
- .history-list {
312
- list-style: none;
313
- padding: 0;
314
- margin: 0;
315
- max-height: 35vh;
316
- overflow-y: auto;
317
- }
318
- .history-item {
319
- display: flex;
320
- justify-content: space-between;
321
- align-items: center;
322
- padding: 14px 4px;
323
- border-bottom: 1px solid rgba(255, 255, 255, 0.05);
324
- }
325
  .history-item:last-child { border-bottom: none; }
326
  .history-details { display: flex; flex-direction: column; }
327
  .history-description { font-size: 1em; font-weight: 500; }
 
328
  .history-date { font-size: 0.8em; color: var(--text-secondary-color); margin-top: 4px; }
329
  .history-amount { font-size: 1.1em; font-weight: 700; }
330
  .history-amount.accrual { color: #4CAF50; }
331
  .history-amount.deduction { color: #F44336; }
332
- .no-history {
333
- text-align: center;
334
- color: var(--text-secondary-color);
335
- padding: 2rem 0;
 
 
 
336
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
337
  </style>
338
  </head>
339
  <body>
@@ -367,6 +315,9 @@ TEMPLATE = """
367
  <li class="history-item">
368
  <div class="history-details">
369
  <span class="history-description">{{ item.description }}</span>
 
 
 
370
  <span class="history-date">{{ item.date_str }}</span>
371
  </div>
372
  {% if item.transaction_type == 'bonus' %}
@@ -377,6 +328,10 @@ TEMPLATE = """
377
  <span class="history-amount {{ 'deduction' if item.type == 'accrual' else 'accrual' }}">
378
  {{ '+' if item.type == 'accrual' else '-' }}{{ "%.2f"|format(item.amount|float) }}
379
  </span>
 
 
 
 
380
  {% endif %}
381
  </li>
382
  {% endfor %}
@@ -387,6 +342,26 @@ TEMPLATE = """
387
  </section>
388
  </div>
389
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
390
  <script>
391
  const tg = window.Telegram.WebApp;
392
 
@@ -398,6 +373,7 @@ TEMPLATE = """
398
  root.style.setProperty('--text-color', themeParams.text_color || '#ffffff');
399
  root.style.setProperty('--text-secondary-color', themeParams.hint_color || '#a0a0a0');
400
  root.style.setProperty('--brand-yellow', themeParams.button_color || '#FFC107');
 
401
  root.style.setProperty('--card-bg', themeParams.secondary_bg_color || (isDark ? '#1c1c1e' : '#f1f1f1'));
402
  }
403
 
@@ -462,6 +438,36 @@ TEMPLATE = """
462
  }
463
  }, 3000);
464
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
465
  </script>
466
  </body>
467
  </html>
@@ -479,19 +485,10 @@ ADMIN_TEMPLATE = """
479
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
480
  <style>
481
  :root {
482
- --admin-bg: #f8f9fa;
483
- --admin-text: #212529;
484
- --admin-card-bg: #ffffff;
485
- --admin-border: #dee2e6;
486
- --admin-shadow: rgba(0, 0, 0, 0.05);
487
- --admin-primary: #FFC107;
488
- --admin-primary-dark: #e0a800;
489
- --admin-secondary: #6c757d;
490
- --admin-success: #198754;
491
- --admin-danger: #dc3545;
492
- --border-radius: 12px;
493
- --padding: 1.5rem;
494
- --font-family: 'Inter', sans-serif;
495
  }
496
  body { font-family: var(--font-family); background-color: var(--admin-bg); color: var(--admin-text); margin: 0; padding: var(--padding); line-height: 1.6; }
497
  .container { max-width: 1200px; margin: 0 auto; }
@@ -509,6 +506,8 @@ ADMIN_TEMPLATE = """
509
  .btn-primary:hover { background-color: var(--admin-primary-dark); }
510
  .btn-delete { background-color: var(--admin-danger); color: white; }
511
  .btn-delete:hover { background-color: #c82333; }
 
 
512
  .user-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); gap: var(--padding); margin-top: var(--padding); }
513
  .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; }
514
  .user-card:hover { transform: translateY(-5px); box-shadow: 0 8px 25px rgba(0, 0, 0, 0.08); }
@@ -520,12 +519,14 @@ ADMIN_TEMPLATE = """
520
  .user-balances .label { font-size: 0.9em; color: var(--admin-secondary); }
521
  .user-balances .amount.bonus { font-size: 1.8em; font-weight: 700; color: var(--admin-primary-dark); }
522
  .user-balances .amount.debt { font-size: 1.8em; font-weight: 700; color: var(--admin-danger); }
523
- .user-actions { margin-top: auto; display: flex; flex-direction: column; gap: 0.5rem; }
 
524
  .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; }
525
  .btn-manage:hover { background-color: var(--admin-primary-dark); }
526
  .no-users { text-align: center; color: var(--admin-secondary); margin-top: 2rem; font-size: 1.1em; }
527
  .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); }
528
- .modal-content { background-color: var(--admin-bg); margin: 5% auto; padding: var(--padding); border: 1px solid var(--admin-border); width: 90%; max-width: 700px; border-radius: var(--border-radius); position: relative; box-shadow: 0 8px 30px rgba(0,0,0,0.15); }
 
529
  .modal-close { color: #aaa; position: absolute; top: 15px; right: 25px; font-size: 28px; font-weight: bold; cursor: pointer; }
530
  .modal-header { padding-bottom: 1rem; margin-bottom: 1.5rem; border-bottom: 1px solid var(--admin-border); }
531
  .modal-header h2 { margin: 0; font-size: 1.5rem; }
@@ -533,93 +534,80 @@ ADMIN_TEMPLATE = """
533
  .form-section { border: 1px solid var(--admin-border); border-radius: 8px; padding: 1rem; margin-bottom: 1.5rem; }
534
  .form-section h3 { margin-top: 0; margin-bottom: 1rem; font-size: 1.1em; }
535
  .form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; align-items: flex-end; }
 
 
536
  .form-group { display: flex; flex-direction: column; }
537
  .form-group label { margin-bottom: 0.5rem; font-weight: 500; font-size: 0.9em; }
538
- .form-group input { padding: 10px; font-size: 1rem; border: 1px solid var(--admin-border); border-radius: 8px; width: 100%; box-sizing: border-box; }
539
- .calculation-summary { background: #f0f0f0; padding: 1rem; border-radius: 8px; margin-top: 1rem; }
 
 
540
  .summary-item { display: flex; justify-content: space-between; margin-bottom: 0.5rem; font-size: 0.95em; }
541
  .summary-item strong { font-weight: 600; }
542
- .history-container { margin-top: 1.5rem; }
543
- .history-container h3 { font-size: 1.2rem; margin-bottom: 1rem; }
544
- .history-list { list-style: none; padding: 0; max-height: 200px; overflow-y: auto; border: 1px solid var(--admin-border); border-radius: 8px; }
545
- .history-item { display: flex; justify-content: space-between; padding: 8px 12px; border-bottom: 1px solid var(--admin-border); }
546
- .history-item:last-child { border-bottom: none; }
547
- .history-item .desc { font-size: 0.9em; }
548
- .history-item .date { font-size: 0.8em; color: var(--admin-secondary); }
549
- .history-item .amount.bonus-accrual { color: var(--admin-success); font-weight: 600; }
550
- .history-item .amount.bonus-deduction { color: var(--admin-danger); font-weight: 600; }
551
- .history-item .amount.debt-accrual { color: var(--admin-danger); font-weight: 600; }
552
- .history-item .amount.debt-payment { color: var(--admin-success); font-weight: 600; }
 
 
 
553
  .modal-footer { margin-top: 1.5rem; display: flex; justify-content: flex-end; align-items: center; gap: 1rem;}
554
  .modal-footer button { padding: 12px 25px; font-size: 1.1em; border-radius: 8px; border: none; font-weight: 600; cursor: pointer; }
555
  .btn-submit { background-color: var(--admin-success); color: white; }
556
- .status-message { text-align: center; font-weight: 500; flex-grow: 1; text-align: left; }
 
 
 
 
557
  </style>
558
  </head>
559
  <body>
560
  <div class="container">
561
  <h1>Панель администратора Bonus</h1>
562
  <div class="summary-bar">
563
- <div class="summary-card">
564
- <div class="value">{{ summary.total_users }}</div>
565
- <div class="label">Всего клиентов</div>
566
- </div>
567
- <div class="summary-card">
568
- <div class="value bonus">{{ "%.2f"|format(summary.total_bonuses|float) }}</div>
569
- <div class="label">Всего бонусов</div>
570
- </div>
571
- <div class="summary-card">
572
- <div class="value debt">{{ "%.2f"|format(summary.total_debts|float) }}</div>
573
- <div class="label">Всего долгов</div>
574
- </div>
575
- <div class="summary-card">
576
- <div class="value debt">{{ summary.users_with_debt }}</div>
577
- <div class="label">Клиенты с долгом</div>
578
- </div>
579
  </div>
580
-
581
  <div class="controls-bar">
582
  <input type="text" id="searchInput" onkeyup="searchUsers()" placeholder="Поиск по имени, ID, username, номеру...">
583
  <button class="btn btn-primary" onclick="openAddClientModal()">Добавить клиента</button>
584
  </div>
585
-
586
- {% if users %}
587
- <div class="user-grid" id="userGrid">
588
- {% for user in users|sort(attribute='visited_at', reverse=true) %}
589
- <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 }}">
590
- <div class="user-info">
591
- <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">
592
- <div class="user-details">
593
- <div class="name">{{ user.first_name or '' }} {{ user.last_name or '' }}</div>
594
- <div class="username">@{{ user.username if user.username else user.phone_number }} | ID: {{ user.id }}</div>
595
- </div>
596
- </div>
597
- <div class="user-balances">
598
- <div>
599
- <div class="label">Бонусы</div>
600
- <div class="amount bonus">{{ "%.2f"|format(user.bonuses|float) }}</div>
601
- </div>
602
- <div>
603
- <div class="label">Долг</div>
604
- <div class="amount debt">{{ "%.2f"|format(user.debts|float if user.debts else 0) }}</div>
605
- </div>
606
- </div>
607
- <div class="user-actions">
608
- <button class="btn-manage" onclick='openTransactionModal({{ user|tojson }})'>Управление счетом</button>
609
- {% if user.telegram_id == None %}
610
- <button class="btn btn-delete" onclick='deleteClient("{{ user.id }}")'>Удалить клиента</button>
611
- {% endif %}
612
  </div>
613
  </div>
614
- {% endfor %}
615
- </div>
616
- {% else %}
617
- <p class="no-users">Пользователей пока нет.</p>
618
- {% endif %}
 
 
 
 
 
 
 
 
619
  </div>
620
 
621
  <div id="transactionModal" class="modal">
622
- <div class="modal-content">
623
  <span class="modal-close" onclick="closeModal('transactionModal')">×</span>
624
  <div class="modal-header">
625
  <h2 id="modalUserName"></h2>
@@ -630,20 +618,13 @@ ADMIN_TEMPLATE = """
630
  <div class="form-section">
631
  <h3>Бонусы</h3>
632
  <div class="form-row">
633
- <div class="form-group">
634
- <label for="purchaseAmount">Сумма покупки (для начисления)</label>
635
- <input type="number" id="purchaseAmount" placeholder="1500" oninput="updateCalculations()">
636
- </div>
637
- <div class="form-group">
638
- <label for="deductAmount">Списать бонусов</label>
639
- <input type="number" id="deductAmount" placeholder="100" oninput="updateCalculations()">
640
- </div>
641
  </div>
642
  <div class="calculation-summary">
643
  <div class="summary-item"><span>Текущий баланс:</span> <strong id="summaryCurrentBalance">0.00</strong></div>
644
  <div class="summary-item"><span>Будет начислено (2%):</span> <strong id="summaryAccrual">+0.00</strong></div>
645
- <div class="summary-item"><span>Будет списано:</span> <strong id="summaryDeduction">-0.00</strong></div>
646
- <hr>
647
  <div class="summary-item"><strong>Итоговый баланс бонусов:</strong> <strong id="summaryFinalBalance">0.00</strong></div>
648
  </div>
649
  </div>
@@ -651,71 +632,104 @@ ADMIN_TEMPLATE = """
651
  <div class="form-section">
652
  <h3>Долги</h3>
653
  <div class="form-row">
654
- <div class="form-group">
655
- <label for="addDebtAmount">Добавить долг</label>
656
- <input type="number" id="addDebtAmount" placeholder="500" oninput="updateCalculations()">
657
- </div>
658
- <div class="form-group">
659
- <label for="repayDebtAmount">Погасить долг</label>
660
- <input type="number" id="repayDebtAmount" placeholder="200" oninput="updateCalculations()">
661
- </div>
662
  </div>
663
  <div class="calculation-summary">
664
  <div class="summary-item"><span>Текущий долг:</span> <strong id="summaryCurrentDebt">0.00</strong></div>
665
  <div class="summary-item"><span>Будет добавлено:</span> <strong id="summaryAddDebt">+0.00</strong></div>
666
- <div class="summary-item"><span>Будет погашено:</span> <strong id="summaryRepayDebt">-0.00</strong></div>
667
- <hr>
668
  <div class="summary-item"><strong>Итоговый долг:</strong> <strong id="summaryFinalDebt">0.00</strong></div>
669
  </div>
670
  </div>
671
-
 
 
 
 
 
 
 
 
 
 
 
 
672
  <div class="history-container">
673
  <h3>Общая история операций</h3>
674
  <ul id="modalHistoryList" class="history-list"></ul>
675
  </div>
676
- <div class="modal-footer">
677
- <div id="modalStatus" class="status-message"></div>
678
- <button class="btn-submit" onclick="submitTransaction()">Провести операцию</button>
679
- </div>
680
  </div>
681
  </div>
682
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
683
  <div id="addClientModal" class="modal">
684
  <div class="modal-content">
685
  <span class="modal-close" onclick="closeModal('addClientModal')">×</span>
686
- <div class="modal-header">
687
- <h2>Добавить нового клиента</h2>
688
- </div>
689
- <div class="form-group" style="margin-bottom: 1rem;">
690
- <label for="newClientFirstName">Имя</label>
691
- <input type="text" id="newClientFirstName" placeholder="Иван">
692
- </div>
693
- <div class="form-group" style="margin-bottom: 1.5rem;">
694
- <label for="newClientPhone">Номер телефона (уникальный)</label>
695
- <input type="tel" id="newClientPhone" placeholder="+79001234567">
696
- </div>
697
- <div class="modal-footer">
698
- <div id="addClientStatus" class="status-message"></div>
699
- <button class="btn-submit" onclick="submitNewClient()">Сохранить клиента</button>
700
- </div>
701
  </div>
702
  </div>
703
 
704
  <script>
705
  const transactionModal = document.getElementById('transactionModal');
706
  const addClientModal = document.getElementById('addClientModal');
 
 
 
707
  let currentUserData = null;
 
708
 
709
  function searchUsers() {
710
  const searchTerm = document.getElementById('searchInput').value.toLowerCase();
711
  const userCards = document.querySelectorAll('.user-card');
712
  userCards.forEach(card => {
713
  const cardSearchTerm = card.getAttribute('data-search-term');
714
- if (cardSearchTerm.includes(searchTerm)) {
715
- card.style.display = 'flex';
716
- } else {
717
- card.style.display = 'none';
718
- }
719
  });
720
  }
721
 
@@ -724,48 +738,182 @@ ADMIN_TEMPLATE = """
724
  document.getElementById('modalUserId').value = userData.id;
725
  document.getElementById('modalUserName').textContent = `${userData.first_name || ''} ${userData.last_name || ''}`;
726
  document.getElementById('modalUserUsername').textContent = `@${userData.username || userData.phone_number} | ID: ${userData.id}`;
727
- document.getElementById('purchaseAmount').value = '';
728
- document.getElementById('deductAmount').value = '';
729
- document.getElementById('addDebtAmount').value = '';
730
- document.getElementById('repayDebtAmount').value = '';
731
  document.getElementById('modalStatus').textContent = '';
732
-
 
 
 
 
 
 
733
  const historyList = document.getElementById('modalHistoryList');
734
  historyList.innerHTML = '';
 
 
 
 
 
 
735
 
736
- const bonusHistory = (userData.history || []).map(h => ({...h, transaction_type: 'bonus'}));
737
- const debtHistory = (userData.debt_history || []).map(h => ({...h, transaction_type: 'debt'}));
738
- const combinedHistory = [...bonusHistory, ...debtHistory].sort((a, b) => new Date(b.date) - new Date(a.date));
739
 
740
  if (combinedHistory.length > 0) {
741
  combinedHistory.forEach(item => {
742
  const li = document.createElement('li');
743
  li.className = 'history-item';
744
- let sign, amountClass, amountText;
745
  if (item.transaction_type === 'bonus') {
746
- sign = item.type === 'accrual' ? '+' : '-';
747
  amountClass = item.type === 'accrual' ? 'bonus-accrual' : 'bonus-deduction';
748
- amountText = `${sign}${parseFloat(item.amount).toFixed(2)} (бонус)`;
749
- } else { // debt
750
- sign = item.type === 'accrual' ? '+' : '-';
751
  amountClass = item.type === 'accrual' ? 'debt-accrual' : 'debt-payment';
752
- amountText = `${sign}${parseFloat(item.amount).toFixed(2)} (долг)`;
 
 
 
 
 
 
753
  }
754
- li.innerHTML = `
755
- <div>
756
- <div class="desc">${item.description}</div>
757
- <div class="date">${item.date_str}</div>
758
- </div>
759
- <div class="amount ${amountClass}">${amountText}</div>
760
- `;
761
  historyList.appendChild(li);
762
  });
763
  } else {
764
  historyList.innerHTML = '<li style="text-align:center; padding: 1rem; color: var(--admin-secondary);">Нет истории</li>';
765
  }
766
 
767
- updateCalculations();
768
- transactionModal.style.display = 'block';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
769
  }
770
 
771
  function openAddClientModal() {
@@ -777,23 +925,25 @@ ADMIN_TEMPLATE = """
777
 
778
  function closeModal(modalId) {
779
  document.getElementById(modalId).style.display = 'none';
780
- if (modalId === 'transactionModal') {
781
- currentUserData = null;
782
- }
783
  }
784
 
785
  function updateCalculations() {
786
  if (!currentUserData) return;
787
-
788
  const currentBalance = parseFloat(currentUserData.bonuses) || 0;
789
  const purchaseAmount = parseFloat(document.getElementById('purchaseAmount').value) || 0;
790
  const deductAmount = parseFloat(document.getElementById('deductAmount').value) || 0;
791
  const accrualAmount = purchaseAmount * 0.02;
792
- let finalDeductAmount = deductAmount;
793
- if (deductAmount > currentBalance) {
 
 
 
794
  finalDeductAmount = currentBalance;
795
  document.getElementById('deductAmount').value = finalDeductAmount > 0 ? finalDeductAmount.toFixed(2) : '';
796
  }
 
797
  const finalBalance = currentBalance + accrualAmount - finalDeductAmount;
798
  document.getElementById('summaryCurrentBalance').textContent = currentBalance.toFixed(2);
799
  document.getElementById('summaryAccrual').textContent = `+${accrualAmount.toFixed(2)}`;
@@ -803,11 +953,17 @@ ADMIN_TEMPLATE = """
803
  const currentDebt = parseFloat(currentUserData.debts) || 0;
804
  const addDebtAmount = parseFloat(document.getElementById('addDebtAmount').value) || 0;
805
  const repayDebtAmount = parseFloat(document.getElementById('repayDebtAmount').value) || 0;
806
- let finalRepayAmount = repayDebtAmount;
807
- if (repayDebtAmount > currentDebt) {
 
 
 
 
808
  finalRepayAmount = currentDebt;
809
  document.getElementById('repayDebtAmount').value = finalRepayAmount > 0 ? finalRepayAmount.toFixed(2) : '';
810
  }
 
 
811
  const finalDebt = currentDebt + addDebtAmount - finalRepayAmount;
812
  document.getElementById('summaryCurrentDebt').textContent = currentDebt.toFixed(2);
813
  document.getElementById('summaryAddDebt').textContent = `+${addDebtAmount.toFixed(2)}`;
@@ -830,7 +986,7 @@ ADMIN_TEMPLATE = """
830
 
831
  if (payload.purchase_amount <= 0 && payload.deduct_amount <= 0 && payload.add_debt_amount <= 0 && payload.repay_debt_amount <= 0) {
832
  statusEl.style.color = 'var(--admin-danger)';
833
- statusEl.textContent = 'Введите сумму для любой из операций.';
834
  return;
835
  }
836
  try {
@@ -843,10 +999,8 @@ ADMIN_TEMPLATE = """
843
  if (response.ok) {
844
  statusEl.style.color = 'var(--admin-success)';
845
  statusEl.textContent = 'Операция успешно проведена!';
846
- setTimeout(() => { location.reload(); }, 1500);
847
- } else {
848
- throw new Error(result.message || 'Произошла ошибка');
849
- }
850
  } catch (error) {
851
  statusEl.style.color = 'var(--admin-danger)';
852
  statusEl.textContent = `Ошибка: ${error.message}`;
@@ -857,18 +1011,15 @@ ADMIN_TEMPLATE = """
857
  const statusEl = document.getElementById('addClientStatus');
858
  statusEl.style.color = 'var(--admin-secondary)';
859
  statusEl.textContent = 'Сохранение...';
860
-
861
  const payload = {
862
  first_name: document.getElementById('newClientFirstName').value.trim(),
863
  phone_number: document.getElementById('newClientPhone').value.trim(),
864
  };
865
-
866
  if (!payload.first_name || !payload.phone_number) {
867
  statusEl.style.color = 'var(--admin-danger)';
868
  statusEl.textContent = 'Имя и номер телефона обязательны.';
869
  return;
870
  }
871
-
872
  try {
873
  const response = await fetch('/admin/add_client', {
874
  method: 'POST',
@@ -880,9 +1031,7 @@ ADMIN_TEMPLATE = """
880
  statusEl.style.color = 'var(--admin-success)';
881
  statusEl.textContent = 'Клиент успешно добавлен!';
882
  setTimeout(() => { location.reload(); }, 1500);
883
- } else {
884
- throw new Error(result.message || 'Произошла ошибка');
885
- }
886
  } catch (error) {
887
  statusEl.style.color = 'var(--admin-danger)';
888
  statusEl.textContent = `Ошибка: ${error.message}`;
@@ -890,9 +1039,7 @@ ADMIN_TEMPLATE = """
890
  }
891
 
892
  async function deleteClient(userId) {
893
- if (!confirm(`Вы уверены, что хотите удалить клиента с ID ${userId}? Это действие необратимо.`)) {
894
- return;
895
- }
896
  try {
897
  const response = await fetch('/admin/delete_client', {
898
  method: 'POST',
@@ -900,23 +1047,41 @@ ADMIN_TEMPLATE = """
900
  body: JSON.stringify({ user_id: userId })
901
  });
902
  const result = await response.json();
903
- if (response.ok) {
904
- location.reload();
905
- } else {
906
- throw new Error(result.message || 'Не удалось удалить клиента.');
 
 
 
 
 
 
 
 
 
 
 
 
 
907
  }
 
 
 
 
 
908
  } catch (error) {
909
- alert(`Ошибка: ${error.message}`);
 
910
  }
911
  }
912
 
 
913
  window.onclick = function(event) {
914
- if (event.target == transactionModal) {
915
- closeModal('transactionModal');
916
- }
917
- if (event.target == addClientModal) {
918
- closeModal('addClientModal');
919
- }
920
  }
921
  </script>
922
  </body>
@@ -926,38 +1091,41 @@ ADMIN_TEMPLATE = """
926
  @app.route('/')
927
  def index():
928
  user_id_str = request.args.get('user_id_for_test')
929
-
930
  current_data = load_visitor_data()
931
  user_data = {}
932
 
933
  if user_id_str and user_id_str in current_data:
934
- user_data = current_data[user_id_str]
935
  user_data['id'] = user_id_str
936
 
 
937
  bonus_history = user_data.get('history', [])
938
  for item in bonus_history:
939
- item['transaction_type'] = 'bonus'
 
 
940
 
941
  debt_history = user_data.get('debt_history', [])
942
  for item in debt_history:
943
- item['transaction_type'] = 'debt'
944
-
945
- combined_history = sorted(
946
- bonus_history + debt_history,
947
- key=lambda x: x['date'],
948
- reverse=True
949
- )
 
 
 
 
 
950
  user_data['combined_history'] = combined_history
951
  else:
952
  user_data = {
953
- "id": "N/A",
954
- "bonuses": 0,
955
- "debts": 0,
956
- "history": [],
957
- "debt_history": [],
958
  "combined_history": []
959
  }
960
-
961
  return render_template_string(TEMPLATE, user=user_data)
962
 
963
  @app.route('/verify', methods=['POST'])
@@ -969,7 +1137,6 @@ def verify_data():
969
  return jsonify({"status": "error", "message": "Missing initData"}), 400
970
 
971
  user_data_parsed, is_valid = verify_telegram_data(init_data_str)
972
-
973
  user_info_dict = {}
974
  if user_data_parsed and 'user' in user_data_parsed:
975
  try:
@@ -982,7 +1149,7 @@ def verify_data():
982
  if is_valid:
983
  tg_user_id = user_info_dict.get('id')
984
  if tg_user_id:
985
- now = datetime.now()
986
  all_data = load_visitor_data()
987
 
988
  existing_user_key = None
@@ -991,6 +1158,7 @@ def verify_data():
991
  existing_user_key = key
992
  break
993
 
 
994
  if existing_user_key:
995
  user_entry = all_data[existing_user_key]
996
  user_entry.update({
@@ -999,40 +1167,27 @@ def verify_data():
999
  'username': user_info_dict.get('username'),
1000
  'photo_url': user_info_dict.get('photo_url'),
1001
  'language_code': user_info_dict.get('language_code'),
1002
- 'visited_at': now.timestamp(),
1003
- 'visited_at_str': now.strftime('%Y-%m-%d %H:%M:%S')
1004
  })
1005
- user_id_to_save = existing_user_key
1006
  else:
1007
  new_user_id = generate_unique_id(all_data)
 
1008
  user_entry = {
1009
- 'id': new_user_id,
1010
- 'telegram_id': tg_user_id,
1011
- 'first_name': user_info_dict.get('first_name'),
1012
- 'last_name': user_info_dict.get('last_name'),
1013
- 'username': user_info_dict.get('username'),
1014
- 'photo_url': user_info_dict.get('photo_url'),
1015
- 'language_code': user_info_dict.get('language_code'),
1016
- 'is_premium': user_info_dict.get('is_premium', False),
1017
- 'phone_number': None,
1018
- 'visited_at': now.timestamp(),
1019
- 'visited_at_str': now.strftime('%Y-%m-%d %H:%M:%S'),
1020
- 'bonuses': 0,
1021
- 'history': [],
1022
- 'debts': 0,
1023
- 'debt_history': []
1024
  }
1025
- user_id_to_save = new_user_id
1026
-
1027
  save_visitor_data({user_id_to_save: user_entry})
1028
-
1029
  return jsonify({"status": "ok", "verified": True, "user_id": user_id_to_save})
1030
  else:
1031
  return jsonify({"status": "error", "verified": True, "message": "User ID not found in parsed data"}), 400
1032
  else:
1033
  logging.warning(f"Verification failed for user: {user_info_dict.get('id')}")
1034
  return jsonify({"status": "error", "verified": False, "message": "Invalid data"}), 403
1035
-
1036
  except Exception as e:
1037
  logging.exception("Error in /verify endpoint")
1038
  return jsonify({"status": "error", "message": "Internal server error"}), 500
@@ -1042,8 +1197,9 @@ def admin_panel():
1042
  current_data = load_visitor_data()
1043
  users_list = []
1044
  for user_id, user_data in current_data.items():
1045
- user_data['id'] = user_id
1046
- users_list.append(user_data)
 
1047
 
1048
  total_users = len(users_list)
1049
  total_bonuses = sum(u.get('bonuses', 0) for u in users_list)
@@ -1051,14 +1207,21 @@ def admin_panel():
1051
  users_with_debt = sum(1 for u in users_list if u.get('debts', 0) > 0)
1052
 
1053
  summary_stats = {
1054
- "total_users": total_users,
1055
- "total_bonuses": total_bonuses,
1056
- "total_debts": total_debts,
1057
- "users_with_debt": users_with_debt
1058
  }
1059
-
1060
  return render_template_string(ADMIN_TEMPLATE, users=users_list, summary=summary_stats)
1061
 
 
 
 
 
 
 
 
 
 
 
1062
  @app.route('/admin/add_client', methods=['POST'])
1063
  def add_client():
1064
  try:
@@ -1070,41 +1233,25 @@ def add_client():
1070
  return jsonify({"status": "error", "message": "Имя и номер телефона обязательны."}), 400
1071
 
1072
  all_data = load_visitor_data()
1073
-
1074
  for user in all_data.values():
1075
  if user.get('phone_number') == phone_number:
1076
  return jsonify({"status": "error", "message": "Клиент с таким номером телефона уже существует."}), 409
1077
 
1078
- now = datetime.now()
1079
  new_id = generate_unique_id(all_data)
1080
 
1081
  new_client = {
1082
- 'id': new_id,
1083
- 'telegram_id': None,
1084
- 'first_name': first_name,
1085
- 'last_name': None,
1086
- 'username': None,
1087
- 'photo_url': None,
1088
- 'language_code': 'ru',
1089
- 'is_premium': False,
1090
- 'phone_number': phone_number,
1091
- 'visited_at': now.timestamp(),
1092
- 'visited_at_str': now.strftime('%Y-%m-%d %H:%M:%S'),
1093
- 'bonuses': 0,
1094
- 'history': [],
1095
- 'debts': 0,
1096
- 'debt_history': []
1097
  }
1098
-
1099
  save_visitor_data({new_id: new_client})
1100
-
1101
  return jsonify({"status": "ok", "message": "Client added successfully"}), 201
1102
-
1103
  except Exception as e:
1104
  logging.exception("Error in /admin/add_client endpoint")
1105
  return jsonify({"status": "error", "message": str(e)}), 500
1106
 
1107
-
1108
  @app.route('/admin/add_transaction', methods=['POST'])
1109
  def add_transaction():
1110
  try:
@@ -1125,62 +1272,76 @@ def add_transaction():
1125
  return jsonify({"status": "error", "message": "User not found"}), 404
1126
 
1127
  user = all_data[user_id_str]
1128
- now = datetime.now()
1129
- now_iso = now.isoformat()
1130
- now_str = now.strftime('%Y-%m-%d %H:%M:%S')
1131
-
1132
- if deduct_amount > user.get('bonuses', 0):
1133
- return jsonify({"status": "error", "message": "Недостаточно бонусов для списания"}), 400
1134
 
1135
- if repay_debt_amount > user.get('debts', 0):
1136
- return jsonify({"status": "error", "message": "Сумма погашения превышает текущий долг"}), 400
1137
-
1138
- # Bonus operations
1139
  accrual_amount = purchase_amount * 0.02
1140
- user['bonuses'] = user.get('bonuses', 0) + accrual_amount - deduct_amount
1141
- if 'history' not in user or not isinstance(user['history'], list):
1142
- user['history'] = []
 
 
 
 
1143
 
 
 
1144
  if accrual_amount > 0:
1145
- user['history'].append({
1146
- "type": "accrual", "amount": accrual_amount,
1147
- "description": f"Начисление с покупки {purchase_amount}",
1148
- "date": now_iso, "date_str": now_str
1149
- })
1150
  if deduct_amount > 0:
1151
- user['history'].append({
1152
- "type": "deduction", "amount": deduct_amount,
1153
- "description": "Списание бонусов",
1154
- "date": now_iso, "date_str": now_str
1155
- })
1156
 
1157
- # Debt operations
1158
  user['debts'] = user.get('debts', 0) + add_debt_amount - repay_debt_amount
1159
- if 'debt_history' not in user or not isinstance(user['debt_history'], list):
1160
- user['debt_history'] = []
1161
-
1162
  if add_debt_amount > 0:
1163
- user['debt_history'].append({
1164
- "type": "accrual", "amount": add_debt_amount,
1165
- "description": "Добавление долга",
1166
- "date": now_iso, "date_str": now_str
1167
- })
1168
  if repay_debt_amount > 0:
1169
- user['debt_history'].append({
1170
- "type": "payment", "amount": repay_debt_amount,
1171
- "description": "Погашение долга",
1172
- "date": now_iso, "date_str": now_str
1173
- })
1174
 
1175
  save_visitor_data({user_id_str: user})
 
 
 
 
1176
 
1177
- return jsonify({
1178
- "status": "ok", "message": "Transaction successful",
1179
- "new_balance": user['bonuses'], "new_debt": user['debts']
1180
- }), 200
 
 
 
 
 
 
1181
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1182
  except Exception as e:
1183
- logging.exception("Error in /admin/add_transaction endpoint")
1184
  return jsonify({"status": "error", "message": str(e)}), 500
1185
 
1186
  @app.route('/admin/delete_client', methods=['POST'])
@@ -1188,23 +1349,16 @@ def delete_client():
1188
  try:
1189
  data = request.get_json()
1190
  user_id = data.get('user_id')
1191
-
1192
- if not user_id:
1193
- return jsonify({"status": "error", "message": "User ID is required"}), 400
1194
-
1195
  user_id_str = str(user_id)
1196
  load_visitor_data()
1197
-
1198
  with _data_lock:
1199
  if user_id_str not in visitor_data_cache:
1200
  return jsonify({"status": "error", "message": "User not found"}), 404
1201
-
1202
  user_to_delete = visitor_data_cache[user_id_str]
1203
  if user_to_delete.get('telegram_id') is not None:
1204
  return jsonify({"status": "error", "message": "Cannot delete a Telegram-linked user"}), 403
1205
-
1206
  del visitor_data_cache[user_id_str]
1207
-
1208
  try:
1209
  with open(DATA_FILE, 'w', encoding='utf-8') as f:
1210
  json.dump(visitor_data_cache, f, ensure_ascii=False, indent=4)
@@ -1213,9 +1367,7 @@ def delete_client():
1213
  except Exception as e:
1214
  logging.error(f"Error saving data after deletion: {e}")
1215
  return jsonify({"status": "error", "message": "Failed to save data after deletion"}), 500
1216
-
1217
  return jsonify({"status": "ok", "message": "Client deleted successfully"}), 200
1218
-
1219
  except Exception as e:
1220
  logging.exception("Error in /admin/delete_client endpoint")
1221
  return jsonify({"status": "error", "message": str(e)}), 500
@@ -1228,15 +1380,11 @@ if __name__ == '__main__':
1228
  else:
1229
  print("Attempting initial data download from Hugging Face...")
1230
  download_data_from_hf()
1231
-
1232
  load_visitor_data()
1233
-
1234
  print("WARNING: The /admin route is NOT protected. Implement proper authentication for production.")
1235
-
1236
  if HF_TOKEN_WRITE:
1237
  backup_thread = threading.Thread(target=periodic_backup, daemon=True)
1238
  backup_thread.start()
1239
  print("Periodic backup thread started (every hour).")
1240
-
1241
  print("--- Server Ready ---")
1242
  app.run(host=HOST, port=PORT, debug=False)
 
7
  import json
8
  from urllib.parse import unquote, parse_qs, quote
9
  import time
10
+ from datetime import datetime, timezone, timedelta
11
  import logging
12
  import threading
13
  import random
 
31
  _data_lock = threading.Lock()
32
  visitor_data_cache = {}
33
 
34
+ BISHKEK_TZ = timezone(timedelta(hours=6))
35
+
36
  def generate_unique_id(all_data):
37
  while True:
38
  new_id = str(random.randint(10000, 99999))
39
  if new_id not in all_data:
40
  return new_id
41
 
42
+ def generate_invoice_id():
43
+ return f"inv_{int(time.time() * 1000)}_{random.randint(100,999)}"
44
+
45
  def download_data_from_hf():
46
  global visitor_data_cache
47
  if not HF_TOKEN_READ:
 
128
  repo_id=REPO_ID,
129
  repo_type="dataset",
130
  token=HF_TOKEN_WRITE,
131
+ commit_message=f"Update bonus data {datetime.now(BISHKEK_TZ).strftime('%Y-%m-%d %H:%M:%S')}"
132
  )
133
  logging.info("Bonus data successfully uploaded to Hugging Face.")
134
  except Exception as e:
 
203
  --shadow-glow: 0 0 35px var(--shadow-color);
204
  --shadow-color-red: rgba(244, 67, 54, 0.15);
205
  --shadow-glow-red: 0 0 35px var(--shadow-color-red);
206
+ --link-color: var(--brand-yellow);
207
  }
208
  * { box-sizing: border-box; margin: 0; padding: 0; }
209
  html, body {
 
224
  flex-direction: column;
225
  gap: var(--padding-m);
226
  }
227
+ .header { text-align: left; padding: var(--padding-m) 0; }
228
+ .logo { font-size: 2.5em; font-weight: 800; color: var(--text-color); letter-spacing: -1px; }
 
 
 
 
 
 
 
 
229
  .logo span { color: var(--brand-yellow); }
230
+ .welcome-text { font-size: 1em; color: var(--text-secondary-color); margin-top: 4px; }
231
+ .card-grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--padding-m); }
 
 
 
 
 
 
 
 
232
  .bonus-card, .debt-card {
233
  background: linear-gradient(145deg, #2a2a2a, #1c1c1c);
234
  border-radius: calc(var(--border-radius) + 8px);
 
237
  position: relative;
238
  overflow: hidden;
239
  }
240
+ .bonus-card { box-shadow: var(--shadow-glow); border: 1px solid rgba(255, 193, 7, 0.2); }
241
+ .debt-card { box-shadow: var(--shadow-glow-red); border: 1px solid rgba(244, 67, 54, 0.2); }
242
+ .card-label { font-size: 1.1em; font-weight: 500; color: var(--text-secondary-color); margin-bottom: 12px; }
243
+ .bonus-amount, .debt-amount { font-size: 3em; font-weight: 800; letter-spacing: -2px; line-height: 1; }
244
+ .bonus-amount { color: var(--brand-yellow); }
245
+ .debt-amount { color: var(--brand-red); }
246
+ .client-id-card { background-color: var(--card-bg); border-radius: var(--border-radius); padding: var(--padding-m); display: flex; justify-content: space-between; align-items: center; }
247
+ .client-id-label { font-weight: 500; color: var(--text-secondary-color); }
248
+ .client-id-value { font-size: 1.3em; font-weight: 700; color: var(--brand-yellow); letter-spacing: 2px; background-color: rgba(255,193,7,0.1); padding: 4px 10px; border-radius: 8px; }
249
+ .history-section { background-color: var(--card-bg); border-radius: var(--border-radius); padding: var(--padding-l); }
250
+ .history-title { font-size: 1.4em; font-weight: 700; margin-bottom: var(--padding-m); padding-bottom: var(--padding-m); border-bottom: 1px solid rgba(255, 255, 255, 0.1); }
251
+ .history-list { list-style: none; padding: 0; margin: 0; max-height: 35vh; overflow-y: auto; }
252
+ .history-item { display: flex; justify-content: space-between; align-items: center; padding: 14px 4px; border-bottom: 1px solid rgba(255, 255, 255, 0.05); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
253
  .history-item:last-child { border-bottom: none; }
254
  .history-details { display: flex; flex-direction: column; }
255
  .history-description { font-size: 1em; font-weight: 500; }
256
+ .history-invoice-details-link { font-size: 0.8em; color: var(--link-color); cursor: pointer; text-decoration: underline; margin-top: 4px; }
257
  .history-date { font-size: 0.8em; color: var(--text-secondary-color); margin-top: 4px; }
258
  .history-amount { font-size: 1.1em; font-weight: 700; }
259
  .history-amount.accrual { color: #4CAF50; }
260
  .history-amount.deduction { color: #F44336; }
261
+ .history-amount.invoice { color: var(--brand-yellow); }
262
+ .no-history { text-align: center; color: var(--text-secondary-color); padding: 2rem 0; }
263
+
264
+ .modal {
265
+ display: none; position: fixed; z-index: 1000; left: 0; top: 0; width: 100%; height: 100%;
266
+ overflow: auto; background-color: rgba(0,0,0,0.7); backdrop-filter: blur(3px);
267
+ align-items: center; justify-content: center; padding: var(--padding-m);
268
  }
269
+ .modal-content {
270
+ background-color: var(--card-bg); color: var(--text-color);
271
+ padding: var(--padding-l); border-radius: var(--border-radius);
272
+ width: 100%; max-width: 500px;
273
+ box-shadow: 0 5px 25px rgba(0,0,0,0.3);
274
+ position: relative;
275
+ }
276
+ .modal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--padding-m); padding-bottom: var(--padding-m); border-bottom: 1px solid rgba(255,255,255,0.1); }
277
+ .modal-title { font-size: 1.3em; font-weight: 700; }
278
+ .modal-close { font-size: 1.8em; font-weight: bold; color: var(--text-secondary-color); cursor: pointer; line-height: 1; }
279
+ .modal-body table { width: 100%; border-collapse: collapse; margin-top: 10px; }
280
+ .modal-body th, .modal-body td { text-align: left; padding: 8px; border-bottom: 1px solid rgba(255,255,255,0.1); font-size: 0.9em; }
281
+ .modal-body th { font-weight: 600; }
282
+ .modal-body td.item-total, .modal-body td.grand-total { text-align: right; }
283
+ .modal-body .grand-total-row td { font-weight: bold; font-size: 1em; padding-top: 10px; border-top: 2px solid rgba(255,255,255,0.2); }
284
+
285
  </style>
286
  </head>
287
  <body>
 
315
  <li class="history-item">
316
  <div class="history-details">
317
  <span class="history-description">{{ item.description }}</span>
318
+ {% if item.transaction_type == 'invoice' %}
319
+ <span class="history-invoice-details-link" onclick="showClientInvoiceDetails('{{ item.items | tojson | e }}', '{{ item.id }}')">Посмотреть детали</span>
320
+ {% endif %}
321
  <span class="history-date">{{ item.date_str }}</span>
322
  </div>
323
  {% if item.transaction_type == 'bonus' %}
 
328
  <span class="history-amount {{ 'deduction' if item.type == 'accrual' else 'accrual' }}">
329
  {{ '+' if item.type == 'accrual' else '-' }}{{ "%.2f"|format(item.amount|float) }}
330
  </span>
331
+ {% elif item.transaction_type == 'invoice' %}
332
+ <span class="history-amount invoice">
333
+ {{ "%.2f"|format(item.total_amount|float) }}
334
+ </span>
335
  {% endif %}
336
  </li>
337
  {% endfor %}
 
342
  </section>
343
  </div>
344
 
345
+ <div id="clientInvoiceDetailModal" class="modal">
346
+ <div class="modal-content">
347
+ <div class="modal-header">
348
+ <h3 id="clientInvoiceModalTitle" class="modal-title">Детали накладной</h3>
349
+ <span class="modal-close" onclick="closeClientInvoiceModal()">×</span>
350
+ </div>
351
+ <div class="modal-body">
352
+ <table id="clientInvoiceItemsTable">
353
+ <thead>
354
+ <tr><th>Товар</th><th>Кол-во</th><th>Цена</th><th>Сумма</th></tr>
355
+ </thead>
356
+ <tbody></tbody>
357
+ <tfoot>
358
+ <tr class="grand-total-row"><td colspan="3">Итого:</td><td id="clientInvoiceGrandTotal" class="grand-total"></td></tr>
359
+ </tfoot>
360
+ </table>
361
+ </div>
362
+ </div>
363
+ </div>
364
+
365
  <script>
366
  const tg = window.Telegram.WebApp;
367
 
 
373
  root.style.setProperty('--text-color', themeParams.text_color || '#ffffff');
374
  root.style.setProperty('--text-secondary-color', themeParams.hint_color || '#a0a0a0');
375
  root.style.setProperty('--brand-yellow', themeParams.button_color || '#FFC107');
376
+ root.style.setProperty('--link-color', themeParams.link_color || themeParams.button_color || '#FFC107');
377
  root.style.setProperty('--card-bg', themeParams.secondary_bg_color || (isDark ? '#1c1c1e' : '#f1f1f1'));
378
  }
379
 
 
438
  }
439
  }, 3000);
440
  }
441
+
442
+ function showClientInvoiceDetails(invoiceItemsJson, invoiceId) {
443
+ const items = JSON.parse(invoiceItemsJson);
444
+ const modal = document.getElementById('clientInvoiceDetailModal');
445
+ document.getElementById('clientInvoiceModalTitle').textContent = `Детали накладной #${invoiceId.substring(0,8)}...`;
446
+ const tableBody = modal.querySelector('#clientInvoiceItemsTable tbody');
447
+ tableBody.innerHTML = '';
448
+ let grandTotal = 0;
449
+ items.forEach(item => {
450
+ const row = tableBody.insertRow();
451
+ row.insertCell().textContent = item.name;
452
+ row.insertCell().textContent = item.quantity;
453
+ row.insertCell().textContent = parseFloat(item.price_per_unit).toFixed(2);
454
+ row.insertCell().textContent = parseFloat(item.item_total).toFixed(2);
455
+ row.cells[3].classList.add('item-total');
456
+ grandTotal += parseFloat(item.item_total);
457
+ });
458
+ document.getElementById('clientInvoiceGrandTotal').textContent = grandTotal.toFixed(2);
459
+ modal.style.display = 'flex';
460
+ }
461
+
462
+ function closeClientInvoiceModal() {
463
+ document.getElementById('clientInvoiceDetailModal').style.display = 'none';
464
+ }
465
+ window.onclick = function(event) {
466
+ if (event.target == document.getElementById('clientInvoiceDetailModal')) {
467
+ closeClientInvoiceModal();
468
+ }
469
+ }
470
+
471
  </script>
472
  </body>
473
  </html>
 
485
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
486
  <style>
487
  :root {
488
+ --admin-bg: #f8f9fa; --admin-text: #212529; --admin-card-bg: #ffffff; --admin-border: #dee2e6;
489
+ --admin-shadow: rgba(0, 0, 0, 0.05); --admin-primary: #FFC107; --admin-primary-dark: #e0a800;
490
+ --admin-secondary: #6c757d; --admin-success: #198754; --admin-danger: #dc3545;
491
+ --admin-info: #0dcaf0; --border-radius: 12px; --padding: 1.5rem; --font-family: 'Inter', sans-serif;
 
 
 
 
 
 
 
 
 
492
  }
493
  body { font-family: var(--font-family); background-color: var(--admin-bg); color: var(--admin-text); margin: 0; padding: var(--padding); line-height: 1.6; }
494
  .container { max-width: 1200px; margin: 0 auto; }
 
506
  .btn-primary:hover { background-color: var(--admin-primary-dark); }
507
  .btn-delete { background-color: var(--admin-danger); color: white; }
508
  .btn-delete:hover { background-color: #c82333; }
509
+ .btn-info { background-color: var(--admin-info); color: white; }
510
+ .btn-info:hover { background-color: #0baccc; }
511
  .user-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); gap: var(--padding); margin-top: var(--padding); }
512
  .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; }
513
  .user-card:hover { transform: translateY(-5px); box-shadow: 0 8px 25px rgba(0, 0, 0, 0.08); }
 
519
  .user-balances .label { font-size: 0.9em; color: var(--admin-secondary); }
520
  .user-balances .amount.bonus { font-size: 1.8em; font-weight: 700; color: var(--admin-primary-dark); }
521
  .user-balances .amount.debt { font-size: 1.8em; font-weight: 700; color: var(--admin-danger); }
522
+ .user-actions { margin-top: auto; display: grid; grid-template-columns: 1fr; gap: 0.5rem; }
523
+ .user-actions.two-buttons { grid-template-columns: 1fr 1fr; }
524
  .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; }
525
  .btn-manage:hover { background-color: var(--admin-primary-dark); }
526
  .no-users { text-align: center; color: var(--admin-secondary); margin-top: 2rem; font-size: 1.1em; }
527
  .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); }
528
+ .modal-content { background-color: var(--admin-bg); margin: 5% auto; padding: var(--padding); border: 1px solid var(--admin-border); width: 90%; max-width: 800px; border-radius: var(--border-radius); position: relative; box-shadow: 0 8px 30px rgba(0,0,0,0.15); }
529
+ .modal-content.large { max-width: 950px; }
530
  .modal-close { color: #aaa; position: absolute; top: 15px; right: 25px; font-size: 28px; font-weight: bold; cursor: pointer; }
531
  .modal-header { padding-bottom: 1rem; margin-bottom: 1.5rem; border-bottom: 1px solid var(--admin-border); }
532
  .modal-header h2 { margin: 0; font-size: 1.5rem; }
 
534
  .form-section { border: 1px solid var(--admin-border); border-radius: 8px; padding: 1rem; margin-bottom: 1.5rem; }
535
  .form-section h3 { margin-top: 0; margin-bottom: 1rem; font-size: 1.1em; }
536
  .form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; align-items: flex-end; }
537
+ .form-row.three-col { grid-template-columns: 2fr 1fr 1fr; }
538
+ .form-row.four-col { grid-template-columns: 2fr 1fr 1fr auto; }
539
  .form-group { display: flex; flex-direction: column; }
540
  .form-group label { margin-bottom: 0.5rem; font-weight: 500; font-size: 0.9em; }
541
+ .form-group input, .form-group button { padding: 10px; font-size: 1rem; border: 1px solid var(--admin-border); border-radius: 8px; width: 100%; box-sizing: border-box; }
542
+ .form-group button { background-color: var(--admin-info); color: white; cursor: pointer; }
543
+ .form-group button:hover { background-color: #0baccc; }
544
+ .calculation-summary, .invoice-summary { background: #f0f0f0; padding: 1rem; border-radius: 8px; margin-top: 1rem; }
545
  .summary-item { display: flex; justify-content: space-between; margin-bottom: 0.5rem; font-size: 0.95em; }
546
  .summary-item strong { font-weight: 600; }
547
+ .history-container, .invoice-items-container, .existing-invoices-container { margin-top: 1.5rem; }
548
+ .history-container h3, .invoice-items-container h3, .existing-invoices-container h3 { font-size: 1.2rem; margin-bottom: 1rem; }
549
+ .history-list, .invoice-items-list, .existing-invoices-list { list-style: none; padding: 0; max-height: 200px; overflow-y: auto; border: 1px solid var(--admin-border); border-radius: 8px; }
550
+ .history-item, .invoice-item-row, .existing-invoice-item { display: flex; justify-content: space-between; align-items: center; padding: 8px 12px; border-bottom: 1px solid var(--admin-border); }
551
+ .history-item:last-child, .invoice-item-row:last-child, .existing-invoice-item:last-child { border-bottom: none; }
552
+ .history-item .desc, .invoice-item-row .name, .existing-invoice-item .desc { font-size: 0.9em; flex-grow: 1; }
553
+ .invoice-item-row .qty, .invoice-item-row .price, .invoice-item-row .total { font-size: 0.9em; width: 60px; text-align: right;}
554
+ .invoice-item-row .actions { width: 40px; text-align: right; }
555
+ .existing-invoice-item .actions button { font-size: 0.8em; padding: 4px 8px; }
556
+ .history-item .date, .existing-invoice-item .date { font-size: 0.8em; color: var(--admin-secondary); }
557
+ .history-item .amount, .existing-invoice-item .amount { font-weight: 600; }
558
+ .history-item .amount.bonus-accrual, .history-item .amount.debt-payment { color: var(--admin-success); }
559
+ .history-item .amount.bonus-deduction, .history-item .amount.debt-accrual { color: var(--admin-danger); }
560
+ .history-item .amount.invoice { color: var(--admin-info); }
561
  .modal-footer { margin-top: 1.5rem; display: flex; justify-content: flex-end; align-items: center; gap: 1rem;}
562
  .modal-footer button { padding: 12px 25px; font-size: 1.1em; border-radius: 8px; border: none; font-weight: 600; cursor: pointer; }
563
  .btn-submit { background-color: var(--admin-success); color: white; }
564
+ .status-message { font-weight: 500; flex-grow: 1; text-align: left; }
565
+ table.invoice-items-table { width: 100%; border-collapse: collapse; margin-top: 10px; }
566
+ table.invoice-items-table th, table.invoice-items-table td { text-align: left; padding: 8px; border-bottom: 1px solid var(--admin-border); font-size: 0.9em; }
567
+ table.invoice-items-table th { font-weight: 600; background-color: #f0f0f0; }
568
+ table.invoice-items-table td.number { text-align: right; }
569
  </style>
570
  </head>
571
  <body>
572
  <div class="container">
573
  <h1>Панель администратора Bonus</h1>
574
  <div class="summary-bar">
575
+ <div class="summary-card"><div class="value">{{ summary.total_users }}</div><div class="label">Всего клиентов</div></div>
576
+ <div class="summary-card"><div class="value bonus">{{ "%.2f"|format(summary.total_bonuses|float) }}</div><div class="label">Всего бонусов</div></div>
577
+ <div class="summary-card"><div class="value debt">{{ "%.2f"|format(summary.total_debts|float) }}</div><div class="label">Всего долгов</div></div>
578
+ <div class="summary-card"><div class="value debt">{{ summary.users_with_debt }}</div><div class="label">Клиенты с долгом</div></div>
 
 
 
 
 
 
 
 
 
 
 
 
579
  </div>
 
580
  <div class="controls-bar">
581
  <input type="text" id="searchInput" onkeyup="searchUsers()" placeholder="Поиск по имени, ID, username, номеру...">
582
  <button class="btn btn-primary" onclick="openAddClientModal()">Добавить клиента</button>
583
  </div>
584
+ <div class="user-grid" id="userGrid">
585
+ {% for user in users|sort(attribute='visited_at', reverse=true) %}
586
+ <div class="user-card" data-user-id="{{ user.id }}" data-search-term="{{ user.first_name|lower }} {{ user.last_name|lower if user.last_name }} {{ user.username|lower if user.username }} {{ user.id }} {{ user.phone_number|lower if user.phone_number }}">
587
+ <div class="user-info">
588
+ <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">
589
+ <div class="user-details">
590
+ <div class="name">{{ user.first_name or '' }} {{ user.last_name or '' }}</div>
591
+ <div class="username">@{{ user.username if user.username else user.phone_number }} | ID: {{ user.id }}</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
592
  </div>
593
  </div>
594
+ <div class="user-balances">
595
+ <div><div class="label">Бонусы</div><div class="amount bonus">{{ "%.2f"|format(user.bonuses|float) }}</div></div>
596
+ <div><div class="label">Долг</div><div class="amount debt">{{ "%.2f"|format(user.debts|float if user.debts else 0) }}</div></div>
597
+ </div>
598
+ <div class="user-actions {% if user.telegram_id == None %}two-buttons{% endif %}">
599
+ <button class="btn-manage" onclick='openTransactionModal({{ user|tojson }})'>Управление счетом</button>
600
+ {% if user.telegram_id == None %}<button class="btn btn-delete" onclick='deleteClient("{{ user.id }}")'>Удалить</button>{% endif %}
601
+ </div>
602
+ </div>
603
+ {% else %}
604
+ <p class="no-users">Пользователей пока нет.</p>
605
+ {% endfor %}
606
+ </div>
607
  </div>
608
 
609
  <div id="transactionModal" class="modal">
610
+ <div class="modal-content large">
611
  <span class="modal-close" onclick="closeModal('transactionModal')">×</span>
612
  <div class="modal-header">
613
  <h2 id="modalUserName"></h2>
 
618
  <div class="form-section">
619
  <h3>Бонусы</h3>
620
  <div class="form-row">
621
+ <div class="form-group"><label for="purchaseAmount">Сумма покупки (для начисления)</label><input type="number" id="purchaseAmount" placeholder="1500" oninput="updateCalculations()"></div>
622
+ <div class="form-group"><label for="deductAmount">Списать бонусов</label><input type="number" id="deductAmount" placeholder="100" oninput="updateCalculations()"></div>
 
 
 
 
 
 
623
  </div>
624
  <div class="calculation-summary">
625
  <div class="summary-item"><span>Текущий баланс:</span> <strong id="summaryCurrentBalance">0.00</strong></div>
626
  <div class="summary-item"><span>Будет начислено (2%):</span> <strong id="summaryAccrual">+0.00</strong></div>
627
+ <div class="summary-item"><span>Будет списано:</span> <strong id="summaryDeduction">-0.00</strong></div><hr>
 
628
  <div class="summary-item"><strong>Итоговый баланс бонусов:</strong> <strong id="summaryFinalBalance">0.00</strong></div>
629
  </div>
630
  </div>
 
632
  <div class="form-section">
633
  <h3>Долги</h3>
634
  <div class="form-row">
635
+ <div class="form-group"><label for="addDebtAmount">Добавить долг</label><input type="number" id="addDebtAmount" placeholder="500" oninput="updateCalculations()"></div>
636
+ <div class="form-group"><label for="repayDebtAmount">Погасить долг</label><input type="number" id="repayDebtAmount" placeholder="200" oninput="updateCalculations()"></div>
 
 
 
 
 
 
637
  </div>
638
  <div class="calculation-summary">
639
  <div class="summary-item"><span>Текущий долг:</span> <strong id="summaryCurrentDebt">0.00</strong></div>
640
  <div class="summary-item"><span>Будет добавлено:</span> <strong id="summaryAddDebt">+0.00</strong></div>
641
+ <div class="summary-item"><span>Будет погашено:</span> <strong id="summaryRepayDebt">-0.00</strong></div><hr>
 
642
  <div class="summary-item"><strong>Итоговый долг:</strong> <strong id="summaryFinalDebt">0.00</strong></div>
643
  </div>
644
  </div>
645
+ <div class="modal-footer">
646
+ <div id="modalStatus" class="status-message"></div>
647
+ <button class="btn-submit" onclick="submitTransaction()">Провести операции с бонусами/долгами</button>
648
+ </div>
649
+ <hr style="margin: 2rem 0;">
650
+ <div class="form-section">
651
+ <h3>Накладные клиента</h3>
652
+ <div style="margin-bottom: 1rem;"><button class="btn btn-info" onclick="openNewInvoiceModal()">Создать новую накладную</button></div>
653
+ <div class="existing-invoices-container">
654
+ <h4>Существующие накладные (<span id="existingInvoicesCount">0</span>)</h4>
655
+ <ul id="existingInvoicesList" class="existing-invoices-list"></ul>
656
+ </div>
657
+ </div>
658
  <div class="history-container">
659
  <h3>Общая история операций</h3>
660
  <ul id="modalHistoryList" class="history-list"></ul>
661
  </div>
 
 
 
 
662
  </div>
663
  </div>
664
 
665
+ <div id="newInvoiceModal" class="modal">
666
+ <div class="modal-content large">
667
+ <span class="modal-close" onclick="closeModal('newInvoiceModal')">×</span>
668
+ <div class="modal-header"><h2>Новая накладная для <span id="newInvoiceModalUserName"></span></h2></div>
669
+ <div class="form-section">
670
+ <h3>Добавить товары</h3>
671
+ <div class="form-row four-col">
672
+ <div class="form-group"><label for="invoiceItemName">Название товара</label><input type="text" id="invoiceItemName"></div>
673
+ <div class="form-group"><label for="invoiceItemQty">Кол-во</label><input type="number" id="invoiceItemQty" value="1" min="1"></div>
674
+ <div class="form-group"><label for="invoiceItemPrice">Цена за ед.</label><input type="number" id="invoiceItemPrice" min="0" step="0.01"></div>
675
+ <div class="form-group" style="justify-content: flex-end;"><button onclick="addItemToInvoiceTable()">Добавить</button></div>
676
+ </div>
677
+ </div>
678
+ <div class="invoice-items-container">
679
+ <h3>Товары в накладной</h3>
680
+ <table id="currentInvoiceItemsTable" class="invoice-items-table">
681
+ <thead><tr><th>Название</th><th class="number">Кол-во</th><th class="number">Цена</th><th class="number">Сумма</th><th></th></tr></thead>
682
+ <tbody></tbody>
683
+ </table>
684
+ <div class="invoice-summary" style="text-align: right; margin-top:1rem;"><strong>Итого по накладной: <span id="currentInvoiceTotalAmount">0.00</span></strong></div>
685
+ </div>
686
+ <div class="modal-footer">
687
+ <div id="newInvoiceStatus" class="status-message"></div>
688
+ <button class="btn-submit" onclick="submitNewInvoice()">Сохранить накладную</button>
689
+ </div>
690
+ </div>
691
+ </div>
692
+
693
+ <div id="viewAdminInvoiceDetailModal" class="modal">
694
+ <div class="modal-content">
695
+ <span class="modal-close" onclick="closeModal('viewAdminInvoiceDetailModal')">×</span>
696
+ <div class="modal-header"><h2 id="viewAdminInvoiceModalTitle">Детали накладной</h2></div>
697
+ <div class="modal-body">
698
+ <table id="viewAdminInvoiceItemsTable" class="invoice-items-table">
699
+ <thead><tr><th>Товар</th><th class="number">Кол-во</th><th class="number">Цена</th><th class="number">Сумма</th></tr></thead>
700
+ <tbody></tbody>
701
+ <tfoot><tr class="grand-total-row"><td colspan="3"><strong>Итого:</strong></td><td id="viewAdminInvoiceGrandTotal" class="number"><strong></strong></td></tr></tfoot>
702
+ </table>
703
+ </div>
704
+ </div>
705
+ </div>
706
+
707
  <div id="addClientModal" class="modal">
708
  <div class="modal-content">
709
  <span class="modal-close" onclick="closeModal('addClientModal')">×</span>
710
+ <div class="modal-header"><h2>Добавить нового клиента</h2></div>
711
+ <div class="form-group" style="margin-bottom: 1rem;"><label for="newClientFirstName">Имя</label><input type="text" id="newClientFirstName" placeholder="Иван"></div>
712
+ <div class="form-group" style="margin-bottom: 1.5rem;"><label for="newClientPhone">Номер телефона (уникальный)</label><input type="tel" id="newClientPhone" placeholder="+79001234567"></div>
713
+ <div class="modal-footer"><div id="addClientStatus" class="status-message"></div><button class="btn-submit" onclick="submitNewClient()">Сохранить клиента</button></div>
 
 
 
 
 
 
 
 
 
 
 
714
  </div>
715
  </div>
716
 
717
  <script>
718
  const transactionModal = document.getElementById('transactionModal');
719
  const addClientModal = document.getElementById('addClientModal');
720
+ const newInvoiceModal = document.getElementById('newInvoiceModal');
721
+ const viewAdminInvoiceDetailModal = document.getElementById('viewAdminInvoiceDetailModal');
722
+
723
  let currentUserData = null;
724
+ let currentInvoiceItems = [];
725
 
726
  function searchUsers() {
727
  const searchTerm = document.getElementById('searchInput').value.toLowerCase();
728
  const userCards = document.querySelectorAll('.user-card');
729
  userCards.forEach(card => {
730
  const cardSearchTerm = card.getAttribute('data-search-term');
731
+ if (cardSearchTerm.includes(searchTerm)) card.style.display = 'flex';
732
+ else card.style.display = 'none';
 
 
 
733
  });
734
  }
735
 
 
738
  document.getElementById('modalUserId').value = userData.id;
739
  document.getElementById('modalUserName').textContent = `${userData.first_name || ''} ${userData.last_name || ''}`;
740
  document.getElementById('modalUserUsername').textContent = `@${userData.username || userData.phone_number} | ID: ${userData.id}`;
741
+ ['purchaseAmount', 'deductAmount', 'addDebtAmount', 'repayDebtAmount'].forEach(id => document.getElementById(id).value = '');
 
 
 
742
  document.getElementById('modalStatus').textContent = '';
743
+
744
+ populateModalHistoryAndInvoices(userData);
745
+ updateCalculations();
746
+ transactionModal.style.display = 'block';
747
+ }
748
+
749
+ function populateModalHistoryAndInvoices(userData) {
750
  const historyList = document.getElementById('modalHistoryList');
751
  historyList.innerHTML = '';
752
+ const existingInvoicesList = document.getElementById('existingInvoicesList');
753
+ existingInvoicesList.innerHTML = '';
754
+
755
+ const bonusHistory = (userData.history || []).map(h => ({...h, transaction_type: 'bonus', date_obj: new Date(h.date)}));
756
+ const debtHistory = (userData.debt_history || []).map(h => ({...h, transaction_type: 'debt', date_obj: new Date(h.date)}));
757
+ const invoices = (userData.invoices || []).map(inv => ({...inv, transaction_type: 'invoice', date_obj: new Date(inv.date)}));
758
 
759
+ const combinedHistory = [...bonusHistory, ...debtHistory, ...invoices].sort((a, b) => b.date_obj - a.date_obj);
 
 
760
 
761
  if (combinedHistory.length > 0) {
762
  combinedHistory.forEach(item => {
763
  const li = document.createElement('li');
764
  li.className = 'history-item';
765
+ let amountClass, amountText, descText = item.description;
766
  if (item.transaction_type === 'bonus') {
 
767
  amountClass = item.type === 'accrual' ? 'bonus-accrual' : 'bonus-deduction';
768
+ amountText = `${item.type === 'accrual' ? '+' : '-'}${parseFloat(item.amount).toFixed(2)}`;
769
+ } else if (item.transaction_type === 'debt') {
 
770
  amountClass = item.type === 'accrual' ? 'debt-accrual' : 'debt-payment';
771
+ amountText = `${item.type === 'accrual' ? '+' : '-'}${parseFloat(item.amount).toFixed(2)}`;
772
+ } else if (item.transaction_type === 'invoice') {
773
+ amountClass = 'invoice';
774
+ descText = `Накладная #${item.id.substring(0,8)}...`;
775
+ amountText = `${parseFloat(item.total_amount).toFixed(2)}`;
776
+ li.style.cursor = "pointer";
777
+ li.onclick = () => showAdminInvoiceDetails(item);
778
  }
779
+ li.innerHTML = `<div><div class="desc">${descText}</div><div class="date">${item.date_str}</div></div><div class="amount ${amountClass}">${amountText}</div>`;
 
 
 
 
 
 
780
  historyList.appendChild(li);
781
  });
782
  } else {
783
  historyList.innerHTML = '<li style="text-align:center; padding: 1rem; color: var(--admin-secondary);">Нет истории</li>';
784
  }
785
 
786
+ document.getElementById('existingInvoicesCount').textContent = invoices.length;
787
+ if (invoices.length > 0) {
788
+ invoices.sort((a,b) => b.date_obj - a.date_obj).forEach(inv => {
789
+ const invLi = document.createElement('li');
790
+ invLi.className = 'existing-invoice-item';
791
+ invLi.innerHTML = `
792
+ <div class="desc">Накладная #${inv.id.substring(0,8)}... (${inv.items.length} поз.)</div>
793
+ <div class="date">${inv.date_str}</div>
794
+ <div class="amount">${parseFloat(inv.total_amount).toFixed(2)}</div>
795
+ <div class="actions"><button class="btn btn-info btn-sm" onclick='showAdminInvoiceDetails(${JSON.stringify(inv)})'>Смотреть</button></div>
796
+ `;
797
+ existingInvoicesList.appendChild(invLi);
798
+ });
799
+ } else {
800
+ existingInvoicesList.innerHTML = '<li style="text-align:center; padding: 1rem; color: var(--admin-secondary);">Нет накладных</li>';
801
+ }
802
+ }
803
+
804
+ function openNewInvoiceModal() {
805
+ if (!currentUserData) return;
806
+ document.getElementById('newInvoiceModalUserName').textContent = `${currentUserData.first_name || ''} ${currentUserData.last_name || ''}`;
807
+ currentInvoiceItems = [];
808
+ renderCurrentInvoiceItems();
809
+ updateCurrentInvoiceTotal();
810
+ ['invoiceItemName', 'invoiceItemQty', 'invoiceItemPrice'].forEach(id => document.getElementById(id).value = (id === 'invoiceItemQty' ? '1' : ''));
811
+ document.getElementById('newInvoiceStatus').textContent = '';
812
+ newInvoiceModal.style.display = 'block';
813
+ }
814
+
815
+ function addItemToInvoiceTable() {
816
+ const name = document.getElementById('invoiceItemName').value.trim();
817
+ const quantity = parseInt(document.getElementById('invoiceItemQty').value);
818
+ const price_per_unit = parseFloat(document.getElementById('invoiceItemPrice').value);
819
+
820
+ if (!name || isNaN(quantity) || quantity <= 0 || isNaN(price_per_unit) || price_per_unit < 0) {
821
+ alert('Пожалуйста, введите корректные данные для товара.');
822
+ return;
823
+ }
824
+ currentInvoiceItems.push({ name, quantity, price_per_unit, item_total: quantity * price_per_unit });
825
+ renderCurrentInvoiceItems();
826
+ updateCurrentInvoiceTotal();
827
+ document.getElementById('invoiceItemName').value = '';
828
+ document.getElementById('invoiceItemQty').value = '1';
829
+ document.getElementById('invoiceItemPrice').value = '';
830
+ document.getElementById('invoiceItemName').focus();
831
+ }
832
+
833
+ function renderCurrentInvoiceItems() {
834
+ const tableBody = document.getElementById('currentInvoiceItemsTable').querySelector('tbody');
835
+ tableBody.innerHTML = '';
836
+ currentInvoiceItems.forEach((item, index) => {
837
+ const row = tableBody.insertRow();
838
+ row.insertCell().textContent = item.name;
839
+ row.insertCell().textContent = item.quantity;
840
+ row.insertCell().textContent = item.price_per_unit.toFixed(2);
841
+ row.insertCell().textContent = item.item_total.toFixed(2);
842
+ const actionCell = row.insertCell();
843
+ const removeBtn = document.createElement('button');
844
+ removeBtn.textContent = '×';
845
+ removeBtn.className = 'btn-delete btn-sm';
846
+ removeBtn.style.padding = '2px 6px';
847
+ removeBtn.onclick = () => removeInvoiceItem(index);
848
+ actionCell.appendChild(removeBtn);
849
+ ['.number', '.number', '.number'].forEach((sel, i) => row.cells[i+1].classList.add(sel.substring(1)));
850
+ });
851
+ }
852
+
853
+ function removeInvoiceItem(index) {
854
+ currentInvoiceItems.splice(index, 1);
855
+ renderCurrentInvoiceItems();
856
+ updateCurrentInvoiceTotal();
857
+ }
858
+
859
+ function updateCurrentInvoiceTotal() {
860
+ const total = currentInvoiceItems.reduce((sum, item) => sum + item.item_total, 0);
861
+ document.getElementById('currentInvoiceTotalAmount').textContent = total.toFixed(2);
862
+ }
863
+
864
+ async function submitNewInvoice() {
865
+ const statusEl = document.getElementById('newInvoiceStatus');
866
+ if (currentInvoiceItems.length === 0) {
867
+ statusEl.style.color = 'var(--admin-danger)';
868
+ statusEl.textContent = 'Добавьте хотя бы один товар в накладную.';
869
+ return;
870
+ }
871
+ statusEl.style.color = 'var(--admin-secondary)';
872
+ statusEl.textContent = 'Сохранение накладной...';
873
+
874
+ const payload = {
875
+ user_id: currentUserData.id,
876
+ items: currentInvoiceItems,
877
+ total_amount: currentInvoiceItems.reduce((sum, item) => sum + item.item_total, 0)
878
+ };
879
+
880
+ try {
881
+ const response = await fetch('/admin/add_invoice', {
882
+ method: 'POST',
883
+ headers: { 'Content-Type': 'application/json' },
884
+ body: JSON.stringify(payload)
885
+ });
886
+ const result = await response.json();
887
+ if (response.ok) {
888
+ statusEl.style.color = 'var(--admin-success)';
889
+ statusEl.textContent = 'Накладная успешно сохранена!';
890
+ setTimeout(() => {
891
+ closeModal('newInvoiceModal');
892
+ refreshUserDataAndModal();
893
+ }, 1500);
894
+ } else { throw new Error(result.message || 'Ошибка сохранения'); }
895
+ } catch (error) {
896
+ statusEl.style.color = 'var(--admin-danger)';
897
+ statusEl.textContent = `Ошибка: ${error.message}`;
898
+ }
899
+ }
900
+
901
+ function showAdminInvoiceDetails(invoiceData) {
902
+ document.getElementById('viewAdminInvoiceModalTitle').textContent = `Детали накладной #${invoiceData.id.substring(0,8)}...`;
903
+ const tableBody = viewAdminInvoiceDetailModal.querySelector('#viewAdminInvoiceItemsTable tbody');
904
+ tableBody.innerHTML = '';
905
+ let grandTotal = 0;
906
+ invoiceData.items.forEach(item => {
907
+ const row = tableBody.insertRow();
908
+ row.insertCell().textContent = item.name;
909
+ row.insertCell().textContent = item.quantity;
910
+ row.insertCell().textContent = parseFloat(item.price_per_unit).toFixed(2);
911
+ row.insertCell().textContent = parseFloat(item.item_total).toFixed(2);
912
+ grandTotal += parseFloat(item.item_total);
913
+ ['.number', '.number', '.number'].forEach((sel, i) => row.cells[i+1].classList.add(sel.substring(1)));
914
+ });
915
+ document.getElementById('viewAdminInvoiceGrandTotal').textContent = grandTotal.toFixed(2);
916
+ viewAdminInvoiceDetailModal.style.display = 'block';
917
  }
918
 
919
  function openAddClientModal() {
 
925
 
926
  function closeModal(modalId) {
927
  document.getElementById(modalId).style.display = 'none';
928
+ if (modalId === 'transactionModal') currentUserData = null;
929
+ if (modalId === 'newInvoiceModal') currentInvoiceItems = [];
 
930
  }
931
 
932
  function updateCalculations() {
933
  if (!currentUserData) return;
 
934
  const currentBalance = parseFloat(currentUserData.bonuses) || 0;
935
  const purchaseAmount = parseFloat(document.getElementById('purchaseAmount').value) || 0;
936
  const deductAmount = parseFloat(document.getElementById('deductAmount').value) || 0;
937
  const accrualAmount = purchaseAmount * 0.02;
938
+ let finalDeductAmount = Math.max(0, deductAmount);
939
+ if (finalDeductAmount > currentBalance + accrualAmount) { // Allow deduction from newly accrued as well
940
+ finalDeductAmount = currentBalance + accrualAmount; // Cap at total available after accrual
941
+ document.getElementById('deductAmount').value = finalDeductAmount > 0 ? finalDeductAmount.toFixed(2) : '';
942
+ } else if (finalDeductAmount > currentBalance && purchaseAmount === 0){ // Cap at current if no accrual
943
  finalDeductAmount = currentBalance;
944
  document.getElementById('deductAmount').value = finalDeductAmount > 0 ? finalDeductAmount.toFixed(2) : '';
945
  }
946
+
947
  const finalBalance = currentBalance + accrualAmount - finalDeductAmount;
948
  document.getElementById('summaryCurrentBalance').textContent = currentBalance.toFixed(2);
949
  document.getElementById('summaryAccrual').textContent = `+${accrualAmount.toFixed(2)}`;
 
953
  const currentDebt = parseFloat(currentUserData.debts) || 0;
954
  const addDebtAmount = parseFloat(document.getElementById('addDebtAmount').value) || 0;
955
  const repayDebtAmount = parseFloat(document.getElementById('repayDebtAmount').value) || 0;
956
+ let finalRepayAmount = Math.max(0, repayDebtAmount);
957
+
958
+ if (finalRepayAmount > currentDebt + addDebtAmount) { // If repaying more than current debt + new debt (e.g. error or covering future)
959
+ finalRepayAmount = currentDebt + addDebtAmount; // Cap at total debt after adding new
960
+ document.getElementById('repayDebtAmount').value = finalRepayAmount > 0 ? finalRepayAmount.toFixed(2) : '';
961
+ } else if (finalRepayAmount > currentDebt && addDebtAmount === 0) { // If only repaying and more than current debt
962
  finalRepayAmount = currentDebt;
963
  document.getElementById('repayDebtAmount').value = finalRepayAmount > 0 ? finalRepayAmount.toFixed(2) : '';
964
  }
965
+
966
+
967
  const finalDebt = currentDebt + addDebtAmount - finalRepayAmount;
968
  document.getElementById('summaryCurrentDebt').textContent = currentDebt.toFixed(2);
969
  document.getElementById('summaryAddDebt').textContent = `+${addDebtAmount.toFixed(2)}`;
 
986
 
987
  if (payload.purchase_amount <= 0 && payload.deduct_amount <= 0 && payload.add_debt_amount <= 0 && payload.repay_debt_amount <= 0) {
988
  statusEl.style.color = 'var(--admin-danger)';
989
+ statusEl.textContent = 'Введите сумму для одной из операций с бонусами/долгами.';
990
  return;
991
  }
992
  try {
 
999
  if (response.ok) {
1000
  statusEl.style.color = 'var(--admin-success)';
1001
  statusEl.textContent = 'Операция успешно проведена!';
1002
+ setTimeout(() => { refreshUserDataAndModal(); statusEl.textContent = '';}, 1500);
1003
+ } else { throw new Error(result.message || 'Произошла ошибка'); }
 
 
1004
  } catch (error) {
1005
  statusEl.style.color = 'var(--admin-danger)';
1006
  statusEl.textContent = `Ошибка: ${error.message}`;
 
1011
  const statusEl = document.getElementById('addClientStatus');
1012
  statusEl.style.color = 'var(--admin-secondary)';
1013
  statusEl.textContent = 'Сохранение...';
 
1014
  const payload = {
1015
  first_name: document.getElementById('newClientFirstName').value.trim(),
1016
  phone_number: document.getElementById('newClientPhone').value.trim(),
1017
  };
 
1018
  if (!payload.first_name || !payload.phone_number) {
1019
  statusEl.style.color = 'var(--admin-danger)';
1020
  statusEl.textContent = 'Имя и номер телефона обязательны.';
1021
  return;
1022
  }
 
1023
  try {
1024
  const response = await fetch('/admin/add_client', {
1025
  method: 'POST',
 
1031
  statusEl.style.color = 'var(--admin-success)';
1032
  statusEl.textContent = 'Клиент успешно добавлен!';
1033
  setTimeout(() => { location.reload(); }, 1500);
1034
+ } else { throw new Error(result.message || 'Произошла ошибка'); }
 
 
1035
  } catch (error) {
1036
  statusEl.style.color = 'var(--admin-danger)';
1037
  statusEl.textContent = `Ошибка: ${error.message}`;
 
1039
  }
1040
 
1041
  async function deleteClient(userId) {
1042
+ if (!confirm(`Вы уверены, что хотите удалить клиента с ID ${userId}? Это действие необратимо.`)) return;
 
 
1043
  try {
1044
  const response = await fetch('/admin/delete_client', {
1045
  method: 'POST',
 
1047
  body: JSON.stringify({ user_id: userId })
1048
  });
1049
  const result = await response.json();
1050
+ if (response.ok) location.reload();
1051
+ else throw new Error(result.message || 'Не удалось удалить клиента.');
1052
+ } catch (error) { alert(`Ошибка: ${error.message}`); }
1053
+ }
1054
+
1055
+ async function refreshUserDataAndModal() {
1056
+ if (!currentUserData || !currentUserData.id) return;
1057
+ try {
1058
+ const response = await fetch(`/admin/get_user_data/${currentUserData.id}`);
1059
+ if (!response.ok) throw new Error('Failed to fetch updated user data');
1060
+ const updatedUserData = await response.json();
1061
+
1062
+ const userCard = document.querySelector(`.user-card[data-user-id="${currentUserData.id}"]`);
1063
+ if(userCard) {
1064
+ userCard.querySelector('.user-balances .amount.bonus').textContent = parseFloat(updatedUserData.bonuses).toFixed(2);
1065
+ userCard.querySelector('.user-balances .amount.debt').textContent = parseFloat(updatedUserData.debts).toFixed(2);
1066
+ userCard.setAttribute('data-search-term', `${updatedUserData.first_name||''} ${updatedUserData.last_name||''} ${updatedUserData.username||''} ${updatedUserData.id} ${updatedUserData.phone_number||''}`.toLowerCase());
1067
  }
1068
+
1069
+ currentUserData = updatedUserData;
1070
+ populateModalHistoryAndInvoices(updatedUserData);
1071
+ updateCalculations();
1072
+
1073
  } catch (error) {
1074
+ console.error("Error refreshing user data:", error);
1075
+ document.getElementById('modalStatus').textContent = 'Ошибка обновления данных. Перезагрузите.';
1076
  }
1077
  }
1078
 
1079
+
1080
  window.onclick = function(event) {
1081
+ if (event.target == transactionModal) closeModal('transactionModal');
1082
+ if (event.target == addClientModal) closeModal('addClientModal');
1083
+ if (event.target == newInvoiceModal) closeModal('newInvoiceModal');
1084
+ if (event.target == viewAdminInvoiceDetailModal) closeModal('viewAdminInvoiceDetailModal');
 
 
1085
  }
1086
  </script>
1087
  </body>
 
1091
  @app.route('/')
1092
  def index():
1093
  user_id_str = request.args.get('user_id_for_test')
 
1094
  current_data = load_visitor_data()
1095
  user_data = {}
1096
 
1097
  if user_id_str and user_id_str in current_data:
1098
+ user_data = current_data[user_id_str].copy()
1099
  user_data['id'] = user_id_str
1100
 
1101
+ combined_history = []
1102
  bonus_history = user_data.get('history', [])
1103
  for item in bonus_history:
1104
+ item_copy = item.copy()
1105
+ item_copy['transaction_type'] = 'bonus'
1106
+ combined_history.append(item_copy)
1107
 
1108
  debt_history = user_data.get('debt_history', [])
1109
  for item in debt_history:
1110
+ item_copy = item.copy()
1111
+ item_copy['transaction_type'] = 'debt'
1112
+ combined_history.append(item_copy)
1113
+
1114
+ invoices = user_data.get('invoices', [])
1115
+ for inv in invoices:
1116
+ inv_copy = inv.copy()
1117
+ inv_copy['transaction_type'] = 'invoice'
1118
+ inv_copy['description'] = f"Покупка по накладной #{inv_copy['id'][:8]}..."
1119
+ combined_history.append(inv_copy)
1120
+
1121
+ combined_history.sort(key=lambda x: x['date'], reverse=True)
1122
  user_data['combined_history'] = combined_history
1123
  else:
1124
  user_data = {
1125
+ "id": "N/A", "bonuses": 0, "debts": 0,
1126
+ "history": [], "debt_history": [], "invoices": [],
 
 
 
1127
  "combined_history": []
1128
  }
 
1129
  return render_template_string(TEMPLATE, user=user_data)
1130
 
1131
  @app.route('/verify', methods=['POST'])
 
1137
  return jsonify({"status": "error", "message": "Missing initData"}), 400
1138
 
1139
  user_data_parsed, is_valid = verify_telegram_data(init_data_str)
 
1140
  user_info_dict = {}
1141
  if user_data_parsed and 'user' in user_data_parsed:
1142
  try:
 
1149
  if is_valid:
1150
  tg_user_id = user_info_dict.get('id')
1151
  if tg_user_id:
1152
+ now_dt = datetime.now(BISHKEK_TZ)
1153
  all_data = load_visitor_data()
1154
 
1155
  existing_user_key = None
 
1158
  existing_user_key = key
1159
  break
1160
 
1161
+ user_id_to_save = existing_user_key
1162
  if existing_user_key:
1163
  user_entry = all_data[existing_user_key]
1164
  user_entry.update({
 
1167
  'username': user_info_dict.get('username'),
1168
  'photo_url': user_info_dict.get('photo_url'),
1169
  'language_code': user_info_dict.get('language_code'),
1170
+ 'visited_at': now_dt.timestamp(),
1171
+ 'visited_at_str': now_dt.strftime('%Y-%m-%d %H:%M:%S')
1172
  })
 
1173
  else:
1174
  new_user_id = generate_unique_id(all_data)
1175
+ user_id_to_save = new_user_id
1176
  user_entry = {
1177
+ 'id': new_user_id, 'telegram_id': tg_user_id,
1178
+ 'first_name': user_info_dict.get('first_name'), 'last_name': user_info_dict.get('last_name'),
1179
+ 'username': user_info_dict.get('username'), 'photo_url': user_info_dict.get('photo_url'),
1180
+ 'language_code': user_info_dict.get('language_code'), 'is_premium': user_info_dict.get('is_premium', False),
1181
+ 'phone_number': None, 'visited_at': now_dt.timestamp(), 'visited_at_str': now_dt.strftime('%Y-%m-%d %H:%M:%S'),
1182
+ 'bonuses': 0, 'history': [], 'debts': 0, 'debt_history': [], 'invoices': []
 
 
 
 
 
 
 
 
 
1183
  }
 
 
1184
  save_visitor_data({user_id_to_save: user_entry})
 
1185
  return jsonify({"status": "ok", "verified": True, "user_id": user_id_to_save})
1186
  else:
1187
  return jsonify({"status": "error", "verified": True, "message": "User ID not found in parsed data"}), 400
1188
  else:
1189
  logging.warning(f"Verification failed for user: {user_info_dict.get('id')}")
1190
  return jsonify({"status": "error", "verified": False, "message": "Invalid data"}), 403
 
1191
  except Exception as e:
1192
  logging.exception("Error in /verify endpoint")
1193
  return jsonify({"status": "error", "message": "Internal server error"}), 500
 
1197
  current_data = load_visitor_data()
1198
  users_list = []
1199
  for user_id, user_data in current_data.items():
1200
+ user_data_copy = user_data.copy()
1201
+ user_data_copy['id'] = user_id
1202
+ users_list.append(user_data_copy)
1203
 
1204
  total_users = len(users_list)
1205
  total_bonuses = sum(u.get('bonuses', 0) for u in users_list)
 
1207
  users_with_debt = sum(1 for u in users_list if u.get('debts', 0) > 0)
1208
 
1209
  summary_stats = {
1210
+ "total_users": total_users, "total_bonuses": total_bonuses,
1211
+ "total_debts": total_debts, "users_with_debt": users_with_debt
 
 
1212
  }
 
1213
  return render_template_string(ADMIN_TEMPLATE, users=users_list, summary=summary_stats)
1214
 
1215
+ @app.route('/admin/get_user_data/<user_id>', methods=['GET'])
1216
+ def get_user_data(user_id):
1217
+ all_data = load_visitor_data()
1218
+ if user_id in all_data:
1219
+ user_data = all_data[user_id].copy()
1220
+ user_data['id'] = user_id # Ensure id is part of the returned data
1221
+ return jsonify(user_data), 200
1222
+ return jsonify({"status": "error", "message": "User not found"}), 404
1223
+
1224
+
1225
  @app.route('/admin/add_client', methods=['POST'])
1226
  def add_client():
1227
  try:
 
1233
  return jsonify({"status": "error", "message": "Имя и номер телефона обязательны."}), 400
1234
 
1235
  all_data = load_visitor_data()
 
1236
  for user in all_data.values():
1237
  if user.get('phone_number') == phone_number:
1238
  return jsonify({"status": "error", "message": "Клиент с таким номером телефона уже существует."}), 409
1239
 
1240
+ now_dt = datetime.now(BISHKEK_TZ)
1241
  new_id = generate_unique_id(all_data)
1242
 
1243
  new_client = {
1244
+ 'id': new_id, 'telegram_id': None, 'first_name': first_name, 'last_name': None, 'username': None,
1245
+ 'photo_url': None, 'language_code': 'ru', 'is_premium': False, 'phone_number': phone_number,
1246
+ 'visited_at': now_dt.timestamp(), 'visited_at_str': now_dt.strftime('%Y-%m-%d %H:%M:%S'),
1247
+ 'bonuses': 0, 'history': [], 'debts': 0, 'debt_history': [], 'invoices': []
 
 
 
 
 
 
 
 
 
 
 
1248
  }
 
1249
  save_visitor_data({new_id: new_client})
 
1250
  return jsonify({"status": "ok", "message": "Client added successfully"}), 201
 
1251
  except Exception as e:
1252
  logging.exception("Error in /admin/add_client endpoint")
1253
  return jsonify({"status": "error", "message": str(e)}), 500
1254
 
 
1255
  @app.route('/admin/add_transaction', methods=['POST'])
1256
  def add_transaction():
1257
  try:
 
1272
  return jsonify({"status": "error", "message": "User not found"}), 404
1273
 
1274
  user = all_data[user_id_str]
1275
+ now_dt = datetime.now(BISHKEK_TZ)
1276
+ now_iso = now_dt.isoformat()
1277
+ now_str = now_dt.strftime('%Y-%m-%d %H:%M:%S')
 
 
 
1278
 
1279
+ current_bonuses = user.get('bonuses', 0)
 
 
 
1280
  accrual_amount = purchase_amount * 0.02
1281
+
1282
+ # Ensure deduct_amount is not more than available after potential accrual
1283
+ if deduct_amount > current_bonuses + accrual_amount:
1284
+ return jsonify({"status": "error", "message": f"Недостаточно бонусов для списания. Доступно: {(current_bonuses + accrual_amount):.2f}"}), 400
1285
+
1286
+ if repay_debt_amount > user.get('debts', 0) + add_debt_amount:
1287
+ return jsonify({"status": "error", "message": f"Сумма погашения превышает текущий долг. Доступно к погашению: {(user.get('debts', 0) + add_debt_amount):.2f}"}), 400
1288
 
1289
+ user['bonuses'] = current_bonuses + accrual_amount - deduct_amount
1290
+ if 'history' not in user or not isinstance(user['history'], list): user['history'] = []
1291
  if accrual_amount > 0:
1292
+ user['history'].append({"type": "accrual", "amount": accrual_amount, "description": f"Начисление с покупки {purchase_amount}", "date": now_iso, "date_str": now_str})
 
 
 
 
1293
  if deduct_amount > 0:
1294
+ user['history'].append({"type": "deduction", "amount": deduct_amount, "description": "Списание бонусов", "date": now_iso, "date_str": now_str})
 
 
 
 
1295
 
 
1296
  user['debts'] = user.get('debts', 0) + add_debt_amount - repay_debt_amount
1297
+ if 'debt_history' not in user or not isinstance(user['debt_history'], list): user['debt_history'] = []
 
 
1298
  if add_debt_amount > 0:
1299
+ user['debt_history'].append({"type": "accrual", "amount": add_debt_amount, "description": "Добавление долга", "date": now_iso, "date_str": now_str})
 
 
 
 
1300
  if repay_debt_amount > 0:
1301
+ user['debt_history'].append({"type": "payment", "amount": repay_debt_amount, "description": "Погашение долга", "date": now_iso, "date_str": now_str})
 
 
 
 
1302
 
1303
  save_visitor_data({user_id_str: user})
1304
+ return jsonify({"status": "ok", "message": "Transaction successful", "new_balance": user['bonuses'], "new_debt": user['debts']}), 200
1305
+ except Exception as e:
1306
+ logging.exception("Error in /admin/add_transaction endpoint")
1307
+ return jsonify({"status": "error", "message": str(e)}), 500
1308
 
1309
+ @app.route('/admin/add_invoice', methods=['POST'])
1310
+ def add_invoice():
1311
+ try:
1312
+ data = request.get_json()
1313
+ user_id = data.get('user_id')
1314
+ items = data.get('items')
1315
+ total_amount = float(data.get('total_amount', 0))
1316
+
1317
+ if not user_id or not items or not isinstance(items, list) or len(items) == 0:
1318
+ return jsonify({"status": "error", "message": "Некорректные данные для накладной."}), 400
1319
 
1320
+ user_id_str = str(user_id)
1321
+ all_data = load_visitor_data()
1322
+ if user_id_str not in all_data:
1323
+ return jsonify({"status": "error", "message": "Клиент не найден."}), 404
1324
+
1325
+ user = all_data[user_id_str]
1326
+ now_dt = datetime.now(BISHKEK_TZ)
1327
+
1328
+ invoice_id = generate_invoice_id()
1329
+ new_invoice = {
1330
+ "id": invoice_id,
1331
+ "date": now_dt.isoformat(),
1332
+ "date_str": now_dt.strftime('%Y-%m-%d %H:%M:%S'),
1333
+ "items": items,
1334
+ "total_amount": total_amount
1335
+ }
1336
+
1337
+ if 'invoices' not in user or not isinstance(user['invoices'], list):
1338
+ user['invoices'] = []
1339
+ user['invoices'].append(new_invoice)
1340
+
1341
+ save_visitor_data({user_id_str: user})
1342
+ return jsonify({"status": "ok", "message": "Накладная успешно добавлена.", "invoice_id": invoice_id}), 201
1343
  except Exception as e:
1344
+ logging.exception("Error in /admin/add_invoice endpoint")
1345
  return jsonify({"status": "error", "message": str(e)}), 500
1346
 
1347
  @app.route('/admin/delete_client', methods=['POST'])
 
1349
  try:
1350
  data = request.get_json()
1351
  user_id = data.get('user_id')
1352
+ if not user_id: return jsonify({"status": "error", "message": "User ID is required"}), 400
 
 
 
1353
  user_id_str = str(user_id)
1354
  load_visitor_data()
 
1355
  with _data_lock:
1356
  if user_id_str not in visitor_data_cache:
1357
  return jsonify({"status": "error", "message": "User not found"}), 404
 
1358
  user_to_delete = visitor_data_cache[user_id_str]
1359
  if user_to_delete.get('telegram_id') is not None:
1360
  return jsonify({"status": "error", "message": "Cannot delete a Telegram-linked user"}), 403
 
1361
  del visitor_data_cache[user_id_str]
 
1362
  try:
1363
  with open(DATA_FILE, 'w', encoding='utf-8') as f:
1364
  json.dump(visitor_data_cache, f, ensure_ascii=False, indent=4)
 
1367
  except Exception as e:
1368
  logging.error(f"Error saving data after deletion: {e}")
1369
  return jsonify({"status": "error", "message": "Failed to save data after deletion"}), 500
 
1370
  return jsonify({"status": "ok", "message": "Client deleted successfully"}), 200
 
1371
  except Exception as e:
1372
  logging.exception("Error in /admin/delete_client endpoint")
1373
  return jsonify({"status": "error", "message": str(e)}), 500
 
1380
  else:
1381
  print("Attempting initial data download from Hugging Face...")
1382
  download_data_from_hf()
 
1383
  load_visitor_data()
 
1384
  print("WARNING: The /admin route is NOT protected. Implement proper authentication for production.")
 
1385
  if HF_TOKEN_WRITE:
1386
  backup_thread = threading.Thread(target=periodic_backup, daemon=True)
1387
  backup_thread.start()
1388
  print("Periodic backup thread started (every hour).")
 
1389
  print("--- Server Ready ---")
1390
  app.run(host=HOST, port=PORT, debug=False)