Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -26,7 +26,7 @@ HF_DATA_FILE_PATH = "data.json"
|
|
| 26 |
HF_TOKEN_WRITE = os.getenv("HF_TOKEN_WRITE")
|
| 27 |
HF_TOKEN_READ = os.getenv("HF_TOKEN_READ")
|
| 28 |
|
| 29 |
-
|
| 30 |
|
| 31 |
app = Flask(__name__)
|
| 32 |
logging.basicConfig(level=logging.INFO)
|
|
@@ -134,7 +134,7 @@ def upload_data_to_hf():
|
|
| 134 |
repo_id=REPO_ID,
|
| 135 |
repo_type="dataset",
|
| 136 |
token=HF_TOKEN_WRITE,
|
| 137 |
-
commit_message=f"Update bonus data {datetime.now(
|
| 138 |
)
|
| 139 |
logging.info("Bonus data successfully uploaded to Hugging Face.")
|
| 140 |
except Exception as e:
|
|
@@ -192,30 +192,27 @@ TEMPLATE = """
|
|
| 192 |
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
| 193 |
<style>
|
| 194 |
:root {
|
| 195 |
-
--
|
| 196 |
-
--
|
|
|
|
|
|
|
|
|
|
|
|
|
| 197 |
--brand-red: #F44336;
|
| 198 |
--brand-green: #4CAF50;
|
| 199 |
--brand-blue: #2196F3;
|
| 200 |
-
--
|
| 201 |
-
--
|
| 202 |
-
--text-secondary-color: #a0a0a0;
|
| 203 |
-
--border-radius: 16px;
|
| 204 |
--padding-m: 16px;
|
| 205 |
-
--padding-l: 24px;
|
| 206 |
--font-family: 'Manrope', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
| 207 |
--shadow-color: rgba(255, 193, 7, 0.15);
|
| 208 |
--shadow-glow: 0 0 35px var(--shadow-color);
|
| 209 |
-
--shadow-color-red: rgba(244, 67, 54, 0.15);
|
| 210 |
-
--shadow-glow-red: 0 0 35px var(--shadow-color-red);
|
| 211 |
-
--shadow-color-blue: rgba(33, 150, 243, 0.15);
|
| 212 |
-
--shadow-glow-blue: 0 0 35px var(--shadow-color-blue);
|
| 213 |
}
|
| 214 |
* { box-sizing: border-box; margin: 0; padding: 0; }
|
| 215 |
html, body {
|
| 216 |
-
background-color: var(--
|
| 217 |
font-family: var(--font-family);
|
| 218 |
-
color: var(--text-color);
|
| 219 |
padding: var(--padding-m);
|
| 220 |
overscroll-behavior-y: none;
|
| 221 |
-webkit-font-smoothing: antialiased;
|
|
@@ -223,156 +220,149 @@ TEMPLATE = """
|
|
| 223 |
visibility: hidden;
|
| 224 |
min-height: 100vh;
|
| 225 |
}
|
| 226 |
-
.container {
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
gap: var(--padding-m);
|
| 232 |
-
}
|
| 233 |
-
.header { text-align: left; padding: var(--padding-m) 0; margin-bottom: 0; }
|
| 234 |
-
.logo { font-size: 2.5em; font-weight: 800; color: var(--text-color); letter-spacing: -1px; }
|
| 235 |
-
.logo span { color: var(--brand-yellow); }
|
| 236 |
-
.welcome-text { font-size: 1em; color: var(--text-secondary-color); margin-top: 4px; }
|
| 237 |
.nav-buttons {
|
| 238 |
-
display: flex;
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
border-radius: var(--border-radius);
|
| 242 |
-
padding: 8px;
|
| 243 |
-
margin-bottom: var(--padding-m);
|
| 244 |
}
|
| 245 |
.nav-btn {
|
| 246 |
-
flex-grow: 1; padding:
|
| 247 |
-
background-color: transparent; color: var(--
|
| 248 |
-
font-family: var(--font-family); font-weight:
|
| 249 |
-
cursor: pointer; transition:
|
| 250 |
}
|
| 251 |
-
.nav-btn.active { background-color: var(--
|
| 252 |
-
.content-section { display: none; flex-direction: column; gap: var(--padding-m); }
|
| 253 |
.content-section.active { display: flex; }
|
| 254 |
-
.card
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
border-radius:
|
| 258 |
-
|
| 259 |
}
|
| 260 |
-
.bonus-card {
|
| 261 |
-
.
|
| 262 |
-
.
|
| 263 |
-
.
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
.bonus-amount { color: var(--brand-yellow); }
|
| 267 |
-
.debt-amount { color: var(--brand-red); }
|
| 268 |
-
.referral-bonus-amount { color: var(--brand-blue); }
|
| 269 |
-
.client-id-card {
|
| 270 |
-
background-color: var(--card-bg); border-radius: var(--border-radius);
|
| 271 |
-
padding: var(--padding-m); display: flex; justify-content: space-between; align-items: center;
|
| 272 |
}
|
| 273 |
-
.
|
| 274 |
-
.
|
| 275 |
-
|
| 276 |
-
|
|
|
|
|
|
|
|
|
|
| 277 |
}
|
|
|
|
| 278 |
.promo-code-display {
|
| 279 |
display: flex; align-items: center; justify-content: center; gap: 12px;
|
| 280 |
background-color: rgba(0,0,0,0.2); border-radius: 12px; padding: 12px;
|
| 281 |
}
|
| 282 |
-
.promo-code-value {
|
| 283 |
-
font-size: 1.5em; font-weight: 700; color: var(--brand-yellow); letter-spacing: 2px;
|
| 284 |
-
}
|
| 285 |
.copy-btn {
|
| 286 |
-
padding:
|
| 287 |
-
background-color: var(--
|
| 288 |
-
transition: background-color 0.2s;
|
| 289 |
-
}
|
| 290 |
-
.copy-btn:hover { background-color: #ffd447; }
|
| 291 |
-
.history-section, .invoices-section, .business-card-section {
|
| 292 |
-
background-color: var(--card-bg); border-radius: var(--border-radius);
|
| 293 |
-
padding: var(--padding-l); display: flex; flex-direction: column; gap: var(--padding-m);
|
| 294 |
}
|
| 295 |
-
.
|
| 296 |
-
|
| 297 |
-
|
|
|
|
|
|
|
|
|
|
| 298 |
}
|
| 299 |
-
.
|
|
|
|
|
|
|
| 300 |
.history-item, .invoice-item {
|
| 301 |
-
display: flex;
|
| 302 |
-
|
| 303 |
}
|
| 304 |
.history-item:last-child, .invoice-item:last-child { border-bottom: none; }
|
| 305 |
-
.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 306 |
.invoice-item:hover { background-color: rgba(255,255,255,0.05); }
|
| 307 |
-
.
|
| 308 |
-
.
|
| 309 |
-
.
|
| 310 |
-
.
|
| 311 |
-
.
|
| 312 |
-
.
|
| 313 |
-
.
|
| 314 |
-
.
|
| 315 |
-
.
|
| 316 |
-
.
|
| 317 |
-
.no-history, .no-invoices { text-align: center; color: var(--text-secondary-color); padding: 2rem 0; }
|
| 318 |
-
.business-card-item { margin-bottom: 10px; }
|
| 319 |
-
.business-card-label { font-weight: 500; color: var(--text-secondary-color); margin-bottom: 4px; }
|
| 320 |
-
.business-card-value { font-size: 1.1em; font-weight: 600; color: var(--text-color); }
|
| 321 |
-
.business-card-value a { color: var(--brand-yellow); text-decoration: none; word-break: break-all; }
|
| 322 |
-
.business-card-value a:hover { text-decoration: underline; }
|
| 323 |
-
.business-card-phone-list { list-style: none; padding: 0; margin: 0; }
|
| 324 |
-
.business-card-phone-item { margin-bottom: 5px; }
|
| 325 |
.business-card-phone-item a {
|
| 326 |
-
display: inline-flex; align-items: center; gap:
|
| 327 |
-
text-decoration: none; background-color:
|
| 328 |
-
border-radius:
|
| 329 |
}
|
| 330 |
-
.business-card-phone-item a:hover { background-color:
|
| 331 |
-
.business-card-phone-item img { height: 20px; width: 20px; }
|
| 332 |
.modal {
|
| 333 |
-
display: none; position: fixed; z-index: 1000; left: 0; top: 0; width: 100%;
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
align-items: center; justify-content: center;
|
| 337 |
}
|
| 338 |
.modal-content {
|
| 339 |
-
background-color: var(--
|
| 340 |
-
border-radius: var(--border-radius);
|
| 341 |
-
box-shadow: 0 5px
|
|
|
|
| 342 |
}
|
|
|
|
| 343 |
.modal-close {
|
| 344 |
-
|
| 345 |
-
|
|
|
|
| 346 |
}
|
| 347 |
-
.modal-
|
| 348 |
-
.
|
| 349 |
-
.invoice-detail-list, .promo-modal-body { list-style: none; padding: 0; margin: 0; }
|
| 350 |
.invoice-detail-item {
|
| 351 |
-
display: flex; justify-content: space-between; padding:
|
| 352 |
-
border-bottom: 1px dashed rgba(255,255,255,0.1);
|
| 353 |
}
|
| 354 |
-
.invoice-detail-item:last-child { border-bottom: none; }
|
| 355 |
.item-name { font-weight: 500; flex-basis: 60%; }
|
| 356 |
-
.item-qty-price { font-size: 0.9em; color: var(--
|
| 357 |
-
.item-total { font-weight: 700; flex-basis: 20%; text-align: right; color: var(--
|
| 358 |
.invoice-total-display {
|
| 359 |
padding-top: var(--padding-m); border-top: 1px solid rgba(255,255,255,0.2); margin-top: var(--padding-m);
|
| 360 |
display: flex; justify-content: space-between; font-size: 1.2em; font-weight: 700;
|
| 361 |
}
|
| 362 |
-
.
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
color: var(--
|
|
|
|
|
|
|
| 368 |
}
|
| 369 |
-
.promo-modal-actions { display: flex; gap: 1rem; }
|
| 370 |
-
|
| 371 |
-
flex-grow: 1; padding:
|
| 372 |
-
border:
|
| 373 |
}
|
| 374 |
-
.btn-apply-promo { background-color: var(--
|
| 375 |
-
.btn-skip-promo { background-color:
|
| 376 |
#promoStatus { margin-top: 1rem; font-weight: 500; min-height: 20px; }
|
| 377 |
</style>
|
| 378 |
</head>
|
|
@@ -388,52 +378,68 @@ TEMPLATE = """
|
|
| 388 |
<button class="nav-btn" data-target="business-card-section">Визитка</button>
|
| 389 |
</nav>
|
| 390 |
<div id="dashboard-section" class="content-section active">
|
| 391 |
-
<section class="card
|
| 392 |
-
<
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
<div class="
|
| 397 |
<p class="card-label">Ваш долг</p>
|
| 398 |
-
<p class="debt
|
| 399 |
</div>
|
| 400 |
-
|
| 401 |
<p class="card-label">Бонусы с друзей</p>
|
| 402 |
-
<p class="referral
|
| 403 |
</div>
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
</div>
|
| 411 |
</section>
|
| 412 |
-
|
| 413 |
<section class="client-id-card">
|
| 414 |
<p class="client-id-label">Ваш ID клиента</p>
|
| 415 |
<p class="client-id-value">{{ user.id }}</p>
|
| 416 |
</section>
|
| 417 |
-
<section class="
|
| 418 |
-
<h2 class="
|
| 419 |
{% if user.combined_history %}
|
| 420 |
<ul class="history-list">
|
| 421 |
{% for item in user.combined_history %}
|
| 422 |
<li class="history-item">
|
| 423 |
-
<div class="history-details">
|
| 424 |
-
<span class="history-description">{{ item.description }}</span>
|
| 425 |
-
<span class="history-date">{{ item.date_str }}</span>
|
| 426 |
-
</div>
|
| 427 |
{% if item.transaction_type == 'bonus' %}
|
| 428 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 429 |
{{ '+' if item.type == 'accrual' else '-' }}{{ "%.2f"|format(item.amount|float) }}
|
| 430 |
</span>
|
| 431 |
{% elif item.transaction_type == 'debt' %}
|
| 432 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 433 |
{{ '-' if item.type == 'payment' else '+' }}{{ "%.2f"|format(item.amount|float) }}
|
| 434 |
</span>
|
| 435 |
{% elif item.transaction_type == 'referral' %}
|
| 436 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 437 |
+{{ "%.2f"|format(item.amount|float) }}
|
| 438 |
</span>
|
| 439 |
{% endif %}
|
|
@@ -441,17 +447,20 @@ TEMPLATE = """
|
|
| 441 |
{% endfor %}
|
| 442 |
</ul>
|
| 443 |
{% else %}
|
| 444 |
-
<p class="no-
|
| 445 |
{% endif %}
|
| 446 |
</section>
|
| 447 |
</div>
|
| 448 |
<div id="invoices-section" class="content-section">
|
| 449 |
-
<section class="
|
| 450 |
-
<h2 class="
|
| 451 |
{% if user.invoices %}
|
| 452 |
<ul class="invoices-list">
|
| 453 |
{% for invoice in user.invoices|sort(attribute='date', reverse=true) %}
|
| 454 |
<li class="invoice-item" onclick='openInvoiceDetailModal({{ invoice|tojson }})'>
|
|
|
|
|
|
|
|
|
|
| 455 |
<div class="invoice-details">
|
| 456 |
<span class="invoice-description">Накладная #{{ invoice.invoice_id }}</span>
|
| 457 |
<span class="invoice-date">{{ invoice.date_str }}</span>
|
|
@@ -461,68 +470,66 @@ TEMPLATE = """
|
|
| 461 |
{% endfor %}
|
| 462 |
</ul>
|
| 463 |
{% else %}
|
| 464 |
-
<p class="no-
|
| 465 |
{% endif %}
|
| 466 |
</section>
|
| 467 |
</div>
|
| 468 |
<div id="business-card-section" class="content-section">
|
| 469 |
-
<section class="
|
| 470 |
-
<h2 class="
|
| 471 |
-
{% if org_details %}
|
|
|
|
| 472 |
<div class="business-card-item">
|
| 473 |
<div class="business-card-label">Название организации</div>
|
| 474 |
-
<div class="business-card-value">{{ org_details.name
|
| 475 |
</div>
|
|
|
|
|
|
|
| 476 |
<div class="business-card-item">
|
| 477 |
<div class="business-card-label">Номера телефонов</div>
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
<
|
| 482 |
-
<
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
</ul>
|
| 489 |
-
{% else %}
|
| 490 |
-
<div class="business-card-value">Не указано</div>
|
| 491 |
-
{% endif %}
|
| 492 |
</div>
|
|
|
|
|
|
|
| 493 |
<div class="business-card-item">
|
| 494 |
<div class="business-card-label">Адрес</div>
|
| 495 |
-
<div class="business-card-value">{{ org_details.address
|
| 496 |
</div>
|
|
|
|
|
|
|
| 497 |
<div class="business-card-item">
|
| 498 |
<div class="business-card-label">WhatsApp</div>
|
| 499 |
<div class="business-card-value">
|
| 500 |
-
{
|
| 501 |
-
<a href="{{ org_details.whatsapp_link }}" target="_blank"> {{ org_details.whatsapp_link }} </a>
|
| 502 |
-
{% else %}
|
| 503 |
-
<div class="business-card-value">Не указано</div>
|
| 504 |
-
{% endif %}
|
| 505 |
</div>
|
| 506 |
</div>
|
|
|
|
|
|
|
| 507 |
<div class="business-card-item">
|
| 508 |
<div class="business-card-label">Telegram</div>
|
| 509 |
<div class="business-card-value">
|
| 510 |
-
|
| 511 |
-
<a href="{{ org_details.telegram_link }}" target="_blank"> {{ org_details.telegram_link }} </a>
|
| 512 |
-
{% else %}
|
| 513 |
-
<div class="business-card-value">Не указано</div>
|
| 514 |
-
{% endif %}
|
| 515 |
</div>
|
| 516 |
</div>
|
|
|
|
| 517 |
{% else %}
|
| 518 |
-
<p class="no-
|
| 519 |
{% endif %}
|
| 520 |
</section>
|
| 521 |
</div>
|
| 522 |
</div>
|
| 523 |
<div id="invoiceDetailModal" class="modal">
|
| 524 |
<div class="modal-content">
|
| 525 |
-
<
|
| 526 |
<h2 id="invoiceDetailTitle" class="modal-title"></h2>
|
| 527 |
<ul id="invoiceDetailList" class="invoice-detail-list"></ul>
|
| 528 |
<div id="invoiceDetailTotal" class="invoice-total-display">
|
|
@@ -532,17 +539,15 @@ TEMPLATE = """
|
|
| 532 |
</div>
|
| 533 |
</div>
|
| 534 |
{% if is_first_visit %}
|
| 535 |
-
<div id="promoCodeModal" class="modal" style="display: flex;">
|
| 536 |
<div class="modal-content">
|
| 537 |
<h2 class="modal-title">Есть промокод?</h2>
|
| 538 |
-
<
|
| 539 |
-
|
| 540 |
-
|
| 541 |
-
|
| 542 |
-
<
|
| 543 |
-
|
| 544 |
-
<button class="btn-apply-promo" onclick="submitPromoCode(true)">Применить</button>
|
| 545 |
-
</div>
|
| 546 |
</div>
|
| 547 |
</div>
|
| 548 |
</div>
|
|
@@ -552,29 +557,28 @@ TEMPLATE = """
|
|
| 552 |
const currentUserId = '{{ user.id }}';
|
| 553 |
|
| 554 |
function applyTheme(themeParams) {
|
| 555 |
-
|
| 556 |
-
const
|
| 557 |
-
|
| 558 |
-
root.
|
| 559 |
-
root.
|
| 560 |
-
root.
|
| 561 |
-
root.
|
| 562 |
-
root.
|
| 563 |
}
|
| 564 |
|
| 565 |
function setupTelegram() {
|
| 566 |
if (!tg || !tg.initData) {
|
| 567 |
console.error("Telegram WebApp script not loaded or initData is missing.");
|
| 568 |
document.body.style.visibility = 'visible';
|
|
|
|
| 569 |
return;
|
| 570 |
}
|
| 571 |
|
| 572 |
tg.ready();
|
| 573 |
tg.expand();
|
| 574 |
|
| 575 |
-
|
| 576 |
-
applyTheme(tg.themeParams);
|
| 577 |
-
}
|
| 578 |
tg.onEvent('themeChanged', () => applyTheme(tg.themeParams));
|
| 579 |
|
| 580 |
const urlParams = new URLSearchParams(window.location.search);
|
|
@@ -624,7 +628,7 @@ TEMPLATE = """
|
|
| 624 |
function closeModal(modalId) { document.getElementById(modalId).style.display = 'none'; }
|
| 625 |
|
| 626 |
function openInvoiceDetailModal(invoiceData) {
|
| 627 |
-
document.getElementById('invoiceDetailTitle').textContent = `Накладная #${invoiceData.invoice_id}
|
| 628 |
const invoiceDetailList = document.getElementById('invoiceDetailList');
|
| 629 |
invoiceDetailList.innerHTML = '';
|
| 630 |
invoiceData.items.forEach(item => {
|
|
@@ -641,7 +645,7 @@ TEMPLATE = """
|
|
| 641 |
const promoCode = document.getElementById('userPromoCode').textContent;
|
| 642 |
const copyBtn = document.querySelector('.copy-btn');
|
| 643 |
navigator.clipboard.writeText(promoCode).then(() => {
|
| 644 |
-
copyBtn.textContent = '
|
| 645 |
setTimeout(() => { copyBtn.textContent = 'Копировать'; }, 2000);
|
| 646 |
}).catch(err => {
|
| 647 |
console.error('Failed to copy text: ', err);
|
|
@@ -651,7 +655,7 @@ TEMPLATE = """
|
|
| 651 |
async function submitPromoCode(withCode) {
|
| 652 |
const promoCode = withCode ? document.getElementById('promoCodeInput').value.trim() : null;
|
| 653 |
const statusEl = document.getElementById('promoStatus');
|
| 654 |
-
statusEl.style.color = 'var(--
|
| 655 |
statusEl.textContent = 'Проверяем...';
|
| 656 |
|
| 657 |
try {
|
|
@@ -688,6 +692,7 @@ TEMPLATE = """
|
|
| 688 |
setTimeout(() => {
|
| 689 |
if (document.body.style.visibility !== 'visible') {
|
| 690 |
document.body.style.visibility = 'visible';
|
|
|
|
| 691 |
}
|
| 692 |
}, 3000);
|
| 693 |
}
|
|
@@ -796,7 +801,8 @@ ADMIN_TEMPLATE = """
|
|
| 796 |
.invoice-items-table th { background-color: #e9ecef; font-weight: 600; color: var(--admin-text); }
|
| 797 |
.invoice-items-table .total-row td { font-weight: 700; background-color: #f0f0f0; }
|
| 798 |
.invoice-items-table .action-btn { background: none; border: none; color: var(--admin-danger); cursor: pointer; font-size: 1.2em; }
|
| 799 |
-
.invoice-section-summary { padding: 1rem; background-color: #
|
|
|
|
| 800 |
.invoice-list-admin { list-style: none; padding: 0; max-height: 200px; overflow-y: auto; border: 1px solid var(--admin-border); border-radius: 8px; }
|
| 801 |
.invoice-list-admin li { padding: 8px 12px; border-bottom: 1px solid var(--admin-border); display: flex; justify-content: space-between; align-items: center; }
|
| 802 |
.invoice-list-admin li:last-child { border-bottom: none; }
|
|
@@ -809,6 +815,13 @@ ADMIN_TEMPLATE = """
|
|
| 809 |
.form-group-horizontal { display: flex; align-items: center; gap: 10px; }
|
| 810 |
.form-group-horizontal input { flex-grow: 1; }
|
| 811 |
.form-group-horizontal span { font-weight: 500; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 812 |
</style>
|
| 813 |
</head>
|
| 814 |
<body>
|
|
@@ -957,14 +970,25 @@ ADMIN_TEMPLATE = """
|
|
| 957 |
</thead>
|
| 958 |
<tbody></tbody>
|
| 959 |
<tfoot>
|
| 960 |
-
<tr>
|
| 961 |
-
<td colspan="3" style="text-align: right;"><strong>Итоговая
|
| 962 |
<td id="newInvoiceTotalAmount">0.00</td><td></td>
|
| 963 |
</tr>
|
| 964 |
</tfoot>
|
| 965 |
</table>
|
| 966 |
<button class="btn btn-primary" style="margin-top: 1rem;" onclick="addNewInvoiceItemRow()">Добавить товар</button>
|
| 967 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 968 |
<div class="modal-footer">
|
| 969 |
<div id="invoiceStatus" class="status-message"></div>
|
| 970 |
<button class="btn-submit" onclick="submitInvoice()">Сохранить накладную</button>
|
|
@@ -994,7 +1018,7 @@ ADMIN_TEMPLATE = """
|
|
| 994 |
</div>
|
| 995 |
<div class="form-group" style="margin-bottom: 1.5rem;">
|
| 996 |
<label for="newClientPhone">Номер телефона (уникальный)</label>
|
| 997 |
-
<input type="tel" id="newClientPhone" placeholder="+
|
| 998 |
</div>
|
| 999 |
<div class="modal-footer">
|
| 1000 |
<div id="addClientStatus" class="status-message"></div>
|
|
@@ -1015,7 +1039,7 @@ ADMIN_TEMPLATE = """
|
|
| 1015 |
</div>
|
| 1016 |
<div class="form-group">
|
| 1017 |
<label for="orgPhoneNumbers">Номера телефонов (через запятую)</label>
|
| 1018 |
-
<input type="text" id="orgPhoneNumbers" placeholder="+
|
| 1019 |
</div>
|
| 1020 |
<div class="form-group">
|
| 1021 |
<label for="orgAddress">Адрес</label>
|
|
@@ -1023,7 +1047,7 @@ ADMIN_TEMPLATE = """
|
|
| 1023 |
</div>
|
| 1024 |
<div class="form-group">
|
| 1025 |
<label for="orgWhatsAppLink">Ссылка на WhatsApp</label>
|
| 1026 |
-
<input type="url" id="orgWhatsAppLink" placeholder="https://wa.me/
|
| 1027 |
</div>
|
| 1028 |
<div class="form-group">
|
| 1029 |
<label for="orgTelegramLink">Ссылка на Telegram</label>
|
|
@@ -1071,11 +1095,11 @@ ADMIN_TEMPLATE = """
|
|
| 1071 |
<div id="adminInvoiceDetailModal" class="modal">
|
| 1072 |
<div class="modal-content">
|
| 1073 |
<span class="modal-close" onclick="closeModal('adminInvoiceDetailModal')">×</span>
|
| 1074 |
-
<
|
|
|
|
|
|
|
| 1075 |
<ul id="adminInvoiceDetailList" class="invoice-detail-list"></ul>
|
| 1076 |
<div id="adminInvoiceDetailTotal" class="invoice-total-display">
|
| 1077 |
-
<span>Итого:</span>
|
| 1078 |
-
<span id="adminInvoiceTotalAmount">0.00</span>
|
| 1079 |
</div>
|
| 1080 |
</div>
|
| 1081 |
</div>
|
|
@@ -1102,6 +1126,9 @@ ADMIN_TEMPLATE = """
|
|
| 1102 |
document.getElementById('modalUserUsername').textContent = `@${userData.username || userData.phone_number || ''} | ID: ${userData.id}`;
|
| 1103 |
['accrueAmount', 'deductAmount', 'addDebtAmount', 'repayDebtAmount'].forEach(id => document.getElementById(id).value = '');
|
| 1104 |
['modalStatus', 'invoiceStatus'].forEach(id => document.getElementById(id).textContent = '');
|
|
|
|
|
|
|
|
|
|
| 1105 |
newInvoiceItems = [];
|
| 1106 |
renderNewInvoiceItems();
|
| 1107 |
loadUserHistoryAndInvoices();
|
|
@@ -1383,8 +1410,8 @@ ADMIN_TEMPLATE = """
|
|
| 1383 |
const tableBody = document.getElementById('newInvoiceItemsTable').getElementsByTagName('tbody')[0];
|
| 1384 |
const newRow = tableBody.insertRow();
|
| 1385 |
const rowIndex = tableBody.rows.length - 1;
|
| 1386 |
-
newInvoiceItems.push({ product_name: '', quantity:
|
| 1387 |
-
newRow.innerHTML = `<td><input type="text" placeholder="Название товара" oninput="updateInvoiceItem(${rowIndex}, 'product_name', this.value)"></td><td><input type="number" step="1" min="
|
| 1388 |
}
|
| 1389 |
|
| 1390 |
function updateInvoiceItem(index, field, value) {
|
|
@@ -1401,34 +1428,37 @@ ADMIN_TEMPLATE = """
|
|
| 1401 |
}
|
| 1402 |
|
| 1403 |
function removeInvoiceItemRow(button, index) {
|
| 1404 |
-
const tableBody = document.getElementById('newInvoiceItemsTable').getElementsByTagName('tbody')[0];
|
| 1405 |
-
tableBody.deleteRow(button.parentNode.parentNode.rowIndex - 1);
|
| 1406 |
newInvoiceItems.splice(index, 1);
|
| 1407 |
-
|
| 1408 |
-
const row = tableBody.rows[i];
|
| 1409 |
-
row.querySelector('input[type="text"]').setAttribute('oninput', `updateInvoiceItem(${i}, 'product_name', this.value)`);
|
| 1410 |
-
row.querySelector('input[type="number"][step="1"]').setAttribute('oninput', `updateInvoiceItem(${i}, 'quantity', parseFloat(this.value))`);
|
| 1411 |
-
row.querySelector('input[type="number"][step="0.01"]').setAttribute('oninput', `updateInvoiceItem(${i}, 'unit_price', parseFloat(this.value))`);
|
| 1412 |
-
row.querySelector('.action-btn').setAttribute('onclick', `removeInvoiceItemRow(this, ${i})`);
|
| 1413 |
-
}
|
| 1414 |
-
updateNewInvoiceTotal();
|
| 1415 |
-
}
|
| 1416 |
-
|
| 1417 |
-
function updateNewInvoiceTotal() {
|
| 1418 |
-
let total = newInvoiceItems.reduce((sum, item) => sum + (parseFloat(item.item_total) || 0), 0);
|
| 1419 |
-
document.getElementById('newInvoiceTotalAmount').textContent = total.toFixed(2);
|
| 1420 |
}
|
| 1421 |
-
|
| 1422 |
function renderNewInvoiceItems() {
|
| 1423 |
const tableBody = document.getElementById('newInvoiceItemsTable').getElementsByTagName('tbody')[0];
|
| 1424 |
tableBody.innerHTML = '';
|
| 1425 |
newInvoiceItems.forEach((item, index) => {
|
| 1426 |
const newRow = tableBody.insertRow();
|
| 1427 |
-
newRow.innerHTML = `<td><input type="text" placeholder="Название товара" value="${item.product_name}" oninput="updateInvoiceItem(${index}, 'product_name', this.value)"></td><td><input type="number" step="1" min="
|
| 1428 |
});
|
| 1429 |
updateNewInvoiceTotal();
|
| 1430 |
}
|
| 1431 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1432 |
async function submitInvoice() {
|
| 1433 |
const statusEl = document.getElementById('invoiceStatus');
|
| 1434 |
statusEl.style.color = 'var(--admin-secondary)';
|
|
@@ -1445,7 +1475,13 @@ ADMIN_TEMPLATE = """
|
|
| 1445 |
return;
|
| 1446 |
}
|
| 1447 |
const totalAmount = itemsToAdd.reduce((sum, item) => sum + item.item_total, 0);
|
| 1448 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1449 |
try {
|
| 1450 |
const response = await fetch('/admin/add_invoice', {
|
| 1451 |
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload)
|
|
@@ -1472,7 +1508,18 @@ ADMIN_TEMPLATE = """
|
|
| 1472 |
li.innerHTML = `<span class="item-name">${item.product_name}</span><span class="item-qty-price">${item.quantity} x ${parseFloat(item.unit_price).toFixed(2)}</span><span class="item-total">${parseFloat(item.item_total).toFixed(2)}</span>`;
|
| 1473 |
invoiceDetailList.appendChild(li);
|
| 1474 |
});
|
| 1475 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1476 |
adminInvoiceDetailModal.style.display = 'block';
|
| 1477 |
}
|
| 1478 |
|
|
@@ -1545,7 +1592,7 @@ def verify_data():
|
|
| 1545 |
if is_valid:
|
| 1546 |
tg_user_id = user_info_dict.get('id')
|
| 1547 |
if tg_user_id:
|
| 1548 |
-
now = datetime.now(
|
| 1549 |
user_id_to_save = None
|
| 1550 |
|
| 1551 |
with _data_lock:
|
|
@@ -1618,7 +1665,7 @@ def submit_referral():
|
|
| 1618 |
|
| 1619 |
if promo_bonus > 0:
|
| 1620 |
user['bonuses'] = user.get('bonuses', 0) + promo_bonus
|
| 1621 |
-
now = datetime.now(
|
| 1622 |
history_entry = {
|
| 1623 |
"type": "accrual", "amount": promo_bonus, "description": "Бонус за промокод",
|
| 1624 |
"date": now.isoformat(), "date_str": now.strftime('%Y-%m-%d %H:%M:%S')
|
|
@@ -1672,7 +1719,7 @@ def add_client():
|
|
| 1672 |
if any(u.get('phone_number') == phone_number for k, u in visitor_data_cache.items() if k not in ["organization_details", "bonus_program_settings"]):
|
| 1673 |
return jsonify({"status": "error", "message": "Клиент с таким номером телефона уже существует."}), 409
|
| 1674 |
|
| 1675 |
-
now = datetime.now(
|
| 1676 |
new_id = generate_unique_id(visitor_data_cache)
|
| 1677 |
new_client = {
|
| 1678 |
'id': new_id, 'telegram_id': None, 'first_name': first_name, 'last_name': None,
|
|
@@ -1703,7 +1750,7 @@ def add_transaction():
|
|
| 1703 |
with _data_lock:
|
| 1704 |
if user_id not in visitor_data_cache: return jsonify({"status": "error", "message": "User not found"}), 404
|
| 1705 |
user = visitor_data_cache[user_id]
|
| 1706 |
-
now = datetime.now(
|
| 1707 |
now_iso, now_str = now.isoformat(), now.strftime('%Y-%m-%d %H:%M:%S')
|
| 1708 |
if deduct_amount > user.get('bonuses', 0): return jsonify({"status": "error", "message": "Недостаточно бонусов для списания"}), 400
|
| 1709 |
if repay_debt_amount > user.get('debts', 0): return jsonify({"status": "error", "message": "Сумма погашения превышает текущий долг"}), 400
|
|
@@ -1731,20 +1778,37 @@ def add_invoice():
|
|
| 1731 |
user_id = str(data.get('user_id'))
|
| 1732 |
total_amount = float(data.get('total_amount', 0))
|
| 1733 |
items = data.get('items', [])
|
|
|
|
|
|
|
| 1734 |
if not user_id: return jsonify({"status": "error", "message": "User ID is required"}), 400
|
| 1735 |
if not items: return jsonify({"status": "error", "message": "Необходимо добавить товары в накладную."}), 400
|
| 1736 |
|
| 1737 |
with _data_lock:
|
| 1738 |
if user_id not in visitor_data_cache: return jsonify({"status": "error", "message": "User not found"}), 404
|
| 1739 |
user = visitor_data_cache[user_id]
|
| 1740 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1741 |
now_iso, now_str = now.isoformat(), now.strftime('%Y-%m-%d %H:%M:%S')
|
| 1742 |
invoice_id = str(uuid.uuid4().hex[:8]).upper()
|
|
|
|
| 1743 |
processed_items = [{"product_name": item.get('product_name'), "quantity": float(item.get('quantity', 0)), "unit_price": float(item.get('unit_price', 0)), "item_total": round(float(item.get('quantity', 0)) * float(item.get('unit_price', 0)), 2)} for item in items]
|
| 1744 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1745 |
if 'invoices' not in user: user['invoices'] = []
|
| 1746 |
user['invoices'].append(new_invoice)
|
| 1747 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1748 |
bonus_settings = visitor_data_cache.get('bonus_program_settings', {})
|
| 1749 |
invoice_bonus_percentage = float(bonus_settings.get('invoice_bonus_percentage', 0))
|
| 1750 |
if invoice_bonus_percentage > 0 and total_amount > 0:
|
|
|
|
| 26 |
HF_TOKEN_WRITE = os.getenv("HF_TOKEN_WRITE")
|
| 27 |
HF_TOKEN_READ = os.getenv("HF_TOKEN_READ")
|
| 28 |
|
| 29 |
+
ALMATY_TZ = pytz.timezone('Asia/Almaty')
|
| 30 |
|
| 31 |
app = Flask(__name__)
|
| 32 |
logging.basicConfig(level=logging.INFO)
|
|
|
|
| 134 |
repo_id=REPO_ID,
|
| 135 |
repo_type="dataset",
|
| 136 |
token=HF_TOKEN_WRITE,
|
| 137 |
+
commit_message=f"Update bonus data {datetime.now(ALMATY_TZ).strftime('%Y-%m-%d %H:%M:%S')}"
|
| 138 |
)
|
| 139 |
logging.info("Bonus data successfully uploaded to Hugging Face.")
|
| 140 |
except Exception as e:
|
|
|
|
| 192 |
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
| 193 |
<style>
|
| 194 |
:root {
|
| 195 |
+
--tg-theme-bg-color: var(--tg-bg-color, #181818);
|
| 196 |
+
--tg-theme-text-color: var(--tg-text-color, #ffffff);
|
| 197 |
+
--tg-theme-hint-color: var(--tg-hint-color, #aaaaaa);
|
| 198 |
+
--tg-theme-button-color: var(--tg-button-color, #FFC107);
|
| 199 |
+
--tg-theme-button-text-color: var(--tg-button-text-color, #000000);
|
| 200 |
+
--tg-theme-secondary-bg-color: var(--tg-secondary-bg-color, #2c2c2e);
|
| 201 |
--brand-red: #F44336;
|
| 202 |
--brand-green: #4CAF50;
|
| 203 |
--brand-blue: #2196F3;
|
| 204 |
+
--border-radius-l: 24px;
|
| 205 |
+
--border-radius-m: 16px;
|
|
|
|
|
|
|
| 206 |
--padding-m: 16px;
|
|
|
|
| 207 |
--font-family: 'Manrope', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
| 208 |
--shadow-color: rgba(255, 193, 7, 0.15);
|
| 209 |
--shadow-glow: 0 0 35px var(--shadow-color);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 210 |
}
|
| 211 |
* { box-sizing: border-box; margin: 0; padding: 0; }
|
| 212 |
html, body {
|
| 213 |
+
background-color: var(--tg-theme-bg-color);
|
| 214 |
font-family: var(--font-family);
|
| 215 |
+
color: var(--tg-theme-text-color);
|
| 216 |
padding: var(--padding-m);
|
| 217 |
overscroll-behavior-y: none;
|
| 218 |
-webkit-font-smoothing: antialiased;
|
|
|
|
| 220 |
visibility: hidden;
|
| 221 |
min-height: 100vh;
|
| 222 |
}
|
| 223 |
+
.container { max-width: 600px; margin: 0 auto; display: flex; flex-direction: column; gap: calc(var(--padding-m) + 4px); }
|
| 224 |
+
.header { text-align: left; padding: 8px 0; }
|
| 225 |
+
.logo { font-size: 2.5em; font-weight: 800; letter-spacing: -1px; }
|
| 226 |
+
.logo span { color: var(--tg-theme-button-color); }
|
| 227 |
+
.welcome-text { font-size: 1em; color: var(--tg-theme-hint-color); margin-top: 4px; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 228 |
.nav-buttons {
|
| 229 |
+
display: flex; justify-content: space-around; background-color: var(--tg-theme-secondary-bg-color);
|
| 230 |
+
border-radius: 50px; padding: 6px; position: sticky; top: var(--padding-m); z-index: 100;
|
| 231 |
+
backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
|
|
|
|
|
|
|
|
|
|
| 232 |
}
|
| 233 |
.nav-btn {
|
| 234 |
+
flex-grow: 1; padding: 12px 15px; border: none; border-radius: 50px;
|
| 235 |
+
background-color: transparent; color: var(--tg-theme-hint-color);
|
| 236 |
+
font-family: var(--font-family); font-weight: 700; font-size: 1em;
|
| 237 |
+
cursor: pointer; transition: all 0.3s ease;
|
| 238 |
}
|
| 239 |
+
.nav-btn.active { background-color: var(--tg-theme-button-color); color: var(--tg-theme-button-text-color); box-shadow: 0 4px 15px rgba(0,0,0,0.2); }
|
| 240 |
+
.content-section { display: none; flex-direction: column; gap: calc(var(--padding-m) + 4px); }
|
| 241 |
.content-section.active { display: flex; }
|
| 242 |
+
.main-bonus-card {
|
| 243 |
+
background: linear-gradient(45deg, var(--tg-theme-button-color), #ffab00);
|
| 244 |
+
color: var(--tg-theme-button-text-color);
|
| 245 |
+
border-radius: var(--border-radius-l); padding: 24px; text-align: center;
|
| 246 |
+
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
|
| 247 |
}
|
| 248 |
+
.main-bonus-card .card-label { font-size: 1.2em; font-weight: 600; opacity: 0.8; margin-bottom: 8px; }
|
| 249 |
+
.main-bonus-card .bonus-amount { font-size: 3.5em; font-weight: 800; letter-spacing: -2px; line-height: 1; }
|
| 250 |
+
.balances-grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--padding-m); }
|
| 251 |
+
.balance-card {
|
| 252 |
+
background-color: var(--tg-theme-secondary-bg-color);
|
| 253 |
+
border-radius: var(--border-radius-m); padding: 20px; text-align: center;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 254 |
}
|
| 255 |
+
.balance-card .card-label { font-size: 1em; font-weight: 500; color: var(--tg-theme-hint-color); margin-bottom: 10px; }
|
| 256 |
+
.balance-card .amount { font-size: 2em; font-weight: 700; }
|
| 257 |
+
.balance-card .amount.debt { color: var(--brand-red); }
|
| 258 |
+
.balance-card .amount.referral { color: var(--brand-blue); }
|
| 259 |
+
.promo-card, .client-id-card {
|
| 260 |
+
background-color: var(--tg-theme-secondary-bg-color);
|
| 261 |
+
border-radius: var(--border-radius-m); padding: 20px;
|
| 262 |
}
|
| 263 |
+
.promo-card .card-label { font-size: 1em; font-weight: 500; color: var(--tg-theme-hint-color); margin-bottom: 12px; text-align: center; }
|
| 264 |
.promo-code-display {
|
| 265 |
display: flex; align-items: center; justify-content: center; gap: 12px;
|
| 266 |
background-color: rgba(0,0,0,0.2); border-radius: 12px; padding: 12px;
|
| 267 |
}
|
| 268 |
+
.promo-code-value { font-size: 1.5em; font-weight: 700; color: var(--tg-theme-button-color); letter-spacing: 2px; }
|
|
|
|
|
|
|
| 269 |
.copy-btn {
|
| 270 |
+
padding: 10px 18px; font-size: 0.9em; font-weight: 700; color: var(--tg-theme-button-text-color);
|
| 271 |
+
background-color: var(--tg-theme-button-color); border: none; border-radius: 10px; cursor: pointer;
|
| 272 |
+
transition: transform 0.2s, background-color 0.2s;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 273 |
}
|
| 274 |
+
.copy-btn:active { transform: scale(0.95); }
|
| 275 |
+
.client-id-card { display: flex; justify-content: space-between; align-items: center; }
|
| 276 |
+
.client-id-label { font-weight: 500; color: var(--tg-theme-hint-color); }
|
| 277 |
+
.client-id-value {
|
| 278 |
+
font-size: 1.3em; font-weight: 700; color: var(--tg-theme-button-color); letter-spacing: 2px;
|
| 279 |
+
background-color: rgba(0,0,0,0.2); padding: 6px 12px; border-radius: 10px;
|
| 280 |
}
|
| 281 |
+
.section-card { background-color: var(--tg-theme-secondary-bg-color); border-radius: var(--border-radius-m); padding: 20px; }
|
| 282 |
+
.section-title { font-size: 1.4em; font-weight: 700; margin-bottom: 16px; }
|
| 283 |
+
.history-list, .invoices-list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 8px; }
|
| 284 |
.history-item, .invoice-item {
|
| 285 |
+
display: flex; align-items: center; padding: 14px;
|
| 286 |
+
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
| 287 |
}
|
| 288 |
.history-item:last-child, .invoice-item:last-child { border-bottom: none; }
|
| 289 |
+
.history-icon { margin-right: 16px; width: 40px; height: 40px; display: flex; align-items: center; justify-content: center; border-radius: 50%; }
|
| 290 |
+
.history-icon svg { width: 22px; height: 22px; }
|
| 291 |
+
.icon-bonus-accrual { background-color: rgba(76, 175, 80, 0.15); color: var(--brand-green); }
|
| 292 |
+
.icon-bonus-deduction { background-color: rgba(244, 67, 54, 0.15); color: var(--brand-red); }
|
| 293 |
+
.icon-debt-accrual { background-color: rgba(244, 67, 54, 0.15); color: var(--brand-red); }
|
| 294 |
+
.icon-debt-payment { background-color: rgba(76, 175, 80, 0.15); color: var(--brand-green); }
|
| 295 |
+
.icon-referral { background-color: rgba(33, 150, 243, 0.15); color: var(--brand-blue); }
|
| 296 |
+
.history-details { flex-grow: 1; }
|
| 297 |
+
.history-description { font-size: 1em; font-weight: 600; }
|
| 298 |
+
.history-date { font-size: 0.8em; color: var(--tg-theme-hint-color); margin-top: 4px; }
|
| 299 |
+
.history-amount { font-size: 1.1em; font-weight: 700; white-space: nowrap; }
|
| 300 |
+
.history-amount.positive { color: var(--brand-green); }
|
| 301 |
+
.history-amount.negative { color: var(--brand-red); }
|
| 302 |
+
.history-amount.referral { color: var(--brand-blue); }
|
| 303 |
+
.invoice-item { cursor: pointer; transition: background-color 0.2s; border-radius: 12px; }
|
| 304 |
.invoice-item:hover { background-color: rgba(255,255,255,0.05); }
|
| 305 |
+
.invoice-details { flex-grow: 1; }
|
| 306 |
+
.invoice-description, .invoice-date { margin-left: 16px; }
|
| 307 |
+
.invoice-amount { font-size: 1.1em; font-weight: 700; color: var(--tg-theme-button-color); }
|
| 308 |
+
.no-data { text-align: center; color: var(--tg-theme-hint-color); padding: 3rem 0; }
|
| 309 |
+
.business-card-item { margin-bottom: 16px; }
|
| 310 |
+
.business-card-label { font-weight: 500; color: var(--tg-theme-hint-color); margin-bottom: 6px; font-size: 0.9em; }
|
| 311 |
+
.business-card-value { font-size: 1.1em; font-weight: 600; }
|
| 312 |
+
.business-card-value a { color: var(--tg-theme-button-color); text-decoration: none; word-break: break-all; transition: opacity 0.2s; }
|
| 313 |
+
.business-card-value a:hover { opacity: 0.8; }
|
| 314 |
+
.business-card-phone-list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 8px; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 315 |
.business-card-phone-item a {
|
| 316 |
+
display: inline-flex; align-items: center; gap: 10px; color: var(--tg-theme-text-color);
|
| 317 |
+
text-decoration: none; background-color: rgba(255,255,255,0.08); padding: 10px 14px;
|
| 318 |
+
border-radius: 12px; transition: background-color 0.2s;
|
| 319 |
}
|
| 320 |
+
.business-card-phone-item a:hover { background-color: rgba(255,255,255,0.12); }
|
|
|
|
| 321 |
.modal {
|
| 322 |
+
display: none; position: fixed; z-index: 1000; left: 0; top: 0; width: 100%; height: 100%;
|
| 323 |
+
background-color: rgba(0,0,0,0.7); backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
|
| 324 |
+
align-items: flex-end; justify-content: center;
|
|
|
|
| 325 |
}
|
| 326 |
.modal-content {
|
| 327 |
+
background-color: var(--tg-theme-secondary-bg-color); width: 100%;
|
| 328 |
+
border-top-left-radius: var(--border-radius-l); border-top-right-radius: var(--border-radius-l);
|
| 329 |
+
box-shadow: 0 -5px 25px rgba(0,0,0,0.3); padding: var(--padding-m); padding-top: 40px; position: relative;
|
| 330 |
+
max-height: 90vh; overflow-y: auto; animation: slideUp 0.3s ease-out;
|
| 331 |
}
|
| 332 |
+
@keyframes slideUp { from { transform: translateY(100%); } to { transform: translateY(0); } }
|
| 333 |
.modal-close {
|
| 334 |
+
position: absolute; top: 12px; left: 50%; transform: translateX(-50%);
|
| 335 |
+
width: 40px; height: 5px; background-color: var(--tg-theme-hint-color);
|
| 336 |
+
border-radius: 3px; cursor: pointer;
|
| 337 |
}
|
| 338 |
+
.modal-title { font-size: 1.5em; font-weight: 700; margin-bottom: 20px; text-align: center; color: var(--tg-theme-button-color); }
|
| 339 |
+
.invoice-detail-list { list-style: none; padding: 0; margin: 0; }
|
|
|
|
| 340 |
.invoice-detail-item {
|
| 341 |
+
display: flex; justify-content: space-between; padding: 12px 0; border-bottom: 1px dashed rgba(255,255,255,0.1);
|
|
|
|
| 342 |
}
|
|
|
|
| 343 |
.item-name { font-weight: 500; flex-basis: 60%; }
|
| 344 |
+
.item-qty-price { font-size: 0.9em; color: var(--tg-theme-hint-color); flex-basis: 20%; text-align: right; }
|
| 345 |
+
.item-total { font-weight: 700; flex-basis: 20%; text-align: right; color: var(--tg-theme-button-color); }
|
| 346 |
.invoice-total-display {
|
| 347 |
padding-top: var(--padding-m); border-top: 1px solid rgba(255,255,255,0.2); margin-top: var(--padding-m);
|
| 348 |
display: flex; justify-content: space-between; font-size: 1.2em; font-weight: 700;
|
| 349 |
}
|
| 350 |
+
#promoCodeModal .modal-content { align-items: center; text-align: center; padding: 40px 24px 24px; }
|
| 351 |
+
#promoCodeModal h2 { margin-bottom: 12px; }
|
| 352 |
+
#promoCodeModal p { color: var(--tg-theme-hint-color); margin-bottom: 20px; }
|
| 353 |
+
#promoCodeModal input {
|
| 354 |
+
width: 100%; padding: 16px; margin-bottom: 16px; font-size: 1.2em;
|
| 355 |
+
background-color: var(--tg-theme-bg-color); border: 1px solid rgba(255,255,255,0.1);
|
| 356 |
+
border-radius: var(--border-radius-m); color: var(--tg-theme-text-color);
|
| 357 |
+
text-align: center; letter-spacing: 2px;
|
| 358 |
}
|
| 359 |
+
#promoCodeModal .promo-modal-actions { display: flex; gap: 1rem; width: 100%; }
|
| 360 |
+
#promoCodeModal button {
|
| 361 |
+
flex-grow: 1; padding: 16px; font-size: 1em; font-weight: 700; border: none;
|
| 362 |
+
border-radius: var(--border-radius-m); cursor: pointer; transition: all 0.2s;
|
| 363 |
}
|
| 364 |
+
.btn-apply-promo { background-color: var(--tg-theme-button-color); color: var(--tg-theme-button-text-color); }
|
| 365 |
+
.btn-skip-promo { background-color: var(--tg-theme-secondary-bg-color); color: var(--tg-theme-text-color); }
|
| 366 |
#promoStatus { margin-top: 1rem; font-weight: 500; min-height: 20px; }
|
| 367 |
</style>
|
| 368 |
</head>
|
|
|
|
| 378 |
<button class="nav-btn" data-target="business-card-section">Визитка</button>
|
| 379 |
</nav>
|
| 380 |
<div id="dashboard-section" class="content-section active">
|
| 381 |
+
<section class="main-bonus-card">
|
| 382 |
+
<p class="card-label">Ваши бонусы</p>
|
| 383 |
+
<p class="bonus-amount">{{ "%.2f"|format(user.bonuses|float) }}</p>
|
| 384 |
+
</section>
|
| 385 |
+
<section class="balances-grid">
|
| 386 |
+
<div class="balance-card">
|
| 387 |
<p class="card-label">Ваш долг</p>
|
| 388 |
+
<p class="amount debt">{{ "%.2f"|format(user.debts|float) }}</p>
|
| 389 |
</div>
|
| 390 |
+
<div class="balance-card">
|
| 391 |
<p class="card-label">Бонусы с друзей</p>
|
| 392 |
+
<p class="amount referral">{{ "%.2f"|format(user.referral_bonuses|float) }}</p>
|
| 393 |
</div>
|
| 394 |
+
</section>
|
| 395 |
+
<section class="promo-card">
|
| 396 |
+
<p class="card-label">Ваш промокод для друзей</p>
|
| 397 |
+
<div class="promo-code-display">
|
| 398 |
+
<span class="promo-code-value" id="userPromoCode">{{ user.referral_code }}</span>
|
| 399 |
+
<button class="copy-btn" onclick="copyPromoCode()">Копировать</button>
|
| 400 |
</div>
|
| 401 |
</section>
|
|
|
|
| 402 |
<section class="client-id-card">
|
| 403 |
<p class="client-id-label">Ваш ID клиента</p>
|
| 404 |
<p class="client-id-value">{{ user.id }}</p>
|
| 405 |
</section>
|
| 406 |
+
<section class="section-card">
|
| 407 |
+
<h2 class="section-title">История операций</h2>
|
| 408 |
{% if user.combined_history %}
|
| 409 |
<ul class="history-list">
|
| 410 |
{% for item in user.combined_history %}
|
| 411 |
<li class="history-item">
|
|
|
|
|
|
|
|
|
|
|
|
|
| 412 |
{% if item.transaction_type == 'bonus' %}
|
| 413 |
+
<div class="history-icon {{ 'icon-bonus-accrual' if item.type == 'accrual' else 'icon-bonus-deduction' }}">
|
| 414 |
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-1-13h2v6h-2zm0 8h2v2h-2z"/></svg>
|
| 415 |
+
</div>
|
| 416 |
+
<div class="history-details">
|
| 417 |
+
<span class="history-description">{{ item.description }}</span>
|
| 418 |
+
<span class="history-date">{{ item.date_str }}</span>
|
| 419 |
+
</div>
|
| 420 |
+
<span class="history-amount {{ 'positive' if item.type == 'accrual' else 'negative' }}">
|
| 421 |
{{ '+' if item.type == 'accrual' else '-' }}{{ "%.2f"|format(item.amount|float) }}
|
| 422 |
</span>
|
| 423 |
{% elif item.transaction_type == 'debt' %}
|
| 424 |
+
<div class="history-icon {{ 'icon-debt-payment' if item.type == 'payment' else 'icon-debt-accrual' }}">
|
| 425 |
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8zm-1-5h2v2h-2zm0-8h2v6h-2z"/></svg>
|
| 426 |
+
</div>
|
| 427 |
+
<div class="history-details">
|
| 428 |
+
<span class="history-description">{{ item.description }}</span>
|
| 429 |
+
<span class="history-date">{{ item.date_str }}</span>
|
| 430 |
+
</div>
|
| 431 |
+
<span class="history-amount {{ 'positive' if item.type == 'payment' else 'negative' }}">
|
| 432 |
{{ '-' if item.type == 'payment' else '+' }}{{ "%.2f"|format(item.amount|float) }}
|
| 433 |
</span>
|
| 434 |
{% elif item.transaction_type == 'referral' %}
|
| 435 |
+
<div class="history-icon icon-referral">
|
| 436 |
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M16 11c1.66 0 2.99-1.34 2.99-3S17.66 5 16 5c-1.66 0-3 1.34-3 3s1.34 3 3 3zm-8 0c1.66 0 2.99-1.34 2.99-3S9.66 5 8 5C6.34 5 5 6.34 5 8s1.34 3 3 3zm0 2c-2.33 0-7 1.17-7 3.5V19h14v-2.5c0-2.33-4.67-3.5-7-3.5zm8 0c-.29 0-.62.02-.97.05 1.16.84 1.97 1.97 1.97 3.45V19h6v-2.5c0-2.33-4.67-3.5-7-3.5z"/></svg>
|
| 437 |
+
</div>
|
| 438 |
+
<div class="history-details">
|
| 439 |
+
<span class="history-description">{{ item.description }}</span>
|
| 440 |
+
<span class="history-date">{{ item.date_str }}</span>
|
| 441 |
+
</div>
|
| 442 |
+
<span class="history-amount referral">
|
| 443 |
+{{ "%.2f"|format(item.amount|float) }}
|
| 444 |
</span>
|
| 445 |
{% endif %}
|
|
|
|
| 447 |
{% endfor %}
|
| 448 |
</ul>
|
| 449 |
{% else %}
|
| 450 |
+
<p class="no-data">Операций пока не было.</p>
|
| 451 |
{% endif %}
|
| 452 |
</section>
|
| 453 |
</div>
|
| 454 |
<div id="invoices-section" class="content-section">
|
| 455 |
+
<section class="section-card">
|
| 456 |
+
<h2 class="section-title">Мои накладные</h2>
|
| 457 |
{% if user.invoices %}
|
| 458 |
<ul class="invoices-list">
|
| 459 |
{% for invoice in user.invoices|sort(attribute='date', reverse=true) %}
|
| 460 |
<li class="invoice-item" onclick='openInvoiceDetailModal({{ invoice|tojson }})'>
|
| 461 |
+
<div class="history-icon icon-bonus-accrual" style="background-color: rgba(255, 193, 7, 0.15); color: var(--tg-theme-button-color);">
|
| 462 |
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M14 2H6c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zm2 16H8v-2h8v2zm0-4H8v-2h8v2zm-3-5V3.5L18.5 9H13z"/></svg>
|
| 463 |
+
</div>
|
| 464 |
<div class="invoice-details">
|
| 465 |
<span class="invoice-description">Накладная #{{ invoice.invoice_id }}</span>
|
| 466 |
<span class="invoice-date">{{ invoice.date_str }}</span>
|
|
|
|
| 470 |
{% endfor %}
|
| 471 |
</ul>
|
| 472 |
{% else %}
|
| 473 |
+
<p class="no-data">Накладных пока нет.</p>
|
| 474 |
{% endif %}
|
| 475 |
</section>
|
| 476 |
</div>
|
| 477 |
<div id="business-card-section" class="content-section">
|
| 478 |
+
<section class="section-card">
|
| 479 |
+
<h2 class="section-title">Визитка организации</h2>
|
| 480 |
+
{% if org_details and (org_details.name or org_details.phone_numbers) %}
|
| 481 |
+
{% if org_details.name %}
|
| 482 |
<div class="business-card-item">
|
| 483 |
<div class="business-card-label">Название организации</div>
|
| 484 |
+
<div class="business-card-value">{{ org_details.name }}</div>
|
| 485 |
</div>
|
| 486 |
+
{% endif %}
|
| 487 |
+
{% if org_details.phone_numbers %}
|
| 488 |
<div class="business-card-item">
|
| 489 |
<div class="business-card-label">Номера телефонов</div>
|
| 490 |
+
<ul class="business-card-phone-list">
|
| 491 |
+
{% for phone in org_details.phone_numbers %}
|
| 492 |
+
<li class="business-card-phone-item">
|
| 493 |
+
<a href="tel:{{ phone.replace(' ', '').replace('-', '') }}">
|
| 494 |
+
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="currentColor"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M6.54 5c.06.89.21 1.76.45 2.59l-1.2 1.2c-.41-1.2-.67-2.47-.76-3.79h1.51m10.92 0h1.51c-.09 1.32-.35 2.59-.76 3.79l-1.2-1.2c.24-.83.39-1.7.45-2.59M7.5 3H4c-.55 0-1 .45-1 1 0 9.39 7.61 17 17 17 .55 0 1-.45 1-1v-3.49c0-.55-.45-1-1-1-1.24 0-2.45-.2-3.57-.57-.1-.04-.21-.05-.31-.05-.26 0-.51.1-.71.29l-2.2 2.2c-2.83-1.44-5.15-3.75-6.59-6.59l2.2-2.2c.28-.28.36-.67.25-1.02C8.7 6.45 8.5 5.25 8.5 4c0-.55-.45-1-1-1z"/></svg>
|
| 495 |
+
{{ phone }}
|
| 496 |
+
</a>
|
| 497 |
+
</li>
|
| 498 |
+
{% endfor %}
|
| 499 |
+
</ul>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 500 |
</div>
|
| 501 |
+
{% endif %}
|
| 502 |
+
{% if org_details.address %}
|
| 503 |
<div class="business-card-item">
|
| 504 |
<div class="business-card-label">Адрес</div>
|
| 505 |
+
<div class="business-card-value">{{ org_details.address }}</div>
|
| 506 |
</div>
|
| 507 |
+
{% endif %}
|
| 508 |
+
{% if org_details.whatsapp_link %}
|
| 509 |
<div class="business-card-item">
|
| 510 |
<div class="business-card-label">WhatsApp</div>
|
| 511 |
<div class="business-card-value">
|
| 512 |
+
<a href="{{ org_details.whatsapp_link }}" target="_blank">Написать в WhatsApp</a>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 513 |
</div>
|
| 514 |
</div>
|
| 515 |
+
{% endif %}
|
| 516 |
+
{% if org_details.telegram_link %}
|
| 517 |
<div class="business-card-item">
|
| 518 |
<div class="business-card-label">Telegram</div>
|
| 519 |
<div class="business-card-value">
|
| 520 |
+
<a href="{{ org_details.telegram_link }}" target="_blank">Написать в Telegram</a>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 521 |
</div>
|
| 522 |
</div>
|
| 523 |
+
{% endif %}
|
| 524 |
{% else %}
|
| 525 |
+
<p class="no-data">Данные организации не указаны.</p>
|
| 526 |
{% endif %}
|
| 527 |
</section>
|
| 528 |
</div>
|
| 529 |
</div>
|
| 530 |
<div id="invoiceDetailModal" class="modal">
|
| 531 |
<div class="modal-content">
|
| 532 |
+
<div class="modal-close" onclick="closeModal('invoiceDetailModal')"></div>
|
| 533 |
<h2 id="invoiceDetailTitle" class="modal-title"></h2>
|
| 534 |
<ul id="invoiceDetailList" class="invoice-detail-list"></ul>
|
| 535 |
<div id="invoiceDetailTotal" class="invoice-total-display">
|
|
|
|
| 539 |
</div>
|
| 540 |
</div>
|
| 541 |
{% if is_first_visit %}
|
| 542 |
+
<div id="promoCodeModal" class="modal" style="display: flex; align-items: center;">
|
| 543 |
<div class="modal-content">
|
| 544 |
<h2 class="modal-title">Есть промокод?</h2>
|
| 545 |
+
<p>Если у вас есть промокод от друга, введите его, чтобы получить бонус.</p>
|
| 546 |
+
<input type="text" id="promoCodeInput" placeholder="PROMO123">
|
| 547 |
+
<div id="promoStatus"></div>
|
| 548 |
+
<div class="promo-modal-actions">
|
| 549 |
+
<button class="btn-skip-promo" onclick="submitPromoCode(false)">Нет, спасибо</button>
|
| 550 |
+
<button class="btn-apply-promo" onclick="submitPromoCode(true)">Применить</button>
|
|
|
|
|
|
|
| 551 |
</div>
|
| 552 |
</div>
|
| 553 |
</div>
|
|
|
|
| 557 |
const currentUserId = '{{ user.id }}';
|
| 558 |
|
| 559 |
function applyTheme(themeParams) {
|
| 560 |
+
if (!themeParams) return;
|
| 561 |
+
const root = document.documentElement.style;
|
| 562 |
+
root.setProperty('--tg-bg-color', themeParams.bg_color || '#181818');
|
| 563 |
+
root.setProperty('--tg-text-color', themeParams.text_color || '#ffffff');
|
| 564 |
+
root.setProperty('--tg-hint-color', themeParams.hint_color || '#aaaaaa');
|
| 565 |
+
root.setProperty('--tg-button-color', themeParams.button_color || '#FFC107');
|
| 566 |
+
root.setProperty('--tg-button-text-color', themeParams.button_text_color || '#000000');
|
| 567 |
+
root.setProperty('--tg-secondary-bg-color', themeParams.secondary_bg_color || '#2c2c2e');
|
| 568 |
}
|
| 569 |
|
| 570 |
function setupTelegram() {
|
| 571 |
if (!tg || !tg.initData) {
|
| 572 |
console.error("Telegram WebApp script not loaded or initData is missing.");
|
| 573 |
document.body.style.visibility = 'visible';
|
| 574 |
+
applyTheme({});
|
| 575 |
return;
|
| 576 |
}
|
| 577 |
|
| 578 |
tg.ready();
|
| 579 |
tg.expand();
|
| 580 |
|
| 581 |
+
applyTheme(tg.themeParams);
|
|
|
|
|
|
|
| 582 |
tg.onEvent('themeChanged', () => applyTheme(tg.themeParams));
|
| 583 |
|
| 584 |
const urlParams = new URLSearchParams(window.location.search);
|
|
|
|
| 628 |
function closeModal(modalId) { document.getElementById(modalId).style.display = 'none'; }
|
| 629 |
|
| 630 |
function openInvoiceDetailModal(invoiceData) {
|
| 631 |
+
document.getElementById('invoiceDetailTitle').textContent = `Накладная #${invoiceData.invoice_id}`;
|
| 632 |
const invoiceDetailList = document.getElementById('invoiceDetailList');
|
| 633 |
invoiceDetailList.innerHTML = '';
|
| 634 |
invoiceData.items.forEach(item => {
|
|
|
|
| 645 |
const promoCode = document.getElementById('userPromoCode').textContent;
|
| 646 |
const copyBtn = document.querySelector('.copy-btn');
|
| 647 |
navigator.clipboard.writeText(promoCode).then(() => {
|
| 648 |
+
copyBtn.textContent = 'Готово!';
|
| 649 |
setTimeout(() => { copyBtn.textContent = 'Копировать'; }, 2000);
|
| 650 |
}).catch(err => {
|
| 651 |
console.error('Failed to copy text: ', err);
|
|
|
|
| 655 |
async function submitPromoCode(withCode) {
|
| 656 |
const promoCode = withCode ? document.getElementById('promoCodeInput').value.trim() : null;
|
| 657 |
const statusEl = document.getElementById('promoStatus');
|
| 658 |
+
statusEl.style.color = 'var(--tg-theme-hint-color)';
|
| 659 |
statusEl.textContent = 'Проверяем...';
|
| 660 |
|
| 661 |
try {
|
|
|
|
| 692 |
setTimeout(() => {
|
| 693 |
if (document.body.style.visibility !== 'visible') {
|
| 694 |
document.body.style.visibility = 'visible';
|
| 695 |
+
applyTheme({});
|
| 696 |
}
|
| 697 |
}, 3000);
|
| 698 |
}
|
|
|
|
| 801 |
.invoice-items-table th { background-color: #e9ecef; font-weight: 600; color: var(--admin-text); }
|
| 802 |
.invoice-items-table .total-row td { font-weight: 700; background-color: #f0f0f0; }
|
| 803 |
.invoice-items-table .action-btn { background: none; border: none; color: var(--admin-danger); cursor: pointer; font-size: 1.2em; }
|
| 804 |
+
.invoice-section-summary { padding: 1rem; background-color: #e9ecef; border-radius: 8px; margin-top: 1rem; font-weight: 600; font-size: 1.1em; text-align: right; }
|
| 805 |
+
.invoice-section-summary span { color: var(--admin-success); }
|
| 806 |
.invoice-list-admin { list-style: none; padding: 0; max-height: 200px; overflow-y: auto; border: 1px solid var(--admin-border); border-radius: 8px; }
|
| 807 |
.invoice-list-admin li { padding: 8px 12px; border-bottom: 1px solid var(--admin-border); display: flex; justify-content: space-between; align-items: center; }
|
| 808 |
.invoice-list-admin li:last-child { border-bottom: none; }
|
|
|
|
| 815 |
.form-group-horizontal { display: flex; align-items: center; gap: 10px; }
|
| 816 |
.form-group-horizontal input { flex-grow: 1; }
|
| 817 |
.form-group-horizontal span { font-weight: 500; }
|
| 818 |
+
.invoice-detail-list { list-style: none; padding: 0; }
|
| 819 |
+
.invoice-detail-item { display: flex; justify-content: space-between; padding: 10px 0; border-bottom: 1px dashed #ccc; }
|
| 820 |
+
.invoice-detail-item:last-child { border-bottom: none; }
|
| 821 |
+
.item-name { flex-basis: 60%; }
|
| 822 |
+
.item-qty-price { flex-basis: 20%; text-align: right; color: #6c757d; }
|
| 823 |
+
.item-total { flex-basis: 20%; text-align: right; font-weight: bold; }
|
| 824 |
+
.invoice-total-display { margin-top: 1rem; padding-top: 1rem; border-top: 1px solid #dee2e6; display: flex; justify-content: space-between; font-size: 1.2em; font-weight: bold; }
|
| 825 |
</style>
|
| 826 |
</head>
|
| 827 |
<body>
|
|
|
|
| 970 |
</thead>
|
| 971 |
<tbody></tbody>
|
| 972 |
<tfoot>
|
| 973 |
+
<tr class="total-row">
|
| 974 |
+
<td colspan="3" style="text-align: right;"><strong>Итоговая сумма:</strong></td>
|
| 975 |
<td id="newInvoiceTotalAmount">0.00</td><td></td>
|
| 976 |
</tr>
|
| 977 |
</tfoot>
|
| 978 |
</table>
|
| 979 |
<button class="btn btn-primary" style="margin-top: 1rem;" onclick="addNewInvoiceItemRow()">Добавить товар</button>
|
| 980 |
</div>
|
| 981 |
+
<div class="form-section">
|
| 982 |
+
<h3>Оплата бонусами</h3>
|
| 983 |
+
<p>Доступно бонусов для списания: <strong id="invoiceAvailableBonuses">0.00</strong></p>
|
| 984 |
+
<div class="form-group">
|
| 985 |
+
<label for="invoiceDeductBonuses">Списать бонусов (не более суммы накладной)</label>
|
| 986 |
+
<input type="number" id="invoiceDeductBonuses" oninput="updateNewInvoiceTotal()" placeholder="0.00" step="0.01">
|
| 987 |
+
</div>
|
| 988 |
+
<div class="invoice-section-summary">
|
| 989 |
+
К оплате: <span id="invoiceFinalAmount">0.00</span>
|
| 990 |
+
</div>
|
| 991 |
+
</div>
|
| 992 |
<div class="modal-footer">
|
| 993 |
<div id="invoiceStatus" class="status-message"></div>
|
| 994 |
<button class="btn-submit" onclick="submitInvoice()">Сохранить накладную</button>
|
|
|
|
| 1018 |
</div>
|
| 1019 |
<div class="form-group" style="margin-bottom: 1.5rem;">
|
| 1020 |
<label for="newClientPhone">Номер телефона (уникальный)</label>
|
| 1021 |
+
<input type="tel" id="newClientPhone" placeholder="+77001234567">
|
| 1022 |
</div>
|
| 1023 |
<div class="modal-footer">
|
| 1024 |
<div id="addClientStatus" class="status-message"></div>
|
|
|
|
| 1039 |
</div>
|
| 1040 |
<div class="form-group">
|
| 1041 |
<label for="orgPhoneNumbers">Номера телефонов (через запятую)</label>
|
| 1042 |
+
<input type="text" id="orgPhoneNumbers" placeholder="+77001112233,+77004445566">
|
| 1043 |
</div>
|
| 1044 |
<div class="form-group">
|
| 1045 |
<label for="orgAddress">Адрес</label>
|
|
|
|
| 1047 |
</div>
|
| 1048 |
<div class="form-group">
|
| 1049 |
<label for="orgWhatsAppLink">Ссылка на WhatsApp</label>
|
| 1050 |
+
<input type="url" id="orgWhatsAppLink" placeholder="https://wa.me/77001112233">
|
| 1051 |
</div>
|
| 1052 |
<div class="form-group">
|
| 1053 |
<label for="orgTelegramLink">Ссылка на Telegram</label>
|
|
|
|
| 1095 |
<div id="adminInvoiceDetailModal" class="modal">
|
| 1096 |
<div class="modal-content">
|
| 1097 |
<span class="modal-close" onclick="closeModal('adminInvoiceDetailModal')">×</span>
|
| 1098 |
+
<div class="modal-header">
|
| 1099 |
+
<h2 id="adminInvoiceDetailTitle"></h2>
|
| 1100 |
+
</div>
|
| 1101 |
<ul id="adminInvoiceDetailList" class="invoice-detail-list"></ul>
|
| 1102 |
<div id="adminInvoiceDetailTotal" class="invoice-total-display">
|
|
|
|
|
|
|
| 1103 |
</div>
|
| 1104 |
</div>
|
| 1105 |
</div>
|
|
|
|
| 1126 |
document.getElementById('modalUserUsername').textContent = `@${userData.username || userData.phone_number || ''} | ID: ${userData.id}`;
|
| 1127 |
['accrueAmount', 'deductAmount', 'addDebtAmount', 'repayDebtAmount'].forEach(id => document.getElementById(id).value = '');
|
| 1128 |
['modalStatus', 'invoiceStatus'].forEach(id => document.getElementById(id).textContent = '');
|
| 1129 |
+
document.getElementById('invoiceDeductBonuses').value = '';
|
| 1130 |
+
document.getElementById('invoiceAvailableBonuses').textContent = (parseFloat(userData.bonuses) || 0).toFixed(2);
|
| 1131 |
+
|
| 1132 |
newInvoiceItems = [];
|
| 1133 |
renderNewInvoiceItems();
|
| 1134 |
loadUserHistoryAndInvoices();
|
|
|
|
| 1410 |
const tableBody = document.getElementById('newInvoiceItemsTable').getElementsByTagName('tbody')[0];
|
| 1411 |
const newRow = tableBody.insertRow();
|
| 1412 |
const rowIndex = tableBody.rows.length - 1;
|
| 1413 |
+
newInvoiceItems.push({ product_name: '', quantity: 1, unit_price: 0, item_total: 0 });
|
| 1414 |
+
newRow.innerHTML = `<td><input type="text" placeholder="Название товара" oninput="updateInvoiceItem(${rowIndex}, 'product_name', this.value)"></td><td><input type="number" step="1" min="1" value="1" placeholder="1" oninput="updateInvoiceItem(${rowIndex}, 'quantity', parseFloat(this.value))"></td><td><input type="number" step="0.01" min="0" placeholder="0.00" oninput="updateInvoiceItem(${rowIndex}, 'unit_price', parseFloat(this.value))"></td><td class="item-total-display">0.00</td><td><button class="action-btn" onclick="removeInvoiceItemRow(this, ${rowIndex})">🗑️</button></td>`;
|
| 1415 |
}
|
| 1416 |
|
| 1417 |
function updateInvoiceItem(index, field, value) {
|
|
|
|
| 1428 |
}
|
| 1429 |
|
| 1430 |
function removeInvoiceItemRow(button, index) {
|
|
|
|
|
|
|
| 1431 |
newInvoiceItems.splice(index, 1);
|
| 1432 |
+
renderNewInvoiceItems();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1433 |
}
|
| 1434 |
+
|
| 1435 |
function renderNewInvoiceItems() {
|
| 1436 |
const tableBody = document.getElementById('newInvoiceItemsTable').getElementsByTagName('tbody')[0];
|
| 1437 |
tableBody.innerHTML = '';
|
| 1438 |
newInvoiceItems.forEach((item, index) => {
|
| 1439 |
const newRow = tableBody.insertRow();
|
| 1440 |
+
newRow.innerHTML = `<td><input type="text" placeholder="Название товара" value="${item.product_name}" oninput="updateInvoiceItem(${index}, 'product_name', this.value)"></td><td><input type="number" step="1" min="1" placeholder="1" value="${item.quantity || '1'}" oninput="updateInvoiceItem(${index}, 'quantity', parseFloat(this.value))"></td><td><input type="number" step="0.01" min="0" placeholder="0.00" value="${item.unit_price || ''}" oninput="updateInvoiceItem(${index}, 'unit_price', parseFloat(this.value))"></td><td class="item-total-display">${(item.item_total || 0).toFixed(2)}</td><td><button class="action-btn" onclick="removeInvoiceItemRow(this, ${index})">🗑️</button></td>`;
|
| 1441 |
});
|
| 1442 |
updateNewInvoiceTotal();
|
| 1443 |
}
|
| 1444 |
|
| 1445 |
+
function updateNewInvoiceTotal() {
|
| 1446 |
+
let total = newInvoiceItems.reduce((sum, item) => sum + (parseFloat(item.item_total) || 0), 0);
|
| 1447 |
+
document.getElementById('newInvoiceTotalAmount').textContent = total.toFixed(2);
|
| 1448 |
+
|
| 1449 |
+
const availableBonuses = parseFloat(currentUserData.bonuses) || 0;
|
| 1450 |
+
const deductBonusesInput = document.getElementById('invoiceDeductBonuses');
|
| 1451 |
+
let deductAmount = parseFloat(deductBonusesInput.value) || 0;
|
| 1452 |
+
|
| 1453 |
+
let cappedDeductAmount = Math.max(0, Math.min(deductAmount, availableBonuses, total));
|
| 1454 |
+
if (deductAmount !== cappedDeductAmount) {
|
| 1455 |
+
deductBonusesInput.value = cappedDeductAmount > 0 ? cappedDeductAmount.toFixed(2) : '';
|
| 1456 |
+
}
|
| 1457 |
+
|
| 1458 |
+
let finalAmount = total - cappedDeductAmount;
|
| 1459 |
+
document.getElementById('invoiceFinalAmount').textContent = finalAmount.toFixed(2);
|
| 1460 |
+
}
|
| 1461 |
+
|
| 1462 |
async function submitInvoice() {
|
| 1463 |
const statusEl = document.getElementById('invoiceStatus');
|
| 1464 |
statusEl.style.color = 'var(--admin-secondary)';
|
|
|
|
| 1475 |
return;
|
| 1476 |
}
|
| 1477 |
const totalAmount = itemsToAdd.reduce((sum, item) => sum + item.item_total, 0);
|
| 1478 |
+
const deductBonuses = parseFloat(document.getElementById('invoiceDeductBonuses').value) || 0;
|
| 1479 |
+
const payload = {
|
| 1480 |
+
user_id: currentUserData.id,
|
| 1481 |
+
total_amount: totalAmount,
|
| 1482 |
+
items: itemsToAdd,
|
| 1483 |
+
deduct_bonuses: deductBonuses
|
| 1484 |
+
};
|
| 1485 |
try {
|
| 1486 |
const response = await fetch('/admin/add_invoice', {
|
| 1487 |
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload)
|
|
|
|
| 1508 |
li.innerHTML = `<span class="item-name">${item.product_name}</span><span class="item-qty-price">${item.quantity} x ${parseFloat(item.unit_price).toFixed(2)}</span><span class="item-total">${parseFloat(item.item_total).toFixed(2)}</span>`;
|
| 1509 |
invoiceDetailList.appendChild(li);
|
| 1510 |
});
|
| 1511 |
+
|
| 1512 |
+
const totalDisplay = document.getElementById('adminInvoiceDetailTotal');
|
| 1513 |
+
let bonusesDeducted = parseFloat(invoiceData.bonuses_deducted || 0);
|
| 1514 |
+
let totalAmount = parseFloat(invoiceData.total_amount);
|
| 1515 |
+
let finalAmount = totalAmount - bonusesDeducted;
|
| 1516 |
+
|
| 1517 |
+
let totalHTML = `<span>Итого:</span><span>${totalAmount.toFixed(2)}</span>`;
|
| 1518 |
+
if (bonusesDeducted > 0) {
|
| 1519 |
+
totalHTML += `<br><span>Списано бонусов:</span><span style="color: var(--admin-danger);">- ${bonusesDeducted.toFixed(2)}</span>`;
|
| 1520 |
+
totalHTML += `<hr style="border: none; border-top: 1px solid #ccc; margin: 5px 0;"><span>К оплате:</span><span style="color: var(--admin-success);">${finalAmount.toFixed(2)}</span>`;
|
| 1521 |
+
}
|
| 1522 |
+
totalDisplay.innerHTML = totalHTML;
|
| 1523 |
adminInvoiceDetailModal.style.display = 'block';
|
| 1524 |
}
|
| 1525 |
|
|
|
|
| 1592 |
if is_valid:
|
| 1593 |
tg_user_id = user_info_dict.get('id')
|
| 1594 |
if tg_user_id:
|
| 1595 |
+
now = datetime.now(ALMATY_TZ)
|
| 1596 |
user_id_to_save = None
|
| 1597 |
|
| 1598 |
with _data_lock:
|
|
|
|
| 1665 |
|
| 1666 |
if promo_bonus > 0:
|
| 1667 |
user['bonuses'] = user.get('bonuses', 0) + promo_bonus
|
| 1668 |
+
now = datetime.now(ALMATY_TZ)
|
| 1669 |
history_entry = {
|
| 1670 |
"type": "accrual", "amount": promo_bonus, "description": "Бонус за промокод",
|
| 1671 |
"date": now.isoformat(), "date_str": now.strftime('%Y-%m-%d %H:%M:%S')
|
|
|
|
| 1719 |
if any(u.get('phone_number') == phone_number for k, u in visitor_data_cache.items() if k not in ["organization_details", "bonus_program_settings"]):
|
| 1720 |
return jsonify({"status": "error", "message": "Клиент с таким номером телефона уже существует."}), 409
|
| 1721 |
|
| 1722 |
+
now = datetime.now(ALMATY_TZ)
|
| 1723 |
new_id = generate_unique_id(visitor_data_cache)
|
| 1724 |
new_client = {
|
| 1725 |
'id': new_id, 'telegram_id': None, 'first_name': first_name, 'last_name': None,
|
|
|
|
| 1750 |
with _data_lock:
|
| 1751 |
if user_id not in visitor_data_cache: return jsonify({"status": "error", "message": "User not found"}), 404
|
| 1752 |
user = visitor_data_cache[user_id]
|
| 1753 |
+
now = datetime.now(ALMATY_TZ)
|
| 1754 |
now_iso, now_str = now.isoformat(), now.strftime('%Y-%m-%d %H:%M:%S')
|
| 1755 |
if deduct_amount > user.get('bonuses', 0): return jsonify({"status": "error", "message": "Недостаточно бонусов для списания"}), 400
|
| 1756 |
if repay_debt_amount > user.get('debts', 0): return jsonify({"status": "error", "message": "Сумма погашения превышает текущий долг"}), 400
|
|
|
|
| 1778 |
user_id = str(data.get('user_id'))
|
| 1779 |
total_amount = float(data.get('total_amount', 0))
|
| 1780 |
items = data.get('items', [])
|
| 1781 |
+
deduct_bonuses = float(data.get('deduct_bonuses', 0))
|
| 1782 |
+
|
| 1783 |
if not user_id: return jsonify({"status": "error", "message": "User ID is required"}), 400
|
| 1784 |
if not items: return jsonify({"status": "error", "message": "Необходимо добавить товары в накладную."}), 400
|
| 1785 |
|
| 1786 |
with _data_lock:
|
| 1787 |
if user_id not in visitor_data_cache: return jsonify({"status": "error", "message": "User not found"}), 404
|
| 1788 |
user = visitor_data_cache[user_id]
|
| 1789 |
+
|
| 1790 |
+
if deduct_bonuses > user.get('bonuses', 0): return jsonify({"status": "error", "message": "Недостаточно бонусов для списания."}), 400
|
| 1791 |
+
if deduct_bonuses > total_amount: return jsonify({"status": "error", "message": "Сумма списания не может превышать сумму накладной."}), 400
|
| 1792 |
+
|
| 1793 |
+
now = datetime.now(ALMATY_TZ)
|
| 1794 |
now_iso, now_str = now.isoformat(), now.strftime('%Y-%m-%d %H:%M:%S')
|
| 1795 |
invoice_id = str(uuid.uuid4().hex[:8]).upper()
|
| 1796 |
+
|
| 1797 |
processed_items = [{"product_name": item.get('product_name'), "quantity": float(item.get('quantity', 0)), "unit_price": float(item.get('unit_price', 0)), "item_total": round(float(item.get('quantity', 0)) * float(item.get('unit_price', 0)), 2)} for item in items]
|
| 1798 |
+
|
| 1799 |
+
new_invoice = {
|
| 1800 |
+
"invoice_id": invoice_id, "date": now_iso, "date_str": now_str,
|
| 1801 |
+
"total_amount": round(total_amount, 2), "items": processed_items,
|
| 1802 |
+
"bonuses_deducted": round(deduct_bonuses, 2)
|
| 1803 |
+
}
|
| 1804 |
if 'invoices' not in user: user['invoices'] = []
|
| 1805 |
user['invoices'].append(new_invoice)
|
| 1806 |
|
| 1807 |
+
if deduct_bonuses > 0:
|
| 1808 |
+
user['bonuses'] = round(user.get('bonuses', 0) - deduct_bonuses, 2)
|
| 1809 |
+
if 'history' not in user: user['history'] = []
|
| 1810 |
+
user['history'].append({"type": "deduction", "amount": deduct_bonuses, "description": f"Оплата по накладной #{invoice_id}", "date": now_iso, "date_str": now_str})
|
| 1811 |
+
|
| 1812 |
bonus_settings = visitor_data_cache.get('bonus_program_settings', {})
|
| 1813 |
invoice_bonus_percentage = float(bonus_settings.get('invoice_bonus_percentage', 0))
|
| 1814 |
if invoice_bonus_percentage > 0 and total_amount > 0:
|