diff --git a/app/Controllers/CartController.php b/app/Controllers/CartController.php new file mode 100644 index 0000000..e4073d7 --- /dev/null +++ b/app/Controllers/CartController.php @@ -0,0 +1,36 @@ + []], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + return; + } + + try { + $snapshots = (new ProductRepository())->cartSnapshots($items); + echo json_encode(['items' => $snapshots], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + } catch (\Throwable) { + http_response_code(500); + echo json_encode([ + 'items' => [], + 'error' => 'cart_sync_failed', + ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + } + } +} diff --git a/app/Repositories/ProductRepository.php b/app/Repositories/ProductRepository.php index bf5bbdb..9f99f08 100644 --- a/app/Repositories/ProductRepository.php +++ b/app/Repositories/ProductRepository.php @@ -211,6 +211,145 @@ final class ProductRepository extends BaseRepository return $statement->fetchAll(); } + /** + * @param array> $items + * @return array> + */ + public function cartSnapshots(array $items): array + { + $result = []; + + foreach ($items as $item) { + $key = (string) ($item['key'] ?? ''); + $productId = (int) ($item['productId'] ?? 0); + $variantId = (int) ($item['variantId'] ?? 0); + $desiredPackageQuantity = (float) ($item['packageQuantity'] ?? 0); + + if ($key === '' || $productId <= 0) { + continue; + } + + $variantJoin = $variantId > 0 + ? 'v.product_id = p.id AND v.id = :variant_id' + : 'v.id = ( + SELECT pv.id + FROM product_variants pv + WHERE pv.product_id = p.id + ORDER BY pv.is_default DESC, pv.sort_order, pv.id + LIMIT 1 + )'; + + $statement = $this->pdo()->prepare( + 'SELECT + p.id AS product_id, + p.name, + p.slug, + p.main_image_path, + p.is_published AS product_is_published, + p.is_available AS product_is_available, + v.id AS variant_id, + v.name AS variant_name, + v.unit, + v.package_quantity, + v.price_per_unit, + v.package_price, + v.is_published AS variant_is_published, + v.is_available AS variant_is_available + FROM products p + LEFT JOIN product_variants v ON ' . $variantJoin . ' + WHERE p.id = :product_id + LIMIT 1' + ); + $params = ['product_id' => $productId]; + if ($variantId > 0) { + $params['variant_id'] = $variantId; + } + $statement->execute($params); + $row = $statement->fetch(); + + if (!$row) { + $result[] = [ + 'key' => $key, + 'isAvailable' => false, + 'reason' => 'Товар больше не найден в каталоге.', + ]; + continue; + } + + if (empty($row['variant_id'])) { + $fallbackStatement = $this->pdo()->prepare( + 'SELECT + p.id AS product_id, + p.name, + p.slug, + p.main_image_path, + p.is_published AS product_is_published, + p.is_available AS product_is_available, + v.id AS variant_id, + v.name AS variant_name, + v.unit, + v.package_quantity, + v.price_per_unit, + v.package_price, + v.is_published AS variant_is_published, + v.is_available AS variant_is_available + FROM products p + INNER JOIN product_variants v ON v.product_id = p.id + WHERE p.id = :product_id + ORDER BY + (v.is_published = 1 AND v.is_available = 1 AND v.package_price > 0) DESC, + CASE WHEN :package_quantity_check > 0 THEN ABS(v.package_quantity - :package_quantity) ELSE 0 END, + v.is_default DESC, + v.sort_order, + v.id + LIMIT 1' + ); + $fallbackStatement->execute([ + 'product_id' => $productId, + 'package_quantity' => $desiredPackageQuantity, + 'package_quantity_check' => $desiredPackageQuantity, + ]); + $fallbackRow = $fallbackStatement->fetch(); + + if ($fallbackRow) { + $row = $fallbackRow; + } + } + + $productIsAvailable = (int) ($row['product_is_published'] ?? 0) === 1 + && (int) ($row['product_is_available'] ?? 0) === 1; + $variantIsAvailable = !empty($row['variant_id']) + && (int) ($row['variant_is_published'] ?? 0) === 1 + && (int) ($row['variant_is_available'] ?? 0) === 1 + && (float) ($row['package_price'] ?? 0) > 0; + + $reason = ''; + if (!$productIsAvailable) { + $reason = 'Товар сейчас не опубликован или отсутствует в наличии.'; + } elseif (!$variantIsAvailable) { + $reason = 'Выбранная фасовка сейчас недоступна.'; + } + + $result[] = [ + 'key' => $key, + 'productId' => (int) $row['product_id'], + 'variantId' => !empty($row['variant_id']) ? (int) $row['variant_id'] : $variantId, + 'name' => (string) ($row['name'] ?? ''), + 'slug' => (string) ($row['slug'] ?? ''), + 'image' => (string) ($row['main_image_path'] ?? ''), + 'variantName' => (string) ($row['variant_name'] ?? 'Фасовка'), + 'unit' => (string) ($row['unit'] ?? ''), + 'packageQuantity' => (float) ($row['package_quantity'] ?? 0), + 'baseUnitPrice' => (float) ($row['price_per_unit'] ?? 0), + 'basePackagePrice' => (float) ($row['package_price'] ?? 0), + 'isAvailable' => $productIsAvailable && $variantIsAvailable, + 'reason' => $reason, + ]; + } + + return $result; + } + /** * @return array> */ diff --git a/public/assets/css/app.css b/public/assets/css/app.css index a15eaac..bb3cd2b 100644 --- a/public/assets/css/app.css +++ b/public/assets/css/app.css @@ -1661,6 +1661,25 @@ a:hover { font-weight: 700; } +.subtle-button { + appearance: none; + flex: 0 0 auto; + border: 1px solid rgba(198, 57, 45, 0.24); + border-radius: 8px; + padding: 8px 12px; + font: inherit; + font-size: 14px; + font-weight: 900; + color: var(--brand-red); + background: #fff7f4; + cursor: pointer; +} + +.subtle-button:hover { + border-color: rgba(198, 57, 45, 0.44); + background: #fff0eb; +} + .muted { margin: 0; font-size: 15px; @@ -2536,6 +2555,14 @@ body[data-payment-mode="cash"] .price-tier-popover:hover { padding: 20px; } +.cart-heading-actions { + display: flex; + flex-wrap: wrap; + gap: 8px; + align-items: center; + justify-content: flex-end; +} + .cart-summary { position: sticky; top: 182px; @@ -2546,6 +2573,17 @@ body[data-payment-mode="cash"] .price-tier-popover:hover { color: var(--muted); } +.cart-sync-note { + margin: 0 0 14px; + border: 1px solid rgba(198, 57, 45, 0.18); + border-radius: 8px; + padding: 11px 13px; + color: var(--brand-green); + font-size: 14px; + font-weight: 800; + background: #fffaf1; +} + .cart-items { display: grid; gap: 10px; @@ -2563,6 +2601,22 @@ body[data-payment-mode="cash"] .price-tier-popover:hover { background: #f8faf5; } +.cart-row.is-unavailable { + border-color: rgba(93, 102, 89, 0.22); + background: #eef0eb; + opacity: 0.84; +} + +.cart-row.has-price-drop { + border-color: rgba(31, 122, 69, 0.34); + background: #f3fbf1; +} + +.cart-row.has-package-change { + border-color: rgba(196, 117, 36, 0.34); + background: #fffaf0; +} + .cart-row-image { overflow: hidden; display: grid; @@ -2579,6 +2633,11 @@ body[data-payment-mode="cash"] .price-tier-popover:hover { object-fit: cover; } +.cart-row.is-unavailable .cart-row-image img { + filter: grayscale(1); + opacity: 0.62; +} + .cart-row-main h2 { margin: 0 0 4px; font-size: 16px; @@ -2590,6 +2649,33 @@ body[data-payment-mode="cash"] .price-tier-popover:hover { color: var(--muted); } +.cart-row-price-drop { + color: #1f7a45; + font-weight: 950; + border-bottom: 1px dotted currentColor; + cursor: help; +} + +.cart-row-warning, +.cart-row-package-change { + margin: 6px 0 0; + font-size: 13px; + font-weight: 900; +} + +.cart-row-warning { + color: var(--brand-red); +} + +.cart-row-package-change { + grid-column: 2 / -1; + border-radius: 8px; + padding: 7px 10px; + color: #7b471a; + background: rgba(196, 117, 36, 0.12); + cursor: help; +} + .cart-row-controls { display: grid; grid-template-columns: 32px 32px 32px; @@ -2610,6 +2696,11 @@ body[data-payment-mode="cash"] .price-tier-popover:hover { cursor: pointer; } +.cart-row-controls button:disabled { + cursor: not-allowed; + opacity: 0.48; +} + .cart-row-controls strong { text-align: center; } @@ -4364,6 +4455,10 @@ body[data-payment-mode="invoice"] .invoice-benefit-banner { justify-self: start; } + .cart-row-package-change { + grid-column: 1 / -1; + } + .cart-payment-options, .cart-delivery-options { display: grid; diff --git a/public/index.php b/public/index.php index 9be6312..4efa6c9 100644 --- a/public/index.php +++ b/public/index.php @@ -3,6 +3,7 @@ declare(strict_types=1); use App\Controllers\CatalogController; +use App\Controllers\CartController; use App\Controllers\HomeController; use App\Controllers\PageController; use App\Controllers\PriceRequestController; @@ -110,6 +111,7 @@ $catalogSearchHandler = static function (?string $rawQuery = null): void { $router->get('/', static fn () => (new HomeController())->index()); $router->post('/price-request', static fn () => (new PriceRequestController())->store()); +$router->post('/api/cart/sync', static fn () => (new CartController())->sync()); $router->get('/search', static fn () => $catalogSearchHandler()); $router->get('/search/{query}', static fn (string $query) => $catalogSearchHandler($query)); $router->get('/catalog', static fn () => trim((string) ($_GET['q'] ?? '')) !== '' ? $catalogSearchHandler() : (new CatalogController())->index()); diff --git a/views/catalog/product.php b/views/catalog/product.php index 83e2f60..6f76a62 100644 --- a/views/catalog/product.php +++ b/views/catalog/product.php @@ -73,6 +73,8 @@ $showUnitPriceForVariant = static function (array $variant) use ($product): bool class="variant-row" data-variant-id="" data-variant-name="" + data-unit="" + data-package-quantity="" data-base-unit-price="" data-base-package-price="" > diff --git a/views/layouts/main.php b/views/layouts/main.php index 743990a..757c825 100644 --- a/views/layouts/main.php +++ b/views/layouts/main.php @@ -365,7 +365,33 @@ try { return []; } }; + const isCartItemAvailable = (item) => item && item.isAvailable !== false && item.unavailable !== true; + const purchasableCartItems = (items = getCartItems()) => items.filter(isCartItemAvailable); + const parsePackageQuantity = (value) => { + const match = String(value || '').replace(',', '.').match(/(\d+(?:\.\d+)?)\s*([a-zа-яё]*)/i); + + if (!match) { + return 0; + } + + const amount = Number(match[1] || 0); + const unit = String(match[2] || '').toLowerCase(); + + if (!amount) { + return 0; + } + + if (unit.startsWith('г') || unit === 'g') { + return amount / 1000; + } + + return amount; + }; const cartBaseTotal = (items = getCartItems()) => items.reduce((sum, item) => { + if (!isCartItemAvailable(item)) { + return sum; + } + return sum + (Number(item.basePackagePrice || 0) * Number(item.quantity || 0)); }, 0); const saveCartItems = (items) => { @@ -674,7 +700,7 @@ try { }; const updateCartCount = () => { - const count = getCartItems().reduce((sum, item) => sum + Number(item.quantity || 0), 0); + const count = purchasableCartItems().reduce((sum, item) => sum + Number(item.quantity || 0), 0); document.querySelectorAll('[data-cart-count]').forEach((node) => { node.textContent = String(count); @@ -684,7 +710,7 @@ try { const updateMiniCart = () => { const items = getCartItems(); - const count = items.reduce((sum, item) => sum + Number(item.quantity || 0), 0); + const count = purchasableCartItems(items).reduce((sum, item) => sum + Number(item.quantity || 0), 0); const total = cartCurrentTotal(items); document.querySelectorAll('[data-mini-cart-count]').forEach((node) => { @@ -696,7 +722,9 @@ try { }); }; - const itemCurrentPrice = (item) => Math.floor(Number(item.basePackagePrice || 0) * priceMode().multiplier); + const itemCurrentPrice = (item) => isCartItemAvailable(item) + ? Math.floor(Number(item.basePackagePrice || 0) * priceMode().multiplier) + : 0; const renderCartPage = () => { const cartPage = document.querySelector('[data-cart-page]'); @@ -722,7 +750,12 @@ try { const addressStatusNode = cartPage.querySelector('[data-cart-address-status]'); const distanceInput = cartPage.querySelector('[data-cart-distance]'); const liftInput = cartPage.querySelector('[data-cart-lift]'); + const syncNoteNode = cartPage.querySelector('[data-cart-sync-note]'); + const clearButton = cartPage.querySelector('[data-cart-clear]'); const items = getCartItems(); + const unavailableCount = items.filter((item) => !isCartItemAvailable(item)).length; + const loweredCount = items.filter((item) => isCartItemAvailable(item) && item.priceChange === 'down').length; + const packageChangedCount = items.filter((item) => isCartItemAvailable(item) && item.packageChanged).length; const baseTotal = cartBaseTotal(items); const currentTotal = cartCurrentTotal(items); const savingTotal = Math.max(0, baseTotal - currentTotal); @@ -736,27 +769,59 @@ try { itemsNode.innerHTML = ''; items.forEach((item) => { + const available = isCartItemAvailable(item); + const currentPrice = itemCurrentPrice(item); + const rowTotal = currentPrice * Number(item.quantity || 0); + const priceChangedDown = available && item.priceChange === 'down'; + const packageChanged = available && item.packageChanged; + const priceTitle = priceChangedDown + ? `Цена снизилась. Было ${formatRub(item.previousPackagePrice)}, сейчас ${formatRub(item.basePackagePrice)} за фасовку.` + : ''; + const packageTitle = packageChanged + ? `Изменилась фасовка: было ${Number(item.originalPackageQuantity || 0).toLocaleString('ru-RU')} кг, сейчас ${Number(item.packageQuantity || 0).toLocaleString('ru-RU')} кг. Цена пересчитана автоматически.` + : ''; const row = document.createElement('article'); - row.className = 'cart-row'; + row.className = 'cart-row' + (available ? '' : ' is-unavailable') + (priceChangedDown ? ' has-price-drop' : '') + (packageChanged ? ' has-package-change' : ''); row.dataset.cartKey = item.key; row.innerHTML = `
${item.image ? `` : ''}

${escapeHtml(item.name)}

-

${escapeHtml(item.variantName || 'Фасовка')} · ${formatRub(itemCurrentPrice(item))} за фасовку

+

${escapeHtml(item.variantName || 'Фасовка')} · ${available ? formatRub(currentPrice) : 'недоступно'} за фасовку

+ ${available ? '' : `

${escapeHtml(item.unavailableReason || 'Позиция сейчас недоступна и не участвует в сумме заказа.')}

`}
+ ${packageChanged ? `

Изменилась фасовка. Сейчас ${Number(item.packageQuantity || 0).toLocaleString('ru-RU')} кг вместо ${Number(item.originalPackageQuantity || 0).toLocaleString('ru-RU')} кг.

` : ''}
- + ${Number(item.quantity || 0)} - +
- ${formatRub(itemCurrentPrice(item) * Number(item.quantity || 0))} + ${available ? formatRub(rowTotal) : 'Не входит в заказ'} `; itemsNode.append(row); }); } + if (syncNoteNode) { + const messages = []; + if (unavailableCount > 0) { + messages.push(`${unavailableCount} поз. сейчас недоступно и не участвует в сумме.`); + } + if (loweredCount > 0) { + messages.push(`${loweredCount} поз. подешевело с момента добавления.`); + } + if (packageChangedCount > 0) { + messages.push(`${packageChangedCount} поз. получили новую фасовку и пересчитаны по актуальному весу.`); + } + syncNoteNode.hidden = messages.length === 0; + syncNoteNode.textContent = messages.join(' '); + } + + if (clearButton) { + clearButton.hidden = items.length === 0; + } + if (baseTotalNode) { baseTotalNode.textContent = formatRub(baseTotal); } @@ -844,6 +909,129 @@ try { renderCartPage(); }; + let cartSyncPromise = null; + const syncCartWithServer = async () => { + const items = getCartItems(); + + if (items.length === 0) { + return; + } + + if (cartSyncPromise) { + return cartSyncPromise; + } + + cartSyncPromise = fetch('/api/cart/sync', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + items: items.map((item) => ({ + key: item.key, + productId: item.productId, + variantId: item.variantId, + packageQuantity: item.packageQuantity || item.originalPackageQuantity || parsePackageQuantity(item.variantName || item.originalVariantName) || 0 + })) + }) + }) + .then((response) => { + if (!response.ok) { + throw new Error('Cart sync failed'); + } + + return response.json(); + }) + .then((payload) => { + const snapshots = new Map((payload.items || []).map((item) => [String(item.key), item])); + const syncedItems = items.map((item) => { + const snapshot = snapshots.get(String(item.key)); + const firstBasePackagePrice = Number(item.firstBasePackagePrice || item.basePackagePrice || snapshot?.basePackagePrice || 0); + const originalPackageQuantity = Number(item.originalPackageQuantity || item.packageQuantity || parsePackageQuantity(item.originalVariantName || item.variantName) || snapshot?.packageQuantity || 0); + const originalVariantName = item.originalVariantName || item.variantName || ''; + + if (!snapshot) { + return { + ...item, + firstBasePackagePrice, + originalPackageQuantity, + originalVariantName, + isAvailable: false, + unavailable: true, + unavailableReason: 'Товар больше не найден в каталоге.', + priceChange: '' + }; + } + + const basePackagePrice = Number(snapshot.basePackagePrice || 0); + const baseUnitPrice = Number(snapshot.baseUnitPrice || 0); + const packageQuantity = Number(snapshot.packageQuantity || 0); + const isAvailable = snapshot.isAvailable !== false && basePackagePrice > 0; + const packageChanged = isAvailable && originalPackageQuantity > 0 && packageQuantity > 0 && Math.abs(packageQuantity - originalPackageQuantity) > 0.001; + const priceWentDown = !packageChanged && isAvailable && firstBasePackagePrice > 0 && basePackagePrice > 0 && basePackagePrice < firstBasePackagePrice; + + return { + ...item, + productId: String(snapshot.productId || item.productId || ''), + variantId: String(snapshot.variantId || item.variantId || ''), + name: snapshot.name || item.name, + slug: snapshot.slug || item.slug, + image: snapshot.image || item.image, + variantName: snapshot.variantName || item.variantName || 'Фасовка', + originalVariantName, + unit: snapshot.unit || item.unit || '', + basePackagePrice, + baseUnitPrice, + packageQuantity, + originalPackageQuantity, + firstBasePackagePrice, + isAvailable, + unavailable: !isAvailable, + unavailableReason: isAvailable ? '' : (snapshot.reason || 'Позиция сейчас недоступна.'), + packageChanged, + priceChange: priceWentDown ? 'down' : '', + previousPackagePrice: priceWentDown ? firstBasePackagePrice : 0 + }; + }); + + const mergedItems = []; + syncedItems.forEach((item) => { + const normalizedKey = item.productId && item.variantId ? `${item.productId}:${item.variantId}` : item.key; + const existingItem = mergedItems.find((mergedItem) => mergedItem.key === normalizedKey); + + item.key = normalizedKey; + + if (existingItem) { + existingItem.quantity = Number(existingItem.quantity || 0) + Number(item.quantity || 0); + const existingFirstBasePackagePrice = Number(existingItem.firstBasePackagePrice || 0); + const itemFirstBasePackagePrice = Number(item.firstBasePackagePrice || 0); + existingItem.firstBasePackagePrice = existingFirstBasePackagePrice > 0 && itemFirstBasePackagePrice > 0 + ? Math.min(existingFirstBasePackagePrice, itemFirstBasePackagePrice) + : Math.max(existingFirstBasePackagePrice, itemFirstBasePackagePrice); + existingItem.originalPackageQuantity = Number(existingItem.originalPackageQuantity || item.originalPackageQuantity || 0); + } else { + mergedItems.push(item); + } + }); + + saveCartItems(mergedItems); + updateAllPrices(); + }) + .catch(() => { + const note = document.querySelector('[data-cart-sync-note]'); + + if (note) { + note.hidden = false; + note.textContent = 'Не удалось сверить корзину с актуальным наличием. Менеджер дополнительно проверит заказ по телефону.'; + } + }) + .finally(() => { + cartSyncPromise = null; + }); + + return cartSyncPromise; + }; + window.RybStockPricing = { setCartTotal(total) { localStorage.setItem(storageKeys.cartTotal, String(Math.max(0, Number(total) || 0))); @@ -859,6 +1047,14 @@ try { update: updateAllPrices }; + syncCartWithServer(); + + document.addEventListener('visibilitychange', () => { + if (document.visibilityState === 'visible') { + syncCartWithServer(); + } + }); + const catalogScrollKey = 'rybstock.catalogScrollY'; const catalogMenuScrollKey = 'rybstock.catalogMenuScrollY'; @@ -988,7 +1184,7 @@ try { } }); - document.addEventListener('submit', (event) => { + document.addEventListener('submit', async (event) => { const form = event.target.closest('[data-cart-order-form]'); if (!form) { @@ -996,11 +1192,13 @@ try { } event.preventDefault(); + await syncCartWithServer(); const cartPage = form.closest('[data-cart-page]'); const successNode = cartPage?.querySelector('[data-cart-order-success]'); const items = getCartItems(); const delivery = deliveryCalculation(items); + const purchasableItems = purchasableCartItems(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() || ''; @@ -1009,7 +1207,7 @@ try { 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) { + if (purchasableItems.length === 0) { window.alert('Добавьте товары в корзину.'); return; } @@ -1048,7 +1246,7 @@ try { distance: getDeliveryDistance(), deliveryZone: getDeliveryZone(), lift: getDeliveryLift(), - items, + items: purchasableItems, productsTotal, deliveryTotal: delivery.total, grandTotal @@ -1123,9 +1321,26 @@ try { const key = String(productScope.dataset.productId || '') + ':' + variantId; const items = getCartItems(); const existing = items.find((item) => item.key === key); + const basePackagePrice = Number(packagePriceNode.dataset.basePackagePrice || 0); + const baseUnitPrice = Number(row?.dataset.baseUnitPrice || variantButton?.dataset.baseUnitPrice || 0); + const packageQuantity = Number(row?.dataset.packageQuantity || variantButton?.dataset.packageQuantity || 0); + const unit = row?.dataset.unit || variantButton?.dataset.unit || ''; + const variantName = row?.dataset.variantName || variantButton?.dataset.variantName || 'Фасовка'; if (existing) { + const storedPackagePrice = Number(existing.basePackagePrice || basePackagePrice || 0); + const storedPackageQuantity = Number(existing.packageQuantity || packageQuantity || 0); existing.quantity = Number(existing.quantity || 0) + addQuantity; + existing.basePackagePrice = basePackagePrice; + existing.baseUnitPrice = baseUnitPrice || Number(existing.baseUnitPrice || 0); + existing.packageQuantity = packageQuantity || Number(existing.packageQuantity || 0); + existing.unit = unit || existing.unit || ''; + existing.firstBasePackagePrice = Number(existing.firstBasePackagePrice || storedPackagePrice || basePackagePrice || 0); + existing.originalPackageQuantity = Number(existing.originalPackageQuantity || storedPackageQuantity || packageQuantity || 0); + existing.originalVariantName = existing.originalVariantName || existing.variantName || ''; + existing.isAvailable = true; + existing.unavailable = false; + existing.unavailableReason = ''; } else { items.push({ key, @@ -1134,15 +1349,23 @@ try { name: productScope.dataset.productName || '', image: productScope.dataset.productImage || '', variantId, - variantName: row?.dataset.variantName || variantButton?.dataset.variantName || 'Фасовка', - basePackagePrice: Number(packagePriceNode.dataset.basePackagePrice || 0), - baseUnitPrice: Number(row?.dataset.baseUnitPrice || variantButton?.dataset.baseUnitPrice || 0), + variantName, + originalVariantName: variantName, + basePackagePrice, + firstBasePackagePrice: basePackagePrice, + baseUnitPrice, + unit, + packageQuantity, + originalPackageQuantity: packageQuantity, + isAvailable: true, + unavailable: false, quantity: addQuantity }); } saveCartItems(items); updateAllPrices(); + syncCartWithServer(); addButton.classList.add('is-added'); addButton.textContent = 'Добавлено'; window.setTimeout(() => { @@ -1162,6 +1385,19 @@ try { notifyButton.textContent = 'Запрос принят'; }); + document.addEventListener('click', (event) => { + const clearButton = event.target.closest('[data-cart-clear]'); + + if (!clearButton) { + return; + } + + if (window.confirm('Очистить корзину полностью?')) { + saveCartItems([]); + updateAllPrices(); + } + }); + document.addEventListener('click', (event) => { const control = event.target.closest('[data-cart-plus], [data-cart-minus], [data-cart-remove]'); diff --git a/views/pages/cart.php b/views/pages/cart.php index 380cfd4..c452586 100644 --- a/views/pages/cart.php +++ b/views/pages/cart.php @@ -15,10 +15,14 @@

Товары

Выбранные позиции

- Добавить товары +
+ + Добавить товары +

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

+
diff --git a/views/partials/product-card.php b/views/partials/product-card.php index 5f2cc82..b937795 100644 --- a/views/partials/product-card.php +++ b/views/partials/product-card.php @@ -131,6 +131,7 @@ $isPurchasable = $isPublished && $isAvailable && $activePackagePrice > 0; data-variant-id="" data-variant-name="" data-unit="" + data-package-quantity="" data-base-unit-price="" data-old-unit-price="" data-base-package-price=""