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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +261 -280
app.py CHANGED
@@ -299,115 +299,43 @@ body { padding-bottom: 30px; }
299
  </body></html>
300
  '''
301
 
302
- ORDER_RECEIPT_HTML = '''
303
- <!DOCTYPE html><html lang="ru"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
304
- <title>Заказ #{{ order.id[-8:] }}</title>
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
- <style>''' + BASE_STYLE + '''
309
- body { padding: 20px; background: var(--background-dark); max-width: 600px; margin: 0 auto; }
310
- .receipt-card { background: var(--card-bg-dark); border-radius: 16px; padding: 20px; box-shadow: var(--shadow); border: 1px solid #333; }
311
- .order-header { text-align: center; border-bottom: 1px solid #444; padding-bottom: 15px; margin-bottom: 15px; }
312
- .order-item { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; padding-bottom: 10px; border-bottom: 1px solid #2a2a2a; }
313
- .order-item img { width: 50px; height: 50px; object-fit: cover; border-radius: 6px; margin-right: 10px; background: #333; }
314
- .item-details { flex-grow: 1; }
315
- .item-title { font-weight: 600; display: block; }
316
- .item-qty { font-size: 0.85em; color: var(--text-muted); }
317
- .item-price { font-weight: bold; color: var(--secondary); }
318
- .total-section { text-align: right; font-size: 1.3em; font-weight: bold; margin-top: 20px; color: var(--text-dark); }
319
- .send-btn { display: block; width: 100%; text-align: center; padding: 15px; border-radius: 12px; margin-top: 25px; text-decoration: none; font-weight: bold; color: white; font-size: 1.1em; }
320
- .whatsapp { background-color: #25D366; }
321
- .telegram { background-color: #0088cc; }
322
- </style></head><body>
323
-
324
- <div class="receipt-card">
325
- <div class="order-header">
326
- <h2>Ваш заказ</h2>
327
- <p style="color: var(--text-muted)">{{ page.org_name }}</p>
328
- <small>#{{ order.id }}</small><br>
329
- <small>{{ order.created_at }}</small>
330
- </div>
331
-
332
- <div id="items-list">
333
- {% for item in order.items %}
334
- <div class="order-item">
335
- {% if item.photo %}
336
- <img src="{{ hf_file_url_jinja(item.photo) }}" alt="">
337
- {% else %}
338
- <div style="width:50px; height:50px; background:#333; border-radius:6px; margin-right:10px;"></div>
339
- {% endif %}
340
- <div class="item-details">
341
- <span class="item-title">{{ item.name }}</span>
342
- <span class="item-qty">{{ item.qty }} шт. x {{ "%.2f"|format(item.price) }}</span>
343
- </div>
344
- <div class="item-price">{{ "%.2f"|format(item.total_item_price) }} {{ page.currency }}</div>
345
- </div>
346
- {% endfor %}
347
- </div>
348
-
349
- <div class="total-section">
350
- Итого: {{ "%.2f"|format(order.total_price) }} {{ page.currency }}
351
- </div>
352
-
353
- {% set link_url = url_for('view_business_order', order_id=order.id, _external=True) %}
354
- {% set msg_text = "Здравствуйте! Я оформил заказ в вашем магазине.\n\nСумма: " + ("%.2f"|format(order.total_price)) + " " + page.currency + "\n\nСсылка на заказ:\n" + link_url %}
355
-
356
- {% if page.order_destination == 'whatsapp' %}
357
- <a href="https://wa.me/{{ page.contact_number }}?text={{ msg_text|urlencode }}" class="send-btn whatsapp" target="_blank">
358
- <i class="fab fa-whatsapp"></i> Отправить заказ в WhatsApp
359
- </a>
360
- {% else %}
361
- <a href="https://t.me/{{ page.contact_number }}?text={{ msg_text|urlencode }}" class="send-btn telegram" target="_blank">
362
- <i class="fab fa-telegram"></i> Отправить заказ в Telegram
363
- </a>
364
- {% endif %}
365
- </div>
366
- </body></html>
367
- '''
368
-
369
  PUBLIC_BUSINESS_PAGE_HTML = '''
370
- <!DOCTYPE html><html lang="ru"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
371
  <title>{{ page.org_name }}</title>
372
  <link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
373
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;800&display=swap" rel="stylesheet">
374
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
 
375
  <style>''' + BASE_STYLE + '''
376
- body { background: var(--card-bg-dark); padding-bottom: 100px; }
377
- .container { max-width: 800px; padding: 20px 10px; }
378
- .biz-header { text-align: center; margin-bottom: 20px; }
379
- .biz-avatar { width: 90px; height: 90px; border-radius: 50%; object-fit: cover; margin: 0 auto 10px auto; border: 3px solid var(--accent); }
380
- .biz-header h1 { font-size: 1.8em; color: var(--text-dark); margin-bottom: 5px; }
381
-
382
- /* 2-Column Grid like WB */
383
- .product-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; padding: 0; }
384
- .product-card { background: var(--background-dark); border-radius: 10px; overflow: hidden; position: relative; display: flex; flex-direction: column; height: 100%; border: 1px solid #2a2a2a; }
385
- .product-image { width: 100%; aspect-ratio: 1/1.2; object-fit: cover; background-color: #222; display: block;}
386
- .product-info { padding: 8px; display: flex; flex-direction: column; flex-grow: 1; }
387
- .product-price { font-size: 1.1em; font-weight: 800; color: var(--text-dark); margin-bottom: 2px; }
388
- .product-currency { font-size: 0.8em; }
389
- .product-name { font-size: 0.85em; font-weight: 400; line-height: 1.2; margin-bottom: 8px; color: #ccc; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; flex-grow: 1;}
390
- .add-btn { background: var(--card-bg-dark); border: 1px solid var(--accent); color: var(--accent); border-radius: 8px; padding: 8px 0; width: 100%; font-weight: 600; font-size: 0.9em; cursor: pointer; margin-top: auto; transition: all 0.2s; }
391
- .add-btn:active { background: var(--accent); color: white; }
392
-
393
- /* Cart Floating Button */
394
- .cart-fab { position: fixed; bottom: 20px; right: 20px; background: var(--primary); width: 60px; height: 60px; border-radius: 50%; display: flex; align-items: center; justify-content: center; box-shadow: 0 5px 20px rgba(255, 77, 109, 0.5); cursor: pointer; z-index: 1000; transition: transform 0.2s; display: none; }
395
- .cart-fab:active { transform: scale(0.9); }
396
- .cart-count { position: absolute; top: 0; right: 0; background: white; color: var(--primary); border-radius: 50%; width: 22px; height: 22px; font-size: 12px; font-weight: bold; display: flex; align-items: center; justify-content: center; border: 2px solid var(--primary); }
397
-
398
- /* Cart Modal */
399
- #cart-modal .modal-content { background: #181818; max-height: 90vh; display: flex; flex-direction: column; }
400
- .cart-header { padding: 15px; font-weight: bold; font-size: 1.2em; border-bottom: 1px solid #333; display: flex; justify-content: space-between; }
401
- .cart-items { flex-grow: 1; overflow-y: auto; padding: 15px; }
402
- .cart-item { display: flex; gap: 10px; margin-bottom: 15px; align-items: center; background: #222; padding: 10px; border-radius: 10px; }
403
- .cart-item img { width: 50px; height: 50px; object-fit: cover; border-radius: 6px; }
404
- .cart-item-details { flex-grow: 1; }
405
- .cart-item-title { font-size: 0.9em; line-height: 1.1; margin-bottom: 4px; display: block; }
406
- .cart-item-price { font-weight: bold; color: var(--secondary); font-size: 0.95em; }
407
- .cart-controls { display: flex; align-items: center; gap: 10px; background: #333; border-radius: 6px; padding: 2px 8px; }
408
- .cart-btn { background: none; border: none; color: white; font-size: 1.2em; padding: 0 5px; cursor: pointer; }
409
- .cart-footer { padding: 15px; border-top: 1px solid #333; background: #181818; }
410
- .checkout-btn { width: 100%; background: var(--primary); color: white; border: none; padding: 15px; border-radius: 12px; font-size: 1.1em; font-weight: bold; cursor: pointer; }
411
  </style></head><body>
412
  <div class="container">
413
  <div class="biz-header">
@@ -417,175 +345,245 @@ body { background: var(--card-bg-dark); padding-bottom: 100px; }
417
  <h1>{{ page.org_name }}</h1>
418
  </div>
419
 
420
- {% if page.products %}
421
  <div class="product-grid">
422
  {% for product in page.products %}
423
  <div class="product-card">
424
- {% if product.photo_path %}
425
- <img src="{{ hf_file_url_jinja(product.photo_path) }}" class="product-image" loading="lazy">
426
- {% else %}
427
- <div class="product-image" style="display: flex; align-items: center; justify-content: center; background: #333;"><i class="fa-solid fa-image" style="font-size:2em; color:#555;"></i></div>
428
- {% endif %}
 
 
429
  <div class="product-info">
430
  {% if page.show_prices and product.price %}
431
- <div class="product-price">{{ "%.0f"|format(product.price|float) if product.price % 1 == 0 else product.price }} <span class="product-currency">{{ page.currency }}</span></div>
432
  {% endif %}
433
  <div class="product-name">{{ product.name }}</div>
434
- {% if page.show_prices %}
435
- <button class="add-btn" onclick="addToCart('{{ product.id }}', '{{ product.name|replace("'", "") }}', {{ product.price or 0 }}, '{{ product.photo_path or "" }}')">В корзину</button>
 
 
 
 
436
  {% endif %}
437
  </div>
438
  </div>
439
  {% endfor %}
440
  </div>
441
- {% else %}
442
- <p style="text-align: center; color: #777;">Товары скоро появятся.</p>
443
- {% endif %}
444
  </div>
445
 
446
- <div class="cart-fab" id="cart-fab" onclick="openCart()">
447
- <i class="fa-solid fa-cart-shopping" style="font-size: 1.5em; color: white;"></i>
448
- <div class="cart-count" id="cart-count">0</div>
 
449
  </div>
450
 
451
- <div class="modal" id="cart-modal" onclick="if(event.target === this) closeCart()">
452
- <div class="modal-content">
453
- <div class="cart-header">
454
- <span>Корзина</span>
455
- <span onclick="closeCart()" style="cursor:pointer">&times;</span>
456
- </div>
457
- <div class="cart-items" id="cart-items-container">
458
- <!-- JS render -->
459
  </div>
460
- <div class="cart-footer">
461
- <button class="checkout-btn" onclick="checkout()" id="checkout-btn">Оформить за 0 {{ page.currency }}</button>
 
 
462
  </div>
 
463
  </div>
464
  </div>
 
465
 
466
  <script>
467
- let cart = JSON.parse(localStorage.getItem('cart_{{ page.login }}')) || {};
468
- const CURRENCY = '{{ page.currency }}';
469
-
470
- function addToCart(id, name, price, photo) {
471
- if (!cart[id]) {
472
- cart[id] = { name: name, price: parseFloat(price), photo: photo, qty: 0 };
 
 
 
 
 
 
 
 
 
473
  }
474
- cart[id].qty += 1;
475
- updateCartUI();
476
- openCart(); // Feedback
477
- }
478
 
479
- function removeFromCart(id) {
480
- if (cart[id]) {
481
- cart[id].qty -= 1;
482
- if (cart[id].qty <= 0) delete cart[id];
 
 
 
 
 
 
 
 
 
 
 
483
  updateCartUI();
484
  }
485
- }
486
 
487
- function updateCartUI() {
488
- localStorage.setItem('cart_{{ page.login }}', JSON.stringify(cart));
489
- const totalQty = Object.values(cart).reduce((sum, item) => sum + item.qty, 0);
490
- const fab = document.getElementById('cart-fab');
491
- const countEl = document.getElementById('cart-count');
492
-
493
- if (totalQty > 0) {
494
- fab.style.display = 'flex';
495
- countEl.textContent = totalQty;
496
- } else {
497
- fab.style.display = 'none';
498
- closeCart();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
499
  }
500
- renderCartModal();
501
- }
502
 
503
- function renderCartModal() {
504
- const container = document.getElementById('cart-items-container');
505
- container.innerHTML = '';
506
- let totalSum = 0;
507
 
508
- Object.keys(cart).forEach(id => {
509
- const item = cart[id];
510
- totalSum += item.qty * item.price;
511
-
512
- let imgHTML = '<div style="width:50px; height:50px; background:#333; border-radius:6px;"></div>';
513
- if (item.photo) {
514
- const imgUrl = `https://huggingface.co/datasets/{{ REPO_ID }}/resolve/main/${item.photo}`;
515
- imgHTML = `<img src="${imgUrl}" alt="">`;
516
  }
 
517
 
518
- const div = document.createElement('div');
519
- div.className = 'cart-item';
520
- div.innerHTML = `
521
- ${imgHTML}
522
- <div class="cart-item-details">
523
- <span class="cart-item-title">${item.name}</span>
524
- <span class="cart-item-price">${item.price} ${CURRENCY}</span>
525
- </div>
526
- <div class="cart-controls">
527
- <button class="cart-btn" onclick="removeFromCart('${id}')">-</button>
528
- <span style="font-weight:bold; width:20px; text-align:center;">${item.qty}</span>
529
- <button class="cart-btn" onclick="addToCart('${id}', '', 0, '')">+</button>
530
- </div>
531
- `;
532
- container.appendChild(div);
533
- });
534
-
535
- document.getElementById('checkout-btn').textContent = `Оформить за ${totalSum.toFixed(2)} ${CURRENCY}`;
536
- }
537
-
538
- function openCart() {
539
- renderCartModal();
540
- document.getElementById('cart-modal').style.display = 'flex';
541
- }
542
-
543
- function closeCart() {
544
- document.getElementById('cart-modal').style.display = 'none';
545
- }
546
-
547
- async function checkout() {
548
- if (Object.keys(cart).length === 0) return;
549
-
550
- const btn = document.getElementById('checkout-btn');
551
- btn.disabled = true;
552
- btn.textContent = 'Загрузка...';
553
-
554
- const payload = {
555
- items: Object.keys(cart).map(id => ({
556
- id: id,
557
- qty: cart[id].qty
558
- }))
559
- };
560
-
561
- try {
562
- const res = await fetch("{{ url_for('create_business_order', login=page.login) }}", {
563
- method: 'POST',
564
- headers: {'Content-Type': 'application/json'},
565
- body: JSON.stringify(payload)
566
- });
567
- const data = await res.json();
568
 
569
- if (data.status === 'success') {
570
- localStorage.removeItem('cart_{{ page.login }}');
571
- window.location.href = data.redirect_url;
572
- } else {
573
- alert(data.message || 'Ошибка создания заказа');
 
 
 
 
 
 
 
 
 
 
 
 
 
574
  btn.disabled = false;
575
- updateCartUI();
576
  }
577
- } catch (e) {
578
- alert('Сетевая ошибка');
579
- btn.disabled = false;
580
- updateCartUI();
581
  }
582
- }
583
 
584
- document.addEventListener('DOMContentLoaded', updateCartUI);
585
  </script>
586
  </body></html>
587
  '''
588
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
589
  def find_node_by_id(filesystem, node_id):
590
  if not filesystem: return None, None
591
  if filesystem.get('id') == node_id:
@@ -688,7 +686,6 @@ def initialize_user_filesystem_tma(user_data, tma_user_id_str):
688
  del user_data['files']
689
  user_data.setdefault('owned_business_pages', [])
690
 
691
-
692
  @cache.memoize(timeout=300)
693
  def load_data():
694
  try:
@@ -1702,7 +1699,7 @@ TMA_CREATE_EDIT_BUSINESS_FORM_HTML = '''
1702
  <div class="form-group">
1703
  <label class="checkbox-label">
1704
  <input type="checkbox" name="show_prices" value="true" {{ 'checked' if page and page.show_prices }}>
1705
- <span>Указывать цены на товары</span>
1706
  </label>
1707
  </div>
1708
  <div class="form-group">
@@ -1766,7 +1763,7 @@ TMA_MANAGE_PRODUCTS_HTML = '''
1766
  <div class="item-name-info">
1767
  <p class="item-name">{{ product.name }}</p>
1768
  {% if page.show_prices %}
1769
- <p class="item-info">{{ "%.2f"|format(product.price|float) }} {{ page.currency }}</p>
1770
  {% endif %}
1771
  </div>
1772
  <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>
@@ -1797,7 +1794,7 @@ TMA_MANAGE_PRODUCTS_HTML = '''
1797
  {% if page.show_prices %}
1798
  <div class="form-group">
1799
  <label for="price">Цена ({{ page.currency }})</label>
1800
- <input type="number" name="price" id="price" step="0.01">
1801
  </div>
1802
  {% endif %}
1803
  <div class="form-group">
@@ -2750,70 +2747,54 @@ def public_business_page(login):
2750
  page = data.get('business_pages', {}).get(login)
2751
  if not page:
2752
  return "Страница не найдена.", 404
2753
- 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 ''}", REPO_ID=REPO_ID)
2754
 
2755
- @app.route('/business/<login>/create_order', methods=['POST'])
2756
- def create_business_order(login):
2757
  data = load_data()
2758
  page = data.get('business_pages', {}).get(login)
2759
- if not page: return jsonify({'status': 'error', 'message': 'Страница не найдена.'}), 404
2760
-
 
2761
  try:
2762
- payload = request.json
2763
- items_req = payload.get('items', [])
2764
- if not items_req:
2765
  return jsonify({'status': 'error', 'message': 'Корзина пуста'}), 400
2766
-
2767
- final_items = []
2768
- total_price = 0
2769
- products_map = {p['id']: p for p in page.get('products', [])}
2770
-
2771
- for item in items_req:
2772
- pid = item.get('id')
2773
- qty = int(item.get('qty', 1))
2774
- if pid in products_map:
2775
- prod = products_map[pid]
2776
- price = float(prod.get('price', 0))
2777
- final_items.append({
2778
- 'name': prod['name'],
2779
- 'price': price,
2780
- 'qty': qty,
2781
- 'photo': prod.get('photo_path'),
2782
- 'total_item_price': price * qty
2783
- })
2784
- total_price += price * qty
2785
-
2786
- if not final_items:
2787
- return jsonify({'status': 'error', 'message': 'Товары не найдены'}), 400
2788
-
2789
  order_id = uuid.uuid4().hex
2790
  order_data = {
2791
  'id': order_id,
2792
  'page_login': login,
2793
- 'items': final_items,
2794
- 'total_price': total_price,
2795
- 'created_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
2796
  }
2797
-
2798
- data.setdefault('orders', {})[order_id] = order_data
2799
- save_data(data)
2800
 
2801
- return jsonify({'status': 'success', 'redirect_url': url_for('view_business_order', order_id=order_id)})
 
2802
 
 
2803
  except Exception as e:
2804
- logging.error(f"Order creation error: {e}")
2805
- return jsonify({'status': 'error', 'message': 'Ошибка сервера'}), 500
2806
 
2807
  @app.route('/order/<order_id>')
2808
- def view_business_order(order_id):
2809
  data = load_data()
2810
  order = data.get('orders', {}).get(order_id)
2811
- if not order: return "Заказ не найден", 404
 
2812
 
2813
  page = data.get('business_pages', {}).get(order['page_login'])
2814
- if not page: return "Страница магазина удалена", 404
2815
-
2816
- return render_template_string(ORDER_RECEIPT_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 ''}")
 
2817
 
2818
  @app.route('/tma_business/manage/<login>')
2819
  def tma_manage_products(login):
 
299
  </body></html>
300
  '''
301
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
302
  PUBLIC_BUSINESS_PAGE_HTML = '''
303
+ <!DOCTYPE html><html lang="ru"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
304
  <title>{{ page.org_name }}</title>
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">
 
345
  <h1>{{ page.org_name }}</h1>
346
  </div>
347
 
 
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>
516
  '''
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
+
587
  def find_node_by_id(filesystem, node_id):
588
  if not filesystem: return None, None
589
  if filesystem.get('id') == node_id:
 
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
  <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
  <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
  {% 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
  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):