Update app.py
Browse files
app.py
CHANGED
|
@@ -159,61 +159,96 @@ CATALOG_TEMPLATE = '''
|
|
| 159 |
<html lang="ru">
|
| 160 |
<head>
|
| 161 |
<meta charset="UTF-8">
|
| 162 |
-
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
| 163 |
<title>Магазин</title>
|
| 164 |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
| 165 |
<style>
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
.header h1 { font-size: 1.5rem; font-weight: bold; }
|
| 170 |
-
.back-btn { display: none; font-size: 1.2rem; cursor: pointer; color: #333; margin-right: 15px; }
|
| 171 |
-
.search-bar { padding: 10px 20px; background: #fff; border-bottom: 1px solid #eee; display: flex; align-items: center; }
|
| 172 |
-
.search-bar input { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 8px; outline: none; font-size: 1rem; }
|
| 173 |
|
| 174 |
-
.
|
| 175 |
-
.
|
| 176 |
-
.
|
| 177 |
-
.category-item span.name { font-size: 0.95rem; }
|
| 178 |
-
.category-item span.count { color: #999; font-size: 0.9rem; }
|
| 179 |
|
| 180 |
-
.
|
| 181 |
-
.
|
| 182 |
-
.
|
| 183 |
-
.
|
| 184 |
-
.
|
| 185 |
-
.product-bottom { display: flex; flex-direction: column; align-items: flex-end; gap: 10px; }
|
| 186 |
-
.product-price { font-weight: bold; font-size: 1rem; }
|
| 187 |
-
.quantity-control { display: flex; align-items: center; background: #f0f0f0; border-radius: 4px; overflow: hidden; }
|
| 188 |
-
.quantity-control button { border: none; background: #e0e0e0; width: 35px; height: 35px; font-size: 1.2rem; cursor: pointer; }
|
| 189 |
-
.quantity-control input { width: 40px; height: 35px; border: none; text-align: center; background: transparent; font-weight: bold; pointer-events: none; }
|
| 190 |
|
| 191 |
-
.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 192 |
.cart-info { display: flex; flex-direction: column; }
|
| 193 |
-
.cart-total { font-size: 1.
|
| 194 |
-
.checkout-btn { background:
|
|
|
|
| 195 |
|
| 196 |
-
.modal-overlay { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.
|
| 197 |
-
.modal-
|
| 198 |
-
.modal-
|
| 199 |
-
.modal-
|
| 200 |
-
.
|
| 201 |
-
.
|
| 202 |
-
.
|
| 203 |
-
.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 204 |
</style>
|
| 205 |
</head>
|
| 206 |
<body>
|
| 207 |
<div class="header">
|
| 208 |
<div style="display: flex; align-items: center;">
|
| 209 |
-
<i class="fas fa-
|
| 210 |
<h1 id="pageTitle">Каталог</h1>
|
| 211 |
</div>
|
| 212 |
-
<i class="fas fa-bars" style="font-size: 1.5rem;"></i>
|
| 213 |
</div>
|
| 214 |
|
| 215 |
<div class="search-bar" id="searchBar">
|
| 216 |
-
<
|
|
|
|
|
|
|
|
|
|
| 217 |
</div>
|
| 218 |
|
| 219 |
<div class="categories-container" id="categoriesContainer"></div>
|
|
@@ -221,23 +256,33 @@ CATALOG_TEMPLATE = '''
|
|
| 221 |
|
| 222 |
<div class="cart-bar" id="cartBar">
|
| 223 |
<div class="cart-info">
|
| 224 |
-
<span style="font-size: 0.
|
| 225 |
<span class="cart-total"><span id="cartTotalSum">0</span> {{ currency_code }}</span>
|
| 226 |
</div>
|
| 227 |
-
<button class="checkout-btn" onclick="openCartModal()">
|
| 228 |
</div>
|
| 229 |
|
| 230 |
-
<div class="modal-overlay" id="cartModal">
|
| 231 |
<div class="modal-content">
|
| 232 |
<div class="modal-header">
|
| 233 |
-
<h2>
|
| 234 |
-
<button class="modal-close" onclick="closeCartModal()">
|
| 235 |
</div>
|
| 236 |
<div class="cart-item-list" id="cartItemList"></div>
|
| 237 |
-
<button class="confirm-btn" onclick="submitOrder()">
|
| 238 |
</div>
|
| 239 |
</div>
|
| 240 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 241 |
<script>
|
| 242 |
const products = {{ products_json|safe }};
|
| 243 |
const categoriesList = {{ categories_json|safe }};
|
|
@@ -245,6 +290,8 @@ CATALOG_TEMPLATE = '''
|
|
| 245 |
const currency = '{{ currency_code }}';
|
| 246 |
|
| 247 |
let cart = {};
|
|
|
|
|
|
|
| 248 |
|
| 249 |
function init() {
|
| 250 |
renderCategories();
|
|
@@ -253,7 +300,8 @@ CATALOG_TEMPLATE = '''
|
|
| 253 |
|
| 254 |
function renderCategories() {
|
| 255 |
const container = document.getElementById('categoriesContainer');
|
| 256 |
-
document.getElementById('productsContainer')
|
|
|
|
| 257 |
container.style.display = 'grid';
|
| 258 |
document.getElementById('backBtn').style.display = 'none';
|
| 259 |
document.getElementById('pageTitle').innerText = 'Каталог';
|
|
@@ -267,7 +315,13 @@ CATALOG_TEMPLATE = '''
|
|
| 267 |
const div = document.createElement('div');
|
| 268 |
div.className = 'category-item';
|
| 269 |
div.onclick = () => showProducts(cat);
|
| 270 |
-
div.innerHTML = `
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 271 |
container.appendChild(div);
|
| 272 |
});
|
| 273 |
}
|
|
@@ -288,7 +342,7 @@ CATALOG_TEMPLATE = '''
|
|
| 288 |
const container = document.getElementById('productsContainer');
|
| 289 |
container.style.display = 'flex';
|
| 290 |
document.getElementById('backBtn').style.display = 'block';
|
| 291 |
-
document.getElementById('pageTitle').innerText = '
|
| 292 |
container.innerHTML = '';
|
| 293 |
|
| 294 |
const matchedProducts = products.filter(p =>
|
|
@@ -296,27 +350,38 @@ CATALOG_TEMPLATE = '''
|
|
| 296 |
p.category.toLowerCase().includes(query)
|
| 297 |
);
|
| 298 |
|
| 299 |
-
matchedProducts.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 300 |
}
|
| 301 |
|
| 302 |
function renderProductCard(p, container) {
|
| 303 |
const qty = cart[p.product_id] ? cart[p.product_id].quantity : 0;
|
| 304 |
-
const
|
|
|
|
| 305 |
? `https://huggingface.co/datasets/${repoId}/resolve/main/photos/${p.photos[0]}`
|
| 306 |
-
: '
|
| 307 |
|
|
|
|
|
|
|
|
|
|
| 308 |
const div = document.createElement('div');
|
| 309 |
div.className = 'product-card';
|
| 310 |
div.innerHTML = `
|
| 311 |
-
<
|
|
|
|
|
|
|
|
|
|
| 312 |
<div class="product-info">
|
| 313 |
<div class="product-title">${p.name}</div>
|
| 314 |
<div class="product-bottom">
|
| 315 |
<div class="product-price">${p.price} ${currency}</div>
|
| 316 |
<div class="quantity-control">
|
| 317 |
-
<button onclick="updateCart('${p.product_id}', -1)">-</button>
|
| 318 |
<input type="text" id="qty-${p.product_id}" value="${qty}" readonly>
|
| 319 |
-
<button onclick="updateCart('${p.product_id}', 1)">
|
| 320 |
</div>
|
| 321 |
</div>
|
| 322 |
</div>
|
|
@@ -334,7 +399,11 @@ CATALOG_TEMPLATE = '''
|
|
| 334 |
container.innerHTML = '';
|
| 335 |
|
| 336 |
const catProducts = products.filter(p => p.category === category);
|
| 337 |
-
catProducts.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 338 |
}
|
| 339 |
|
| 340 |
function updateCart(productId, change) {
|
|
@@ -382,19 +451,28 @@ CATALOG_TEMPLATE = '''
|
|
| 382 |
list.innerHTML += `
|
| 383 |
<div class="cart-item">
|
| 384 |
<div class="cart-item-name">${item.name}</div>
|
| 385 |
-
<div
|
| 386 |
</div>
|
| 387 |
`;
|
| 388 |
}
|
| 389 |
-
document.getElementById('cartModal')
|
|
|
|
|
|
|
| 390 |
}
|
| 391 |
|
| 392 |
function closeCartModal() {
|
| 393 |
-
document.getElementById('cartModal')
|
|
|
|
|
|
|
| 394 |
}
|
| 395 |
|
| 396 |
function submitOrder() {
|
| 397 |
const cartArray = Object.values(cart);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 398 |
fetch('/create_order', {
|
| 399 |
method: 'POST',
|
| 400 |
headers: { 'Content-Type': 'application/json' },
|
|
@@ -406,9 +484,71 @@ CATALOG_TEMPLATE = '''
|
|
| 406 |
cart = {};
|
| 407 |
window.location.href = `/order/${data.order_id}`;
|
| 408 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 409 |
});
|
| 410 |
}
|
| 411 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 412 |
init();
|
| 413 |
</script>
|
| 414 |
</body>
|
|
@@ -420,92 +560,109 @@ ORDER_TEMPLATE = '''
|
|
| 420 |
<html lang="ru">
|
| 421 |
<head>
|
| 422 |
<meta charset="UTF-8">
|
| 423 |
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 424 |
<title>Накладная №{{ order.id }}</title>
|
|
|
|
| 425 |
<style>
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
.
|
| 430 |
-
.header
|
| 431 |
-
.
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 437 |
|
| 438 |
-
.action-bar { position: fixed; bottom: 0; left: 0; width: 100%; background:
|
| 439 |
-
.
|
| 440 |
-
.btn
|
| 441 |
-
.btn
|
|
|
|
|
|
|
|
|
|
| 442 |
|
| 443 |
@media print {
|
| 444 |
body { background: #fff; padding: 0; }
|
| 445 |
-
.invoice-box { box-shadow: none; padding: 0; max-width: 100%; }
|
|
|
|
|
|
|
|
|
|
| 446 |
.action-bar { display: none; }
|
| 447 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 448 |
</style>
|
| 449 |
</head>
|
| 450 |
<body>
|
| 451 |
<div class="invoice-box">
|
| 452 |
<div class="header">
|
| 453 |
-
<div>
|
| 454 |
-
<span style="font-size: 24px; font-weight: bold;"></span>
|
| 455 |
-
</div>
|
| 456 |
<h1>Накладная</h1>
|
| 457 |
-
<div>
|
| 458 |
-
<
|
|
|
|
| 459 |
</div>
|
| 460 |
</div>
|
| 461 |
|
| 462 |
<div class="info-row">
|
| 463 |
-
<div>
|
| 464 |
-
<div>
|
| 465 |
-
</div>
|
| 466 |
-
|
| 467 |
-
<div class="info-row">
|
| 468 |
-
<div>покупатель: _________________</div>
|
| 469 |
</div>
|
| 470 |
|
| 471 |
-
<table>
|
| 472 |
-
<
|
| 473 |
-
<
|
| 474 |
-
<
|
| 475 |
-
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
<
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
<
|
| 495 |
-
|
| 496 |
-
|
| 497 |
-
|
| 498 |
-
|
|
|
|
|
|
|
| 499 |
</div>
|
| 500 |
|
| 501 |
<div class="action-bar">
|
| 502 |
-
<
|
| 503 |
-
|
|
|
|
|
|
|
|
|
|
| 504 |
</div>
|
| 505 |
|
| 506 |
<script>
|
| 507 |
function sendToWA() {
|
| 508 |
-
let msg = `Заказ №{{ order.id }}\nНакладная: ${window.location.href}`;
|
| 509 |
window.open(`https://api.whatsapp.com/send?phone={{ whatsapp_number }}&text=${encodeURIComponent(msg)}`, '_blank');
|
| 510 |
}
|
| 511 |
</script>
|
|
@@ -518,88 +675,152 @@ ADMIN_TEMPLATE = '''
|
|
| 518 |
<html lang="ru">
|
| 519 |
<head>
|
| 520 |
<meta charset="UTF-8">
|
| 521 |
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 522 |
<title>Админ-панель</title>
|
|
|
|
| 523 |
<style>
|
| 524 |
-
|
| 525 |
-
|
| 526 |
-
|
| 527 |
-
|
| 528 |
-
|
| 529 |
-
|
| 530 |
-
|
| 531 |
-
|
| 532 |
-
.
|
| 533 |
-
.
|
| 534 |
-
.
|
| 535 |
-
.
|
| 536 |
-
.
|
| 537 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 538 |
</style>
|
| 539 |
</head>
|
| 540 |
<body>
|
| 541 |
<div class="container">
|
| 542 |
-
<div
|
| 543 |
-
<h1>Админ-панель</h1>
|
| 544 |
-
<a href="/"
|
| 545 |
</div>
|
| 546 |
|
| 547 |
-
<div
|
| 548 |
-
<form method="POST" action="/force_upload"
|
| 549 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 550 |
</div>
|
| 551 |
|
| 552 |
-
<
|
| 553 |
-
|
| 554 |
-
<
|
| 555 |
-
|
| 556 |
-
|
| 557 |
-
|
|
|
|
|
|
|
| 558 |
|
| 559 |
{% for category in categories %}
|
| 560 |
<div class="category-block">
|
| 561 |
<div class="category-header">
|
| 562 |
-
<span>{{ category }}</span>
|
| 563 |
-
<form method="POST" style="margin:0;" onsubmit="return confirm('Удалить категорию?');">
|
| 564 |
<input type="hidden" name="action" value="delete_category">
|
| 565 |
<input type="hidden" name="category_name" value="{{ category }}">
|
| 566 |
-
<button type="submit" class="danger">
|
| 567 |
</form>
|
| 568 |
</div>
|
| 569 |
-
<div class="category-content"
|
| 570 |
|
| 571 |
{% for product in products %}
|
| 572 |
{% if product.category == category %}
|
| 573 |
<div class="product-item">
|
| 574 |
-
<div
|
| 575 |
-
|
| 576 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 577 |
</div>
|
| 578 |
<form method="POST" style="margin:0;" onsubmit="return confirm('Удалить товар?');">
|
| 579 |
<input type="hidden" name="action" value="delete_product">
|
| 580 |
<input type="hidden" name="product_id" value="{{ product.product_id }}">
|
| 581 |
-
<button type="submit" class="danger">Удалить</button>
|
| 582 |
</form>
|
| 583 |
</div>
|
| 584 |
{% endif %}
|
| 585 |
{% endfor %}
|
| 586 |
|
| 587 |
-
<form class="add-product-form" method="POST" enctype="multipart/form-data">
|
| 588 |
<input type="hidden" name="action" value="add_product">
|
| 589 |
<input type="hidden" name="category" value="{{ category }}">
|
| 590 |
-
<div style="
|
| 591 |
-
|
| 592 |
-
<input type="
|
|
|
|
| 593 |
</div>
|
| 594 |
-
<div
|
| 595 |
-
<input type="file" name="
|
|
|
|
| 596 |
</div>
|
| 597 |
-
<button type="submit" style="
|
| 598 |
</form>
|
| 599 |
</div>
|
| 600 |
</div>
|
| 601 |
{% endfor %}
|
| 602 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 603 |
</body>
|
| 604 |
</html>
|
| 605 |
'''
|
|
@@ -633,7 +854,7 @@ def create_order():
|
|
| 633 |
"name": item['name'],
|
| 634 |
"price": float(item['price']),
|
| 635 |
"quantity": int(item['quantity']),
|
| 636 |
-
"photo_url": f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/photos/{item['photos'][0]}" if item.get('photos') else "
|
| 637 |
})
|
| 638 |
|
| 639 |
order_id = f"SA-{datetime.now().strftime('%Y%m%d')}-{str(len(load_data().get('orders', {}))+1).zfill(3)}"
|
|
@@ -693,31 +914,35 @@ def admin():
|
|
| 693 |
name = request.form.get('name', '').strip()
|
| 694 |
price = float(request.form.get('price', 0))
|
| 695 |
category = request.form.get('category')
|
| 696 |
-
|
| 697 |
|
| 698 |
photos_list = []
|
| 699 |
-
if
|
| 700 |
uploads_dir = 'uploads_temp'
|
| 701 |
os.makedirs(uploads_dir, exist_ok=True)
|
| 702 |
-
|
| 703 |
-
|
| 704 |
-
|
| 705 |
-
|
| 706 |
-
|
| 707 |
-
|
| 708 |
-
|
| 709 |
-
|
| 710 |
-
|
| 711 |
-
|
| 712 |
-
|
| 713 |
-
|
| 714 |
-
|
| 715 |
-
|
| 716 |
-
|
| 717 |
-
|
| 718 |
-
|
| 719 |
-
|
| 720 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 721 |
|
| 722 |
new_product = {
|
| 723 |
'product_id': uuid4().hex,
|
|
@@ -763,4 +988,4 @@ if __name__ == '__main__':
|
|
| 763 |
threading.Thread(target=periodic_backup, daemon=True).start()
|
| 764 |
|
| 765 |
port = int(os.environ.get('PORT', 7860))
|
| 766 |
-
app.run(host='0.0.0.0', port=port)
|
|
|
|
| 159 |
<html lang="ru">
|
| 160 |
<head>
|
| 161 |
<meta charset="UTF-8">
|
| 162 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
|
| 163 |
<title>Магазин</title>
|
| 164 |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
| 165 |
<style>
|
| 166 |
+
:root { --primary: #1a1a1a; --bg: #f8f9fa; --surface: #ffffff; --text: #2d3436; --text-muted: #636e72; --border: #edf2f7; --accent: #25D366; }
|
| 167 |
+
* { margin: 0; padding: 0; box-sizing: border-box; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; -webkit-tap-highlight-color: transparent; }
|
| 168 |
+
body { background-color: var(--bg); color: var(--text); padding-bottom: calc(90px + env(safe-area-inset-bottom)); }
|
|
|
|
|
|
|
|
|
|
|
|
|
| 169 |
|
| 170 |
+
.header { display: flex; align-items: center; justify-content: space-between; padding: max(15px, env(safe-area-inset-top)) 20px 15px; background: var(--surface); box-shadow: 0 2px 10px rgba(0,0,0,0.03); position: sticky; top: 0; z-index: 100; }
|
| 171 |
+
.header h1 { font-size: 1.4rem; font-weight: 700; letter-spacing: -0.5px; }
|
| 172 |
+
.back-btn { display: none; font-size: 1.2rem; cursor: pointer; color: var(--text); margin-right: 15px; padding: 5px; }
|
|
|
|
|
|
|
| 173 |
|
| 174 |
+
.search-bar { padding: 15px 20px; background: var(--surface); border-bottom: 1px solid var(--border); }
|
| 175 |
+
.search-container { position: relative; display: flex; align-items: center; background: var(--bg); border-radius: 12px; padding: 0 15px; border: 1px solid transparent; transition: all 0.2s; }
|
| 176 |
+
.search-container:focus-within { border-color: #dcdde1; background: var(--surface); box-shadow: 0 4px 12px rgba(0,0,0,0.05); }
|
| 177 |
+
.search-container i { color: var(--text-muted); font-size: 0.9rem; }
|
| 178 |
+
.search-bar input { width: 100%; padding: 12px 10px; border: none; background: transparent; outline: none; font-size: 0.95rem; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 179 |
|
| 180 |
+
.categories-container { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 15px; padding: 20px; }
|
| 181 |
+
.category-item { background: var(--surface); padding: 20px 15px; border-radius: 16px; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px; cursor: pointer; box-shadow: 0 4px 15px rgba(0,0,0,0.03); transition: transform 0.2s; text-align: center; }
|
| 182 |
+
.category-item:active { transform: scale(0.96); }
|
| 183 |
+
.category-item span.name { font-size: 0.95rem; font-weight: 600; line-height: 1.3; }
|
| 184 |
+
.category-item span.count { color: var(--text-muted); font-size: 0.8rem; background: var(--bg); padding: 4px 10px; border-radius: 20px; }
|
| 185 |
+
|
| 186 |
+
.products-container { display: none; padding: 20px; flex-direction: column; gap: 15px; }
|
| 187 |
+
.product-card { background: var(--surface); border-radius: 16px; padding: 12px; display: flex; box-shadow: 0 4px 15px rgba(0,0,0,0.03); align-items: stretch; gap: 15px; width: 100%; }
|
| 188 |
+
.product-img-wrapper { position: relative; width: 110px; height: 110px; flex-shrink: 0; }
|
| 189 |
+
.product-img { width: 100%; height: 100%; border-radius: 12px; object-fit: cover; cursor: pointer; background: var(--bg); border: 1px solid var(--border); }
|
| 190 |
+
.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; }
|
| 191 |
+
.product-info { flex-grow: 1; display: flex; flex-direction: column; justify-content: space-between; min-width: 0; padding: 5px 0; }
|
| 192 |
+
.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; }
|
| 193 |
+
.product-bottom { display: flex; align-items: center; justify-content: space-between; margin-top: 10px; }
|
| 194 |
+
.product-price { font-weight: 700; font-size: 1rem; color: var(--primary); }
|
| 195 |
+
.quantity-control { display: flex; align-items: center; background: var(--bg); border-radius: 8px; overflow: hidden; border: 1px solid var(--border); }
|
| 196 |
+
.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; }
|
| 197 |
+
.quantity-control button:active { background: #e0e0e0; }
|
| 198 |
+
.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); }
|
| 199 |
+
|
| 200 |
+
.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; }
|
| 201 |
.cart-info { display: flex; flex-direction: column; }
|
| 202 |
+
.cart-total { font-size: 1.25rem; font-weight: 800; color: var(--primary); }
|
| 203 |
+
.checkout-btn { background: var(--primary); color: #fff; padding: 12px 28px; border: none; border-radius: 12px; font-weight: 600; font-size: 1rem; cursor: pointer; box-shadow: 0 4px 12px rgba(26,26,26,0.2); transition: transform 0.2s; }
|
| 204 |
+
.checkout-btn:active { transform: scale(0.95); }
|
| 205 |
|
| 206 |
+
.modal-overlay { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.6); backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px); z-index: 200; justify-content: center; align-items: flex-end; opacity: 0; transition: opacity 0.3s; }
|
| 207 |
+
.modal-overlay.active { opacity: 1; }
|
| 208 |
+
.modal-content { background: var(--surface); width: 100%; max-height: 85vh; border-radius: 24px 24px 0 0; padding: 25px 20px calc(25px + env(safe-area-inset-bottom)); overflow-y: auto; display: flex; flex-direction: column; transform: translateY(100%); transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1); }
|
| 209 |
+
.modal-overlay.active .modal-content { transform: translateY(0); }
|
| 210 |
+
.modal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 25px; }
|
| 211 |
+
.modal-header h2 { font-size: 1.3rem; font-weight: 700; }
|
| 212 |
+
.modal-close { font-size: 1.5rem; cursor: pointer; border: none; background: #f1f2f6; width: 36px; height: 36px; border-radius: 50%; display: flex; align-items: center; justify-content: center; color: var(--text); }
|
| 213 |
+
.cart-item-list { display: flex; flex-direction: column; gap: 15px; margin-bottom: 25px; }
|
| 214 |
+
.cart-item { display: flex; justify-content: space-between; align-items: center; background: var(--bg); padding: 15px; border-radius: 12px; }
|
| 215 |
+
.cart-item-name { flex-grow: 1; font-size: 0.95rem; font-weight: 500; line-height: 1.3; margin-right: 15px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
| 216 |
+
.cart-item-price { font-weight: 700; white-space: nowrap; color: var(--primary); }
|
| 217 |
+
.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); }
|
| 218 |
+
|
| 219 |
+
.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; }
|
| 220 |
+
.gallery-close { position: absolute; top: max(20px, env(safe-area-inset-top)); right: 20px; color: #fff; font-size: 2rem; cursor: pointer; background: rgba(0,0,0,0.5); width: 40px; height: 40px; border-radius: 50%; display: flex; align-items: center; justify-content: center; border: none; z-index: 302; }
|
| 221 |
+
.gallery-img-container { position: relative; width: 100%; height: 70vh; display: flex; align-items: center; justify-content: center; }
|
| 222 |
+
.gallery-img { max-width: 100%; max-height: 100%; object-fit: contain; }
|
| 223 |
+
.gallery-nav { position: absolute; top: 50%; transform: translateY(-50%); color: #fff; font-size: 2rem; background: rgba(0,0,0,0.5); border: none; width: 50px; height: 50px; border-radius: 50%; cursor: pointer; display: flex; align-items: center; justify-content: center; z-index: 301; }
|
| 224 |
+
.gallery-nav.prev { left: 10px; }
|
| 225 |
+
.gallery-nav.next { right: 10px; }
|
| 226 |
+
.gallery-dots { display: flex; gap: 8px; margin-top: 20px; }
|
| 227 |
+
.gallery-dot { width: 8px; height: 8px; border-radius: 50%; background: rgba(255,255,255,0.3); transition: background 0.3s; }
|
| 228 |
+
.gallery-dot.active { background: #fff; }
|
| 229 |
+
|
| 230 |
+
@media (min-width: 768px) {
|
| 231 |
+
.categories-container { grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); }
|
| 232 |
+
.products-container { display: grid; grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); }
|
| 233 |
+
.modal-content { max-width: 500px; margin: 0 auto; border-radius: 24px; top: 50%; transform: translateY(-50%) scale(0.9); bottom: auto; position: relative; max-height: 90vh; }
|
| 234 |
+
.modal-overlay.active .modal-content { transform: translateY(-50%) scale(1); }
|
| 235 |
+
.cart-bar { max-width: 500px; left: 50%; transform: translateX(-50%); border-radius: 20px 20px 0 0; }
|
| 236 |
+
}
|
| 237 |
</style>
|
| 238 |
</head>
|
| 239 |
<body>
|
| 240 |
<div class="header">
|
| 241 |
<div style="display: flex; align-items: center;">
|
| 242 |
+
<i class="fas fa-arrow-left back-btn" id="backBtn" onclick="showCategories()"></i>
|
| 243 |
<h1 id="pageTitle">Каталог</h1>
|
| 244 |
</div>
|
|
|
|
| 245 |
</div>
|
| 246 |
|
| 247 |
<div class="search-bar" id="searchBar">
|
| 248 |
+
<div class="search-container">
|
| 249 |
+
<i class="fas fa-search"></i>
|
| 250 |
+
<input type="text" id="searchInput" placeholder="Поиск товаров..." oninput="filterCategories()">
|
| 251 |
+
</div>
|
| 252 |
</div>
|
| 253 |
|
| 254 |
<div class="categories-container" id="categoriesContainer"></div>
|
|
|
|
| 256 |
|
| 257 |
<div class="cart-bar" id="cartBar">
|
| 258 |
<div class="cart-info">
|
| 259 |
+
<span style="font-size: 0.85rem; color: var(--text-muted); font-weight: 500;">Сумма заказа:</span>
|
| 260 |
<span class="cart-total"><span id="cartTotalSum">0</span> {{ currency_code }}</span>
|
| 261 |
</div>
|
| 262 |
+
<button class="checkout-btn" onclick="openCartModal()">Корзина <i class="fas fa-shopping-bag" style="margin-left:5px;"></i></button>
|
| 263 |
</div>
|
| 264 |
|
| 265 |
+
<div class="modal-overlay" id="cartModal" onclick="if(event.target === this) closeCartModal()">
|
| 266 |
<div class="modal-content">
|
| 267 |
<div class="modal-header">
|
| 268 |
+
<h2>Ваш заказ</h2>
|
| 269 |
+
<button class="modal-close" onclick="closeCartModal()"><i class="fas fa-times"></i></button>
|
| 270 |
</div>
|
| 271 |
<div class="cart-item-list" id="cartItemList"></div>
|
| 272 |
+
<button class="confirm-btn" onclick="submitOrder()">Оформить заказ</button>
|
| 273 |
</div>
|
| 274 |
</div>
|
| 275 |
|
| 276 |
+
<div class="gallery-modal" id="galleryModal">
|
| 277 |
+
<button class="gallery-close" onclick="closeGallery()"><i class="fas fa-times"></i></button>
|
| 278 |
+
<div class="gallery-img-container" id="gallerySwipeArea">
|
| 279 |
+
<button class="gallery-nav prev" onclick="prevPhoto(event)"><i class="fas fa-chevron-left"></i></button>
|
| 280 |
+
<img src="" class="gallery-img" id="galleryImage">
|
| 281 |
+
<button class="gallery-nav next" onclick="nextPhoto(event)"><i class="fas fa-chevron-right"></i></button>
|
| 282 |
+
</div>
|
| 283 |
+
<div class="gallery-dots" id="galleryDots"></div>
|
| 284 |
+
</div>
|
| 285 |
+
|
| 286 |
<script>
|
| 287 |
const products = {{ products_json|safe }};
|
| 288 |
const categoriesList = {{ categories_json|safe }};
|
|
|
|
| 290 |
const currency = '{{ currency_code }}';
|
| 291 |
|
| 292 |
let cart = {};
|
| 293 |
+
let currentGalleryPhotos = [];
|
| 294 |
+
let currentGalleryIndex = 0;
|
| 295 |
|
| 296 |
function init() {
|
| 297 |
renderCategories();
|
|
|
|
| 300 |
|
| 301 |
function renderCategories() {
|
| 302 |
const container = document.getElementById('categoriesContainer');
|
| 303 |
+
const prodContainer = document.getElementById('productsContainer');
|
| 304 |
+
prodContainer.style.display = 'none';
|
| 305 |
container.style.display = 'grid';
|
| 306 |
document.getElementById('backBtn').style.display = 'none';
|
| 307 |
document.getElementById('pageTitle').innerText = 'Каталог';
|
|
|
|
| 315 |
const div = document.createElement('div');
|
| 316 |
div.className = 'category-item';
|
| 317 |
div.onclick = () => showProducts(cat);
|
| 318 |
+
div.innerHTML = `
|
| 319 |
+
<div style="background: var(--bg); width: 50px; height: 50px; border-radius: 12px; display: flex; align-items: center; justify-content: center; margin-bottom: 5px;">
|
| 320 |
+
<i class="fas fa-box-open" style="font-size: 1.5rem; color: var(--primary);"></i>
|
| 321 |
+
</div>
|
| 322 |
+
<span class="name">${cat}</span>
|
| 323 |
+
<span class="count">${count} шт</span>
|
| 324 |
+
`;
|
| 325 |
container.appendChild(div);
|
| 326 |
});
|
| 327 |
}
|
|
|
|
| 342 |
const container = document.getElementById('productsContainer');
|
| 343 |
container.style.display = 'flex';
|
| 344 |
document.getElementById('backBtn').style.display = 'block';
|
| 345 |
+
document.getElementById('pageTitle').innerText = 'Поиск';
|
| 346 |
container.innerHTML = '';
|
| 347 |
|
| 348 |
const matchedProducts = products.filter(p =>
|
|
|
|
| 350 |
p.category.toLowerCase().includes(query)
|
| 351 |
);
|
| 352 |
|
| 353 |
+
if(matchedProducts.length === 0) {
|
| 354 |
+
container.innerHTML = '<div style="text-align:center; padding: 40px; color: var(--text-muted);">Ничего не найдено</div>';
|
| 355 |
+
} else {
|
| 356 |
+
matchedProducts.forEach(p => renderProductCard(p, container));
|
| 357 |
+
}
|
| 358 |
}
|
| 359 |
|
| 360 |
function renderProductCard(p, container) {
|
| 361 |
const qty = cart[p.product_id] ? cart[p.product_id].quantity : 0;
|
| 362 |
+
const hasPhotos = p.photos && p.photos.length > 0;
|
| 363 |
+
const photoUrl = hasPhotos
|
| 364 |
? `https://huggingface.co/datasets/${repoId}/resolve/main/photos/${p.photos[0]}`
|
| 365 |
+
: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDAiIGhlaWdodD0iMTAwIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjZjBmMHcwIi8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIGRvbWluYW50LWJhc2VsaW5lPSJtaWRkbGUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiNhMGEwYTAiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmb250LXNpemU9IjE0Ij7QndC10YIg0YTQvtGC0L48L3RleHQ+PC9zdmc+';
|
| 366 |
|
| 367 |
+
const photoIndicator = hasPhotos && p.photos.length > 1 ? `<div class="photo-count"><i class="fas fa-images"></i> ${p.photos.length}</div>` : '';
|
| 368 |
+
const imgClick = hasPhotos ? `onclick="openGallery('${p.product_id}')"` : '';
|
| 369 |
+
|
| 370 |
const div = document.createElement('div');
|
| 371 |
div.className = 'product-card';
|
| 372 |
div.innerHTML = `
|
| 373 |
+
<div class="product-img-wrapper" ${imgClick}>
|
| 374 |
+
<img src="${photoUrl}" class="product-img">
|
| 375 |
+
${photoIndicator}
|
| 376 |
+
</div>
|
| 377 |
<div class="product-info">
|
| 378 |
<div class="product-title">${p.name}</div>
|
| 379 |
<div class="product-bottom">
|
| 380 |
<div class="product-price">${p.price} ${currency}</div>
|
| 381 |
<div class="quantity-control">
|
| 382 |
+
<button onclick="updateCart('${p.product_id}', -1)"><i class="fas fa-minus" style="font-size:0.8rem;"></i></button>
|
| 383 |
<input type="text" id="qty-${p.product_id}" value="${qty}" readonly>
|
| 384 |
+
<button onclick="updateCart('${p.product_id}', 1)"><i class="fas fa-plus" style="font-size:0.8rem;"></i></button>
|
| 385 |
</div>
|
| 386 |
</div>
|
| 387 |
</div>
|
|
|
|
| 399 |
container.innerHTML = '';
|
| 400 |
|
| 401 |
const catProducts = products.filter(p => p.category === category);
|
| 402 |
+
if(catProducts.length === 0) {
|
| 403 |
+
container.innerHTML = '<div style="text-align:center; padding: 40px; color: var(--text-muted);">В этой категории пока нет товаров</div>';
|
| 404 |
+
} else {
|
| 405 |
+
catProducts.forEach(p => renderProductCard(p, container));
|
| 406 |
+
}
|
| 407 |
}
|
| 408 |
|
| 409 |
function updateCart(productId, change) {
|
|
|
|
| 451 |
list.innerHTML += `
|
| 452 |
<div class="cart-item">
|
| 453 |
<div class="cart-item-name">${item.name}</div>
|
| 454 |
+
<div class="cart-item-price">${item.quantity} x ${item.price} ${currency}</div>
|
| 455 |
</div>
|
| 456 |
`;
|
| 457 |
}
|
| 458 |
+
const modal = document.getElementById('cartModal');
|
| 459 |
+
modal.style.display = 'flex';
|
| 460 |
+
setTimeout(() => modal.classList.add('active'), 10);
|
| 461 |
}
|
| 462 |
|
| 463 |
function closeCartModal() {
|
| 464 |
+
const modal = document.getElementById('cartModal');
|
| 465 |
+
modal.classList.remove('active');
|
| 466 |
+
setTimeout(() => modal.style.display = 'none', 300);
|
| 467 |
}
|
| 468 |
|
| 469 |
function submitOrder() {
|
| 470 |
const cartArray = Object.values(cart);
|
| 471 |
+
if(cartArray.length === 0) return;
|
| 472 |
+
const btn = document.querySelector('.confirm-btn');
|
| 473 |
+
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Оформление...';
|
| 474 |
+
btn.disabled = true;
|
| 475 |
+
|
| 476 |
fetch('/create_order', {
|
| 477 |
method: 'POST',
|
| 478 |
headers: { 'Content-Type': 'application/json' },
|
|
|
|
| 484 |
cart = {};
|
| 485 |
window.location.href = `/order/${data.order_id}`;
|
| 486 |
}
|
| 487 |
+
})
|
| 488 |
+
.catch(() => {
|
| 489 |
+
btn.innerHTML = 'Оформить заказ';
|
| 490 |
+
btn.disabled = false;
|
| 491 |
+
alert('Произошла ошибка. Попробуйте еще раз.');
|
| 492 |
});
|
| 493 |
}
|
| 494 |
|
| 495 |
+
function openGallery(productId) {
|
| 496 |
+
const product = products.find(p => p.product_id === productId);
|
| 497 |
+
if (!product || !product.photos || product.photos.length === 0) return;
|
| 498 |
+
|
| 499 |
+
currentGalleryPhotos = product.photos.map(p => `https://huggingface.co/datasets/${repoId}/resolve/main/photos/${p}`);
|
| 500 |
+
currentGalleryIndex = 0;
|
| 501 |
+
|
| 502 |
+
document.getElementById('galleryModal').style.display = 'flex';
|
| 503 |
+
document.body.style.overflow = 'hidden';
|
| 504 |
+
updateGalleryView();
|
| 505 |
+
}
|
| 506 |
+
|
| 507 |
+
function closeGallery() {
|
| 508 |
+
document.getElementById('galleryModal').style.display = 'none';
|
| 509 |
+
document.body.style.overflow = '';
|
| 510 |
+
}
|
| 511 |
+
|
| 512 |
+
function updateGalleryView() {
|
| 513 |
+
document.getElementById('galleryImage').src = currentGalleryPhotos[currentGalleryIndex];
|
| 514 |
+
const dotsContainer = document.getElementById('galleryDots');
|
| 515 |
+
dotsContainer.innerHTML = '';
|
| 516 |
+
if(currentGalleryPhotos.length > 1) {
|
| 517 |
+
currentGalleryPhotos.forEach((_, index) => {
|
| 518 |
+
const dot = document.createElement('div');
|
| 519 |
+
dot.className = `gallery-dot ${index === currentGalleryIndex ? 'active' : ''}`;
|
| 520 |
+
dotsContainer.appendChild(dot);
|
| 521 |
+
});
|
| 522 |
+
document.querySelectorAll('.gallery-nav').forEach(el => el.style.display = 'flex');
|
| 523 |
+
} else {
|
| 524 |
+
document.querySelectorAll('.gallery-nav').forEach(el => el.style.display = 'none');
|
| 525 |
+
}
|
| 526 |
+
}
|
| 527 |
+
|
| 528 |
+
function nextPhoto(e) {
|
| 529 |
+
if(e) e.stopPropagation();
|
| 530 |
+
if(currentGalleryPhotos.length <= 1) return;
|
| 531 |
+
currentGalleryIndex = (currentGalleryIndex + 1) % currentGalleryPhotos.length;
|
| 532 |
+
updateGalleryView();
|
| 533 |
+
}
|
| 534 |
+
|
| 535 |
+
function prevPhoto(e) {
|
| 536 |
+
if(e) e.stopPropagation();
|
| 537 |
+
if(currentGalleryPhotos.length <= 1) return;
|
| 538 |
+
currentGalleryIndex = (currentGalleryIndex - 1 + currentGalleryPhotos.length) % currentGalleryPhotos.length;
|
| 539 |
+
updateGalleryView();
|
| 540 |
+
}
|
| 541 |
+
|
| 542 |
+
let touchstartX = 0;
|
| 543 |
+
let touchendX = 0;
|
| 544 |
+
const swipeArea = document.getElementById('gallerySwipeArea');
|
| 545 |
+
swipeArea.addEventListener('touchstart', e => { touchstartX = e.changedTouches[0].screenX; });
|
| 546 |
+
swipeArea.addEventListener('touchend', e => {
|
| 547 |
+
touchendX = e.changedTouches[0].screenX;
|
| 548 |
+
if (touchstartX - touchendX > 50) nextPhoto();
|
| 549 |
+
if (touchendX - touchstartX > 50) prevPhoto();
|
| 550 |
+
});
|
| 551 |
+
|
| 552 |
init();
|
| 553 |
</script>
|
| 554 |
</body>
|
|
|
|
| 560 |
<html lang="ru">
|
| 561 |
<head>
|
| 562 |
<meta charset="UTF-8">
|
| 563 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
|
| 564 |
<title>Накладная №{{ order.id }}</title>
|
| 565 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
| 566 |
<style>
|
| 567 |
+
:root { --bg: #f4f6f8; --surface: #ffffff; --text: #2d3436; --border: #dfe6e9; --wa: #25D366; --print: #1e272e; }
|
| 568 |
+
* { box-sizing: border-box; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }
|
| 569 |
+
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); }
|
| 570 |
+
.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; }
|
| 571 |
+
.header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 25px; border-bottom: 2px solid var(--border); padding-bottom: 15px; flex-wrap: wrap; gap: 10px; }
|
| 572 |
+
.header h1 { margin: 0; font-size: 1.8rem; font-weight: 800; }
|
| 573 |
+
.info-row { display: flex; justify-content: space-between; margin-bottom: 15px; font-size: 1rem; font-weight: 600; flex-wrap: wrap; gap: 10px; }
|
| 574 |
+
|
| 575 |
+
.table-responsive { width: 100%; overflow-x: auto; -webkit-overflow-scrolling: touch; margin-bottom: 20px; border-radius: 8px; border: 1px solid var(--border); }
|
| 576 |
+
table { width: 100%; border-collapse: collapse; min-width: 500px; }
|
| 577 |
+
th, td { border-bottom: 1px solid var(--border); padding: 12px; text-align: center; font-size: 0.95rem; }
|
| 578 |
+
th { background: #fafafa; font-weight: 700; color: #636e72; text-transform: uppercase; font-size: 0.8rem; letter-spacing: 0.5px; }
|
| 579 |
+
.img-cell img { width: 45px; height: 45px; object-fit: cover; border-radius: 8px; border: 1px solid #eee; }
|
| 580 |
+
.total-row { background: #fafafa; font-weight: 800; }
|
| 581 |
+
.total-row td { font-size: 1.1rem; border-bottom: none; }
|
| 582 |
|
| 583 |
+
.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; }
|
| 584 |
+
.action-bar-inner { display: flex; gap: 15px; width: 100%; max-width: 900px; }
|
| 585 |
+
.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; }
|
| 586 |
+
.btn:active { transform: scale(0.96); }
|
| 587 |
+
.btn-wa { background: var(--wa); box-shadow: 0 4px 15px rgba(37,211,102,0.3); }
|
| 588 |
+
.btn-print { background: var(--print); }
|
| 589 |
+
.btn-home { background: #0984e3; box-shadow: 0 4px 15px rgba(9,132,227,0.3); flex: 0 0 auto; padding: 15px 20px; }
|
| 590 |
|
| 591 |
@media print {
|
| 592 |
body { background: #fff; padding: 0; }
|
| 593 |
+
.invoice-box { box-shadow: none; padding: 0; max-width: 100%; border-radius: 0; }
|
| 594 |
+
.table-responsive { border: none; overflow: visible; }
|
| 595 |
+
table { min-width: 100%; }
|
| 596 |
+
th, td { border: 1px solid #000; }
|
| 597 |
.action-bar { display: none; }
|
| 598 |
}
|
| 599 |
+
@media (max-width: 600px) {
|
| 600 |
+
.header h1 { font-size: 1.4rem; }
|
| 601 |
+
.info-row { font-size: 0.9rem; }
|
| 602 |
+
.invoice-box { padding: 20px 15px; }
|
| 603 |
+
.btn { font-size: 0.9rem; flex-direction: column; padding: 10px; gap: 4px; }
|
| 604 |
+
.btn i { font-size: 1.2rem; }
|
| 605 |
+
}
|
| 606 |
</style>
|
| 607 |
</head>
|
| 608 |
<body>
|
| 609 |
<div class="invoice-box">
|
| 610 |
<div class="header">
|
|
|
|
|
|
|
|
|
|
| 611 |
<h1>Накладная</h1>
|
| 612 |
+
<div style="text-align: right;">
|
| 613 |
+
<div style="font-size: 1.1rem; font-weight: bold;">№ {{ order.id }}</div>
|
| 614 |
+
<div style="color: #636e72; font-size: 0.9rem;">{{ order.created_at.split(' ')[0] }}</div>
|
| 615 |
</div>
|
| 616 |
</div>
|
| 617 |
|
| 618 |
<div class="info-row">
|
| 619 |
+
<div>Покупатель: _________________</div>
|
| 620 |
+
<div>Статус: <span style="color: #00b894;">Новый</span></div>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 621 |
</div>
|
| 622 |
|
| 623 |
+
<div class="table-responsive">
|
| 624 |
+
<table>
|
| 625 |
+
<thead>
|
| 626 |
+
<tr>
|
| 627 |
+
<th style="width: 50px;">№</th>
|
| 628 |
+
<th style="text-align: left;">Наименование</th>
|
| 629 |
+
<th>Фото</th>
|
| 630 |
+
<th>Кол-во</th>
|
| 631 |
+
<th>Цена</th>
|
| 632 |
+
<th>Сумма</th>
|
| 633 |
+
</tr>
|
| 634 |
+
</thead>
|
| 635 |
+
<tbody>
|
| 636 |
+
{% for item in order.cart %}
|
| 637 |
+
<tr>
|
| 638 |
+
<td>{{ loop.index }}</td>
|
| 639 |
+
<td style="text-align: left; font-weight: 500;">{{ item.name }}</td>
|
| 640 |
+
<td class="img-cell"><img src="{{ item.photo_url }}" alt="img"></td>
|
| 641 |
+
<td>{{ item.quantity }}</td>
|
| 642 |
+
<td>{{ item.price }}</td>
|
| 643 |
+
<td>{{ item.price * item.quantity }}</td>
|
| 644 |
+
</tr>
|
| 645 |
+
{% endfor %}
|
| 646 |
+
<tr class="total-row">
|
| 647 |
+
<td colspan="5" style="text-align: right; padding-right: 20px;">Итого:</td>
|
| 648 |
+
<td>{{ order.total_price }} {{ currency_code }}</td>
|
| 649 |
+
</tr>
|
| 650 |
+
</tbody>
|
| 651 |
+
</table>
|
| 652 |
+
</div>
|
| 653 |
</div>
|
| 654 |
|
| 655 |
<div class="action-bar">
|
| 656 |
+
<div class="action-bar-inner">
|
| 657 |
+
<a href="/" class="btn btn-home"><i class="fas fa-home"></i></a>
|
| 658 |
+
<button class="btn btn-print" onclick="window.print()"><i class="fas fa-print"></i> Печать</button>
|
| 659 |
+
<button class="btn btn-wa" onclick="sendToWA()"><i class="fab fa-whatsapp" style="font-size: 1.2rem;"></i> WhatsApp</button>
|
| 660 |
+
</div>
|
| 661 |
</div>
|
| 662 |
|
| 663 |
<script>
|
| 664 |
function sendToWA() {
|
| 665 |
+
let msg = `Здравствуйте! Мой заказ №{{ order.id }}\nНакладная: ${window.location.href}`;
|
| 666 |
window.open(`https://api.whatsapp.com/send?phone={{ whatsapp_number }}&text=${encodeURIComponent(msg)}`, '_blank');
|
| 667 |
}
|
| 668 |
</script>
|
|
|
|
| 675 |
<html lang="ru">
|
| 676 |
<head>
|
| 677 |
<meta charset="UTF-8">
|
| 678 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
|
| 679 |
<title>Админ-панель</title>
|
| 680 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
| 681 |
<style>
|
| 682 |
+
:root { --primary: #2d3436; --bg: #f4f6f9; --surface: #ffffff; --border: #e0e6ed; --danger: #ff7675; --success: #00b894; --info: #0984e3; }
|
| 683 |
+
* { box-sizing: border-box; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }
|
| 684 |
+
body { background: var(--bg); padding: max(20px, env(safe-area-inset-top)) 15px calc(20px + env(safe-area-inset-bottom)); margin: 0; color: #2d3436; }
|
| 685 |
+
.container { max-width: 1000px; margin: 0 auto; }
|
| 686 |
+
|
| 687 |
+
.header-panel { background: var(--surface); padding: 20px; border-radius: 16px; box-shadow: 0 4px 15px rgba(0,0,0,0.03); margin-bottom: 20px; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 15px; }
|
| 688 |
+
.header-panel h1 { margin: 0; font-size: 1.5rem; font-weight: 800; }
|
| 689 |
+
.btn { padding: 12px 20px; border: none; border-radius: 10px; font-weight: 600; cursor: pointer; color: #fff; text-decoration: none; display: inline-flex; align-items: center; gap: 8px; font-size: 0.95rem; transition: opacity 0.2s; }
|
| 690 |
+
.btn:active { opacity: 0.8; }
|
| 691 |
+
.btn-primary { background: var(--info); }
|
| 692 |
+
.btn-success { background: var(--success); }
|
| 693 |
+
.btn-danger { background: var(--danger); padding: 8px 15px; font-size: 0.85rem; }
|
| 694 |
+
.btn-dark { background: var(--primary); }
|
| 695 |
+
|
| 696 |
+
.sync-panel { display: flex; gap: 10px; margin-bottom: 25px; flex-wrap: wrap; }
|
| 697 |
+
.sync-panel form { flex: 1; min-width: 200px; }
|
| 698 |
+
.sync-panel button { width: 100%; }
|
| 699 |
+
|
| 700 |
+
.card { background: var(--surface); padding: 20px; border-radius: 16px; box-shadow: 0 4px 15px rgba(0,0,0,0.03); margin-bottom: 20px; }
|
| 701 |
+
.card h2 { margin-top: 0; margin-bottom: 15px; font-size: 1.2rem; }
|
| 702 |
+
|
| 703 |
+
input[type="text"], input[type="number"], select { width: 100%; padding: 12px 15px; border: 1px solid var(--border); border-radius: 10px; font-size: 0.95rem; outline: none; transition: border-color 0.2s; background: #fafafa; }
|
| 704 |
+
input[type="text"]:focus, input[type="number"]:focus { border-color: var(--info); background: #fff; }
|
| 705 |
+
|
| 706 |
+
.add-cat-form { display: flex; gap: 10px; flex-wrap: wrap; }
|
| 707 |
+
.add-cat-form input { flex: 1; min-width: 200px; }
|
| 708 |
+
.add-cat-form button { white-space: nowrap; }
|
| 709 |
+
|
| 710 |
+
.category-block { border: 1px solid var(--border); margin-bottom: 15px; border-radius: 12px; overflow: hidden; background: #fff; }
|
| 711 |
+
.category-header { background: #fafafa; padding: 15px 20px; font-weight: 700; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid var(--border); }
|
| 712 |
+
.category-content { padding: 0; }
|
| 713 |
+
|
| 714 |
+
.product-item { display: flex; justify-content: space-between; align-items: center; padding: 15px 20px; border-bottom: 1px solid var(--border); flex-wrap: wrap; gap: 10px; }
|
| 715 |
+
.product-item:last-child { border-bottom: none; }
|
| 716 |
+
.product-info { display: flex; align-items: center; gap: 15px; min-width: 250px; flex: 1; }
|
| 717 |
+
.product-img { width: 50px; height: 50px; object-fit: cover; border-radius: 8px; border: 1px solid #eee; background: #fafafa; }
|
| 718 |
+
.product-details { display: flex; flex-direction: column; }
|
| 719 |
+
.product-name { font-weight: 600; font-size: 0.95rem; }
|
| 720 |
+
.product-meta { font-size: 0.8rem; color: #636e72; margin-top: 4px; }
|
| 721 |
+
|
| 722 |
+
.add-product-form { background: #fdfdfd; padding: 20px; border-top: 1px dashed var(--border); display: flex; flex-direction: column; gap: 15px; }
|
| 723 |
+
.form-row { display: flex; gap: 10px; flex-wrap: wrap; }
|
| 724 |
+
.form-row > * { flex: 1; min-width: 150px; }
|
| 725 |
+
|
| 726 |
+
.file-input-wrapper { position: relative; width: 100%; }
|
| 727 |
+
input[type="file"] { width: 100%; padding: 10px; border: 1px dashed #ccc; border-radius: 10px; background: #fafafa; font-size: 0.9rem; }
|
| 728 |
+
|
| 729 |
+
@media (max-width: 600px) {
|
| 730 |
+
.header-panel { flex-direction: column; align-items: stretch; text-align: center; }
|
| 731 |
+
.product-item { flex-direction: column; align-items: stretch; }
|
| 732 |
+
.product-info { width: 100%; }
|
| 733 |
+
.product-item form { align-self: flex-end; }
|
| 734 |
+
.form-row { flex-direction: column; }
|
| 735 |
+
}
|
| 736 |
</style>
|
| 737 |
</head>
|
| 738 |
<body>
|
| 739 |
<div class="container">
|
| 740 |
+
<div class="header-panel">
|
| 741 |
+
<h1><i class="fas fa-cog"></i> Админ-панель</h1>
|
| 742 |
+
<a href="/" class="btn btn-primary"><i class="fas fa-store"></i> В каталог</a>
|
| 743 |
</div>
|
| 744 |
|
| 745 |
+
<div class="sync-panel">
|
| 746 |
+
<form method="POST" action="/force_upload" onsubmit="showLoading(this)">
|
| 747 |
+
<button type="submit" class="btn btn-success"><i class="fas fa-cloud-upload-alt"></i> Сохранить на сервер</button>
|
| 748 |
+
</form>
|
| 749 |
+
<form method="POST" action="/force_download" onsubmit="showLoading(this)">
|
| 750 |
+
<button type="submit" class="btn btn-info" style="background:#0984e3;"><i class="fas fa-cloud-download-alt"></i> Скачать с сервера</button>
|
| 751 |
+
</form>
|
| 752 |
</div>
|
| 753 |
|
| 754 |
+
<div class="card">
|
| 755 |
+
<h2>Управление категориями</h2>
|
| 756 |
+
<form method="POST" class="add-cat-form">
|
| 757 |
+
<input type="hidden" name="action" value="add_category">
|
| 758 |
+
<input type="text" name="category_name" placeholder="Название новой категории" required autocomplete="off">
|
| 759 |
+
<button type="submit" class="btn btn-dark"><i class="fas fa-plus"></i> Добавить</button>
|
| 760 |
+
</form>
|
| 761 |
+
</div>
|
| 762 |
|
| 763 |
{% for category in categories %}
|
| 764 |
<div class="category-block">
|
| 765 |
<div class="category-header">
|
| 766 |
+
<span><i class="fas fa-folder-open" style="color:var(--info); margin-right:8px;"></i> {{ category }}</span>
|
| 767 |
+
<form method="POST" style="margin:0;" onsubmit="return confirm('Удалить категорию и все ее товары?');">
|
| 768 |
<input type="hidden" name="action" value="delete_category">
|
| 769 |
<input type="hidden" name="category_name" value="{{ category }}">
|
| 770 |
+
<button type="submit" class="btn btn-danger"><i class="fas fa-trash-alt"></i></button>
|
| 771 |
</form>
|
| 772 |
</div>
|
| 773 |
+
<div class="category-content">
|
| 774 |
|
| 775 |
{% for product in products %}
|
| 776 |
{% if product.category == category %}
|
| 777 |
<div class="product-item">
|
| 778 |
+
<div class="product-info">
|
| 779 |
+
{% if product.photos and product.photos|length > 0 %}
|
| 780 |
+
<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ product.photos[0] }}" class="product-img">
|
| 781 |
+
{% else %}
|
| 782 |
+
<div class="product-img" style="display:flex;align-items:center;justify-content:center;color:#ccc;"><i class="fas fa-image"></i></div>
|
| 783 |
+
{% endif %}
|
| 784 |
+
<div class="product-details">
|
| 785 |
+
<span class="product-name">{{ product.name }}</span>
|
| 786 |
+
<span class="product-meta">{{ product.price }} {{ currency_code }} • Фото: {{ product.photos|length if product.photos else 0 }}/10</span>
|
| 787 |
+
</div>
|
| 788 |
</div>
|
| 789 |
<form method="POST" style="margin:0;" onsubmit="return confirm('Удалить товар?');">
|
| 790 |
<input type="hidden" name="action" value="delete_product">
|
| 791 |
<input type="hidden" name="product_id" value="{{ product.product_id }}">
|
| 792 |
+
<button type="submit" class="btn btn-danger"><i class="fas fa-times"></i> Удалить</button>
|
| 793 |
</form>
|
| 794 |
</div>
|
| 795 |
{% endif %}
|
| 796 |
{% endfor %}
|
| 797 |
|
| 798 |
+
<form class="add-product-form" method="POST" enctype="multipart/form-data" onsubmit="showLoading(this)">
|
| 799 |
<input type="hidden" name="action" value="add_product">
|
| 800 |
<input type="hidden" name="category" value="{{ category }}">
|
| 801 |
+
<div style="font-weight: 600; font-size: 0.9rem; color: #636e72;">Добавить товар в "{{ category }}"</div>
|
| 802 |
+
<div class="form-row">
|
| 803 |
+
<input type="text" name="name" placeholder="Название товара" required autocomplete="off" style="flex:2;">
|
| 804 |
+
<input type="number" name="price" placeholder="Цена" required step="0.01" style="flex:1;">
|
| 805 |
</div>
|
| 806 |
+
<div class="file-input-wrapper">
|
| 807 |
+
<input type="file" name="photos" accept="image/*" multiple max="10" required>
|
| 808 |
+
<div style="font-size: 0.8rem; color: #999; margin-top: 5px;">Можно выбрать до 10 фото</div>
|
| 809 |
</div>
|
| 810 |
+
<button type="submit" class="btn btn-success" style="width: 100%; justify-content: center;"><i class="fas fa-plus-circle"></i> Добавить товар</button>
|
| 811 |
</form>
|
| 812 |
</div>
|
| 813 |
</div>
|
| 814 |
{% endfor %}
|
| 815 |
</div>
|
| 816 |
+
<script>
|
| 817 |
+
function showLoading(form) {
|
| 818 |
+
const btn = form.querySelector('button[type="submit"]');
|
| 819 |
+
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Загрузка...';
|
| 820 |
+
btn.style.pointerEvents = 'none';
|
| 821 |
+
btn.style.opacity = '0.7';
|
| 822 |
+
}
|
| 823 |
+
</script>
|
| 824 |
</body>
|
| 825 |
</html>
|
| 826 |
'''
|
|
|
|
| 854 |
"name": item['name'],
|
| 855 |
"price": float(item['price']),
|
| 856 |
"quantity": int(item['quantity']),
|
| 857 |
+
"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+"
|
| 858 |
})
|
| 859 |
|
| 860 |
order_id = f"SA-{datetime.now().strftime('%Y%m%d')}-{str(len(load_data().get('orders', {}))+1).zfill(3)}"
|
|
|
|
| 914 |
name = request.form.get('name', '').strip()
|
| 915 |
price = float(request.form.get('price', 0))
|
| 916 |
category = request.form.get('category')
|
| 917 |
+
uploaded_photos = request.files.getlist('photos')[:10]
|
| 918 |
|
| 919 |
photos_list = []
|
| 920 |
+
if uploaded_photos and HF_TOKEN_WRITE:
|
| 921 |
uploads_dir = 'uploads_temp'
|
| 922 |
os.makedirs(uploads_dir, exist_ok=True)
|
| 923 |
+
api = HfApi()
|
| 924 |
+
for photo in uploaded_photos:
|
| 925 |
+
if photo and photo.filename:
|
| 926 |
+
ext = os.path.splitext(photo.filename)[1].lower()
|
| 927 |
+
if ext not in ['.jpg', '.jpeg', '.png', '.webp', '.gif']:
|
| 928 |
+
continue
|
| 929 |
+
photo_filename = f"{uuid4().hex}{ext}"
|
| 930 |
+
temp_path = os.path.join(uploads_dir, photo_filename)
|
| 931 |
+
photo.save(temp_path)
|
| 932 |
+
try:
|
| 933 |
+
api.upload_file(
|
| 934 |
+
path_or_fileobj=temp_path,
|
| 935 |
+
path_in_repo=f"photos/{photo_filename}",
|
| 936 |
+
repo_id=REPO_ID,
|
| 937 |
+
repo_type="dataset",
|
| 938 |
+
token=HF_TOKEN_WRITE
|
| 939 |
+
)
|
| 940 |
+
photos_list.append(photo_filename)
|
| 941 |
+
except Exception:
|
| 942 |
+
pass
|
| 943 |
+
finally:
|
| 944 |
+
if os.path.exists(temp_path):
|
| 945 |
+
os.remove(temp_path)
|
| 946 |
|
| 947 |
new_product = {
|
| 948 |
'product_id': uuid4().hex,
|
|
|
|
| 988 |
threading.Thread(target=periodic_backup, daemon=True).start()
|
| 989 |
|
| 990 |
port = int(os.environ.get('PORT', 7860))
|
| 991 |
+
app.run(host='0.0.0.0', port=port)
|