Kgshop commited on
Commit
568e5ba
·
verified ·
1 Parent(s): 2ae3775

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +166 -106
app.py CHANGED
@@ -260,7 +260,13 @@ TEMPLATE = """
260
  background-color: var(--tg-theme-secondary-bg-color);
261
  border-radius: var(--border-radius-m); padding: 20px;
262
  }
263
- .promo-card .card-label { font-size: 1em; font-weight: 500; color: var(--tg-theme-hint-color); margin-bottom: 12px; text-align: center; }
 
 
 
 
 
 
264
  .promo-code-display {
265
  display: flex; align-items: center; justify-content: center; gap: 12px;
266
  background-color: rgba(0,0,0,0.2); border-radius: 12px; padding: 12px;
@@ -330,7 +336,7 @@ TEMPLATE = """
330
  max-height: 90vh; overflow-y: auto; animation: slideUp 0.3s ease-out;
331
  }
332
  @keyframes slideUp { from { transform: translateY(100%); } to { transform: translateY(0); } }
333
- .modal-close {
334
  position: absolute; top: 12px; left: 50%; transform: translateX(-50%);
335
  width: 40px; height: 5px; background-color: var(--tg-theme-hint-color);
336
  border-radius: 3px; cursor: pointer;
@@ -345,11 +351,12 @@ TEMPLATE = """
345
  .item-total { font-weight: 700; flex-basis: 20%; text-align: right; color: var(--tg-theme-button-color); }
346
  .invoice-total-display {
347
  padding-top: var(--padding-m); border-top: 1px solid rgba(255,255,255,0.2); margin-top: var(--padding-m);
348
- display: flex; justify-content: space-between; font-size: 1.2em; font-weight: 700;
349
  }
350
- #promoCodeModal .modal-content { align-items: center; text-align: center; padding: 40px 24px 24px; }
351
- #promoCodeModal h2 { margin-bottom: 12px; }
352
- #promoCodeModal p { color: var(--tg-theme-hint-color); margin-bottom: 20px; }
 
353
  #promoCodeModal input {
354
  width: 100%; padding: 16px; margin-bottom: 16px; font-size: 1.2em;
355
  background-color: var(--tg-theme-bg-color); border: 1px solid rgba(255,255,255,0.1);
@@ -357,11 +364,11 @@ TEMPLATE = """
357
  text-align: center; letter-spacing: 2px;
358
  }
359
  #promoCodeModal .promo-modal-actions { display: flex; gap: 1rem; width: 100%; }
360
- #promoCodeModal button {
361
  flex-grow: 1; padding: 16px; font-size: 1em; font-weight: 700; border: none;
362
  border-radius: var(--border-radius-m); cursor: pointer; transition: all 0.2s;
363
  }
364
- .btn-apply-promo { background-color: var(--tg-theme-button-color); color: var(--tg-theme-button-text-color); }
365
  .btn-skip-promo { background-color: var(--tg-theme-secondary-bg-color); color: var(--tg-theme-text-color); }
366
  #promoStatus { margin-top: 1rem; font-weight: 500; min-height: 20px; }
367
  </style>
@@ -393,7 +400,10 @@ TEMPLATE = """
393
  </div>
394
  </section>
395
  <section class="promo-card">
396
- <p class="card-label">Ваш промокод для друзей</p>
 
 
 
397
  <div class="promo-code-display">
398
  <span class="promo-code-value" id="userPromoCode">{{ user.referral_code }}</span>
399
  <button class="copy-btn" onclick="copyPromoCode()">Копировать</button>
@@ -439,8 +449,8 @@ TEMPLATE = """
439
  <span class="history-description">{{ item.description }}</span>
440
  <span class="history-date">{{ item.date_str }}</span>
441
  </div>
442
- <span class="history-amount referral">
443
- +{{ "%.2f"|format(item.amount|float) }}
444
  </span>
445
  {% endif %}
446
  </li>
@@ -529,13 +539,19 @@ TEMPLATE = """
529
  </div>
530
  <div id="invoiceDetailModal" class="modal">
531
  <div class="modal-content">
532
- <div class="modal-close" onclick="closeModal('invoiceDetailModal')"></div>
533
  <h2 id="invoiceDetailTitle" class="modal-title"></h2>
534
  <ul id="invoiceDetailList" class="invoice-detail-list"></ul>
535
- <div id="invoiceDetailTotal" class="invoice-total-display">
536
- <span>Итого:</span>
537
- <span id="invoiceTotalAmount">0.00</span>
538
- </div>
 
 
 
 
 
 
539
  </div>
540
  </div>
541
  {% if is_first_visit %}
@@ -637,7 +653,16 @@ TEMPLATE = """
637
  li.innerHTML = `<span class="item-name">${item.product_name}</span><span class="item-qty-price">${item.quantity} x ${parseFloat(item.unit_price).toFixed(2)}</span><span class="item-total">${parseFloat(item.item_total).toFixed(2)}</span>`;
638
  invoiceDetailList.appendChild(li);
639
  });
640
- document.getElementById('invoiceTotalAmount').textContent = parseFloat(invoiceData.total_amount).toFixed(2);
 
 
 
 
 
 
 
 
 
641
  openModal('invoiceDetailModal');
642
  }
643
 
@@ -786,7 +811,9 @@ ADMIN_TEMPLATE = """
786
  .history-item .amount.bonus-deduction { color: var(--admin-danger); font-weight: 600; }
787
  .history-item .amount.debt-accrual { color: var(--admin-danger); font-weight: 600; }
788
  .history-item .amount.debt-payment { color: var(--admin-success); font-weight: 600; }
789
- .history-item .amount.referral-accrual { color: var(--admin-info); font-weight: 600; }
 
 
790
  .modal-footer { margin-top: 1.5rem; display: flex; justify-content: flex-end; align-items: center; gap: 1rem;}
791
  .modal-footer button { padding: 12px 25px; font-size: 1.1em; border-radius: 8px; border: none; font-weight: 600; cursor: pointer; }
792
  .btn-submit { background-color: var(--admin-success); color: white; }
@@ -821,7 +848,11 @@ ADMIN_TEMPLATE = """
821
  .item-name { flex-basis: 60%; }
822
  .item-qty-price { flex-basis: 20%; text-align: right; color: #6c757d; }
823
  .item-total { flex-basis: 20%; text-align: right; font-weight: bold; }
824
- .invoice-total-display { margin-top: 1rem; padding-top: 1rem; border-top: 1px solid #dee2e6; display: flex; justify-content: space-between; font-size: 1.2em; font-weight: bold; }
 
 
 
 
825
  </style>
826
  </head>
827
  <body>
@@ -904,29 +935,39 @@ ADMIN_TEMPLATE = """
904
  </div>
905
  <input type="hidden" id="modalUserId">
906
  <div class="tab-buttons">
907
- <button class="tab-btn active" data-tab="bonus-debt-tab">Бонусы и долги</button>
908
  <button class="tab-btn" data-tab="invoice-tab">Накладные</button>
909
- <button class="tab-btn" data-tab="referral-history-tab">История от друзей</button>
910
  </div>
911
  <div id="bonus-debt-tab" class="tab-content active">
912
- <div class="form-section">
913
- <h3>Бонусы</h3>
914
  <div class="form-row">
915
  <div class="form-group">
916
- <label for="accrueAmount">Начислить бонусов</label>
917
- <input type="number" id="accrueAmount" placeholder="50" oninput="updateCalculations()">
918
  </div>
919
  <div class="form-group">
920
- <label for="deductAmount">Списать бонусов</label>
921
- <input type="number" id="deductAmount" placeholder="100" oninput="updateCalculations()">
922
  </div>
923
  </div>
924
  <div class="calculation-summary">
925
  <div class="summary-item"><span>Текущий баланс:</span> <strong id="summaryCurrentBalance">0.00</strong></div>
926
- <div class="summary-item"><span>Будет начислено:</span> <strong id="summaryAccrual">+0.00</strong></div>
927
- <div class="summary-item"><span>Будет списано:</span> <strong id="summaryDeduction">-0.00</strong></div>
928
- <hr>
929
- <div class="summary-item"><strong>Итоговый баланс бонусов:</strong> <strong id="summaryFinalBalance">0.00</strong></div>
 
 
 
 
 
 
 
 
 
 
930
  </div>
931
  </div>
932
  <div class="form-section">
@@ -934,25 +975,18 @@ ADMIN_TEMPLATE = """
934
  <div class="form-row">
935
  <div class="form-group">
936
  <label for="addDebtAmount">Добавить долг</label>
937
- <input type="number" id="addDebtAmount" placeholder="500" oninput="updateCalculations()">
938
  </div>
939
  <div class="form-group">
940
  <label for="repayDebtAmount">Погасить долг</label>
941
- <input type="number" id="repayDebtAmount" placeholder="200" oninput="updateCalculations()">
942
  </div>
943
  </div>
944
  <div class="calculation-summary">
945
  <div class="summary-item"><span>Текущий долг:</span> <strong id="summaryCurrentDebt">0.00</strong></div>
946
- <div class="summary-item"><span>Будет добавлено:</span> <strong id="summaryAddDebt">+0.00</strong></div>
947
- <div class="summary-item"><span>Будет погашено:</span> <strong id="summaryRepayDebt">-0.00</strong></div>
948
- <hr>
949
  <div class="summary-item"><strong>Итоговый долг:</strong> <strong id="summaryFinalDebt">0.00</strong></div>
950
  </div>
951
  </div>
952
- <div class="history-container">
953
- <h3>Общая история операций</h3>
954
- <ul id="modalHistoryList" class="history-list"></ul>
955
- </div>
956
  <div class="modal-footer">
957
  <div id="modalStatus" class="status-message"></div>
958
  <button class="btn-submit" onclick="submitTransaction()">Провести операцию</button>
@@ -980,9 +1014,13 @@ ADMIN_TEMPLATE = """
980
  </div>
981
  <div class="form-section">
982
  <h3>Оплата бонусами</h3>
983
- <p>Доступно бонусов для списания: <strong id="invoiceAvailableBonuses">0.00</strong></p>
 
 
 
 
984
  <div class="form-group">
985
- <label for="invoiceDeductBonuses">Списать бонусов (не более суммы накладной)</label>
986
  <input type="number" id="invoiceDeductBonuses" oninput="updateNewInvoiceTotal()" placeholder="0.00" step="0.01">
987
  </div>
988
  <div class="invoice-section-summary">
@@ -993,15 +1031,15 @@ ADMIN_TEMPLATE = """
993
  <div id="invoiceStatus" class="status-message"></div>
994
  <button class="btn-submit" onclick="submitInvoice()">Сохранить накладную</button>
995
  </div>
996
- <div class="history-container">
997
  <h3>История накладных клиента</h3>
998
  <ul id="modalInvoiceList" class="invoice-list-admin"></ul>
999
  </div>
1000
  </div>
1001
- <div id="referral-history-tab" class="tab-content">
1002
  <div class="history-container">
1003
- <h3>История бонусов от друзей</h3>
1004
- <ul id="modalReferralHistoryList" class="history-list"></ul>
1005
  </div>
1006
  </div>
1007
  </div>
@@ -1124,10 +1162,8 @@ ADMIN_TEMPLATE = """
1124
  document.getElementById('modalUserId').value = userData.id;
1125
  document.getElementById('modalUserName').textContent = `${userData.first_name || ''} ${userData.last_name || ''}`;
1126
  document.getElementById('modalUserUsername').textContent = `@${userData.username || userData.phone_number || ''} | ID: ${userData.id}`;
1127
- ['accrueAmount', 'deductAmount', 'addDebtAmount', 'repayDebtAmount'].forEach(id => document.getElementById(id).value = '');
1128
  ['modalStatus', 'invoiceStatus'].forEach(id => document.getElementById(id).textContent = '');
1129
- document.getElementById('invoiceDeductBonuses').value = '';
1130
- document.getElementById('invoiceAvailableBonuses').textContent = (parseFloat(userData.bonuses) || 0).toFixed(2);
1131
 
1132
  newInvoiceItems = [];
1133
  renderNewInvoiceItems();
@@ -1152,11 +1188,11 @@ ADMIN_TEMPLATE = """
1152
  sign = item.type === 'accrual' ? '+' : '-';
1153
  amountClass = item.type === 'accrual' ? 'bonus-accrual' : 'bonus-deduction';
1154
  } else if (item.transaction_type === 'debt') {
1155
- sign = item.type === 'accrual' ? '+' : '-';
1156
- amountClass = item.type === 'accrual' ? 'debt-accrual' : 'debt-payment';
1157
  } else if (item.transaction_type === 'referral') {
1158
- sign = '+';
1159
- amountClass = 'referral-accrual';
1160
  }
1161
  amountText = `${sign}${parseFloat(item.amount).toFixed(2)}`;
1162
  li.innerHTML = `<div><div class="desc">${item.description}</div><div class="date">${item.date_str}</div></div><div class="amount ${amountClass}">${amountText}</div>`;
@@ -1165,20 +1201,6 @@ ADMIN_TEMPLATE = """
1165
  } else {
1166
  historyList.innerHTML = '<li style="text-align:center; padding: 1rem; color: var(--admin-secondary);">Нет истории</li>';
1167
  }
1168
-
1169
- const modalReferralHistoryList = document.getElementById('modalReferralHistoryList');
1170
- modalReferralHistoryList.innerHTML = '';
1171
- if (referralHistory.length > 0) {
1172
- referralHistory.forEach(item => {
1173
- const li = document.createElement('li');
1174
- li.className = 'history-item';
1175
- 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>`;
1176
- modalReferralHistoryList.appendChild(li);
1177
- });
1178
- } else {
1179
- modalReferralHistoryList.innerHTML = '<li style="text-align:center; padding: 1rem; color: var(--admin-secondary);">Нет бонусов от друзей.</li>';
1180
- }
1181
-
1182
 
1183
  const modalInvoiceList = document.getElementById('modalInvoiceList');
1184
  modalInvoiceList.innerHTML = '';
@@ -1260,23 +1282,31 @@ ADMIN_TEMPLATE = """
1260
  if (!currentUserData) return;
1261
  const currentBalance = parseFloat(currentUserData.bonuses) || 0;
1262
  const accrueAmount = parseFloat(document.getElementById('accrueAmount').value) || 0;
1263
- const deductAmount = parseFloat(document.getElementById('deductAmount').value) || 0;
 
1264
  let finalDeductAmount = Math.min(deductAmount, currentBalance);
1265
- if (deductAmount > currentBalance) document.getElementById('deductAmount').value = finalDeductAmount > 0 ? finalDeductAmount.toFixed(2) : '';
1266
  const finalBalance = currentBalance + accrueAmount - finalDeductAmount;
1267
  document.getElementById('summaryCurrentBalance').textContent = currentBalance.toFixed(2);
1268
- document.getElementById('summaryAccrual').textContent = `+${accrueAmount.toFixed(2)}`;
1269
- document.getElementById('summaryDeduction').textContent = `-${finalDeductAmount.toFixed(2)}`;
1270
  document.getElementById('summaryFinalBalance').textContent = finalBalance.toFixed(2);
 
 
 
 
 
 
 
 
 
 
1271
  const currentDebt = parseFloat(currentUserData.debts) || 0;
1272
  const addDebtAmount = parseFloat(document.getElementById('addDebtAmount').value) || 0;
1273
- const repayDebtAmount = parseFloat(document.getElementById('repayDebtAmount').value) || 0;
 
1274
  let finalRepayAmount = Math.min(repayDebtAmount, currentDebt);
1275
- if (repayDebtAmount > currentDebt) document.getElementById('repayDebtAmount').value = finalRepayAmount > 0 ? finalRepayAmount.toFixed(2) : '';
1276
  const finalDebt = currentDebt + addDebtAmount - finalRepayAmount;
1277
  document.getElementById('summaryCurrentDebt').textContent = currentDebt.toFixed(2);
1278
- document.getElementById('summaryAddDebt').textContent = `+${addDebtAmount.toFixed(2)}`;
1279
- document.getElementById('summaryRepayDebt').textContent = `-${finalRepayAmount.toFixed(2)}`;
1280
  document.getElementById('summaryFinalDebt').textContent = finalDebt.toFixed(2);
1281
  }
1282
 
@@ -1288,6 +1318,7 @@ ADMIN_TEMPLATE = """
1288
  user_id: document.getElementById('modalUserId').value,
1289
  accrue_amount: parseFloat(document.getElementById('accrueAmount').value) || 0,
1290
  deduct_amount: parseFloat(document.getElementById('deductAmount').value) || 0,
 
1291
  add_debt_amount: parseFloat(document.getElementById('addDebtAmount').value) || 0,
1292
  repay_debt_amount: parseFloat(document.getElementById('repayDebtAmount').value) || 0,
1293
  };
@@ -1411,7 +1442,12 @@ ADMIN_TEMPLATE = """
1411
  const newRow = tableBody.insertRow();
1412
  const rowIndex = tableBody.rows.length - 1;
1413
  newInvoiceItems.push({ product_name: '', quantity: 1, unit_price: 0, item_total: 0 });
1414
- newRow.innerHTML = `<td><input type="text" placeholder="Название товара" oninput="updateInvoiceItem(${rowIndex}, 'product_name', this.value)"></td><td><input type="number" step="1" min="1" value="1" placeholder="1" oninput="updateInvoiceItem(${rowIndex}, 'quantity', parseFloat(this.value))"></td><td><input type="number" step="0.01" min="0" placeholder="0.00" oninput="updateInvoiceItem(${rowIndex}, 'unit_price', parseFloat(this.value))"></td><td class="item-total-display">0.00</td><td><button class="action-btn" onclick="removeInvoiceItemRow(this, ${rowIndex})">🗑️</button></td>`;
 
 
 
 
 
1415
  }
1416
 
1417
  function updateInvoiceItem(index, field, value) {
@@ -1419,25 +1455,17 @@ ADMIN_TEMPLATE = """
1419
  newInvoiceItems[index][field] = value;
1420
  const qty = parseFloat(newInvoiceItems[index].quantity) || 0;
1421
  const price = parseFloat(newInvoiceItems[index].unit_price) || 0;
1422
- const itemTotal = qty * price;
1423
- newInvoiceItems[index].item_total = itemTotal;
1424
- const tableBody = document.getElementById('newInvoiceItemsTable').getElementsByTagName('tbody')[0];
1425
- tableBody.rows[index].querySelector('.item-total-display').textContent = itemTotal.toFixed(2);
1426
- updateNewInvoiceTotal();
1427
  }
1428
  }
1429
-
1430
- function removeInvoiceItemRow(button, index) {
1431
- newInvoiceItems.splice(index, 1);
1432
- renderNewInvoiceItems();
1433
- }
1434
 
1435
  function renderNewInvoiceItems() {
1436
  const tableBody = document.getElementById('newInvoiceItemsTable').getElementsByTagName('tbody')[0];
1437
  tableBody.innerHTML = '';
1438
  newInvoiceItems.forEach((item, index) => {
1439
  const newRow = tableBody.insertRow();
1440
- newRow.innerHTML = `<td><input type="text" placeholder="Название товара" value="${item.product_name}" oninput="updateInvoiceItem(${index}, 'product_name', this.value)"></td><td><input type="number" step="1" min="1" placeholder="1" value="${item.quantity || '1'}" oninput="updateInvoiceItem(${index}, 'quantity', parseFloat(this.value))"></td><td><input type="number" step="0.01" min="0" placeholder="0.00" value="${item.unit_price || ''}" oninput="updateInvoiceItem(${index}, 'unit_price', parseFloat(this.value))"></td><td class="item-total-display">${(item.item_total || 0).toFixed(2)}</td><td><button class="action-btn" onclick="removeInvoiceItemRow(this, ${index})">🗑️</button></td>`;
1441
  });
1442
  updateNewInvoiceTotal();
1443
  }
@@ -1446,7 +1474,12 @@ ADMIN_TEMPLATE = """
1446
  let total = newInvoiceItems.reduce((sum, item) => sum + (parseFloat(item.item_total) || 0), 0);
1447
  document.getElementById('newInvoiceTotalAmount').textContent = total.toFixed(2);
1448
 
1449
- const availableBonuses = parseFloat(currentUserData.bonuses) || 0;
 
 
 
 
 
1450
  const deductBonusesInput = document.getElementById('invoiceDeductBonuses');
1451
  let deductAmount = parseFloat(deductBonusesInput.value) || 0;
1452
 
@@ -1476,11 +1509,13 @@ ADMIN_TEMPLATE = """
1476
  }
1477
  const totalAmount = itemsToAdd.reduce((sum, item) => sum + item.item_total, 0);
1478
  const deductBonuses = parseFloat(document.getElementById('invoiceDeductBonuses').value) || 0;
 
1479
  const payload = {
1480
  user_id: currentUserData.id,
1481
  total_amount: totalAmount,
1482
  items: itemsToAdd,
1483
- deduct_bonuses: deductBonuses
 
1484
  };
1485
  try {
1486
  const response = await fetch('/admin/add_invoice', {
@@ -1513,11 +1548,12 @@ ADMIN_TEMPLATE = """
1513
  let bonusesDeducted = parseFloat(invoiceData.bonuses_deducted || 0);
1514
  let totalAmount = parseFloat(invoiceData.total_amount);
1515
  let finalAmount = totalAmount - bonusesDeducted;
 
1516
 
1517
- let totalHTML = `<span>Итого:</span><span>${totalAmount.toFixed(2)}</span>`;
1518
  if (bonusesDeducted > 0) {
1519
- totalHTML += `<br><span>Списано бонусов:</span><span style="color: var(--admin-danger);">- ${bonusesDeducted.toFixed(2)}</span>`;
1520
- totalHTML += `<hr style="border: none; border-top: 1px solid #ccc; margin: 5px 0;"><span>К оплате:</span><span style="color: var(--admin-success);">${finalAmount.toFixed(2)}</span>`;
1521
  }
1522
  totalDisplay.innerHTML = totalHTML;
1523
  adminInvoiceDetailModal.style.display = 'block';
@@ -1571,7 +1607,8 @@ def index():
1571
  user_data = {"id": "N/A", "bonuses": 0, "debts": 0, "referral_bonuses": 0, "combined_history": [], "invoices": [], "referral_code": "N/A"}
1572
 
1573
  org_details = visitor_data_cache.get('organization_details', {})
1574
- return render_template_string(TEMPLATE, user=user_data, org_details=org_details, is_first_visit=is_first_visit)
 
1575
 
1576
  @app.route('/verify', methods=['POST'])
1577
  def verify_data():
@@ -1743,8 +1780,10 @@ def add_transaction():
1743
  user_id = str(data.get('user_id'))
1744
  accrue_amount = float(data.get('accrue_amount', 0))
1745
  deduct_amount = float(data.get('deduct_amount', 0))
 
1746
  add_debt_amount = float(data.get('add_debt_amount', 0))
1747
  repay_debt_amount = float(data.get('repay_debt_amount', 0))
 
1748
  if not user_id: return jsonify({"status": "error", "message": "User ID is required"}), 400
1749
 
1750
  with _data_lock:
@@ -1752,21 +1791,34 @@ def add_transaction():
1752
  user = visitor_data_cache[user_id]
1753
  now = datetime.now(ALMATY_TZ)
1754
  now_iso, now_str = now.isoformat(), now.strftime('%Y-%m-%d %H:%M:%S')
 
1755
  if deduct_amount > user.get('bonuses', 0): return jsonify({"status": "error", "message": "Недостаточно бонусов для списания"}), 400
 
1756
  if repay_debt_amount > user.get('debts', 0): return jsonify({"status": "error", "message": "Сумма погашени�� превышает текущий долг"}), 400
1757
 
1758
- user['bonuses'] = round(user.get('bonuses', 0) + accrue_amount - deduct_amount, 2)
1759
  if 'history' not in user: user['history'] = []
1760
- if accrue_amount > 0: user['history'].append({"type": "accrual", "amount": round(accrue_amount, 2), "description": f"Начисление бонусов", "date": now_iso, "date_str": now_str})
1761
- if deduct_amount > 0: user['history'].append({"type": "deduction", "amount": round(deduct_amount, 2), "description": "Списание бонусов", "date": now_iso, "date_str": now_str})
 
 
 
 
 
 
 
 
 
1762
 
1763
- user['debts'] = round(user.get('debts', 0) + add_debt_amount - repay_debt_amount, 2)
1764
  if 'debt_history' not in user: user['debt_history'] = []
1765
- if add_debt_amount > 0: user['debt_history'].append({"type": "accrual", "amount": round(add_debt_amount, 2), "description": "Добавление долга", "date": now_iso, "date_str": now_str})
1766
- if repay_debt_amount > 0: user['debt_history'].append({"type": "payment", "amount": round(repay_debt_amount, 2), "description": "Погашение долга", "date": now_iso, "date_str": now_str})
 
 
 
 
1767
 
1768
  save_visitor_data()
1769
- return jsonify({"status": "ok", "message": "Transaction successful", "new_balance": user['bonuses'], "new_debt": user['debts']}), 200
1770
  except Exception as e:
1771
  logging.exception("Error in /admin/add_transaction endpoint")
1772
  return jsonify({"status": "error", "message": str(e)}), 500
@@ -1779,6 +1831,7 @@ def add_invoice():
1779
  total_amount = float(data.get('total_amount', 0))
1780
  items = data.get('items', [])
1781
  deduct_bonuses = float(data.get('deduct_bonuses', 0))
 
1782
 
1783
  if not user_id: return jsonify({"status": "error", "message": "User ID is required"}), 400
1784
  if not items: return jsonify({"status": "error", "message": "Необходимо добавить товары в накладную."}), 400
@@ -1787,7 +1840,8 @@ def add_invoice():
1787
  if user_id not in visitor_data_cache: return jsonify({"status": "error", "message": "User not found"}), 404
1788
  user = visitor_data_cache[user_id]
1789
 
1790
- if deduct_bonuses > user.get('bonuses', 0): return jsonify({"status": "error", "message": "Недостаточно бонусов для списания."}), 400
 
1791
  if deduct_bonuses > total_amount: return jsonify({"status": "error", "message": "Сумма списания не может превышать сумму накладной."}), 400
1792
 
1793
  now = datetime.now(ALMATY_TZ)
@@ -1799,16 +1853,22 @@ def add_invoice():
1799
  new_invoice = {
1800
  "invoice_id": invoice_id, "date": now_iso, "date_str": now_str,
1801
  "total_amount": round(total_amount, 2), "items": processed_items,
1802
- "bonuses_deducted": round(deduct_bonuses, 2)
 
1803
  }
1804
  if 'invoices' not in user: user['invoices'] = []
1805
  user['invoices'].append(new_invoice)
1806
 
1807
  if deduct_bonuses > 0:
1808
- user['bonuses'] = round(user.get('bonuses', 0) - deduct_bonuses, 2)
1809
- if 'history' not in user: user['history'] = []
1810
- user['history'].append({"type": "deduction", "amount": deduct_bonuses, "description": f"Оплата по накладной #{invoice_id}", "date": now_iso, "date_str": now_str})
1811
-
 
 
 
 
 
1812
  bonus_settings = visitor_data_cache.get('bonus_program_settings', {})
1813
  invoice_bonus_percentage = float(bonus_settings.get('invoice_bonus_percentage', 0))
1814
  if invoice_bonus_percentage > 0 and total_amount > 0:
 
260
  background-color: var(--tg-theme-secondary-bg-color);
261
  border-radius: var(--border-radius-m); padding: 20px;
262
  }
263
+ .promo-card .card-label-container { display: flex; align-items: center; justify-content: center; gap: 8px; margin-bottom: 12px; }
264
+ .promo-card .card-label { font-size: 1em; font-weight: 500; color: var(--tg-theme-hint-color); }
265
+ .info-icon {
266
+ font-weight: bold; cursor: pointer; color: var(--tg-theme-hint-color);
267
+ border: 1.5px solid var(--tg-theme-hint-color); border-radius: 50%; width: 20px; height: 20px;
268
+ display: inline-flex; justify-content: center; align-items: center; font-size: 0.8em;
269
+ }
270
  .promo-code-display {
271
  display: flex; align-items: center; justify-content: center; gap: 12px;
272
  background-color: rgba(0,0,0,0.2); border-radius: 12px; padding: 12px;
 
336
  max-height: 90vh; overflow-y: auto; animation: slideUp 0.3s ease-out;
337
  }
338
  @keyframes slideUp { from { transform: translateY(100%); } to { transform: translateY(0); } }
339
+ .modal-close-handle {
340
  position: absolute; top: 12px; left: 50%; transform: translateX(-50%);
341
  width: 40px; height: 5px; background-color: var(--tg-theme-hint-color);
342
  border-radius: 3px; cursor: pointer;
 
351
  .item-total { font-weight: 700; flex-basis: 20%; text-align: right; color: var(--tg-theme-button-color); }
352
  .invoice-total-display {
353
  padding-top: var(--padding-m); border-top: 1px solid rgba(255,255,255,0.2); margin-top: var(--padding-m);
354
+ display: flex; flex-direction: column; gap: 8px; font-size: 1.1em; font-weight: 600;
355
  }
356
+ .total-row { display: flex; justify-content: space-between; }
357
+ #promoCodeModal .modal-content, #promoInfoModal .modal-content { align-items: center; text-align: center; padding: 40px 24px 24px; }
358
+ #promoCodeModal h2, #promoInfoModal h2 { margin-bottom: 12px; }
359
+ #promoCodeModal p, #promoInfoModal p { color: var(--tg-theme-hint-color); margin-bottom: 20px; line-height: 1.5; }
360
  #promoCodeModal input {
361
  width: 100%; padding: 16px; margin-bottom: 16px; font-size: 1.2em;
362
  background-color: var(--tg-theme-bg-color); border: 1px solid rgba(255,255,255,0.1);
 
364
  text-align: center; letter-spacing: 2px;
365
  }
366
  #promoCodeModal .promo-modal-actions { display: flex; gap: 1rem; width: 100%; }
367
+ #promoCodeModal button, .btn-close-modal {
368
  flex-grow: 1; padding: 16px; font-size: 1em; font-weight: 700; border: none;
369
  border-radius: var(--border-radius-m); cursor: pointer; transition: all 0.2s;
370
  }
371
+ .btn-apply-promo, .btn-close-modal { background-color: var(--tg-theme-button-color); color: var(--tg-theme-button-text-color); }
372
  .btn-skip-promo { background-color: var(--tg-theme-secondary-bg-color); color: var(--tg-theme-text-color); }
373
  #promoStatus { margin-top: 1rem; font-weight: 500; min-height: 20px; }
374
  </style>
 
400
  </div>
401
  </section>
402
  <section class="promo-card">
403
+ <div class="card-label-container">
404
+ <p class="card-label">Ваш промокод для друзей</p>
405
+ <span class="info-icon" onclick="openModal('promoInfoModal')">?</span>
406
+ </div>
407
  <div class="promo-code-display">
408
  <span class="promo-code-value" id="userPromoCode">{{ user.referral_code }}</span>
409
  <button class="copy-btn" onclick="copyPromoCode()">Копировать</button>
 
449
  <span class="history-description">{{ item.description }}</span>
450
  <span class="history-date">{{ item.date_str }}</span>
451
  </div>
452
+ <span class="history-amount {{ 'referral' if item.type == 'accrual' else 'negative' }}">
453
+ {{ '+' if item.type == 'accrual' else '-' }}{{ "%.2f"|format(item.amount|float) }}
454
  </span>
455
  {% endif %}
456
  </li>
 
539
  </div>
540
  <div id="invoiceDetailModal" class="modal">
541
  <div class="modal-content">
542
+ <div class="modal-close-handle" onclick="closeModal('invoiceDetailModal')"></div>
543
  <h2 id="invoiceDetailTitle" class="modal-title"></h2>
544
  <ul id="invoiceDetailList" class="invoice-detail-list"></ul>
545
+ <div id="invoiceDetailTotal" class="invoice-total-display"></div>
546
+ </div>
547
+ </div>
548
+ <div id="promoInfoModal" class="modal">
549
+ <div class="modal-content">
550
+ <h2 class="modal-title">Как это работает?</h2>
551
+ <p>
552
+ Передайте ваш промокод другу. При активации промокода он получит <strong>{{ bonus_settings.referral_promo_bonus|int }} бонусов</strong>, а вы получите <strong>{{ bonus_settings.referrer_first_purchase_percentage|float }}%</strong> на ваш бонусный счет "от друзей" с его первой покупки.
553
+ </p>
554
+ <button class="btn-close-modal" onclick="closeModal('promoInfoModal')" style="width: 100%;">Понятно</button>
555
  </div>
556
  </div>
557
  {% if is_first_visit %}
 
653
  li.innerHTML = `<span class="item-name">${item.product_name}</span><span class="item-qty-price">${item.quantity} x ${parseFloat(item.unit_price).toFixed(2)}</span><span class="item-total">${parseFloat(item.item_total).toFixed(2)}</span>`;
654
  invoiceDetailList.appendChild(li);
655
  });
656
+ const totalDisplay = document.getElementById('invoiceDetailTotal');
657
+ const totalAmount = parseFloat(invoiceData.total_amount);
658
+ const bonusesDeducted = parseFloat(invoiceData.bonuses_deducted || 0);
659
+
660
+ let totalHtml = `<div class="total-row"><span>Итого:</span><span>${totalAmount.toFixed(2)}</span></div>`;
661
+ if (bonusesDeducted > 0) {
662
+ totalHtml += `<div class="total-row"><span>Списано бонусов:</span><span style="color: var(--brand-red);">- ${bonusesDeducted.toFixed(2)}</span></div>`;
663
+ totalHtml += `<div class="total-row" style="margin-top: 8px; padding-top: 8px; border-top: 1px solid rgba(255,255,255,0.1);"><strong>К оплате:</strong><strong>${(totalAmount - bonusesDeducted).toFixed(2)}</strong></div>`;
664
+ }
665
+ totalDisplay.innerHTML = totalHtml;
666
  openModal('invoiceDetailModal');
667
  }
668
 
 
811
  .history-item .amount.bonus-deduction { color: var(--admin-danger); font-weight: 600; }
812
  .history-item .amount.debt-accrual { color: var(--admin-danger); font-weight: 600; }
813
  .history-item .amount.debt-payment { color: var(--admin-success); font-weight: 600; }
814
+ .history-item .amount.referral-accrual, .history-item .amount.referral-deduction { font-weight: 600; }
815
+ .history-item .amount.referral-accrual { color: var(--admin-info); }
816
+ .history-item .amount.referral-deduction { color: var(--admin-danger); }
817
  .modal-footer { margin-top: 1.5rem; display: flex; justify-content: flex-end; align-items: center; gap: 1rem;}
818
  .modal-footer button { padding: 12px 25px; font-size: 1.1em; border-radius: 8px; border: none; font-weight: 600; cursor: pointer; }
819
  .btn-submit { background-color: var(--admin-success); color: white; }
 
848
  .item-name { flex-basis: 60%; }
849
  .item-qty-price { flex-basis: 20%; text-align: right; color: #6c757d; }
850
  .item-total { flex-basis: 20%; text-align: right; font-weight: bold; }
851
+ .invoice-total-display { margin-top: 1rem; padding-top: 1rem; border-top: 1px solid #dee2e6; display: flex; flex-direction: column; gap: 0.5rem; font-size: 1.1em; }
852
+ .invoice-total-display div { display: flex; justify-content: space-between; }
853
+ .invoice-total-display strong { font-weight: bold; }
854
+ .bonus-type-selector { display: flex; gap: 15px; margin-bottom: 1rem; }
855
+ .bonus-type-selector label { font-weight: 500; }
856
  </style>
857
  </head>
858
  <body>
 
935
  </div>
936
  <input type="hidden" id="modalUserId">
937
  <div class="tab-buttons">
938
+ <button class="tab-btn active" data-tab="bonus-debt-tab">Счета</button>
939
  <button class="tab-btn" data-tab="invoice-tab">Накладные</button>
940
+ <button class="tab-btn" data-tab="history-tab">История</button>
941
  </div>
942
  <div id="bonus-debt-tab" class="tab-content active">
943
+ <div class="form-section">
944
+ <h3>Основные бонусы</h3>
945
  <div class="form-row">
946
  <div class="form-group">
947
+ <label for="accrueAmount">Начислить</label>
948
+ <input type="number" id="accrueAmount" placeholder="0" oninput="updateCalculations()">
949
  </div>
950
  <div class="form-group">
951
+ <label for="deductAmount">Списать</label>
952
+ <input type="number" id="deductAmount" placeholder="0" oninput="updateCalculations()">
953
  </div>
954
  </div>
955
  <div class="calculation-summary">
956
  <div class="summary-item"><span>Текущий баланс:</span> <strong id="summaryCurrentBalance">0.00</strong></div>
957
+ <div class="summary-item"><strong>Итоговый баланс:</strong> <strong id="summaryFinalBalance">0.00</strong></div>
958
+ </div>
959
+ </div>
960
+ <div class="form-section">
961
+ <h3>Бонусы от друзей</h3>
962
+ <div class="form-row">
963
+ <div class="form-group">
964
+ <label for="deductReferralAmount">Списать</label>
965
+ <input type="number" id="deductReferralAmount" placeholder="0" oninput="updateCalculations()">
966
+ </div>
967
+ </div>
968
+ <div class="calculation-summary">
969
+ <div class="summary-item"><span>Текущий баланс:</span> <strong id="summaryCurrentReferralBalance">0.00</strong></div>
970
+ <div class="summary-item"><strong>Итоговый баланс:</strong> <strong id="summaryFinalReferralBalance">0.00</strong></div>
971
  </div>
972
  </div>
973
  <div class="form-section">
 
975
  <div class="form-row">
976
  <div class="form-group">
977
  <label for="addDebtAmount">Добавить долг</label>
978
+ <input type="number" id="addDebtAmount" placeholder="0" oninput="updateCalculations()">
979
  </div>
980
  <div class="form-group">
981
  <label for="repayDebtAmount">Погасить долг</label>
982
+ <input type="number" id="repayDebtAmount" placeholder="0" oninput="updateCalculations()">
983
  </div>
984
  </div>
985
  <div class="calculation-summary">
986
  <div class="summary-item"><span>Текущий долг:</span> <strong id="summaryCurrentDebt">0.00</strong></div>
 
 
 
987
  <div class="summary-item"><strong>Итоговый долг:</strong> <strong id="summaryFinalDebt">0.00</strong></div>
988
  </div>
989
  </div>
 
 
 
 
990
  <div class="modal-footer">
991
  <div id="modalStatus" class="status-message"></div>
992
  <button class="btn-submit" onclick="submitTransaction()">Провести операцию</button>
 
1014
  </div>
1015
  <div class="form-section">
1016
  <h3>Оплата бонусами</h3>
1017
+ <div class="bonus-type-selector">
1018
+ <label><input type="radio" name="bonus_type" value="regular" onchange="updateNewInvoiceTotal()" checked> Обычные</label>
1019
+ <label><input type="radio" name="bonus_type" value="referral" onchange="updateNewInvoiceTotal()"> От друзей</label>
1020
+ </div>
1021
+ <p>Доступно для списания: <strong id="invoiceAvailableBonuses">0.00</strong></p>
1022
  <div class="form-group">
1023
+ <label for="invoiceDeductBonuses">Списать бонусов</label>
1024
  <input type="number" id="invoiceDeductBonuses" oninput="updateNewInvoiceTotal()" placeholder="0.00" step="0.01">
1025
  </div>
1026
  <div class="invoice-section-summary">
 
1031
  <div id="invoiceStatus" class="status-message"></div>
1032
  <button class="btn-submit" onclick="submitInvoice()">Сохранить накладную</button>
1033
  </div>
1034
+ <div class="history-container">
1035
  <h3>История накладных клиента</h3>
1036
  <ul id="modalInvoiceList" class="invoice-list-admin"></ul>
1037
  </div>
1038
  </div>
1039
+ <div id="history-tab" class="tab-content">
1040
  <div class="history-container">
1041
+ <h3>Общая история операций</h3>
1042
+ <ul id="modalHistoryList" class="history-list"></ul>
1043
  </div>
1044
  </div>
1045
  </div>
 
1162
  document.getElementById('modalUserId').value = userData.id;
1163
  document.getElementById('modalUserName').textContent = `${userData.first_name || ''} ${userData.last_name || ''}`;
1164
  document.getElementById('modalUserUsername').textContent = `@${userData.username || userData.phone_number || ''} | ID: ${userData.id}`;
1165
+ ['accrueAmount', 'deductAmount', 'addDebtAmount', 'repayDebtAmount', 'deductReferralAmount', 'invoiceDeductBonuses'].forEach(id => document.getElementById(id).value = '');
1166
  ['modalStatus', 'invoiceStatus'].forEach(id => document.getElementById(id).textContent = '');
 
 
1167
 
1168
  newInvoiceItems = [];
1169
  renderNewInvoiceItems();
 
1188
  sign = item.type === 'accrual' ? '+' : '-';
1189
  amountClass = item.type === 'accrual' ? 'bonus-accrual' : 'bonus-deduction';
1190
  } else if (item.transaction_type === 'debt') {
1191
+ sign = item.type === 'payment' ? '-' : '+';
1192
+ amountClass = item.type === 'payment' ? 'debt-payment' : 'debt-accrual';
1193
  } else if (item.transaction_type === 'referral') {
1194
+ sign = item.type === 'accrual' ? '+' : '-';
1195
+ amountClass = item.type === 'accrual' ? 'referral-accrual' : 'referral-deduction';
1196
  }
1197
  amountText = `${sign}${parseFloat(item.amount).toFixed(2)}`;
1198
  li.innerHTML = `<div><div class="desc">${item.description}</div><div class="date">${item.date_str}</div></div><div class="amount ${amountClass}">${amountText}</div>`;
 
1201
  } else {
1202
  historyList.innerHTML = '<li style="text-align:center; padding: 1rem; color: var(--admin-secondary);">Нет истории</li>';
1203
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1204
 
1205
  const modalInvoiceList = document.getElementById('modalInvoiceList');
1206
  modalInvoiceList.innerHTML = '';
 
1282
  if (!currentUserData) return;
1283
  const currentBalance = parseFloat(currentUserData.bonuses) || 0;
1284
  const accrueAmount = parseFloat(document.getElementById('accrueAmount').value) || 0;
1285
+ const deductAmountInput = document.getElementById('deductAmount');
1286
+ const deductAmount = parseFloat(deductAmountInput.value) || 0;
1287
  let finalDeductAmount = Math.min(deductAmount, currentBalance);
1288
+ if (deductAmount > currentBalance) deductAmountInput.value = finalDeductAmount > 0 ? finalDeductAmount.toFixed(2) : '';
1289
  const finalBalance = currentBalance + accrueAmount - finalDeductAmount;
1290
  document.getElementById('summaryCurrentBalance').textContent = currentBalance.toFixed(2);
 
 
1291
  document.getElementById('summaryFinalBalance').textContent = finalBalance.toFixed(2);
1292
+
1293
+ const currentReferralBalance = parseFloat(currentUserData.referral_bonuses) || 0;
1294
+ const deductReferralAmountInput = document.getElementById('deductReferralAmount');
1295
+ const deductReferralAmount = parseFloat(deductReferralAmountInput.value) || 0;
1296
+ let finalDeductReferralAmount = Math.min(deductReferralAmount, currentReferralBalance);
1297
+ if (deductReferralAmount > currentReferralBalance) deductReferralAmountInput.value = finalDeductReferralAmount > 0 ? finalDeductReferralAmount.toFixed(2) : '';
1298
+ const finalReferralBalance = currentReferralBalance - finalDeductReferralAmount;
1299
+ document.getElementById('summaryCurrentReferralBalance').textContent = currentReferralBalance.toFixed(2);
1300
+ document.getElementById('summaryFinalReferralBalance').textContent = finalReferralBalance.toFixed(2);
1301
+
1302
  const currentDebt = parseFloat(currentUserData.debts) || 0;
1303
  const addDebtAmount = parseFloat(document.getElementById('addDebtAmount').value) || 0;
1304
+ const repayDebtAmountInput = document.getElementById('repayDebtAmount');
1305
+ const repayDebtAmount = parseFloat(repayDebtAmountInput.value) || 0;
1306
  let finalRepayAmount = Math.min(repayDebtAmount, currentDebt);
1307
+ if (repayDebtAmount > currentDebt) repayDebtAmountInput.value = finalRepayAmount > 0 ? finalRepayAmount.toFixed(2) : '';
1308
  const finalDebt = currentDebt + addDebtAmount - finalRepayAmount;
1309
  document.getElementById('summaryCurrentDebt').textContent = currentDebt.toFixed(2);
 
 
1310
  document.getElementById('summaryFinalDebt').textContent = finalDebt.toFixed(2);
1311
  }
1312
 
 
1318
  user_id: document.getElementById('modalUserId').value,
1319
  accrue_amount: parseFloat(document.getElementById('accrueAmount').value) || 0,
1320
  deduct_amount: parseFloat(document.getElementById('deductAmount').value) || 0,
1321
+ deduct_referral_amount: parseFloat(document.getElementById('deductReferralAmount').value) || 0,
1322
  add_debt_amount: parseFloat(document.getElementById('addDebtAmount').value) || 0,
1323
  repay_debt_amount: parseFloat(document.getElementById('repayDebtAmount').value) || 0,
1324
  };
 
1442
  const newRow = tableBody.insertRow();
1443
  const rowIndex = tableBody.rows.length - 1;
1444
  newInvoiceItems.push({ product_name: '', quantity: 1, unit_price: 0, item_total: 0 });
1445
+ newRow.innerHTML = `<td><input type="text" placeholder="Название товара" oninput="updateInvoiceItem(${rowIndex}, 'product_name', this.value)"></td><td><input type="number" step="1" min="1" value="1" placeholder="1" oninput="updateInvoiceItem(${rowIndex}, 'quantity', parseFloat(this.value))"></td><td><input type="number" step="0.01" min="0" placeholder="0.00" oninput="updateInvoiceItem(${rowIndex}, 'unit_price', parseFloat(this.value))"></td><td class="item-total-display">0.00</td><td><button class="action-btn" onclick="removeInvoiceItemRow(${rowIndex})">🗑️</button></td>`;
1446
+ }
1447
+
1448
+ function removeInvoiceItemRow(index) {
1449
+ newInvoiceItems.splice(index, 1);
1450
+ renderNewInvoiceItems();
1451
  }
1452
 
1453
  function updateInvoiceItem(index, field, value) {
 
1455
  newInvoiceItems[index][field] = value;
1456
  const qty = parseFloat(newInvoiceItems[index].quantity) || 0;
1457
  const price = parseFloat(newInvoiceItems[index].unit_price) || 0;
1458
+ newInvoiceItems[index].item_total = qty * price;
1459
+ renderNewInvoiceItems();
 
 
 
1460
  }
1461
  }
 
 
 
 
 
1462
 
1463
  function renderNewInvoiceItems() {
1464
  const tableBody = document.getElementById('newInvoiceItemsTable').getElementsByTagName('tbody')[0];
1465
  tableBody.innerHTML = '';
1466
  newInvoiceItems.forEach((item, index) => {
1467
  const newRow = tableBody.insertRow();
1468
+ newRow.innerHTML = `<td><input type="text" placeholder="Название товара" value="${item.product_name}" oninput="updateInvoiceItem(${index}, 'product_name', this.value)"></td><td><input type="number" step="1" min="1" placeholder="1" value="${item.quantity || '1'}" oninput="updateInvoiceItem(${index}, 'quantity', parseFloat(this.value))"></td><td><input type="number" step="0.01" min="0" placeholder="0.00" value="${item.unit_price || ''}" oninput="updateInvoiceItem(${index}, 'unit_price', parseFloat(this.value))"></td><td class="item-total-display">${(item.item_total || 0).toFixed(2)}</td><td><button class="action-btn" onclick="removeInvoiceItemRow(${index})">🗑️</button></td>`;
1469
  });
1470
  updateNewInvoiceTotal();
1471
  }
 
1474
  let total = newInvoiceItems.reduce((sum, item) => sum + (parseFloat(item.item_total) || 0), 0);
1475
  document.getElementById('newInvoiceTotalAmount').textContent = total.toFixed(2);
1476
 
1477
+ const selectedBonusType = document.querySelector('input[name="bonus_type"]:checked').value;
1478
+ const availableBonuses = (selectedBonusType === 'referral')
1479
+ ? (parseFloat(currentUserData.referral_bonuses) || 0)
1480
+ : (parseFloat(currentUserData.bonuses) || 0);
1481
+
1482
+ document.getElementById('invoiceAvailableBonuses').textContent = availableBonuses.toFixed(2);
1483
  const deductBonusesInput = document.getElementById('invoiceDeductBonuses');
1484
  let deductAmount = parseFloat(deductBonusesInput.value) || 0;
1485
 
 
1509
  }
1510
  const totalAmount = itemsToAdd.reduce((sum, item) => sum + item.item_total, 0);
1511
  const deductBonuses = parseFloat(document.getElementById('invoiceDeductBonuses').value) || 0;
1512
+ const bonusType = document.querySelector('input[name="bonus_type"]:checked').value;
1513
  const payload = {
1514
  user_id: currentUserData.id,
1515
  total_amount: totalAmount,
1516
  items: itemsToAdd,
1517
+ deduct_bonuses: deductBonuses,
1518
+ bonus_type: bonusType
1519
  };
1520
  try {
1521
  const response = await fetch('/admin/add_invoice', {
 
1548
  let bonusesDeducted = parseFloat(invoiceData.bonuses_deducted || 0);
1549
  let totalAmount = parseFloat(invoiceData.total_amount);
1550
  let finalAmount = totalAmount - bonusesDeducted;
1551
+ let bonusTypeLabel = invoiceData.bonus_type_deducted === 'referral' ? ' (от друзей)' : '';
1552
 
1553
+ let totalHTML = `<div><span>Итого:</span><strong>${totalAmount.toFixed(2)}</strong></div>`;
1554
  if (bonusesDeducted > 0) {
1555
+ totalHTML += `<div><span>Списано бонусов${bonusTypeLabel}:</span><strong style="color: var(--admin-danger);">- ${bonusesDeducted.toFixed(2)}</strong></div>`;
1556
+ totalHTML += `<hr style="border: none; border-top: 1px solid #ccc; margin: 8px 0;"><div style="font-size: 1.2em;"><span>К оплате:</span><strong style="color: var(--admin-success);">${finalAmount.toFixed(2)}</strong></div>`;
1557
  }
1558
  totalDisplay.innerHTML = totalHTML;
1559
  adminInvoiceDetailModal.style.display = 'block';
 
1607
  user_data = {"id": "N/A", "bonuses": 0, "debts": 0, "referral_bonuses": 0, "combined_history": [], "invoices": [], "referral_code": "N/A"}
1608
 
1609
  org_details = visitor_data_cache.get('organization_details', {})
1610
+ bonus_settings = visitor_data_cache.get('bonus_program_settings', {})
1611
+ return render_template_string(TEMPLATE, user=user_data, org_details=org_details, is_first_visit=is_first_visit, bonus_settings=bonus_settings)
1612
 
1613
  @app.route('/verify', methods=['POST'])
1614
  def verify_data():
 
1780
  user_id = str(data.get('user_id'))
1781
  accrue_amount = float(data.get('accrue_amount', 0))
1782
  deduct_amount = float(data.get('deduct_amount', 0))
1783
+ deduct_referral_amount = float(data.get('deduct_referral_amount', 0))
1784
  add_debt_amount = float(data.get('add_debt_amount', 0))
1785
  repay_debt_amount = float(data.get('repay_debt_amount', 0))
1786
+
1787
  if not user_id: return jsonify({"status": "error", "message": "User ID is required"}), 400
1788
 
1789
  with _data_lock:
 
1791
  user = visitor_data_cache[user_id]
1792
  now = datetime.now(ALMATY_TZ)
1793
  now_iso, now_str = now.isoformat(), now.strftime('%Y-%m-%d %H:%M:%S')
1794
+
1795
  if deduct_amount > user.get('bonuses', 0): return jsonify({"status": "error", "message": "Недостаточно бонусов для списания"}), 400
1796
+ if deduct_referral_amount > user.get('referral_bonuses', 0): return jsonify({"status": "error", "message": "Недостаточно бонусов от друзей для списания"}), 400
1797
  if repay_debt_amount > user.get('debts', 0): return jsonify({"status": "error", "message": "Сумма погашени�� превышает текущий долг"}), 400
1798
 
 
1799
  if 'history' not in user: user['history'] = []
1800
+ if accrue_amount > 0:
1801
+ user['bonuses'] = round(user.get('bonuses', 0) + accrue_amount, 2)
1802
+ user['history'].append({"type": "accrual", "amount": round(accrue_amount, 2), "description": f"Начисление бонусов (админ)", "date": now_iso, "date_str": now_str})
1803
+ if deduct_amount > 0:
1804
+ user['bonuses'] = round(user.get('bonuses', 0) - deduct_amount, 2)
1805
+ user['history'].append({"type": "deduction", "amount": round(deduct_amount, 2), "description": "Списание бонусов (админ)", "date": now_iso, "date_str": now_str})
1806
+
1807
+ if 'referral_bonus_history' not in user: user['referral_bonus_history'] = []
1808
+ if deduct_referral_amount > 0:
1809
+ user['referral_bonuses'] = round(user.get('referral_bonuses', 0) - deduct_referral_amount, 2)
1810
+ user['referral_bonus_history'].append({"type": "deduction", "amount": round(deduct_referral_amount, 2), "description": "Списание бонусов от друзей (админ)", "date": now_iso, "date_str": now_str})
1811
 
 
1812
  if 'debt_history' not in user: user['debt_history'] = []
1813
+ if add_debt_amount > 0:
1814
+ user['debts'] = round(user.get('debts', 0) + add_debt_amount, 2)
1815
+ user['debt_history'].append({"type": "accrual", "amount": round(add_debt_amount, 2), "description": "Добавление долга", "date": now_iso, "date_str": now_str})
1816
+ if repay_debt_amount > 0:
1817
+ user['debts'] = round(user.get('debts', 0) - repay_debt_amount, 2)
1818
+ user['debt_history'].append({"type": "payment", "amount": round(repay_debt_amount, 2), "description": "Погашение долга", "date": now_iso, "date_str": now_str})
1819
 
1820
  save_visitor_data()
1821
+ return jsonify({"status": "ok", "message": "Transaction successful"}), 200
1822
  except Exception as e:
1823
  logging.exception("Error in /admin/add_transaction endpoint")
1824
  return jsonify({"status": "error", "message": str(e)}), 500
 
1831
  total_amount = float(data.get('total_amount', 0))
1832
  items = data.get('items', [])
1833
  deduct_bonuses = float(data.get('deduct_bonuses', 0))
1834
+ bonus_type = data.get('bonus_type', 'regular')
1835
 
1836
  if not user_id: return jsonify({"status": "error", "message": "User ID is required"}), 400
1837
  if not items: return jsonify({"status": "error", "message": "Необходимо добавить товары в накладную."}), 400
 
1840
  if user_id not in visitor_data_cache: return jsonify({"status": "error", "message": "User not found"}), 404
1841
  user = visitor_data_cache[user_id]
1842
 
1843
+ if bonus_type == 'regular' and deduct_bonuses > user.get('bonuses', 0): return jsonify({"status": "error", "message": "Недостаточно обычных бонусов для списания."}), 400
1844
+ if bonus_type == 'referral' and deduct_bonuses > user.get('referral_bonuses', 0): return jsonify({"status": "error", "message": "Недостаточно бонусов от друзей для списания."}), 400
1845
  if deduct_bonuses > total_amount: return jsonify({"status": "error", "message": "Сумма списания не может превышать сумму накладной."}), 400
1846
 
1847
  now = datetime.now(ALMATY_TZ)
 
1853
  new_invoice = {
1854
  "invoice_id": invoice_id, "date": now_iso, "date_str": now_str,
1855
  "total_amount": round(total_amount, 2), "items": processed_items,
1856
+ "bonuses_deducted": round(deduct_bonuses, 2),
1857
+ "bonus_type_deducted": bonus_type if deduct_bonuses > 0 else None
1858
  }
1859
  if 'invoices' not in user: user['invoices'] = []
1860
  user['invoices'].append(new_invoice)
1861
 
1862
  if deduct_bonuses > 0:
1863
+ if bonus_type == 'regular':
1864
+ user['bonuses'] = round(user.get('bonuses', 0) - deduct_bonuses, 2)
1865
+ if 'history' not in user: user['history'] = []
1866
+ user['history'].append({"type": "deduction", "amount": deduct_bonuses, "description": f"Оплата по накладной #{invoice_id}", "date": now_iso, "date_str": now_str})
1867
+ elif bonus_type == 'referral':
1868
+ user['referral_bonuses'] = round(user.get('referral_bonuses', 0) - deduct_bonuses, 2)
1869
+ if 'referral_bonus_history' not in user: user['referral_bonus_history'] = []
1870
+ user['referral_bonus_history'].append({"type": "deduction", "amount": deduct_bonuses, "description": f"Оплата по накладной #{invoice_id}", "date": now_iso, "date_str": now_str})
1871
+
1872
  bonus_settings = visitor_data_cache.get('bonus_program_settings', {})
1873
  invoice_bonus_percentage = float(bonus_settings.get('invoice_bonus_percentage', 0))
1874
  if invoice_bonus_percentage > 0 and total_amount > 0: