корзина кеш
This commit is contained in:
+250
-14
@@ -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]');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user