доставка в плавающем
This commit is contained in:
+343
-48
@@ -10,15 +10,25 @@ declare(strict_types=1);
|
||||
/** @var bool $showBackButton */
|
||||
$megaCategories = [];
|
||||
$currentPath = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?: '/';
|
||||
$canonicalBase = rtrim((string) app_env('SITE_URL', ''), '/');
|
||||
if ($canonicalBase === '') {
|
||||
$requestHost = (string) ($_SERVER['HTTP_HOST'] ?? 'new.rybstock.ru');
|
||||
$canonicalBase = 'https://' . preg_replace('/[^a-z0-9.\-:]/i', '', $requestHost);
|
||||
}
|
||||
$canonicalPath = '/' . ltrim($currentPath, '/');
|
||||
$canonicalUrl = rtrim($canonicalBase, '/') . ($canonicalPath === '/' ? '/' : $canonicalPath);
|
||||
$isCartPage = $currentPath === '/cart';
|
||||
$isCatalogLikePage = $currentPath === '/'
|
||||
|| $currentPath === '/catalog'
|
||||
|| $currentPath === '/promo'
|
||||
|| $currentPath === '/new'
|
||||
|| str_starts_with($currentPath, '/catalog/')
|
||||
|| $currentPath === '/search'
|
||||
|| str_starts_with($currentPath, '/search/');
|
||||
$isProductPage = str_starts_with($currentPath, '/product/');
|
||||
$showPriceModeBar = !$isCartPage && ($isCatalogLikePage || $isProductPage);
|
||||
$yandexMapsApiKey = (string) app_env('YANDEX_MAPS_API_KEY', '9ab8ad56-c350-4a2c-8f23-35acb102ee7a');
|
||||
$yandexGeocoderApiKey = (string) app_env('YANDEX_GEOCODER_API_KEY', '0eaa1b4d-8e5c-454a-a40e-ca6ed7b4fbd1');
|
||||
$queryParameters = [];
|
||||
parse_str((string) (parse_url($_SERVER['REQUEST_URI'] ?? '', PHP_URL_QUERY) ?? ''), $queryParameters);
|
||||
$currentSearchQuery = (string) ($_GET['q'] ?? $queryParameters['q'] ?? '');
|
||||
@@ -103,11 +113,14 @@ try {
|
||||
<?php if ($metaKeywords !== ''): ?>
|
||||
<meta name="keywords" content="<?= e($metaKeywords) ?>">
|
||||
<?php endif; ?>
|
||||
<link rel="canonical" href="<?= e($canonicalUrl) ?>">
|
||||
<link rel="icon" type="image/svg+xml" href="<?= e($faviconHref) ?>">
|
||||
<?php if ($yandexMapsApiKey !== ''): ?>
|
||||
<meta name="yandex-maps-api-key" content="<?= e($yandexMapsApiKey) ?>">
|
||||
<meta name="yandex-geocoder-api-key" content="<?= e($yandexGeocoderApiKey) ?>">
|
||||
<script>
|
||||
window.RYBSTOCK_YANDEX_MAPS_API_KEY = <?= json_encode($yandexMapsApiKey, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
|
||||
window.RYBSTOCK_YANDEX_GEOCODER_API_KEY = <?= json_encode($yandexGeocoderApiKey, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
<style id="embedded-home-images">
|
||||
@@ -387,6 +400,16 @@ try {
|
||||
|
||||
return amount;
|
||||
};
|
||||
const formatPackageQuantity = (quantity, unit) => {
|
||||
const normalizedUnit = String(unit || '').trim() || 'ед.';
|
||||
const amount = Number(quantity || 0);
|
||||
|
||||
if (!amount) {
|
||||
return normalizedUnit;
|
||||
}
|
||||
|
||||
return `${amount.toLocaleString('ru-RU')} ${normalizedUnit}`;
|
||||
};
|
||||
const cartBaseTotal = (items = getCartItems()) => items.reduce((sum, item) => {
|
||||
if (!isCartItemAvailable(item)) {
|
||||
return sum;
|
||||
@@ -464,7 +487,11 @@ try {
|
||||
};
|
||||
|
||||
const moscowCenter = [55.755864, 37.617698];
|
||||
const mkadRadiusKm = 17.7;
|
||||
const deliveryBounds = [[54.1, 35.0], [56.95, 40.45]];
|
||||
const kmPerLatDegree = 110.574;
|
||||
const kmPerLonDegree = 111.320 * Math.cos(moscowCenter[0] * Math.PI / 180);
|
||||
const mkadLatRadiusKm = 18.8;
|
||||
const mkadLonRadiusKm = 17.8;
|
||||
let yandexMapsPromise = null;
|
||||
let deliveryAddressTimer = null;
|
||||
|
||||
@@ -514,6 +541,188 @@ try {
|
||||
return 2 * earthRadiusKm * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
};
|
||||
|
||||
const coordsToMoscowKm = (coords) => ({
|
||||
x: (coords[1] - moscowCenter[1]) * kmPerLonDegree,
|
||||
y: (coords[0] - moscowCenter[0]) * kmPerLatDegree
|
||||
});
|
||||
|
||||
const isInsideMkadApprox = (coords) => {
|
||||
const point = coordsToMoscowKm(coords);
|
||||
const ellipseValue = (point.x * point.x) / (mkadLonRadiusKm * mkadLonRadiusKm)
|
||||
+ (point.y * point.y) / (mkadLatRadiusKm * mkadLatRadiusKm);
|
||||
|
||||
return ellipseValue <= 1;
|
||||
};
|
||||
|
||||
const mkadBoundaryPoint = (coords) => {
|
||||
const point = coordsToMoscowKm(coords);
|
||||
const angle = Math.atan2(point.y / mkadLatRadiusKm, point.x / mkadLonRadiusKm);
|
||||
const x = Math.cos(angle) * mkadLonRadiusKm;
|
||||
const y = Math.sin(angle) * mkadLatRadiusKm;
|
||||
|
||||
return [
|
||||
moscowCenter[0] + y / kmPerLatDegree,
|
||||
moscowCenter[1] + x / kmPerLonDegree
|
||||
];
|
||||
};
|
||||
|
||||
const roadDistanceFromMkad = async (coords) => {
|
||||
if (isInsideMkadApprox(coords)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const boundary = mkadBoundaryPoint(coords);
|
||||
|
||||
try {
|
||||
const route = await window.ymaps.route([boundary, coords], { routingMode: 'auto' });
|
||||
const meters = Number(route.getLength ? route.getLength() : 0);
|
||||
|
||||
if (meters > 0) {
|
||||
return Math.max(1, Math.ceil(meters / 1000));
|
||||
}
|
||||
} catch (error) {
|
||||
// If route building fails, fall back to a direct distance so checkout is not blocked.
|
||||
}
|
||||
|
||||
return Math.max(1, Math.ceil(haversineKm(boundary, coords)));
|
||||
};
|
||||
|
||||
const normalizeDeliveryAddressQuery = (address) => {
|
||||
const value = String(address || '').replace(/\s+/g, ' ').trim();
|
||||
|
||||
if (value === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
const lower = value.toLocaleLowerCase('ru-RU');
|
||||
const hasRegion = /москва|московская|люберц|сергиев|посад|химк|балаших|мытищ|красногорск|одинцов|видн|реутов|долгопруд|подольск|домодедов|лобн|королев|пушкино|раменск|зеленоград/.test(lower);
|
||||
|
||||
return hasRegion ? value : `Москва и Московская область, ${value}`;
|
||||
};
|
||||
|
||||
const suggestDeliveryAddress = async (query) => {
|
||||
const normalizedQuery = normalizeDeliveryAddressQuery(query);
|
||||
|
||||
if (normalizedQuery.length < 3) {
|
||||
return [];
|
||||
}
|
||||
|
||||
await loadYandexMapsApi();
|
||||
|
||||
if (!window.ymaps?.suggest) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const items = await window.ymaps.suggest(normalizedQuery, {
|
||||
results: 7,
|
||||
boundedBy: deliveryBounds,
|
||||
strictBounds: false
|
||||
});
|
||||
|
||||
return (items || [])
|
||||
.map((item) => ({
|
||||
value: String(item.value || item.displayName || '').trim(),
|
||||
displayName: String(item.displayName || item.value || '').trim()
|
||||
}))
|
||||
.filter((item) => item.value !== '');
|
||||
};
|
||||
|
||||
let deliverySuggestCounter = 0;
|
||||
|
||||
const attachDeliveryAddressSuggestions = (input) => {
|
||||
if (!input || input.dataset.deliverySuggestReady === '1') {
|
||||
return;
|
||||
}
|
||||
|
||||
input.dataset.deliverySuggestReady = '1';
|
||||
input.setAttribute('autocomplete', 'off');
|
||||
|
||||
const datalist = document.createElement('datalist');
|
||||
datalist.id = `rybstock-delivery-addresses-${++deliverySuggestCounter}`;
|
||||
document.body.append(datalist);
|
||||
input.setAttribute('list', datalist.id);
|
||||
|
||||
let suggestTimer = null;
|
||||
let suggestRequestId = 0;
|
||||
|
||||
input.addEventListener('input', () => {
|
||||
const query = input.value.trim();
|
||||
window.clearTimeout(suggestTimer);
|
||||
|
||||
if (query.length < 3) {
|
||||
datalist.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const requestId = ++suggestRequestId;
|
||||
suggestTimer = window.setTimeout(() => {
|
||||
suggestDeliveryAddress(query)
|
||||
.then((items) => {
|
||||
if (requestId !== suggestRequestId) {
|
||||
return;
|
||||
}
|
||||
|
||||
datalist.innerHTML = '';
|
||||
items.forEach((item) => {
|
||||
const option = document.createElement('option');
|
||||
option.value = item.value;
|
||||
if (item.displayName && item.displayName !== item.value) {
|
||||
option.label = item.displayName;
|
||||
}
|
||||
datalist.append(option);
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
if (requestId === suggestRequestId) {
|
||||
datalist.innerHTML = '';
|
||||
}
|
||||
});
|
||||
}, 250);
|
||||
});
|
||||
|
||||
input.addEventListener('change', () => {
|
||||
input.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
});
|
||||
};
|
||||
|
||||
const setupDeliveryAddressSuggestions = () => {
|
||||
document
|
||||
.querySelectorAll('[data-cart-address], #delivery-calculator input[name="address"]')
|
||||
.forEach(attachDeliveryAddressSuggestions);
|
||||
};
|
||||
|
||||
const resolveDeliveryDistance = async (address) => {
|
||||
const normalizedAddress = normalizeDeliveryAddressQuery(address);
|
||||
|
||||
if (normalizedAddress.length < 6) {
|
||||
return { distance: 0, zone: '', status: '', coords: null };
|
||||
}
|
||||
|
||||
await loadYandexMapsApi();
|
||||
const result = await window.ymaps.geocode(normalizedAddress, {
|
||||
results: 1,
|
||||
boundedBy: deliveryBounds,
|
||||
strictBounds: false
|
||||
});
|
||||
const first = result.geoObjects.get(0);
|
||||
|
||||
if (!first) {
|
||||
return {
|
||||
distance: 0,
|
||||
zone: '',
|
||||
status: 'Адрес не найден. Проверьте написание, менеджер уточнит доставку по телефону.',
|
||||
coords: null
|
||||
};
|
||||
}
|
||||
|
||||
const coords = first.geometry.getCoordinates();
|
||||
const distance = await roadDistanceFromMkad(coords);
|
||||
|
||||
return distance <= 0
|
||||
? { distance: 0, zone: 'inside', status: 'Адрес найден: внутри МКАД.', coords }
|
||||
: { distance, zone: 'outside', status: `Адрес найден: примерно ${distance} км от МКАД.`, coords };
|
||||
};
|
||||
|
||||
const resolveDeliveryAddress = async (address) => {
|
||||
const normalizedAddress = String(address || '').trim();
|
||||
|
||||
@@ -527,24 +736,15 @@ try {
|
||||
updateAllPrices();
|
||||
|
||||
try {
|
||||
await loadYandexMapsApi();
|
||||
const result = await window.ymaps.geocode(normalizedAddress, { results: 1 });
|
||||
const first = result.geoObjects.get(0);
|
||||
const resolved = await resolveDeliveryDistance(normalizedAddress);
|
||||
setDeliveryGeo(resolved.distance, resolved.zone, resolved.status);
|
||||
|
||||
if (!first) {
|
||||
setDeliveryGeo(0, '', 'Адрес не найден. Проверьте написание, менеджер уточнит доставку по телефону.');
|
||||
updateAllPrices();
|
||||
return;
|
||||
}
|
||||
|
||||
const coords = first.geometry.getCoordinates();
|
||||
const distanceFromCenter = haversineKm(moscowCenter, coords);
|
||||
const outsideKm = Math.max(0, Math.ceil(distanceFromCenter - mkadRadiusKm));
|
||||
|
||||
if (outsideKm <= 0) {
|
||||
setDeliveryGeo(0, 'inside', 'Адрес найден: внутри МКАД.');
|
||||
} else {
|
||||
setDeliveryGeo(outsideKm, 'outside', `Адрес найден: примерно ${outsideKm} км от МКАД.`);
|
||||
if (getDeliveryType() !== 'pickup') {
|
||||
if (resolved.zone === 'outside') {
|
||||
localStorage.setItem(storageKeys.deliveryType, 'outside_mkad');
|
||||
} else if (resolved.zone === 'inside') {
|
||||
localStorage.setItem(storageKeys.deliveryType, 'inside_mkad');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
setDeliveryGeo(0, '', 'Не удалось автоматически рассчитать адрес. Менеджер уточнит доставку по телефону.');
|
||||
@@ -558,17 +758,16 @@ try {
|
||||
deliveryAddressTimer = window.setTimeout(() => resolveDeliveryAddress(address), 700);
|
||||
};
|
||||
|
||||
const deliveryCalculation = (items = getCartItems()) => {
|
||||
const type = getDeliveryType();
|
||||
const amount = cartCurrentTotal(items);
|
||||
const lift = getDeliveryLift() && type !== 'pickup' ? 390 : 0;
|
||||
const distance = getDeliveryDistance();
|
||||
const zone = getDeliveryZone();
|
||||
const minimum = deliveryMinimums[type] || 0;
|
||||
const minimumOk = type ? amount >= minimum : false;
|
||||
const minimumMessage = type ? deliveryMinimumMessage(type, amount) : '';
|
||||
const deliveryQuote = (type, amount, distance = 0, liftSelected = false, zone = '') => {
|
||||
const normalizedType = type || '';
|
||||
const normalizedAmount = Math.max(0, Number(amount) || 0);
|
||||
const normalizedDistance = Math.max(0, Math.ceil(Number(distance) || 0));
|
||||
const lift = liftSelected && normalizedType !== 'pickup' ? 390 : 0;
|
||||
const minimum = deliveryMinimums[normalizedType] || 0;
|
||||
const minimumOk = normalizedType ? normalizedAmount >= minimum : false;
|
||||
const minimumMessage = normalizedType ? deliveryMinimumMessage(normalizedType, normalizedAmount) : '';
|
||||
|
||||
if (!type) {
|
||||
if (!normalizedType) {
|
||||
return {
|
||||
selected: false,
|
||||
minimumOk: false,
|
||||
@@ -581,7 +780,7 @@ try {
|
||||
};
|
||||
}
|
||||
|
||||
if (type === 'pickup') {
|
||||
if (normalizedType === 'pickup') {
|
||||
return {
|
||||
selected: true,
|
||||
minimumOk,
|
||||
@@ -594,8 +793,16 @@ try {
|
||||
};
|
||||
}
|
||||
|
||||
if (type === 'inside_mkad') {
|
||||
const free = amount >= 5000;
|
||||
if (normalizedType === 'inside_mkad' && (zone === 'outside' || normalizedDistance > 0)) {
|
||||
return deliveryQuote('outside_mkad', normalizedAmount, normalizedDistance, liftSelected, 'outside');
|
||||
}
|
||||
|
||||
if (normalizedType === 'outside_mkad' && zone === 'inside') {
|
||||
return deliveryQuote('inside_mkad', normalizedAmount, 0, liftSelected, 'inside');
|
||||
}
|
||||
|
||||
if (normalizedType === 'inside_mkad') {
|
||||
const free = normalizedAmount >= 5000;
|
||||
const price = free ? 0 : 599;
|
||||
|
||||
return {
|
||||
@@ -612,7 +819,7 @@ try {
|
||||
};
|
||||
}
|
||||
|
||||
if (type === 'outside_mkad' && distance <= 0) {
|
||||
if (normalizedType === 'outside_mkad' && normalizedDistance <= 0) {
|
||||
const zoneNote = zone === 'inside'
|
||||
? 'Адрес похож на адрес внутри МКАД. Выберите доставку внутри МКАД, если это верно.'
|
||||
: 'Введите адрес доставки, и сайт рассчитает примерное расстояние за МКАД автоматически.';
|
||||
@@ -622,16 +829,16 @@ try {
|
||||
minimumOk,
|
||||
minimumMessage,
|
||||
price: 0,
|
||||
lift,
|
||||
total: lift,
|
||||
lift: 0,
|
||||
total: 0,
|
||||
label: 'Доставка за МКАД',
|
||||
note: `${zoneNote} Доставка за МКАД доступна от 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);
|
||||
const freeThreshold = normalizedDistance <= 10 ? 10000 : normalizedDistance * 1000;
|
||||
const free = normalizedAmount >= freeThreshold;
|
||||
const price = free ? 0 : Math.floor(199 + normalizedDistance * 60);
|
||||
|
||||
return {
|
||||
selected: true,
|
||||
@@ -641,14 +848,18 @@ try {
|
||||
lift,
|
||||
total: price + lift,
|
||||
label: free ? 'Бесплатно за МКАД' : 'Доставка за МКАД',
|
||||
note: distance > 0
|
||||
? (free
|
||||
? `Заказ проходит на бесплатную доставку: расстояние ${distance} км от МКАД.`
|
||||
: `Расчет доставки: 199 руб. + ${distance} км × 60 руб. Минимум для бесплатной доставки: ${formatRub(freeThreshold)}.`)
|
||||
: 'Доставка за МКАД доступна от 5000 руб. До 10 км от МКАД бесплатная доставка от 10000 руб.; дальше минимальная сумма для бесплатной доставки считается как 1000 руб. за каждый км от МКАД. Если заказ не проходит на бесплатную доставку, расчет: 199 руб. + 60 руб. за км.'
|
||||
note: free
|
||||
? `Заказ проходит на бесплатную доставку: расстояние ${normalizedDistance} км от МКАД.`
|
||||
: `Расчет доставки: 199 руб. + ${normalizedDistance} км × 60 руб. Минимум для бесплатной доставки: ${formatRub(freeThreshold)}.`
|
||||
};
|
||||
};
|
||||
|
||||
const deliveryCalculation = (items = getCartItems()) => {
|
||||
const type = getDeliveryType();
|
||||
const amount = cartCurrentTotal(items);
|
||||
return deliveryQuote(type, amount, getDeliveryDistance(), getDeliveryLift(), getDeliveryZone());
|
||||
};
|
||||
|
||||
const tierValues = (base) => ({
|
||||
base: base,
|
||||
cash20: base * 0.95,
|
||||
@@ -676,6 +887,34 @@ try {
|
||||
const unitNode = card.querySelector('[data-dynamic-unit-price]');
|
||||
updateDynamicPrices(card);
|
||||
|
||||
const variantRows = card.querySelectorAll('.variant-row');
|
||||
|
||||
if (variantRows.length > 0) {
|
||||
variantRows.forEach((row) => {
|
||||
const rowPackageNode = row.querySelector('[data-dynamic-package-price]');
|
||||
const rowUnitNode = row.querySelector('[data-dynamic-unit-price]');
|
||||
const unitBase = Number(rowUnitNode?.dataset.baseUnitPrice || 0);
|
||||
const packageBase = Number(rowPackageNode?.dataset.basePackagePrice || 0);
|
||||
const unitPriceRow = row.querySelector('[data-unit-price-row]');
|
||||
const showUnitPrice = !unitPriceRow?.classList.contains('is-price-placeholder') && unitBase > 0;
|
||||
const base = showUnitPrice ? unitBase : packageBase;
|
||||
const tiers = tierValues(base);
|
||||
const tierBase = row.querySelector('[data-tier-base]');
|
||||
const tierCash20 = row.querySelector('[data-tier-cash-20]');
|
||||
const tierCash50 = row.querySelector('[data-tier-cash-50]');
|
||||
const tierUnitLabel = row.querySelector('[data-tier-unit-label]');
|
||||
|
||||
if (tierBase) tierBase.textContent = formatRub(tiers.base);
|
||||
if (tierCash20) tierCash20.textContent = formatRub(tiers.cash20);
|
||||
if (tierCash50) tierCash50.textContent = formatRub(tiers.cash50);
|
||||
if (tierUnitLabel) tierUnitLabel.textContent = showUnitPrice
|
||||
? (row.querySelector('[data-unit-label]')?.textContent || 'кг')
|
||||
: 'фасовку';
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (unitNode || packageNode) {
|
||||
const unitBase = Number(unitNode?.dataset.baseUnitPrice || 0);
|
||||
const packageBase = Number(packageNode?.dataset.basePackagePrice || 0);
|
||||
@@ -759,7 +998,7 @@ try {
|
||||
const baseTotal = cartBaseTotal(items);
|
||||
const currentTotal = cartCurrentTotal(items);
|
||||
const savingTotal = Math.max(0, baseTotal - currentTotal);
|
||||
const delivery = deliveryCalculation(items);
|
||||
let delivery = deliveryCalculation(items);
|
||||
|
||||
if (emptyNode) {
|
||||
emptyNode.hidden = items.length !== 0;
|
||||
@@ -774,23 +1013,31 @@ try {
|
||||
const rowTotal = currentPrice * Number(item.quantity || 0);
|
||||
const priceChangedDown = available && item.priceChange === 'down';
|
||||
const packageChanged = available && item.packageChanged;
|
||||
const currentPackageLabel = formatPackageQuantity(item.packageQuantity, item.unit);
|
||||
const originalPackageLabel = formatPackageQuantity(item.originalPackageQuantity, item.unit);
|
||||
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')} кг. Цена пересчитана автоматически.`
|
||||
? `Изменилась фасовка: было ${originalPackageLabel}, сейчас ${currentPackageLabel}. Цена пересчитана автоматически.`
|
||||
: '';
|
||||
const rowStateBadges = [
|
||||
priceChangedDown ? '<span class="cart-row-state is-price-drop">Цена снизилась</span>' : '',
|
||||
packageChanged ? '<span class="cart-row-state is-package-change">Новая фасовка</span>' : '',
|
||||
!available ? '<span class="cart-row-state is-unavailable">Нет в наличии</span>' : ''
|
||||
].join('');
|
||||
const row = document.createElement('article');
|
||||
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">
|
||||
<div class="cart-row-states">${rowStateBadges}</div>
|
||||
<h2>${escapeHtml(item.name)}</h2>
|
||||
<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>` : ''}
|
||||
${packageChanged ? `<p class="cart-row-package-change" title="${escapeHtml(packageTitle)}">Фасовка обновилась после Вашего добавления в корзину: сейчас ${escapeHtml(currentPackageLabel)} вместо ${escapeHtml(originalPackageLabel)}. Сумма пересчитана автоматически.</p>` : ''}
|
||||
<div class="cart-row-controls">
|
||||
<button type="button" data-cart-minus ${available ? '' : 'disabled'}>-</button>
|
||||
<strong>${Number(item.quantity || 0)}</strong>
|
||||
@@ -1047,6 +1294,22 @@ try {
|
||||
update: updateAllPrices
|
||||
};
|
||||
|
||||
window.RybStockDelivery = {
|
||||
quote: deliveryQuote,
|
||||
resolveAddress: resolveDeliveryDistance,
|
||||
suggestAddress: suggestDeliveryAddress,
|
||||
setupAddressSuggestions: setupDeliveryAddressSuggestions,
|
||||
formatRub
|
||||
};
|
||||
|
||||
setupDeliveryAddressSuggestions();
|
||||
|
||||
document.addEventListener('focusin', (event) => {
|
||||
if (event.target.matches('[data-cart-address], #delivery-calculator input[name="address"]')) {
|
||||
setupDeliveryAddressSuggestions();
|
||||
}
|
||||
});
|
||||
|
||||
syncCartWithServer();
|
||||
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
@@ -1197,7 +1460,7 @@ try {
|
||||
const cartPage = form.closest('[data-cart-page]');
|
||||
const successNode = cartPage?.querySelector('[data-cart-order-success]');
|
||||
const items = getCartItems();
|
||||
const delivery = deliveryCalculation(items);
|
||||
let 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() || '';
|
||||
@@ -1217,6 +1480,38 @@ try {
|
||||
return;
|
||||
}
|
||||
|
||||
let deliveryType = getDeliveryType();
|
||||
const deliveryAddress = (localStorage.getItem(storageKeys.deliveryAddress) || '').trim();
|
||||
|
||||
if (deliveryType !== 'pickup' && deliveryAddress === '') {
|
||||
window.alert('Укажите адрес доставки.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (deliveryType !== 'pickup' && deliveryAddress !== '' && window.RybStockDelivery?.resolveAddress) {
|
||||
try {
|
||||
const resolved = await window.RybStockDelivery.resolveAddress(deliveryAddress);
|
||||
setDeliveryGeo(resolved.distance, resolved.zone, resolved.status);
|
||||
|
||||
if (resolved.zone === 'outside') {
|
||||
localStorage.setItem(storageKeys.deliveryType, 'outside_mkad');
|
||||
} else if (resolved.zone === 'inside') {
|
||||
localStorage.setItem(storageKeys.deliveryType, 'inside_mkad');
|
||||
}
|
||||
|
||||
deliveryType = getDeliveryType();
|
||||
delivery = deliveryCalculation(items);
|
||||
updateAllPrices();
|
||||
} catch (error) {
|
||||
setDeliveryGeo(0, '', 'Не удалось автоматически рассчитать адрес. Менеджер уточнит доставку по телефону.');
|
||||
}
|
||||
}
|
||||
|
||||
if (deliveryType === 'outside_mkad' && getDeliveryDistance() <= 0) {
|
||||
window.alert('Введите адрес доставки за МКАД и дождитесь автоматического расчета расстояния.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!delivery.minimumOk) {
|
||||
window.alert(delivery.minimumMessage);
|
||||
return;
|
||||
@@ -1241,8 +1536,8 @@ try {
|
||||
invoiceRequisites,
|
||||
invoiceFiles,
|
||||
paymentMode: getPaymentMode(),
|
||||
deliveryType: getDeliveryType(),
|
||||
address: localStorage.getItem(storageKeys.deliveryAddress) || '',
|
||||
deliveryType,
|
||||
address: deliveryAddress,
|
||||
distance: getDeliveryDistance(),
|
||||
deliveryZone: getDeliveryZone(),
|
||||
lift: getDeliveryLift(),
|
||||
|
||||
Reference in New Issue
Block a user