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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +337 -237
app.py CHANGED
@@ -260,11 +260,16 @@ 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 {
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,7 +332,6 @@ TEMPLATE = """
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);
@@ -335,26 +339,29 @@ TEMPLATE = """
335
  max-height: 90vh; overflow-y: auto; animation: slideUp 0.3s ease-out;
336
  }
337
  @keyframes slideUp { from { transform: translateY(100%); } to { transform: translateY(0); } }
338
- .modal-close-handle {
339
  position: absolute; top: 12px; left: 50%; transform: translateX(-50%);
340
  width: 40px; height: 5px; background-color: var(--tg-theme-hint-color);
341
  border-radius: 3px; cursor: pointer;
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);
@@ -367,7 +374,7 @@ TEMPLATE = """
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>
@@ -399,8 +406,11 @@ TEMPLATE = """
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>
@@ -537,26 +547,14 @@ TEMPLATE = """
537
  </div>
538
  <div id="invoiceDetailModal" class="modal">
539
  <div class="modal-content">
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,14 +639,8 @@ TEMPLATE = """
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,21 +652,19 @@ TEMPLATE = """
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
  }
@@ -715,12 +705,6 @@ TEMPLATE = """
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 => {
@@ -866,10 +850,10 @@ ADMIN_TEMPLATE = """
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>
@@ -954,37 +938,46 @@ ADMIN_TEMPLATE = """
954
  <div class="tab-buttons">
955
  <button class="tab-btn active" data-tab="bonus-debt-tab">Счета</button>
956
  <button class="tab-btn" data-tab="invoice-tab">Накладные</button>
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,24 +989,37 @@ ADMIN_TEMPLATE = """
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>
@@ -1024,7 +1030,7 @@ ADMIN_TEMPLATE = """
1024
  <ul id="modalInvoiceList" class="invoice-list-admin"></ul>
1025
  </div>
1026
  </div>
1027
- <div id="history-tab" class="tab-content">
1028
  <div class="history-container">
1029
  <h3>Общая история операций</h3>
1030
  <ul id="modalHistoryList" class="history-list"></ul>
@@ -1035,42 +1041,95 @@ ADMIN_TEMPLATE = """
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>
@@ -1096,10 +1155,10 @@ ADMIN_TEMPLATE = """
1096
  document.getElementById('modalUserId').value = userData.id;
1097
  document.getElementById('modalUserName').textContent = `${userData.first_name || ''} ${userData.last_name || ''}`;
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,7 +1178,7 @@ ADMIN_TEMPLATE = """
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') {
@@ -1136,7 +1195,7 @@ ADMIN_TEMPLATE = """
1136
  } else {
1137
  historyList.innerHTML = '<li style="text-align:center; padding: 1rem; color: var(--admin-secondary);">Нет истории</li>';
1138
  }
1139
-
1140
  const modalInvoiceList = document.getElementById('modalInvoiceList');
1141
  modalInvoiceList.innerHTML = '';
1142
  const userInvoices = (currentUserData.invoices || []).sort((a, b) => new Date(b.date) - new Date(a.date));
@@ -1160,31 +1219,41 @@ ADMIN_TEMPLATE = """
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) {
@@ -1205,79 +1274,74 @@ ADMIN_TEMPLATE = """
1205
 
1206
  function updateCalculations() {
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,
1234
  accrue_amount: parseFloat(document.getElementById('accrueAmount').value) || 0,
1235
  deduct_amount: parseFloat(document.getElementById('deductAmount').value) || 0,
 
1236
  deduct_referral_amount: parseFloat(document.getElementById('deductReferralAmount').value) || 0,
1237
  add_debt_amount: parseFloat(document.getElementById('addDebtAmount').value) || 0,
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,19 +1352,24 @@ ADMIN_TEMPLATE = """
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,13 +1377,17 @@ ADMIN_TEMPLATE = """
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,7 +1395,9 @@ ADMIN_TEMPLATE = """
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 || 'Не удалось удалить клиента.');
@@ -1362,18 +1437,19 @@ ADMIN_TEMPLATE = """
1362
  }
1363
 
1364
  function updateNewInvoiceTotal() {
 
 
 
 
 
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
 
1375
  let cappedDeductAmount = Math.max(0, Math.min(deductAmount, availableBonuses, total));
1376
- if (deductAmount !== cappedDeductAmount) {
1377
  deductBonusesInput.value = cappedDeductAmount > 0 ? cappedDeductAmount.toFixed(2) : '';
1378
  }
1379
 
@@ -1383,59 +1459,67 @@ ADMIN_TEMPLATE = """
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,7 +1528,9 @@ ADMIN_TEMPLATE = """
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,9 +1538,11 @@ ADMIN_TEMPLATE = """
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(); });
@@ -1469,23 +1557,25 @@ def index():
1469
  user_data = {}
1470
  is_first_visit = False
1471
 
1472
- if user_id_str and user_id_str in visitor_data_cache:
1473
- user_data = visitor_data_cache[user_id_str]
1474
- user_data['id'] = user_id_str
1475
- is_first_visit = not user_data.get('has_been_welcomed', False)
1476
- bonus_history = user_data.get('history', [])
1477
- debt_history = user_data.get('debt_history', [])
1478
- referral_history = user_data.get('referral_bonus_history', [])
1479
- for item in bonus_history: item['transaction_type'] = 'bonus'
1480
- for item in debt_history: item['transaction_type'] = 'debt'
1481
- for item in referral_history: item['transaction_type'] = 'referral'
1482
- user_data['combined_history'] = sorted(bonus_history + debt_history + referral_history, key=lambda x: datetime.fromisoformat(x['date']), reverse=True)
1483
- user_data['invoices'] = user_data.get('invoices', [])
1484
- else:
1485
- user_data = {"id": "N/A", "bonuses": 0, "debts": 0, "referral_bonuses": 0, "combined_history": [], "invoices": [], "referral_code": "N/A"}
1486
-
1487
- org_details = visitor_data_cache.get('organization_details', {})
1488
- bonus_settings = visitor_data_cache.get('bonus_program_settings', {})
 
 
1489
  return render_template_string(TEMPLATE, user=user_data, org_details=org_details, is_first_visit=is_first_visit, bonus_settings=bonus_settings)
1490
 
1491
  @app.route('/verify', methods=['POST'])
@@ -1573,16 +1663,19 @@ def submit_referral():
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,7 +1706,10 @@ def admin_panel():
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,9 +1728,11 @@ def add_client():
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
@@ -1651,6 +1749,7 @@ def add_transaction():
1651
  user_id = str(data.get('user_id'))
1652
  accrue_amount = float(data.get('accrue_amount', 0))
1653
  deduct_amount = float(data.get('deduct_amount', 0))
 
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))
@@ -1663,35 +1762,30 @@ def add_transaction():
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,7 +1796,7 @@ def add_invoice():
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,8 +1805,8 @@ def add_invoice():
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,25 +1819,25 @@ def add_invoice():
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,11 +1850,16 @@ def add_invoice():
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,6 +1961,7 @@ def save_bonus_settings():
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
 
 
260
  background-color: var(--tg-theme-secondary-bg-color);
261
  border-radius: var(--border-radius-m); padding: 20px;
262
  }
263
+ .promo-card .card-label { display: flex; align-items: center; justify-content: center; gap: 8px; font-size: 1em; font-weight: 500; color: var(--tg-theme-hint-color); margin-bottom: 12px; }
264
+ .promo-info-icon { cursor: pointer; position: relative; }
265
+ .promo-info-icon .tooltip {
266
+ visibility: hidden; opacity: 0; position: absolute; bottom: 130%; left: 50%;
267
+ transform: translateX(-50%); background-color: #333; color: #fff; text-align: left;
268
+ border-radius: 8px; padding: 12px; z-index: 1; width: 250px;
269
+ font-size: 0.9em; font-weight: 400; line-height: 1.4;
270
+ box-shadow: 0 4px 10px rgba(0,0,0,0.3); transition: opacity 0.2s, visibility 0.2s;
271
  }
272
+ .promo-info-icon:hover .tooltip { visibility: visible; opacity: 1; }
273
  .promo-code-display {
274
  display: flex; align-items: center; justify-content: center; gap: 12px;
275
  background-color: rgba(0,0,0,0.2); border-radius: 12px; padding: 12px;
 
332
  background-color: rgba(0,0,0,0.7); backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
333
  align-items: flex-end; justify-content: center;
334
  }
 
335
  .modal-content {
336
  background-color: var(--tg-theme-secondary-bg-color); width: 100%;
337
  border-top-left-radius: var(--border-radius-l); border-top-right-radius: var(--border-radius-l);
 
339
  max-height: 90vh; overflow-y: auto; animation: slideUp 0.3s ease-out;
340
  }
341
  @keyframes slideUp { from { transform: translateY(100%); } to { transform: translateY(0); } }
342
+ .modal-close {
343
  position: absolute; top: 12px; left: 50%; transform: translateX(-50%);
344
  width: 40px; height: 5px; background-color: var(--tg-theme-hint-color);
345
  border-radius: 3px; cursor: pointer;
346
  }
347
  .modal-title { font-size: 1.5em; font-weight: 700; margin-bottom: 20px; text-align: center; color: var(--tg-theme-button-color); }
348
  .invoice-detail-list { list-style: none; padding: 0; margin: 0; }
349
+ .invoice-detail-item {
350
+ display: grid; grid-template-columns: 1fr auto; gap: 4px 16px; padding: 12px 0; border-bottom: 1px dashed rgba(255,255,255,0.1);
351
+ }
352
+ .item-name { font-weight: 500; grid-column: 1; }
353
+ .item-qty-price { font-size: 0.9em; color: var(--tg-theme-hint-color); grid-column: 1; }
354
+ .item-total { font-weight: 700; grid-column: 2; grid-row: 1 / 3; align-self: center; color: var(--tg-theme-button-color); }
355
+ .invoice-total-section {
356
+ padding-top: var(--padding-m); border-top: 1px solid rgba(255,255,255,0.2); margin-top: var(--padding-m);
357
+ display: flex; flex-direction: column; gap: 8px; font-size: 1.1em;
358
+ }
359
+ .total-row { display: flex; justify-content: space-between; }
360
+ .total-row.final { font-weight: 700; font-size: 1.2em; margin-top: 8px; }
361
+ .total-row .deduction { color: var(--brand-red); }
362
+ #promoCodeModal .modal-content { align-items: center; text-align: center; padding: 40px 24px 24px; }
363
+ #promoCodeModal h2 { margin-bottom: 12px; }
364
+ #promoCodeModal p { color: var(--tg-theme-hint-color); margin-bottom: 20px; }
365
  #promoCodeModal input {
366
  width: 100%; padding: 16px; margin-bottom: 16px; font-size: 1.2em;
367
  background-color: var(--tg-theme-bg-color); border: 1px solid rgba(255,255,255,0.1);
 
374
  border-radius: var(--border-radius-m); cursor: pointer; transition: all 0.2s;
375
  }
376
  .btn-apply-promo { background-color: var(--tg-theme-button-color); color: var(--tg-theme-button-text-color); }
377
+ .btn-skip-promo { background-color: var(--tg-theme-secondary-bg-color); color: var(--tg-theme-text-color); }
378
  #promoStatus { margin-top: 1rem; font-weight: 500; min-height: 20px; }
379
  </style>
380
  </head>
 
406
  </section>
407
  <section class="promo-card">
408
  <p class="card-label">
409
+ Ваш промокод для друзей
410
+ <span class="promo-info-icon">
411
+ <svg xmlns="http://www.w3.org/2000/svg" height="20px" viewBox="0 0 24 24" width="20px" fill="currentColor"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M11 7h2v2h-2zm0 4h2v6h-2zm1-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z"/></svg>
412
+ <span class="tooltip">Передай этот промокод другу. При активации он получит <strong>{{ "%.0f"|format(bonus_settings.referral_promo_bonus|float) }}</strong> бонусов, а ты получишь <strong>{{ "%.1f"|format(bonus_settings.referrer_first_purchase_percentage|float) }}%</strong> на свой счет "от друзей" с его первой покупки.</span>
413
+ </span>
414
  </p>
415
  <div class="promo-code-display">
416
  <span class="promo-code-value" id="userPromoCode">{{ user.referral_code }}</span>
 
547
  </div>
548
  <div id="invoiceDetailModal" class="modal">
549
  <div class="modal-content">
550
+ <div class="modal-close" onclick="closeModal('invoiceDetailModal')"></div>
551
  <h2 id="invoiceDetailTitle" class="modal-title"></h2>
552
  <ul id="invoiceDetailList" class="invoice-detail-list"></ul>
553
+ <div id="invoiceTotalSection" class="invoice-total-section"></div>
 
 
 
 
 
 
 
 
 
 
 
 
554
  </div>
555
  </div>
556
  {% if is_first_visit %}
557
+ <div id="promoCodeModal" class="modal" style="display: flex; align-items: center;">
558
  <div class="modal-content">
559
  <h2 class="modal-title">Есть промокод?</h2>
560
  <p>Если у вас есть промокод от друга, введите его, чтобы получить бонус.</p>
 
639
  document.querySelector(`.nav-btn[data-target="${sectionId}"]`).classList.add('active');
640
  }
641
 
642
+ function openModal(modalId) { document.getElementById(modalId).style.display = 'flex'; }
643
+ function closeModal(modalId) { document.getElementById(modalId).style.display = 'none'; }
 
 
 
 
 
 
644
 
645
  function openInvoiceDetailModal(invoiceData) {
646
  document.getElementById('invoiceDetailTitle').textContent = `Накладная #${invoiceData.invoice_id}`;
 
652
  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>`;
653
  invoiceDetailList.appendChild(li);
654
  });
 
 
 
 
 
655
 
656
+ const totalSection = document.getElementById('invoiceTotalSection');
657
+ const totalAmount = parseFloat(invoiceData.total_amount);
658
+ const bonusesDeducted = parseFloat(invoiceData.bonuses_deducted || 0);
659
+
660
+ let html = `<div class="total-row"><span>Сумма</span> <span>${totalAmount.toFixed(2)}</span></div>`;
661
  if (bonusesDeducted > 0) {
662
+ html += `<div class="total-row"><span>Списано бонусов</span> <span class="deduction">- ${bonusesDeducted.toFixed(2)}</span></div>`;
663
+ html += `<div class="total-row final"><span оплате</span> <span>${(totalAmount - bonusesDeducted).toFixed(2)}</span></div>`;
 
664
  } else {
665
+ html += `<div class="total-row final"><span оплате</span> <span>${totalAmount.toFixed(2)}</span></div>`;
666
  }
667
+ totalSection.innerHTML = html;
668
 
669
  openModal('invoiceDetailModal');
670
  }
 
705
  statusEl.textContent = error.message;
706
  }
707
  }
 
 
 
 
 
 
708
 
709
  document.addEventListener('DOMContentLoaded', () => {
710
  document.querySelectorAll('.nav-btn').forEach(button => {
 
850
  .item-name { flex-basis: 60%; }
851
  .item-qty-price { flex-basis: 20%; text-align: right; color: #6c757d; }
852
  .item-total { flex-basis: 20%; text-align: right; font-weight: bold; }
853
+ .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; }
854
+ .invoice-total-display div { display: flex; justify-content: space-between; }
855
+ .bonus-source-selector { display: flex; gap: 1rem; margin-bottom: 1rem; align-items: center; }
856
+ .bonus-source-selector label { display: flex; align-items: center; gap: 0.5rem; cursor: pointer; }
857
  </style>
858
  </head>
859
  <body>
 
938
  <div class="tab-buttons">
939
  <button class="tab-btn active" data-tab="bonus-debt-tab">Счета</button>
940
  <button class="tab-btn" data-tab="invoice-tab">Накладные</button>
941
+ <button class="tab-btn" data-tab="referral-history-tab">История</button>
942
  </div>
943
  <div id="bonus-debt-tab" class="tab-content active">
944
  <div class="form-section">
945
  <h3>Основные бонусы</h3>
946
  <div class="form-row">
947
+ <div class="form-group">
948
+ <label for="accrueAmount">Начислить</label>
949
+ <input type="number" id="accrueAmount" placeholder="0" oninput="updateCalculations()">
950
+ </div>
951
+ <div class="form-group">
952
+ <label for="deductAmount">Списать</label>
953
+ <input type="number" id="deductAmount" placeholder="0" oninput="updateCalculations()">
954
+ </div>
955
  </div>
956
  </div>
957
+ <div class="form-section">
958
  <h3>Бонусы от друзей</h3>
959
+ <div class="form-row">
960
+ <div class="form-group">
961
+ <label for="accrueReferralAmount">Начислить</label>
962
+ <input type="number" id="accrueReferralAmount" placeholder="0" oninput="updateCalculations()">
963
+ </div>
964
+ <div class="form-group">
965
+ <label for="deductReferralAmount">Списать</label>
966
+ <input type="number" id="deductReferralAmount" placeholder="0" oninput="updateCalculations()">
967
+ </div>
968
+ </div>
969
  </div>
970
  <div class="form-section">
971
  <h3>Долги</h3>
972
  <div class="form-row">
973
+ <div class="form-group">
974
+ <label for="addDebtAmount">Добавить долг</label>
975
+ <input type="number" id="addDebtAmount" placeholder="0" oninput="updateCalculations()">
976
+ </div>
977
+ <div class="form-group">
978
+ <label for="repayDebtAmount">Погасить долг</label>
979
+ <input type="number" id="repayDebtAmount" placeholder="0" oninput="updateCalculations()">
980
+ </div>
981
  </div>
982
  </div>
983
  <div class="modal-footer">
 
989
  <div class="form-section">
990
  <h3>Добавить новую накладную</h3>
991
  <table class="invoice-items-table" id="newInvoiceItemsTable">
992
+ <thead>
993
+ <tr>
994
+ <th>Товар</th><th>Кол-во</th><th>Цена за ед.</th>
995
+ <th>Сумма</th><th></th>
996
+ </tr>
997
+ </thead>
998
  <tbody></tbody>
999
+ <tfoot>
1000
+ <tr class="total-row">
1001
+ <td colspan="3" style="text-align: right;"><strong>Итоговая сумма:</strong></td>
1002
+ <td id="newInvoiceTotalAmount">0.00</td><td></td>
1003
+ </tr>
1004
+ </tfoot>
1005
  </table>
1006
  <button class="btn btn-primary" style="margin-top: 1rem;" onclick="addNewInvoiceItemRow()">Добавить товар</button>
1007
  </div>
1008
  <div class="form-section">
1009
  <h3>Оплата бонусами</h3>
1010
+ <div class="bonus-source-selector">
1011
+ <strong>Списать с:</strong>
1012
+ <label><input type="radio" name="bonusSource" value="main" onchange="updateNewInvoiceTotal()" checked> Основного счета</label>
1013
+ <label><input type="radio" name="bonusSource" value="referral" onchange="updateNewInvoiceTotal()"> Счета "от друзей"</label>
1014
  </div>
1015
  <p>Доступно для списания: <strong id="invoiceAvailableBonuses">0.00</strong></p>
1016
  <div class="form-group">
1017
+ <label for="invoiceDeductBonuses">Сумма списания</label>
1018
  <input type="number" id="invoiceDeductBonuses" oninput="updateNewInvoiceTotal()" placeholder="0.00" step="0.01">
1019
  </div>
1020
+ <div class="invoice-section-summary">
1021
+ К оплате: <span id="invoiceFinalAmount">0.00</span>
1022
+ </div>
1023
  </div>
1024
  <div class="modal-footer">
1025
  <div id="invoiceStatus" class="status-message"></div>
 
1030
  <ul id="modalInvoiceList" class="invoice-list-admin"></ul>
1031
  </div>
1032
  </div>
1033
+ <div id="referral-history-tab" class="tab-content">
1034
  <div class="history-container">
1035
  <h3>Общая история операций</h3>
1036
  <ul id="modalHistoryList" class="history-list"></ul>
 
1041
  <div id="addClientModal" class="modal">
1042
  <div class="modal-content">
1043
  <span class="modal-close" onclick="closeModal('addClientModal')">×</span>
1044
+ <div class="modal-header">
1045
+ <h2>Добавить нового клиента</h2>
1046
+ </div>
1047
+ <div class="form-group" style="margin-bottom: 1rem;">
1048
+ <label for="newClientFirstName">Имя</label>
1049
+ <input type="text" id="newClientFirstName" placeholder="Иван">
1050
+ </div>
1051
+ <div class="form-group" style="margin-bottom: 1.5rem;">
1052
+ <label for="newClientPhone">Номер телефона (уникальный)</label>
1053
+ <input type="tel" id="newClientPhone" placeholder="+77001234567">
1054
+ </div>
1055
+ <div class="modal-footer">
1056
+ <div id="addClientStatus" class="status-message"></div>
1057
+ <button class="btn-submit" onclick="submitNewClient()">Сохранить клиента</button>
1058
+ </div>
1059
  </div>
1060
  </div>
1061
  <div id="orgSettingsModal" class="modal">
1062
  <div class="modal-content">
1063
  <span class="modal-close" onclick="closeModal('orgSettingsModal')">×</span>
1064
+ <div class="modal-header">
1065
+ <h2>Настройки организации</h2>
1066
+ </div>
1067
  <div class="details-form">
1068
+ <div class="form-group">
1069
+ <label for="orgName">Название организации</label>
1070
+ <input type="text" id="orgName" placeholder="Название вашей организации">
1071
+ </div>
1072
+ <div class="form-group">
1073
+ <label for="orgPhoneNumbers">Номера телефонов (через запятую)</label>
1074
+ <input type="text" id="orgPhoneNumbers" placeholder="+77001112233,+77004445566">
1075
+ </div>
1076
+ <div class="form-group">
1077
+ <label for="orgAddress">Адрес</label>
1078
+ <textarea id="orgAddress" placeholder="Город, улица, дом"></textarea>
1079
+ </div>
1080
+ <div class="form-group">
1081
+ <label for="orgWhatsAppLink">Ссылка на WhatsApp</label>
1082
+ <input type="url" id="orgWhatsAppLink" placeholder="https://wa.me/77001112233">
1083
+ </div>
1084
+ <div class="form-group">
1085
+ <label for="orgTelegramLink">Ссылка на Telegram</label>
1086
+ <input type="url" id="orgTelegramLink" placeholder="https://t.me/your_telegram_username">
1087
+ </div>
1088
+ </div>
1089
+ <div class="modal-footer">
1090
+ <div id="orgStatus" class="status-message"></div>
1091
+ <button class="btn-submit" onclick="saveOrgSettings()">Сохранить настройки</button>
1092
  </div>
 
1093
  </div>
1094
  </div>
1095
  <div id="bonusSettingsModal" class="modal">
1096
  <div class="modal-content">
1097
  <span class="modal-close" onclick="closeModal('bonusSettingsModal')">×</span>
1098
+ <div class="modal-header">
1099
+ <h2>Настройка бонусной программы</h2>
1100
+ </div>
1101
  <div class="details-form">
1102
+ <div class="form-group">
1103
+ <label for="settingInvoiceBonus">Процент бонусов с накладных</label>
1104
+ <div class="form-group-horizontal">
1105
+ <input type="number" step="0.1" id="settingInvoiceBonus" placeholder="2">
1106
+ <span>%</span>
1107
+ </div>
1108
+ </div>
1109
+ <div class="form-group">
1110
+ <label for="settingPromoBonus">Количество бонусов за ввод промокода</label>
1111
+ <input type="number" id="settingPromoBonus" placeholder="50">
1112
+ </div>
1113
+ <div class="form-group">
1114
+ <label for="settingReferrerBonus">Процент партнеру с первой покупки друга</label>
1115
+ <div class="form-group-horizontal">
1116
+ <input type="number" step="0.1" id="settingReferrerBonus" placeholder="5">
1117
+ <span>%</span>
1118
+ </div>
1119
+ </div>
1120
+ </div>
1121
+ <div class="modal-footer">
1122
+ <div id="bonusSettingsStatus" class="status-message"></div>
1123
+ <button class="btn-submit" onclick="saveBonusSettings()">Сохранить настройки</button>
1124
  </div>
 
1125
  </div>
1126
  </div>
1127
  <div id="adminInvoiceDetailModal" class="modal">
1128
  <div class="modal-content">
1129
  <span class="modal-close" onclick="closeModal('adminInvoiceDetailModal')">×</span>
1130
+ <div class="modal-header">
1131
+ <h2 id="adminInvoiceDetailTitle"></h2>
1132
+ </div>
1133
  <ul id="adminInvoiceDetailList" class="invoice-detail-list"></ul>
1134
  <div id="adminInvoiceDetailTotal" class="invoice-total-display"></div>
1135
  </div>
 
1155
  document.getElementById('modalUserId').value = userData.id;
1156
  document.getElementById('modalUserName').textContent = `${userData.first_name || ''} ${userData.last_name || ''}`;
1157
  document.getElementById('modalUserUsername').textContent = `@${userData.username || userData.phone_number || ''} | ID: ${userData.id}`;
1158
+ ['accrueAmount', 'deductAmount', 'addDebtAmount', 'repayDebtAmount', 'accrueReferralAmount', 'deductReferralAmount'].forEach(id => document.getElementById(id).value = '');
1159
  ['modalStatus', 'invoiceStatus'].forEach(id => document.getElementById(id).textContent = '');
1160
+ document.getElementById('invoiceDeductBonuses').value = '';
1161
+
1162
  newInvoiceItems = [];
1163
  renderNewInvoiceItems();
1164
  loadUserHistoryAndInvoices();
 
1178
  const li = document.createElement('li');
1179
  li.className = 'history-item';
1180
  let sign, amountClass, amountText;
1181
+ if (item.transaction_type === 'bonus') {
1182
  sign = item.type === 'accrual' ? '+' : '-';
1183
  amountClass = item.type === 'accrual' ? 'bonus-accrual' : 'bonus-deduction';
1184
  } else if (item.transaction_type === 'debt') {
 
1195
  } else {
1196
  historyList.innerHTML = '<li style="text-align:center; padding: 1rem; color: var(--admin-secondary);">Нет истории</li>';
1197
  }
1198
+
1199
  const modalInvoiceList = document.getElementById('modalInvoiceList');
1200
  modalInvoiceList.innerHTML = '';
1201
  const userInvoices = (currentUserData.invoices || []).sort((a, b) => new Date(b.date) - new Date(a.date));
 
1219
  }
1220
 
1221
  function openOrgSettingsModal() {
1222
+ fetch('/admin/organization_details')
1223
+ .then(response => response.json())
1224
+ .then(data => {
1225
+ document.getElementById('orgName').value = data.name || '';
1226
+ document.getElementById('orgPhoneNumbers').value = (data.phone_numbers || []).join(',') || '';
1227
+ document.getElementById('orgAddress').value = data.address || '';
1228
+ document.getElementById('orgWhatsAppLink').value = data.whatsapp_link || '';
1229
+ document.getElementById('orgTelegramLink').value = data.telegram_link || '';
1230
+ document.getElementById('orgStatus').textContent = '';
1231
+ orgSettingsModal.style.display = 'block';
1232
+ })
1233
+ .catch(error => {
1234
+ console.error('Error fetching organization details:', error);
1235
+ const orgStatus = document.getElementById('orgStatus');
1236
+ orgStatus.style.color = 'var(--admin-danger)';
1237
+ orgStatus.textContent = 'Ошибка загрузки данных.';
1238
+ orgSettingsModal.style.display = 'block';
1239
+ });
1240
  }
1241
 
1242
  function openBonusSettingsModal() {
1243
+ fetch('/admin/bonus_settings')
1244
+ .then(response => response.json())
1245
+ .then(data => {
1246
+ document.getElementById('settingInvoiceBonus').value = data.invoice_bonus_percentage || 0;
1247
+ document.getElementById('settingPromoBonus').value = data.referral_promo_bonus || 0;
1248
+ document.getElementById('settingReferrerBonus').value = data.referrer_first_purchase_percentage || 0;
1249
+ document.getElementById('bonusSettingsStatus').textContent = '';
1250
+ bonusSettingsModal.style.display = 'block';
1251
+ })
1252
+ .catch(error => {
1253
+ console.error('Error fetching bonus settings:', error);
1254
+ document.getElementById('bonusSettingsStatus').textContent = 'Ошибка загрузки настроек.';
1255
+ bonusSettingsModal.style.display = 'block';
1256
+ });
1257
  }
1258
 
1259
  function closeModal(modalId) {
 
1274
 
1275
  function updateCalculations() {
1276
  if (!currentUserData) return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1277
  }
1278
 
1279
  async function submitTransaction() {
1280
  const statusEl = document.getElementById('modalStatus');
1281
+ statusEl.style.color = 'var(--admin-secondary)';
1282
  statusEl.textContent = 'Обработка...';
1283
  const payload = {
1284
  user_id: document.getElementById('modalUserId').value,
1285
  accrue_amount: parseFloat(document.getElementById('accrueAmount').value) || 0,
1286
  deduct_amount: parseFloat(document.getElementById('deductAmount').value) || 0,
1287
+ accrue_referral_amount: parseFloat(document.getElementById('accrueReferralAmount').value) || 0,
1288
  deduct_referral_amount: parseFloat(document.getElementById('deductReferralAmount').value) || 0,
1289
  add_debt_amount: parseFloat(document.getElementById('addDebtAmount').value) || 0,
1290
  repay_debt_amount: parseFloat(document.getElementById('repayDebtAmount').value) || 0,
1291
  };
1292
  if (Object.values(payload).slice(1).every(v => v <= 0)) {
1293
+ statusEl.style.color = 'var(--admin-danger)';
1294
  statusEl.textContent = 'Введите сумму для любой из операций.';
1295
  return;
1296
  }
1297
  try {
1298
+ const response = await fetch('/admin/add_transaction', {
1299
+ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload)
1300
+ });
1301
  const result = await response.json();
1302
  if (response.ok) {
1303
+ statusEl.style.color = 'var(--admin-success)';
1304
  statusEl.textContent = 'Операция успешно проведена!';
1305
  setTimeout(() => location.reload(), 1500);
1306
  } else { throw new Error(result.message || 'Произошла ошибка'); }
1307
  } catch (error) {
1308
+ statusEl.style.color = 'var(--admin-danger)';
1309
  statusEl.textContent = `Ошибка: ${error.message}`;
1310
  }
1311
  }
1312
 
1313
  async function submitNewClient() {
1314
  const statusEl = document.getElementById('addClientStatus');
1315
+ statusEl.style.color = 'var(--admin-secondary)';
1316
  statusEl.textContent = 'Сохранение...';
1317
  const payload = {
1318
  first_name: document.getElementById('newClientFirstName').value.trim(),
1319
  phone_number: document.getElementById('newClientPhone').value.trim(),
1320
  };
1321
  if (!payload.first_name || !payload.phone_number) {
1322
+ statusEl.style.color = 'var(--admin-danger)';
1323
  statusEl.textContent = 'Имя и номер телефона обязательны.';
1324
  return;
1325
  }
1326
  try {
1327
+ const response = await fetch('/admin/add_client', {
1328
+ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload)
1329
+ });
1330
  const result = await response.json();
1331
  if (response.ok) {
1332
+ statusEl.style.color = 'var(--admin-success)';
1333
  statusEl.textContent = 'Клиент успешно добавлен!';
1334
  setTimeout(() => location.reload(), 1500);
1335
  } else { throw new Error(result.message || 'Произошла ошибка'); }
1336
  } catch (error) {
1337
+ statusEl.style.color = 'var(--admin-danger)';
1338
  statusEl.textContent = `Ошибка: ${error.message}`;
1339
  }
1340
  }
1341
 
1342
  async function saveOrgSettings() {
1343
  const statusEl = document.getElementById('orgStatus');
1344
+ statusEl.style.color = 'var(--admin-secondary)';
1345
  statusEl.textContent = 'Сохранение...';
1346
  const phoneNumbersRaw = document.getElementById('orgPhoneNumbers').value.trim();
1347
  const payload = {
 
1352
  telegram_link: document.getElementById('orgTelegramLink').value.trim(),
1353
  };
1354
  try {
1355
+ const response = await fetch('/admin/organization_details', {
1356
+ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload)
1357
+ });
1358
  const result = await response.json();
1359
  if (response.ok) {
1360
+ statusEl.style.color = 'var(--admin-success)';
1361
  statusEl.textContent = 'Настройки организации успешно сохранены!';
1362
  setTimeout(() => closeModal('orgSettingsModal'), 1500);
1363
  } else { throw new Error(result.message || 'Произошла ошибка'); }
1364
  } catch (error) {
1365
+ statusEl.style.color = 'var(--admin-danger)';
1366
  statusEl.textContent = `Ошибка: ${error.message}`;
1367
  }
1368
  }
1369
 
1370
  async function saveBonusSettings() {
1371
  const statusEl = document.getElementById('bonusSettingsStatus');
1372
+ statusEl.style.color = 'var(--admin-secondary)';
1373
  statusEl.textContent = 'Сохранение...';
1374
  const payload = {
1375
  invoice_bonus_percentage: parseFloat(document.getElementById('settingInvoiceBonus').value) || 0,
 
1377
  referrer_first_purchase_percentage: parseFloat(document.getElementById('settingReferrerBonus').value) || 0,
1378
  };
1379
  try {
1380
+ const response = await fetch('/admin/bonus_settings', {
1381
+ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload)
1382
+ });
1383
  const result = await response.json();
1384
  if (response.ok) {
1385
+ statusEl.style.color = 'var(--admin-success)';
1386
  statusEl.textContent = 'Настройки бонусной программы сохранены!';
1387
  setTimeout(() => closeModal('bonusSettingsModal'), 1500);
1388
  } else { throw new Error(result.message || 'Произошла ошибка'); }
1389
  } catch (error) {
1390
+ statusEl.style.color = 'var(--admin-danger)';
1391
  statusEl.textContent = `Ошибка: ${error.message}`;
1392
  }
1393
  }
 
1395
  async function deleteClient(userId) {
1396
  if (!confirm(`Вы уверены, что хотите удалить клиента с ID ${userId}? Это действие необратимо.`)) return;
1397
  try {
1398
+ const response = await fetch('/admin/delete_client', {
1399
+ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ user_id: userId })
1400
+ });
1401
  const result = await response.json();
1402
  if (response.ok) location.reload();
1403
  else throw new Error(result.message || 'Не удалось удалить клиента.');
 
1437
  }
1438
 
1439
  function updateNewInvoiceTotal() {
1440
+ if (!currentUserData) return;
1441
+ const bonusSource = document.querySelector('input[name="bonusSource"]:checked').value;
1442
+ const availableBonuses = bonusSource === 'main' ? (parseFloat(currentUserData.bonuses) || 0) : (parseFloat(currentUserData.referral_bonuses) || 0);
1443
+ document.getElementById('invoiceAvailableBonuses').textContent = availableBonuses.toFixed(2);
1444
+
1445
  let total = newInvoiceItems.reduce((sum, item) => sum + (parseFloat(item.item_total) || 0), 0);
1446
  document.getElementById('newInvoiceTotalAmount').textContent = total.toFixed(2);
1447
 
 
 
 
 
1448
  const deductBonusesInput = document.getElementById('invoiceDeductBonuses');
1449
  let deductAmount = parseFloat(deductBonusesInput.value) || 0;
1450
 
1451
  let cappedDeductAmount = Math.max(0, Math.min(deductAmount, availableBonuses, total));
1452
+ if (deductAmount !== cappedDeductAmount && document.activeElement === deductBonusesInput) {
1453
  deductBonusesInput.value = cappedDeductAmount > 0 ? cappedDeductAmount.toFixed(2) : '';
1454
  }
1455
 
 
1459
 
1460
  async function submitInvoice() {
1461
  const statusEl = document.getElementById('invoiceStatus');
1462
+ statusEl.style.color = 'var(--admin-secondary)';
1463
  statusEl.textContent = 'Сохранение...';
1464
  if (!currentUserData) {
1465
+ statusEl.style.color = 'var(--admin-danger)';
1466
  statusEl.textContent = 'Пользователь не выбран.';
1467
  return;
1468
  }
1469
  const itemsToAdd = newInvoiceItems.filter(item => item.product_name && (item.quantity > 0 || item.unit_price > 0));
1470
  if (itemsToAdd.length === 0) {
1471
+ statusEl.style.color = 'var(--admin-danger)';
1472
  statusEl.textContent = 'Добавьте хотя бы один товар.';
1473
  return;
1474
  }
1475
  const totalAmount = itemsToAdd.reduce((sum, item) => sum + item.item_total, 0);
1476
  const deductBonuses = parseFloat(document.getElementById('invoiceDeductBonuses').value) || 0;
1477
+ const bonusSource = document.querySelector('input[name="bonusSource"]:checked').value;
1478
+
1479
  const payload = {
1480
  user_id: currentUserData.id,
1481
  total_amount: totalAmount,
1482
  items: itemsToAdd,
1483
  deduct_bonuses: deductBonuses,
1484
+ bonus_source: bonusSource
1485
  };
1486
  try {
1487
+ const response = await fetch('/admin/add_invoice', {
1488
+ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload)
1489
+ });
1490
  const result = await response.json();
1491
  if (response.ok) {
1492
+ statusEl.style.color = 'var(--admin-success)';
1493
  statusEl.textContent = 'Накладная успешно сохранена!';
1494
  setTimeout(() => location.reload(), 1500);
1495
  } else { throw new Error(result.message || 'Произошла ошибка'); }
1496
  } catch (error) {
1497
+ statusEl.style.color = 'var(--admin-danger)';
1498
  statusEl.textContent = `Ошибка: ${error.message}`;
1499
  }
1500
  }
1501
 
1502
  function openAdminInvoiceDetailModal(invoiceData) {
1503
  document.getElementById('adminInvoiceDetailTitle').textContent = `Накладная #${invoiceData.invoice_id} от ${invoiceData.date_str}`;
1504
+ const invoiceDetailList = document.getElementById('adminInvoiceDetailList');
1505
+ invoiceDetailList.innerHTML = '';
1506
  invoiceData.items.forEach(item => {
1507
  const li = document.createElement('li');
1508
  li.className = 'invoice-detail-item';
1509
  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>`;
1510
+ invoiceDetailList.appendChild(li);
1511
  });
1512
 
1513
  const totalDisplay = document.getElementById('adminInvoiceDetailTotal');
1514
  let bonusesDeducted = parseFloat(invoiceData.bonuses_deducted || 0);
1515
  let totalAmount = parseFloat(invoiceData.total_amount);
1516
  let finalAmount = totalAmount - bonusesDeducted;
1517
+ let bonusSourceText = invoiceData.bonus_source_used === 'referral' ? ' (от друзей)' : ' (основных)';
1518
+
1519
  let totalHTML = `<div><span>Итого:</span><span>${totalAmount.toFixed(2)}</span></div>`;
1520
  if (bonusesDeducted > 0) {
1521
+ totalHTML += `<div><span>Списано бонусов${bonusSourceText}:</span><span style="color: var(--admin-danger);">- ${bonusesDeducted.toFixed(2)}</span></div>`;
1522
+ totalHTML += `<hr style="border: none; border-top: 1px solid #dee2e6; margin: 5px 0;"><div><strong style="font-size: 1.1em;">К оплате:</strong><strong style="font-size: 1.1em; color: var(--admin-success);">${finalAmount.toFixed(2)}</strong></div>`;
 
1523
  }
1524
  totalDisplay.innerHTML = totalHTML;
1525
  adminInvoiceDetailModal.style.display = 'block';
 
1528
  async function deleteInvoice(userId, invoiceId) {
1529
  if (!confirm(`Вы уверены, что хотите удалить накладную #${invoiceId} для клиента ID ${userId}?`)) return;
1530
  try {
1531
+ const response = await fetch('/admin/delete_invoice', {
1532
+ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ user_id: userId, invoice_id: invoiceId })
1533
+ });
1534
  const result = await response.json();
1535
  if (response.ok) location.reload();
1536
  else throw new Error(result.message || 'Не удалось удалить накладную.');
 
1538
  }
1539
 
1540
  window.onclick = function(event) {
1541
+ if (event.target == transactionModal) closeModal('transactionModal');
1542
+ if (event.target == addClientModal) closeModal('addClientModal');
1543
+ if (event.target == orgSettingsModal) closeModal('orgSettingsModal');
1544
+ if (event.target == bonusSettingsModal) closeModal('bonusSettingsModal');
1545
+ if (event.target == adminInvoiceDetailModal) closeModal('adminInvoiceDetailModal');
1546
  }
1547
 
1548
  document.addEventListener('DOMContentLoaded', () => { addNewInvoiceItemRow(); });
 
1557
  user_data = {}
1558
  is_first_visit = False
1559
 
1560
+ with _data_lock:
1561
+ if user_id_str and user_id_str in visitor_data_cache:
1562
+ user_data = visitor_data_cache[user_id_str]
1563
+ user_data['id'] = user_id_str
1564
+ is_first_visit = not user_data.get('has_been_welcomed', False)
1565
+ bonus_history = user_data.get('history', [])
1566
+ debt_history = user_data.get('debt_history', [])
1567
+ referral_history = user_data.get('referral_bonus_history', [])
1568
+ for item in bonus_history: item['transaction_type'] = 'bonus'
1569
+ for item in debt_history: item['transaction_type'] = 'debt'
1570
+ for item in referral_history: item['transaction_type'] = 'referral'
1571
+ user_data['combined_history'] = sorted(bonus_history + debt_history + referral_history, key=lambda x: datetime.fromisoformat(x['date']), reverse=True)
1572
+ user_data['invoices'] = user_data.get('invoices', [])
1573
+ else:
1574
+ user_data = {"id": "N/A", "bonuses": 0, "debts": 0, "referral_bonuses": 0, "combined_history": [], "invoices": [], "referral_code": "N/A"}
1575
+
1576
+ org_details = visitor_data_cache.get('organization_details', {})
1577
+ bonus_settings = visitor_data_cache.get('bonus_program_settings', {})
1578
+
1579
  return render_template_string(TEMPLATE, user=user_data, org_details=org_details, is_first_visit=is_first_visit, bonus_settings=bonus_settings)
1580
 
1581
  @app.route('/verify', methods=['POST'])
 
1663
  return jsonify({"status": "error", "message": "Промокод не найден."}), 404
1664
  if referrer_id == user_id:
1665
  return jsonify({"status": "error", "message": "Нельзя использовать свой промокод."}), 400
1666
+
 
1667
  user['referred_by'] = referrer_id
1668
+ referrer = visitor_data_cache[referrer_id]
1669
  if 'referrals' not in referrer: referrer['referrals'] = []
1670
  referrer['referrals'].append(user_id)
1671
 
1672
  if promo_bonus > 0:
1673
  user['bonuses'] = user.get('bonuses', 0) + promo_bonus
1674
  now = datetime.now(ALMATY_TZ)
1675
+ history_entry = {
1676
+ "type": "accrual", "amount": promo_bonus, "description": "Бонус за промокод",
1677
+ "date": now.isoformat(), "date_str": now.strftime('%Y-%m-%d %H:%M:%S')
1678
+ }
1679
  if 'history' not in user: user['history'] = []
1680
  user['history'].append(history_entry)
1681
 
 
1706
  total_debts = sum(u.get('debts', 0) for u in users_list)
1707
  total_referral_bonuses = sum(u.get('referral_bonuses', 0) for u in users_list)
1708
 
1709
+ summary_stats = {
1710
+ "total_users": total_users, "total_bonuses": total_bonuses,
1711
+ "total_debts": total_debts, "total_referral_bonuses": total_referral_bonuses
1712
+ }
1713
  return render_template_string(ADMIN_TEMPLATE, users=users_list, summary=summary_stats)
1714
 
1715
  @app.route('/admin/add_client', methods=['POST'])
 
1728
  now = datetime.now(ALMATY_TZ)
1729
  new_id = generate_unique_id(visitor_data_cache)
1730
  new_client = {
1731
+ 'id': new_id, 'telegram_id': None, 'first_name': first_name, 'last_name': None,
1732
+ 'username': None, 'photo_url': None, 'is_premium': False, 'phone_number': phone_number,
1733
+ 'visited_at': now.timestamp(), 'visited_at_str': now.strftime('%Y-%m-%d %H:%M:%S'),
1734
+ 'bonuses': 0, 'history': [], 'debts': 0, 'debt_history': [], 'invoices': [],
1735
+ 'referral_code': f'PROMO{new_id}', 'referred_by': None, 'referrals': [], 'has_been_welcomed': True,
1736
  'referral_bonuses': 0, 'referral_bonus_history': [], 'has_made_first_purchase': False,
1737
  }
1738
  visitor_data_cache[new_id] = new_client
 
1749
  user_id = str(data.get('user_id'))
1750
  accrue_amount = float(data.get('accrue_amount', 0))
1751
  deduct_amount = float(data.get('deduct_amount', 0))
1752
+ accrue_referral_amount = float(data.get('accrue_referral_amount', 0))
1753
  deduct_referral_amount = float(data.get('deduct_referral_amount', 0))
1754
  add_debt_amount = float(data.get('add_debt_amount', 0))
1755
  repay_debt_amount = float(data.get('repay_debt_amount', 0))
 
1762
  now = datetime.now(ALMATY_TZ)
1763
  now_iso, now_str = now.isoformat(), now.strftime('%Y-%m-%d %H:%M:%S')
1764
 
1765
+ if deduct_amount > user.get('bonuses', 0): return jsonify({"status": "error", "message": "Недостаточно основных бонусов для списания"}), 400
1766
  if deduct_referral_amount > user.get('referral_bonuses', 0): return jsonify({"status": "error", "message": "Недостаточно бонусов от друзей для списания"}), 400
1767
+ if repay_debt_amount > user.get('debts', 0): return jsonify({"status": "error", "message": "Сумма погашения превышает текущий долг"}), 400
1768
 
1769
  if 'history' not in user: user['history'] = []
 
 
 
 
 
 
 
1770
  if 'referral_bonus_history' not in user: user['referral_bonus_history'] = []
 
 
 
 
1771
  if 'debt_history' not in user: user['debt_history'] = []
1772
+
1773
+ user['bonuses'] = round(user.get('bonuses', 0) + accrue_amount - deduct_amount, 2)
1774
+ if accrue_amount > 0: user['history'].append({"type": "accrual", "amount": accrue_amount, "description": "Начисление бонусов (админ)", "date": now_iso, "date_str": now_str})
1775
+ if deduct_amount > 0: user['history'].append({"type": "deduction", "amount": deduct_amount, "description": "Списание бонусов (админ)", "date": now_iso, "date_str": now_str})
1776
+
1777
+ user['referral_bonuses'] = round(user.get('referral_bonuses', 0) + accrue_referral_amount - deduct_referral_amount, 2)
1778
+ if accrue_referral_amount > 0: user['referral_bonus_history'].append({"type": "accrual", "amount": accrue_referral_amount, "description": "Начисление бонусов от друзей (админ)", "date": now_iso, "date_str": now_str})
1779
+ if deduct_referral_amount > 0: user['referral_bonus_history'].append({"type": "deduction", "amount": deduct_referral_amount, "description": "Списание бонусов от друзей (админ)", "date": now_iso, "date_str": now_str})
1780
+
1781
+ user['debts'] = round(user.get('debts', 0) + add_debt_amount - repay_debt_amount, 2)
1782
+ if add_debt_amount > 0: user['debt_history'].append({"type": "accrual", "amount": add_debt_amount, "description": "Добавление долга", "date": now_iso, "date_str": now_str})
1783
+ if repay_debt_amount > 0: user['debt_history'].append({"type": "payment", "amount": repay_debt_amount, "description": "Погашение долга", "date": now_iso, "date_str": now_str})
1784
 
1785
  save_visitor_data()
1786
  return jsonify({"status": "ok", "message": "Transaction successful"}), 200
1787
  except Exception as e:
1788
+ logging.exception("Error in /admin/add_transaction endpoint")
1789
  return jsonify({"status": "error", "message": str(e)}), 500
1790
 
1791
  @app.route('/admin/add_invoice', methods=['POST'])
 
1796
  total_amount = float(data.get('total_amount', 0))
1797
  items = data.get('items', [])
1798
  deduct_bonuses = float(data.get('deduct_bonuses', 0))
1799
+ bonus_source = data.get('bonus_source', 'main')
1800
 
1801
  if not user_id: return jsonify({"status": "error", "message": "User ID is required"}), 400
1802
  if not items: return jsonify({"status": "error", "message": "Необходимо добавить товары в накладную."}), 400
 
1805
  if user_id not in visitor_data_cache: return jsonify({"status": "error", "message": "User not found"}), 404
1806
  user = visitor_data_cache[user_id]
1807
 
1808
+ if bonus_source == 'main' and deduct_bonuses > user.get('bonuses', 0): return jsonify({"status": "error", "message": "Недостаточно основных бонусов для списания."}), 400
1809
+ if bonus_source == 'referral' and deduct_bonuses > user.get('referral_bonuses', 0): return jsonify({"status": "error", "message": "Недостаточно бонусов от друзей для списания."}), 400
1810
  if deduct_bonuses > total_amount: return jsonify({"status": "error", "message": "Сумма списания не может превышать сумму накладной."}), 400
1811
 
1812
  now = datetime.now(ALMATY_TZ)
 
1819
  "invoice_id": invoice_id, "date": now_iso, "date_str": now_str,
1820
  "total_amount": round(total_amount, 2), "items": processed_items,
1821
  "bonuses_deducted": round(deduct_bonuses, 2),
1822
+ "bonus_source_used": bonus_source if deduct_bonuses > 0 else None
1823
  }
1824
  if 'invoices' not in user: user['invoices'] = []
1825
  user['invoices'].append(new_invoice)
1826
 
1827
  if deduct_bonuses > 0:
1828
+ if bonus_source == 'main':
 
 
 
 
1829
  user['bonuses'] = round(user.get('bonuses', 0) - deduct_bonuses, 2)
1830
  if 'history' not in user: user['history'] = []
1831
  user['history'].append({"type": "deduction", "amount": deduct_bonuses, "description": f"Оплата по на��ладной #{invoice_id}", "date": now_iso, "date_str": now_str})
1832
+ elif bonus_source == 'referral':
1833
+ user['referral_bonuses'] = round(user.get('referral_bonuses', 0) - deduct_bonuses, 2)
1834
+ if 'referral_bonus_history' not in user: user['referral_bonus_history'] = []
1835
+ user['referral_bonus_history'].append({"type": "deduction", "amount": deduct_bonuses, "description": f"Оплата по накладной #{invoice_id}", "date": now_iso, "date_str": now_str})
1836
+
1837
  bonus_settings = visitor_data_cache.get('bonus_program_settings', {})
1838
  invoice_bonus_percentage = float(bonus_settings.get('invoice_bonus_percentage', 0))
1839
  if invoice_bonus_percentage > 0 and total_amount > 0:
1840
+ bonus_from_invoice = round((total_amount * invoice_bonus_percentage) / 100, 2)
1841
  if bonus_from_invoice > 0:
1842
  user['bonuses'] = user.get('bonuses', 0) + bonus_from_invoice
1843
  if 'history' not in user: user['history'] = []
 
1850
  referrer = visitor_data_cache[referrer_id]
1851
  referrer_bonus_percentage = float(bonus_settings.get('referrer_first_purchase_percentage', 0))
1852
  if referrer_bonus_percentage > 0:
1853
+ commission = round((total_amount * referrer_bonus_percentage) / 100, 2)
1854
  if commission > 0:
1855
  referrer['referral_bonuses'] = referrer.get('referral_bonuses', 0) + commission
1856
  if 'referral_bonus_history' not in referrer: referrer['referral_bonus_history'] = []
1857
+ referrer['referral_bonus_history'].append({
1858
+ "type": "accrual", "amount": commission,
1859
+ "description": f"Бонус от друга {user.get('first_name', 'ID:'+user_id)}",
1860
+ "date": now_iso, "date_str": now_str
1861
+ })
1862
+
1863
  save_visitor_data()
1864
  return jsonify({"status": "ok", "message": "Invoice added successfully", "invoice_id": invoice_id}), 200
1865
  except Exception as e:
 
1961
  if __name__ == '__main__':
1962
  print("--- BONUS SYSTEM SERVER ---")
1963
  print(f"Server starting on http://{HOST}:{PORT}")
1964
+
1965
  print("Attempting to load local data file...")
1966
  load_visitor_data()
1967