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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +292 -344
app.py CHANGED
@@ -1,4 +1,3 @@
1
- #!/usr/bin/env python3
2
 
3
  import os
4
  from flask import Flask, request, Response, render_template_string, jsonify, redirect, url_for
@@ -11,8 +10,9 @@ from datetime import datetime
11
  import logging
12
  import threading
13
  import random
14
- import pytz # Import pytz for timezone handling
15
- import uuid # For generating unique invoice IDs
 
16
 
17
  from huggingface_hub import HfApi, hf_hub_download
18
  from huggingface_hub.utils import RepositoryNotFoundError
@@ -27,7 +27,6 @@ HF_DATA_FILE_PATH = "data.json"
27
  HF_TOKEN_WRITE = os.getenv("HF_TOKEN_WRITE")
28
  HF_TOKEN_READ = os.getenv("HF_TOKEN_READ")
29
 
30
- # Define Bishkek timezone
31
  BISHKEK_TZ = pytz.timezone('Asia/Bishkek')
32
 
33
  app = Flask(__name__)
@@ -35,15 +34,27 @@ logging.basicConfig(level=logging.INFO)
35
  app.secret_key = os.urandom(24)
36
 
37
  _data_lock = threading.Lock()
38
- visitor_data_cache = {} # This will store all data, including organization details
39
 
40
  def generate_unique_id(all_data):
41
  while True:
42
- # Check against both client IDs and invoice IDs
43
  new_id = str(random.randint(10000, 99999))
44
  if new_id not in all_data:
45
  return new_id
46
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  def download_data_from_hf():
48
  global visitor_data_cache
49
  if not HF_TOKEN_READ:
@@ -80,14 +91,14 @@ def download_data_from_hf():
80
  def load_visitor_data():
81
  global visitor_data_cache
82
  with _data_lock:
83
- if not visitor_data_cache: # Only load from file if cache is empty
84
  try:
85
  with open(DATA_FILE, 'r', encoding='utf-8') as f:
86
  visitor_data_cache = json.load(f)
87
  logging.info("Visitor data loaded from local JSON.")
88
  except FileNotFoundError:
89
  logging.warning(f"{DATA_FILE} not found locally. Starting with empty data.")
90
- visitor_data_cache = {"organization_details": {}} # Initialize with empty org details
91
  except json.JSONDecodeError:
92
  logging.error(f"Error decoding {DATA_FILE}. Starting with empty data.")
93
  visitor_data_cache = {"organization_details": {}}
@@ -95,7 +106,6 @@ def load_visitor_data():
95
  logging.error(f"Unexpected error loading visitor data: {e}")
96
  visitor_data_cache = {"organization_details": {}}
97
 
98
- # Ensure organization_details key exists
99
  if "organization_details" not in visitor_data_cache:
100
  visitor_data_cache["organization_details"] = {}
101
 
@@ -104,29 +114,6 @@ def load_visitor_data():
104
  def save_visitor_data(data):
105
  with _data_lock:
106
  try:
107
- # When `data` is a dictionary, update it directly.
108
- # If `data` is a partial update for `visitor_data_cache`, merge it.
109
- # For simplicity, this function now assumes `data` is the complete `visitor_data_cache`
110
- # or a mergeable dictionary that should be applied to the cache before saving.
111
- # Given current usage, it's typically `save_visitor_data({user_id: user_entry})`
112
- # or `save_visitor_data({"organization_details": new_org_details})` etc.
113
- # It should ideally update the global `visitor_data_cache` and then dump it.
114
-
115
- # This line needs to be careful: if `data` is a single user, it overwrites.
116
- # It's better to update specific parts of the cache or always pass the full cache.
117
- # Let's adjust existing call sites to pass the full `all_data` after modification.
118
- # For now, let's assume `data` is what needs to be *merged* into `visitor_data_cache`
119
- # or `data` IS the new `visitor_data_cache`.
120
-
121
- # A more robust approach for `save_visitor_data` would be:
122
- # 1. Take a user_id and user_data to update a specific user
123
- # 2. Take an org_details dict to update org details
124
- # 3. Then, always dump the *entire* `visitor_data_cache`.
125
-
126
- # Simpler change for existing code:
127
- # Ensure `visitor_data_cache` is directly modified by operations,
128
- # and `save_visitor_data` just dumps the current `visitor_data_cache`.
129
-
130
  with open(DATA_FILE, 'w', encoding='utf-8') as f:
131
  json.dump(visitor_data_cache, f, ensure_ascii=False, indent=4)
132
  logging.info(f"Visitor data successfully saved to {DATA_FILE}.")
@@ -255,7 +242,7 @@ TEMPLATE = """
255
  .header {
256
  text-align: left;
257
  padding: var(--padding-m) 0;
258
- margin-bottom: 0; /* Adjusted for nav buttons */
259
  }
260
  .logo {
261
  font-size: 2.5em;
@@ -296,12 +283,12 @@ TEMPLATE = """
296
  box-shadow: 0 2px 10px rgba(255,193,7,0.3);
297
  }
298
  .content-section {
299
- display: none; /* Hidden by default */
300
  flex-direction: column;
301
  gap: var(--padding-m);
302
  }
303
  .content-section.active {
304
- display: flex; /* Shown when active */
305
  }
306
  .card-grid {
307
  display: grid;
@@ -406,8 +393,6 @@ TEMPLATE = """
406
  color: var(--text-secondary-color);
407
  padding: 2rem 0;
408
  }
409
-
410
- /* Business Card Styles */
411
  .business-card-item {
412
  margin-bottom: 10px;
413
  }
@@ -424,7 +409,7 @@ TEMPLATE = """
424
  .business-card-value a {
425
  color: var(--brand-yellow);
426
  text-decoration: none;
427
- word-break: break-all; /* For long URLs */
428
  }
429
  .business-card-value a:hover {
430
  text-decoration: underline;
@@ -455,8 +440,6 @@ TEMPLATE = """
455
  height: 20px;
456
  width: 20px;
457
  }
458
-
459
- /* Invoice Detail Modal */
460
  .modal {
461
  display: none;
462
  position: fixed;
@@ -577,6 +560,25 @@ TEMPLATE = """
577
  <p class="client-id-label">Ваш ID клиента</p>
578
  <p class="client-id-value">{{ user.id }}</p>
579
  </section>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
580
 
581
  <section class="history-section">
582
  <h2 class="history-title">История операций</h2>
@@ -661,7 +663,7 @@ TEMPLATE = """
661
  <div class="business-card-value">
662
  {% if org_details.whatsapp_link %}
663
  <a href="{{ org_details.whatsapp_link }}" target="_blank">
664
- <img src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgZmlsbD0iI2ZmZiIgY2xhc3M9ImJpIGJpLXdoYXRzYXBwIiB2aWV3Qm94PSIwIDAgMTYgMTYiPjxwYXRoIGQ9Ik0xMy42NzYgMS40NTJBMTAuNTE1IDEwLjUxNSAwIDAgMCA3Ljg4MyAwQzMuNTEgMCAuMDYzIDMuNDQuMDYzIDcuNjU0YzAgMS40MS4zNDcgMi44NjIgLjk2NiA0LjExOC4wNC4wNy4wNjYuMTc2LjA3My4yOTlsLjA2NSAxLjg5MmExLjIzMyAxLjIzMyAwIDAgMCAxLjU1MyAxLjQ1NmwxLjg4Ljc4Yy4xMzYuMDU2LjI3Ni4wOTYuNDI4LjE3MiAxLjI1Ni42NjggMi42MTQuOTgyIDQuMDQ4Ljk4MiA0LjM3MiAwIDcuOTEtMy40NCA3LjkxLTcuNjU0IDAtMi4wNDItLjg1LTMuOTY1LTIuMzQxLTUuMzUzWm0tMi42NzYgOS4yMzFhLjg4MS44ODEgMCAwIDEtMS4yMjUuMDM2bC0uNzk0LS40ODMtLjQ2NC4zNjYtLjY1MS0uNDktMS4xNjYgMS4xNjYtLjUzLS4zMTctLjA4NC41NTUtLjEwMi40NTktLjI1MS4xNjItLjU3LjU3LS41NTUuMDgyLS4zMjIuMDY5LS42Mi0uMTc3LS45ODQtLjE5LS45MDgtLjYxNy0uNDc4LS45MDktLjY3Ny0uNzUxLS40MzQtMS4xNzgtLjMyMi0xLjQ3MS0uMzIyLS42MDcgMC0uODguMDQyLS45NjYuMDg4LS40OTcuMjU3LS42MTEuNzMyLS42MTEgMS4xMzdhMS42MzkgMS42MzkgMCAwIDAgLjQ4NiAxLjY5Yy40MTYuNDE2Ljc2Mi43MjQuNzYyLjk1NSAwIC41NDIuMjgyIDEuMDQ2LjMwNCAxLjQxMS41ODYgMS4wNTEgMS43NzUgMS44NDcgMy4wNCAyLjAxOCAxLjMxMi4xNzcgMi4xMDYuMDk1IDIuNzYzLjA3Mi4wOTYtLjE1LjM3Mi0uMjg1LjcwMi0uNDgzLjMxLS40OC41MDktLjUxNS42NjktLjQ1Mi4xMDkuMDUxLjQxNy4yMTEuNDYzLjI2Ny4xNDEuMDgyLjI4NC4xNjEuNDQzLjIyNWExLjIyNyAxLjIyNyAwIDAgMCAuNzQ2LjAyNGwuMjg0LS4xMzVhMy45NjcgMy45NjcgMCAwIDAgLjY2Mi0uNjQzLjkwOC45MDggMCAwIDAgLjMwMi0uNjc4LjE5OC4xOTggMCAwIDAgMC0uMTU2LjgxNS44MTUgMCAwIDAgMC0uNDc3eiIvPjwvc3ZnPg==">
665
  {{ org_details.whatsapp_link }}
666
  </a>
667
  {% else %}
@@ -689,13 +691,11 @@ TEMPLATE = """
689
  </div>
690
  </div>
691
 
692
- <!-- Invoice Detail Modal -->
693
  <div id="invoiceDetailModal" class="modal">
694
  <div class="modal-content">
695
  <span class="modal-close" onclick="closeModal('invoiceDetailModal')">×</span>
696
  <h2 id="invoiceDetailTitle" class="modal-title"></h2>
697
  <ul id="invoiceDetailList" class="invoice-detail-list">
698
- <!-- Invoice items will be loaded here -->
699
  </ul>
700
  <div id="invoiceDetailTotal" class="invoice-total-display">
701
  <span>Итого:</span>
@@ -807,14 +807,59 @@ TEMPLATE = """
807
  openModal('invoiceDetailModal');
808
  }
809
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
810
  document.addEventListener('DOMContentLoaded', () => {
811
  document.querySelectorAll('.nav-btn').forEach(button => {
812
  button.addEventListener('click', () => {
813
  showSection(button.dataset.target);
814
  });
815
  });
816
-
817
- // Initial section display
818
  showSection('dashboard-section');
819
  });
820
 
@@ -920,122 +965,32 @@ ADMIN_TEMPLATE = """
920
  .modal-footer button { padding: 12px 25px; font-size: 1.1em; border-radius: 8px; border: none; font-weight: 600; cursor: pointer; }
921
  .btn-submit { background-color: var(--admin-success); color: white; }
922
  .status-message { text-align: center; font-weight: 500; flex-grow: 1; text-align: left; }
923
-
924
- /* Tabs for Transaction Modal */
925
- .tab-buttons {
926
- display: flex;
927
- margin-bottom: 1rem;
928
- border-bottom: 1px solid var(--admin-border);
929
- }
930
- .tab-btn {
931
- padding: 10px 15px;
932
- border: none;
933
- background-color: transparent;
934
- color: var(--admin-secondary);
935
- font-weight: 600;
936
- cursor: pointer;
937
- border-bottom: 3px solid transparent;
938
- transition: all 0.2s ease;
939
- }
940
- .tab-btn.active {
941
- color: var(--admin-primary-dark);
942
- border-bottom-color: var(--admin-primary);
943
- }
944
- .tab-content {
945
- display: none;
946
- }
947
- .tab-content.active {
948
- display: block;
949
- }
950
-
951
- /* Invoice table */
952
- .invoice-items-table {
953
- width: 100%;
954
- border-collapse: collapse;
955
- margin-top: 1rem;
956
- }
957
- .invoice-items-table th, .invoice-items-table td {
958
- border: 1px solid var(--admin-border);
959
- padding: 8px;
960
- text-align: left;
961
- font-size: 0.9em;
962
- }
963
- .invoice-items-table th {
964
- background-color: #e9ecef;
965
- font-weight: 600;
966
- color: var(--admin-text);
967
- }
968
- .invoice-items-table .total-row td {
969
- font-weight: 700;
970
- background-color: #f0f0f0;
971
- }
972
- .invoice-items-table .action-btn {
973
- background: none;
974
- border: none;
975
- color: var(--admin-danger);
976
- cursor: pointer;
977
- font-size: 1.2em;
978
- }
979
- .invoice-section-summary {
980
- padding: 1rem;
981
- background-color: #f0f0f0;
982
- border-radius: 8px;
983
- margin-top: 1rem;
984
- font-weight: 600;
985
- }
986
- .invoice-list-admin {
987
- list-style: none;
988
- padding: 0;
989
- max-height: 200px;
990
- overflow-y: auto;
991
- border: 1px solid var(--admin-border);
992
- border-radius: 8px;
993
- }
994
- .invoice-list-admin li {
995
- padding: 8px 12px;
996
- border-bottom: 1px solid var(--admin-border);
997
- display: flex;
998
- justify-content: space-between;
999
- align-items: center;
1000
- }
1001
- .invoice-list-admin li:last-child {
1002
- border-bottom: none;
1003
- }
1004
- .invoice-list-admin .invoice-info {
1005
- font-size: 0.9em;
1006
- }
1007
- .invoice-list-admin .invoice-amount {
1008
- font-weight: 700;
1009
- color: var(--admin-primary-dark);
1010
- }
1011
- .invoice-list-admin .view-btn {
1012
- background: none;
1013
- border: none;
1014
- color: var(--admin-secondary);
1015
- cursor: pointer;
1016
- font-size: 0.9em;
1017
- margin-left: 10px;
1018
- }
1019
- .invoice-list-admin .delete-btn {
1020
- background: none;
1021
- border: none;
1022
- color: var(--admin-danger);
1023
- cursor: pointer;
1024
- font-size: 0.9em;
1025
- margin-left: 5px;
1026
- }
1027
- .organization-details-form {
1028
- display: flex;
1029
- flex-direction: column;
1030
- gap: 1rem;
1031
- }
1032
- .organization-details-form textarea {
1033
- min-height: 80px;
1034
- resize: vertical;
1035
- }
1036
  </style>
1037
  </head>
1038
  <body>
 
 
 
1039
  <div class="container">
1040
  <h1>Панель администратора Bonus</h1>
1041
  <div class="summary-bar">
@@ -1055,6 +1010,10 @@ ADMIN_TEMPLATE = """
1055
  <div class="value debt">{{ summary.users_with_debt }}</div>
1056
  <div class="label">Клиенты с долгом</div>
1057
  </div>
 
 
 
 
1058
  </div>
1059
 
1060
  <div class="controls-bar">
@@ -1072,6 +1031,9 @@ ADMIN_TEMPLATE = """
1072
  <div class="user-details">
1073
  <div class="name">{{ user.first_name or '' }} {{ user.last_name or '' }}</div>
1074
  <div class="username">@{{ user.username if user.username else user.phone_number }} | ID: {{ user.id }}</div>
 
 
 
1075
  </div>
1076
  </div>
1077
  <div class="user-balances">
@@ -1098,7 +1060,6 @@ ADMIN_TEMPLATE = """
1098
  {% endif %}
1099
  </div>
1100
 
1101
- <!-- Transaction/Invoice Modal -->
1102
  <div id="transactionModal" class="modal">
1103
  <div class="modal-content">
1104
  <span class="modal-close" onclick="closeModal('transactionModal')">×</span>
@@ -1156,6 +1117,11 @@ ADMIN_TEMPLATE = """
1156
  </div>
1157
  </div>
1158
 
 
 
 
 
 
1159
  <div class="history-container">
1160
  <h3>Общая история операций</h3>
1161
  <ul id="modalHistoryList" class="history-list"></ul>
@@ -1180,7 +1146,6 @@ ADMIN_TEMPLATE = """
1180
  </tr>
1181
  </thead>
1182
  <tbody>
1183
- <!-- New invoice items will be added here -->
1184
  </tbody>
1185
  <tfoot>
1186
  <tr>
@@ -1205,7 +1170,6 @@ ADMIN_TEMPLATE = """
1205
  </div>
1206
  </div>
1207
 
1208
- <!-- Add Client Modal -->
1209
  <div id="addClientModal" class="modal">
1210
  <div class="modal-content">
1211
  <span class="modal-close" onclick="closeModal('addClientModal')">×</span>
@@ -1227,7 +1191,6 @@ ADMIN_TEMPLATE = """
1227
  </div>
1228
  </div>
1229
 
1230
- <!-- Organization Settings Modal -->
1231
  <div id="orgSettingsModal" class="modal">
1232
  <div class="modal-content">
1233
  <span class="modal-close" onclick="closeModal('orgSettingsModal')">×</span>
@@ -1255,6 +1218,10 @@ ADMIN_TEMPLATE = """
1255
  <label for="orgTelegramLink">Ссылка на Telegram</label>
1256
  <input type="url" id="orgTelegramLink" placeholder="https://t.me/your_telegram_username">
1257
  </div>
 
 
 
 
1258
  </div>
1259
  <div class="modal-footer">
1260
  <div id="orgStatus" class="status-message"></div>
@@ -1263,13 +1230,11 @@ ADMIN_TEMPLATE = """
1263
  </div>
1264
  </div>
1265
 
1266
- <!-- Invoice Detail Modal (for Admin) -->
1267
  <div id="adminInvoiceDetailModal" class="modal">
1268
  <div class="modal-content">
1269
  <span class="modal-close" onclick="closeModal('adminInvoiceDetailModal')">×</span>
1270
  <h2 id="adminInvoiceDetailTitle" class="modal-title"></h2>
1271
  <ul id="adminInvoiceDetailList" class="invoice-detail-list">
1272
- <!-- Invoice items will be loaded here -->
1273
  </ul>
1274
  <div id="adminInvoiceDetailTotal" class="invoice-total-display">
1275
  <span>Итого:</span>
@@ -1310,19 +1275,36 @@ ADMIN_TEMPLATE = """
1310
  document.getElementById('repayDebtAmount').value = '';
1311
  document.getElementById('modalStatus').textContent = '';
1312
  document.getElementById('invoiceStatus').textContent = '';
1313
-
1314
- // Reset new invoice items
1315
  newInvoiceItems = [];
1316
  renderNewInvoiceItems();
1317
-
1318
- loadUserHistoryAndInvoices(); // Load history and invoices for current user
1319
-
1320
- // Set default tab
1321
  showTab('bonus-debt-tab');
1322
-
1323
  transactionModal.style.display = 'block';
1324
  }
1325
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1326
  function loadUserHistoryAndInvoices() {
1327
  const historyList = document.getElementById('modalHistoryList');
1328
  historyList.innerHTML = '';
@@ -1340,8 +1322,8 @@ ADMIN_TEMPLATE = """
1340
  sign = item.type === 'accrual' ? '+' : '-';
1341
  amountClass = item.type === 'accrual' ? 'bonus-accrual' : 'bonus-deduction';
1342
  amountText = `${sign}${parseFloat(item.amount).toFixed(2)}`;
1343
- } else { // debt
1344
- sign = item.type === 'accrual' ? '+' : '-'; // 'accrual' for debt means debt increases (positive)
1345
  amountClass = item.type === 'accrual' ? 'debt-accrual' : 'debt-payment';
1346
  amountText = `${item.type === 'accrual' ? '+' : '-'}${parseFloat(item.amount).toFixed(2)}`;
1347
  }
@@ -1358,7 +1340,6 @@ ADMIN_TEMPLATE = """
1358
  historyList.innerHTML = '<li style="text-align:center; padding: 1rem; color: var(--admin-secondary);">Нет истории</li>';
1359
  }
1360
 
1361
- // Load invoices for invoice tab
1362
  const modalInvoiceList = document.getElementById('modalInvoiceList');
1363
  modalInvoiceList.innerHTML = '';
1364
  const userInvoices = (currentUserData.invoices || []).sort((a, b) => new Date(b.date) - new Date(a.date));
@@ -1402,6 +1383,7 @@ ADMIN_TEMPLATE = """
1402
  document.getElementById('orgAddress').value = data.address || '';
1403
  document.getElementById('orgWhatsAppLink').value = data.whatsapp_link || '';
1404
  document.getElementById('orgTelegramLink').value = data.telegram_link || '';
 
1405
  document.getElementById('orgStatus').textContent = '';
1406
  orgSettingsModal.style.display = 'block';
1407
  })
@@ -1559,6 +1541,7 @@ ADMIN_TEMPLATE = """
1559
  address: document.getElementById('orgAddress').value.trim(),
1560
  whatsapp_link: document.getElementById('orgWhatsAppLink').value.trim(),
1561
  telegram_link: document.getElementById('orgTelegramLink').value.trim(),
 
1562
  };
1563
 
1564
  try {
@@ -1605,14 +1588,9 @@ ADMIN_TEMPLATE = """
1605
  function addNewInvoiceItemRow() {
1606
  const tableBody = document.getElementById('newInvoiceItemsTable').getElementsByTagName('tbody')[0];
1607
  const newRow = tableBody.insertRow();
1608
- const rowIndex = tableBody.rows.length - 1; // Index for current item in newInvoiceItems
1609
 
1610
- newInvoiceItems.push({
1611
- product_name: '',
1612
- quantity: 0,
1613
- unit_price: 0,
1614
- item_total: 0
1615
- });
1616
 
1617
  newRow.innerHTML = `
1618
  <td><input type="text" placeholder="Название товара" oninput="updateInvoiceItem(${rowIndex}, 'product_name', this.value)"></td>
@@ -1626,12 +1604,10 @@ ADMIN_TEMPLATE = """
1626
  function updateInvoiceItem(index, field, value) {
1627
  if (newInvoiceItems[index]) {
1628
  newInvoiceItems[index][field] = value;
1629
-
1630
  const qty = parseFloat(newInvoiceItems[index].quantity) || 0;
1631
  const price = parseFloat(newInvoiceItems[index].unit_price) || 0;
1632
  const itemTotal = qty * price;
1633
  newInvoiceItems[index].item_total = itemTotal;
1634
-
1635
  const tableBody = document.getElementById('newInvoiceItemsTable').getElementsByTagName('tbody')[0];
1636
  tableBody.rows[index].querySelector('.item-total-display').textContent = itemTotal.toFixed(2);
1637
  updateNewInvoiceTotal();
@@ -1640,9 +1616,7 @@ ADMIN_TEMPLATE = """
1640
 
1641
  function removeInvoiceItemRow(button, index) {
1642
  const tableBody = document.getElementById('newInvoiceItemsTable').getElementsByTagName('tbody')[0];
1643
- tableBody.deleteRow(button.parentNode.parentNode.rowIndex - 1); // rowIndex is 1-based, -1 for header
1644
-
1645
- // Re-index newInvoiceItems and update the oninput attributes
1646
  newInvoiceItems.splice(index, 1);
1647
  for (let i = 0; i < tableBody.rows.length; i++) {
1648
  const row = tableBody.rows[i];
@@ -1651,16 +1625,12 @@ ADMIN_TEMPLATE = """
1651
  row.querySelector('input[type="number"][step="0.01"]').setAttribute('oninput', `updateInvoiceItem(${i}, 'unit_price', parseFloat(this.value))`);
1652
  row.querySelector('.action-btn').setAttribute('onclick', `removeInvoiceItemRow(this, ${i})`);
1653
  }
1654
-
1655
  updateNewInvoiceTotal();
1656
  }
1657
 
1658
-
1659
  function updateNewInvoiceTotal() {
1660
  let total = 0;
1661
- newInvoiceItems.forEach(item => {
1662
- total += parseFloat(item.item_total) || 0;
1663
- });
1664
  document.getElementById('newInvoiceTotalAmount').textContent = total.toFixed(2);
1665
  }
1666
 
@@ -1690,17 +1660,13 @@ ADMIN_TEMPLATE = """
1690
  statusEl.textContent = 'Пользователь не выбран.';
1691
  return;
1692
  }
1693
-
1694
  const itemsToAdd = newInvoiceItems.filter(item => item.product_name && (item.quantity > 0 || item.unit_price > 0));
1695
-
1696
  if (itemsToAdd.length === 0) {
1697
  statusEl.style.color = 'var(--admin-danger)';
1698
  statusEl.textContent = 'Добавьте хотя бы один товар в накладную.';
1699
  return;
1700
  }
1701
-
1702
  const totalAmount = itemsToAdd.reduce((sum, item) => sum + item.item_total, 0);
1703
-
1704
  const payload = {
1705
  user_id: currentUserData.id,
1706
  total_amount: totalAmount,
@@ -1757,7 +1723,7 @@ ADMIN_TEMPLATE = """
1757
  });
1758
  const result = await response.json();
1759
  if (response.ok) {
1760
- location.reload(); // Reload the page to update the list
1761
  } else {
1762
  throw new Error(result.message || 'Не удалось удалить накладную.');
1763
  }
@@ -1768,24 +1734,13 @@ ADMIN_TEMPLATE = """
1768
 
1769
 
1770
  window.onclick = function(event) {
1771
- if (event.target == transactionModal) {
1772
- closeModal('transactionModal');
1773
- }
1774
- if (event.target == addClientModal) {
1775
- closeModal('addClientModal');
1776
- }
1777
- if (event.target == orgSettingsModal) {
1778
- closeModal('orgSettingsModal');
1779
- }
1780
- if (event.target == adminInvoiceDetailModal) {
1781
- closeModal('adminInvoiceDetailModal');
1782
- }
1783
  }
1784
 
1785
- // Initial row for new invoice
1786
- document.addEventListener('DOMContentLoaded', () => {
1787
- addNewInvoiceItemRow(); // Add an empty row for new invoice input
1788
- });
1789
  </script>
1790
  </body>
1791
  </html>
@@ -1794,8 +1749,7 @@ ADMIN_TEMPLATE = """
1794
  @app.route('/')
1795
  def index():
1796
  user_id_str = request.args.get('user_id_for_test')
1797
-
1798
- all_data = load_visitor_data() # Load all data, including organization details
1799
  user_data = {}
1800
 
1801
  if user_id_str and user_id_str in all_data:
@@ -1816,20 +1770,14 @@ def index():
1816
  reverse=True
1817
  )
1818
  user_data['combined_history'] = combined_history
1819
- user_data['invoices'] = user_data.get('invoices', []) # Pass invoices to template
1820
  else:
1821
  user_data = {
1822
- "id": "N/A",
1823
- "bonuses": 0,
1824
- "debts": 0,
1825
- "history": [],
1826
- "debt_history": [],
1827
- "combined_history": [],
1828
- "invoices": []
1829
  }
1830
 
1831
  org_details = all_data.get('organization_details', {})
1832
-
1833
  return render_template_string(TEMPLATE, user=user_data, org_details=org_details)
1834
 
1835
  @app.route('/verify', methods=['POST'])
@@ -1841,7 +1789,6 @@ def verify_data():
1841
  return jsonify({"status": "error", "message": "Missing initData"}), 400
1842
 
1843
  user_data_parsed, is_valid = verify_telegram_data(init_data_str)
1844
-
1845
  user_info_dict = {}
1846
  if user_data_parsed and 'user' in user_data_parsed:
1847
  try:
@@ -1855,13 +1802,11 @@ def verify_data():
1855
  tg_user_id = user_info_dict.get('id')
1856
  if tg_user_id:
1857
  now = datetime.now(BISHKEK_TZ)
1858
- all_data = load_visitor_data() # Get current state of all data
1859
 
1860
  existing_user_key = None
1861
  for key, user_data_item in all_data.items():
1862
- # Skip 'organization_details' when iterating through users
1863
- if key == "organization_details":
1864
- continue
1865
  if str(user_data_item.get('telegram_id')) == str(tg_user_id):
1866
  existing_user_key = key
1867
  break
@@ -1869,40 +1814,27 @@ def verify_data():
1869
  if existing_user_key:
1870
  user_entry = all_data[existing_user_key]
1871
  user_entry.update({
1872
- 'first_name': user_info_dict.get('first_name'),
1873
- 'last_name': user_info_dict.get('last_name'),
1874
- 'username': user_info_dict.get('username'),
1875
- 'photo_url': user_info_dict.get('photo_url'),
1876
- 'language_code': user_info_dict.get('language_code'),
1877
- 'visited_at': now.timestamp(),
1878
  'visited_at_str': now.strftime('%Y-%m-%d %H:%M:%S')
1879
  })
1880
  user_id_to_save = existing_user_key
1881
  else:
1882
  new_user_id = generate_unique_id(all_data)
1883
  user_entry = {
1884
- 'id': new_user_id,
1885
- 'telegram_id': tg_user_id,
1886
- 'first_name': user_info_dict.get('first_name'),
1887
- 'last_name': user_info_dict.get('last_name'),
1888
- 'username': user_info_dict.get('username'),
1889
- 'photo_url': user_info_dict.get('photo_url'),
1890
- 'language_code': user_info_dict.get('language_code'),
1891
- 'is_premium': user_info_dict.get('is_premium', False),
1892
- 'phone_number': None, # No phone number from Telegram initData by default
1893
- 'visited_at': now.timestamp(),
1894
- 'visited_at_str': now.strftime('%Y-%m-%d %H:%M:%S'),
1895
- 'bonuses': 0,
1896
- 'history': [],
1897
- 'debts': 0,
1898
- 'debt_history': [],
1899
- 'invoices': [] # Initialize invoices list
1900
  }
1901
  user_id_to_save = new_user_id
1902
 
1903
- all_data[user_id_to_save] = user_entry # Update the global cache
1904
- save_visitor_data(all_data) # Save the entire cache
1905
-
1906
  return jsonify({"status": "ok", "verified": True, "user_id": user_id_to_save})
1907
  else:
1908
  return jsonify({"status": "error", "verified": True, "message": "User ID not found in parsed data"}), 400
@@ -1919,7 +1851,7 @@ def admin_panel():
1919
  all_data = load_visitor_data()
1920
  users_list = []
1921
  for user_id, user_data in all_data.items():
1922
- if user_id == "organization_details": # Skip organization details
1923
  continue
1924
  user_data['id'] = user_id
1925
  users_list.append(user_data)
@@ -1930,13 +1862,63 @@ def admin_panel():
1930
  users_with_debt = sum(1 for u in users_list if u.get('debts', 0) > 0)
1931
 
1932
  summary_stats = {
1933
- "total_users": total_users,
1934
- "total_bonuses": total_bonuses,
1935
- "total_debts": total_debts,
1936
- "users_with_debt": users_with_debt
1937
  }
1938
-
1939
- return render_template_string(ADMIN_TEMPLATE, users=users_list, summary=summary_stats)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1940
 
1941
  @app.route('/admin/add_client', methods=['POST'])
1942
  def add_client():
@@ -1950,10 +1932,8 @@ def add_client():
1950
 
1951
  all_data = load_visitor_data()
1952
 
1953
- # Check for existing phone number, excluding the 'organization_details' key
1954
  for key, user in all_data.items():
1955
- if key == "organization_details":
1956
- continue
1957
  if user.get('phone_number') == phone_number:
1958
  return jsonify({"status": "error", "message": "Клиент с таким номером телефона уже существует."}), 409
1959
 
@@ -1961,25 +1941,14 @@ def add_client():
1961
  new_id = generate_unique_id(all_data)
1962
 
1963
  new_client = {
1964
- 'id': new_id,
1965
- 'telegram_id': None,
1966
- 'first_name': first_name,
1967
- 'last_name': None,
1968
- 'username': None,
1969
- 'photo_url': None,
1970
- 'language_code': 'ru',
1971
- 'is_premium': False,
1972
- 'phone_number': phone_number,
1973
- 'visited_at': now.timestamp(),
1974
- 'visited_at_str': now.strftime('%Y-%m-%d %H:%M:%S'),
1975
- 'bonuses': 0,
1976
- 'history': [],
1977
- 'debts': 0,
1978
- 'debt_history': [],
1979
- 'invoices': [] # Initialize invoices for new manual client
1980
  }
1981
 
1982
- all_data[new_id] = new_client # Update the global cache
1983
  save_visitor_data(all_data)
1984
 
1985
  return jsonify({"status": "ok", "message": "Client added successfully"}), 201
@@ -1988,7 +1957,6 @@ def add_client():
1988
  logging.exception("Error in /admin/add_client endpoint")
1989
  return jsonify({"status": "error", "message": str(e)}), 500
1990
 
1991
-
1992
  @app.route('/admin/add_transaction', methods=['POST'])
1993
  def add_transaction():
1994
  try:
@@ -2019,50 +1987,45 @@ def add_transaction():
2019
  if repay_debt_amount > user.get('debts', 0):
2020
  return jsonify({"status": "error", "message": "Сумма погашения превышает текущий долг"}), 400
2021
 
2022
- # Bonus operations
2023
  accrual_amount = purchase_amount * 0.02
2024
  user['bonuses'] = round(user.get('bonuses', 0) + accrual_amount - deduct_amount, 2)
2025
- if 'history' not in user or not isinstance(user['history'], list):
2026
- user['history'] = []
2027
 
2028
  if accrual_amount > 0:
2029
- user['history'].append({
2030
- "type": "accrual", "amount": round(accrual_amount, 2),
2031
- "description": f"Начисление с покупки {round(purchase_amount, 2)}",
2032
- "date": now_iso, "date_str": now_str
2033
- })
2034
  if deduct_amount > 0:
2035
- user['history'].append({
2036
- "type": "deduction", "amount": round(deduct_amount, 2),
2037
- "description": "Списание бонусов",
2038
- "date": now_iso, "date_str": now_str
2039
- })
2040
 
2041
- # Debt operations
2042
  user['debts'] = round(user.get('debts', 0) + add_debt_amount - repay_debt_amount, 2)
2043
- if 'debt_history' not in user or not isinstance(user['debt_history'], list):
2044
- user['debt_history'] = []
2045
 
2046
  if add_debt_amount > 0:
2047
- user['debt_history'].append({
2048
- "type": "accrual", "amount": round(add_debt_amount, 2),
2049
- "description": "Добавление долга",
2050
- "date": now_iso, "date_str": now_str
2051
- })
2052
  if repay_debt_amount > 0:
2053
- user['debt_history'].append({
2054
- "type": "payment", "amount": round(repay_debt_amount, 2),
2055
- "description": "Погашение долга",
2056
- "date": now_iso, "date_str": now_str
2057
- })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2058
 
2059
- all_data[user_id_str] = user # Update the global cache
2060
  save_visitor_data(all_data)
2061
 
2062
- return jsonify({
2063
- "status": "ok", "message": "Transaction successful",
2064
- "new_balance": user['bonuses'], "new_debt": user['debts']
2065
- }), 200
2066
 
2067
  except Exception as e:
2068
  logging.exception("Error in /admin/add_transaction endpoint")
@@ -2076,10 +2039,8 @@ def add_invoice():
2076
  total_amount = float(data.get('total_amount', 0))
2077
  items = data.get('items', [])
2078
 
2079
- if not user_id:
2080
- return jsonify({"status": "error", "message": "User ID is required"}), 400
2081
- if not items:
2082
- return jsonify({"status": "error", "message": "Необходимо добавить товары в накладную."}), 400
2083
 
2084
  user_id_str = str(user_id)
2085
  all_data = load_visitor_data()
@@ -2092,7 +2053,7 @@ def add_invoice():
2092
  now_iso = now.isoformat()
2093
  now_str = now.strftime('%Y-%m-%d %H:%M:%S')
2094
 
2095
- invoice_id = str(uuid.uuid4().hex[:8]).upper() # Generate a short unique ID
2096
 
2097
  processed_items = []
2098
  for item in items:
@@ -2100,23 +2061,14 @@ def add_invoice():
2100
  qty = float(item.get('quantity', 0))
2101
  u_price = float(item.get('unit_price', 0))
2102
  i_total = round(qty * u_price, 2)
2103
- processed_items.append({
2104
- "product_name": p_name,
2105
- "quantity": qty,
2106
- "unit_price": u_price,
2107
- "item_total": i_total
2108
- })
2109
 
2110
  new_invoice = {
2111
- "invoice_id": invoice_id,
2112
- "date": now_iso,
2113
- "date_str": now_str,
2114
- "total_amount": round(total_amount, 2),
2115
- "items": processed_items
2116
  }
2117
 
2118
- if 'invoices' not in user or not isinstance(user['invoices'], list):
2119
- user['invoices'] = []
2120
  user['invoices'].append(new_invoice)
2121
 
2122
  all_data[user_id_str] = user
@@ -2163,20 +2115,18 @@ def delete_invoice():
2163
  logging.exception("Error in /admin/delete_invoice endpoint")
2164
  return jsonify({"status": "error", "message": str(e)}), 500
2165
 
2166
-
2167
  @app.route('/admin/delete_client', methods=['POST'])
2168
  def delete_client():
2169
  try:
2170
  data = request.get_json()
2171
  user_id = data.get('user_id')
2172
 
2173
- if not user_id:
2174
- return jsonify({"status": "error", "message": "User ID is required"}), 400
2175
 
2176
  user_id_str = str(user_id)
2177
- all_data = load_visitor_data() # Load current state
2178
 
2179
- with _data_lock: # Ensure thread-safe modification of cache
2180
  if user_id_str not in all_data or user_id_str == "organization_details":
2181
  return jsonify({"status": "error", "message": "User not found"}), 404
2182
 
@@ -2184,10 +2134,9 @@ def delete_client():
2184
  if user_to_delete.get('telegram_id') is not None:
2185
  return jsonify({"status": "error", "message": "Cannot delete a Telegram-linked user"}), 403
2186
 
2187
- del all_data[user_id_str] # Modify the loaded data
2188
 
2189
  try:
2190
- # Save the modified all_data (which is visitor_data_cache)
2191
  with open(DATA_FILE, 'w', encoding='utf-8') as f:
2192
  json.dump(all_data, f, ensure_ascii=False, indent=4)
2193
  logging.info(f"User {user_id_str} deleted. Data saved to {DATA_FILE}.")
@@ -2217,16 +2166,15 @@ def save_organization_details():
2217
  try:
2218
  data = request.get_json()
2219
  new_org_details = {
2220
- "name": data.get("name", ""),
2221
- "phone_numbers": data.get("phone_numbers", []),
2222
- "address": data.get("address", ""),
2223
- "whatsapp_link": data.get("whatsapp_link", ""),
2224
- "telegram_link": data.get("telegram_link", "")
2225
  }
2226
 
2227
  all_data = load_visitor_data()
2228
  all_data['organization_details'] = new_org_details
2229
- save_visitor_data(all_data) # Save the entire updated cache
2230
 
2231
  return jsonify({"status": "ok", "message": "Organization details saved successfully"}), 200
2232
  except Exception as e:
@@ -2242,7 +2190,7 @@ if __name__ == '__main__':
2242
  print("Attempting initial data download from Hugging Face...")
2243
  download_data_from_hf()
2244
 
2245
- load_visitor_data() # Ensure data is loaded into cache at startup
2246
 
2247
  print("WARNING: The /admin route is NOT protected. Implement proper authentication for production.")
2248
 
 
 
1
 
2
  import os
3
  from flask import Flask, request, Response, render_template_string, jsonify, redirect, url_for
 
10
  import logging
11
  import threading
12
  import random
13
+ import pytz
14
+ import uuid
15
+ import string
16
 
17
  from huggingface_hub import HfApi, hf_hub_download
18
  from huggingface_hub.utils import RepositoryNotFoundError
 
27
  HF_TOKEN_WRITE = os.getenv("HF_TOKEN_WRITE")
28
  HF_TOKEN_READ = os.getenv("HF_TOKEN_READ")
29
 
 
30
  BISHKEK_TZ = pytz.timezone('Asia/Bishkek')
31
 
32
  app = Flask(__name__)
 
34
  app.secret_key = os.urandom(24)
35
 
36
  _data_lock = threading.Lock()
37
+ visitor_data_cache = {}
38
 
39
  def generate_unique_id(all_data):
40
  while True:
 
41
  new_id = str(random.randint(10000, 99999))
42
  if new_id not in all_data:
43
  return new_id
44
 
45
+ def generate_referral_code(all_data):
46
+ while True:
47
+ code = ''.join(random.choices(string.ascii_uppercase + string.digits, k=6))
48
+ is_unique = True
49
+ for key, user_data in all_data.items():
50
+ if key == "organization_details":
51
+ continue
52
+ if user_data.get('referral_code') == code:
53
+ is_unique = False
54
+ break
55
+ if is_unique:
56
+ return code
57
+
58
  def download_data_from_hf():
59
  global visitor_data_cache
60
  if not HF_TOKEN_READ:
 
91
  def load_visitor_data():
92
  global visitor_data_cache
93
  with _data_lock:
94
+ if not visitor_data_cache:
95
  try:
96
  with open(DATA_FILE, 'r', encoding='utf-8') as f:
97
  visitor_data_cache = json.load(f)
98
  logging.info("Visitor data loaded from local JSON.")
99
  except FileNotFoundError:
100
  logging.warning(f"{DATA_FILE} not found locally. Starting with empty data.")
101
+ visitor_data_cache = {"organization_details": {}}
102
  except json.JSONDecodeError:
103
  logging.error(f"Error decoding {DATA_FILE}. Starting with empty data.")
104
  visitor_data_cache = {"organization_details": {}}
 
106
  logging.error(f"Unexpected error loading visitor data: {e}")
107
  visitor_data_cache = {"organization_details": {}}
108
 
 
109
  if "organization_details" not in visitor_data_cache:
110
  visitor_data_cache["organization_details"] = {}
111
 
 
114
  def save_visitor_data(data):
115
  with _data_lock:
116
  try:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
  with open(DATA_FILE, 'w', encoding='utf-8') as f:
118
  json.dump(visitor_data_cache, f, ensure_ascii=False, indent=4)
119
  logging.info(f"Visitor data successfully saved to {DATA_FILE}.")
 
242
  .header {
243
  text-align: left;
244
  padding: var(--padding-m) 0;
245
+ margin-bottom: 0;
246
  }
247
  .logo {
248
  font-size: 2.5em;
 
283
  box-shadow: 0 2px 10px rgba(255,193,7,0.3);
284
  }
285
  .content-section {
286
+ display: none;
287
  flex-direction: column;
288
  gap: var(--padding-m);
289
  }
290
  .content-section.active {
291
+ display: flex;
292
  }
293
  .card-grid {
294
  display: grid;
 
393
  color: var(--text-secondary-color);
394
  padding: 2rem 0;
395
  }
 
 
396
  .business-card-item {
397
  margin-bottom: 10px;
398
  }
 
409
  .business-card-value a {
410
  color: var(--brand-yellow);
411
  text-decoration: none;
412
+ word-break: break-all;
413
  }
414
  .business-card-value a:hover {
415
  text-decoration: underline;
 
440
  height: 20px;
441
  width: 20px;
442
  }
 
 
443
  .modal {
444
  display: none;
445
  position: fixed;
 
560
  <p class="client-id-label">Ваш ID клиента</p>
561
  <p class="client-id-value">{{ user.id }}</p>
562
  </section>
563
+
564
+ <section class="client-id-card">
565
+ <p class="client-id-label">Ваш реферальный код</p>
566
+ <div style="display: flex; align-items: center; gap: 8px;">
567
+ <p id="referralCode" class="client-id-value" style="margin: 0;">{{ user.referral_code }}</p>
568
+ <button onclick="copyReferralCode()" style="padding: 10px; border-radius: 8px; border: none; background: var(--brand-yellow); color: var(--brand-black); font-weight: 600; cursor: pointer;">Копировать</button>
569
+ </div>
570
+ </section>
571
+
572
+ {% if not user.referred_by_user_id %}
573
+ <section class="history-section">
574
+ <h2 class="history-title">Активировать код</h2>
575
+ <div style="display: flex; gap: 10px; align-items: center;">
576
+ <input type="text" id="referralCodeInput" placeholder="Введите реферальный код" style="flex-grow: 1; padding: 12px; font-size: 1em; border-radius: 8px; border: 1px solid #333; background: #2a2a2a; color: white; text-transform: uppercase;">
577
+ <button onclick="activateReferralCode()" style="padding: 12px 18px; border-radius: 8px; border: none; background: var(--brand-yellow); color: var(--brand-black); font-weight: 600; cursor: pointer;">Активировать</button>
578
+ </div>
579
+ <p id="referralStatus" style="text-align: center; margin-top: 10px; font-weight: 500; min-height: 1em;"></p>
580
+ </section>
581
+ {% endif %}
582
 
583
  <section class="history-section">
584
  <h2 class="history-title">История операций</h2>
 
663
  <div class="business-card-value">
664
  {% if org_details.whatsapp_link %}
665
  <a href="{{ org_details.whatsapp_link }}" target="_blank">
666
+ <img src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgZmlsbD0iI2ZmZiIgY2xhc3M9ImJpIGJpLXdoYXRzYXBwIiB2aWV3Qm94PSIwIDAgMTYgMTYiPjxwYXRoIGQ9Ik0xMy42NzYgMS40NTJBMTAuNTE1IDEwLjUxNSAwIDAgMCA3Ljg4MyAwQzMuNTEgMCAuMDYzIDMuNDQuMDYzIDcuNjU0YzAgMS40MS4zNDcgMi44NjIgLjk2NiA0LjExOC4wNC4wNy4wNjYuMTc2LjA3My4yOTlsLjA2NSAxLjg5MmExLjIzMyAxLjIzMyAwIDAgMCAxLjU1MyAxLjQ1NmwxLjg4Ljc4Yy4xMzYuMDU2LjI3Ni4wOTYuNDI4LjE3MiAxLjI1Ni42NjggMi42MTQuOTgyIDQuMDQ4Ljk4MiA0LjM3MiAwIDcuOTEtMy40NCA3LjkxLTcuNjU0IDAtMi4wNDItLjg1LTMuOTY1LTIuMzQxLTUuMzUzWm0tMi42NzYgOS4yMzFhLjg4MS44ODEgMCAwIDEtMS4yMjUuMDM2bC0uNzk0LS40ODMtLjQ2NC4zNjYtLjY1MS0uNDktMS4xNjYgMS4xNjYtLjUzLS4zMTctLjA4NC41NTUtLjEwMi40NTktLjI1MS4xNjItLjU3LjU3LS41NTUuMDgyLS4zMjIuMDY5LS42Mi0uMTc3LS45ODQtLjE5LS45MDgtLjYxNy0uNDc4LS45MDktLjY3Ny0uNzUxLS40MzQtMS4xNzgtLjMyMi0xLjQ3MS0uMzIyLS4xMDkgMC0uNDg3LjA0Mi0uOTY2LjA4OC0uNDk3LjI1Ny0uNjExuNzMyLS42MTEgMS4xMzdhMS42MzkgMS42MzkgMCAwIDAgLjQ4NiAxLjY5Yy40MTYuNDE2Ljc2Mi43MjQuNzYyLjk1NSAwIC41NDIuMjgyIDEuMDQ2LjMwNCAxLjQxMS41ODYgMS4wNTEgMS43NzUgMS44NDcgMy4wNCAyLjAxOCAxLjMxMi4xNzcgMi4xMDYuMDk1IDIuNzYzLjA3Mi4wOTYtLjE1LjM3Mi0uMjg1LjcwMi0uNDgzLjMxLS40OC41MDktLjUxNS42NjktLjQ1Mi4xMDkuMDUxLjQxNy4yMTEuNDYzLjI2Ny4xNDEuMDgyLjI4NC4xNjEuNDQzLjIyNWExLjIyNyAxLjIyNyAwIDAgMCAuNzQ2LjAyNGwuMjg0LS4xMzVhMy45NjcgMy45NjcgMCAwIDAgLjY2Mi0uNjQzLjkwOC45MDggMCAwIDAgLjMwMi0uNjc4LjE5OC4xOTggMCAwIDAgMC0uMTU2LjgxNS44MTUgMCAwIDAgMC0uNDc3eiIvPjwvc3ZnPg==">
667
  {{ org_details.whatsapp_link }}
668
  </a>
669
  {% else %}
 
691
  </div>
692
  </div>
693
 
 
694
  <div id="invoiceDetailModal" class="modal">
695
  <div class="modal-content">
696
  <span class="modal-close" onclick="closeModal('invoiceDetailModal')">×</span>
697
  <h2 id="invoiceDetailTitle" class="modal-title"></h2>
698
  <ul id="invoiceDetailList" class="invoice-detail-list">
 
699
  </ul>
700
  <div id="invoiceDetailTotal" class="invoice-total-display">
701
  <span>Итого:</span>
 
807
  openModal('invoiceDetailModal');
808
  }
809
 
810
+ function copyReferralCode() {
811
+ const code = document.getElementById('referralCode').textContent;
812
+ navigator.clipboard.writeText(code).then(() => {
813
+ tg.showAlert('Код скопирован!');
814
+ }, () => {
815
+ tg.showAlert('Не удалось скопировать код.');
816
+ });
817
+ }
818
+
819
+ function activateReferralCode() {
820
+ const codeInput = document.getElementById('referralCodeInput');
821
+ const code = codeInput.value.trim().toUpperCase();
822
+ const statusEl = document.getElementById('referralStatus');
823
+ if (!code) {
824
+ statusEl.textContent = 'Введите код.';
825
+ statusEl.style.color = 'var(--brand-red)';
826
+ return;
827
+ }
828
+
829
+ const urlParams = new URLSearchParams(window.location.search);
830
+ const userId = urlParams.get('user_id_for_test');
831
+
832
+ statusEl.textContent = 'Активация...';
833
+ statusEl.style.color = 'var(--text-secondary-color)';
834
+
835
+ fetch('/api/activate_referral', {
836
+ method: 'POST',
837
+ headers: { 'Content-Type': 'application/json' },
838
+ body: JSON.stringify({ user_id: userId, referral_code: code })
839
+ })
840
+ .then(response => response.json())
841
+ .then(data => {
842
+ if (data.status === 'ok') {
843
+ statusEl.textContent = 'Код успешно активирован!';
844
+ statusEl.style.color = '#4CAF50';
845
+ setTimeout(() => { window.location.reload(); }, 1500);
846
+ } else {
847
+ statusEl.textContent = data.message || 'Ошибка активации.';
848
+ statusEl.style.color = 'var(--brand-red)';
849
+ }
850
+ })
851
+ .catch(error => {
852
+ statusEl.textContent = 'Произошла ошибка сети.';
853
+ statusEl.style.color = 'var(--brand-red)';
854
+ });
855
+ }
856
+
857
  document.addEventListener('DOMContentLoaded', () => {
858
  document.querySelectorAll('.nav-btn').forEach(button => {
859
  button.addEventListener('click', () => {
860
  showSection(button.dataset.target);
861
  });
862
  });
 
 
863
  showSection('dashboard-section');
864
  });
865
 
 
965
  .modal-footer button { padding: 12px 25px; font-size: 1.1em; border-radius: 8px; border: none; font-weight: 600; cursor: pointer; }
966
  .btn-submit { background-color: var(--admin-success); color: white; }
967
  .status-message { text-align: center; font-weight: 500; flex-grow: 1; text-align: left; }
968
+ .tab-buttons { display: flex; margin-bottom: 1rem; border-bottom: 1px solid var(--admin-border); }
969
+ .tab-btn { padding: 10px 15px; border: none; background-color: transparent; color: var(--admin-secondary); font-weight: 600; cursor: pointer; border-bottom: 3px solid transparent; transition: all 0.2s ease; }
970
+ .tab-btn.active { color: var(--admin-primary-dark); border-bottom-color: var(--admin-primary); }
971
+ .tab-content { display: none; }
972
+ .tab-content.active { display: block; }
973
+ .invoice-items-table { width: 100%; border-collapse: collapse; margin-top: 1rem; }
974
+ .invoice-items-table th, .invoice-items-table td { border: 1px solid var(--admin-border); padding: 8px; text-align: left; font-size: 0.9em; }
975
+ .invoice-items-table th { background-color: #e9ecef; font-weight: 600; color: var(--admin-text); }
976
+ .invoice-items-table .total-row td { font-weight: 700; background-color: #f0f0f0; }
977
+ .invoice-items-table .action-btn { background: none; border: none; color: var(--admin-danger); cursor: pointer; font-size: 1.2em; }
978
+ .invoice-section-summary { padding: 1rem; background-color: #f0f0f0; border-radius: 8px; margin-top: 1rem; font-weight: 600; }
979
+ .invoice-list-admin { list-style: none; padding: 0; max-height: 200px; overflow-y: auto; border: 1px solid var(--admin-border); border-radius: 8px; }
980
+ .invoice-list-admin li { padding: 8px 12px; border-bottom: 1px solid var(--admin-border); display: flex; justify-content: space-between; align-items: center; }
981
+ .invoice-list-admin li:last-child { border-bottom: none; }
982
+ .invoice-list-admin .invoice-info { font-size: 0.9em; }
983
+ .invoice-list-admin .invoice-amount { font-weight: 700; color: var(--admin-primary-dark); }
984
+ .invoice-list-admin .view-btn { background: none; border: none; color: var(--admin-secondary); cursor: pointer; font-size: 0.9em; margin-left: 10px; }
985
+ .invoice-list-admin .delete-btn { background: none; border: none; color: var(--admin-danger); cursor: pointer; font-size: 0.9em; margin-left: 5px; }
986
+ .organization-details-form { display: flex; flex-direction: column; gap: 1rem; }
987
+ .organization-details-form textarea { min-height: 80px; resize: vertical; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
988
  </style>
989
  </head>
990
  <body>
991
+ <script id="allUsersData" type="application/json">
992
+ {{ all_data|tojson }}
993
+ </script>
994
  <div class="container">
995
  <h1>Панель администратора Bonus</h1>
996
  <div class="summary-bar">
 
1010
  <div class="value debt">{{ summary.users_with_debt }}</div>
1011
  <div class="label">Клиенты с долгом</div>
1012
  </div>
1013
+ <div class="summary-card">
1014
+ <div class="value bonus">{{ "%.1f"|format(org_details.referral_bonus_percentage|float) }}%</div>
1015
+ <div class="label">Реферальный бонус</div>
1016
+ </div>
1017
  </div>
1018
 
1019
  <div class="controls-bar">
 
1031
  <div class="user-details">
1032
  <div class="name">{{ user.first_name or '' }} {{ user.last_name or '' }}</div>
1033
  <div class="username">@{{ user.username if user.username else user.phone_number }} | ID: {{ user.id }}</div>
1034
+ {% if user.referred_by_user_id and all_data[user.referred_by_user_id] %}
1035
+ <div class="username" style="font-size: 0.8em; color: var(--admin-success); margin-top: 4px;">Приведен: {{ all_data[user.referred_by_user_id].first_name }} ({{ user.referred_by_user_id }})</div>
1036
+ {% endif %}
1037
  </div>
1038
  </div>
1039
  <div class="user-balances">
 
1060
  {% endif %}
1061
  </div>
1062
 
 
1063
  <div id="transactionModal" class="modal">
1064
  <div class="modal-content">
1065
  <span class="modal-close" onclick="closeModal('transactionModal')">×</span>
 
1117
  </div>
1118
  </div>
1119
 
1120
+ <div class="history-container">
1121
+ <h3>Приведенные клиенты</h3>
1122
+ <ul id="modalReferralsList" class="history-list"></ul>
1123
+ </div>
1124
+
1125
  <div class="history-container">
1126
  <h3>Общая история операций</h3>
1127
  <ul id="modalHistoryList" class="history-list"></ul>
 
1146
  </tr>
1147
  </thead>
1148
  <tbody>
 
1149
  </tbody>
1150
  <tfoot>
1151
  <tr>
 
1170
  </div>
1171
  </div>
1172
 
 
1173
  <div id="addClientModal" class="modal">
1174
  <div class="modal-content">
1175
  <span class="modal-close" onclick="closeModal('addClientModal')">×</span>
 
1191
  </div>
1192
  </div>
1193
 
 
1194
  <div id="orgSettingsModal" class="modal">
1195
  <div class="modal-content">
1196
  <span class="modal-close" onclick="closeModal('orgSettingsModal')">×</span>
 
1218
  <label for="orgTelegramLink">Ссылка на Telegram</label>
1219
  <input type="url" id="orgTelegramLink" placeholder="https://t.me/your_telegram_username">
1220
  </div>
1221
+ <div class="form-group">
1222
+ <label for="orgReferralBonusPercentage">Процент реферального бонуса (от суммы покупки)</label>
1223
+ <input type="number" step="0.1" min="0" id="orgReferralBonusPercentage" placeholder="2.0">
1224
+ </div>
1225
  </div>
1226
  <div class="modal-footer">
1227
  <div id="orgStatus" class="status-message"></div>
 
1230
  </div>
1231
  </div>
1232
 
 
1233
  <div id="adminInvoiceDetailModal" class="modal">
1234
  <div class="modal-content">
1235
  <span class="modal-close" onclick="closeModal('adminInvoiceDetailModal')">×</span>
1236
  <h2 id="adminInvoiceDetailTitle" class="modal-title"></h2>
1237
  <ul id="adminInvoiceDetailList" class="invoice-detail-list">
 
1238
  </ul>
1239
  <div id="adminInvoiceDetailTotal" class="invoice-total-display">
1240
  <span>Итого:</span>
 
1275
  document.getElementById('repayDebtAmount').value = '';
1276
  document.getElementById('modalStatus').textContent = '';
1277
  document.getElementById('invoiceStatus').textContent = '';
 
 
1278
  newInvoiceItems = [];
1279
  renderNewInvoiceItems();
1280
+ loadUserHistoryAndInvoices();
1281
+ populateReferralsList(userData);
 
 
1282
  showTab('bonus-debt-tab');
 
1283
  transactionModal.style.display = 'block';
1284
  }
1285
 
1286
+ function populateReferralsList(userData) {
1287
+ const referralsListEl = document.getElementById('modalReferralsList');
1288
+ referralsListEl.innerHTML = '';
1289
+ const referrals = userData.referrals || [];
1290
+ const allUsersData = JSON.parse(document.getElementById('allUsersData').textContent);
1291
+
1292
+ if (referrals.length > 0) {
1293
+ referrals.forEach(refId => {
1294
+ const referrerData = allUsersData[refId];
1295
+ const li = document.createElement('li');
1296
+ if (referrerData) {
1297
+ li.textContent = `${referrerData.first_name || 'Клиент'} (ID: ${refId})`;
1298
+ } else {
1299
+ li.textContent = `Неизвестный клиент (ID: ${refId})`;
1300
+ }
1301
+ referralsListEl.appendChild(li);
1302
+ });
1303
+ } else {
1304
+ referralsListEl.innerHTML = '<li style="text-align:center; padding: 1rem; color: var(--admin-secondary);">Нет рефералов</li>';
1305
+ }
1306
+ }
1307
+
1308
  function loadUserHistoryAndInvoices() {
1309
  const historyList = document.getElementById('modalHistoryList');
1310
  historyList.innerHTML = '';
 
1322
  sign = item.type === 'accrual' ? '+' : '-';
1323
  amountClass = item.type === 'accrual' ? 'bonus-accrual' : 'bonus-deduction';
1324
  amountText = `${sign}${parseFloat(item.amount).toFixed(2)}`;
1325
+ } else {
1326
+ sign = item.type === 'accrual' ? '+' : '-';
1327
  amountClass = item.type === 'accrual' ? 'debt-accrual' : 'debt-payment';
1328
  amountText = `${item.type === 'accrual' ? '+' : '-'}${parseFloat(item.amount).toFixed(2)}`;
1329
  }
 
1340
  historyList.innerHTML = '<li style="text-align:center; padding: 1rem; color: var(--admin-secondary);">Нет истории</li>';
1341
  }
1342
 
 
1343
  const modalInvoiceList = document.getElementById('modalInvoiceList');
1344
  modalInvoiceList.innerHTML = '';
1345
  const userInvoices = (currentUserData.invoices || []).sort((a, b) => new Date(b.date) - new Date(a.date));
 
1383
  document.getElementById('orgAddress').value = data.address || '';
1384
  document.getElementById('orgWhatsAppLink').value = data.whatsapp_link || '';
1385
  document.getElementById('orgTelegramLink').value = data.telegram_link || '';
1386
+ document.getElementById('orgReferralBonusPercentage').value = data.referral_bonus_percentage || '';
1387
  document.getElementById('orgStatus').textContent = '';
1388
  orgSettingsModal.style.display = 'block';
1389
  })
 
1541
  address: document.getElementById('orgAddress').value.trim(),
1542
  whatsapp_link: document.getElementById('orgWhatsAppLink').value.trim(),
1543
  telegram_link: document.getElementById('orgTelegramLink').value.trim(),
1544
+ referral_bonus_percentage: parseFloat(document.getElementById('orgReferralBonusPercentage').value) || 0,
1545
  };
1546
 
1547
  try {
 
1588
  function addNewInvoiceItemRow() {
1589
  const tableBody = document.getElementById('newInvoiceItemsTable').getElementsByTagName('tbody')[0];
1590
  const newRow = tableBody.insertRow();
1591
+ const rowIndex = tableBody.rows.length - 1;
1592
 
1593
+ newInvoiceItems.push({ product_name: '', quantity: 0, unit_price: 0, item_total: 0 });
 
 
 
 
 
1594
 
1595
  newRow.innerHTML = `
1596
  <td><input type="text" placeholder="Название товара" oninput="updateInvoiceItem(${rowIndex}, 'product_name', this.value)"></td>
 
1604
  function updateInvoiceItem(index, field, value) {
1605
  if (newInvoiceItems[index]) {
1606
  newInvoiceItems[index][field] = value;
 
1607
  const qty = parseFloat(newInvoiceItems[index].quantity) || 0;
1608
  const price = parseFloat(newInvoiceItems[index].unit_price) || 0;
1609
  const itemTotal = qty * price;
1610
  newInvoiceItems[index].item_total = itemTotal;
 
1611
  const tableBody = document.getElementById('newInvoiceItemsTable').getElementsByTagName('tbody')[0];
1612
  tableBody.rows[index].querySelector('.item-total-display').textContent = itemTotal.toFixed(2);
1613
  updateNewInvoiceTotal();
 
1616
 
1617
  function removeInvoiceItemRow(button, index) {
1618
  const tableBody = document.getElementById('newInvoiceItemsTable').getElementsByTagName('tbody')[0];
1619
+ tableBody.deleteRow(button.parentNode.parentNode.rowIndex - 1);
 
 
1620
  newInvoiceItems.splice(index, 1);
1621
  for (let i = 0; i < tableBody.rows.length; i++) {
1622
  const row = tableBody.rows[i];
 
1625
  row.querySelector('input[type="number"][step="0.01"]').setAttribute('oninput', `updateInvoiceItem(${i}, 'unit_price', parseFloat(this.value))`);
1626
  row.querySelector('.action-btn').setAttribute('onclick', `removeInvoiceItemRow(this, ${i})`);
1627
  }
 
1628
  updateNewInvoiceTotal();
1629
  }
1630
 
 
1631
  function updateNewInvoiceTotal() {
1632
  let total = 0;
1633
+ newInvoiceItems.forEach(item => { total += parseFloat(item.item_total) || 0; });
 
 
1634
  document.getElementById('newInvoiceTotalAmount').textContent = total.toFixed(2);
1635
  }
1636
 
 
1660
  statusEl.textContent = 'Пользователь не выбран.';
1661
  return;
1662
  }
 
1663
  const itemsToAdd = newInvoiceItems.filter(item => item.product_name && (item.quantity > 0 || item.unit_price > 0));
 
1664
  if (itemsToAdd.length === 0) {
1665
  statusEl.style.color = 'var(--admin-danger)';
1666
  statusEl.textContent = 'Добавьте хотя бы один товар в накладную.';
1667
  return;
1668
  }
 
1669
  const totalAmount = itemsToAdd.reduce((sum, item) => sum + item.item_total, 0);
 
1670
  const payload = {
1671
  user_id: currentUserData.id,
1672
  total_amount: totalAmount,
 
1723
  });
1724
  const result = await response.json();
1725
  if (response.ok) {
1726
+ location.reload();
1727
  } else {
1728
  throw new Error(result.message || 'Не удалось удалить накладную.');
1729
  }
 
1734
 
1735
 
1736
  window.onclick = function(event) {
1737
+ if (event.target == transactionModal) { closeModal('transactionModal'); }
1738
+ if (event.target == addClientModal) { closeModal('addClientModal'); }
1739
+ if (event.target == orgSettingsModal) { closeModal('orgSettingsModal'); }
1740
+ if (event.target == adminInvoiceDetailModal) { closeModal('adminInvoiceDetailModal'); }
 
 
 
 
 
 
 
 
1741
  }
1742
 
1743
+ document.addEventListener('DOMContentLoaded', () => { addNewInvoiceItemRow(); });
 
 
 
1744
  </script>
1745
  </body>
1746
  </html>
 
1749
  @app.route('/')
1750
  def index():
1751
  user_id_str = request.args.get('user_id_for_test')
1752
+ all_data = load_visitor_data()
 
1753
  user_data = {}
1754
 
1755
  if user_id_str and user_id_str in all_data:
 
1770
  reverse=True
1771
  )
1772
  user_data['combined_history'] = combined_history
1773
+ user_data['invoices'] = user_data.get('invoices', [])
1774
  else:
1775
  user_data = {
1776
+ "id": "N/A", "bonuses": 0, "debts": 0, "history": [], "debt_history": [],
1777
+ "combined_history": [], "invoices": [], "referral_code": "N/A", "referred_by_user_id": None
 
 
 
 
 
1778
  }
1779
 
1780
  org_details = all_data.get('organization_details', {})
 
1781
  return render_template_string(TEMPLATE, user=user_data, org_details=org_details)
1782
 
1783
  @app.route('/verify', methods=['POST'])
 
1789
  return jsonify({"status": "error", "message": "Missing initData"}), 400
1790
 
1791
  user_data_parsed, is_valid = verify_telegram_data(init_data_str)
 
1792
  user_info_dict = {}
1793
  if user_data_parsed and 'user' in user_data_parsed:
1794
  try:
 
1802
  tg_user_id = user_info_dict.get('id')
1803
  if tg_user_id:
1804
  now = datetime.now(BISHKEK_TZ)
1805
+ all_data = load_visitor_data()
1806
 
1807
  existing_user_key = None
1808
  for key, user_data_item in all_data.items():
1809
+ if key == "organization_details": continue
 
 
1810
  if str(user_data_item.get('telegram_id')) == str(tg_user_id):
1811
  existing_user_key = key
1812
  break
 
1814
  if existing_user_key:
1815
  user_entry = all_data[existing_user_key]
1816
  user_entry.update({
1817
+ 'first_name': user_info_dict.get('first_name'), 'last_name': user_info_dict.get('last_name'),
1818
+ 'username': user_info_dict.get('username'), 'photo_url': user_info_dict.get('photo_url'),
1819
+ 'language_code': user_info_dict.get('language_code'), 'visited_at': now.timestamp(),
 
 
 
1820
  'visited_at_str': now.strftime('%Y-%m-%d %H:%M:%S')
1821
  })
1822
  user_id_to_save = existing_user_key
1823
  else:
1824
  new_user_id = generate_unique_id(all_data)
1825
  user_entry = {
1826
+ 'id': new_user_id, 'telegram_id': tg_user_id, 'first_name': user_info_dict.get('first_name'),
1827
+ 'last_name': user_info_dict.get('last_name'), 'username': user_info_dict.get('username'),
1828
+ 'photo_url': user_info_dict.get('photo_url'), 'language_code': user_info_dict.get('language_code'),
1829
+ 'is_premium': user_info_dict.get('is_premium', False), 'phone_number': None,
1830
+ 'visited_at': now.timestamp(), 'visited_at_str': now.strftime('%Y-%m-%d %H:%M:%S'),
1831
+ 'bonuses': 0, 'history': [], 'debts': 0, 'debt_history': [], 'invoices': [],
1832
+ 'referral_code': generate_referral_code(all_data), 'referred_by_user_id': None, 'referrals': []
 
 
 
 
 
 
 
 
 
1833
  }
1834
  user_id_to_save = new_user_id
1835
 
1836
+ all_data[user_id_to_save] = user_entry
1837
+ save_visitor_data(all_data)
 
1838
  return jsonify({"status": "ok", "verified": True, "user_id": user_id_to_save})
1839
  else:
1840
  return jsonify({"status": "error", "verified": True, "message": "User ID not found in parsed data"}), 400
 
1851
  all_data = load_visitor_data()
1852
  users_list = []
1853
  for user_id, user_data in all_data.items():
1854
+ if user_id == "organization_details":
1855
  continue
1856
  user_data['id'] = user_id
1857
  users_list.append(user_data)
 
1862
  users_with_debt = sum(1 for u in users_list if u.get('debts', 0) > 0)
1863
 
1864
  summary_stats = {
1865
+ "total_users": total_users, "total_bonuses": total_bonuses,
1866
+ "total_debts": total_debts, "users_with_debt": users_with_debt
 
 
1867
  }
1868
+ org_details = all_data.get('organization_details', {})
1869
+ return render_template_string(ADMIN_TEMPLATE, users=users_list, summary=summary_stats, all_data=all_data, org_details=org_details)
1870
+
1871
+ @app.route('/api/activate_referral', methods=['POST'])
1872
+ def activate_referral():
1873
+ try:
1874
+ data = request.get_json()
1875
+ user_id = str(data.get('user_id'))
1876
+ referral_code = data.get('referral_code', '').upper()
1877
+
1878
+ if not user_id or not referral_code:
1879
+ return jsonify({"status": "error", "message": "Необходим ID пользователя и реферальный код."}), 400
1880
+
1881
+ all_data = load_visitor_data()
1882
+
1883
+ if user_id not in all_data:
1884
+ return jsonify({"status": "error", "message": "Пользователь не найден."}), 404
1885
+
1886
+ user = all_data[user_id]
1887
+
1888
+ if user.get('referred_by_user_id'):
1889
+ return jsonify({"status": "error", "message": "Вы уже активировали реферальный код."}), 400
1890
+
1891
+ if user.get('referral_code') == referral_code:
1892
+ return jsonify({"status": "error", "message": "Вы не можете использовать свой собственный код."}), 400
1893
+
1894
+ referrer_id = None
1895
+ referrer_user = None
1896
+ for key, value in all_data.items():
1897
+ if key == "organization_details": continue
1898
+ if value.get('referral_code') == referral_code:
1899
+ referrer_id = key
1900
+ referrer_user = value
1901
+ break
1902
+
1903
+ if not referrer_id or not referrer_user:
1904
+ return jsonify({"status": "error", "message": "Введен неверный реферальный код."}), 404
1905
+
1906
+ user['referred_by_user_id'] = referrer_id
1907
+
1908
+ if 'referrals' not in referrer_user or not isinstance(referrer_user['referrals'], list):
1909
+ referrer_user['referrals'] = []
1910
+ referrer_user['referrals'].append(user_id)
1911
+
1912
+ all_data[user_id] = user
1913
+ all_data[referrer_id] = referrer_user
1914
+
1915
+ save_visitor_data(all_data)
1916
+
1917
+ return jsonify({"status": "ok", "message": "Реферальный код успешно активирован."})
1918
+
1919
+ except Exception as e:
1920
+ logging.exception("Error in /api/activate_referral")
1921
+ return jsonify({"status": "error", "message": "Внутренняя ошибка сервера."}), 500
1922
 
1923
  @app.route('/admin/add_client', methods=['POST'])
1924
  def add_client():
 
1932
 
1933
  all_data = load_visitor_data()
1934
 
 
1935
  for key, user in all_data.items():
1936
+ if key == "organization_details": continue
 
1937
  if user.get('phone_number') == phone_number:
1938
  return jsonify({"status": "error", "message": "Клиент с таким номером телефона уже существует."}), 409
1939
 
 
1941
  new_id = generate_unique_id(all_data)
1942
 
1943
  new_client = {
1944
+ 'id': new_id, 'telegram_id': None, 'first_name': first_name, 'last_name': None, 'username': None,
1945
+ 'photo_url': None, 'language_code': 'ru', 'is_premium': False, 'phone_number': phone_number,
1946
+ 'visited_at': now.timestamp(), 'visited_at_str': now.strftime('%Y-%m-%d %H:%M:%S'),
1947
+ 'bonuses': 0, 'history': [], 'debts': 0, 'debt_history': [], 'invoices': [],
1948
+ 'referral_code': generate_referral_code(all_data), 'referred_by_user_id': None, 'referrals': []
 
 
 
 
 
 
 
 
 
 
 
1949
  }
1950
 
1951
+ all_data[new_id] = new_client
1952
  save_visitor_data(all_data)
1953
 
1954
  return jsonify({"status": "ok", "message": "Client added successfully"}), 201
 
1957
  logging.exception("Error in /admin/add_client endpoint")
1958
  return jsonify({"status": "error", "message": str(e)}), 500
1959
 
 
1960
  @app.route('/admin/add_transaction', methods=['POST'])
1961
  def add_transaction():
1962
  try:
 
1987
  if repay_debt_amount > user.get('debts', 0):
1988
  return jsonify({"status": "error", "message": "Сумма погашения превышает текущий долг"}), 400
1989
 
 
1990
  accrual_amount = purchase_amount * 0.02
1991
  user['bonuses'] = round(user.get('bonuses', 0) + accrual_amount - deduct_amount, 2)
1992
+ if 'history' not in user or not isinstance(user['history'], list): user['history'] = []
 
1993
 
1994
  if accrual_amount > 0:
1995
+ user['history'].append({"type": "accrual", "amount": round(accrual_amount, 2), "description": f"Начисление с покупки {round(purchase_amount, 2)}", "date": now_iso, "date_str": now_str})
 
 
 
 
1996
  if deduct_amount > 0:
1997
+ user['history'].append({"type": "deduction", "amount": round(deduct_amount, 2), "description": "Списание бонусов", "date": now_iso, "date_str": now_str})
 
 
 
 
1998
 
 
1999
  user['debts'] = round(user.get('debts', 0) + add_debt_amount - repay_debt_amount, 2)
2000
+ if 'debt_history' not in user or not isinstance(user['debt_history'], list): user['debt_history'] = []
 
2001
 
2002
  if add_debt_amount > 0:
2003
+ user['debt_history'].append({"type": "accrual", "amount": round(add_debt_amount, 2), "description": "Добавление долга", "date": now_iso, "date_str": now_str})
 
 
 
 
2004
  if repay_debt_amount > 0:
2005
+ user['debt_history'].append({"type": "payment", "amount": round(repay_debt_amount, 2), "description": "Погашение долга", "date": now_iso, "date_str": now_str})
2006
+
2007
+ all_data[user_id_str] = user
2008
+
2009
+ org_details = all_data.get('organization_details', {})
2010
+ referral_bonus_percentage = float(org_details.get('referral_bonus_percentage', 0))
2011
+ if user.get('referred_by_user_id') and purchase_amount > 0 and referral_bonus_percentage > 0:
2012
+ referrer_id = user['referred_by_user_id']
2013
+ if referrer_id in all_data:
2014
+ referrer_user = all_data[referrer_id]
2015
+ referral_bonus = round(purchase_amount * (referral_bonus_percentage / 100), 2)
2016
+ if referral_bonus > 0:
2017
+ referrer_user['bonuses'] = round(referrer_user.get('bonuses', 0) + referral_bonus, 2)
2018
+ if 'history' not in referrer_user or not isinstance(referrer_user['history'], list): referrer_user['history'] = []
2019
+ referrer_user['history'].append({
2020
+ "type": "accrual", "amount": referral_bonus,
2021
+ "description": f"Бонус от реферала {user.get('first_name', user_id_str)}",
2022
+ "date": now_iso, "date_str": now_str
2023
+ })
2024
+ all_data[referrer_id] = referrer_user
2025
 
 
2026
  save_visitor_data(all_data)
2027
 
2028
+ return jsonify({"status": "ok", "message": "Transaction successful", "new_balance": user['bonuses'], "new_debt": user['debts']}), 200
 
 
 
2029
 
2030
  except Exception as e:
2031
  logging.exception("Error in /admin/add_transaction endpoint")
 
2039
  total_amount = float(data.get('total_amount', 0))
2040
  items = data.get('items', [])
2041
 
2042
+ if not user_id: return jsonify({"status": "error", "message": "User ID is required"}), 400
2043
+ if not items: return jsonify({"status": "error", "message": "Необходимо добавить товары в накладную."}), 400
 
 
2044
 
2045
  user_id_str = str(user_id)
2046
  all_data = load_visitor_data()
 
2053
  now_iso = now.isoformat()
2054
  now_str = now.strftime('%Y-%m-%d %H:%M:%S')
2055
 
2056
+ invoice_id = str(uuid.uuid4().hex[:8]).upper()
2057
 
2058
  processed_items = []
2059
  for item in items:
 
2061
  qty = float(item.get('quantity', 0))
2062
  u_price = float(item.get('unit_price', 0))
2063
  i_total = round(qty * u_price, 2)
2064
+ processed_items.append({"product_name": p_name, "quantity": qty, "unit_price": u_price, "item_total": i_total})
 
 
 
 
 
2065
 
2066
  new_invoice = {
2067
+ "invoice_id": invoice_id, "date": now_iso, "date_str": now_str,
2068
+ "total_amount": round(total_amount, 2), "items": processed_items
 
 
 
2069
  }
2070
 
2071
+ if 'invoices' not in user or not isinstance(user['invoices'], list): user['invoices'] = []
 
2072
  user['invoices'].append(new_invoice)
2073
 
2074
  all_data[user_id_str] = user
 
2115
  logging.exception("Error in /admin/delete_invoice endpoint")
2116
  return jsonify({"status": "error", "message": str(e)}), 500
2117
 
 
2118
  @app.route('/admin/delete_client', methods=['POST'])
2119
  def delete_client():
2120
  try:
2121
  data = request.get_json()
2122
  user_id = data.get('user_id')
2123
 
2124
+ if not user_id: return jsonify({"status": "error", "message": "User ID is required"}), 400
 
2125
 
2126
  user_id_str = str(user_id)
2127
+ all_data = load_visitor_data()
2128
 
2129
+ with _data_lock:
2130
  if user_id_str not in all_data or user_id_str == "organization_details":
2131
  return jsonify({"status": "error", "message": "User not found"}), 404
2132
 
 
2134
  if user_to_delete.get('telegram_id') is not None:
2135
  return jsonify({"status": "error", "message": "Cannot delete a Telegram-linked user"}), 403
2136
 
2137
+ del all_data[user_id_str]
2138
 
2139
  try:
 
2140
  with open(DATA_FILE, 'w', encoding='utf-8') as f:
2141
  json.dump(all_data, f, ensure_ascii=False, indent=4)
2142
  logging.info(f"User {user_id_str} deleted. Data saved to {DATA_FILE}.")
 
2166
  try:
2167
  data = request.get_json()
2168
  new_org_details = {
2169
+ "name": data.get("name", ""), "phone_numbers": data.get("phone_numbers", []),
2170
+ "address": data.get("address", ""), "whatsapp_link": data.get("whatsapp_link", ""),
2171
+ "telegram_link": data.get("telegram_link", ""),
2172
+ "referral_bonus_percentage": data.get("referral_bonus_percentage", 0)
 
2173
  }
2174
 
2175
  all_data = load_visitor_data()
2176
  all_data['organization_details'] = new_org_details
2177
+ save_visitor_data(all_data)
2178
 
2179
  return jsonify({"status": "ok", "message": "Organization details saved successfully"}), 200
2180
  except Exception as e:
 
2190
  print("Attempting initial data download from Hugging Face...")
2191
  download_data_from_hf()
2192
 
2193
+ load_visitor_dat
2194
 
2195
  print("WARNING: The /admin route is NOT protected. Implement proper authentication for production.")
2196