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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +322 -43
app.py CHANGED
@@ -299,31 +299,115 @@ body { padding-bottom: 30px; }
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
  <style>''' + BASE_STYLE + '''
309
- body { background: var(--card-bg-dark); }
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(auto-fill, minmax(200px, 1fr)); gap: 20px; }
315
- .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); }
316
- .product-card:hover { transform: translateY(-5px); }
317
- .product-image { width: 100%; height: 180px; object-fit: cover; background-color: #2a2a2a; }
318
- .product-info { padding: 15px; }
319
- .product-name { font-size: 1.1em; font-weight: 600; margin-bottom: 5px; }
320
- .product-price { font-size: 1.2em; font-weight: bold; color: var(--secondary); margin-bottom: 10px; }
321
- .product-desc { font-size: 0.9em; color: var(--text-muted); }
322
- .order-fab { position: fixed; bottom: 20px; right: 20px; z-index: 100; }
323
- .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); }
324
- .order-btn.whatsapp { background: #25D366; color: white; }
325
- .order-btn.telegram { background: #0088cc; color: white; }
326
- .order-btn i { font-size: 1.4em; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
327
  </style></head><body>
328
  <div class="container">
329
  <div class="biz-header">
@@ -338,35 +422,167 @@ body { background: var(--card-bg-dark); }
338
  {% for product in page.products %}
339
  <div class="product-card">
340
  {% if product.photo_path %}
341
- <img src="{{ hf_file_url_jinja(product.photo_path) }}" alt="{{ product.name }}" class="product-image">
 
 
342
  {% endif %}
343
  <div class="product-info">
344
- <h3 class="product-name">{{ product.name }}</h3>
345
  {% if page.show_prices and product.price %}
346
- <p class="product-price">{{ "%.2f"|format(product.price|float) }} {{ page.currency }}</p>
 
 
 
 
347
  {% endif %}
348
- <p class="product-desc">{{ product.description }}</p>
349
  </div>
350
  </div>
351
  {% endfor %}
352
  </div>
353
  {% else %}
354
- <p style="text-align: center;">Товары скоро появятся.</p>
355
  {% endif %}
356
  </div>
357
 
358
- <div class="order-fab">
359
- {% set phone_number = page.contact_number | replace('+', '') | replace(' ', '') %}
360
- {% if page.order_destination == 'whatsapp' %}
361
- <a href="https://wa.me/{{ phone_number }}" class="btn order-btn whatsapp" target="_blank">
362
- <i class="fab fa-whatsapp"></i> Заказать
363
- </a>
364
- {% elif page.order_destination == 'telegram' %}
365
- <a href="https://t.me/{{ phone_number }}" class="btn order-btn telegram" target="_blank">
366
- <i class="fab fa-telegram"></i> Заказать
367
- </a>
368
- {% endif %}
 
 
 
 
 
 
 
369
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
370
  </body></html>
371
  '''
372
 
@@ -478,17 +694,18 @@ def load_data():
478
  try:
479
  download_db_from_hf()
480
  with open(DATA_FILE, 'r', encoding='utf-8') as file: data = json.load(file)
481
- if not isinstance(data, dict): data = {'users': {}, 'shared_links': {}, 'business_pages': {}}
482
  data.setdefault('users', {})
483
  data.setdefault('shared_links', {})
484
  data.setdefault('business_pages', {})
 
485
  for tma_user_id_str, user_data_item in data['users'].items():
486
  initialize_user_filesystem_tma(user_data_item, tma_user_id_str)
487
  user_data_item.setdefault('reminders', [])
488
  return data
489
  except Exception as e:
490
  logging.error(f"Error loading data: {e}")
491
- return {'users': {}, 'shared_links': {}, 'business_pages': {}}
492
 
493
  def save_data(data):
494
  with save_data_lock:
@@ -512,17 +729,17 @@ def upload_db_to_hf():
512
  def download_db_from_hf():
513
  if not HF_TOKEN_READ:
514
  if not os.path.exists(DATA_FILE):
515
- with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump({'users': {}, 'shared_links': {}, 'business_pages': {}}, f)
516
  return
517
  try:
518
  hf_hub_download(repo_id=REPO_ID, filename=DATA_FILE, repo_type="dataset", token=HF_TOKEN_READ, local_dir=".", local_dir_use_symlinks=False)
519
  except (hf_utils.RepositoryNotFoundError, hf_utils.EntryNotFoundError):
520
  if not os.path.exists(DATA_FILE):
521
- with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump({'users': {}, 'shared_links': {}, 'business_pages': {}}, f)
522
  except Exception as e:
523
  logging.error(f"Error downloading database: {e}")
524
  if not os.path.exists(DATA_FILE):
525
- with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump({'users': {}, 'shared_links': {}, 'business_pages': {}}, f)
526
 
527
  def periodic_backup():
528
  while True:
@@ -2533,7 +2750,70 @@ def public_business_page(login):
2533
  page = data.get('business_pages', {}).get(login)
2534
  if not page:
2535
  return "Страница не найдена.", 404
2536
- 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 ''}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2537
 
2538
  @app.route('/tma_business/manage/<login>')
2539
  def tma_manage_products(login):
@@ -3174,7 +3454,7 @@ if __name__ == '__main__':
3174
  download_db_from_hf()
3175
  else:
3176
  if not os.path.exists(DATA_FILE):
3177
- with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump({'users': {}, 'shared_links': {}, 'business_pages': {}}, f)
3178
 
3179
  if HF_TOKEN_WRITE:
3180
  threading.Thread(target=periodic_backup, daemon=True).start()
@@ -3182,4 +3462,3 @@ if __name__ == '__main__':
3182
  threading.Thread(target=check_reminders, daemon=True).start()
3183
 
3184
  app.run(debug=False, host='0.0.0.0', port=7860)
3185
-
 
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">
 
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
 
 
694
  try:
695
  download_db_from_hf()
696
  with open(DATA_FILE, 'r', encoding='utf-8') as file: data = json.load(file)
697
+ if not isinstance(data, dict): data = {'users': {}, 'shared_links': {}, 'business_pages': {}, 'orders': {}}
698
  data.setdefault('users', {})
699
  data.setdefault('shared_links', {})
700
  data.setdefault('business_pages', {})
701
+ data.setdefault('orders', {})
702
  for tma_user_id_str, user_data_item in data['users'].items():
703
  initialize_user_filesystem_tma(user_data_item, tma_user_id_str)
704
  user_data_item.setdefault('reminders', [])
705
  return data
706
  except Exception as e:
707
  logging.error(f"Error loading data: {e}")
708
+ return {'users': {}, 'shared_links': {}, 'business_pages': {}, 'orders': {}}
709
 
710
  def save_data(data):
711
  with save_data_lock:
 
729
  def download_db_from_hf():
730
  if not HF_TOKEN_READ:
731
  if not os.path.exists(DATA_FILE):
732
+ with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump({'users': {}, 'shared_links': {}, 'business_pages': {}, 'orders': {}}, f)
733
  return
734
  try:
735
  hf_hub_download(repo_id=REPO_ID, filename=DATA_FILE, repo_type="dataset", token=HF_TOKEN_READ, local_dir=".", local_dir_use_symlinks=False)
736
  except (hf_utils.RepositoryNotFoundError, hf_utils.EntryNotFoundError):
737
  if not os.path.exists(DATA_FILE):
738
+ with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump({'users': {}, 'shared_links': {}, 'business_pages': {}, 'orders': {}}, f)
739
  except Exception as e:
740
  logging.error(f"Error downloading database: {e}")
741
  if not os.path.exists(DATA_FILE):
742
+ with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump({'users': {}, 'shared_links': {}, 'business_pages': {}, 'orders': {}}, f)
743
 
744
  def periodic_backup():
745
  while True:
 
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):
 
3454
  download_db_from_hf()
3455
  else:
3456
  if not os.path.exists(DATA_FILE):
3457
+ with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump({'users': {}, 'shared_links': {}, 'business_pages': {}, 'orders': {}}, f)
3458
 
3459
  if HF_TOKEN_WRITE:
3460
  threading.Thread(target=periodic_backup, daemon=True).start()
 
3462
  threading.Thread(target=check_reminders, daemon=True).start()
3463
 
3464
  app.run(debug=False, host='0.0.0.0', port=7860)