434 lines
16 KiB
HTML
434 lines
16 KiB
HTML
{% extends "base.html" %}
|
||
|
||
{% block title %}Моя корзина{% endblock %}
|
||
|
||
{% block content %}
|
||
<div class="container">
|
||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||
<h2 class="page-title">Моя корзина</h2>
|
||
<a href="{{ url_for('catalog') }}" class="btn btn-primary">
|
||
<i class="fas fa-arrow-left"></i> Продолжить покупки
|
||
</a>
|
||
</div>
|
||
|
||
{% if cart_items %}
|
||
<!-- Таблица товаров в корзине -->
|
||
<div class="table-responsive">
|
||
<table class="table table-hover" id="cartTable">
|
||
<thead>
|
||
<tr>
|
||
<th>Товар</th>
|
||
<th>Артикул</th>
|
||
<th>Категория</th>
|
||
<th>Материал</th>
|
||
<th>Цена</th>
|
||
<th style="text-align: center;">Количество</th>
|
||
<th>Сумма</th>
|
||
<th>Действия</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="cartBody">
|
||
{% for item in cart_items %}
|
||
{% set item_total = item.price * item.quantity %}
|
||
<tr id="item-{{ item.id }}" data-price="{{ item.price }}" data-max="{{ item.in_stock }}">
|
||
<td>{{ item.product_name }}</td>
|
||
<td>{{ item.article }}</td>
|
||
<td>{{ item.category_name }}</td>
|
||
<td>{{ item.material_name or '-' }}</td>
|
||
<td class="item-price">{{ item.price|int }} руб.</td>
|
||
<td style="text-align: center;">
|
||
<div class="quantity-control">
|
||
<button type="button" class="quantity-btn quantity-minus" data-item-id="{{ item.id }}">−</button>
|
||
<input type="number" class="quantity-input"
|
||
value="{{ item.quantity }}" min="1" max="{{ item.in_stock }}"
|
||
data-item-id="{{ item.id }}"
|
||
id="quantity-{{ item.id }}"
|
||
onkeypress="return isNumberKey(event)">
|
||
<button type="button" class="quantity-btn quantity-plus" data-item-id="{{ item.id }}">+</button>
|
||
<span class="stock-error" id="error-{{ item.id }}" style="display: none;">
|
||
Макс. {{ item.in_stock }}
|
||
</span>
|
||
</div>
|
||
</td>
|
||
<td class="item-total fw-bold">{{ item_total|int }} руб.</td>
|
||
<td>
|
||
<button class="btn btn-sm btn-outline-danger remove-item"
|
||
data-item-id="{{ item.id }}"
|
||
data-item-name="{{ item.product_name }}">
|
||
<i class="fas fa-trash"></i>
|
||
</button>
|
||
</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<!-- Расчет стоимости -->
|
||
<div class="row mt-4">
|
||
<div class="col-md-6 offset-md-6">
|
||
<div class="card">
|
||
<div class="card-header" style="background: linear-gradient(135deg, var(--steel-blue) 0%, var(--light-steel-blue) 100%); color: white;">
|
||
<h5 class="mb-0"><i class="fas fa-calculator"></i> Итого по заказу</h5>
|
||
</div>
|
||
<div class="card-body">
|
||
{% set ns = namespace(subtotal=0) %}
|
||
{% for item in cart_items %}
|
||
{% set ns.subtotal = ns.subtotal + (item.price * item.quantity) %}
|
||
{% endfor %}
|
||
|
||
{% set assembly_percent = 15 %}
|
||
{% set delivery_percent = 10 %}
|
||
{% set assembly_amount = ns.subtotal * assembly_percent // 100 %}
|
||
{% set delivery_amount = ns.subtotal * delivery_percent // 100 %}
|
||
{% set total = ns.subtotal + assembly_amount + delivery_amount %}
|
||
|
||
<div class="row mb-2">
|
||
<div class="col-6">Сумма без услуг:</div>
|
||
<div class="col-6 text-end fw-bold" id="cart-subtotal">{{ ns.subtotal|int }} руб.</div>
|
||
</div>
|
||
<div class="row mb-2">
|
||
<div class="col-6">Сборка ({{ assembly_percent }}%):</div>
|
||
<div class="col-6 text-end text-info" id="assembly-amount">+{{ assembly_amount|int }} руб.</div>
|
||
</div>
|
||
<div class="row mb-2">
|
||
<div class="col-6">Доставка ({{ delivery_percent }}%):</div>
|
||
<div class="col-6 text-end text-info" id="delivery-amount">+{{ delivery_amount|int }} руб.</div>
|
||
</div>
|
||
<hr>
|
||
<div class="row">
|
||
<div class="col-6 fs-5 fw-bold">ИТОГО:</div>
|
||
<div class="col-6 text-end fs-5 fw-bold" style="color: var(--steel-blue);" id="cart-total">{{ total|int }} руб.</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Кнопка оформления заказа -->
|
||
<div class="text-end mt-4">
|
||
<form method="POST" action="{{ url_for('checkout') }}" id="checkoutForm">
|
||
<button type="submit" class="btn btn-success btn-lg"
|
||
onclick="return confirm('Оформить заказ?')">
|
||
<i class="fas fa-check"></i> Оформить заказ ({{ total|int }} руб.)
|
||
</button>
|
||
</form>
|
||
</div>
|
||
|
||
{% else %}
|
||
<!-- Пустая корзина -->
|
||
<div class="text-center py-5">
|
||
<i class="fas fa-shopping-cart fa-4x mb-3" style="color: var(--light-steel-blue);"></i>
|
||
<h4>Корзина пуста</h4>
|
||
<p class="text-muted mb-4">Добавьте товары в корзину, чтобы оформить заказ</p>
|
||
<a href="{{ url_for('catalog') }}" class="btn btn-primary btn-lg">
|
||
<i class="fas fa-arrow-left"></i> Перейти в каталог
|
||
</a>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<!-- JavaScript -->
|
||
<script>
|
||
function isNumberKey(evt) {
|
||
var charCode = (evt.which) ? evt.which : evt.keyCode;
|
||
if (charCode == 8 || charCode == 46 || charCode == 9 || charCode == 13 ||
|
||
charCode == 37 || charCode == 38 || charCode == 39 || charCode == 40) {
|
||
return true;
|
||
}
|
||
if (charCode >= 48 && charCode <= 57) {
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
const assemblyPercent = 15;
|
||
const deliveryPercent = 10;
|
||
|
||
function updateAllTotals() {
|
||
let subtotal = 0;
|
||
document.querySelectorAll('.item-total').forEach(cell => {
|
||
const value = parseFloat(cell.textContent) || 0;
|
||
subtotal += value;
|
||
});
|
||
|
||
const assemblyAmount = Math.round(subtotal * assemblyPercent / 100);
|
||
const deliveryAmount = Math.round(subtotal * deliveryPercent / 100);
|
||
const total = subtotal + assemblyAmount + deliveryAmount;
|
||
|
||
const subtotalEl = document.getElementById('cart-subtotal');
|
||
const assemblyEl = document.getElementById('assembly-amount');
|
||
const deliveryEl = document.getElementById('delivery-amount');
|
||
const totalEl = document.getElementById('cart-total');
|
||
const checkoutBtn = document.querySelector('#checkoutForm button');
|
||
|
||
if (subtotalEl) subtotalEl.textContent = subtotal.toFixed(0) + ' руб.';
|
||
if (assemblyEl) assemblyEl.textContent = '+' + assemblyAmount.toFixed(0) + ' руб.';
|
||
if (deliveryEl) deliveryEl.textContent = '+' + deliveryAmount.toFixed(0) + ' руб.';
|
||
if (totalEl) totalEl.textContent = total.toFixed(0) + ' руб.';
|
||
|
||
if (checkoutBtn) {
|
||
checkoutBtn.innerHTML = `<i class="fas fa-check"></i> Оформить заказ (${total.toFixed(0)} руб.)`;
|
||
}
|
||
}
|
||
|
||
function updateQuantity(itemId, newQuantity) {
|
||
const input = document.getElementById(`quantity-${itemId}`);
|
||
const row = document.getElementById(`item-${itemId}`);
|
||
const price = parseFloat(row.dataset.price);
|
||
const maxStock = parseInt(row.dataset.max);
|
||
|
||
if (newQuantity < 1) newQuantity = 1;
|
||
if (newQuantity > maxStock) {
|
||
newQuantity = maxStock;
|
||
const error = document.getElementById(`error-${itemId}`);
|
||
if (error) {
|
||
error.style.display = 'block';
|
||
setTimeout(() => {
|
||
error.style.display = 'none';
|
||
}, 2000);
|
||
}
|
||
}
|
||
|
||
input.value = newQuantity;
|
||
const total = price * newQuantity;
|
||
const totalCell = row.querySelector('.item-total');
|
||
if (totalCell) totalCell.textContent = total.toFixed(0) + ' руб.';
|
||
|
||
fetch(`/update_cart/${itemId}`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/x-www-form-urlencoded',
|
||
},
|
||
body: `quantity=${newQuantity}`
|
||
})
|
||
.then(response => {
|
||
if (response.ok) {
|
||
updateAllTotals();
|
||
}
|
||
})
|
||
.catch(error => console.error('Error:', error));
|
||
}
|
||
|
||
// Обработчики для кнопок
|
||
document.querySelectorAll('.quantity-minus').forEach(btn => {
|
||
btn.addEventListener('click', function() {
|
||
const itemId = this.dataset.itemId;
|
||
const input = document.getElementById(`quantity-${itemId}`);
|
||
if (input) {
|
||
let currentValue = parseInt(input.value) || 1;
|
||
updateQuantity(itemId, currentValue - 1);
|
||
}
|
||
});
|
||
});
|
||
|
||
document.querySelectorAll('.quantity-plus').forEach(btn => {
|
||
btn.addEventListener('click', function() {
|
||
const itemId = this.dataset.itemId;
|
||
const input = document.getElementById(`quantity-${itemId}`);
|
||
if (input) {
|
||
let currentValue = parseInt(input.value) || 1;
|
||
updateQuantity(itemId, currentValue + 1);
|
||
}
|
||
});
|
||
});
|
||
|
||
document.querySelectorAll('.quantity-input').forEach(input => {
|
||
input.addEventListener('paste', function(e) {
|
||
e.preventDefault();
|
||
return false;
|
||
});
|
||
|
||
let timeoutId;
|
||
input.addEventListener('input', function() {
|
||
this.value = this.value.replace(/[^0-9]/g, '');
|
||
|
||
const itemId = this.dataset.itemId;
|
||
const row = document.getElementById(`item-${itemId}`);
|
||
if (!row) return;
|
||
|
||
const price = parseFloat(row.dataset.price);
|
||
let quantity = parseInt(this.value);
|
||
|
||
if (isNaN(quantity) || quantity < 1) {
|
||
quantity = 1;
|
||
this.value = 1;
|
||
}
|
||
|
||
const total = price * quantity;
|
||
const totalCell = row.querySelector('.item-total');
|
||
if (totalCell) totalCell.textContent = total.toFixed(0) + ' руб.';
|
||
|
||
clearTimeout(timeoutId);
|
||
timeoutId = setTimeout(() => {
|
||
updateQuantity(itemId, quantity);
|
||
}, 500);
|
||
});
|
||
});
|
||
|
||
document.querySelectorAll('.remove-item').forEach(btn => {
|
||
btn.addEventListener('click', function() {
|
||
const itemId = this.dataset.itemId;
|
||
const itemName = this.dataset.itemName;
|
||
if (confirm(`Удалить "${itemName}" из корзины?`)) {
|
||
fetch(`/remove_from_cart/${itemId}`, {
|
||
method: 'POST'
|
||
})
|
||
.then(() => {
|
||
location.reload();
|
||
});
|
||
}
|
||
});
|
||
});
|
||
|
||
updateAllTotals();
|
||
});
|
||
</script>
|
||
|
||
<style>
|
||
.page-title {
|
||
color: var(--steel-blue);
|
||
font-size: 2rem;
|
||
font-weight: 600;
|
||
font-family: 'Franklin Gothic', 'Arial Narrow', Arial, sans-serif;
|
||
}
|
||
|
||
.table thead th {
|
||
background-color: var(--steel-blue);
|
||
color: white;
|
||
font-weight: 600;
|
||
border: none;
|
||
padding: 12px;
|
||
font-family: 'Franklin Gothic', 'Arial Narrow', Arial, sans-serif;
|
||
}
|
||
|
||
.table tbody tr:hover {
|
||
background-color: rgba(176, 196, 222, 0.2);
|
||
}
|
||
|
||
.table tbody td {
|
||
font-family: 'Franklin Gothic', 'Arial Narrow', Arial, sans-serif;
|
||
color: #2c3e50;
|
||
vertical-align: middle;
|
||
}
|
||
|
||
.quantity-control {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 5px;
|
||
position: relative;
|
||
}
|
||
|
||
.quantity-btn {
|
||
width: 30px;
|
||
height: 30px;
|
||
border: 1px solid var(--light-steel-blue);
|
||
background: linear-gradient(135deg, var(--alice-blue) 0%, #E6F2FF 100%);
|
||
color: var(--steel-blue);
|
||
font-size: 1.2rem;
|
||
font-weight: bold;
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.quantity-btn:hover {
|
||
background: linear-gradient(135deg, var(--light-steel-blue) 0%, #9fb6d4 100%);
|
||
border-color: var(--steel-blue);
|
||
}
|
||
|
||
.quantity-input {
|
||
width: 60px;
|
||
height: 30px;
|
||
border: 2px solid var(--light-steel-blue);
|
||
border-radius: 6px;
|
||
text-align: center;
|
||
font-size: 1rem;
|
||
font-family: 'Franklin Gothic', 'Arial Narrow', Arial, sans-serif;
|
||
padding: 0;
|
||
margin: 0;
|
||
-moz-appearance: textfield;
|
||
appearance: textfield;
|
||
}
|
||
|
||
.quantity-input:focus {
|
||
border-color: var(--steel-blue);
|
||
box-shadow: 0 0 0 0.2rem rgba(70, 130, 180, 0.25);
|
||
outline: none;
|
||
}
|
||
|
||
.stock-error {
|
||
color: #dc3545;
|
||
font-size: 0.75rem;
|
||
position: absolute;
|
||
bottom: -20px;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
white-space: nowrap;
|
||
background-color: white;
|
||
padding: 2px 8px;
|
||
border-radius: 12px;
|
||
border: 1px solid #dc3545;
|
||
z-index: 10;
|
||
}
|
||
|
||
.btn-success {
|
||
background: linear-gradient(135deg, #28a745 0%, #218838 100%);
|
||
border: none;
|
||
padding: 12px 30px;
|
||
font-size: 1.1rem;
|
||
}
|
||
|
||
.btn-success:hover {
|
||
background: linear-gradient(135deg, #218838 0%, #28a745 100%);
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 4px 15px rgba(40, 167, 69, 0.3);
|
||
}
|
||
|
||
.btn-primary {
|
||
background: linear-gradient(135deg, var(--steel-blue) 0%, #3a6d99 100%);
|
||
border: none;
|
||
padding: 12px 30px;
|
||
font-size: 1.1rem;
|
||
}
|
||
|
||
.btn-primary:hover {
|
||
background: linear-gradient(135deg, #3a6d99 0%, var(--steel-blue) 100%);
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 4px 15px rgba(70, 130, 180, 0.3);
|
||
}
|
||
|
||
.card {
|
||
border: 1px solid var(--light-steel-blue);
|
||
border-radius: 12px;
|
||
box-shadow: 0 4px 8px rgba(70, 130, 180, 0.1);
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.table {
|
||
font-size: 0.9rem;
|
||
}
|
||
|
||
.quantity-control {
|
||
gap: 3px;
|
||
}
|
||
|
||
.quantity-btn {
|
||
width: 25px;
|
||
height: 25px;
|
||
font-size: 1rem;
|
||
}
|
||
|
||
.quantity-input {
|
||
width: 45px;
|
||
height: 25px;
|
||
font-size: 0.9rem;
|
||
}
|
||
}
|
||
</style>
|
||
{% endblock %} |