Kgshop commited on
Commit
3488e6f
·
verified ·
1 Parent(s): 52c2d99

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +343 -391
app.py CHANGED
@@ -140,7 +140,7 @@ def load_data():
140
  'logo_url': DEFAULT_LOGO_URL,
141
  'whatsapp_number': DEFAULT_WHATSAPP_NUMBER,
142
  'track_inventory': False,
143
- 'enable_barcodes': False,
144
  'business_type': 'mixed',
145
  'system_mode': 'both',
146
  'customer_fields': {
@@ -173,7 +173,7 @@ def load_data():
173
  if 'logo_url' not in settings: settings['logo_url'] = DEFAULT_LOGO_URL; changed = True
174
  if 'whatsapp_number' not in settings: settings['whatsapp_number'] = DEFAULT_WHATSAPP_NUMBER; changed = True
175
  if 'track_inventory' not in settings: settings['track_inventory'] = False; changed = True
176
- if 'enable_barcodes' not in settings: settings['enable_barcodes'] = False; changed = True
177
  if 'business_type' not in settings: settings['business_type'] = 'mixed'; changed = True
178
  if 'system_mode' not in settings: settings['system_mode'] = 'both'; changed = True
179
  if 'customer_fields' not in settings:
@@ -204,8 +204,7 @@ def load_data():
204
  for order_id, order in env_data['orders'].items():
205
  if 'status' not in order: order['status'] = 'confirmed'; changed = True
206
  if 'staff_name' not in order: order['staff_name'] = ''; changed = True
207
- for item in order.get('cart', []):
208
- if 'assembled' not in item: item['assembled'] = 0; changed = True
209
 
210
  if changed or not os.path.exists(DATA_FILE):
211
  try:
@@ -242,7 +241,7 @@ def get_env_data(env_id):
242
  'logo_url': DEFAULT_LOGO_URL,
243
  'whatsapp_number': DEFAULT_WHATSAPP_NUMBER,
244
  'track_inventory': False,
245
- 'enable_barcodes': False,
246
  'business_type': 'mixed',
247
  'system_mode': 'both',
248
  'customer_fields': {'name': True, 'phone': True, 'city': True, 'address': False, 'zip': False},
@@ -441,7 +440,7 @@ CATALOG_TEMPLATE = '''
441
  .search-bar { padding: 15px 20px; background: var(--surface); border-bottom: 1px solid var(--border); }
442
  .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; }
443
  .search-container:focus-within { border-color: #dcdde1; background: var(--surface); box-shadow: 0 4px 12px rgba(0,0,0,0.05); }
444
- .search-container i { color: var(--text-muted); font-size: 0.9rem; }
445
  .search-bar input { width: 100%; padding: 12px 10px; border: none; background: transparent; outline: none; font-size: 0.95rem; }
446
 
447
  .categories-container { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 15px; padding: 20px; }
@@ -532,7 +531,7 @@ CATALOG_TEMPLATE = '''
532
  .btn-float-ig { background: radial-gradient(circle at 30% 107%, #fdf497 0%, #fdf497 5%, #fd5949 45%, #d6249f 60%, #285AEB 90%); }
533
  .btn-float-tg { background: #0088cc; }
534
  .btn-float-returns { background: #e17055; }
535
- .btn-float-assembly { background: #0984e3; }
536
 
537
  .staff-banner { background: #ffeaa7; color: #d63031; text-align: center; padding: 10px; font-weight: 600; font-size: 0.9rem; }
538
  .returns-list { display: flex; flex-direction: column; gap: 15px; }
@@ -540,9 +539,8 @@ CATALOG_TEMPLATE = '''
540
  .return-item-row { display: flex; justify-content: space-between; align-items: center; margin-top: 10px; font-size: 0.9rem; border-top: 1px dashed #ccc; padding-top: 10px; }
541
  .return-input { width: 50px; padding: 5px; border: 1px solid #ccc; border-radius: 6px; text-align: center; }
542
  .process-return-btn { background: #e17055; color: white; border: none; padding: 8px 15px; border-radius: 8px; cursor: pointer; margin-top: 10px; width: 100%; font-weight: 600; }
543
-
544
- #scannerModal .modal-content { max-height: none; background: #fff; }
545
- #reader { width: 100%; min-height: 300px; margin-top: 10px; }
546
 
547
  @media (min-width: 768px) {
548
  .categories-container { grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); }
@@ -573,8 +571,8 @@ CATALOG_TEMPLATE = '''
573
  <div class="search-container">
574
  <i class="fas fa-search"></i>
575
  <input type="text" id="searchInput" placeholder="Поиск товаров..." oninput="filterCategories()">
576
- {% if settings.enable_barcodes %}
577
- <i class="fas fa-qrcode" style="cursor:pointer; padding:5px 10px; font-size:1.1rem; color:var(--primary);" onclick="openScanner('search')"></i>
578
  {% endif %}
579
  </div>
580
  </div>
@@ -584,7 +582,7 @@ CATALOG_TEMPLATE = '''
584
 
585
  <div class="floating-socials">
586
  {% if mode == 'pos' and staff_id %}
587
- <a href="#" class="social-btn btn-float-assembly" onclick="openAssemblyModal()"><i class="fas fa-tasks"></i></a>
588
  <a href="#" class="social-btn btn-float-returns" onclick="openReturnsModal()"><i class="fas fa-undo"></i></a>
589
  {% endif %}
590
  {% if mode != 'pos' %}
@@ -644,32 +642,22 @@ CATALOG_TEMPLATE = '''
644
  </div>
645
  </div>
646
  </div>
647
-
648
- <div class="modal-overlay" id="assemblyModal" onclick="if(event.target === this) closeAssemblyModal()">
649
  <div class="modal-content">
650
  <div class="modal-header">
651
- <h2>Сборка накладных</h2>
652
- <button class="modal-close" onclick="closeAssemblyModal()"><i class="fas fa-times"></i></button>
653
  </div>
654
- <div style="margin-bottom:15px;">
655
- <input type="date" id="assemblyDateInput" onchange="loadAssemblyOrders()" style="padding:10px; border-radius:8px; border:1px solid #ccc; width:100%;">
656
  </div>
657
- <div id="assemblyContent" class="returns-list">
658
  <div style="text-align:center; padding:20px;"><i class="fas fa-spinner fa-spin"></i> Загрузка...</div>
659
  </div>
660
  </div>
661
  </div>
662
 
663
- <div class="modal-overlay" id="scannerModal">
664
- <div class="modal-content" style="max-height:90vh;">
665
- <div class="modal-header">
666
- <h2>Сканер штрихкода</h2>
667
- <button class="modal-close" onclick="closeScanner()"><i class="fas fa-times"></i></button>
668
- </div>
669
- <div id="reader"></div>
670
- </div>
671
- </div>
672
-
673
  <div class="gallery-modal" id="galleryModal">
674
  <button class="gallery-close" onclick="closeGallery()"><i class="fas fa-times"></i></button>
675
  <div class="gallery-img-container" id="gallerySwipeArea">
@@ -679,6 +667,14 @@ CATALOG_TEMPLATE = '''
679
  </div>
680
  <div class="gallery-dots" id="galleryDots"></div>
681
  </div>
 
 
 
 
 
 
 
 
682
 
683
  <script>
684
  const products = {{ products_json|safe }};
@@ -688,7 +684,7 @@ CATALOG_TEMPLATE = '''
688
  const envId = '{{ env_id }}';
689
  const mode = '{{ mode }}';
690
  const staffId = '{{ staff_id }}';
691
- const trackInventory = {{ 'true' if settings.track_inventory and settings.system_mode != 'external' else 'false' }};
692
  const businessType = '{{ settings.business_type }}';
693
  const cFields = {{ settings.customer_fields|tojson }};
694
 
@@ -696,33 +692,14 @@ CATALOG_TEMPLATE = '''
696
  let currentGalleryPhotos = [];
697
  let currentGalleryIndex = 0;
698
 
699
- let html5QrcodeScanner = null;
700
-
701
- function openScanner(target) {
702
- document.getElementById('scannerModal').style.display = 'flex';
703
- setTimeout(() => document.getElementById('scannerModal').classList.add('active'), 10);
704
- html5QrcodeScanner = new Html5QrcodeScanner("reader", { fps: 10, qrbox: {width: 250, height: 250} }, false);
705
- html5QrcodeScanner.render((decodedText) => {
706
- closeScanner();
707
- document.getElementById('searchInput').value = decodedText;
708
- filterCategories();
709
- }, () => {});
710
- }
711
-
712
- function closeScanner() {
713
- if(html5QrcodeScanner) { html5QrcodeScanner.clear(); html5QrcodeScanner = null; }
714
- const modal = document.getElementById('scannerModal');
715
- modal.classList.remove('active');
716
- setTimeout(() => modal.style.display = 'none', 300);
717
- }
718
-
719
  function init() {
720
  renderCategories();
721
  updateCartUI();
722
 
723
- const offset = new Date().getTimezoneOffset() * 60000;
724
- const localToday = new Date(Date.now() - offset).toISOString().split('T')[0];
725
- document.getElementById('assemblyDateInput').value = localToday;
 
726
  }
727
 
728
  function getCartKey(productId, variantIdx) {
@@ -777,11 +754,10 @@ CATALOG_TEMPLATE = '''
777
  container.innerHTML = '';
778
 
779
  const matchedProducts = products.filter(p => {
780
- const q = query.toLowerCase();
781
- const m1 = p.name.toLowerCase().includes(q) || p.category.toLowerCase().includes(q);
782
- const m2 = p.barcode && p.barcode.toLowerCase().includes(q);
783
- const m3 = p.variants && p.variants.some(v => v.barcode && v.barcode.toLowerCase().includes(q));
784
- return m1 || m2 || m3;
785
  });
786
 
787
  if(matchedProducts.length === 0) {
@@ -1259,40 +1235,37 @@ CATALOG_TEMPLATE = '''
1259
  }
1260
  });
1261
  }
1262
-
1263
- function openAssemblyModal() {
1264
- const modal = document.getElementById('assemblyModal');
1265
  modal.style.display = 'flex';
1266
  setTimeout(() => modal.classList.add('active'), 10);
1267
- loadAssemblyOrders();
1268
  }
1269
-
1270
- function closeAssemblyModal() {
1271
- const modal = document.getElementById('assemblyModal');
1272
  modal.classList.remove('active');
1273
  setTimeout(() => modal.style.display = 'none', 300);
1274
  }
1275
-
1276
- function loadAssemblyOrders() {
1277
- const dateVal = document.getElementById('assemblyDateInput').value;
1278
- fetch(`/${envId}/api/staff_orders/${staffId}?date=${dateVal}`)
1279
  .then(r => r.json())
1280
  .then(data => {
1281
- const content = document.getElementById('assemblyContent');
1282
  content.innerHTML = '';
1283
  if(data.length === 0) {
1284
- content.innerHTML = '<div style="text-align:center; padding:20px;">Нет накладных за эту дату</div>';
1285
  return;
1286
  }
1287
-
1288
  data.forEach(order => {
1289
  content.innerHTML += `
1290
  <div class="return-order-item" style="margin-bottom:10px;">
1291
- <div style="display:flex; justify-content:space-between; align-items:center;">
1292
- <div><b>№ ${order.id}</b> <div style="color:#636e72; font-size:0.85rem;">${order.created_at}</div></div>
1293
- <a href="/${envId}/assembly/${order.id}" target="_blank" class="process-return-btn" style="text-decoration:none; display:inline-block; width:auto; padding:6px 12px; background:var(--primary);">Перейти к сборке</a>
1294
- </div>
1295
- <div style="font-size:0.9rem; margin-top:5px;">Покупатель: ${order.customer_name || 'Касса'}</div>
1296
  </div>
1297
  `;
1298
  });
@@ -1356,150 +1329,31 @@ CATALOG_TEMPLATE = '''
1356
  if (touchendX - touchstartX > 50) prevPhoto();
1357
  });
1358
 
1359
- init();
1360
- </script>
1361
- </body>
1362
- </html>
1363
- '''
1364
-
1365
- ASSEMBLY_TEMPLATE = '''
1366
- <!DOCTYPE html>
1367
- <html lang="ru">
1368
- <head>
1369
- <meta charset="UTF-8">
1370
- <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
1371
- <title>Сборка №{{ order.id }}</title>
1372
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
1373
- <style>
1374
- :root { --bg: #f4f6f8; --surface: #ffffff; --text: #2d3436; --border: #dfe6e9; --primary: #0984e3; --success: #00b894; }
1375
- * { box-sizing: border-box; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }
1376
- 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); }
1377
- .assembly-box { background: var(--surface); width: 100%; max-width: 900px; padding: 20px; box-shadow: 0 4px 20px rgba(0,0,0,0.05); border-radius: 16px; }
1378
- .header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; border-bottom: 2px solid var(--border); padding-bottom: 15px; flex-wrap: wrap; gap: 10px; }
1379
- .header h1 { margin: 0; font-size: 1.5rem; font-weight: 800; }
1380
- .item-row { display: flex; justify-content: space-between; align-items: center; padding: 15px 0; border-bottom: 1px dashed var(--border); gap: 15px; flex-wrap: wrap; }
1381
- .item-info { flex: 1; min-width: 200px; }
1382
- .item-name { font-weight: 600; font-size: 1rem; }
1383
- .item-meta { font-size: 0.85rem; color: #636e72; margin-top: 4px; }
1384
- .control-group { display: flex; align-items: center; gap: 15px; background: #fafafa; padding: 8px 12px; border-radius: 12px; border: 1px solid var(--border); }
1385
- .target-qty { font-weight: 700; color: #d63031; width: 40px; text-align: center; }
1386
- .qty-controls { display: flex; align-items: center; background: #fff; border-radius: 8px; border: 1px solid var(--border); overflow: hidden; }
1387
- .qty-controls button { border: none; background: transparent; width: 35px; height: 35px; font-size: 1.2rem; cursor: pointer; color: var(--primary); display: flex; align-items: center; justify-content: center; }
1388
- .qty-controls button:active { background: #e0e0e0; }
1389
- .qty-controls input { width: 45px; text-align: center; font-weight: 700; font-size: 1.1rem; border: none; background: transparent; color: var(--success); outline: none; }
1390
-
1391
- .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; }
1392
- .action-bar-inner { display: flex; gap: 15px; width: 100%; max-width: 900px; }
1393
- .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; }
1394
- .btn:active { transform: scale(0.96); }
1395
- .btn-save { background: var(--success); }
1396
- .btn-copy { background: var(--primary); }
1397
- </style>
1398
- </head>
1399
- <body>
1400
- <div class="assembly-box">
1401
- <div class="header">
1402
- <h1>Сборка заказа</h1>
1403
- <div style="text-align: right;">
1404
- <div style="font-size: 1.1rem; font-weight: bold;">№ {{ order.id }}</div>
1405
- <div style="color: #636e72; font-size: 0.9rem;">{{ order.customer_name if order.customer_name else 'Касса' }}</div>
1406
- </div>
1407
- </div>
1408
-
1409
- <div id="itemsContainer">
1410
- {% for item in order.cart %}
1411
- {% if item.quantity > 0 %}
1412
- <div class="item-row" data-ckey="{{ item.c_key }}" data-target="{{ item.quantity }}">
1413
- <div class="item-info">
1414
- <div class="item-name">{{ item.name }}</div>
1415
- {% if item.variant_name %}
1416
- <div class="item-meta">Вариант: {{ item.variant_name }}</div>
1417
- {% endif %}
1418
- </div>
1419
- <div class="control-group">
1420
- <div style="font-size:0.8rem; color:#636e72; text-align:center;">Нужно<br><span class="target-qty">{{ item.quantity }}</span></div>
1421
- <div class="qty-controls">
1422
- <button onclick="changeQty(this, -1)"><i class="fas fa-minus"></i></button>
1423
- <input type="number" class="assembled-input" value="{{ item.assembled|default(0) }}" min="0" max="{{ item.quantity }}" onchange="validateQty(this)">
1424
- <button onclick="changeQty(this, 1)"><i class="fas fa-plus"></i></button>
1425
- </div>
1426
- </div>
1427
- </div>
1428
- {% endif %}
1429
- {% endfor %}
1430
- </div>
1431
- </div>
1432
-
1433
- <div class="action-bar">
1434
- <div class="action-bar-inner">
1435
- <button class="btn btn-copy" onclick="copyLink()"><i class="fas fa-link"></i> Копировать ссылку</button>
1436
- <button class="btn btn-save" onclick="saveAssembly()" id="saveBtn"><i class="fas fa-check-circle"></i> Сохранить сборку</button>
1437
- </div>
1438
- </div>
1439
-
1440
- <script>
1441
- const envId = '{{ env_id }}';
1442
- const orderId = '{{ order.id }}';
1443
-
1444
- function changeQty(btn, change) {
1445
- const input = btn.parentElement.querySelector('input');
1446
- const target = parseInt(btn.closest('.item-row').dataset.target);
1447
- let val = parseInt(input.value) || 0;
1448
- val += change;
1449
- if(val < 0) val = 0;
1450
- if(val > target) val = target;
1451
- input.value = val;
1452
- }
1453
-
1454
- function validateQty(input) {
1455
- const target = parseInt(input.closest('.item-row').dataset.target);
1456
- let val = parseInt(input.value);
1457
- if(isNaN(val) || val < 0) val = 0;
1458
- if(val > target) val = target;
1459
- input.value = val;
1460
- }
1461
-
1462
- function copyLink() {
1463
- const link = window.location.href;
1464
- navigator.clipboard.writeText(link).then(() => {
1465
- alert('Ссылка на сборку скопирована!');
1466
  }).catch(err => {
1467
- alert('Не удалось скопировать ссылку');
 
 
1468
  });
 
1469
  }
1470
 
1471
- function saveAssembly() {
1472
- const btn = document.getElementById('saveBtn');
1473
- btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Сохранение...';
1474
- btn.disabled = true;
1475
-
1476
- const rows = document.querySelectorAll('.item-row');
1477
- let assemblyData = {};
1478
- rows.forEach(row => {
1479
- const ckey = row.dataset.ckey;
1480
- const qty = parseInt(row.querySelector('.assembled-input').value) || 0;
1481
- assemblyData[ckey] = qty;
1482
- });
1483
-
1484
- fetch(`/${envId}/api/assembly/${orderId}`, {
1485
- method: 'POST',
1486
- headers: { 'Content-Type': 'application/json' },
1487
- body: JSON.stringify(assemblyData)
1488
- })
1489
- .then(r => r.json())
1490
- .then(data => {
1491
- btn.innerHTML = '<i class="fas fa-check-circle"></i> Сохранено';
1492
- setTimeout(() => {
1493
- btn.innerHTML = '<i class="fas fa-check-circle"></i> Сохранить сборку';
1494
- btn.disabled = false;
1495
- }, 2000);
1496
- })
1497
- .catch(() => {
1498
- alert('Ошибка сохранения');
1499
- btn.innerHTML = '<i class="fas fa-check-circle"></i> Сохранить сборку';
1500
- btn.disabled = false;
1501
- });
1502
  }
 
 
1503
  </script>
1504
  </body>
1505
  </html>
@@ -1514,7 +1368,7 @@ ORDER_TEMPLATE = '''
1514
  <title>Накладная №{{ order.id }}</title>
1515
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
1516
  <style>
1517
- :root { --bg: #f4f6f8; --surface: #ffffff; --text: #2d3436; --border: #dfe6e9; --wa: #25D366; --print: #1e272e; --primary: #1a1a1a; --info: #0984e3; }
1518
  * { box-sizing: border-box; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }
1519
  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); }
1520
  .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; }
@@ -1636,6 +1490,7 @@ ORDER_TEMPLATE = '''
1636
  {% set ppb = item.pieces_per_box|default(1)|int %}
1637
  {% set boxes = item.quantity // ppb %}
1638
  {% set remainder = item.quantity % ppb %}
 
1639
  {% if item.quantity > 0 %}
1640
  <tr>
1641
  <td>{{ loop.index }}</td>
@@ -1644,9 +1499,7 @@ ORDER_TEMPLATE = '''
1644
  {% if item.variant_name %}
1645
  <div style="font-size: 0.85rem; color: #636e72;">Вариант: {{ item.variant_name }}</div>
1646
  {% endif %}
1647
- {% if item.assembled and item.assembled > 0 %}
1648
- <div style="font-size:0.8rem; color:var(--info); font-weight:600; margin-top:4px;"><i class="fas fa-box-open"></i> Собрано: {{ item.assembled }} из {{ item.quantity }}</div>
1649
- {% endif %}
1650
  </td>
1651
  <td class="img-cell"><img src="{{ item.photo_url }}" alt="img"></td>
1652
  <td style="text-align: center;">
@@ -1767,6 +1620,134 @@ ORDER_TEMPLATE = '''
1767
  </html>
1768
  '''
1769
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1770
  ADMIN_TEMPLATE = '''
1771
  <!DOCTYPE html>
1772
  <html lang="ru">
@@ -1809,7 +1790,7 @@ ADMIN_TEMPLATE = '''
1809
  .add-cat-form button { white-space: nowrap; }
1810
 
1811
  .search-bar-admin { position: relative; margin-bottom: 20px; }
1812
- .search-bar-admin i { position: absolute; left: 15px; top: 50%; transform: translateY(-50%); color: #636e72; }
1813
  .search-bar-admin input { padding-left: 40px; background: var(--surface); border: none; box-shadow: 0 4px 15px rgba(0,0,0,0.03); }
1814
 
1815
  .category-block { border: 1px solid var(--border); margin-bottom: 15px; border-radius: 12px; overflow: hidden; background: #fff; }
@@ -1863,17 +1844,6 @@ ADMIN_TEMPLATE = '''
1863
 
1864
  .badge { background: var(--danger); color: white; padding: 2px 8px; border-radius: 12px; font-size: 0.8rem; margin-left: 5px; }
1865
 
1866
- .barcode-wrapper { display: flex; align-items: center; gap: 5px; }
1867
- .barcode-wrapper input { flex: 1; }
1868
- .barcode-btn { background: #fafafa; border: 1px solid var(--border); padding: 10px 15px; border-radius: 10px; cursor: pointer; color: var(--primary); }
1869
-
1870
- .modal-overlay { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.6); z-index: 200; justify-content: center; align-items: center; }
1871
- .modal-overlay.active { display: flex; }
1872
- .modal-content { background: var(--surface); width: 90%; max-width: 500px; border-radius: 16px; padding: 20px; position: relative; }
1873
- .modal-header { display: flex; justify-content: space-between; margin-bottom: 15px; align-items: center; }
1874
- .modal-header h2 { margin: 0; font-size: 1.2rem; }
1875
- .modal-close { background: none; border: none; font-size: 1.5rem; cursor: pointer; color: var(--text); }
1876
-
1877
  @media (max-width: 600px) {
1878
  .header-panel { flex-direction: column; align-items: stretch; text-align: center; }
1879
  .product-item { flex-direction: column; align-items: stretch; }
@@ -2046,7 +2016,7 @@ ADMIN_TEMPLATE = '''
2046
  {% if sys_mode != 'external' %}
2047
  <div style="font-weight: 600; margin-bottom: 5px; border-top: 1px solid var(--border); padding-top: 15px;">Учет товара:</div>
2048
  <label><input type="checkbox" name="track_inventory" {% if settings.track_inventory %}checked{% endif %}> Включить остатки на складе</label>
2049
- <label style="margin-top:10px; display:block;"><input type="checkbox" name="enable_barcodes" {% if settings.enable_barcodes %}checked{% endif %}> Учет по штрихкодам</label>
2050
  {% endif %}
2051
 
2052
  <div style="font-weight: 600; margin-bottom: 5px; border-top: 1px solid var(--border); padding-top: 15px; margin-top: 10px;">Социальные сети:</div>
@@ -2080,10 +2050,7 @@ ADMIN_TEMPLATE = '''
2080
 
2081
  <div class="search-bar-admin">
2082
  <i class="fas fa-search"></i>
2083
- <input type="text" id="adminSearch" placeholder="Поиск по категориям, товарам, штрихкодам..." oninput="filterAdmin()">
2084
- {% if settings.enable_barcodes and sys_mode != 'external' %}
2085
- <i class="fas fa-qrcode" style="cursor:pointer; right:15px; left:auto; color:var(--primary);" onclick="openScanner('adminSearch', filterAdmin)"></i>
2086
- {% endif %}
2087
  </div>
2088
 
2089
  {% for category in categories %}
@@ -2116,6 +2083,17 @@ ADMIN_TEMPLATE = '''
2116
  <label>Название товара</label>
2117
  <input type="text" name="name" placeholder="Введите название" required autocomplete="off">
2118
  </div>
 
 
 
 
 
 
 
 
 
 
 
2119
  <div class="form-group main-price-container">
2120
  <label>Цена за ед.</label>
2121
  <input type="number" name="price" placeholder="Цена" step="0.01" class="main-price-input">
@@ -2148,16 +2126,6 @@ ADMIN_TEMPLATE = '''
2148
  <input type="number" name="stock" placeholder="Остаток" class="main-stock-input">
2149
  </div>
2150
  {% endif %}
2151
-
2152
- {% if settings.enable_barcodes and sys_mode != 'external' %}
2153
- <div class="form-group">
2154
- <label>Штрихкод</label>
2155
- <div class="barcode-wrapper">
2156
- <input type="text" name="barcode" id="bc_add_{{ loop.index }}" placeholder="Штрихкод">
2157
- <button type="button" class="barcode-btn" onclick="openScanner('bc_add_{{ loop.index }}')"><i class="fas fa-qrcode"></i></button>
2158
- </div>
2159
- </div>
2160
- {% endif %}
2161
  </div>
2162
 
2163
  <div class="variants-container" id="variants-container-add-{{ loop.index }}">
@@ -2184,7 +2152,7 @@ ADMIN_TEMPLATE = '''
2184
 
2185
  {% for product in products %}
2186
  {% if product.category == category %}
2187
- <div class="product-item" data-barcode="{{ product.barcode|default('') }}" data-varbarcodes="{{ product.variants|map(attribute='barcode')|join(',') }}">
2188
  <div class="product-info">
2189
  {% if product.photos and product.photos|length > 0 %}
2190
  <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ product.photos[0] }}" class="product-img">
@@ -2219,10 +2187,6 @@ ADMIN_TEMPLATE = '''
2219
  • Остаток по вариантам
2220
  {% endif %}
2221
  {% endif %}
2222
-
2223
- {% if settings.enable_barcodes and product.barcode and sys_mode != 'external' %}
2224
- • Штрихкод: {{ product.barcode }}
2225
- {% endif %}
2226
  </span>
2227
  </div>
2228
  </div>
@@ -2247,6 +2211,17 @@ ADMIN_TEMPLATE = '''
2247
  <label>Название товара</label>
2248
  <input type="text" name="name" value="{{ product.name }}" required autocomplete="off">
2249
  </div>
 
 
 
 
 
 
 
 
 
 
 
2250
  <div class="form-group main-price-container" {% if product.has_variant_prices %}style="display:none;"{% endif %}>
2251
  <label>Цена за ед.</label>
2252
  <input type="number" name="price" value="{{ product.price }}" step="0.01" class="main-price-input" {% if not product.has_variant_prices %}required{% endif %}>
@@ -2279,16 +2254,6 @@ ADMIN_TEMPLATE = '''
2279
  <input type="number" name="stock" value="{{ product.stock }}" class="main-stock-input">
2280
  </div>
2281
  {% endif %}
2282
-
2283
- {% if settings.enable_barcodes and sys_mode != 'external' %}
2284
- <div class="form-group">
2285
- <label>Штрихкод</label>
2286
- <div class="barcode-wrapper">
2287
- <input type="text" name="barcode" id="bc_edit_{{ product.product_id }}" value="{{ product.barcode|default('') }}" placeholder="Штрихкод">
2288
- <button type="button" class="barcode-btn" onclick="openScanner('bc_edit_{{ product.product_id }}')"><i class="fas fa-qrcode"></i></button>
2289
- </div>
2290
- </div>
2291
- {% endif %}
2292
  </div>
2293
 
2294
  <div class="variants-container" id="variants-container-edit-{{ product.product_id }}">
@@ -2298,12 +2263,20 @@ ADMIN_TEMPLATE = '''
2298
  </div>
2299
  <div id="variants-list-edit-{{ product.product_id }}">
2300
  {% for variant in product.variants %}
2301
- {% set vid = uuid.uuid4().hex %}
2302
  <div class="variant-row">
2303
  <div class="form-group">
2304
  <label>Название</label>
2305
  <input type="text" name="variant_name[]" value="{{ variant.name }}" placeholder="Цвет, размер" required>
2306
  </div>
 
 
 
 
 
 
 
 
 
2307
  <div class="form-group var-price-input" {% if not product.has_variant_prices %}style="display:none;"{% endif %}>
2308
  <label>Цена</label>
2309
  <input type="number" name="variant_price[]" value="{{ variant.price }}" placeholder="Цена за ед." step="0.01" {% if product.has_variant_prices %}required{% endif %}>
@@ -2320,17 +2293,6 @@ ADMIN_TEMPLATE = '''
2320
  <input type="number" name="variant_stock[]" value="{{ variant.stock }}" placeholder="Остаток">
2321
  </div>
2322
  {% endif %}
2323
- {% if settings.enable_barcodes and sys_mode != 'external' %}
2324
- <div class="form-group">
2325
- <label>Штрихкод</label>
2326
- <div class="barcode-wrapper">
2327
- <input type="text" name="variant_barcode[]" id="bc_var_{{ vid }}" value="{{ variant.barcode|default('') }}" placeholder="Штрихкод">
2328
- <button type="button" class="barcode-btn" onclick="openScanner('bc_var_{{ vid }}')"><i class="fas fa-qrcode"></i></button>
2329
- </div>
2330
- </div>
2331
- {% else %}
2332
- <input type="hidden" name="variant_barcode[]" value="{{ variant.barcode|default('') }}">
2333
- {% endif %}
2334
  <button type="button" class="remove-variant-btn" onclick="this.parentElement.remove(); updateMainStockVisibility('edit-prod-{{ product.product_id }}')"><i class="fas fa-times-circle"></i></button>
2335
  </div>
2336
  {% endfor %}
@@ -2357,53 +2319,20 @@ ADMIN_TEMPLATE = '''
2357
  </div>
2358
  {% endfor %}
2359
  </div>
2360
-
2361
- <div class="modal-overlay" id="scannerModal">
2362
- <div class="modal-content">
2363
- <div class="modal-header">
2364
- <h2>Сканер штрихкода</h2>
2365
- <button class="modal-close" onclick="closeScanner()"><i class="fas fa-times"></i></button>
2366
- </div>
2367
- <div id="reader" style="width:100%; min-height:300px;"></div>
2368
  </div>
2369
  </div>
2370
 
2371
  <script>
2372
  const trackInventory = {{ 'true' if settings.track_inventory and sys_mode != 'external' else 'false' }};
2373
- const enableBarcodes = {{ 'true' if settings.enable_barcodes and sys_mode != 'external' else 'false' }};
2374
  const businessType = '{{ settings.business_type }}';
2375
 
2376
- let html5QrcodeScanner = null;
2377
- let currentScanTarget = '';
2378
- let scanCallback = null;
2379
-
2380
- function openScanner(targetId, callback = null) {
2381
- currentScanTarget = targetId;
2382
- scanCallback = callback;
2383
- document.getElementById('scannerModal').style.display = 'flex';
2384
- setTimeout(() => document.getElementById('scannerModal').classList.add('active'), 10);
2385
-
2386
- html5QrcodeScanner = new Html5QrcodeScanner("reader", { fps: 10, qrbox: {width: 250, height: 250} }, false);
2387
- html5QrcodeScanner.render(onScanSuccess, onScanFailure);
2388
- }
2389
-
2390
- function closeScanner() {
2391
- if(html5QrcodeScanner) { html5QrcodeScanner.clear(); html5QrcodeScanner = null; }
2392
- const modal = document.getElementById('scannerModal');
2393
- modal.classList.remove('active');
2394
- setTimeout(()=>modal.style.display='none', 300);
2395
- }
2396
-
2397
- function onScanSuccess(decodedText, decodedResult) {
2398
- closeScanner();
2399
- const el = document.getElementById(currentScanTarget);
2400
- if(el) {
2401
- el.value = decodedText;
2402
- if(scanCallback) scanCallback();
2403
- }
2404
- }
2405
- function onScanFailure(error) {}
2406
-
2407
  function showLoading(form) {
2408
  const btn = form.querySelector('button[type="submit"]');
2409
  btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Загрузка...';
@@ -2458,6 +2387,20 @@ ADMIN_TEMPLATE = '''
2458
  <label>Название</label>
2459
  <input type="text" name="variant_name[]" placeholder="Цвет, размер" required>
2460
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2461
  <div class="form-group var-price-input" ${displayStyle}>
2462
  <label>Цена</label>
2463
  <input type="number" name="variant_price[]" placeholder="Цена за ед." step="0.01" ${reqAttr}>
@@ -2479,20 +2422,6 @@ ADMIN_TEMPLATE = '''
2479
  <input type="number" name="variant_stock[]" placeholder="Остаток">
2480
  </div>`;
2481
  }
2482
-
2483
- if (enableBarcodes) {
2484
- let vid = 'vbc_' + Math.floor(Math.random()*1000000);
2485
- html += `
2486
- <div class="form-group">
2487
- <label>Штрихкод</label>
2488
- <div class="barcode-wrapper">
2489
- <input type="text" name="variant_barcode[]" id="${vid}" placeholder="Штрихкод">
2490
- <button type="button" class="barcode-btn" onclick="openScanner('${vid}')"><i class="fas fa-qrcode"></i></button>
2491
- </div>
2492
- </div>`;
2493
- } else {
2494
- html += `<input type="hidden" name="variant_barcode[]" value="">`;
2495
- }
2496
 
2497
  html += `<button type="button" class="remove-variant-btn" onclick="this.parentElement.remove(); updateMainStockVisibility('${formId}')"><i class="fas fa-times-circle"></i></button>`;
2498
 
@@ -2532,15 +2461,22 @@ ADMIN_TEMPLATE = '''
2532
  }
2533
 
2534
  function updateMainStockVisibility(formId) {
2535
- if(!trackInventory) return;
2536
  const form = document.getElementById(formId);
2537
  const variants = form.querySelectorAll('.variant-row');
2538
- const mainStock = form.querySelector('.main-stock-container');
2539
- if(mainStock) {
2540
- if(variants.length > 0) {
2541
- mainStock.style.display = 'none';
2542
- } else {
2543
- mainStock.style.display = 'flex';
 
 
 
 
 
 
 
 
2544
  }
2545
  }
2546
  }
@@ -2562,10 +2498,7 @@ ADMIN_TEMPLATE = '''
2562
 
2563
  products.forEach(prod => {
2564
  const prodName = prod.querySelector('.product-name').innerText.toLowerCase();
2565
- const barcode = prod.dataset.barcode.toLowerCase();
2566
- const varbarcodes = prod.dataset.varbarcodes.toLowerCase();
2567
-
2568
- if (prodName.includes(query) || catMatch || barcode.includes(query) || varbarcodes.includes(query)) {
2569
  prod.style.display = 'flex';
2570
  hasVisibleProduct = true;
2571
  } else {
@@ -2619,6 +2552,30 @@ ADMIN_TEMPLATE = '''
2619
  }
2620
  document.body.removeChild(textArea);
2621
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2622
 
2623
  document.querySelectorAll('.add-product-wrapper').forEach(wrapper => {
2624
  const cb = wrapper.querySelector('input[name="has_variant_prices"]');
@@ -2933,7 +2890,7 @@ INVENTORY_TEMPLATE = '''
2933
  <div id="current" class="tab-content active">
2934
  <div class="search-bar">
2935
  <i class="fas fa-search"></i>
2936
- <input type="text" id="searchInput" placeholder="Поиск товара или штрихкода..." oninput="filterTable()">
2937
  </div>
2938
 
2939
  <div class="table-container">
@@ -2942,7 +2899,6 @@ INVENTORY_TEMPLATE = '''
2942
  <tr>
2943
  <th>Товар</th>
2944
  <th>Категория</th>
2945
- <th>Штрихкод</th>
2946
  <th>Остаток</th>
2947
  <th>Действие (Приход / Списание)</th>
2948
  </tr>
@@ -2950,10 +2906,9 @@ INVENTORY_TEMPLATE = '''
2950
  <tbody id="inventoryTable">
2951
  {% for p in products %}
2952
  {% if not p.variants %}
2953
- <tr class="inv-row" data-barcode="{{ p.barcode|default('') }}">
2954
  <td class="prod-name"><strong>{{ p.name }}</strong></td>
2955
  <td>{{ p.category }}</td>
2956
- <td>{{ p.barcode|default('-') }}</td>
2957
  <td><strong>{{ p.stock if p.stock != "" else "0" }}</strong></td>
2958
  <td>
2959
  <div class="action-cell">
@@ -2966,10 +2921,9 @@ INVENTORY_TEMPLATE = '''
2966
  </tr>
2967
  {% else %}
2968
  {% for v in p.variants %}
2969
- <tr class="inv-row" data-barcode="{{ v.barcode|default('') }}">
2970
  <td class="prod-name"><strong>{{ p.name }}</strong> <span style="color:#636e72;">({{ v.name }})</span></td>
2971
  <td>{{ p.category }}</td>
2972
- <td>{{ v.barcode|default('-') }}</td>
2973
  <td><strong>{{ v.stock if v.stock != "" else "0" }}</strong></td>
2974
  <td>
2975
  <div class="action-cell">
@@ -3039,8 +2993,7 @@ INVENTORY_TEMPLATE = '''
3039
  const rows = document.querySelectorAll('.inv-row');
3040
  rows.forEach(row => {
3041
  const text = row.querySelector('.prod-name').innerText.toLowerCase();
3042
- const barcode = row.dataset.barcode.toLowerCase();
3043
- if(text.includes(query) || barcode.includes(query)) row.style.display = 'table-row';
3044
  else row.style.display = 'none';
3045
  });
3046
  }
@@ -3121,7 +3074,7 @@ def create_environment():
3121
  "logo_url": DEFAULT_LOGO_URL,
3122
  "whatsapp_number": DEFAULT_WHATSAPP_NUMBER,
3123
  "track_inventory": False,
3124
- "enable_barcodes": False,
3125
  "business_type": "mixed",
3126
  "system_mode": "both",
3127
  "customer_fields": {'name': True, 'phone': True, 'city': True, 'address': False, 'zip': False},
@@ -3158,7 +3111,7 @@ def update_env_mode(env_id):
3158
  all_data[env_id]['settings']['system_mode'] = mode
3159
  if mode == 'external':
3160
  all_data[env_id]['settings']['track_inventory'] = False
3161
- all_data[env_id]['settings']['enable_barcodes'] = False
3162
  save_data(all_data)
3163
  flash(f'Режим среды {env_id} обновлен.', 'success')
3164
  return redirect(url_for('admhosto'))
@@ -3269,18 +3222,36 @@ def restore_stock(c_key, pid, vidx, return_qty, products):
3269
  def get_staff_orders(env_id, staff_id):
3270
  data = get_env_data(env_id)
3271
  orders = data.get('orders', {})
3272
- date_filter = request.args.get('date', '')
 
3273
 
3274
- staff_orders = []
3275
- for o in orders.values():
3276
- if o.get('staff_id') == staff_id and (o.get('status') == 'pos' or o.get('status') == 'confirmed'):
3277
- if date_filter and not o.get('created_at', '').startswith(date_filter):
3278
- continue
3279
- staff_orders.append(o)
3280
-
3281
  staff_orders.sort(key=lambda x: x.get('created_at', ''), reverse=True)
3282
  return jsonify(staff_orders[:50])
3283
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3284
  @app.route('/<env_id>/process_return/<order_id>', methods=['POST'])
3285
  def process_return(env_id, order_id):
3286
  data = get_env_data(env_id)
@@ -3355,7 +3326,6 @@ def create_order(env_id):
3355
  "price": float(item['cart_price']),
3356
  "calculated_price": float(item.get('calculated_price', item['cart_price'])),
3357
  "quantity": int(item['quantity']),
3358
- "assembled": 0,
3359
  "pieces_per_box": int(item.get('pieces_per_box', 1)) if str(item.get('pieces_per_box', 1)).strip() != "" else 1,
3360
  "variant_name": item.get('variant_name', ''),
3361
  "variant_idx": item.get('variant_idx', -1),
@@ -3378,7 +3348,8 @@ def create_order(env_id):
3378
  "customer_city": customer_city,
3379
  "customer_address": customer_address,
3380
  "customer_zip": customer_zip,
3381
- "customer_whatsapp": customer_whatsapp
 
3382
  }
3383
 
3384
  if order_status == 'pos' and data['settings'].get('track_inventory', False) and data['settings'].get('system_mode', 'both') != 'external':
@@ -3420,23 +3391,6 @@ def view_assembly(env_id, order_id):
3420
  env_id=env_id
3421
  )
3422
 
3423
- @app.route('/<env_id>/api/assembly/<order_id>', methods=['POST'])
3424
- def save_assembly(env_id, order_id):
3425
- data = get_env_data(env_id)
3426
- order = data.get('orders', {}).get(order_id)
3427
- if not order:
3428
- return jsonify({"success": False, "error": "Order not found"}), 404
3429
-
3430
- req_data = request.get_json()
3431
-
3432
- for item in order['cart']:
3433
- ckey = item.get('c_key')
3434
- if ckey in req_data:
3435
- item['assembled'] = int(req_data[ckey])
3436
-
3437
- save_env_data(env_id, data)
3438
- return jsonify({"success": True})
3439
-
3440
  @app.route('/<env_id>/edit_order/<order_id>', methods=['POST'])
3441
  def edit_order(env_id, order_id):
3442
  data = get_env_data(env_id)
@@ -3599,10 +3553,10 @@ def admin(env_id):
3599
 
3600
  if settings.get('system_mode', 'both') == 'external':
3601
  settings['track_inventory'] = False
3602
- settings['enable_barcodes'] = False
3603
  else:
3604
  settings['track_inventory'] = 'track_inventory' in request.form
3605
- settings['enable_barcodes'] = 'enable_barcodes' in request.form
3606
 
3607
  settings['customer_fields'] = {
3608
  'name': 'cf_name' in request.form,
@@ -3664,6 +3618,7 @@ def admin(env_id):
3664
 
3665
  elif action == 'add_product':
3666
  name = request.form.get('name', '').strip()
 
3667
  price_str = request.form.get('price', '')
3668
  price = float(price_str) if price_str else ""
3669
 
@@ -3679,17 +3634,15 @@ def admin(env_id):
3679
  stock_str = request.form.get('stock', '')
3680
  main_stock = int(stock_str) if stock_str else ""
3681
 
3682
- barcode = request.form.get('barcode', '').strip()
3683
-
3684
  description = request.form.get('description', '').strip()
3685
  category = request.form.get('category')
3686
  has_variant_prices = 'has_variant_prices' in request.form
3687
 
3688
  variant_names = request.form.getlist('variant_name[]')
 
3689
  variant_prices = request.form.getlist('variant_price[]')
3690
  variant_box_prices = request.form.getlist('variant_box_price[]')
3691
  variant_stocks = request.form.getlist('variant_stock[]')
3692
- variant_barcodes = request.form.getlist('variant_barcode[]')
3693
  variants = []
3694
 
3695
  for i in range(len(variant_names)):
@@ -3713,10 +3666,10 @@ def admin(env_id):
3713
 
3714
  variants.append({
3715
  "name": v_name,
 
3716
  "price": v_price,
3717
  "box_price": v_box_price,
3718
- "stock": v_stock,
3719
- "barcode": v_barcode
3720
  })
3721
 
3722
  uploaded_photos = request.files.getlist('photos')[:10]
@@ -3752,12 +3705,12 @@ def admin(env_id):
3752
  new_product = {
3753
  'product_id': uuid4().hex,
3754
  'name': name,
 
3755
  'price': price,
3756
  'pieces_per_box': pieces_per_box,
3757
  'box_price': box_price,
3758
  'min_order': min_order,
3759
  'stock': main_stock,
3760
- 'barcode': barcode,
3761
  'description': description,
3762
  'category': category,
3763
  'photos': photos_list,
@@ -3771,6 +3724,7 @@ def admin(env_id):
3771
  elif action == 'edit_product':
3772
  pid = request.form.get('product_id')
3773
  name = request.form.get('name', '').strip()
 
3774
 
3775
  price_str = request.form.get('price', '')
3776
  price = float(price_str) if price_str else ""
@@ -3787,16 +3741,14 @@ def admin(env_id):
3787
  stock_str = request.form.get('stock', '')
3788
  main_stock = int(stock_str) if stock_str else ""
3789
 
3790
- barcode = request.form.get('barcode', '').strip()
3791
-
3792
  description = request.form.get('description', '').strip()
3793
  has_variant_prices = 'has_variant_prices' in request.form
3794
 
3795
  variant_names = request.form.getlist('variant_name[]')
 
3796
  variant_prices = request.form.getlist('variant_price[]')
3797
  variant_box_prices = request.form.getlist('variant_box_price[]')
3798
  variant_stocks = request.form.getlist('variant_stock[]')
3799
- variant_barcodes = request.form.getlist('variant_barcode[]')
3800
  variants = []
3801
 
3802
  for i in range(len(variant_names)):
@@ -3817,10 +3769,10 @@ def admin(env_id):
3817
  v_barcode = variant_barcodes[i].strip()
3818
  variants.append({
3819
  "name": v_name,
 
3820
  "price": v_price,
3821
  "box_price": v_box_price,
3822
- "stock": v_stock,
3823
- "barcode": v_barcode
3824
  })
3825
 
3826
  uploaded_photos = request.files.getlist('photos')[:10]
@@ -3856,12 +3808,12 @@ def admin(env_id):
3856
  for p in products:
3857
  if p.get('product_id') == pid:
3858
  p['name'] = name
 
3859
  p['price'] = price
3860
  p['pieces_per_box'] = pieces_per_box
3861
  p['box_price'] = box_price
3862
  p['min_order'] = min_order
3863
  p['stock'] = main_stock
3864
- p['barcode'] = barcode
3865
  p['description'] = description
3866
  p['variants'] = variants
3867
  p['has_variant_prices'] = has_variant_prices
 
140
  'logo_url': DEFAULT_LOGO_URL,
141
  'whatsapp_number': DEFAULT_WHATSAPP_NUMBER,
142
  'track_inventory': False,
143
+ 'use_barcodes': False,
144
  'business_type': 'mixed',
145
  'system_mode': 'both',
146
  'customer_fields': {
 
173
  if 'logo_url' not in settings: settings['logo_url'] = DEFAULT_LOGO_URL; changed = True
174
  if 'whatsapp_number' not in settings: settings['whatsapp_number'] = DEFAULT_WHATSAPP_NUMBER; changed = True
175
  if 'track_inventory' not in settings: settings['track_inventory'] = False; changed = True
176
+ if 'use_barcodes' not in settings: settings['use_barcodes'] = False; changed = True
177
  if 'business_type' not in settings: settings['business_type'] = 'mixed'; changed = True
178
  if 'system_mode' not in settings: settings['system_mode'] = 'both'; changed = True
179
  if 'customer_fields' not in settings:
 
204
  for order_id, order in env_data['orders'].items():
205
  if 'status' not in order: order['status'] = 'confirmed'; changed = True
206
  if 'staff_name' not in order: order['staff_name'] = ''; changed = True
207
+ if 'assembled' not in order: order['assembled'] = {}; changed = True
 
208
 
209
  if changed or not os.path.exists(DATA_FILE):
210
  try:
 
241
  'logo_url': DEFAULT_LOGO_URL,
242
  'whatsapp_number': DEFAULT_WHATSAPP_NUMBER,
243
  'track_inventory': False,
244
+ 'use_barcodes': False,
245
  'business_type': 'mixed',
246
  'system_mode': 'both',
247
  'customer_fields': {'name': True, 'phone': True, 'city': True, 'address': False, 'zip': False},
 
440
  .search-bar { padding: 15px 20px; background: var(--surface); border-bottom: 1px solid var(--border); }
441
  .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; }
442
  .search-container:focus-within { border-color: #dcdde1; background: var(--surface); box-shadow: 0 4px 12px rgba(0,0,0,0.05); }
443
+ .search-container i.fa-search { color: var(--text-muted); font-size: 0.9rem; }
444
  .search-bar input { width: 100%; padding: 12px 10px; border: none; background: transparent; outline: none; font-size: 0.95rem; }
445
 
446
  .categories-container { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 15px; padding: 20px; }
 
531
  .btn-float-ig { background: radial-gradient(circle at 30% 107%, #fdf497 0%, #fdf497 5%, #fd5949 45%, #d6249f 60%, #285AEB 90%); }
532
  .btn-float-tg { background: #0088cc; }
533
  .btn-float-returns { background: #e17055; }
534
+ .btn-float-history { background: #0984e3; }
535
 
536
  .staff-banner { background: #ffeaa7; color: #d63031; text-align: center; padding: 10px; font-weight: 600; font-size: 0.9rem; }
537
  .returns-list { display: flex; flex-direction: column; gap: 15px; }
 
539
  .return-item-row { display: flex; justify-content: space-between; align-items: center; margin-top: 10px; font-size: 0.9rem; border-top: 1px dashed #ccc; padding-top: 10px; }
540
  .return-input { width: 50px; padding: 5px; border: 1px solid #ccc; border-radius: 6px; text-align: center; }
541
  .process-return-btn { background: #e17055; color: white; border: none; padding: 8px 15px; border-radius: 8px; cursor: pointer; margin-top: 10px; width: 100%; font-weight: 600; }
542
+
543
+ .history-btn { background: #0984e3; color: white; border: none; padding: 8px 15px; border-radius: 8px; cursor: pointer; margin-top: 10px; width: 100%; font-weight: 600; text-decoration: none; display: block; text-align: center; box-sizing: border-box; }
 
544
 
545
  @media (min-width: 768px) {
546
  .categories-container { grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); }
 
571
  <div class="search-container">
572
  <i class="fas fa-search"></i>
573
  <input type="text" id="searchInput" placeholder="Поиск товаров..." oninput="filterCategories()">
574
+ {% if settings.use_barcodes %}
575
+ <i class="fas fa-barcode" style="cursor:pointer; padding: 10px; color: var(--primary);" onclick="startScanner(val => { document.getElementById('searchInput').value = val; filterCategories(); })"></i>
576
  {% endif %}
577
  </div>
578
  </div>
 
582
 
583
  <div class="floating-socials">
584
  {% if mode == 'pos' and staff_id %}
585
+ <a href="#" class="social-btn btn-float-history" onclick="openStaffHistoryModal()"><i class="fas fa-list-alt"></i></a>
586
  <a href="#" class="social-btn btn-float-returns" onclick="openReturnsModal()"><i class="fas fa-undo"></i></a>
587
  {% endif %}
588
  {% if mode != 'pos' %}
 
642
  </div>
643
  </div>
644
  </div>
645
+
646
+ <div class="modal-overlay" id="staffHistoryModal" onclick="if(event.target === this) closeStaffHistoryModal()">
647
  <div class="modal-content">
648
  <div class="modal-header">
649
+ <h2>Мои накладные</h2>
650
+ <button class="modal-close" onclick="closeStaffHistoryModal()"><i class="fas fa-times"></i></button>
651
  </div>
652
+ <div style="margin-bottom: 15px; display:flex; gap:10px;">
653
+ <input type="date" id="historyDateFilter" style="padding: 10px; border: 1px solid var(--border); border-radius: 8px; flex: 1;" onchange="loadStaffHistory()">
654
  </div>
655
+ <div id="staffHistoryContent" class="returns-list">
656
  <div style="text-align:center; padding:20px;"><i class="fas fa-spinner fa-spin"></i> Загрузка...</div>
657
  </div>
658
  </div>
659
  </div>
660
 
 
 
 
 
 
 
 
 
 
 
661
  <div class="gallery-modal" id="galleryModal">
662
  <button class="gallery-close" onclick="closeGallery()"><i class="fas fa-times"></i></button>
663
  <div class="gallery-img-container" id="gallerySwipeArea">
 
667
  </div>
668
  <div class="gallery-dots" id="galleryDots"></div>
669
  </div>
670
+
671
+ <div class="modal-overlay" id="scannerModal" style="z-index:9999;">
672
+ <div style="background:#fff; padding:20px; border-radius:12px; width:100%; max-width:400px; text-align:center; position:absolute; top:50%; left:50%; transform:translate(-50%, -50%);">
673
+ <h3 style="margin-top:0;">Сканирование</h3>
674
+ <div id="reader" style="width:100%; min-height:300px; margin-bottom:15px;"></div>
675
+ <button class="btn btn-danger" style="background:#ff7675; color:white; border:none; padding:10px 20px; border-radius:8px; font-weight:bold; cursor:pointer;" onclick="stopScanner()">Отмена</button>
676
+ </div>
677
+ </div>
678
 
679
  <script>
680
  const products = {{ products_json|safe }};
 
684
  const envId = '{{ env_id }}';
685
  const mode = '{{ mode }}';
686
  const staffId = '{{ staff_id }}';
687
+ const trackInventory = {{ 'true' if settings.track_inventory else 'false' }};
688
  const businessType = '{{ settings.business_type }}';
689
  const cFields = {{ settings.customer_fields|tojson }};
690
 
 
692
  let currentGalleryPhotos = [];
693
  let currentGalleryIndex = 0;
694
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
695
  function init() {
696
  renderCategories();
697
  updateCartUI();
698
 
699
+ let today = new Date();
700
+ let offset = today.getTimezoneOffset() * 60000;
701
+ let localToday = new Date(today.getTime() - offset).toISOString().split('T')[0];
702
+ document.getElementById('historyDateFilter').value = localToday;
703
  }
704
 
705
  function getCartKey(productId, variantIdx) {
 
754
  container.innerHTML = '';
755
 
756
  const matchedProducts = products.filter(p => {
757
+ if(p.name.toLowerCase().includes(query) || p.category.toLowerCase().includes(query)) return true;
758
+ if(p.barcode && p.barcode.toLowerCase().includes(query)) return true;
759
+ if(p.variants && p.variants.some(v => v.barcode && v.barcode.toLowerCase().includes(query))) return true;
760
+ return false;
 
761
  });
762
 
763
  if(matchedProducts.length === 0) {
 
1235
  }
1236
  });
1237
  }
1238
+
1239
+ function openStaffHistoryModal() {
1240
+ const modal = document.getElementById('staffHistoryModal');
1241
  modal.style.display = 'flex';
1242
  setTimeout(() => modal.classList.add('active'), 10);
1243
+ loadStaffHistory();
1244
  }
1245
+
1246
+ function closeStaffHistoryModal() {
1247
+ const modal = document.getElementById('staffHistoryModal');
1248
  modal.classList.remove('active');
1249
  setTimeout(() => modal.style.display = 'none', 300);
1250
  }
1251
+
1252
+ function loadStaffHistory() {
1253
+ const dateStr = document.getElementById('historyDateFilter').value;
1254
+ fetch(`/${envId}/api/staff_orders/${staffId}?date=${dateStr}`)
1255
  .then(r => r.json())
1256
  .then(data => {
1257
+ const content = document.getElementById('staffHistoryContent');
1258
  content.innerHTML = '';
1259
  if(data.length === 0) {
1260
+ content.innerHTML = '<div style="text-align:center; padding:20px;">За этот день нет накладных</div>';
1261
  return;
1262
  }
 
1263
  data.forEach(order => {
1264
  content.innerHTML += `
1265
  <div class="return-order-item" style="margin-bottom:10px;">
1266
+ <div><b>№ ${order.id}</b> <span style="float:right; color:#636e72; font-size:0.85rem;">${order.created_at.split(' ')[1]}</span></div>
1267
+ <div style="font-size:0.9rem; margin-top:5px; margin-bottom:10px;">Сумма: ${order.total_price} ${currency}</div>
1268
+ <a href="/${envId}/assembly/${order.id}" class="history-btn">Сборка накладной</a>
 
 
1269
  </div>
1270
  `;
1271
  });
 
1329
  if (touchendX - touchstartX > 50) prevPhoto();
1330
  });
1331
 
1332
+ function startScanner(callback) {
1333
+ document.getElementById('scannerModal').style.display = 'block';
1334
+ const html5QrCode = new Html5Qrcode("reader");
1335
+ const config = { fps: 10, qrbox: { width: 250, height: 250 } };
1336
+ html5QrCode.start({ facingMode: "environment" }, config, (text) => {
1337
+ html5QrCode.stop().then(() => {
1338
+ document.getElementById('scannerModal').style.display = 'none';
1339
+ callback(text);
1340
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1341
  }).catch(err => {
1342
+ console.log(err);
1343
+ alert('Не удалось запустить камеру');
1344
+ document.getElementById('scannerModal').style.display = 'none';
1345
  });
1346
+ window.currentScanner = html5QrCode;
1347
  }
1348
 
1349
+ function stopScanner() {
1350
+ if(window.currentScanner) {
1351
+ window.currentScanner.stop().catch(()=>{});
1352
+ }
1353
+ document.getElementById('scannerModal').style.display = 'none';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1354
  }
1355
+
1356
+ init();
1357
  </script>
1358
  </body>
1359
  </html>
 
1368
  <title>Накладная №{{ order.id }}</title>
1369
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
1370
  <style>
1371
+ :root { --bg: #f4f6f8; --surface: #ffffff; --text: #2d3436; --border: #dfe6e9; --wa: #25D366; --print: #1e272e; --primary: #1a1a1a; }
1372
  * { box-sizing: border-box; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }
1373
  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); }
1374
  .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; }
 
1490
  {% set ppb = item.pieces_per_box|default(1)|int %}
1491
  {% set boxes = item.quantity // ppb %}
1492
  {% set remainder = item.quantity % ppb %}
1493
+ {% set assembled = order.assembled.get(item.c_key, 0) if order.assembled else 0 %}
1494
  {% if item.quantity > 0 %}
1495
  <tr>
1496
  <td>{{ loop.index }}</td>
 
1499
  {% if item.variant_name %}
1500
  <div style="font-size: 0.85rem; color: #636e72;">Вариант: {{ item.variant_name }}</div>
1501
  {% endif %}
1502
+ <div style="font-size: 0.8rem; color: {% if assembled == item.quantity %}#00b894{% else %}#0984e3{% endif %}; margin-top: 4px; font-weight:600;">Собрано: {{ assembled }} / {{ item.quantity }}</div>
 
 
1503
  </td>
1504
  <td class="img-cell"><img src="{{ item.photo_url }}" alt="img"></td>
1505
  <td style="text-align: center;">
 
1620
  </html>
1621
  '''
1622
 
1623
+ ASSEMBLY_TEMPLATE = '''
1624
+ <!DOCTYPE html>
1625
+ <html lang="ru">
1626
+ <head>
1627
+ <meta charset="UTF-8">
1628
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
1629
+ <title>Сборка №{{ order.id }}</title>
1630
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
1631
+ <style>
1632
+ :root { --bg: #f4f6f8; --surface: #ffffff; --text: #2d3436; --border: #dfe6e9; --primary: #0984e3; --success: #00b894; }
1633
+ * { box-sizing: border-box; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }
1634
+ body { margin: 0; padding: max(20px, env(safe-area-inset-top)) 20px calc(100px + env(safe-area-inset-bottom)); background: var(--bg); color: var(--text); }
1635
+ .container { max-width: 800px; margin: 0 auto; }
1636
+ .header { background: var(--surface); padding: 20px; border-radius: 16px; box-shadow: 0 4px 15px rgba(0,0,0,0.05); margin-bottom: 20px; text-align: center; }
1637
+ .header h1 { margin: 0 0 10px 0; font-size: 1.5rem; }
1638
+ .copy-btn { background: var(--primary); color: #fff; border: none; padding: 10px 20px; border-radius: 8px; font-weight: 600; cursor: pointer; display: inline-flex; align-items: center; gap: 8px; }
1639
+
1640
+ .item-card { background: var(--surface); padding: 15px; border-radius: 12px; margin-bottom: 15px; display: flex; align-items: center; gap: 15px; box-shadow: 0 4px 15px rgba(0,0,0,0.03); flex-wrap: wrap; }
1641
+ .item-img { width: 60px; height: 60px; border-radius: 8px; object-fit: cover; border: 1px solid #eee; flex-shrink: 0; }
1642
+ .item-info { flex: 1; min-width: 200px; }
1643
+ .item-name { font-weight: 600; font-size: 1rem; }
1644
+ .item-variant { font-size: 0.85rem; color: #636e72; margin-top: 2px; }
1645
+ .item-target { font-size: 0.9rem; font-weight: bold; margin-top: 5px; }
1646
+
1647
+ .assembly-controls { display: flex; align-items: center; background: var(--bg); border-radius: 8px; border: 1px solid var(--border); overflow: hidden; }
1648
+ .assembly-controls button { border: none; background: #f8f9fa; width: 40px; height: 40px; font-size: 1.2rem; cursor: pointer; color: var(--primary); transition: background 0.2s; }
1649
+ .assembly-controls button:active { background: #e0e0e0; }
1650
+ .assembly-controls input { width: 50px; text-align: center; font-weight: 700; font-size: 1rem; border: none; background: transparent; color: var(--primary); outline: none; }
1651
+ .assembly-controls input[type="number"]::-webkit-inner-spin-button,
1652
+ .assembly-controls input[type="number"]::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }
1653
+
1654
+ .status-badge { font-size: 0.8rem; padding: 4px 8px; border-radius: 12px; font-weight: 600; }
1655
+ .status-done { background: #d4edda; color: #155724; }
1656
+ .status-pending { background: #fff3cd; color: #856404; }
1657
+ </style>
1658
+ </head>
1659
+ <body>
1660
+ <div class="container">
1661
+ <div class="header">
1662
+ <h1>Сборка накладной № {{ order.id }}</h1>
1663
+ <div style="color: #636e72; margin-bottom: 15px;">{{ order.created_at }}</div>
1664
+ <button class="copy-btn" onclick="copyLink()"><i class="fas fa-link"></i> Скопировать ссылку</button>
1665
+ </div>
1666
+
1667
+ {% for item in order.cart %}
1668
+ {% if item.quantity > 0 %}
1669
+ {% set assembled = order.assembled.get(item.c_key, 0) if order.assembled else 0 %}
1670
+ <div class="item-card" id="card_{{ item.c_key }}">
1671
+ <img src="{{ item.photo_url }}" class="item-img">
1672
+ <div class="item-info">
1673
+ <div class="item-name">{{ item.name }}</div>
1674
+ {% if item.variant_name %}
1675
+ <div class="item-variant">Вариант: {{ item.variant_name }}</div>
1676
+ {% endif %}
1677
+ <div class="item-target">Нужно: {{ item.quantity }} шт.</div>
1678
+ <div style="margin-top: 5px;">
1679
+ <span id="badge_{{ item.c_key }}" class="status-badge {% if assembled >= item.quantity %}status-done{% else %}status-pending{% endif %}">
1680
+ {% if assembled >= item.quantity %}Собрано{% else %}В процессе{% endif %}
1681
+ </span>
1682
+ </div>
1683
+ </div>
1684
+ <div class="assembly-controls">
1685
+ <button onclick="changeQty('{{ item.c_key }}', -1, {{ item.quantity }})"><i class="fas fa-minus"></i></button>
1686
+ <input type="number" id="qty_{{ item.c_key }}" value="{{ assembled }}" onchange="setQty('{{ item.c_key }}', this.value, {{ item.quantity }})">
1687
+ <button onclick="changeQty('{{ item.c_key }}', 1, {{ item.quantity }})"><i class="fas fa-plus"></i></button>
1688
+ </div>
1689
+ </div>
1690
+ {% endif %}
1691
+ {% endfor %}
1692
+ </div>
1693
+
1694
+ <script>
1695
+ const envId = '{{ env_id }}';
1696
+ const orderId = '{{ order.id }}';
1697
+
1698
+ function copyLink() {
1699
+ let dummy = document.createElement('input');
1700
+ document.body.appendChild(dummy);
1701
+ dummy.value = window.location.href;
1702
+ dummy.select();
1703
+ document.execCommand('copy');
1704
+ document.body.removeChild(dummy);
1705
+ alert('Ссылка скопирована!');
1706
+ }
1707
+
1708
+ function updateBackend(cKey, qty, maxQty) {
1709
+ fetch(`/${envId}/api/assembly/${orderId}`, {
1710
+ method: 'POST',
1711
+ headers: { 'Content-Type': 'application/json' },
1712
+ body: JSON.stringify({ c_key: cKey, qty: qty })
1713
+ })
1714
+ .then(r => r.json())
1715
+ .then(data => {
1716
+ if(data.success) {
1717
+ const badge = document.getElementById(`badge_${cKey}`);
1718
+ if(qty >= maxQty) {
1719
+ badge.className = 'status-badge status-done';
1720
+ badge.innerText = 'Собрано';
1721
+ } else {
1722
+ badge.className = 'status-badge status-pending';
1723
+ badge.innerText = 'В процессе';
1724
+ }
1725
+ }
1726
+ });
1727
+ }
1728
+
1729
+ function changeQty(cKey, diff, maxQty) {
1730
+ let input = document.getElementById(`qty_${cKey}`);
1731
+ let val = parseInt(input.value) || 0;
1732
+ val += diff;
1733
+ if(val < 0) val = 0;
1734
+ if(val > maxQty) val = maxQty;
1735
+ input.value = val;
1736
+ updateBackend(cKey, val, maxQty);
1737
+ }
1738
+
1739
+ function setQty(cKey, val, maxQty) {
1740
+ let num = parseInt(val) || 0;
1741
+ if(num < 0) num = 0;
1742
+ if(num > maxQty) num = maxQty;
1743
+ document.getElementById(`qty_${cKey}`).value = num;
1744
+ updateBackend(cKey, num, maxQty);
1745
+ }
1746
+ </script>
1747
+ </body>
1748
+ </html>
1749
+ '''
1750
+
1751
  ADMIN_TEMPLATE = '''
1752
  <!DOCTYPE html>
1753
  <html lang="ru">
 
1790
  .add-cat-form button { white-space: nowrap; }
1791
 
1792
  .search-bar-admin { position: relative; margin-bottom: 20px; }
1793
+ .search-bar-admin i.fa-search { position: absolute; left: 15px; top: 50%; transform: translateY(-50%); color: #636e72; }
1794
  .search-bar-admin input { padding-left: 40px; background: var(--surface); border: none; box-shadow: 0 4px 15px rgba(0,0,0,0.03); }
1795
 
1796
  .category-block { border: 1px solid var(--border); margin-bottom: 15px; border-radius: 12px; overflow: hidden; background: #fff; }
 
1844
 
1845
  .badge { background: var(--danger); color: white; padding: 2px 8px; border-radius: 12px; font-size: 0.8rem; margin-left: 5px; }
1846
 
 
 
 
 
 
 
 
 
 
 
 
1847
  @media (max-width: 600px) {
1848
  .header-panel { flex-direction: column; align-items: stretch; text-align: center; }
1849
  .product-item { flex-direction: column; align-items: stretch; }
 
2016
  {% if sys_mode != 'external' %}
2017
  <div style="font-weight: 600; margin-bottom: 5px; border-top: 1px solid var(--border); padding-top: 15px;">Учет товара:</div>
2018
  <label><input type="checkbox" name="track_inventory" {% if settings.track_inventory %}checked{% endif %}> Включить остатки на складе</label>
2019
+ <label><input type="checkbox" name="use_barcodes" {% if settings.use_barcodes %}checked{% endif %}> Использовать штрих-коды</label>
2020
  {% endif %}
2021
 
2022
  <div style="font-weight: 600; margin-bottom: 5px; border-top: 1px solid var(--border); padding-top: 15px; margin-top: 10px;">Социальные сети:</div>
 
2050
 
2051
  <div class="search-bar-admin">
2052
  <i class="fas fa-search"></i>
2053
+ <input type="text" id="adminSearch" placeholder="Поиск по категориям и товарам..." oninput="filterAdmin()">
 
 
 
2054
  </div>
2055
 
2056
  {% for category in categories %}
 
2083
  <label>Название товара</label>
2084
  <input type="text" name="name" placeholder="Введите название" required autocomplete="off">
2085
  </div>
2086
+
2087
+ {% if settings.use_barcodes and sys_mode != 'external' %}
2088
+ <div class="form-group main-barcode-container">
2089
+ <label>Штрих-код</label>
2090
+ <div style="display:flex; gap:5px;">
2091
+ <input type="text" name="barcode" placeholder="Штрих-код" class="main-barcode-input">
2092
+ <button type="button" class="btn btn-outline" style="padding: 12px;" onclick="startScanner(val => this.previousElementSibling.value = val)"><i class="fas fa-barcode"></i></button>
2093
+ </div>
2094
+ </div>
2095
+ {% endif %}
2096
+
2097
  <div class="form-group main-price-container">
2098
  <label>Цена за ед.</label>
2099
  <input type="number" name="price" placeholder="Цена" step="0.01" class="main-price-input">
 
2126
  <input type="number" name="stock" placeholder="Остаток" class="main-stock-input">
2127
  </div>
2128
  {% endif %}
 
 
 
 
 
 
 
 
 
 
2129
  </div>
2130
 
2131
  <div class="variants-container" id="variants-container-add-{{ loop.index }}">
 
2152
 
2153
  {% for product in products %}
2154
  {% if product.category == category %}
2155
+ <div class="product-item">
2156
  <div class="product-info">
2157
  {% if product.photos and product.photos|length > 0 %}
2158
  <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ product.photos[0] }}" class="product-img">
 
2187
  • Остаток по вариантам
2188
  {% endif %}
2189
  {% endif %}
 
 
 
 
2190
  </span>
2191
  </div>
2192
  </div>
 
2211
  <label>Название товара</label>
2212
  <input type="text" name="name" value="{{ product.name }}" required autocomplete="off">
2213
  </div>
2214
+
2215
+ {% if settings.use_barcodes and sys_mode != 'external' %}
2216
+ <div class="form-group main-barcode-container" {% if product.variants %}style="display:none;"{% endif %}>
2217
+ <label>Штрих-код</label>
2218
+ <div style="display:flex; gap:5px;">
2219
+ <input type="text" name="barcode" value="{{ product.barcode }}" class="main-barcode-input">
2220
+ <button type="button" class="btn btn-outline" style="padding: 12px;" onclick="startScanner(val => this.previousElementSibling.value = val)"><i class="fas fa-barcode"></i></button>
2221
+ </div>
2222
+ </div>
2223
+ {% endif %}
2224
+
2225
  <div class="form-group main-price-container" {% if product.has_variant_prices %}style="display:none;"{% endif %}>
2226
  <label>Цена за ед.</label>
2227
  <input type="number" name="price" value="{{ product.price }}" step="0.01" class="main-price-input" {% if not product.has_variant_prices %}required{% endif %}>
 
2254
  <input type="number" name="stock" value="{{ product.stock }}" class="main-stock-input">
2255
  </div>
2256
  {% endif %}
 
 
 
 
 
 
 
 
 
 
2257
  </div>
2258
 
2259
  <div class="variants-container" id="variants-container-edit-{{ product.product_id }}">
 
2263
  </div>
2264
  <div id="variants-list-edit-{{ product.product_id }}">
2265
  {% for variant in product.variants %}
 
2266
  <div class="variant-row">
2267
  <div class="form-group">
2268
  <label>Название</label>
2269
  <input type="text" name="variant_name[]" value="{{ variant.name }}" placeholder="Цвет, размер" required>
2270
  </div>
2271
+ {% if settings.use_barcodes and sys_mode != 'external' %}
2272
+ <div class="form-group">
2273
+ <label>Штрих-код</label>
2274
+ <div style="display:flex; gap:5px;">
2275
+ <input type="text" name="variant_barcode[]" value="{{ variant.barcode }}" placeholder="Код">
2276
+ <button type="button" class="btn btn-outline" style="padding: 12px;" onclick="startScanner(val => this.previousElementSibling.value = val)"><i class="fas fa-barcode"></i></button>
2277
+ </div>
2278
+ </div>
2279
+ {% endif %}
2280
  <div class="form-group var-price-input" {% if not product.has_variant_prices %}style="display:none;"{% endif %}>
2281
  <label>Цена</label>
2282
  <input type="number" name="variant_price[]" value="{{ variant.price }}" placeholder="Цена за ед." step="0.01" {% if product.has_variant_prices %}required{% endif %}>
 
2293
  <input type="number" name="variant_stock[]" value="{{ variant.stock }}" placeholder="Остаток">
2294
  </div>
2295
  {% endif %}
 
 
 
 
 
 
 
 
 
 
 
2296
  <button type="button" class="remove-variant-btn" onclick="this.parentElement.remove(); updateMainStockVisibility('edit-prod-{{ product.product_id }}')"><i class="fas fa-times-circle"></i></button>
2297
  </div>
2298
  {% endfor %}
 
2319
  </div>
2320
  {% endfor %}
2321
  </div>
2322
+
2323
+ <div class="modal-overlay" id="scannerModal" style="z-index:9999; display:none; position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.6); align-items:center; justify-content:center;">
2324
+ <div style="background:#fff; padding:20px; border-radius:12px; width:100%; max-width:400px; text-align:center;">
2325
+ <h3 style="margin-top:0;">Сканирование</h3>
2326
+ <div id="reader" style="width:100%; min-height:300px; margin-bottom:15px;"></div>
2327
+ <button class="btn btn-danger" onclick="stopScanner()">Отмена</button>
 
 
2328
  </div>
2329
  </div>
2330
 
2331
  <script>
2332
  const trackInventory = {{ 'true' if settings.track_inventory and sys_mode != 'external' else 'false' }};
2333
+ const useBarcodes = {{ 'true' if settings.use_barcodes and sys_mode != 'external' else 'false' }};
2334
  const businessType = '{{ settings.business_type }}';
2335
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2336
  function showLoading(form) {
2337
  const btn = form.querySelector('button[type="submit"]');
2338
  btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Загрузка...';
 
2387
  <label>Название</label>
2388
  <input type="text" name="variant_name[]" placeholder="Цвет, размер" required>
2389
  </div>
2390
+ `;
2391
+
2392
+ if (useBarcodes) {
2393
+ html += `
2394
+ <div class="form-group">
2395
+ <label>Штрих-код</label>
2396
+ <div style="display:flex; gap:5px;">
2397
+ <input type="text" name="variant_barcode[]" placeholder="Код">
2398
+ <button type="button" class="btn btn-outline" style="padding: 12px;" onclick="startScanner(val => this.previousElementSibling.value = val)"><i class="fas fa-barcode"></i></button>
2399
+ </div>
2400
+ </div>`;
2401
+ }
2402
+
2403
+ html += `
2404
  <div class="form-group var-price-input" ${displayStyle}>
2405
  <label>Цена</label>
2406
  <input type="number" name="variant_price[]" placeholder="Цена за ед." step="0.01" ${reqAttr}>
 
2422
  <input type="number" name="variant_stock[]" placeholder="Остаток">
2423
  </div>`;
2424
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2425
 
2426
  html += `<button type="button" class="remove-variant-btn" onclick="this.parentElement.remove(); updateMainStockVisibility('${formId}')"><i class="fas fa-times-circle"></i></button>`;
2427
 
 
2461
  }
2462
 
2463
  function updateMainStockVisibility(formId) {
 
2464
  const form = document.getElementById(formId);
2465
  const variants = form.querySelectorAll('.variant-row');
2466
+
2467
+ if(trackInventory) {
2468
+ const mainStock = form.querySelector('.main-stock-container');
2469
+ if(mainStock) {
2470
+ if(variants.length > 0) mainStock.style.display = 'none';
2471
+ else mainStock.style.display = 'flex';
2472
+ }
2473
+ }
2474
+
2475
+ if(useBarcodes) {
2476
+ const mainBc = form.querySelector('.main-barcode-container');
2477
+ if(mainBc) {
2478
+ if(variants.length > 0) mainBc.style.display = 'none';
2479
+ else mainBc.style.display = 'flex';
2480
  }
2481
  }
2482
  }
 
2498
 
2499
  products.forEach(prod => {
2500
  const prodName = prod.querySelector('.product-name').innerText.toLowerCase();
2501
+ if (prodName.includes(query) || catMatch) {
 
 
 
2502
  prod.style.display = 'flex';
2503
  hasVisibleProduct = true;
2504
  } else {
 
2552
  }
2553
  document.body.removeChild(textArea);
2554
  }
2555
+
2556
+ function startScanner(callback) {
2557
+ document.getElementById('scannerModal').style.display = 'flex';
2558
+ const html5QrCode = new Html5Qrcode("reader");
2559
+ const config = { fps: 10, qrbox: { width: 250, height: 250 } };
2560
+ html5QrCode.start({ facingMode: "environment" }, config, (text) => {
2561
+ html5QrCode.stop().then(() => {
2562
+ document.getElementById('scannerModal').style.display = 'none';
2563
+ callback(text);
2564
+ });
2565
+ }).catch(err => {
2566
+ console.log(err);
2567
+ alert('Не удалось запустить камеру');
2568
+ document.getElementById('scannerModal').style.display = 'none';
2569
+ });
2570
+ window.currentScanner = html5QrCode;
2571
+ }
2572
+
2573
+ function stopScanner() {
2574
+ if(window.currentScanner) {
2575
+ window.currentScanner.stop().catch(()=>{});
2576
+ }
2577
+ document.getElementById('scannerModal').style.display = 'none';
2578
+ }
2579
 
2580
  document.querySelectorAll('.add-product-wrapper').forEach(wrapper => {
2581
  const cb = wrapper.querySelector('input[name="has_variant_prices"]');
 
2890
  <div id="current" class="tab-content active">
2891
  <div class="search-bar">
2892
  <i class="fas fa-search"></i>
2893
+ <input type="text" id="searchInput" placeholder="Поиск товара..." oninput="filterTable()">
2894
  </div>
2895
 
2896
  <div class="table-container">
 
2899
  <tr>
2900
  <th>Товар</th>
2901
  <th>Категория</th>
 
2902
  <th>Остаток</th>
2903
  <th>Действие (Приход / Списание)</th>
2904
  </tr>
 
2906
  <tbody id="inventoryTable">
2907
  {% for p in products %}
2908
  {% if not p.variants %}
2909
+ <tr class="inv-row">
2910
  <td class="prod-name"><strong>{{ p.name }}</strong></td>
2911
  <td>{{ p.category }}</td>
 
2912
  <td><strong>{{ p.stock if p.stock != "" else "0" }}</strong></td>
2913
  <td>
2914
  <div class="action-cell">
 
2921
  </tr>
2922
  {% else %}
2923
  {% for v in p.variants %}
2924
+ <tr class="inv-row">
2925
  <td class="prod-name"><strong>{{ p.name }}</strong> <span style="color:#636e72;">({{ v.name }})</span></td>
2926
  <td>{{ p.category }}</td>
 
2927
  <td><strong>{{ v.stock if v.stock != "" else "0" }}</strong></td>
2928
  <td>
2929
  <div class="action-cell">
 
2993
  const rows = document.querySelectorAll('.inv-row');
2994
  rows.forEach(row => {
2995
  const text = row.querySelector('.prod-name').innerText.toLowerCase();
2996
+ if(text.includes(query)) row.style.display = 'table-row';
 
2997
  else row.style.display = 'none';
2998
  });
2999
  }
 
3074
  "logo_url": DEFAULT_LOGO_URL,
3075
  "whatsapp_number": DEFAULT_WHATSAPP_NUMBER,
3076
  "track_inventory": False,
3077
+ "use_barcodes": False,
3078
  "business_type": "mixed",
3079
  "system_mode": "both",
3080
  "customer_fields": {'name': True, 'phone': True, 'city': True, 'address': False, 'zip': False},
 
3111
  all_data[env_id]['settings']['system_mode'] = mode
3112
  if mode == 'external':
3113
  all_data[env_id]['settings']['track_inventory'] = False
3114
+ all_data[env_id]['settings']['use_barcodes'] = False
3115
  save_data(all_data)
3116
  flash(f'Режим среды {env_id} обновлен.', 'success')
3117
  return redirect(url_for('admhosto'))
 
3222
  def get_staff_orders(env_id, staff_id):
3223
  data = get_env_data(env_id)
3224
  orders = data.get('orders', {})
3225
+ date_filter = request.args.get('date')
3226
+ staff_orders = [o for o in orders.values() if o.get('staff_id') == staff_id and (o.get('status') == 'pos' or o.get('status') == 'confirmed')]
3227
 
3228
+ if date_filter:
3229
+ filtered = []
3230
+ for o in staff_orders:
3231
+ if o.get('created_at', '').startswith(date_filter):
3232
+ filtered.append(o)
3233
+ staff_orders = filtered
3234
+
3235
  staff_orders.sort(key=lambda x: x.get('created_at', ''), reverse=True)
3236
  return jsonify(staff_orders[:50])
3237
 
3238
+ @app.route('/<env_id>/api/assembly/<order_id>', methods=['POST'])
3239
+ def update_assembly(env_id, order_id):
3240
+ data = get_env_data(env_id)
3241
+ order = data.get('orders', {}).get(order_id)
3242
+ if not order:
3243
+ return jsonify({"success": False, "error": "Order not found"}), 404
3244
+
3245
+ req = request.json
3246
+ c_key = req.get('c_key')
3247
+ qty = int(req.get('qty', 0))
3248
+ if 'assembled' not in order:
3249
+ order['assembled'] = {}
3250
+ order['assembled'][c_key] = qty
3251
+
3252
+ save_env_data(env_id, data)
3253
+ return jsonify({"success": True})
3254
+
3255
  @app.route('/<env_id>/process_return/<order_id>', methods=['POST'])
3256
  def process_return(env_id, order_id):
3257
  data = get_env_data(env_id)
 
3326
  "price": float(item['cart_price']),
3327
  "calculated_price": float(item.get('calculated_price', item['cart_price'])),
3328
  "quantity": int(item['quantity']),
 
3329
  "pieces_per_box": int(item.get('pieces_per_box', 1)) if str(item.get('pieces_per_box', 1)).strip() != "" else 1,
3330
  "variant_name": item.get('variant_name', ''),
3331
  "variant_idx": item.get('variant_idx', -1),
 
3348
  "customer_city": customer_city,
3349
  "customer_address": customer_address,
3350
  "customer_zip": customer_zip,
3351
+ "customer_whatsapp": customer_whatsapp,
3352
+ "assembled": {}
3353
  }
3354
 
3355
  if order_status == 'pos' and data['settings'].get('track_inventory', False) and data['settings'].get('system_mode', 'both') != 'external':
 
3391
  env_id=env_id
3392
  )
3393
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3394
  @app.route('/<env_id>/edit_order/<order_id>', methods=['POST'])
3395
  def edit_order(env_id, order_id):
3396
  data = get_env_data(env_id)
 
3553
 
3554
  if settings.get('system_mode', 'both') == 'external':
3555
  settings['track_inventory'] = False
3556
+ settings['use_barcodes'] = False
3557
  else:
3558
  settings['track_inventory'] = 'track_inventory' in request.form
3559
+ settings['use_barcodes'] = 'use_barcodes' in request.form
3560
 
3561
  settings['customer_fields'] = {
3562
  'name': 'cf_name' in request.form,
 
3618
 
3619
  elif action == 'add_product':
3620
  name = request.form.get('name', '').strip()
3621
+ barcode = request.form.get('barcode', '').strip()
3622
  price_str = request.form.get('price', '')
3623
  price = float(price_str) if price_str else ""
3624
 
 
3634
  stock_str = request.form.get('stock', '')
3635
  main_stock = int(stock_str) if stock_str else ""
3636
 
 
 
3637
  description = request.form.get('description', '').strip()
3638
  category = request.form.get('category')
3639
  has_variant_prices = 'has_variant_prices' in request.form
3640
 
3641
  variant_names = request.form.getlist('variant_name[]')
3642
+ variant_barcodes = request.form.getlist('variant_barcode[]')
3643
  variant_prices = request.form.getlist('variant_price[]')
3644
  variant_box_prices = request.form.getlist('variant_box_price[]')
3645
  variant_stocks = request.form.getlist('variant_stock[]')
 
3646
  variants = []
3647
 
3648
  for i in range(len(variant_names)):
 
3666
 
3667
  variants.append({
3668
  "name": v_name,
3669
+ "barcode": v_barcode,
3670
  "price": v_price,
3671
  "box_price": v_box_price,
3672
+ "stock": v_stock
 
3673
  })
3674
 
3675
  uploaded_photos = request.files.getlist('photos')[:10]
 
3705
  new_product = {
3706
  'product_id': uuid4().hex,
3707
  'name': name,
3708
+ 'barcode': barcode,
3709
  'price': price,
3710
  'pieces_per_box': pieces_per_box,
3711
  'box_price': box_price,
3712
  'min_order': min_order,
3713
  'stock': main_stock,
 
3714
  'description': description,
3715
  'category': category,
3716
  'photos': photos_list,
 
3724
  elif action == 'edit_product':
3725
  pid = request.form.get('product_id')
3726
  name = request.form.get('name', '').strip()
3727
+ barcode = request.form.get('barcode', '').strip()
3728
 
3729
  price_str = request.form.get('price', '')
3730
  price = float(price_str) if price_str else ""
 
3741
  stock_str = request.form.get('stock', '')
3742
  main_stock = int(stock_str) if stock_str else ""
3743
 
 
 
3744
  description = request.form.get('description', '').strip()
3745
  has_variant_prices = 'has_variant_prices' in request.form
3746
 
3747
  variant_names = request.form.getlist('variant_name[]')
3748
+ variant_barcodes = request.form.getlist('variant_barcode[]')
3749
  variant_prices = request.form.getlist('variant_price[]')
3750
  variant_box_prices = request.form.getlist('variant_box_price[]')
3751
  variant_stocks = request.form.getlist('variant_stock[]')
 
3752
  variants = []
3753
 
3754
  for i in range(len(variant_names)):
 
3769
  v_barcode = variant_barcodes[i].strip()
3770
  variants.append({
3771
  "name": v_name,
3772
+ "barcode": v_barcode,
3773
  "price": v_price,
3774
  "box_price": v_box_price,
3775
+ "stock": v_stock
 
3776
  })
3777
 
3778
  uploaded_photos = request.files.getlist('photos')[:10]
 
3808
  for p in products:
3809
  if p.get('product_id') == pid:
3810
  p['name'] = name
3811
+ p['barcode'] = barcode
3812
  p['price'] = price
3813
  p['pieces_per_box'] = pieces_per_box
3814
  p['box_price'] = box_price
3815
  p['min_order'] = min_order
3816
  p['stock'] = main_stock
 
3817
  p['description'] = description
3818
  p['variants'] = variants
3819
  p['has_variant_prices'] = has_variant_prices