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