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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +363 -267
app.py CHANGED
@@ -1,5 +1,4 @@
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
@@ -309,30 +308,45 @@ PUBLIC_BUSINESS_PAGE_HTML = '''
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">
@@ -342,224 +356,294 @@ body { background: var(--card-bg-dark); padding-bottom: 80px; }
342
  <h1>{{ page.org_name }}</h1>
343
  </div>
344
 
 
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>
489
  '''
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
 
@@ -2729,66 +2813,6 @@ def public_business_page(login):
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):
2794
  if 'telegram_user_id' not in session: return redirect(url_for('tma_entry_page'))
@@ -2913,6 +2937,78 @@ def tma_delete_product(login, product_id):
2913
 
2914
  return redirect(url_for('tma_manage_products', login=login))
2915
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2916
 
2917
  ADMIN_LOGIN_HTML = '''
2918
  <!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>Admin Login</title>
@@ -3435,4 +3531,4 @@ if __name__ == '__main__':
3435
 
3436
  threading.Thread(target=check_reminders, daemon=True).start()
3437
 
3438
- app.run(debug=False, host='0.0.0.0', port=7860)
 
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
 
308
  <style>''' + BASE_STYLE + '''
309
  body { background: var(--card-bg-dark); padding-bottom: 80px; }
310
  .container { max-width: 800px; padding-top: 20px; }
311
+ .biz-header { text-align: center; margin-bottom: 30px; }
312
+ .biz-avatar { width: 100px; height: 100px; border-radius: 50%; object-fit: cover; margin: 0 auto 15px auto; border: 3px solid var(--accent); }
313
+ .biz-header h1 { font-size: 2em; color: var(--text-dark); }
314
+ .product-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 15px; }
315
+ @media (max-width: 600px) { .product-grid { grid-template-columns: 1fr; } }
316
+ .product-card { background: var(--background-dark); border-radius: 16px; overflow: hidden; box-shadow: 0 5px 15px rgba(0,0,0,0.2); transition: var(--transition); display: flex; flex-direction: column; }
317
+ .product-card:hover { transform: translateY(-5px); }
318
+ .product-image { width: 100%; padding-top: 100%; position: relative; background-color: #2a2a2a; }
319
+ .product-image img { position: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: cover; }
320
+ .product-info { padding: 15px; flex-grow: 1; display: flex; flex-direction: column; }
321
+ .product-name { font-size: 1.1em; font-weight: 600; margin-bottom: 5px; flex-grow: 1; }
322
+ .product-price { font-size: 1.2em; font-weight: bold; color: var(--secondary); margin-bottom: 10px; }
323
+ .product-desc { font-size: 0.9em; color: var(--text-muted); margin-bottom: 15px; }
324
+ .add-to-cart-btn { margin-top: auto; background: var(--accent); }
325
+ .add-to-cart-btn:hover { background: var(--accent); filter: brightness(1.2); }
326
+ .order-fab { position: fixed; bottom: 20px; right: 20px; z-index: 100; }
327
+ .order-btn { display: flex; align-items: center; gap: 10px; padding: 15px 25px; border-radius: 30px; font-size: 1.1em; font-weight: 600; box-shadow: var(--shadow); }
328
+ .order-btn.whatsapp { background: #25D366; color: white; }
329
+ .order-btn.telegram { background: #0088cc; color: white; }
330
+ .order-btn i { font-size: 1.4em; }
331
+ .cart-fab { position: fixed; bottom: 20px; left: 20px; z-index: 1050; width: 60px; height: 60px; background: var(--accent); border-radius: 50%; display: flex; align-items: center; justify-content: center; color: white; font-size: 24px; cursor: pointer; box-shadow: var(--shadow); transition: transform 0.3s; }
332
+ .cart-fab .cart-count { position: absolute; top: 0; right: 0; background: var(--primary); color: white; 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(--accent); }
333
+ .cart-modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.7); z-index: 2000; }
334
+ .cart-modal.visible { display: block; }
335
+ .cart-modal-content { position: fixed; bottom: 0; left: 0; right: 0; background: var(--card-bg-dark); border-top-left-radius: 20px; border-top-right-radius: 20px; padding: 20px; max-height: 80vh; display: flex; flex-direction: column; transform: translateY(100%); animation: slideUp 0.4s cubic-bezier(0.25, 0.8, 0.25, 1) forwards; }
336
  @keyframes slideUp { from { transform: translateY(100%); } to { transform: translateY(0); } }
337
+ .cart-modal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; border-bottom: 1px solid #333; padding-bottom: 10px; }
338
+ .cart-modal-header h2 { margin: 0; }
339
+ .cart-modal-close { font-size: 24px; cursor: pointer; background: none; border: none; color: var(--text-muted); }
340
+ .cart-items { flex-grow: 1; overflow-y: auto; }
341
+ .cart-item { display: flex; align-items: center; gap: 15px; margin-bottom: 15px; }
342
+ .cart-item-img { width: 50px; height: 50px; object-fit: cover; border-radius: 8px; }
343
+ .cart-item-details { flex-grow: 1; }
344
+ .cart-item-name { font-weight: 500; }
345
+ .cart-item-price { font-size: 0.9em; color: var(--text-muted); }
346
+ .cart-quantity { display: flex; align-items: center; gap: 8px; }
347
+ .cart-quantity button { width: 28px; height: 28px; border-radius: 50%; background: #333; color: white; border: none; cursor: pointer; font-weight: bold; }
348
+ .cart-footer { margin-top: 20px; border-top: 1px solid #333; padding-top: 15px; }
349
+ .cart-total { display: flex; justify-content: space-between; font-size: 1.2em; font-weight: bold; margin-bottom: 15px; }
350
  </style></head><body>
351
  <div class="container">
352
  <div class="biz-header">
 
356
  <h1>{{ page.org_name }}</h1>
357
  </div>
358
 
359
+ {% if page.products %}
360
  <div class="product-grid">
361
  {% for product in page.products %}
362
  <div class="product-card">
363
+ <div class="product-image">
364
+ {% if product.photo_path %}
365
+ <img src="{{ hf_file_url_jinja(product.photo_path) }}" alt="{{ product.name }}">
366
+ {% endif %}
367
+ </div>
368
  <div class="product-info">
369
+ <h3 class="product-name">{{ product.name }}</h3>
370
+ <p class="product-desc">{{ product.description }}</p>
371
  {% if page.show_prices and product.price %}
372
+ <p class="product-price">{{ "%.2f"|format(product.price|float) }} {{ page.currency }}</p>
373
+ <button class="btn add-to-cart-btn"
374
+ data-id="{{ product.id }}"
375
+ data-name="{{ product.name }}"
376
+ data-price="{{ product.price }}"
377
+ data-image="{{ product.photo_path or '' }}">
378
+ <i class="fa-solid fa-cart-plus"></i> В корзину
379
+ </button>
380
  {% endif %}
381
  </div>
382
  </div>
383
  {% endfor %}
384
  </div>
385
+ {% else %}
386
+ <p style="text-align: center;">Товары скоро появятся.</p>
387
+ {% endif %}
388
+ </div>
389
+
390
+ <div class="order-fab">
391
+ {% set phone_number = page.contact_number | replace('+', '') | replace(' ', '') %}
392
+ {% if page.order_destination == 'whatsapp' %}
393
+ <a href="https://wa.me/{{ phone_number }}" class="btn order-btn whatsapp" target="_blank">
394
+ <i class="fab fa-whatsapp"></i> Заказать
395
+ </a>
396
+ {% elif page.order_destination == 'telegram' %}
397
+ <a href="https://t.me/{{ phone_number }}" class="btn order-btn telegram" target="_blank">
398
+ <i class="fab fa-telegram"></i> Заказать
399
+ </a>
400
+ {% endif %}
401
  </div>
402
 
403
  {% if page.show_prices %}
404
+ <div id="cart-fab" class="cart-fab">
405
+ <i class="fa-solid fa-shopping-cart"></i>
406
+ <span id="cart-count" class="cart-count">0</span>
407
  </div>
408
 
409
+ <div id="cart-modal" class="cart-modal">
410
+ <div class="cart-modal-content">
411
+ <div class="cart-modal-header">
412
+ <h2>Корзина</h2>
413
+ <button id="cart-modal-close" class="cart-modal-close">&times;</button>
414
  </div>
415
+ <div id="cart-items-container" class="cart-items">
416
+ <p>Корзина пуста.</p>
417
+ </div>
418
+ <div class="cart-footer">
419
+ <div class="cart-total">
420
+ <span>Итого:</span>
421
+ <span id="cart-total-price">0.00 {{ page.currency }}</span>
422
+ </div>
423
+ <button id="checkout-btn" class="btn business-btn" style="width: 100%;">Оформить заказ</button>
424
  </div>
 
425
  </div>
426
  </div>
 
 
 
 
427
  {% endif %}
 
428
  <script>
429
+ document.addEventListener('DOMContentLoaded', () => {
430
+ {% if page.show_prices %}
431
+ const cartManager = {
432
+ login: '{{ page.login }}',
433
+ currency: '{{ page.currency }}',
434
+ cart: {},
435
+
436
+ init() {
437
+ this.load();
438
+ this.render();
439
+ this.attachEventListeners();
440
+ },
441
+
442
+ load() {
443
+ this.cart = JSON.parse(localStorage.getItem(`cart_${this.login}`)) || {};
444
+ },
445
+
446
+ save() {
447
+ localStorage.setItem(`cart_${this.login}`, JSON.stringify(this.cart));
448
+ },
449
+
450
+ add(id, name, price, image) {
451
+ if (this.cart[id]) {
452
+ this.cart[id].quantity++;
453
+ } else {
454
+ this.cart[id] = { name, price: parseFloat(price), image, quantity: 1 };
455
+ }
456
+ this.save();
457
+ this.render();
458
+ },
459
+
460
+ updateQuantity(id, change) {
461
+ if (this.cart[id]) {
462
+ this.cart[id].quantity += change;
463
+ if (this.cart[id].quantity <= 0) {
464
+ delete this.cart[id];
465
+ }
466
+ this.save();
467
+ this.render();
468
+ }
469
+ },
470
+
471
+ clear() {
472
+ this.cart = {};
473
+ this.save();
474
+ this.render();
475
+ },
476
+
477
+ render() {
478
+ const totalItems = Object.values(this.cart).reduce((sum, item) => sum + item.quantity, 0);
479
+ const totalPrice = Object.values(this.cart).reduce((sum, item) => sum + item.price * item.quantity, 0);
480
+
481
+ document.getElementById('cart-count').textContent = totalItems;
482
+ document.getElementById('cart-total-price').textContent = `${totalPrice.toFixed(2)} ${this.currency}`;
483
+
484
+ const itemsContainer = document.getElementById('cart-items-container');
485
+ itemsContainer.innerHTML = '';
486
+
487
+ if (totalItems === 0) {
488
+ itemsContainer.innerHTML = '<p style="text-align:center; color:var(--text-muted);">Корзина пуста</p>';
489
+ document.getElementById('checkout-btn').disabled = true;
490
+ } else {
491
+ document.getElementById('checkout-btn').disabled = false;
492
+ for (const [id, item] of Object.entries(this.cart)) {
493
+ const itemEl = document.createElement('div');
494
+ itemEl.className = 'cart-item';
495
+ const imageHtml = item.image ? `<img src="{{ hf_file_url_jinja('') }}${item.image}" class="cart-item-img">` : '<div class="cart-item-img" style="background:#333"></div>';
496
+ itemEl.innerHTML = `
497
+ ${imageHtml}
498
+ <div class="cart-item-details">
499
+ <div class="cart-item-name">${item.name}</div>
500
+ <div class="cart-item-price">${item.price.toFixed(2)} ${this.currency}</div>
501
+ </div>
502
+ <div class="cart-quantity">
503
+ <button class="quantity-btn" data-id="${id}" data-change="-1">-</button>
504
+ <span>${item.quantity}</span>
505
+ <button class="quantity-btn" data-id="${id}" data-change="1">+</button>
506
+ </div>
507
+ `;
508
+ itemsContainer.appendChild(itemEl);
509
+ }
510
+ }
511
+ },
512
+
513
+ async checkout() {
514
+ const btn = document.getElementById('checkout-btn');
515
+ btn.disabled = true;
516
+ btn.innerHTML = '<div class="loading-spinner" style="width:20px; height:20px; border-width:2px;"></div>';
517
+
518
+ try {
519
+ const response = await fetch("{{ url_for('create_order', login=page.login) }}", {
520
+ method: 'POST',
521
+ headers: { 'Content-Type': 'application/json' },
522
+ body: JSON.stringify(this.cart)
523
+ });
524
+ const result = await response.json();
525
+ if (result.status === 'success' && result.order_url) {
526
+ this.clear();
527
+ window.location.href = result.order_url;
528
+ } else {
529
+ throw new Error(result.message || 'Не удалось создать заказ.');
530
+ }
531
+ } catch (error) {
532
+ alert('Ошибка: ' + error.message);
533
+ btn.disabled = false;
534
+ btn.innerHTML = 'Оформить заказ';
535
+ }
536
+ },
537
+
538
+ attachEventListeners() {
539
+ document.querySelectorAll('.add-to-cart-btn').forEach(btn => {
540
+ btn.addEventListener('click', (e) => {
541
+ const data = e.currentTarget.dataset;
542
+ this.add(data.id, data.name, data.price, data.image);
543
+
544
+ // Animation
545
+ const cartFab = document.getElementById('cart-fab');
546
+ cartFab.style.transform = 'scale(1.2)';
547
+ setTimeout(() => cartFab.style.transform = 'scale(1)', 200);
548
+ });
549
+ });
550
+
551
+ const cartModal = document.getElementById('cart-modal');
552
+ document.getElementById('cart-fab').addEventListener('click', () => cartModal.classList.add('visible'));
553
+ document.getElementById('cart-modal-close').addEventListener('click', () => cartModal.classList.remove('visible'));
554
+ cartModal.addEventListener('click', (e) => {
555
+ if (e.target.id === 'cart-modal') {
556
+ cartModal.classList.remove('visible');
557
+ }
558
+ });
559
 
560
+ document.getElementById('cart-items-container').addEventListener('click', (e) => {
561
+ if (e.target.classList.contains('quantity-btn')) {
562
+ const id = e.target.dataset.id;
563
+ const change = parseInt(e.target.dataset.change);
564
+ this.updateQuantity(id, change);
565
+ }
566
+ });
567
+
568
+ document.getElementById('checkout-btn').addEventListener('click', () => this.checkout());
 
 
 
 
 
 
 
 
569
  }
570
+ };
571
+
572
+ cartManager.init();
573
+ {% endif %}
574
+ });
 
 
575
  </script>
576
  </body></html>
577
  '''
578
 
579
  PUBLIC_ORDER_PAGE_HTML = '''
580
  <!DOCTYPE html><html lang="ru"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
581
+ <title>Заказ {{ order.id[:8] }}</title>
582
+ <link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
583
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;800&display=swap" rel="stylesheet">
584
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
585
  <style>''' + BASE_STYLE + '''
586
+ body { background: var(--card-bg-dark); }
587
+ .container { max-width: 800px; padding-top: 20px; padding-bottom: 100px; }
588
+ .order-header { text-align: center; margin-bottom: 20px; }
589
+ .order-header h1 { font-size: 1.8em; }
590
+ .order-header p { color: var(--text-muted); }
591
+ .order-details { background: var(--background-dark); border-radius: 16px; padding: 20px; margin-bottom: 20px; }
592
+ .order-item { display: flex; align-items: center; gap: 15px; padding: 10px 0; border-bottom: 1px solid #333; }
593
+ .order-item:last-child { border-bottom: none; }
594
+ .item-image { width: 60px; height: 60px; object-fit: cover; border-radius: 8px; background-color: #2a2a2a; flex-shrink: 0; }
595
+ .item-info { flex-grow: 1; }
596
+ .item-name { font-weight: 600; }
597
+ .item-price { font-size: 0.9em; color: var(--text-muted); }
598
+ .item-subtotal { font-weight: bold; }
599
+ .order-total { text-align: right; font-size: 1.3em; font-weight: bold; margin-top: 20px; }
600
+ .send-order-fab { position: fixed; bottom: 20px; right: 20px; z-index: 100; }
601
+ .send-order-btn { display: flex; align-items: center; gap: 10px; padding: 15px 25px; border-radius: 30px; font-size: 1.1em; font-weight: 600; box-shadow: var(--shadow); text-decoration: none; }
602
+ .send-order-btn.whatsapp { background: #25D366; color: white; }
603
+ .send-order-btn.telegram { background: #0088cc; color: white; }
604
+ .send-order-btn i { font-size: 1.4em; }
605
  </style></head><body>
606
+ <div class="container">
607
+ <div class="order-header">
608
+ <h1>Ваш заказ для {{ business.org_name }}</h1>
609
+ <p>Номер заказа: {{ order.id[:8] }}</p>
610
+ <p>Дата: {{ order.timestamp }}</p>
 
 
 
611
  </div>
612
+ <div class="order-details">
613
+ {% for item in order.items %}
614
+ <div class="order-item">
615
+ {% if item.image %}
616
+ <img src="{{ hf_file_url_jinja(item.image) }}" alt="{{ item.name }}" class="item-image">
617
+ {% else %}
618
+ <div class="item-image" style="display: flex; align-items: center; justify-content: center; color: var(--text-muted);"><i class="fa-solid fa-box-open"></i></div>
619
+ {% endif %}
620
+ <div class="item-info">
621
+ <div class="item-name">{{ item.name }}</div>
622
+ <div class="item-price">{{ item.quantity }} x {{ "%.2f"|format(item.price|float) }} {{ business.currency }}</div>
623
+ </div>
624
+ <div class="item-subtotal">{{ "%.2f"|format(item.price|float * item.quantity|int) }} {{ business.currency }}</div>
625
+ </div>
626
+ {% endfor %}
627
+ <div class="order-total">
628
+ Итого: {{ "%.2f"|format(order.total_price|float) }} {{ business.currency }}
629
  </div>
 
 
 
 
 
 
 
630
  </div>
631
  </div>
632
+ <div class="send-order-fab">
633
+ {% set phone_number = business.contact_number | replace('+', '') | replace(' ', '') %}
634
+ {% set message = "Здравствуйте, хочу сделать заказ. Ссылка на мой заказ: " + url_for('public_order_page', order_id=order.id, _external=True) %}
635
+ {% set encoded_message = quote_plus(message) %}
636
+
637
+ {% if business.order_destination == 'whatsapp' %}
638
+ <a href="https://wa.me/{{ phone_number }}?text={{ encoded_message }}" class="send-order-btn whatsapp" target="_blank">
639
+ <i class="fab fa-whatsapp"></i> Отправить заказ
640
+ </a>
641
+ {% elif business.order_destination == 'telegram' %}
642
+ <a href="https://t.me/{{ phone_number }}?text={{ encoded_message }}" class="send-order-btn telegram" target="_blank">
643
+ <i class="fab fa-telegram"></i> Отправить заказ
644
+ </a>
645
+ {% endif %}
646
+ </div>
 
 
 
 
647
  </body></html>
648
  '''
649
 
 
2813
  return "Страница не найдена.", 404
2814
  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 ''}")
2815
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2816
  @app.route('/tma_business/manage/<login>')
2817
  def tma_manage_products(login):
2818
  if 'telegram_user_id' not in session: return redirect(url_for('tma_entry_page'))
 
2937
 
2938
  return redirect(url_for('tma_manage_products', login=login))
2939
 
2940
+ @app.route('/api/business/<login>/create_order', methods=['POST'])
2941
+ def create_order(login):
2942
+ data = load_data()
2943
+ page = data.get('business_pages', {}).get(login)
2944
+ if not page:
2945
+ return jsonify({'status': 'error', 'message': 'Business page not found.'}), 404
2946
+
2947
+ cart_data = request.json
2948
+ if not cart_data:
2949
+ return jsonify({'status': 'error', 'message': 'Cart data is empty.'}), 400
2950
+
2951
+ order_items = []
2952
+ total_price = 0
2953
+
2954
+ products_on_server = {p['id']: p for p in page.get('products', [])}
2955
+
2956
+ for product_id, cart_item in cart_data.items():
2957
+ server_product = products_on_server.get(product_id)
2958
+ if not server_product:
2959
+ continue
2960
+
2961
+ quantity = int(cart_item.get('quantity', 0))
2962
+ if quantity <= 0:
2963
+ continue
2964
+
2965
+ price = float(server_product.get('price', 0))
2966
+
2967
+ order_items.append({
2968
+ 'id': product_id,
2969
+ 'name': server_product['name'],
2970
+ 'price': price,
2971
+ 'quantity': quantity,
2972
+ 'image': server_product.get('photo_path')
2973
+ })
2974
+ total_price += price * quantity
2975
+
2976
+ if not order_items:
2977
+ return jsonify({'status': 'error', 'message': 'No valid items in the order.'}), 400
2978
+
2979
+ order_id = uuid.uuid4().hex
2980
+ new_order = {
2981
+ 'id': order_id,
2982
+ 'business_login': login,
2983
+ 'items': order_items,
2984
+ 'total_price': total_price,
2985
+ 'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
2986
+ }
2987
+
2988
+ data.setdefault('orders', {})[order_id] = new_order
2989
+ try:
2990
+ save_data(data)
2991
+ return jsonify({
2992
+ 'status': 'success',
2993
+ 'order_url': url_for('public_order_page', order_id=order_id, _external=True)
2994
+ })
2995
+ except Exception as e:
2996
+ logging.error(f"Error saving order: {e}")
2997
+ return jsonify({'status': 'error', 'message': 'Could not save the order.'}), 500
2998
+
2999
+ @app.route('/order/<order_id>')
3000
+ def public_order_page(order_id):
3001
+ data = load_data()
3002
+ order = data.get('orders', {}).get(order_id)
3003
+ if not order:
3004
+ return "Заказ не найден.", 404
3005
+
3006
+ business_page = data.get('business_pages', {}).get(order['business_login'])
3007
+ if not business_page:
3008
+ return "Страница бизнеса, связанная с этим заказом, не найдена.", 404
3009
+
3010
+ return render_template_string(PUBLIC_ORDER_PAGE_HTML, order=order, business=business_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 ''}")
3011
+
3012
 
3013
  ADMIN_LOGIN_HTML = '''
3014
  <!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>Admin Login</title>
 
3531
 
3532
  threading.Thread(target=check_reminders, daemon=True).start()
3533
 
3534
+ app.run(debug=False, host='0.0.0.0', port=7860)