Kgshop commited on
Commit
2ae3775
·
verified ·
1 Parent(s): 93cc53b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +312 -248
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
- BISHKEK_TZ = pytz.timezone('Asia/Bishkek')
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(BISHKEK_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,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
- --brand-yellow: #FFC107;
196
- --brand-black: #101010;
 
 
 
 
197
  --brand-red: #F44336;
198
  --brand-green: #4CAF50;
199
  --brand-blue: #2196F3;
200
- --card-bg: #1c1c1e;
201
- --text-color: #ffffff;
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(--brand-black);
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
- max-width: 600px;
228
- margin: 0 auto;
229
- display: flex;
230
- flex-direction: column;
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
- justify-content: space-around;
240
- background-color: var(--card-bg);
241
- border-radius: var(--border-radius);
242
- padding: 8px;
243
- margin-bottom: var(--padding-m);
244
  }
245
  .nav-btn {
246
- flex-grow: 1; padding: 10px 15px; border: none; border-radius: 12px;
247
- background-color: transparent; color: var(--text-secondary-color);
248
- font-family: var(--font-family); font-weight: 600; font-size: 1em;
249
- cursor: pointer; transition: background-color 0.2s, color 0.2s;
250
  }
251
- .nav-btn.active { background-color: var(--brand-yellow); color: var(--brand-black); box-shadow: 0 2px 10px rgba(255,193,7,0.3); }
252
- .content-section { display: none; flex-direction: column; gap: var(--padding-m); }
253
  .content-section.active { display: flex; }
254
- .card-grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--padding-m); }
255
- .bonus-card, .debt-card, .promo-card, .referral-bonus-card {
256
- background: linear-gradient(145deg, #2a2a2a, #1c1c1c);
257
- border-radius: calc(var(--border-radius) + 8px); padding: var(--padding-l);
258
- text-align: center; position: relative; overflow: hidden;
259
  }
260
- .bonus-card { box-shadow: var(--shadow-glow); border: 1px solid rgba(255, 193, 7, 0.2); }
261
- .debt-card { box-shadow: var(--shadow-glow-red); border: 1px solid rgba(244, 67, 54, 0.2); }
262
- .referral-bonus-card { grid-column: 1 / -1; box-shadow: var(--shadow-glow-blue); border: 1px solid rgba(33, 150, 243, 0.2); }
263
- .promo-card { grid-column: 1 / -1; border: 1px solid rgba(255,255,255,0.1); }
264
- .card-label { font-size: 1.1em; font-weight: 500; color: var(--text-secondary-color); margin-bottom: 12px; }
265
- .bonus-amount, .debt-amount, .referral-bonus-amount { font-size: 3em; font-weight: 800; letter-spacing: -2px; line-height: 1; }
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
- .client-id-label { font-weight: 500; color: var(--text-secondary-color); }
274
- .client-id-value {
275
- font-size: 1.3em; font-weight: 700; color: var(--brand-yellow);
276
- letter-spacing: 2px; background-color: rgba(255,193,7,0.1); padding: 4px 10px; border-radius: 8px;
 
 
 
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: 8px 16px; font-size: 0.9em; font-weight: 600; color: var(--brand-black);
287
- background-color: var(--brand-yellow); border: none; border-radius: 8px; cursor: pointer;
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
- .history-title, .invoices-title, .business-card-title {
296
- font-size: 1.4em; font-weight: 700; padding-bottom: var(--padding-m);
297
- border-bottom: 1px solid rgba(255, 255, 255, 0.1);
 
 
 
298
  }
299
- .history-list, .invoices-list { list-style: none; padding: 0; margin: 0; max-height: 35vh; overflow-y: auto; }
 
 
300
  .history-item, .invoice-item {
301
- display: flex; justify-content: space-between; align-items: center;
302
- padding: 14px 4px; border-bottom: 1px solid rgba(255, 255, 255, 0.05);
303
  }
304
  .history-item:last-child, .invoice-item:last-child { border-bottom: none; }
305
- .invoice-item { cursor: pointer; transition: background-color 0.2s; border-radius: 8px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
306
  .invoice-item:hover { background-color: rgba(255,255,255,0.05); }
307
- .history-details, .invoice-details { display: flex; flex-direction: column; }
308
- .history-description, .invoice-description { font-size: 1em; font-weight: 500; }
309
- .history-date, .invoice-date { font-size: 0.8em; color: var(--text-secondary-color); margin-top: 4px; }
310
- .history-amount, .invoice-amount { font-size: 1.1em; font-weight: 700; }
311
- .history-amount.bonus.accrual { color: var(--brand-green); }
312
- .history-amount.bonus.deduction { color: var(--brand-red); }
313
- .history-amount.debt.accrual { color: var(--brand-red); }
314
- .history-amount.debt.payment { color: var(--brand-green); }
315
- .history-amount.referral.accrual { color: var(--brand-blue); }
316
- .invoice-amount { color: var(--brand-yellow); }
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: 8px; color: var(--text-color);
327
- text-decoration: none; background-color: #2a2a2a; padding: 8px 12px;
328
- border-radius: 8px; transition: background-color 0.2s;
329
  }
330
- .business-card-phone-item a:hover { background-color: #3a3a3a; }
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
- height: 100%; overflow: auto; background-color: rgba(0,0,0,0.7);
335
- backdrop-filter: blur(5px); -webkit-backdrop-filter: blur(5px);
336
- align-items: center; justify-content: center;
337
  }
338
  .modal-content {
339
- background-color: var(--card-bg); margin: auto; padding: var(--padding-l);
340
- border-radius: var(--border-radius); max-width: 90%; width: 500px;
341
- box-shadow: 0 5px 15px rgba(0,0,0,0.5); position: relative;
 
342
  }
 
343
  .modal-close {
344
- color: var(--text-secondary-color); position: absolute; top: 10px; right: 20px;
345
- font-size: 28px; font-weight: bold; cursor: pointer;
 
346
  }
347
- .modal-close:hover, .modal-close:focus { color: var(--text-color); text-decoration: none; cursor: pointer; }
348
- .modal-title { font-size: 1.5em; font-weight: 700; margin-bottom: var(--padding-m); color: var(--brand-yellow); }
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: 10px 0;
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(--text-secondary-color); flex-basis: 20%; text-align: right; }
357
- .item-total { font-weight: 700; flex-basis: 20%; text-align: right; color: var(--brand-yellow); }
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
- .promo-modal-body { text-align: center; }
363
- .promo-modal-body p { color: var(--text-secondary-color); margin-bottom: 1rem; }
364
- .promo-modal-body input {
365
- width: 100%; padding: 14px; margin-bottom: 1rem; font-size: 1.1em;
366
- background-color: #2a2a2a; border: 1px solid #3a3a3a; border-radius: 12px;
367
- color: var(--text-color); text-align: center; letter-spacing: 2px;
 
 
368
  }
369
- .promo-modal-actions { display: flex; gap: 1rem; }
370
- .promo-modal-actions button {
371
- flex-grow: 1; padding: 14px; font-size: 1em; font-weight: 700;
372
- border: none; border-radius: 12px; cursor: pointer; transition: all 0.2s;
373
  }
374
- .btn-apply-promo { background-color: var(--brand-yellow); color: var(--brand-black); }
375
- .btn-skip-promo { background-color: transparent; border: 2px solid #3a3a3a; color: var(--text-secondary-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-grid">
392
- <div class="bonus-card">
393
- <p class="card-label">Ваши бонусы</p>
394
- <p class="bonus-amount">{{ "%.2f"|format(user.bonuses|float) }}</p>
395
- </div>
396
- <div class="debt-card">
397
  <p class="card-label">Ваш долг</p>
398
- <p class="debt-amount">{{ "%.2f"|format(user.debts|float) }}</p>
399
  </div>
400
- <div class="referral-bonus-card">
401
  <p class="card-label">Бонусы с друзей</p>
402
- <p class="referral-bonus-amount">{{ "%.2f"|format(user.referral_bonuses|float) }}</p>
403
  </div>
404
- <div class="promo-card">
405
- <p class="card-label">Ваш промокод для друзей</p>
406
- <div class="promo-code-display">
407
- <span class="promo-code-value" id="userPromoCode">{{ user.referral_code }}</span>
408
- <button class="copy-btn" onclick="copyPromoCode()">Копировать</button>
409
- </div>
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="history-section">
418
- <h2 class="history-title">История операций</h2>
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
- <span class="history-amount bonus {{ 'accrual' if item.type == 'accrual' else 'deduction' }}">
 
 
 
 
 
 
 
429
  {{ '+' if item.type == 'accrual' else '-' }}{{ "%.2f"|format(item.amount|float) }}
430
  </span>
431
  {% elif item.transaction_type == 'debt' %}
432
- <span class="history-amount debt {{ 'payment' if item.type == 'payment' else 'accrual' }}">
 
 
 
 
 
 
 
433
  {{ '-' if item.type == 'payment' else '+' }}{{ "%.2f"|format(item.amount|float) }}
434
  </span>
435
  {% elif item.transaction_type == 'referral' %}
436
- <span class="history-amount referral accrual">
 
 
 
 
 
 
 
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-history">Операций пока не было.</p>
445
  {% endif %}
446
  </section>
447
  </div>
448
  <div id="invoices-section" class="content-section">
449
- <section class="invoices-section">
450
- <h2 class="invoices-title">Мои накладные</h2>
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-invoices">Накладных пока нет.</p>
465
  {% endif %}
466
  </section>
467
  </div>
468
  <div id="business-card-section" class="content-section">
469
- <section class="business-card-section">
470
- <h2 class="business-card-title">Визитка организации</h2>
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 | default('Не указано') }}</div>
475
  </div>
 
 
476
  <div class="business-card-item">
477
  <div class="business-card-label">Номера телефонов</div>
478
- {% if org_details.phone_numbers %}
479
- <ul class="business-card-phone-list">
480
- {% for phone in org_details.phone_numbers %}
481
- <li class="business-card-phone-item">
482
- <a href="tel:{{ phone }}">
483
- <img src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSJjdXJyZW50Q2vb3IiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48cGF0aCBkPSJtMjIuMDIyIDEuOTY3LTMuMDkgMy41MjNhOC40OCA4LjQ4IDAgMCAwLTEzLjA5OSAwTDEuOTc4IDEuOTY3Yy0uOTc0LTEuMTA3LTIuNTI1LS40MS0yLjUxMiAxLjAzNXYxOC44NjhjLjAwMSAxLjQ0NSAxLjUyOCAyLjEzNiAyLjQ4OCAxLjAzN2wzLjA5LTMuNTIzYTEwLjE2IDEwLjE2IDAgMCAwIDEzLjA5OSAwbDMuMDkgMy41MjNjLjk3NCAxLjEwNyAyLjUyNS40MSAyLjUxMi0xLjAzN1YyLjAwMWMtLjAwMS0xLjQ0NS0xLjUyOC0yLjEzNi0yLjQ4OC0xLjAzN3oiLz48cGF0aCBkPSJtNy41IDIuOTc2YzEuMjI1LjA2MSAyLjQ1LjA5MSAzLjY3NS4wOTFzMi40NS0uMDMxIDMuNjc1LS4wOTFjLjY4Ny0uMDM0IDEuNDMzLjMyNSAxLjcwOS43NDdsMS40NzMgMS42NzhjLjQ5LjU1Ny44MzcgMS4yNjcuODk5IDIuMDgzLjA0MS41NTEuMDYyIDEuMS4wNjIgMS42NnYxLjUwNGMwIC43NzUtLjE3IDEuNTUzLS41MDggMi4yMzMtLjQ3Mi45MjUtMS4xMTcgMS43NDgtMS45MzUgMi4zODhsLS4xMzQuMTA1Yy0uNzU0LjU4My0xLjY0Ny45NzUtMi41NjIgMS4xODQtLjYwMi4xMzYtMS4wMDkuMjA4LTEuNzI1LjIwOC0xLjExNSAwLjc1NC0uMTUzIDMuNjgyLS4xNTMgNS41NDJzLjE1NCA0Ljc4Ni4xNTMgNS41NDJjLjAwMi0uNjU3LS4wNzgtMS4yNy0uMjItMS44MjQtLjE0OC0uNTU4LS40MTctMS4wODgtLjc2NC0xLjU1MS0uNjMyLS44NDUtMS40NDctMS41NTctMi40MTItMi4wNjItLjk2My0uNTA3LTEuOTk0LS44Mi0zLjA0Ni0uOTUyLS44MTMt.MTA0LTEuNTcxLS4xNDUtMi4zMzgtLjA5OS0uNjk0LjAxMS0xLjMzNy4xMDYtMS45MjQuMjg1LS41ODkuMTg0LTEuMTI2LjQyMS0xLjYwMS42OTMtLjQ3Ni4yNzMtLjkwNi41NzQtMS4yOTcuODktLjM4OC4zMTQtLjc0My42NDctMS4wNjcuOTk4LS4zMjYuMzUzLS42NDYuNzIyLS45NTkuMTA1OS0uMzEzLjMyOC0uNjIuNjUzLS45MjEuOTc1LS4yOTQuMzExLS41NzYuNjIzLS44NDQuOTMyLS4yNy4zMDktLjUyMy42MTctLjc1Ny45MTgtLjE5Ny4yNTQtLjM2MS40OTMtLjQ4MS43MjUtLjExOS4yMzItLjE4NS40NTItLjE5My41OTMtLjAwOS4xNDUtLjAxNC4yOTMtLjAxNi40NDF2MS43NmMwIC41NzYtLjE3NSAxLjEyMi0uNDQ3IDEuNTYtLjIyNy4zOTMtLjU2NS41OTktMS4wMTkuNTk5LTEuMTg2LS4wMDEtMS45OTYtMS4zOTctMi4yOTYtMi44NDItLjMyMy0xLjU1OC0uMzIzLTQuNTY5LS4zMjMtNi40MTFzLjAxNS00Ljg1NC4zMjMtNi40MTJjLjI5OS0xLjQ0NSAxLjExLTIuODQxIDIuMjk2LTIuODQyLjQ1NC4wMDcuNzgxLjI1OSAxLjAxOS42MDkuMjE1LjMzNC4zMjMuNzMuMzIzIDEuMTQ3di45NWMuMDMgMS4zMTQtLjAxNSAyLjYxLS4xNDcgMy44NzUtLjEwNiAxLjAzLS4yMzQgMi4wNDYtLjM1MSAyLjk5NmwuNTkzLS4zNzljLjMyNi0uMjA2LjY4Mi0uMzgxIDEuMDQ5LS41NzEuMzg2LS4xOTcgLjc5LS4zNjUgMS4xOTQtLjQ5NC40MDUtLjEyOS43ODctLjIzMyAxLjEyOC0uMjkwLjM0Mi0uMDU4LjYwNC0uMDc0Ljc4Mi0uMDQ3WiIvPjwvc3ZnPg==">
484
- {{ phone }}
485
- </a>
486
- </li>
487
- {% endfor %}
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 | default('Не указано') }}</div>
496
  </div>
 
 
497
  <div class="business-card-item">
498
  <div class="business-card-label">WhatsApp</div>
499
  <div class="business-card-value">
500
- {% if org_details.whatsapp_link %}
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
- {% if org_details.telegram_link %}
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-history">Данные организации не указаны.</p>
519
  {% endif %}
520
  </section>
521
  </div>
522
  </div>
523
  <div id="invoiceDetailModal" class="modal">
524
  <div class="modal-content">
525
- <span class="modal-close" onclick="closeModal('invoiceDetailModal')">×</span>
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
- <div class="promo-modal-body">
539
- <p>Если у вас есть промокод от друга, введите его, чтобы получить бонус.</p>
540
- <input type="text" id="promoCodeInput" placeholder="PROMO123">
541
- <div id="promoStatus"></div>
542
- <div class="promo-modal-actions">
543
- <button class="btn-skip-promo" onclick="submitPromoCode(false)">Нет, спасибо</button>
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
- const root = document.documentElement;
556
- const isDark = themeParams.bg_color ? (parseInt(themeParams.bg_color.substring(1, 3), 16) < 128) : true;
557
-
558
- root.style.setProperty('--brand-black', themeParams.bg_color || '#101010');
559
- root.style.setProperty('--text-color', themeParams.text_color || '#ffffff');
560
- root.style.setProperty('--text-secondary-color', themeParams.hint_color || '#a0a0a0');
561
- root.style.setProperty('--brand-yellow', themeParams.button_color || '#FFC107');
562
- root.style.setProperty('--card-bg', themeParams.secondary_bg_color || (isDark ? '#1c1c1e' : '#f1f1f1'));
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
- if (tg.themeParams && Object.keys(tg.themeParams).length > 0) {
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} от ${invoiceData.date_str}`;
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(--text-secondary-color)';
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: #f0f0f0; border-radius: 8px; margin-top: 1rem; font-weight: 600; }
 
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>Итоговая сумма накладной:</strong></td>
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="+79001234567">
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="+79001112233,+79004445566">
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/79001112233">
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
- <h2 id="adminInvoiceDetailTitle" class="modal-title"></h2>
 
 
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: 0, unit_price: 0, item_total: 0 });
1387
- newRow.innerHTML = `<td><input type="text" placeholder="Название товара" oninput="updateInvoiceItem(${rowIndex}, 'product_name', this.value)"></td><td><input type="number" step="1" min="0" placeholder="0" 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>`;
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
- for (let i = 0; i < tableBody.rows.length; i++) {
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="0" placeholder="0" value="${item.quantity || ''}" 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>`;
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 payload = { user_id: currentUserData.id, total_amount: totalAmount, items: itemsToAdd };
 
 
 
 
 
 
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
- document.getElementById('adminInvoiceTotalAmount').textContent = parseFloat(invoiceData.total_amount).toFixed(2);
 
 
 
 
 
 
 
 
 
 
 
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(BISHKEK_TZ)
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(BISHKEK_TZ)
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(BISHKEK_TZ)
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(BISHKEK_TZ)
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
- now = datetime.now(BISHKEK_TZ)
 
 
 
 
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
- new_invoice = {"invoice_id": invoice_id, "date": now_iso, "date_str": now_str, "total_amount": round(total_amount, 2), "items": processed_items}
 
 
 
 
 
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: