Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -365,14 +365,14 @@ TEMPLATE = """
|
|
| 365 |
if (themeParams.button_text_color) root.style.setProperty('--tg-theme-button-text-color', themeParams.button_text_color);
|
| 366 |
if (themeParams.secondary_bg_color) root.style.setProperty('--tg-theme-secondary-bg-color', themeParams.secondary_bg_color);
|
| 367 |
}
|
| 368 |
-
|
| 369 |
function setupTelegram() {
|
| 370 |
if (!tg || !tg.initData) {
|
| 371 |
console.error("Telegram WebApp script not loaded or initData is missing.");
|
| 372 |
document.body.style.visibility = 'visible';
|
| 373 |
return;
|
| 374 |
}
|
| 375 |
-
|
| 376 |
tg.ready();
|
| 377 |
tg.expand();
|
| 378 |
|
|
@@ -381,38 +381,50 @@ TEMPLATE = """
|
|
| 381 |
}
|
| 382 |
tg.onEvent('themeChanged', () => applyTheme(tg.themeParams));
|
| 383 |
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 401 |
|
| 402 |
const user = tg.initDataUnsafe?.user;
|
| 403 |
const greetingElement = document.getElementById('greeting');
|
| 404 |
if (user) {
|
| 405 |
const name = user.first_name || user.username || 'Гость';
|
| 406 |
greetingElement.textContent = `Добро пожаловать, ${name}! 👋`;
|
|
|
|
|
|
|
| 407 |
}
|
| 408 |
-
|
| 409 |
-
document.body.style.visibility = 'visible';
|
| 410 |
}
|
| 411 |
|
| 412 |
if (window.Telegram && window.Telegram.WebApp) {
|
| 413 |
setupTelegram();
|
| 414 |
} else {
|
| 415 |
-
|
| 416 |
setTimeout(() => {
|
| 417 |
if (document.body.style.visibility !== 'visible') {
|
| 418 |
document.body.style.visibility = 'visible';
|
|
@@ -453,8 +465,11 @@ ADMIN_TEMPLATE = """
|
|
| 453 |
body { font-family: var(--font-family); background-color: var(--admin-bg); color: var(--admin-text); margin: 0; padding: var(--padding); line-height: 1.6; }
|
| 454 |
.container { max-width: 1200px; margin: 0 auto; }
|
| 455 |
h1 { text-align: center; color: var(--admin-secondary); margin-bottom: var(--padding); font-weight: 600; }
|
| 456 |
-
.controls-bar { 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); }
|
| 457 |
-
.controls-bar input[type="text"] {
|
|
|
|
|
|
|
|
|
|
| 458 |
.user-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: var(--padding); margin-top: var(--padding); }
|
| 459 |
.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; }
|
| 460 |
.user-card:hover { transform: translateY(-5px); box-shadow: 0 8px 25px rgba(0, 0, 0, 0.08); }
|
|
@@ -465,10 +480,9 @@ ADMIN_TEMPLATE = """
|
|
| 465 |
.user-bonuses { text-align: center; margin-bottom: 1rem; }
|
| 466 |
.user-bonuses .label { font-size: 0.9em; color: var(--admin-secondary); }
|
| 467 |
.user-bonuses .amount { font-size: 1.8em; font-weight: 700; color: var(--admin-primary-dark); }
|
| 468 |
-
.user-actions .btn { 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; }
|
| 469 |
-
.user-actions .btn:hover { background-color: var(--admin-primary-dark); }
|
| 470 |
.no-users { text-align: center; color: var(--admin-secondary); margin-top: 2rem; font-size: 1.1em; }
|
| 471 |
-
|
| 472 |
.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); }
|
| 473 |
.modal-content { background-color: var(--admin-bg); margin: 10% auto; padding: var(--padding); border: 1px solid var(--admin-border); width: 90%; max-width: 600px; border-radius: var(--border-radius); position: relative; box-shadow: 0 8px 30px rgba(0,0,0,0.15); }
|
| 474 |
.modal-close { color: #aaa; position: absolute; top: 15px; right: 25px; font-size: 28px; font-weight: bold; cursor: pointer; }
|
|
@@ -478,7 +492,7 @@ ADMIN_TEMPLATE = """
|
|
| 478 |
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; align-items: center; margin-bottom: 1.5rem; }
|
| 479 |
.form-group { display: flex; flex-direction: column; }
|
| 480 |
.form-group label { margin-bottom: 0.5rem; font-weight: 500; font-size: 0.9em; }
|
| 481 |
-
.form-group input { padding: 10px; font-size: 1rem; border: 1px solid var(--admin-border); border-radius: 8px; }
|
| 482 |
.calculation-summary { background: #f0f0f0; padding: 1rem; border-radius: 8px; margin-bottom: 1.5rem; }
|
| 483 |
.summary-item { display: flex; justify-content: space-between; margin-bottom: 0.5rem; font-size: 0.95em; }
|
| 484 |
.summary-item strong { font-weight: 600; }
|
|
@@ -491,16 +505,18 @@ ADMIN_TEMPLATE = """
|
|
| 491 |
.history-item .date { font-size: 0.8em; color: var(--admin-secondary); }
|
| 492 |
.history-item .amount.accrual { color: var(--admin-success); font-weight: 600; }
|
| 493 |
.history-item .amount.deduction { color: var(--admin-danger); font-weight: 600; }
|
|
|
|
| 494 |
.modal-footer button { padding: 12px 25px; font-size: 1.1em; border-radius: 8px; border: none; font-weight: 600; cursor: pointer; }
|
| 495 |
.btn-submit { background-color: var(--admin-success); color: white; }
|
| 496 |
-
.status-message {
|
| 497 |
</style>
|
| 498 |
</head>
|
| 499 |
<body>
|
| 500 |
<div class="container">
|
| 501 |
<h1>Панель администратора Druzhba</h1>
|
| 502 |
<div class="controls-bar">
|
| 503 |
-
<input type="text" id="searchInput" onkeyup="searchUsers()" placeholder="Поиск по имени, ID, username
|
|
|
|
| 504 |
</div>
|
| 505 |
|
| 506 |
{% if users %}
|
|
@@ -519,7 +535,7 @@ ADMIN_TEMPLATE = """
|
|
| 519 |
<div class="amount">{{ "%.2f"|format(user.bonuses|float) }}</div>
|
| 520 |
</div>
|
| 521 |
<div class="user-actions">
|
| 522 |
-
<button class="btn" onclick='openTransactionModal({{ user|tojson }})'>Управление бонусами</button>
|
| 523 |
</div>
|
| 524 |
</div>
|
| 525 |
{% endfor %}
|
|
@@ -531,14 +547,12 @@ ADMIN_TEMPLATE = """
|
|
| 531 |
|
| 532 |
<div id="transactionModal" class="modal">
|
| 533 |
<div class="modal-content">
|
| 534 |
-
<span class="modal-close" onclick="closeModal()">×</span>
|
| 535 |
<div class="modal-header">
|
| 536 |
<h2 id="modalUserName"></h2>
|
| 537 |
<div id="modalUserUsername" class="username"></div>
|
| 538 |
</div>
|
| 539 |
-
|
| 540 |
<input type="hidden" id="modalUserId">
|
| 541 |
-
|
| 542 |
<div class="form-row">
|
| 543 |
<div class="form-group">
|
| 544 |
<label for="purchaseAmount">Сумма покупки</label>
|
|
@@ -549,7 +563,6 @@ ADMIN_TEMPLATE = """
|
|
| 549 |
<input type="number" id="deductAmount" placeholder="Например, 100" oninput="calculateBonuses()">
|
| 550 |
</div>
|
| 551 |
</div>
|
| 552 |
-
|
| 553 |
<div class="calculation-summary">
|
| 554 |
<div class="summary-item"><span>Текущий баланс:</span> <strong id="summaryCurrentBalance">0.00</strong></div>
|
| 555 |
<div class="summary-item"><span>Будет начислено (2%):</span> <strong id="summaryAccrual">+0.00</strong></div>
|
|
@@ -557,22 +570,45 @@ ADMIN_TEMPLATE = """
|
|
| 557 |
<hr>
|
| 558 |
<div class="summary-item"><strong>Итоговый баланс:</strong> <strong id="summaryFinalBalance">0.00</strong></div>
|
| 559 |
</div>
|
| 560 |
-
|
| 561 |
<div class="history-container">
|
| 562 |
<h3>История операций</h3>
|
| 563 |
<ul id="modalHistoryList" class="history-list"></ul>
|
| 564 |
</div>
|
| 565 |
-
|
| 566 |
<div class="modal-footer">
|
| 567 |
-
<button class="btn-submit" onclick="submitTransaction()">Провести операцию</button>
|
| 568 |
<div id="modalStatus" class="status-message"></div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 569 |
</div>
|
| 570 |
</div>
|
| 571 |
</div>
|
| 572 |
|
| 573 |
<script>
|
| 574 |
-
const
|
| 575 |
-
const
|
| 576 |
let currentUserData = null;
|
| 577 |
|
| 578 |
function searchUsers() {
|
|
@@ -620,31 +656,36 @@ ADMIN_TEMPLATE = """
|
|
| 620 |
}
|
| 621 |
|
| 622 |
calculateBonuses();
|
| 623 |
-
|
| 624 |
}
|
| 625 |
|
| 626 |
-
function
|
| 627 |
-
|
| 628 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 629 |
}
|
| 630 |
|
| 631 |
function calculateBonuses() {
|
| 632 |
if (!currentUserData) return;
|
| 633 |
-
|
| 634 |
const currentBalance = parseFloat(currentUserData.bonuses) || 0;
|
| 635 |
const purchaseAmount = parseFloat(document.getElementById('purchaseAmount').value) || 0;
|
| 636 |
const deductAmount = parseFloat(document.getElementById('deductAmount').value) || 0;
|
| 637 |
-
|
| 638 |
const accrualAmount = purchaseAmount * 0.02;
|
| 639 |
-
|
| 640 |
let finalDeductAmount = deductAmount;
|
| 641 |
if (deductAmount > currentBalance) {
|
| 642 |
finalDeductAmount = currentBalance;
|
| 643 |
-
document.getElementById('deductAmount').value = finalDeductAmount;
|
| 644 |
}
|
| 645 |
-
|
| 646 |
const finalBalance = currentBalance + accrualAmount - finalDeductAmount;
|
| 647 |
-
|
| 648 |
document.getElementById('summaryCurrentBalance').textContent = currentBalance.toFixed(2);
|
| 649 |
document.getElementById('summaryAccrual').textContent = `+${accrualAmount.toFixed(2)}`;
|
| 650 |
document.getElementById('summaryDeduction').textContent = `-${finalDeductAmount.toFixed(2)}`;
|
|
@@ -655,19 +696,16 @@ ADMIN_TEMPLATE = """
|
|
| 655 |
const statusEl = document.getElementById('modalStatus');
|
| 656 |
statusEl.style.color = 'var(--admin-secondary)';
|
| 657 |
statusEl.textContent = 'Обработка...';
|
| 658 |
-
|
| 659 |
const payload = {
|
| 660 |
user_id: document.getElementById('modalUserId').value,
|
| 661 |
purchase_amount: parseFloat(document.getElementById('purchaseAmount').value) || 0,
|
| 662 |
deduct_amount: parseFloat(document.getElementById('deductAmount').value) || 0,
|
| 663 |
};
|
| 664 |
-
|
| 665 |
if (payload.purchase_amount <= 0 && payload.deduct_amount <= 0) {
|
| 666 |
statusEl.style.color = 'var(--admin-danger)';
|
| 667 |
statusEl.textContent = 'Введите сумму покупки или сумму для списания.';
|
| 668 |
return;
|
| 669 |
}
|
| 670 |
-
|
| 671 |
try {
|
| 672 |
const response = await fetch('/admin/add_transaction', {
|
| 673 |
method: 'POST',
|
|
@@ -675,13 +713,47 @@ ADMIN_TEMPLATE = """
|
|
| 675 |
body: JSON.stringify(payload)
|
| 676 |
});
|
| 677 |
const result = await response.json();
|
| 678 |
-
|
| 679 |
if (response.ok) {
|
| 680 |
statusEl.style.color = 'var(--admin-success)';
|
| 681 |
statusEl.textContent = 'Операция успешно проведена!';
|
| 682 |
-
setTimeout(() => {
|
| 683 |
-
|
| 684 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 685 |
} else {
|
| 686 |
throw new Error(result.message || 'Произошла ошибка');
|
| 687 |
}
|
|
@@ -692,8 +764,11 @@ ADMIN_TEMPLATE = """
|
|
| 692 |
}
|
| 693 |
|
| 694 |
window.onclick = function(event) {
|
| 695 |
-
if (event.target ==
|
| 696 |
-
closeModal();
|
|
|
|
|
|
|
|
|
|
| 697 |
}
|
| 698 |
}
|
| 699 |
</script>
|
|
@@ -770,9 +845,9 @@ def verify_data():
|
|
| 770 |
|
| 771 |
save_visitor_data({user_id_str: user_entry})
|
| 772 |
|
| 773 |
-
return
|
| 774 |
-
|
| 775 |
-
|
| 776 |
else:
|
| 777 |
logging.warning(f"Verification failed for user: {user_info_dict.get('id')}")
|
| 778 |
return jsonify({"status": "error", "verified": False, "message": "Invalid data"}), 403
|
|
@@ -781,13 +856,52 @@ def verify_data():
|
|
| 781 |
logging.exception("Error in /verify endpoint")
|
| 782 |
return jsonify({"status": "error", "message": "Internal server error"}), 500
|
| 783 |
|
| 784 |
-
|
| 785 |
@app.route('/admin')
|
| 786 |
def admin_panel():
|
| 787 |
current_data = load_visitor_data()
|
| 788 |
users_list = list(current_data.values())
|
| 789 |
return render_template_string(ADMIN_TEMPLATE, users=users_list)
|
| 790 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 791 |
@app.route('/admin/add_transaction', methods=['POST'])
|
| 792 |
def add_transaction():
|
| 793 |
try:
|
|
|
|
| 365 |
if (themeParams.button_text_color) root.style.setProperty('--tg-theme-button-text-color', themeParams.button_text_color);
|
| 366 |
if (themeParams.secondary_bg_color) root.style.setProperty('--tg-theme-secondary-bg-color', themeParams.secondary_bg_color);
|
| 367 |
}
|
| 368 |
+
|
| 369 |
function setupTelegram() {
|
| 370 |
if (!tg || !tg.initData) {
|
| 371 |
console.error("Telegram WebApp script not loaded or initData is missing.");
|
| 372 |
document.body.style.visibility = 'visible';
|
| 373 |
return;
|
| 374 |
}
|
| 375 |
+
|
| 376 |
tg.ready();
|
| 377 |
tg.expand();
|
| 378 |
|
|
|
|
| 381 |
}
|
| 382 |
tg.onEvent('themeChanged', () => applyTheme(tg.themeParams));
|
| 383 |
|
| 384 |
+
const urlParams = new URLSearchParams(window.location.search);
|
| 385 |
+
const userIdForTest = urlParams.get('user_id_for_test');
|
| 386 |
+
|
| 387 |
+
if (!userIdForTest) {
|
| 388 |
+
fetch('/verify', {
|
| 389 |
+
method: 'POST',
|
| 390 |
+
headers: {
|
| 391 |
+
'Content-Type': 'application/json',
|
| 392 |
+
'Accept': 'application/json'
|
| 393 |
+
},
|
| 394 |
+
body: JSON.stringify({ initData: tg.initData }),
|
| 395 |
+
})
|
| 396 |
+
.then(response => response.json())
|
| 397 |
+
.then(data => {
|
| 398 |
+
if (data.status === 'ok' && data.verified && data.user_id) {
|
| 399 |
+
console.log('Backend verification successful. Reloading with user data.');
|
| 400 |
+
window.location.replace('/?user_id_for_test=' + data.user_id);
|
| 401 |
+
} else {
|
| 402 |
+
console.warn('Backend verification failed:', data.message);
|
| 403 |
+
document.body.style.visibility = 'visible';
|
| 404 |
+
}
|
| 405 |
+
})
|
| 406 |
+
.catch(error => {
|
| 407 |
+
console.error('Error sending initData for verification:', error);
|
| 408 |
+
document.body.style.visibility = 'visible';
|
| 409 |
+
});
|
| 410 |
+
} else {
|
| 411 |
+
document.body.style.visibility = 'visible';
|
| 412 |
+
}
|
| 413 |
|
| 414 |
const user = tg.initDataUnsafe?.user;
|
| 415 |
const greetingElement = document.getElementById('greeting');
|
| 416 |
if (user) {
|
| 417 |
const name = user.first_name || user.username || 'Гость';
|
| 418 |
greetingElement.textContent = `Добро пожаловать, ${name}! 👋`;
|
| 419 |
+
} else {
|
| 420 |
+
greetingElement.textContent = `Добро пожаловать, {{ user.first_name or 'Гость' }}! 👋`;
|
| 421 |
}
|
|
|
|
|
|
|
| 422 |
}
|
| 423 |
|
| 424 |
if (window.Telegram && window.Telegram.WebApp) {
|
| 425 |
setupTelegram();
|
| 426 |
} else {
|
| 427 |
+
window.addEventListener('load', setupTelegram, {once: true});
|
| 428 |
setTimeout(() => {
|
| 429 |
if (document.body.style.visibility !== 'visible') {
|
| 430 |
document.body.style.visibility = 'visible';
|
|
|
|
| 465 |
body { font-family: var(--font-family); background-color: var(--admin-bg); color: var(--admin-text); margin: 0; padding: var(--padding); line-height: 1.6; }
|
| 466 |
.container { max-width: 1200px; margin: 0 auto; }
|
| 467 |
h1 { text-align: center; color: var(--admin-secondary); margin-bottom: var(--padding); font-weight: 600; }
|
| 468 |
+
.controls-bar { display: flex; 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); }
|
| 469 |
+
.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; }
|
| 470 |
+
.btn { padding: 12px 20px; font-size: 1em; border: none; border-radius: 8px; font-weight: 600; cursor: pointer; transition: background-color 0.2s ease; }
|
| 471 |
+
.btn-primary { background-color: var(--admin-primary); color: #000; }
|
| 472 |
+
.btn-primary:hover { background-color: var(--admin-primary-dark); }
|
| 473 |
.user-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: var(--padding); margin-top: var(--padding); }
|
| 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); }
|
|
|
|
| 480 |
.user-bonuses { text-align: center; margin-bottom: 1rem; }
|
| 481 |
.user-bonuses .label { font-size: 0.9em; color: var(--admin-secondary); }
|
| 482 |
.user-bonuses .amount { font-size: 1.8em; font-weight: 700; color: var(--admin-primary-dark); }
|
| 483 |
+
.user-actions .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; }
|
| 484 |
+
.user-actions .btn-manage:hover { background-color: var(--admin-primary-dark); }
|
| 485 |
.no-users { text-align: center; color: var(--admin-secondary); margin-top: 2rem; font-size: 1.1em; }
|
|
|
|
| 486 |
.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); }
|
| 487 |
.modal-content { background-color: var(--admin-bg); margin: 10% auto; padding: var(--padding); border: 1px solid var(--admin-border); width: 90%; max-width: 600px; border-radius: var(--border-radius); position: relative; box-shadow: 0 8px 30px rgba(0,0,0,0.15); }
|
| 488 |
.modal-close { color: #aaa; position: absolute; top: 15px; right: 25px; font-size: 28px; font-weight: bold; cursor: pointer; }
|
|
|
|
| 492 |
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; align-items: center; margin-bottom: 1.5rem; }
|
| 493 |
.form-group { display: flex; flex-direction: column; }
|
| 494 |
.form-group label { margin-bottom: 0.5rem; font-weight: 500; font-size: 0.9em; }
|
| 495 |
+
.form-group input { padding: 10px; font-size: 1rem; border: 1px solid var(--admin-border); border-radius: 8px; width: 100%; box-sizing: border-box; }
|
| 496 |
.calculation-summary { background: #f0f0f0; padding: 1rem; border-radius: 8px; margin-bottom: 1.5rem; }
|
| 497 |
.summary-item { display: flex; justify-content: space-between; margin-bottom: 0.5rem; font-size: 0.95em; }
|
| 498 |
.summary-item strong { font-weight: 600; }
|
|
|
|
| 505 |
.history-item .date { font-size: 0.8em; color: var(--admin-secondary); }
|
| 506 |
.history-item .amount.accrual { color: var(--admin-success); font-weight: 600; }
|
| 507 |
.history-item .amount.deduction { color: var(--admin-danger); font-weight: 600; }
|
| 508 |
+
.modal-footer { margin-top: 1.5rem; display: flex; justify-content: flex-end; align-items: center; gap: 1rem;}
|
| 509 |
.modal-footer button { padding: 12px 25px; font-size: 1.1em; border-radius: 8px; border: none; font-weight: 600; cursor: pointer; }
|
| 510 |
.btn-submit { background-color: var(--admin-success); color: white; }
|
| 511 |
+
.status-message { text-align: center; font-weight: 500; flex-grow: 1; text-align: left; }
|
| 512 |
</style>
|
| 513 |
</head>
|
| 514 |
<body>
|
| 515 |
<div class="container">
|
| 516 |
<h1>Панель администратора Druzhba</h1>
|
| 517 |
<div class="controls-bar">
|
| 518 |
+
<input type="text" id="searchInput" onkeyup="searchUsers()" placeholder="Поиск по имени, ID, username, номеру...">
|
| 519 |
+
<button class="btn btn-primary" onclick="openAddClientModal()">Добавить клиента</button>
|
| 520 |
</div>
|
| 521 |
|
| 522 |
{% if users %}
|
|
|
|
| 535 |
<div class="amount">{{ "%.2f"|format(user.bonuses|float) }}</div>
|
| 536 |
</div>
|
| 537 |
<div class="user-actions">
|
| 538 |
+
<button class="btn-manage" onclick='openTransactionModal({{ user|tojson }})'>Управление бонусами</button>
|
| 539 |
</div>
|
| 540 |
</div>
|
| 541 |
{% endfor %}
|
|
|
|
| 547 |
|
| 548 |
<div id="transactionModal" class="modal">
|
| 549 |
<div class="modal-content">
|
| 550 |
+
<span class="modal-close" onclick="closeModal('transactionModal')">×</span>
|
| 551 |
<div class="modal-header">
|
| 552 |
<h2 id="modalUserName"></h2>
|
| 553 |
<div id="modalUserUsername" class="username"></div>
|
| 554 |
</div>
|
|
|
|
| 555 |
<input type="hidden" id="modalUserId">
|
|
|
|
| 556 |
<div class="form-row">
|
| 557 |
<div class="form-group">
|
| 558 |
<label for="purchaseAmount">Сумма покупки</label>
|
|
|
|
| 563 |
<input type="number" id="deductAmount" placeholder="Например, 100" oninput="calculateBonuses()">
|
| 564 |
</div>
|
| 565 |
</div>
|
|
|
|
| 566 |
<div class="calculation-summary">
|
| 567 |
<div class="summary-item"><span>Текущий баланс:</span> <strong id="summaryCurrentBalance">0.00</strong></div>
|
| 568 |
<div class="summary-item"><span>Будет начислено (2%):</span> <strong id="summaryAccrual">+0.00</strong></div>
|
|
|
|
| 570 |
<hr>
|
| 571 |
<div class="summary-item"><strong>Итоговый баланс:</strong> <strong id="summaryFinalBalance">0.00</strong></div>
|
| 572 |
</div>
|
|
|
|
| 573 |
<div class="history-container">
|
| 574 |
<h3>История операций</h3>
|
| 575 |
<ul id="modalHistoryList" class="history-list"></ul>
|
| 576 |
</div>
|
|
|
|
| 577 |
<div class="modal-footer">
|
|
|
|
| 578 |
<div id="modalStatus" class="status-message"></div>
|
| 579 |
+
<button class="btn-submit" onclick="submitTransaction()">Провести операцию</button>
|
| 580 |
+
</div>
|
| 581 |
+
</div>
|
| 582 |
+
</div>
|
| 583 |
+
|
| 584 |
+
<div id="addClientModal" class="modal">
|
| 585 |
+
<div class="modal-content">
|
| 586 |
+
<span class="modal-close" onclick="closeModal('addClientModal')">×</span>
|
| 587 |
+
<div class="modal-header">
|
| 588 |
+
<h2>Добавить нового клиента</h2>
|
| 589 |
+
</div>
|
| 590 |
+
<div class="form-group" style="margin-bottom: 1rem;">
|
| 591 |
+
<label for="newClientFirstName">Имя</label>
|
| 592 |
+
<input type="text" id="newClientFirstName" placeholder="Иван">
|
| 593 |
+
</div>
|
| 594 |
+
<div class="form-group" style="margin-bottom: 1rem;">
|
| 595 |
+
<label for="newClientLastName">Фамилия</label>
|
| 596 |
+
<input type="text" id="newClientLastName" placeholder="Иванов">
|
| 597 |
+
</div>
|
| 598 |
+
<div class="form-group" style="margin-bottom: 1.5rem;">
|
| 599 |
+
<label for="newClientPhone">Номер телефона (уникальный)</label>
|
| 600 |
+
<input type="tel" id="newClientPhone" placeholder="+79001234567">
|
| 601 |
+
</div>
|
| 602 |
+
<div class="modal-footer">
|
| 603 |
+
<div id="addClientStatus" class="status-message"></div>
|
| 604 |
+
<button class="btn-submit" onclick="submitNewClient()">Сохранить клиента</button>
|
| 605 |
</div>
|
| 606 |
</div>
|
| 607 |
</div>
|
| 608 |
|
| 609 |
<script>
|
| 610 |
+
const transactionModal = document.getElementById('transactionModal');
|
| 611 |
+
const addClientModal = document.getElementById('addClientModal');
|
| 612 |
let currentUserData = null;
|
| 613 |
|
| 614 |
function searchUsers() {
|
|
|
|
| 656 |
}
|
| 657 |
|
| 658 |
calculateBonuses();
|
| 659 |
+
transactionModal.style.display = 'block';
|
| 660 |
}
|
| 661 |
|
| 662 |
+
function openAddClientModal() {
|
| 663 |
+
document.getElementById('newClientFirstName').value = '';
|
| 664 |
+
document.getElementById('newClientLastName').value = '';
|
| 665 |
+
document.getElementById('newClientPhone').value = '';
|
| 666 |
+
document.getElementById('addClientStatus').textContent = '';
|
| 667 |
+
addClientModal.style.display = 'block';
|
| 668 |
+
}
|
| 669 |
+
|
| 670 |
+
function closeModal(modalId) {
|
| 671 |
+
document.getElementById(modalId).style.display = 'none';
|
| 672 |
+
if (modalId === 'transactionModal') {
|
| 673 |
+
currentUserData = null;
|
| 674 |
+
}
|
| 675 |
}
|
| 676 |
|
| 677 |
function calculateBonuses() {
|
| 678 |
if (!currentUserData) return;
|
|
|
|
| 679 |
const currentBalance = parseFloat(currentUserData.bonuses) || 0;
|
| 680 |
const purchaseAmount = parseFloat(document.getElementById('purchaseAmount').value) || 0;
|
| 681 |
const deductAmount = parseFloat(document.getElementById('deductAmount').value) || 0;
|
|
|
|
| 682 |
const accrualAmount = purchaseAmount * 0.02;
|
|
|
|
| 683 |
let finalDeductAmount = deductAmount;
|
| 684 |
if (deductAmount > currentBalance) {
|
| 685 |
finalDeductAmount = currentBalance;
|
| 686 |
+
document.getElementById('deductAmount').value = finalDeductAmount.toFixed(2);
|
| 687 |
}
|
|
|
|
| 688 |
const finalBalance = currentBalance + accrualAmount - finalDeductAmount;
|
|
|
|
| 689 |
document.getElementById('summaryCurrentBalance').textContent = currentBalance.toFixed(2);
|
| 690 |
document.getElementById('summaryAccrual').textContent = `+${accrualAmount.toFixed(2)}`;
|
| 691 |
document.getElementById('summaryDeduction').textContent = `-${finalDeductAmount.toFixed(2)}`;
|
|
|
|
| 696 |
const statusEl = document.getElementById('modalStatus');
|
| 697 |
statusEl.style.color = 'var(--admin-secondary)';
|
| 698 |
statusEl.textContent = 'Обработка...';
|
|
|
|
| 699 |
const payload = {
|
| 700 |
user_id: document.getElementById('modalUserId').value,
|
| 701 |
purchase_amount: parseFloat(document.getElementById('purchaseAmount').value) || 0,
|
| 702 |
deduct_amount: parseFloat(document.getElementById('deductAmount').value) || 0,
|
| 703 |
};
|
|
|
|
| 704 |
if (payload.purchase_amount <= 0 && payload.deduct_amount <= 0) {
|
| 705 |
statusEl.style.color = 'var(--admin-danger)';
|
| 706 |
statusEl.textContent = 'Введите сумму покупки или сумму для списания.';
|
| 707 |
return;
|
| 708 |
}
|
|
|
|
| 709 |
try {
|
| 710 |
const response = await fetch('/admin/add_transaction', {
|
| 711 |
method: 'POST',
|
|
|
|
| 713 |
body: JSON.stringify(payload)
|
| 714 |
});
|
| 715 |
const result = await response.json();
|
|
|
|
| 716 |
if (response.ok) {
|
| 717 |
statusEl.style.color = 'var(--admin-success)';
|
| 718 |
statusEl.textContent = 'Операция успешно проведена!';
|
| 719 |
+
setTimeout(() => { location.reload(); }, 1500);
|
| 720 |
+
} else {
|
| 721 |
+
throw new Error(result.message || 'Произошла ошибка');
|
| 722 |
+
}
|
| 723 |
+
} catch (error) {
|
| 724 |
+
statusEl.style.color = 'var(--admin-danger)';
|
| 725 |
+
statusEl.textContent = `Ошибка: ${error.message}`;
|
| 726 |
+
}
|
| 727 |
+
}
|
| 728 |
+
|
| 729 |
+
async function submitNewClient() {
|
| 730 |
+
const statusEl = document.getElementById('addClientStatus');
|
| 731 |
+
statusEl.style.color = 'var(--admin-secondary)';
|
| 732 |
+
statusEl.textContent = 'Сохранение...';
|
| 733 |
+
|
| 734 |
+
const payload = {
|
| 735 |
+
first_name: document.getElementById('newClientFirstName').value.trim(),
|
| 736 |
+
last_name: document.getElementById('newClientLastName').value.trim(),
|
| 737 |
+
phone_number: document.getElementById('newClientPhone').value.trim(),
|
| 738 |
+
};
|
| 739 |
+
|
| 740 |
+
if (!payload.first_name || !payload.phone_number) {
|
| 741 |
+
statusEl.style.color = 'var(--admin-danger)';
|
| 742 |
+
statusEl.textContent = 'Имя и номер телефона обязательны.';
|
| 743 |
+
return;
|
| 744 |
+
}
|
| 745 |
+
|
| 746 |
+
try {
|
| 747 |
+
const response = await fetch('/admin/add_client', {
|
| 748 |
+
method: 'POST',
|
| 749 |
+
headers: { 'Content-Type': 'application/json' },
|
| 750 |
+
body: JSON.stringify(payload)
|
| 751 |
+
});
|
| 752 |
+
const result = await response.json();
|
| 753 |
+
if (response.ok) {
|
| 754 |
+
statusEl.style.color = 'var(--admin-success)';
|
| 755 |
+
statusEl.textContent = 'Клиент успешно добавлен!';
|
| 756 |
+
setTimeout(() => { location.reload(); }, 1500);
|
| 757 |
} else {
|
| 758 |
throw new Error(result.message || 'Произошла ошибка');
|
| 759 |
}
|
|
|
|
| 764 |
}
|
| 765 |
|
| 766 |
window.onclick = function(event) {
|
| 767 |
+
if (event.target == transactionModal) {
|
| 768 |
+
closeModal('transactionModal');
|
| 769 |
+
}
|
| 770 |
+
if (event.target == addClientModal) {
|
| 771 |
+
closeModal('addClientModal');
|
| 772 |
}
|
| 773 |
}
|
| 774 |
</script>
|
|
|
|
| 845 |
|
| 846 |
save_visitor_data({user_id_str: user_entry})
|
| 847 |
|
| 848 |
+
return jsonify({"status": "ok", "verified": True, "user_id": user_id_str})
|
| 849 |
+
else:
|
| 850 |
+
return jsonify({"status": "error", "verified": True, "message": "User ID not found in parsed data"}), 400
|
| 851 |
else:
|
| 852 |
logging.warning(f"Verification failed for user: {user_info_dict.get('id')}")
|
| 853 |
return jsonify({"status": "error", "verified": False, "message": "Invalid data"}), 403
|
|
|
|
| 856 |
logging.exception("Error in /verify endpoint")
|
| 857 |
return jsonify({"status": "error", "message": "Internal server error"}), 500
|
| 858 |
|
|
|
|
| 859 |
@app.route('/admin')
|
| 860 |
def admin_panel():
|
| 861 |
current_data = load_visitor_data()
|
| 862 |
users_list = list(current_data.values())
|
| 863 |
return render_template_string(ADMIN_TEMPLATE, users=users_list)
|
| 864 |
|
| 865 |
+
@app.route('/admin/add_client', methods=['POST'])
|
| 866 |
+
def add_client():
|
| 867 |
+
try:
|
| 868 |
+
data = request.get_json()
|
| 869 |
+
phone_number = data.get('phone_number')
|
| 870 |
+
first_name = data.get('first_name')
|
| 871 |
+
last_name = data.get('last_name')
|
| 872 |
+
|
| 873 |
+
if not phone_number or not first_name:
|
| 874 |
+
return jsonify({"status": "error", "message": "Имя и номер телефона обязательны."}), 400
|
| 875 |
+
|
| 876 |
+
all_data = load_visitor_data()
|
| 877 |
+
|
| 878 |
+
if phone_number in all_data:
|
| 879 |
+
return jsonify({"status": "error", "message": "Клиент с таким номером телефона уже существует."}), 409
|
| 880 |
+
|
| 881 |
+
now = datetime.now()
|
| 882 |
+
new_client = {
|
| 883 |
+
'id': phone_number,
|
| 884 |
+
'first_name': first_name,
|
| 885 |
+
'last_name': last_name,
|
| 886 |
+
'username': phone_number,
|
| 887 |
+
'photo_url': None,
|
| 888 |
+
'language_code': 'ru',
|
| 889 |
+
'is_premium': False,
|
| 890 |
+
'visited_at': now.timestamp(),
|
| 891 |
+
'visited_at_str': now.strftime('%Y-%m-%d %H:%M:%S'),
|
| 892 |
+
'bonuses': 0,
|
| 893 |
+
'history': []
|
| 894 |
+
}
|
| 895 |
+
|
| 896 |
+
save_visitor_data({phone_number: new_client})
|
| 897 |
+
|
| 898 |
+
return jsonify({"status": "ok", "message": "Client added successfully"}), 201
|
| 899 |
+
|
| 900 |
+
except Exception as e:
|
| 901 |
+
logging.exception("Error in /admin/add_client endpoint")
|
| 902 |
+
return jsonify({"status": "error", "message": str(e)}), 500
|
| 903 |
+
|
| 904 |
+
|
| 905 |
@app.route('/admin/add_transaction', methods=['POST'])
|
| 906 |
def add_transaction():
|
| 907 |
try:
|