Kgshop commited on
Commit
3ab8a08
·
verified ·
1 Parent(s): 568e5ba

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +204 -339
app.py CHANGED
@@ -260,13 +260,11 @@ 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-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;
@@ -329,6 +327,7 @@ TEMPLATE = """
329
  background-color: rgba(0,0,0,0.7); backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
330
  align-items: flex-end; justify-content: center;
331
  }
 
332
  .modal-content {
333
  background-color: var(--tg-theme-secondary-bg-color); width: 100%;
334
  border-top-left-radius: var(--border-radius-l); border-top-right-radius: var(--border-radius-l);
@@ -343,20 +342,19 @@ TEMPLATE = """
343
  }
344
  .modal-title { font-size: 1.5em; font-weight: 700; margin-bottom: 20px; text-align: center; color: var(--tg-theme-button-color); }
345
  .invoice-detail-list { list-style: none; padding: 0; margin: 0; }
346
- .invoice-detail-item {
347
- display: flex; justify-content: space-between; padding: 12px 0; border-bottom: 1px dashed rgba(255,255,255,0.1);
348
- }
349
  .item-name { font-weight: 500; flex-basis: 60%; }
350
  .item-qty-price { font-size: 0.9em; color: var(--tg-theme-hint-color); flex-basis: 20%; text-align: right; }
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,12 +362,12 @@ TEMPLATE = """
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>
375
  </head>
@@ -400,10 +398,10 @@ TEMPLATE = """
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,7 +447,7 @@ TEMPLATE = """
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 %}
@@ -542,20 +540,23 @@ TEMPLATE = """
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 %}
558
- <div id="promoCodeModal" class="modal" style="display: flex; align-items: center;">
559
  <div class="modal-content">
560
  <h2 class="modal-title">Есть промокод?</h2>
561
  <p>Если у вас есть промокод от друга, введите его, чтобы получить бонус.</p>
@@ -640,8 +641,14 @@ TEMPLATE = """
640
  document.querySelector(`.nav-btn[data-target="${sectionId}"]`).classList.add('active');
641
  }
642
 
643
- function openModal(modalId) { document.getElementById(modalId).style.display = 'flex'; }
644
- function closeModal(modalId) { document.getElementById(modalId).style.display = 'none'; }
 
 
 
 
 
 
645
 
646
  function openInvoiceDetailModal(invoiceData) {
647
  document.getElementById('invoiceDetailTitle').textContent = `Накладная #${invoiceData.invoice_id}`;
@@ -653,16 +660,22 @@ TEMPLATE = """
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
 
@@ -702,6 +715,12 @@ TEMPLATE = """
702
  statusEl.textContent = error.message;
703
  }
704
  }
 
 
 
 
 
 
705
 
706
  document.addEventListener('DOMContentLoaded', () => {
707
  document.querySelectorAll('.nav-btn').forEach(button => {
@@ -811,9 +830,8 @@ ADMIN_TEMPLATE = """
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,11 +866,10 @@ ADMIN_TEMPLATE = """
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>
@@ -940,51 +957,34 @@ ADMIN_TEMPLATE = """
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">
974
  <h3>Долги</h3>
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">
@@ -996,42 +996,30 @@ ADMIN_TEMPLATE = """
996
  <div class="form-section">
997
  <h3>Добавить новую накладную</h3>
998
  <table class="invoice-items-table" id="newInvoiceItemsTable">
999
- <thead>
1000
- <tr>
1001
- <th>Товар</th><th>Кол-во</th><th>Цена за ед.</th>
1002
- <th>Сумма</th><th></th>
1003
- </tr>
1004
- </thead>
1005
  <tbody></tbody>
1006
- <tfoot>
1007
- <tr class="total-row">
1008
- <td colspan="3" style="text-align: right;"><strong>Итоговая сумма:</strong></td>
1009
- <td id="newInvoiceTotalAmount">0.00</td><td></td>
1010
- </tr>
1011
- </tfoot>
1012
  </table>
1013
  <button class="btn btn-primary" style="margin-top: 1rem;" onclick="addNewInvoiceItemRow()">Добавить товар</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">
1027
- К оплате: <span id="invoiceFinalAmount">0.00</span>
1028
- </div>
1029
  </div>
1030
  <div class="modal-footer">
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>
@@ -1047,98 +1035,44 @@ ADMIN_TEMPLATE = """
1047
  <div id="addClientModal" class="modal">
1048
  <div class="modal-content">
1049
  <span class="modal-close" onclick="closeModal('addClientModal')">×</span>
1050
- <div class="modal-header">
1051
- <h2>Добавить нового клиента</h2>
1052
- </div>
1053
- <div class="form-group" style="margin-bottom: 1rem;">
1054
- <label for="newClientFirstName">Имя</label>
1055
- <input type="text" id="newClientFirstName" placeholder="Иван">
1056
- </div>
1057
- <div class="form-group" style="margin-bottom: 1.5rem;">
1058
- <label for="newClientPhone">Номер телефона (уникальный)</label>
1059
- <input type="tel" id="newClientPhone" placeholder="+77001234567">
1060
- </div>
1061
- <div class="modal-footer">
1062
- <div id="addClientStatus" class="status-message"></div>
1063
- <button class="btn-submit" onclick="submitNewClient()">Сохранить клиента</button>
1064
- </div>
1065
  </div>
1066
  </div>
1067
  <div id="orgSettingsModal" class="modal">
1068
  <div class="modal-content">
1069
  <span class="modal-close" onclick="closeModal('orgSettingsModal')">×</span>
1070
- <div class="modal-header">
1071
- <h2>Настройки организации</h2>
1072
- </div>
1073
  <div class="details-form">
1074
- <div class="form-group">
1075
- <label for="orgName">Название организации</label>
1076
- <input type="text" id="orgName" placeholder="Название вашей организации">
1077
- </div>
1078
- <div class="form-group">
1079
- <label for="orgPhoneNumbers">Номера телефонов (через запятую)</label>
1080
- <input type="text" id="orgPhoneNumbers" placeholder="+77001112233,+77004445566">
1081
- </div>
1082
- <div class="form-group">
1083
- <label for="orgAddress">Адрес</label>
1084
- <textarea id="orgAddress" placeholder="Город, улица, дом"></textarea>
1085
- </div>
1086
- <div class="form-group">
1087
- <label for="orgWhatsAppLink">Ссылка на WhatsApp</label>
1088
- <input type="url" id="orgWhatsAppLink" placeholder="https://wa.me/77001112233">
1089
- </div>
1090
- <div class="form-group">
1091
- <label for="orgTelegramLink">Ссылка на Telegram</label>
1092
- <input type="url" id="orgTelegramLink" placeholder="https://t.me/your_telegram_username">
1093
- </div>
1094
- </div>
1095
- <div class="modal-footer">
1096
- <div id="orgStatus" class="status-message"></div>
1097
- <button class="btn-submit" onclick="saveOrgSettings()">Сохранить настройки</button>
1098
  </div>
 
1099
  </div>
1100
  </div>
1101
  <div id="bonusSettingsModal" class="modal">
1102
  <div class="modal-content">
1103
  <span class="modal-close" onclick="closeModal('bonusSettingsModal')">×</span>
1104
- <div class="modal-header">
1105
- <h2>Настройка бонусной программы</h2>
1106
- </div>
1107
  <div class="details-form">
1108
- <div class="form-group">
1109
- <label for="settingInvoiceBonus">Процент бонусов с накладных</label>
1110
- <div class="form-group-horizontal">
1111
- <input type="number" step="0.1" id="settingInvoiceBonus" placeholder="2">
1112
- <span>%</span>
1113
- </div>
1114
- </div>
1115
- <div class="form-group">
1116
- <label for="settingPromoBonus">Количество бонусов за ввод промокода</label>
1117
- <input type="number" id="settingPromoBonus" placeholder="50">
1118
- </div>
1119
- <div class="form-group">
1120
- <label for="settingReferrerBonus">Процент партнеру с первой покупки друга</label>
1121
- <div class="form-group-horizontal">
1122
- <input type="number" step="0.1" id="settingReferrerBonus" placeholder="5">
1123
- <span>%</span>
1124
- </div>
1125
- </div>
1126
- </div>
1127
- <div class="modal-footer">
1128
- <div id="bonusSettingsStatus" class="status-message"></div>
1129
- <button class="btn-submit" onclick="saveBonusSettings()">Сохранить настройки</button>
1130
  </div>
 
1131
  </div>
1132
  </div>
1133
  <div id="adminInvoiceDetailModal" class="modal">
1134
  <div class="modal-content">
1135
  <span class="modal-close" onclick="closeModal('adminInvoiceDetailModal')">×</span>
1136
- <div class="modal-header">
1137
- <h2 id="adminInvoiceDetailTitle"></h2>
1138
- </div>
1139
  <ul id="adminInvoiceDetailList" class="invoice-detail-list"></ul>
1140
- <div id="adminInvoiceDetailTotal" class="invoice-total-display">
1141
- </div>
1142
  </div>
1143
  </div>
1144
  <script>
@@ -1164,7 +1098,8 @@ ADMIN_TEMPLATE = """
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();
1170
  loadUserHistoryAndInvoices();
@@ -1184,12 +1119,12 @@ ADMIN_TEMPLATE = """
1184
  const li = document.createElement('li');
1185
  li.className = 'history-item';
1186
  let sign, amountClass, amountText;
1187
- if (item.transaction_type === 'bonus') {
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';
@@ -1225,41 +1160,31 @@ ADMIN_TEMPLATE = """
1225
  }
1226
 
1227
  function openOrgSettingsModal() {
1228
- fetch('/admin/organization_details')
1229
- .then(response => response.json())
1230
- .then(data => {
1231
- document.getElementById('orgName').value = data.name || '';
1232
- document.getElementById('orgPhoneNumbers').value = (data.phone_numbers || []).join(',') || '';
1233
- document.getElementById('orgAddress').value = data.address || '';
1234
- document.getElementById('orgWhatsAppLink').value = data.whatsapp_link || '';
1235
- document.getElementById('orgTelegramLink').value = data.telegram_link || '';
1236
- document.getElementById('orgStatus').textContent = '';
1237
- orgSettingsModal.style.display = 'block';
1238
- })
1239
- .catch(error => {
1240
- console.error('Error fetching organization details:', error);
1241
- const orgStatus = document.getElementById('orgStatus');
1242
- orgStatus.style.color = 'var(--admin-danger)';
1243
- orgStatus.textContent = 'Ошибка загрузки данных.';
1244
- orgSettingsModal.style.display = 'block';
1245
- });
1246
  }
1247
 
1248
  function openBonusSettingsModal() {
1249
- fetch('/admin/bonus_settings')
1250
- .then(response => response.json())
1251
- .then(data => {
1252
- document.getElementById('settingInvoiceBonus').value = data.invoice_bonus_percentage || 0;
1253
- document.getElementById('settingPromoBonus').value = data.referral_promo_bonus || 0;
1254
- document.getElementById('settingReferrerBonus').value = data.referrer_first_purchase_percentage || 0;
1255
- document.getElementById('bonusSettingsStatus').textContent = '';
1256
- bonusSettingsModal.style.display = 'block';
1257
- })
1258
- .catch(error => {
1259
- console.error('Error fetching bonus settings:', error);
1260
- document.getElementById('bonusSettingsStatus').textContent = 'Ошибка загрузки настроек.';
1261
- bonusSettingsModal.style.display = 'block';
1262
- });
1263
  }
1264
 
1265
  function closeModal(modalId) {
@@ -1282,37 +1207,27 @@ ADMIN_TEMPLATE = """
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
 
1313
  async function submitTransaction() {
1314
  const statusEl = document.getElementById('modalStatus');
1315
- statusEl.style.color = 'var(--admin-secondary)';
1316
  statusEl.textContent = 'Обработка...';
1317
  const payload = {
1318
  user_id: document.getElementById('modalUserId').value,
@@ -1323,58 +1238,46 @@ ADMIN_TEMPLATE = """
1323
  repay_debt_amount: parseFloat(document.getElementById('repayDebtAmount').value) || 0,
1324
  };
1325
  if (Object.values(payload).slice(1).every(v => v <= 0)) {
1326
- statusEl.style.color = 'var(--admin-danger)';
1327
  statusEl.textContent = 'Введите сумму для любой из операций.';
1328
  return;
1329
  }
1330
  try {
1331
- const response = await fetch('/admin/add_transaction', {
1332
- method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload)
1333
- });
1334
  const result = await response.json();
1335
  if (response.ok) {
1336
- statusEl.style.color = 'var(--admin-success)';
1337
  statusEl.textContent = 'Операция успешно проведена!';
1338
  setTimeout(() => location.reload(), 1500);
1339
  } else { throw new Error(result.message || 'Произошла ошибка'); }
1340
  } catch (error) {
1341
- statusEl.style.color = 'var(--admin-danger)';
1342
  statusEl.textContent = `Ошибка: ${error.message}`;
1343
  }
1344
  }
1345
 
1346
  async function submitNewClient() {
1347
  const statusEl = document.getElementById('addClientStatus');
1348
- statusEl.style.color = 'var(--admin-secondary)';
1349
  statusEl.textContent = 'Сохранение...';
1350
  const payload = {
1351
  first_name: document.getElementById('newClientFirstName').value.trim(),
1352
  phone_number: document.getElementById('newClientPhone').value.trim(),
1353
  };
1354
  if (!payload.first_name || !payload.phone_number) {
1355
- statusEl.style.color = 'var(--admin-danger)';
1356
  statusEl.textContent = 'Имя и номер телефона обязательны.';
1357
  return;
1358
  }
1359
  try {
1360
- const response = await fetch('/admin/add_client', {
1361
- method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload)
1362
- });
1363
  const result = await response.json();
1364
  if (response.ok) {
1365
- statusEl.style.color = 'var(--admin-success)';
1366
  statusEl.textContent = 'Клиент успешно добавлен!';
1367
  setTimeout(() => location.reload(), 1500);
1368
  } else { throw new Error(result.message || 'Произошла ошибка'); }
1369
  } catch (error) {
1370
- statusEl.style.color = 'var(--admin-danger)';
1371
  statusEl.textContent = `Ошибка: ${error.message}`;
1372
  }
1373
  }
1374
 
1375
  async function saveOrgSettings() {
1376
  const statusEl = document.getElementById('orgStatus');
1377
- statusEl.style.color = 'var(--admin-secondary)';
1378
  statusEl.textContent = 'Сохранение...';
1379
  const phoneNumbersRaw = document.getElementById('orgPhoneNumbers').value.trim();
1380
  const payload = {
@@ -1385,24 +1288,19 @@ ADMIN_TEMPLATE = """
1385
  telegram_link: document.getElementById('orgTelegramLink').value.trim(),
1386
  };
1387
  try {
1388
- const response = await fetch('/admin/organization_details', {
1389
- method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload)
1390
- });
1391
  const result = await response.json();
1392
  if (response.ok) {
1393
- statusEl.style.color = 'var(--admin-success)';
1394
  statusEl.textContent = 'Настройки организации успешно сохранены!';
1395
  setTimeout(() => closeModal('orgSettingsModal'), 1500);
1396
  } else { throw new Error(result.message || 'Произошла ошибка'); }
1397
  } catch (error) {
1398
- statusEl.style.color = 'var(--admin-danger)';
1399
  statusEl.textContent = `Ошибка: ${error.message}`;
1400
  }
1401
  }
1402
 
1403
  async function saveBonusSettings() {
1404
  const statusEl = document.getElementById('bonusSettingsStatus');
1405
- statusEl.style.color = 'var(--admin-secondary)';
1406
  statusEl.textContent = 'Сохранение...';
1407
  const payload = {
1408
  invoice_bonus_percentage: parseFloat(document.getElementById('settingInvoiceBonus').value) || 0,
@@ -1410,17 +1308,13 @@ ADMIN_TEMPLATE = """
1410
  referrer_first_purchase_percentage: parseFloat(document.getElementById('settingReferrerBonus').value) || 0,
1411
  };
1412
  try {
1413
- const response = await fetch('/admin/bonus_settings', {
1414
- method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload)
1415
- });
1416
  const result = await response.json();
1417
  if (response.ok) {
1418
- statusEl.style.color = 'var(--admin-success)';
1419
  statusEl.textContent = 'Настройки бонусной программы сохранены!';
1420
  setTimeout(() => closeModal('bonusSettingsModal'), 1500);
1421
  } else { throw new Error(result.message || 'Произошла ошибка'); }
1422
  } catch (error) {
1423
- statusEl.style.color = 'var(--admin-danger)';
1424
  statusEl.textContent = `Ошибка: ${error.message}`;
1425
  }
1426
  }
@@ -1428,9 +1322,7 @@ ADMIN_TEMPLATE = """
1428
  async function deleteClient(userId) {
1429
  if (!confirm(`Вы уверены, что хотите удалить клиента с ID ${userId}? Это действие необратимо.`)) return;
1430
  try {
1431
- const response = await fetch('/admin/delete_client', {
1432
- method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ user_id: userId })
1433
- });
1434
  const result = await response.json();
1435
  if (response.ok) location.reload();
1436
  else throw new Error(result.message || 'Не удалось удалить клиента.');
@@ -1439,14 +1331,8 @@ ADMIN_TEMPLATE = """
1439
 
1440
  function addNewInvoiceItemRow() {
1441
  const tableBody = document.getElementById('newInvoiceItemsTable').getElementsByTagName('tbody')[0];
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
 
@@ -1459,6 +1345,11 @@ ADMIN_TEMPLATE = """
1459
  renderNewInvoiceItems();
1460
  }
1461
  }
 
 
 
 
 
1462
 
1463
  function renderNewInvoiceItems() {
1464
  const tableBody = document.getElementById('newInvoiceItemsTable').getElementsByTagName('tbody')[0];
@@ -1474,12 +1365,10 @@ ADMIN_TEMPLATE = """
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
 
@@ -1494,66 +1383,59 @@ ADMIN_TEMPLATE = """
1494
 
1495
  async function submitInvoice() {
1496
  const statusEl = document.getElementById('invoiceStatus');
1497
- statusEl.style.color = 'var(--admin-secondary)';
1498
- statusEl.textContent = 'Сохранение накладной...';
1499
  if (!currentUserData) {
1500
- statusEl.style.color = 'var(--admin-danger)';
1501
  statusEl.textContent = 'Пользователь не выбран.';
1502
  return;
1503
  }
1504
  const itemsToAdd = newInvoiceItems.filter(item => item.product_name && (item.quantity > 0 || item.unit_price > 0));
1505
  if (itemsToAdd.length === 0) {
1506
- statusEl.style.color = 'var(--admin-danger)';
1507
- statusEl.textContent = 'Добавьте хотя бы один товар в накладную.';
1508
  return;
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', {
1522
- method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload)
1523
- });
1524
  const result = await response.json();
1525
  if (response.ok) {
1526
- statusEl.style.color = 'var(--admin-success)';
1527
  statusEl.textContent = 'Накладная успешно сохранена!';
1528
  setTimeout(() => location.reload(), 1500);
1529
  } else { throw new Error(result.message || 'Произошла ошибка'); }
1530
  } catch (error) {
1531
- statusEl.style.color = 'var(--admin-danger)';
1532
  statusEl.textContent = `Ошибка: ${error.message}`;
1533
  }
1534
  }
1535
 
1536
  function openAdminInvoiceDetailModal(invoiceData) {
1537
  document.getElementById('adminInvoiceDetailTitle').textContent = `Накладная #${invoiceData.invoice_id} от ${invoiceData.date_str}`;
1538
- const invoiceDetailList = document.getElementById('adminInvoiceDetailList');
1539
- invoiceDetailList.innerHTML = '';
1540
  invoiceData.items.forEach(item => {
1541
  const li = document.createElement('li');
1542
  li.className = 'invoice-detail-item';
1543
  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>`;
1544
- invoiceDetailList.appendChild(li);
1545
  });
1546
 
1547
  const totalDisplay = document.getElementById('adminInvoiceDetailTotal');
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';
@@ -1562,9 +1444,7 @@ ADMIN_TEMPLATE = """
1562
  async function deleteInvoice(userId, invoiceId) {
1563
  if (!confirm(`Вы уверены, что хотите удалить накладную #${invoiceId} для клиента ID ${userId}?`)) return;
1564
  try {
1565
- const response = await fetch('/admin/delete_invoice', {
1566
- method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ user_id: userId, invoice_id: invoiceId })
1567
- });
1568
  const result = await response.json();
1569
  if (response.ok) location.reload();
1570
  else throw new Error(result.message || 'Не удалось удалить накладную.');
@@ -1572,11 +1452,9 @@ ADMIN_TEMPLATE = """
1572
  }
1573
 
1574
  window.onclick = function(event) {
1575
- if (event.target == transactionModal) closeModal('transactionModal');
1576
- if (event.target == addClientModal) closeModal('addClientModal');
1577
- if (event.target == orgSettingsModal) closeModal('orgSettingsModal');
1578
- if (event.target == bonusSettingsModal) closeModal('bonusSettingsModal');
1579
- if (event.target == adminInvoiceDetailModal) closeModal('adminInvoiceDetailModal');
1580
  }
1581
 
1582
  document.addEventListener('DOMContentLoaded', () => { addNewInvoiceItemRow(); });
@@ -1689,24 +1567,22 @@ def submit_referral():
1689
  promo_bonus = float(bonus_settings.get('referral_promo_bonus', 0))
1690
 
1691
  if referral_code:
1692
- 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)
1693
 
1694
- if not referrer:
1695
  return jsonify({"status": "error", "message": "Промокод не найден."}), 404
1696
- if referrer['id'] == user_id:
1697
  return jsonify({"status": "error", "message": "Нельзя использовать свой промокод."}), 400
1698
-
1699
- user['referred_by'] = referrer['id']
 
1700
  if 'referrals' not in referrer: referrer['referrals'] = []
1701
  referrer['referrals'].append(user_id)
1702
 
1703
  if promo_bonus > 0:
1704
  user['bonuses'] = user.get('bonuses', 0) + promo_bonus
1705
  now = datetime.now(ALMATY_TZ)
1706
- history_entry = {
1707
- "type": "accrual", "amount": promo_bonus, "description": "Бонус за промокод",
1708
- "date": now.isoformat(), "date_str": now.strftime('%Y-%m-%d %H:%M:%S')
1709
- }
1710
  if 'history' not in user: user['history'] = []
1711
  user['history'].append(history_entry)
1712
 
@@ -1737,10 +1613,7 @@ def admin_panel():
1737
  total_debts = sum(u.get('debts', 0) for u in users_list)
1738
  total_referral_bonuses = sum(u.get('referral_bonuses', 0) for u in users_list)
1739
 
1740
- summary_stats = {
1741
- "total_users": total_users, "total_bonuses": total_bonuses,
1742
- "total_debts": total_debts, "total_referral_bonuses": total_referral_bonuses
1743
- }
1744
  return render_template_string(ADMIN_TEMPLATE, users=users_list, summary=summary_stats)
1745
 
1746
  @app.route('/admin/add_client', methods=['POST'])
@@ -1759,11 +1632,9 @@ def add_client():
1759
  now = datetime.now(ALMATY_TZ)
1760
  new_id = generate_unique_id(visitor_data_cache)
1761
  new_client = {
1762
- 'id': new_id, 'telegram_id': None, 'first_name': first_name, 'last_name': None,
1763
- 'username': None, 'photo_url': None, 'is_premium': False, 'phone_number': phone_number,
1764
- 'visited_at': now.timestamp(), 'visited_at_str': now.strftime('%Y-%m-%d %H:%M:%S'),
1765
- 'bonuses': 0, 'history': [], 'debts': 0, 'debt_history': [], 'invoices': [],
1766
- 'referral_code': f'PROMO{new_id}', 'referred_by': None, 'referrals': [], 'has_been_welcomed': True,
1767
  'referral_bonuses': 0, 'referral_bonus_history': [], 'has_made_first_purchase': False,
1768
  }
1769
  visitor_data_cache[new_id] = new_client
@@ -1783,7 +1654,7 @@ def add_transaction():
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,36 +1662,36 @@ def add_transaction():
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
1825
 
1826
  @app.route('/admin/add_invoice', methods=['POST'])
@@ -1831,7 +1702,7 @@ def add_invoice():
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,8 +1711,8 @@ def add_invoice():
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)
@@ -1854,25 +1725,25 @@ def add_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:
1875
- bonus_from_invoice = round((total_amount * invoice_bonus_percentage) / 100, 2)
1876
  if bonus_from_invoice > 0:
1877
  user['bonuses'] = user.get('bonuses', 0) + bonus_from_invoice
1878
  if 'history' not in user: user['history'] = []
@@ -1885,16 +1756,11 @@ def add_invoice():
1885
  referrer = visitor_data_cache[referrer_id]
1886
  referrer_bonus_percentage = float(bonus_settings.get('referrer_first_purchase_percentage', 0))
1887
  if referrer_bonus_percentage > 0:
1888
- commission = round((total_amount * referrer_bonus_percentage) / 100, 2)
1889
  if commission > 0:
1890
  referrer['referral_bonuses'] = referrer.get('referral_bonuses', 0) + commission
1891
  if 'referral_bonus_history' not in referrer: referrer['referral_bonus_history'] = []
1892
- referrer['referral_bonus_history'].append({
1893
- "type": "accrual", "amount": commission,
1894
- "description": f"Бонус от друга {user.get('first_name', 'ID:'+user_id)}",
1895
- "date": now_iso, "date_str": now_str
1896
- })
1897
-
1898
  save_visitor_data()
1899
  return jsonify({"status": "ok", "message": "Invoice added successfully", "invoice_id": invoice_id}), 200
1900
  except Exception as e:
@@ -1996,7 +1862,6 @@ def save_bonus_settings():
1996
  if __name__ == '__main__':
1997
  print("--- BONUS SYSTEM SERVER ---")
1998
  print(f"Server starting on http://{HOST}:{PORT}")
1999
-
2000
  print("Attempting to load local data file...")
2001
  load_visitor_data()
2002
 
 
260
  background-color: var(--tg-theme-secondary-bg-color);
261
  border-radius: var(--border-radius-m); padding: 20px;
262
  }
263
+ .promo-card .card-label {
264
+ font-size: 1em; font-weight: 500; color: var(--tg-theme-hint-color);
265
+ margin-bottom: 12px; text-align: center; display: flex; align-items: center; justify-content: center; gap: 8px;
 
 
 
266
  }
267
+ .info-icon { cursor: pointer; font-weight: bold; color: var(--tg-theme-button-color); border-radius: 50%; width: 20px; height: 20px; display: inline-flex; justify-content: center; align-items: center; background-color: rgba(0,0,0,0.2); }
268
  .promo-code-display {
269
  display: flex; align-items: center; justify-content: center; gap: 12px;
270
  background-color: rgba(0,0,0,0.2); border-radius: 12px; padding: 12px;
 
327
  background-color: rgba(0,0,0,0.7); backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
328
  align-items: flex-end; justify-content: center;
329
  }
330
+ .modal.is-open { display: flex; }
331
  .modal-content {
332
  background-color: var(--tg-theme-secondary-bg-color); width: 100%;
333
  border-top-left-radius: var(--border-radius-l); border-top-right-radius: var(--border-radius-l);
 
342
  }
343
  .modal-title { font-size: 1.5em; font-weight: 700; margin-bottom: 20px; text-align: center; color: var(--tg-theme-button-color); }
344
  .invoice-detail-list { list-style: none; padding: 0; margin: 0; }
345
+ .invoice-detail-item { display: flex; justify-content: space-between; padding: 12px 0; border-bottom: 1px dashed rgba(255,255,255,0.1); }
 
 
346
  .item-name { font-weight: 500; flex-basis: 60%; }
347
  .item-qty-price { font-size: 0.9em; color: var(--tg-theme-hint-color); flex-basis: 20%; text-align: right; }
348
+ .item-total { font-weight: 700; flex-basis: 20%; text-align: right; }
349
+ .invoice-summary-grid { display: grid; gap: 8px; margin-top: 1rem; padding-top: 1rem; border-top: 1px solid rgba(255,255,255,0.2); }
350
+ .summary-row { display: flex; justify-content: space-between; font-size: 1.1em; }
351
+ .summary-row.total { font-weight: bold; font-size: 1.2em; margin-top: 8px; }
352
+ .summary-row .label { color: var(--tg-theme-hint-color); }
353
+ .summary-row .value.deducted { color: var(--brand-red); }
354
+ .summary-row .value.final { color: var(--brand-green); }
355
  #promoCodeModal .modal-content, #promoInfoModal .modal-content { align-items: center; text-align: center; padding: 40px 24px 24px; }
356
  #promoCodeModal h2, #promoInfoModal h2 { margin-bottom: 12px; }
357
+ #promoCodeModal p, #promoInfoModal p { color: var(--tg-theme-hint-color); margin-bottom: 20px; max-width: 300px; }
358
  #promoCodeModal input {
359
  width: 100%; padding: 16px; margin-bottom: 16px; font-size: 1.2em;
360
  background-color: var(--tg-theme-bg-color); border: 1px solid rgba(255,255,255,0.1);
 
362
  text-align: center; letter-spacing: 2px;
363
  }
364
  #promoCodeModal .promo-modal-actions { display: flex; gap: 1rem; width: 100%; }
365
+ #promoCodeModal button {
366
  flex-grow: 1; padding: 16px; font-size: 1em; font-weight: 700; border: none;
367
  border-radius: var(--border-radius-m); cursor: pointer; transition: all 0.2s;
368
  }
369
+ .btn-apply-promo { background-color: var(--tg-theme-button-color); color: var(--tg-theme-button-text-color); }
370
+ .btn-skip-promo, .btn-close-modal { background-color: var(--tg-theme-secondary-bg-color); color: var(--tg-theme-text-color); }
371
  #promoStatus { margin-top: 1rem; font-weight: 500; min-height: 20px; }
372
  </style>
373
  </head>
 
398
  </div>
399
  </section>
400
  <section class="promo-card">
401
+ <p class="card-label">
402
+ Ваш промокод для друзей
403
  <span class="info-icon" onclick="openModal('promoInfoModal')">?</span>
404
+ </p>
405
  <div class="promo-code-display">
406
  <span class="promo-code-value" id="userPromoCode">{{ user.referral_code }}</span>
407
  <button class="copy-btn" onclick="copyPromoCode()">Копировать</button>
 
447
  <span class="history-description">{{ item.description }}</span>
448
  <span class="history-date">{{ item.date_str }}</span>
449
  </div>
450
+ <span class="history-amount referral">
451
  {{ '+' if item.type == 'accrual' else '-' }}{{ "%.2f"|format(item.amount|float) }}
452
  </span>
453
  {% endif %}
 
540
  <div class="modal-close-handle" onclick="closeModal('invoiceDetailModal')"></div>
541
  <h2 id="invoiceDetailTitle" class="modal-title"></h2>
542
  <ul id="invoiceDetailList" class="invoice-detail-list"></ul>
543
+ <div id="invoiceSummary" class="invoice-summary-grid"></div>
544
  </div>
545
  </div>
546
  <div id="promoInfoModal" class="modal">
547
  <div class="modal-content">
548
+ <h2 class="modal-title">Реферальная программа</h2>
549
  <p>
550
+ Передай свой промокод другу! При активации твоего промокода он получит
551
+ <strong>{{ bonus_settings.referral_promo_bonus|int }}</strong> бонусов,
552
+ а ты получишь <strong>{{ bonus_settings.referrer_first_purchase_percentage|float }}%</strong>
553
+ на свой бонусный счет "с друзей" с его первой покупки.
554
  </p>
555
+ <button class="btn-close-modal" style="width: 100%; padding: 16px; font-size: 1em; font-weight: 700;" onclick="closeModal('promoInfoModal')">Понятно</button>
556
  </div>
557
  </div>
558
  {% if is_first_visit %}
559
+ <div id="promoCodeModal" class="modal is-open" style="align-items: center;">
560
  <div class="modal-content">
561
  <h2 class="modal-title">Есть промокод?</h2>
562
  <p>Если у вас есть промокод от друга, введите его, чтобы получить бонус.</p>
 
641
  document.querySelector(`.nav-btn[data-target="${sectionId}"]`).classList.add('active');
642
  }
643
 
644
+ function openModal(modalId) {
645
+ const modal = document.getElementById(modalId);
646
+ if(modal) modal.classList.add('is-open');
647
+ }
648
+ function closeModal(modalId) {
649
+ const modal = document.getElementById(modalId);
650
+ if(modal) modal.classList.remove('is-open');
651
+ }
652
 
653
  function openInvoiceDetailModal(invoiceData) {
654
  document.getElementById('invoiceDetailTitle').textContent = `Накладная #${invoiceData.invoice_id}`;
 
660
  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>`;
661
  invoiceDetailList.appendChild(li);
662
  });
 
 
 
663
 
664
+ const summaryContainer = document.getElementById('invoiceSummary');
665
+ let totalAmount = parseFloat(invoiceData.total_amount);
666
+ let bonusesDeducted = parseFloat(invoiceData.bonuses_deducted || 0);
667
+ let finalAmount = totalAmount - bonusesDeducted;
668
+
669
+ let summaryHTML = `<div class="summary-row"><span class="label">Сумма:</span><span class="value">${totalAmount.toFixed(2)}</span></div>`;
670
  if (bonusesDeducted > 0) {
671
+ let bonusType = invoiceData.bonus_type_deducted === 'referral' ? 'Бонусы от друзей' : 'Бонусы';
672
+ summaryHTML += `<div class="summary-row"><span class="label">Списано (${bonusType}):</span><span class="value deducted">- ${bonusesDeducted.toFixed(2)}</span></div>`;
673
+ summaryHTML += `<div class="summary-row total"><span class="label">К оплате:</span><span class="value final">${finalAmount.toFixed(2)}</span></div>`;
674
+ } else {
675
+ summaryHTML += `<div class="summary-row total"><span class="label">Итого:</span><span class="value">${totalAmount.toFixed(2)}</span></div>`;
676
  }
677
+ summaryContainer.innerHTML = summaryHTML;
678
+
679
  openModal('invoiceDetailModal');
680
  }
681
 
 
715
  statusEl.textContent = error.message;
716
  }
717
  }
718
+
719
+ document.addEventListener('click', function (event) {
720
+ if (event.target.classList.contains('modal')) {
721
+ event.target.classList.remove('is-open');
722
+ }
723
+ });
724
 
725
  document.addEventListener('DOMContentLoaded', () => {
726
  document.querySelectorAll('.nav-btn').forEach(button => {
 
830
  .history-item .amount.bonus-deduction { color: var(--admin-danger); font-weight: 600; }
831
  .history-item .amount.debt-accrual { color: var(--admin-danger); font-weight: 600; }
832
  .history-item .amount.debt-payment { color: var(--admin-success); font-weight: 600; }
833
+ .history-item .amount.referral-accrual { color: var(--admin-info); font-weight: 600; }
834
+ .history-item .amount.referral-deduction { color: var(--admin-danger); font-weight: 600; }
 
835
  .modal-footer { margin-top: 1.5rem; display: flex; justify-content: flex-end; align-items: center; gap: 1rem;}
836
  .modal-footer button { padding: 12px 25px; font-size: 1.1em; border-radius: 8px; border: none; font-weight: 600; cursor: pointer; }
837
  .btn-submit { background-color: var(--admin-success); color: white; }
 
866
  .item-name { flex-basis: 60%; }
867
  .item-qty-price { flex-basis: 20%; text-align: right; color: #6c757d; }
868
  .item-total { flex-basis: 20%; text-align: right; font-weight: bold; }
869
+ .invoice-total-display { margin-top: 1rem; padding-top: 1rem; border-top: 1px solid #dee2e6; font-size: 1.2em; font-weight: bold; }
870
+ .invoice-total-display > div { display: flex; justify-content: space-between; margin-bottom: 5px; }
871
+ .bonus-type-selector { margin-bottom: 1rem; }
872
+ .bonus-type-selector label { margin-right: 15px; font-weight: 500; }
 
873
  </style>
874
  </head>
875
  <body>
 
957
  <button class="tab-btn" data-tab="history-tab">История</button>
958
  </div>
959
  <div id="bonus-debt-tab" class="tab-content active">
960
+ <div class="form-section">
961
  <h3>Основные бонусы</h3>
962
  <div class="form-row">
963
+ <div class="form-group"><label for="accrueAmount">Начислить</label><input type="number" id="accrueAmount" placeholder="0" oninput="updateCalculations()"></div>
964
+ <div class="form-group"><label for="deductAmount">Списать</label><input type="number" id="deductAmount" placeholder="0" oninput="updateCalculations()"></div>
 
 
 
 
 
 
965
  </div>
966
  <div class="calculation-summary">
967
+ <div class="summary-item"><span>Итоговый баланс:</span> <strong id="summaryFinalBalance">0.00</strong></div>
 
968
  </div>
969
  </div>
970
+ <div class="form-section">
971
  <h3>Бонусы от друзей</h3>
972
+ <div class="form-row">
973
+ <div class="form-group"><label>Начислить</label><input type="number" disabled placeholder="Автоматически"></div>
974
+ <div class="form-group"><label for="deductReferralAmount">Списать</label><input type="number" id="deductReferralAmount" placeholder="0" oninput="updateCalculations()"></div>
 
 
975
  </div>
976
+ <div class="calculation-summary">
977
+ <div class="summary-item"><span>Итоговый баланс:</span> <strong id="summaryFinalReferralBalance">0.00</strong></div>
 
978
  </div>
979
  </div>
980
  <div class="form-section">
981
  <h3>Долги</h3>
982
  <div class="form-row">
983
+ <div class="form-group"><label for="addDebtAmount">Добавить долг</label><input type="number" id="addDebtAmount" placeholder="0" oninput="updateCalculations()"></div>
984
+ <div class="form-group"><label for="repayDebtAmount">Погасить долг</label><input type="number" id="repayDebtAmount" placeholder="0" oninput="updateCalculations()"></div>
 
 
 
 
 
 
985
  </div>
986
  <div class="calculation-summary">
987
+ <div class="summary-item"><span>Итоговый долг:</span> <strong id="summaryFinalDebt">0.00</strong></div>
 
988
  </div>
989
  </div>
990
  <div class="modal-footer">
 
996
  <div class="form-section">
997
  <h3>Добавить новую накладную</h3>
998
  <table class="invoice-items-table" id="newInvoiceItemsTable">
999
+ <thead><tr><th>Товар</th><th>Кол-во</th><th>Цена за ед.</th><th>Сумма</th><th></th></tr></thead>
 
 
 
 
 
1000
  <tbody></tbody>
1001
+ <tfoot><tr class="total-row"><td colspan="3" style="text-align: right;"><strong>Итоговая сумма:</strong></td><td id="newInvoiceTotalAmount">0.00</td><td></td></tr></tfoot>
 
 
 
 
 
1002
  </table>
1003
  <button class="btn btn-primary" style="margin-top: 1rem;" onclick="addNewInvoiceItemRow()">Добавить товар</button>
1004
  </div>
1005
  <div class="form-section">
1006
  <h3>Оплата бонусами</h3>
1007
  <div class="bonus-type-selector">
1008
+ <label><input type="radio" name="bonus_type_to_use" value="main" checked onchange="updateNewInvoiceTotal()"> Основные</label>
1009
+ <label><input type="radio" name="bonus_type_to_use" value="referral" onchange="updateNewInvoiceTotal()"> От друзей</label>
1010
  </div>
1011
  <p>Доступно для списания: <strong id="invoiceAvailableBonuses">0.00</strong></p>
1012
  <div class="form-group">
1013
  <label for="invoiceDeductBonuses">Списать бонусов</label>
1014
  <input type="number" id="invoiceDeductBonuses" oninput="updateNewInvoiceTotal()" placeholder="0.00" step="0.01">
1015
  </div>
1016
+ <div class="invoice-section-summary">К оплате: <span id="invoiceFinalAmount">0.00</span></div>
 
 
1017
  </div>
1018
  <div class="modal-footer">
1019
  <div id="invoiceStatus" class="status-message"></div>
1020
  <button class="btn-submit" onclick="submitInvoice()">Сохранить накладную</button>
1021
  </div>
1022
+ <div class="history-container">
1023
  <h3>История накладных клиента</h3>
1024
  <ul id="modalInvoiceList" class="invoice-list-admin"></ul>
1025
  </div>
 
1035
  <div id="addClientModal" class="modal">
1036
  <div class="modal-content">
1037
  <span class="modal-close" onclick="closeModal('addClientModal')">×</span>
1038
+ <div class="modal-header"><h2>Добавить нового клиента</h2></div>
1039
+ <div class="form-group" style="margin-bottom: 1rem;"><label for="newClientFirstName">Имя</label><input type="text" id="newClientFirstName" placeholder="Иван"></div>
1040
+ <div class="form-group" style="margin-bottom: 1.5rem;"><label for="newClientPhone">Номер телефона (уникальный)</label><input type="tel" id="newClientPhone" placeholder="+77001234567"></div>
1041
+ <div class="modal-footer"><div id="addClientStatus" class="status-message"></div><button class="btn-submit" onclick="submitNewClient()">Сохранить клиента</button></div>
 
 
 
 
 
 
 
 
 
 
 
1042
  </div>
1043
  </div>
1044
  <div id="orgSettingsModal" class="modal">
1045
  <div class="modal-content">
1046
  <span class="modal-close" onclick="closeModal('orgSettingsModal')">×</span>
1047
+ <div class="modal-header"><h2>Настройки организации</h2></div>
 
 
1048
  <div class="details-form">
1049
+ <div class="form-group"><label for="orgName">Название организации</label><input type="text" id="orgName" placeholder="Название вашей организации"></div>
1050
+ <div class="form-group"><label for="orgPhoneNumbers">Номера телефонов (через запятую)</label><input type="text" id="orgPhoneNumbers" placeholder="+77001112233,+77004445566"></div>
1051
+ <div class="form-group"><label for="orgAddress">Адрес</label><textarea id="orgAddress" placeholder="Город, улица, дом"></textarea></div>
1052
+ <div class="form-group"><label for="orgWhatsAppLink">Ссылка на WhatsApp</label><input type="url" id="orgWhatsAppLink" placeholder="https://wa.me/77001112233"></div>
1053
+ <div class="form-group"><label for="orgTelegramLink">Ссылка на Telegram</label><input type="url" id="orgTelegramLink" placeholder="https://t.me/your_telegram_username"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1054
  </div>
1055
+ <div class="modal-footer"><div id="orgStatus" class="status-message"></div><button class="btn-submit" onclick="saveOrgSettings()">Сохранить настройки</button></div>
1056
  </div>
1057
  </div>
1058
  <div id="bonusSettingsModal" class="modal">
1059
  <div class="modal-content">
1060
  <span class="modal-close" onclick="closeModal('bonusSettingsModal')">×</span>
1061
+ <div class="modal-header"><h2>Настройка бонусной программы</h2></div>
 
 
1062
  <div class="details-form">
1063
+ <div class="form-group"><label for="settingInvoiceBonus">Процент бонусов с накладных</label><div class="form-group-horizontal"><input type="number" step="0.1" id="settingInvoiceBonus" placeholder="2"><span>%</span></div></div>
1064
+ <div class="form-group"><label for="settingPromoBonus">Количество бонусов за ввод промокода</label><input type="number" id="settingPromoBonus" placeholder="50"></div>
1065
+ <div class="form-group"><label for="settingReferrerBonus">Процент партнеру с первой покупки друга</label><div class="form-group-horizontal"><input type="number" step="0.1" id="settingReferrerBonus" placeholder="5"><span>%</span></div></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1066
  </div>
1067
+ <div class="modal-footer"><div id="bonusSettingsStatus" class="status-message"></div><button class="btn-submit" onclick="saveBonusSettings()">Сохранить настройки</button></div>
1068
  </div>
1069
  </div>
1070
  <div id="adminInvoiceDetailModal" class="modal">
1071
  <div class="modal-content">
1072
  <span class="modal-close" onclick="closeModal('adminInvoiceDetailModal')">×</span>
1073
+ <div class="modal-header"><h2 id="adminInvoiceDetailTitle"></h2></div>
 
 
1074
  <ul id="adminInvoiceDetailList" class="invoice-detail-list"></ul>
1075
+ <div id="adminInvoiceDetailTotal" class="invoice-total-display"></div>
 
1076
  </div>
1077
  </div>
1078
  <script>
 
1098
  document.getElementById('modalUserUsername').textContent = `@${userData.username || userData.phone_number || ''} | ID: ${userData.id}`;
1099
  ['accrueAmount', 'deductAmount', 'addDebtAmount', 'repayDebtAmount', 'deductReferralAmount', 'invoiceDeductBonuses'].forEach(id => document.getElementById(id).value = '');
1100
  ['modalStatus', 'invoiceStatus'].forEach(id => document.getElementById(id).textContent = '');
1101
+ document.querySelector('input[name="bonus_type_to_use"][value="main"]').checked = true;
1102
+
1103
  newInvoiceItems = [];
1104
  renderNewInvoiceItems();
1105
  loadUserHistoryAndInvoices();
 
1119
  const li = document.createElement('li');
1120
  li.className = 'history-item';
1121
  let sign, amountClass, amountText;
1122
+ if (item.transaction_type === 'bonus') {
1123
  sign = item.type === 'accrual' ? '+' : '-';
1124
  amountClass = item.type === 'accrual' ? 'bonus-accrual' : 'bonus-deduction';
1125
  } else if (item.transaction_type === 'debt') {
1126
+ sign = item.type === 'accrual' ? '+' : '-';
1127
+ amountClass = item.type === 'accrual' ? 'debt-accrual' : 'debt-payment';
1128
  } else if (item.transaction_type === 'referral') {
1129
  sign = item.type === 'accrual' ? '+' : '-';
1130
  amountClass = item.type === 'accrual' ? 'referral-accrual' : 'referral-deduction';
 
1160
  }
1161
 
1162
  function openOrgSettingsModal() {
1163
+ fetch('/admin/organization_details').then(response => response.json()).then(data => {
1164
+ document.getElementById('orgName').value = data.name || '';
1165
+ document.getElementById('orgPhoneNumbers').value = (data.phone_numbers || []).join(',') || '';
1166
+ document.getElementById('orgAddress').value = data.address || '';
1167
+ document.getElementById('orgWhatsAppLink').value = data.whatsapp_link || '';
1168
+ document.getElementById('orgTelegramLink').value = data.telegram_link || '';
1169
+ document.getElementById('orgStatus').textContent = '';
1170
+ orgSettingsModal.style.display = 'block';
1171
+ }).catch(error => {
1172
+ document.getElementById('orgStatus').textContent = 'Ошибка загрузки данных.';
1173
+ orgSettingsModal.style.display = 'block';
1174
+ });
 
 
 
 
 
 
1175
  }
1176
 
1177
  function openBonusSettingsModal() {
1178
+ fetch('/admin/bonus_settings').then(response => response.json()).then(data => {
1179
+ document.getElementById('settingInvoiceBonus').value = data.invoice_bonus_percentage || 0;
1180
+ document.getElementById('settingPromoBonus').value = data.referral_promo_bonus || 0;
1181
+ document.getElementById('settingReferrerBonus').value = data.referrer_first_purchase_percentage || 0;
1182
+ document.getElementById('bonusSettingsStatus').textContent = '';
1183
+ bonusSettingsModal.style.display = 'block';
1184
+ }).catch(error => {
1185
+ document.getElementById('bonusSettingsStatus').textContent = 'Ошибка загрузки настроек.';
1186
+ bonusSettingsModal.style.display = 'block';
1187
+ });
 
 
 
 
1188
  }
1189
 
1190
  function closeModal(modalId) {
 
1207
  if (!currentUserData) return;
1208
  const currentBalance = parseFloat(currentUserData.bonuses) || 0;
1209
  const accrueAmount = parseFloat(document.getElementById('accrueAmount').value) || 0;
1210
+ const deductAmount = parseFloat(document.getElementById('deductAmount').value) || 0;
1211
+ const finalDeductAmount = Math.min(deductAmount, currentBalance + accrueAmount);
1212
+ if (deductAmount > currentBalance + accrueAmount) document.getElementById('deductAmount').value = finalDeductAmount > 0 ? finalDeductAmount.toFixed(2) : '';
1213
+ document.getElementById('summaryFinalBalance').textContent = (currentBalance + accrueAmount - finalDeductAmount).toFixed(2);
1214
+
 
 
 
1215
  const currentReferralBalance = parseFloat(currentUserData.referral_bonuses) || 0;
1216
+ const deductReferralAmount = parseFloat(document.getElementById('deductReferralAmount').value) || 0;
1217
+ const finalDeductReferralAmount = Math.min(deductReferralAmount, currentReferralBalance);
1218
+ if(deductReferralAmount > currentReferralBalance) document.getElementById('deductReferralAmount').value = finalDeductReferralAmount > 0 ? finalDeductReferralAmount.toFixed(2) : '';
1219
+ document.getElementById('summaryFinalReferralBalance').textContent = (currentReferralBalance - finalDeductReferralAmount).toFixed(2);
 
 
 
1220
 
1221
  const currentDebt = parseFloat(currentUserData.debts) || 0;
1222
  const addDebtAmount = parseFloat(document.getElementById('addDebtAmount').value) || 0;
1223
+ const repayDebtAmount = parseFloat(document.getElementById('repayDebtAmount').value) || 0;
1224
+ const finalRepayAmount = Math.min(repayDebtAmount, currentDebt + addDebtAmount);
1225
+ if (repayDebtAmount > currentDebt + addDebtAmount) document.getElementById('repayDebtAmount').value = finalRepayAmount > 0 ? finalRepayAmount.toFixed(2) : '';
1226
+ document.getElementById('summaryFinalDebt').textContent = (currentDebt + addDebtAmount - finalRepayAmount).toFixed(2);
 
 
 
1227
  }
1228
 
1229
  async function submitTransaction() {
1230
  const statusEl = document.getElementById('modalStatus');
 
1231
  statusEl.textContent = 'Обработка...';
1232
  const payload = {
1233
  user_id: document.getElementById('modalUserId').value,
 
1238
  repay_debt_amount: parseFloat(document.getElementById('repayDebtAmount').value) || 0,
1239
  };
1240
  if (Object.values(payload).slice(1).every(v => v <= 0)) {
 
1241
  statusEl.textContent = 'Введите сумму для любой из операций.';
1242
  return;
1243
  }
1244
  try {
1245
+ const response = await fetch('/admin/add_transaction', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) });
 
 
1246
  const result = await response.json();
1247
  if (response.ok) {
 
1248
  statusEl.textContent = 'Операция успешно проведена!';
1249
  setTimeout(() => location.reload(), 1500);
1250
  } else { throw new Error(result.message || 'Произошла ошибка'); }
1251
  } catch (error) {
 
1252
  statusEl.textContent = `Ошибка: ${error.message}`;
1253
  }
1254
  }
1255
 
1256
  async function submitNewClient() {
1257
  const statusEl = document.getElementById('addClientStatus');
 
1258
  statusEl.textContent = 'Сохранение...';
1259
  const payload = {
1260
  first_name: document.getElementById('newClientFirstName').value.trim(),
1261
  phone_number: document.getElementById('newClientPhone').value.trim(),
1262
  };
1263
  if (!payload.first_name || !payload.phone_number) {
 
1264
  statusEl.textContent = 'Имя и номер телефона обязательны.';
1265
  return;
1266
  }
1267
  try {
1268
+ const response = await fetch('/admin/add_client', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) });
 
 
1269
  const result = await response.json();
1270
  if (response.ok) {
 
1271
  statusEl.textContent = 'Клиент успешно добавлен!';
1272
  setTimeout(() => location.reload(), 1500);
1273
  } else { throw new Error(result.message || 'Произошла ошибка'); }
1274
  } catch (error) {
 
1275
  statusEl.textContent = `Ошибка: ${error.message}`;
1276
  }
1277
  }
1278
 
1279
  async function saveOrgSettings() {
1280
  const statusEl = document.getElementById('orgStatus');
 
1281
  statusEl.textContent = 'Сохранение...';
1282
  const phoneNumbersRaw = document.getElementById('orgPhoneNumbers').value.trim();
1283
  const payload = {
 
1288
  telegram_link: document.getElementById('orgTelegramLink').value.trim(),
1289
  };
1290
  try {
1291
+ const response = await fetch('/admin/organization_details', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) });
 
 
1292
  const result = await response.json();
1293
  if (response.ok) {
 
1294
  statusEl.textContent = 'Настройки организации успешно сохранены!';
1295
  setTimeout(() => closeModal('orgSettingsModal'), 1500);
1296
  } else { throw new Error(result.message || 'Произошла ошибка'); }
1297
  } catch (error) {
 
1298
  statusEl.textContent = `Ошибка: ${error.message}`;
1299
  }
1300
  }
1301
 
1302
  async function saveBonusSettings() {
1303
  const statusEl = document.getElementById('bonusSettingsStatus');
 
1304
  statusEl.textContent = 'Сохранение...';
1305
  const payload = {
1306
  invoice_bonus_percentage: parseFloat(document.getElementById('settingInvoiceBonus').value) || 0,
 
1308
  referrer_first_purchase_percentage: parseFloat(document.getElementById('settingReferrerBonus').value) || 0,
1309
  };
1310
  try {
1311
+ const response = await fetch('/admin/bonus_settings', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) });
 
 
1312
  const result = await response.json();
1313
  if (response.ok) {
 
1314
  statusEl.textContent = 'Настройки бонусной программы сохранены!';
1315
  setTimeout(() => closeModal('bonusSettingsModal'), 1500);
1316
  } else { throw new Error(result.message || 'Произошла ошибка'); }
1317
  } catch (error) {
 
1318
  statusEl.textContent = `Ошибка: ${error.message}`;
1319
  }
1320
  }
 
1322
  async function deleteClient(userId) {
1323
  if (!confirm(`Вы уверены, что хотите удалить клиента с ID ${userId}? Это действие необратимо.`)) return;
1324
  try {
1325
+ const response = await fetch('/admin/delete_client', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ user_id: userId }) });
 
 
1326
  const result = await response.json();
1327
  if (response.ok) location.reload();
1328
  else throw new Error(result.message || 'Не удалось удалить клиента.');
 
1331
 
1332
  function addNewInvoiceItemRow() {
1333
  const tableBody = document.getElementById('newInvoiceItemsTable').getElementsByTagName('tbody')[0];
1334
+ const rowIndex = tableBody.rows.length;
 
1335
  newInvoiceItems.push({ product_name: '', quantity: 1, unit_price: 0, item_total: 0 });
 
 
 
 
 
1336
  renderNewInvoiceItems();
1337
  }
1338
 
 
1345
  renderNewInvoiceItems();
1346
  }
1347
  }
1348
+
1349
+ function removeInvoiceItemRow(index) {
1350
+ newInvoiceItems.splice(index, 1);
1351
+ renderNewInvoiceItems();
1352
+ }
1353
 
1354
  function renderNewInvoiceItems() {
1355
  const tableBody = document.getElementById('newInvoiceItemsTable').getElementsByTagName('tbody')[0];
 
1365
  let total = newInvoiceItems.reduce((sum, item) => sum + (parseFloat(item.item_total) || 0), 0);
1366
  document.getElementById('newInvoiceTotalAmount').textContent = total.toFixed(2);
1367
 
1368
+ const bonusType = document.querySelector('input[name="bonus_type_to_use"]:checked').value;
1369
+ const availableBonuses = bonusType === 'main' ? (parseFloat(currentUserData.bonuses) || 0) : (parseFloat(currentUserData.referral_bonuses) || 0);
 
 
 
1370
  document.getElementById('invoiceAvailableBonuses').textContent = availableBonuses.toFixed(2);
1371
+
1372
  const deductBonusesInput = document.getElementById('invoiceDeductBonuses');
1373
  let deductAmount = parseFloat(deductBonusesInput.value) || 0;
1374
 
 
1383
 
1384
  async function submitInvoice() {
1385
  const statusEl = document.getElementById('invoiceStatus');
1386
+ statusEl.textContent = 'Сохранение...';
 
1387
  if (!currentUserData) {
 
1388
  statusEl.textContent = 'Пользователь не выбран.';
1389
  return;
1390
  }
1391
  const itemsToAdd = newInvoiceItems.filter(item => item.product_name && (item.quantity > 0 || item.unit_price > 0));
1392
  if (itemsToAdd.length === 0) {
1393
+ statusEl.textContent = 'Добавьте хотя бы один товар.';
 
1394
  return;
1395
  }
1396
  const totalAmount = itemsToAdd.reduce((sum, item) => sum + item.item_total, 0);
1397
  const deductBonuses = parseFloat(document.getElementById('invoiceDeductBonuses').value) || 0;
1398
+ const bonusType = document.querySelector('input[name="bonus_type_to_use"]:checked').value;
1399
  const payload = {
1400
  user_id: currentUserData.id,
1401
  total_amount: totalAmount,
1402
  items: itemsToAdd,
1403
  deduct_bonuses: deductBonuses,
1404
+ bonus_type_to_deduct: bonusType
1405
  };
1406
  try {
1407
+ const response = await fetch('/admin/add_invoice', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) });
 
 
1408
  const result = await response.json();
1409
  if (response.ok) {
 
1410
  statusEl.textContent = 'Накладная успешно сохранена!';
1411
  setTimeout(() => location.reload(), 1500);
1412
  } else { throw new Error(result.message || 'Произошла ошибка'); }
1413
  } catch (error) {
 
1414
  statusEl.textContent = `Ошибка: ${error.message}`;
1415
  }
1416
  }
1417
 
1418
  function openAdminInvoiceDetailModal(invoiceData) {
1419
  document.getElementById('adminInvoiceDetailTitle').textContent = `Накладная #${invoiceData.invoice_id} от ${invoiceData.date_str}`;
1420
+ const list = document.getElementById('adminInvoiceDetailList');
1421
+ list.innerHTML = '';
1422
  invoiceData.items.forEach(item => {
1423
  const li = document.createElement('li');
1424
  li.className = 'invoice-detail-item';
1425
  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>`;
1426
+ list.appendChild(li);
1427
  });
1428
 
1429
  const totalDisplay = document.getElementById('adminInvoiceDetailTotal');
1430
  let bonusesDeducted = parseFloat(invoiceData.bonuses_deducted || 0);
1431
  let totalAmount = parseFloat(invoiceData.total_amount);
1432
  let finalAmount = totalAmount - bonusesDeducted;
1433
+
1434
+ let totalHTML = `<div><span>Итого:</span><span>${totalAmount.toFixed(2)}</span></div>`;
 
1435
  if (bonusesDeducted > 0) {
1436
+ let bonusTypeLabel = invoiceData.bonus_type_deducted === 'referral' ? 'Бонусы от друзей' : 'Основные бонусы';
1437
+ totalHTML += `<div><span>Списано (${bonusTypeLabel}):</span><span style="color: var(--admin-danger);">- ${bonusesDeducted.toFixed(2)}</span></div>`;
1438
+ totalHTML += `<hr style="border-top: 1px solid #dee2e6; margin: 5px 0;"><div><strong>К оплате:</strong><strong style="color: var(--admin-success);">${finalAmount.toFixed(2)}</strong></div>`;
1439
  }
1440
  totalDisplay.innerHTML = totalHTML;
1441
  adminInvoiceDetailModal.style.display = 'block';
 
1444
  async function deleteInvoice(userId, invoiceId) {
1445
  if (!confirm(`Вы уверены, что хотите удалить накладную #${invoiceId} для клиента ID ${userId}?`)) return;
1446
  try {
1447
+ const response = await fetch('/admin/delete_invoice', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ user_id: userId, invoice_id: invoiceId }) });
 
 
1448
  const result = await response.json();
1449
  if (response.ok) location.reload();
1450
  else throw new Error(result.message || 'Не удалось удалить накладную.');
 
1452
  }
1453
 
1454
  window.onclick = function(event) {
1455
+ if (event.target.classList.contains('modal')) {
1456
+ event.target.style.display = 'none';
1457
+ }
 
 
1458
  }
1459
 
1460
  document.addEventListener('DOMContentLoaded', () => { addNewInvoiceItemRow(); });
 
1567
  promo_bonus = float(bonus_settings.get('referral_promo_bonus', 0))
1568
 
1569
  if referral_code:
1570
+ referrer_id = next((u_id 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)
1571
 
1572
+ if not referrer_id:
1573
  return jsonify({"status": "error", "message": "Промокод не найден."}), 404
1574
+ if referrer_id == user_id:
1575
  return jsonify({"status": "error", "message": "Нельзя использовать свой промокод."}), 400
1576
+
1577
+ referrer = visitor_data_cache[referrer_id]
1578
+ user['referred_by'] = referrer_id
1579
  if 'referrals' not in referrer: referrer['referrals'] = []
1580
  referrer['referrals'].append(user_id)
1581
 
1582
  if promo_bonus > 0:
1583
  user['bonuses'] = user.get('bonuses', 0) + promo_bonus
1584
  now = datetime.now(ALMATY_TZ)
1585
+ history_entry = { "type": "accrual", "amount": promo_bonus, "description": "Бонус за промокод", "date": now.isoformat(), "date_str": now.strftime('%Y-%m-%d %H:%M:%S') }
 
 
 
1586
  if 'history' not in user: user['history'] = []
1587
  user['history'].append(history_entry)
1588
 
 
1613
  total_debts = sum(u.get('debts', 0) for u in users_list)
1614
  total_referral_bonuses = sum(u.get('referral_bonuses', 0) for u in users_list)
1615
 
1616
+ summary_stats = { "total_users": total_users, "total_bonuses": total_bonuses, "total_debts": total_debts, "total_referral_bonuses": total_referral_bonuses }
 
 
 
1617
  return render_template_string(ADMIN_TEMPLATE, users=users_list, summary=summary_stats)
1618
 
1619
  @app.route('/admin/add_client', methods=['POST'])
 
1632
  now = datetime.now(ALMATY_TZ)
1633
  new_id = generate_unique_id(visitor_data_cache)
1634
  new_client = {
1635
+ 'id': new_id, 'telegram_id': None, 'first_name': first_name, 'last_name': None, 'username': None, 'photo_url': None, 'is_premium': False,
1636
+ 'phone_number': phone_number, 'visited_at': now.timestamp(), 'visited_at_str': now.strftime('%Y-%m-%d %H:%M:%S'), 'bonuses': 0, 'history': [],
1637
+ 'debts': 0, 'debt_history': [], 'invoices': [], 'referral_code': f'PROMO{new_id}', 'referred_by': None, 'referrals': [], 'has_been_welcomed': True,
 
 
1638
  'referral_bonuses': 0, 'referral_bonus_history': [], 'has_made_first_purchase': False,
1639
  }
1640
  visitor_data_cache[new_id] = new_client
 
1654
  deduct_referral_amount = float(data.get('deduct_referral_amount', 0))
1655
  add_debt_amount = float(data.get('add_debt_amount', 0))
1656
  repay_debt_amount = float(data.get('repay_debt_amount', 0))
1657
+
1658
  if not user_id: return jsonify({"status": "error", "message": "User ID is required"}), 400
1659
 
1660
  with _data_lock:
 
1662
  user = visitor_data_cache[user_id]
1663
  now = datetime.now(ALMATY_TZ)
1664
  now_iso, now_str = now.isoformat(), now.strftime('%Y-%m-%d %H:%M:%S')
1665
+
1666
+ if deduct_amount > user.get('bonuses', 0) + accrue_amount: return jsonify({"status": "error", "message": "Недостаточно основных бонусов для списания"}), 400
1667
  if deduct_referral_amount > user.get('referral_bonuses', 0): return jsonify({"status": "error", "message": "Недостаточно бонусов от друзей для списания"}), 400
1668
+ if repay_debt_amount > user.get('debts', 0) + add_debt_amount: return jsonify({"status": "error", "message": "Сумма погашения превышает долг"}), 400
1669
 
1670
  if 'history' not in user: user['history'] = []
1671
  if accrue_amount > 0:
1672
  user['bonuses'] = round(user.get('bonuses', 0) + accrue_amount, 2)
1673
+ user['history'].append({"type": "accrual", "amount": accrue_amount, "description": "Начисление бонусов", "date": now_iso, "date_str": now_str})
1674
  if deduct_amount > 0:
1675
  user['bonuses'] = round(user.get('bonuses', 0) - deduct_amount, 2)
1676
+ user['history'].append({"type": "deduction", "amount": deduct_amount, "description": "Списание бонусов", "date": now_iso, "date_str": now_str})
1677
 
1678
  if 'referral_bonus_history' not in user: user['referral_bonus_history'] = []
1679
  if deduct_referral_amount > 0:
1680
  user['referral_bonuses'] = round(user.get('referral_bonuses', 0) - deduct_referral_amount, 2)
1681
+ user['referral_bonus_history'].append({"type": "deduction", "amount": deduct_referral_amount, "description": "Списание бонусов от друзей", "date": now_iso, "date_str": now_str})
1682
 
1683
  if 'debt_history' not in user: user['debt_history'] = []
1684
  if add_debt_amount > 0:
1685
  user['debts'] = round(user.get('debts', 0) + add_debt_amount, 2)
1686
+ user['debt_history'].append({"type": "accrual", "amount": add_debt_amount, "description": "Добавление долга", "date": now_iso, "date_str": now_str})
1687
  if repay_debt_amount > 0:
1688
  user['debts'] = round(user.get('debts', 0) - repay_debt_amount, 2)
1689
+ user['debt_history'].append({"type": "payment", "amount": repay_debt_amount, "description": "Погашение долга", "date": now_iso, "date_str": now_str})
1690
 
1691
  save_visitor_data()
1692
  return jsonify({"status": "ok", "message": "Transaction successful"}), 200
1693
  except Exception as e:
1694
+ logging.exception("Error in /admin/add_transaction")
1695
  return jsonify({"status": "error", "message": str(e)}), 500
1696
 
1697
  @app.route('/admin/add_invoice', methods=['POST'])
 
1702
  total_amount = float(data.get('total_amount', 0))
1703
  items = data.get('items', [])
1704
  deduct_bonuses = float(data.get('deduct_bonuses', 0))
1705
+ bonus_type_to_deduct = data.get('bonus_type_to_deduct', 'main')
1706
 
1707
  if not user_id: return jsonify({"status": "error", "message": "User ID is required"}), 400
1708
  if not items: return jsonify({"status": "error", "message": "Необходимо добавить товары в накладную."}), 400
 
1711
  if user_id not in visitor_data_cache: return jsonify({"status": "error", "message": "User not found"}), 404
1712
  user = visitor_data_cache[user_id]
1713
 
1714
+ available_bonuses = user.get('referral_bonuses', 0) if bonus_type_to_deduct == 'referral' else user.get('bonuses', 0)
1715
+ if deduct_bonuses > available_bonuses: return jsonify({"status": "error", "message": "Недостаточно бонусов для списания."}), 400
1716
  if deduct_bonuses > total_amount: return jsonify({"status": "error", "message": "Сумма списания не может превышать сумму накладной."}), 400
1717
 
1718
  now = datetime.now(ALMATY_TZ)
 
1725
  "invoice_id": invoice_id, "date": now_iso, "date_str": now_str,
1726
  "total_amount": round(total_amount, 2), "items": processed_items,
1727
  "bonuses_deducted": round(deduct_bonuses, 2),
1728
+ "bonus_type_deducted": bonus_type_to_deduct
1729
  }
1730
  if 'invoices' not in user: user['invoices'] = []
1731
  user['invoices'].append(new_invoice)
1732
 
1733
  if deduct_bonuses > 0:
1734
+ if bonus_type_to_deduct == 'referral':
 
 
 
 
1735
  user['referral_bonuses'] = round(user.get('referral_bonuses', 0) - deduct_bonuses, 2)
1736
  if 'referral_bonus_history' not in user: user['referral_bonus_history'] = []
1737
  user['referral_bonus_history'].append({"type": "deduction", "amount": deduct_bonuses, "description": f"Оплата по накладной #{invoice_id}", "date": now_iso, "date_str": now_str})
1738
+ else:
1739
+ user['bonuses'] = round(user.get('bonuses', 0) - deduct_bonuses, 2)
1740
+ if 'history' not in user: user['history'] = []
1741
+ user['history'].append({"type": "deduction", "amount": deduct_bonuses, "description": f"Оплата по накладной #{invoice_id}", "date": now_iso, "date_str": now_str})
1742
+
1743
  bonus_settings = visitor_data_cache.get('bonus_program_settings', {})
1744
  invoice_bonus_percentage = float(bonus_settings.get('invoice_bonus_percentage', 0))
1745
  if invoice_bonus_percentage > 0 and total_amount > 0:
1746
+ bonus_from_invoice = round(((total_amount - deduct_bonuses) * invoice_bonus_percentage) / 100, 2)
1747
  if bonus_from_invoice > 0:
1748
  user['bonuses'] = user.get('bonuses', 0) + bonus_from_invoice
1749
  if 'history' not in user: user['history'] = []
 
1756
  referrer = visitor_data_cache[referrer_id]
1757
  referrer_bonus_percentage = float(bonus_settings.get('referrer_first_purchase_percentage', 0))
1758
  if referrer_bonus_percentage > 0:
1759
+ commission = round(((total_amount - deduct_bonuses) * referrer_bonus_percentage) / 100, 2)
1760
  if commission > 0:
1761
  referrer['referral_bonuses'] = referrer.get('referral_bonuses', 0) + commission
1762
  if 'referral_bonus_history' not in referrer: referrer['referral_bonus_history'] = []
1763
+ referrer['referral_bonus_history'].append({ "type": "accrual", "amount": commission, "description": f"Бонус от друга {user.get('first_name', 'ID:'+user_id)}", "date": now_iso, "date_str": now_str })
 
 
 
 
 
1764
  save_visitor_data()
1765
  return jsonify({"status": "ok", "message": "Invoice added successfully", "invoice_id": invoice_id}), 200
1766
  except Exception as e:
 
1862
  if __name__ == '__main__':
1863
  print("--- BONUS SYSTEM SERVER ---")
1864
  print(f"Server starting on http://{HOST}:{PORT}")
 
1865
  print("Attempting to load local data file...")
1866
  load_visitor_data()
1867