diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -125,33 +125,6 @@ def load_data(): else: data = {} - if 'products' in data or 'categories' in data: - data = { - 'default_env': { - 'products': data.get('products', []), - 'categories': data.get('categories', []), - 'orders': data.get('orders', {}), - 'staff': [], - 'settings': { - 'organization_name': 'Default Shop', - 'admin_password_enabled': False, - 'admin_password': '', - 'logo_url': DEFAULT_LOGO_URL, - 'whatsapp_number': DEFAULT_WHATSAPP_NUMBER, - 'track_inventory': False, - 'business_type': 'retail', - 'customer_fields': { - 'name': True, 'phone': True, 'city': True, 'address': False, 'zip': False - }, - 'socials': { - 'wa': {'enabled': True, 'url': 'https://wa.me/77011333885'}, - 'ig': {'enabled': True, 'url': 'https://instagram.com/14sklad_baisat'}, - 'tg': {'enabled': True, 'url': 'https://t.me/posuda15konteiner'} - } - } - } - } - changed = False for env_id, env_data in data.items(): if 'products' not in env_data: env_data['products'] = [] @@ -163,44 +136,55 @@ def load_data(): changed = True settings = env_data['settings'] - if 'organization_name' not in settings: settings['organization_name'] = f'Shop {env_id}'; changed = True - if 'admin_password_enabled' not in settings: settings['admin_password_enabled'] = False; changed = True - if 'admin_password' not in settings: settings['admin_password'] = ''; changed = True - if 'logo_url' not in settings: settings['logo_url'] = DEFAULT_LOGO_URL; changed = True - if 'whatsapp_number' not in settings: settings['whatsapp_number'] = DEFAULT_WHATSAPP_NUMBER; changed = True - if 'track_inventory' not in settings: settings['track_inventory'] = False; changed = True - if 'business_type' not in settings: settings['business_type'] = 'retail'; changed = True - if 'customer_fields' not in settings: - settings['customer_fields'] = {'name': True, 'phone': True, 'city': True, 'address': False, 'zip': False} - changed = True - if 'socials' not in settings: - settings['socials'] = { - 'wa': {'enabled': True, 'url': 'https://wa.me/77011333885'}, - 'ig': {'enabled': True, 'url': 'https://instagram.com/14sklad_baisat'}, - 'tg': {'enabled': True, 'url': 'https://t.me/posuda15konteiner'} + defaults = { + 'organization_name': f'Shop {env_id}', + 'admin_password_enabled': False, + 'admin_password': '', + 'logo_url': DEFAULT_LOGO_URL, + 'whatsapp_number': DEFAULT_WHATSAPP_NUMBER, + 'track_inventory': False, + 'business_type': 'оптово-розничный', + 'customer_fields': {'name': True, 'phone': True, 'city': True, 'address': False, 'zip': False}, + 'socials': { + 'wa': {'enabled': True, 'url': ''}, + 'ig': {'enabled': True, 'url': ''}, + 'tg': {'enabled': True, 'url': ''} } - changed = True + } + for key, value in defaults.items(): + if key not in settings: + settings[key] = value + changed = True for product in env_data['products']: - if 'product_id' not in product: product['product_id'] = uuid4().hex; changed = True - if 'pieces_per_box' not in product: product['pieces_per_box'] = 1; changed = True - if 'min_qty' not in product: product['min_qty'] = 1; changed = True - if 'box_price' not in product: product['box_price'] = 0; changed = True - if 'variants' not in product: product['variants'] = []; changed = True - if 'has_variant_prices' not in product: product['has_variant_prices'] = False; changed = True - if 'stock' not in product: product['stock'] = 0; changed = True - for v in product['variants']: - if 'stock' not in v: v['stock'] = 0; changed = True - if 'box_price' not in v: v['box_price'] = 0; changed = True + p_defaults = { + 'product_id': uuid4().hex, + 'pieces_per_box': 1, + 'variants': [], + 'has_variant_prices': False, + 'stock': 0, + 'min_order_qty': 1 + } + for key, value in p_defaults.items(): + if key not in product: + product[key] = value + changed = True + for v in product.get('variants', []): + if 'stock' not in v: + v['stock'] = 0 + changed = True for order_id, order in env_data['orders'].items(): - if 'status' not in order: order['status'] = 'confirmed'; changed = True - if 'staff_name' not in order: order['staff_name'] = ''; changed = True + o_defaults = {'status': 'confirmed', 'staff_name': ''} + for key, value in o_defaults.items(): + if key not in order: + order[key] = value + changed = True if changed or not os.path.exists(DATA_FILE): try: with open(DATA_FILE, 'w', encoding='utf-8') as f: - json.dump(data, f) + json.dump(data, f, ensure_ascii=False, indent=4) except Exception: pass @@ -231,7 +215,7 @@ def get_env_data(env_id): 'logo_url': DEFAULT_LOGO_URL, 'whatsapp_number': DEFAULT_WHATSAPP_NUMBER, 'track_inventory': False, - 'business_type': 'retail', + 'business_type': 'оптово-розничный', 'customer_fields': {'name': True, 'phone': True, 'city': True, 'address': False, 'zip': False}, 'socials': { 'wa': {'enabled': True, 'url': ''}, @@ -435,7 +419,7 @@ CATALOG_TEMPLATE = ''' .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; } .product-desc { font-size: 0.8rem; color: var(--text-muted); display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } .product-box-info { font-size: 0.8rem; color: #00b894; font-weight: 600; } - .product-wholesale-info { font-size: 0.8rem; color: #e17055; font-weight: 600; } + .product-min-order { font-size: 0.8rem; color: #e17055; font-weight: 600; } .product-bottom { display: flex; align-items: center; justify-content: space-between; width: 100%; margin-top: 5px; flex-wrap: wrap; gap: 10px; } .product-price { font-weight: 700; font-size: 1rem; color: var(--primary); } @@ -586,8 +570,8 @@ CATALOG_TEMPLATE = '''
{% if mode == 'pos' %} - - + + {% else %} {% if settings.customer_fields.name %} {% endif %} {% if settings.customer_fields.phone %} {% endif %} @@ -632,8 +616,8 @@ CATALOG_TEMPLATE = ''' const mode = '{{ mode }}'; const staffId = '{{ staff_id }}'; const trackInventory = {{ 'true' if settings.track_inventory else 'false' }}; + const businessType = '{{ settings.business_type }}'; const cFields = {{ settings.customer_fields|tojson }}; - const bType = '{{ settings.business_type }}'; let cart = {}; let currentGalleryPhotos = []; @@ -707,25 +691,8 @@ CATALOG_TEMPLATE = ''' } } - function formatQtyText(qty, ppb) { - if (bType === 'retail') return `${qty} шт.`; - ppb = parseInt(ppb) || 1; - if (ppb > 1 && qty >= ppb) { - let boxes = Math.floor(qty / ppb); - let remainder = qty % ppb; - return `${boxes} кор.` + (remainder > 0 ? ` ${remainder} шт.` : ''); - } - return `${qty} шт.`; - } - function renderProductCard(p, container) { - const isRetail = bType === 'retail'; - const isWholesaleRetail = bType === 'wholesale_retail'; - const isWholesale = bType === 'wholesale'; - const ppb = parseInt(p.pieces_per_box) || 1; - const minQty = parseInt(p.min_qty) || 1; - const hasPhotos = p.photos && p.photos.length > 0; const photoUrl = hasPhotos ? `https://huggingface.co/datasets/${repoId}/resolve/main/photos/${p.photos[0]}` @@ -734,12 +701,13 @@ CATALOG_TEMPLATE = ''' const photoIndicator = hasPhotos && p.photos.length > 1 ? `
${p.photos.length}
` : ''; const imgClick = hasPhotos ? `onclick="openGallery('${p.product_id}')"` : ''; const descHtml = p.description ? `
${p.description}
` : ''; - let boxInfoHtml = ''; - if (isWholesaleRetail && ppb > 1) { + if (businessType !== 'розница' && ppb > 1) { boxInfoHtml = `
В коробке: ${ppb} шт
`; - } else if (isWholesale && minQty > 1) { - boxInfoHtml = `
Мин. заказ: ${minQty} шт
`; + } + let minOrderHtml = ''; + if (businessType === 'оптовый' && p.min_order_qty > 1) { + minOrderHtml = `
Мин. заказ: ${p.min_order_qty} шт
`; } let variantsHtml = ''; @@ -749,33 +717,25 @@ CATALOG_TEMPLATE = ''' variantsHtml = `
`; p.variants.forEach((v, idx) => { let vPrice = p.has_variant_prices ? v.price : p.price; - let vBoxPrice = v.box_price || (vPrice * ppb); - - let priceDisplay = `${vPrice} ${currency}`; - if (isWholesaleRetail && ppb > 1) { - priceDisplay = `${vPrice} ${currency}/шт
Коробка: ${vBoxPrice} ${currency}`; + let boxPriceHtml = ''; + if (businessType === 'оптово-розничный' && ppb > 1) { + boxPriceHtml = ` / ${(vPrice * ppb).toFixed(0)} ${currency} за коробку`; } - let vStockHtml = trackInventory ? `
Остаток: ${v.stock || 0} шт
` : ''; let cKey = getCartKey(p.product_id, idx); let qty = cart[cKey] ? cart[cKey].quantity : 0; - let addBoxBtn = (isWholesaleRetail && ppb > 1) ? `` : ''; - variantsHtml += `
${v.name} - ${priceDisplay} + ${vPrice} ${currency}${boxPriceHtml} ${vStockHtml}
-
- ${addBoxBtn} -
- - - -
+
+ + +
`; @@ -784,19 +744,17 @@ CATALOG_TEMPLATE = ''' } else { let mStockHtml = trackInventory ? `
Остаток: ${p.stock || 0} шт
` : ''; let qty = cart[p.product_id] ? cart[p.product_id].quantity : 0; - - let bPrice = p.box_price || (p.price * ppb); - let priceDisplay = `${p.price} ${currency}`; - if (isWholesaleRetail && ppb > 1) { - priceDisplay = `${p.price} ${currency}/шт
Коробка: ${bPrice} ${currency}`; + let addBoxBtn = ''; + let boxPriceText = ''; + if (businessType === 'оптово-розничный' && ppb > 1) { + addBoxBtn = ``; + boxPriceText = ` / ${(p.price * ppb).toFixed(0)} ${currency} за коробку`; } - - let addBoxBtn = (isWholesaleRetail && ppb > 1) ? `` : ''; mainControlsHtml = `
-
${priceDisplay}
+
${p.price} ${currency}${boxPriceText}
${mStockHtml}
@@ -823,6 +781,7 @@ CATALOG_TEMPLATE = '''
${p.name}
${descHtml} ${boxInfoHtml} + ${minOrderHtml}
${variantsHtml} @@ -859,36 +818,27 @@ CATALOG_TEMPLATE = ''' varIdx = parseInt(cKey.split('___')[1]); } - let currentQty = cart[cKey] ? cart[cKey].quantity : 0; - let newQty = exactValue !== null ? exactValue : currentQty + change; - - if (bType === 'wholesale') { - let minQ = parseInt(p.min_qty) || 1; - if (change > 0 && currentQty === 0 && newQty < minQ) { - newQty = minQ; - } else if (change < 0 && newQty > 0 && newQty < minQ) { - newQty = 0; - } - } - - if (!cart[cKey] && newQty > 0) { + if (!cart[cKey]) { let price = p.price; - let bPrice = p.box_price || (price * (p.pieces_per_box || 1)); let vName = ""; if (varIdx !== -1 && p.variants[varIdx]) { if (p.has_variant_prices) price = p.variants[varIdx].price; - bPrice = p.variants[varIdx].box_price || (price * (p.pieces_per_box || 1)); vName = p.variants[varIdx].name; } - cart[cKey] = { ...p, quantity: 0, cart_price: price, box_price: bPrice, variant_name: vName, variant_idx: varIdx }; + cart[cKey] = { ...p, quantity: 0, cart_price: price, variant_name: vName, variant_idx: varIdx }; + } + + if (exactValue !== null) { + cart[cKey].quantity = exactValue; + } else { + cart[cKey].quantity += change; } - if (newQty <= 0) { + if (cart[cKey].quantity <= 0) { delete cart[cKey]; const input = document.getElementById(`qty-${cKey}`); if(input) input.value = 0; } else { - cart[cKey].quantity = newQty; const input = document.getElementById(`qty-${cKey}`); if(input) input.value = cart[cKey].quantity; } @@ -913,14 +863,7 @@ CATALOG_TEMPLATE = ''' function updateCartUI() { let total = 0; for (let cKey in cart) { - const item = cart[cKey]; - if (bType === 'wholesale_retail' && item.pieces_per_box > 1) { - let boxes = Math.floor(item.quantity / item.pieces_per_box); - let units = item.quantity % item.pieces_per_box; - total += (boxes * item.box_price) + (units * item.cart_price); - } else { - total += item.cart_price * item.quantity; - } + total += cart[cKey].cart_price * cart[cKey].quantity; } const cartBar = document.getElementById('cartBar'); @@ -943,8 +886,6 @@ CATALOG_TEMPLATE = ''' for (let cKey in cart) { const item = cart[cKey]; - const ppb = parseInt(item.pieces_per_box) || 1; - const formattedQty = formatQtyText(item.quantity, ppb); const pId = item.product_id; let nameDisplay = item.name; @@ -952,20 +893,10 @@ CATALOG_TEMPLATE = ''' nameDisplay += `
(${item.variant_name})
`; } - let lineTotal = 0; - if (bType === 'wholesale_retail' && ppb > 1) { - let boxes = Math.floor(item.quantity / ppb); - let units = item.quantity % ppb; - lineTotal = (boxes * item.box_price) + (units * item.cart_price); - } else { - lineTotal = item.cart_price * item.quantity; - } - list.innerHTML += `
${nameDisplay} -
${formattedQty}
@@ -975,7 +906,7 @@ CATALOG_TEMPLATE = '''
-
${lineTotal} ${currency}
+
${item.cart_price * item.quantity} ${currency}
`; } @@ -1008,40 +939,31 @@ CATALOG_TEMPLATE = ''' let orderData = { cart: cartArray, mode: mode, staff_id: staffId }; if (mode === 'pos') { - const nameEl = document.getElementById('custNamePos'); - if(!nameEl.value.trim()) { - alert('Укажите имя клиента!'); - return; - } - orderData.customer_name = nameEl.value.trim(); + const nameEl = document.getElementById('custName'); const waEl = document.getElementById('custWhatsapp'); + orderData.customer_name = nameEl ? nameEl.value.trim() : ''; orderData.customer_whatsapp = waEl ? waEl.value.trim() : ''; } else { let fail = false; if(cFields.name) { const el = document.getElementById('custName'); - if(!el.value.trim()) fail = true; - orderData.customer_name = el.value.trim(); + if(!el || !el.value.trim()) fail = true; else orderData.customer_name = el.value.trim(); } if(cFields.phone) { const el = document.getElementById('custPhone'); - if(!el.value.trim()) fail = true; - orderData.customer_phone = el.value.trim(); + if(!el || !el.value.trim()) fail = true; else orderData.customer_phone = el.value.trim(); } if(cFields.city) { const el = document.getElementById('custCity'); - if(!el.value.trim()) fail = true; - orderData.customer_city = el.value.trim(); + if(!el || !el.value.trim()) fail = true; else orderData.customer_city = el.value.trim(); } if(cFields.address) { const el = document.getElementById('custAddress'); - if(!el.value.trim()) fail = true; - orderData.customer_address = el.value.trim(); + if(!el || !el.value.trim()) fail = true; else orderData.customer_address = el.value.trim(); } if(cFields.zip) { const el = document.getElementById('custZip'); - if(!el.value.trim()) fail = true; - orderData.customer_zip = el.value.trim(); + if(!el || !el.value.trim()) fail = true; else orderData.customer_zip = el.value.trim(); } if(fail) { alert('Пожалуйста, заполните все обязательные поля.'); @@ -1192,29 +1114,13 @@ CATALOG_TEMPLATE = ''' } } - function nextPhoto(e) { - if(e) e.stopPropagation(); - if(currentGalleryPhotos.length <= 1) return; - currentGalleryIndex = (currentGalleryIndex + 1) % currentGalleryPhotos.length; - updateGalleryView(); - } + function nextPhoto(e) { if(e) e.stopPropagation(); if(currentGalleryPhotos.length <= 1) return; currentGalleryIndex = (currentGalleryIndex + 1) % currentGalleryPhotos.length; updateGalleryView(); } + function prevPhoto(e) { if(e) e.stopPropagation(); if(currentGalleryPhotos.length <= 1) return; currentGalleryIndex = (currentGalleryIndex - 1 + currentGalleryPhotos.length) % currentGalleryPhotos.length; updateGalleryView(); } - function prevPhoto(e) { - if(e) e.stopPropagation(); - if(currentGalleryPhotos.length <= 1) return; - currentGalleryIndex = (currentGalleryIndex - 1 + currentGalleryPhotos.length) % currentGalleryPhotos.length; - updateGalleryView(); - } - - let touchstartX = 0; - let touchendX = 0; + let touchstartX = 0, touchendX = 0; const swipeArea = document.getElementById('gallerySwipeArea'); swipeArea.addEventListener('touchstart', e => { touchstartX = e.changedTouches[0].screenX; }); - swipeArea.addEventListener('touchend', e => { - touchendX = e.changedTouches[0].screenX; - if (touchstartX - touchendX > 50) nextPhoto(); - if (touchendX - touchstartX > 50) prevPhoto(); - }); + swipeArea.addEventListener('touchend', e => { touchendX = e.changedTouches[0].screenX; if (touchstartX - touchendX > 50) nextPhoto(); if (touchendX - touchstartX > 50) prevPhoto(); }); init(); @@ -1353,13 +1259,6 @@ ORDER_TEMPLATE = ''' {% set ppb = item.pieces_per_box|default(1)|int %} {% set boxes = item.quantity // ppb %} {% set remainder = item.quantity % ppb %} - - {% set line_total = item.price * item.quantity %} - {% if settings.business_type == 'wholesale_retail' and ppb > 1 %} - {% set b_price = item.box_price if item.box_price else (item.price * ppb) %} - {% set line_total = (boxes * b_price) + (remainder * item.price) %} - {% endif %} - {% if item.quantity > 0 %} {{ loop.index }} @@ -1384,7 +1283,7 @@ ORDER_TEMPLATE = '''
{% endif %}
- {% if settings.business_type != 'retail' and ppb > 1 and boxes > 0 %} + {% if ppb > 1 and boxes > 0 %} {{ boxes }} кор.{% if remainder > 0 %} {{ remainder }} шт.{% endif %} {% else %} {{ item.quantity }} шт. @@ -1393,7 +1292,7 @@ ORDER_TEMPLATE = '''
{{ item.price }} - {{ line_total }} + {{ item.price * item.quantity }} {% endif %} {% endfor %} @@ -1424,7 +1323,6 @@ ORDER_TEMPLATE = '''