Spaces:
Build error
Build error
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,4 +1,3 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
|
| 3 |
import os
|
| 4 |
from flask import Flask, request, Response, render_template_string, jsonify, redirect, url_for
|
|
@@ -11,8 +10,9 @@ from datetime import datetime
|
|
| 11 |
import logging
|
| 12 |
import threading
|
| 13 |
import random
|
| 14 |
-
import pytz
|
| 15 |
-
import uuid
|
|
|
|
| 16 |
|
| 17 |
from huggingface_hub import HfApi, hf_hub_download
|
| 18 |
from huggingface_hub.utils import RepositoryNotFoundError
|
|
@@ -27,7 +27,6 @@ HF_DATA_FILE_PATH = "data.json"
|
|
| 27 |
HF_TOKEN_WRITE = os.getenv("HF_TOKEN_WRITE")
|
| 28 |
HF_TOKEN_READ = os.getenv("HF_TOKEN_READ")
|
| 29 |
|
| 30 |
-
# Define Bishkek timezone
|
| 31 |
BISHKEK_TZ = pytz.timezone('Asia/Bishkek')
|
| 32 |
|
| 33 |
app = Flask(__name__)
|
|
@@ -35,15 +34,27 @@ logging.basicConfig(level=logging.INFO)
|
|
| 35 |
app.secret_key = os.urandom(24)
|
| 36 |
|
| 37 |
_data_lock = threading.Lock()
|
| 38 |
-
visitor_data_cache = {}
|
| 39 |
|
| 40 |
def generate_unique_id(all_data):
|
| 41 |
while True:
|
| 42 |
-
# Check against both client IDs and invoice IDs
|
| 43 |
new_id = str(random.randint(10000, 99999))
|
| 44 |
if new_id not in all_data:
|
| 45 |
return new_id
|
| 46 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
def download_data_from_hf():
|
| 48 |
global visitor_data_cache
|
| 49 |
if not HF_TOKEN_READ:
|
|
@@ -80,14 +91,14 @@ def download_data_from_hf():
|
|
| 80 |
def load_visitor_data():
|
| 81 |
global visitor_data_cache
|
| 82 |
with _data_lock:
|
| 83 |
-
if not visitor_data_cache:
|
| 84 |
try:
|
| 85 |
with open(DATA_FILE, 'r', encoding='utf-8') as f:
|
| 86 |
visitor_data_cache = json.load(f)
|
| 87 |
logging.info("Visitor data loaded from local JSON.")
|
| 88 |
except FileNotFoundError:
|
| 89 |
logging.warning(f"{DATA_FILE} not found locally. Starting with empty data.")
|
| 90 |
-
visitor_data_cache = {"organization_details": {}}
|
| 91 |
except json.JSONDecodeError:
|
| 92 |
logging.error(f"Error decoding {DATA_FILE}. Starting with empty data.")
|
| 93 |
visitor_data_cache = {"organization_details": {}}
|
|
@@ -95,7 +106,6 @@ def load_visitor_data():
|
|
| 95 |
logging.error(f"Unexpected error loading visitor data: {e}")
|
| 96 |
visitor_data_cache = {"organization_details": {}}
|
| 97 |
|
| 98 |
-
# Ensure organization_details key exists
|
| 99 |
if "organization_details" not in visitor_data_cache:
|
| 100 |
visitor_data_cache["organization_details"] = {}
|
| 101 |
|
|
@@ -104,29 +114,6 @@ def load_visitor_data():
|
|
| 104 |
def save_visitor_data(data):
|
| 105 |
with _data_lock:
|
| 106 |
try:
|
| 107 |
-
# When `data` is a dictionary, update it directly.
|
| 108 |
-
# If `data` is a partial update for `visitor_data_cache`, merge it.
|
| 109 |
-
# For simplicity, this function now assumes `data` is the complete `visitor_data_cache`
|
| 110 |
-
# or a mergeable dictionary that should be applied to the cache before saving.
|
| 111 |
-
# Given current usage, it's typically `save_visitor_data({user_id: user_entry})`
|
| 112 |
-
# or `save_visitor_data({"organization_details": new_org_details})` etc.
|
| 113 |
-
# It should ideally update the global `visitor_data_cache` and then dump it.
|
| 114 |
-
|
| 115 |
-
# This line needs to be careful: if `data` is a single user, it overwrites.
|
| 116 |
-
# It's better to update specific parts of the cache or always pass the full cache.
|
| 117 |
-
# Let's adjust existing call sites to pass the full `all_data` after modification.
|
| 118 |
-
# For now, let's assume `data` is what needs to be *merged* into `visitor_data_cache`
|
| 119 |
-
# or `data` IS the new `visitor_data_cache`.
|
| 120 |
-
|
| 121 |
-
# A more robust approach for `save_visitor_data` would be:
|
| 122 |
-
# 1. Take a user_id and user_data to update a specific user
|
| 123 |
-
# 2. Take an org_details dict to update org details
|
| 124 |
-
# 3. Then, always dump the *entire* `visitor_data_cache`.
|
| 125 |
-
|
| 126 |
-
# Simpler change for existing code:
|
| 127 |
-
# Ensure `visitor_data_cache` is directly modified by operations,
|
| 128 |
-
# and `save_visitor_data` just dumps the current `visitor_data_cache`.
|
| 129 |
-
|
| 130 |
with open(DATA_FILE, 'w', encoding='utf-8') as f:
|
| 131 |
json.dump(visitor_data_cache, f, ensure_ascii=False, indent=4)
|
| 132 |
logging.info(f"Visitor data successfully saved to {DATA_FILE}.")
|
|
@@ -255,7 +242,7 @@ TEMPLATE = """
|
|
| 255 |
.header {
|
| 256 |
text-align: left;
|
| 257 |
padding: var(--padding-m) 0;
|
| 258 |
-
margin-bottom: 0;
|
| 259 |
}
|
| 260 |
.logo {
|
| 261 |
font-size: 2.5em;
|
|
@@ -296,12 +283,12 @@ TEMPLATE = """
|
|
| 296 |
box-shadow: 0 2px 10px rgba(255,193,7,0.3);
|
| 297 |
}
|
| 298 |
.content-section {
|
| 299 |
-
display: none;
|
| 300 |
flex-direction: column;
|
| 301 |
gap: var(--padding-m);
|
| 302 |
}
|
| 303 |
.content-section.active {
|
| 304 |
-
display: flex;
|
| 305 |
}
|
| 306 |
.card-grid {
|
| 307 |
display: grid;
|
|
@@ -406,8 +393,6 @@ TEMPLATE = """
|
|
| 406 |
color: var(--text-secondary-color);
|
| 407 |
padding: 2rem 0;
|
| 408 |
}
|
| 409 |
-
|
| 410 |
-
/* Business Card Styles */
|
| 411 |
.business-card-item {
|
| 412 |
margin-bottom: 10px;
|
| 413 |
}
|
|
@@ -424,7 +409,7 @@ TEMPLATE = """
|
|
| 424 |
.business-card-value a {
|
| 425 |
color: var(--brand-yellow);
|
| 426 |
text-decoration: none;
|
| 427 |
-
word-break: break-all;
|
| 428 |
}
|
| 429 |
.business-card-value a:hover {
|
| 430 |
text-decoration: underline;
|
|
@@ -455,8 +440,6 @@ TEMPLATE = """
|
|
| 455 |
height: 20px;
|
| 456 |
width: 20px;
|
| 457 |
}
|
| 458 |
-
|
| 459 |
-
/* Invoice Detail Modal */
|
| 460 |
.modal {
|
| 461 |
display: none;
|
| 462 |
position: fixed;
|
|
@@ -577,6 +560,25 @@ TEMPLATE = """
|
|
| 577 |
<p class="client-id-label">Ваш ID клиента</p>
|
| 578 |
<p class="client-id-value">{{ user.id }}</p>
|
| 579 |
</section>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 580 |
|
| 581 |
<section class="history-section">
|
| 582 |
<h2 class="history-title">История операций</h2>
|
|
@@ -661,7 +663,7 @@ TEMPLATE = """
|
|
| 661 |
<div class="business-card-value">
|
| 662 |
{% if org_details.whatsapp_link %}
|
| 663 |
<a href="{{ org_details.whatsapp_link }}" target="_blank">
|
| 664 |
-
<img src="data:image/svg+xml;base64,
|
| 665 |
{{ org_details.whatsapp_link }}
|
| 666 |
</a>
|
| 667 |
{% else %}
|
|
@@ -689,13 +691,11 @@ TEMPLATE = """
|
|
| 689 |
</div>
|
| 690 |
</div>
|
| 691 |
|
| 692 |
-
<!-- Invoice Detail Modal -->
|
| 693 |
<div id="invoiceDetailModal" class="modal">
|
| 694 |
<div class="modal-content">
|
| 695 |
<span class="modal-close" onclick="closeModal('invoiceDetailModal')">×</span>
|
| 696 |
<h2 id="invoiceDetailTitle" class="modal-title"></h2>
|
| 697 |
<ul id="invoiceDetailList" class="invoice-detail-list">
|
| 698 |
-
<!-- Invoice items will be loaded here -->
|
| 699 |
</ul>
|
| 700 |
<div id="invoiceDetailTotal" class="invoice-total-display">
|
| 701 |
<span>Итого:</span>
|
|
@@ -807,14 +807,59 @@ TEMPLATE = """
|
|
| 807 |
openModal('invoiceDetailModal');
|
| 808 |
}
|
| 809 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 810 |
document.addEventListener('DOMContentLoaded', () => {
|
| 811 |
document.querySelectorAll('.nav-btn').forEach(button => {
|
| 812 |
button.addEventListener('click', () => {
|
| 813 |
showSection(button.dataset.target);
|
| 814 |
});
|
| 815 |
});
|
| 816 |
-
|
| 817 |
-
// Initial section display
|
| 818 |
showSection('dashboard-section');
|
| 819 |
});
|
| 820 |
|
|
@@ -920,122 +965,32 @@ ADMIN_TEMPLATE = """
|
|
| 920 |
.modal-footer button { padding: 12px 25px; font-size: 1.1em; border-radius: 8px; border: none; font-weight: 600; cursor: pointer; }
|
| 921 |
.btn-submit { background-color: var(--admin-success); color: white; }
|
| 922 |
.status-message { text-align: center; font-weight: 500; flex-grow: 1; text-align: left; }
|
| 923 |
-
|
| 924 |
-
|
| 925 |
-
.tab-
|
| 926 |
-
|
| 927 |
-
|
| 928 |
-
|
| 929 |
-
}
|
| 930 |
-
.
|
| 931 |
-
|
| 932 |
-
|
| 933 |
-
|
| 934 |
-
|
| 935 |
-
|
| 936 |
-
|
| 937 |
-
|
| 938 |
-
|
| 939 |
-
}
|
| 940 |
-
.
|
| 941 |
-
|
| 942 |
-
|
| 943 |
-
}
|
| 944 |
-
.tab-content {
|
| 945 |
-
display: none;
|
| 946 |
-
}
|
| 947 |
-
.tab-content.active {
|
| 948 |
-
display: block;
|
| 949 |
-
}
|
| 950 |
-
|
| 951 |
-
/* Invoice table */
|
| 952 |
-
.invoice-items-table {
|
| 953 |
-
width: 100%;
|
| 954 |
-
border-collapse: collapse;
|
| 955 |
-
margin-top: 1rem;
|
| 956 |
-
}
|
| 957 |
-
.invoice-items-table th, .invoice-items-table td {
|
| 958 |
-
border: 1px solid var(--admin-border);
|
| 959 |
-
padding: 8px;
|
| 960 |
-
text-align: left;
|
| 961 |
-
font-size: 0.9em;
|
| 962 |
-
}
|
| 963 |
-
.invoice-items-table th {
|
| 964 |
-
background-color: #e9ecef;
|
| 965 |
-
font-weight: 600;
|
| 966 |
-
color: var(--admin-text);
|
| 967 |
-
}
|
| 968 |
-
.invoice-items-table .total-row td {
|
| 969 |
-
font-weight: 700;
|
| 970 |
-
background-color: #f0f0f0;
|
| 971 |
-
}
|
| 972 |
-
.invoice-items-table .action-btn {
|
| 973 |
-
background: none;
|
| 974 |
-
border: none;
|
| 975 |
-
color: var(--admin-danger);
|
| 976 |
-
cursor: pointer;
|
| 977 |
-
font-size: 1.2em;
|
| 978 |
-
}
|
| 979 |
-
.invoice-section-summary {
|
| 980 |
-
padding: 1rem;
|
| 981 |
-
background-color: #f0f0f0;
|
| 982 |
-
border-radius: 8px;
|
| 983 |
-
margin-top: 1rem;
|
| 984 |
-
font-weight: 600;
|
| 985 |
-
}
|
| 986 |
-
.invoice-list-admin {
|
| 987 |
-
list-style: none;
|
| 988 |
-
padding: 0;
|
| 989 |
-
max-height: 200px;
|
| 990 |
-
overflow-y: auto;
|
| 991 |
-
border: 1px solid var(--admin-border);
|
| 992 |
-
border-radius: 8px;
|
| 993 |
-
}
|
| 994 |
-
.invoice-list-admin li {
|
| 995 |
-
padding: 8px 12px;
|
| 996 |
-
border-bottom: 1px solid var(--admin-border);
|
| 997 |
-
display: flex;
|
| 998 |
-
justify-content: space-between;
|
| 999 |
-
align-items: center;
|
| 1000 |
-
}
|
| 1001 |
-
.invoice-list-admin li:last-child {
|
| 1002 |
-
border-bottom: none;
|
| 1003 |
-
}
|
| 1004 |
-
.invoice-list-admin .invoice-info {
|
| 1005 |
-
font-size: 0.9em;
|
| 1006 |
-
}
|
| 1007 |
-
.invoice-list-admin .invoice-amount {
|
| 1008 |
-
font-weight: 700;
|
| 1009 |
-
color: var(--admin-primary-dark);
|
| 1010 |
-
}
|
| 1011 |
-
.invoice-list-admin .view-btn {
|
| 1012 |
-
background: none;
|
| 1013 |
-
border: none;
|
| 1014 |
-
color: var(--admin-secondary);
|
| 1015 |
-
cursor: pointer;
|
| 1016 |
-
font-size: 0.9em;
|
| 1017 |
-
margin-left: 10px;
|
| 1018 |
-
}
|
| 1019 |
-
.invoice-list-admin .delete-btn {
|
| 1020 |
-
background: none;
|
| 1021 |
-
border: none;
|
| 1022 |
-
color: var(--admin-danger);
|
| 1023 |
-
cursor: pointer;
|
| 1024 |
-
font-size: 0.9em;
|
| 1025 |
-
margin-left: 5px;
|
| 1026 |
-
}
|
| 1027 |
-
.organization-details-form {
|
| 1028 |
-
display: flex;
|
| 1029 |
-
flex-direction: column;
|
| 1030 |
-
gap: 1rem;
|
| 1031 |
-
}
|
| 1032 |
-
.organization-details-form textarea {
|
| 1033 |
-
min-height: 80px;
|
| 1034 |
-
resize: vertical;
|
| 1035 |
-
}
|
| 1036 |
</style>
|
| 1037 |
</head>
|
| 1038 |
<body>
|
|
|
|
|
|
|
|
|
|
| 1039 |
<div class="container">
|
| 1040 |
<h1>Панель администратора Bonus</h1>
|
| 1041 |
<div class="summary-bar">
|
|
@@ -1055,6 +1010,10 @@ ADMIN_TEMPLATE = """
|
|
| 1055 |
<div class="value debt">{{ summary.users_with_debt }}</div>
|
| 1056 |
<div class="label">Клиенты с долгом</div>
|
| 1057 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1058 |
</div>
|
| 1059 |
|
| 1060 |
<div class="controls-bar">
|
|
@@ -1072,6 +1031,9 @@ ADMIN_TEMPLATE = """
|
|
| 1072 |
<div class="user-details">
|
| 1073 |
<div class="name">{{ user.first_name or '' }} {{ user.last_name or '' }}</div>
|
| 1074 |
<div class="username">@{{ user.username if user.username else user.phone_number }} | ID: {{ user.id }}</div>
|
|
|
|
|
|
|
|
|
|
| 1075 |
</div>
|
| 1076 |
</div>
|
| 1077 |
<div class="user-balances">
|
|
@@ -1098,7 +1060,6 @@ ADMIN_TEMPLATE = """
|
|
| 1098 |
{% endif %}
|
| 1099 |
</div>
|
| 1100 |
|
| 1101 |
-
<!-- Transaction/Invoice Modal -->
|
| 1102 |
<div id="transactionModal" class="modal">
|
| 1103 |
<div class="modal-content">
|
| 1104 |
<span class="modal-close" onclick="closeModal('transactionModal')">×</span>
|
|
@@ -1156,6 +1117,11 @@ ADMIN_TEMPLATE = """
|
|
| 1156 |
</div>
|
| 1157 |
</div>
|
| 1158 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1159 |
<div class="history-container">
|
| 1160 |
<h3>Общая история операций</h3>
|
| 1161 |
<ul id="modalHistoryList" class="history-list"></ul>
|
|
@@ -1180,7 +1146,6 @@ ADMIN_TEMPLATE = """
|
|
| 1180 |
</tr>
|
| 1181 |
</thead>
|
| 1182 |
<tbody>
|
| 1183 |
-
<!-- New invoice items will be added here -->
|
| 1184 |
</tbody>
|
| 1185 |
<tfoot>
|
| 1186 |
<tr>
|
|
@@ -1205,7 +1170,6 @@ ADMIN_TEMPLATE = """
|
|
| 1205 |
</div>
|
| 1206 |
</div>
|
| 1207 |
|
| 1208 |
-
<!-- Add Client Modal -->
|
| 1209 |
<div id="addClientModal" class="modal">
|
| 1210 |
<div class="modal-content">
|
| 1211 |
<span class="modal-close" onclick="closeModal('addClientModal')">×</span>
|
|
@@ -1227,7 +1191,6 @@ ADMIN_TEMPLATE = """
|
|
| 1227 |
</div>
|
| 1228 |
</div>
|
| 1229 |
|
| 1230 |
-
<!-- Organization Settings Modal -->
|
| 1231 |
<div id="orgSettingsModal" class="modal">
|
| 1232 |
<div class="modal-content">
|
| 1233 |
<span class="modal-close" onclick="closeModal('orgSettingsModal')">×</span>
|
|
@@ -1255,6 +1218,10 @@ ADMIN_TEMPLATE = """
|
|
| 1255 |
<label for="orgTelegramLink">Ссылка на Telegram</label>
|
| 1256 |
<input type="url" id="orgTelegramLink" placeholder="https://t.me/your_telegram_username">
|
| 1257 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1258 |
</div>
|
| 1259 |
<div class="modal-footer">
|
| 1260 |
<div id="orgStatus" class="status-message"></div>
|
|
@@ -1263,13 +1230,11 @@ ADMIN_TEMPLATE = """
|
|
| 1263 |
</div>
|
| 1264 |
</div>
|
| 1265 |
|
| 1266 |
-
<!-- Invoice Detail Modal (for Admin) -->
|
| 1267 |
<div id="adminInvoiceDetailModal" class="modal">
|
| 1268 |
<div class="modal-content">
|
| 1269 |
<span class="modal-close" onclick="closeModal('adminInvoiceDetailModal')">×</span>
|
| 1270 |
<h2 id="adminInvoiceDetailTitle" class="modal-title"></h2>
|
| 1271 |
<ul id="adminInvoiceDetailList" class="invoice-detail-list">
|
| 1272 |
-
<!-- Invoice items will be loaded here -->
|
| 1273 |
</ul>
|
| 1274 |
<div id="adminInvoiceDetailTotal" class="invoice-total-display">
|
| 1275 |
<span>Итого:</span>
|
|
@@ -1310,19 +1275,36 @@ ADMIN_TEMPLATE = """
|
|
| 1310 |
document.getElementById('repayDebtAmount').value = '';
|
| 1311 |
document.getElementById('modalStatus').textContent = '';
|
| 1312 |
document.getElementById('invoiceStatus').textContent = '';
|
| 1313 |
-
|
| 1314 |
-
// Reset new invoice items
|
| 1315 |
newInvoiceItems = [];
|
| 1316 |
renderNewInvoiceItems();
|
| 1317 |
-
|
| 1318 |
-
|
| 1319 |
-
|
| 1320 |
-
// Set default tab
|
| 1321 |
showTab('bonus-debt-tab');
|
| 1322 |
-
|
| 1323 |
transactionModal.style.display = 'block';
|
| 1324 |
}
|
| 1325 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1326 |
function loadUserHistoryAndInvoices() {
|
| 1327 |
const historyList = document.getElementById('modalHistoryList');
|
| 1328 |
historyList.innerHTML = '';
|
|
@@ -1340,8 +1322,8 @@ ADMIN_TEMPLATE = """
|
|
| 1340 |
sign = item.type === 'accrual' ? '+' : '-';
|
| 1341 |
amountClass = item.type === 'accrual' ? 'bonus-accrual' : 'bonus-deduction';
|
| 1342 |
amountText = `${sign}${parseFloat(item.amount).toFixed(2)}`;
|
| 1343 |
-
} else {
|
| 1344 |
-
sign = item.type === 'accrual' ? '+' : '-';
|
| 1345 |
amountClass = item.type === 'accrual' ? 'debt-accrual' : 'debt-payment';
|
| 1346 |
amountText = `${item.type === 'accrual' ? '+' : '-'}${parseFloat(item.amount).toFixed(2)}`;
|
| 1347 |
}
|
|
@@ -1358,7 +1340,6 @@ ADMIN_TEMPLATE = """
|
|
| 1358 |
historyList.innerHTML = '<li style="text-align:center; padding: 1rem; color: var(--admin-secondary);">Нет истории</li>';
|
| 1359 |
}
|
| 1360 |
|
| 1361 |
-
// Load invoices for invoice tab
|
| 1362 |
const modalInvoiceList = document.getElementById('modalInvoiceList');
|
| 1363 |
modalInvoiceList.innerHTML = '';
|
| 1364 |
const userInvoices = (currentUserData.invoices || []).sort((a, b) => new Date(b.date) - new Date(a.date));
|
|
@@ -1402,6 +1383,7 @@ ADMIN_TEMPLATE = """
|
|
| 1402 |
document.getElementById('orgAddress').value = data.address || '';
|
| 1403 |
document.getElementById('orgWhatsAppLink').value = data.whatsapp_link || '';
|
| 1404 |
document.getElementById('orgTelegramLink').value = data.telegram_link || '';
|
|
|
|
| 1405 |
document.getElementById('orgStatus').textContent = '';
|
| 1406 |
orgSettingsModal.style.display = 'block';
|
| 1407 |
})
|
|
@@ -1559,6 +1541,7 @@ ADMIN_TEMPLATE = """
|
|
| 1559 |
address: document.getElementById('orgAddress').value.trim(),
|
| 1560 |
whatsapp_link: document.getElementById('orgWhatsAppLink').value.trim(),
|
| 1561 |
telegram_link: document.getElementById('orgTelegramLink').value.trim(),
|
|
|
|
| 1562 |
};
|
| 1563 |
|
| 1564 |
try {
|
|
@@ -1605,14 +1588,9 @@ ADMIN_TEMPLATE = """
|
|
| 1605 |
function addNewInvoiceItemRow() {
|
| 1606 |
const tableBody = document.getElementById('newInvoiceItemsTable').getElementsByTagName('tbody')[0];
|
| 1607 |
const newRow = tableBody.insertRow();
|
| 1608 |
-
const rowIndex = tableBody.rows.length - 1;
|
| 1609 |
|
| 1610 |
-
newInvoiceItems.push({
|
| 1611 |
-
product_name: '',
|
| 1612 |
-
quantity: 0,
|
| 1613 |
-
unit_price: 0,
|
| 1614 |
-
item_total: 0
|
| 1615 |
-
});
|
| 1616 |
|
| 1617 |
newRow.innerHTML = `
|
| 1618 |
<td><input type="text" placeholder="Название товара" oninput="updateInvoiceItem(${rowIndex}, 'product_name', this.value)"></td>
|
|
@@ -1626,12 +1604,10 @@ ADMIN_TEMPLATE = """
|
|
| 1626 |
function updateInvoiceItem(index, field, value) {
|
| 1627 |
if (newInvoiceItems[index]) {
|
| 1628 |
newInvoiceItems[index][field] = value;
|
| 1629 |
-
|
| 1630 |
const qty = parseFloat(newInvoiceItems[index].quantity) || 0;
|
| 1631 |
const price = parseFloat(newInvoiceItems[index].unit_price) || 0;
|
| 1632 |
const itemTotal = qty * price;
|
| 1633 |
newInvoiceItems[index].item_total = itemTotal;
|
| 1634 |
-
|
| 1635 |
const tableBody = document.getElementById('newInvoiceItemsTable').getElementsByTagName('tbody')[0];
|
| 1636 |
tableBody.rows[index].querySelector('.item-total-display').textContent = itemTotal.toFixed(2);
|
| 1637 |
updateNewInvoiceTotal();
|
|
@@ -1640,9 +1616,7 @@ ADMIN_TEMPLATE = """
|
|
| 1640 |
|
| 1641 |
function removeInvoiceItemRow(button, index) {
|
| 1642 |
const tableBody = document.getElementById('newInvoiceItemsTable').getElementsByTagName('tbody')[0];
|
| 1643 |
-
tableBody.deleteRow(button.parentNode.parentNode.rowIndex - 1);
|
| 1644 |
-
|
| 1645 |
-
// Re-index newInvoiceItems and update the oninput attributes
|
| 1646 |
newInvoiceItems.splice(index, 1);
|
| 1647 |
for (let i = 0; i < tableBody.rows.length; i++) {
|
| 1648 |
const row = tableBody.rows[i];
|
|
@@ -1651,16 +1625,12 @@ ADMIN_TEMPLATE = """
|
|
| 1651 |
row.querySelector('input[type="number"][step="0.01"]').setAttribute('oninput', `updateInvoiceItem(${i}, 'unit_price', parseFloat(this.value))`);
|
| 1652 |
row.querySelector('.action-btn').setAttribute('onclick', `removeInvoiceItemRow(this, ${i})`);
|
| 1653 |
}
|
| 1654 |
-
|
| 1655 |
updateNewInvoiceTotal();
|
| 1656 |
}
|
| 1657 |
|
| 1658 |
-
|
| 1659 |
function updateNewInvoiceTotal() {
|
| 1660 |
let total = 0;
|
| 1661 |
-
newInvoiceItems.forEach(item => {
|
| 1662 |
-
total += parseFloat(item.item_total) || 0;
|
| 1663 |
-
});
|
| 1664 |
document.getElementById('newInvoiceTotalAmount').textContent = total.toFixed(2);
|
| 1665 |
}
|
| 1666 |
|
|
@@ -1690,17 +1660,13 @@ ADMIN_TEMPLATE = """
|
|
| 1690 |
statusEl.textContent = 'Пользователь не выбран.';
|
| 1691 |
return;
|
| 1692 |
}
|
| 1693 |
-
|
| 1694 |
const itemsToAdd = newInvoiceItems.filter(item => item.product_name && (item.quantity > 0 || item.unit_price > 0));
|
| 1695 |
-
|
| 1696 |
if (itemsToAdd.length === 0) {
|
| 1697 |
statusEl.style.color = 'var(--admin-danger)';
|
| 1698 |
statusEl.textContent = 'Добавьте хотя бы один товар в накладную.';
|
| 1699 |
return;
|
| 1700 |
}
|
| 1701 |
-
|
| 1702 |
const totalAmount = itemsToAdd.reduce((sum, item) => sum + item.item_total, 0);
|
| 1703 |
-
|
| 1704 |
const payload = {
|
| 1705 |
user_id: currentUserData.id,
|
| 1706 |
total_amount: totalAmount,
|
|
@@ -1757,7 +1723,7 @@ ADMIN_TEMPLATE = """
|
|
| 1757 |
});
|
| 1758 |
const result = await response.json();
|
| 1759 |
if (response.ok) {
|
| 1760 |
-
location.reload();
|
| 1761 |
} else {
|
| 1762 |
throw new Error(result.message || 'Не удалось удалить накладную.');
|
| 1763 |
}
|
|
@@ -1768,24 +1734,13 @@ ADMIN_TEMPLATE = """
|
|
| 1768 |
|
| 1769 |
|
| 1770 |
window.onclick = function(event) {
|
| 1771 |
-
if (event.target == transactionModal) {
|
| 1772 |
-
|
| 1773 |
-
}
|
| 1774 |
-
if (event.target ==
|
| 1775 |
-
closeModal('addClientModal');
|
| 1776 |
-
}
|
| 1777 |
-
if (event.target == orgSettingsModal) {
|
| 1778 |
-
closeModal('orgSettingsModal');
|
| 1779 |
-
}
|
| 1780 |
-
if (event.target == adminInvoiceDetailModal) {
|
| 1781 |
-
closeModal('adminInvoiceDetailModal');
|
| 1782 |
-
}
|
| 1783 |
}
|
| 1784 |
|
| 1785 |
-
|
| 1786 |
-
document.addEventListener('DOMContentLoaded', () => {
|
| 1787 |
-
addNewInvoiceItemRow(); // Add an empty row for new invoice input
|
| 1788 |
-
});
|
| 1789 |
</script>
|
| 1790 |
</body>
|
| 1791 |
</html>
|
|
@@ -1794,8 +1749,7 @@ ADMIN_TEMPLATE = """
|
|
| 1794 |
@app.route('/')
|
| 1795 |
def index():
|
| 1796 |
user_id_str = request.args.get('user_id_for_test')
|
| 1797 |
-
|
| 1798 |
-
all_data = load_visitor_data() # Load all data, including organization details
|
| 1799 |
user_data = {}
|
| 1800 |
|
| 1801 |
if user_id_str and user_id_str in all_data:
|
|
@@ -1816,20 +1770,14 @@ def index():
|
|
| 1816 |
reverse=True
|
| 1817 |
)
|
| 1818 |
user_data['combined_history'] = combined_history
|
| 1819 |
-
user_data['invoices'] = user_data.get('invoices', [])
|
| 1820 |
else:
|
| 1821 |
user_data = {
|
| 1822 |
-
"id": "N/A",
|
| 1823 |
-
"
|
| 1824 |
-
"debts": 0,
|
| 1825 |
-
"history": [],
|
| 1826 |
-
"debt_history": [],
|
| 1827 |
-
"combined_history": [],
|
| 1828 |
-
"invoices": []
|
| 1829 |
}
|
| 1830 |
|
| 1831 |
org_details = all_data.get('organization_details', {})
|
| 1832 |
-
|
| 1833 |
return render_template_string(TEMPLATE, user=user_data, org_details=org_details)
|
| 1834 |
|
| 1835 |
@app.route('/verify', methods=['POST'])
|
|
@@ -1841,7 +1789,6 @@ def verify_data():
|
|
| 1841 |
return jsonify({"status": "error", "message": "Missing initData"}), 400
|
| 1842 |
|
| 1843 |
user_data_parsed, is_valid = verify_telegram_data(init_data_str)
|
| 1844 |
-
|
| 1845 |
user_info_dict = {}
|
| 1846 |
if user_data_parsed and 'user' in user_data_parsed:
|
| 1847 |
try:
|
|
@@ -1855,13 +1802,11 @@ def verify_data():
|
|
| 1855 |
tg_user_id = user_info_dict.get('id')
|
| 1856 |
if tg_user_id:
|
| 1857 |
now = datetime.now(BISHKEK_TZ)
|
| 1858 |
-
all_data = load_visitor_data()
|
| 1859 |
|
| 1860 |
existing_user_key = None
|
| 1861 |
for key, user_data_item in all_data.items():
|
| 1862 |
-
|
| 1863 |
-
if key == "organization_details":
|
| 1864 |
-
continue
|
| 1865 |
if str(user_data_item.get('telegram_id')) == str(tg_user_id):
|
| 1866 |
existing_user_key = key
|
| 1867 |
break
|
|
@@ -1869,40 +1814,27 @@ def verify_data():
|
|
| 1869 |
if existing_user_key:
|
| 1870 |
user_entry = all_data[existing_user_key]
|
| 1871 |
user_entry.update({
|
| 1872 |
-
'first_name': user_info_dict.get('first_name'),
|
| 1873 |
-
'
|
| 1874 |
-
'
|
| 1875 |
-
'photo_url': user_info_dict.get('photo_url'),
|
| 1876 |
-
'language_code': user_info_dict.get('language_code'),
|
| 1877 |
-
'visited_at': now.timestamp(),
|
| 1878 |
'visited_at_str': now.strftime('%Y-%m-%d %H:%M:%S')
|
| 1879 |
})
|
| 1880 |
user_id_to_save = existing_user_key
|
| 1881 |
else:
|
| 1882 |
new_user_id = generate_unique_id(all_data)
|
| 1883 |
user_entry = {
|
| 1884 |
-
'id': new_user_id,
|
| 1885 |
-
'
|
| 1886 |
-
'
|
| 1887 |
-
'
|
| 1888 |
-
'
|
| 1889 |
-
'
|
| 1890 |
-
'
|
| 1891 |
-
'is_premium': user_info_dict.get('is_premium', False),
|
| 1892 |
-
'phone_number': None, # No phone number from Telegram initData by default
|
| 1893 |
-
'visited_at': now.timestamp(),
|
| 1894 |
-
'visited_at_str': now.strftime('%Y-%m-%d %H:%M:%S'),
|
| 1895 |
-
'bonuses': 0,
|
| 1896 |
-
'history': [],
|
| 1897 |
-
'debts': 0,
|
| 1898 |
-
'debt_history': [],
|
| 1899 |
-
'invoices': [] # Initialize invoices list
|
| 1900 |
}
|
| 1901 |
user_id_to_save = new_user_id
|
| 1902 |
|
| 1903 |
-
all_data[user_id_to_save] = user_entry
|
| 1904 |
-
save_visitor_data(all_data)
|
| 1905 |
-
|
| 1906 |
return jsonify({"status": "ok", "verified": True, "user_id": user_id_to_save})
|
| 1907 |
else:
|
| 1908 |
return jsonify({"status": "error", "verified": True, "message": "User ID not found in parsed data"}), 400
|
|
@@ -1919,7 +1851,7 @@ def admin_panel():
|
|
| 1919 |
all_data = load_visitor_data()
|
| 1920 |
users_list = []
|
| 1921 |
for user_id, user_data in all_data.items():
|
| 1922 |
-
if user_id == "organization_details":
|
| 1923 |
continue
|
| 1924 |
user_data['id'] = user_id
|
| 1925 |
users_list.append(user_data)
|
|
@@ -1930,13 +1862,63 @@ def admin_panel():
|
|
| 1930 |
users_with_debt = sum(1 for u in users_list if u.get('debts', 0) > 0)
|
| 1931 |
|
| 1932 |
summary_stats = {
|
| 1933 |
-
"total_users": total_users,
|
| 1934 |
-
"
|
| 1935 |
-
"total_debts": total_debts,
|
| 1936 |
-
"users_with_debt": users_with_debt
|
| 1937 |
}
|
| 1938 |
-
|
| 1939 |
-
return render_template_string(ADMIN_TEMPLATE, users=users_list, summary=summary_stats)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1940 |
|
| 1941 |
@app.route('/admin/add_client', methods=['POST'])
|
| 1942 |
def add_client():
|
|
@@ -1950,10 +1932,8 @@ def add_client():
|
|
| 1950 |
|
| 1951 |
all_data = load_visitor_data()
|
| 1952 |
|
| 1953 |
-
# Check for existing phone number, excluding the 'organization_details' key
|
| 1954 |
for key, user in all_data.items():
|
| 1955 |
-
if key == "organization_details":
|
| 1956 |
-
continue
|
| 1957 |
if user.get('phone_number') == phone_number:
|
| 1958 |
return jsonify({"status": "error", "message": "Клиент с таким номером телефона уже существует."}), 409
|
| 1959 |
|
|
@@ -1961,25 +1941,14 @@ def add_client():
|
|
| 1961 |
new_id = generate_unique_id(all_data)
|
| 1962 |
|
| 1963 |
new_client = {
|
| 1964 |
-
'id': new_id,
|
| 1965 |
-
'
|
| 1966 |
-
'
|
| 1967 |
-
'
|
| 1968 |
-
'
|
| 1969 |
-
'photo_url': None,
|
| 1970 |
-
'language_code': 'ru',
|
| 1971 |
-
'is_premium': False,
|
| 1972 |
-
'phone_number': phone_number,
|
| 1973 |
-
'visited_at': now.timestamp(),
|
| 1974 |
-
'visited_at_str': now.strftime('%Y-%m-%d %H:%M:%S'),
|
| 1975 |
-
'bonuses': 0,
|
| 1976 |
-
'history': [],
|
| 1977 |
-
'debts': 0,
|
| 1978 |
-
'debt_history': [],
|
| 1979 |
-
'invoices': [] # Initialize invoices for new manual client
|
| 1980 |
}
|
| 1981 |
|
| 1982 |
-
all_data[new_id] = new_client
|
| 1983 |
save_visitor_data(all_data)
|
| 1984 |
|
| 1985 |
return jsonify({"status": "ok", "message": "Client added successfully"}), 201
|
|
@@ -1988,7 +1957,6 @@ def add_client():
|
|
| 1988 |
logging.exception("Error in /admin/add_client endpoint")
|
| 1989 |
return jsonify({"status": "error", "message": str(e)}), 500
|
| 1990 |
|
| 1991 |
-
|
| 1992 |
@app.route('/admin/add_transaction', methods=['POST'])
|
| 1993 |
def add_transaction():
|
| 1994 |
try:
|
|
@@ -2019,50 +1987,45 @@ def add_transaction():
|
|
| 2019 |
if repay_debt_amount > user.get('debts', 0):
|
| 2020 |
return jsonify({"status": "error", "message": "Сумма погашения превышает текущий долг"}), 400
|
| 2021 |
|
| 2022 |
-
# Bonus operations
|
| 2023 |
accrual_amount = purchase_amount * 0.02
|
| 2024 |
user['bonuses'] = round(user.get('bonuses', 0) + accrual_amount - deduct_amount, 2)
|
| 2025 |
-
if 'history' not in user or not isinstance(user['history'], list):
|
| 2026 |
-
user['history'] = []
|
| 2027 |
|
| 2028 |
if accrual_amount > 0:
|
| 2029 |
-
user['history'].append({
|
| 2030 |
-
"type": "accrual", "amount": round(accrual_amount, 2),
|
| 2031 |
-
"description": f"Начисление с покупки {round(purchase_amount, 2)}",
|
| 2032 |
-
"date": now_iso, "date_str": now_str
|
| 2033 |
-
})
|
| 2034 |
if deduct_amount > 0:
|
| 2035 |
-
user['history'].append({
|
| 2036 |
-
"type": "deduction", "amount": round(deduct_amount, 2),
|
| 2037 |
-
"description": "Списание бонусов",
|
| 2038 |
-
"date": now_iso, "date_str": now_str
|
| 2039 |
-
})
|
| 2040 |
|
| 2041 |
-
# Debt operations
|
| 2042 |
user['debts'] = round(user.get('debts', 0) + add_debt_amount - repay_debt_amount, 2)
|
| 2043 |
-
if 'debt_history' not in user or not isinstance(user['debt_history'], list):
|
| 2044 |
-
user['debt_history'] = []
|
| 2045 |
|
| 2046 |
if add_debt_amount > 0:
|
| 2047 |
-
user['debt_history'].append({
|
| 2048 |
-
"type": "accrual", "amount": round(add_debt_amount, 2),
|
| 2049 |
-
"description": "Добавление долга",
|
| 2050 |
-
"date": now_iso, "date_str": now_str
|
| 2051 |
-
})
|
| 2052 |
if repay_debt_amount > 0:
|
| 2053 |
-
user['debt_history'].append({
|
| 2054 |
-
|
| 2055 |
-
|
| 2056 |
-
|
| 2057 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2058 |
|
| 2059 |
-
all_data[user_id_str] = user # Update the global cache
|
| 2060 |
save_visitor_data(all_data)
|
| 2061 |
|
| 2062 |
-
return jsonify({
|
| 2063 |
-
"status": "ok", "message": "Transaction successful",
|
| 2064 |
-
"new_balance": user['bonuses'], "new_debt": user['debts']
|
| 2065 |
-
}), 200
|
| 2066 |
|
| 2067 |
except Exception as e:
|
| 2068 |
logging.exception("Error in /admin/add_transaction endpoint")
|
|
@@ -2076,10 +2039,8 @@ def add_invoice():
|
|
| 2076 |
total_amount = float(data.get('total_amount', 0))
|
| 2077 |
items = data.get('items', [])
|
| 2078 |
|
| 2079 |
-
if not user_id:
|
| 2080 |
-
|
| 2081 |
-
if not items:
|
| 2082 |
-
return jsonify({"status": "error", "message": "Необходимо добавить товары в накладную."}), 400
|
| 2083 |
|
| 2084 |
user_id_str = str(user_id)
|
| 2085 |
all_data = load_visitor_data()
|
|
@@ -2092,7 +2053,7 @@ def add_invoice():
|
|
| 2092 |
now_iso = now.isoformat()
|
| 2093 |
now_str = now.strftime('%Y-%m-%d %H:%M:%S')
|
| 2094 |
|
| 2095 |
-
invoice_id = str(uuid.uuid4().hex[:8]).upper()
|
| 2096 |
|
| 2097 |
processed_items = []
|
| 2098 |
for item in items:
|
|
@@ -2100,23 +2061,14 @@ def add_invoice():
|
|
| 2100 |
qty = float(item.get('quantity', 0))
|
| 2101 |
u_price = float(item.get('unit_price', 0))
|
| 2102 |
i_total = round(qty * u_price, 2)
|
| 2103 |
-
processed_items.append({
|
| 2104 |
-
"product_name": p_name,
|
| 2105 |
-
"quantity": qty,
|
| 2106 |
-
"unit_price": u_price,
|
| 2107 |
-
"item_total": i_total
|
| 2108 |
-
})
|
| 2109 |
|
| 2110 |
new_invoice = {
|
| 2111 |
-
"invoice_id": invoice_id,
|
| 2112 |
-
"
|
| 2113 |
-
"date_str": now_str,
|
| 2114 |
-
"total_amount": round(total_amount, 2),
|
| 2115 |
-
"items": processed_items
|
| 2116 |
}
|
| 2117 |
|
| 2118 |
-
if 'invoices' not in user or not isinstance(user['invoices'], list):
|
| 2119 |
-
user['invoices'] = []
|
| 2120 |
user['invoices'].append(new_invoice)
|
| 2121 |
|
| 2122 |
all_data[user_id_str] = user
|
|
@@ -2163,20 +2115,18 @@ def delete_invoice():
|
|
| 2163 |
logging.exception("Error in /admin/delete_invoice endpoint")
|
| 2164 |
return jsonify({"status": "error", "message": str(e)}), 500
|
| 2165 |
|
| 2166 |
-
|
| 2167 |
@app.route('/admin/delete_client', methods=['POST'])
|
| 2168 |
def delete_client():
|
| 2169 |
try:
|
| 2170 |
data = request.get_json()
|
| 2171 |
user_id = data.get('user_id')
|
| 2172 |
|
| 2173 |
-
if not user_id:
|
| 2174 |
-
return jsonify({"status": "error", "message": "User ID is required"}), 400
|
| 2175 |
|
| 2176 |
user_id_str = str(user_id)
|
| 2177 |
-
all_data = load_visitor_data()
|
| 2178 |
|
| 2179 |
-
with _data_lock:
|
| 2180 |
if user_id_str not in all_data or user_id_str == "organization_details":
|
| 2181 |
return jsonify({"status": "error", "message": "User not found"}), 404
|
| 2182 |
|
|
@@ -2184,10 +2134,9 @@ def delete_client():
|
|
| 2184 |
if user_to_delete.get('telegram_id') is not None:
|
| 2185 |
return jsonify({"status": "error", "message": "Cannot delete a Telegram-linked user"}), 403
|
| 2186 |
|
| 2187 |
-
del all_data[user_id_str]
|
| 2188 |
|
| 2189 |
try:
|
| 2190 |
-
# Save the modified all_data (which is visitor_data_cache)
|
| 2191 |
with open(DATA_FILE, 'w', encoding='utf-8') as f:
|
| 2192 |
json.dump(all_data, f, ensure_ascii=False, indent=4)
|
| 2193 |
logging.info(f"User {user_id_str} deleted. Data saved to {DATA_FILE}.")
|
|
@@ -2217,16 +2166,15 @@ def save_organization_details():
|
|
| 2217 |
try:
|
| 2218 |
data = request.get_json()
|
| 2219 |
new_org_details = {
|
| 2220 |
-
"name": data.get("name", ""),
|
| 2221 |
-
"
|
| 2222 |
-
"
|
| 2223 |
-
"
|
| 2224 |
-
"telegram_link": data.get("telegram_link", "")
|
| 2225 |
}
|
| 2226 |
|
| 2227 |
all_data = load_visitor_data()
|
| 2228 |
all_data['organization_details'] = new_org_details
|
| 2229 |
-
save_visitor_data(all_data)
|
| 2230 |
|
| 2231 |
return jsonify({"status": "ok", "message": "Organization details saved successfully"}), 200
|
| 2232 |
except Exception as e:
|
|
@@ -2242,7 +2190,7 @@ if __name__ == '__main__':
|
|
| 2242 |
print("Attempting initial data download from Hugging Face...")
|
| 2243 |
download_data_from_hf()
|
| 2244 |
|
| 2245 |
-
|
| 2246 |
|
| 2247 |
print("WARNING: The /admin route is NOT protected. Implement proper authentication for production.")
|
| 2248 |
|
|
|
|
|
|
|
| 1 |
|
| 2 |
import os
|
| 3 |
from flask import Flask, request, Response, render_template_string, jsonify, redirect, url_for
|
|
|
|
| 10 |
import logging
|
| 11 |
import threading
|
| 12 |
import random
|
| 13 |
+
import pytz
|
| 14 |
+
import uuid
|
| 15 |
+
import string
|
| 16 |
|
| 17 |
from huggingface_hub import HfApi, hf_hub_download
|
| 18 |
from huggingface_hub.utils import RepositoryNotFoundError
|
|
|
|
| 27 |
HF_TOKEN_WRITE = os.getenv("HF_TOKEN_WRITE")
|
| 28 |
HF_TOKEN_READ = os.getenv("HF_TOKEN_READ")
|
| 29 |
|
|
|
|
| 30 |
BISHKEK_TZ = pytz.timezone('Asia/Bishkek')
|
| 31 |
|
| 32 |
app = Flask(__name__)
|
|
|
|
| 34 |
app.secret_key = os.urandom(24)
|
| 35 |
|
| 36 |
_data_lock = threading.Lock()
|
| 37 |
+
visitor_data_cache = {}
|
| 38 |
|
| 39 |
def generate_unique_id(all_data):
|
| 40 |
while True:
|
|
|
|
| 41 |
new_id = str(random.randint(10000, 99999))
|
| 42 |
if new_id not in all_data:
|
| 43 |
return new_id
|
| 44 |
|
| 45 |
+
def generate_referral_code(all_data):
|
| 46 |
+
while True:
|
| 47 |
+
code = ''.join(random.choices(string.ascii_uppercase + string.digits, k=6))
|
| 48 |
+
is_unique = True
|
| 49 |
+
for key, user_data in all_data.items():
|
| 50 |
+
if key == "organization_details":
|
| 51 |
+
continue
|
| 52 |
+
if user_data.get('referral_code') == code:
|
| 53 |
+
is_unique = False
|
| 54 |
+
break
|
| 55 |
+
if is_unique:
|
| 56 |
+
return code
|
| 57 |
+
|
| 58 |
def download_data_from_hf():
|
| 59 |
global visitor_data_cache
|
| 60 |
if not HF_TOKEN_READ:
|
|
|
|
| 91 |
def load_visitor_data():
|
| 92 |
global visitor_data_cache
|
| 93 |
with _data_lock:
|
| 94 |
+
if not visitor_data_cache:
|
| 95 |
try:
|
| 96 |
with open(DATA_FILE, 'r', encoding='utf-8') as f:
|
| 97 |
visitor_data_cache = json.load(f)
|
| 98 |
logging.info("Visitor data loaded from local JSON.")
|
| 99 |
except FileNotFoundError:
|
| 100 |
logging.warning(f"{DATA_FILE} not found locally. Starting with empty data.")
|
| 101 |
+
visitor_data_cache = {"organization_details": {}}
|
| 102 |
except json.JSONDecodeError:
|
| 103 |
logging.error(f"Error decoding {DATA_FILE}. Starting with empty data.")
|
| 104 |
visitor_data_cache = {"organization_details": {}}
|
|
|
|
| 106 |
logging.error(f"Unexpected error loading visitor data: {e}")
|
| 107 |
visitor_data_cache = {"organization_details": {}}
|
| 108 |
|
|
|
|
| 109 |
if "organization_details" not in visitor_data_cache:
|
| 110 |
visitor_data_cache["organization_details"] = {}
|
| 111 |
|
|
|
|
| 114 |
def save_visitor_data(data):
|
| 115 |
with _data_lock:
|
| 116 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
with open(DATA_FILE, 'w', encoding='utf-8') as f:
|
| 118 |
json.dump(visitor_data_cache, f, ensure_ascii=False, indent=4)
|
| 119 |
logging.info(f"Visitor data successfully saved to {DATA_FILE}.")
|
|
|
|
| 242 |
.header {
|
| 243 |
text-align: left;
|
| 244 |
padding: var(--padding-m) 0;
|
| 245 |
+
margin-bottom: 0;
|
| 246 |
}
|
| 247 |
.logo {
|
| 248 |
font-size: 2.5em;
|
|
|
|
| 283 |
box-shadow: 0 2px 10px rgba(255,193,7,0.3);
|
| 284 |
}
|
| 285 |
.content-section {
|
| 286 |
+
display: none;
|
| 287 |
flex-direction: column;
|
| 288 |
gap: var(--padding-m);
|
| 289 |
}
|
| 290 |
.content-section.active {
|
| 291 |
+
display: flex;
|
| 292 |
}
|
| 293 |
.card-grid {
|
| 294 |
display: grid;
|
|
|
|
| 393 |
color: var(--text-secondary-color);
|
| 394 |
padding: 2rem 0;
|
| 395 |
}
|
|
|
|
|
|
|
| 396 |
.business-card-item {
|
| 397 |
margin-bottom: 10px;
|
| 398 |
}
|
|
|
|
| 409 |
.business-card-value a {
|
| 410 |
color: var(--brand-yellow);
|
| 411 |
text-decoration: none;
|
| 412 |
+
word-break: break-all;
|
| 413 |
}
|
| 414 |
.business-card-value a:hover {
|
| 415 |
text-decoration: underline;
|
|
|
|
| 440 |
height: 20px;
|
| 441 |
width: 20px;
|
| 442 |
}
|
|
|
|
|
|
|
| 443 |
.modal {
|
| 444 |
display: none;
|
| 445 |
position: fixed;
|
|
|
|
| 560 |
<p class="client-id-label">Ваш ID клиента</p>
|
| 561 |
<p class="client-id-value">{{ user.id }}</p>
|
| 562 |
</section>
|
| 563 |
+
|
| 564 |
+
<section class="client-id-card">
|
| 565 |
+
<p class="client-id-label">Ваш реферальный код</p>
|
| 566 |
+
<div style="display: flex; align-items: center; gap: 8px;">
|
| 567 |
+
<p id="referralCode" class="client-id-value" style="margin: 0;">{{ user.referral_code }}</p>
|
| 568 |
+
<button onclick="copyReferralCode()" style="padding: 10px; border-radius: 8px; border: none; background: var(--brand-yellow); color: var(--brand-black); font-weight: 600; cursor: pointer;">Копировать</button>
|
| 569 |
+
</div>
|
| 570 |
+
</section>
|
| 571 |
+
|
| 572 |
+
{% if not user.referred_by_user_id %}
|
| 573 |
+
<section class="history-section">
|
| 574 |
+
<h2 class="history-title">Активировать код</h2>
|
| 575 |
+
<div style="display: flex; gap: 10px; align-items: center;">
|
| 576 |
+
<input type="text" id="referralCodeInput" placeholder="Введите реферальный код" style="flex-grow: 1; padding: 12px; font-size: 1em; border-radius: 8px; border: 1px solid #333; background: #2a2a2a; color: white; text-transform: uppercase;">
|
| 577 |
+
<button onclick="activateReferralCode()" style="padding: 12px 18px; border-radius: 8px; border: none; background: var(--brand-yellow); color: var(--brand-black); font-weight: 600; cursor: pointer;">Активировать</button>
|
| 578 |
+
</div>
|
| 579 |
+
<p id="referralStatus" style="text-align: center; margin-top: 10px; font-weight: 500; min-height: 1em;"></p>
|
| 580 |
+
</section>
|
| 581 |
+
{% endif %}
|
| 582 |
|
| 583 |
<section class="history-section">
|
| 584 |
<h2 class="history-title">История операций</h2>
|
|
|
|
| 663 |
<div class="business-card-value">
|
| 664 |
{% if org_details.whatsapp_link %}
|
| 665 |
<a href="{{ org_details.whatsapp_link }}" target="_blank">
|
| 666 |
+
<img src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgZmlsbD0iI2ZmZiIgY2xhc3M9ImJpIGJpLXdoYXRzYXBwIiB2aWV3Qm94PSIwIDAgMTYgMTYiPjxwYXRoIGQ9Ik0xMy42NzYgMS40NTJBMTAuNTE1IDEwLjUxNSAwIDAgMCA3Ljg4MyAwQzMuNTEgMCAuMDYzIDMuNDQuMDYzIDcuNjU0YzAgMS40MS4zNDcgMi44NjIgLjk2NiA0LjExOC4wNC4wNy4wNjYuMTc2LjA3My4yOTlsLjA2NSAxLjg5MmExLjIzMyAxLjIzMyAwIDAgMCAxLjU1MyAxLjQ1NmwxLjg4Ljc4Yy4xMzYuMDU2LjI3Ni4wOTYuNDI4LjE3MiAxLjI1Ni42NjggMi42MTQuOTgyIDQuMDQ4Ljk4MiA0LjM3MiAwIDcuOTEtMy40NCA3LjkxLTcuNjU0IDAtMi4wNDItLjg1LTMuOTY1LTIuMzQxLTUuMzUzWm0tMi42NzYgOS4yMzFhLjg4MS44ODEgMCAwIDEtMS4yMjUuMDM2bC0uNzk0LS40ODMtLjQ2NC4zNjYtLjY1MS0uNDktMS4xNjYgMS4xNjYtLjUzLS4zMTctLjA4NC41NTUtLjEwMi40NTktLjI1MS4xNjItLjU3LjU3LS41NTUuMDgyLS4zMjIuMDY5LS42Mi0uMTc3LS45ODQtLjE5LS45MDgtLjYxNy0uNDc4LS45MDktLjY3Ny0uNzUxLS40MzQtMS4xNzgtLjMyMi0xLjQ3MS0uMzIyLS4xMDkgMC0uNDg3LjA0Mi0uOTY2LjA4OC0uNDk3LjI1Ny0uNjExuNzMyLS42MTEgMS4xMzdhMS42MzkgMS42MzkgMCAwIDAgLjQ4NiAxLjY5Yy40MTYuNDE2Ljc2Mi43MjQuNzYyLjk1NSAwIC41NDIuMjgyIDEuMDQ2LjMwNCAxLjQxMS41ODYgMS4wNTEgMS43NzUgMS44NDcgMy4wNCAyLjAxOCAxLjMxMi4xNzcgMi4xMDYuMDk1IDIuNzYzLjA3Mi4wOTYtLjE1LjM3Mi0uMjg1LjcwMi0uNDgzLjMxLS40OC41MDktLjUxNS42NjktLjQ1Mi4xMDkuMDUxLjQxNy4yMTEuNDYzLjI2Ny4xNDEuMDgyLjI4NC4xNjEuNDQzLjIyNWExLjIyNyAxLjIyNyAwIDAgMCAuNzQ2LjAyNGwuMjg0LS4xMzVhMy45NjcgMy45NjcgMCAwIDAgLjY2Mi0uNjQzLjkwOC45MDggMCAwIDAgLjMwMi0uNjc4LjE5OC4xOTggMCAwIDAgMC0uMTU2LjgxNS44MTUgMCAwIDAgMC0uNDc3eiIvPjwvc3ZnPg==">
|
| 667 |
{{ org_details.whatsapp_link }}
|
| 668 |
</a>
|
| 669 |
{% else %}
|
|
|
|
| 691 |
</div>
|
| 692 |
</div>
|
| 693 |
|
|
|
|
| 694 |
<div id="invoiceDetailModal" class="modal">
|
| 695 |
<div class="modal-content">
|
| 696 |
<span class="modal-close" onclick="closeModal('invoiceDetailModal')">×</span>
|
| 697 |
<h2 id="invoiceDetailTitle" class="modal-title"></h2>
|
| 698 |
<ul id="invoiceDetailList" class="invoice-detail-list">
|
|
|
|
| 699 |
</ul>
|
| 700 |
<div id="invoiceDetailTotal" class="invoice-total-display">
|
| 701 |
<span>Итого:</span>
|
|
|
|
| 807 |
openModal('invoiceDetailModal');
|
| 808 |
}
|
| 809 |
|
| 810 |
+
function copyReferralCode() {
|
| 811 |
+
const code = document.getElementById('referralCode').textContent;
|
| 812 |
+
navigator.clipboard.writeText(code).then(() => {
|
| 813 |
+
tg.showAlert('Код скопирован!');
|
| 814 |
+
}, () => {
|
| 815 |
+
tg.showAlert('Не удалось скопировать код.');
|
| 816 |
+
});
|
| 817 |
+
}
|
| 818 |
+
|
| 819 |
+
function activateReferralCode() {
|
| 820 |
+
const codeInput = document.getElementById('referralCodeInput');
|
| 821 |
+
const code = codeInput.value.trim().toUpperCase();
|
| 822 |
+
const statusEl = document.getElementById('referralStatus');
|
| 823 |
+
if (!code) {
|
| 824 |
+
statusEl.textContent = 'Введите код.';
|
| 825 |
+
statusEl.style.color = 'var(--brand-red)';
|
| 826 |
+
return;
|
| 827 |
+
}
|
| 828 |
+
|
| 829 |
+
const urlParams = new URLSearchParams(window.location.search);
|
| 830 |
+
const userId = urlParams.get('user_id_for_test');
|
| 831 |
+
|
| 832 |
+
statusEl.textContent = 'Активация...';
|
| 833 |
+
statusEl.style.color = 'var(--text-secondary-color)';
|
| 834 |
+
|
| 835 |
+
fetch('/api/activate_referral', {
|
| 836 |
+
method: 'POST',
|
| 837 |
+
headers: { 'Content-Type': 'application/json' },
|
| 838 |
+
body: JSON.stringify({ user_id: userId, referral_code: code })
|
| 839 |
+
})
|
| 840 |
+
.then(response => response.json())
|
| 841 |
+
.then(data => {
|
| 842 |
+
if (data.status === 'ok') {
|
| 843 |
+
statusEl.textContent = 'Код успешно активирован!';
|
| 844 |
+
statusEl.style.color = '#4CAF50';
|
| 845 |
+
setTimeout(() => { window.location.reload(); }, 1500);
|
| 846 |
+
} else {
|
| 847 |
+
statusEl.textContent = data.message || 'Ошибка активации.';
|
| 848 |
+
statusEl.style.color = 'var(--brand-red)';
|
| 849 |
+
}
|
| 850 |
+
})
|
| 851 |
+
.catch(error => {
|
| 852 |
+
statusEl.textContent = 'Произошла ошибка сети.';
|
| 853 |
+
statusEl.style.color = 'var(--brand-red)';
|
| 854 |
+
});
|
| 855 |
+
}
|
| 856 |
+
|
| 857 |
document.addEventListener('DOMContentLoaded', () => {
|
| 858 |
document.querySelectorAll('.nav-btn').forEach(button => {
|
| 859 |
button.addEventListener('click', () => {
|
| 860 |
showSection(button.dataset.target);
|
| 861 |
});
|
| 862 |
});
|
|
|
|
|
|
|
| 863 |
showSection('dashboard-section');
|
| 864 |
});
|
| 865 |
|
|
|
|
| 965 |
.modal-footer button { padding: 12px 25px; font-size: 1.1em; border-radius: 8px; border: none; font-weight: 600; cursor: pointer; }
|
| 966 |
.btn-submit { background-color: var(--admin-success); color: white; }
|
| 967 |
.status-message { text-align: center; font-weight: 500; flex-grow: 1; text-align: left; }
|
| 968 |
+
.tab-buttons { display: flex; margin-bottom: 1rem; border-bottom: 1px solid var(--admin-border); }
|
| 969 |
+
.tab-btn { padding: 10px 15px; border: none; background-color: transparent; color: var(--admin-secondary); font-weight: 600; cursor: pointer; border-bottom: 3px solid transparent; transition: all 0.2s ease; }
|
| 970 |
+
.tab-btn.active { color: var(--admin-primary-dark); border-bottom-color: var(--admin-primary); }
|
| 971 |
+
.tab-content { display: none; }
|
| 972 |
+
.tab-content.active { display: block; }
|
| 973 |
+
.invoice-items-table { width: 100%; border-collapse: collapse; margin-top: 1rem; }
|
| 974 |
+
.invoice-items-table th, .invoice-items-table td { border: 1px solid var(--admin-border); padding: 8px; text-align: left; font-size: 0.9em; }
|
| 975 |
+
.invoice-items-table th { background-color: #e9ecef; font-weight: 600; color: var(--admin-text); }
|
| 976 |
+
.invoice-items-table .total-row td { font-weight: 700; background-color: #f0f0f0; }
|
| 977 |
+
.invoice-items-table .action-btn { background: none; border: none; color: var(--admin-danger); cursor: pointer; font-size: 1.2em; }
|
| 978 |
+
.invoice-section-summary { padding: 1rem; background-color: #f0f0f0; border-radius: 8px; margin-top: 1rem; font-weight: 600; }
|
| 979 |
+
.invoice-list-admin { list-style: none; padding: 0; max-height: 200px; overflow-y: auto; border: 1px solid var(--admin-border); border-radius: 8px; }
|
| 980 |
+
.invoice-list-admin li { padding: 8px 12px; border-bottom: 1px solid var(--admin-border); display: flex; justify-content: space-between; align-items: center; }
|
| 981 |
+
.invoice-list-admin li:last-child { border-bottom: none; }
|
| 982 |
+
.invoice-list-admin .invoice-info { font-size: 0.9em; }
|
| 983 |
+
.invoice-list-admin .invoice-amount { font-weight: 700; color: var(--admin-primary-dark); }
|
| 984 |
+
.invoice-list-admin .view-btn { background: none; border: none; color: var(--admin-secondary); cursor: pointer; font-size: 0.9em; margin-left: 10px; }
|
| 985 |
+
.invoice-list-admin .delete-btn { background: none; border: none; color: var(--admin-danger); cursor: pointer; font-size: 0.9em; margin-left: 5px; }
|
| 986 |
+
.organization-details-form { display: flex; flex-direction: column; gap: 1rem; }
|
| 987 |
+
.organization-details-form textarea { min-height: 80px; resize: vertical; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 988 |
</style>
|
| 989 |
</head>
|
| 990 |
<body>
|
| 991 |
+
<script id="allUsersData" type="application/json">
|
| 992 |
+
{{ all_data|tojson }}
|
| 993 |
+
</script>
|
| 994 |
<div class="container">
|
| 995 |
<h1>Панель администратора Bonus</h1>
|
| 996 |
<div class="summary-bar">
|
|
|
|
| 1010 |
<div class="value debt">{{ summary.users_with_debt }}</div>
|
| 1011 |
<div class="label">Клиенты с долгом</div>
|
| 1012 |
</div>
|
| 1013 |
+
<div class="summary-card">
|
| 1014 |
+
<div class="value bonus">{{ "%.1f"|format(org_details.referral_bonus_percentage|float) }}%</div>
|
| 1015 |
+
<div class="label">Реферальный бонус</div>
|
| 1016 |
+
</div>
|
| 1017 |
</div>
|
| 1018 |
|
| 1019 |
<div class="controls-bar">
|
|
|
|
| 1031 |
<div class="user-details">
|
| 1032 |
<div class="name">{{ user.first_name or '' }} {{ user.last_name or '' }}</div>
|
| 1033 |
<div class="username">@{{ user.username if user.username else user.phone_number }} | ID: {{ user.id }}</div>
|
| 1034 |
+
{% if user.referred_by_user_id and all_data[user.referred_by_user_id] %}
|
| 1035 |
+
<div class="username" style="font-size: 0.8em; color: var(--admin-success); margin-top: 4px;">Приведен: {{ all_data[user.referred_by_user_id].first_name }} ({{ user.referred_by_user_id }})</div>
|
| 1036 |
+
{% endif %}
|
| 1037 |
</div>
|
| 1038 |
</div>
|
| 1039 |
<div class="user-balances">
|
|
|
|
| 1060 |
{% endif %}
|
| 1061 |
</div>
|
| 1062 |
|
|
|
|
| 1063 |
<div id="transactionModal" class="modal">
|
| 1064 |
<div class="modal-content">
|
| 1065 |
<span class="modal-close" onclick="closeModal('transactionModal')">×</span>
|
|
|
|
| 1117 |
</div>
|
| 1118 |
</div>
|
| 1119 |
|
| 1120 |
+
<div class="history-container">
|
| 1121 |
+
<h3>Приведенные клиенты</h3>
|
| 1122 |
+
<ul id="modalReferralsList" class="history-list"></ul>
|
| 1123 |
+
</div>
|
| 1124 |
+
|
| 1125 |
<div class="history-container">
|
| 1126 |
<h3>Общая история операций</h3>
|
| 1127 |
<ul id="modalHistoryList" class="history-list"></ul>
|
|
|
|
| 1146 |
</tr>
|
| 1147 |
</thead>
|
| 1148 |
<tbody>
|
|
|
|
| 1149 |
</tbody>
|
| 1150 |
<tfoot>
|
| 1151 |
<tr>
|
|
|
|
| 1170 |
</div>
|
| 1171 |
</div>
|
| 1172 |
|
|
|
|
| 1173 |
<div id="addClientModal" class="modal">
|
| 1174 |
<div class="modal-content">
|
| 1175 |
<span class="modal-close" onclick="closeModal('addClientModal')">×</span>
|
|
|
|
| 1191 |
</div>
|
| 1192 |
</div>
|
| 1193 |
|
|
|
|
| 1194 |
<div id="orgSettingsModal" class="modal">
|
| 1195 |
<div class="modal-content">
|
| 1196 |
<span class="modal-close" onclick="closeModal('orgSettingsModal')">×</span>
|
|
|
|
| 1218 |
<label for="orgTelegramLink">Ссылка на Telegram</label>
|
| 1219 |
<input type="url" id="orgTelegramLink" placeholder="https://t.me/your_telegram_username">
|
| 1220 |
</div>
|
| 1221 |
+
<div class="form-group">
|
| 1222 |
+
<label for="orgReferralBonusPercentage">Процент реферального бонуса (от суммы покупки)</label>
|
| 1223 |
+
<input type="number" step="0.1" min="0" id="orgReferralBonusPercentage" placeholder="2.0">
|
| 1224 |
+
</div>
|
| 1225 |
</div>
|
| 1226 |
<div class="modal-footer">
|
| 1227 |
<div id="orgStatus" class="status-message"></div>
|
|
|
|
| 1230 |
</div>
|
| 1231 |
</div>
|
| 1232 |
|
|
|
|
| 1233 |
<div id="adminInvoiceDetailModal" class="modal">
|
| 1234 |
<div class="modal-content">
|
| 1235 |
<span class="modal-close" onclick="closeModal('adminInvoiceDetailModal')">×</span>
|
| 1236 |
<h2 id="adminInvoiceDetailTitle" class="modal-title"></h2>
|
| 1237 |
<ul id="adminInvoiceDetailList" class="invoice-detail-list">
|
|
|
|
| 1238 |
</ul>
|
| 1239 |
<div id="adminInvoiceDetailTotal" class="invoice-total-display">
|
| 1240 |
<span>Итого:</span>
|
|
|
|
| 1275 |
document.getElementById('repayDebtAmount').value = '';
|
| 1276 |
document.getElementById('modalStatus').textContent = '';
|
| 1277 |
document.getElementById('invoiceStatus').textContent = '';
|
|
|
|
|
|
|
| 1278 |
newInvoiceItems = [];
|
| 1279 |
renderNewInvoiceItems();
|
| 1280 |
+
loadUserHistoryAndInvoices();
|
| 1281 |
+
populateReferralsList(userData);
|
|
|
|
|
|
|
| 1282 |
showTab('bonus-debt-tab');
|
|
|
|
| 1283 |
transactionModal.style.display = 'block';
|
| 1284 |
}
|
| 1285 |
|
| 1286 |
+
function populateReferralsList(userData) {
|
| 1287 |
+
const referralsListEl = document.getElementById('modalReferralsList');
|
| 1288 |
+
referralsListEl.innerHTML = '';
|
| 1289 |
+
const referrals = userData.referrals || [];
|
| 1290 |
+
const allUsersData = JSON.parse(document.getElementById('allUsersData').textContent);
|
| 1291 |
+
|
| 1292 |
+
if (referrals.length > 0) {
|
| 1293 |
+
referrals.forEach(refId => {
|
| 1294 |
+
const referrerData = allUsersData[refId];
|
| 1295 |
+
const li = document.createElement('li');
|
| 1296 |
+
if (referrerData) {
|
| 1297 |
+
li.textContent = `${referrerData.first_name || 'Клиент'} (ID: ${refId})`;
|
| 1298 |
+
} else {
|
| 1299 |
+
li.textContent = `Неизвестный клиент (ID: ${refId})`;
|
| 1300 |
+
}
|
| 1301 |
+
referralsListEl.appendChild(li);
|
| 1302 |
+
});
|
| 1303 |
+
} else {
|
| 1304 |
+
referralsListEl.innerHTML = '<li style="text-align:center; padding: 1rem; color: var(--admin-secondary);">Нет рефералов</li>';
|
| 1305 |
+
}
|
| 1306 |
+
}
|
| 1307 |
+
|
| 1308 |
function loadUserHistoryAndInvoices() {
|
| 1309 |
const historyList = document.getElementById('modalHistoryList');
|
| 1310 |
historyList.innerHTML = '';
|
|
|
|
| 1322 |
sign = item.type === 'accrual' ? '+' : '-';
|
| 1323 |
amountClass = item.type === 'accrual' ? 'bonus-accrual' : 'bonus-deduction';
|
| 1324 |
amountText = `${sign}${parseFloat(item.amount).toFixed(2)}`;
|
| 1325 |
+
} else {
|
| 1326 |
+
sign = item.type === 'accrual' ? '+' : '-';
|
| 1327 |
amountClass = item.type === 'accrual' ? 'debt-accrual' : 'debt-payment';
|
| 1328 |
amountText = `${item.type === 'accrual' ? '+' : '-'}${parseFloat(item.amount).toFixed(2)}`;
|
| 1329 |
}
|
|
|
|
| 1340 |
historyList.innerHTML = '<li style="text-align:center; padding: 1rem; color: var(--admin-secondary);">Нет истории</li>';
|
| 1341 |
}
|
| 1342 |
|
|
|
|
| 1343 |
const modalInvoiceList = document.getElementById('modalInvoiceList');
|
| 1344 |
modalInvoiceList.innerHTML = '';
|
| 1345 |
const userInvoices = (currentUserData.invoices || []).sort((a, b) => new Date(b.date) - new Date(a.date));
|
|
|
|
| 1383 |
document.getElementById('orgAddress').value = data.address || '';
|
| 1384 |
document.getElementById('orgWhatsAppLink').value = data.whatsapp_link || '';
|
| 1385 |
document.getElementById('orgTelegramLink').value = data.telegram_link || '';
|
| 1386 |
+
document.getElementById('orgReferralBonusPercentage').value = data.referral_bonus_percentage || '';
|
| 1387 |
document.getElementById('orgStatus').textContent = '';
|
| 1388 |
orgSettingsModal.style.display = 'block';
|
| 1389 |
})
|
|
|
|
| 1541 |
address: document.getElementById('orgAddress').value.trim(),
|
| 1542 |
whatsapp_link: document.getElementById('orgWhatsAppLink').value.trim(),
|
| 1543 |
telegram_link: document.getElementById('orgTelegramLink').value.trim(),
|
| 1544 |
+
referral_bonus_percentage: parseFloat(document.getElementById('orgReferralBonusPercentage').value) || 0,
|
| 1545 |
};
|
| 1546 |
|
| 1547 |
try {
|
|
|
|
| 1588 |
function addNewInvoiceItemRow() {
|
| 1589 |
const tableBody = document.getElementById('newInvoiceItemsTable').getElementsByTagName('tbody')[0];
|
| 1590 |
const newRow = tableBody.insertRow();
|
| 1591 |
+
const rowIndex = tableBody.rows.length - 1;
|
| 1592 |
|
| 1593 |
+
newInvoiceItems.push({ product_name: '', quantity: 0, unit_price: 0, item_total: 0 });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1594 |
|
| 1595 |
newRow.innerHTML = `
|
| 1596 |
<td><input type="text" placeholder="Название товара" oninput="updateInvoiceItem(${rowIndex}, 'product_name', this.value)"></td>
|
|
|
|
| 1604 |
function updateInvoiceItem(index, field, value) {
|
| 1605 |
if (newInvoiceItems[index]) {
|
| 1606 |
newInvoiceItems[index][field] = value;
|
|
|
|
| 1607 |
const qty = parseFloat(newInvoiceItems[index].quantity) || 0;
|
| 1608 |
const price = parseFloat(newInvoiceItems[index].unit_price) || 0;
|
| 1609 |
const itemTotal = qty * price;
|
| 1610 |
newInvoiceItems[index].item_total = itemTotal;
|
|
|
|
| 1611 |
const tableBody = document.getElementById('newInvoiceItemsTable').getElementsByTagName('tbody')[0];
|
| 1612 |
tableBody.rows[index].querySelector('.item-total-display').textContent = itemTotal.toFixed(2);
|
| 1613 |
updateNewInvoiceTotal();
|
|
|
|
| 1616 |
|
| 1617 |
function removeInvoiceItemRow(button, index) {
|
| 1618 |
const tableBody = document.getElementById('newInvoiceItemsTable').getElementsByTagName('tbody')[0];
|
| 1619 |
+
tableBody.deleteRow(button.parentNode.parentNode.rowIndex - 1);
|
|
|
|
|
|
|
| 1620 |
newInvoiceItems.splice(index, 1);
|
| 1621 |
for (let i = 0; i < tableBody.rows.length; i++) {
|
| 1622 |
const row = tableBody.rows[i];
|
|
|
|
| 1625 |
row.querySelector('input[type="number"][step="0.01"]').setAttribute('oninput', `updateInvoiceItem(${i}, 'unit_price', parseFloat(this.value))`);
|
| 1626 |
row.querySelector('.action-btn').setAttribute('onclick', `removeInvoiceItemRow(this, ${i})`);
|
| 1627 |
}
|
|
|
|
| 1628 |
updateNewInvoiceTotal();
|
| 1629 |
}
|
| 1630 |
|
|
|
|
| 1631 |
function updateNewInvoiceTotal() {
|
| 1632 |
let total = 0;
|
| 1633 |
+
newInvoiceItems.forEach(item => { total += parseFloat(item.item_total) || 0; });
|
|
|
|
|
|
|
| 1634 |
document.getElementById('newInvoiceTotalAmount').textContent = total.toFixed(2);
|
| 1635 |
}
|
| 1636 |
|
|
|
|
| 1660 |
statusEl.textContent = 'Пользователь не выбран.';
|
| 1661 |
return;
|
| 1662 |
}
|
|
|
|
| 1663 |
const itemsToAdd = newInvoiceItems.filter(item => item.product_name && (item.quantity > 0 || item.unit_price > 0));
|
|
|
|
| 1664 |
if (itemsToAdd.length === 0) {
|
| 1665 |
statusEl.style.color = 'var(--admin-danger)';
|
| 1666 |
statusEl.textContent = 'Добавьте хотя бы один товар в накладную.';
|
| 1667 |
return;
|
| 1668 |
}
|
|
|
|
| 1669 |
const totalAmount = itemsToAdd.reduce((sum, item) => sum + item.item_total, 0);
|
|
|
|
| 1670 |
const payload = {
|
| 1671 |
user_id: currentUserData.id,
|
| 1672 |
total_amount: totalAmount,
|
|
|
|
| 1723 |
});
|
| 1724 |
const result = await response.json();
|
| 1725 |
if (response.ok) {
|
| 1726 |
+
location.reload();
|
| 1727 |
} else {
|
| 1728 |
throw new Error(result.message || 'Не удалось удалить накладную.');
|
| 1729 |
}
|
|
|
|
| 1734 |
|
| 1735 |
|
| 1736 |
window.onclick = function(event) {
|
| 1737 |
+
if (event.target == transactionModal) { closeModal('transactionModal'); }
|
| 1738 |
+
if (event.target == addClientModal) { closeModal('addClientModal'); }
|
| 1739 |
+
if (event.target == orgSettingsModal) { closeModal('orgSettingsModal'); }
|
| 1740 |
+
if (event.target == adminInvoiceDetailModal) { closeModal('adminInvoiceDetailModal'); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1741 |
}
|
| 1742 |
|
| 1743 |
+
document.addEventListener('DOMContentLoaded', () => { addNewInvoiceItemRow(); });
|
|
|
|
|
|
|
|
|
|
| 1744 |
</script>
|
| 1745 |
</body>
|
| 1746 |
</html>
|
|
|
|
| 1749 |
@app.route('/')
|
| 1750 |
def index():
|
| 1751 |
user_id_str = request.args.get('user_id_for_test')
|
| 1752 |
+
all_data = load_visitor_data()
|
|
|
|
| 1753 |
user_data = {}
|
| 1754 |
|
| 1755 |
if user_id_str and user_id_str in all_data:
|
|
|
|
| 1770 |
reverse=True
|
| 1771 |
)
|
| 1772 |
user_data['combined_history'] = combined_history
|
| 1773 |
+
user_data['invoices'] = user_data.get('invoices', [])
|
| 1774 |
else:
|
| 1775 |
user_data = {
|
| 1776 |
+
"id": "N/A", "bonuses": 0, "debts": 0, "history": [], "debt_history": [],
|
| 1777 |
+
"combined_history": [], "invoices": [], "referral_code": "N/A", "referred_by_user_id": None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1778 |
}
|
| 1779 |
|
| 1780 |
org_details = all_data.get('organization_details', {})
|
|
|
|
| 1781 |
return render_template_string(TEMPLATE, user=user_data, org_details=org_details)
|
| 1782 |
|
| 1783 |
@app.route('/verify', methods=['POST'])
|
|
|
|
| 1789 |
return jsonify({"status": "error", "message": "Missing initData"}), 400
|
| 1790 |
|
| 1791 |
user_data_parsed, is_valid = verify_telegram_data(init_data_str)
|
|
|
|
| 1792 |
user_info_dict = {}
|
| 1793 |
if user_data_parsed and 'user' in user_data_parsed:
|
| 1794 |
try:
|
|
|
|
| 1802 |
tg_user_id = user_info_dict.get('id')
|
| 1803 |
if tg_user_id:
|
| 1804 |
now = datetime.now(BISHKEK_TZ)
|
| 1805 |
+
all_data = load_visitor_data()
|
| 1806 |
|
| 1807 |
existing_user_key = None
|
| 1808 |
for key, user_data_item in all_data.items():
|
| 1809 |
+
if key == "organization_details": continue
|
|
|
|
|
|
|
| 1810 |
if str(user_data_item.get('telegram_id')) == str(tg_user_id):
|
| 1811 |
existing_user_key = key
|
| 1812 |
break
|
|
|
|
| 1814 |
if existing_user_key:
|
| 1815 |
user_entry = all_data[existing_user_key]
|
| 1816 |
user_entry.update({
|
| 1817 |
+
'first_name': user_info_dict.get('first_name'), 'last_name': user_info_dict.get('last_name'),
|
| 1818 |
+
'username': user_info_dict.get('username'), 'photo_url': user_info_dict.get('photo_url'),
|
| 1819 |
+
'language_code': user_info_dict.get('language_code'), 'visited_at': now.timestamp(),
|
|
|
|
|
|
|
|
|
|
| 1820 |
'visited_at_str': now.strftime('%Y-%m-%d %H:%M:%S')
|
| 1821 |
})
|
| 1822 |
user_id_to_save = existing_user_key
|
| 1823 |
else:
|
| 1824 |
new_user_id = generate_unique_id(all_data)
|
| 1825 |
user_entry = {
|
| 1826 |
+
'id': new_user_id, 'telegram_id': tg_user_id, 'first_name': user_info_dict.get('first_name'),
|
| 1827 |
+
'last_name': user_info_dict.get('last_name'), 'username': user_info_dict.get('username'),
|
| 1828 |
+
'photo_url': user_info_dict.get('photo_url'), 'language_code': user_info_dict.get('language_code'),
|
| 1829 |
+
'is_premium': user_info_dict.get('is_premium', False), 'phone_number': None,
|
| 1830 |
+
'visited_at': now.timestamp(), 'visited_at_str': now.strftime('%Y-%m-%d %H:%M:%S'),
|
| 1831 |
+
'bonuses': 0, 'history': [], 'debts': 0, 'debt_history': [], 'invoices': [],
|
| 1832 |
+
'referral_code': generate_referral_code(all_data), 'referred_by_user_id': None, 'referrals': []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1833 |
}
|
| 1834 |
user_id_to_save = new_user_id
|
| 1835 |
|
| 1836 |
+
all_data[user_id_to_save] = user_entry
|
| 1837 |
+
save_visitor_data(all_data)
|
|
|
|
| 1838 |
return jsonify({"status": "ok", "verified": True, "user_id": user_id_to_save})
|
| 1839 |
else:
|
| 1840 |
return jsonify({"status": "error", "verified": True, "message": "User ID not found in parsed data"}), 400
|
|
|
|
| 1851 |
all_data = load_visitor_data()
|
| 1852 |
users_list = []
|
| 1853 |
for user_id, user_data in all_data.items():
|
| 1854 |
+
if user_id == "organization_details":
|
| 1855 |
continue
|
| 1856 |
user_data['id'] = user_id
|
| 1857 |
users_list.append(user_data)
|
|
|
|
| 1862 |
users_with_debt = sum(1 for u in users_list if u.get('debts', 0) > 0)
|
| 1863 |
|
| 1864 |
summary_stats = {
|
| 1865 |
+
"total_users": total_users, "total_bonuses": total_bonuses,
|
| 1866 |
+
"total_debts": total_debts, "users_with_debt": users_with_debt
|
|
|
|
|
|
|
| 1867 |
}
|
| 1868 |
+
org_details = all_data.get('organization_details', {})
|
| 1869 |
+
return render_template_string(ADMIN_TEMPLATE, users=users_list, summary=summary_stats, all_data=all_data, org_details=org_details)
|
| 1870 |
+
|
| 1871 |
+
@app.route('/api/activate_referral', methods=['POST'])
|
| 1872 |
+
def activate_referral():
|
| 1873 |
+
try:
|
| 1874 |
+
data = request.get_json()
|
| 1875 |
+
user_id = str(data.get('user_id'))
|
| 1876 |
+
referral_code = data.get('referral_code', '').upper()
|
| 1877 |
+
|
| 1878 |
+
if not user_id or not referral_code:
|
| 1879 |
+
return jsonify({"status": "error", "message": "Необходим ID пользователя и реферальный код."}), 400
|
| 1880 |
+
|
| 1881 |
+
all_data = load_visitor_data()
|
| 1882 |
+
|
| 1883 |
+
if user_id not in all_data:
|
| 1884 |
+
return jsonify({"status": "error", "message": "Пользователь не найден."}), 404
|
| 1885 |
+
|
| 1886 |
+
user = all_data[user_id]
|
| 1887 |
+
|
| 1888 |
+
if user.get('referred_by_user_id'):
|
| 1889 |
+
return jsonify({"status": "error", "message": "Вы уже активировали реферальный код."}), 400
|
| 1890 |
+
|
| 1891 |
+
if user.get('referral_code') == referral_code:
|
| 1892 |
+
return jsonify({"status": "error", "message": "Вы не можете использовать свой собственный код."}), 400
|
| 1893 |
+
|
| 1894 |
+
referrer_id = None
|
| 1895 |
+
referrer_user = None
|
| 1896 |
+
for key, value in all_data.items():
|
| 1897 |
+
if key == "organization_details": continue
|
| 1898 |
+
if value.get('referral_code') == referral_code:
|
| 1899 |
+
referrer_id = key
|
| 1900 |
+
referrer_user = value
|
| 1901 |
+
break
|
| 1902 |
+
|
| 1903 |
+
if not referrer_id or not referrer_user:
|
| 1904 |
+
return jsonify({"status": "error", "message": "Введен неверный реферальный код."}), 404
|
| 1905 |
+
|
| 1906 |
+
user['referred_by_user_id'] = referrer_id
|
| 1907 |
+
|
| 1908 |
+
if 'referrals' not in referrer_user or not isinstance(referrer_user['referrals'], list):
|
| 1909 |
+
referrer_user['referrals'] = []
|
| 1910 |
+
referrer_user['referrals'].append(user_id)
|
| 1911 |
+
|
| 1912 |
+
all_data[user_id] = user
|
| 1913 |
+
all_data[referrer_id] = referrer_user
|
| 1914 |
+
|
| 1915 |
+
save_visitor_data(all_data)
|
| 1916 |
+
|
| 1917 |
+
return jsonify({"status": "ok", "message": "Реферальный код успешно активирован."})
|
| 1918 |
+
|
| 1919 |
+
except Exception as e:
|
| 1920 |
+
logging.exception("Error in /api/activate_referral")
|
| 1921 |
+
return jsonify({"status": "error", "message": "Внутренняя ошибка сервера."}), 500
|
| 1922 |
|
| 1923 |
@app.route('/admin/add_client', methods=['POST'])
|
| 1924 |
def add_client():
|
|
|
|
| 1932 |
|
| 1933 |
all_data = load_visitor_data()
|
| 1934 |
|
|
|
|
| 1935 |
for key, user in all_data.items():
|
| 1936 |
+
if key == "organization_details": continue
|
|
|
|
| 1937 |
if user.get('phone_number') == phone_number:
|
| 1938 |
return jsonify({"status": "error", "message": "Клиент с таким номером телефона уже существует."}), 409
|
| 1939 |
|
|
|
|
| 1941 |
new_id = generate_unique_id(all_data)
|
| 1942 |
|
| 1943 |
new_client = {
|
| 1944 |
+
'id': new_id, 'telegram_id': None, 'first_name': first_name, 'last_name': None, 'username': None,
|
| 1945 |
+
'photo_url': None, 'language_code': 'ru', 'is_premium': False, 'phone_number': phone_number,
|
| 1946 |
+
'visited_at': now.timestamp(), 'visited_at_str': now.strftime('%Y-%m-%d %H:%M:%S'),
|
| 1947 |
+
'bonuses': 0, 'history': [], 'debts': 0, 'debt_history': [], 'invoices': [],
|
| 1948 |
+
'referral_code': generate_referral_code(all_data), 'referred_by_user_id': None, 'referrals': []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1949 |
}
|
| 1950 |
|
| 1951 |
+
all_data[new_id] = new_client
|
| 1952 |
save_visitor_data(all_data)
|
| 1953 |
|
| 1954 |
return jsonify({"status": "ok", "message": "Client added successfully"}), 201
|
|
|
|
| 1957 |
logging.exception("Error in /admin/add_client endpoint")
|
| 1958 |
return jsonify({"status": "error", "message": str(e)}), 500
|
| 1959 |
|
|
|
|
| 1960 |
@app.route('/admin/add_transaction', methods=['POST'])
|
| 1961 |
def add_transaction():
|
| 1962 |
try:
|
|
|
|
| 1987 |
if repay_debt_amount > user.get('debts', 0):
|
| 1988 |
return jsonify({"status": "error", "message": "Сумма погашения превышает текущий долг"}), 400
|
| 1989 |
|
|
|
|
| 1990 |
accrual_amount = purchase_amount * 0.02
|
| 1991 |
user['bonuses'] = round(user.get('bonuses', 0) + accrual_amount - deduct_amount, 2)
|
| 1992 |
+
if 'history' not in user or not isinstance(user['history'], list): user['history'] = []
|
|
|
|
| 1993 |
|
| 1994 |
if accrual_amount > 0:
|
| 1995 |
+
user['history'].append({"type": "accrual", "amount": round(accrual_amount, 2), "description": f"Начисление с покупки {round(purchase_amount, 2)}", "date": now_iso, "date_str": now_str})
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1996 |
if deduct_amount > 0:
|
| 1997 |
+
user['history'].append({"type": "deduction", "amount": round(deduct_amount, 2), "description": "Списание бонусов", "date": now_iso, "date_str": now_str})
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1998 |
|
|
|
|
| 1999 |
user['debts'] = round(user.get('debts', 0) + add_debt_amount - repay_debt_amount, 2)
|
| 2000 |
+
if 'debt_history' not in user or not isinstance(user['debt_history'], list): user['debt_history'] = []
|
|
|
|
| 2001 |
|
| 2002 |
if add_debt_amount > 0:
|
| 2003 |
+
user['debt_history'].append({"type": "accrual", "amount": round(add_debt_amount, 2), "description": "Добавление долга", "date": now_iso, "date_str": now_str})
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2004 |
if repay_debt_amount > 0:
|
| 2005 |
+
user['debt_history'].append({"type": "payment", "amount": round(repay_debt_amount, 2), "description": "Погашение долга", "date": now_iso, "date_str": now_str})
|
| 2006 |
+
|
| 2007 |
+
all_data[user_id_str] = user
|
| 2008 |
+
|
| 2009 |
+
org_details = all_data.get('organization_details', {})
|
| 2010 |
+
referral_bonus_percentage = float(org_details.get('referral_bonus_percentage', 0))
|
| 2011 |
+
if user.get('referred_by_user_id') and purchase_amount > 0 and referral_bonus_percentage > 0:
|
| 2012 |
+
referrer_id = user['referred_by_user_id']
|
| 2013 |
+
if referrer_id in all_data:
|
| 2014 |
+
referrer_user = all_data[referrer_id]
|
| 2015 |
+
referral_bonus = round(purchase_amount * (referral_bonus_percentage / 100), 2)
|
| 2016 |
+
if referral_bonus > 0:
|
| 2017 |
+
referrer_user['bonuses'] = round(referrer_user.get('bonuses', 0) + referral_bonus, 2)
|
| 2018 |
+
if 'history' not in referrer_user or not isinstance(referrer_user['history'], list): referrer_user['history'] = []
|
| 2019 |
+
referrer_user['history'].append({
|
| 2020 |
+
"type": "accrual", "amount": referral_bonus,
|
| 2021 |
+
"description": f"Бонус от реферала {user.get('first_name', user_id_str)}",
|
| 2022 |
+
"date": now_iso, "date_str": now_str
|
| 2023 |
+
})
|
| 2024 |
+
all_data[referrer_id] = referrer_user
|
| 2025 |
|
|
|
|
| 2026 |
save_visitor_data(all_data)
|
| 2027 |
|
| 2028 |
+
return jsonify({"status": "ok", "message": "Transaction successful", "new_balance": user['bonuses'], "new_debt": user['debts']}), 200
|
|
|
|
|
|
|
|
|
|
| 2029 |
|
| 2030 |
except Exception as e:
|
| 2031 |
logging.exception("Error in /admin/add_transaction endpoint")
|
|
|
|
| 2039 |
total_amount = float(data.get('total_amount', 0))
|
| 2040 |
items = data.get('items', [])
|
| 2041 |
|
| 2042 |
+
if not user_id: return jsonify({"status": "error", "message": "User ID is required"}), 400
|
| 2043 |
+
if not items: return jsonify({"status": "error", "message": "Необходимо добавить товары в накладную."}), 400
|
|
|
|
|
|
|
| 2044 |
|
| 2045 |
user_id_str = str(user_id)
|
| 2046 |
all_data = load_visitor_data()
|
|
|
|
| 2053 |
now_iso = now.isoformat()
|
| 2054 |
now_str = now.strftime('%Y-%m-%d %H:%M:%S')
|
| 2055 |
|
| 2056 |
+
invoice_id = str(uuid.uuid4().hex[:8]).upper()
|
| 2057 |
|
| 2058 |
processed_items = []
|
| 2059 |
for item in items:
|
|
|
|
| 2061 |
qty = float(item.get('quantity', 0))
|
| 2062 |
u_price = float(item.get('unit_price', 0))
|
| 2063 |
i_total = round(qty * u_price, 2)
|
| 2064 |
+
processed_items.append({"product_name": p_name, "quantity": qty, "unit_price": u_price, "item_total": i_total})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2065 |
|
| 2066 |
new_invoice = {
|
| 2067 |
+
"invoice_id": invoice_id, "date": now_iso, "date_str": now_str,
|
| 2068 |
+
"total_amount": round(total_amount, 2), "items": processed_items
|
|
|
|
|
|
|
|
|
|
| 2069 |
}
|
| 2070 |
|
| 2071 |
+
if 'invoices' not in user or not isinstance(user['invoices'], list): user['invoices'] = []
|
|
|
|
| 2072 |
user['invoices'].append(new_invoice)
|
| 2073 |
|
| 2074 |
all_data[user_id_str] = user
|
|
|
|
| 2115 |
logging.exception("Error in /admin/delete_invoice endpoint")
|
| 2116 |
return jsonify({"status": "error", "message": str(e)}), 500
|
| 2117 |
|
|
|
|
| 2118 |
@app.route('/admin/delete_client', methods=['POST'])
|
| 2119 |
def delete_client():
|
| 2120 |
try:
|
| 2121 |
data = request.get_json()
|
| 2122 |
user_id = data.get('user_id')
|
| 2123 |
|
| 2124 |
+
if not user_id: return jsonify({"status": "error", "message": "User ID is required"}), 400
|
|
|
|
| 2125 |
|
| 2126 |
user_id_str = str(user_id)
|
| 2127 |
+
all_data = load_visitor_data()
|
| 2128 |
|
| 2129 |
+
with _data_lock:
|
| 2130 |
if user_id_str not in all_data or user_id_str == "organization_details":
|
| 2131 |
return jsonify({"status": "error", "message": "User not found"}), 404
|
| 2132 |
|
|
|
|
| 2134 |
if user_to_delete.get('telegram_id') is not None:
|
| 2135 |
return jsonify({"status": "error", "message": "Cannot delete a Telegram-linked user"}), 403
|
| 2136 |
|
| 2137 |
+
del all_data[user_id_str]
|
| 2138 |
|
| 2139 |
try:
|
|
|
|
| 2140 |
with open(DATA_FILE, 'w', encoding='utf-8') as f:
|
| 2141 |
json.dump(all_data, f, ensure_ascii=False, indent=4)
|
| 2142 |
logging.info(f"User {user_id_str} deleted. Data saved to {DATA_FILE}.")
|
|
|
|
| 2166 |
try:
|
| 2167 |
data = request.get_json()
|
| 2168 |
new_org_details = {
|
| 2169 |
+
"name": data.get("name", ""), "phone_numbers": data.get("phone_numbers", []),
|
| 2170 |
+
"address": data.get("address", ""), "whatsapp_link": data.get("whatsapp_link", ""),
|
| 2171 |
+
"telegram_link": data.get("telegram_link", ""),
|
| 2172 |
+
"referral_bonus_percentage": data.get("referral_bonus_percentage", 0)
|
|
|
|
| 2173 |
}
|
| 2174 |
|
| 2175 |
all_data = load_visitor_data()
|
| 2176 |
all_data['organization_details'] = new_org_details
|
| 2177 |
+
save_visitor_data(all_data)
|
| 2178 |
|
| 2179 |
return jsonify({"status": "ok", "message": "Organization details saved successfully"}), 200
|
| 2180 |
except Exception as e:
|
|
|
|
| 2190 |
print("Attempting initial data download from Hugging Face...")
|
| 2191 |
download_data_from_hf()
|
| 2192 |
|
| 2193 |
+
load_visitor_dat
|
| 2194 |
|
| 2195 |
print("WARNING: The /admin route is NOT protected. Implement proper authentication for production.")
|
| 2196 |
|