Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -177,7 +177,7 @@ TEMPLATE = """
|
|
| 177 |
<head>
|
| 178 |
<meta charset="UTF-8">
|
| 179 |
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no, user-scalable=no, viewport-fit=cover">
|
| 180 |
-
<title>
|
| 181 |
<script src="https://telegram.org/js/telegram-web-app.js"></script>
|
| 182 |
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 183 |
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
@@ -186,6 +186,7 @@ TEMPLATE = """
|
|
| 186 |
:root {
|
| 187 |
--brand-yellow: #FFC107;
|
| 188 |
--brand-black: #101010;
|
|
|
|
| 189 |
--card-bg: #1c1c1e;
|
| 190 |
--text-color: #ffffff;
|
| 191 |
--text-secondary-color: #a0a0a0;
|
|
@@ -195,6 +196,8 @@ TEMPLATE = """
|
|
| 195 |
--font-family: 'Manrope', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
| 196 |
--shadow-color: rgba(255, 193, 7, 0.15);
|
| 197 |
--shadow-glow: 0 0 35px var(--shadow-color);
|
|
|
|
|
|
|
| 198 |
}
|
| 199 |
* { box-sizing: border-box; margin: 0; padding: 0; }
|
| 200 |
html, body {
|
|
@@ -213,7 +216,7 @@ TEMPLATE = """
|
|
| 213 |
margin: 0 auto;
|
| 214 |
display: flex;
|
| 215 |
flex-direction: column;
|
| 216 |
-
gap: var(--padding-
|
| 217 |
}
|
| 218 |
.header {
|
| 219 |
text-align: left;
|
|
@@ -231,29 +234,47 @@ TEMPLATE = """
|
|
| 231 |
color: var(--text-secondary-color);
|
| 232 |
margin-top: 4px;
|
| 233 |
}
|
| 234 |
-
.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 235 |
background: linear-gradient(145deg, #2a2a2a, #1c1c1c);
|
| 236 |
border-radius: calc(var(--border-radius) + 8px);
|
| 237 |
padding: var(--padding-l);
|
| 238 |
text-align: center;
|
| 239 |
-
box-shadow: var(--shadow-glow);
|
| 240 |
-
border: 1px solid rgba(255, 193, 7, 0.2);
|
| 241 |
position: relative;
|
| 242 |
overflow: hidden;
|
| 243 |
}
|
| 244 |
-
.bonus-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 245 |
font-size: 1.1em;
|
| 246 |
font-weight: 500;
|
| 247 |
color: var(--text-secondary-color);
|
| 248 |
margin-bottom: 12px;
|
| 249 |
}
|
| 250 |
.bonus-amount {
|
| 251 |
-
font-size:
|
| 252 |
font-weight: 800;
|
| 253 |
color: var(--brand-yellow);
|
| 254 |
letter-spacing: -2px;
|
| 255 |
line-height: 1;
|
| 256 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 257 |
.client-id-card {
|
| 258 |
background-color: var(--card-bg);
|
| 259 |
border-radius: var(--border-radius);
|
|
@@ -318,13 +339,19 @@ TEMPLATE = """
|
|
| 318 |
<body>
|
| 319 |
<div class="container">
|
| 320 |
<header class="header">
|
| 321 |
-
<div class="logo">
|
| 322 |
<p id="greeting" class="welcome-text">Добро пожаловать!</p>
|
| 323 |
</header>
|
| 324 |
|
| 325 |
-
<section class="
|
| 326 |
-
<
|
| 327 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 328 |
</section>
|
| 329 |
|
| 330 |
<section class="client-id-card">
|
|
@@ -334,17 +361,23 @@ TEMPLATE = """
|
|
| 334 |
|
| 335 |
<section class="history-section">
|
| 336 |
<h2 class="history-title">История операций</h2>
|
| 337 |
-
{% if user.
|
| 338 |
<ul class="history-list">
|
| 339 |
-
{% for item in user.
|
| 340 |
<li class="history-item">
|
| 341 |
<div class="history-details">
|
| 342 |
<span class="history-description">{{ item.description }}</span>
|
| 343 |
<span class="history-date">{{ item.date_str }}</span>
|
| 344 |
</div>
|
| 345 |
-
|
| 346 |
-
{{ '
|
| 347 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 348 |
</li>
|
| 349 |
{% endfor %}
|
| 350 |
</ul>
|
|
@@ -440,7 +473,7 @@ ADMIN_TEMPLATE = """
|
|
| 440 |
<head>
|
| 441 |
<meta charset="UTF-8">
|
| 442 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 443 |
-
<title>
|
| 444 |
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 445 |
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 446 |
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
|
@@ -463,6 +496,12 @@ ADMIN_TEMPLATE = """
|
|
| 463 |
body { font-family: var(--font-family); background-color: var(--admin-bg); color: var(--admin-text); margin: 0; padding: var(--padding); line-height: 1.6; }
|
| 464 |
.container { max-width: 1200px; margin: 0 auto; }
|
| 465 |
h1 { text-align: center; color: var(--admin-secondary); margin-bottom: var(--padding); font-weight: 600; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 466 |
.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); }
|
| 467 |
.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; }
|
| 468 |
.btn { padding: 12px 20px; font-size: 1em; border: none; border-radius: 8px; font-weight: 600; cursor: pointer; transition: background-color 0.2s ease; }
|
|
@@ -470,31 +509,34 @@ ADMIN_TEMPLATE = """
|
|
| 470 |
.btn-primary:hover { background-color: var(--admin-primary-dark); }
|
| 471 |
.btn-delete { background-color: var(--admin-danger); color: white; }
|
| 472 |
.btn-delete:hover { background-color: #c82333; }
|
| 473 |
-
.user-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(
|
| 474 |
.user-card { background-color: var(--admin-card-bg); border-radius: var(--border-radius); padding: var(--padding); box-shadow: 0 4px 15px var(--admin-shadow); border: 1px solid var(--admin-border); display: flex; flex-direction: column; transition: transform 0.2s ease, box-shadow 0.2s ease; }
|
| 475 |
.user-card:hover { transform: translateY(-5px); box-shadow: 0 8px 25px rgba(0, 0, 0, 0.08); }
|
| 476 |
.user-info { display: flex; align-items: center; gap: 1rem; margin-bottom: 1rem; }
|
| 477 |
.user-info img { width: 60px; height: 60px; border-radius: 50%; object-fit: cover; border: 3px solid var(--admin-border); background-color: #eee; }
|
| 478 |
.user-details .name { font-weight: 600; font-size: 1.2em; }
|
| 479 |
.user-details .username { color: var(--admin-secondary); font-size: 0.9em; }
|
| 480 |
-
.user-
|
| 481 |
-
.user-
|
| 482 |
-
.user-
|
|
|
|
| 483 |
.user-actions { margin-top: auto; display: flex; flex-direction: column; gap: 0.5rem; }
|
| 484 |
.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; }
|
| 485 |
.btn-manage:hover { background-color: var(--admin-primary-dark); }
|
| 486 |
.no-users { text-align: center; color: var(--admin-secondary); margin-top: 2rem; font-size: 1.1em; }
|
| 487 |
.modal { display: none; position: fixed; z-index: 1001; left: 0; top: 0; width: 100%; height: 100%; overflow: auto; background-color: rgba(0,0,0,0.5); backdrop-filter: blur(5px); }
|
| 488 |
-
.modal-content { background-color: var(--admin-bg); margin:
|
| 489 |
.modal-close { color: #aaa; position: absolute; top: 15px; right: 25px; font-size: 28px; font-weight: bold; cursor: pointer; }
|
| 490 |
.modal-header { padding-bottom: 1rem; margin-bottom: 1.5rem; border-bottom: 1px solid var(--admin-border); }
|
| 491 |
.modal-header h2 { margin: 0; font-size: 1.5rem; }
|
| 492 |
.modal-header .username { font-size: 1rem; color: var(--admin-secondary); }
|
| 493 |
-
.form-
|
|
|
|
|
|
|
| 494 |
.form-group { display: flex; flex-direction: column; }
|
| 495 |
.form-group label { margin-bottom: 0.5rem; font-weight: 500; font-size: 0.9em; }
|
| 496 |
.form-group input { padding: 10px; font-size: 1rem; border: 1px solid var(--admin-border); border-radius: 8px; width: 100%; box-sizing: border-box; }
|
| 497 |
-
.calculation-summary { background: #f0f0f0; padding: 1rem; border-radius: 8px; margin-
|
| 498 |
.summary-item { display: flex; justify-content: space-between; margin-bottom: 0.5rem; font-size: 0.95em; }
|
| 499 |
.summary-item strong { font-weight: 600; }
|
| 500 |
.history-container { margin-top: 1.5rem; }
|
|
@@ -504,8 +546,10 @@ ADMIN_TEMPLATE = """
|
|
| 504 |
.history-item:last-child { border-bottom: none; }
|
| 505 |
.history-item .desc { font-size: 0.9em; }
|
| 506 |
.history-item .date { font-size: 0.8em; color: var(--admin-secondary); }
|
| 507 |
-
.history-item .amount.accrual { color: var(--admin-success); font-weight: 600; }
|
| 508 |
-
.history-item .amount.deduction { color: var(--admin-danger); font-weight: 600; }
|
|
|
|
|
|
|
| 509 |
.modal-footer { margin-top: 1.5rem; display: flex; justify-content: flex-end; align-items: center; gap: 1rem;}
|
| 510 |
.modal-footer button { padding: 12px 25px; font-size: 1.1em; border-radius: 8px; border: none; font-weight: 600; cursor: pointer; }
|
| 511 |
.btn-submit { background-color: var(--admin-success); color: white; }
|
|
@@ -514,7 +558,26 @@ ADMIN_TEMPLATE = """
|
|
| 514 |
</head>
|
| 515 |
<body>
|
| 516 |
<div class="container">
|
| 517 |
-
<h1>Панель администратора
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 518 |
<div class="controls-bar">
|
| 519 |
<input type="text" id="searchInput" onkeyup="searchUsers()" placeholder="Поиск по имени, ID, username, номеру...">
|
| 520 |
<button class="btn btn-primary" onclick="openAddClientModal()">Добавить клиента</button>
|
|
@@ -531,12 +594,18 @@ ADMIN_TEMPLATE = """
|
|
| 531 |
<div class="username">@{{ user.username if user.username else user.phone_number }} | ID: {{ user.id }}</div>
|
| 532 |
</div>
|
| 533 |
</div>
|
| 534 |
-
<div class="user-
|
| 535 |
-
<div
|
| 536 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 537 |
</div>
|
| 538 |
<div class="user-actions">
|
| 539 |
-
<button class="btn-manage" onclick='openTransactionModal({{ user|tojson }})'>Управление
|
| 540 |
{% if user.telegram_id == None %}
|
| 541 |
<button class="btn btn-delete" onclick='deleteClient("{{ user.id }}")'>Удалить клиента</button>
|
| 542 |
{% endif %}
|
|
@@ -557,25 +626,51 @@ ADMIN_TEMPLATE = """
|
|
| 557 |
<div id="modalUserUsername" class="username"></div>
|
| 558 |
</div>
|
| 559 |
<input type="hidden" id="modalUserId">
|
| 560 |
-
|
| 561 |
-
|
| 562 |
-
|
| 563 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 564 |
</div>
|
| 565 |
-
<div class="
|
| 566 |
-
<
|
| 567 |
-
<
|
|
|
|
|
|
|
|
|
|
| 568 |
</div>
|
| 569 |
</div>
|
| 570 |
-
|
| 571 |
-
|
| 572 |
-
<
|
| 573 |
-
<div class="
|
| 574 |
-
|
| 575 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 576 |
</div>
|
|
|
|
| 577 |
<div class="history-container">
|
| 578 |
-
<h3
|
| 579 |
<ul id="modalHistoryList" class="history-list"></ul>
|
| 580 |
</div>
|
| 581 |
<div class="modal-footer">
|
|
@@ -631,23 +726,37 @@ ADMIN_TEMPLATE = """
|
|
| 631 |
document.getElementById('modalUserUsername').textContent = `@${userData.username || userData.phone_number} | ID: ${userData.id}`;
|
| 632 |
document.getElementById('purchaseAmount').value = '';
|
| 633 |
document.getElementById('deductAmount').value = '';
|
|
|
|
|
|
|
| 634 |
document.getElementById('modalStatus').textContent = '';
|
| 635 |
|
| 636 |
const historyList = document.getElementById('modalHistoryList');
|
| 637 |
historyList.innerHTML = '';
|
| 638 |
-
|
| 639 |
-
|
| 640 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 641 |
const li = document.createElement('li');
|
| 642 |
li.className = 'history-item';
|
| 643 |
-
|
| 644 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 645 |
li.innerHTML = `
|
| 646 |
<div>
|
| 647 |
<div class="desc">${item.description}</div>
|
| 648 |
<div class="date">${item.date_str}</div>
|
| 649 |
</div>
|
| 650 |
-
<div class="amount ${amountClass}">${
|
| 651 |
`;
|
| 652 |
historyList.appendChild(li);
|
| 653 |
});
|
|
@@ -655,7 +764,7 @@ ADMIN_TEMPLATE = """
|
|
| 655 |
historyList.innerHTML = '<li style="text-align:center; padding: 1rem; color: var(--admin-secondary);">Нет истории</li>';
|
| 656 |
}
|
| 657 |
|
| 658 |
-
|
| 659 |
transactionModal.style.display = 'block';
|
| 660 |
}
|
| 661 |
|
|
@@ -673,8 +782,9 @@ ADMIN_TEMPLATE = """
|
|
| 673 |
}
|
| 674 |
}
|
| 675 |
|
| 676 |
-
function
|
| 677 |
if (!currentUserData) return;
|
|
|
|
| 678 |
const currentBalance = parseFloat(currentUserData.bonuses) || 0;
|
| 679 |
const purchaseAmount = parseFloat(document.getElementById('purchaseAmount').value) || 0;
|
| 680 |
const deductAmount = parseFloat(document.getElementById('deductAmount').value) || 0;
|
|
@@ -682,27 +792,45 @@ ADMIN_TEMPLATE = """
|
|
| 682 |
let finalDeductAmount = deductAmount;
|
| 683 |
if (deductAmount > currentBalance) {
|
| 684 |
finalDeductAmount = currentBalance;
|
| 685 |
-
document.getElementById('deductAmount').value = finalDeductAmount.toFixed(2);
|
| 686 |
}
|
| 687 |
const finalBalance = currentBalance + accrualAmount - finalDeductAmount;
|
| 688 |
document.getElementById('summaryCurrentBalance').textContent = currentBalance.toFixed(2);
|
| 689 |
document.getElementById('summaryAccrual').textContent = `+${accrualAmount.toFixed(2)}`;
|
| 690 |
document.getElementById('summaryDeduction').textContent = `-${finalDeductAmount.toFixed(2)}`;
|
| 691 |
document.getElementById('summaryFinalBalance').textContent = finalBalance.toFixed(2);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 692 |
}
|
| 693 |
|
| 694 |
async function submitTransaction() {
|
| 695 |
const statusEl = document.getElementById('modalStatus');
|
| 696 |
statusEl.style.color = 'var(--admin-secondary)';
|
| 697 |
statusEl.textContent = 'Обработка...';
|
|
|
|
| 698 |
const payload = {
|
| 699 |
user_id: document.getElementById('modalUserId').value,
|
| 700 |
purchase_amount: parseFloat(document.getElementById('purchaseAmount').value) || 0,
|
| 701 |
deduct_amount: parseFloat(document.getElementById('deductAmount').value) || 0,
|
|
|
|
|
|
|
| 702 |
};
|
| 703 |
-
|
|
|
|
| 704 |
statusEl.style.color = 'var(--admin-danger)';
|
| 705 |
-
statusEl.textContent = 'Введите сумму
|
| 706 |
return;
|
| 707 |
}
|
| 708 |
try {
|
|
@@ -805,8 +933,30 @@ def index():
|
|
| 805 |
if user_id_str and user_id_str in current_data:
|
| 806 |
user_data = current_data[user_id_str]
|
| 807 |
user_data['id'] = user_id_str
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 808 |
else:
|
| 809 |
-
user_data = {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 810 |
|
| 811 |
return render_template_string(TEMPLATE, user=user_data)
|
| 812 |
|
|
@@ -868,7 +1018,9 @@ def verify_data():
|
|
| 868 |
'visited_at': now.timestamp(),
|
| 869 |
'visited_at_str': now.strftime('%Y-%m-%d %H:%M:%S'),
|
| 870 |
'bonuses': 0,
|
| 871 |
-
'history': []
|
|
|
|
|
|
|
| 872 |
}
|
| 873 |
user_id_to_save = new_user_id
|
| 874 |
|
|
@@ -892,7 +1044,20 @@ def admin_panel():
|
|
| 892 |
for user_id, user_data in current_data.items():
|
| 893 |
user_data['id'] = user_id
|
| 894 |
users_list.append(user_data)
|
| 895 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 896 |
|
| 897 |
@app.route('/admin/add_client', methods=['POST'])
|
| 898 |
def add_client():
|
|
@@ -926,7 +1091,9 @@ def add_client():
|
|
| 926 |
'visited_at': now.timestamp(),
|
| 927 |
'visited_at_str': now.strftime('%Y-%m-%d %H:%M:%S'),
|
| 928 |
'bonuses': 0,
|
| 929 |
-
'history': []
|
|
|
|
|
|
|
| 930 |
}
|
| 931 |
|
| 932 |
save_visitor_data({new_id: new_client})
|
|
@@ -945,6 +1112,8 @@ def add_transaction():
|
|
| 945 |
user_id = data.get('user_id')
|
| 946 |
purchase_amount = float(data.get('purchase_amount', 0))
|
| 947 |
deduct_amount = float(data.get('deduct_amount', 0))
|
|
|
|
|
|
|
| 948 |
|
| 949 |
if not user_id:
|
| 950 |
return jsonify({"status": "error", "message": "User ID is required"}), 400
|
|
@@ -957,39 +1126,58 @@ def add_transaction():
|
|
| 957 |
|
| 958 |
user = all_data[user_id_str]
|
| 959 |
now = datetime.now()
|
|
|
|
| 960 |
now_str = now.strftime('%Y-%m-%d %H:%M:%S')
|
| 961 |
|
| 962 |
-
accrual_amount = purchase_amount * 0.02
|
| 963 |
-
|
| 964 |
if deduct_amount > user.get('bonuses', 0):
|
| 965 |
-
return jsonify({"status": "error", "message": "
|
|
|
|
|
|
|
|
|
|
| 966 |
|
|
|
|
|
|
|
| 967 |
user['bonuses'] = user.get('bonuses', 0) + accrual_amount - deduct_amount
|
| 968 |
-
|
| 969 |
if 'history' not in user or not isinstance(user['history'], list):
|
| 970 |
user['history'] = []
|
| 971 |
|
| 972 |
if accrual_amount > 0:
|
| 973 |
user['history'].append({
|
| 974 |
-
"type": "accrual",
|
| 975 |
-
"amount": accrual_amount,
|
| 976 |
"description": f"Начисление с покупки {purchase_amount}",
|
| 977 |
-
"date":
|
| 978 |
-
"date_str": now_str
|
| 979 |
})
|
| 980 |
-
|
| 981 |
if deduct_amount > 0:
|
| 982 |
user['history'].append({
|
| 983 |
-
"type": "deduction",
|
| 984 |
-
"amount": deduct_amount,
|
| 985 |
"description": "Списание бонусов",
|
| 986 |
-
"date":
|
| 987 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 988 |
})
|
| 989 |
|
| 990 |
save_visitor_data({user_id_str: user})
|
| 991 |
|
| 992 |
-
return jsonify({
|
|
|
|
|
|
|
|
|
|
| 993 |
|
| 994 |
except Exception as e:
|
| 995 |
logging.exception("Error in /admin/add_transaction endpoint")
|
|
@@ -1033,7 +1221,7 @@ def delete_client():
|
|
| 1033 |
return jsonify({"status": "error", "message": str(e)}), 500
|
| 1034 |
|
| 1035 |
if __name__ == '__main__':
|
| 1036 |
-
print("---
|
| 1037 |
print(f"Server starting on http://{HOST}:{PORT}")
|
| 1038 |
if not HF_TOKEN_READ or not HF_TOKEN_WRITE:
|
| 1039 |
print("WARNING: Hugging Face token(s) not set. Backup/restore functionality will be limited.")
|
|
|
|
| 177 |
<head>
|
| 178 |
<meta charset="UTF-8">
|
| 179 |
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no, user-scalable=no, viewport-fit=cover">
|
| 180 |
+
<title>Bonus</title>
|
| 181 |
<script src="https://telegram.org/js/telegram-web-app.js"></script>
|
| 182 |
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 183 |
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
|
|
| 186 |
:root {
|
| 187 |
--brand-yellow: #FFC107;
|
| 188 |
--brand-black: #101010;
|
| 189 |
+
--brand-red: #F44336;
|
| 190 |
--card-bg: #1c1c1e;
|
| 191 |
--text-color: #ffffff;
|
| 192 |
--text-secondary-color: #a0a0a0;
|
|
|
|
| 196 |
--font-family: 'Manrope', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
| 197 |
--shadow-color: rgba(255, 193, 7, 0.15);
|
| 198 |
--shadow-glow: 0 0 35px var(--shadow-color);
|
| 199 |
+
--shadow-color-red: rgba(244, 67, 54, 0.15);
|
| 200 |
+
--shadow-glow-red: 0 0 35px var(--shadow-color-red);
|
| 201 |
}
|
| 202 |
* { box-sizing: border-box; margin: 0; padding: 0; }
|
| 203 |
html, body {
|
|
|
|
| 216 |
margin: 0 auto;
|
| 217 |
display: flex;
|
| 218 |
flex-direction: column;
|
| 219 |
+
gap: var(--padding-m);
|
| 220 |
}
|
| 221 |
.header {
|
| 222 |
text-align: left;
|
|
|
|
| 234 |
color: var(--text-secondary-color);
|
| 235 |
margin-top: 4px;
|
| 236 |
}
|
| 237 |
+
.card-grid {
|
| 238 |
+
display: grid;
|
| 239 |
+
grid-template-columns: 1fr 1fr;
|
| 240 |
+
gap: var(--padding-m);
|
| 241 |
+
}
|
| 242 |
+
.bonus-card, .debt-card {
|
| 243 |
background: linear-gradient(145deg, #2a2a2a, #1c1c1c);
|
| 244 |
border-radius: calc(var(--border-radius) + 8px);
|
| 245 |
padding: var(--padding-l);
|
| 246 |
text-align: center;
|
|
|
|
|
|
|
| 247 |
position: relative;
|
| 248 |
overflow: hidden;
|
| 249 |
}
|
| 250 |
+
.bonus-card {
|
| 251 |
+
box-shadow: var(--shadow-glow);
|
| 252 |
+
border: 1px solid rgba(255, 193, 7, 0.2);
|
| 253 |
+
}
|
| 254 |
+
.debt-card {
|
| 255 |
+
box-shadow: var(--shadow-glow-red);
|
| 256 |
+
border: 1px solid rgba(244, 67, 54, 0.2);
|
| 257 |
+
}
|
| 258 |
+
.card-label {
|
| 259 |
font-size: 1.1em;
|
| 260 |
font-weight: 500;
|
| 261 |
color: var(--text-secondary-color);
|
| 262 |
margin-bottom: 12px;
|
| 263 |
}
|
| 264 |
.bonus-amount {
|
| 265 |
+
font-size: 3em;
|
| 266 |
font-weight: 800;
|
| 267 |
color: var(--brand-yellow);
|
| 268 |
letter-spacing: -2px;
|
| 269 |
line-height: 1;
|
| 270 |
}
|
| 271 |
+
.debt-amount {
|
| 272 |
+
font-size: 3em;
|
| 273 |
+
font-weight: 800;
|
| 274 |
+
color: var(--brand-red);
|
| 275 |
+
letter-spacing: -2px;
|
| 276 |
+
line-height: 1;
|
| 277 |
+
}
|
| 278 |
.client-id-card {
|
| 279 |
background-color: var(--card-bg);
|
| 280 |
border-radius: var(--border-radius);
|
|
|
|
| 339 |
<body>
|
| 340 |
<div class="container">
|
| 341 |
<header class="header">
|
| 342 |
+
<div class="logo">BONUS<span>.</span></div>
|
| 343 |
<p id="greeting" class="welcome-text">Добро пожаловать!</p>
|
| 344 |
</header>
|
| 345 |
|
| 346 |
+
<section class="card-grid">
|
| 347 |
+
<div class="bonus-card">
|
| 348 |
+
<p class="card-label">Ваши бонусы</p>
|
| 349 |
+
<p class="bonus-amount">{{ "%.2f"|format(user.bonuses|float) }}</p>
|
| 350 |
+
</div>
|
| 351 |
+
<div class="debt-card">
|
| 352 |
+
<p class="card-label">Ваш долг</p>
|
| 353 |
+
<p class="debt-amount">{{ "%.2f"|format(user.debts|float) }}</p>
|
| 354 |
+
</div>
|
| 355 |
</section>
|
| 356 |
|
| 357 |
<section class="client-id-card">
|
|
|
|
| 361 |
|
| 362 |
<section class="history-section">
|
| 363 |
<h2 class="history-title">История операций</h2>
|
| 364 |
+
{% if user.combined_history %}
|
| 365 |
<ul class="history-list">
|
| 366 |
+
{% for item in user.combined_history %}
|
| 367 |
<li class="history-item">
|
| 368 |
<div class="history-details">
|
| 369 |
<span class="history-description">{{ item.description }}</span>
|
| 370 |
<span class="history-date">{{ item.date_str }}</span>
|
| 371 |
</div>
|
| 372 |
+
{% if item.transaction_type == 'bonus' %}
|
| 373 |
+
<span class="history-amount {{ 'accrual' if item.type == 'accrual' else 'deduction' }}">
|
| 374 |
+
{{ '+' if item.type == 'accrual' else '-' }}{{ "%.2f"|format(item.amount|float) }}
|
| 375 |
+
</span>
|
| 376 |
+
{% elif item.transaction_type == 'debt' %}
|
| 377 |
+
<span class="history-amount {{ 'deduction' if item.type == 'accrual' else 'accrual' }}">
|
| 378 |
+
{{ '+' if item.type == 'accrual' else '-' }}{{ "%.2f"|format(item.amount|float) }}
|
| 379 |
+
</span>
|
| 380 |
+
{% endif %}
|
| 381 |
</li>
|
| 382 |
{% endfor %}
|
| 383 |
</ul>
|
|
|
|
| 473 |
<head>
|
| 474 |
<meta charset="UTF-8">
|
| 475 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 476 |
+
<title>Bonus Admin</title>
|
| 477 |
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 478 |
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 479 |
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
|
|
|
| 496 |
body { font-family: var(--font-family); background-color: var(--admin-bg); color: var(--admin-text); margin: 0; padding: var(--padding); line-height: 1.6; }
|
| 497 |
.container { max-width: 1200px; margin: 0 auto; }
|
| 498 |
h1 { text-align: center; color: var(--admin-secondary); margin-bottom: var(--padding); font-weight: 600; }
|
| 499 |
+
.summary-bar { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: var(--padding); margin-bottom: var(--padding); }
|
| 500 |
+
.summary-card { 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); text-align: center; }
|
| 501 |
+
.summary-card .value { font-size: 2em; font-weight: 700; }
|
| 502 |
+
.summary-card .label { font-size: 0.9em; color: var(--admin-secondary); margin-top: 0.5rem; }
|
| 503 |
+
.summary-card .value.bonus { color: var(--admin-primary-dark); }
|
| 504 |
+
.summary-card .value.debt { color: var(--admin-danger); }
|
| 505 |
.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); }
|
| 506 |
.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; }
|
| 507 |
.btn { padding: 12px 20px; font-size: 1em; border: none; border-radius: 8px; font-weight: 600; cursor: pointer; transition: background-color 0.2s ease; }
|
|
|
|
| 509 |
.btn-primary:hover { background-color: var(--admin-primary-dark); }
|
| 510 |
.btn-delete { background-color: var(--admin-danger); color: white; }
|
| 511 |
.btn-delete:hover { background-color: #c82333; }
|
| 512 |
+
.user-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); gap: var(--padding); margin-top: var(--padding); }
|
| 513 |
.user-card { background-color: var(--admin-card-bg); border-radius: var(--border-radius); padding: var(--padding); box-shadow: 0 4px 15px var(--admin-shadow); border: 1px solid var(--admin-border); display: flex; flex-direction: column; transition: transform 0.2s ease, box-shadow 0.2s ease; }
|
| 514 |
.user-card:hover { transform: translateY(-5px); box-shadow: 0 8px 25px rgba(0, 0, 0, 0.08); }
|
| 515 |
.user-info { display: flex; align-items: center; gap: 1rem; margin-bottom: 1rem; }
|
| 516 |
.user-info img { width: 60px; height: 60px; border-radius: 50%; object-fit: cover; border: 3px solid var(--admin-border); background-color: #eee; }
|
| 517 |
.user-details .name { font-weight: 600; font-size: 1.2em; }
|
| 518 |
.user-details .username { color: var(--admin-secondary); font-size: 0.9em; }
|
| 519 |
+
.user-balances { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; text-align: center; margin-bottom: 1rem; }
|
| 520 |
+
.user-balances .label { font-size: 0.9em; color: var(--admin-secondary); }
|
| 521 |
+
.user-balances .amount.bonus { font-size: 1.8em; font-weight: 700; color: var(--admin-primary-dark); }
|
| 522 |
+
.user-balances .amount.debt { font-size: 1.8em; font-weight: 700; color: var(--admin-danger); }
|
| 523 |
.user-actions { margin-top: auto; display: flex; flex-direction: column; gap: 0.5rem; }
|
| 524 |
.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; }
|
| 525 |
.btn-manage:hover { background-color: var(--admin-primary-dark); }
|
| 526 |
.no-users { text-align: center; color: var(--admin-secondary); margin-top: 2rem; font-size: 1.1em; }
|
| 527 |
.modal { display: none; position: fixed; z-index: 1001; left: 0; top: 0; width: 100%; height: 100%; overflow: auto; background-color: rgba(0,0,0,0.5); backdrop-filter: blur(5px); }
|
| 528 |
+
.modal-content { background-color: var(--admin-bg); margin: 5% auto; padding: var(--padding); border: 1px solid var(--admin-border); width: 90%; max-width: 700px; border-radius: var(--border-radius); position: relative; box-shadow: 0 8px 30px rgba(0,0,0,0.15); }
|
| 529 |
.modal-close { color: #aaa; position: absolute; top: 15px; right: 25px; font-size: 28px; font-weight: bold; cursor: pointer; }
|
| 530 |
.modal-header { padding-bottom: 1rem; margin-bottom: 1.5rem; border-bottom: 1px solid var(--admin-border); }
|
| 531 |
.modal-header h2 { margin: 0; font-size: 1.5rem; }
|
| 532 |
.modal-header .username { font-size: 1rem; color: var(--admin-secondary); }
|
| 533 |
+
.form-section { border: 1px solid var(--admin-border); border-radius: 8px; padding: 1rem; margin-bottom: 1.5rem; }
|
| 534 |
+
.form-section h3 { margin-top: 0; margin-bottom: 1rem; font-size: 1.1em; }
|
| 535 |
+
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; align-items: flex-end; }
|
| 536 |
.form-group { display: flex; flex-direction: column; }
|
| 537 |
.form-group label { margin-bottom: 0.5rem; font-weight: 500; font-size: 0.9em; }
|
| 538 |
.form-group input { padding: 10px; font-size: 1rem; border: 1px solid var(--admin-border); border-radius: 8px; width: 100%; box-sizing: border-box; }
|
| 539 |
+
.calculation-summary { background: #f0f0f0; padding: 1rem; border-radius: 8px; margin-top: 1rem; }
|
| 540 |
.summary-item { display: flex; justify-content: space-between; margin-bottom: 0.5rem; font-size: 0.95em; }
|
| 541 |
.summary-item strong { font-weight: 600; }
|
| 542 |
.history-container { margin-top: 1.5rem; }
|
|
|
|
| 546 |
.history-item:last-child { border-bottom: none; }
|
| 547 |
.history-item .desc { font-size: 0.9em; }
|
| 548 |
.history-item .date { font-size: 0.8em; color: var(--admin-secondary); }
|
| 549 |
+
.history-item .amount.bonus-accrual { color: var(--admin-success); font-weight: 600; }
|
| 550 |
+
.history-item .amount.bonus-deduction { color: var(--admin-danger); font-weight: 600; }
|
| 551 |
+
.history-item .amount.debt-accrual { color: var(--admin-danger); font-weight: 600; }
|
| 552 |
+
.history-item .amount.debt-payment { color: var(--admin-success); font-weight: 600; }
|
| 553 |
.modal-footer { margin-top: 1.5rem; display: flex; justify-content: flex-end; align-items: center; gap: 1rem;}
|
| 554 |
.modal-footer button { padding: 12px 25px; font-size: 1.1em; border-radius: 8px; border: none; font-weight: 600; cursor: pointer; }
|
| 555 |
.btn-submit { background-color: var(--admin-success); color: white; }
|
|
|
|
| 558 |
</head>
|
| 559 |
<body>
|
| 560 |
<div class="container">
|
| 561 |
+
<h1>Панель администратора Bonus</h1>
|
| 562 |
+
<div class="summary-bar">
|
| 563 |
+
<div class="summary-card">
|
| 564 |
+
<div class="value">{{ summary.total_users }}</div>
|
| 565 |
+
<div class="label">Всего клиентов</div>
|
| 566 |
+
</div>
|
| 567 |
+
<div class="summary-card">
|
| 568 |
+
<div class="value bonus">{{ "%.2f"|format(summary.total_bonuses|float) }}</div>
|
| 569 |
+
<div class="label">Всего бонусов</div>
|
| 570 |
+
</div>
|
| 571 |
+
<div class="summary-card">
|
| 572 |
+
<div class="value debt">{{ "%.2f"|format(summary.total_debts|float) }}</div>
|
| 573 |
+
<div class="label">Всего долгов</div>
|
| 574 |
+
</div>
|
| 575 |
+
<div class="summary-card">
|
| 576 |
+
<div class="value debt">{{ summary.users_with_debt }}</div>
|
| 577 |
+
<div class="label">Клиенты с долгом</div>
|
| 578 |
+
</div>
|
| 579 |
+
</div>
|
| 580 |
+
|
| 581 |
<div class="controls-bar">
|
| 582 |
<input type="text" id="searchInput" onkeyup="searchUsers()" placeholder="Поиск по имени, ID, username, номеру...">
|
| 583 |
<button class="btn btn-primary" onclick="openAddClientModal()">Добавить клиента</button>
|
|
|
|
| 594 |
<div class="username">@{{ user.username if user.username else user.phone_number }} | ID: {{ user.id }}</div>
|
| 595 |
</div>
|
| 596 |
</div>
|
| 597 |
+
<div class="user-balances">
|
| 598 |
+
<div>
|
| 599 |
+
<div class="label">Бонусы</div>
|
| 600 |
+
<div class="amount bonus">{{ "%.2f"|format(user.bonuses|float) }}</div>
|
| 601 |
+
</div>
|
| 602 |
+
<div>
|
| 603 |
+
<div class="label">Долг</div>
|
| 604 |
+
<div class="amount debt">{{ "%.2f"|format(user.debts|float if user.debts else 0) }}</div>
|
| 605 |
+
</div>
|
| 606 |
</div>
|
| 607 |
<div class="user-actions">
|
| 608 |
+
<button class="btn-manage" onclick='openTransactionModal({{ user|tojson }})'>Управление счетом</button>
|
| 609 |
{% if user.telegram_id == None %}
|
| 610 |
<button class="btn btn-delete" onclick='deleteClient("{{ user.id }}")'>Удалить клиента</button>
|
| 611 |
{% endif %}
|
|
|
|
| 626 |
<div id="modalUserUsername" class="username"></div>
|
| 627 |
</div>
|
| 628 |
<input type="hidden" id="modalUserId">
|
| 629 |
+
|
| 630 |
+
<div class="form-section">
|
| 631 |
+
<h3>Бонусы</h3>
|
| 632 |
+
<div class="form-row">
|
| 633 |
+
<div class="form-group">
|
| 634 |
+
<label for="purchaseAmount">Сумма покупки (для начисления)</label>
|
| 635 |
+
<input type="number" id="purchaseAmount" placeholder="1500" oninput="updateCalculations()">
|
| 636 |
+
</div>
|
| 637 |
+
<div class="form-group">
|
| 638 |
+
<label for="deductAmount">Списать бонусов</label>
|
| 639 |
+
<input type="number" id="deductAmount" placeholder="100" oninput="updateCalculations()">
|
| 640 |
+
</div>
|
| 641 |
</div>
|
| 642 |
+
<div class="calculation-summary">
|
| 643 |
+
<div class="summary-item"><span>Текущий баланс:</span> <strong id="summaryCurrentBalance">0.00</strong></div>
|
| 644 |
+
<div class="summary-item"><span>Будет начислено (2%):</span> <strong id="summaryAccrual">+0.00</strong></div>
|
| 645 |
+
<div class="summary-item"><span>Будет списано:</span> <strong id="summaryDeduction">-0.00</strong></div>
|
| 646 |
+
<hr>
|
| 647 |
+
<div class="summary-item"><strong>Итоговый баланс бонусов:</strong> <strong id="summaryFinalBalance">0.00</strong></div>
|
| 648 |
</div>
|
| 649 |
</div>
|
| 650 |
+
|
| 651 |
+
<div class="form-section">
|
| 652 |
+
<h3>Долги</h3>
|
| 653 |
+
<div class="form-row">
|
| 654 |
+
<div class="form-group">
|
| 655 |
+
<label for="addDebtAmount">Добавить долг</label>
|
| 656 |
+
<input type="number" id="addDebtAmount" placeholder="500" oninput="updateCalculations()">
|
| 657 |
+
</div>
|
| 658 |
+
<div class="form-group">
|
| 659 |
+
<label for="repayDebtAmount">Погасить долг</label>
|
| 660 |
+
<input type="number" id="repayDebtAmount" placeholder="200" oninput="updateCalculations()">
|
| 661 |
+
</div>
|
| 662 |
+
</div>
|
| 663 |
+
<div class="calculation-summary">
|
| 664 |
+
<div class="summary-item"><span>Текущий долг:</span> <strong id="summaryCurrentDebt">0.00</strong></div>
|
| 665 |
+
<div class="summary-item"><span>Будет добавлено:</span> <strong id="summaryAddDebt">+0.00</strong></div>
|
| 666 |
+
<div class="summary-item"><span>Будет погашено:</span> <strong id="summaryRepayDebt">-0.00</strong></div>
|
| 667 |
+
<hr>
|
| 668 |
+
<div class="summary-item"><strong>Итоговый долг:</strong> <strong id="summaryFinalDebt">0.00</strong></div>
|
| 669 |
+
</div>
|
| 670 |
</div>
|
| 671 |
+
|
| 672 |
<div class="history-container">
|
| 673 |
+
<h3>Общая история операций</h3>
|
| 674 |
<ul id="modalHistoryList" class="history-list"></ul>
|
| 675 |
</div>
|
| 676 |
<div class="modal-footer">
|
|
|
|
| 726 |
document.getElementById('modalUserUsername').textContent = `@${userData.username || userData.phone_number} | ID: ${userData.id}`;
|
| 727 |
document.getElementById('purchaseAmount').value = '';
|
| 728 |
document.getElementById('deductAmount').value = '';
|
| 729 |
+
document.getElementById('addDebtAmount').value = '';
|
| 730 |
+
document.getElementById('repayDebtAmount').value = '';
|
| 731 |
document.getElementById('modalStatus').textContent = '';
|
| 732 |
|
| 733 |
const historyList = document.getElementById('modalHistoryList');
|
| 734 |
historyList.innerHTML = '';
|
| 735 |
+
|
| 736 |
+
const bonusHistory = (userData.history || []).map(h => ({...h, transaction_type: 'bonus'}));
|
| 737 |
+
const debtHistory = (userData.debt_history || []).map(h => ({...h, transaction_type: 'debt'}));
|
| 738 |
+
const combinedHistory = [...bonusHistory, ...debtHistory].sort((a, b) => new Date(b.date) - new Date(a.date));
|
| 739 |
+
|
| 740 |
+
if (combinedHistory.length > 0) {
|
| 741 |
+
combinedHistory.forEach(item => {
|
| 742 |
const li = document.createElement('li');
|
| 743 |
li.className = 'history-item';
|
| 744 |
+
let sign, amountClass, amountText;
|
| 745 |
+
if (item.transaction_type === 'bonus') {
|
| 746 |
+
sign = item.type === 'accrual' ? '+' : '-';
|
| 747 |
+
amountClass = item.type === 'accrual' ? 'bonus-accrual' : 'bonus-deduction';
|
| 748 |
+
amountText = `${sign}${parseFloat(item.amount).toFixed(2)} (бонус)`;
|
| 749 |
+
} else { // debt
|
| 750 |
+
sign = item.type === 'accrual' ? '+' : '-';
|
| 751 |
+
amountClass = item.type === 'accrual' ? 'debt-accrual' : 'debt-payment';
|
| 752 |
+
amountText = `${sign}${parseFloat(item.amount).toFixed(2)} (долг)`;
|
| 753 |
+
}
|
| 754 |
li.innerHTML = `
|
| 755 |
<div>
|
| 756 |
<div class="desc">${item.description}</div>
|
| 757 |
<div class="date">${item.date_str}</div>
|
| 758 |
</div>
|
| 759 |
+
<div class="amount ${amountClass}">${amountText}</div>
|
| 760 |
`;
|
| 761 |
historyList.appendChild(li);
|
| 762 |
});
|
|
|
|
| 764 |
historyList.innerHTML = '<li style="text-align:center; padding: 1rem; color: var(--admin-secondary);">Нет истории</li>';
|
| 765 |
}
|
| 766 |
|
| 767 |
+
updateCalculations();
|
| 768 |
transactionModal.style.display = 'block';
|
| 769 |
}
|
| 770 |
|
|
|
|
| 782 |
}
|
| 783 |
}
|
| 784 |
|
| 785 |
+
function updateCalculations() {
|
| 786 |
if (!currentUserData) return;
|
| 787 |
+
|
| 788 |
const currentBalance = parseFloat(currentUserData.bonuses) || 0;
|
| 789 |
const purchaseAmount = parseFloat(document.getElementById('purchaseAmount').value) || 0;
|
| 790 |
const deductAmount = parseFloat(document.getElementById('deductAmount').value) || 0;
|
|
|
|
| 792 |
let finalDeductAmount = deductAmount;
|
| 793 |
if (deductAmount > currentBalance) {
|
| 794 |
finalDeductAmount = currentBalance;
|
| 795 |
+
document.getElementById('deductAmount').value = finalDeductAmount > 0 ? finalDeductAmount.toFixed(2) : '';
|
| 796 |
}
|
| 797 |
const finalBalance = currentBalance + accrualAmount - finalDeductAmount;
|
| 798 |
document.getElementById('summaryCurrentBalance').textContent = currentBalance.toFixed(2);
|
| 799 |
document.getElementById('summaryAccrual').textContent = `+${accrualAmount.toFixed(2)}`;
|
| 800 |
document.getElementById('summaryDeduction').textContent = `-${finalDeductAmount.toFixed(2)}`;
|
| 801 |
document.getElementById('summaryFinalBalance').textContent = finalBalance.toFixed(2);
|
| 802 |
+
|
| 803 |
+
const currentDebt = parseFloat(currentUserData.debts) || 0;
|
| 804 |
+
const addDebtAmount = parseFloat(document.getElementById('addDebtAmount').value) || 0;
|
| 805 |
+
const repayDebtAmount = parseFloat(document.getElementById('repayDebtAmount').value) || 0;
|
| 806 |
+
let finalRepayAmount = repayDebtAmount;
|
| 807 |
+
if (repayDebtAmount > currentDebt) {
|
| 808 |
+
finalRepayAmount = currentDebt;
|
| 809 |
+
document.getElementById('repayDebtAmount').value = finalRepayAmount > 0 ? finalRepayAmount.toFixed(2) : '';
|
| 810 |
+
}
|
| 811 |
+
const finalDebt = currentDebt + addDebtAmount - finalRepayAmount;
|
| 812 |
+
document.getElementById('summaryCurrentDebt').textContent = currentDebt.toFixed(2);
|
| 813 |
+
document.getElementById('summaryAddDebt').textContent = `+${addDebtAmount.toFixed(2)}`;
|
| 814 |
+
document.getElementById('summaryRepayDebt').textContent = `-${finalRepayAmount.toFixed(2)}`;
|
| 815 |
+
document.getElementById('summaryFinalDebt').textContent = finalDebt.toFixed(2);
|
| 816 |
}
|
| 817 |
|
| 818 |
async function submitTransaction() {
|
| 819 |
const statusEl = document.getElementById('modalStatus');
|
| 820 |
statusEl.style.color = 'var(--admin-secondary)';
|
| 821 |
statusEl.textContent = 'Обработка...';
|
| 822 |
+
|
| 823 |
const payload = {
|
| 824 |
user_id: document.getElementById('modalUserId').value,
|
| 825 |
purchase_amount: parseFloat(document.getElementById('purchaseAmount').value) || 0,
|
| 826 |
deduct_amount: parseFloat(document.getElementById('deductAmount').value) || 0,
|
| 827 |
+
add_debt_amount: parseFloat(document.getElementById('addDebtAmount').value) || 0,
|
| 828 |
+
repay_debt_amount: parseFloat(document.getElementById('repayDebtAmount').value) || 0,
|
| 829 |
};
|
| 830 |
+
|
| 831 |
+
if (payload.purchase_amount <= 0 && payload.deduct_amount <= 0 && payload.add_debt_amount <= 0 && payload.repay_debt_amount <= 0) {
|
| 832 |
statusEl.style.color = 'var(--admin-danger)';
|
| 833 |
+
statusEl.textContent = 'Введите сумму для любой из операций.';
|
| 834 |
return;
|
| 835 |
}
|
| 836 |
try {
|
|
|
|
| 933 |
if user_id_str and user_id_str in current_data:
|
| 934 |
user_data = current_data[user_id_str]
|
| 935 |
user_data['id'] = user_id_str
|
| 936 |
+
|
| 937 |
+
bonus_history = user_data.get('history', [])
|
| 938 |
+
for item in bonus_history:
|
| 939 |
+
item['transaction_type'] = 'bonus'
|
| 940 |
+
|
| 941 |
+
debt_history = user_data.get('debt_history', [])
|
| 942 |
+
for item in debt_history:
|
| 943 |
+
item['transaction_type'] = 'debt'
|
| 944 |
+
|
| 945 |
+
combined_history = sorted(
|
| 946 |
+
bonus_history + debt_history,
|
| 947 |
+
key=lambda x: x['date'],
|
| 948 |
+
reverse=True
|
| 949 |
+
)
|
| 950 |
+
user_data['combined_history'] = combined_history
|
| 951 |
else:
|
| 952 |
+
user_data = {
|
| 953 |
+
"id": "N/A",
|
| 954 |
+
"bonuses": 0,
|
| 955 |
+
"debts": 0,
|
| 956 |
+
"history": [],
|
| 957 |
+
"debt_history": [],
|
| 958 |
+
"combined_history": []
|
| 959 |
+
}
|
| 960 |
|
| 961 |
return render_template_string(TEMPLATE, user=user_data)
|
| 962 |
|
|
|
|
| 1018 |
'visited_at': now.timestamp(),
|
| 1019 |
'visited_at_str': now.strftime('%Y-%m-%d %H:%M:%S'),
|
| 1020 |
'bonuses': 0,
|
| 1021 |
+
'history': [],
|
| 1022 |
+
'debts': 0,
|
| 1023 |
+
'debt_history': []
|
| 1024 |
}
|
| 1025 |
user_id_to_save = new_user_id
|
| 1026 |
|
|
|
|
| 1044 |
for user_id, user_data in current_data.items():
|
| 1045 |
user_data['id'] = user_id
|
| 1046 |
users_list.append(user_data)
|
| 1047 |
+
|
| 1048 |
+
total_users = len(users_list)
|
| 1049 |
+
total_bonuses = sum(u.get('bonuses', 0) for u in users_list)
|
| 1050 |
+
total_debts = sum(u.get('debts', 0) for u in users_list)
|
| 1051 |
+
users_with_debt = sum(1 for u in users_list if u.get('debts', 0) > 0)
|
| 1052 |
+
|
| 1053 |
+
summary_stats = {
|
| 1054 |
+
"total_users": total_users,
|
| 1055 |
+
"total_bonuses": total_bonuses,
|
| 1056 |
+
"total_debts": total_debts,
|
| 1057 |
+
"users_with_debt": users_with_debt
|
| 1058 |
+
}
|
| 1059 |
+
|
| 1060 |
+
return render_template_string(ADMIN_TEMPLATE, users=users_list, summary=summary_stats)
|
| 1061 |
|
| 1062 |
@app.route('/admin/add_client', methods=['POST'])
|
| 1063 |
def add_client():
|
|
|
|
| 1091 |
'visited_at': now.timestamp(),
|
| 1092 |
'visited_at_str': now.strftime('%Y-%m-%d %H:%M:%S'),
|
| 1093 |
'bonuses': 0,
|
| 1094 |
+
'history': [],
|
| 1095 |
+
'debts': 0,
|
| 1096 |
+
'debt_history': []
|
| 1097 |
}
|
| 1098 |
|
| 1099 |
save_visitor_data({new_id: new_client})
|
|
|
|
| 1112 |
user_id = data.get('user_id')
|
| 1113 |
purchase_amount = float(data.get('purchase_amount', 0))
|
| 1114 |
deduct_amount = float(data.get('deduct_amount', 0))
|
| 1115 |
+
add_debt_amount = float(data.get('add_debt_amount', 0))
|
| 1116 |
+
repay_debt_amount = float(data.get('repay_debt_amount', 0))
|
| 1117 |
|
| 1118 |
if not user_id:
|
| 1119 |
return jsonify({"status": "error", "message": "User ID is required"}), 400
|
|
|
|
| 1126 |
|
| 1127 |
user = all_data[user_id_str]
|
| 1128 |
now = datetime.now()
|
| 1129 |
+
now_iso = now.isoformat()
|
| 1130 |
now_str = now.strftime('%Y-%m-%d %H:%M:%S')
|
| 1131 |
|
|
|
|
|
|
|
| 1132 |
if deduct_amount > user.get('bonuses', 0):
|
| 1133 |
+
return jsonify({"status": "error", "message": "Недостаточно бонусов для списания"}), 400
|
| 1134 |
+
|
| 1135 |
+
if repay_debt_amount > user.get('debts', 0):
|
| 1136 |
+
return jsonify({"status": "error", "message": "Сумма погашения превышает текущий долг"}), 400
|
| 1137 |
|
| 1138 |
+
# Bonus operations
|
| 1139 |
+
accrual_amount = purchase_amount * 0.02
|
| 1140 |
user['bonuses'] = user.get('bonuses', 0) + accrual_amount - deduct_amount
|
|
|
|
| 1141 |
if 'history' not in user or not isinstance(user['history'], list):
|
| 1142 |
user['history'] = []
|
| 1143 |
|
| 1144 |
if accrual_amount > 0:
|
| 1145 |
user['history'].append({
|
| 1146 |
+
"type": "accrual", "amount": accrual_amount,
|
|
|
|
| 1147 |
"description": f"Начисление с покупки {purchase_amount}",
|
| 1148 |
+
"date": now_iso, "date_str": now_str
|
|
|
|
| 1149 |
})
|
|
|
|
| 1150 |
if deduct_amount > 0:
|
| 1151 |
user['history'].append({
|
| 1152 |
+
"type": "deduction", "amount": deduct_amount,
|
|
|
|
| 1153 |
"description": "Списание бонусов",
|
| 1154 |
+
"date": now_iso, "date_str": now_str
|
| 1155 |
+
})
|
| 1156 |
+
|
| 1157 |
+
# Debt operations
|
| 1158 |
+
user['debts'] = user.get('debts', 0) + add_debt_amount - repay_debt_amount
|
| 1159 |
+
if 'debt_history' not in user or not isinstance(user['debt_history'], list):
|
| 1160 |
+
user['debt_history'] = []
|
| 1161 |
+
|
| 1162 |
+
if add_debt_amount > 0:
|
| 1163 |
+
user['debt_history'].append({
|
| 1164 |
+
"type": "accrual", "amount": add_debt_amount,
|
| 1165 |
+
"description": "Добавление долга",
|
| 1166 |
+
"date": now_iso, "date_str": now_str
|
| 1167 |
+
})
|
| 1168 |
+
if repay_debt_amount > 0:
|
| 1169 |
+
user['debt_history'].append({
|
| 1170 |
+
"type": "payment", "amount": repay_debt_amount,
|
| 1171 |
+
"description": "Погашение долга",
|
| 1172 |
+
"date": now_iso, "date_str": now_str
|
| 1173 |
})
|
| 1174 |
|
| 1175 |
save_visitor_data({user_id_str: user})
|
| 1176 |
|
| 1177 |
+
return jsonify({
|
| 1178 |
+
"status": "ok", "message": "Transaction successful",
|
| 1179 |
+
"new_balance": user['bonuses'], "new_debt": user['debts']
|
| 1180 |
+
}), 200
|
| 1181 |
|
| 1182 |
except Exception as e:
|
| 1183 |
logging.exception("Error in /admin/add_transaction endpoint")
|
|
|
|
| 1221 |
return jsonify({"status": "error", "message": str(e)}), 500
|
| 1222 |
|
| 1223 |
if __name__ == '__main__':
|
| 1224 |
+
print("--- BONUS SYSTEM SERVER ---")
|
| 1225 |
print(f"Server starting on http://{HOST}:{PORT}")
|
| 1226 |
if not HF_TOKEN_READ or not HF_TOKEN_WRITE:
|
| 1227 |
print("WARNING: Hugging Face token(s) not set. Backup/restore functionality will be limited.")
|