Kgshop commited on
Commit
5e5256b
·
verified ·
1 Parent(s): 0ba30ce

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +405 -180
app.py CHANGED
@@ -159,61 +159,96 @@ CATALOG_TEMPLATE = '''
159
  <html lang="ru">
160
  <head>
161
  <meta charset="UTF-8">
162
- <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
163
  <title>Магазин</title>
164
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
165
  <style>
166
- * { margin: 0; padding: 0; box-sizing: border-box; font-family: sans-serif; -webkit-tap-highlight-color: transparent; }
167
- body { background-color: #f5f5f5; color: #333; }
168
- .header { display: flex; align-items: center; justify-content: space-between; padding: 15px 20px; background: #fff; box-shadow: 0 2px 5px rgba(0,0,0,0.05); position: sticky; top: 0; z-index: 100; }
169
- .header h1 { font-size: 1.5rem; font-weight: bold; }
170
- .back-btn { display: none; font-size: 1.2rem; cursor: pointer; color: #333; margin-right: 15px; }
171
- .search-bar { padding: 10px 20px; background: #fff; border-bottom: 1px solid #eee; display: flex; align-items: center; }
172
- .search-bar input { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 8px; outline: none; font-size: 1rem; }
173
 
174
- .categories-container { display: grid; grid-template-columns: 1fr 1fr; background: #fff; }
175
- .category-item { padding: 15px 20px; border-bottom: 1px dashed #ccc; display: flex; justify-content: space-between; align-items: center; cursor: pointer; }
176
- .category-item:nth-child(odd) { border-right: 1px dashed #ccc; }
177
- .category-item span.name { font-size: 0.95rem; }
178
- .category-item span.count { color: #999; font-size: 0.9rem; }
179
 
180
- .products-container { display: none; padding: 15px; display: flex; flex-direction: column; gap: 15px; }
181
- .product-card { background: #fff; border-radius: 12px; padding: 15px; display: flex; box-shadow: 0 2px 8px rgba(0,0,0,0.08); align-items: stretch; gap: 15px; }
182
- .product-img { width: 100px; height: 100px; border-radius: 8px; object-fit: cover; flex-shrink: 0; border: 1px solid #eee;}
183
- .product-info { flex-grow: 1; display: flex; flex-direction: column; justify-content: space-between; }
184
- .product-title { font-size: 0.95rem; font-weight: bold; margin-bottom: 10px; line-height: 1.3; }
185
- .product-bottom { display: flex; flex-direction: column; align-items: flex-end; gap: 10px; }
186
- .product-price { font-weight: bold; font-size: 1rem; }
187
- .quantity-control { display: flex; align-items: center; background: #f0f0f0; border-radius: 4px; overflow: hidden; }
188
- .quantity-control button { border: none; background: #e0e0e0; width: 35px; height: 35px; font-size: 1.2rem; cursor: pointer; }
189
- .quantity-control input { width: 40px; height: 35px; border: none; text-align: center; background: transparent; font-weight: bold; pointer-events: none; }
190
 
191
- .cart-bar { position: fixed; bottom: 0; left: 0; width: 100%; background: #fff; box-shadow: 0 -2px 10px rgba(0,0,0,0.1); padding: 15px 20px; display: none; justify-content: space-between; align-items: center; z-index: 100; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
192
  .cart-info { display: flex; flex-direction: column; }
193
- .cart-total { font-size: 1.2rem; font-weight: bold; }
194
- .checkout-btn { background: #333; color: #fff; padding: 12px 25px; border: none; border-radius: 8px; font-weight: bold; font-size: 1rem; cursor: pointer; }
 
195
 
196
- .modal-overlay { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 200; justify-content: center; align-items: flex-end; }
197
- .modal-content { background: #fff; width: 100%; max-height: 80vh; border-radius: 20px 20px 0 0; padding: 20px; overflow-y: auto; display: flex; flex-direction: column; }
198
- .modal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
199
- .modal-close { font-size: 1.5rem; cursor: pointer; border: none; background: transparent; }
200
- .cart-item-list { display: flex; flex-direction: column; gap: 15px; margin-bottom: 20px; }
201
- .cart-item { display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #eee; padding-bottom: 10px; }
202
- .cart-item-name { flex-grow: 1; font-size: 0.9rem; padding-right: 10px; }
203
- .confirm-btn { background: #25D366; color: #fff; width: 100%; padding: 15px; border: none; border-radius: 10px; font-size: 1.1rem; font-weight: bold; cursor: pointer; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
204
  </style>
205
  </head>
206
  <body>
207
  <div class="header">
208
  <div style="display: flex; align-items: center;">
209
- <i class="fas fa-chevron-left back-btn" id="backBtn" onclick="showCategories()"></i>
210
  <h1 id="pageTitle">Каталог</h1>
211
  </div>
212
- <i class="fas fa-bars" style="font-size: 1.5rem;"></i>
213
  </div>
214
 
215
  <div class="search-bar" id="searchBar">
216
- <input type="text" id="searchInput" placeholder="Поиск..." oninput="filterCategories()">
 
 
 
217
  </div>
218
 
219
  <div class="categories-container" id="categoriesContainer"></div>
@@ -221,23 +256,33 @@ CATALOG_TEMPLATE = '''
221
 
222
  <div class="cart-bar" id="cartBar">
223
  <div class="cart-info">
224
- <span style="font-size: 0.8rem; color: #666;">Итого:</span>
225
  <span class="cart-total"><span id="cartTotalSum">0</span> {{ currency_code }}</span>
226
  </div>
227
- <button class="checkout-btn" onclick="openCartModal()">Оформить</button>
228
  </div>
229
 
230
- <div class="modal-overlay" id="cartModal">
231
  <div class="modal-content">
232
  <div class="modal-header">
233
- <h2>Корзина</h2>
234
- <button class="modal-close" onclick="closeCartModal()">&times;</button>
235
  </div>
236
  <div class="cart-item-list" id="cartItemList"></div>
237
- <button class="confirm-btn" onclick="submitOrder()">Сформировать заказ</button>
238
  </div>
239
  </div>
240
 
 
 
 
 
 
 
 
 
 
 
241
  <script>
242
  const products = {{ products_json|safe }};
243
  const categoriesList = {{ categories_json|safe }};
@@ -245,6 +290,8 @@ CATALOG_TEMPLATE = '''
245
  const currency = '{{ currency_code }}';
246
 
247
  let cart = {};
 
 
248
 
249
  function init() {
250
  renderCategories();
@@ -253,7 +300,8 @@ CATALOG_TEMPLATE = '''
253
 
254
  function renderCategories() {
255
  const container = document.getElementById('categoriesContainer');
256
- document.getElementById('productsContainer').style.display = 'none';
 
257
  container.style.display = 'grid';
258
  document.getElementById('backBtn').style.display = 'none';
259
  document.getElementById('pageTitle').innerText = 'Каталог';
@@ -267,7 +315,13 @@ CATALOG_TEMPLATE = '''
267
  const div = document.createElement('div');
268
  div.className = 'category-item';
269
  div.onclick = () => showProducts(cat);
270
- div.innerHTML = `<span class="name">${cat}</span><span class="count">${count}</span>`;
 
 
 
 
 
 
271
  container.appendChild(div);
272
  });
273
  }
@@ -288,7 +342,7 @@ CATALOG_TEMPLATE = '''
288
  const container = document.getElementById('productsContainer');
289
  container.style.display = 'flex';
290
  document.getElementById('backBtn').style.display = 'block';
291
- document.getElementById('pageTitle').innerText = 'Результаты поиска';
292
  container.innerHTML = '';
293
 
294
  const matchedProducts = products.filter(p =>
@@ -296,27 +350,38 @@ CATALOG_TEMPLATE = '''
296
  p.category.toLowerCase().includes(query)
297
  );
298
 
299
- matchedProducts.forEach(p => renderProductCard(p, container));
 
 
 
 
300
  }
301
 
302
  function renderProductCard(p, container) {
303
  const qty = cart[p.product_id] ? cart[p.product_id].quantity : 0;
304
- const photoUrl = p.photos && p.photos.length > 0
 
305
  ? `https://huggingface.co/datasets/${repoId}/resolve/main/photos/${p.photos[0]}`
306
- : 'https://via.placeholder.com/100';
307
 
 
 
 
308
  const div = document.createElement('div');
309
  div.className = 'product-card';
310
  div.innerHTML = `
311
- <img src="${photoUrl}" class="product-img">
 
 
 
312
  <div class="product-info">
313
  <div class="product-title">${p.name}</div>
314
  <div class="product-bottom">
315
  <div class="product-price">${p.price} ${currency}</div>
316
  <div class="quantity-control">
317
- <button onclick="updateCart('${p.product_id}', -1)">-</button>
318
  <input type="text" id="qty-${p.product_id}" value="${qty}" readonly>
319
- <button onclick="updateCart('${p.product_id}', 1)">+</button>
320
  </div>
321
  </div>
322
  </div>
@@ -334,7 +399,11 @@ CATALOG_TEMPLATE = '''
334
  container.innerHTML = '';
335
 
336
  const catProducts = products.filter(p => p.category === category);
337
- catProducts.forEach(p => renderProductCard(p, container));
 
 
 
 
338
  }
339
 
340
  function updateCart(productId, change) {
@@ -382,19 +451,28 @@ CATALOG_TEMPLATE = '''
382
  list.innerHTML += `
383
  <div class="cart-item">
384
  <div class="cart-item-name">${item.name}</div>
385
- <div style="font-weight: bold; white-space: nowrap;">${item.quantity} x ${item.price} ${currency}</div>
386
  </div>
387
  `;
388
  }
389
- document.getElementById('cartModal').style.display = 'flex';
 
 
390
  }
391
 
392
  function closeCartModal() {
393
- document.getElementById('cartModal').style.display = 'none';
 
 
394
  }
395
 
396
  function submitOrder() {
397
  const cartArray = Object.values(cart);
 
 
 
 
 
398
  fetch('/create_order', {
399
  method: 'POST',
400
  headers: { 'Content-Type': 'application/json' },
@@ -406,9 +484,71 @@ CATALOG_TEMPLATE = '''
406
  cart = {};
407
  window.location.href = `/order/${data.order_id}`;
408
  }
 
 
 
 
 
409
  });
410
  }
411
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
412
  init();
413
  </script>
414
  </body>
@@ -420,92 +560,109 @@ ORDER_TEMPLATE = '''
420
  <html lang="ru">
421
  <head>
422
  <meta charset="UTF-8">
423
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
424
  <title>Накладная №{{ order.id }}</title>
 
425
  <style>
426
- * { box-sizing: border-box; font-family: 'Times New Roman', serif; }
427
- body { margin: 0; padding: 20px; padding-bottom: 90px; background: #f0f0f0; display: flex; flex-direction: column; align-items: center; }
428
- .invoice-box { background: #fff; width: 100%; max-width: 900px; padding: 40px; box-shadow: 0 0 10px rgba(0,0,0,0.1); }
429
- .header { display: flex; justify-content: space-between; align-items: flex-end; margin-bottom: 30px; border-bottom: 2px solid #000; padding-bottom: 10px; }
430
- .header h1 { margin: 0; font-size: 32px; font-weight: bold; }
431
- .info-row { display: flex; justify-content: space-between; margin-bottom: 20px; font-size: 18px; font-weight: bold; }
432
- table { width: 100%; border-collapse: collapse; margin-bottom: 20px; }
433
- th, td { border: 1px solid #000; padding: 8px; text-align: center; font-size: 16px; font-weight: bold; }
434
- th { background: #f9f9f9; }
435
- .img-cell img { width: 50px; height: 50px; object-fit: cover; }
436
- .total-row td { font-size: 18px; }
 
 
 
 
437
 
438
- .action-bar { position: fixed; bottom: 0; left: 0; width: 100%; background: #fff; box-shadow: 0 -2px 10px rgba(0,0,0,0.1); padding: 15px 20px; display: flex; gap: 15px; z-index: 100; }
439
- .btn { flex: 1; padding: 15px; border-radius: 10px; border: none; font-size: 16px; font-weight: bold; cursor: pointer; color: #fff; display: flex; align-items: center; justify-content: center; text-decoration: none; box-shadow: 0 2px 5px rgba(0,0,0,0.2); }
440
- .btn-wa { background: #25D366; }
441
- .btn-print { background: #333; }
 
 
 
442
 
443
  @media print {
444
  body { background: #fff; padding: 0; }
445
- .invoice-box { box-shadow: none; padding: 0; max-width: 100%; }
 
 
 
446
  .action-bar { display: none; }
447
  }
 
 
 
 
 
 
 
448
  </style>
449
  </head>
450
  <body>
451
  <div class="invoice-box">
452
  <div class="header">
453
- <div>
454
- <span style="font-size: 24px; font-weight: bold;"></span>
455
- </div>
456
  <h1>Накладная</h1>
457
- <div>
458
- <span style="font-size: 16px;">1 | 1</span>
 
459
  </div>
460
  </div>
461
 
462
  <div class="info-row">
463
- <div>NO: {{ order.id }}</div>
464
- <div>дата: {{ order.created_at.split(' ')[0] }}</div>
465
- </div>
466
-
467
- <div class="info-row">
468
- <div>покупатель: _________________</div>
469
  </div>
470
 
471
- <table>
472
- <thead>
473
- <tr>
474
- <th>NO</th>
475
- <th>Наименование</th>
476
- <th>Фото</th>
477
- <th>кол-во</th>
478
- <th>Цена</th>
479
- <th>Сумма</th>
480
- </tr>
481
- </thead>
482
- <tbody>
483
- {% for item in order.cart %}
484
- <tr>
485
- <td>{{ loop.index }}</td>
486
- <td style="text-align: left;">{{ item.name }}</td>
487
- <td class="img-cell"><img src="{{ item.photo_url }}" alt="img"></td>
488
- <td>{{ item.quantity }}</td>
489
- <td>{{ item.price }}</td>
490
- <td>{{ item.price * item.quantity }}</td>
491
- </tr>
492
- {% endfor %}
493
- <tr class="total-row">
494
- <td colspan="5" style="text-align: left;">Итого</td>
495
- <td>{{ order.total_price }}</td>
496
- </tr>
497
- </tbody>
498
- </table>
 
 
499
  </div>
500
 
501
  <div class="action-bar">
502
- <button class="btn btn-print" onclick="window.print()">Печать</button>
503
- <button class="btn btn-wa" onclick="sendToWA()">Отправить в WhatsApp</button>
 
 
 
504
  </div>
505
 
506
  <script>
507
  function sendToWA() {
508
- let msg = `Заказ №{{ order.id }}\nНакладная: ${window.location.href}`;
509
  window.open(`https://api.whatsapp.com/send?phone={{ whatsapp_number }}&text=${encodeURIComponent(msg)}`, '_blank');
510
  }
511
  </script>
@@ -518,88 +675,152 @@ ADMIN_TEMPLATE = '''
518
  <html lang="ru">
519
  <head>
520
  <meta charset="UTF-8">
521
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
522
  <title>Админ-панель</title>
 
523
  <style>
524
- * { box-sizing: border-box; font-family: sans-serif; }
525
- body { background: #f4f6f9; padding: 20px; }
526
- .container { max-width: 1000px; margin: 0 auto; background: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
527
- h1, h2 { color: #333; }
528
- .form-group { margin-bottom: 15px; }
529
- input[type="text"], input[type="number"], select { width: 100%; padding: 10px; border: 1px solid #ccc; border-radius: 4px; }
530
- button { padding: 10px 15px; border: none; border-radius: 4px; background: #333; color: #fff; cursor: pointer; }
531
- button.danger { background: #dc3545; }
532
- .category-block { border: 1px solid #ddd; margin-bottom: 10px; border-radius: 4px; }
533
- .category-header { background: #f8f9fa; padding: 15px; font-weight: bold; display: flex; justify-content: space-between; align-items: center; }
534
- .category-content { padding: 15px; display: block; border-top: 1px solid #ddd; }
535
- .product-item { display: flex; justify-content: space-between; align-items: center; padding: 10px 0; border-bottom: 1px solid #eee; }
536
- .product-item img { width: 40px; height: 40px; object-fit: cover; margin-right: 10px; border-radius: 4px; }
537
- .add-product-form { background: #fdfdfd; padding: 15px; border: 1px dashed #ccc; margin-top: 15px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
538
  </style>
539
  </head>
540
  <body>
541
  <div class="container">
542
- <div style="display: flex; justify-content: space-between; align-items: center;">
543
- <h1>Админ-панель</h1>
544
- <a href="/" style="text-decoration: none; background: #007bff; color: white; padding: 10px 15px; border-radius: 4px;">В каталог</a>
545
  </div>
546
 
547
- <div style="margin-bottom: 20px; display: flex; gap: 10px;">
548
- <form method="POST" action="/force_upload"><button type="submit" style="background:#28a745;">Сохранить БД на сервер</button></form>
549
- <form method="POST" action="/force_download"><button type="submit" style="background:#17a2b8;"качать БД с сервера</button></form>
 
 
 
 
550
  </div>
551
 
552
- <h2>Категории и Товары</h2>
553
- <form method="POST" style="margin-bottom: 20px; display: flex; gap: 10px;">
554
- <input type="hidden" name="action" value="add_category">
555
- <input type="text" name="category_name" placeholder="Новая категория" required>
556
- <button type="submit">Добавить категорию</button>
557
- </form>
 
 
558
 
559
  {% for category in categories %}
560
  <div class="category-block">
561
  <div class="category-header">
562
- <span>{{ category }}</span>
563
- <form method="POST" style="margin:0;" onsubmit="return confirm('Удалить категорию?');">
564
  <input type="hidden" name="action" value="delete_category">
565
  <input type="hidden" name="category_name" value="{{ category }}">
566
- <button type="submit" class="danger">Удалить</button>
567
  </form>
568
  </div>
569
- <div class="category-content" id="cat-{{ loop.index }}">
570
 
571
  {% for product in products %}
572
  {% if product.category == category %}
573
  <div class="product-item">
574
- <div style="display: flex; align-items: center;">
575
- <img src="{{ 'https://huggingface.co/datasets/' + repo_id + '/resolve/main/photos/' + product.photos[0] if product.photos else 'https://via.placeholder.com/40' }}">
576
- <span>{{ product.name }} - {{ product.price }} {{ currency_code }}</span>
 
 
 
 
 
 
 
577
  </div>
578
  <form method="POST" style="margin:0;" onsubmit="return confirm('Удалить товар?');">
579
  <input type="hidden" name="action" value="delete_product">
580
  <input type="hidden" name="product_id" value="{{ product.product_id }}">
581
- <button type="submit" class="danger">Удалить</button>
582
  </form>
583
  </div>
584
  {% endif %}
585
  {% endfor %}
586
 
587
- <form class="add-product-form" method="POST" enctype="multipart/form-data">
588
  <input type="hidden" name="action" value="add_product">
589
  <input type="hidden" name="category" value="{{ category }}">
590
- <div style="display: flex; gap: 10px; margin-bottom: 10px;">
591
- <input type="text" name="name" placeholder="Название товара" required style="flex:2;">
592
- <input type="number" name="price" placeholder="Цена" required style="flex:1;">
 
593
  </div>
594
- <div style="margin-bottom: 10px;">
595
- <input type="file" name="photo" accept="image/*" required>
 
596
  </div>
597
- <button type="submit" style="background: #28a745;">Добавить товар в "{{ category }}"</button>
598
  </form>
599
  </div>
600
  </div>
601
  {% endfor %}
602
  </div>
 
 
 
 
 
 
 
 
603
  </body>
604
  </html>
605
  '''
@@ -633,7 +854,7 @@ def create_order():
633
  "name": item['name'],
634
  "price": float(item['price']),
635
  "quantity": int(item['quantity']),
636
- "photo_url": f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/photos/{item['photos'][0]}" if item.get('photos') else "https://via.placeholder.com/50"
637
  })
638
 
639
  order_id = f"SA-{datetime.now().strftime('%Y%m%d')}-{str(len(load_data().get('orders', {}))+1).zfill(3)}"
@@ -693,31 +914,35 @@ def admin():
693
  name = request.form.get('name', '').strip()
694
  price = float(request.form.get('price', 0))
695
  category = request.form.get('category')
696
- photo = request.files.get('photo')
697
 
698
  photos_list = []
699
- if photo and photo.filename and HF_TOKEN_WRITE:
700
  uploads_dir = 'uploads_temp'
701
  os.makedirs(uploads_dir, exist_ok=True)
702
- ext = os.path.splitext(photo.filename)[1].lower()
703
- photo_filename = f"{uuid4().hex}{ext}"
704
- temp_path = os.path.join(uploads_dir, photo_filename)
705
- photo.save(temp_path)
706
- try:
707
- api = HfApi()
708
- api.upload_file(
709
- path_or_fileobj=temp_path,
710
- path_in_repo=f"photos/{photo_filename}",
711
- repo_id=REPO_ID,
712
- repo_type="dataset",
713
- token=HF_TOKEN_WRITE
714
- )
715
- photos_list.append(photo_filename)
716
- except Exception:
717
- pass
718
- finally:
719
- if os.path.exists(temp_path):
720
- os.remove(temp_path)
 
 
 
 
721
 
722
  new_product = {
723
  'product_id': uuid4().hex,
@@ -763,4 +988,4 @@ if __name__ == '__main__':
763
  threading.Thread(target=periodic_backup, daemon=True).start()
764
 
765
  port = int(os.environ.get('PORT', 7860))
766
- app.run(host='0.0.0.0', port=port)
 
159
  <html lang="ru">
160
  <head>
161
  <meta charset="UTF-8">
162
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
163
  <title>Магазин</title>
164
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
165
  <style>
166
+ :root { --primary: #1a1a1a; --bg: #f8f9fa; --surface: #ffffff; --text: #2d3436; --text-muted: #636e72; --border: #edf2f7; --accent: #25D366; }
167
+ * { margin: 0; padding: 0; box-sizing: border-box; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; -webkit-tap-highlight-color: transparent; }
168
+ body { background-color: var(--bg); color: var(--text); padding-bottom: calc(90px + env(safe-area-inset-bottom)); }
 
 
 
 
169
 
170
+ .header { display: flex; align-items: center; justify-content: space-between; padding: max(15px, env(safe-area-inset-top)) 20px 15px; background: var(--surface); box-shadow: 0 2px 10px rgba(0,0,0,0.03); position: sticky; top: 0; z-index: 100; }
171
+ .header h1 { font-size: 1.4rem; font-weight: 700; letter-spacing: -0.5px; }
172
+ .back-btn { display: none; font-size: 1.2rem; cursor: pointer; color: var(--text); margin-right: 15px; padding: 5px; }
 
 
173
 
174
+ .search-bar { padding: 15px 20px; background: var(--surface); border-bottom: 1px solid var(--border); }
175
+ .search-container { position: relative; display: flex; align-items: center; background: var(--bg); border-radius: 12px; padding: 0 15px; border: 1px solid transparent; transition: all 0.2s; }
176
+ .search-container:focus-within { border-color: #dcdde1; background: var(--surface); box-shadow: 0 4px 12px rgba(0,0,0,0.05); }
177
+ .search-container i { color: var(--text-muted); font-size: 0.9rem; }
178
+ .search-bar input { width: 100%; padding: 12px 10px; border: none; background: transparent; outline: none; font-size: 0.95rem; }
 
 
 
 
 
179
 
180
+ .categories-container { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 15px; padding: 20px; }
181
+ .category-item { background: var(--surface); padding: 20px 15px; border-radius: 16px; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px; cursor: pointer; box-shadow: 0 4px 15px rgba(0,0,0,0.03); transition: transform 0.2s; text-align: center; }
182
+ .category-item:active { transform: scale(0.96); }
183
+ .category-item span.name { font-size: 0.95rem; font-weight: 600; line-height: 1.3; }
184
+ .category-item span.count { color: var(--text-muted); font-size: 0.8rem; background: var(--bg); padding: 4px 10px; border-radius: 20px; }
185
+
186
+ .products-container { display: none; padding: 20px; flex-direction: column; gap: 15px; }
187
+ .product-card { background: var(--surface); border-radius: 16px; padding: 12px; display: flex; box-shadow: 0 4px 15px rgba(0,0,0,0.03); align-items: stretch; gap: 15px; width: 100%; }
188
+ .product-img-wrapper { position: relative; width: 110px; height: 110px; flex-shrink: 0; }
189
+ .product-img { width: 100%; height: 100%; border-radius: 12px; object-fit: cover; cursor: pointer; background: var(--bg); border: 1px solid var(--border); }
190
+ .photo-count { position: absolute; bottom: 5px; right: 5px; background: rgba(0,0,0,0.6); color: white; font-size: 0.7rem; padding: 2px 6px; border-radius: 10px; pointer-events: none; }
191
+ .product-info { flex-grow: 1; display: flex; flex-direction: column; justify-content: space-between; min-width: 0; padding: 5px 0; }
192
+ .product-title { font-size: 0.95rem; font-weight: 600; line-height: 1.4; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
193
+ .product-bottom { display: flex; align-items: center; justify-content: space-between; margin-top: 10px; }
194
+ .product-price { font-weight: 700; font-size: 1rem; color: var(--primary); }
195
+ .quantity-control { display: flex; align-items: center; background: var(--bg); border-radius: 8px; overflow: hidden; border: 1px solid var(--border); }
196
+ .quantity-control button { border: none; background: transparent; width: 32px; height: 32px; font-size: 1.1rem; cursor: pointer; color: var(--primary); display: flex; align-items: center; justify-content: center; transition: background 0.2s; }
197
+ .quantity-control button:active { background: #e0e0e0; }
198
+ .quantity-control input { width: 36px; height: 32px; border: none; text-align: center; background: transparent; font-weight: 600; font-size: 0.95rem; pointer-events: none; color: var(--primary); }
199
+
200
+ .cart-bar { position: fixed; bottom: 0; left: 0; width: 100%; background: var(--surface); box-shadow: 0 -4px 20px rgba(0,0,0,0.06); padding: 15px 20px calc(15px + env(safe-area-inset-bottom)); display: none; justify-content: space-between; align-items: center; z-index: 100; border-top-left-radius: 20px; border-top-right-radius: 20px; }
201
  .cart-info { display: flex; flex-direction: column; }
202
+ .cart-total { font-size: 1.25rem; font-weight: 800; color: var(--primary); }
203
+ .checkout-btn { background: var(--primary); color: #fff; padding: 12px 28px; border: none; border-radius: 12px; font-weight: 600; font-size: 1rem; cursor: pointer; box-shadow: 0 4px 12px rgba(26,26,26,0.2); transition: transform 0.2s; }
204
+ .checkout-btn:active { transform: scale(0.95); }
205
 
206
+ .modal-overlay { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.6); backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px); z-index: 200; justify-content: center; align-items: flex-end; opacity: 0; transition: opacity 0.3s; }
207
+ .modal-overlay.active { opacity: 1; }
208
+ .modal-content { background: var(--surface); width: 100%; max-height: 85vh; border-radius: 24px 24px 0 0; padding: 25px 20px calc(25px + env(safe-area-inset-bottom)); overflow-y: auto; display: flex; flex-direction: column; transform: translateY(100%); transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1); }
209
+ .modal-overlay.active .modal-content { transform: translateY(0); }
210
+ .modal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 25px; }
211
+ .modal-header h2 { font-size: 1.3rem; font-weight: 700; }
212
+ .modal-close { font-size: 1.5rem; cursor: pointer; border: none; background: #f1f2f6; width: 36px; height: 36px; border-radius: 50%; display: flex; align-items: center; justify-content: center; color: var(--text); }
213
+ .cart-item-list { display: flex; flex-direction: column; gap: 15px; margin-bottom: 25px; }
214
+ .cart-item { display: flex; justify-content: space-between; align-items: center; background: var(--bg); padding: 15px; border-radius: 12px; }
215
+ .cart-item-name { flex-grow: 1; font-size: 0.95rem; font-weight: 500; line-height: 1.3; margin-right: 15px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
216
+ .cart-item-price { font-weight: 700; white-space: nowrap; color: var(--primary); }
217
+ .confirm-btn { background: var(--accent); color: #fff; width: 100%; padding: 16px; border: none; border-radius: 14px; font-size: 1.1rem; font-weight: 700; cursor: pointer; box-shadow: 0 4px 15px rgba(37,211,102,0.3); }
218
+
219
+ .gallery-modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: #000; z-index: 300; justify-content: center; align-items: center; flex-direction: column; }
220
+ .gallery-close { position: absolute; top: max(20px, env(safe-area-inset-top)); right: 20px; color: #fff; font-size: 2rem; cursor: pointer; background: rgba(0,0,0,0.5); width: 40px; height: 40px; border-radius: 50%; display: flex; align-items: center; justify-content: center; border: none; z-index: 302; }
221
+ .gallery-img-container { position: relative; width: 100%; height: 70vh; display: flex; align-items: center; justify-content: center; }
222
+ .gallery-img { max-width: 100%; max-height: 100%; object-fit: contain; }
223
+ .gallery-nav { position: absolute; top: 50%; transform: translateY(-50%); color: #fff; font-size: 2rem; background: rgba(0,0,0,0.5); border: none; width: 50px; height: 50px; border-radius: 50%; cursor: pointer; display: flex; align-items: center; justify-content: center; z-index: 301; }
224
+ .gallery-nav.prev { left: 10px; }
225
+ .gallery-nav.next { right: 10px; }
226
+ .gallery-dots { display: flex; gap: 8px; margin-top: 20px; }
227
+ .gallery-dot { width: 8px; height: 8px; border-radius: 50%; background: rgba(255,255,255,0.3); transition: background 0.3s; }
228
+ .gallery-dot.active { background: #fff; }
229
+
230
+ @media (min-width: 768px) {
231
+ .categories-container { grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); }
232
+ .products-container { display: grid; grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); }
233
+ .modal-content { max-width: 500px; margin: 0 auto; border-radius: 24px; top: 50%; transform: translateY(-50%) scale(0.9); bottom: auto; position: relative; max-height: 90vh; }
234
+ .modal-overlay.active .modal-content { transform: translateY(-50%) scale(1); }
235
+ .cart-bar { max-width: 500px; left: 50%; transform: translateX(-50%); border-radius: 20px 20px 0 0; }
236
+ }
237
  </style>
238
  </head>
239
  <body>
240
  <div class="header">
241
  <div style="display: flex; align-items: center;">
242
+ <i class="fas fa-arrow-left back-btn" id="backBtn" onclick="showCategories()"></i>
243
  <h1 id="pageTitle">Каталог</h1>
244
  </div>
 
245
  </div>
246
 
247
  <div class="search-bar" id="searchBar">
248
+ <div class="search-container">
249
+ <i class="fas fa-search"></i>
250
+ <input type="text" id="searchInput" placeholder="Поиск товаров..." oninput="filterCategories()">
251
+ </div>
252
  </div>
253
 
254
  <div class="categories-container" id="categoriesContainer"></div>
 
256
 
257
  <div class="cart-bar" id="cartBar">
258
  <div class="cart-info">
259
+ <span style="font-size: 0.85rem; color: var(--text-muted); font-weight: 500;">Сумма заказа:</span>
260
  <span class="cart-total"><span id="cartTotalSum">0</span> {{ currency_code }}</span>
261
  </div>
262
+ <button class="checkout-btn" onclick="openCartModal()">Корзина <i class="fas fa-shopping-bag" style="margin-left:5px;"></i></button>
263
  </div>
264
 
265
+ <div class="modal-overlay" id="cartModal" onclick="if(event.target === this) closeCartModal()">
266
  <div class="modal-content">
267
  <div class="modal-header">
268
+ <h2>Ваш заказ</h2>
269
+ <button class="modal-close" onclick="closeCartModal()"><i class="fas fa-times"></i></button>
270
  </div>
271
  <div class="cart-item-list" id="cartItemList"></div>
272
+ <button class="confirm-btn" onclick="submitOrder()">Оформить заказ</button>
273
  </div>
274
  </div>
275
 
276
+ <div class="gallery-modal" id="galleryModal">
277
+ <button class="gallery-close" onclick="closeGallery()"><i class="fas fa-times"></i></button>
278
+ <div class="gallery-img-container" id="gallerySwipeArea">
279
+ <button class="gallery-nav prev" onclick="prevPhoto(event)"><i class="fas fa-chevron-left"></i></button>
280
+ <img src="" class="gallery-img" id="galleryImage">
281
+ <button class="gallery-nav next" onclick="nextPhoto(event)"><i class="fas fa-chevron-right"></i></button>
282
+ </div>
283
+ <div class="gallery-dots" id="galleryDots"></div>
284
+ </div>
285
+
286
  <script>
287
  const products = {{ products_json|safe }};
288
  const categoriesList = {{ categories_json|safe }};
 
290
  const currency = '{{ currency_code }}';
291
 
292
  let cart = {};
293
+ let currentGalleryPhotos = [];
294
+ let currentGalleryIndex = 0;
295
 
296
  function init() {
297
  renderCategories();
 
300
 
301
  function renderCategories() {
302
  const container = document.getElementById('categoriesContainer');
303
+ const prodContainer = document.getElementById('productsContainer');
304
+ prodContainer.style.display = 'none';
305
  container.style.display = 'grid';
306
  document.getElementById('backBtn').style.display = 'none';
307
  document.getElementById('pageTitle').innerText = 'Каталог';
 
315
  const div = document.createElement('div');
316
  div.className = 'category-item';
317
  div.onclick = () => showProducts(cat);
318
+ div.innerHTML = `
319
+ <div style="background: var(--bg); width: 50px; height: 50px; border-radius: 12px; display: flex; align-items: center; justify-content: center; margin-bottom: 5px;">
320
+ <i class="fas fa-box-open" style="font-size: 1.5rem; color: var(--primary);"></i>
321
+ </div>
322
+ <span class="name">${cat}</span>
323
+ <span class="count">${count} шт</span>
324
+ `;
325
  container.appendChild(div);
326
  });
327
  }
 
342
  const container = document.getElementById('productsContainer');
343
  container.style.display = 'flex';
344
  document.getElementById('backBtn').style.display = 'block';
345
+ document.getElementById('pageTitle').innerText = 'Поиск';
346
  container.innerHTML = '';
347
 
348
  const matchedProducts = products.filter(p =>
 
350
  p.category.toLowerCase().includes(query)
351
  );
352
 
353
+ if(matchedProducts.length === 0) {
354
+ container.innerHTML = '<div style="text-align:center; padding: 40px; color: var(--text-muted);">Ничего не найдено</div>';
355
+ } else {
356
+ matchedProducts.forEach(p => renderProductCard(p, container));
357
+ }
358
  }
359
 
360
  function renderProductCard(p, container) {
361
  const qty = cart[p.product_id] ? cart[p.product_id].quantity : 0;
362
+ const hasPhotos = p.photos && p.photos.length > 0;
363
+ const photoUrl = hasPhotos
364
  ? `https://huggingface.co/datasets/${repoId}/resolve/main/photos/${p.photos[0]}`
365
+ : 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDAiIGhlaWdodD0iMTAwIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjZjBmMHcwIi8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIGRvbWluYW50LWJhc2VsaW5lPSJtaWRkbGUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiNhMGEwYTAiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmb250LXNpemU9IjE0Ij7QndC10YIg0YTQvtGC0L48L3RleHQ+PC9zdmc+';
366
 
367
+ const photoIndicator = hasPhotos && p.photos.length > 1 ? `<div class="photo-count"><i class="fas fa-images"></i> ${p.photos.length}</div>` : '';
368
+ const imgClick = hasPhotos ? `onclick="openGallery('${p.product_id}')"` : '';
369
+
370
  const div = document.createElement('div');
371
  div.className = 'product-card';
372
  div.innerHTML = `
373
+ <div class="product-img-wrapper" ${imgClick}>
374
+ <img src="${photoUrl}" class="product-img">
375
+ ${photoIndicator}
376
+ </div>
377
  <div class="product-info">
378
  <div class="product-title">${p.name}</div>
379
  <div class="product-bottom">
380
  <div class="product-price">${p.price} ${currency}</div>
381
  <div class="quantity-control">
382
+ <button onclick="updateCart('${p.product_id}', -1)"><i class="fas fa-minus" style="font-size:0.8rem;"></i></button>
383
  <input type="text" id="qty-${p.product_id}" value="${qty}" readonly>
384
+ <button onclick="updateCart('${p.product_id}', 1)"><i class="fas fa-plus" style="font-size:0.8rem;"></i></button>
385
  </div>
386
  </div>
387
  </div>
 
399
  container.innerHTML = '';
400
 
401
  const catProducts = products.filter(p => p.category === category);
402
+ if(catProducts.length === 0) {
403
+ container.innerHTML = '<div style="text-align:center; padding: 40px; color: var(--text-muted);">В этой категории пока нет товаров</div>';
404
+ } else {
405
+ catProducts.forEach(p => renderProductCard(p, container));
406
+ }
407
  }
408
 
409
  function updateCart(productId, change) {
 
451
  list.innerHTML += `
452
  <div class="cart-item">
453
  <div class="cart-item-name">${item.name}</div>
454
+ <div class="cart-item-price">${item.quantity} x ${item.price} ${currency}</div>
455
  </div>
456
  `;
457
  }
458
+ const modal = document.getElementById('cartModal');
459
+ modal.style.display = 'flex';
460
+ setTimeout(() => modal.classList.add('active'), 10);
461
  }
462
 
463
  function closeCartModal() {
464
+ const modal = document.getElementById('cartModal');
465
+ modal.classList.remove('active');
466
+ setTimeout(() => modal.style.display = 'none', 300);
467
  }
468
 
469
  function submitOrder() {
470
  const cartArray = Object.values(cart);
471
+ if(cartArray.length === 0) return;
472
+ const btn = document.querySelector('.confirm-btn');
473
+ btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Оформление...';
474
+ btn.disabled = true;
475
+
476
  fetch('/create_order', {
477
  method: 'POST',
478
  headers: { 'Content-Type': 'application/json' },
 
484
  cart = {};
485
  window.location.href = `/order/${data.order_id}`;
486
  }
487
+ })
488
+ .catch(() => {
489
+ btn.innerHTML = 'Оформить заказ';
490
+ btn.disabled = false;
491
+ alert('Произошла ошибка. Попробуйте еще раз.');
492
  });
493
  }
494
 
495
+ function openGallery(productId) {
496
+ const product = products.find(p => p.product_id === productId);
497
+ if (!product || !product.photos || product.photos.length === 0) return;
498
+
499
+ currentGalleryPhotos = product.photos.map(p => `https://huggingface.co/datasets/${repoId}/resolve/main/photos/${p}`);
500
+ currentGalleryIndex = 0;
501
+
502
+ document.getElementById('galleryModal').style.display = 'flex';
503
+ document.body.style.overflow = 'hidden';
504
+ updateGalleryView();
505
+ }
506
+
507
+ function closeGallery() {
508
+ document.getElementById('galleryModal').style.display = 'none';
509
+ document.body.style.overflow = '';
510
+ }
511
+
512
+ function updateGalleryView() {
513
+ document.getElementById('galleryImage').src = currentGalleryPhotos[currentGalleryIndex];
514
+ const dotsContainer = document.getElementById('galleryDots');
515
+ dotsContainer.innerHTML = '';
516
+ if(currentGalleryPhotos.length > 1) {
517
+ currentGalleryPhotos.forEach((_, index) => {
518
+ const dot = document.createElement('div');
519
+ dot.className = `gallery-dot ${index === currentGalleryIndex ? 'active' : ''}`;
520
+ dotsContainer.appendChild(dot);
521
+ });
522
+ document.querySelectorAll('.gallery-nav').forEach(el => el.style.display = 'flex');
523
+ } else {
524
+ document.querySelectorAll('.gallery-nav').forEach(el => el.style.display = 'none');
525
+ }
526
+ }
527
+
528
+ function nextPhoto(e) {
529
+ if(e) e.stopPropagation();
530
+ if(currentGalleryPhotos.length <= 1) return;
531
+ currentGalleryIndex = (currentGalleryIndex + 1) % currentGalleryPhotos.length;
532
+ updateGalleryView();
533
+ }
534
+
535
+ function prevPhoto(e) {
536
+ if(e) e.stopPropagation();
537
+ if(currentGalleryPhotos.length <= 1) return;
538
+ currentGalleryIndex = (currentGalleryIndex - 1 + currentGalleryPhotos.length) % currentGalleryPhotos.length;
539
+ updateGalleryView();
540
+ }
541
+
542
+ let touchstartX = 0;
543
+ let touchendX = 0;
544
+ const swipeArea = document.getElementById('gallerySwipeArea');
545
+ swipeArea.addEventListener('touchstart', e => { touchstartX = e.changedTouches[0].screenX; });
546
+ swipeArea.addEventListener('touchend', e => {
547
+ touchendX = e.changedTouches[0].screenX;
548
+ if (touchstartX - touchendX > 50) nextPhoto();
549
+ if (touchendX - touchstartX > 50) prevPhoto();
550
+ });
551
+
552
  init();
553
  </script>
554
  </body>
 
560
  <html lang="ru">
561
  <head>
562
  <meta charset="UTF-8">
563
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
564
  <title>Накладная №{{ order.id }}</title>
565
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
566
  <style>
567
+ :root { --bg: #f4f6f8; --surface: #ffffff; --text: #2d3436; --border: #dfe6e9; --wa: #25D366; --print: #1e272e; }
568
+ * { box-sizing: border-box; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }
569
+ body { margin: 0; padding: max(20px, env(safe-area-inset-top)) 20px calc(100px + env(safe-area-inset-bottom)); background: var(--bg); display: flex; flex-direction: column; align-items: center; color: var(--text); }
570
+ .invoice-box { background: var(--surface); width: 100%; max-width: 900px; padding: 30px; box-shadow: 0 4px 20px rgba(0,0,0,0.05); border-radius: 16px; }
571
+ .header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 25px; border-bottom: 2px solid var(--border); padding-bottom: 15px; flex-wrap: wrap; gap: 10px; }
572
+ .header h1 { margin: 0; font-size: 1.8rem; font-weight: 800; }
573
+ .info-row { display: flex; justify-content: space-between; margin-bottom: 15px; font-size: 1rem; font-weight: 600; flex-wrap: wrap; gap: 10px; }
574
+
575
+ .table-responsive { width: 100%; overflow-x: auto; -webkit-overflow-scrolling: touch; margin-bottom: 20px; border-radius: 8px; border: 1px solid var(--border); }
576
+ table { width: 100%; border-collapse: collapse; min-width: 500px; }
577
+ th, td { border-bottom: 1px solid var(--border); padding: 12px; text-align: center; font-size: 0.95rem; }
578
+ th { background: #fafafa; font-weight: 700; color: #636e72; text-transform: uppercase; font-size: 0.8rem; letter-spacing: 0.5px; }
579
+ .img-cell img { width: 45px; height: 45px; object-fit: cover; border-radius: 8px; border: 1px solid #eee; }
580
+ .total-row { background: #fafafa; font-weight: 800; }
581
+ .total-row td { font-size: 1.1rem; border-bottom: none; }
582
 
583
+ .action-bar { position: fixed; bottom: 0; left: 0; width: 100%; background: var(--surface); box-shadow: 0 -4px 20px rgba(0,0,0,0.08); padding: 15px 20px calc(15px + env(safe-area-inset-bottom)); display: flex; gap: 15px; z-index: 100; justify-content: center; border-top-left-radius: 20px; border-top-right-radius: 20px; }
584
+ .action-bar-inner { display: flex; gap: 15px; width: 100%; max-width: 900px; }
585
+ .btn { flex: 1; padding: 15px 10px; border-radius: 12px; border: none; font-size: 1rem; font-weight: 700; cursor: pointer; color: #fff; display: flex; align-items: center; justify-content: center; gap: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); transition: transform 0.2s; white-space: nowrap; }
586
+ .btn:active { transform: scale(0.96); }
587
+ .btn-wa { background: var(--wa); box-shadow: 0 4px 15px rgba(37,211,102,0.3); }
588
+ .btn-print { background: var(--print); }
589
+ .btn-home { background: #0984e3; box-shadow: 0 4px 15px rgba(9,132,227,0.3); flex: 0 0 auto; padding: 15px 20px; }
590
 
591
  @media print {
592
  body { background: #fff; padding: 0; }
593
+ .invoice-box { box-shadow: none; padding: 0; max-width: 100%; border-radius: 0; }
594
+ .table-responsive { border: none; overflow: visible; }
595
+ table { min-width: 100%; }
596
+ th, td { border: 1px solid #000; }
597
  .action-bar { display: none; }
598
  }
599
+ @media (max-width: 600px) {
600
+ .header h1 { font-size: 1.4rem; }
601
+ .info-row { font-size: 0.9rem; }
602
+ .invoice-box { padding: 20px 15px; }
603
+ .btn { font-size: 0.9rem; flex-direction: column; padding: 10px; gap: 4px; }
604
+ .btn i { font-size: 1.2rem; }
605
+ }
606
  </style>
607
  </head>
608
  <body>
609
  <div class="invoice-box">
610
  <div class="header">
 
 
 
611
  <h1>Накладная</h1>
612
+ <div style="text-align: right;">
613
+ <div style="font-size: 1.1rem; font-weight: bold;"> {{ order.id }}</div>
614
+ <div style="color: #636e72; font-size: 0.9rem;">{{ order.created_at.split(' ')[0] }}</div>
615
  </div>
616
  </div>
617
 
618
  <div class="info-row">
619
+ <div>Покупатель: _________________</div>
620
+ <div>Статус: <span style="color: #00b894;">Новый</span></div>
 
 
 
 
621
  </div>
622
 
623
+ <div class="table-responsive">
624
+ <table>
625
+ <thead>
626
+ <tr>
627
+ <th style="width: 50px;"></th>
628
+ <th style="text-align: left;">Наименование</th>
629
+ <th>Фото</th>
630
+ <th>Кол-во</th>
631
+ <th>Цена</th>
632
+ <th>Сумма</th>
633
+ </tr>
634
+ </thead>
635
+ <tbody>
636
+ {% for item in order.cart %}
637
+ <tr>
638
+ <td>{{ loop.index }}</td>
639
+ <td style="text-align: left; font-weight: 500;">{{ item.name }}</td>
640
+ <td class="img-cell"><img src="{{ item.photo_url }}" alt="img"></td>
641
+ <td>{{ item.quantity }}</td>
642
+ <td>{{ item.price }}</td>
643
+ <td>{{ item.price * item.quantity }}</td>
644
+ </tr>
645
+ {% endfor %}
646
+ <tr class="total-row">
647
+ <td colspan="5" style="text-align: right; padding-right: 20px;">Итого:</td>
648
+ <td>{{ order.total_price }} {{ currency_code }}</td>
649
+ </tr>
650
+ </tbody>
651
+ </table>
652
+ </div>
653
  </div>
654
 
655
  <div class="action-bar">
656
+ <div class="action-bar-inner">
657
+ <a href="/" class="btn btn-home"><i class="fas fa-home"></i></a>
658
+ <button class="btn btn-print" onclick="window.print()"><i class="fas fa-print"></i> Печать</button>
659
+ <button class="btn btn-wa" onclick="sendToWA()"><i class="fab fa-whatsapp" style="font-size: 1.2rem;"></i> WhatsApp</button>
660
+ </div>
661
  </div>
662
 
663
  <script>
664
  function sendToWA() {
665
+ let msg = `Здравствуйте! Мой заказ №{{ order.id }}\nНакладная: ${window.location.href}`;
666
  window.open(`https://api.whatsapp.com/send?phone={{ whatsapp_number }}&text=${encodeURIComponent(msg)}`, '_blank');
667
  }
668
  </script>
 
675
  <html lang="ru">
676
  <head>
677
  <meta charset="UTF-8">
678
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
679
  <title>Админ-панель</title>
680
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
681
  <style>
682
+ :root { --primary: #2d3436; --bg: #f4f6f9; --surface: #ffffff; --border: #e0e6ed; --danger: #ff7675; --success: #00b894; --info: #0984e3; }
683
+ * { box-sizing: border-box; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }
684
+ body { background: var(--bg); padding: max(20px, env(safe-area-inset-top)) 15px calc(20px + env(safe-area-inset-bottom)); margin: 0; color: #2d3436; }
685
+ .container { max-width: 1000px; margin: 0 auto; }
686
+
687
+ .header-panel { background: var(--surface); padding: 20px; border-radius: 16px; box-shadow: 0 4px 15px rgba(0,0,0,0.03); margin-bottom: 20px; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 15px; }
688
+ .header-panel h1 { margin: 0; font-size: 1.5rem; font-weight: 800; }
689
+ .btn { padding: 12px 20px; border: none; border-radius: 10px; font-weight: 600; cursor: pointer; color: #fff; text-decoration: none; display: inline-flex; align-items: center; gap: 8px; font-size: 0.95rem; transition: opacity 0.2s; }
690
+ .btn:active { opacity: 0.8; }
691
+ .btn-primary { background: var(--info); }
692
+ .btn-success { background: var(--success); }
693
+ .btn-danger { background: var(--danger); padding: 8px 15px; font-size: 0.85rem; }
694
+ .btn-dark { background: var(--primary); }
695
+
696
+ .sync-panel { display: flex; gap: 10px; margin-bottom: 25px; flex-wrap: wrap; }
697
+ .sync-panel form { flex: 1; min-width: 200px; }
698
+ .sync-panel button { width: 100%; }
699
+
700
+ .card { background: var(--surface); padding: 20px; border-radius: 16px; box-shadow: 0 4px 15px rgba(0,0,0,0.03); margin-bottom: 20px; }
701
+ .card h2 { margin-top: 0; margin-bottom: 15px; font-size: 1.2rem; }
702
+
703
+ input[type="text"], input[type="number"], select { width: 100%; padding: 12px 15px; border: 1px solid var(--border); border-radius: 10px; font-size: 0.95rem; outline: none; transition: border-color 0.2s; background: #fafafa; }
704
+ input[type="text"]:focus, input[type="number"]:focus { border-color: var(--info); background: #fff; }
705
+
706
+ .add-cat-form { display: flex; gap: 10px; flex-wrap: wrap; }
707
+ .add-cat-form input { flex: 1; min-width: 200px; }
708
+ .add-cat-form button { white-space: nowrap; }
709
+
710
+ .category-block { border: 1px solid var(--border); margin-bottom: 15px; border-radius: 12px; overflow: hidden; background: #fff; }
711
+ .category-header { background: #fafafa; padding: 15px 20px; font-weight: 700; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid var(--border); }
712
+ .category-content { padding: 0; }
713
+
714
+ .product-item { display: flex; justify-content: space-between; align-items: center; padding: 15px 20px; border-bottom: 1px solid var(--border); flex-wrap: wrap; gap: 10px; }
715
+ .product-item:last-child { border-bottom: none; }
716
+ .product-info { display: flex; align-items: center; gap: 15px; min-width: 250px; flex: 1; }
717
+ .product-img { width: 50px; height: 50px; object-fit: cover; border-radius: 8px; border: 1px solid #eee; background: #fafafa; }
718
+ .product-details { display: flex; flex-direction: column; }
719
+ .product-name { font-weight: 600; font-size: 0.95rem; }
720
+ .product-meta { font-size: 0.8rem; color: #636e72; margin-top: 4px; }
721
+
722
+ .add-product-form { background: #fdfdfd; padding: 20px; border-top: 1px dashed var(--border); display: flex; flex-direction: column; gap: 15px; }
723
+ .form-row { display: flex; gap: 10px; flex-wrap: wrap; }
724
+ .form-row > * { flex: 1; min-width: 150px; }
725
+
726
+ .file-input-wrapper { position: relative; width: 100%; }
727
+ input[type="file"] { width: 100%; padding: 10px; border: 1px dashed #ccc; border-radius: 10px; background: #fafafa; font-size: 0.9rem; }
728
+
729
+ @media (max-width: 600px) {
730
+ .header-panel { flex-direction: column; align-items: stretch; text-align: center; }
731
+ .product-item { flex-direction: column; align-items: stretch; }
732
+ .product-info { width: 100%; }
733
+ .product-item form { align-self: flex-end; }
734
+ .form-row { flex-direction: column; }
735
+ }
736
  </style>
737
  </head>
738
  <body>
739
  <div class="container">
740
+ <div class="header-panel">
741
+ <h1><i class="fas fa-cog"></i> Админ-панель</h1>
742
+ <a href="/" class="btn btn-primary"><i class="fas fa-store"></i> В каталог</a>
743
  </div>
744
 
745
+ <div class="sync-panel">
746
+ <form method="POST" action="/force_upload" onsubmit="showLoading(this)">
747
+ <button type="submit" class="btn btn-success"><i class="fas fa-cloud-upload-alt"></i> Сохранить на сервер</button>
748
+ </form>
749
+ <form method="POST" action="/force_download" onsubmit="showLoading(this)">
750
+ <button type="submit" class="btn btn-info" style="background:#0984e3;"><i class="fas fa-cloud-download-alt"></i> Скачать с сервера</button>
751
+ </form>
752
  </div>
753
 
754
+ <div class="card">
755
+ <h2>Управление категориями</h2>
756
+ <form method="POST" class="add-cat-form">
757
+ <input type="hidden" name="action" value="add_category">
758
+ <input type="text" name="category_name" placeholder="Название новой категории" required autocomplete="off">
759
+ <button type="submit" class="btn btn-dark"><i class="fas fa-plus"></i> Добавить</button>
760
+ </form>
761
+ </div>
762
 
763
  {% for category in categories %}
764
  <div class="category-block">
765
  <div class="category-header">
766
+ <span><i class="fas fa-folder-open" style="color:var(--info); margin-right:8px;"></i> {{ category }}</span>
767
+ <form method="POST" style="margin:0;" onsubmit="return confirm('Удалить категорию и все ее товары?');">
768
  <input type="hidden" name="action" value="delete_category">
769
  <input type="hidden" name="category_name" value="{{ category }}">
770
+ <button type="submit" class="btn btn-danger"><i class="fas fa-trash-alt"></i></button>
771
  </form>
772
  </div>
773
+ <div class="category-content">
774
 
775
  {% for product in products %}
776
  {% if product.category == category %}
777
  <div class="product-item">
778
+ <div class="product-info">
779
+ {% if product.photos and product.photos|length > 0 %}
780
+ <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ product.photos[0] }}" class="product-img">
781
+ {% else %}
782
+ <div class="product-img" style="display:flex;align-items:center;justify-content:center;color:#ccc;"><i class="fas fa-image"></i></div>
783
+ {% endif %}
784
+ <div class="product-details">
785
+ <span class="product-name">{{ product.name }}</span>
786
+ <span class="product-meta">{{ product.price }} {{ currency_code }} • Фото: {{ product.photos|length if product.photos else 0 }}/10</span>
787
+ </div>
788
  </div>
789
  <form method="POST" style="margin:0;" onsubmit="return confirm('Удалить товар?');">
790
  <input type="hidden" name="action" value="delete_product">
791
  <input type="hidden" name="product_id" value="{{ product.product_id }}">
792
+ <button type="submit" class="btn btn-danger"><i class="fas fa-times"></i> Удалить</button>
793
  </form>
794
  </div>
795
  {% endif %}
796
  {% endfor %}
797
 
798
+ <form class="add-product-form" method="POST" enctype="multipart/form-data" onsubmit="showLoading(this)">
799
  <input type="hidden" name="action" value="add_product">
800
  <input type="hidden" name="category" value="{{ category }}">
801
+ <div style="font-weight: 600; font-size: 0.9rem; color: #636e72;">Добавить товар в "{{ category }}"</div>
802
+ <div class="form-row">
803
+ <input type="text" name="name" placeholder="Название товара" required autocomplete="off" style="flex:2;">
804
+ <input type="number" name="price" placeholder="Цена" required step="0.01" style="flex:1;">
805
  </div>
806
+ <div class="file-input-wrapper">
807
+ <input type="file" name="photos" accept="image/*" multiple max="10" required>
808
+ <div style="font-size: 0.8rem; color: #999; margin-top: 5px;">Можно выбрать до 10 фото</div>
809
  </div>
810
+ <button type="submit" class="btn btn-success" style="width: 100%; justify-content: center;"><i class="fas fa-plus-circle"></i> Добавить товар</button>
811
  </form>
812
  </div>
813
  </div>
814
  {% endfor %}
815
  </div>
816
+ <script>
817
+ function showLoading(form) {
818
+ const btn = form.querySelector('button[type="submit"]');
819
+ btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Загрузка...';
820
+ btn.style.pointerEvents = 'none';
821
+ btn.style.opacity = '0.7';
822
+ }
823
+ </script>
824
  </body>
825
  </html>
826
  '''
 
854
  "name": item['name'],
855
  "price": float(item['price']),
856
  "quantity": int(item['quantity']),
857
+ "photo_url": f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/photos/{item['photos'][0]}" if item.get('photos') else "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDAiIGhlaWdodD0iMTAwIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjZjBmMHcwIi8+PC9zdmc+"
858
  })
859
 
860
  order_id = f"SA-{datetime.now().strftime('%Y%m%d')}-{str(len(load_data().get('orders', {}))+1).zfill(3)}"
 
914
  name = request.form.get('name', '').strip()
915
  price = float(request.form.get('price', 0))
916
  category = request.form.get('category')
917
+ uploaded_photos = request.files.getlist('photos')[:10]
918
 
919
  photos_list = []
920
+ if uploaded_photos and HF_TOKEN_WRITE:
921
  uploads_dir = 'uploads_temp'
922
  os.makedirs(uploads_dir, exist_ok=True)
923
+ api = HfApi()
924
+ for photo in uploaded_photos:
925
+ if photo and photo.filename:
926
+ ext = os.path.splitext(photo.filename)[1].lower()
927
+ if ext not in ['.jpg', '.jpeg', '.png', '.webp', '.gif']:
928
+ continue
929
+ photo_filename = f"{uuid4().hex}{ext}"
930
+ temp_path = os.path.join(uploads_dir, photo_filename)
931
+ photo.save(temp_path)
932
+ try:
933
+ api.upload_file(
934
+ path_or_fileobj=temp_path,
935
+ path_in_repo=f"photos/{photo_filename}",
936
+ repo_id=REPO_ID,
937
+ repo_type="dataset",
938
+ token=HF_TOKEN_WRITE
939
+ )
940
+ photos_list.append(photo_filename)
941
+ except Exception:
942
+ pass
943
+ finally:
944
+ if os.path.exists(temp_path):
945
+ os.remove(temp_path)
946
 
947
  new_product = {
948
  'product_id': uuid4().hex,
 
988
  threading.Thread(target=periodic_backup, daemon=True).start()
989
 
990
  port = int(os.environ.get('PORT', 7860))
991
+ app.run(host='0.0.0.0', port=port)