Kgshop commited on
Commit
444c10b
·
verified ·
1 Parent(s): f702da5

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +403 -68
app.py CHANGED
@@ -235,11 +235,16 @@ def load_data():
235
  if 'variants' not in product: product['variants'] =[]; changed = True
236
  if 'has_variant_prices' not in product: product['has_variant_prices'] = False; changed = True
237
  if 'stock' not in product: product['stock'] = ""; changed = True
 
 
 
238
  for v in product['variants']:
239
  if 'stock' not in v: v['stock'] = ""; changed = True
240
  if 'box_price' not in v: v['box_price'] = ""; changed = True
241
  if 'barcode' not in v: v['barcode'] = ""; changed = True
242
  if 'pieces_per_box' not in v: v['pieces_per_box'] = product.get('pieces_per_box', ""); changed = True
 
 
243
 
244
  for order_id, order in env_data['orders'].items():
245
  if 'status' not in order: order['status'] = 'confirmed'; changed = True
@@ -249,6 +254,7 @@ def load_data():
249
  for item in order.get('cart', []):
250
  if 'discount' not in item: item['discount'] = 0; changed = True
251
  if 'category' not in item: item['category'] = 'Без категории'; changed = True
 
252
 
253
  if changed or not os.path.exists(DATA_FILE):
254
  try:
@@ -325,10 +331,16 @@ def update_order_totals(order, business_type):
325
  c_box_price = float(i.get('cart_box_price', 0))
326
  item_discount = float(i.get('discount', 0))
327
 
328
- if business_type in ['mixed', 'wholesale'] and c_box_price > 0 and ppb > 1 and qty >= ppb:
 
 
 
 
 
 
 
 
329
  base_price = c_box_price / ppb
330
- else:
331
- base_price = c_price
332
 
333
  discounted_price = max(0, base_price - item_discount)
334
  item_total = discounted_price * qty
@@ -586,10 +598,10 @@ CATALOG_TEMPLATE = '''
586
  .category-item span.count { color: var(--text-muted); font-size: 0.8rem; background: var(--bg); padding: 4px 10px; border-radius: 20px; }
587
 
588
  .products-container { display: none; padding: 20px; flex-direction: column; gap: 15px; }
589
- .product-card { background: var(--surface); border-radius: 16px; padding: 15px; display: flex; flex-direction: column; box-shadow: 0 4px 15px rgba(0,0,0,0.03); width: 100%; gap: 10px; }
590
  .product-main-content { display: flex; width: 100%; gap: 15px; align-items: stretch; }
591
  .product-img-wrapper { position: relative; width: 100px; height: 100px; flex-shrink: 0; }
592
- .product-img { width: 100%; height: 100%; border-radius: 12px; object-fit: cover; cursor: pointer; background: var(--bg); border: 1px solid var(--border); }
593
  .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; }
594
  .product-info { flex-grow: 1; display: flex; flex-direction: column; min-width: 0; justify-content: flex-start; gap: 4px; }
595
  .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; }
@@ -602,15 +614,18 @@ CATALOG_TEMPLATE = '''
602
  .quantity-control { display: flex; align-items: center; background: var(--bg); border-radius: 8px; overflow: hidden; border: 1px solid var(--border); }
603
  .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; }
604
  .quantity-control button:active { background: #e0e0e0; }
 
605
  .quantity-control input { width: 36px; height: 32px; border: none; text-align: center; background: transparent; font-weight: 600; font-size: 0.95rem; color: var(--primary); outline: none; }
 
606
  .quantity-control input[type="number"]::-webkit-inner-spin-button,
607
  .quantity-control input[type="number"]::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }
608
  .quantity-control input[type="number"] { -moz-appearance: textfield; }
609
  .box-btn { background: var(--primary); color: #fff; border: none; border-radius: 8px; padding: 0 10px; height: 32px; font-size: 0.8rem; font-weight: 600; cursor: pointer; transition: opacity 0.2s; }
610
  .box-btn:active { opacity: 0.8; }
 
611
 
612
  .variants-list { display: flex; flex-direction: column; gap: 8px; margin-top: 5px; width: 100%; }
613
- .variant-item { display: flex; justify-content: space-between; align-items: center; background: var(--bg); padding: 10px; border-radius: 8px; flex-wrap: wrap; gap: 10px; border: 1px solid var(--border); }
614
  .variant-info { display: flex; flex-direction: column; flex: 1; min-width: 120px; }
615
  .variant-name { font-weight: 600; font-size: 0.9rem; }
616
  .variant-price { font-size: 0.85rem; color: var(--primary); font-weight: 500; }
@@ -953,6 +968,10 @@ CATALOG_TEMPLATE = '''
953
  const imgClick = hasPhotos ? `onclick="openGallery('${p.product_id}')"` : '';
954
  const descHtml = p.description ? `<div class="product-desc">${p.description}</div>` : '';
955
 
 
 
 
 
956
  let boxInfoHtml = '';
957
  if (businessType !== 'retail') {
958
  if (ppb > 1) boxInfoHtml += `<div class="product-box-info">В упаковке: ${ppb} шт</div>`;
@@ -971,9 +990,18 @@ CATALOG_TEMPLATE = '''
971
  if (p.variants && p.variants.length > 0) {
972
  variantsHtml = `<div class="variants-list">`;
973
  p.variants.forEach((v, idx) => {
 
 
 
 
974
  let vPrice = p.has_variant_prices ? v.price : p.price;
975
  let vBoxPrice = p.has_variant_prices ? (v.box_price || '') : (p.box_price || '');
 
976
  let vStockHtml = showStock && v.stock !== "" && v.stock !== null ? `<div class="variant-stock">Остаток: ${v.stock} шт</div>` : '';
 
 
 
 
977
  let cKey = getCartKey(p.product_id, idx);
978
  let qty = cart[cKey] ? cart[cKey].quantity : 0;
979
  let vPpb = parseInt(v.pieces_per_box) || ppb;
@@ -982,14 +1010,26 @@ CATALOG_TEMPLATE = '''
982
  if (businessType === 'mixed' && vBoxPrice && vPpb > 1) {
983
  priceText += `<br><span style="font-size:0.8rem; color:#636e72;">Упаковка: ${vBoxPrice} ${currency}</span>`;
984
  }
 
 
 
 
 
 
 
 
 
 
 
 
985
 
986
  let addBoxBtnVariant = '';
987
  if (businessType !== 'retail' && vPpb > 1) {
988
- addBoxBtnVariant = `<button class="box-btn" style="height:32px; margin-right:5px;" onclick="updateCart('${p.product_id}', ${vPpb}, null, false, '${cKey}', ${moq})">+ Упаковка</button>`;
989
  }
990
 
991
  variantsHtml += `
992
- <div class="variant-item">
993
  <div class="variant-info">
994
  <span class="variant-name">${v.name}</span>
995
  <span class="variant-price">${priceText}</span>
@@ -998,9 +1038,9 @@ CATALOG_TEMPLATE = '''
998
  <div style="display:flex; align-items:center;">
999
  ${addBoxBtnVariant}
1000
  <div class="quantity-control" style="border:none; background:var(--surface);">
1001
- <button onclick="updateCart('${p.product_id}', -1, null, false, '${cKey}', ${moq})"><i class="fas fa-minus" style="font-size:0.8rem;"></i></button>
1002
- <input type="number" id="qty-${cKey}" value="${qty}" onchange="manualUpdateCart('${cKey}', this.value, ${moq})">
1003
- <button onclick="updateCart('${p.product_id}', 1, null, false, '${cKey}', ${moq})"><i class="fas fa-plus" style="font-size:0.8rem;"></i></button>
1004
  </div>
1005
  </div>
1006
  </div>
@@ -1009,17 +1049,32 @@ CATALOG_TEMPLATE = '''
1009
  variantsHtml += `</div>`;
1010
  } else {
1011
  let mStockHtml = showStock && p.stock !== "" && p.stock !== null ? `<div style="font-size:0.8rem; color:#0984e3; margin-top:4px;">Остаток: ${p.stock} шт</div>` : '';
 
 
 
 
1012
  let qty = cart[p.product_id] ? cart[p.product_id].quantity : 0;
1013
 
1014
  let addBoxBtn = '';
1015
  if (businessType !== 'retail' && ppb > 1) {
1016
- addBoxBtn = `<button class="box-btn" onclick="updateCart('${p.product_id}', ${ppb}, null, false, null, ${moq})">+ Упаковка</button>`;
1017
  }
1018
 
1019
  let priceText = `${p.price} ${currency}`;
1020
  if (businessType === 'mixed' && p.box_price && ppb > 1) {
1021
  priceText += `<br><span style="font-size:0.8rem; color:#636e72;">Упаковка: ${p.box_price} ${currency}</span>`;
1022
  }
 
 
 
 
 
 
 
 
 
 
 
1023
 
1024
  mainControlsHtml = `
1025
  <div class="product-bottom">
@@ -1030,9 +1085,9 @@ CATALOG_TEMPLATE = '''
1030
  <div class="controls-wrapper">
1031
  ${addBoxBtn}
1032
  <div class="quantity-control">
1033
- <button onclick="updateCart('${p.product_id}', -1, null, false, null, ${moq})"><i class="fas fa-minus" style="font-size:0.8rem;"></i></button>
1034
- <input type="number" id="qty-${p.product_id}" value="${qty}" onchange="manualUpdateCart('${p.product_id}', this.value, ${moq})">
1035
- <button onclick="updateCart('${p.product_id}', 1, null, false, null, ${moq})"><i class="fas fa-plus" style="font-size:0.8rem;"></i></button>
1036
  </div>
1037
  </div>
1038
  </div>
@@ -1041,6 +1096,7 @@ CATALOG_TEMPLATE = '''
1041
 
1042
  const div = document.createElement('div');
1043
  div.className = 'product-card';
 
1044
  div.innerHTML = `
1045
  <div class="product-main-content">
1046
  <div class="product-img-wrapper" ${imgClick}>
@@ -1080,12 +1136,14 @@ CATALOG_TEMPLATE = '''
1080
  function updateCart(productId, change, exactValue = null, fromCartModal = false, cartKeyOverride = null, moq = 1) {
1081
  const p = products.find(x => x.product_id === productId);
1082
  if (!p) return;
 
1083
 
1084
  let cKey = cartKeyOverride !== null ? cartKeyOverride : productId;
1085
  let varIdx = -1;
1086
 
1087
  if (cKey.includes('___')) {
1088
  varIdx = parseInt(cKey.split('___')[1]);
 
1089
  }
1090
 
1091
  let pStock = "";
@@ -1104,14 +1162,19 @@ CATALOG_TEMPLATE = '''
1104
  let price = p.price;
1105
  let bPrice = p.box_price || 0;
1106
  let vName = "";
 
 
1107
  if (varIdx !== -1 && p.variants[varIdx]) {
1108
  if (p.has_variant_prices) {
1109
  price = p.variants[varIdx].price;
1110
  bPrice = p.variants[varIdx].box_price || 0;
 
 
 
1111
  }
1112
  vName = p.variants[varIdx].name;
1113
  }
1114
- cart[cKey] = { ...p, quantity: 0, cart_price: price, cart_box_price: bPrice, pieces_per_box: pPpb, variant_name: vName, variant_idx: varIdx, discount: 0 };
1115
  }
1116
 
1117
  let currentQty = cart[cKey].quantity;
@@ -1177,7 +1240,12 @@ CATALOG_TEMPLATE = '''
1177
  let disc = parseFloat(item.discount) || 0;
1178
 
1179
  let unit = cPrice;
1180
- if ((businessType === 'mixed' || businessType === 'wholesale') && cBoxPrice > 0 && ppb > 1 && qty >= ppb) {
 
 
 
 
 
1181
  unit = cBoxPrice / ppb;
1182
  }
1183
  return Math.max(0, unit - disc) * qty;
@@ -2219,8 +2287,7 @@ ADMIN_TEMPLATE = '''
2219
  .social-item label { display: flex; align-items: center; gap: 5px; width: 150px; cursor: pointer; }
2220
 
2221
  .variants-container { background: #f4f6f9; padding: 15px; border-radius: 10px; border: 1px dashed var(--border); display: flex; flex-direction: column; gap: 10px; }
2222
- .variant-row { display: flex; flex-wrap: wrap; gap: 10px; align-items: flex-end; background: #fff; padding: 10px; border-radius: 8px; border: 1px solid var(--border); }
2223
- .variant-row .form-group { flex: 1 1 30%; min-width: 120px; }
2224
  .remove-variant-btn { color: var(--danger); background: none; border: none; font-size: 1.2rem; cursor: pointer; padding: 12px 5px; flex: 0 0 auto; }
2225
 
2226
  .order-item { background: #fff; padding: 15px; border-radius: 10px; border: 1px solid var(--border); margin-bottom: 10px; }
@@ -2527,11 +2594,25 @@ ADMIN_TEMPLATE = '''
2527
  <i class="fas fa-chevron-down" id="icon-cat-{{ loop.index }}" style="color: #636e72;"></i>
2528
  <span class="cat-title-text"><i class="fas fa-folder-open" style="color:var(--info); margin-right:5px;"></i> {{ category }}</span>
2529
  </div>
2530
- <form method="POST" style="margin:0;" onclick="event.stopPropagation();" onsubmit="return confirm('Удалить категорию и все ее товары?');">
2531
- <input type="hidden" name="action" value="delete_category">
2532
- <input type="hidden" name="category_name" value="{{ category }}">
2533
- <button type="submit" class="btn btn-danger"><i class="fas fa-trash-alt"></i></button>
2534
- </form>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2535
  </div>
2536
  <div class="category-content" id="cat-{{ loop.index }}">
2537
 
@@ -2571,6 +2652,14 @@ ADMIN_TEMPLATE = '''
2571
  <label>Название товара</label>
2572
  <input type="text" name="name" placeholder="Введите название" required autocomplete="off">
2573
  </div>
 
 
 
 
 
 
 
 
2574
 
2575
  {% if settings.use_barcodes and sys_mode not in ['external', 'light_external'] %}
2576
  <div class="form-group main-barcode-container">
@@ -2584,7 +2673,7 @@ ADMIN_TEMPLATE = '''
2584
 
2585
  <div class="form-group main-price-container">
2586
  <label>Цена за ед.</label>
2587
- <input type="number" name="price" placeholder="Цена" step="0.01" class="main-price-input">
2588
  </div>
2589
 
2590
  {% if settings.business_type != 'retail' %}
@@ -2615,6 +2704,14 @@ ADMIN_TEMPLATE = '''
2615
  </div>
2616
  {% endif %}
2617
  </div>
 
 
 
 
 
 
 
 
2618
 
2619
  <div class="variants-container" id="variants-container-add-{{ loop.index }}">
2620
  <div style="display:flex; justify-content:space-between; align-items:center;">
@@ -2643,17 +2740,20 @@ ADMIN_TEMPLATE = '''
2643
  <div class="product-item" data-pid="{{ product.product_id }}">
2644
  <div class="product-info">
2645
  {% if product.photos and product.photos|length > 0 %}
2646
- <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ product.photos[0] }}" class="product-img">
2647
  {% else %}
2648
  <div class="product-img" style="display:flex;align-items:center;justify-content:center;color:#ccc;"><i class="fas fa-image"></i></div>
2649
  {% endif %}
2650
  <div class="product-details">
2651
- <span class="product-name">{{ product.name }}</span>
2652
  <div style="font-size:0.8rem; color:#b2bec3;">ID: {{ product.product_id }}</div>
2653
  {% if product.description %}
2654
  <span class="product-desc">{{ product.description[:50] }}{{ '...' if product.description|length > 50 else '' }}</span>
2655
  {% endif %}
2656
  <span class="product-meta">
 
 
 
2657
  {% if product.has_variant_prices %}
2658
  Цена по вариантам
2659
  {% else %}
@@ -2679,7 +2779,20 @@ ADMIN_TEMPLATE = '''
2679
  </span>
2680
  </div>
2681
  </div>
2682
- <div class="product-actions">
 
 
 
 
 
 
 
 
 
 
 
 
 
2683
  <button class="btn btn-warning" onclick="toggleEditProduct('edit-prod-{{ product.product_id }}')"><i class="fas fa-edit"></i></button>
2684
  <form method="POST" style="margin:0;" onsubmit="return confirm('Удалить товар?');">
2685
  <input type="hidden" name="action" value="delete_product">
@@ -2700,6 +2813,14 @@ ADMIN_TEMPLATE = '''
2700
  <label>Название товара</label>
2701
  <input type="text" name="name" value="{{ product.name }}" required autocomplete="off">
2702
  </div>
 
 
 
 
 
 
 
 
2703
 
2704
  {% if settings.use_barcodes and sys_mode not in ['external', 'light_external'] %}
2705
  <div class="form-group main-barcode-container" {% if product.variants %}style="display:none;"{% endif %}>
@@ -2745,6 +2866,22 @@ ADMIN_TEMPLATE = '''
2745
  {% endif %}
2746
  </div>
2747
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2748
  <div class="variants-container" id="variants-container-edit-{{ product.product_id }}">
2749
  <div style="display:flex; justify-content:space-between; align-items:center;">
2750
  <label style="font-weight:600; font-size:0.9rem;"><i class="fas fa-tags"></i> Варианты товара</label>
@@ -2753,42 +2890,72 @@ ADMIN_TEMPLATE = '''
2753
  <div id="variants-list-edit-{{ product.product_id }}">
2754
  {% for variant in product.variants %}
2755
  <div class="variant-row">
2756
- <div class="form-group">
2757
- <label>Название</label>
2758
- <input type="text" name="variant_name[]" value="{{ variant.name }}" placeholder="Цвет, размер" required>
2759
  </div>
2760
- {% if settings.business_type != 'retail' %}
2761
- <div class="form-group">
2762
- <label>В уп. (шт)</label>
2763
- <input type="number" name="variant_pieces_per_box[]" value="{{ variant.pieces_per_box }}" placeholder="Шт">
2764
- </div>
2765
- {% endif %}
2766
- {% if settings.use_barcodes and sys_mode not in['external', 'light_external'] %}
2767
- <div class="form-group">
2768
- <label>Штрих-код</label>
2769
- <div style="display:flex; gap:5px;">
2770
- <input type="text" name="variant_barcode[]" value="{{ variant.barcode }}" placeholder="Код">
2771
- <button type="button" class="btn btn-outline" style="padding: 12px;" onclick="startScanner(val => this.previousElementSibling.value = val)"><i class="fas fa-barcode"></i></button>
2772
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2773
  </div>
2774
- {% endif %}
2775
- <div class="form-group var-price-input" {% if not product.has_variant_prices %}style="display:none;"{% endif %}>
2776
- <label>Цена</label>
2777
- <input type="number" name="variant_price[]" value="{{ variant.price }}" placeholder="Цена за ед." step="0.01" {% if product.has_variant_prices %}required{% endif %}>
2778
- </div>
2779
- {% if settings.business_type == 'mixed' %}
2780
- <div class="form-group var-price-input" {% if not product.has_variant_prices %}style="display:none;"{% endif %}>
2781
- <label>Цена уп.</label>
2782
- <input type="number" name="variant_box_price[]" value="{{ variant.box_price }}" placeholder="Опционально" step="0.01">
2783
- </div>
2784
- {% endif %}
2785
- {% if settings.track_inventory and sys_mode not in ['external', 'light_external'] %}
2786
- <div class="form-group">
2787
- <label>Остаток</label>
2788
- <input type="number" name="variant_stock[]" value="{{ variant.stock }}" placeholder="Остаток">
2789
- </div>
2790
- {% endif %}
2791
- <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>
2792
  </div>
2793
  {% endfor %}
2794
  </div>
@@ -2889,6 +3056,52 @@ ADMIN_TEMPLATE = '''
2889
  document.getElementById(elId).style.display = 'none';
2890
  }
2891
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2892
  function addVariantRow(containerId) {
2893
  const container = document.getElementById(containerId);
2894
  const formBlock = container.closest('form');
@@ -2902,6 +3115,18 @@ ADMIN_TEMPLATE = '''
2902
  let reqAttr = hasVariantPrices ? 'required' : '';
2903
 
2904
  let html = `
 
 
 
 
 
 
 
 
 
 
 
 
2905
  <div class="form-group">
2906
  <label>Название</label>
2907
  <input type="text" name="variant_name[]" placeholder="Цвет, размер" required>
@@ -2938,7 +3163,7 @@ ADMIN_TEMPLATE = '''
2938
  html += `
2939
  <div class="form-group var-price-input" ${displayStyle}>
2940
  <label>Цена уп.</label>
2941
- <input type="number" name="variant_box_price[]" placeholder="Опционально" step="0.01">
2942
  </div>`;
2943
  }
2944
 
@@ -2949,11 +3174,21 @@ ADMIN_TEMPLATE = '''
2949
  <input type="number" name="variant_stock[]" placeholder="Остаток">
2950
  </div>`;
2951
  }
 
 
 
 
 
 
 
 
 
2952
 
2953
- html += `<button type="button" class="remove-variant-btn" onclick="this.parentElement.remove(); updateMainStockVisibility('${formId}')"><i class="fas fa-times-circle"></i></button>`;
2954
 
2955
  div.innerHTML = html;
2956
  container.appendChild(div);
 
2957
  updateMainStockVisibility(formId);
2958
  }
2959
 
@@ -2962,12 +3197,14 @@ ADMIN_TEMPLATE = '''
2962
  const mainPriceContainer = form.querySelector('.main-price-container');
2963
  const mainPriceInput = form.querySelector('.main-price-input');
2964
  const mainBoxPriceContainer = form.querySelector('.main-box-price-container');
 
2965
  const varPriceInputs = form.querySelectorAll('.var-price-input');
2966
 
2967
  if (cb.checked) {
2968
  if(mainPriceContainer) mainPriceContainer.style.display = 'none';
2969
  if(mainPriceInput) mainPriceInput.removeAttribute('required');
2970
  if(mainBoxPriceContainer) mainBoxPriceContainer.style.display = 'none';
 
2971
 
2972
  varPriceInputs.forEach(input => {
2973
  input.style.display = 'flex';
@@ -2978,6 +3215,7 @@ ADMIN_TEMPLATE = '''
2978
  if(mainPriceContainer) mainPriceContainer.style.display = 'flex';
2979
  if(mainPriceInput) mainPriceInput.setAttribute('required', 'required');
2980
  if(mainBoxPriceContainer) mainBoxPriceContainer.style.display = 'flex';
 
2981
 
2982
  varPriceInputs.forEach(input => {
2983
  input.style.display = 'none';
@@ -4005,6 +4243,7 @@ def create_order(env_id):
4005
  "variant_name": item.get('variant_name', ''),
4006
  "variant_idx": item.get('variant_idx', -1),
4007
  "discount": float(item.get('discount', 0)),
 
4008
  "category": product_dict.get(item.get('product_id'), 'Без категории'),
4009
  "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+"
4010
  })
@@ -4423,6 +4662,18 @@ def admin(env_id):
4423
  data['products'] = products
4424
  save_env_data(env_id, data)
4425
 
 
 
 
 
 
 
 
 
 
 
 
 
4426
  elif action == 'delete_category':
4427
  cat_name = request.form.get('category_name')
4428
  if cat_name in categories:
@@ -4436,6 +4687,8 @@ def admin(env_id):
4436
  elif action == 'add_product':
4437
  name = request.form.get('name', '').strip()
4438
  barcode = request.form.get('barcode', '').strip()
 
 
4439
  price_str = request.form.get('price', '')
4440
  price = float(price_str) if price_str else ""
4441
 
@@ -4455,25 +4708,45 @@ def admin(env_id):
4455
  category = request.form.get('category')
4456
  has_variant_prices = 'has_variant_prices' in request.form
4457
 
 
 
 
 
 
 
 
 
4458
  variant_names = request.form.getlist('variant_name[]')
4459
  variant_barcodes = request.form.getlist('variant_barcode[]')
4460
  variant_prices = request.form.getlist('variant_price[]')
4461
  variant_box_prices = request.form.getlist('variant_box_price[]')
4462
  variant_stocks = request.form.getlist('variant_stock[]')
4463
  variant_ppbs = request.form.getlist('variant_pieces_per_box[]')
4464
- variants =[]
 
4465
 
4466
  for i in range(len(variant_names)):
4467
  v_name = variant_names[i].strip()
4468
  if v_name:
4469
  v_price = price
4470
  v_box_price = box_price
 
 
 
 
 
4471
  if has_variant_prices:
4472
  if i < len(variant_prices) and variant_prices[i]:
4473
  v_price = float(variant_prices[i])
4474
  if i < len(variant_box_prices) and variant_box_prices[i]:
4475
  v_box_price = float(variant_box_prices[i])
4476
 
 
 
 
 
 
 
4477
  v_stock = ""
4478
  if i < len(variant_stocks) and variant_stocks[i]:
4479
  v_stock = int(variant_stocks[i])
@@ -4492,7 +4765,9 @@ def admin(env_id):
4492
  "price": v_price,
4493
  "box_price": v_box_price,
4494
  "stock": v_stock,
4495
- "pieces_per_box": v_ppb
 
 
4496
  })
4497
 
4498
  uploaded_photos = request.files.getlist('photos')[:10]
@@ -4538,7 +4813,9 @@ def admin(env_id):
4538
  'category': category,
4539
  'photos': photos_list,
4540
  'variants': variants,
4541
- 'has_variant_prices': has_variant_prices
 
 
4542
  }
4543
  products.append(new_product)
4544
  data['products'] = products
@@ -4548,6 +4825,7 @@ def admin(env_id):
4548
  pid = request.form.get('product_id')
4549
  name = request.form.get('name', '').strip()
4550
  barcode = request.form.get('barcode', '').strip()
 
4551
 
4552
  price_str = request.form.get('price', '')
4553
  price = float(price_str) if price_str else ""
@@ -4567,6 +4845,14 @@ def admin(env_id):
4567
  description = request.form.get('description', '').strip()
4568
  has_variant_prices = 'has_variant_prices' in request.form
4569
 
 
 
 
 
 
 
 
 
4570
  remove_photos = request.form.getlist('remove_photos[]')
4571
 
4572
  variant_names = request.form.getlist('variant_name[]')
@@ -4575,18 +4861,31 @@ def admin(env_id):
4575
  variant_box_prices = request.form.getlist('variant_box_price[]')
4576
  variant_stocks = request.form.getlist('variant_stock[]')
4577
  variant_ppbs = request.form.getlist('variant_pieces_per_box[]')
4578
- variants =[]
 
4579
 
4580
  for i in range(len(variant_names)):
4581
  v_name = variant_names[i].strip()
4582
  if v_name:
4583
  v_price = price
4584
  v_box_price = box_price
 
 
 
 
 
4585
  if has_variant_prices:
4586
  if i < len(variant_prices) and variant_prices[i]:
4587
  v_price = float(variant_prices[i])
4588
  if i < len(variant_box_prices) and variant_box_prices[i]:
4589
  v_box_price = float(variant_box_prices[i])
 
 
 
 
 
 
 
4590
  v_stock = ""
4591
  if i < len(variant_stocks) and variant_stocks[i]:
4592
  v_stock = int(variant_stocks[i])
@@ -4602,7 +4901,9 @@ def admin(env_id):
4602
  "price": v_price,
4603
  "box_price": v_box_price,
4604
  "stock": v_stock,
4605
- "pieces_per_box": v_ppb
 
 
4606
  })
4607
 
4608
  uploaded_photos = request.files.getlist('photos')[:10]
@@ -4647,6 +4948,8 @@ def admin(env_id):
4647
  p['description'] = description
4648
  p['variants'] = variants
4649
  p['has_variant_prices'] = has_variant_prices
 
 
4650
 
4651
  existing_photos = p.get('photos', [])
4652
  existing_photos =[ph for ph in existing_photos if ph not in remove_photos]
@@ -4655,6 +4958,38 @@ def admin(env_id):
4655
  data['products'] = products
4656
  save_env_data(env_id, data)
4657
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4658
  elif action == 'delete_product':
4659
  pid = request.form.get('product_id')
4660
  data['products'] =[p for p in products if p.get('product_id') != pid]
 
235
  if 'variants' not in product: product['variants'] =[]; changed = True
236
  if 'has_variant_prices' not in product: product['has_variant_prices'] = False; changed = True
237
  if 'stock' not in product: product['stock'] = ""; changed = True
238
+ if 'is_available' not in product: product['is_available'] = True; changed = True
239
+ if 'wholesale_tiers' not in product: product['wholesale_tiers'] = []; changed = True
240
+
241
  for v in product['variants']:
242
  if 'stock' not in v: v['stock'] = ""; changed = True
243
  if 'box_price' not in v: v['box_price'] = ""; changed = True
244
  if 'barcode' not in v: v['barcode'] = ""; changed = True
245
  if 'pieces_per_box' not in v: v['pieces_per_box'] = product.get('pieces_per_box', ""); changed = True
246
+ if 'is_available' not in v: v['is_available'] = True; changed = True
247
+ if 'wholesale_tiers' not in v: v['wholesale_tiers'] = []; changed = True
248
 
249
  for order_id, order in env_data['orders'].items():
250
  if 'status' not in order: order['status'] = 'confirmed'; changed = True
 
254
  for item in order.get('cart', []):
255
  if 'discount' not in item: item['discount'] = 0; changed = True
256
  if 'category' not in item: item['category'] = 'Без категории'; changed = True
257
+ if 'wholesale_tiers' not in item: item['wholesale_tiers'] = []; changed = True
258
 
259
  if changed or not os.path.exists(DATA_FILE):
260
  try:
 
331
  c_box_price = float(i.get('cart_box_price', 0))
332
  item_discount = float(i.get('discount', 0))
333
 
334
+ base_price = c_price
335
+ tiers = i.get('wholesale_tiers', [])
336
+
337
+ if business_type == 'wholesale' and tiers:
338
+ valid_tiers = [t for t in tiers if qty >= t.get('qty', 0)]
339
+ if valid_tiers:
340
+ valid_tiers.sort(key=lambda x: x['qty'], reverse=True)
341
+ base_price = float(valid_tiers[0]['price'])
342
+ elif business_type in ['mixed', 'wholesale'] and c_box_price > 0 and ppb > 1 and qty >= ppb:
343
  base_price = c_box_price / ppb
 
 
344
 
345
  discounted_price = max(0, base_price - item_discount)
346
  item_total = discounted_price * qty
 
598
  .category-item span.count { color: var(--text-muted); font-size: 0.8rem; background: var(--bg); padding: 4px 10px; border-radius: 20px; }
599
 
600
  .products-container { display: none; padding: 20px; flex-direction: column; gap: 15px; }
601
+ .product-card { background: var(--surface); border-radius: 16px; padding: 15px; display: flex; flex-direction: column; box-shadow: 0 4px 15px rgba(0,0,0,0.03); width: 100%; gap: 10px; transition: opacity 0.3s, filter 0.3s; }
602
  .product-main-content { display: flex; width: 100%; gap: 15px; align-items: stretch; }
603
  .product-img-wrapper { position: relative; width: 100px; height: 100px; flex-shrink: 0; }
604
+ .product-img { width: 100%; height: 100%; border-radius: 12px; object-fit: cover; cursor: pointer; background: var(--bg); border: 1px solid var(--border); transition: filter 0.3s; }
605
  .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; }
606
  .product-info { flex-grow: 1; display: flex; flex-direction: column; min-width: 0; justify-content: flex-start; gap: 4px; }
607
  .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; }
 
614
  .quantity-control { display: flex; align-items: center; background: var(--bg); border-radius: 8px; overflow: hidden; border: 1px solid var(--border); }
615
  .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; }
616
  .quantity-control button:active { background: #e0e0e0; }
617
+ .quantity-control button:disabled { color: #ccc; cursor: not-allowed; }
618
  .quantity-control input { width: 36px; height: 32px; border: none; text-align: center; background: transparent; font-weight: 600; font-size: 0.95rem; color: var(--primary); outline: none; }
619
+ .quantity-control input:disabled { color: #ccc; }
620
  .quantity-control input[type="number"]::-webkit-inner-spin-button,
621
  .quantity-control input[type="number"]::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }
622
  .quantity-control input[type="number"] { -moz-appearance: textfield; }
623
  .box-btn { background: var(--primary); color: #fff; border: none; border-radius: 8px; padding: 0 10px; height: 32px; font-size: 0.8rem; font-weight: 600; cursor: pointer; transition: opacity 0.2s; }
624
  .box-btn:active { opacity: 0.8; }
625
+ .box-btn:disabled { background: #ccc; cursor: not-allowed; }
626
 
627
  .variants-list { display: flex; flex-direction: column; gap: 8px; margin-top: 5px; width: 100%; }
628
+ .variant-item { display: flex; justify-content: space-between; align-items: center; background: var(--bg); padding: 10px; border-radius: 8px; flex-wrap: wrap; gap: 10px; border: 1px solid var(--border); transition: opacity 0.3s; }
629
  .variant-info { display: flex; flex-direction: column; flex: 1; min-width: 120px; }
630
  .variant-name { font-weight: 600; font-size: 0.9rem; }
631
  .variant-price { font-size: 0.85rem; color: var(--primary); font-weight: 500; }
 
968
  const imgClick = hasPhotos ? `onclick="openGallery('${p.product_id}')"` : '';
969
  const descHtml = p.description ? `<div class="product-desc">${p.description}</div>` : '';
970
 
971
+ let isMainAvailable = p.is_available !== false;
972
+ let cardStyle = !isMainAvailable ? 'opacity: 0.6; filter: grayscale(100%);' : '';
973
+ let mainDisabledAttr = !isMainAvailable ? 'disabled' : '';
974
+
975
  let boxInfoHtml = '';
976
  if (businessType !== 'retail') {
977
  if (ppb > 1) boxInfoHtml += `<div class="product-box-info">В упаковке: ${ppb} шт</div>`;
 
990
  if (p.variants && p.variants.length > 0) {
991
  variantsHtml = `<div class="variants-list">`;
992
  p.variants.forEach((v, idx) => {
993
+ let vAvailable = v.is_available !== false && isMainAvailable;
994
+ let vDisabledAttr = !vAvailable ? 'disabled' : '';
995
+ let vStyle = !vAvailable ? 'opacity: 0.6;' : '';
996
+
997
  let vPrice = p.has_variant_prices ? v.price : p.price;
998
  let vBoxPrice = p.has_variant_prices ? (v.box_price || '') : (p.box_price || '');
999
+
1000
  let vStockHtml = showStock && v.stock !== "" && v.stock !== null ? `<div class="variant-stock">Остаток: ${v.stock} шт</div>` : '';
1001
+ if (!vAvailable) {
1002
+ vStockHtml = `<div class="variant-stock" style="color:#e17055; font-weight:bold;">Нет в наличии</div>`;
1003
+ }
1004
+
1005
  let cKey = getCartKey(p.product_id, idx);
1006
  let qty = cart[cKey] ? cart[cKey].quantity : 0;
1007
  let vPpb = parseInt(v.pieces_per_box) || ppb;
 
1010
  if (businessType === 'mixed' && vBoxPrice && vPpb > 1) {
1011
  priceText += `<br><span style="font-size:0.8rem; color:#636e72;">Упаковка: ${vBoxPrice} ${currency}</span>`;
1012
  }
1013
+ if (businessType === 'wholesale') {
1014
+ let tiers = v.wholesale_tiers || [];
1015
+ if (!p.has_variant_prices) tiers = p.wholesale_tiers || [];
1016
+ if (tiers.length > 0) {
1017
+ tiers.sort((a,b) => a.qty - b.qty);
1018
+ priceText += `<br><div style="font-size:0.8rem; color:#0984e3; margin-top:4px;">`;
1019
+ tiers.forEach(t => {
1020
+ priceText += `От ${t.qty} шт: ${t.price} ${currency}<br>`;
1021
+ });
1022
+ priceText += `</div>`;
1023
+ }
1024
+ }
1025
 
1026
  let addBoxBtnVariant = '';
1027
  if (businessType !== 'retail' && vPpb > 1) {
1028
+ addBoxBtnVariant = `<button class="box-btn" style="height:32px; margin-right:5px;" onclick="updateCart('${p.product_id}', ${vPpb}, null, false, '${cKey}', ${moq})" ${vDisabledAttr}>+ Упаковка</button>`;
1029
  }
1030
 
1031
  variantsHtml += `
1032
+ <div class="variant-item" style="${vStyle}">
1033
  <div class="variant-info">
1034
  <span class="variant-name">${v.name}</span>
1035
  <span class="variant-price">${priceText}</span>
 
1038
  <div style="display:flex; align-items:center;">
1039
  ${addBoxBtnVariant}
1040
  <div class="quantity-control" style="border:none; background:var(--surface);">
1041
+ <button onclick="updateCart('${p.product_id}', -1, null, false, '${cKey}', ${moq})" ${vDisabledAttr}><i class="fas fa-minus" style="font-size:0.8rem;"></i></button>
1042
+ <input type="number" id="qty-${cKey}" value="${qty}" onchange="manualUpdateCart('${cKey}', this.value, ${moq})" ${vDisabledAttr}>
1043
+ <button onclick="updateCart('${p.product_id}', 1, null, false, '${cKey}', ${moq})" ${vDisabledAttr}><i class="fas fa-plus" style="font-size:0.8rem;"></i></button>
1044
  </div>
1045
  </div>
1046
  </div>
 
1049
  variantsHtml += `</div>`;
1050
  } else {
1051
  let mStockHtml = showStock && p.stock !== "" && p.stock !== null ? `<div style="font-size:0.8rem; color:#0984e3; margin-top:4px;">Остаток: ${p.stock} шт</div>` : '';
1052
+ if (!isMainAvailable) {
1053
+ mStockHtml = `<div style="font-size:0.8rem; color:#e17055; margin-top:4px; font-weight:bold;">Нет в наличии</div>`;
1054
+ }
1055
+
1056
  let qty = cart[p.product_id] ? cart[p.product_id].quantity : 0;
1057
 
1058
  let addBoxBtn = '';
1059
  if (businessType !== 'retail' && ppb > 1) {
1060
+ addBoxBtn = `<button class="box-btn" onclick="updateCart('${p.product_id}', ${ppb}, null, false, null, ${moq})" ${mainDisabledAttr}>+ Упаковка</button>`;
1061
  }
1062
 
1063
  let priceText = `${p.price} ${currency}`;
1064
  if (businessType === 'mixed' && p.box_price && ppb > 1) {
1065
  priceText += `<br><span style="font-size:0.8rem; color:#636e72;">Упаковка: ${p.box_price} ${currency}</span>`;
1066
  }
1067
+ if (businessType === 'wholesale') {
1068
+ let tiers = p.wholesale_tiers || [];
1069
+ if (tiers.length > 0) {
1070
+ tiers.sort((a,b) => a.qty - b.qty);
1071
+ priceText += `<br><div style="font-size:0.8rem; color:#0984e3; margin-top:4px;">`;
1072
+ tiers.forEach(t => {
1073
+ priceText += `От ${t.qty} шт: ${t.price} ${currency}<br>`;
1074
+ });
1075
+ priceText += `</div>`;
1076
+ }
1077
+ }
1078
 
1079
  mainControlsHtml = `
1080
  <div class="product-bottom">
 
1085
  <div class="controls-wrapper">
1086
  ${addBoxBtn}
1087
  <div class="quantity-control">
1088
+ <button onclick="updateCart('${p.product_id}', -1, null, false, null, ${moq})" ${mainDisabledAttr}><i class="fas fa-minus" style="font-size:0.8rem;"></i></button>
1089
+ <input type="number" id="qty-${p.product_id}" value="${qty}" onchange="manualUpdateCart('${p.product_id}', this.value, ${moq})" ${mainDisabledAttr}>
1090
+ <button onclick="updateCart('${p.product_id}', 1, null, false, null, ${moq})" ${mainDisabledAttr}><i class="fas fa-plus" style="font-size:0.8rem;"></i></button>
1091
  </div>
1092
  </div>
1093
  </div>
 
1096
 
1097
  const div = document.createElement('div');
1098
  div.className = 'product-card';
1099
+ div.style = cardStyle;
1100
  div.innerHTML = `
1101
  <div class="product-main-content">
1102
  <div class="product-img-wrapper" ${imgClick}>
 
1136
  function updateCart(productId, change, exactValue = null, fromCartModal = false, cartKeyOverride = null, moq = 1) {
1137
  const p = products.find(x => x.product_id === productId);
1138
  if (!p) return;
1139
+ if (p.is_available === false && change > 0) return;
1140
 
1141
  let cKey = cartKeyOverride !== null ? cartKeyOverride : productId;
1142
  let varIdx = -1;
1143
 
1144
  if (cKey.includes('___')) {
1145
  varIdx = parseInt(cKey.split('___')[1]);
1146
+ if (p.variants[varIdx] && p.variants[varIdx].is_available === false && change > 0) return;
1147
  }
1148
 
1149
  let pStock = "";
 
1162
  let price = p.price;
1163
  let bPrice = p.box_price || 0;
1164
  let vName = "";
1165
+ let tiers = p.wholesale_tiers || [];
1166
+
1167
  if (varIdx !== -1 && p.variants[varIdx]) {
1168
  if (p.has_variant_prices) {
1169
  price = p.variants[varIdx].price;
1170
  bPrice = p.variants[varIdx].box_price || 0;
1171
+ tiers = p.variants[varIdx].wholesale_tiers || [];
1172
+ } else {
1173
+ tiers = p.wholesale_tiers || [];
1174
  }
1175
  vName = p.variants[varIdx].name;
1176
  }
1177
+ cart[cKey] = { ...p, quantity: 0, cart_price: price, cart_box_price: bPrice, pieces_per_box: pPpb, variant_name: vName, variant_idx: varIdx, discount: 0, wholesale_tiers: tiers };
1178
  }
1179
 
1180
  let currentQty = cart[cKey].quantity;
 
1240
  let disc = parseFloat(item.discount) || 0;
1241
 
1242
  let unit = cPrice;
1243
+ if (businessType === 'wholesale' && item.wholesale_tiers && item.wholesale_tiers.length > 0) {
1244
+ let validTiers = item.wholesale_tiers.filter(t => qty >= t.qty).sort((a, b) => b.qty - a.qty);
1245
+ if (validTiers.length > 0) {
1246
+ unit = validTiers[0].price;
1247
+ }
1248
+ } else if ((businessType === 'mixed' || businessType === 'wholesale') && cBoxPrice > 0 && ppb > 1 && qty >= ppb) {
1249
  unit = cBoxPrice / ppb;
1250
  }
1251
  return Math.max(0, unit - disc) * qty;
 
2287
  .social-item label { display: flex; align-items: center; gap: 5px; width: 150px; cursor: pointer; }
2288
 
2289
  .variants-container { background: #f4f6f9; padding: 15px; border-radius: 10px; border: 1px dashed var(--border); display: flex; flex-direction: column; gap: 10px; }
2290
+ .variant-row { display: flex; flex-wrap: wrap; gap: 10px; align-items: flex-start; background: #fff; padding: 10px; border-radius: 8px; border: 1px solid var(--border); }
 
2291
  .remove-variant-btn { color: var(--danger); background: none; border: none; font-size: 1.2rem; cursor: pointer; padding: 12px 5px; flex: 0 0 auto; }
2292
 
2293
  .order-item { background: #fff; padding: 15px; border-radius: 10px; border: 1px solid var(--border); margin-bottom: 10px; }
 
2594
  <i class="fas fa-chevron-down" id="icon-cat-{{ loop.index }}" style="color: #636e72;"></i>
2595
  <span class="cat-title-text"><i class="fas fa-folder-open" style="color:var(--info); margin-right:5px;"></i> {{ category }}</span>
2596
  </div>
2597
+ <div style="display: flex; gap: 5px; align-items: center;">
2598
+ <form method="POST" style="margin:0;" onclick="event.stopPropagation();">
2599
+ <input type="hidden" name="action" value="move_category">
2600
+ <input type="hidden" name="direction" value="up">
2601
+ <input type="hidden" name="category_name" value="{{ category }}">
2602
+ <button type="submit" class="btn btn-outline" style="padding: 2px 6px;"><i class="fas fa-arrow-up"></i></button>
2603
+ </form>
2604
+ <form method="POST" style="margin:0;" onclick="event.stopPropagation();">
2605
+ <input type="hidden" name="action" value="move_category">
2606
+ <input type="hidden" name="direction" value="down">
2607
+ <input type="hidden" name="category_name" value="{{ category }}">
2608
+ <button type="submit" class="btn btn-outline" style="padding: 2px 6px;"><i class="fas fa-arrow-down"></i></button>
2609
+ </form>
2610
+ <form method="POST" style="margin:0;" onclick="event.stopPropagation();" onsubmit="return confirm('Удалить категорию и все ее товары?');">
2611
+ <input type="hidden" name="action" value="delete_category">
2612
+ <input type="hidden" name="category_name" value="{{ category }}">
2613
+ <button type="submit" class="btn btn-danger" style="margin-left:10px;"><i class="fas fa-trash-alt"></i></button>
2614
+ </form>
2615
+ </div>
2616
  </div>
2617
  <div class="category-content" id="cat-{{ loop.index }}">
2618
 
 
2652
  <label>Название товара</label>
2653
  <input type="text" name="name" placeholder="Введите название" required autocomplete="off">
2654
  </div>
2655
+
2656
+ <div class="form-group" style="flex:1;">
2657
+ <label>Наличие</label>
2658
+ <select name="is_available">
2659
+ <option value="1" selected>В наличии</option>
2660
+ <option value="0">Нет в наличии</option>
2661
+ </select>
2662
+ </div>
2663
 
2664
  {% if settings.use_barcodes and sys_mode not in ['external', 'light_external'] %}
2665
  <div class="form-group main-barcode-container">
 
2673
 
2674
  <div class="form-group main-price-container">
2675
  <label>Цена за ед.</label>
2676
+ <input type="number" name="price" placeholder="Цена" step="0.01" class="main-price-input" required>
2677
  </div>
2678
 
2679
  {% if settings.business_type != 'retail' %}
 
2704
  </div>
2705
  {% endif %}
2706
  </div>
2707
+
2708
+ {% if settings.business_type == 'wholesale' %}
2709
+ <div class="form-group main-tiers-container">
2710
+ <label>Оптовые цены (от кол-ва)</label>
2711
+ <div id="main-tiers-list-add-{{ loop.index }}"></div>
2712
+ <button type="button" class="btn btn-outline" style="padding: 5px; font-size:0.8rem; margin-top:5px;" onclick="addTierRow('main-tiers-list-add-{{ loop.index }}', 'main')"><i class="fas fa-plus"></i> Добавить оптовую цену</button>
2713
+ </div>
2714
+ {% endif %}
2715
 
2716
  <div class="variants-container" id="variants-container-add-{{ loop.index }}">
2717
  <div style="display:flex; justify-content:space-between; align-items:center;">
 
2740
  <div class="product-item" data-pid="{{ product.product_id }}">
2741
  <div class="product-info">
2742
  {% if product.photos and product.photos|length > 0 %}
2743
+ <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ product.photos[0] }}" class="product-img" style="{% if product.is_available == False %}filter: grayscale(100%);{% endif %}">
2744
  {% else %}
2745
  <div class="product-img" style="display:flex;align-items:center;justify-content:center;color:#ccc;"><i class="fas fa-image"></i></div>
2746
  {% endif %}
2747
  <div class="product-details">
2748
+ <span class="product-name" style="{% if product.is_available == False %}color:#b2bec3; text-decoration:line-through;{% endif %}">{{ product.name }}</span>
2749
  <div style="font-size:0.8rem; color:#b2bec3;">ID: {{ product.product_id }}</div>
2750
  {% if product.description %}
2751
  <span class="product-desc">{{ product.description[:50] }}{{ '...' if product.description|length > 50 else '' }}</span>
2752
  {% endif %}
2753
  <span class="product-meta">
2754
+ {% if product.is_available == False %}
2755
+ <strong style="color:var(--danger);">Нет в наличии</strong> •
2756
+ {% endif %}
2757
  {% if product.has_variant_prices %}
2758
  Цена по вариантам
2759
  {% else %}
 
2779
  </span>
2780
  </div>
2781
  </div>
2782
+ <div class="product-actions" style="align-items: center;">
2783
+ <form method="POST" style="margin:0;">
2784
+ <input type="hidden" name="action" value="move_product">
2785
+ <input type="hidden" name="direction" value="up">
2786
+ <input type="hidden" name="product_id" value="{{ product.product_id }}">
2787
+ <button type="submit" class="btn btn-outline" style="padding: 5px;"><i class="fas fa-arrow-up"></i></button>
2788
+ </form>
2789
+ <form method="POST" style="margin:0;">
2790
+ <input type="hidden" name="action" value="move_product">
2791
+ <input type="hidden" name="direction" value="down">
2792
+ <input type="hidden" name="product_id" value="{{ product.product_id }}">
2793
+ <button type="submit" class="btn btn-outline" style="padding: 5px; margin-right:10px;"><i class="fas fa-arrow-down"></i></button>
2794
+ </form>
2795
+
2796
  <button class="btn btn-warning" onclick="toggleEditProduct('edit-prod-{{ product.product_id }}')"><i class="fas fa-edit"></i></button>
2797
  <form method="POST" style="margin:0;" onsubmit="return confirm('Удалить товар?');">
2798
  <input type="hidden" name="action" value="delete_product">
 
2813
  <label>Название товара</label>
2814
  <input type="text" name="name" value="{{ product.name }}" required autocomplete="off">
2815
  </div>
2816
+
2817
+ <div class="form-group" style="flex:1;">
2818
+ <label>Наличие</label>
2819
+ <select name="is_available">
2820
+ <option value="1" {% if product.is_available != False %}selected{% endif %}>В наличии</option>
2821
+ <option value="0" {% if product.is_available == False %}selected{% endif %}>Нет в наличии</option>
2822
+ </select>
2823
+ </div>
2824
 
2825
  {% if settings.use_barcodes and sys_mode not in ['external', 'light_external'] %}
2826
  <div class="form-group main-barcode-container" {% if product.variants %}style="display:none;"{% endif %}>
 
2866
  {% endif %}
2867
  </div>
2868
 
2869
+ {% if settings.business_type == 'wholesale' %}
2870
+ <div class="form-group main-tiers-container" {% if product.has_variant_prices %}style="display:none;"{% endif %}>
2871
+ <label>Оптовые цены (от кол-ва)</label>
2872
+ <div id="main-tiers-list-edit-{{ product.product_id }}">
2873
+ {% for tier in product.wholesale_tiers %}
2874
+ <div style="display:flex; gap:5px; margin-top:5px;">
2875
+ <input type="number" name="main_tier_qty[]" value="{{ tier.qty }}" placeholder="От (шт)" required style="width:80px;">
2876
+ <input type="number" name="main_tier_price[]" value="{{ tier.price }}" placeholder="Цена" step="0.01" required>
2877
+ <button type="button" onclick="this.parentElement.remove()" style="color:red; border:none; background:none; font-size:1.2rem;">&times;</button>
2878
+ </div>
2879
+ {% endfor %}
2880
+ </div>
2881
+ <button type="button" class="btn btn-outline" style="padding: 5px; font-size:0.8rem; margin-top:5px;" onclick="addTierRow('main-tiers-list-edit-{{ product.product_id }}', 'main')"><i class="fas fa-plus"></i> Добавить оптовую цену</button>
2882
+ </div>
2883
+ {% endif %}
2884
+
2885
  <div class="variants-container" id="variants-container-edit-{{ product.product_id }}">
2886
  <div style="display:flex; justify-content:space-between; align-items:center;">
2887
  <label style="font-weight:600; font-size:0.9rem;"><i class="fas fa-tags"></i> Варианты товара</label>
 
2890
  <div id="variants-list-edit-{{ product.product_id }}">
2891
  {% for variant in product.variants %}
2892
  <div class="variant-row">
2893
+ <div style="display:flex; flex-direction:column; gap:5px; margin-right: 5px;">
2894
+ <button type="button" class="btn btn-outline" style="padding: 2px 5px;" onclick="moveVariant(this, 'up')"><i class="fas fa-arrow-up"></i></button>
2895
+ <button type="button" class="btn btn-outline" style="padding: 2px 5px;" onclick="moveVariant(this, 'down')"><i class="fas fa-arrow-down"></i></button>
2896
  </div>
2897
+ <div style="flex:1; display:flex; flex-wrap:wrap; gap:10px;">
2898
+ <div class="form-group">
2899
+ <label>Наличие</label>
2900
+ <select name="variant_is_available[]">
2901
+ <option value="1" {% if variant.is_available != False %}selected{% endif %}>Да</option>
2902
+ <option value="0" {% if variant.is_available == False %}selected{% endif %}>Нет</option>
2903
+ </select>
 
 
 
 
 
2904
  </div>
2905
+ <div class="form-group">
2906
+ <label>Название</label>
2907
+ <input type="text" name="variant_name[]" value="{{ variant.name }}" placeholder="Цвет, размер" required>
2908
+ </div>
2909
+ {% if settings.business_type != 'retail' %}
2910
+ <div class="form-group">
2911
+ <label>В уп. (шт)</label>
2912
+ <input type="number" name="variant_pieces_per_box[]" value="{{ variant.pieces_per_box }}" placeholder="Шт">
2913
+ </div>
2914
+ {% endif %}
2915
+ {% if settings.use_barcodes and sys_mode not in['external', 'light_external'] %}
2916
+ <div class="form-group">
2917
+ <label>Штрих-код</label>
2918
+ <div style="display:flex; gap:5px;">
2919
+ <input type="text" name="variant_barcode[]" value="{{ variant.barcode }}" placeholder="Код">
2920
+ <button type="button" class="btn btn-outline" style="padding: 12px;" onclick="startScanner(val => this.previousElementSibling.value = val)"><i class="fas fa-barcode"></i></button>
2921
+ </div>
2922
+ </div>
2923
+ {% endif %}
2924
+ <div class="form-group var-price-input" {% if not product.has_variant_prices %}style="display:none;"{% endif %}>
2925
+ <label>Цена</label>
2926
+ <input type="number" name="variant_price[]" value="{{ variant.price }}" placeholder="Цена за ед." step="0.01" {% if product.has_variant_prices %}required{% endif %}>
2927
+ </div>
2928
+ {% if settings.business_type == 'mixed' %}
2929
+ <div class="form-group var-price-input" {% if not product.has_variant_prices %}style="display:none;"{% endif %}>
2930
+ <label>Цена уп.</label>
2931
+ <input type="number" name="variant_box_price[]" value="{{ variant.box_price }}" placeholder="Опционально" step="0.01">
2932
+ </div>
2933
+ {% endif %}
2934
+ {% if settings.track_inventory and sys_mode not in ['external', 'light_external'] %}
2935
+ <div class="form-group">
2936
+ <label>Остаток</label>
2937
+ <input type="number" name="variant_stock[]" value="{{ variant.stock }}" placeholder="Остаток">
2938
+ </div>
2939
+ {% endif %}
2940
+
2941
+ {% if settings.business_type == 'wholesale' %}
2942
+ <div class="variant-tiers-container var-price-input" {% if not product.has_variant_prices %}style="display:none;"{% endif %} style="width:100%; margin-top:5px; padding:10px; background:#fafafa; border:1px solid #ddd; border-radius:6px;">
2943
+ <label style="font-size:0.8rem; font-weight:bold;">Оптовые цены варианта</label>
2944
+ <div id="var-tiers-list-edit-{{ product.product_id }}-{{ loop.index0 }}" class="var-tier-list">
2945
+ {% for tier in variant.wholesale_tiers %}
2946
+ <div style="display:flex; gap:5px; margin-top:5px;">
2947
+ <input type="number" name="variant_{{ loop.index0 }}_tier_qty[]" value="{{ tier.qty }}" placeholder="От (шт)" required style="width:80px;">
2948
+ <input type="number" name="variant_{{ loop.index0 }}_tier_price[]" value="{{ tier.price }}" placeholder="Цена" step="0.01" required>
2949
+ <button type="button" onclick="this.parentElement.remove()" style="color:red; border:none; background:none; font-size:1.2rem;">&times;</button>
2950
+ </div>
2951
+ {% endfor %}
2952
+ </div>
2953
+ <button type="button" class="btn btn-outline btn-add-var-tier" style="padding: 4px; font-size:0.75rem; margin-top:5px;" onclick="addTierRow('var-tiers-list-edit-{{ product.product_id }}-{{ loop.index0 }}', 'variant', {{ loop.index0 }})"><i class="fas fa-plus"></i> Добавить оптовую цену</button>
2954
+ </div>
2955
+ {% endif %}
2956
+
2957
  </div>
2958
+ <button type="button" class="remove-variant-btn" onclick="const row = this.closest('.variant-row'); const listId = row.parentNode.id; row.remove(); updateMainStockVisibility('edit-prod-{{ product.product_id }}'); updateVariantIndices(listId);"><i class="fas fa-times-circle"></i></button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2959
  </div>
2960
  {% endfor %}
2961
  </div>
 
3056
  document.getElementById(elId).style.display = 'none';
3057
  }
3058
 
3059
+ function addTierRow(containerId, type, varIndex = 0) {
3060
+ const container = document.getElementById(containerId);
3061
+ const div = document.createElement('div');
3062
+ div.style.display = 'flex'; div.style.gap = '5px'; div.style.marginTop = '5px';
3063
+ let namePrefix = type === 'main' ? 'main' : `variant_${varIndex}`;
3064
+ div.innerHTML = `
3065
+ <input type="number" name="${namePrefix}_tier_qty[]" placeholder="От (шт)" required style="width:80px;">
3066
+ <input type="number" name="${namePrefix}_tier_price[]" placeholder="Цена" step="0.01" required>
3067
+ <button type="button" onclick="this.parentElement.remove()" style="color:red; border:none; background:none; font-size:1.2rem;">&times;</button>
3068
+ `;
3069
+ container.appendChild(div);
3070
+ }
3071
+
3072
+ function updateVariantIndices(listId) {
3073
+ const list = document.getElementById(listId);
3074
+ if (!list) return;
3075
+ const rows = list.querySelectorAll(':scope > .variant-row');
3076
+ rows.forEach((row, index) => {
3077
+ row.querySelectorAll('input[name*="_tier_qty[]"]').forEach(inp => {
3078
+ inp.name = `variant_${index}_tier_qty[]`;
3079
+ });
3080
+ row.querySelectorAll('input[name*="_tier_price[]"]').forEach(inp => {
3081
+ inp.name = `variant_${index}_tier_price[]`;
3082
+ });
3083
+ const btn = row.querySelector('.btn-add-var-tier');
3084
+ if (btn) {
3085
+ const tierList = row.querySelector('.var-tier-list');
3086
+ if(tierList) {
3087
+ tierList.id = `var-tiers-list-${listId}-${index}`;
3088
+ btn.setAttribute('onclick', `addTierRow('${tierList.id}', 'variant', ${index})`);
3089
+ }
3090
+ }
3091
+ });
3092
+ }
3093
+
3094
+ function moveVariant(btn, direction) {
3095
+ const row = btn.closest('.variant-row');
3096
+ const list = row.parentNode;
3097
+ if (direction === 'up' && row.previousElementSibling) {
3098
+ list.insertBefore(row, row.previousElementSibling);
3099
+ } else if (direction === 'down' && row.nextElementSibling) {
3100
+ list.insertBefore(row.nextElementSibling, row);
3101
+ }
3102
+ updateVariantIndices(list.id);
3103
+ }
3104
+
3105
  function addVariantRow(containerId) {
3106
  const container = document.getElementById(containerId);
3107
  const formBlock = container.closest('form');
 
3115
  let reqAttr = hasVariantPrices ? 'required' : '';
3116
 
3117
  let html = `
3118
+ <div style="display:flex; flex-direction:column; gap:5px; margin-right: 5px;">
3119
+ <button type="button" class="btn btn-outline" style="padding: 2px 5px;" onclick="moveVariant(this, 'up')"><i class="fas fa-arrow-up"></i></button>
3120
+ <button type="button" class="btn btn-outline" style="padding: 2px 5px;" onclick="moveVariant(this, 'down')"><i class="fas fa-arrow-down"></i></button>
3121
+ </div>
3122
+ <div style="flex:1; display:flex; flex-wrap:wrap; gap:10px;">
3123
+ <div class="form-group">
3124
+ <label>Наличие</label>
3125
+ <select name="variant_is_available[]">
3126
+ <option value="1" selected>Да</option>
3127
+ <option value="0">Нет</option>
3128
+ </select>
3129
+ </div>
3130
  <div class="form-group">
3131
  <label>Название</label>
3132
  <input type="text" name="variant_name[]" placeholder="Цвет, размер" required>
 
3163
  html += `
3164
  <div class="form-group var-price-input" ${displayStyle}>
3165
  <label>Цена уп.</label>
3166
+ <input type="number" name="variant_box_price[]" placeholder="Опционально" step="0.01">
3167
  </div>`;
3168
  }
3169
 
 
3174
  <input type="number" name="variant_stock[]" placeholder="Остаток">
3175
  </div>`;
3176
  }
3177
+
3178
+ if (businessType === 'wholesale') {
3179
+ html += `
3180
+ <div class="variant-tiers-container var-price-input" ${displayStyle} style="width:100%; margin-top:5px; padding:10px; background:#fafafa; border:1px solid #ddd; border-radius:6px;">
3181
+ <label style="font-size:0.8rem; font-weight:bold;">Оптовые цены варианта</label>
3182
+ <div class="var-tier-list"></div>
3183
+ <button type="button" class="btn btn-outline btn-add-var-tier" style="padding: 4px; font-size:0.75rem; margin-top:5px;"><i class="fas fa-plus"></i> Добавить оптовую цену</button>
3184
+ </div>`;
3185
+ }
3186
 
3187
+ html += `</div><button type="button" class="remove-variant-btn" onclick="const row = this.closest('.variant-row'); const listId = row.parentNode.id; row.remove(); updateMainStockVisibility('${formId}'); updateVariantIndices(listId);"><i class="fas fa-times-circle"></i></button>`;
3188
 
3189
  div.innerHTML = html;
3190
  container.appendChild(div);
3191
+ updateVariantIndices(containerId);
3192
  updateMainStockVisibility(formId);
3193
  }
3194
 
 
3197
  const mainPriceContainer = form.querySelector('.main-price-container');
3198
  const mainPriceInput = form.querySelector('.main-price-input');
3199
  const mainBoxPriceContainer = form.querySelector('.main-box-price-container');
3200
+ const mainTiersContainer = form.querySelector('.main-tiers-container');
3201
  const varPriceInputs = form.querySelectorAll('.var-price-input');
3202
 
3203
  if (cb.checked) {
3204
  if(mainPriceContainer) mainPriceContainer.style.display = 'none';
3205
  if(mainPriceInput) mainPriceInput.removeAttribute('required');
3206
  if(mainBoxPriceContainer) mainBoxPriceContainer.style.display = 'none';
3207
+ if(mainTiersContainer) mainTiersContainer.style.display = 'none';
3208
 
3209
  varPriceInputs.forEach(input => {
3210
  input.style.display = 'flex';
 
3215
  if(mainPriceContainer) mainPriceContainer.style.display = 'flex';
3216
  if(mainPriceInput) mainPriceInput.setAttribute('required', 'required');
3217
  if(mainBoxPriceContainer) mainBoxPriceContainer.style.display = 'flex';
3218
+ if(mainTiersContainer) mainTiersContainer.style.display = 'block';
3219
 
3220
  varPriceInputs.forEach(input => {
3221
  input.style.display = 'none';
 
4243
  "variant_name": item.get('variant_name', ''),
4244
  "variant_idx": item.get('variant_idx', -1),
4245
  "discount": float(item.get('discount', 0)),
4246
+ "wholesale_tiers": item.get('wholesale_tiers', []),
4247
  "category": product_dict.get(item.get('product_id'), 'Без категории'),
4248
  "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+"
4249
  })
 
4662
  data['products'] = products
4663
  save_env_data(env_id, data)
4664
 
4665
+ elif action == 'move_category':
4666
+ cat_name = request.form.get('category_name')
4667
+ direction = request.form.get('direction')
4668
+ if cat_name in categories:
4669
+ idx = categories.index(cat_name)
4670
+ if direction == 'up' and idx > 0:
4671
+ categories[idx], categories[idx-1] = categories[idx-1], categories[idx]
4672
+ elif direction == 'down' and idx < len(categories) - 1:
4673
+ categories[idx], categories[idx+1] = categories[idx+1], categories[idx]
4674
+ data['categories'] = categories
4675
+ save_env_data(env_id, data)
4676
+
4677
  elif action == 'delete_category':
4678
  cat_name = request.form.get('category_name')
4679
  if cat_name in categories:
 
4687
  elif action == 'add_product':
4688
  name = request.form.get('name', '').strip()
4689
  barcode = request.form.get('barcode', '').strip()
4690
+ is_available = request.form.get('is_available', '1') == '1'
4691
+
4692
  price_str = request.form.get('price', '')
4693
  price = float(price_str) if price_str else ""
4694
 
 
4708
  category = request.form.get('category')
4709
  has_variant_prices = 'has_variant_prices' in request.form
4710
 
4711
+ main_wholesale_tiers = []
4712
+ if not has_variant_prices:
4713
+ mt_qtys = request.form.getlist('main_tier_qty[]')
4714
+ mt_prices = request.form.getlist('main_tier_price[]')
4715
+ for q, p_val in zip(mt_qtys, mt_prices):
4716
+ if q and p_val:
4717
+ main_wholesale_tiers.append({'qty': int(q), 'price': float(p_val)})
4718
+
4719
  variant_names = request.form.getlist('variant_name[]')
4720
  variant_barcodes = request.form.getlist('variant_barcode[]')
4721
  variant_prices = request.form.getlist('variant_price[]')
4722
  variant_box_prices = request.form.getlist('variant_box_price[]')
4723
  variant_stocks = request.form.getlist('variant_stock[]')
4724
  variant_ppbs = request.form.getlist('variant_pieces_per_box[]')
4725
+ variant_is_availables = request.form.getlist('variant_is_available[]')
4726
+ variants = []
4727
 
4728
  for i in range(len(variant_names)):
4729
  v_name = variant_names[i].strip()
4730
  if v_name:
4731
  v_price = price
4732
  v_box_price = box_price
4733
+ v_is_avail = True
4734
+ if i < len(variant_is_availables):
4735
+ v_is_avail = variant_is_availables[i] == '1'
4736
+
4737
+ v_wholesale_tiers = []
4738
  if has_variant_prices:
4739
  if i < len(variant_prices) and variant_prices[i]:
4740
  v_price = float(variant_prices[i])
4741
  if i < len(variant_box_prices) and variant_box_prices[i]:
4742
  v_box_price = float(variant_box_prices[i])
4743
 
4744
+ vt_qtys = request.form.getlist(f'variant_{i}_tier_qty[]')
4745
+ vt_prices = request.form.getlist(f'variant_{i}_tier_price[]')
4746
+ for q, p_val in zip(vt_qtys, vt_prices):
4747
+ if q and p_val:
4748
+ v_wholesale_tiers.append({'qty': int(q), 'price': float(p_val)})
4749
+
4750
  v_stock = ""
4751
  if i < len(variant_stocks) and variant_stocks[i]:
4752
  v_stock = int(variant_stocks[i])
 
4765
  "price": v_price,
4766
  "box_price": v_box_price,
4767
  "stock": v_stock,
4768
+ "pieces_per_box": v_ppb,
4769
+ "is_available": v_is_avail,
4770
+ "wholesale_tiers": v_wholesale_tiers
4771
  })
4772
 
4773
  uploaded_photos = request.files.getlist('photos')[:10]
 
4813
  'category': category,
4814
  'photos': photos_list,
4815
  'variants': variants,
4816
+ 'has_variant_prices': has_variant_prices,
4817
+ 'is_available': is_available,
4818
+ 'wholesale_tiers': main_wholesale_tiers
4819
  }
4820
  products.append(new_product)
4821
  data['products'] = products
 
4825
  pid = request.form.get('product_id')
4826
  name = request.form.get('name', '').strip()
4827
  barcode = request.form.get('barcode', '').strip()
4828
+ is_available = request.form.get('is_available', '1') == '1'
4829
 
4830
  price_str = request.form.get('price', '')
4831
  price = float(price_str) if price_str else ""
 
4845
  description = request.form.get('description', '').strip()
4846
  has_variant_prices = 'has_variant_prices' in request.form
4847
 
4848
+ main_wholesale_tiers = []
4849
+ if not has_variant_prices:
4850
+ mt_qtys = request.form.getlist('main_tier_qty[]')
4851
+ mt_prices = request.form.getlist('main_tier_price[]')
4852
+ for q, p_val in zip(mt_qtys, mt_prices):
4853
+ if q and p_val:
4854
+ main_wholesale_tiers.append({'qty': int(q), 'price': float(p_val)})
4855
+
4856
  remove_photos = request.form.getlist('remove_photos[]')
4857
 
4858
  variant_names = request.form.getlist('variant_name[]')
 
4861
  variant_box_prices = request.form.getlist('variant_box_price[]')
4862
  variant_stocks = request.form.getlist('variant_stock[]')
4863
  variant_ppbs = request.form.getlist('variant_pieces_per_box[]')
4864
+ variant_is_availables = request.form.getlist('variant_is_available[]')
4865
+ variants = []
4866
 
4867
  for i in range(len(variant_names)):
4868
  v_name = variant_names[i].strip()
4869
  if v_name:
4870
  v_price = price
4871
  v_box_price = box_price
4872
+ v_is_avail = True
4873
+ if i < len(variant_is_availables):
4874
+ v_is_avail = variant_is_availables[i] == '1'
4875
+
4876
+ v_wholesale_tiers = []
4877
  if has_variant_prices:
4878
  if i < len(variant_prices) and variant_prices[i]:
4879
  v_price = float(variant_prices[i])
4880
  if i < len(variant_box_prices) and variant_box_prices[i]:
4881
  v_box_price = float(variant_box_prices[i])
4882
+
4883
+ vt_qtys = request.form.getlist(f'variant_{i}_tier_qty[]')
4884
+ vt_prices = request.form.getlist(f'variant_{i}_tier_price[]')
4885
+ for q, p_val in zip(vt_qtys, vt_prices):
4886
+ if q and p_val:
4887
+ v_wholesale_tiers.append({'qty': int(q), 'price': float(p_val)})
4888
+
4889
  v_stock = ""
4890
  if i < len(variant_stocks) and variant_stocks[i]:
4891
  v_stock = int(variant_stocks[i])
 
4901
  "price": v_price,
4902
  "box_price": v_box_price,
4903
  "stock": v_stock,
4904
+ "pieces_per_box": v_ppb,
4905
+ "is_available": v_is_avail,
4906
+ "wholesale_tiers": v_wholesale_tiers
4907
  })
4908
 
4909
  uploaded_photos = request.files.getlist('photos')[:10]
 
4948
  p['description'] = description
4949
  p['variants'] = variants
4950
  p['has_variant_prices'] = has_variant_prices
4951
+ p['is_available'] = is_available
4952
+ p['wholesale_tiers'] = main_wholesale_tiers
4953
 
4954
  existing_photos = p.get('photos', [])
4955
  existing_photos =[ph for ph in existing_photos if ph not in remove_photos]
 
4958
  data['products'] = products
4959
  save_env_data(env_id, data)
4960
 
4961
+ elif action == 'move_product':
4962
+ pid = request.form.get('product_id')
4963
+ direction = request.form.get('direction')
4964
+
4965
+ idx = -1
4966
+ for i, p in enumerate(products):
4967
+ if p.get('product_id') == pid:
4968
+ idx = i
4969
+ break
4970
+
4971
+ if idx != -1:
4972
+ cat = products[idx].get('category')
4973
+ if direction == 'up':
4974
+ swap_idx = -1
4975
+ for i in range(idx - 1, -1, -1):
4976
+ if products[i].get('category') == cat:
4977
+ swap_idx = i
4978
+ break
4979
+ if swap_idx != -1:
4980
+ products[idx], products[swap_idx] = products[swap_idx], products[idx]
4981
+ elif direction == 'down':
4982
+ swap_idx = -1
4983
+ for i in range(idx + 1, len(products)):
4984
+ if products[i].get('category') == cat:
4985
+ swap_idx = i
4986
+ break
4987
+ if swap_idx != -1:
4988
+ products[idx], products[swap_idx] = products[swap_idx], products[idx]
4989
+
4990
+ data['products'] = products
4991
+ save_env_data(env_id, data)
4992
+
4993
  elif action == 'delete_product':
4994
  pid = request.form.get('product_id')
4995
  data['products'] =[p for p in products if p.get('product_id') != pid]