Update app.py
Browse files
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 (
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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-
|
| 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 |
-
<
|
| 2531 |
-
<
|
| 2532 |
-
|
| 2533 |
-
|
| 2534 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 2757 |
-
<
|
| 2758 |
-
<
|
| 2759 |
</div>
|
| 2760 |
-
|
| 2761 |
-
|
| 2762 |
-
|
| 2763 |
-
|
| 2764 |
-
|
| 2765 |
-
|
| 2766 |
-
|
| 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 |
-
{
|
| 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 |
-
|
| 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.
|
| 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 |
-
|
|
|
|
| 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 |
-
|
|
|
|
| 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;">×</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;">×</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;">×</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]
|