Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -11,6 +11,7 @@ import time
|
|
| 11 |
from datetime import datetime
|
| 12 |
import logging
|
| 13 |
import threading
|
|
|
|
| 14 |
from huggingface_hub import HfApi, hf_hub_download
|
| 15 |
from huggingface_hub.utils import RepositoryNotFoundError
|
| 16 |
|
|
@@ -165,6 +166,13 @@ def verify_telegram_data(init_data_str):
|
|
| 165 |
logging.error(f"Error verifying Telegram data: {e}")
|
| 166 |
return None, False
|
| 167 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 168 |
TEMPLATE = """
|
| 169 |
<!DOCTYPE html>
|
| 170 |
<html lang="ru">
|
|
@@ -175,30 +183,31 @@ TEMPLATE = """
|
|
| 175 |
<script src="https://telegram.org/js/telegram-web-app.js"></script>
|
| 176 |
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 177 |
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 178 |
-
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
| 179 |
<style>
|
| 180 |
:root {
|
| 181 |
-
--tg-theme-bg-color: #111111;
|
| 182 |
-
--tg-theme-text-color: #ffffff;
|
| 183 |
-
--tg-theme-hint-color: #aaaaaa;
|
| 184 |
-
--tg-theme-link-color: #FFC107;
|
| 185 |
-
--tg-theme-button-color: #FFC107;
|
| 186 |
-
--tg-theme-button-text-color: #000000;
|
| 187 |
-
--tg-theme-secondary-bg-color: #1e1e1e;
|
| 188 |
-
|
| 189 |
--brand-yellow: #FFC107;
|
| 190 |
-
--brand-
|
| 191 |
-
--
|
| 192 |
-
--
|
| 193 |
-
--
|
| 194 |
-
--
|
| 195 |
-
--
|
| 196 |
-
--
|
| 197 |
-
--padding-
|
|
|
|
| 198 |
--font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
| 199 |
-
--shadow-color: rgba(0, 0, 0, 0.
|
| 200 |
-
--shadow-
|
| 201 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 202 |
}
|
| 203 |
* { box-sizing: border-box; margin: 0; padding: 0; }
|
| 204 |
html, body {
|
|
@@ -217,56 +226,65 @@ TEMPLATE = """
|
|
| 217 |
margin: 0 auto;
|
| 218 |
display: flex;
|
| 219 |
flex-direction: column;
|
| 220 |
-
gap:
|
|
|
|
| 221 |
}
|
| 222 |
.header {
|
| 223 |
text-align: center;
|
| 224 |
padding: var(--padding-m) 0;
|
|
|
|
| 225 |
}
|
| 226 |
.logo {
|
| 227 |
-
font-size:
|
| 228 |
-
font-weight:
|
| 229 |
color: var(--brand-yellow);
|
|
|
|
|
|
|
| 230 |
}
|
| 231 |
.welcome-text {
|
| 232 |
font-size: 1.1em;
|
| 233 |
color: var(--text-secondary-color);
|
| 234 |
-
margin-top:
|
| 235 |
}
|
| 236 |
.bonus-card {
|
| 237 |
-
background:
|
| 238 |
border-radius: var(--border-radius-l);
|
| 239 |
padding: var(--padding-l);
|
| 240 |
text-align: center;
|
| 241 |
-
box-shadow: var(--shadow-medium);
|
| 242 |
border: 1px solid rgba(255, 193, 7, 0.2);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 243 |
}
|
| 244 |
.bonus-label {
|
| 245 |
-
font-size: 1.
|
| 246 |
font-weight: 500;
|
| 247 |
color: var(--text-secondary-color);
|
| 248 |
-
margin-bottom:
|
|
|
|
|
|
|
| 249 |
}
|
| 250 |
.bonus-amount {
|
| 251 |
-
font-size:
|
| 252 |
-
font-weight:
|
| 253 |
color: var(--brand-yellow);
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
.bonus-amount-minor {
|
| 257 |
-
font-size: 0.5em;
|
| 258 |
-
opacity: 0.8;
|
| 259 |
}
|
| 260 |
.history-section {
|
| 261 |
background-color: var(--card-bg);
|
| 262 |
border-radius: var(--border-radius-l);
|
| 263 |
padding: var(--padding-l);
|
| 264 |
-
box-shadow: var(--shadow-light);
|
| 265 |
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
|
|
| 266 |
}
|
| 267 |
.history-title {
|
| 268 |
-
font-size: 1.
|
| 269 |
-
font-weight:
|
| 270 |
margin-bottom: var(--padding-m);
|
| 271 |
padding-bottom: var(--padding-m);
|
| 272 |
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
|
@@ -282,40 +300,21 @@ TEMPLATE = """
|
|
| 282 |
display: flex;
|
| 283 |
justify-content: space-between;
|
| 284 |
align-items: center;
|
| 285 |
-
padding:
|
| 286 |
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
|
|
|
| 287 |
}
|
| 288 |
-
.history-item:
|
| 289 |
-
|
| 290 |
-
}
|
| 291 |
-
.history-details {
|
| 292 |
-
display: flex;
|
| 293 |
-
flex-direction: column;
|
| 294 |
-
}
|
| 295 |
-
.history-description {
|
| 296 |
-
font-size: 1em;
|
| 297 |
-
font-weight: 500;
|
| 298 |
-
}
|
| 299 |
-
.history-date {
|
| 300 |
-
font-size: 0.85em;
|
| 301 |
-
color: var(--text-secondary-color);
|
| 302 |
-
margin-top: 4px;
|
| 303 |
-
}
|
| 304 |
-
.history-amount {
|
| 305 |
-
font-size: 1.2em;
|
| 306 |
-
font-weight: 600;
|
| 307 |
-
}
|
| 308 |
-
.history-amount.accrual {
|
| 309 |
-
color: #4CAF50;
|
| 310 |
-
}
|
| 311 |
-
.history-amount.deduction {
|
| 312 |
-
color: #F44336;
|
| 313 |
-
}
|
| 314 |
-
.no-history {
|
| 315 |
-
text-align: center;
|
| 316 |
-
color: var(--text-secondary-color);
|
| 317 |
-
padding: 2rem 0;
|
| 318 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 319 |
</style>
|
| 320 |
</head>
|
| 321 |
<body>
|
|
@@ -357,13 +356,11 @@ TEMPLATE = """
|
|
| 357 |
|
| 358 |
function applyTheme(themeParams) {
|
| 359 |
const root = document.documentElement;
|
| 360 |
-
if (themeParams.bg_color) root.style.setProperty('--
|
| 361 |
-
if (themeParams.text_color) root.style.setProperty('--
|
| 362 |
-
if (themeParams.hint_color) root.style.setProperty('--
|
| 363 |
-
if (themeParams.
|
| 364 |
-
if (themeParams.
|
| 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() {
|
|
@@ -387,24 +384,18 @@ TEMPLATE = """
|
|
| 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 {
|
|
@@ -467,7 +458,8 @@ ADMIN_TEMPLATE = """
|
|
| 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); }
|
|
@@ -477,6 +469,7 @@ ADMIN_TEMPLATE = """
|
|
| 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.95em; }
|
|
|
|
| 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); }
|
|
@@ -522,12 +515,13 @@ ADMIN_TEMPLATE = """
|
|
| 522 |
{% if users %}
|
| 523 |
<div class="user-grid" id="userGrid">
|
| 524 |
{% for user in users|sort(attribute='visited_at', reverse=true) %}
|
| 525 |
-
<div class="user-card" data-user-id="{{ user.id }}" data-search-term="{{ user.first_name|lower }} {{ user.last_name|lower }} {{ user.username|lower }} {{ user.id }}">
|
| 526 |
<div class="user-info">
|
| 527 |
<img src="{{ user.photo_url if user.photo_url else 'data:image/svg+xml;charset=UTF-8,%3csvg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%270 0 100 100%27%3e%3crect width=%27100%27 height=%27100%27 fill=%27%23e9ecef%27/%3e%3ctext x=%2750%25%27 y=%2755%25%27 dominant-baseline=%27middle%27 text-anchor=%27middle%27 font-size=%2745%27 font-family=%27sans-serif%27 fill=%27%23adb5bd%27%3e?%3c/text%3e%3c/svg%3e' }}" alt="User Avatar">
|
| 528 |
<div class="user-details">
|
| 529 |
<div class="name">{{ user.first_name or '' }} {{ user.last_name or '' }}</div>
|
| 530 |
<div class="username">@{{ user.username or 'N/A' }}</div>
|
|
|
|
| 531 |
</div>
|
| 532 |
</div>
|
| 533 |
<div class="user-bonuses">
|
|
@@ -588,12 +582,8 @@ ADMIN_TEMPLATE = """
|
|
| 588 |
<h2>Добавить нового клиента</h2>
|
| 589 |
</div>
|
| 590 |
<div class="form-group" style="margin-bottom: 1rem;">
|
| 591 |
-
<label for="
|
| 592 |
-
<input type="text" id="
|
| 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>
|
|
@@ -660,8 +650,7 @@ ADMIN_TEMPLATE = """
|
|
| 660 |
}
|
| 661 |
|
| 662 |
function openAddClientModal() {
|
| 663 |
-
document.getElementById('
|
| 664 |
-
document.getElementById('newClientLastName').value = '';
|
| 665 |
document.getElementById('newClientPhone').value = '';
|
| 666 |
document.getElementById('addClientStatus').textContent = '';
|
| 667 |
addClientModal.style.display = 'block';
|
|
@@ -732,12 +721,11 @@ ADMIN_TEMPLATE = """
|
|
| 732 |
statusEl.textContent = 'Сохранение...';
|
| 733 |
|
| 734 |
const payload = {
|
| 735 |
-
|
| 736 |
-
last_name: document.getElementById('newClientLastName').value.trim(),
|
| 737 |
phone_number: document.getElementById('newClientPhone').value.trim(),
|
| 738 |
};
|
| 739 |
|
| 740 |
-
if (!payload.
|
| 741 |
statusEl.style.color = 'var(--admin-danger)';
|
| 742 |
statusEl.textContent = 'Имя и номер телефона обязательны.';
|
| 743 |
return;
|
|
@@ -828,9 +816,12 @@ def verify_data():
|
|
| 828 |
'visited_at': now.timestamp(),
|
| 829 |
'visited_at_str': now.strftime('%Y-%m-%d %H:%M:%S')
|
| 830 |
})
|
|
|
|
|
|
|
| 831 |
else:
|
| 832 |
user_entry = {
|
| 833 |
'id': user_id,
|
|
|
|
| 834 |
'first_name': user_info_dict.get('first_name'),
|
| 835 |
'last_name': user_info_dict.get('last_name'),
|
| 836 |
'username': user_info_dict.get('username'),
|
|
@@ -867,22 +858,23 @@ def add_client():
|
|
| 867 |
try:
|
| 868 |
data = request.get_json()
|
| 869 |
phone_number = data.get('phone_number')
|
| 870 |
-
|
| 871 |
-
last_name = data.get('last_name')
|
| 872 |
|
| 873 |
-
if not phone_number or not
|
| 874 |
return jsonify({"status": "error", "message": "Имя и номер телефона обязательны."}), 400
|
| 875 |
|
| 876 |
all_data = load_visitor_data()
|
| 877 |
|
| 878 |
-
|
| 879 |
-
|
|
|
|
| 880 |
|
| 881 |
now = datetime.now()
|
| 882 |
new_client = {
|
| 883 |
'id': phone_number,
|
| 884 |
-
'
|
| 885 |
-
'
|
|
|
|
| 886 |
'username': phone_number,
|
| 887 |
'photo_url': None,
|
| 888 |
'language_code': 'ru',
|
|
|
|
| 11 |
from datetime import datetime
|
| 12 |
import logging
|
| 13 |
import threading
|
| 14 |
+
import random
|
| 15 |
from huggingface_hub import HfApi, hf_hub_download
|
| 16 |
from huggingface_hub.utils import RepositoryNotFoundError
|
| 17 |
|
|
|
|
| 166 |
logging.error(f"Error verifying Telegram data: {e}")
|
| 167 |
return None, False
|
| 168 |
|
| 169 |
+
def generate_unique_bonus_id(all_data):
|
| 170 |
+
existing_ids = {user.get('bonus_id') for user in all_data.values() if 'bonus_id' in user}
|
| 171 |
+
while True:
|
| 172 |
+
new_id = str(random.randint(10000, 99999))
|
| 173 |
+
if new_id not in existing_ids:
|
| 174 |
+
return new_id
|
| 175 |
+
|
| 176 |
TEMPLATE = """
|
| 177 |
<!DOCTYPE html>
|
| 178 |
<html lang="ru">
|
|
|
|
| 183 |
<script src="https://telegram.org/js/telegram-web-app.js"></script>
|
| 184 |
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 185 |
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 186 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
|
| 187 |
<style>
|
| 188 |
:root {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 189 |
--brand-yellow: #FFC107;
|
| 190 |
+
--brand-yellow-glow: rgba(255, 193, 7, 0.5);
|
| 191 |
+
--brand-black: #0d0d0d;
|
| 192 |
+
--card-bg: #1a1a1a;
|
| 193 |
+
--card-bg-gradient: radial-gradient(circle, #2a2a2a 0%, #1a1a1a 100%);
|
| 194 |
+
--text-color: #f5f5f7;
|
| 195 |
+
--text-secondary-color: #888888;
|
| 196 |
+
--border-radius-l: 24px;
|
| 197 |
+
--padding-m: 20px;
|
| 198 |
+
--padding-l: 30px;
|
| 199 |
--font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
| 200 |
+
--shadow-color: rgba(0, 0, 0, 0.6);
|
| 201 |
+
--shadow-glow: 0 0 40px var(--brand-yellow-glow);
|
| 202 |
+
}
|
| 203 |
+
@keyframes fadeIn {
|
| 204 |
+
from { opacity: 0; transform: translateY(20px); }
|
| 205 |
+
to { opacity: 1; transform: translateY(0); }
|
| 206 |
+
}
|
| 207 |
+
@keyframes pulseGlow {
|
| 208 |
+
0% { text-shadow: 0 0 4px var(--brand-yellow-glow), 0 0 10px var(--brand-yellow-glow); }
|
| 209 |
+
50% { text-shadow: 0 0 10px var(--brand-yellow-glow), 0 0 30px var(--brand-yellow-glow); }
|
| 210 |
+
100% { text-shadow: 0 0 4px var(--brand-yellow-glow), 0 0 10px var(--brand-yellow-glow); }
|
| 211 |
}
|
| 212 |
* { box-sizing: border-box; margin: 0; padding: 0; }
|
| 213 |
html, body {
|
|
|
|
| 226 |
margin: 0 auto;
|
| 227 |
display: flex;
|
| 228 |
flex-direction: column;
|
| 229 |
+
gap: 2rem;
|
| 230 |
+
animation: fadeIn 0.8s ease-out forwards;
|
| 231 |
}
|
| 232 |
.header {
|
| 233 |
text-align: center;
|
| 234 |
padding: var(--padding-m) 0;
|
| 235 |
+
animation-delay: 0.1s;
|
| 236 |
}
|
| 237 |
.logo {
|
| 238 |
+
font-size: 2.5em;
|
| 239 |
+
font-weight: 800;
|
| 240 |
color: var(--brand-yellow);
|
| 241 |
+
letter-spacing: 2px;
|
| 242 |
+
text-transform: uppercase;
|
| 243 |
}
|
| 244 |
.welcome-text {
|
| 245 |
font-size: 1.1em;
|
| 246 |
color: var(--text-secondary-color);
|
| 247 |
+
margin-top: 10px;
|
| 248 |
}
|
| 249 |
.bonus-card {
|
| 250 |
+
background: var(--card-bg-gradient);
|
| 251 |
border-radius: var(--border-radius-l);
|
| 252 |
padding: var(--padding-l);
|
| 253 |
text-align: center;
|
|
|
|
| 254 |
border: 1px solid rgba(255, 193, 7, 0.2);
|
| 255 |
+
box-shadow: 0 10px 30px var(--shadow-color);
|
| 256 |
+
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
| 257 |
+
animation-delay: 0.2s;
|
| 258 |
+
}
|
| 259 |
+
.bonus-card:hover {
|
| 260 |
+
transform: translateY(-8px) scale(1.02);
|
| 261 |
+
box-shadow: 0 15px 40px var(--shadow-color), var(--shadow-glow);
|
| 262 |
}
|
| 263 |
.bonus-label {
|
| 264 |
+
font-size: 1.3em;
|
| 265 |
font-weight: 500;
|
| 266 |
color: var(--text-secondary-color);
|
| 267 |
+
margin-bottom: 15px;
|
| 268 |
+
text-transform: uppercase;
|
| 269 |
+
letter-spacing: 1px;
|
| 270 |
}
|
| 271 |
.bonus-amount {
|
| 272 |
+
font-size: 4.5em;
|
| 273 |
+
font-weight: 800;
|
| 274 |
color: var(--brand-yellow);
|
| 275 |
+
line-height: 1;
|
| 276 |
+
animation: pulseGlow 3s infinite ease-in-out;
|
|
|
|
|
|
|
|
|
|
| 277 |
}
|
| 278 |
.history-section {
|
| 279 |
background-color: var(--card-bg);
|
| 280 |
border-radius: var(--border-radius-l);
|
| 281 |
padding: var(--padding-l);
|
|
|
|
| 282 |
border: 1px solid rgba(255, 255, 255, 0.08);
|
| 283 |
+
animation-delay: 0.3s;
|
| 284 |
}
|
| 285 |
.history-title {
|
| 286 |
+
font-size: 1.6em;
|
| 287 |
+
font-weight: 700;
|
| 288 |
margin-bottom: var(--padding-m);
|
| 289 |
padding-bottom: var(--padding-m);
|
| 290 |
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
|
|
|
| 300 |
display: flex;
|
| 301 |
justify-content: space-between;
|
| 302 |
align-items: center;
|
| 303 |
+
padding: 18px 8px;
|
| 304 |
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
| 305 |
+
transition: background-color 0.2s ease;
|
| 306 |
}
|
| 307 |
+
.history-item:hover {
|
| 308 |
+
background-color: rgba(255, 255, 255, 0.03);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 309 |
}
|
| 310 |
+
.history-item:last-child { border-bottom: none; }
|
| 311 |
+
.history-details { display: flex; flex-direction: column; }
|
| 312 |
+
.history-description { font-size: 1.05em; font-weight: 500; }
|
| 313 |
+
.history-date { font-size: 0.85em; color: var(--text-secondary-color); margin-top: 5px; }
|
| 314 |
+
.history-amount { font-size: 1.3em; font-weight: 700; }
|
| 315 |
+
.history-amount.accrual { color: #28a745; }
|
| 316 |
+
.history-amount.deduction { color: #dc3545; }
|
| 317 |
+
.no-history { text-align: center; color: var(--text-secondary-color); padding: 3rem 0; font-size: 1.1em; }
|
| 318 |
</style>
|
| 319 |
</head>
|
| 320 |
<body>
|
|
|
|
| 356 |
|
| 357 |
function applyTheme(themeParams) {
|
| 358 |
const root = document.documentElement;
|
| 359 |
+
if (themeParams.bg_color) root.style.setProperty('--brand-black', themeParams.bg_color);
|
| 360 |
+
if (themeParams.text_color) root.style.setProperty('--text-color', themeParams.text_color);
|
| 361 |
+
if (themeParams.hint_color) root.style.setProperty('--text-secondary-color', themeParams.hint_color);
|
| 362 |
+
if (themeParams.button_color) root.style.setProperty('--brand-yellow', themeParams.button_color);
|
| 363 |
+
if (themeParams.secondary_bg_color) root.style.setProperty('--card-bg', themeParams.secondary_bg_color);
|
|
|
|
|
|
|
| 364 |
}
|
| 365 |
|
| 366 |
function setupTelegram() {
|
|
|
|
| 384 |
if (!userIdForTest) {
|
| 385 |
fetch('/verify', {
|
| 386 |
method: 'POST',
|
| 387 |
+
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
|
|
|
|
|
|
|
|
|
|
| 388 |
body: JSON.stringify({ initData: tg.initData }),
|
| 389 |
})
|
| 390 |
.then(response => response.json())
|
| 391 |
.then(data => {
|
| 392 |
if (data.status === 'ok' && data.verified && data.user_id) {
|
|
|
|
| 393 |
window.location.replace('/?user_id_for_test=' + data.user_id);
|
| 394 |
} else {
|
|
|
|
| 395 |
document.body.style.visibility = 'visible';
|
| 396 |
}
|
| 397 |
})
|
| 398 |
.catch(error => {
|
|
|
|
| 399 |
document.body.style.visibility = 'visible';
|
| 400 |
});
|
| 401 |
} else {
|
|
|
|
| 458 |
h1 { text-align: center; color: var(--admin-secondary); margin-bottom: var(--padding); font-weight: 600; }
|
| 459 |
.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); }
|
| 460 |
.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; }
|
| 461 |
+
.btn { padding: 12px 20px; font-size: 1em; border: none; border-radius: 8px; font-weight: 600; cursor: pointer; transition: background-color 0.2s ease, transform 0.2s ease; }
|
| 462 |
+
.btn:hover { transform: translateY(-2px); }
|
| 463 |
.btn-primary { background-color: var(--admin-primary); color: #000; }
|
| 464 |
.btn-primary:hover { background-color: var(--admin-primary-dark); }
|
| 465 |
.user-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: var(--padding); margin-top: var(--padding); }
|
|
|
|
| 469 |
.user-info img { width: 60px; height: 60px; border-radius: 50%; object-fit: cover; border: 3px solid var(--admin-border); background-color: #eee; }
|
| 470 |
.user-details .name { font-weight: 600; font-size: 1.2em; }
|
| 471 |
.user-details .username { color: var(--admin-secondary); font-size: 0.95em; }
|
| 472 |
+
.user-details .bonus-id { color: var(--admin-primary-dark); font-size: 0.9em; font-weight: 500; margin-top: 4px; background: #fff8e1; padding: 2px 6px; border-radius: 4px; display: inline-block;}
|
| 473 |
.user-bonuses { text-align: center; margin-bottom: 1rem; }
|
| 474 |
.user-bonuses .label { font-size: 0.9em; color: var(--admin-secondary); }
|
| 475 |
.user-bonuses .amount { font-size: 1.8em; font-weight: 700; color: var(--admin-primary-dark); }
|
|
|
|
| 515 |
{% if users %}
|
| 516 |
<div class="user-grid" id="userGrid">
|
| 517 |
{% for user in users|sort(attribute='visited_at', reverse=true) %}
|
| 518 |
+
<div class="user-card" data-user-id="{{ user.id }}" data-search-term="{{ user.first_name|lower }} {{ user.last_name|lower }} {{ user.username|lower }} {{ user.id }} {{ user.bonus_id }}">
|
| 519 |
<div class="user-info">
|
| 520 |
<img src="{{ user.photo_url if user.photo_url else 'data:image/svg+xml;charset=UTF-8,%3csvg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%270 0 100 100%27%3e%3crect width=%27100%27 height=%27100%27 fill=%27%23e9ecef%27/%3e%3ctext x=%2750%25%27 y=%2755%25%27 dominant-baseline=%27middle%27 text-anchor=%27middle%27 font-size=%2745%27 font-family=%27sans-serif%27 fill=%27%23adb5bd%27%3e?%3c/text%3e%3c/svg%3e' }}" alt="User Avatar">
|
| 521 |
<div class="user-details">
|
| 522 |
<div class="name">{{ user.first_name or '' }} {{ user.last_name or '' }}</div>
|
| 523 |
<div class="username">@{{ user.username or 'N/A' }}</div>
|
| 524 |
+
<div class="bonus-id">ID: {{ user.bonus_id or 'N/A' }}</div>
|
| 525 |
</div>
|
| 526 |
</div>
|
| 527 |
<div class="user-bonuses">
|
|
|
|
| 582 |
<h2>Добавить нового клиента</h2>
|
| 583 |
</div>
|
| 584 |
<div class="form-group" style="margin-bottom: 1rem;">
|
| 585 |
+
<label for="newClientName">Имя</label>
|
| 586 |
+
<input type="text" id="newClientName" placeholder="Иван Иванов">
|
|
|
|
|
|
|
|
|
|
|
|
|
| 587 |
</div>
|
| 588 |
<div class="form-group" style="margin-bottom: 1.5rem;">
|
| 589 |
<label for="newClientPhone">Номер телефона (уникальный)</label>
|
|
|
|
| 650 |
}
|
| 651 |
|
| 652 |
function openAddClientModal() {
|
| 653 |
+
document.getElementById('newClientName').value = '';
|
|
|
|
| 654 |
document.getElementById('newClientPhone').value = '';
|
| 655 |
document.getElementById('addClientStatus').textContent = '';
|
| 656 |
addClientModal.style.display = 'block';
|
|
|
|
| 721 |
statusEl.textContent = 'Сохранение...';
|
| 722 |
|
| 723 |
const payload = {
|
| 724 |
+
name: document.getElementById('newClientName').value.trim(),
|
|
|
|
| 725 |
phone_number: document.getElementById('newClientPhone').value.trim(),
|
| 726 |
};
|
| 727 |
|
| 728 |
+
if (!payload.name || !payload.phone_number) {
|
| 729 |
statusEl.style.color = 'var(--admin-danger)';
|
| 730 |
statusEl.textContent = 'Имя и номер телефона обязательны.';
|
| 731 |
return;
|
|
|
|
| 816 |
'visited_at': now.timestamp(),
|
| 817 |
'visited_at_str': now.strftime('%Y-%m-%d %H:%M:%S')
|
| 818 |
})
|
| 819 |
+
if 'bonus_id' not in user_entry or not user_entry['bonus_id']:
|
| 820 |
+
user_entry['bonus_id'] = generate_unique_bonus_id(all_data)
|
| 821 |
else:
|
| 822 |
user_entry = {
|
| 823 |
'id': user_id,
|
| 824 |
+
'bonus_id': generate_unique_bonus_id(all_data),
|
| 825 |
'first_name': user_info_dict.get('first_name'),
|
| 826 |
'last_name': user_info_dict.get('last_name'),
|
| 827 |
'username': user_info_dict.get('username'),
|
|
|
|
| 858 |
try:
|
| 859 |
data = request.get_json()
|
| 860 |
phone_number = data.get('phone_number')
|
| 861 |
+
name = data.get('name')
|
|
|
|
| 862 |
|
| 863 |
+
if not phone_number or not name:
|
| 864 |
return jsonify({"status": "error", "message": "Имя и номер телефона обязательны."}), 400
|
| 865 |
|
| 866 |
all_data = load_visitor_data()
|
| 867 |
|
| 868 |
+
for user in all_data.values():
|
| 869 |
+
if user.get('id') == phone_number or user.get('username') == phone_number:
|
| 870 |
+
return jsonify({"status": "error", "message": "Клиент с таким номером телефона уже существует."}), 409
|
| 871 |
|
| 872 |
now = datetime.now()
|
| 873 |
new_client = {
|
| 874 |
'id': phone_number,
|
| 875 |
+
'bonus_id': generate_unique_bonus_id(all_data),
|
| 876 |
+
'first_name': name,
|
| 877 |
+
'last_name': "",
|
| 878 |
'username': phone_number,
|
| 879 |
'photo_url': None,
|
| 880 |
'language_code': 'ru',
|