Eluza133 commited on
Commit
084241c
·
verified ·
1 Parent(s): f599f65

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +220 -227
app.py CHANGED
@@ -1,4 +1,5 @@
1
 
 
2
  import flask
3
  from flask import Flask, render_template_string, request, redirect, url_for, session, flash, send_file, jsonify, Response, stream_with_context
4
  from flask_caching import Cache
@@ -305,37 +306,33 @@ PUBLIC_BUSINESS_PAGE_HTML = '''
305
  <link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
306
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;800&display=swap" rel="stylesheet">
307
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
308
- <script src="https://telegram.org/js/telegram-web-app.js"></script>
309
  <style>''' + BASE_STYLE + '''
310
- body { background: var(--background-dark); padding-bottom: 80px; }
311
- .container { max-width: 1000px; padding-top: 20px; }
312
- .biz-header { text-align: center; margin-bottom: 30px; }
313
- .biz-avatar { width: 100px; height: 100px; border-radius: 50%; object-fit: cover; margin: 0 auto 15px auto; border: 3px solid var(--accent); }
314
- .biz-header h1 { font-size: 2em; color: var(--text-dark); }
315
- .product-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; }
316
- @media (min-width: 768px) { .product-grid { grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 20px; } }
317
- .product-card { background: var(--card-bg-dark); border-radius: 12px; overflow: hidden; display: flex; flex-direction: column; height: 100%; transition: transform 0.2s; border: 1px solid #2a2a2a; }
318
- .product-image-wrapper { width: 100%; padding-top: 100%; position: relative; background: #2a2a2a; }
319
- .product-image { position: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: cover; }
320
- .product-info { padding: 10px; display: flex; flex-direction: column; flex-grow: 1; }
321
- .product-price { font-weight: 800; font-size: 1.1em; color: var(--text-dark); margin-bottom: 4px; }
322
- .product-name { font-size: 0.9em; line-height: 1.3; margin-bottom: 8px; color: #bbb; overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; height: 2.6em; }
323
- .product-bottom { margin-top: auto; }
324
- .add-to-cart-btn { background: rgba(255, 255, 255, 0.1); color: white; border: none; width: 100%; padding: 8px; border-radius: 8px; font-weight: 600; cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 5px; transition: 0.2s; }
325
- .add-to-cart-btn:hover { background: var(--primary); }
326
- .add-to-cart-btn.added { background: var(--share-color); }
327
- .cart-fab { position: fixed; bottom: 20px; right: 20px; width: 60px; height: 60px; background: var(--primary); border-radius: 50%; display: flex; align-items: center; justify-content: center; color: white; font-size: 24px; box-shadow: var(--shadow); cursor: pointer; z-index: 1000; }
328
- .cart-badge { position: absolute; top: -5px; right: -5px; background: #fff; color: var(--primary); font-size: 12px; font-weight: bold; width: 22px; height: 22px; border-radius: 50%; display: flex; align-items: center; justify-content: center; border: 2px solid var(--primary); }
329
- .cart-modal { display: none; position: fixed; bottom: 0; left: 0; right: 0; top: 0; background: rgba(0,0,0,0.8); z-index: 2000; justify-content: center; align-items: flex-end; }
330
- .cart-content { background: var(--card-bg-dark); width: 100%; max-width: 600px; max-height: 90vh; border-radius: 20px 20px 0 0; padding: 20px; display: flex; flex-direction: column; margin: 0 auto; }
331
- .cart-items { flex-grow: 1; overflow-y: auto; margin-bottom: 15px; }
332
- .cart-item { display: flex; align-items: center; gap: 10px; padding: 10px 0; border-bottom: 1px solid #333; }
333
- .cart-item img { width: 50px; height: 50px; border-radius: 8px; object-fit: cover; }
334
  .cart-item-info { flex-grow: 1; }
335
- .cart-item-controls { display: flex; align-items: center; gap: 10px; }
336
- .qty-btn { width: 28px; height: 28px; border-radius: 50%; border: none; background: #333; color: white; cursor: pointer; }
337
- .checkout-btn { width: 100%; padding: 15px; background: var(--primary); color: white; border: none; border-radius: 12px; font-weight: bold; font-size: 1.1em; cursor: pointer; }
338
- .empty-cart { text-align: center; padding: 40px; color: var(--text-muted); }
 
339
  </style></head><body>
340
  <div class="container">
341
  <div class="biz-header">
@@ -348,168 +345,144 @@ body { background: var(--background-dark); padding-bottom: 80px; }
348
  <div class="product-grid">
349
  {% for product in page.products %}
350
  <div class="product-card">
351
- <div class="product-image-wrapper">
352
- {% if product.photo_path %}
353
- <img src="{{ hf_file_url_jinja(product.photo_path) }}" alt="{{ product.name }}" class="product-image">
354
- {% else %}
355
- <div class="product-image" style="background: #333; display: flex; align-items: center; justify-content: center;"><i class="fa-solid fa-image" style="font-size: 2em; color: #555;"></i></div>
356
- {% endif %}
357
- </div>
358
  <div class="product-info">
359
- {% if page.show_prices and product.price %}
360
- <div class="product-price">{{ "%.0f"|format(product.price|float) }} {{ page.currency }}</div>
361
- {% endif %}
362
  <div class="product-name">{{ product.name }}</div>
363
  {% if page.show_prices and product.price %}
364
- <div class="product-bottom">
365
- <button class="add-to-cart-btn" onclick='addToCart({{ product|tojson }})' id="btn-{{ product.id }}">
366
- <i class="fa-solid fa-cart-plus"></i> В корзину
367
- </button>
368
- </div>
369
  {% endif %}
370
  </div>
371
  </div>
372
  {% endfor %}
373
  </div>
 
374
  </div>
375
 
376
  {% if page.show_prices %}
377
  <div class="cart-fab" onclick="openCart()">
378
- <i class="fa-solid fa-shopping-cart"></i>
379
- <span class="cart-badge" id="cart-badge" style="display: none;">0</span>
380
  </div>
381
 
382
  <div class="cart-modal" id="cart-modal">
383
  <div class="cart-content">
384
- <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
385
  <h3>Корзина</h3>
386
- <button onclick="closeCart()" style="background:none; border:none; color:white; font-size: 1.5em;"><i class="fa-solid fa-times"></i></button>
387
  </div>
388
- <div class="cart-items" id="cart-items"></div>
389
- <div style="border-top: 1px solid #333; padding-top: 15px; margin-bottom: 15px; display: flex; justify-content: space-between; font-weight: bold;">
390
  <span>Итого:</span>
391
- <span id="cart-total">0 {{ page.currency }}</span>
392
  </div>
393
  <button class="checkout-btn" onclick="checkout()">Оформить заказ</button>
394
  </div>
395
  </div>
 
 
 
 
396
  {% endif %}
397
 
398
  <script>
399
- window.Telegram.WebApp.ready();
400
- window.Telegram.WebApp.expand();
401
- const haptic = window.Telegram.WebApp.HapticFeedback;
402
- let cart = JSON.parse(localStorage.getItem('cart_{{ page.login }}')) || [];
403
 
 
 
404
  function updateCartUI() {
405
- const badge = document.getElementById('cart-badge');
406
- const totalQty = cart.reduce((sum, item) => sum + item.qty, 0);
407
- if (totalQty > 0) {
408
- badge.style.display = 'flex';
409
- badge.textContent = totalQty;
410
- } else {
411
- badge.style.display = 'none';
412
- }
413
- localStorage.setItem('cart_{{ page.login }}', JSON.stringify(cart));
414
  }
415
 
416
- function addToCart(product) {
417
- haptic.impactOccurred('light');
418
- const existing = cart.find(item => item.id === product.id);
419
- if (existing) {
420
- existing.qty++;
421
- } else {
422
- cart.push({ ...product, qty: 1 });
 
 
 
 
 
 
 
423
  }
424
- const btn = document.getElementById('btn-' + product.id);
425
- btn.classList.add('added');
426
- btn.innerHTML = '<i class="fa-solid fa-check"></i> Добавлено';
427
- setTimeout(() => {
428
- btn.classList.remove('added');
429
- btn.innerHTML = '<i class="fa-solid fa-cart-plus"></i> В корзину';
430
- }, 1000);
431
- updateCartUI();
432
  }
433
 
434
- function openCart() {
435
- haptic.impactOccurred('medium');
436
- const modal = document.getElementById('cart-modal');
437
- const container = document.getElementById('cart-items');
438
  container.innerHTML = '';
439
  let total = 0;
440
-
441
- if (cart.length === 0) {
442
- container.innerHTML = '<div class="empty-cart"><i class="fa-solid fa-cart-arrow-down" style="font-size: 3em; margin-bottom: 10px;"></i><p>Корзина пуста</p></div>';
443
- document.getElementById('cart-total').textContent = '0 {{ page.currency }}';
444
  } else {
445
- cart.forEach(item => {
446
  total += item.price * item.qty;
447
- const div = document.createElement('div');
448
- div.className = 'cart-item';
449
- div.innerHTML = `
450
- ${item.photo_path ? `<img src="https://huggingface.co/datasets/{{ REPO_ID }}/resolve/main/${item.photo_path}">` : '<div style="width:50px; height:50px; background:#333; border-radius:8px;"></div>'}
451
- <div class="cart-item-info">
452
- <div style="font-weight:500; font-size: 0.9em;">${item.name}</div>
453
- <div style="color: var(--text-muted); font-size: 0.8em;">${item.price} {{ page.currency }}</div>
454
- </div>
455
- <div class="cart-item-controls">
456
- <button class="qty-btn" onclick="changeQty('${item.id}', -1)">-</button>
457
- <span>${item.qty}</span>
458
- <button class="qty-btn" onclick="changeQty('${item.id}', 1)">+</button>
459
  </div>
460
  `;
461
- container.appendChild(div);
462
  });
463
- document.getElementById('cart-total').textContent = Math.round(total) + ' {{ page.currency }}';
464
  }
465
- modal.style.display = 'flex';
466
  }
467
 
468
- function closeCart() {
469
- document.getElementById('cart-modal').style.display = 'none';
470
- }
471
 
472
- function changeQty(id, delta) {
473
- haptic.impactOccurred('light');
474
- const idx = cart.findIndex(item => item.id === id);
475
- if (idx !== -1) {
476
- cart[idx].qty += delta;
477
- if (cart[idx].qty <= 0) cart.splice(idx, 1);
478
- updateCartUI();
479
- openCart();
480
- }
481
  }
482
 
483
  async function checkout() {
484
- if (cart.length === 0) return;
485
- haptic.impactOccurred('heavy');
486
  const btn = document.querySelector('.checkout-btn');
487
- btn.disabled = true;
488
- btn.textContent = 'Обработка...';
489
 
490
  try {
491
- const response = await fetch('{{ url_for("create_order_api", login=page.login) }}', {
492
  method: 'POST',
493
- headers: {'Content-Type': 'application/json'},
494
  body: JSON.stringify({ items: cart })
495
  });
496
- const result = await response.json();
497
- if (result.status === 'success') {
498
- cart = [];
499
- localStorage.removeItem('cart_{{ page.login }}');
500
- window.location.href = result.redirect_url;
501
  } else {
502
- alert('Ошибка: ' + result.message);
503
- btn.disabled = false;
504
- btn.textContent = 'Оформить заказ';
505
  }
506
  } catch (e) {
507
  alert('Ошибка сети');
508
- btn.disabled = false;
509
- btn.textContent = 'Оформить заказ';
510
  }
511
  }
512
-
513
  updateCartUI();
514
  </script>
515
  </body></html>
@@ -517,70 +490,76 @@ body { background: var(--background-dark); padding-bottom: 80px; }
517
 
518
  PUBLIC_ORDER_PAGE_HTML = '''
519
  <!DOCTYPE html><html lang="ru"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
520
- <title>Заказ #{{ order.id[:8] }}</title>
521
- <link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
522
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;800&display=swap" rel="stylesheet">
523
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
524
  <style>''' + BASE_STYLE + '''
525
- .container { max-width: 600px; margin-top: 20px; }
526
- .order-card { background: var(--card-bg-dark); border-radius: 16px; padding: 20px; margin-bottom: 20px; }
527
- .order-item { display: flex; gap: 15px; padding: 15px 0; border-bottom: 1px solid #333; }
528
- .order-item:last-child { border-bottom: none; }
529
- .item-img { width: 70px; height: 70px; border-radius: 10px; object-fit: cover; background: #333; }
530
- .total-row { display: flex; justify-content: space-between; font-size: 1.2em; font-weight: 800; margin-top: 20px; color: var(--secondary); }
531
- .send-btn { width: 100%; padding: 16px; border-radius: 12px; font-weight: bold; font-size: 1.1em; text-decoration: none; display: flex; justify-content: center; align-items: center; gap: 10px; margin-top: 10px; color: white; border: none; cursor: pointer; box-shadow: 0 4px 15px rgba(0,0,0,0.3); }
532
- .whatsapp { background: #25D366; }
533
- .telegram { background: #0088cc; }
 
 
 
 
 
 
 
534
  </style></head><body>
535
- <div class="container">
536
- <h2 style="text-align: center;">Заказ оформлен!</h2>
537
- <div class="order-card">
538
- <h4 style="color: var(--text-muted); margin-bottom: 15px;">Заказ #{{ order.id[:8] }}</h4>
539
- <div class="order-items">
540
- {% for item in order.items %}
541
- <div class="order-item">
542
- {% if item.photo_path %}
543
- <img src="{{ hf_file_url_jinja(item.photo_path) }}" class="item-img">
544
- {% else %}
545
- <div class="item-img" style="display: flex; align-items: center; justify-content: center;"><i class="fa-solid fa-box"></i></div>
546
- {% endif %}
547
- <div style="flex-grow: 1;">
548
- <div style="font-weight: 600;">{{ item.name }}</div>
549
- <div style="color: var(--text-muted); font-size: 0.9em;">{{ item.qty }} x {{ item.price }} {{ page.currency }}</div>
550
- </div>
551
- <div style="font-weight: bold;">{{ item.price * item.qty }} {{ page.currency }}</div>
552
- </div>
553
- {% endfor %}
554
  </div>
555
- <div class="total-row">
556
- <span>Итого:</span>
557
- <span>{{ order.total }} {{ page.currency }}</span>
 
 
 
 
 
 
 
 
 
558
  </div>
 
559
  </div>
 
560
 
561
- <p style="text-align: center; color: var(--text-muted); margin-bottom: 15px;">Нажмите кнопку ниже, чтобы отправить заказ продавцу.</p>
562
-
563
- {% set order_url = url_for('view_order', order_id=order.id, _external=True) %}
564
- {% set msg_text = "Здравствуйте! Я оформил заказ. Ссылка: " + order_url %}
565
-
566
- {% if page.order_destination == 'whatsapp' %}
567
- <a href="https://wa.me/{{ page.contact_number }}?text={{ msg_text|urlencode }}" class="send-btn whatsapp" target="_blank">
568
- <i class="fab fa-whatsapp"></i> Отправить в WhatsApp
569
- </a>
570
- {% else %}
571
- <a href="https://t.me/{{ page.contact_number }}" onclick="copyAndOpenTg('{{ page.contact_number }}', '{{ msg_text }}')" class="send-btn telegram" target="_blank">
572
- <i class="fab fa-telegram"></i> Отправить в Telegram
573
- </a>
574
- <script>
575
- function copyAndOpenTg(username, text) {
576
- // Telegram links don't support pre-filled text easily across all devices, so we copy to clipboard
577
- navigator.clipboard.writeText(text).then(() => {
578
- alert('Текст сообщения скопирован! Вставьте его в чат.');
579
- });
580
- }
581
- </script>
582
- {% endif %}
583
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
584
  </body></html>
585
  '''
586
 
@@ -686,6 +665,7 @@ def initialize_user_filesystem_tma(user_data, tma_user_id_str):
686
  del user_data['files']
687
  user_data.setdefault('owned_business_pages', [])
688
 
 
689
  @cache.memoize(timeout=300)
690
  def load_data():
691
  try:
@@ -1699,7 +1679,7 @@ TMA_CREATE_EDIT_BUSINESS_FORM_HTML = '''
1699
  <div class="form-group">
1700
  <label class="checkbox-label">
1701
  <input type="checkbox" name="show_prices" value="true" {{ 'checked' if page and page.show_prices }}>
1702
- <span>Указывать цены на товары и разрешить заказы</span>
1703
  </label>
1704
  </div>
1705
  <div class="form-group">
@@ -1763,7 +1743,7 @@ TMA_MANAGE_PRODUCTS_HTML = '''
1763
  <div class="item-name-info">
1764
  <p class="item-name">{{ product.name }}</p>
1765
  {% if page.show_prices %}
1766
- <p class="item-info">{{ "%.0f"|format(product.price|float) }} {{ page.currency }}</p>
1767
  {% endif %}
1768
  </div>
1769
  <button onclick="openProductModal('{{ product.id }}')" class="btn" style="background: var(--accent); padding: 8px 12px; font-size: 0.8em; margin-left: auto;"><i class="fa-solid fa-pencil"></i></button>
@@ -1794,7 +1774,7 @@ TMA_MANAGE_PRODUCTS_HTML = '''
1794
  {% if page.show_prices %}
1795
  <div class="form-group">
1796
  <label for="price">Цена ({{ page.currency }})</label>
1797
- <input type="number" name="price" id="price" step="1">
1798
  </div>
1799
  {% endif %}
1800
  <div class="form-group">
@@ -2747,54 +2727,67 @@ def public_business_page(login):
2747
  page = data.get('business_pages', {}).get(login)
2748
  if not page:
2749
  return "Страница не найдена.", 404
2750
- return render_template_string(PUBLIC_BUSINESS_PAGE_HTML, page=page, REPO_ID=REPO_ID, hf_file_url_jinja=lambda path, download=False: f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{quote(path)}{'?download=true' if download else ''}")
2751
 
2752
- @app.route('/api/business/<login>/create_order', methods=['POST'])
2753
- def create_order_api(login):
2754
  data = load_data()
2755
  page = data.get('business_pages', {}).get(login)
2756
- if not page:
2757
- return jsonify({'status': 'error', 'message': 'Страница не найдена'}), 404
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2758
 
 
2759
  try:
2760
- req_data = request.json
2761
- items = req_data.get('items', [])
2762
- if not items:
2763
- return jsonify({'status': 'error', 'message': 'Корзина пуста'}), 400
2764
-
2765
- total = 0
2766
- for item in items:
2767
- total += float(item.get('price', 0)) * int(item.get('qty', 1))
2768
-
2769
- order_id = uuid.uuid4().hex
2770
- order_data = {
2771
- 'id': order_id,
2772
- 'page_login': login,
2773
- 'items': items,
2774
- 'total': total,
2775
- 'created_at': datetime.now().isoformat()
2776
- }
2777
-
2778
- data['orders'][order_id] = order_data
2779
  save_data(data)
2780
-
2781
- return jsonify({'status': 'success', 'redirect_url': url_for('view_order', order_id=order_id)})
2782
  except Exception as e:
2783
- logging.error(f"Order creation failed: {e}")
2784
- return jsonify({'status': 'error', 'message': 'Server error'}), 500
2785
 
2786
  @app.route('/order/<order_id>')
2787
- def view_order(order_id):
2788
  data = load_data()
2789
  order = data.get('orders', {}).get(order_id)
2790
- if not order:
2791
- return "Заказ не найден", 404
2792
 
2793
  page = data.get('business_pages', {}).get(order['page_login'])
2794
- if not page:
2795
- return "Страница магазина не найдена", 404
2796
-
2797
- return render_template_string(PUBLIC_ORDER_PAGE_HTML, order=order, page=page, hf_file_url_jinja=lambda path, download=False: f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{quote(path)}{'?download=true' if download else ''}")
2798
 
2799
  @app.route('/tma_business/manage/<login>')
2800
  def tma_manage_products(login):
@@ -3442,4 +3435,4 @@ if __name__ == '__main__':
3442
 
3443
  threading.Thread(target=check_reminders, daemon=True).start()
3444
 
3445
- app.run(debug=False, host='0.0.0.0', port=7860)
 
1
 
2
+
3
  import flask
4
  from flask import Flask, render_template_string, request, redirect, url_for, session, flash, send_file, jsonify, Response, stream_with_context
5
  from flask_caching import Cache
 
306
  <link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
307
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;800&display=swap" rel="stylesheet">
308
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
 
309
  <style>''' + BASE_STYLE + '''
310
+ body { background: var(--card-bg-dark); padding-bottom: 80px; }
311
+ .container { max-width: 800px; padding-top: 20px; }
312
+ .biz-header { text-align: center; margin-bottom: 20px; }
313
+ .biz-avatar { width: 90px; height: 90px; border-radius: 50%; object-fit: cover; margin: 0 auto 10px auto; border: 3px solid var(--accent); }
314
+ .biz-header h1 { font-size: 1.8em; color: var(--text-dark); margin-bottom: 5px; }
315
+ .product-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; padding-bottom: 20px; }
316
+ .product-card { background: var(--background-dark); border-radius: 12px; overflow: hidden; box-shadow: 0 2px 8px rgba(0,0,0,0.2); display: flex; flex-direction: column; height: 100%; }
317
+ .product-image { width: 100%; height: 160px; object-fit: cover; background-color: #2a2a2a; }
318
+ .product-info { padding: 10px; flex-grow: 1; display: flex; flex-direction: column; }
319
+ .product-name { font-size: 0.95em; font-weight: 600; margin-bottom: 4px; line-height: 1.3; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; flex-grow: 1; }
320
+ .product-price { font-size: 1.1em; font-weight: 800; color: var(--secondary); margin-bottom: 8px; }
321
+ .add-to-cart-btn { width: 100%; background: var(--primary); border: none; color: white; padding: 8px; border-radius: 8px; font-weight: 600; cursor: pointer; font-size: 0.9em; margin-top: auto; }
322
+ .cart-fab { position: fixed; bottom: 20px; right: 20px; width: 60px; height: 60px; background: var(--secondary); border-radius: 50%; display: flex; justify-content: center; align-items: center; color: white; font-size: 1.5em; box-shadow: var(--shadow); cursor: pointer; z-index: 1000; }
323
+ .cart-badge { position: absolute; top: -5px; right: -5px; background: var(--delete-color); color: white; border-radius: 50%; width: 24px; height: 24px; font-size: 0.8em; display: flex; justify-content: center; align-items: center; font-weight: bold; border: 2px solid var(--card-bg-dark); }
324
+ .cart-modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); z-index: 2000; align-items: flex-end; }
325
+ .cart-content { background: var(--card-bg-dark); width: 100%; max-height: 80vh; border-radius: 20px 20px 0 0; padding: 20px; display: flex; flex-direction: column; animation: slideUp 0.3s ease-out; }
326
+ @keyframes slideUp { from { transform: translateY(100%); } to { transform: translateY(0); } }
327
+ .cart-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; border-bottom: 1px solid #333; padding-bottom: 10px; }
328
+ .cart-items { overflow-y: auto; flex-grow: 1; margin-bottom: 15px; }
329
+ .cart-item { display: flex; justify-content: space-between; align-items: center; padding: 10px 0; border-bottom: 1px solid #2a2a2a; }
 
 
 
 
330
  .cart-item-info { flex-grow: 1; }
331
+ .cart-item-title { font-weight: 600; font-size: 0.95em; }
332
+ .cart-item-price { color: var(--secondary); font-size: 0.9em; }
333
+ .cart-controls { display: flex; align-items: center; gap: 10px; }
334
+ .cart-controls button { background: #333; color: white; border: none; width: 30px; height: 30px; border-radius: 50%; font-weight: bold; cursor: pointer; }
335
+ .checkout-btn { background: var(--secondary); color: white; border: none; width: 100%; padding: 15px; border-radius: 12px; font-size: 1.1em; font-weight: bold; cursor: pointer; }
336
  </style></head><body>
337
  <div class="container">
338
  <div class="biz-header">
 
345
  <div class="product-grid">
346
  {% for product in page.products %}
347
  <div class="product-card">
348
+ {% if product.photo_path %}
349
+ <img src="{{ hf_file_url_jinja(product.photo_path) }}" class="product-image">
350
+ {% else %}
351
+ <div class="product-image" style="display: flex; justify-content: center; align-items: center; color: #555;"><i class="fa-solid fa-image fa-2x"></i></div>
352
+ {% endif %}
 
 
353
  <div class="product-info">
 
 
 
354
  <div class="product-name">{{ product.name }}</div>
355
  {% if page.show_prices and product.price %}
356
+ <div class="product-price">{{ "%.2f"|format(product.price|float) }} {{ page.currency }}</div>
357
+ <button class="add-to-cart-btn" onclick="addToCart('{{ product.id }}', '{{ product.name|escape }}', {{ product.price }})">В корзину</button>
358
+ {% else %}
359
+ <p style="font-size:0.8em; color:var(--text-muted); margin-top:auto;">{{ product.description|truncate(50) }}</p>
 
360
  {% endif %}
361
  </div>
362
  </div>
363
  {% endfor %}
364
  </div>
365
+ {% if not page.products %}<p style="text-align: center;">Товары скоро появятся.</p>{% endif %}
366
  </div>
367
 
368
  {% if page.show_prices %}
369
  <div class="cart-fab" onclick="openCart()">
370
+ <i class="fa-solid fa-cart-shopping"></i>
371
+ <div class="cart-badge" id="cart-count">0</div>
372
  </div>
373
 
374
  <div class="cart-modal" id="cart-modal">
375
  <div class="cart-content">
376
+ <div class="cart-header">
377
  <h3>Корзина</h3>
378
+ <button onclick="closeCart()" style="background:none; border:none; color:white; font-size:1.5em;">&times;</button>
379
  </div>
380
+ <div class="cart-items" id="cart-items-container"></div>
381
+ <div style="display:flex; justify-content:space-between; font-weight:bold; margin-bottom:15px; font-size:1.1em;">
382
  <span>Итого:</span>
383
+ <span id="cart-total">0.00 {{ page.currency }}</span>
384
  </div>
385
  <button class="checkout-btn" onclick="checkout()">Оформить заказ</button>
386
  </div>
387
  </div>
388
+ {% else %}
389
+ <div class="cart-fab" style="background: var(--business-color); width: auto; padding: 0 20px; border-radius: 30px;" onclick="contactDirect()">
390
+ <i class="{{ 'fa-brands fa-whatsapp' if page.order_destination == 'whatsapp' else 'fa-brands fa-telegram' }}" style="margin-right: 8px;"></i> Связаться
391
+ </div>
392
  {% endif %}
393
 
394
  <script>
395
+ const CURRENCY = '{{ page.currency }}';
396
+ const LOGIN = '{{ page.login }}';
397
+ let cart = JSON.parse(localStorage.getItem(`cart_${LOGIN}`)) || {};
 
398
 
399
+ function saveCart() { localStorage.setItem(`cart_${LOGIN}`, JSON.stringify(cart)); updateCartUI(); }
400
+
401
  function updateCartUI() {
402
+ const count = Object.values(cart).reduce((a, c) => a + c.qty, 0);
403
+ document.getElementById('cart-count').textContent = count;
404
+ if(document.getElementById('cart-items-container')) renderCartItems();
 
 
 
 
 
 
405
  }
406
 
407
+ function addToCart(id, name, price) {
408
+ if (!cart[id]) cart[id] = { name, price, qty: 0 };
409
+ cart[id].qty++;
410
+ saveCart();
411
+ const badge = document.getElementById('cart-count');
412
+ badge.style.transform = 'scale(1.3)';
413
+ setTimeout(() => badge.style.transform = 'scale(1)', 200);
414
+ }
415
+
416
+ function removeFromCart(id) {
417
+ if (cart[id]) {
418
+ cart[id].qty--;
419
+ if (cart[id].qty <= 0) delete cart[id];
420
+ saveCart();
421
  }
 
 
 
 
 
 
 
 
422
  }
423
 
424
+ function renderCartItems() {
425
+ const container = document.getElementById('cart-items-container');
 
 
426
  container.innerHTML = '';
427
  let total = 0;
428
+ if (Object.keys(cart).length === 0) {
429
+ container.innerHTML = '<p style="text-align:center; color:var(--text-muted);">Корзина пуста</p>';
 
 
430
  } else {
431
+ Object.entries(cart).forEach(([id, item]) => {
432
  total += item.price * item.qty;
433
+ container.innerHTML += `
434
+ <div class="cart-item">
435
+ <div class="cart-item-info">
436
+ <div class="cart-item-title">${item.name}</div>
437
+ <div class="cart-item-price">${item.price} ${CURRENCY}</div>
438
+ </div>
439
+ <div class="cart-controls">
440
+ <button onclick="removeFromCart('${id}')">-</button>
441
+ <span>${item.qty}</span>
442
+ <button onclick="addToCart('${id}', '${item.name.replace(/'/g, "\\'")}', ${item.price})">+</button>
443
+ </div>
 
444
  </div>
445
  `;
 
446
  });
 
447
  }
448
+ document.getElementById('cart-total').textContent = `${total.toFixed(2)} ${CURRENCY}`;
449
  }
450
 
451
+ function openCart() { document.getElementById('cart-modal').style.display = 'flex'; updateCartUI(); }
452
+ function closeCart() { document.getElementById('cart-modal').style.display = 'none'; }
453
+ document.getElementById('cart-modal')?.addEventListener('click', (e) => { if(e.target.id === 'cart-modal') closeCart(); });
454
 
455
+ function contactDirect() {
456
+ const phone = '{{ page.contact_number }}'.replace(/[^0-9]/g, '');
457
+ const dest = '{{ page.order_destination }}';
458
+ if(dest === 'whatsapp') window.open(`https://wa.me/${phone}`, '_blank');
459
+ else window.open(`https://t.me/${'{{ page.contact_number }}'.replace('@','')}`, '_blank');
 
 
 
 
460
  }
461
 
462
  async function checkout() {
463
+ if (Object.keys(cart).length === 0) return;
 
464
  const btn = document.querySelector('.checkout-btn');
465
+ btn.disabled = true; btn.textContent = 'Создание заказа...';
 
466
 
467
  try {
468
+ const response = await fetch(`{{ url_for('create_business_order', login=page.login) }}`, {
469
  method: 'POST',
470
+ headers: { 'Content-Type': 'application/json' },
471
  body: JSON.stringify({ items: cart })
472
  });
473
+ const res = await response.json();
474
+ if (res.status === 'success') {
475
+ localStorage.removeItem(`cart_${LOGIN}`);
476
+ window.location.href = res.redirect_url;
 
477
  } else {
478
+ alert(res.message || 'Ошибка создания заказа');
 
 
479
  }
480
  } catch (e) {
481
  alert('Ошибка сети');
482
+ } finally {
483
+ btn.disabled = false; btn.textContent = 'Оформить заказ';
484
  }
485
  }
 
486
  updateCartUI();
487
  </script>
488
  </body></html>
 
490
 
491
  PUBLIC_ORDER_PAGE_HTML = '''
492
  <!DOCTYPE html><html lang="ru"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
493
+ <title>Заказ #{{ order.order_id }}</title>
494
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
 
495
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
496
  <style>''' + BASE_STYLE + '''
497
+ body { background: #121212; padding: 20px; }
498
+ .receipt-card { background: white; color: black; border-radius: 0; max-width: 600px; margin: 20px auto; padding: 30px; box-shadow: 0 5px 25px rgba(0,0,0,0.5); position: relative; }
499
+ .receipt-card::before { content: ""; position: absolute; top: -10px; left: 0; right: 0; height: 10px; background: radial-gradient(circle, transparent 50%, white 50%) 0 -5px/20px 20px repeat-x; transform: rotate(180deg); }
500
+ .receipt-card::after { content: ""; position: absolute; bottom: -10px; left: 0; right: 0; height: 10px; background: radial-gradient(circle, transparent 50%, white 50%) 0 -5px/20px 20px repeat-x; }
501
+ .receipt-header { text-align: center; border-bottom: 2px dashed #ccc; padding-bottom: 20px; margin-bottom: 20px; }
502
+ .receipt-header h1 { color: black; margin: 0; font-size: 1.8em; }
503
+ .receipt-meta { font-family: monospace; color: #555; margin-top: 10px; }
504
+ .item-row { display: flex; gap: 15px; border-bottom: 1px solid #eee; padding: 15px 0; align-items: center; }
505
+ .item-thumb { width: 50px; height: 50px; object-fit: cover; border-radius: 4px; background: #eee; }
506
+ .item-details { flex-grow: 1; }
507
+ .item-name { font-weight: bold; font-size: 0.95em; }
508
+ .item-calc { font-size: 0.85em; color: #666; }
509
+ .item-total { font-weight: bold; }
510
+ .total-row { display: flex; justify-content: space-between; font-size: 1.3em; font-weight: 800; margin-top: 20px; padding-top: 20px; border-top: 2px solid black; }
511
+ .send-btn { display: block; width: 100%; max-width: 600px; margin: 30px auto; background: #25D366; color: white; text-align: center; padding: 15px; border-radius: 12px; font-weight: bold; text-decoration: none; font-size: 1.1em; box-shadow: 0 5px 15px rgba(37, 211, 102, 0.3); }
512
+ .telegram-btn { background: #0088cc; box-shadow: 0 5px 15px rgba(0, 136, 204, 0.3); }
513
  </style></head><body>
514
+
515
+ <div class="receipt-card">
516
+ <div class="receipt-header">
517
+ <h1>{{ page.org_name }}</h1>
518
+ <div class="receipt-meta">
519
+ ЗАКАЗ #{{ order.order_id }}<br>
520
+ {{ order.timestamp }}
 
 
 
 
 
 
 
 
 
 
 
 
521
  </div>
522
+ </div>
523
+
524
+ {% for item in order.items %}
525
+ <div class="item-row">
526
+ {% if item.photo_url %}
527
+ <img src="{{ item.photo_url }}" class="item-thumb">
528
+ {% else %}
529
+ <div class="item-thumb"></div>
530
+ {% endif %}
531
+ <div class="item-details">
532
+ <div class="item-name">{{ item.name }}</div>
533
+ <div class="item-calc">{{ item.qty }} x {{ item.price }}</div>
534
  </div>
535
+ <div class="item-total">{{ "%.2f"|format(item.total|float) }}</div>
536
  </div>
537
+ {% endfor %}
538
 
539
+ <div class="total-row">
540
+ <span>ИТОГО:</span>
541
+ <span>{{ "%.2f"|format(order.total_price|float) }} {{ page.currency }}</span>
542
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
543
  </div>
544
+
545
+ {% set order_url = url_for('view_business_order', order_id=order.order_id, _external=True) %}
546
+ {% if page.order_destination == 'whatsapp' %}
547
+ {% set wa_text = "Здравствуйте! Я хочу оформить заказ. Вот детали: " + order_url %}
548
+ <a href="https://wa.me/{{ page.contact_number|replace('+','')|replace(' ','') }}?text={{ wa_text|urlencode }}" class="send-btn" target="_blank">
549
+ <i class="fa-brands fa-whatsapp"></i> Отправить заказ в WhatsApp
550
+ </a>
551
+ {% else %}
552
+ <a href="https://t.me/{{ page.contact_number|replace('@','') }}" class="send-btn telegram-btn" target="_blank" onclick="copyLink()">
553
+ <i class="fa-brands fa-telegram"></i> Отправить заказ в Telegram
554
+ </a>
555
+ <p style="text-align:center; color:#888; font-size:0.9em;">Нажмите кнопку, затем вставьте ссылку на заказ в диалог.</p>
556
+ <script>
557
+ function copyLink() {
558
+ navigator.clipboard.writeText("Здравствуйте! Мой заказ: {{ order_url }}");
559
+ }
560
+ </script>
561
+ {% endif %}
562
+
563
  </body></html>
564
  '''
565
 
 
665
  del user_data['files']
666
  user_data.setdefault('owned_business_pages', [])
667
 
668
+
669
  @cache.memoize(timeout=300)
670
  def load_data():
671
  try:
 
1679
  <div class="form-group">
1680
  <label class="checkbox-label">
1681
  <input type="checkbox" name="show_prices" value="true" {{ 'checked' if page and page.show_prices }}>
1682
+ <span>Указывать цены на товары</span>
1683
  </label>
1684
  </div>
1685
  <div class="form-group">
 
1743
  <div class="item-name-info">
1744
  <p class="item-name">{{ product.name }}</p>
1745
  {% if page.show_prices %}
1746
+ <p class="item-info">{{ "%.2f"|format(product.price|float) }} {{ page.currency }}</p>
1747
  {% endif %}
1748
  </div>
1749
  <button onclick="openProductModal('{{ product.id }}')" class="btn" style="background: var(--accent); padding: 8px 12px; font-size: 0.8em; margin-left: auto;"><i class="fa-solid fa-pencil"></i></button>
 
1774
  {% if page.show_prices %}
1775
  <div class="form-group">
1776
  <label for="price">Цена ({{ page.currency }})</label>
1777
+ <input type="number" name="price" id="price" step="0.01">
1778
  </div>
1779
  {% endif %}
1780
  <div class="form-group">
 
2727
  page = data.get('business_pages', {}).get(login)
2728
  if not page:
2729
  return "Страница не найдена.", 404
2730
+ return render_template_string(PUBLIC_BUSINESS_PAGE_HTML, page=page, hf_file_url_jinja=lambda path, download=False: f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{quote(path)}{'?download=true' if download else ''}")
2731
 
2732
+ @app.route('/api/create_order/<login>', methods=['POST'])
2733
+ def create_business_order(login):
2734
  data = load_data()
2735
  page = data.get('business_pages', {}).get(login)
2736
+ if not page: return jsonify({'status': 'error', 'message': 'Страница не найдена'}), 404
2737
+
2738
+ req_data = request.json
2739
+ items_dict = req_data.get('items', {})
2740
+ if not items_dict: return jsonify({'status': 'error', 'message': 'Корзина пуста'}), 400
2741
+
2742
+ products_map = {p['id']: p for p in page['products']}
2743
+ order_items = []
2744
+ total_price = 0.0
2745
+
2746
+ for pid, info in items_dict.items():
2747
+ product = products_map.get(pid)
2748
+ if product:
2749
+ qty = info.get('qty', 1)
2750
+ price = float(product.get('price', 0))
2751
+ total = price * qty
2752
+ total_price += total
2753
+ photo_url = None
2754
+ if product.get('photo_path'):
2755
+ photo_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{quote(product['photo_path'])}"
2756
+
2757
+ order_items.append({
2758
+ 'name': product['name'],
2759
+ 'price': price,
2760
+ 'qty': qty,
2761
+ 'total': total,
2762
+ 'photo_url': photo_url
2763
+ })
2764
+
2765
+ order_id = uuid.uuid4().hex[:10].upper()
2766
+ new_order = {
2767
+ 'order_id': order_id,
2768
+ 'page_login': login,
2769
+ 'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
2770
+ 'items': order_items,
2771
+ 'total_price': total_price
2772
+ }
2773
 
2774
+ data.setdefault('orders', {})[order_id] = new_order
2775
  try:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2776
  save_data(data)
2777
+ return jsonify({'status': 'success', 'redirect_url': url_for('view_business_order', order_id=order_id)})
 
2778
  except Exception as e:
2779
+ return jsonify({'status': 'error', 'message': str(e)}), 500
 
2780
 
2781
  @app.route('/order/<order_id>')
2782
+ def view_business_order(order_id):
2783
  data = load_data()
2784
  order = data.get('orders', {}).get(order_id)
2785
+ if not order: return "Заказ не найден", 404
 
2786
 
2787
  page = data.get('business_pages', {}).get(order['page_login'])
2788
+ if not page: return "Страница магазина не найдена", 404
2789
+
2790
+ return render_template_string(PUBLIC_ORDER_PAGE_HTML, order=order, page=page)
 
2791
 
2792
  @app.route('/tma_business/manage/<login>')
2793
  def tma_manage_products(login):
 
3435
 
3436
  threading.Thread(target=check_reminders, daemon=True).start()
3437
 
3438
+ app.run(debug=False, host='0.0.0.0', port=7860)