Kgshop commited on
Commit
93cc53b
·
verified ·
1 Parent(s): cc7b121

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +249 -49
app.py CHANGED
@@ -1,5 +1,4 @@
1
 
2
-
3
  import os
4
  from flask import Flask, request, Response, render_template_string, jsonify, redirect, url_for
5
  import hmac
@@ -97,6 +96,13 @@ def load_visitor_data():
97
  if "organization_details" not in visitor_data_cache:
98
  visitor_data_cache["organization_details"] = {}
99
 
 
 
 
 
 
 
 
100
  def save_visitor_data():
101
  try:
102
  with open(DATA_FILE, 'w', encoding='utf-8') as f:
@@ -190,6 +196,7 @@ TEMPLATE = """
190
  --brand-black: #101010;
191
  --brand-red: #F44336;
192
  --brand-green: #4CAF50;
 
193
  --card-bg: #1c1c1e;
194
  --text-color: #ffffff;
195
  --text-secondary-color: #a0a0a0;
@@ -201,6 +208,8 @@ TEMPLATE = """
201
  --shadow-glow: 0 0 35px var(--shadow-color);
202
  --shadow-color-red: rgba(244, 67, 54, 0.15);
203
  --shadow-glow-red: 0 0 35px var(--shadow-color-red);
 
 
204
  }
205
  * { box-sizing: border-box; margin: 0; padding: 0; }
206
  html, body {
@@ -243,18 +252,20 @@ TEMPLATE = """
243
  .content-section { display: none; flex-direction: column; gap: var(--padding-m); }
244
  .content-section.active { display: flex; }
245
  .card-grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--padding-m); }
246
- .bonus-card, .debt-card, .promo-card {
247
  background: linear-gradient(145deg, #2a2a2a, #1c1c1c);
248
  border-radius: calc(var(--border-radius) + 8px); padding: var(--padding-l);
249
  text-align: center; position: relative; overflow: hidden;
250
  }
251
  .bonus-card { box-shadow: var(--shadow-glow); border: 1px solid rgba(255, 193, 7, 0.2); }
252
  .debt-card { box-shadow: var(--shadow-glow-red); border: 1px solid rgba(244, 67, 54, 0.2); }
 
253
  .promo-card { grid-column: 1 / -1; border: 1px solid rgba(255,255,255,0.1); }
254
  .card-label { font-size: 1.1em; font-weight: 500; color: var(--text-secondary-color); margin-bottom: 12px; }
255
- .bonus-amount, .debt-amount { font-size: 3em; font-weight: 800; letter-spacing: -2px; line-height: 1; }
256
  .bonus-amount { color: var(--brand-yellow); }
257
  .debt-amount { color: var(--brand-red); }
 
258
  .client-id-card {
259
  background-color: var(--card-bg); border-radius: var(--border-radius);
260
  padding: var(--padding-m); display: flex; justify-content: space-between; align-items: center;
@@ -297,8 +308,11 @@ TEMPLATE = """
297
  .history-description, .invoice-description { font-size: 1em; font-weight: 500; }
298
  .history-date, .invoice-date { font-size: 0.8em; color: var(--text-secondary-color); margin-top: 4px; }
299
  .history-amount, .invoice-amount { font-size: 1.1em; font-weight: 700; }
300
- .history-amount.accrual { color: var(--brand-green); }
301
- .history-amount.deduction { color: var(--brand-red); }
 
 
 
302
  .invoice-amount { color: var(--brand-yellow); }
303
  .no-history, .no-invoices { text-align: center; color: var(--text-secondary-color); padding: 2rem 0; }
304
  .business-card-item { margin-bottom: 10px; }
@@ -382,6 +396,10 @@ TEMPLATE = """
382
  <div class="debt-card">
383
  <p class="card-label">Ваш долг</p>
384
  <p class="debt-amount">{{ "%.2f"|format(user.debts|float) }}</p>
 
 
 
 
385
  </div>
386
  <div class="promo-card">
387
  <p class="card-label">Ваш промокод для друзей</p>
@@ -407,12 +425,16 @@ TEMPLATE = """
407
  <span class="history-date">{{ item.date_str }}</span>
408
  </div>
409
  {% if item.transaction_type == 'bonus' %}
410
- <span class="history-amount {{ 'accrual' if item.type == 'accrual' else 'deduction' }}">
411
  {{ '+' if item.type == 'accrual' else '-' }}{{ "%.2f"|format(item.amount|float) }}
412
  </span>
413
  {% elif item.transaction_type == 'debt' %}
414
- <span class="history-amount {{ 'deduction' if item.type == 'accrual' else 'accrual' }}">
415
- {{ '+' if item.type == 'accrual' else '-' }}{{ "%.2f"|format(item.amount|float) }}
 
 
 
 
416
  </span>
417
  {% endif %}
418
  </li>
@@ -701,6 +723,7 @@ ADMIN_TEMPLATE = """
701
  .summary-card .label { font-size: 0.9em; color: var(--admin-secondary); margin-top: 0.5rem; }
702
  .summary-card .value.bonus { color: var(--admin-primary-dark); }
703
  .summary-card .value.debt { color: var(--admin-danger); }
 
704
  .controls-bar { display: flex; flex-wrap: wrap; gap: 1rem; align-items: center; background: var(--admin-card-bg); padding: var(--padding); border-radius: var(--border-radius); box-shadow: 0 4px 15px var(--admin-shadow); border: 1px solid var(--admin-border); margin-bottom: var(--padding); }
705
  .controls-bar input[type="text"] { flex-grow: 1; padding: 12px 15px; font-size: 1.1em; border-radius: 8px; border: 1px solid var(--admin-border); box-sizing: border-box; min-width: 250px; }
706
  .btn { padding: 12px 20px; font-size: 1em; border: none; border-radius: 8px; font-weight: 600; cursor: pointer; transition: background-color 0.2s ease; }
@@ -722,10 +745,12 @@ ADMIN_TEMPLATE = """
722
  }
723
  .referral-info .ref-by { color: var(--admin-success); }
724
  .referral-info .ref-count { color: var(--admin-info); font-weight: 600; }
725
- .user-balances { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; text-align: center; margin-bottom: 1rem; margin-top: 1rem; }
726
  .user-balances .label { font-size: 0.9em; color: var(--admin-secondary); }
727
- .user-balances .amount.bonus { font-size: 1.8em; font-weight: 700; color: var(--admin-primary-dark); }
728
- .user-balances .amount.debt { font-size: 1.8em; font-weight: 700; color: var(--admin-danger); }
 
 
729
  .user-actions { margin-top: auto; display: flex; flex-direction: column; gap: 0.5rem; }
730
  .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; }
731
  .btn-manage:hover { background-color: var(--admin-primary-dark); }
@@ -756,6 +781,7 @@ ADMIN_TEMPLATE = """
756
  .history-item .amount.bonus-deduction { color: var(--admin-danger); font-weight: 600; }
757
  .history-item .amount.debt-accrual { color: var(--admin-danger); font-weight: 600; }
758
  .history-item .amount.debt-payment { color: var(--admin-success); font-weight: 600; }
 
759
  .modal-footer { margin-top: 1.5rem; display: flex; justify-content: flex-end; align-items: center; gap: 1rem;}
760
  .modal-footer button { padding: 12px 25px; font-size: 1.1em; border-radius: 8px; border: none; font-weight: 600; cursor: pointer; }
761
  .btn-submit { background-color: var(--admin-success); color: white; }
@@ -778,8 +804,11 @@ ADMIN_TEMPLATE = """
778
  .invoice-list-admin .invoice-amount { font-weight: 700; color: var(--admin-primary-dark); }
779
  .invoice-list-admin .view-btn { background: none; border: none; color: var(--admin-secondary); cursor: pointer; font-size: 0.9em; margin-left: 10px; }
780
  .invoice-list-admin .delete-btn { background: none; border: none; color: var(--admin-danger); cursor: pointer; font-size: 0.9em; margin-left: 5px; }
781
- .organization-details-form { display: flex; flex-direction: column; gap: 1rem; }
782
- .organization-details-form textarea { min-height: 80px; resize: vertical; }
 
 
 
783
  </style>
784
  </head>
785
  <body>
@@ -799,14 +828,15 @@ ADMIN_TEMPLATE = """
799
  <div class="label">Всего долгов</div>
800
  </div>
801
  <div class="summary-card">
802
- <div class="value debt">{{ summary.users_with_debt }}</div>
803
- <div class="label">Клиенты с долгом</div>
804
  </div>
805
  </div>
806
  <div class="controls-bar">
807
  <input type="text" id="searchInput" onkeyup="searchUsers()" placeholder="Поиск по имени, ID, username, номеру...">
808
  <button class="btn btn-primary" onclick="openAddClientModal()">Добавить клиента</button>
809
  <button class="btn btn-primary" onclick="openOrgSettingsModal()">Настройки организации</button>
 
810
  </div>
811
  {% if users %}
812
  <div class="user-grid" id="userGrid">
@@ -834,6 +864,10 @@ ADMIN_TEMPLATE = """
834
  <div class="label">Долг</div>
835
  <div class="amount debt">{{ "%.2f"|format(user.debts|float if user.debts else 0) }}</div>
836
  </div>
 
 
 
 
837
  </div>
838
  <div class="user-actions">
839
  <button class="btn-manage" onclick='openTransactionModal({{ user|tojson }})'>Управление счетом</button>
@@ -859,14 +893,15 @@ ADMIN_TEMPLATE = """
859
  <div class="tab-buttons">
860
  <button class="tab-btn active" data-tab="bonus-debt-tab">Бонусы и долги</button>
861
  <button class="tab-btn" data-tab="invoice-tab">Накладные</button>
 
862
  </div>
863
  <div id="bonus-debt-tab" class="tab-content active">
864
  <div class="form-section">
865
  <h3>Бонусы</h3>
866
  <div class="form-row">
867
  <div class="form-group">
868
- <label for="purchaseAmount">Сумма покупки (для начисления)</label>
869
- <input type="number" id="purchaseAmount" placeholder="1500" oninput="updateCalculations()">
870
  </div>
871
  <div class="form-group">
872
  <label for="deductAmount">Списать бонусов</label>
@@ -875,7 +910,7 @@ ADMIN_TEMPLATE = """
875
  </div>
876
  <div class="calculation-summary">
877
  <div class="summary-item"><span>Текущий баланс:</span> <strong id="summaryCurrentBalance">0.00</strong></div>
878
- <div class="summary-item"><span>Будет начислено (2%):</span> <strong id="summaryAccrual">+0.00</strong></div>
879
  <div class="summary-item"><span>Будет списано:</span> <strong id="summaryDeduction">-0.00</strong></div>
880
  <hr>
881
  <div class="summary-item"><strong>Итоговый баланс бонусов:</strong> <strong id="summaryFinalBalance">0.00</strong></div>
@@ -939,6 +974,12 @@ ADMIN_TEMPLATE = """
939
  <ul id="modalInvoiceList" class="invoice-list-admin"></ul>
940
  </div>
941
  </div>
 
 
 
 
 
 
942
  </div>
943
  </div>
944
  <div id="addClientModal" class="modal">
@@ -967,7 +1008,7 @@ ADMIN_TEMPLATE = """
967
  <div class="modal-header">
968
  <h2>Настройки организации</h2>
969
  </div>
970
- <div class="organization-details-form">
971
  <div class="form-group">
972
  <label for="orgName">Название организации</label>
973
  <input type="text" id="orgName" placeholder="Название вашей организации">
@@ -994,6 +1035,38 @@ ADMIN_TEMPLATE = """
994
  <button class="btn-submit" onclick="saveOrgSettings()">Сохранить настройки</button>
995
  </div>
996
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
997
  </div>
998
  <div id="adminInvoiceDetailModal" class="modal">
999
  <div class="modal-content">
@@ -1010,6 +1083,7 @@ ADMIN_TEMPLATE = """
1010
  const transactionModal = document.getElementById('transactionModal');
1011
  const addClientModal = document.getElementById('addClientModal');
1012
  const orgSettingsModal = document.getElementById('orgSettingsModal');
 
1013
  const adminInvoiceDetailModal = document.getElementById('adminInvoiceDetailModal');
1014
  let currentUserData = null;
1015
  let newInvoiceItems = [];
@@ -1026,7 +1100,7 @@ ADMIN_TEMPLATE = """
1026
  document.getElementById('modalUserId').value = userData.id;
1027
  document.getElementById('modalUserName').textContent = `${userData.first_name || ''} ${userData.last_name || ''}`;
1028
  document.getElementById('modalUserUsername').textContent = `@${userData.username || userData.phone_number || ''} | ID: ${userData.id}`;
1029
- ['purchaseAmount', 'deductAmount', 'addDebtAmount', 'repayDebtAmount'].forEach(id => document.getElementById(id).value = '');
1030
  ['modalStatus', 'invoiceStatus'].forEach(id => document.getElementById(id).textContent = '');
1031
  newInvoiceItems = [];
1032
  renderNewInvoiceItems();
@@ -1040,27 +1114,45 @@ ADMIN_TEMPLATE = """
1040
  historyList.innerHTML = '';
1041
  const bonusHistory = (currentUserData.history || []).map(h => ({...h, transaction_type: 'bonus'}));
1042
  const debtHistory = (currentUserData.debt_history || []).map(h => ({...h, transaction_type: 'debt'}));
1043
- const combinedHistory = [...bonusHistory, ...debtHistory].sort((a, b) => new Date(b.date) - new Date(a.date));
 
1044
  if (combinedHistory.length > 0) {
1045
  combinedHistory.forEach(item => {
1046
  const li = document.createElement('li');
1047
  li.className = 'history-item';
1048
  let sign, amountClass, amountText;
1049
- if (item.transaction_type === 'bonus') {
1050
  sign = item.type === 'accrual' ? '+' : '-';
1051
  amountClass = item.type === 'accrual' ? 'bonus-accrual' : 'bonus-deduction';
1052
- amountText = `${sign}${parseFloat(item.amount).toFixed(2)}`;
1053
- } else {
1054
  sign = item.type === 'accrual' ? '+' : '-';
1055
  amountClass = item.type === 'accrual' ? 'debt-accrual' : 'debt-payment';
1056
- amountText = `${item.type === 'accrual' ? '+' : '-'}${parseFloat(item.amount).toFixed(2)}`;
 
 
1057
  }
 
1058
  li.innerHTML = `<div><div class="desc">${item.description}</div><div class="date">${item.date_str}</div></div><div class="amount ${amountClass}">${amountText}</div>`;
1059
  historyList.appendChild(li);
1060
  });
1061
  } else {
1062
  historyList.innerHTML = '<li style="text-align:center; padding: 1rem; color: var(--admin-secondary);">Нет истории</li>';
1063
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1064
  const modalInvoiceList = document.getElementById('modalInvoiceList');
1065
  modalInvoiceList.innerHTML = '';
1066
  const userInvoices = (currentUserData.invoices || []).sort((a, b) => new Date(b.date) - new Date(a.date));
@@ -1104,6 +1196,23 @@ ADMIN_TEMPLATE = """
1104
  });
1105
  }
1106
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1107
  function closeModal(modalId) {
1108
  document.getElementById(modalId).style.display = 'none';
1109
  if (modalId === 'transactionModal') currentUserData = null;
@@ -1123,20 +1232,19 @@ ADMIN_TEMPLATE = """
1123
  function updateCalculations() {
1124
  if (!currentUserData) return;
1125
  const currentBalance = parseFloat(currentUserData.bonuses) || 0;
1126
- const purchaseAmount = parseFloat(document.getElementById('purchaseAmount').value) || 0;
1127
  const deductAmount = parseFloat(document.getElementById('deductAmount').value) || 0;
1128
- const accrualAmount = purchaseAmount * 0.02;
1129
- let finalDeductAmount = deductAmount > currentBalance ? currentBalance : deductAmount;
1130
  if (deductAmount > currentBalance) document.getElementById('deductAmount').value = finalDeductAmount > 0 ? finalDeductAmount.toFixed(2) : '';
1131
- const finalBalance = currentBalance + accrualAmount - finalDeductAmount;
1132
  document.getElementById('summaryCurrentBalance').textContent = currentBalance.toFixed(2);
1133
- document.getElementById('summaryAccrual').textContent = `+${accrualAmount.toFixed(2)}`;
1134
  document.getElementById('summaryDeduction').textContent = `-${finalDeductAmount.toFixed(2)}`;
1135
  document.getElementById('summaryFinalBalance').textContent = finalBalance.toFixed(2);
1136
  const currentDebt = parseFloat(currentUserData.debts) || 0;
1137
  const addDebtAmount = parseFloat(document.getElementById('addDebtAmount').value) || 0;
1138
  const repayDebtAmount = parseFloat(document.getElementById('repayDebtAmount').value) || 0;
1139
- let finalRepayAmount = repayDebtAmount > currentDebt ? currentDebt : repayDebtAmount;
1140
  if (repayDebtAmount > currentDebt) document.getElementById('repayDebtAmount').value = finalRepayAmount > 0 ? finalRepayAmount.toFixed(2) : '';
1141
  const finalDebt = currentDebt + addDebtAmount - finalRepayAmount;
1142
  document.getElementById('summaryCurrentDebt').textContent = currentDebt.toFixed(2);
@@ -1151,7 +1259,7 @@ ADMIN_TEMPLATE = """
1151
  statusEl.textContent = 'Обработка...';
1152
  const payload = {
1153
  user_id: document.getElementById('modalUserId').value,
1154
- purchase_amount: parseFloat(document.getElementById('purchaseAmount').value) || 0,
1155
  deduct_amount: parseFloat(document.getElementById('deductAmount').value) || 0,
1156
  add_debt_amount: parseFloat(document.getElementById('addDebtAmount').value) || 0,
1157
  repay_debt_amount: parseFloat(document.getElementById('repayDebtAmount').value) || 0,
@@ -1226,7 +1334,32 @@ ADMIN_TEMPLATE = """
1226
  if (response.ok) {
1227
  statusEl.style.color = 'var(--admin-success)';
1228
  statusEl.textContent = 'Настройки организации успешно сохранены!';
1229
- setTimeout(() => location.reload(), 1500);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1230
  } else { throw new Error(result.message || 'Произошла ошибка'); }
1231
  } catch (error) {
1232
  statusEl.style.color = 'var(--admin-danger)';
@@ -1359,6 +1492,7 @@ ADMIN_TEMPLATE = """
1359
  if (event.target == transactionModal) closeModal('transactionModal');
1360
  if (event.target == addClientModal) closeModal('addClientModal');
1361
  if (event.target == orgSettingsModal) closeModal('orgSettingsModal');
 
1362
  if (event.target == adminInvoiceDetailModal) closeModal('adminInvoiceDetailModal');
1363
  }
1364
 
@@ -1380,12 +1514,14 @@ def index():
1380
  is_first_visit = not user_data.get('has_been_welcomed', False)
1381
  bonus_history = user_data.get('history', [])
1382
  debt_history = user_data.get('debt_history', [])
 
1383
  for item in bonus_history: item['transaction_type'] = 'bonus'
1384
  for item in debt_history: item['transaction_type'] = 'debt'
1385
- user_data['combined_history'] = sorted(bonus_history + debt_history, key=lambda x: x['date'], reverse=True)
 
1386
  user_data['invoices'] = user_data.get('invoices', [])
1387
  else:
1388
- user_data = {"id": "N/A", "bonuses": 0, "debts": 0, "combined_history": [], "invoices": [], "referral_code": "N/A"}
1389
 
1390
  org_details = visitor_data_cache.get('organization_details', {})
1391
  return render_template_string(TEMPLATE, user=user_data, org_details=org_details, is_first_visit=is_first_visit)
@@ -1432,7 +1568,8 @@ def verify_data():
1432
  'is_premium': user_info_dict.get('is_premium', False), 'phone_number': None,
1433
  'visited_at': now.timestamp(), 'visited_at_str': now.strftime('%Y-%m-%d %H:%M:%S'),
1434
  'bonuses': 0, 'history': [], 'debts': 0, 'debt_history': [], 'invoices': [],
1435
- 'referral_code': f'PROMO{new_user_id}', 'referred_by': None, 'referrals': [], 'has_been_welcomed': False
 
1436
  }
1437
  visitor_data_cache[new_user_id] = user_entry
1438
  user_id_to_save = new_user_id
@@ -1464,9 +1601,11 @@ def submit_referral():
1464
  return jsonify({"status": "ok", "message": "Вы уже прошли этот шаг."}), 200
1465
 
1466
  user['has_been_welcomed'] = True
 
 
1467
 
1468
  if referral_code:
1469
- referrer = next((u for u_id, u in visitor_data_cache.items() if u_id != "organization_details" and u.get('referral_code') == referral_code), None)
1470
 
1471
  if not referrer:
1472
  return jsonify({"status": "error", "message": "Промокод не найден."}), 404
@@ -1476,6 +1615,16 @@ def submit_referral():
1476
  user['referred_by'] = referrer['id']
1477
  if 'referrals' not in referrer: referrer['referrals'] = []
1478
  referrer['referrals'].append(user_id)
 
 
 
 
 
 
 
 
 
 
1479
 
1480
  save_visitor_data()
1481
  message = "Промокод успешно применен!" if referral_code else "Добро пожаловать!"
@@ -1488,10 +1637,10 @@ def submit_referral():
1488
  @app.route('/admin')
1489
  def admin_panel():
1490
  users_list = []
1491
- user_name_map = {uid: f"{ud.get('first_name', '')} {ud.get('last_name', '')}".strip() or f"ID: {uid}" for uid, ud in visitor_data_cache.items() if uid != "organization_details"}
1492
 
1493
  for user_id, user_data in visitor_data_cache.items():
1494
- if user_id == "organization_details": continue
1495
  user_data_copy = user_data.copy()
1496
  user_data_copy['id'] = user_id
1497
  user_data_copy['referrals_count'] = len(user_data_copy.get('referrals', []))
@@ -1502,11 +1651,11 @@ def admin_panel():
1502
  total_users = len(users_list)
1503
  total_bonuses = sum(u.get('bonuses', 0) for u in users_list)
1504
  total_debts = sum(u.get('debts', 0) for u in users_list)
1505
- users_with_debt = sum(1 for u in users_list if u.get('debts', 0) > 0)
1506
 
1507
  summary_stats = {
1508
  "total_users": total_users, "total_bonuses": total_bonuses,
1509
- "total_debts": total_debts, "users_with_debt": users_with_debt
1510
  }
1511
  return render_template_string(ADMIN_TEMPLATE, users=users_list, summary=summary_stats)
1512
 
@@ -1520,7 +1669,7 @@ def add_client():
1520
  return jsonify({"status": "error", "message": "Имя и н��мер телефона обязательны."}), 400
1521
 
1522
  with _data_lock:
1523
- if any(u.get('phone_number') == phone_number for k, u in visitor_data_cache.items() if k != "organization_details"):
1524
  return jsonify({"status": "error", "message": "Клиент с таким номером телефона уже существует."}), 409
1525
 
1526
  now = datetime.now(BISHKEK_TZ)
@@ -1530,7 +1679,8 @@ def add_client():
1530
  'username': None, 'photo_url': None, 'is_premium': False, 'phone_number': phone_number,
1531
  'visited_at': now.timestamp(), 'visited_at_str': now.strftime('%Y-%m-%d %H:%M:%S'),
1532
  'bonuses': 0, 'history': [], 'debts': 0, 'debt_history': [], 'invoices': [],
1533
- 'referral_code': f'PROMO{new_id}', 'referred_by': None, 'referrals': [], 'has_been_welcomed': True
 
1534
  }
1535
  visitor_data_cache[new_id] = new_client
1536
  save_visitor_data()
@@ -1544,7 +1694,7 @@ def add_transaction():
1544
  try:
1545
  data = request.get_json()
1546
  user_id = str(data.get('user_id'))
1547
- purchase_amount = float(data.get('purchase_amount', 0))
1548
  deduct_amount = float(data.get('deduct_amount', 0))
1549
  add_debt_amount = float(data.get('add_debt_amount', 0))
1550
  repay_debt_amount = float(data.get('repay_debt_amount', 0))
@@ -1558,10 +1708,9 @@ def add_transaction():
1558
  if deduct_amount > user.get('bonuses', 0): return jsonify({"status": "error", "message": "Недостаточно бонусов для списания"}), 400
1559
  if repay_debt_amount > user.get('debts', 0): return jsonify({"status": "error", "message": "Сумма погашения превышает текущий долг"}), 400
1560
 
1561
- accrual_amount = purchase_amount * 0.02
1562
- user['bonuses'] = round(user.get('bonuses', 0) + accrual_amount - deduct_amount, 2)
1563
  if 'history' not in user: user['history'] = []
1564
- if accrual_amount > 0: user['history'].append({"type": "accrual", "amount": round(accrual_amount, 2), "description": f"Начисление с покупки {round(purchase_amount, 2)}", "date": now_iso, "date_str": now_str})
1565
  if deduct_amount > 0: user['history'].append({"type": "deduction", "amount": round(deduct_amount, 2), "description": "Списание бонусов", "date": now_iso, "date_str": now_str})
1566
 
1567
  user['debts'] = round(user.get('debts', 0) + add_debt_amount - repay_debt_amount, 2)
@@ -1595,6 +1744,33 @@ def add_invoice():
1595
  new_invoice = {"invoice_id": invoice_id, "date": now_iso, "date_str": now_str, "total_amount": round(total_amount, 2), "items": processed_items}
1596
  if 'invoices' not in user: user['invoices'] = []
1597
  user['invoices'].append(new_invoice)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1598
  save_visitor_data()
1599
  return jsonify({"status": "ok", "message": "Invoice added successfully", "invoice_id": invoice_id}), 200
1600
  except Exception as e:
@@ -1669,6 +1845,30 @@ def save_organization_details():
1669
  logging.exception("Error saving organization details")
1670
  return jsonify({"status": "error", "message": str(e)}), 500
1671
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1672
  if __name__ == '__main__':
1673
  print("--- BONUS SYSTEM SERVER ---")
1674
  print(f"Server starting on http://{HOST}:{PORT}")
@@ -1676,7 +1876,7 @@ if __name__ == '__main__':
1676
  print("Attempting to load local data file...")
1677
  load_visitor_data()
1678
 
1679
- if not visitor_data_cache or len(visitor_data_cache) <= 1:
1680
  print("Local data file not found or is empty.")
1681
  if HF_TOKEN_READ:
1682
  print("Attempting to restore data from Hugging Face...")
@@ -1685,7 +1885,7 @@ if __name__ == '__main__':
1685
  else:
1686
  print("HF_TOKEN_READ not set. Cannot restore from backup. Starting fresh.")
1687
  else:
1688
- user_count = len([k for k in visitor_data_cache if k != 'organization_details'])
1689
  print(f"Successfully loaded data for {user_count} users from local file.")
1690
 
1691
  print("WARNING: The /admin route is NOT protected. Implement proper authentication for production.")
 
1
 
 
2
  import os
3
  from flask import Flask, request, Response, render_template_string, jsonify, redirect, url_for
4
  import hmac
 
96
  if "organization_details" not in visitor_data_cache:
97
  visitor_data_cache["organization_details"] = {}
98
 
99
+ if "bonus_program_settings" not in visitor_data_cache:
100
+ visitor_data_cache["bonus_program_settings"] = {
101
+ "invoice_bonus_percentage": 2,
102
+ "referral_promo_bonus": 50,
103
+ "referrer_first_purchase_percentage": 5
104
+ }
105
+
106
  def save_visitor_data():
107
  try:
108
  with open(DATA_FILE, 'w', encoding='utf-8') as f:
 
196
  --brand-black: #101010;
197
  --brand-red: #F44336;
198
  --brand-green: #4CAF50;
199
+ --brand-blue: #2196F3;
200
  --card-bg: #1c1c1e;
201
  --text-color: #ffffff;
202
  --text-secondary-color: #a0a0a0;
 
208
  --shadow-glow: 0 0 35px var(--shadow-color);
209
  --shadow-color-red: rgba(244, 67, 54, 0.15);
210
  --shadow-glow-red: 0 0 35px var(--shadow-color-red);
211
+ --shadow-color-blue: rgba(33, 150, 243, 0.15);
212
+ --shadow-glow-blue: 0 0 35px var(--shadow-color-blue);
213
  }
214
  * { box-sizing: border-box; margin: 0; padding: 0; }
215
  html, body {
 
252
  .content-section { display: none; flex-direction: column; gap: var(--padding-m); }
253
  .content-section.active { display: flex; }
254
  .card-grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--padding-m); }
255
+ .bonus-card, .debt-card, .promo-card, .referral-bonus-card {
256
  background: linear-gradient(145deg, #2a2a2a, #1c1c1c);
257
  border-radius: calc(var(--border-radius) + 8px); padding: var(--padding-l);
258
  text-align: center; position: relative; overflow: hidden;
259
  }
260
  .bonus-card { box-shadow: var(--shadow-glow); border: 1px solid rgba(255, 193, 7, 0.2); }
261
  .debt-card { box-shadow: var(--shadow-glow-red); border: 1px solid rgba(244, 67, 54, 0.2); }
262
+ .referral-bonus-card { grid-column: 1 / -1; box-shadow: var(--shadow-glow-blue); border: 1px solid rgba(33, 150, 243, 0.2); }
263
  .promo-card { grid-column: 1 / -1; border: 1px solid rgba(255,255,255,0.1); }
264
  .card-label { font-size: 1.1em; font-weight: 500; color: var(--text-secondary-color); margin-bottom: 12px; }
265
+ .bonus-amount, .debt-amount, .referral-bonus-amount { font-size: 3em; font-weight: 800; letter-spacing: -2px; line-height: 1; }
266
  .bonus-amount { color: var(--brand-yellow); }
267
  .debt-amount { color: var(--brand-red); }
268
+ .referral-bonus-amount { color: var(--brand-blue); }
269
  .client-id-card {
270
  background-color: var(--card-bg); border-radius: var(--border-radius);
271
  padding: var(--padding-m); display: flex; justify-content: space-between; align-items: center;
 
308
  .history-description, .invoice-description { font-size: 1em; font-weight: 500; }
309
  .history-date, .invoice-date { font-size: 0.8em; color: var(--text-secondary-color); margin-top: 4px; }
310
  .history-amount, .invoice-amount { font-size: 1.1em; font-weight: 700; }
311
+ .history-amount.bonus.accrual { color: var(--brand-green); }
312
+ .history-amount.bonus.deduction { color: var(--brand-red); }
313
+ .history-amount.debt.accrual { color: var(--brand-red); }
314
+ .history-amount.debt.payment { color: var(--brand-green); }
315
+ .history-amount.referral.accrual { color: var(--brand-blue); }
316
  .invoice-amount { color: var(--brand-yellow); }
317
  .no-history, .no-invoices { text-align: center; color: var(--text-secondary-color); padding: 2rem 0; }
318
  .business-card-item { margin-bottom: 10px; }
 
396
  <div class="debt-card">
397
  <p class="card-label">Ваш долг</p>
398
  <p class="debt-amount">{{ "%.2f"|format(user.debts|float) }}</p>
399
+ </div>
400
+ <div class="referral-bonus-card">
401
+ <p class="card-label">Бонусы с друзей</p>
402
+ <p class="referral-bonus-amount">{{ "%.2f"|format(user.referral_bonuses|float) }}</p>
403
  </div>
404
  <div class="promo-card">
405
  <p class="card-label">Ваш промокод для друзей</p>
 
425
  <span class="history-date">{{ item.date_str }}</span>
426
  </div>
427
  {% if item.transaction_type == 'bonus' %}
428
+ <span class="history-amount bonus {{ 'accrual' if item.type == 'accrual' else 'deduction' }}">
429
  {{ '+' if item.type == 'accrual' else '-' }}{{ "%.2f"|format(item.amount|float) }}
430
  </span>
431
  {% elif item.transaction_type == 'debt' %}
432
+ <span class="history-amount debt {{ 'payment' if item.type == 'payment' else 'accrual' }}">
433
+ {{ '-' if item.type == 'payment' else '+' }}{{ "%.2f"|format(item.amount|float) }}
434
+ </span>
435
+ {% elif item.transaction_type == 'referral' %}
436
+ <span class="history-amount referral accrual">
437
+ +{{ "%.2f"|format(item.amount|float) }}
438
  </span>
439
  {% endif %}
440
  </li>
 
723
  .summary-card .label { font-size: 0.9em; color: var(--admin-secondary); margin-top: 0.5rem; }
724
  .summary-card .value.bonus { color: var(--admin-primary-dark); }
725
  .summary-card .value.debt { color: var(--admin-danger); }
726
+ .summary-card .value.referral { color: var(--admin-info); }
727
  .controls-bar { display: flex; flex-wrap: wrap; gap: 1rem; align-items: center; background: var(--admin-card-bg); padding: var(--padding); border-radius: var(--border-radius); box-shadow: 0 4px 15px var(--admin-shadow); border: 1px solid var(--admin-border); margin-bottom: var(--padding); }
728
  .controls-bar input[type="text"] { flex-grow: 1; padding: 12px 15px; font-size: 1.1em; border-radius: 8px; border: 1px solid var(--admin-border); box-sizing: border-box; min-width: 250px; }
729
  .btn { padding: 12px 20px; font-size: 1em; border: none; border-radius: 8px; font-weight: 600; cursor: pointer; transition: background-color 0.2s ease; }
 
745
  }
746
  .referral-info .ref-by { color: var(--admin-success); }
747
  .referral-info .ref-count { color: var(--admin-info); font-weight: 600; }
748
+ .user-balances { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; text-align: center; margin-bottom: 1rem; margin-top: 1rem; }
749
  .user-balances .label { font-size: 0.9em; color: var(--admin-secondary); }
750
+ .user-balances .amount { font-size: 1.5em; font-weight: 700; }
751
+ .user-balances .amount.bonus { color: var(--admin-primary-dark); }
752
+ .user-balances .amount.debt { color: var(--admin-danger); }
753
+ .user-balances .amount.referral { color: var(--admin-info); }
754
  .user-actions { margin-top: auto; display: flex; flex-direction: column; gap: 0.5rem; }
755
  .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; }
756
  .btn-manage:hover { background-color: var(--admin-primary-dark); }
 
781
  .history-item .amount.bonus-deduction { color: var(--admin-danger); font-weight: 600; }
782
  .history-item .amount.debt-accrual { color: var(--admin-danger); font-weight: 600; }
783
  .history-item .amount.debt-payment { color: var(--admin-success); font-weight: 600; }
784
+ .history-item .amount.referral-accrual { color: var(--admin-info); font-weight: 600; }
785
  .modal-footer { margin-top: 1.5rem; display: flex; justify-content: flex-end; align-items: center; gap: 1rem;}
786
  .modal-footer button { padding: 12px 25px; font-size: 1.1em; border-radius: 8px; border: none; font-weight: 600; cursor: pointer; }
787
  .btn-submit { background-color: var(--admin-success); color: white; }
 
804
  .invoice-list-admin .invoice-amount { font-weight: 700; color: var(--admin-primary-dark); }
805
  .invoice-list-admin .view-btn { background: none; border: none; color: var(--admin-secondary); cursor: pointer; font-size: 0.9em; margin-left: 10px; }
806
  .invoice-list-admin .delete-btn { background: none; border: none; color: var(--admin-danger); cursor: pointer; font-size: 0.9em; margin-left: 5px; }
807
+ .details-form { display: flex; flex-direction: column; gap: 1rem; }
808
+ .details-form textarea { min-height: 80px; resize: vertical; }
809
+ .form-group-horizontal { display: flex; align-items: center; gap: 10px; }
810
+ .form-group-horizontal input { flex-grow: 1; }
811
+ .form-group-horizontal span { font-weight: 500; }
812
  </style>
813
  </head>
814
  <body>
 
828
  <div class="label">Всего долгов</div>
829
  </div>
830
  <div class="summary-card">
831
+ <div class="value referral">{{ "%.2f"|format(summary.total_referral_bonuses|float) }}</div>
832
+ <div class="label">Бонусов с друзей</div>
833
  </div>
834
  </div>
835
  <div class="controls-bar">
836
  <input type="text" id="searchInput" onkeyup="searchUsers()" placeholder="Поиск по имени, ID, username, номеру...">
837
  <button class="btn btn-primary" onclick="openAddClientModal()">Добавить клиента</button>
838
  <button class="btn btn-primary" onclick="openOrgSettingsModal()">Настройки организации</button>
839
+ <button class="btn btn-primary" onclick="openBonusSettingsModal()">Настройка бонусов</button>
840
  </div>
841
  {% if users %}
842
  <div class="user-grid" id="userGrid">
 
864
  <div class="label">Долг</div>
865
  <div class="amount debt">{{ "%.2f"|format(user.debts|float if user.debts else 0) }}</div>
866
  </div>
867
+ <div>
868
+ <div class="label">От друзей</div>
869
+ <div class="amount referral">{{ "%.2f"|format(user.referral_bonuses|float if user.referral_bonuses else 0) }}</div>
870
+ </div>
871
  </div>
872
  <div class="user-actions">
873
  <button class="btn-manage" onclick='openTransactionModal({{ user|tojson }})'>Управление счетом</button>
 
893
  <div class="tab-buttons">
894
  <button class="tab-btn active" data-tab="bonus-debt-tab">Бонусы и долги</button>
895
  <button class="tab-btn" data-tab="invoice-tab">Накладные</button>
896
+ <button class="tab-btn" data-tab="referral-history-tab">История от друзей</button>
897
  </div>
898
  <div id="bonus-debt-tab" class="tab-content active">
899
  <div class="form-section">
900
  <h3>Бонусы</h3>
901
  <div class="form-row">
902
  <div class="form-group">
903
+ <label for="accrueAmount">Начислить бонусов</label>
904
+ <input type="number" id="accrueAmount" placeholder="50" oninput="updateCalculations()">
905
  </div>
906
  <div class="form-group">
907
  <label for="deductAmount">Списать бонусов</label>
 
910
  </div>
911
  <div class="calculation-summary">
912
  <div class="summary-item"><span>Текущий баланс:</span> <strong id="summaryCurrentBalance">0.00</strong></div>
913
+ <div class="summary-item"><span>Будет начислено:</span> <strong id="summaryAccrual">+0.00</strong></div>
914
  <div class="summary-item"><span>Будет списано:</span> <strong id="summaryDeduction">-0.00</strong></div>
915
  <hr>
916
  <div class="summary-item"><strong>Итоговый баланс бонусов:</strong> <strong id="summaryFinalBalance">0.00</strong></div>
 
974
  <ul id="modalInvoiceList" class="invoice-list-admin"></ul>
975
  </div>
976
  </div>
977
+ <div id="referral-history-tab" class="tab-content">
978
+ <div class="history-container">
979
+ <h3>История бонусов от друзей</h3>
980
+ <ul id="modalReferralHistoryList" class="history-list"></ul>
981
+ </div>
982
+ </div>
983
  </div>
984
  </div>
985
  <div id="addClientModal" class="modal">
 
1008
  <div class="modal-header">
1009
  <h2>Настройки организации</h2>
1010
  </div>
1011
+ <div class="details-form">
1012
  <div class="form-group">
1013
  <label for="orgName">Название организации</label>
1014
  <input type="text" id="orgName" placeholder="Название вашей организации">
 
1035
  <button class="btn-submit" onclick="saveOrgSettings()">Сохранить настройки</button>
1036
  </div>
1037
  </div>
1038
+ </div>
1039
+ <div id="bonusSettingsModal" class="modal">
1040
+ <div class="modal-content">
1041
+ <span class="modal-close" onclick="closeModal('bonusSettingsModal')">×</span>
1042
+ <div class="modal-header">
1043
+ <h2>Настройка бонусной программы</h2>
1044
+ </div>
1045
+ <div class="details-form">
1046
+ <div class="form-group">
1047
+ <label for="settingInvoiceBonus">Процент бонусов с накладных</label>
1048
+ <div class="form-group-horizontal">
1049
+ <input type="number" step="0.1" id="settingInvoiceBonus" placeholder="2">
1050
+ <span>%</span>
1051
+ </div>
1052
+ </div>
1053
+ <div class="form-group">
1054
+ <label for="settingPromoBonus">Количество бонусов за ввод промокода</label>
1055
+ <input type="number" id="settingPromoBonus" placeholder="50">
1056
+ </div>
1057
+ <div class="form-group">
1058
+ <label for="settingReferrerBonus">Процент партнеру с первой покупки друга</label>
1059
+ <div class="form-group-horizontal">
1060
+ <input type="number" step="0.1" id="settingReferrerBonus" placeholder="5">
1061
+ <span>%</span>
1062
+ </div>
1063
+ </div>
1064
+ </div>
1065
+ <div class="modal-footer">
1066
+ <div id="bonusSettingsStatus" class="status-message"></div>
1067
+ <button class="btn-submit" onclick="saveBonusSettings()">Сохранить настройки</button>
1068
+ </div>
1069
+ </div>
1070
  </div>
1071
  <div id="adminInvoiceDetailModal" class="modal">
1072
  <div class="modal-content">
 
1083
  const transactionModal = document.getElementById('transactionModal');
1084
  const addClientModal = document.getElementById('addClientModal');
1085
  const orgSettingsModal = document.getElementById('orgSettingsModal');
1086
+ const bonusSettingsModal = document.getElementById('bonusSettingsModal');
1087
  const adminInvoiceDetailModal = document.getElementById('adminInvoiceDetailModal');
1088
  let currentUserData = null;
1089
  let newInvoiceItems = [];
 
1100
  document.getElementById('modalUserId').value = userData.id;
1101
  document.getElementById('modalUserName').textContent = `${userData.first_name || ''} ${userData.last_name || ''}`;
1102
  document.getElementById('modalUserUsername').textContent = `@${userData.username || userData.phone_number || ''} | ID: ${userData.id}`;
1103
+ ['accrueAmount', 'deductAmount', 'addDebtAmount', 'repayDebtAmount'].forEach(id => document.getElementById(id).value = '');
1104
  ['modalStatus', 'invoiceStatus'].forEach(id => document.getElementById(id).textContent = '');
1105
  newInvoiceItems = [];
1106
  renderNewInvoiceItems();
 
1114
  historyList.innerHTML = '';
1115
  const bonusHistory = (currentUserData.history || []).map(h => ({...h, transaction_type: 'bonus'}));
1116
  const debtHistory = (currentUserData.debt_history || []).map(h => ({...h, transaction_type: 'debt'}));
1117
+ const referralHistory = (currentUserData.referral_bonus_history || []).map(h => ({...h, transaction_type: 'referral'}));
1118
+ const combinedHistory = [...bonusHistory, ...debtHistory, ...referralHistory].sort((a, b) => new Date(b.date) - new Date(a.date));
1119
  if (combinedHistory.length > 0) {
1120
  combinedHistory.forEach(item => {
1121
  const li = document.createElement('li');
1122
  li.className = 'history-item';
1123
  let sign, amountClass, amountText;
1124
+ if (item.transaction_type === 'bonus') {
1125
  sign = item.type === 'accrual' ? '+' : '-';
1126
  amountClass = item.type === 'accrual' ? 'bonus-accrual' : 'bonus-deduction';
1127
+ } else if (item.transaction_type === 'debt') {
 
1128
  sign = item.type === 'accrual' ? '+' : '-';
1129
  amountClass = item.type === 'accrual' ? 'debt-accrual' : 'debt-payment';
1130
+ } else if (item.transaction_type === 'referral') {
1131
+ sign = '+';
1132
+ amountClass = 'referral-accrual';
1133
  }
1134
+ amountText = `${sign}${parseFloat(item.amount).toFixed(2)}`;
1135
  li.innerHTML = `<div><div class="desc">${item.description}</div><div class="date">${item.date_str}</div></div><div class="amount ${amountClass}">${amountText}</div>`;
1136
  historyList.appendChild(li);
1137
  });
1138
  } else {
1139
  historyList.innerHTML = '<li style="text-align:center; padding: 1rem; color: var(--admin-secondary);">Нет истории</li>';
1140
  }
1141
+
1142
+ const modalReferralHistoryList = document.getElementById('modalReferralHistoryList');
1143
+ modalReferralHistoryList.innerHTML = '';
1144
+ if (referralHistory.length > 0) {
1145
+ referralHistory.forEach(item => {
1146
+ const li = document.createElement('li');
1147
+ li.className = 'history-item';
1148
+ li.innerHTML = `<div><div class="desc">${item.description}</div><div class="date">${item.date_str}</div></div><div class="amount referral-accrual">+${parseFloat(item.amount).toFixed(2)}</div>`;
1149
+ modalReferralHistoryList.appendChild(li);
1150
+ });
1151
+ } else {
1152
+ modalReferralHistoryList.innerHTML = '<li style="text-align:center; padding: 1rem; color: var(--admin-secondary);">Нет бонусов от друзей.</li>';
1153
+ }
1154
+
1155
+
1156
  const modalInvoiceList = document.getElementById('modalInvoiceList');
1157
  modalInvoiceList.innerHTML = '';
1158
  const userInvoices = (currentUserData.invoices || []).sort((a, b) => new Date(b.date) - new Date(a.date));
 
1196
  });
1197
  }
1198
 
1199
+ function openBonusSettingsModal() {
1200
+ fetch('/admin/bonus_settings')
1201
+ .then(response => response.json())
1202
+ .then(data => {
1203
+ document.getElementById('settingInvoiceBonus').value = data.invoice_bonus_percentage || 0;
1204
+ document.getElementById('settingPromoBonus').value = data.referral_promo_bonus || 0;
1205
+ document.getElementById('settingReferrerBonus').value = data.referrer_first_purchase_percentage || 0;
1206
+ document.getElementById('bonusSettingsStatus').textContent = '';
1207
+ bonusSettingsModal.style.display = 'block';
1208
+ })
1209
+ .catch(error => {
1210
+ console.error('Error fetching bonus settings:', error);
1211
+ document.getElementById('bonusSettingsStatus').textContent = 'Ошибка загрузки настроек.';
1212
+ bonusSettingsModal.style.display = 'block';
1213
+ });
1214
+ }
1215
+
1216
  function closeModal(modalId) {
1217
  document.getElementById(modalId).style.display = 'none';
1218
  if (modalId === 'transactionModal') currentUserData = null;
 
1232
  function updateCalculations() {
1233
  if (!currentUserData) return;
1234
  const currentBalance = parseFloat(currentUserData.bonuses) || 0;
1235
+ const accrueAmount = parseFloat(document.getElementById('accrueAmount').value) || 0;
1236
  const deductAmount = parseFloat(document.getElementById('deductAmount').value) || 0;
1237
+ let finalDeductAmount = Math.min(deductAmount, currentBalance);
 
1238
  if (deductAmount > currentBalance) document.getElementById('deductAmount').value = finalDeductAmount > 0 ? finalDeductAmount.toFixed(2) : '';
1239
+ const finalBalance = currentBalance + accrueAmount - finalDeductAmount;
1240
  document.getElementById('summaryCurrentBalance').textContent = currentBalance.toFixed(2);
1241
+ document.getElementById('summaryAccrual').textContent = `+${accrueAmount.toFixed(2)}`;
1242
  document.getElementById('summaryDeduction').textContent = `-${finalDeductAmount.toFixed(2)}`;
1243
  document.getElementById('summaryFinalBalance').textContent = finalBalance.toFixed(2);
1244
  const currentDebt = parseFloat(currentUserData.debts) || 0;
1245
  const addDebtAmount = parseFloat(document.getElementById('addDebtAmount').value) || 0;
1246
  const repayDebtAmount = parseFloat(document.getElementById('repayDebtAmount').value) || 0;
1247
+ let finalRepayAmount = Math.min(repayDebtAmount, currentDebt);
1248
  if (repayDebtAmount > currentDebt) document.getElementById('repayDebtAmount').value = finalRepayAmount > 0 ? finalRepayAmount.toFixed(2) : '';
1249
  const finalDebt = currentDebt + addDebtAmount - finalRepayAmount;
1250
  document.getElementById('summaryCurrentDebt').textContent = currentDebt.toFixed(2);
 
1259
  statusEl.textContent = 'Обработка...';
1260
  const payload = {
1261
  user_id: document.getElementById('modalUserId').value,
1262
+ accrue_amount: parseFloat(document.getElementById('accrueAmount').value) || 0,
1263
  deduct_amount: parseFloat(document.getElementById('deductAmount').value) || 0,
1264
  add_debt_amount: parseFloat(document.getElementById('addDebtAmount').value) || 0,
1265
  repay_debt_amount: parseFloat(document.getElementById('repayDebtAmount').value) || 0,
 
1334
  if (response.ok) {
1335
  statusEl.style.color = 'var(--admin-success)';
1336
  statusEl.textContent = 'Настройки организации успешно сохранены!';
1337
+ setTimeout(() => closeModal('orgSettingsModal'), 1500);
1338
+ } else { throw new Error(result.message || 'Произошла ошибка'); }
1339
+ } catch (error) {
1340
+ statusEl.style.color = 'var(--admin-danger)';
1341
+ statusEl.textContent = `Ошибка: ${error.message}`;
1342
+ }
1343
+ }
1344
+
1345
+ async function saveBonusSettings() {
1346
+ const statusEl = document.getElementById('bonusSettingsStatus');
1347
+ statusEl.style.color = 'var(--admin-secondary)';
1348
+ statusEl.textContent = 'Сохранение...';
1349
+ const payload = {
1350
+ invoice_bonus_percentage: parseFloat(document.getElementById('settingInvoiceBonus').value) || 0,
1351
+ referral_promo_bonus: parseFloat(document.getElementById('settingPromoBonus').value) || 0,
1352
+ referrer_first_purchase_percentage: parseFloat(document.getElementById('settingReferrerBonus').value) || 0,
1353
+ };
1354
+ try {
1355
+ const response = await fetch('/admin/bonus_settings', {
1356
+ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload)
1357
+ });
1358
+ const result = await response.json();
1359
+ if (response.ok) {
1360
+ statusEl.style.color = 'var(--admin-success)';
1361
+ statusEl.textContent = 'Настройки бонусной программы сохранены!';
1362
+ setTimeout(() => closeModal('bonusSettingsModal'), 1500);
1363
  } else { throw new Error(result.message || 'Произошла ошибка'); }
1364
  } catch (error) {
1365
  statusEl.style.color = 'var(--admin-danger)';
 
1492
  if (event.target == transactionModal) closeModal('transactionModal');
1493
  if (event.target == addClientModal) closeModal('addClientModal');
1494
  if (event.target == orgSettingsModal) closeModal('orgSettingsModal');
1495
+ if (event.target == bonusSettingsModal) closeModal('bonusSettingsModal');
1496
  if (event.target == adminInvoiceDetailModal) closeModal('adminInvoiceDetailModal');
1497
  }
1498
 
 
1514
  is_first_visit = not user_data.get('has_been_welcomed', False)
1515
  bonus_history = user_data.get('history', [])
1516
  debt_history = user_data.get('debt_history', [])
1517
+ referral_history = user_data.get('referral_bonus_history', [])
1518
  for item in bonus_history: item['transaction_type'] = 'bonus'
1519
  for item in debt_history: item['transaction_type'] = 'debt'
1520
+ for item in referral_history: item['transaction_type'] = 'referral'
1521
+ user_data['combined_history'] = sorted(bonus_history + debt_history + referral_history, key=lambda x: datetime.fromisoformat(x['date']), reverse=True)
1522
  user_data['invoices'] = user_data.get('invoices', [])
1523
  else:
1524
+ user_data = {"id": "N/A", "bonuses": 0, "debts": 0, "referral_bonuses": 0, "combined_history": [], "invoices": [], "referral_code": "N/A"}
1525
 
1526
  org_details = visitor_data_cache.get('organization_details', {})
1527
  return render_template_string(TEMPLATE, user=user_data, org_details=org_details, is_first_visit=is_first_visit)
 
1568
  'is_premium': user_info_dict.get('is_premium', False), 'phone_number': None,
1569
  'visited_at': now.timestamp(), 'visited_at_str': now.strftime('%Y-%m-%d %H:%M:%S'),
1570
  'bonuses': 0, 'history': [], 'debts': 0, 'debt_history': [], 'invoices': [],
1571
+ 'referral_code': f'PROMO{new_user_id}', 'referred_by': None, 'referrals': [], 'has_been_welcomed': False,
1572
+ 'referral_bonuses': 0, 'referral_bonus_history': [], 'has_made_first_purchase': False,
1573
  }
1574
  visitor_data_cache[new_user_id] = user_entry
1575
  user_id_to_save = new_user_id
 
1601
  return jsonify({"status": "ok", "message": "Вы уже прошли этот шаг."}), 200
1602
 
1603
  user['has_been_welcomed'] = True
1604
+ bonus_settings = visitor_data_cache.get('bonus_program_settings', {})
1605
+ promo_bonus = float(bonus_settings.get('referral_promo_bonus', 0))
1606
 
1607
  if referral_code:
1608
+ referrer = next((u for u_id, u in visitor_data_cache.items() if u_id not in ["organization_details", "bonus_program_settings"] and u.get('referral_code') == referral_code), None)
1609
 
1610
  if not referrer:
1611
  return jsonify({"status": "error", "message": "Промокод не найден."}), 404
 
1615
  user['referred_by'] = referrer['id']
1616
  if 'referrals' not in referrer: referrer['referrals'] = []
1617
  referrer['referrals'].append(user_id)
1618
+
1619
+ if promo_bonus > 0:
1620
+ user['bonuses'] = user.get('bonuses', 0) + promo_bonus
1621
+ now = datetime.now(BISHKEK_TZ)
1622
+ history_entry = {
1623
+ "type": "accrual", "amount": promo_bonus, "description": "Бонус за промокод",
1624
+ "date": now.isoformat(), "date_str": now.strftime('%Y-%m-%d %H:%M:%S')
1625
+ }
1626
+ if 'history' not in user: user['history'] = []
1627
+ user['history'].append(history_entry)
1628
 
1629
  save_visitor_data()
1630
  message = "Промокод успешно применен!" if referral_code else "Добро пожаловать!"
 
1637
  @app.route('/admin')
1638
  def admin_panel():
1639
  users_list = []
1640
+ user_name_map = {uid: f"{ud.get('first_name', '')} {ud.get('last_name', '')}".strip() or f"ID: {uid}" for uid, ud in visitor_data_cache.items() if uid not in ["organization_details", "bonus_program_settings"]}
1641
 
1642
  for user_id, user_data in visitor_data_cache.items():
1643
+ if user_id in ["organization_details", "bonus_program_settings"]: continue
1644
  user_data_copy = user_data.copy()
1645
  user_data_copy['id'] = user_id
1646
  user_data_copy['referrals_count'] = len(user_data_copy.get('referrals', []))
 
1651
  total_users = len(users_list)
1652
  total_bonuses = sum(u.get('bonuses', 0) for u in users_list)
1653
  total_debts = sum(u.get('debts', 0) for u in users_list)
1654
+ total_referral_bonuses = sum(u.get('referral_bonuses', 0) for u in users_list)
1655
 
1656
  summary_stats = {
1657
  "total_users": total_users, "total_bonuses": total_bonuses,
1658
+ "total_debts": total_debts, "total_referral_bonuses": total_referral_bonuses
1659
  }
1660
  return render_template_string(ADMIN_TEMPLATE, users=users_list, summary=summary_stats)
1661
 
 
1669
  return jsonify({"status": "error", "message": "Имя и н��мер телефона обязательны."}), 400
1670
 
1671
  with _data_lock:
1672
+ if any(u.get('phone_number') == phone_number for k, u in visitor_data_cache.items() if k not in ["organization_details", "bonus_program_settings"]):
1673
  return jsonify({"status": "error", "message": "Клиент с таким номером телефона уже существует."}), 409
1674
 
1675
  now = datetime.now(BISHKEK_TZ)
 
1679
  'username': None, 'photo_url': None, 'is_premium': False, 'phone_number': phone_number,
1680
  'visited_at': now.timestamp(), 'visited_at_str': now.strftime('%Y-%m-%d %H:%M:%S'),
1681
  'bonuses': 0, 'history': [], 'debts': 0, 'debt_history': [], 'invoices': [],
1682
+ 'referral_code': f'PROMO{new_id}', 'referred_by': None, 'referrals': [], 'has_been_welcomed': True,
1683
+ 'referral_bonuses': 0, 'referral_bonus_history': [], 'has_made_first_purchase': False,
1684
  }
1685
  visitor_data_cache[new_id] = new_client
1686
  save_visitor_data()
 
1694
  try:
1695
  data = request.get_json()
1696
  user_id = str(data.get('user_id'))
1697
+ accrue_amount = float(data.get('accrue_amount', 0))
1698
  deduct_amount = float(data.get('deduct_amount', 0))
1699
  add_debt_amount = float(data.get('add_debt_amount', 0))
1700
  repay_debt_amount = float(data.get('repay_debt_amount', 0))
 
1708
  if deduct_amount > user.get('bonuses', 0): return jsonify({"status": "error", "message": "Недостаточно бонусов для списания"}), 400
1709
  if repay_debt_amount > user.get('debts', 0): return jsonify({"status": "error", "message": "Сумма погашения превышает текущий долг"}), 400
1710
 
1711
+ user['bonuses'] = round(user.get('bonuses', 0) + accrue_amount - deduct_amount, 2)
 
1712
  if 'history' not in user: user['history'] = []
1713
+ if accrue_amount > 0: user['history'].append({"type": "accrual", "amount": round(accrue_amount, 2), "description": f"Начисление бонусов", "date": now_iso, "date_str": now_str})
1714
  if deduct_amount > 0: user['history'].append({"type": "deduction", "amount": round(deduct_amount, 2), "description": "Списание бонусов", "date": now_iso, "date_str": now_str})
1715
 
1716
  user['debts'] = round(user.get('debts', 0) + add_debt_amount - repay_debt_amount, 2)
 
1744
  new_invoice = {"invoice_id": invoice_id, "date": now_iso, "date_str": now_str, "total_amount": round(total_amount, 2), "items": processed_items}
1745
  if 'invoices' not in user: user['invoices'] = []
1746
  user['invoices'].append(new_invoice)
1747
+
1748
+ bonus_settings = visitor_data_cache.get('bonus_program_settings', {})
1749
+ invoice_bonus_percentage = float(bonus_settings.get('invoice_bonus_percentage', 0))
1750
+ if invoice_bonus_percentage > 0 and total_amount > 0:
1751
+ bonus_from_invoice = round((total_amount * invoice_bonus_percentage) / 100, 2)
1752
+ if bonus_from_invoice > 0:
1753
+ user['bonuses'] = user.get('bonuses', 0) + bonus_from_invoice
1754
+ if 'history' not in user: user['history'] = []
1755
+ user['history'].append({"type": "accrual", "amount": bonus_from_invoice, "description": f"Бонус с накладной #{invoice_id}", "date": now_iso, "date_str": now_str})
1756
+
1757
+ if not user.get('has_made_first_purchase', False) and total_amount > 0:
1758
+ user['has_made_first_purchase'] = True
1759
+ referrer_id = user.get('referred_by')
1760
+ if referrer_id and referrer_id in visitor_data_cache:
1761
+ referrer = visitor_data_cache[referrer_id]
1762
+ referrer_bonus_percentage = float(bonus_settings.get('referrer_first_purchase_percentage', 0))
1763
+ if referrer_bonus_percentage > 0:
1764
+ commission = round((total_amount * referrer_bonus_percentage) / 100, 2)
1765
+ if commission > 0:
1766
+ referrer['referral_bonuses'] = referrer.get('referral_bonuses', 0) + commission
1767
+ if 'referral_bonus_history' not in referrer: referrer['referral_bonus_history'] = []
1768
+ referrer['referral_bonus_history'].append({
1769
+ "type": "accrual", "amount": commission,
1770
+ "description": f"Бонус от друга {user.get('first_name', 'ID:'+user_id)}",
1771
+ "date": now_iso, "date_str": now_str
1772
+ })
1773
+
1774
  save_visitor_data()
1775
  return jsonify({"status": "ok", "message": "Invoice added successfully", "invoice_id": invoice_id}), 200
1776
  except Exception as e:
 
1845
  logging.exception("Error saving organization details")
1846
  return jsonify({"status": "error", "message": str(e)}), 500
1847
 
1848
+ @app.route('/admin/bonus_settings', methods=['GET'])
1849
+ def get_bonus_settings():
1850
+ try:
1851
+ return jsonify(visitor_data_cache.get('bonus_program_settings', {})), 200
1852
+ except Exception as e:
1853
+ logging.exception("Error getting bonus settings")
1854
+ return jsonify({"status": "error", "message": str(e)}), 500
1855
+
1856
+ @app.route('/admin/bonus_settings', methods=['POST'])
1857
+ def save_bonus_settings():
1858
+ try:
1859
+ data = request.get_json()
1860
+ with _data_lock:
1861
+ settings = visitor_data_cache.get('bonus_program_settings', {})
1862
+ settings['invoice_bonus_percentage'] = float(data.get('invoice_bonus_percentage', 0))
1863
+ settings['referral_promo_bonus'] = float(data.get('referral_promo_bonus', 0))
1864
+ settings['referrer_first_purchase_percentage'] = float(data.get('referrer_first_purchase_percentage', 0))
1865
+ visitor_data_cache['bonus_program_settings'] = settings
1866
+ save_visitor_data()
1867
+ return jsonify({"status": "ok", "message": "Bonus program settings saved"}), 200
1868
+ except Exception as e:
1869
+ logging.exception("Error saving bonus settings")
1870
+ return jsonify({"status": "error", "message": str(e)}), 500
1871
+
1872
  if __name__ == '__main__':
1873
  print("--- BONUS SYSTEM SERVER ---")
1874
  print(f"Server starting on http://{HOST}:{PORT}")
 
1876
  print("Attempting to load local data file...")
1877
  load_visitor_data()
1878
 
1879
+ if not visitor_data_cache or len(visitor_data_cache) <= 2:
1880
  print("Local data file not found or is empty.")
1881
  if HF_TOKEN_READ:
1882
  print("Attempting to restore data from Hugging Face...")
 
1885
  else:
1886
  print("HF_TOKEN_READ not set. Cannot restore from backup. Starting fresh.")
1887
  else:
1888
+ user_count = len([k for k in visitor_data_cache if k not in ['organization_details', 'bonus_program_settings']])
1889
  print(f"Successfully loaded data for {user_count} users from local file.")
1890
 
1891
  print("WARNING: The /admin route is NOT protected. Implement proper authentication for production.")