Update app.py
Browse files
app.py
CHANGED
|
@@ -132,6 +132,8 @@ def load_data():
|
|
| 132 |
for product in data['products']:
|
| 133 |
if 'product_id' not in product:
|
| 134 |
product['product_id'] = uuid4().hex
|
|
|
|
|
|
|
| 135 |
|
| 136 |
if not os.path.exists(DATA_FILE):
|
| 137 |
try:
|
|
@@ -195,12 +197,16 @@ CATALOG_TEMPLATE = '''
|
|
| 195 |
.product-info { flex-grow: 1; display: flex; flex-direction: column; justify-content: space-between; min-width: 0; padding: 5px 0; }
|
| 196 |
.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; }
|
| 197 |
.product-desc { font-size: 0.8rem; color: var(--text-muted); margin-top: 4px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
| 198 |
-
.product-
|
|
|
|
| 199 |
.product-price { font-weight: 700; font-size: 1rem; color: var(--primary); }
|
|
|
|
| 200 |
.quantity-control { display: flex; align-items: center; background: var(--bg); border-radius: 8px; overflow: hidden; border: 1px solid var(--border); }
|
| 201 |
.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; }
|
| 202 |
.quantity-control button:active { background: #e0e0e0; }
|
| 203 |
.quantity-control input { width: 36px; height: 32px; border: none; text-align: center; background: transparent; font-weight: 600; font-size: 0.95rem; pointer-events: none; color: var(--primary); }
|
|
|
|
|
|
|
| 204 |
|
| 205 |
.cart-bar { position: fixed; bottom: 0; left: 0; width: 100%; background: var(--surface); box-shadow: 0 -4px 20px rgba(0,0,0,0.06); padding: 15px 20px calc(15px + env(safe-area-inset-bottom)); display: none; justify-content: space-between; align-items: center; z-index: 100; border-top-left-radius: 20px; border-top-right-radius: 20px; }
|
| 206 |
.cart-info { display: flex; flex-direction: column; }
|
|
@@ -221,9 +227,15 @@ CATALOG_TEMPLATE = '''
|
|
| 221 |
.customer-form input:focus { border-color: var(--primary); background: var(--surface); }
|
| 222 |
|
| 223 |
.cart-item-list { display: flex; flex-direction: column; gap: 15px; margin-bottom: 25px; }
|
| 224 |
-
.cart-item { display: flex; justify-content: space-between; align-items: center; background: var(--bg); padding: 15px; border-radius: 12px; }
|
| 225 |
-
.cart-item-name { flex
|
| 226 |
-
.cart-item-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 227 |
.confirm-btn { background: var(--accent); color: #fff; width: 100%; padding: 16px; border: none; border-radius: 14px; font-size: 1.1rem; font-weight: 700; cursor: pointer; box-shadow: 0 4px 15px rgba(37,211,102,0.3); }
|
| 228 |
|
| 229 |
.gallery-modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: #000; z-index: 300; justify-content: center; align-items: center; flex-direction: column; }
|
|
@@ -391,8 +403,19 @@ CATALOG_TEMPLATE = '''
|
|
| 391 |
}
|
| 392 |
}
|
| 393 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 394 |
function renderProductCard(p, container) {
|
| 395 |
const qty = cart[p.product_id] ? cart[p.product_id].quantity : 0;
|
|
|
|
| 396 |
const hasPhotos = p.photos && p.photos.length > 0;
|
| 397 |
const photoUrl = hasPhotos
|
| 398 |
? `https://huggingface.co/datasets/${repoId}/resolve/main/photos/${p.photos[0]}`
|
|
@@ -401,6 +424,8 @@ CATALOG_TEMPLATE = '''
|
|
| 401 |
const photoIndicator = hasPhotos && p.photos.length > 1 ? `<div class="photo-count"><i class="fas fa-images"></i> ${p.photos.length}</div>` : '';
|
| 402 |
const imgClick = hasPhotos ? `onclick="openGallery('${p.product_id}')"` : '';
|
| 403 |
const descHtml = p.description ? `<div class="product-desc">${p.description}</div>` : '';
|
|
|
|
|
|
|
| 404 |
|
| 405 |
const div = document.createElement('div');
|
| 406 |
div.className = 'product-card';
|
|
@@ -413,13 +438,17 @@ CATALOG_TEMPLATE = '''
|
|
| 413 |
<div>
|
| 414 |
<div class="product-title">${p.name}</div>
|
| 415 |
${descHtml}
|
|
|
|
| 416 |
</div>
|
| 417 |
<div class="product-bottom">
|
| 418 |
<div class="product-price">${p.price} ${currency}</div>
|
| 419 |
-
<div class="
|
| 420 |
-
|
| 421 |
-
<
|
| 422 |
-
|
|
|
|
|
|
|
|
|
|
| 423 |
</div>
|
| 424 |
</div>
|
| 425 |
</div>
|
|
@@ -444,7 +473,7 @@ CATALOG_TEMPLATE = '''
|
|
| 444 |
}
|
| 445 |
}
|
| 446 |
|
| 447 |
-
function updateCart(productId, change) {
|
| 448 |
const product = products.find(p => p.product_id === productId);
|
| 449 |
if (!product) return;
|
| 450 |
|
|
@@ -452,13 +481,19 @@ CATALOG_TEMPLATE = '''
|
|
| 452 |
cart[productId] = { ...product, quantity: 0 };
|
| 453 |
}
|
| 454 |
|
| 455 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 456 |
|
| 457 |
if (cart[productId].quantity <= 0) {
|
| 458 |
delete cart[productId];
|
| 459 |
-
document.getElementById(`qty-${productId}`)
|
|
|
|
| 460 |
} else {
|
| 461 |
-
document.getElementById(`qty-${productId}`)
|
|
|
|
| 462 |
}
|
| 463 |
|
| 464 |
updateCartUI();
|
|
@@ -478,21 +513,43 @@ CATALOG_TEMPLATE = '''
|
|
| 478 |
cartBar.style.display = 'none';
|
| 479 |
closeCartModal();
|
| 480 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 481 |
}
|
| 482 |
|
| 483 |
-
function
|
| 484 |
const list = document.getElementById('cartItemList');
|
| 485 |
list.innerHTML = '';
|
| 486 |
|
| 487 |
for (let id in cart) {
|
| 488 |
const item = cart[id];
|
|
|
|
|
|
|
|
|
|
| 489 |
list.innerHTML += `
|
| 490 |
<div class="cart-item">
|
| 491 |
-
<div class="cart-item-name">
|
| 492 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 493 |
</div>
|
| 494 |
`;
|
| 495 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 496 |
const modal = document.getElementById('cartModal');
|
| 497 |
modal.style.display = 'flex';
|
| 498 |
setTimeout(() => modal.classList.add('active'), 10);
|
|
@@ -617,7 +674,7 @@ ORDER_TEMPLATE = '''
|
|
| 617 |
<title>Накладная №{{ order.id }}</title>
|
| 618 |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
| 619 |
<style>
|
| 620 |
-
:root { --bg: #f4f6f8; --surface: #ffffff; --text: #2d3436; --border: #dfe6e9; --wa: #25D366; --print: #1e272e; }
|
| 621 |
* { box-sizing: border-box; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }
|
| 622 |
body { margin: 0; padding: max(20px, env(safe-area-inset-top)) 20px calc(100px + env(safe-area-inset-bottom)); background: var(--bg); display: flex; flex-direction: column; align-items: center; color: var(--text); }
|
| 623 |
.invoice-box { background: var(--surface); width: 100%; max-width: 900px; padding: 30px; box-shadow: 0 4px 20px rgba(0,0,0,0.05); border-radius: 16px; }
|
|
@@ -628,13 +685,20 @@ ORDER_TEMPLATE = '''
|
|
| 628 |
.customer-details span { font-weight: 600; color: #1a1a1a; }
|
| 629 |
|
| 630 |
.table-responsive { width: 100%; overflow-x: auto; -webkit-overflow-scrolling: touch; margin-bottom: 20px; border-radius: 8px; border: 1px solid var(--border); }
|
| 631 |
-
table { width: 100%; border-collapse: collapse; min-width:
|
| 632 |
th, td { border-bottom: 1px solid var(--border); padding: 12px; text-align: center; font-size: 0.95rem; }
|
| 633 |
th { background: #fafafa; font-weight: 700; color: #636e72; text-transform: uppercase; font-size: 0.8rem; letter-spacing: 0.5px; }
|
| 634 |
.img-cell img { width: 45px; height: 45px; object-fit: cover; border-radius: 8px; border: 1px solid #eee; }
|
| 635 |
.total-row { background: #fafafa; font-weight: 800; }
|
| 636 |
.total-row td { font-size: 1.1rem; border-bottom: none; }
|
| 637 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 638 |
.action-bar { position: fixed; bottom: 0; left: 0; width: 100%; background: var(--surface); box-shadow: 0 -4px 20px rgba(0,0,0,0.08); padding: 15px 20px calc(15px + env(safe-area-inset-bottom)); display: flex; gap: 15px; z-index: 100; justify-content: center; border-top-left-radius: 20px; border-top-right-radius: 20px; }
|
| 639 |
.action-bar-inner { display: flex; gap: 15px; width: 100%; max-width: 900px; }
|
| 640 |
.btn { flex: 1; padding: 15px 10px; border-radius: 12px; border: none; font-size: 1rem; font-weight: 700; cursor: pointer; color: #fff; display: flex; align-items: center; justify-content: center; gap: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); transition: transform 0.2s; white-space: nowrap; }
|
|
@@ -643,13 +707,15 @@ ORDER_TEMPLATE = '''
|
|
| 643 |
.btn-print { background: var(--print); }
|
| 644 |
.btn-home { background: #0984e3; box-shadow: 0 4px 15px rgba(9,132,227,0.3); flex: 0 0 auto; padding: 15px 20px; }
|
| 645 |
|
|
|
|
|
|
|
| 646 |
@media print {
|
| 647 |
body { background: #fff; padding: 0; }
|
| 648 |
.invoice-box { box-shadow: none; padding: 0; max-width: 100%; border-radius: 0; }
|
| 649 |
.table-responsive { border: none; overflow: visible; }
|
| 650 |
table { min-width: 100%; }
|
| 651 |
th, td { border: 1px solid #000; }
|
| 652 |
-
.action-bar { display: none; }
|
| 653 |
}
|
| 654 |
@media (max-width: 600px) {
|
| 655 |
.header h1 { font-size: 1.4rem; }
|
|
@@ -661,6 +727,7 @@ ORDER_TEMPLATE = '''
|
|
| 661 |
</style>
|
| 662 |
</head>
|
| 663 |
<body>
|
|
|
|
| 664 |
<div class="invoice-box">
|
| 665 |
<div style="text-align: center; margin-bottom: 25px;">
|
| 666 |
<img src="{{ logo_url }}" style="max-height: 80px; max-width: 100%; object-fit: contain;">
|
|
@@ -691,23 +758,43 @@ ORDER_TEMPLATE = '''
|
|
| 691 |
<th style="text-align: left;">Наименование</th>
|
| 692 |
<th>Фото</th>
|
| 693 |
<th>Кол-во</th>
|
|
|
|
| 694 |
<th>Цена</th>
|
| 695 |
<th>Сумма</th>
|
| 696 |
</tr>
|
| 697 |
</thead>
|
| 698 |
<tbody>
|
| 699 |
{% for item in order.cart %}
|
|
|
|
|
|
|
|
|
|
| 700 |
<tr>
|
| 701 |
<td>{{ loop.index }}</td>
|
| 702 |
-
<td style="text-align: left; font-weight: 500;">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 703 |
<td class="img-cell"><img src="{{ item.photo_url }}" alt="img"></td>
|
| 704 |
-
<td>{{ item.quantity }}</td>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 705 |
<td>{{ item.price }}</td>
|
| 706 |
<td>{{ item.price * item.quantity }}</td>
|
| 707 |
</tr>
|
| 708 |
{% endfor %}
|
| 709 |
<tr class="total-row">
|
| 710 |
-
<td colspan="
|
| 711 |
<td>{{ order.total_price }} {{ currency_code }}</td>
|
| 712 |
</tr>
|
| 713 |
</tbody>
|
|
@@ -728,6 +815,28 @@ ORDER_TEMPLATE = '''
|
|
| 728 |
let msg = `Здравствуйте! Мой заказ №{{ order.id }}\nНакладная: ${window.location.href}`;
|
| 729 |
window.open(`https://api.whatsapp.com/send?phone={{ whatsapp_number }}&text=${encodeURIComponent(msg)}`, '_blank');
|
| 730 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 731 |
</script>
|
| 732 |
</body>
|
| 733 |
</html>
|
|
@@ -870,6 +979,7 @@ ADMIN_TEMPLATE = '''
|
|
| 870 |
<div class="form-row">
|
| 871 |
<input type="text" name="name" placeholder="Название товара" required autocomplete="off" style="flex:2;">
|
| 872 |
<input type="number" name="price" placeholder="Цена" required step="0.01" style="flex:1;">
|
|
|
|
| 873 |
</div>
|
| 874 |
<textarea name="description" placeholder="Описание товара (необязательно)"></textarea>
|
| 875 |
<div class="file-input-wrapper">
|
|
@@ -894,7 +1004,7 @@ ADMIN_TEMPLATE = '''
|
|
| 894 |
{% if product.description %}
|
| 895 |
<span class="product-desc">{{ product.description[:50] }}{{ '...' if product.description|length > 50 else '' }}</span>
|
| 896 |
{% endif %}
|
| 897 |
-
<span class="product-meta">{{ product.price }} {{ currency_code }} • Фото: {{ product.photos|length if product.photos else 0 }}/10</span>
|
| 898 |
</div>
|
| 899 |
</div>
|
| 900 |
<div class="product-actions">
|
|
@@ -914,6 +1024,7 @@ ADMIN_TEMPLATE = '''
|
|
| 914 |
<div class="form-row">
|
| 915 |
<input type="text" name="name" value="{{ product.name }}" required autocomplete="off" style="flex:2;">
|
| 916 |
<input type="number" name="price" value="{{ product.price }}" required step="0.01" style="flex:1;">
|
|
|
|
| 917 |
</div>
|
| 918 |
<textarea name="description">{{ product.description }}</textarea>
|
| 919 |
<div class="file-input-wrapper">
|
|
@@ -1034,9 +1145,11 @@ def create_order():
|
|
| 1034 |
processed_cart = []
|
| 1035 |
for item in cart_items:
|
| 1036 |
processed_cart.append({
|
|
|
|
| 1037 |
"name": item['name'],
|
| 1038 |
"price": float(item['price']),
|
| 1039 |
"quantity": int(item['quantity']),
|
|
|
|
| 1040 |
"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+"
|
| 1041 |
})
|
| 1042 |
|
|
@@ -1073,6 +1186,33 @@ def view_order(order_id):
|
|
| 1073 |
logo_url=LOGO_URL
|
| 1074 |
)
|
| 1075 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1076 |
@app.route('/admin', methods=['GET', 'POST'])
|
| 1077 |
def admin():
|
| 1078 |
data = load_data()
|
|
@@ -1100,6 +1240,7 @@ def admin():
|
|
| 1100 |
elif action == 'add_product':
|
| 1101 |
name = request.form.get('name', '').strip()
|
| 1102 |
price = float(request.form.get('price', 0))
|
|
|
|
| 1103 |
description = request.form.get('description', '').strip()
|
| 1104 |
category = request.form.get('category')
|
| 1105 |
uploaded_photos = request.files.getlist('photos')[:10]
|
|
@@ -1136,6 +1277,7 @@ def admin():
|
|
| 1136 |
'product_id': uuid4().hex,
|
| 1137 |
'name': name,
|
| 1138 |
'price': price,
|
|
|
|
| 1139 |
'description': description,
|
| 1140 |
'category': category,
|
| 1141 |
'photos': photos_list
|
|
@@ -1148,6 +1290,7 @@ def admin():
|
|
| 1148 |
pid = request.form.get('product_id')
|
| 1149 |
name = request.form.get('name', '').strip()
|
| 1150 |
price = float(request.form.get('price', 0))
|
|
|
|
| 1151 |
description = request.form.get('description', '').strip()
|
| 1152 |
uploaded_photos = request.files.getlist('photos')[:10]
|
| 1153 |
|
|
@@ -1183,6 +1326,7 @@ def admin():
|
|
| 1183 |
if p.get('product_id') == pid:
|
| 1184 |
p['name'] = name
|
| 1185 |
p['price'] = price
|
|
|
|
| 1186 |
p['description'] = description
|
| 1187 |
if photos_list:
|
| 1188 |
p['photos'] = photos_list
|
|
@@ -1223,4 +1367,4 @@ if __name__ == '__main__':
|
|
| 1223 |
threading.Thread(target=periodic_backup, daemon=True).start()
|
| 1224 |
|
| 1225 |
port = int(os.environ.get('PORT', 7860))
|
| 1226 |
-
app.run(host='0.0.0.0', port=port)
|
|
|
|
| 132 |
for product in data['products']:
|
| 133 |
if 'product_id' not in product:
|
| 134 |
product['product_id'] = uuid4().hex
|
| 135 |
+
if 'pieces_per_box' not in product:
|
| 136 |
+
product['pieces_per_box'] = 1
|
| 137 |
|
| 138 |
if not os.path.exists(DATA_FILE):
|
| 139 |
try:
|
|
|
|
| 197 |
.product-info { flex-grow: 1; display: flex; flex-direction: column; justify-content: space-between; min-width: 0; padding: 5px 0; }
|
| 198 |
.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; }
|
| 199 |
.product-desc { font-size: 0.8rem; color: var(--text-muted); margin-top: 4px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
| 200 |
+
.product-box-info { font-size: 0.8rem; color: #00b894; margin-top: 4px; font-weight: 600; }
|
| 201 |
+
.product-bottom { display: flex; align-items: center; justify-content: space-between; margin-top: 10px; flex-wrap: wrap; gap: 10px; }
|
| 202 |
.product-price { font-weight: 700; font-size: 1rem; color: var(--primary); }
|
| 203 |
+
.controls-wrapper { display: flex; gap: 8px; align-items: center; }
|
| 204 |
.quantity-control { display: flex; align-items: center; background: var(--bg); border-radius: 8px; overflow: hidden; border: 1px solid var(--border); }
|
| 205 |
.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; }
|
| 206 |
.quantity-control button:active { background: #e0e0e0; }
|
| 207 |
.quantity-control input { width: 36px; height: 32px; border: none; text-align: center; background: transparent; font-weight: 600; font-size: 0.95rem; pointer-events: none; color: var(--primary); }
|
| 208 |
+
.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; }
|
| 209 |
+
.box-btn:active { opacity: 0.8; }
|
| 210 |
|
| 211 |
.cart-bar { position: fixed; bottom: 0; left: 0; width: 100%; background: var(--surface); box-shadow: 0 -4px 20px rgba(0,0,0,0.06); padding: 15px 20px calc(15px + env(safe-area-inset-bottom)); display: none; justify-content: space-between; align-items: center; z-index: 100; border-top-left-radius: 20px; border-top-right-radius: 20px; }
|
| 212 |
.cart-info { display: flex; flex-direction: column; }
|
|
|
|
| 227 |
.customer-form input:focus { border-color: var(--primary); background: var(--surface); }
|
| 228 |
|
| 229 |
.cart-item-list { display: flex; flex-direction: column; gap: 15px; margin-bottom: 25px; }
|
| 230 |
+
.cart-item { display: flex; justify-content: space-between; align-items: center; background: var(--bg); padding: 15px; border-radius: 12px; flex-wrap: wrap; gap: 10px; }
|
| 231 |
+
.cart-item-name { flex: 1; min-width: 120px; font-size: 0.95rem; font-weight: 500; line-height: 1.3; }
|
| 232 |
+
.cart-item-controls { display: flex; align-items: center; background: var(--surface); border-radius: 8px; border: 1px solid var(--border); overflow: hidden; }
|
| 233 |
+
.cart-item-controls button { border: none; background: transparent; width: 30px; height: 30px; font-size: 1rem; cursor: pointer; color: var(--primary); }
|
| 234 |
+
.cart-item-controls button:active { background: #e0e0e0; }
|
| 235 |
+
.cart-item-controls span { width: 35px; text-align: center; font-weight: 600; font-size: 0.9rem; }
|
| 236 |
+
.cart-item-price { font-weight: 700; color: var(--primary); min-width: 70px; text-align: right; }
|
| 237 |
+
.cart-item-delete { color: #ff7675; background: none; border: none; font-size: 1.1rem; cursor: pointer; padding: 5px; }
|
| 238 |
+
|
| 239 |
.confirm-btn { background: var(--accent); color: #fff; width: 100%; padding: 16px; border: none; border-radius: 14px; font-size: 1.1rem; font-weight: 700; cursor: pointer; box-shadow: 0 4px 15px rgba(37,211,102,0.3); }
|
| 240 |
|
| 241 |
.gallery-modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: #000; z-index: 300; justify-content: center; align-items: center; flex-direction: column; }
|
|
|
|
| 403 |
}
|
| 404 |
}
|
| 405 |
|
| 406 |
+
function formatQtyText(qty, ppb) {
|
| 407 |
+
ppb = parseInt(ppb) || 1;
|
| 408 |
+
if (ppb > 1 && qty >= ppb) {
|
| 409 |
+
let boxes = Math.floor(qty / ppb);
|
| 410 |
+
let remainder = qty % ppb;
|
| 411 |
+
return `${boxes} кор.` + (remainder > 0 ? ` ${remainder} шт.` : '');
|
| 412 |
+
}
|
| 413 |
+
return `${qty} шт.`;
|
| 414 |
+
}
|
| 415 |
+
|
| 416 |
function renderProductCard(p, container) {
|
| 417 |
const qty = cart[p.product_id] ? cart[p.product_id].quantity : 0;
|
| 418 |
+
const ppb = parseInt(p.pieces_per_box) || 1;
|
| 419 |
const hasPhotos = p.photos && p.photos.length > 0;
|
| 420 |
const photoUrl = hasPhotos
|
| 421 |
? `https://huggingface.co/datasets/${repoId}/resolve/main/photos/${p.photos[0]}`
|
|
|
|
| 424 |
const photoIndicator = hasPhotos && p.photos.length > 1 ? `<div class="photo-count"><i class="fas fa-images"></i> ${p.photos.length}</div>` : '';
|
| 425 |
const imgClick = hasPhotos ? `onclick="openGallery('${p.product_id}')"` : '';
|
| 426 |
const descHtml = p.description ? `<div class="product-desc">${p.description}</div>` : '';
|
| 427 |
+
const boxInfoHtml = ppb > 1 ? `<div class="product-box-info">В коробке: ${ppb} шт</div>` : '';
|
| 428 |
+
const addBoxBtn = ppb > 1 ? `<button class="box-btn" onclick="updateCart('${p.product_id}', ${ppb})">+ Коробка</button>` : '';
|
| 429 |
|
| 430 |
const div = document.createElement('div');
|
| 431 |
div.className = 'product-card';
|
|
|
|
| 438 |
<div>
|
| 439 |
<div class="product-title">${p.name}</div>
|
| 440 |
${descHtml}
|
| 441 |
+
${boxInfoHtml}
|
| 442 |
</div>
|
| 443 |
<div class="product-bottom">
|
| 444 |
<div class="product-price">${p.price} ${currency}</div>
|
| 445 |
+
<div class="controls-wrapper">
|
| 446 |
+
${addBoxBtn}
|
| 447 |
+
<div class="quantity-control">
|
| 448 |
+
<button onclick="updateCart('${p.product_id}', -1)"><i class="fas fa-minus" style="font-size:0.8rem;"></i></button>
|
| 449 |
+
<input type="text" id="qty-${p.product_id}" value="${qty}" readonly>
|
| 450 |
+
<button onclick="updateCart('${p.product_id}', 1)"><i class="fas fa-plus" style="font-size:0.8rem;"></i></button>
|
| 451 |
+
</div>
|
| 452 |
</div>
|
| 453 |
</div>
|
| 454 |
</div>
|
|
|
|
| 473 |
}
|
| 474 |
}
|
| 475 |
|
| 476 |
+
function updateCart(productId, change, exactValue = null) {
|
| 477 |
const product = products.find(p => p.product_id === productId);
|
| 478 |
if (!product) return;
|
| 479 |
|
|
|
|
| 481 |
cart[productId] = { ...product, quantity: 0 };
|
| 482 |
}
|
| 483 |
|
| 484 |
+
if (exactValue !== null) {
|
| 485 |
+
cart[productId].quantity = exactValue;
|
| 486 |
+
} else {
|
| 487 |
+
cart[productId].quantity += change;
|
| 488 |
+
}
|
| 489 |
|
| 490 |
if (cart[productId].quantity <= 0) {
|
| 491 |
delete cart[productId];
|
| 492 |
+
const qtyInput = document.getElementById(`qty-${productId}`);
|
| 493 |
+
if (qtyInput) qtyInput.value = 0;
|
| 494 |
} else {
|
| 495 |
+
const qtyInput = document.getElementById(`qty-${productId}`);
|
| 496 |
+
if (qtyInput) qtyInput.value = cart[productId].quantity;
|
| 497 |
}
|
| 498 |
|
| 499 |
updateCartUI();
|
|
|
|
| 513 |
cartBar.style.display = 'none';
|
| 514 |
closeCartModal();
|
| 515 |
}
|
| 516 |
+
|
| 517 |
+
if (document.getElementById('cartModal').classList.contains('active')) {
|
| 518 |
+
renderCartModalItems();
|
| 519 |
+
}
|
| 520 |
}
|
| 521 |
|
| 522 |
+
function renderCartModalItems() {
|
| 523 |
const list = document.getElementById('cartItemList');
|
| 524 |
list.innerHTML = '';
|
| 525 |
|
| 526 |
for (let id in cart) {
|
| 527 |
const item = cart[id];
|
| 528 |
+
const ppb = parseInt(item.pieces_per_box) || 1;
|
| 529 |
+
const formattedQty = formatQtyText(item.quantity, ppb);
|
| 530 |
+
|
| 531 |
list.innerHTML += `
|
| 532 |
<div class="cart-item">
|
| 533 |
+
<div class="cart-item-name">
|
| 534 |
+
${item.name}
|
| 535 |
+
<div style="font-size: 0.8rem; color: #00b894; margin-top:2px;">${formattedQty}</div>
|
| 536 |
+
</div>
|
| 537 |
+
<div style="display:flex; align-items:center; gap: 10px;">
|
| 538 |
+
<div class="cart-item-controls">
|
| 539 |
+
<button onclick="updateCart('${id}', -1)"><i class="fas fa-minus" style="font-size:0.8rem;"></i></button>
|
| 540 |
+
<span>${item.quantity}</span>
|
| 541 |
+
<button onclick="updateCart('${id}', 1)"><i class="fas fa-plus" style="font-size:0.8rem;"></i></button>
|
| 542 |
+
</div>
|
| 543 |
+
<button class="cart-item-delete" onclick="updateCart('${id}', 0, 0)"><i class="fas fa-trash-alt"></i></button>
|
| 544 |
+
</div>
|
| 545 |
+
<div class="cart-item-price">${item.price * item.quantity} ${currency}</div>
|
| 546 |
</div>
|
| 547 |
`;
|
| 548 |
}
|
| 549 |
+
}
|
| 550 |
+
|
| 551 |
+
function openCartModal() {
|
| 552 |
+
renderCartModalItems();
|
| 553 |
const modal = document.getElementById('cartModal');
|
| 554 |
modal.style.display = 'flex';
|
| 555 |
setTimeout(() => modal.classList.add('active'), 10);
|
|
|
|
| 674 |
<title>Накладная №{{ order.id }}</title>
|
| 675 |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
| 676 |
<style>
|
| 677 |
+
:root { --bg: #f4f6f8; --surface: #ffffff; --text: #2d3436; --border: #dfe6e9; --wa: #25D366; --print: #1e272e; --primary: #1a1a1a; }
|
| 678 |
* { box-sizing: border-box; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }
|
| 679 |
body { margin: 0; padding: max(20px, env(safe-area-inset-top)) 20px calc(100px + env(safe-area-inset-bottom)); background: var(--bg); display: flex; flex-direction: column; align-items: center; color: var(--text); }
|
| 680 |
.invoice-box { background: var(--surface); width: 100%; max-width: 900px; padding: 30px; box-shadow: 0 4px 20px rgba(0,0,0,0.05); border-radius: 16px; }
|
|
|
|
| 685 |
.customer-details span { font-weight: 600; color: #1a1a1a; }
|
| 686 |
|
| 687 |
.table-responsive { width: 100%; overflow-x: auto; -webkit-overflow-scrolling: touch; margin-bottom: 20px; border-radius: 8px; border: 1px solid var(--border); }
|
| 688 |
+
table { width: 100%; border-collapse: collapse; min-width: 600px; }
|
| 689 |
th, td { border-bottom: 1px solid var(--border); padding: 12px; text-align: center; font-size: 0.95rem; }
|
| 690 |
th { background: #fafafa; font-weight: 700; color: #636e72; text-transform: uppercase; font-size: 0.8rem; letter-spacing: 0.5px; }
|
| 691 |
.img-cell img { width: 45px; height: 45px; object-fit: cover; border-radius: 8px; border: 1px solid #eee; }
|
| 692 |
.total-row { background: #fafafa; font-weight: 800; }
|
| 693 |
.total-row td { font-size: 1.1rem; border-bottom: none; }
|
| 694 |
|
| 695 |
+
.qty-info { font-size: 0.8rem; color: #00b894; font-weight: 600; margin-top: 4px; display: block; }
|
| 696 |
+
|
| 697 |
+
.action-btns { display: flex; align-items: center; justify-content: center; gap: 5px; }
|
| 698 |
+
.edit-btn { background: #f1f2f6; border: 1px solid var(--border); border-radius: 6px; width: 28px; height: 28px; cursor: pointer; color: var(--primary); display: flex; align-items: center; justify-content: center; }
|
| 699 |
+
.edit-btn:active { background: #dfe6e9; }
|
| 700 |
+
.delete-btn { color: #ff7675; }
|
| 701 |
+
|
| 702 |
.action-bar { position: fixed; bottom: 0; left: 0; width: 100%; background: var(--surface); box-shadow: 0 -4px 20px rgba(0,0,0,0.08); padding: 15px 20px calc(15px + env(safe-area-inset-bottom)); display: flex; gap: 15px; z-index: 100; justify-content: center; border-top-left-radius: 20px; border-top-right-radius: 20px; }
|
| 703 |
.action-bar-inner { display: flex; gap: 15px; width: 100%; max-width: 900px; }
|
| 704 |
.btn { flex: 1; padding: 15px 10px; border-radius: 12px; border: none; font-size: 1rem; font-weight: 700; cursor: pointer; color: #fff; display: flex; align-items: center; justify-content: center; gap: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); transition: transform 0.2s; white-space: nowrap; }
|
|
|
|
| 707 |
.btn-print { background: var(--print); }
|
| 708 |
.btn-home { background: #0984e3; box-shadow: 0 4px 15px rgba(9,132,227,0.3); flex: 0 0 auto; padding: 15px 20px; }
|
| 709 |
|
| 710 |
+
#loadingOverlay { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(255,255,255,0.8); z-index: 999; justify-content: center; align-items: center; font-size: 2rem; color: var(--primary); }
|
| 711 |
+
|
| 712 |
@media print {
|
| 713 |
body { background: #fff; padding: 0; }
|
| 714 |
.invoice-box { box-shadow: none; padding: 0; max-width: 100%; border-radius: 0; }
|
| 715 |
.table-responsive { border: none; overflow: visible; }
|
| 716 |
table { min-width: 100%; }
|
| 717 |
th, td { border: 1px solid #000; }
|
| 718 |
+
.action-bar, .no-print { display: none !important; }
|
| 719 |
}
|
| 720 |
@media (max-width: 600px) {
|
| 721 |
.header h1 { font-size: 1.4rem; }
|
|
|
|
| 727 |
</style>
|
| 728 |
</head>
|
| 729 |
<body>
|
| 730 |
+
<div id="loadingOverlay"><i class="fas fa-spinner fa-spin"></i></div>
|
| 731 |
<div class="invoice-box">
|
| 732 |
<div style="text-align: center; margin-bottom: 25px;">
|
| 733 |
<img src="{{ logo_url }}" style="max-height: 80px; max-width: 100%; object-fit: contain;">
|
|
|
|
| 758 |
<th style="text-align: left;">Наименование</th>
|
| 759 |
<th>Фото</th>
|
| 760 |
<th>Кол-во</th>
|
| 761 |
+
<th class="no-print">Действия</th>
|
| 762 |
<th>Цена</th>
|
| 763 |
<th>Сумма</th>
|
| 764 |
</tr>
|
| 765 |
</thead>
|
| 766 |
<tbody>
|
| 767 |
{% for item in order.cart %}
|
| 768 |
+
{% set ppb = item.pieces_per_box|default(1)|int %}
|
| 769 |
+
{% set boxes = (item.quantity / ppb)|round(0, 'floor')|int %}
|
| 770 |
+
{% set remainder = item.quantity % ppb %}
|
| 771 |
<tr>
|
| 772 |
<td>{{ loop.index }}</td>
|
| 773 |
+
<td style="text-align: left; font-weight: 500;">
|
| 774 |
+
{{ item.name }}
|
| 775 |
+
<span class="qty-info">
|
| 776 |
+
{% if ppb > 1 and boxes > 0 %}
|
| 777 |
+
{{ boxes }} кор.{% if remainder > 0 %} {{ remainder }} шт.{% endif %}
|
| 778 |
+
{% else %}
|
| 779 |
+
{{ item.quantity }} шт.
|
| 780 |
+
{% endif %}
|
| 781 |
+
</span>
|
| 782 |
+
</td>
|
| 783 |
<td class="img-cell"><img src="{{ item.photo_url }}" alt="img"></td>
|
| 784 |
+
<td style="font-weight: bold;">{{ item.quantity }}</td>
|
| 785 |
+
<td class="no-print">
|
| 786 |
+
<div class="action-btns">
|
| 787 |
+
<button class="edit-btn" onclick="updateItem('{{ item.product_id }}', -1)"><i class="fas fa-minus" style="font-size:0.7rem;"></i></button>
|
| 788 |
+
<button class="edit-btn" onclick="updateItem('{{ item.product_id }}', 1)"><i class="fas fa-plus" style="font-size:0.7rem;"></i></button>
|
| 789 |
+
<button class="edit-btn delete-btn" onclick="updateItem('{{ item.product_id }}', 0, true)"><i class="fas fa-trash"></i></button>
|
| 790 |
+
</div>
|
| 791 |
+
</td>
|
| 792 |
<td>{{ item.price }}</td>
|
| 793 |
<td>{{ item.price * item.quantity }}</td>
|
| 794 |
</tr>
|
| 795 |
{% endfor %}
|
| 796 |
<tr class="total-row">
|
| 797 |
+
<td colspan="6" style="text-align: right; padding-right: 20px;">Итого:</td>
|
| 798 |
<td>{{ order.total_price }} {{ currency_code }}</td>
|
| 799 |
</tr>
|
| 800 |
</tbody>
|
|
|
|
| 815 |
let msg = `Здравствуйте! Мой заказ №{{ order.id }}\nНакладная: ${window.location.href}`;
|
| 816 |
window.open(`https://api.whatsapp.com/send?phone={{ whatsapp_number }}&text=${encodeURIComponent(msg)}`, '_blank');
|
| 817 |
}
|
| 818 |
+
|
| 819 |
+
function updateItem(productId, change, isRemove = false) {
|
| 820 |
+
document.getElementById('loadingOverlay').style.display = 'flex';
|
| 821 |
+
fetch(`/edit_order/{{ order.id }}`, {
|
| 822 |
+
method: 'POST',
|
| 823 |
+
headers: { 'Content-Type': 'application/json' },
|
| 824 |
+
body: JSON.stringify({ product_id: productId, change: change, remove: isRemove })
|
| 825 |
+
})
|
| 826 |
+
.then(r => r.json())
|
| 827 |
+
.then(data => {
|
| 828 |
+
if(data.success) {
|
| 829 |
+
window.location.reload();
|
| 830 |
+
} else {
|
| 831 |
+
alert('Ошибка обновления');
|
| 832 |
+
document.getElementById('loadingOverlay').style.display = 'none';
|
| 833 |
+
}
|
| 834 |
+
})
|
| 835 |
+
.catch(() => {
|
| 836 |
+
alert('Произошла ошибка');
|
| 837 |
+
document.getElementById('loadingOverlay').style.display = 'none';
|
| 838 |
+
});
|
| 839 |
+
}
|
| 840 |
</script>
|
| 841 |
</body>
|
| 842 |
</html>
|
|
|
|
| 979 |
<div class="form-row">
|
| 980 |
<input type="text" name="name" placeholder="Название товара" required autocomplete="off" style="flex:2;">
|
| 981 |
<input type="number" name="price" placeholder="Цена" required step="0.01" style="flex:1;">
|
| 982 |
+
<input type="number" name="pieces_per_box" placeholder="В коробке (шт)" value="1" min="1" required style="flex:1;">
|
| 983 |
</div>
|
| 984 |
<textarea name="description" placeholder="Описание товара (необязательно)"></textarea>
|
| 985 |
<div class="file-input-wrapper">
|
|
|
|
| 1004 |
{% if product.description %}
|
| 1005 |
<span class="product-desc">{{ product.description[:50] }}{{ '...' if product.description|length > 50 else '' }}</span>
|
| 1006 |
{% endif %}
|
| 1007 |
+
<span class="product-meta">{{ product.price }} {{ currency_code }} • В коробке: {{ product.pieces_per_box|default(1) }} шт • Фото: {{ product.photos|length if product.photos else 0 }}/10</span>
|
| 1008 |
</div>
|
| 1009 |
</div>
|
| 1010 |
<div class="product-actions">
|
|
|
|
| 1024 |
<div class="form-row">
|
| 1025 |
<input type="text" name="name" value="{{ product.name }}" required autocomplete="off" style="flex:2;">
|
| 1026 |
<input type="number" name="price" value="{{ product.price }}" required step="0.01" style="flex:1;">
|
| 1027 |
+
<input type="number" name="pieces_per_box" value="{{ product.pieces_per_box|default(1) }}" min="1" required style="flex:1;">
|
| 1028 |
</div>
|
| 1029 |
<textarea name="description">{{ product.description }}</textarea>
|
| 1030 |
<div class="file-input-wrapper">
|
|
|
|
| 1145 |
processed_cart = []
|
| 1146 |
for item in cart_items:
|
| 1147 |
processed_cart.append({
|
| 1148 |
+
"product_id": item.get('product_id'),
|
| 1149 |
"name": item['name'],
|
| 1150 |
"price": float(item['price']),
|
| 1151 |
"quantity": int(item['quantity']),
|
| 1152 |
+
"pieces_per_box": int(item.get('pieces_per_box', 1)),
|
| 1153 |
"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+"
|
| 1154 |
})
|
| 1155 |
|
|
|
|
| 1186 |
logo_url=LOGO_URL
|
| 1187 |
)
|
| 1188 |
|
| 1189 |
+
@app.route('/edit_order/<order_id>', methods=['POST'])
|
| 1190 |
+
def edit_order(order_id):
|
| 1191 |
+
data = load_data()
|
| 1192 |
+
order = data.get('orders', {}).get(order_id)
|
| 1193 |
+
if not order:
|
| 1194 |
+
return jsonify({"success": False, "error": "Order not found"}), 404
|
| 1195 |
+
|
| 1196 |
+
req_data = request.get_json()
|
| 1197 |
+
product_id = req_data.get('product_id')
|
| 1198 |
+
change = req_data.get('change', 0)
|
| 1199 |
+
remove = req_data.get('remove', False)
|
| 1200 |
+
|
| 1201 |
+
for item in order['cart']:
|
| 1202 |
+
if item.get('product_id') == product_id:
|
| 1203 |
+
if remove:
|
| 1204 |
+
order['cart'].remove(item)
|
| 1205 |
+
else:
|
| 1206 |
+
item['quantity'] += change
|
| 1207 |
+
if item['quantity'] <= 0:
|
| 1208 |
+
order['cart'].remove(item)
|
| 1209 |
+
break
|
| 1210 |
+
|
| 1211 |
+
order['total_price'] = sum(float(i['price']) * int(i['quantity']) for i in order['cart'])
|
| 1212 |
+
save_data(data)
|
| 1213 |
+
|
| 1214 |
+
return jsonify({"success": True, "total_price": order['total_price']})
|
| 1215 |
+
|
| 1216 |
@app.route('/admin', methods=['GET', 'POST'])
|
| 1217 |
def admin():
|
| 1218 |
data = load_data()
|
|
|
|
| 1240 |
elif action == 'add_product':
|
| 1241 |
name = request.form.get('name', '').strip()
|
| 1242 |
price = float(request.form.get('price', 0))
|
| 1243 |
+
pieces_per_box = int(request.form.get('pieces_per_box', 1))
|
| 1244 |
description = request.form.get('description', '').strip()
|
| 1245 |
category = request.form.get('category')
|
| 1246 |
uploaded_photos = request.files.getlist('photos')[:10]
|
|
|
|
| 1277 |
'product_id': uuid4().hex,
|
| 1278 |
'name': name,
|
| 1279 |
'price': price,
|
| 1280 |
+
'pieces_per_box': pieces_per_box,
|
| 1281 |
'description': description,
|
| 1282 |
'category': category,
|
| 1283 |
'photos': photos_list
|
|
|
|
| 1290 |
pid = request.form.get('product_id')
|
| 1291 |
name = request.form.get('name', '').strip()
|
| 1292 |
price = float(request.form.get('price', 0))
|
| 1293 |
+
pieces_per_box = int(request.form.get('pieces_per_box', 1))
|
| 1294 |
description = request.form.get('description', '').strip()
|
| 1295 |
uploaded_photos = request.files.getlist('photos')[:10]
|
| 1296 |
|
|
|
|
| 1326 |
if p.get('product_id') == pid:
|
| 1327 |
p['name'] = name
|
| 1328 |
p['price'] = price
|
| 1329 |
+
p['pieces_per_box'] = pieces_per_box
|
| 1330 |
p['description'] = description
|
| 1331 |
if photos_list:
|
| 1332 |
p['photos'] = photos_list
|
|
|
|
| 1367 |
threading.Thread(target=periodic_backup, daemon=True).start()
|
| 1368 |
|
| 1369 |
port = int(os.environ.get('PORT', 7860))
|
| 1370 |
+
app.run(host='0.0.0.0', port=port)
|