diff --git a/public/assets/css/app.css b/public/assets/css/app.css index 3d9b107..568b358 100644 --- a/public/assets/css/app.css +++ b/public/assets/css/app.css @@ -496,7 +496,14 @@ body { line-height: 1; color: #ffffff; background: var(--brand-red); - white-space: nowrap; + text-align: center; + white-space: normal; +} + +.price-mode-button-qr b { + max-width: 190px; + line-height: 1.1; + background: var(--brand-green-2); } .price-mode-button-cash { @@ -1817,6 +1824,20 @@ body[data-payment-mode="cash"] .price-tier-popover:hover { margin-bottom: 0; } +.cart-weight-notice { + max-width: 980px; + border: 1px solid rgba(196, 61, 49, 0.24); + border-left: 5px solid var(--brand-red); + border-radius: 8px; + margin-top: 18px; + padding: 14px 16px; + color: var(--brand-green); + font-size: 15px; + font-weight: 850; + line-height: 1.45; + background: #fff8f4; +} + .cart-layout { display: grid; grid-template-columns: minmax(0, 1fr) minmax(300px, 0.36fr); @@ -1825,11 +1846,20 @@ body[data-payment-mode="cash"] .price-tier-popover:hover { margin-top: 18px; } +.cart-layout-checkout { + grid-template-columns: minmax(0, 1fr) minmax(320px, 0.38fr); +} + .cart-items-panel, .cart-summary { padding: 20px; } +.cart-summary { + position: sticky; + top: 182px; +} + .cart-empty { margin: 0; color: var(--muted); @@ -1838,6 +1868,7 @@ body[data-payment-mode="cash"] .price-tier-popover:hover { .cart-items { display: grid; gap: 10px; + margin-bottom: 18px; } .cart-row { @@ -1966,10 +1997,215 @@ body[data-payment-mode="cash"] .price-tier-popover:hover { color: var(--muted); } -.cart-summary-action { +.cart-checkout-panel { + display: grid; + gap: 16px; + border: 1px solid var(--line); + border-radius: 8px; + margin-top: 14px; + padding: 18px; + background: #fbfcf8; +} + +.cart-payment-options, +.cart-delivery-options { + display: flex; + flex-wrap: wrap; + gap: 10px; +} + +.cart-payment-trust { + width: fit-content; + border-radius: 999px; + margin-bottom: -4px; + padding: 7px 12px; + color: #ffffff; + font-size: 13px; + font-weight: 950; + letter-spacing: 0.01em; + background: linear-gradient(135deg, var(--brand-green), var(--brand-green-2)); + box-shadow: 0 8px 18px rgba(23, 61, 44, 0.12); +} + +.cart-invoice-details { + display: grid; + gap: 12px; + max-height: 0; + overflow: hidden; + border: 0 solid transparent; + border-radius: 8px; + padding: 0 14px; + opacity: 0; + background: #f7faf4; + transition: max-height 0.28s ease, padding 0.28s ease, opacity 0.2s ease, border-color 0.2s ease; +} + +body[data-payment-mode="invoice"] .cart-invoice-details { + max-height: 520px; + border-width: 1px; + border-color: #dfe8dc; + padding: 14px; + opacity: 1; +} + +.cart-invoice-details p { + margin: 0; + color: var(--muted); + font-size: 14px; +} + +.cart-invoice-details label { + display: grid; + gap: 7px; + font-size: 14px; + font-weight: 850; + color: #394437; +} + +.cart-delivery-options label { + display: inline-flex; + gap: 8px; + align-items: center; + min-height: 42px; + border: 1px solid #cfd8c9; + border-radius: 999px; + padding: 9px 14px; + font-weight: 900; + color: var(--brand-green); + background: #ffffff; + cursor: pointer; + user-select: none; + transition: border-color 0.16s ease, background 0.16s ease, color 0.16s ease, transform 0.16s ease; +} + +.cart-delivery-options label:hover { + border-color: var(--brand-green); + background: #f2f7ef; + transform: translateY(-1px); +} + +.cart-delivery-options label:has(input:checked) { + color: #ffffff; + border-color: var(--brand-green); + background: var(--brand-green); +} + +.cart-delivery-options input { + flex: 0 0 auto; +} + +.cart-minimum-warning { + border: 1px solid rgba(196, 61, 49, 0.24); + border-radius: 8px; + margin: 0; + padding: 12px 14px; + color: #7c2118; + font-size: 15px; + font-weight: 850; + background: #fff1ee; +} + +.cart-delivery-fields, +.cart-form-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px; +} + +.cart-delivery-fields label, +.cart-order-form label { + display: grid; + gap: 7px; + font-size: 14px; + font-weight: 850; + color: #394437; +} + +.cart-lift-label { + grid-template-columns: auto minmax(0, 1fr); + align-items: center; + align-self: end; + min-height: 44px; + border: 1px solid #cfd8c9; + border-radius: 8px; + padding: 12px; + background: #ffffff; + cursor: pointer; + user-select: none; +} + +.cart-lift-label input { + width: auto; +} + +.cart-lift-label:has(input:checked) { + border-color: var(--brand-green); + background: #eef6e9; +} + +.cart-final-sum-label { + grid-template-columns: auto minmax(0, 1fr); + align-items: start; + border: 1px solid #dfe8dc; + border-radius: 8px; + padding: 12px; + background: #f7faf4; + cursor: pointer; +} + +.cart-final-sum-label input { + width: auto; + margin-top: 3px; +} + +.cart-delivery-fields input, +.cart-order-form input, +.cart-order-form textarea, +.cart-invoice-details textarea, +.cart-invoice-details input { width: 100%; - justify-content: center; - margin-top: 10px; + border: 1px solid #cfd8c9; + border-radius: 8px; + padding: 12px; + font: inherit; + background: #ffffff; +} + +.cart-order-form { + display: grid; + gap: 12px; +} + +.cart-confirm-button { + width: fit-content; + min-width: 220px; +} + +.cart-form-note { + margin: 0; + color: var(--muted); + font-size: 14px; +} + +.cart-order-success { + border: 1px solid rgba(23, 61, 44, 0.18); + border-radius: 8px; + margin-top: 14px; + padding: 14px; + color: var(--brand-green); + background: #eef6e9; +} + +.cart-order-success strong { + display: block; + margin-bottom: 6px; + color: var(--brand-green); + font-size: 18px; +} + +.cart-order-success p { + margin: 0; + color: #394437; } .product-description { @@ -3004,6 +3240,10 @@ body[data-payment-mode="invoice"] .invoice-benefit-banner { max-height: none; } + .cart-summary { + position: static; + } + .catalog-sidebar { position: static; max-height: none; @@ -3168,7 +3408,9 @@ body[data-payment-mode="invoice"] .invoice-benefit-banner { } .content-grid, - .calculator-form { + .calculator-form, + .cart-delivery-fields, + .cart-form-grid { grid-template-columns: 1fr; } @@ -3235,4 +3477,13 @@ body[data-payment-mode="invoice"] .invoice-benefit-banner { grid-column: 2; justify-self: start; } + + .cart-payment-options, + .cart-delivery-options { + display: grid; + } + + .cart-confirm-button { + width: 100%; + } } diff --git a/views/layouts/main.php b/views/layouts/main.php index 9c95c0a..0870602 100644 --- a/views/layouts/main.php +++ b/views/layouts/main.php @@ -9,6 +9,8 @@ declare(strict_types=1); /** @var array $breadcrumbs */ /** @var bool $showBackButton */ $megaCategories = []; +$currentPath = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?: '/'; +$isCartPage = $currentPath === '/cart'; $brandLogoSvg = <<<'SVG' Рыбсток @@ -122,6 +124,7 @@ try { +
Выберите систему оплаты при получении: @@ -130,11 +133,15 @@ try { Наличными Выгодно от 7000р. - +
+
@@ -261,10 +268,17 @@ try { const storageKeys = { paymentMode: 'rybstock_payment_mode', cartTotal: 'rybstock_cart_total', - cartItems: 'rybstock_cart_items' + cartItems: 'rybstock_cart_items', + deliveryType: 'rybstock_delivery_type', + deliveryDistance: 'rybstock_delivery_distance', + deliveryLift: 'rybstock_delivery_lift', + deliveryAddress: 'rybstock_delivery_address' }; const getPaymentMode = () => localStorage.getItem(storageKeys.paymentMode) || 'cash'; + const getDeliveryType = () => localStorage.getItem(storageKeys.deliveryType) || ''; + const getDeliveryDistance = () => Math.max(0, Number(localStorage.getItem(storageKeys.deliveryDistance) || 0)); + const getDeliveryLift = () => localStorage.getItem(storageKeys.deliveryLift) === '1'; const getCartTotal = () => Number(localStorage.getItem(storageKeys.cartTotal) || 0); const getCartItems = () => { try { @@ -294,7 +308,7 @@ try { const total = getCartTotal(); if (payment === 'invoice') { - return { multiplier: 1.08, fixedDiscount: 0, label: 'Оплата по расчетному счету: +8%' }; + return { multiplier: 1.08, fixedDiscount: 0, label: 'Расчетный счет: счет день в день' }; } if (payment === 'qr') { @@ -323,6 +337,115 @@ try { return Math.max(0, subtotal - Number(mode.fixedDiscount || 0)); }; + const deliveryLabels = { + pickup: 'самовывоз', + inside_mkad: 'доставку внутри МКАД', + outside_mkad: 'доставку за МКАД' + }; + + const deliveryMinimums = { + pickup: 1, + inside_mkad: 2000, + outside_mkad: 5000 + }; + + const deliveryMinimumMessage = (type, amount) => { + const minimum = deliveryMinimums[type] || 0; + const missing = Math.max(0, minimum - amount); + + if (missing <= 0) { + return ''; + } + + return `Уважаемый клиент, Вы выбрали ${deliveryLabels[type] || 'получение'}, минимальная сумма - ${formatRub(minimum)}, добавьте пожалуйста товары на сумму ${formatRub(missing)}.`; + }; + + const deliveryCalculation = (items = getCartItems()) => { + const type = getDeliveryType(); + const amount = cartCurrentTotal(items); + const lift = getDeliveryLift() && type !== 'pickup' ? 390 : 0; + const distance = getDeliveryDistance(); + const minimum = deliveryMinimums[type] || 0; + const minimumOk = type ? amount >= minimum : false; + const minimumMessage = type ? deliveryMinimumMessage(type, amount) : ''; + + if (!type) { + return { + selected: false, + minimumOk: false, + minimumMessage: '', + price: 0, + lift, + total: 0, + label: 'Выберите получение', + note: 'Выберите самовывоз, доставку внутри МКАД или доставку за МКАД.' + }; + } + + if (type === 'pickup') { + return { + selected: true, + minimumOk, + minimumMessage, + price: 0, + lift: 0, + total: 0, + label: 'Самовывоз бесплатно', + note: 'Самовывоз осуществляется после оформления заказа на сайте и подтверждения заказа по телефону.' + }; + } + + if (type === 'inside_mkad') { + const free = amount >= 5000; + const price = free ? 0 : 599; + + return { + selected: true, + minimumOk, + minimumMessage, + price, + lift, + total: price + lift, + label: free ? 'Бесплатно внутри МКАД' : 'Доставка внутри МКАД', + note: free + ? 'Заказ от 5000 руб. доставим внутри МКАД бесплатно до подъезда. При заказе от 2000 до 5000 руб. доставка внутри МКАД стоит 599 руб.' + : 'Доставка внутри МКАД доступна от 2000 руб. При заказе от 2000 до 5000 руб. доставка стоит 599 руб., от 5000 руб. - бесплатно до подъезда.' + }; + } + + if (type === 'outside_mkad' && distance <= 0) { + return { + selected: true, + minimumOk, + minimumMessage, + price: 0, + lift, + total: lift, + label: 'Доставка за МКАД', + note: 'Доставка за МКАД доступна от 5000 руб. До 10 км от МКАД бесплатная доставка от 10000 руб.; дальше минимальная сумма для бесплатной доставки считается как 1000 руб. за каждый км от МКАД. Если заказ не проходит на бесплатную доставку, расчет: 199 руб. + 60 руб. за км.' + }; + } + + const freeThreshold = distance <= 10 ? 10000 : distance * 1000; + const free = amount >= freeThreshold; + const price = free ? 0 : Math.floor(199 + distance * 60); + + return { + selected: true, + minimumOk, + minimumMessage, + price, + lift, + total: price + lift, + label: free ? 'Бесплатно за МКАД' : 'Доставка за МКАД', + note: distance > 0 + ? (free + ? `Заказ проходит на бесплатную доставку: расстояние ${distance} км от МКАД.` + : `Расчет доставки: 199 руб. + ${distance} км × 60 руб. Минимум для бесплатной доставки: ${formatRub(freeThreshold)}.`) + : 'Доставка за МКАД доступна от 5000 руб. До 10 км от МКАД бесплатная доставка от 10000 руб.; дальше минимальная сумма для бесплатной доставки считается как 1000 руб. за каждый км от МКАД. Если заказ не проходит на бесплатную доставку, расчет: 199 руб. + 60 руб. за км.' + }; + }; + const tierValues = (base) => ({ base: base, cash20: base * 0.95, @@ -412,10 +535,20 @@ try { const modeNode = cartPage.querySelector('[data-cart-current-mode]'); const savingRow = cartPage.querySelector('[data-cart-saving-row]'); const savingNode = cartPage.querySelector('[data-cart-saving-total]'); + const deliveryTotalNode = cartPage.querySelector('[data-cart-delivery-total]'); + const deliveryNoteNode = cartPage.querySelector('[data-cart-delivery-note]'); + const minimumWarningNode = cartPage.querySelector('[data-cart-minimum-warning]'); + const grandTotalNode = cartPage.querySelector('[data-cart-grand-total]'); + const outsideDistanceField = cartPage.querySelector('[data-outside-distance-field]'); + const liftField = cartPage.querySelector('[data-lift-field]'); + const addressInput = cartPage.querySelector('[data-cart-address]'); + const distanceInput = cartPage.querySelector('[data-cart-distance]'); + const liftInput = cartPage.querySelector('[data-cart-lift]'); const items = getCartItems(); const baseTotal = cartBaseTotal(items); const currentTotal = cartCurrentTotal(items); const savingTotal = Math.max(0, baseTotal - currentTotal); + const delivery = deliveryCalculation(items); if (emptyNode) { emptyNode.hidden = items.length !== 0; @@ -465,6 +598,47 @@ try { if (savingNode) { savingNode.textContent = formatRub(savingTotal); } + + cartPage.querySelectorAll('[data-cart-delivery-type]').forEach((input) => { + input.checked = input.value === getDeliveryType(); + }); + + if (outsideDistanceField) { + outsideDistanceField.hidden = getDeliveryType() !== 'outside_mkad'; + } + + if (liftField) { + liftField.hidden = !['inside_mkad', 'outside_mkad'].includes(getDeliveryType()); + } + + if (addressInput && document.activeElement !== addressInput) { + addressInput.value = localStorage.getItem(storageKeys.deliveryAddress) || ''; + } + + if (distanceInput && document.activeElement !== distanceInput) { + distanceInput.value = getDeliveryDistance() > 0 ? String(getDeliveryDistance()) : ''; + } + + if (liftInput) { + liftInput.checked = getDeliveryLift(); + } + + if (deliveryTotalNode) { + deliveryTotalNode.textContent = delivery.selected ? formatRub(delivery.total) : delivery.label; + } + + if (deliveryNoteNode) { + deliveryNoteNode.textContent = delivery.note; + } + + if (minimumWarningNode) { + minimumWarningNode.hidden = !delivery.minimumMessage; + minimumWarningNode.textContent = delivery.minimumMessage || ''; + } + + if (grandTotalNode) { + grandTotalNode.textContent = formatRub(currentTotal + delivery.total); + } }; const updateModeButtons = () => { @@ -547,6 +721,158 @@ try { window.RybStockPricing.setPaymentMode(modeButton.dataset.paymentMode || 'cash'); }); + document.addEventListener('click', (event) => { + const deliveryLabel = event.target.closest('.cart-delivery-options label'); + + if (deliveryLabel) { + const input = deliveryLabel.querySelector('[data-cart-delivery-type]'); + + if (input && !input.checked) { + event.preventDefault(); + input.checked = true; + input.dispatchEvent(new Event('change', { bubbles: true })); + } + } + }); + + document.addEventListener('click', (event) => { + const liftLabel = event.target.closest('.cart-lift-label'); + + if (!liftLabel || event.target.closest('[data-cart-lift]')) { + return; + } + + const input = liftLabel.querySelector('[data-cart-lift]'); + + if (input) { + event.preventDefault(); + input.checked = !input.checked; + input.dispatchEvent(new Event('change', { bubbles: true })); + } + }); + + document.addEventListener('change', (event) => { + const deliveryType = event.target.closest('[data-cart-delivery-type]'); + + if (deliveryType) { + localStorage.setItem(storageKeys.deliveryType, deliveryType.value); + localStorage.setItem(storageKeys.deliveryDistance, '0'); + + if (deliveryType.value === 'pickup') { + localStorage.setItem(storageKeys.deliveryLift, '0'); + } + + updateAllPrices(); + return; + } + + const liftInput = event.target.closest('[data-cart-lift]'); + + if (liftInput) { + localStorage.setItem(storageKeys.deliveryLift, liftInput.checked ? '1' : '0'); + updateAllPrices(); + } + }); + + document.addEventListener('input', (event) => { + const distanceInput = event.target.closest('[data-cart-distance]'); + + if (distanceInput) { + localStorage.setItem(storageKeys.deliveryDistance, String(Math.max(0, Number(distanceInput.value || 0)))); + updateAllPrices(); + return; + } + + const addressInput = event.target.closest('[data-cart-address]'); + + if (addressInput) { + localStorage.setItem(storageKeys.deliveryAddress, addressInput.value); + localStorage.setItem(storageKeys.deliveryDistance, '0'); + updateAllPrices(); + } + }); + + document.addEventListener('submit', (event) => { + const form = event.target.closest('[data-cart-order-form]'); + + if (!form) { + return; + } + + event.preventDefault(); + + const cartPage = form.closest('[data-cart-page]'); + const successNode = cartPage?.querySelector('[data-cart-order-success]'); + const items = getCartItems(); + const delivery = deliveryCalculation(items); + const name = form.querySelector('[data-order-name]')?.value.trim() || ''; + const phone = form.querySelector('[data-order-phone]')?.value.trim() || ''; + const email = form.querySelector('[data-order-email]')?.value.trim() || ''; + const comment = form.querySelector('[data-order-comment]')?.value.trim() || ''; + const finalSumNeeded = form.querySelector('[data-order-final-sum-needed]')?.checked || false; + const invoiceRequisites = form.closest('[data-cart-page]')?.querySelector('[data-order-invoice-requisites]')?.value.trim() || ''; + const invoiceFiles = [...(form.closest('[data-cart-page]')?.querySelector('[data-order-invoice-file]')?.files || [])].map((file) => file.name); + + if (items.length === 0) { + window.alert('Добавьте товары в корзину.'); + return; + } + + if (!getDeliveryType()) { + window.alert('Выберите самовывоз, доставку внутри МКАД или доставку за МКАД.'); + return; + } + + if (!delivery.minimumOk) { + window.alert(delivery.minimumMessage); + return; + } + + if (!name || !phone) { + window.alert('Укажите имя и телефон.'); + return; + } + + const orderNumber = 'RS-' + new Date().toISOString().replace(/\D/g, '').slice(0, 14); + const productsTotal = cartCurrentTotal(items); + const grandTotal = productsTotal + delivery.total; + const order = { + orderNumber, + createdAt: new Date().toISOString(), + name, + phone, + email, + comment, + finalSumNeeded, + invoiceRequisites, + invoiceFiles, + paymentMode: getPaymentMode(), + deliveryType: getDeliveryType(), + address: localStorage.getItem(storageKeys.deliveryAddress) || '', + distance: getDeliveryDistance(), + lift: getDeliveryLift(), + items, + productsTotal, + deliveryTotal: delivery.total, + grandTotal + }; + + localStorage.setItem('rybstock_last_order', JSON.stringify(order)); + + if (successNode) { + successNode.hidden = false; + successNode.innerHTML = ` + Спасибо за Ваш заказ. Заказ ${escapeHtml(orderNumber)} принят в обработку. +

Спасибо, что оформили заказ на сайте Рыбсток. Мы благодарим Вас за доверие.

+

Доставка и самовывоз товара происходят только после подтверждения заказа по телефону. Сумма к оплате при получении: ${formatRub(grandTotal)}.

+

Если заказ оформлен в будний день до 14.00, мы перезвоним Вам сегодня. Если заказ оформлен после 14.00, мы свяжемся на следующий рабочий день с 10.00 до 14.00.

+

Суббота и воскресенье - выходные. Заказы, оформленные в пятницу после 14.00, автоматически переносятся на понедельник.

+

Если у Вас срочный вопрос, свяжитесь с нами по телефону +7-977-736-33-63.

+ `; + successNode.scrollIntoView({ behavior: 'smooth', block: 'center' }); + } + }); + document.addEventListener('click', (event) => { const quantityButton = event.target.closest('[data-quantity-minus], [data-quantity-plus]'); diff --git a/views/pages/cart.php b/views/pages/cart.php index 0bfc563..d40c2fa 100644 --- a/views/pages/cart.php +++ b/views/pages/cart.php @@ -2,10 +2,13 @@

Ваш заказ

Корзина

-

Цены пересчитываются автоматически: наличными от 20 000р. действует -5%, от 50 000р. -7%, QR-код показывает базовую цену, расчетный счет показывает +8%.

+

Проверьте фасовки, выберите способ оплаты и получение. Сайт сразу пересчитает выгоду, доставку и итоговую сумму.

+
+ Весовые позиции пересчитываются при сборке по фактическому весу. Если Вам нужна итоговая сумма до получения товара, отметьте это в форме заказа: после сборки менеджер перезвонит и сообщит сумму. +
-
+
@@ -17,6 +20,109 @@

Корзина пока пустая. Перейдите в каталог и добавьте нужные фасовки.

+ +
+
Оплата только при получении
+
+
+

Оплата

+

Выберите способ оплаты

+
+
+
+ + + +
+
+

Для счета можно указать реквизиты текстом или прикрепить файл. Счет подготовим день в день после подтверждения заказа.

+ + +
+
+ +
+
+
+

Получение

+

Доставка или самовывоз

+
+
+ +
+ + + +
+ + +
+ + +
+
+ +
+
+
+

Контакты

+

Куда перезвонить

+
+
+
+
+ + + +
+ + + +

На сайте Вы оформляете предзаказ. Мы перезвоним Вам и утвердим наличие, оплата производится при получении товара.

+
+