корзина кеш

This commit is contained in:
Alisa
2026-06-07 17:43:45 +03:00
parent 170bea76d5
commit 76bf5d8191
8 changed files with 530 additions and 15 deletions
+36
View File
@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace App\Controllers;
use App\Repositories\ProductRepository;
final class CartController
{
public function sync(): void
{
header('Content-Type: application/json; charset=utf-8');
$payload = json_decode((string) file_get_contents('php://input'), true);
$items = is_array($payload) && isset($payload['items']) && is_array($payload['items'])
? $payload['items']
: [];
if ($items === []) {
echo json_encode(['items' => []], 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);
}
}
}
+139
View File
@@ -211,6 +211,145 @@ final class ProductRepository extends BaseRepository
return $statement->fetchAll();
}
/**
* @param array<int, array<string, mixed>> $items
* @return array<int, array<string, mixed>>
*/
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<int, array<string, mixed>>
*/
+95
View File
@@ -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;
+2
View File
@@ -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());
+2
View File
@@ -73,6 +73,8 @@ $showUnitPriceForVariant = static function (array $variant) use ($product): bool
class="variant-row"
data-variant-id="<?= e((string) ($variant['id'] ?? '')) ?>"
data-variant-name="<?= e((string) ($variant['name'] ?? '')) ?>"
data-unit="<?= e(unit_label((string) $variant['unit'])) ?>"
data-package-quantity="<?= e((string) ($variant['package_quantity'] ?? '')) ?>"
data-base-unit-price="<?= e((string) ($variant['price_per_unit'] ?? '')) ?>"
data-base-package-price="<?= e((string) ($variant['package_price'] ?? '')) ?>"
>
+250 -14
View File
@@ -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 = `
<div class="cart-row-image">${item.image ? `<img src="${escapeHtml(item.image)}" alt="">` : ''}</div>
<div class="cart-row-main">
<h2>${escapeHtml(item.name)}</h2>
<p>${escapeHtml(item.variantName || 'Фасовка')} · ${formatRub(itemCurrentPrice(item))} за фасовку</p>
<p>${escapeHtml(item.variantName || 'Фасовка')} · <span class="${priceChangedDown ? 'cart-row-price-drop' : ''}" title="${escapeHtml(priceTitle)}">${available ? formatRub(currentPrice) : 'недоступно'}</span> за фасовку</p>
${available ? '' : `<p class="cart-row-warning">${escapeHtml(item.unavailableReason || 'Позиция сейчас недоступна и не участвует в сумме заказа.')}</p>`}
</div>
${packageChanged ? `<p class="cart-row-package-change" title="${escapeHtml(packageTitle)}">Изменилась фасовка. Сейчас ${Number(item.packageQuantity || 0).toLocaleString('ru-RU')} кг вместо ${Number(item.originalPackageQuantity || 0).toLocaleString('ru-RU')} кг.</p>` : ''}
<div class="cart-row-controls">
<button type="button" data-cart-minus>-</button>
<button type="button" data-cart-minus ${available ? '' : 'disabled'}>-</button>
<strong>${Number(item.quantity || 0)}</strong>
<button type="button" data-cart-plus>+</button>
<button type="button" data-cart-plus ${available ? '' : 'disabled'}>+</button>
</div>
<strong class="cart-row-total">${formatRub(itemCurrentPrice(item) * Number(item.quantity || 0))}</strong>
<strong class="cart-row-total">${available ? formatRub(rowTotal) : 'Не входит в заказ'}</strong>
<button class="cart-row-remove" type="button" data-cart-remove>Убрать</button>
`;
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]');
+5 -1
View File
@@ -15,10 +15,14 @@
<p class="eyebrow">Товары</p>
<h2>Выбранные позиции</h2>
</div>
<a class="subtle-link" href="/catalog">Добавить товары</a>
<div class="cart-heading-actions">
<button class="subtle-button" type="button" data-cart-clear>Очистить корзину</button>
<a class="subtle-link" href="/catalog">Добавить товары</a>
</div>
</div>
<p class="cart-empty" data-cart-empty>Корзина пока пустая. Перейдите в каталог и добавьте нужные фасовки.</p>
<div class="cart-sync-note" data-cart-sync-note hidden></div>
<div class="cart-items" data-cart-items></div>
<section class="cart-checkout-panel">
+1
View File
@@ -131,6 +131,7 @@ $isPurchasable = $isPublished && $isAvailable && $activePackagePrice > 0;
data-variant-id="<?= e((string) ($variant['id'] ?? $index)) ?>"
data-variant-name="<?= e((string) ($variant['name'] ?? '')) ?>"
data-unit="<?= e(unit_label((string) $variant['unit'])) ?>"
data-package-quantity="<?= e((string) ($variant['package_quantity'] ?? '')) ?>"
data-base-unit-price="<?= e((string) ($variant['price_per_unit'] ?? '')) ?>"
data-old-unit-price="<?= e($variantOldUnitPrice === null ? '' : (string) $variantOldUnitPrice) ?>"
data-base-package-price="<?= e((string) ($variant['package_price'] ?? '')) ?>"