A continuación — productos universales No están vinculados a un modelo concreto y son compatibles con distintos vehículos, incluido Mazda CX-5 2012-2017.
@php use EvolutionCMS\Ddaudio\Facades\Domain; use EvolutionCMS\Ddaudio\Services\CustomerLoyaltyProgram; use EvolutionCMS\Ddaudio\Services\Product\StorefrontPricePresentation; use EvolutionCMS\Ddaudio\Services\Product\StorefrontFreeShipping; use EvolutionCMS\Ddaudio\Services\Product\StorefrontCardTerms; use EvolutionCMS\Ddaudio\Services\Product\StorefrontDeliveryTerms; $item = StorefrontDeliveryTerms::forProduct($item); $url = (string)($item['url'] ?? '#'); $title = trim((string)($item['pagetitle'] ?? '')); $normalizeImage = static function (mixed $image): string { $image = is_string($image) ? trim($image) : ''; return $image ==='' || preg_match('~^(?:https?:)?//|^data:|^/~i', $image) ? $image : '/' . ltrim($image, '/'); }; $sliderLarge = array_values((array)($item['sliderLarge'] ?? [])); $largeImages = []; $images = collect(array_values((array)($item['slider'] ?? []))) ->map(static fn(mixed $image, int $index): array => [$normalizeImage($image), $normalizeImage($sliderLarge[$index] ?? '')]) ->filter(static fn(array $pair): bool => $pair[0] !=='') ->unique(static fn(array $pair): string => $pair[0]) ->take(4) ->values() ->map(static function (array $pair) use (&$largeImages): string { if ($pair[1] !=='') { $largeImages[$pair[0]] = $pair[1]; } return $pair[0]; }) ->all(); $imageSrcset = static fn(string $image): string => isset($largeImages[$image]) ? $image . ' 320w, ' . $largeImages[$image] . ' 920w' : ''; $imageSizes ='(max-width: 575px) 100vw, (max-width: 1199px) 50vw, 360px'; if ($images === [] && is_string($item['image'] ?? null) && trim($item['image']) !=='') { $fallbackImage = trim($item['image']); $images[] = preg_match('~^(?:https?:)?//|^data:|^/~i', $fallbackImage) ? $fallbackImage : '/' . ltrim($fallbackImage, '/'); } $image = (string)($images[0] ?? ''); /* Фото немає — сірий логотип замість картинки товару. Клас is-placeholder не дає заглушці розтягнутись на всю плитку (див. card-product.scss). */ $imageIsPlaceholder = $image ===''; if ($imageIsPlaceholder) { $image = rtrim($storefrontThemeUrl, '/') . '/img/icons/logo-gray.svg'; } $hasImageSlider = count($images) > 1; $labels = (array)($item['labels'] ?? []); $rating = (array)($item['rating'] ?? []); $ratingValue = (float)($rating['rating'] ?? 0); $ratingTotal = 5.0; // total is the sum of votes, not the scale maximum. $ratingVotes = max(0, (int)($rating['votes'] ?? 0)); $ratingFilled = max(0, min(5, (int)round($ratingValue))); $currency = strtoupper((string)($_SESSION['currency'] ?? 'UAH')); $currencyLabel = \EvolutionCMS\Ddaudio\Support\StorefrontMoney::label($currency); $forSale = (bool)($item['forSale'] ?? false); $price = $item['price'] ?? null; $oldPrice = $item['price_old'] ?? null; $isAuthenticatedCustomer = (bool)evo()->getLoginUserID('web') && empty($_SESSION['webSellerId']); $pricePresentation = StorefrontPricePresentation::forProduct($item, $isAuthenticatedCustomer); $oldPrice = $pricePresentation['old_price']; $discountPercent = $pricePresentation['discount_percent']; $clientDiscountPercent = $pricePresentation['client_percent']; $isClientOnlyDiscount = $clientDiscountPercent > 0; $promoUntil = $pricePresentation['promo_until']; $formatCardAmount = static fn(float|int $amount): string => \EvolutionCMS\Ddaudio\Support\StorefrontMoney::format($amount); $showCardLoyaltyBonus = empty($_SESSION['webSellerId']) && Domain::isCountry('UA'); $stockCount = (int)($item['inStockCount'] ?? 0); $availabilityBadge = (array)($item['availabilityBadge'] ?? []); $availability = trim((string)($availabilityBadge['label'] ?? '')); $availabilityCta = (array)($availabilityBadge['cta'] ?? []); $availabilityCtaType = (string)($availabilityCta['type'] ?? ($stockCount > 0 ? 'cart' : 'request')); $isOrderable = (bool)($availabilityBadge['is_orderable'] ?? ($stockCount > 0)); $cardTerms = StorefrontCardTerms::forProduct($item); $productOrderId = (int)($item['productId'] ?? 0); $favoriteProductId = (int)($item['id'] ?? $productOrderId); if ((int)($item['template'] ?? 0) === 32) { $favoriteProductId = (int)($item['parent'] ?? $favoriteProductId); } $favoriteIds = array_map('intval', (array)($_SESSION['favorite'] ?? [])); $isFavorite = $favoriteProductId > 0 && in_array($favoriteProductId, $favoriteIds, true); $favoriteAddLabel = __('dd::global.favorite_add'); $favoriteRemoveLabel = __('dd::global.favorite_remove'); $canAddToCart = $forSale && $isOrderable && $availabilityCtaType ==='cart' && $productOrderId > 0; $canRequestProduct = $forSale && $isOrderable && $availabilityCtaType ==='request' && $productOrderId > 0; $managerEntries = []; $managerId = (int)evo()->getLoginUserID('mgr'); $isManager = $managerId > 0; $isWholesaler = !empty($_SESSION['webSellerId']); $cardMetaSku = trim((string)($item['sku'] ?? '')); $hasStaffVariants = ($isManager || $isWholesaler) && count((array)($item['children'] ?? [])) > 1; $cardMetaId = $hasStaffVariants ? $productOrderId : (int)($item['id'] ?? 0); $cardShowMetaId = $cardMetaId > 0 && \EvolutionCMS\Ddaudio\Services\Storefront\StorefrontProductIdVisibility::visible(); $hasKitComposition = !empty($item['stock']['data']['sku']['related']) && $productOrderId > 0; $canViewKitComposition = \EvolutionCMS\Ddaudio\Helpers\DevHelper::isManager() && $hasKitComposition; $hasCardNuances = ($isManager || $isWholesaler) && !empty($item['nuances']['full']); $cardMarginPercent = ($isManager || $isWholesaler) ? trim((string)($item['marginality']['percent'] ?? '')) : ''; $canPrintLabels = \EvolutionCMS\Ddaudio\Helpers\DevHelper::isManager() && $productOrderId > 0; $showCardManagerTools = ($isManager && $cardMetaId > 0) || $canViewKitComposition || $canPrintLabels || $hasCardNuances || $cardMarginPercent !==''; // "від" the lowest retail price; managers and wholesalers also see each variant's SKU and stock. $cardVariantLabel = trim((string)($item['variant_option_name'] ?? '')) ?: __('dd::storefront.variants'); $cardVariantOptions = []; if (count((array)($item['children'] ?? [])) > 1) { foreach ((array)($item['children'] ?? []) as $child) { if (!is_array($child) || (int)($child['id'] ?? 0) <= 0) { continue; } $child = StorefrontDeliveryTerms::forProduct($child); $childId = (int)$child['id']; $childBadge = (array)($child['availabilityBadge'] ?? []); $childStockCount = (int)($child['inStockCount'] ?? 0); $childCta = (array)($childBadge['cta'] ?? []); $childCtaType = (string)($childCta['type'] ?? ($childStockCount > 0 ? 'cart' : 'request')); $childOrderable = (bool)($childBadge['is_orderable'] ?? ($childStockCount > 0)); $childUnavailableForDomain = (Domain::isEuMarket() && (int)($child['not_sale_europe'] ?? 0) === 1) || (int)($child['not_sale_' . Domain::siteKey()] ?? 0) === 1 || (int)($child['not_sale'] ?? 0) === 1; $childForSale = (int)($child['hidemenu'] ?? 0) === 0 && !$childUnavailableForDomain; $childAvailability = trim((string)($childBadge['label'] ?? '')); if ($childAvailability ==='') { $childAvailability = trim(strip_tags((string)($child['inStock'] ?? ''))); } $childPrice = $child['price'] ?? ''; if (!$childForSale) { $childPrice = (int)($child['hidemenu'] ?? 0) === 1 ? __('dd::global.out_of_production') : __('dd::global.not_sale'); } $childPreorder = (array)($child['preorder'] ?? []); $childPreorder['hint'] = \EvolutionCMS\Ddaudio\Services\Preorder\PreorderHintText::for($childPreorder); $childPresentation = StorefrontPricePresentation::forProduct($child, $isAuthenticatedCustomer); $childOldPrice = $childForSale ? ($childPresentation['old_price'] ?? '') : ''; $childBonusAmount = $showCardLoyaltyBonus && $childForSale && is_numeric($childPrice) ? CustomerLoyaltyProgram::purchaseBonusAmount((float)$childPrice) : 0; $cardVariantOptions[] = [ 'id' => $childId, 'title' => trim((string)($child['pagetitle'] ?? '')) ?: '#' . $childId, 'url' => trim((string)($child['alias'] ?? '')) !=='' ? (string)$child['alias'] . '.html' : $url, 'price' => (string)$childPrice, 'oldPrice' => (string)$childOldPrice, 'discountPercent' => $childForSale ? $childPresentation['discount_percent'] : 0, 'clientPercent' => $childForSale ? $childPresentation['client_percent'] : 0, 'clientBasePrice' => $childForSale ? $childPresentation['client_base_price'] : null, 'delivery' => StorefrontCardTerms::forProduct($child), 'dispatch' => \EvolutionCMS\Ddaudio\Services\Product\StorefrontCardDispatch::forProduct(array_replace($child, ['forSale' => $childForSale])), 'promoUntil' => $childForSale ? $childPresentation['promo_until'] : '', 'priceRefreshRequired' => $childForSale && $childPresentation['refresh_required'], 'bonusAmount' => $childBonusAmount, 'forSale' => $childForSale, // ProductListItemPresenter turns the category TV into 'categories' and drops 'category'. 'freeShipping' => StorefrontFreeShipping::forProduct([ 'price' => $childPrice, 'forSale' => $childForSale, 'categories' => $item['categories'] ?? [], 'category' => $item['category'] ?? '', ]), 'isMarkdown' => \EvolutionCMS\Ddaudio\Services\Product\StorefrontMarkdown::forProduct($child), 'availability' => $childAvailability, 'stockCount' => $childStockCount, 'sku' => trim((string)($child['sku'] ?? '')), 'sku2' => trim((string)($child['sku2'] ?? '')), 'canCart' => $childForSale && $childOrderable && $childCtaType ==='cart', 'canRequest' => $childForSale && $childOrderable && $childCtaType ==='request', 'preorder' => $childPreorder, ]; } } $hasCardVariants = count($cardVariantOptions) > 1; // it); the card it returns has that variant chosen. Until then, as for retail, nothing is chosen. $staffVariantChosen = $hasStaffVariants && $hasCardVariants && !empty($item['variantChosen']); $cardVariantPending = $hasCardVariants && !$staffVariantChosen; $minimumCardVariantPrice = collect($cardVariantOptions) ->filter(static fn(array $option): bool => $option['forSale'] && is_numeric($option['price'])) ->min(static fn(array $option): float => (float)$option['price']); $variantHasCartAction = $cardVariantPending && collect($cardVariantOptions)->contains(static fn(array $option): bool => $option['canCart']); $variantHasRequestAction = $cardVariantPending && collect($cardVariantOptions)->contains(static fn(array $option): bool => $option['canRequest']); $loyaltyBonusPrice = $cardVariantPending && $minimumCardVariantPrice !== null ? $minimumCardVariantPrice : $price; $loyaltyBonusEligible = $cardVariantPending ? $minimumCardVariantPrice !== null : $forSale; $freeShipping = StorefrontFreeShipping::forProduct([ 'price' => $loyaltyBonusPrice, 'forSale' => $loyaltyBonusEligible, 'categories' => $item['categories'] ?? [], 'category' => $item['category'] ?? '', ]); $loyaltyBonusAmount = $showCardLoyaltyBonus && $loyaltyBonusEligible && is_numeric($loyaltyBonusPrice) ? CustomerLoyaltyProgram::purchaseBonusAmount((float)$loyaltyBonusPrice) : 0; $loyaltyBonusTemplate = __('dd::product.loyalty_bonus_amount', [ 'amount' => '__AMOUNT__', 'currency' => $currencyLabel, ]); $wholesaleStock = (array)($ite @php use EvolutionCMS\Ddaudio\Facades\Domain; use EvolutionCMS\Ddaudio\Services\CustomerLoyaltyProgram; use EvolutionCMS\Ddaudio\Services\Product\StorefrontPricePresentation; use EvolutionCMS\Ddaudio\Services\Product\StorefrontFreeShipping; use EvolutionCMS\Ddaudio\Services\Product\StorefrontCardTerms; use EvolutionCMS\Ddaudio\Services\Product\StorefrontDeliveryTerms; $item = StorefrontDeliveryTerms::forProduct($item); $url = (string)($item['url'] ?? '#'); $title = trim((string)($item['pagetitle'] ?? '')); $normalizeImage = static function (mixed $image): string { $image = is_string($image) ? trim($image) : ''; return $image ==='' || preg_match('~^(?:https?:)?//|^data:|^/~i', $image) ? $image : '/' . ltrim($image, '/'); }; $sliderLarge = array_values((array)($item['sliderLarge'] ?? [])); $largeImages = []; $images = collect(array_values((array)($item['slider'] ?? []))) ->map(static fn(mixed $image, int $index): array => [$normalizeImage($image), $normalizeImage($sliderLarge[$index] ?? '')]) ->filter(static fn(array $pair): bool => $pair[0] !=='') ->unique(static fn(array $pair): string => $pair[0]) ->take(4) ->values() ->map(static function (array $pair) use (&$largeImages): string { if ($pair[1] !=='') { $largeImages[$pair[0]] = $pair[1]; } return $pair[0]; }) ->all(); $imageSrcset = static fn(string $image): string => isset($largeImages[$image]) ? $image . ' 320w, ' . $largeImages[$image] . ' 920w' : ''; $imageSizes ='(max-width: 575px) 100vw, (max-width: 1199px) 50vw, 360px'; if ($images === [] && is_string($item['image'] ?? null) && trim($item['image']) !=='') { $fallbackImage = trim($item['image']); $images[] = preg_match('~^(?:https?:)?//|^data:|^/~i', $fallbackImage) ? $fallbackImage : '/' . ltrim($fallbackImage, '/'); } $image = (string)($images[0] ?? ''); /* Фото немає — сірий логотип замість картинки товару. Клас is-placeholder не дає заглушці розтягнутись на всю плитку (див. card-product.scss). */ $imageIsPlaceholder = $image ===''; if ($imageIsPlaceholder) { $image = rtrim($storefrontThemeUrl, '/') . '/img/icons/logo-gray.svg'; } $hasImageSlider = count($images) > 1; $labels = (array)($item['labels'] ?? []); $rating = (array)($item['rating'] ?? []); $ratingValue = (float)($rating['rating'] ?? 0); $ratingTotal = 5.0; // total is the sum of votes, not the scale maximum. $ratingVotes = max(0, (int)($rating['votes'] ?? 0)); $ratingFilled = max(0, min(5, (int)round($ratingValue))); $currency = strtoupper((string)($_SESSION['currency'] ?? 'UAH')); $currencyLabel = \EvolutionCMS\Ddaudio\Support\StorefrontMoney::label($currency); $forSale = (bool)($item['forSale'] ?? false); $price = $item['price'] ?? null; $oldPrice = $item['price_old'] ?? null; $isAuthenticatedCustomer = (bool)evo()->getLoginUserID('web') && empty($_SESSION['webSellerId']); $pricePresentation = StorefrontPricePresentation::forProduct($item, $isAuthenticatedCustomer); $oldPrice = $pricePresentation['old_price']; $discountPercent = $pricePresentation['discount_percent']; $clientDiscountPercent = $pricePresentation['client_percent']; $isClientOnlyDiscount = $clientDiscountPercent > 0; $promoUntil = $pricePresentation['promo_until']; $formatCardAmount = static fn(float|int $amount): string => \EvolutionCMS\Ddaudio\Support\StorefrontMoney::format($amount); $showCardLoyaltyBonus = empty($_SESSION['webSellerId']) && Domain::isCountry('UA'); $stockCount = (int)($item['inStockCount'] ?? 0); $availabilityBadge = (array)($item['availabilityBadge'] ?? []); $availability = trim((string)($availabilityBadge['label'] ?? '')); $availabilityCta = (array)($availabilityBadge['cta'] ?? []); $availabilityCtaType = (string)($availabilityCta['type'] ?? ($stockCount > 0 ? 'cart' : 'request')); $isOrderable = (bool)($availabilityBadge['is_orderable'] ?? ($stockCount > 0)); $cardTerms = StorefrontCardTerms::forProduct($item); $productOrderId = (int)($item['productId'] ?? 0); $favoriteProductId = (int)($item['id'] ?? $productOrderId); if ((int)($item['template'] ?? 0) === 32) { $favoriteProductId = (int)($item['parent'] ?? $favoriteProductId); } $favoriteIds = array_map('intval', (array)($_SESSION['favorite'] ?? [])); $isFavorite = $favoriteProductId > 0 && in_array($favoriteProductId, $favoriteIds, true); $favoriteAddLabel = __('dd::global.favorite_add'); $favoriteRemoveLabel = __('dd::global.favorite_remove'); $canAddToCart = $forSale && $isOrderable && $availabilityCtaType ==='cart' && $productOrderId > 0; $canRequestProduct = $forSale && $isOrderable && $availabilityCtaType ==='request' && $productOrderId > 0; $managerEntries = []; $managerId = (int)evo()->getLoginUserID('mgr'); $isManager = $managerId > 0; $isWholesaler = !empty($_SESSION['webSellerId']); $cardMetaSku = trim((string)($item['sku'] ?? '')); $hasStaffVariants = ($isManager || $isWholesaler) && count((array)($item['children'] ?? [])) > 1; $cardMetaId = $hasStaffVariants ? $productOrderId : (int)($item['id'] ?? 0); $cardShowMetaId = $cardMetaId > 0 && \EvolutionCMS\Ddaudio\Services\Storefront\StorefrontProductIdVisibility::visible(); $hasKitComposition = !empty($item['stock']['data']['sku']['related']) && $productOrderId > 0; $canViewKitComposition = \EvolutionCMS\Ddaudio\Helpers\DevHelper::isManager() && $hasKitComposition; $hasCardNuances = ($isManager || $isWholesaler) && !empty($item['nuances']['full']); $cardMarginPercent = ($isManager || $isWholesaler) ? trim((string)($item['marginality']['percent'] ?? '')) : ''; $canPrintLabels = \EvolutionCMS\Ddaudio\Helpers\DevHelper::isManager() && $productOrderId > 0; $showCardManagerTools = ($isManager && $cardMetaId > 0) || $canViewKitComposition || $canPrintLabels || $hasCardNuances || $cardMarginPercent !==''; // "від" the lowest retail price; managers and wholesalers also see each variant's SKU and stock. $cardVariantLabel = trim((string)($item['variant_option_name'] ?? '')) ?: __('dd::storefront.variants'); $cardVariantOptions = []; if (count((array)($item['children'] ?? [])) > 1) { foreach ((array)($item['children'] ?? []) as $child) { if (!is_array($child) || (int)($child['id'] ?? 0) <= 0) { continue; } $child = StorefrontDeliveryTerms::forProduct($child); $childId = (int)$child['id']; $childBadge = (array)($child['availabilityBadge'] ?? []); $childStockCount = (int)($child['inStockCount'] ?? 0); $childCta = (array)($childBadge['cta'] ?? []); $childCtaType = (string)($childCta['type'] ?? ($childStockCount > 0 ? 'cart' : 'request')); $childOrderable = (bool)($childBadge['is_orderable'] ?? ($childStockCount > 0)); $childUnavailableForDomain = (Domain::isEuMarket() && (int)($child['not_sale_europe'] ?? 0) === 1) || (int)($child['not_sale_' . Domain::siteKey()] ?? 0) === 1 || (int)($child['not_sale'] ?? 0) === 1; $childForSale = (int)($child['hidemenu'] ?? 0) === 0 && !$childUnavailableForDomain; $childAvailability = trim((string)($childBadge['label'] ?? '')); if ($childAvailability ==='') { $childAvailability = trim(strip_tags((string)($child['inStock'] ?? ''))); } $childPrice = $child['price'] ?? ''; if (!$childForSale) { $childPrice = (int)($child['hidemenu'] ?? 0) === 1 ? __('dd::global.out_of_production') : __('dd::global.not_sale'); } $childPreorder = (array)($child['preorder'] ?? []); $childPreorder['hint'] = \EvolutionCMS\Ddaudio\Services\Preorder\PreorderHintText::for($childPreorder); $childPresentation = StorefrontPricePresentation::forProduct($child, $isAuthenticatedCustomer); $childOldPrice = $childForSale ? ($childPresentation['old_price'] ?? '') : ''; $childBonusAmount = $showCardLoyaltyBonus && $childForSale && is_numeric($childPrice) ? CustomerLoyaltyProgram::purchaseBonusAmount((float)$childPrice) : 0; $cardVariantOptions[] = [ 'id' => $childId, 'title' => trim((string)($child['pagetitle'] ?? '')) ?: '#' . $childId, 'url' => trim((string)($child['alias'] ?? '')) !=='' ? (string)$child['alias'] . '.html' : $url, 'price' => (string)$childPrice, 'oldPrice' => (string)$childOldPrice, 'discountPercent' => $childForSale ? $childPresentation['discount_percent'] : 0, 'clientPercent' => $childForSale ? $childPresentation['client_percent'] : 0, 'clientBasePrice' => $childForSale ? $childPresentation['client_base_price'] : null, 'delivery' => StorefrontCardTerms::forProduct($child), 'dispatch' => \EvolutionCMS\Ddaudio\Services\Product\StorefrontCardDispatch::forProduct(array_replace($child, ['forSale' => $childForSale])), 'promoUntil' => $childForSale ? $childPresentation['promo_until'] : '', 'priceRefreshRequired' => $childForSale && $childPresentation['refresh_required'], 'bonusAmount' => $childBonusAmount, 'forSale' => $childForSale, // ProductListItemPresenter turns the category TV into 'categories' and drops 'category'. 'freeShipping' => StorefrontFreeShipping::forProduct([ 'price' => $childPrice, 'forSale' => $childForSale, 'categories' => $item['categories'] ?? [], 'category' => $item['category'] ?? '', ]), 'isMarkdown' => \EvolutionCMS\Ddaudio\Services\Product\StorefrontMarkdown::forProduct($child), 'availability' => $childAvailability, 'stockCount' => $childStockCount, 'sku' => trim((string)($child['sku'] ?? '')), 'sku2' => trim((string)($child['sku2'] ?? '')), 'canCart' => $childForSale && $childOrderable && $childCtaType ==='cart', 'canRequest' => $childForSale && $childOrderable && $childCtaType ==='request', 'preorder' => $childPreorder, ]; } } $hasCardVariants = count($cardVariantOptions) > 1; // it); the card it returns has that variant chosen. Until then, as for retail, nothing is chosen. $staffVariantChosen = $hasStaffVariants && $hasCardVariants && !empty($item['variantChosen']); $cardVariantPending = $hasCardVariants && !$staffVariantChosen; $minimumCardVariantPrice = collect($cardVariantOptions) ->filter(static fn(array $option): bool => $option['forSale'] && is_numeric($option['price'])) ->min(static fn(array $option): float => (float)$option['price']); $variantHasCartAction = $cardVariantPending && collect($cardVariantOptions)->contains(static fn(array $option): bool => $option['canCart']); $variantHasRequestAction = $cardVariantPending && collect($cardVariantOptions)->contains(static fn(array $option): bool => $option['canRequest']); $loyaltyBonusPrice = $cardVariantPending && $minimumCardVariantPrice !== null ? $minimumCardVariantPrice : $price; $loyaltyBonusEligible = $cardVariantPending ? $minimumCardVariantPrice !== null : $forSale; $freeShipping = StorefrontFreeShipping::forProduct([ 'price' => $loyaltyBonusPrice, 'forSale' => $loyaltyBonusEligible, 'categories' => $item['categories'] ?? [], 'category' => $item['category'] ?? '', ]); $loyaltyBonusAmount = $showCardLoyaltyBonus && $loyaltyBonusEligible && is_numeric($loyaltyBonusPrice) ? CustomerLoyaltyProgram::purchaseBonusAmount((float)$loyaltyBonusPrice) : 0; $loyaltyBonusTemplate = __('dd::product.loyalty_bonus_amount', [ 'amount' => '__AMOUNT__', 'currency' => $currencyLabel, ]); $wholesaleStock = (array)($ite @php use EvolutionCMS\Ddaudio\Facades\Domain; use EvolutionCMS\Ddaudio\Services\CustomerLoyaltyProgram; use EvolutionCMS\Ddaudio\Services\Product\StorefrontPricePresentation; use EvolutionCMS\Ddaudio\Services\Product\StorefrontFreeShipping; use EvolutionCMS\Ddaudio\Services\Product\StorefrontCardTerms; use EvolutionCMS\Ddaudio\Services\Product\StorefrontDeliveryTerms; $item = StorefrontDeliveryTerms::forProduct($item); $url = (string)($item['url'] ?? '#'); $title = trim((string)($item['pagetitle'] ?? '')); $normalizeImage = static function (mixed $image): string { $image = is_string($image) ? trim($image) : ''; return $image ==='' || preg_match('~^(?:https?:)?//|^data:|^/~i', $image) ? $image : '/' . ltrim($image, '/'); }; $sliderLarge = array_values((array)($item['sliderLarge'] ?? [])); $largeImages = []; $images = collect(array_values((array)($item['slider'] ?? []))) ->map(static fn(mixed $image, int $index): array => [$normalizeImage($image), $normalizeImage($sliderLarge[$index] ?? '')]) ->filter(static fn(array $pair): bool => $pair[0] !=='') ->unique(static fn(array $pair): string => $pair[0]) ->take(4) ->values() ->map(static function (array $pair) use (&$largeImages): string { if ($pair[1] !=='') { $largeImages[$pair[0]] = $pair[1]; } return $pair[0]; }) ->all(); $imageSrcset = static fn(string $image): string => isset($largeImages[$image]) ? $image . ' 320w, ' . $largeImages[$image] . ' 920w' : ''; $imageSizes ='(max-width: 575px) 100vw, (max-width: 1199px) 50vw, 360px'; if ($images === [] && is_string($item['image'] ?? null) && trim($item['image']) !=='') { $fallbackImage = trim($item['image']); $images[] = preg_match('~^(?:https?:)?//|^data:|^/~i', $fallbackImage) ? $fallbackImage : '/' . ltrim($fallbackImage, '/'); } $image = (string)($images[0] ?? ''); /* Фото немає — сірий логотип замість картинки товару. Клас is-placeholder не дає заглушці розтягнутись на всю плитку (див. card-product.scss). */ $imageIsPlaceholder = $image ===''; if ($imageIsPlaceholder) { $image = rtrim($storefrontThemeUrl, '/') . '/img/icons/logo-gray.svg'; } $hasImageSlider = count($images) > 1; $labels = (array)($item['labels'] ?? []); $rating = (array)($item['rating'] ?? []); $ratingValue = (float)($rating['rating'] ?? 0); $ratingTotal = 5.0; // total is the sum of votes, not the scale maximum. $ratingVotes = max(0, (int)($rating['votes'] ?? 0)); $ratingFilled = max(0, min(5, (int)round($ratingValue))); $currency = strtoupper((string)($_SESSION['currency'] ?? 'UAH')); $currencyLabel = \EvolutionCMS\Ddaudio\Support\StorefrontMoney::label($currency); $forSale = (bool)($item['forSale'] ?? false); $price = $item['price'] ?? null; $oldPrice = $item['price_old'] ?? null; $isAuthenticatedCustomer = (bool)evo()->getLoginUserID('web') && empty($_SESSION['webSellerId']); $pricePresentation = StorefrontPricePresentation::forProduct($item, $isAuthenticatedCustomer); $oldPrice = $pricePresentation['old_price']; $discountPercent = $pricePresentation['discount_percent']; $clientDiscountPercent = $pricePresentation['client_percent']; $isClientOnlyDiscount = $clientDiscountPercent > 0; $promoUntil = $pricePresentation['promo_until']; $formatCardAmount = static fn(float|int $amount): string => \EvolutionCMS\Ddaudio\Support\StorefrontMoney::format($amount); $showCardLoyaltyBonus = empty($_SESSION['webSellerId']) && Domain::isCountry('UA'); $stockCount = (int)($item['inStockCount'] ?? 0); $availabilityBadge = (array)($item['availabilityBadge'] ?? []); $availability = trim((string)($availabilityBadge['label'] ?? '')); $availabilityCta = (array)($availabilityBadge['cta'] ?? []); $availabilityCtaType = (string)($availabilityCta['type'] ?? ($stockCount > 0 ? 'cart' : 'request')); $isOrderable = (bool)($availabilityBadge['is_orderable'] ?? ($stockCount > 0)); $cardTerms = StorefrontCardTerms::forProduct($item); $productOrderId = (int)($item['productId'] ?? 0); $favoriteProductId = (int)($item['id'] ?? $productOrderId); if ((int)($item['template'] ?? 0) === 32) { $favoriteProductId = (int)($item['parent'] ?? $favoriteProductId); } $favoriteIds = array_map('intval', (array)($_SESSION['favorite'] ?? [])); $isFavorite = $favoriteProductId > 0 && in_array($favoriteProductId, $favoriteIds, true); $favoriteAddLabel = __('dd::global.favorite_add'); $favoriteRemoveLabel = __('dd::global.favorite_remove'); $canAddToCart = $forSale && $isOrderable && $availabilityCtaType ==='cart' && $productOrderId > 0; $canRequestProduct = $forSale && $isOrderable && $availabilityCtaType ==='request' && $productOrderId > 0; $managerEntries = []; $managerId = (int)evo()->getLoginUserID('mgr'); $isManager = $managerId > 0; $isWholesaler = !empty($_SESSION['webSellerId']); $cardMetaSku = trim((string)($item['sku'] ?? '')); $hasStaffVariants = ($isManager || $isWholesaler) && count((array)($item['children'] ?? [])) > 1; $cardMetaId = $hasStaffVariants ? $productOrderId : (int)($item['id'] ?? 0); $cardShowMetaId = $cardMetaId > 0 && \EvolutionCMS\Ddaudio\Services\Storefront\StorefrontProductIdVisibility::visible(); $hasKitComposition = !empty($item['stock']['data']['sku']['related']) && $productOrderId > 0; $canViewKitComposition = \EvolutionCMS\Ddaudio\Helpers\DevHelper::isManager() && $hasKitComposition; $hasCardNuances = ($isManager || $isWholesaler) && !empty($item['nuances']['full']); $cardMarginPercent = ($isManager || $isWholesaler) ? trim((string)($item['marginality']['percent'] ?? '')) : ''; $canPrintLabels = \EvolutionCMS\Ddaudio\Helpers\DevHelper::isManager() && $productOrderId > 0; $showCardManagerTools = ($isManager && $cardMetaId > 0) || $canViewKitComposition || $canPrintLabels || $hasCardNuances || $cardMarginPercent !==''; // "від" the lowest retail price; managers and wholesalers also see each variant's SKU and stock. $cardVariantLabel = trim((string)($item['variant_option_name'] ?? '')) ?: __('dd::storefront.variants'); $cardVariantOptions = []; if (count((array)($item['children'] ?? [])) > 1) { foreach ((array)($item['children'] ?? []) as $child) { if (!is_array($child) || (int)($child['id'] ?? 0) <= 0) { continue; } $child = StorefrontDeliveryTerms::forProduct($child); $childId = (int)$child['id']; $childBadge = (array)($child['availabilityBadge'] ?? []); $childStockCount = (int)($child['inStockCount'] ?? 0); $childCta = (array)($childBadge['cta'] ?? []); $childCtaType = (string)($childCta['type'] ?? ($childStockCount > 0 ? 'cart' : 'request')); $childOrderable = (bool)($childBadge['is_orderable'] ?? ($childStockCount > 0)); $childUnavailableForDomain = (Domain::isEuMarket() && (int)($child['not_sale_europe'] ?? 0) === 1) || (int)($child['not_sale_' . Domain::siteKey()] ?? 0) === 1 || (int)($child['not_sale'] ?? 0) === 1; $childForSale = (int)($child['hidemenu'] ?? 0) === 0 && !$childUnavailableForDomain; $childAvailability = trim((string)($childBadge['label'] ?? '')); if ($childAvailability ==='') { $childAvailability = trim(strip_tags((string)($child['inStock'] ?? ''))); } $childPrice = $child['price'] ?? ''; if (!$childForSale) { $childPrice = (int)($child['hidemenu'] ?? 0) === 1 ? __('dd::global.out_of_production') : __('dd::global.not_sale'); } $childPreorder = (array)($child['preorder'] ?? []); $childPreorder['hint'] = \EvolutionCMS\Ddaudio\Services\Preorder\PreorderHintText::for($childPreorder); $childPresentation = StorefrontPricePresentation::forProduct($child, $isAuthenticatedCustomer); $childOldPrice = $childForSale ? ($childPresentation['old_price'] ?? '') : ''; $childBonusAmount = $showCardLoyaltyBonus && $childForSale && is_numeric($childPrice) ? CustomerLoyaltyProgram::purchaseBonusAmount((float)$childPrice) : 0; $cardVariantOptions[] = [ 'id' => $childId, 'title' => trim((string)($child['pagetitle'] ?? '')) ?: '#' . $childId, 'url' => trim((string)($child['alias'] ?? '')) !=='' ? (string)$child['alias'] . '.html' : $url, 'price' => (string)$childPrice, 'oldPrice' => (string)$childOldPrice, 'discountPercent' => $childForSale ? $childPresentation['discount_percent'] : 0, 'clientPercent' => $childForSale ? $childPresentation['client_percent'] : 0, 'clientBasePrice' => $childForSale ? $childPresentation['client_base_price'] : null, 'delivery' => StorefrontCardTerms::forProduct($child), 'dispatch' => \EvolutionCMS\Ddaudio\Services\Product\StorefrontCardDispatch::forProduct(array_replace($child, ['forSale' => $childForSale])), 'promoUntil' => $childForSale ? $childPresentation['promo_until'] : '', 'priceRefreshRequired' => $childForSale && $childPresentation['refresh_required'], 'bonusAmount' => $childBonusAmount, 'forSale' => $childForSale, // ProductListItemPresenter turns the category TV into 'categories' and drops 'category'. 'freeShipping' => StorefrontFreeShipping::forProduct([ 'price' => $childPrice, 'forSale' => $childForSale, 'categories' => $item['categories'] ?? [], 'category' => $item['category'] ?? '', ]), 'isMarkdown' => \EvolutionCMS\Ddaudio\Services\Product\StorefrontMarkdown::forProduct($child), 'availability' => $childAvailability, 'stockCount' => $childStockCount, 'sku' => trim((string)($child['sku'] ?? '')), 'sku2' => trim((string)($child['sku2'] ?? '')), 'canCart' => $childForSale && $childOrderable && $childCtaType ==='cart', 'canRequest' => $childForSale && $childOrderable && $childCtaType ==='request', 'preorder' => $childPreorder, ]; } } $hasCardVariants = count($cardVariantOptions) > 1; // it); the card it returns has that variant chosen. Until then, as for retail, nothing is chosen. $staffVariantChosen = $hasStaffVariants && $hasCardVariants && !empty($item['variantChosen']); $cardVariantPending = $hasCardVariants && !$staffVariantChosen; $minimumCardVariantPrice = collect($cardVariantOptions) ->filter(static fn(array $option): bool => $option['forSale'] && is_numeric($option['price'])) ->min(static fn(array $option): float => (float)$option['price']); $variantHasCartAction = $cardVariantPending && collect($cardVariantOptions)->contains(static fn(array $option): bool => $option['canCart']); $variantHasRequestAction = $cardVariantPending && collect($cardVariantOptions)->contains(static fn(array $option): bool => $option['canRequest']); $loyaltyBonusPrice = $cardVariantPending && $minimumCardVariantPrice !== null ? $minimumCardVariantPrice : $price; $loyaltyBonusEligible = $cardVariantPending ? $minimumCardVariantPrice !== null : $forSale; $freeShipping = StorefrontFreeShipping::forProduct([ 'price' => $loyaltyBonusPrice, 'forSale' => $loyaltyBonusEligible, 'categories' => $item['categories'] ?? [], 'category' => $item['category'] ?? '', ]); $loyaltyBonusAmount = $showCardLoyaltyBonus && $loyaltyBonusEligible && is_numeric($loyaltyBonusPrice) ? CustomerLoyaltyProgram::purchaseBonusAmount((float)$loyaltyBonusPrice) : 0; $loyaltyBonusTemplate = __('dd::product.loyalty_bonus_amount', [ 'amount' => '__AMOUNT__', 'currency' => $currencyLabel, ]); $wholesaleStock = (array)($ite @php use EvolutionCMS\Ddaudio\Facades\Domain; use EvolutionCMS\Ddaudio\Services\CustomerLoyaltyProgram; use EvolutionCMS\Ddaudio\Services\Product\StorefrontPricePresentation; use EvolutionCMS\Ddaudio\Services\Product\StorefrontFreeShipping; use EvolutionCMS\Ddaudio\Services\Product\StorefrontCardTerms; use EvolutionCMS\Ddaudio\Services\Product\StorefrontDeliveryTerms; $item = StorefrontDeliveryTerms::forProduct($item); $url = (string)($item['url'] ?? '#'); $title = trim((string)($item['pagetitle'] ?? '')); $normalizeImage = static function (mixed $image): string { $image = is_string($image) ? trim($image) : ''; return $image ==='' || preg_match('~^(?:https?:)?//|^data:|^/~i', $image) ? $image : '/' . ltrim($image, '/'); }; $sliderLarge = array_values((array)($item['sliderLarge'] ?? [])); $largeImages = []; $images = collect(array_values((array)($item['slider'] ?? []))) ->map(static fn(mixed $image, int $index): array => [$normalizeImage($image), $normalizeImage($sliderLarge[$index] ?? '')]) ->filter(static fn(array $pair): bool => $pair[0] !=='') ->unique(static fn(array $pair): string => $pair[0]) ->take(4) ->values() ->map(static function (array $pair) use (&$largeImages): string { if ($pair[1] !=='') { $largeImages[$pair[0]] = $pair[1]; } return $pair[0]; }) ->all(); $imageSrcset = static fn(string $image): string => isset($largeImages[$image]) ? $image . ' 320w, ' . $largeImages[$image] . ' 920w' : ''; $imageSizes ='(max-width: 575px) 100vw, (max-width: 1199px) 50vw, 360px'; if ($images === [] && is_string($item['image'] ?? null) && trim($item['image']) !=='') { $fallbackImage = trim($item['image']); $images[] = preg_match('~^(?:https?:)?//|^data:|^/~i', $fallbackImage) ? $fallbackImage : '/' . ltrim($fallbackImage, '/'); } $image = (string)($images[0] ?? ''); /* Фото немає — сірий логотип замість картинки товару. Клас is-placeholder не дає заглушці розтягнутись на всю плитку (див. card-product.scss). */ $imageIsPlaceholder = $image ===''; if ($imageIsPlaceholder) { $image = rtrim($storefrontThemeUrl, '/') . '/img/icons/logo-gray.svg'; } $hasImageSlider = count($images) > 1; $labels = (array)($item['labels'] ?? []); $rating = (array)($item['rating'] ?? []); $ratingValue = (float)($rating['rating'] ?? 0); $ratingTotal = 5.0; // total is the sum of votes, not the scale maximum. $ratingVotes = max(0, (int)($rating['votes'] ?? 0)); $ratingFilled = max(0, min(5, (int)round($ratingValue))); $currency = strtoupper((string)($_SESSION['currency'] ?? 'UAH')); $currencyLabel = \EvolutionCMS\Ddaudio\Support\StorefrontMoney::label($currency); $forSale = (bool)($item['forSale'] ?? false); $price = $item['price'] ?? null; $oldPrice = $item['price_old'] ?? null; $isAuthenticatedCustomer = (bool)evo()->getLoginUserID('web') && empty($_SESSION['webSellerId']); $pricePresentation = StorefrontPricePresentation::forProduct($item, $isAuthenticatedCustomer); $oldPrice = $pricePresentation['old_price']; $discountPercent = $pricePresentation['discount_percent']; $clientDiscountPercent = $pricePresentation['client_percent']; $isClientOnlyDiscount = $clientDiscountPercent > 0; $promoUntil = $pricePresentation['promo_until']; $formatCardAmount = static fn(float|int $amount): string => \EvolutionCMS\Ddaudio\Support\StorefrontMoney::format($amount); $showCardLoyaltyBonus = empty($_SESSION['webSellerId']) && Domain::isCountry('UA'); $stockCount = (int)($item['inStockCount'] ?? 0); $availabilityBadge = (array)($item['availabilityBadge'] ?? []); $availability = trim((string)($availabilityBadge['label'] ?? '')); $availabilityCta = (array)($availabilityBadge['cta'] ?? []); $availabilityCtaType = (string)($availabilityCta['type'] ?? ($stockCount > 0 ? 'cart' : 'request')); $isOrderable = (bool)($availabilityBadge['is_orderable'] ?? ($stockCount > 0)); $cardTerms = StorefrontCardTerms::forProduct($item); $productOrderId = (int)($item['productId'] ?? 0); $favoriteProductId = (int)($item['id'] ?? $productOrderId); if ((int)($item['template'] ?? 0) === 32) { $favoriteProductId = (int)($item['parent'] ?? $favoriteProductId); } $favoriteIds = array_map('intval', (array)($_SESSION['favorite'] ?? [])); $isFavorite = $favoriteProductId > 0 && in_array($favoriteProductId, $favoriteIds, true); $favoriteAddLabel = __('dd::global.favorite_add'); $favoriteRemoveLabel = __('dd::global.favorite_remove'); $canAddToCart = $forSale && $isOrderable && $availabilityCtaType ==='cart' && $productOrderId > 0; $canRequestProduct = $forSale && $isOrderable && $availabilityCtaType ==='request' && $productOrderId > 0; $managerEntries = []; $managerId = (int)evo()->getLoginUserID('mgr'); $isManager = $managerId > 0; $isWholesaler = !empty($_SESSION['webSellerId']); $cardMetaSku = trim((string)($item['sku'] ?? '')); $hasStaffVariants = ($isManager || $isWholesaler) && count((array)($item['children'] ?? [])) > 1; $cardMetaId = $hasStaffVariants ? $productOrderId : (int)($item['id'] ?? 0); $cardShowMetaId = $cardMetaId > 0 && \EvolutionCMS\Ddaudio\Services\Storefront\StorefrontProductIdVisibility::visible(); $hasKitComposition = !empty($item['stock']['data']['sku']['related']) && $productOrderId > 0; $canViewKitComposition = \EvolutionCMS\Ddaudio\Helpers\DevHelper::isManager() && $hasKitComposition; $hasCardNuances = ($isManager || $isWholesaler) && !empty($item['nuances']['full']); $cardMarginPercent = ($isManager || $isWholesaler) ? trim((string)($item['marginality']['percent'] ?? '')) : ''; $canPrintLabels = \EvolutionCMS\Ddaudio\Helpers\DevHelper::isManager() && $productOrderId > 0; $showCardManagerTools = ($isManager && $cardMetaId > 0) || $canViewKitComposition || $canPrintLabels || $hasCardNuances || $cardMarginPercent !==''; // "від" the lowest retail price; managers and wholesalers also see each variant's SKU and stock. $cardVariantLabel = trim((string)($item['variant_option_name'] ?? '')) ?: __('dd::storefront.variants'); $cardVariantOptions = []; if (count((array)($item['children'] ?? [])) > 1) { foreach ((array)($item['children'] ?? []) as $child) { if (!is_array($child) || (int)($child['id'] ?? 0) <= 0) { continue; } $child = StorefrontDeliveryTerms::forProduct($child); $childId = (int)$child['id']; $childBadge = (array)($child['availabilityBadge'] ?? []); $childStockCount = (int)($child['inStockCount'] ?? 0); $childCta = (array)($childBadge['cta'] ?? []); $childCtaType = (string)($childCta['type'] ?? ($childStockCount > 0 ? 'cart' : 'request')); $childOrderable = (bool)($childBadge['is_orderable'] ?? ($childStockCount > 0)); $childUnavailableForDomain = (Domain::isEuMarket() && (int)($child['not_sale_europe'] ?? 0) === 1) || (int)($child['not_sale_' . Domain::siteKey()] ?? 0) === 1 || (int)($child['not_sale'] ?? 0) === 1; $childForSale = (int)($child['hidemenu'] ?? 0) === 0 && !$childUnavailableForDomain; $childAvailability = trim((string)($childBadge['label'] ?? '')); if ($childAvailability ==='') { $childAvailability = trim(strip_tags((string)($child['inStock'] ?? ''))); } $childPrice = $child['price'] ?? ''; if (!$childForSale) { $childPrice = (int)($child['hidemenu'] ?? 0) === 1 ? __('dd::global.out_of_production') : __('dd::global.not_sale'); } $childPreorder = (array)($child['preorder'] ?? []); $childPreorder['hint'] = \EvolutionCMS\Ddaudio\Services\Preorder\PreorderHintText::for($childPreorder); $childPresentation = StorefrontPricePresentation::forProduct($child, $isAuthenticatedCustomer); $childOldPrice = $childForSale ? ($childPresentation['old_price'] ?? '') : ''; $childBonusAmount = $showCardLoyaltyBonus && $childForSale && is_numeric($childPrice) ? CustomerLoyaltyProgram::purchaseBonusAmount((float)$childPrice) : 0; $cardVariantOptions[] = [ 'id' => $childId, 'title' => trim((string)($child['pagetitle'] ?? '')) ?: '#' . $childId, 'url' => trim((string)($child['alias'] ?? '')) !=='' ? (string)$child['alias'] . '.html' : $url, 'price' => (string)$childPrice, 'oldPrice' => (string)$childOldPrice, 'discountPercent' => $childForSale ? $childPresentation['discount_percent'] : 0, 'clientPercent' => $childForSale ? $childPresentation['client_percent'] : 0, 'clientBasePrice' => $childForSale ? $childPresentation['client_base_price'] : null, 'delivery' => StorefrontCardTerms::forProduct($child), 'dispatch' => \EvolutionCMS\Ddaudio\Services\Product\StorefrontCardDispatch::forProduct(array_replace($child, ['forSale' => $childForSale])), 'promoUntil' => $childForSale ? $childPresentation['promo_until'] : '', 'priceRefreshRequired' => $childForSale && $childPresentation['refresh_required'], 'bonusAmount' => $childBonusAmount, 'forSale' => $childForSale, // ProductListItemPresenter turns the category TV into 'categories' and drops 'category'. 'freeShipping' => StorefrontFreeShipping::forProduct([ 'price' => $childPrice, 'forSale' => $childForSale, 'categories' => $item['categories'] ?? [], 'category' => $item['category'] ?? '', ]), 'isMarkdown' => \EvolutionCMS\Ddaudio\Services\Product\StorefrontMarkdown::forProduct($child), 'availability' => $childAvailability, 'stockCount' => $childStockCount, 'sku' => trim((string)($child['sku'] ?? '')), 'sku2' => trim((string)($child['sku2'] ?? '')), 'canCart' => $childForSale && $childOrderable && $childCtaType ==='cart', 'canRequest' => $childForSale && $childOrderable && $childCtaType ==='request', 'preorder' => $childPreorder, ]; } } $hasCardVariants = count($cardVariantOptions) > 1; // it); the card it returns has that variant chosen. Until then, as for retail, nothing is chosen. $staffVariantChosen = $hasStaffVariants && $hasCardVariants && !empty($item['variantChosen']); $cardVariantPending = $hasCardVariants && !$staffVariantChosen; $minimumCardVariantPrice = collect($cardVariantOptions) ->filter(static fn(array $option): bool => $option['forSale'] && is_numeric($option['price'])) ->min(static fn(array $option): float => (float)$option['price']); $variantHasCartAction = $cardVariantPending && collect($cardVariantOptions)->contains(static fn(array $option): bool => $option['canCart']); $variantHasRequestAction = $cardVariantPending && collect($cardVariantOptions)->contains(static fn(array $option): bool => $option['canRequest']); $loyaltyBonusPrice = $cardVariantPending && $minimumCardVariantPrice !== null ? $minimumCardVariantPrice : $price; $loyaltyBonusEligible = $cardVariantPending ? $minimumCardVariantPrice !== null : $forSale; $freeShipping = StorefrontFreeShipping::forProduct([ 'price' => $loyaltyBonusPrice, 'forSale' => $loyaltyBonusEligible, 'categories' => $item['categories'] ?? [], 'category' => $item['category'] ?? '', ]); $loyaltyBonusAmount = $showCardLoyaltyBonus && $loyaltyBonusEligible && is_numeric($loyaltyBonusPrice) ? CustomerLoyaltyProgram::purchaseBonusAmount((float)$loyaltyBonusPrice) : 0; $loyaltyBonusTemplate = __('dd::product.loyalty_bonus_amount', [ 'amount' => '__AMOUNT__', 'currency' => $currencyLabel, ]); $wholesaleStock = (array)($ite @php use EvolutionCMS\Ddaudio\Facades\Domain; use EvolutionCMS\Ddaudio\Services\CustomerLoyaltyProgram; use EvolutionCMS\Ddaudio\Services\Product\StorefrontPricePresentation; use EvolutionCMS\Ddaudio\Services\Product\StorefrontFreeShipping; use EvolutionCMS\Ddaudio\Services\Product\StorefrontCardTerms; use EvolutionCMS\Ddaudio\Services\Product\StorefrontDeliveryTerms; $item = StorefrontDeliveryTerms::forProduct($item); $url = (string)($item['url'] ?? '#'); $title = trim((string)($item['pagetitle'] ?? '')); $normalizeImage = static function (mixed $image): string { $image = is_string($image) ? trim($image) : ''; return $image ==='' || preg_match('~^(?:https?:)?//|^data:|^/~i', $image) ? $image : '/' . ltrim($image, '/'); }; $sliderLarge = array_values((array)($item['sliderLarge'] ?? [])); $largeImages = []; $images = collect(array_values((array)($item['slider'] ?? []))) ->map(static fn(mixed $image, int $index): array => [$normalizeImage($image), $normalizeImage($sliderLarge[$index] ?? '')]) ->filter(static fn(array $pair): bool => $pair[0] !=='') ->unique(static fn(array $pair): string => $pair[0]) ->take(4) ->values() ->map(static function (array $pair) use (&$largeImages): string { if ($pair[1] !=='') { $largeImages[$pair[0]] = $pair[1]; } return $pair[0]; }) ->all(); $imageSrcset = static fn(string $image): string => isset($largeImages[$image]) ? $image . ' 320w, ' . $largeImages[$image] . ' 920w' : ''; $imageSizes ='(max-width: 575px) 100vw, (max-width: 1199px) 50vw, 360px'; if ($images === [] && is_string($item['image'] ?? null) && trim($item['image']) !=='') { $fallbackImage = trim($item['image']); $images[] = preg_match('~^(?:https?:)?//|^data:|^/~i', $fallbackImage) ? $fallbackImage : '/' . ltrim($fallbackImage, '/'); } $image = (string)($images[0] ?? ''); /* Фото немає — сірий логотип замість картинки товару. Клас is-placeholder не дає заглушці розтягнутись на всю плитку (див. card-product.scss). */ $imageIsPlaceholder = $image ===''; if ($imageIsPlaceholder) { $image = rtrim($storefrontThemeUrl, '/') . '/img/icons/logo-gray.svg'; } $hasImageSlider = count($images) > 1; $labels = (array)($item['labels'] ?? []); $rating = (array)($item['rating'] ?? []); $ratingValue = (float)($rating['rating'] ?? 0); $ratingTotal = 5.0; // total is the sum of votes, not the scale maximum. $ratingVotes = max(0, (int)($rating['votes'] ?? 0)); $ratingFilled = max(0, min(5, (int)round($ratingValue))); $currency = strtoupper((string)($_SESSION['currency'] ?? 'UAH')); $currencyLabel = \EvolutionCMS\Ddaudio\Support\StorefrontMoney::label($currency); $forSale = (bool)($item['forSale'] ?? false); $price = $item['price'] ?? null; $oldPrice = $item['price_old'] ?? null; $isAuthenticatedCustomer = (bool)evo()->getLoginUserID('web') && empty($_SESSION['webSellerId']); $pricePresentation = StorefrontPricePresentation::forProduct($item, $isAuthenticatedCustomer); $oldPrice = $pricePresentation['old_price']; $discountPercent = $pricePresentation['discount_percent']; $clientDiscountPercent = $pricePresentation['client_percent']; $isClientOnlyDiscount = $clientDiscountPercent > 0; $promoUntil = $pricePresentation['promo_until']; $formatCardAmount = static fn(float|int $amount): string => \EvolutionCMS\Ddaudio\Support\StorefrontMoney::format($amount); $showCardLoyaltyBonus = empty($_SESSION['webSellerId']) && Domain::isCountry('UA'); $stockCount = (int)($item['inStockCount'] ?? 0); $availabilityBadge = (array)($item['availabilityBadge'] ?? []); $availability = trim((string)($availabilityBadge['label'] ?? '')); $availabilityCta = (array)($availabilityBadge['cta'] ?? []); $availabilityCtaType = (string)($availabilityCta['type'] ?? ($stockCount > 0 ? 'cart' : 'request')); $isOrderable = (bool)($availabilityBadge['is_orderable'] ?? ($stockCount > 0)); $cardTerms = StorefrontCardTerms::forProduct($item); $productOrderId = (int)($item['productId'] ?? 0); $favoriteProductId = (int)($item['id'] ?? $productOrderId); if ((int)($item['template'] ?? 0) === 32) { $favoriteProductId = (int)($item['parent'] ?? $favoriteProductId); } $favoriteIds = array_map('intval', (array)($_SESSION['favorite'] ?? [])); $isFavorite = $favoriteProductId > 0 && in_array($favoriteProductId, $favoriteIds, true); $favoriteAddLabel = __('dd::global.favorite_add'); $favoriteRemoveLabel = __('dd::global.favorite_remove'); $canAddToCart = $forSale && $isOrderable && $availabilityCtaType ==='cart' && $productOrderId > 0; $canRequestProduct = $forSale && $isOrderable && $availabilityCtaType ==='request' && $productOrderId > 0; $managerEntries = []; $managerId = (int)evo()->getLoginUserID('mgr'); $isManager = $managerId > 0; $isWholesaler = !empty($_SESSION['webSellerId']); $cardMetaSku = trim((string)($item['sku'] ?? '')); $hasStaffVariants = ($isManager || $isWholesaler) && count((array)($item['children'] ?? [])) > 1; $cardMetaId = $hasStaffVariants ? $productOrderId : (int)($item['id'] ?? 0); $cardShowMetaId = $cardMetaId > 0 && \EvolutionCMS\Ddaudio\Services\Storefront\StorefrontProductIdVisibility::visible(); $hasKitComposition = !empty($item['stock']['data']['sku']['related']) && $productOrderId > 0; $canViewKitComposition = \EvolutionCMS\Ddaudio\Helpers\DevHelper::isManager() && $hasKitComposition; $hasCardNuances = ($isManager || $isWholesaler) && !empty($item['nuances']['full']); $cardMarginPercent = ($isManager || $isWholesaler) ? trim((string)($item['marginality']['percent'] ?? '')) : ''; $canPrintLabels = \EvolutionCMS\Ddaudio\Helpers\DevHelper::isManager() && $productOrderId > 0; $showCardManagerTools = ($isManager && $cardMetaId > 0) || $canViewKitComposition || $canPrintLabels || $hasCardNuances || $cardMarginPercent !==''; // "від" the lowest retail price; managers and wholesalers also see each variant's SKU and stock. $cardVariantLabel = trim((string)($item['variant_option_name'] ?? '')) ?: __('dd::storefront.variants'); $cardVariantOptions = []; if (count((array)($item['children'] ?? [])) > 1) { foreach ((array)($item['children'] ?? []) as $child) { if (!is_array($child) || (int)($child['id'] ?? 0) <= 0) { continue; } $child = StorefrontDeliveryTerms::forProduct($child); $childId = (int)$child['id']; $childBadge = (array)($child['availabilityBadge'] ?? []); $childStockCount = (int)($child['inStockCount'] ?? 0); $childCta = (array)($childBadge['cta'] ?? []); $childCtaType = (string)($childCta['type'] ?? ($childStockCount > 0 ? 'cart' : 'request')); $childOrderable = (bool)($childBadge['is_orderable'] ?? ($childStockCount > 0)); $childUnavailableForDomain = (Domain::isEuMarket() && (int)($child['not_sale_europe'] ?? 0) === 1) || (int)($child['not_sale_' . Domain::siteKey()] ?? 0) === 1 || (int)($child['not_sale'] ?? 0) === 1; $childForSale = (int)($child['hidemenu'] ?? 0) === 0 && !$childUnavailableForDomain; $childAvailability = trim((string)($childBadge['label'] ?? '')); if ($childAvailability ==='') { $childAvailability = trim(strip_tags((string)($child['inStock'] ?? ''))); } $childPrice = $child['price'] ?? ''; if (!$childForSale) { $childPrice = (int)($child['hidemenu'] ?? 0) === 1 ? __('dd::global.out_of_production') : __('dd::global.not_sale'); } $childPreorder = (array)($child['preorder'] ?? []); $childPreorder['hint'] = \EvolutionCMS\Ddaudio\Services\Preorder\PreorderHintText::for($childPreorder); $childPresentation = StorefrontPricePresentation::forProduct($child, $isAuthenticatedCustomer); $childOldPrice = $childForSale ? ($childPresentation['old_price'] ?? '') : ''; $childBonusAmount = $showCardLoyaltyBonus && $childForSale && is_numeric($childPrice) ? CustomerLoyaltyProgram::purchaseBonusAmount((float)$childPrice) : 0; $cardVariantOptions[] = [ 'id' => $childId, 'title' => trim((string)($child['pagetitle'] ?? '')) ?: '#' . $childId, 'url' => trim((string)($child['alias'] ?? '')) !=='' ? (string)$child['alias'] . '.html' : $url, 'price' => (string)$childPrice, 'oldPrice' => (string)$childOldPrice, 'discountPercent' => $childForSale ? $childPresentation['discount_percent'] : 0, 'clientPercent' => $childForSale ? $childPresentation['client_percent'] : 0, 'clientBasePrice' => $childForSale ? $childPresentation['client_base_price'] : null, 'delivery' => StorefrontCardTerms::forProduct($child), 'dispatch' => \EvolutionCMS\Ddaudio\Services\Product\StorefrontCardDispatch::forProduct(array_replace($child, ['forSale' => $childForSale])), 'promoUntil' => $childForSale ? $childPresentation['promo_until'] : '', 'priceRefreshRequired' => $childForSale && $childPresentation['refresh_required'], 'bonusAmount' => $childBonusAmount, 'forSale' => $childForSale, // ProductListItemPresenter turns the category TV into 'categories' and drops 'category'. 'freeShipping' => StorefrontFreeShipping::forProduct([ 'price' => $childPrice, 'forSale' => $childForSale, 'categories' => $item['categories'] ?? [], 'category' => $item['category'] ?? '', ]), 'isMarkdown' => \EvolutionCMS\Ddaudio\Services\Product\StorefrontMarkdown::forProduct($child), 'availability' => $childAvailability, 'stockCount' => $childStockCount, 'sku' => trim((string)($child['sku'] ?? '')), 'sku2' => trim((string)($child['sku2'] ?? '')), 'canCart' => $childForSale && $childOrderable && $childCtaType ==='cart', 'canRequest' => $childForSale && $childOrderable && $childCtaType ==='request', 'preorder' => $childPreorder, ]; } } $hasCardVariants = count($cardVariantOptions) > 1; // it); the card it returns has that variant chosen. Until then, as for retail, nothing is chosen. $staffVariantChosen = $hasStaffVariants && $hasCardVariants && !empty($item['variantChosen']); $cardVariantPending = $hasCardVariants && !$staffVariantChosen; $minimumCardVariantPrice = collect($cardVariantOptions) ->filter(static fn(array $option): bool => $option['forSale'] && is_numeric($option['price'])) ->min(static fn(array $option): float => (float)$option['price']); $variantHasCartAction = $cardVariantPending && collect($cardVariantOptions)->contains(static fn(array $option): bool => $option['canCart']); $variantHasRequestAction = $cardVariantPending && collect($cardVariantOptions)->contains(static fn(array $option): bool => $option['canRequest']); $loyaltyBonusPrice = $cardVariantPending && $minimumCardVariantPrice !== null ? $minimumCardVariantPrice : $price; $loyaltyBonusEligible = $cardVariantPending ? $minimumCardVariantPrice !== null : $forSale; $freeShipping = StorefrontFreeShipping::forProduct([ 'price' => $loyaltyBonusPrice, 'forSale' => $loyaltyBonusEligible, 'categories' => $item['categories'] ?? [], 'category' => $item['category'] ?? '', ]); $loyaltyBonusAmount = $showCardLoyaltyBonus && $loyaltyBonusEligible && is_numeric($loyaltyBonusPrice) ? CustomerLoyaltyProgram::purchaseBonusAmount((float)$loyaltyBonusPrice) : 0; $loyaltyBonusTemplate = __('dd::product.loyalty_bonus_amount', [ 'amount' => '__AMOUNT__', 'currency' => $currencyLabel, ]); $wholesaleStock = (array)($ite @php use EvolutionCMS\Ddaudio\Facades\Domain; use EvolutionCMS\Ddaudio\Services\CustomerLoyaltyProgram; use EvolutionCMS\Ddaudio\Services\Product\StorefrontPricePresentation; use EvolutionCMS\Ddaudio\Services\Product\StorefrontFreeShipping; use EvolutionCMS\Ddaudio\Services\Product\StorefrontCardTerms; use EvolutionCMS\Ddaudio\Services\Product\StorefrontDeliveryTerms; $item = StorefrontDeliveryTerms::forProduct($item); $url = (string)($item['url'] ?? '#'); $title = trim((string)($item['pagetitle'] ?? '')); $normalizeImage = static function (mixed $image): string { $image = is_string($image) ? trim($image) : ''; return $image ==='' || preg_match('~^(?:https?:)?//|^data:|^/~i', $image) ? $image : '/' . ltrim($image, '/'); }; $sliderLarge = array_values((array)($item['sliderLarge'] ?? [])); $largeImages = []; $images = collect(array_values((array)($item['slider'] ?? []))) ->map(static fn(mixed $image, int $index): array => [$normalizeImage($image), $normalizeImage($sliderLarge[$index] ?? '')]) ->filter(static fn(array $pair): bool => $pair[0] !=='') ->unique(static fn(array $pair): string => $pair[0]) ->take(4) ->values() ->map(static function (array $pair) use (&$largeImages): string { if ($pair[1] !=='') { $largeImages[$pair[0]] = $pair[1]; } return $pair[0]; }) ->all(); $imageSrcset = static fn(string $image): string => isset($largeImages[$image]) ? $image . ' 320w, ' . $largeImages[$image] . ' 920w' : ''; $imageSizes ='(max-width: 575px) 100vw, (max-width: 1199px) 50vw, 360px'; if ($images === [] && is_string($item['image'] ?? null) && trim($item['image']) !=='') { $fallbackImage = trim($item['image']); $images[] = preg_match('~^(?:https?:)?//|^data:|^/~i', $fallbackImage) ? $fallbackImage : '/' . ltrim($fallbackImage, '/'); } $image = (string)($images[0] ?? ''); /* Фото немає — сірий логотип замість картинки товару. Клас is-placeholder не дає заглушці розтягнутись на всю плитку (див. card-product.scss). */ $imageIsPlaceholder = $image ===''; if ($imageIsPlaceholder) { $image = rtrim($storefrontThemeUrl, '/') . '/img/icons/logo-gray.svg'; } $hasImageSlider = count($images) > 1; $labels = (array)($item['labels'] ?? []); $rating = (array)($item['rating'] ?? []); $ratingValue = (float)($rating['rating'] ?? 0); $ratingTotal = 5.0; // total is the sum of votes, not the scale maximum. $ratingVotes = max(0, (int)($rating['votes'] ?? 0)); $ratingFilled = max(0, min(5, (int)round($ratingValue))); $currency = strtoupper((string)($_SESSION['currency'] ?? 'UAH')); $currencyLabel = \EvolutionCMS\Ddaudio\Support\StorefrontMoney::label($currency); $forSale = (bool)($item['forSale'] ?? false); $price = $item['price'] ?? null; $oldPrice = $item['price_old'] ?? null; $isAuthenticatedCustomer = (bool)evo()->getLoginUserID('web') && empty($_SESSION['webSellerId']); $pricePresentation = StorefrontPricePresentation::forProduct($item, $isAuthenticatedCustomer); $oldPrice = $pricePresentation['old_price']; $discountPercent = $pricePresentation['discount_percent']; $clientDiscountPercent = $pricePresentation['client_percent']; $isClientOnlyDiscount = $clientDiscountPercent > 0; $promoUntil = $pricePresentation['promo_until']; $formatCardAmount = static fn(float|int $amount): string => \EvolutionCMS\Ddaudio\Support\StorefrontMoney::format($amount); $showCardLoyaltyBonus = empty($_SESSION['webSellerId']) && Domain::isCountry('UA'); $stockCount = (int)($item['inStockCount'] ?? 0); $availabilityBadge = (array)($item['availabilityBadge'] ?? []); $availability = trim((string)($availabilityBadge['label'] ?? '')); $availabilityCta = (array)($availabilityBadge['cta'] ?? []); $availabilityCtaType = (string)($availabilityCta['type'] ?? ($stockCount > 0 ? 'cart' : 'request')); $isOrderable = (bool)($availabilityBadge['is_orderable'] ?? ($stockCount > 0)); $cardTerms = StorefrontCardTerms::forProduct($item); $productOrderId = (int)($item['productId'] ?? 0); $favoriteProductId = (int)($item['id'] ?? $productOrderId); if ((int)($item['template'] ?? 0) === 32) { $favoriteProductId = (int)($item['parent'] ?? $favoriteProductId); } $favoriteIds = array_map('intval', (array)($_SESSION['favorite'] ?? [])); $isFavorite = $favoriteProductId > 0 && in_array($favoriteProductId, $favoriteIds, true); $favoriteAddLabel = __('dd::global.favorite_add'); $favoriteRemoveLabel = __('dd::global.favorite_remove'); $canAddToCart = $forSale && $isOrderable && $availabilityCtaType ==='cart' && $productOrderId > 0; $canRequestProduct = $forSale && $isOrderable && $availabilityCtaType ==='request' && $productOrderId > 0; $managerEntries = []; $managerId = (int)evo()->getLoginUserID('mgr'); $isManager = $managerId > 0; $isWholesaler = !empty($_SESSION['webSellerId']); $cardMetaSku = trim((string)($item['sku'] ?? '')); $hasStaffVariants = ($isManager || $isWholesaler) && count((array)($item['children'] ?? [])) > 1; $cardMetaId = $hasStaffVariants ? $productOrderId : (int)($item['id'] ?? 0); $cardShowMetaId = $cardMetaId > 0 && \EvolutionCMS\Ddaudio\Services\Storefront\StorefrontProductIdVisibility::visible(); $hasKitComposition = !empty($item['stock']['data']['sku']['related']) && $productOrderId > 0; $canViewKitComposition = \EvolutionCMS\Ddaudio\Helpers\DevHelper::isManager() && $hasKitComposition; $hasCardNuances = ($isManager || $isWholesaler) && !empty($item['nuances']['full']); $cardMarginPercent = ($isManager || $isWholesaler) ? trim((string)($item['marginality']['percent'] ?? '')) : ''; $canPrintLabels = \EvolutionCMS\Ddaudio\Helpers\DevHelper::isManager() && $productOrderId > 0; $showCardManagerTools = ($isManager && $cardMetaId > 0) || $canViewKitComposition || $canPrintLabels || $hasCardNuances || $cardMarginPercent !==''; // "від" the lowest retail price; managers and wholesalers also see each variant's SKU and stock. $cardVariantLabel = trim((string)($item['variant_option_name'] ?? '')) ?: __('dd::storefront.variants'); $cardVariantOptions = []; if (count((array)($item['children'] ?? [])) > 1) { foreach ((array)($item['children'] ?? []) as $child) { if (!is_array($child) || (int)($child['id'] ?? 0) <= 0) { continue; } $child = StorefrontDeliveryTerms::forProduct($child); $childId = (int)$child['id']; $childBadge = (array)($child['availabilityBadge'] ?? []); $childStockCount = (int)($child['inStockCount'] ?? 0); $childCta = (array)($childBadge['cta'] ?? []); $childCtaType = (string)($childCta['type'] ?? ($childStockCount > 0 ? 'cart' : 'request')); $childOrderable = (bool)($childBadge['is_orderable'] ?? ($childStockCount > 0)); $childUnavailableForDomain = (Domain::isEuMarket() && (int)($child['not_sale_europe'] ?? 0) === 1) || (int)($child['not_sale_' . Domain::siteKey()] ?? 0) === 1 || (int)($child['not_sale'] ?? 0) === 1; $childForSale = (int)($child['hidemenu'] ?? 0) === 0 && !$childUnavailableForDomain; $childAvailability = trim((string)($childBadge['label'] ?? '')); if ($childAvailability ==='') { $childAvailability = trim(strip_tags((string)($child['inStock'] ?? ''))); } $childPrice = $child['price'] ?? ''; if (!$childForSale) { $childPrice = (int)($child['hidemenu'] ?? 0) === 1 ? __('dd::global.out_of_production') : __('dd::global.not_sale'); } $childPreorder = (array)($child['preorder'] ?? []); $childPreorder['hint'] = \EvolutionCMS\Ddaudio\Services\Preorder\PreorderHintText::for($childPreorder); $childPresentation = StorefrontPricePresentation::forProduct($child, $isAuthenticatedCustomer); $childOldPrice = $childForSale ? ($childPresentation['old_price'] ?? '') : ''; $childBonusAmount = $showCardLoyaltyBonus && $childForSale && is_numeric($childPrice) ? CustomerLoyaltyProgram::purchaseBonusAmount((float)$childPrice) : 0; $cardVariantOptions[] = [ 'id' => $childId, 'title' => trim((string)($child['pagetitle'] ?? '')) ?: '#' . $childId, 'url' => trim((string)($child['alias'] ?? '')) !=='' ? (string)$child['alias'] . '.html' : $url, 'price' => (string)$childPrice, 'oldPrice' => (string)$childOldPrice, 'discountPercent' => $childForSale ? $childPresentation['discount_percent'] : 0, 'clientPercent' => $childForSale ? $childPresentation['client_percent'] : 0, 'clientBasePrice' => $childForSale ? $childPresentation['client_base_price'] : null, 'delivery' => StorefrontCardTerms::forProduct($child), 'dispatch' => \EvolutionCMS\Ddaudio\Services\Product\StorefrontCardDispatch::forProduct(array_replace($child, ['forSale' => $childForSale])), 'promoUntil' => $childForSale ? $childPresentation['promo_until'] : '', 'priceRefreshRequired' => $childForSale && $childPresentation['refresh_required'], 'bonusAmount' => $childBonusAmount, 'forSale' => $childForSale, // ProductListItemPresenter turns the category TV into 'categories' and drops 'category'. 'freeShipping' => StorefrontFreeShipping::forProduct([ 'price' => $childPrice, 'forSale' => $childForSale, 'categories' => $item['categories'] ?? [], 'category' => $item['category'] ?? '', ]), 'isMarkdown' => \EvolutionCMS\Ddaudio\Services\Product\StorefrontMarkdown::forProduct($child), 'availability' => $childAvailability, 'stockCount' => $childStockCount, 'sku' => trim((string)($child['sku'] ?? '')), 'sku2' => trim((string)($child['sku2'] ?? '')), 'canCart' => $childForSale && $childOrderable && $childCtaType ==='cart', 'canRequest' => $childForSale && $childOrderable && $childCtaType ==='request', 'preorder' => $childPreorder, ]; } } $hasCardVariants = count($cardVariantOptions) > 1; // it); the card it returns has that variant chosen. Until then, as for retail, nothing is chosen. $staffVariantChosen = $hasStaffVariants && $hasCardVariants && !empty($item['variantChosen']); $cardVariantPending = $hasCardVariants && !$staffVariantChosen; $minimumCardVariantPrice = collect($cardVariantOptions) ->filter(static fn(array $option): bool => $option['forSale'] && is_numeric($option['price'])) ->min(static fn(array $option): float => (float)$option['price']); $variantHasCartAction = $cardVariantPending && collect($cardVariantOptions)->contains(static fn(array $option): bool => $option['canCart']); $variantHasRequestAction = $cardVariantPending && collect($cardVariantOptions)->contains(static fn(array $option): bool => $option['canRequest']); $loyaltyBonusPrice = $cardVariantPending && $minimumCardVariantPrice !== null ? $minimumCardVariantPrice : $price; $loyaltyBonusEligible = $cardVariantPending ? $minimumCardVariantPrice !== null : $forSale; $freeShipping = StorefrontFreeShipping::forProduct([ 'price' => $loyaltyBonusPrice, 'forSale' => $loyaltyBonusEligible, 'categories' => $item['categories'] ?? [], 'category' => $item['category'] ?? '', ]); $loyaltyBonusAmount = $showCardLoyaltyBonus && $loyaltyBonusEligible && is_numeric($loyaltyBonusPrice) ? CustomerLoyaltyProgram::purchaseBonusAmount((float)$loyaltyBonusPrice) : 0; $loyaltyBonusTemplate = __('dd::product.loyalty_bonus_amount', [ 'amount' => '__AMOUNT__', 'currency' => $currencyLabel, ]); $wholesaleStock = (array)($ite @php use EvolutionCMS\Ddaudio\Facades\Domain; use EvolutionCMS\Ddaudio\Services\CustomerLoyaltyProgram; use EvolutionCMS\Ddaudio\Services\Product\StorefrontPricePresentation; use EvolutionCMS\Ddaudio\Services\Product\StorefrontFreeShipping; use EvolutionCMS\Ddaudio\Services\Product\StorefrontCardTerms; use EvolutionCMS\Ddaudio\Services\Product\StorefrontDeliveryTerms; $item = StorefrontDeliveryTerms::forProduct($item); $url = (string)($item['url'] ?? '#'); $title = trim((string)($item['pagetitle'] ?? '')); $normalizeImage = static function (mixed $image): string { $image = is_string($image) ? trim($image) : ''; return $image ==='' || preg_match('~^(?:https?:)?//|^data:|^/~i', $image) ? $image : '/' . ltrim($image, '/'); }; $sliderLarge = array_values((array)($item['sliderLarge'] ?? [])); $largeImages = []; $images = collect(array_values((array)($item['slider'] ?? []))) ->map(static fn(mixed $image, int $index): array => [$normalizeImage($image), $normalizeImage($sliderLarge[$index] ?? '')]) ->filter(static fn(array $pair): bool => $pair[0] !=='') ->unique(static fn(array $pair): string => $pair[0]) ->take(4) ->values() ->map(static function (array $pair) use (&$largeImages): string { if ($pair[1] !=='') { $largeImages[$pair[0]] = $pair[1]; } return $pair[0]; }) ->all(); $imageSrcset = static fn(string $image): string => isset($largeImages[$image]) ? $image . ' 320w, ' . $largeImages[$image] . ' 920w' : ''; $imageSizes ='(max-width: 575px) 100vw, (max-width: 1199px) 50vw, 360px'; if ($images === [] && is_string($item['image'] ?? null) && trim($item['image']) !=='') { $fallbackImage = trim($item['image']); $images[] = preg_match('~^(?:https?:)?//|^data:|^/~i', $fallbackImage) ? $fallbackImage : '/' . ltrim($fallbackImage, '/'); } $image = (string)($images[0] ?? ''); /* Фото немає — сірий логотип замість картинки товару. Клас is-placeholder не дає заглушці розтягнутись на всю плитку (див. card-product.scss). */ $imageIsPlaceholder = $image ===''; if ($imageIsPlaceholder) { $image = rtrim($storefrontThemeUrl, '/') . '/img/icons/logo-gray.svg'; } $hasImageSlider = count($images) > 1; $labels = (array)($item['labels'] ?? []); $rating = (array)($item['rating'] ?? []); $ratingValue = (float)($rating['rating'] ?? 0); $ratingTotal = 5.0; // total is the sum of votes, not the scale maximum. $ratingVotes = max(0, (int)($rating['votes'] ?? 0)); $ratingFilled = max(0, min(5, (int)round($ratingValue))); $currency = strtoupper((string)($_SESSION['currency'] ?? 'UAH')); $currencyLabel = \EvolutionCMS\Ddaudio\Support\StorefrontMoney::label($currency); $forSale = (bool)($item['forSale'] ?? false); $price = $item['price'] ?? null; $oldPrice = $item['price_old'] ?? null; $isAuthenticatedCustomer = (bool)evo()->getLoginUserID('web') && empty($_SESSION['webSellerId']); $pricePresentation = StorefrontPricePresentation::forProduct($item, $isAuthenticatedCustomer); $oldPrice = $pricePresentation['old_price']; $discountPercent = $pricePresentation['discount_percent']; $clientDiscountPercent = $pricePresentation['client_percent']; $isClientOnlyDiscount = $clientDiscountPercent > 0; $promoUntil = $pricePresentation['promo_until']; $formatCardAmount = static fn(float|int $amount): string => \EvolutionCMS\Ddaudio\Support\StorefrontMoney::format($amount); $showCardLoyaltyBonus = empty($_SESSION['webSellerId']) && Domain::isCountry('UA'); $stockCount = (int)($item['inStockCount'] ?? 0); $availabilityBadge = (array)($item['availabilityBadge'] ?? []); $availability = trim((string)($availabilityBadge['label'] ?? '')); $availabilityCta = (array)($availabilityBadge['cta'] ?? []); $availabilityCtaType = (string)($availabilityCta['type'] ?? ($stockCount > 0 ? 'cart' : 'request')); $isOrderable = (bool)($availabilityBadge['is_orderable'] ?? ($stockCount > 0)); $cardTerms = StorefrontCardTerms::forProduct($item); $productOrderId = (int)($item['productId'] ?? 0); $favoriteProductId = (int)($item['id'] ?? $productOrderId); if ((int)($item['template'] ?? 0) === 32) { $favoriteProductId = (int)($item['parent'] ?? $favoriteProductId); } $favoriteIds = array_map('intval', (array)($_SESSION['favorite'] ?? [])); $isFavorite = $favoriteProductId > 0 && in_array($favoriteProductId, $favoriteIds, true); $favoriteAddLabel = __('dd::global.favorite_add'); $favoriteRemoveLabel = __('dd::global.favorite_remove'); $canAddToCart = $forSale && $isOrderable && $availabilityCtaType ==='cart' && $productOrderId > 0; $canRequestProduct = $forSale && $isOrderable && $availabilityCtaType ==='request' && $productOrderId > 0; $managerEntries = []; $managerId = (int)evo()->getLoginUserID('mgr'); $isManager = $managerId > 0; $isWholesaler = !empty($_SESSION['webSellerId']); $cardMetaSku = trim((string)($item['sku'] ?? '')); $hasStaffVariants = ($isManager || $isWholesaler) && count((array)($item['children'] ?? [])) > 1; $cardMetaId = $hasStaffVariants ? $productOrderId : (int)($item['id'] ?? 0); $cardShowMetaId = $cardMetaId > 0 && \EvolutionCMS\Ddaudio\Services\Storefront\StorefrontProductIdVisibility::visible(); $hasKitComposition = !empty($item['stock']['data']['sku']['related']) && $productOrderId > 0; $canViewKitComposition = \EvolutionCMS\Ddaudio\Helpers\DevHelper::isManager() && $hasKitComposition; $hasCardNuances = ($isManager || $isWholesaler) && !empty($item['nuances']['full']); $cardMarginPercent = ($isManager || $isWholesaler) ? trim((string)($item['marginality']['percent'] ?? '')) : ''; $canPrintLabels = \EvolutionCMS\Ddaudio\Helpers\DevHelper::isManager() && $productOrderId > 0; $showCardManagerTools = ($isManager && $cardMetaId > 0) || $canViewKitComposition || $canPrintLabels || $hasCardNuances || $cardMarginPercent !==''; // "від" the lowest retail price; managers and wholesalers also see each variant's SKU and stock. $cardVariantLabel = trim((string)($item['variant_option_name'] ?? '')) ?: __('dd::storefront.variants'); $cardVariantOptions = []; if (count((array)($item['children'] ?? [])) > 1) { foreach ((array)($item['children'] ?? []) as $child) { if (!is_array($child) || (int)($child['id'] ?? 0) <= 0) { continue; } $child = StorefrontDeliveryTerms::forProduct($child); $childId = (int)$child['id']; $childBadge = (array)($child['availabilityBadge'] ?? []); $childStockCount = (int)($child['inStockCount'] ?? 0); $childCta = (array)($childBadge['cta'] ?? []); $childCtaType = (string)($childCta['type'] ?? ($childStockCount > 0 ? 'cart' : 'request')); $childOrderable = (bool)($childBadge['is_orderable'] ?? ($childStockCount > 0)); $childUnavailableForDomain = (Domain::isEuMarket() && (int)($child['not_sale_europe'] ?? 0) === 1) || (int)($child['not_sale_' . Domain::siteKey()] ?? 0) === 1 || (int)($child['not_sale'] ?? 0) === 1; $childForSale = (int)($child['hidemenu'] ?? 0) === 0 && !$childUnavailableForDomain; $childAvailability = trim((string)($childBadge['label'] ?? '')); if ($childAvailability ==='') { $childAvailability = trim(strip_tags((string)($child['inStock'] ?? ''))); } $childPrice = $child['price'] ?? ''; if (!$childForSale) { $childPrice = (int)($child['hidemenu'] ?? 0) === 1 ? __('dd::global.out_of_production') : __('dd::global.not_sale'); } $childPreorder = (array)($child['preorder'] ?? []); $childPreorder['hint'] = \EvolutionCMS\Ddaudio\Services\Preorder\PreorderHintText::for($childPreorder); $childPresentation = StorefrontPricePresentation::forProduct($child, $isAuthenticatedCustomer); $childOldPrice = $childForSale ? ($childPresentation['old_price'] ?? '') : ''; $childBonusAmount = $showCardLoyaltyBonus && $childForSale && is_numeric($childPrice) ? CustomerLoyaltyProgram::purchaseBonusAmount((float)$childPrice) : 0; $cardVariantOptions[] = [ 'id' => $childId, 'title' => trim((string)($child['pagetitle'] ?? '')) ?: '#' . $childId, 'url' => trim((string)($child['alias'] ?? '')) !=='' ? (string)$child['alias'] . '.html' : $url, 'price' => (string)$childPrice, 'oldPrice' => (string)$childOldPrice, 'discountPercent' => $childForSale ? $childPresentation['discount_percent'] : 0, 'clientPercent' => $childForSale ? $childPresentation['client_percent'] : 0, 'clientBasePrice' => $childForSale ? $childPresentation['client_base_price'] : null, 'delivery' => StorefrontCardTerms::forProduct($child), 'dispatch' => \EvolutionCMS\Ddaudio\Services\Product\StorefrontCardDispatch::forProduct(array_replace($child, ['forSale' => $childForSale])), 'promoUntil' => $childForSale ? $childPresentation['promo_until'] : '', 'priceRefreshRequired' => $childForSale && $childPresentation['refresh_required'], 'bonusAmount' => $childBonusAmount, 'forSale' => $childForSale, // ProductListItemPresenter turns the category TV into 'categories' and drops 'category'. 'freeShipping' => StorefrontFreeShipping::forProduct([ 'price' => $childPrice, 'forSale' => $childForSale, 'categories' => $item['categories'] ?? [], 'category' => $item['category'] ?? '', ]), 'isMarkdown' => \EvolutionCMS\Ddaudio\Services\Product\StorefrontMarkdown::forProduct($child), 'availability' => $childAvailability, 'stockCount' => $childStockCount, 'sku' => trim((string)($child['sku'] ?? '')), 'sku2' => trim((string)($child['sku2'] ?? '')), 'canCart' => $childForSale && $childOrderable && $childCtaType ==='cart', 'canRequest' => $childForSale && $childOrderable && $childCtaType ==='request', 'preorder' => $childPreorder, ]; } } $hasCardVariants = count($cardVariantOptions) > 1; // it); the card it returns has that variant chosen. Until then, as for retail, nothing is chosen. $staffVariantChosen = $hasStaffVariants && $hasCardVariants && !empty($item['variantChosen']); $cardVariantPending = $hasCardVariants && !$staffVariantChosen; $minimumCardVariantPrice = collect($cardVariantOptions) ->filter(static fn(array $option): bool => $option['forSale'] && is_numeric($option['price'])) ->min(static fn(array $option): float => (float)$option['price']); $variantHasCartAction = $cardVariantPending && collect($cardVariantOptions)->contains(static fn(array $option): bool => $option['canCart']); $variantHasRequestAction = $cardVariantPending && collect($cardVariantOptions)->contains(static fn(array $option): bool => $option['canRequest']); $loyaltyBonusPrice = $cardVariantPending && $minimumCardVariantPrice !== null ? $minimumCardVariantPrice : $price; $loyaltyBonusEligible = $cardVariantPending ? $minimumCardVariantPrice !== null : $forSale; $freeShipping = StorefrontFreeShipping::forProduct([ 'price' => $loyaltyBonusPrice, 'forSale' => $loyaltyBonusEligible, 'categories' => $item['categories'] ?? [], 'category' => $item['category'] ?? '', ]); $loyaltyBonusAmount = $showCardLoyaltyBonus && $loyaltyBonusEligible && is_numeric($loyaltyBonusPrice) ? CustomerLoyaltyProgram::purchaseBonusAmount((float)$loyaltyBonusPrice) : 0; $loyaltyBonusTemplate = __('dd::product.loyalty_bonus_amount', [ 'amount' => '__AMOUNT__', 'currency' => $currencyLabel, ]); $wholesaleStock = (array)($ite @php use EvolutionCMS\Ddaudio\Facades\Domain; use EvolutionCMS\Ddaudio\Services\CustomerLoyaltyProgram; use EvolutionCMS\Ddaudio\Services\Product\StorefrontPricePresentation; use EvolutionCMS\Ddaudio\Services\Product\StorefrontFreeShipping; use EvolutionCMS\Ddaudio\Services\Product\StorefrontCardTerms; use EvolutionCMS\Ddaudio\Services\Product\StorefrontDeliveryTerms; $item = StorefrontDeliveryTerms::forProduct($item); $url = (string)($item['url'] ?? '#'); $title = trim((string)($item['pagetitle'] ?? '')); $normalizeImage = static function (mixed $image): string { $image = is_string($image) ? trim($image) : ''; return $image ==='' || preg_match('~^(?:https?:)?//|^data:|^/~i', $image) ? $image : '/' . ltrim($image, '/'); }; $sliderLarge = array_values((array)($item['sliderLarge'] ?? [])); $largeImages = []; $images = collect(array_values((array)($item['slider'] ?? []))) ->map(static fn(mixed $image, int $index): array => [$normalizeImage($image), $normalizeImage($sliderLarge[$index] ?? '')]) ->filter(static fn(array $pair): bool => $pair[0] !=='') ->unique(static fn(array $pair): string => $pair[0]) ->take(4) ->values() ->map(static function (array $pair) use (&$largeImages): string { if ($pair[1] !=='') { $largeImages[$pair[0]] = $pair[1]; } return $pair[0]; }) ->all(); $imageSrcset = static fn(string $image): string => isset($largeImages[$image]) ? $image . ' 320w, ' . $largeImages[$image] . ' 920w' : ''; $imageSizes ='(max-width: 575px) 100vw, (max-width: 1199px) 50vw, 360px'; if ($images === [] && is_string($item['image'] ?? null) && trim($item['image']) !=='') { $fallbackImage = trim($item['image']); $images[] = preg_match('~^(?:https?:)?//|^data:|^/~i', $fallbackImage) ? $fallbackImage : '/' . ltrim($fallbackImage, '/'); } $image = (string)($images[0] ?? ''); /* Фото немає — сірий логотип замість картинки товару. Клас is-placeholder не дає заглушці розтягнутись на всю плитку (див. card-product.scss). */ $imageIsPlaceholder = $image ===''; if ($imageIsPlaceholder) { $image = rtrim($storefrontThemeUrl, '/') . '/img/icons/logo-gray.svg'; } $hasImageSlider = count($images) > 1; $labels = (array)($item['labels'] ?? []); $rating = (array)($item['rating'] ?? []); $ratingValue = (float)($rating['rating'] ?? 0); $ratingTotal = 5.0; // total is the sum of votes, not the scale maximum. $ratingVotes = max(0, (int)($rating['votes'] ?? 0)); $ratingFilled = max(0, min(5, (int)round($ratingValue))); $currency = strtoupper((string)($_SESSION['currency'] ?? 'UAH')); $currencyLabel = \EvolutionCMS\Ddaudio\Support\StorefrontMoney::label($currency); $forSale = (bool)($item['forSale'] ?? false); $price = $item['price'] ?? null; $oldPrice = $item['price_old'] ?? null; $isAuthenticatedCustomer = (bool)evo()->getLoginUserID('web') && empty($_SESSION['webSellerId']); $pricePresentation = StorefrontPricePresentation::forProduct($item, $isAuthenticatedCustomer); $oldPrice = $pricePresentation['old_price']; $discountPercent = $pricePresentation['discount_percent']; $clientDiscountPercent = $pricePresentation['client_percent']; $isClientOnlyDiscount = $clientDiscountPercent > 0; $promoUntil = $pricePresentation['promo_until']; $formatCardAmount = static fn(float|int $amount): string => \EvolutionCMS\Ddaudio\Support\StorefrontMoney::format($amount); $showCardLoyaltyBonus = empty($_SESSION['webSellerId']) && Domain::isCountry('UA'); $stockCount = (int)($item['inStockCount'] ?? 0); $availabilityBadge = (array)($item['availabilityBadge'] ?? []); $availability = trim((string)($availabilityBadge['label'] ?? '')); $availabilityCta = (array)($availabilityBadge['cta'] ?? []); $availabilityCtaType = (string)($availabilityCta['type'] ?? ($stockCount > 0 ? 'cart' : 'request')); $isOrderable = (bool)($availabilityBadge['is_orderable'] ?? ($stockCount > 0)); $cardTerms = StorefrontCardTerms::forProduct($item); $productOrderId = (int)($item['productId'] ?? 0); $favoriteProductId = (int)($item['id'] ?? $productOrderId); if ((int)($item['template'] ?? 0) === 32) { $favoriteProductId = (int)($item['parent'] ?? $favoriteProductId); } $favoriteIds = array_map('intval', (array)($_SESSION['favorite'] ?? [])); $isFavorite = $favoriteProductId > 0 && in_array($favoriteProductId, $favoriteIds, true); $favoriteAddLabel = __('dd::global.favorite_add'); $favoriteRemoveLabel = __('dd::global.favorite_remove'); $canAddToCart = $forSale && $isOrderable && $availabilityCtaType ==='cart' && $productOrderId > 0; $canRequestProduct = $forSale && $isOrderable && $availabilityCtaType ==='request' && $productOrderId > 0; $managerEntries = []; $managerId = (int)evo()->getLoginUserID('mgr'); $isManager = $managerId > 0; $isWholesaler = !empty($_SESSION['webSellerId']); $cardMetaSku = trim((string)($item['sku'] ?? '')); $hasStaffVariants = ($isManager || $isWholesaler) && count((array)($item['children'] ?? [])) > 1; $cardMetaId = $hasStaffVariants ? $productOrderId : (int)($item['id'] ?? 0); $cardShowMetaId = $cardMetaId > 0 && \EvolutionCMS\Ddaudio\Services\Storefront\StorefrontProductIdVisibility::visible(); $hasKitComposition = !empty($item['stock']['data']['sku']['related']) && $productOrderId > 0; $canViewKitComposition = \EvolutionCMS\Ddaudio\Helpers\DevHelper::isManager() && $hasKitComposition; $hasCardNuances = ($isManager || $isWholesaler) && !empty($item['nuances']['full']); $cardMarginPercent = ($isManager || $isWholesaler) ? trim((string)($item['marginality']['percent'] ?? '')) : ''; $canPrintLabels = \EvolutionCMS\Ddaudio\Helpers\DevHelper::isManager() && $productOrderId > 0; $showCardManagerTools = ($isManager && $cardMetaId > 0) || $canViewKitComposition || $canPrintLabels || $hasCardNuances || $cardMarginPercent !==''; // "від" the lowest retail price; managers and wholesalers also see each variant's SKU and stock. $cardVariantLabel = trim((string)($item['variant_option_name'] ?? '')) ?: __('dd::storefront.variants'); $cardVariantOptions = []; if (count((array)($item['children'] ?? [])) > 1) { foreach ((array)($item['children'] ?? []) as $child) { if (!is_array($child) || (int)($child['id'] ?? 0) <= 0) { continue; } $child = StorefrontDeliveryTerms::forProduct($child); $childId = (int)$child['id']; $childBadge = (array)($child['availabilityBadge'] ?? []); $childStockCount = (int)($child['inStockCount'] ?? 0); $childCta = (array)($childBadge['cta'] ?? []); $childCtaType = (string)($childCta['type'] ?? ($childStockCount > 0 ? 'cart' : 'request')); $childOrderable = (bool)($childBadge['is_orderable'] ?? ($childStockCount > 0)); $childUnavailableForDomain = (Domain::isEuMarket() && (int)($child['not_sale_europe'] ?? 0) === 1) || (int)($child['not_sale_' . Domain::siteKey()] ?? 0) === 1 || (int)($child['not_sale'] ?? 0) === 1; $childForSale = (int)($child['hidemenu'] ?? 0) === 0 && !$childUnavailableForDomain; $childAvailability = trim((string)($childBadge['label'] ?? '')); if ($childAvailability ==='') { $childAvailability = trim(strip_tags((string)($child['inStock'] ?? ''))); } $childPrice = $child['price'] ?? ''; if (!$childForSale) { $childPrice = (int)($child['hidemenu'] ?? 0) === 1 ? __('dd::global.out_of_production') : __('dd::global.not_sale'); } $childPreorder = (array)($child['preorder'] ?? []); $childPreorder['hint'] = \EvolutionCMS\Ddaudio\Services\Preorder\PreorderHintText::for($childPreorder); $childPresentation = StorefrontPricePresentation::forProduct($child, $isAuthenticatedCustomer); $childOldPrice = $childForSale ? ($childPresentation['old_price'] ?? '') : ''; $childBonusAmount = $showCardLoyaltyBonus && $childForSale && is_numeric($childPrice) ? CustomerLoyaltyProgram::purchaseBonusAmount((float)$childPrice) : 0; $cardVariantOptions[] = [ 'id' => $childId, 'title' => trim((string)($child['pagetitle'] ?? '')) ?: '#' . $childId, 'url' => trim((string)($child['alias'] ?? '')) !=='' ? (string)$child['alias'] . '.html' : $url, 'price' => (string)$childPrice, 'oldPrice' => (string)$childOldPrice, 'discountPercent' => $childForSale ? $childPresentation['discount_percent'] : 0, 'clientPercent' => $childForSale ? $childPresentation['client_percent'] : 0, 'clientBasePrice' => $childForSale ? $childPresentation['client_base_price'] : null, 'delivery' => StorefrontCardTerms::forProduct($child), 'dispatch' => \EvolutionCMS\Ddaudio\Services\Product\StorefrontCardDispatch::forProduct(array_replace($child, ['forSale' => $childForSale])), 'promoUntil' => $childForSale ? $childPresentation['promo_until'] : '', 'priceRefreshRequired' => $childForSale && $childPresentation['refresh_required'], 'bonusAmount' => $childBonusAmount, 'forSale' => $childForSale, // ProductListItemPresenter turns the category TV into 'categories' and drops 'category'. 'freeShipping' => StorefrontFreeShipping::forProduct([ 'price' => $childPrice, 'forSale' => $childForSale, 'categories' => $item['categories'] ?? [], 'category' => $item['category'] ?? '', ]), 'isMarkdown' => \EvolutionCMS\Ddaudio\Services\Product\StorefrontMarkdown::forProduct($child), 'availability' => $childAvailability, 'stockCount' => $childStockCount, 'sku' => trim((string)($child['sku'] ?? '')), 'sku2' => trim((string)($child['sku2'] ?? '')), 'canCart' => $childForSale && $childOrderable && $childCtaType ==='cart', 'canRequest' => $childForSale && $childOrderable && $childCtaType ==='request', 'preorder' => $childPreorder, ]; } } $hasCardVariants = count($cardVariantOptions) > 1; // it); the card it returns has that variant chosen. Until then, as for retail, nothing is chosen. $staffVariantChosen = $hasStaffVariants && $hasCardVariants && !empty($item['variantChosen']); $cardVariantPending = $hasCardVariants && !$staffVariantChosen; $minimumCardVariantPrice = collect($cardVariantOptions) ->filter(static fn(array $option): bool => $option['forSale'] && is_numeric($option['price'])) ->min(static fn(array $option): float => (float)$option['price']); $variantHasCartAction = $cardVariantPending && collect($cardVariantOptions)->contains(static fn(array $option): bool => $option['canCart']); $variantHasRequestAction = $cardVariantPending && collect($cardVariantOptions)->contains(static fn(array $option): bool => $option['canRequest']); $loyaltyBonusPrice = $cardVariantPending && $minimumCardVariantPrice !== null ? $minimumCardVariantPrice : $price; $loyaltyBonusEligible = $cardVariantPending ? $minimumCardVariantPrice !== null : $forSale; $freeShipping = StorefrontFreeShipping::forProduct([ 'price' => $loyaltyBonusPrice, 'forSale' => $loyaltyBonusEligible, 'categories' => $item['categories'] ?? [], 'category' => $item['category'] ?? '', ]); $loyaltyBonusAmount = $showCardLoyaltyBonus && $loyaltyBonusEligible && is_numeric($loyaltyBonusPrice) ? CustomerLoyaltyProgram::purchaseBonusAmount((float)$loyaltyBonusPrice) : 0; $loyaltyBonusTemplate = __('dd::product.loyalty_bonus_amount', [ 'amount' => '__AMOUNT__', 'currency' => $currencyLabel, ]); $wholesaleStock = (array)($ite @php use EvolutionCMS\Ddaudio\Facades\Domain; use EvolutionCMS\Ddaudio\Services\CustomerLoyaltyProgram; use EvolutionCMS\Ddaudio\Services\Product\StorefrontPricePresentation; use EvolutionCMS\Ddaudio\Services\Product\StorefrontFreeShipping; use EvolutionCMS\Ddaudio\Services\Product\StorefrontCardTerms; use EvolutionCMS\Ddaudio\Services\Product\StorefrontDeliveryTerms; $item = StorefrontDeliveryTerms::forProduct($item); $url = (string)($item['url'] ?? '#'); $title = trim((string)($item['pagetitle'] ?? '')); $normalizeImage = static function (mixed $image): string { $image = is_string($image) ? trim($image) : ''; return $image ==='' || preg_match('~^(?:https?:)?//|^data:|^/~i', $image) ? $image : '/' . ltrim($image, '/'); }; $sliderLarge = array_values((array)($item['sliderLarge'] ?? [])); $largeImages = []; $images = collect(array_values((array)($item['slider'] ?? []))) ->map(static fn(mixed $image, int $index): array => [$normalizeImage($image), $normalizeImage($sliderLarge[$index] ?? '')]) ->filter(static fn(array $pair): bool => $pair[0] !=='') ->unique(static fn(array $pair): string => $pair[0]) ->take(4) ->values() ->map(static function (array $pair) use (&$largeImages): string { if ($pair[1] !=='') { $largeImages[$pair[0]] = $pair[1]; } return $pair[0]; }) ->all(); $imageSrcset = static fn(string $image): string => isset($largeImages[$image]) ? $image . ' 320w, ' . $largeImages[$image] . ' 920w' : ''; $imageSizes ='(max-width: 575px) 100vw, (max-width: 1199px) 50vw, 360px'; if ($images === [] && is_string($item['image'] ?? null) && trim($item['image']) !=='') { $fallbackImage = trim($item['image']); $images[] = preg_match('~^(?:https?:)?//|^data:|^/~i', $fallbackImage) ? $fallbackImage : '/' . ltrim($fallbackImage, '/'); } $image = (string)($images[0] ?? ''); /* Фото немає — сірий логотип замість картинки товару. Клас is-placeholder не дає заглушці розтягнутись на всю плитку (див. card-product.scss). */ $imageIsPlaceholder = $image ===''; if ($imageIsPlaceholder) { $image = rtrim($storefrontThemeUrl, '/') . '/img/icons/logo-gray.svg'; } $hasImageSlider = count($images) > 1; $labels = (array)($item['labels'] ?? []); $rating = (array)($item['rating'] ?? []); $ratingValue = (float)($rating['rating'] ?? 0); $ratingTotal = 5.0; // total is the sum of votes, not the scale maximum. $ratingVotes = max(0, (int)($rating['votes'] ?? 0)); $ratingFilled = max(0, min(5, (int)round($ratingValue))); $currency = strtoupper((string)($_SESSION['currency'] ?? 'UAH')); $currencyLabel = \EvolutionCMS\Ddaudio\Support\StorefrontMoney::label($currency); $forSale = (bool)($item['forSale'] ?? false); $price = $item['price'] ?? null; $oldPrice = $item['price_old'] ?? null; $isAuthenticatedCustomer = (bool)evo()->getLoginUserID('web') && empty($_SESSION['webSellerId']); $pricePresentation = StorefrontPricePresentation::forProduct($item, $isAuthenticatedCustomer); $oldPrice = $pricePresentation['old_price']; $discountPercent = $pricePresentation['discount_percent']; $clientDiscountPercent = $pricePresentation['client_percent']; $isClientOnlyDiscount = $clientDiscountPercent > 0; $promoUntil = $pricePresentation['promo_until']; $formatCardAmount = static fn(float|int $amount): string => \EvolutionCMS\Ddaudio\Support\StorefrontMoney::format($amount); $showCardLoyaltyBonus = empty($_SESSION['webSellerId']) && Domain::isCountry('UA'); $stockCount = (int)($item['inStockCount'] ?? 0); $availabilityBadge = (array)($item['availabilityBadge'] ?? []); $availability = trim((string)($availabilityBadge['label'] ?? '')); $availabilityCta = (array)($availabilityBadge['cta'] ?? []); $availabilityCtaType = (string)($availabilityCta['type'] ?? ($stockCount > 0 ? 'cart' : 'request')); $isOrderable = (bool)($availabilityBadge['is_orderable'] ?? ($stockCount > 0)); $cardTerms = StorefrontCardTerms::forProduct($item); $productOrderId = (int)($item['productId'] ?? 0); $favoriteProductId = (int)($item['id'] ?? $productOrderId); if ((int)($item['template'] ?? 0) === 32) { $favoriteProductId = (int)($item['parent'] ?? $favoriteProductId); } $favoriteIds = array_map('intval', (array)($_SESSION['favorite'] ?? [])); $isFavorite = $favoriteProductId > 0 && in_array($favoriteProductId, $favoriteIds, true); $favoriteAddLabel = __('dd::global.favorite_add'); $favoriteRemoveLabel = __('dd::global.favorite_remove'); $canAddToCart = $forSale && $isOrderable && $availabilityCtaType ==='cart' && $productOrderId > 0; $canRequestProduct = $forSale && $isOrderable && $availabilityCtaType ==='request' && $productOrderId > 0; $managerEntries = []; $managerId = (int)evo()->getLoginUserID('mgr'); $isManager = $managerId > 0; $isWholesaler = !empty($_SESSION['webSellerId']); $cardMetaSku = trim((string)($item['sku'] ?? '')); $hasStaffVariants = ($isManager || $isWholesaler) && count((array)($item['children'] ?? [])) > 1; $cardMetaId = $hasStaffVariants ? $productOrderId : (int)($item['id'] ?? 0); $cardShowMetaId = $cardMetaId > 0 && \EvolutionCMS\Ddaudio\Services\Storefront\StorefrontProductIdVisibility::visible(); $hasKitComposition = !empty($item['stock']['data']['sku']['related']) && $productOrderId > 0; $canViewKitComposition = \EvolutionCMS\Ddaudio\Helpers\DevHelper::isManager() && $hasKitComposition; $hasCardNuances = ($isManager || $isWholesaler) && !empty($item['nuances']['full']); $cardMarginPercent = ($isManager || $isWholesaler) ? trim((string)($item['marginality']['percent'] ?? '')) : ''; $canPrintLabels = \EvolutionCMS\Ddaudio\Helpers\DevHelper::isManager() && $productOrderId > 0; $showCardManagerTools = ($isManager && $cardMetaId > 0) || $canViewKitComposition || $canPrintLabels || $hasCardNuances || $cardMarginPercent !==''; // "від" the lowest retail price; managers and wholesalers also see each variant's SKU and stock. $cardVariantLabel = trim((string)($item['variant_option_name'] ?? '')) ?: __('dd::storefront.variants'); $cardVariantOptions = []; if (count((array)($item['children'] ?? [])) > 1) { foreach ((array)($item['children'] ?? []) as $child) { if (!is_array($child) || (int)($child['id'] ?? 0) <= 0) { continue; } $child = StorefrontDeliveryTerms::forProduct($child); $childId = (int)$child['id']; $childBadge = (array)($child['availabilityBadge'] ?? []); $childStockCount = (int)($child['inStockCount'] ?? 0); $childCta = (array)($childBadge['cta'] ?? []); $childCtaType = (string)($childCta['type'] ?? ($childStockCount > 0 ? 'cart' : 'request')); $childOrderable = (bool)($childBadge['is_orderable'] ?? ($childStockCount > 0)); $childUnavailableForDomain = (Domain::isEuMarket() && (int)($child['not_sale_europe'] ?? 0) === 1) || (int)($child['not_sale_' . Domain::siteKey()] ?? 0) === 1 || (int)($child['not_sale'] ?? 0) === 1; $childForSale = (int)($child['hidemenu'] ?? 0) === 0 && !$childUnavailableForDomain; $childAvailability = trim((string)($childBadge['label'] ?? '')); if ($childAvailability ==='') { $childAvailability = trim(strip_tags((string)($child['inStock'] ?? ''))); } $childPrice = $child['price'] ?? ''; if (!$childForSale) { $childPrice = (int)($child['hidemenu'] ?? 0) === 1 ? __('dd::global.out_of_production') : __('dd::global.not_sale'); } $childPreorder = (array)($child['preorder'] ?? []); $childPreorder['hint'] = \EvolutionCMS\Ddaudio\Services\Preorder\PreorderHintText::for($childPreorder); $childPresentation = StorefrontPricePresentation::forProduct($child, $isAuthenticatedCustomer); $childOldPrice = $childForSale ? ($childPresentation['old_price'] ?? '') : ''; $childBonusAmount = $showCardLoyaltyBonus && $childForSale && is_numeric($childPrice) ? CustomerLoyaltyProgram::purchaseBonusAmount((float)$childPrice) : 0; $cardVariantOptions[] = [ 'id' => $childId, 'title' => trim((string)($child['pagetitle'] ?? '')) ?: '#' . $childId, 'url' => trim((string)($child['alias'] ?? '')) !=='' ? (string)$child['alias'] . '.html' : $url, 'price' => (string)$childPrice, 'oldPrice' => (string)$childOldPrice, 'discountPercent' => $childForSale ? $childPresentation['discount_percent'] : 0, 'clientPercent' => $childForSale ? $childPresentation['client_percent'] : 0, 'clientBasePrice' => $childForSale ? $childPresentation['client_base_price'] : null, 'delivery' => StorefrontCardTerms::forProduct($child), 'dispatch' => \EvolutionCMS\Ddaudio\Services\Product\StorefrontCardDispatch::forProduct(array_replace($child, ['forSale' => $childForSale])), 'promoUntil' => $childForSale ? $childPresentation['promo_until'] : '', 'priceRefreshRequired' => $childForSale && $childPresentation['refresh_required'], 'bonusAmount' => $childBonusAmount, 'forSale' => $childForSale, // ProductListItemPresenter turns the category TV into 'categories' and drops 'category'. 'freeShipping' => StorefrontFreeShipping::forProduct([ 'price' => $childPrice, 'forSale' => $childForSale, 'categories' => $item['categories'] ?? [], 'category' => $item['category'] ?? '', ]), 'isMarkdown' => \EvolutionCMS\Ddaudio\Services\Product\StorefrontMarkdown::forProduct($child), 'availability' => $childAvailability, 'stockCount' => $childStockCount, 'sku' => trim((string)($child['sku'] ?? '')), 'sku2' => trim((string)($child['sku2'] ?? '')), 'canCart' => $childForSale && $childOrderable && $childCtaType ==='cart', 'canRequest' => $childForSale && $childOrderable && $childCtaType ==='request', 'preorder' => $childPreorder, ]; } } $hasCardVariants = count($cardVariantOptions) > 1; // it); the card it returns has that variant chosen. Until then, as for retail, nothing is chosen. $staffVariantChosen = $hasStaffVariants && $hasCardVariants && !empty($item['variantChosen']); $cardVariantPending = $hasCardVariants && !$staffVariantChosen; $minimumCardVariantPrice = collect($cardVariantOptions) ->filter(static fn(array $option): bool => $option['forSale'] && is_numeric($option['price'])) ->min(static fn(array $option): float => (float)$option['price']); $variantHasCartAction = $cardVariantPending && collect($cardVariantOptions)->contains(static fn(array $option): bool => $option['canCart']); $variantHasRequestAction = $cardVariantPending && collect($cardVariantOptions)->contains(static fn(array $option): bool => $option['canRequest']); $loyaltyBonusPrice = $cardVariantPending && $minimumCardVariantPrice !== null ? $minimumCardVariantPrice : $price; $loyaltyBonusEligible = $cardVariantPending ? $minimumCardVariantPrice !== null : $forSale; $freeShipping = StorefrontFreeShipping::forProduct([ 'price' => $loyaltyBonusPrice, 'forSale' => $loyaltyBonusEligible, 'categories' => $item['categories'] ?? [], 'category' => $item['category'] ?? '', ]); $loyaltyBonusAmount = $showCardLoyaltyBonus && $loyaltyBonusEligible && is_numeric($loyaltyBonusPrice) ? CustomerLoyaltyProgram::purchaseBonusAmount((float)$loyaltyBonusPrice) : 0; $loyaltyBonusTemplate = __('dd::product.loyalty_bonus_amount', [ 'amount' => '__AMOUNT__', 'currency' => $currencyLabel, ]); $wholesaleStock = (array)($ite @php use EvolutionCMS\Ddaudio\Facades\Domain; use EvolutionCMS\Ddaudio\Services\CustomerLoyaltyProgram; use EvolutionCMS\Ddaudio\Services\Product\StorefrontPricePresentation; use EvolutionCMS\Ddaudio\Services\Product\StorefrontFreeShipping; use EvolutionCMS\Ddaudio\Services\Product\StorefrontCardTerms; use EvolutionCMS\Ddaudio\Services\Product\StorefrontDeliveryTerms; $item = StorefrontDeliveryTerms::forProduct($item); $url = (string)($item['url'] ?? '#'); $title = trim((string)($item['pagetitle'] ?? '')); $normalizeImage = static function (mixed $image): string { $image = is_string($image) ? trim($image) : ''; return $image ==='' || preg_match('~^(?:https?:)?//|^data:|^/~i', $image) ? $image : '/' . ltrim($image, '/'); }; $sliderLarge = array_values((array)($item['sliderLarge'] ?? [])); $largeImages = []; $images = collect(array_values((array)($item['slider'] ?? []))) ->map(static fn(mixed $image, int $index): array => [$normalizeImage($image), $normalizeImage($sliderLarge[$index] ?? '')]) ->filter(static fn(array $pair): bool => $pair[0] !=='') ->unique(static fn(array $pair): string => $pair[0]) ->take(4) ->values() ->map(static function (array $pair) use (&$largeImages): string { if ($pair[1] !=='') { $largeImages[$pair[0]] = $pair[1]; } return $pair[0]; }) ->all(); $imageSrcset = static fn(string $image): string => isset($largeImages[$image]) ? $image . ' 320w, ' . $largeImages[$image] . ' 920w' : ''; $imageSizes ='(max-width: 575px) 100vw, (max-width: 1199px) 50vw, 360px'; if ($images === [] && is_string($item['image'] ?? null) && trim($item['image']) !=='') { $fallbackImage = trim($item['image']); $images[] = preg_match('~^(?:https?:)?//|^data:|^/~i', $fallbackImage) ? $fallbackImage : '/' . ltrim($fallbackImage, '/'); } $image = (string)($images[0] ?? ''); /* Фото немає — сірий логотип замість картинки товару. Клас is-placeholder не дає заглушці розтягнутись на всю плитку (див. card-product.scss). */ $imageIsPlaceholder = $image ===''; if ($imageIsPlaceholder) { $image = rtrim($storefrontThemeUrl, '/') . '/img/icons/logo-gray.svg'; } $hasImageSlider = count($images) > 1; $labels = (array)($item['labels'] ?? []); $rating = (array)($item['rating'] ?? []); $ratingValue = (float)($rating['rating'] ?? 0); $ratingTotal = 5.0; // total is the sum of votes, not the scale maximum. $ratingVotes = max(0, (int)($rating['votes'] ?? 0)); $ratingFilled = max(0, min(5, (int)round($ratingValue))); $currency = strtoupper((string)($_SESSION['currency'] ?? 'UAH')); $currencyLabel = \EvolutionCMS\Ddaudio\Support\StorefrontMoney::label($currency); $forSale = (bool)($item['forSale'] ?? false); $price = $item['price'] ?? null; $oldPrice = $item['price_old'] ?? null; $isAuthenticatedCustomer = (bool)evo()->getLoginUserID('web') && empty($_SESSION['webSellerId']); $pricePresentation = StorefrontPricePresentation::forProduct($item, $isAuthenticatedCustomer); $oldPrice = $pricePresentation['old_price']; $discountPercent = $pricePresentation['discount_percent']; $clientDiscountPercent = $pricePresentation['client_percent']; $isClientOnlyDiscount = $clientDiscountPercent > 0; $promoUntil = $pricePresentation['promo_until']; $formatCardAmount = static fn(float|int $amount): string => \EvolutionCMS\Ddaudio\Support\StorefrontMoney::format($amount); $showCardLoyaltyBonus = empty($_SESSION['webSellerId']) && Domain::isCountry('UA'); $stockCount = (int)($item['inStockCount'] ?? 0); $availabilityBadge = (array)($item['availabilityBadge'] ?? []); $availability = trim((string)($availabilityBadge['label'] ?? '')); $availabilityCta = (array)($availabilityBadge['cta'] ?? []); $availabilityCtaType = (string)($availabilityCta['type'] ?? ($stockCount > 0 ? 'cart' : 'request')); $isOrderable = (bool)($availabilityBadge['is_orderable'] ?? ($stockCount > 0)); $cardTerms = StorefrontCardTerms::forProduct($item); $productOrderId = (int)($item['productId'] ?? 0); $favoriteProductId = (int)($item['id'] ?? $productOrderId); if ((int)($item['template'] ?? 0) === 32) { $favoriteProductId = (int)($item['parent'] ?? $favoriteProductId); } $favoriteIds = array_map('intval', (array)($_SESSION['favorite'] ?? [])); $isFavorite = $favoriteProductId > 0 && in_array($favoriteProductId, $favoriteIds, true); $favoriteAddLabel = __('dd::global.favorite_add'); $favoriteRemoveLabel = __('dd::global.favorite_remove'); $canAddToCart = $forSale && $isOrderable && $availabilityCtaType ==='cart' && $productOrderId > 0; $canRequestProduct = $forSale && $isOrderable && $availabilityCtaType ==='request' && $productOrderId > 0; $managerEntries = []; $managerId = (int)evo()->getLoginUserID('mgr'); $isManager = $managerId > 0; $isWholesaler = !empty($_SESSION['webSellerId']); $cardMetaSku = trim((string)($item['sku'] ?? '')); $hasStaffVariants = ($isManager || $isWholesaler) && count((array)($item['children'] ?? [])) > 1; $cardMetaId = $hasStaffVariants ? $productOrderId : (int)($item['id'] ?? 0); $cardShowMetaId = $cardMetaId > 0 && \EvolutionCMS\Ddaudio\Services\Storefront\StorefrontProductIdVisibility::visible(); $hasKitComposition = !empty($item['stock']['data']['sku']['related']) && $productOrderId > 0; $canViewKitComposition = \EvolutionCMS\Ddaudio\Helpers\DevHelper::isManager() && $hasKitComposition; $hasCardNuances = ($isManager || $isWholesaler) && !empty($item['nuances']['full']); $cardMarginPercent = ($isManager || $isWholesaler) ? trim((string)($item['marginality']['percent'] ?? '')) : ''; $canPrintLabels = \EvolutionCMS\Ddaudio\Helpers\DevHelper::isManager() && $productOrderId > 0; $showCardManagerTools = ($isManager && $cardMetaId > 0) || $canViewKitComposition || $canPrintLabels || $hasCardNuances || $cardMarginPercent !==''; // "від" the lowest retail price; managers and wholesalers also see each variant's SKU and stock. $cardVariantLabel = trim((string)($item['variant_option_name'] ?? '')) ?: __('dd::storefront.variants'); $cardVariantOptions = []; if (count((array)($item['children'] ?? [])) > 1) { foreach ((array)($item['children'] ?? []) as $child) { if (!is_array($child) || (int)($child['id'] ?? 0) <= 0) { continue; } $child = StorefrontDeliveryTerms::forProduct($child); $childId = (int)$child['id']; $childBadge = (array)($child['availabilityBadge'] ?? []); $childStockCount = (int)($child['inStockCount'] ?? 0); $childCta = (array)($childBadge['cta'] ?? []); $childCtaType = (string)($childCta['type'] ?? ($childStockCount > 0 ? 'cart' : 'request')); $childOrderable = (bool)($childBadge['is_orderable'] ?? ($childStockCount > 0)); $childUnavailableForDomain = (Domain::isEuMarket() && (int)($child['not_sale_europe'] ?? 0) === 1) || (int)($child['not_sale_' . Domain::siteKey()] ?? 0) === 1 || (int)($child['not_sale'] ?? 0) === 1; $childForSale = (int)($child['hidemenu'] ?? 0) === 0 && !$childUnavailableForDomain; $childAvailability = trim((string)($childBadge['label'] ?? '')); if ($childAvailability ==='') { $childAvailability = trim(strip_tags((string)($child['inStock'] ?? ''))); } $childPrice = $child['price'] ?? ''; if (!$childForSale) { $childPrice = (int)($child['hidemenu'] ?? 0) === 1 ? __('dd::global.out_of_production') : __('dd::global.not_sale'); } $childPreorder = (array)($child['preorder'] ?? []); $childPreorder['hint'] = \EvolutionCMS\Ddaudio\Services\Preorder\PreorderHintText::for($childPreorder); $childPresentation = StorefrontPricePresentation::forProduct($child, $isAuthenticatedCustomer); $childOldPrice = $childForSale ? ($childPresentation['old_price'] ?? '') : ''; $childBonusAmount = $showCardLoyaltyBonus && $childForSale && is_numeric($childPrice) ? CustomerLoyaltyProgram::purchaseBonusAmount((float)$childPrice) : 0; $cardVariantOptions[] = [ 'id' => $childId, 'title' => trim((string)($child['pagetitle'] ?? '')) ?: '#' . $childId, 'url' => trim((string)($child['alias'] ?? '')) !=='' ? (string)$child['alias'] . '.html' : $url, 'price' => (string)$childPrice, 'oldPrice' => (string)$childOldPrice, 'discountPercent' => $childForSale ? $childPresentation['discount_percent'] : 0, 'clientPercent' => $childForSale ? $childPresentation['client_percent'] : 0, 'clientBasePrice' => $childForSale ? $childPresentation['client_base_price'] : null, 'delivery' => StorefrontCardTerms::forProduct($child), 'dispatch' => \EvolutionCMS\Ddaudio\Services\Product\StorefrontCardDispatch::forProduct(array_replace($child, ['forSale' => $childForSale])), 'promoUntil' => $childForSale ? $childPresentation['promo_until'] : '', 'priceRefreshRequired' => $childForSale && $childPresentation['refresh_required'], 'bonusAmount' => $childBonusAmount, 'forSale' => $childForSale, // ProductListItemPresenter turns the category TV into 'categories' and drops 'category'. 'freeShipping' => StorefrontFreeShipping::forProduct([ 'price' => $childPrice, 'forSale' => $childForSale, 'categories' => $item['categories'] ?? [], 'category' => $item['category'] ?? '', ]), 'isMarkdown' => \EvolutionCMS\Ddaudio\Services\Product\StorefrontMarkdown::forProduct($child), 'availability' => $childAvailability, 'stockCount' => $childStockCount, 'sku' => trim((string)($child['sku'] ?? '')), 'sku2' => trim((string)($child['sku2'] ?? '')), 'canCart' => $childForSale && $childOrderable && $childCtaType ==='cart', 'canRequest' => $childForSale && $childOrderable && $childCtaType ==='request', 'preorder' => $childPreorder, ]; } } $hasCardVariants = count($cardVariantOptions) > 1; // it); the card it returns has that variant chosen. Until then, as for retail, nothing is chosen. $staffVariantChosen = $hasStaffVariants && $hasCardVariants && !empty($item['variantChosen']); $cardVariantPending = $hasCardVariants && !$staffVariantChosen; $minimumCardVariantPrice = collect($cardVariantOptions) ->filter(static fn(array $option): bool => $option['forSale'] && is_numeric($option['price'])) ->min(static fn(array $option): float => (float)$option['price']); $variantHasCartAction = $cardVariantPending && collect($cardVariantOptions)->contains(static fn(array $option): bool => $option['canCart']); $variantHasRequestAction = $cardVariantPending && collect($cardVariantOptions)->contains(static fn(array $option): bool => $option['canRequest']); $loyaltyBonusPrice = $cardVariantPending && $minimumCardVariantPrice !== null ? $minimumCardVariantPrice : $price; $loyaltyBonusEligible = $cardVariantPending ? $minimumCardVariantPrice !== null : $forSale; $freeShipping = StorefrontFreeShipping::forProduct([ 'price' => $loyaltyBonusPrice, 'forSale' => $loyaltyBonusEligible, 'categories' => $item['categories'] ?? [], 'category' => $item['category'] ?? '', ]); $loyaltyBonusAmount = $showCardLoyaltyBonus && $loyaltyBonusEligible && is_numeric($loyaltyBonusPrice) ? CustomerLoyaltyProgram::purchaseBonusAmount((float)$loyaltyBonusPrice) : 0; $loyaltyBonusTemplate = __('dd::product.loyalty_bonus_amount', [ 'amount' => '__AMOUNT__', 'currency' => $currencyLabel, ]); $wholesaleStock = (array)($ite @php use EvolutionCMS\Ddaudio\Facades\Domain; use EvolutionCMS\Ddaudio\Services\CustomerLoyaltyProgram; use EvolutionCMS\Ddaudio\Services\Product\StorefrontPricePresentation; use EvolutionCMS\Ddaudio\Services\Product\StorefrontFreeShipping; use EvolutionCMS\Ddaudio\Services\Product\StorefrontCardTerms; use EvolutionCMS\Ddaudio\Services\Product\StorefrontDeliveryTerms; $item = StorefrontDeliveryTerms::forProduct($item); $url = (string)($item['url'] ?? '#'); $title = trim((string)($item['pagetitle'] ?? '')); $normalizeImage = static function (mixed $image): string { $image = is_string($image) ? trim($image) : ''; return $image ==='' || preg_match('~^(?:https?:)?//|^data:|^/~i', $image) ? $image : '/' . ltrim($image, '/'); }; $sliderLarge = array_values((array)($item['sliderLarge'] ?? [])); $largeImages = []; $images = collect(array_values((array)($item['slider'] ?? []))) ->map(static fn(mixed $image, int $index): array => [$normalizeImage($image), $normalizeImage($sliderLarge[$index] ?? '')]) ->filter(static fn(array $pair): bool => $pair[0] !=='') ->unique(static fn(array $pair): string => $pair[0]) ->take(4) ->values() ->map(static function (array $pair) use (&$largeImages): string { if ($pair[1] !=='') { $largeImages[$pair[0]] = $pair[1]; } return $pair[0]; }) ->all(); $imageSrcset = static fn(string $image): string => isset($largeImages[$image]) ? $image . ' 320w, ' . $largeImages[$image] . ' 920w' : ''; $imageSizes ='(max-width: 575px) 100vw, (max-width: 1199px) 50vw, 360px'; if ($images === [] && is_string($item['image'] ?? null) && trim($item['image']) !=='') { $fallbackImage = trim($item['image']); $images[] = preg_match('~^(?:https?:)?//|^data:|^/~i', $fallbackImage) ? $fallbackImage : '/' . ltrim($fallbackImage, '/'); } $image = (string)($images[0] ?? ''); /* Фото немає — сірий логотип замість картинки товару. Клас is-placeholder не дає заглушці розтягнутись на всю плитку (див. card-product.scss). */ $imageIsPlaceholder = $image ===''; if ($imageIsPlaceholder) { $image = rtrim($storefrontThemeUrl, '/') . '/img/icons/logo-gray.svg'; } $hasImageSlider = count($images) > 1; $labels = (array)($item['labels'] ?? []); $rating = (array)($item['rating'] ?? []); $ratingValue = (float)($rating['rating'] ?? 0); $ratingTotal = 5.0; // total is the sum of votes, not the scale maximum. $ratingVotes = max(0, (int)($rating['votes'] ?? 0)); $ratingFilled = max(0, min(5, (int)round($ratingValue))); $currency = strtoupper((string)($_SESSION['currency'] ?? 'UAH')); $currencyLabel = \EvolutionCMS\Ddaudio\Support\StorefrontMoney::label($currency); $forSale = (bool)($item['forSale'] ?? false); $price = $item['price'] ?? null; $oldPrice = $item['price_old'] ?? null; $isAuthenticatedCustomer = (bool)evo()->getLoginUserID('web') && empty($_SESSION['webSellerId']); $pricePresentation = StorefrontPricePresentation::forProduct($item, $isAuthenticatedCustomer); $oldPrice = $pricePresentation['old_price']; $discountPercent = $pricePresentation['discount_percent']; $clientDiscountPercent = $pricePresentation['client_percent']; $isClientOnlyDiscount = $clientDiscountPercent > 0; $promoUntil = $pricePresentation['promo_until']; $formatCardAmount = static fn(float|int $amount): string => \EvolutionCMS\Ddaudio\Support\StorefrontMoney::format($amount); $showCardLoyaltyBonus = empty($_SESSION['webSellerId']) && Domain::isCountry('UA'); $stockCount = (int)($item['inStockCount'] ?? 0); $availabilityBadge = (array)($item['availabilityBadge'] ?? []); $availability = trim((string)($availabilityBadge['label'] ?? '')); $availabilityCta = (array)($availabilityBadge['cta'] ?? []); $availabilityCtaType = (string)($availabilityCta['type'] ?? ($stockCount > 0 ? 'cart' : 'request')); $isOrderable = (bool)($availabilityBadge['is_orderable'] ?? ($stockCount > 0)); $cardTerms = StorefrontCardTerms::forProduct($item); $productOrderId = (int)($item['productId'] ?? 0); $favoriteProductId = (int)($item['id'] ?? $productOrderId); if ((int)($item['template'] ?? 0) === 32) { $favoriteProductId = (int)($item['parent'] ?? $favoriteProductId); } $favoriteIds = array_map('intval', (array)($_SESSION['favorite'] ?? [])); $isFavorite = $favoriteProductId > 0 && in_array($favoriteProductId, $favoriteIds, true); $favoriteAddLabel = __('dd::global.favorite_add'); $favoriteRemoveLabel = __('dd::global.favorite_remove'); $canAddToCart = $forSale && $isOrderable && $availabilityCtaType ==='cart' && $productOrderId > 0; $canRequestProduct = $forSale && $isOrderable && $availabilityCtaType ==='request' && $productOrderId > 0; $managerEntries = []; $managerId = (int)evo()->getLoginUserID('mgr'); $isManager = $managerId > 0; $isWholesaler = !empty($_SESSION['webSellerId']); $cardMetaSku = trim((string)($item['sku'] ?? '')); $hasStaffVariants = ($isManager || $isWholesaler) && count((array)($item['children'] ?? [])) > 1; $cardMetaId = $hasStaffVariants ? $productOrderId : (int)($item['id'] ?? 0); $cardShowMetaId = $cardMetaId > 0 && \EvolutionCMS\Ddaudio\Services\Storefront\StorefrontProductIdVisibility::visible(); $hasKitComposition = !empty($item['stock']['data']['sku']['related']) && $productOrderId > 0; $canViewKitComposition = \EvolutionCMS\Ddaudio\Helpers\DevHelper::isManager() && $hasKitComposition; $hasCardNuances = ($isManager || $isWholesaler) && !empty($item['nuances']['full']); $cardMarginPercent = ($isManager || $isWholesaler) ? trim((string)($item['marginality']['percent'] ?? '')) : ''; $canPrintLabels = \EvolutionCMS\Ddaudio\Helpers\DevHelper::isManager() && $productOrderId > 0; $showCardManagerTools = ($isManager && $cardMetaId > 0) || $canViewKitComposition || $canPrintLabels || $hasCardNuances || $cardMarginPercent !==''; // "від" the lowest retail price; managers and wholesalers also see each variant's SKU and stock. $cardVariantLabel = trim((string)($item['variant_option_name'] ?? '')) ?: __('dd::storefront.variants'); $cardVariantOptions = []; if (count((array)($item['children'] ?? [])) > 1) { foreach ((array)($item['children'] ?? []) as $child) { if (!is_array($child) || (int)($child['id'] ?? 0) <= 0) { continue; } $child = StorefrontDeliveryTerms::forProduct($child); $childId = (int)$child['id']; $childBadge = (array)($child['availabilityBadge'] ?? []); $childStockCount = (int)($child['inStockCount'] ?? 0); $childCta = (array)($childBadge['cta'] ?? []); $childCtaType = (string)($childCta['type'] ?? ($childStockCount > 0 ? 'cart' : 'request')); $childOrderable = (bool)($childBadge['is_orderable'] ?? ($childStockCount > 0)); $childUnavailableForDomain = (Domain::isEuMarket() && (int)($child['not_sale_europe'] ?? 0) === 1) || (int)($child['not_sale_' . Domain::siteKey()] ?? 0) === 1 || (int)($child['not_sale'] ?? 0) === 1; $childForSale = (int)($child['hidemenu'] ?? 0) === 0 && !$childUnavailableForDomain; $childAvailability = trim((string)($childBadge['label'] ?? '')); if ($childAvailability ==='') { $childAvailability = trim(strip_tags((string)($child['inStock'] ?? ''))); } $childPrice = $child['price'] ?? ''; if (!$childForSale) { $childPrice = (int)($child['hidemenu'] ?? 0) === 1 ? __('dd::global.out_of_production') : __('dd::global.not_sale'); } $childPreorder = (array)($child['preorder'] ?? []); $childPreorder['hint'] = \EvolutionCMS\Ddaudio\Services\Preorder\PreorderHintText::for($childPreorder); $childPresentation = StorefrontPricePresentation::forProduct($child, $isAuthenticatedCustomer); $childOldPrice = $childForSale ? ($childPresentation['old_price'] ?? '') : ''; $childBonusAmount = $showCardLoyaltyBonus && $childForSale && is_numeric($childPrice) ? CustomerLoyaltyProgram::purchaseBonusAmount((float)$childPrice) : 0; $cardVariantOptions[] = [ 'id' => $childId, 'title' => trim((string)($child['pagetitle'] ?? '')) ?: '#' . $childId, 'url' => trim((string)($child['alias'] ?? '')) !=='' ? (string)$child['alias'] . '.html' : $url, 'price' => (string)$childPrice, 'oldPrice' => (string)$childOldPrice, 'discountPercent' => $childForSale ? $childPresentation['discount_percent'] : 0, 'clientPercent' => $childForSale ? $childPresentation['client_percent'] : 0, 'clientBasePrice' => $childForSale ? $childPresentation['client_base_price'] : null, 'delivery' => StorefrontCardTerms::forProduct($child), 'dispatch' => \EvolutionCMS\Ddaudio\Services\Product\StorefrontCardDispatch::forProduct(array_replace($child, ['forSale' => $childForSale])), 'promoUntil' => $childForSale ? $childPresentation['promo_until'] : '', 'priceRefreshRequired' => $childForSale && $childPresentation['refresh_required'], 'bonusAmount' => $childBonusAmount, 'forSale' => $childForSale, // ProductListItemPresenter turns the category TV into 'categories' and drops 'category'. 'freeShipping' => StorefrontFreeShipping::forProduct([ 'price' => $childPrice, 'forSale' => $childForSale, 'categories' => $item['categories'] ?? [], 'category' => $item['category'] ?? '', ]), 'isMarkdown' => \EvolutionCMS\Ddaudio\Services\Product\StorefrontMarkdown::forProduct($child), 'availability' => $childAvailability, 'stockCount' => $childStockCount, 'sku' => trim((string)($child['sku'] ?? '')), 'sku2' => trim((string)($child['sku2'] ?? '')), 'canCart' => $childForSale && $childOrderable && $childCtaType ==='cart', 'canRequest' => $childForSale && $childOrderable && $childCtaType ==='request', 'preorder' => $childPreorder, ]; } } $hasCardVariants = count($cardVariantOptions) > 1; // it); the card it returns has that variant chosen. Until then, as for retail, nothing is chosen. $staffVariantChosen = $hasStaffVariants && $hasCardVariants && !empty($item['variantChosen']); $cardVariantPending = $hasCardVariants && !$staffVariantChosen; $minimumCardVariantPrice = collect($cardVariantOptions) ->filter(static fn(array $option): bool => $option['forSale'] && is_numeric($option['price'])) ->min(static fn(array $option): float => (float)$option['price']); $variantHasCartAction = $cardVariantPending && collect($cardVariantOptions)->contains(static fn(array $option): bool => $option['canCart']); $variantHasRequestAction = $cardVariantPending && collect($cardVariantOptions)->contains(static fn(array $option): bool => $option['canRequest']); $loyaltyBonusPrice = $cardVariantPending && $minimumCardVariantPrice !== null ? $minimumCardVariantPrice : $price; $loyaltyBonusEligible = $cardVariantPending ? $minimumCardVariantPrice !== null : $forSale; $freeShipping = StorefrontFreeShipping::forProduct([ 'price' => $loyaltyBonusPrice, 'forSale' => $loyaltyBonusEligible, 'categories' => $item['categories'] ?? [], 'category' => $item['category'] ?? '', ]); $loyaltyBonusAmount = $showCardLoyaltyBonus && $loyaltyBonusEligible && is_numeric($loyaltyBonusPrice) ? CustomerLoyaltyProgram::purchaseBonusAmount((float)$loyaltyBonusPrice) : 0; $loyaltyBonusTemplate = __('dd::product.loyalty_bonus_amount', [ 'amount' => '__AMOUNT__', 'currency' => $currencyLabel, ]); $wholesaleStock = (array)($ite @php use EvolutionCMS\Ddaudio\Facades\Domain; use EvolutionCMS\Ddaudio\Services\CustomerLoyaltyProgram; use EvolutionCMS\Ddaudio\Services\Product\StorefrontPricePresentation; use EvolutionCMS\Ddaudio\Services\Product\StorefrontFreeShipping; use EvolutionCMS\Ddaudio\Services\Product\StorefrontCardTerms; use EvolutionCMS\Ddaudio\Services\Product\StorefrontDeliveryTerms; $item = StorefrontDeliveryTerms::forProduct($item); $url = (string)($item['url'] ?? '#'); $title = trim((string)($item['pagetitle'] ?? '')); $normalizeImage = static function (mixed $image): string { $image = is_string($image) ? trim($image) : ''; return $image ==='' || preg_match('~^(?:https?:)?//|^data:|^/~i', $image) ? $image : '/' . ltrim($image, '/'); }; $sliderLarge = array_values((array)($item['sliderLarge'] ?? [])); $largeImages = []; $images = collect(array_values((array)($item['slider'] ?? []))) ->map(static fn(mixed $image, int $index): array => [$normalizeImage($image), $normalizeImage($sliderLarge[$index] ?? '')]) ->filter(static fn(array $pair): bool => $pair[0] !=='') ->unique(static fn(array $pair): string => $pair[0]) ->take(4) ->values() ->map(static function (array $pair) use (&$largeImages): string { if ($pair[1] !=='') { $largeImages[$pair[0]] = $pair[1]; } return $pair[0]; }) ->all(); $imageSrcset = static fn(string $image): string => isset($largeImages[$image]) ? $image . ' 320w, ' . $largeImages[$image] . ' 920w' : ''; $imageSizes ='(max-width: 575px) 100vw, (max-width: 1199px) 50vw, 360px'; if ($images === [] && is_string($item['image'] ?? null) && trim($item['image']) !=='') { $fallbackImage = trim($item['image']); $images[] = preg_match('~^(?:https?:)?//|^data:|^/~i', $fallbackImage) ? $fallbackImage : '/' . ltrim($fallbackImage, '/'); } $image = (string)($images[0] ?? ''); /* Фото немає — сірий логотип замість картинки товару. Клас is-placeholder не дає заглушці розтягнутись на всю плитку (див. card-product.scss). */ $imageIsPlaceholder = $image ===''; if ($imageIsPlaceholder) { $image = rtrim($storefrontThemeUrl, '/') . '/img/icons/logo-gray.svg'; } $hasImageSlider = count($images) > 1; $labels = (array)($item['labels'] ?? []); $rating = (array)($item['rating'] ?? []); $ratingValue = (float)($rating['rating'] ?? 0); $ratingTotal = 5.0; // total is the sum of votes, not the scale maximum. $ratingVotes = max(0, (int)($rating['votes'] ?? 0)); $ratingFilled = max(0, min(5, (int)round($ratingValue))); $currency = strtoupper((string)($_SESSION['currency'] ?? 'UAH')); $currencyLabel = \EvolutionCMS\Ddaudio\Support\StorefrontMoney::label($currency); $forSale = (bool)($item['forSale'] ?? false); $price = $item['price'] ?? null; $oldPrice = $item['price_old'] ?? null; $isAuthenticatedCustomer = (bool)evo()->getLoginUserID('web') && empty($_SESSION['webSellerId']); $pricePresentation = StorefrontPricePresentation::forProduct($item, $isAuthenticatedCustomer); $oldPrice = $pricePresentation['old_price']; $discountPercent = $pricePresentation['discount_percent']; $clientDiscountPercent = $pricePresentation['client_percent']; $isClientOnlyDiscount = $clientDiscountPercent > 0; $promoUntil = $pricePresentation['promo_until']; $formatCardAmount = static fn(float|int $amount): string => \EvolutionCMS\Ddaudio\Support\StorefrontMoney::format($amount); $showCardLoyaltyBonus = empty($_SESSION['webSellerId']) && Domain::isCountry('UA'); $stockCount = (int)($item['inStockCount'] ?? 0); $availabilityBadge = (array)($item['availabilityBadge'] ?? []); $availability = trim((string)($availabilityBadge['label'] ?? '')); $availabilityCta = (array)($availabilityBadge['cta'] ?? []); $availabilityCtaType = (string)($availabilityCta['type'] ?? ($stockCount > 0 ? 'cart' : 'request')); $isOrderable = (bool)($availabilityBadge['is_orderable'] ?? ($stockCount > 0)); $cardTerms = StorefrontCardTerms::forProduct($item); $productOrderId = (int)($item['productId'] ?? 0); $favoriteProductId = (int)($item['id'] ?? $productOrderId); if ((int)($item['template'] ?? 0) === 32) { $favoriteProductId = (int)($item['parent'] ?? $favoriteProductId); } $favoriteIds = array_map('intval', (array)($_SESSION['favorite'] ?? [])); $isFavorite = $favoriteProductId > 0 && in_array($favoriteProductId, $favoriteIds, true); $favoriteAddLabel = __('dd::global.favorite_add'); $favoriteRemoveLabel = __('dd::global.favorite_remove'); $canAddToCart = $forSale && $isOrderable && $availabilityCtaType ==='cart' && $productOrderId > 0; $canRequestProduct = $forSale && $isOrderable && $availabilityCtaType ==='request' && $productOrderId > 0; $managerEntries = []; $managerId = (int)evo()->getLoginUserID('mgr'); $isManager = $managerId > 0; $isWholesaler = !empty($_SESSION['webSellerId']); $cardMetaSku = trim((string)($item['sku'] ?? '')); $hasStaffVariants = ($isManager || $isWholesaler) && count((array)($item['children'] ?? [])) > 1; $cardMetaId = $hasStaffVariants ? $productOrderId : (int)($item['id'] ?? 0); $cardShowMetaId = $cardMetaId > 0 && \EvolutionCMS\Ddaudio\Services\Storefront\StorefrontProductIdVisibility::visible(); $hasKitComposition = !empty($item['stock']['data']['sku']['related']) && $productOrderId > 0; $canViewKitComposition = \EvolutionCMS\Ddaudio\Helpers\DevHelper::isManager() && $hasKitComposition; $hasCardNuances = ($isManager || $isWholesaler) && !empty($item['nuances']['full']); $cardMarginPercent = ($isManager || $isWholesaler) ? trim((string)($item['marginality']['percent'] ?? '')) : ''; $canPrintLabels = \EvolutionCMS\Ddaudio\Helpers\DevHelper::isManager() && $productOrderId > 0; $showCardManagerTools = ($isManager && $cardMetaId > 0) || $canViewKitComposition || $canPrintLabels || $hasCardNuances || $cardMarginPercent !==''; // "від" the lowest retail price; managers and wholesalers also see each variant's SKU and stock. $cardVariantLabel = trim((string)($item['variant_option_name'] ?? '')) ?: __('dd::storefront.variants'); $cardVariantOptions = []; if (count((array)($item['children'] ?? [])) > 1) { foreach ((array)($item['children'] ?? []) as $child) { if (!is_array($child) || (int)($child['id'] ?? 0) <= 0) { continue; } $child = StorefrontDeliveryTerms::forProduct($child); $childId = (int)$child['id']; $childBadge = (array)($child['availabilityBadge'] ?? []); $childStockCount = (int)($child['inStockCount'] ?? 0); $childCta = (array)($childBadge['cta'] ?? []); $childCtaType = (string)($childCta['type'] ?? ($childStockCount > 0 ? 'cart' : 'request')); $childOrderable = (bool)($childBadge['is_orderable'] ?? ($childStockCount > 0)); $childUnavailableForDomain = (Domain::isEuMarket() && (int)($child['not_sale_europe'] ?? 0) === 1) || (int)($child['not_sale_' . Domain::siteKey()] ?? 0) === 1 || (int)($child['not_sale'] ?? 0) === 1; $childForSale = (int)($child['hidemenu'] ?? 0) === 0 && !$childUnavailableForDomain; $childAvailability = trim((string)($childBadge['label'] ?? '')); if ($childAvailability ==='') { $childAvailability = trim(strip_tags((string)($child['inStock'] ?? ''))); } $childPrice = $child['price'] ?? ''; if (!$childForSale) { $childPrice = (int)($child['hidemenu'] ?? 0) === 1 ? __('dd::global.out_of_production') : __('dd::global.not_sale'); } $childPreorder = (array)($child['preorder'] ?? []); $childPreorder['hint'] = \EvolutionCMS\Ddaudio\Services\Preorder\PreorderHintText::for($childPreorder); $childPresentation = StorefrontPricePresentation::forProduct($child, $isAuthenticatedCustomer); $childOldPrice = $childForSale ? ($childPresentation['old_price'] ?? '') : ''; $childBonusAmount = $showCardLoyaltyBonus && $childForSale && is_numeric($childPrice) ? CustomerLoyaltyProgram::purchaseBonusAmount((float)$childPrice) : 0; $cardVariantOptions[] = [ 'id' => $childId, 'title' => trim((string)($child['pagetitle'] ?? '')) ?: '#' . $childId, 'url' => trim((string)($child['alias'] ?? '')) !=='' ? (string)$child['alias'] . '.html' : $url, 'price' => (string)$childPrice, 'oldPrice' => (string)$childOldPrice, 'discountPercent' => $childForSale ? $childPresentation['discount_percent'] : 0, 'clientPercent' => $childForSale ? $childPresentation['client_percent'] : 0, 'clientBasePrice' => $childForSale ? $childPresentation['client_base_price'] : null, 'delivery' => StorefrontCardTerms::forProduct($child), 'dispatch' => \EvolutionCMS\Ddaudio\Services\Product\StorefrontCardDispatch::forProduct(array_replace($child, ['forSale' => $childForSale])), 'promoUntil' => $childForSale ? $childPresentation['promo_until'] : '', 'priceRefreshRequired' => $childForSale && $childPresentation['refresh_required'], 'bonusAmount' => $childBonusAmount, 'forSale' => $childForSale, // ProductListItemPresenter turns the category TV into 'categories' and drops 'category'. 'freeShipping' => StorefrontFreeShipping::forProduct([ 'price' => $childPrice, 'forSale' => $childForSale, 'categories' => $item['categories'] ?? [], 'category' => $item['category'] ?? '', ]), 'isMarkdown' => \EvolutionCMS\Ddaudio\Services\Product\StorefrontMarkdown::forProduct($child), 'availability' => $childAvailability, 'stockCount' => $childStockCount, 'sku' => trim((string)($child['sku'] ?? '')), 'sku2' => trim((string)($child['sku2'] ?? '')), 'canCart' => $childForSale && $childOrderable && $childCtaType ==='cart', 'canRequest' => $childForSale && $childOrderable && $childCtaType ==='request', 'preorder' => $childPreorder, ]; } } $hasCardVariants = count($cardVariantOptions) > 1; // it); the card it returns has that variant chosen. Until then, as for retail, nothing is chosen. $staffVariantChosen = $hasStaffVariants && $hasCardVariants && !empty($item['variantChosen']); $cardVariantPending = $hasCardVariants && !$staffVariantChosen; $minimumCardVariantPrice = collect($cardVariantOptions) ->filter(static fn(array $option): bool => $option['forSale'] && is_numeric($option['price'])) ->min(static fn(array $option): float => (float)$option['price']); $variantHasCartAction = $cardVariantPending && collect($cardVariantOptions)->contains(static fn(array $option): bool => $option['canCart']); $variantHasRequestAction = $cardVariantPending && collect($cardVariantOptions)->contains(static fn(array $option): bool => $option['canRequest']); $loyaltyBonusPrice = $cardVariantPending && $minimumCardVariantPrice !== null ? $minimumCardVariantPrice : $price; $loyaltyBonusEligible = $cardVariantPending ? $minimumCardVariantPrice !== null : $forSale; $freeShipping = StorefrontFreeShipping::forProduct([ 'price' => $loyaltyBonusPrice, 'forSale' => $loyaltyBonusEligible, 'categories' => $item['categories'] ?? [], 'category' => $item['category'] ?? '', ]); $loyaltyBonusAmount = $showCardLoyaltyBonus && $loyaltyBonusEligible && is_numeric($loyaltyBonusPrice) ? CustomerLoyaltyProgram::purchaseBonusAmount((float)$loyaltyBonusPrice) : 0; $loyaltyBonusTemplate = __('dd::product.loyalty_bonus_amount', [ 'amount' => '__AMOUNT__', 'currency' => $currencyLabel, ]); $wholesaleStock = (array)($ite @php use EvolutionCMS\Ddaudio\Facades\Domain; use EvolutionCMS\Ddaudio\Services\CustomerLoyaltyProgram; use EvolutionCMS\Ddaudio\Services\Product\StorefrontPricePresentation; use EvolutionCMS\Ddaudio\Services\Product\StorefrontFreeShipping; use EvolutionCMS\Ddaudio\Services\Product\StorefrontCardTerms; use EvolutionCMS\Ddaudio\Services\Product\StorefrontDeliveryTerms; $item = StorefrontDeliveryTerms::forProduct($item); $url = (string)($item['url'] ?? '#'); $title = trim((string)($item['pagetitle'] ?? '')); $normalizeImage = static function (mixed $image): string { $image = is_string($image) ? trim($image) : ''; return $image ==='' || preg_match('~^(?:https?:)?//|^data:|^/~i', $image) ? $image : '/' . ltrim($image, '/'); }; $sliderLarge = array_values((array)($item['sliderLarge'] ?? [])); $largeImages = []; $images = collect(array_values((array)($item['slider'] ?? []))) ->map(static fn(mixed $image, int $index): array => [$normalizeImage($image), $normalizeImage($sliderLarge[$index] ?? '')]) ->filter(static fn(array $pair): bool => $pair[0] !=='') ->unique(static fn(array $pair): string => $pair[0]) ->take(4) ->values() ->map(static function (array $pair) use (&$largeImages): string { if ($pair[1] !=='') { $largeImages[$pair[0]] = $pair[1]; } return $pair[0]; }) ->all(); $imageSrcset = static fn(string $image): string => isset($largeImages[$image]) ? $image . ' 320w, ' . $largeImages[$image] . ' 920w' : ''; $imageSizes ='(max-width: 575px) 100vw, (max-width: 1199px) 50vw, 360px'; if ($images === [] && is_string($item['image'] ?? null) && trim($item['image']) !=='') { $fallbackImage = trim($item['image']); $images[] = preg_match('~^(?:https?:)?//|^data:|^/~i', $fallbackImage) ? $fallbackImage : '/' . ltrim($fallbackImage, '/'); } $image = (string)($images[0] ?? ''); /* Фото немає — сірий логотип замість картинки товару. Клас is-placeholder не дає заглушці розтягнутись на всю плитку (див. card-product.scss). */ $imageIsPlaceholder = $image ===''; if ($imageIsPlaceholder) { $image = rtrim($storefrontThemeUrl, '/') . '/img/icons/logo-gray.svg'; } $hasImageSlider = count($images) > 1; $labels = (array)($item['labels'] ?? []); $rating = (array)($item['rating'] ?? []); $ratingValue = (float)($rating['rating'] ?? 0); $ratingTotal = 5.0; // total is the sum of votes, not the scale maximum. $ratingVotes = max(0, (int)($rating['votes'] ?? 0)); $ratingFilled = max(0, min(5, (int)round($ratingValue))); $currency = strtoupper((string)($_SESSION['currency'] ?? 'UAH')); $currencyLabel = \EvolutionCMS\Ddaudio\Support\StorefrontMoney::label($currency); $forSale = (bool)($item['forSale'] ?? false); $price = $item['price'] ?? null; $oldPrice = $item['price_old'] ?? null; $isAuthenticatedCustomer = (bool)evo()->getLoginUserID('web') && empty($_SESSION['webSellerId']); $pricePresentation = StorefrontPricePresentation::forProduct($item, $isAuthenticatedCustomer); $oldPrice = $pricePresentation['old_price']; $discountPercent = $pricePresentation['discount_percent']; $clientDiscountPercent = $pricePresentation['client_percent']; $isClientOnlyDiscount = $clientDiscountPercent > 0; $promoUntil = $pricePresentation['promo_until']; $formatCardAmount = static fn(float|int $amount): string => \EvolutionCMS\Ddaudio\Support\StorefrontMoney::format($amount); $showCardLoyaltyBonus = empty($_SESSION['webSellerId']) && Domain::isCountry('UA'); $stockCount = (int)($item['inStockCount'] ?? 0); $availabilityBadge = (array)($item['availabilityBadge'] ?? []); $availability = trim((string)($availabilityBadge['label'] ?? '')); $availabilityCta = (array)($availabilityBadge['cta'] ?? []); $availabilityCtaType = (string)($availabilityCta['type'] ?? ($stockCount > 0 ? 'cart' : 'request')); $isOrderable = (bool)($availabilityBadge['is_orderable'] ?? ($stockCount > 0)); $cardTerms = StorefrontCardTerms::forProduct($item); $productOrderId = (int)($item['productId'] ?? 0); $favoriteProductId = (int)($item['id'] ?? $productOrderId); if ((int)($item['template'] ?? 0) === 32) { $favoriteProductId = (int)($item['parent'] ?? $favoriteProductId); } $favoriteIds = array_map('intval', (array)($_SESSION['favorite'] ?? [])); $isFavorite = $favoriteProductId > 0 && in_array($favoriteProductId, $favoriteIds, true); $favoriteAddLabel = __('dd::global.favorite_add'); $favoriteRemoveLabel = __('dd::global.favorite_remove'); $canAddToCart = $forSale && $isOrderable && $availabilityCtaType ==='cart' && $productOrderId > 0; $canRequestProduct = $forSale && $isOrderable && $availabilityCtaType ==='request' && $productOrderId > 0; $managerEntries = []; $managerId = (int)evo()->getLoginUserID('mgr'); $isManager = $managerId > 0; $isWholesaler = !empty($_SESSION['webSellerId']); $cardMetaSku = trim((string)($item['sku'] ?? '')); $hasStaffVariants = ($isManager || $isWholesaler) && count((array)($item['children'] ?? [])) > 1; $cardMetaId = $hasStaffVariants ? $productOrderId : (int)($item['id'] ?? 0); $cardShowMetaId = $cardMetaId > 0 && \EvolutionCMS\Ddaudio\Services\Storefront\StorefrontProductIdVisibility::visible(); $hasKitComposition = !empty($item['stock']['data']['sku']['related']) && $productOrderId > 0; $canViewKitComposition = \EvolutionCMS\Ddaudio\Helpers\DevHelper::isManager() && $hasKitComposition; $hasCardNuances = ($isManager || $isWholesaler) && !empty($item['nuances']['full']); $cardMarginPercent = ($isManager || $isWholesaler) ? trim((string)($item['marginality']['percent'] ?? '')) : ''; $canPrintLabels = \EvolutionCMS\Ddaudio\Helpers\DevHelper::isManager() && $productOrderId > 0; $showCardManagerTools = ($isManager && $cardMetaId > 0) || $canViewKitComposition || $canPrintLabels || $hasCardNuances || $cardMarginPercent !==''; // "від" the lowest retail price; managers and wholesalers also see each variant's SKU and stock. $cardVariantLabel = trim((string)($item['variant_option_name'] ?? '')) ?: __('dd::storefront.variants'); $cardVariantOptions = []; if (count((array)($item['children'] ?? [])) > 1) { foreach ((array)($item['children'] ?? []) as $child) { if (!is_array($child) || (int)($child['id'] ?? 0) <= 0) { continue; } $child = StorefrontDeliveryTerms::forProduct($child); $childId = (int)$child['id']; $childBadge = (array)($child['availabilityBadge'] ?? []); $childStockCount = (int)($child['inStockCount'] ?? 0); $childCta = (array)($childBadge['cta'] ?? []); $childCtaType = (string)($childCta['type'] ?? ($childStockCount > 0 ? 'cart' : 'request')); $childOrderable = (bool)($childBadge['is_orderable'] ?? ($childStockCount > 0)); $childUnavailableForDomain = (Domain::isEuMarket() && (int)($child['not_sale_europe'] ?? 0) === 1) || (int)($child['not_sale_' . Domain::siteKey()] ?? 0) === 1 || (int)($child['not_sale'] ?? 0) === 1; $childForSale = (int)($child['hidemenu'] ?? 0) === 0 && !$childUnavailableForDomain; $childAvailability = trim((string)($childBadge['label'] ?? '')); if ($childAvailability ==='') { $childAvailability = trim(strip_tags((string)($child['inStock'] ?? ''))); } $childPrice = $child['price'] ?? ''; if (!$childForSale) { $childPrice = (int)($child['hidemenu'] ?? 0) === 1 ? __('dd::global.out_of_production') : __('dd::global.not_sale'); } $childPreorder = (array)($child['preorder'] ?? []); $childPreorder['hint'] = \EvolutionCMS\Ddaudio\Services\Preorder\PreorderHintText::for($childPreorder); $childPresentation = StorefrontPricePresentation::forProduct($child, $isAuthenticatedCustomer); $childOldPrice = $childForSale ? ($childPresentation['old_price'] ?? '') : ''; $childBonusAmount = $showCardLoyaltyBonus && $childForSale && is_numeric($childPrice) ? CustomerLoyaltyProgram::purchaseBonusAmount((float)$childPrice) : 0; $cardVariantOptions[] = [ 'id' => $childId, 'title' => trim((string)($child['pagetitle'] ?? '')) ?: '#' . $childId, 'url' => trim((string)($child['alias'] ?? '')) !=='' ? (string)$child['alias'] . '.html' : $url, 'price' => (string)$childPrice, 'oldPrice' => (string)$childOldPrice, 'discountPercent' => $childForSale ? $childPresentation['discount_percent'] : 0, 'clientPercent' => $childForSale ? $childPresentation['client_percent'] : 0, 'clientBasePrice' => $childForSale ? $childPresentation['client_base_price'] : null, 'delivery' => StorefrontCardTerms::forProduct($child), 'dispatch' => \EvolutionCMS\Ddaudio\Services\Product\StorefrontCardDispatch::forProduct(array_replace($child, ['forSale' => $childForSale])), 'promoUntil' => $childForSale ? $childPresentation['promo_until'] : '', 'priceRefreshRequired' => $childForSale && $childPresentation['refresh_required'], 'bonusAmount' => $childBonusAmount, 'forSale' => $childForSale, // ProductListItemPresenter turns the category TV into 'categories' and drops 'category'. 'freeShipping' => StorefrontFreeShipping::forProduct([ 'price' => $childPrice, 'forSale' => $childForSale, 'categories' => $item['categories'] ?? [], 'category' => $item['category'] ?? '', ]), 'isMarkdown' => \EvolutionCMS\Ddaudio\Services\Product\StorefrontMarkdown::forProduct($child), 'availability' => $childAvailability, 'stockCount' => $childStockCount, 'sku' => trim((string)($child['sku'] ?? '')), 'sku2' => trim((string)($child['sku2'] ?? '')), 'canCart' => $childForSale && $childOrderable && $childCtaType ==='cart', 'canRequest' => $childForSale && $childOrderable && $childCtaType ==='request', 'preorder' => $childPreorder, ]; } } $hasCardVariants = count($cardVariantOptions) > 1; // it); the card it returns has that variant chosen. Until then, as for retail, nothing is chosen. $staffVariantChosen = $hasStaffVariants && $hasCardVariants && !empty($item['variantChosen']); $cardVariantPending = $hasCardVariants && !$staffVariantChosen; $minimumCardVariantPrice = collect($cardVariantOptions) ->filter(static fn(array $option): bool => $option['forSale'] && is_numeric($option['price'])) ->min(static fn(array $option): float => (float)$option['price']); $variantHasCartAction = $cardVariantPending && collect($cardVariantOptions)->contains(static fn(array $option): bool => $option['canCart']); $variantHasRequestAction = $cardVariantPending && collect($cardVariantOptions)->contains(static fn(array $option): bool => $option['canRequest']); $loyaltyBonusPrice = $cardVariantPending && $minimumCardVariantPrice !== null ? $minimumCardVariantPrice : $price; $loyaltyBonusEligible = $cardVariantPending ? $minimumCardVariantPrice !== null : $forSale; $freeShipping = StorefrontFreeShipping::forProduct([ 'price' => $loyaltyBonusPrice, 'forSale' => $loyaltyBonusEligible, 'categories' => $item['categories'] ?? [], 'category' => $item['category'] ?? '', ]); $loyaltyBonusAmount = $showCardLoyaltyBonus && $loyaltyBonusEligible && is_numeric($loyaltyBonusPrice) ? CustomerLoyaltyProgram::purchaseBonusAmount((float)$loyaltyBonusPrice) : 0; $loyaltyBonusTemplate = __('dd::product.loyalty_bonus_amount', [ 'amount' => '__AMOUNT__', 'currency' => $currencyLabel, ]); $wholesaleStock = (array)($ite @php use EvolutionCMS\Ddaudio\Facades\Domain; use EvolutionCMS\Ddaudio\Services\CustomerLoyaltyProgram; use EvolutionCMS\Ddaudio\Services\Product\StorefrontPricePresentation; use EvolutionCMS\Ddaudio\Services\Product\StorefrontFreeShipping; use EvolutionCMS\Ddaudio\Services\Product\StorefrontCardTerms; use EvolutionCMS\Ddaudio\Services\Product\StorefrontDeliveryTerms; $item = StorefrontDeliveryTerms::forProduct($item); $url = (string)($item['url'] ?? '#'); $title = trim((string)($item['pagetitle'] ?? '')); $normalizeImage = static function (mixed $image): string { $image = is_string($image) ? trim($image) : ''; return $image ==='' || preg_match('~^(?:https?:)?//|^data:|^/~i', $image) ? $image : '/' . ltrim($image, '/'); }; $sliderLarge = array_values((array)($item['sliderLarge'] ?? [])); $largeImages = []; $images = collect(array_values((array)($item['slider'] ?? []))) ->map(static fn(mixed $image, int $index): array => [$normalizeImage($image), $normalizeImage($sliderLarge[$index] ?? '')]) ->filter(static fn(array $pair): bool => $pair[0] !=='') ->unique(static fn(array $pair): string => $pair[0]) ->take(4) ->values() ->map(static function (array $pair) use (&$largeImages): string { if ($pair[1] !=='') { $largeImages[$pair[0]] = $pair[1]; } return $pair[0]; }) ->all(); $imageSrcset = static fn(string $image): string => isset($largeImages[$image]) ? $image . ' 320w, ' . $largeImages[$image] . ' 920w' : ''; $imageSizes ='(max-width: 575px) 100vw, (max-width: 1199px) 50vw, 360px'; if ($images === [] && is_string($item['image'] ?? null) && trim($item['image']) !=='') { $fallbackImage = trim($item['image']); $images[] = preg_match('~^(?:https?:)?//|^data:|^/~i', $fallbackImage) ? $fallbackImage : '/' . ltrim($fallbackImage, '/'); } $image = (string)($images[0] ?? ''); /* Фото немає — сірий логотип замість картинки товару. Клас is-placeholder не дає заглушці розтягнутись на всю плитку (див. card-product.scss). */ $imageIsPlaceholder = $image ===''; if ($imageIsPlaceholder) { $image = rtrim($storefrontThemeUrl, '/') . '/img/icons/logo-gray.svg'; } $hasImageSlider = count($images) > 1; $labels = (array)($item['labels'] ?? []); $rating = (array)($item['rating'] ?? []); $ratingValue = (float)($rating['rating'] ?? 0); $ratingTotal = 5.0; // total is the sum of votes, not the scale maximum. $ratingVotes = max(0, (int)($rating['votes'] ?? 0)); $ratingFilled = max(0, min(5, (int)round($ratingValue))); $currency = strtoupper((string)($_SESSION['currency'] ?? 'UAH')); $currencyLabel = \EvolutionCMS\Ddaudio\Support\StorefrontMoney::label($currency); $forSale = (bool)($item['forSale'] ?? false); $price = $item['price'] ?? null; $oldPrice = $item['price_old'] ?? null; $isAuthenticatedCustomer = (bool)evo()->getLoginUserID('web') && empty($_SESSION['webSellerId']); $pricePresentation = StorefrontPricePresentation::forProduct($item, $isAuthenticatedCustomer); $oldPrice = $pricePresentation['old_price']; $discountPercent = $pricePresentation['discount_percent']; $clientDiscountPercent = $pricePresentation['client_percent']; $isClientOnlyDiscount = $clientDiscountPercent > 0; $promoUntil = $pricePresentation['promo_until']; $formatCardAmount = static fn(float|int $amount): string => \EvolutionCMS\Ddaudio\Support\StorefrontMoney::format($amount); $showCardLoyaltyBonus = empty($_SESSION['webSellerId']) && Domain::isCountry('UA'); $stockCount = (int)($item['inStockCount'] ?? 0); $availabilityBadge = (array)($item['availabilityBadge'] ?? []); $availability = trim((string)($availabilityBadge['label'] ?? '')); $availabilityCta = (array)($availabilityBadge['cta'] ?? []); $availabilityCtaType = (string)($availabilityCta['type'] ?? ($stockCount > 0 ? 'cart' : 'request')); $isOrderable = (bool)($availabilityBadge['is_orderable'] ?? ($stockCount > 0)); $cardTerms = StorefrontCardTerms::forProduct($item); $productOrderId = (int)($item['productId'] ?? 0); $favoriteProductId = (int)($item['id'] ?? $productOrderId); if ((int)($item['template'] ?? 0) === 32) { $favoriteProductId = (int)($item['parent'] ?? $favoriteProductId); } $favoriteIds = array_map('intval', (array)($_SESSION['favorite'] ?? [])); $isFavorite = $favoriteProductId > 0 && in_array($favoriteProductId, $favoriteIds, true); $favoriteAddLabel = __('dd::global.favorite_add'); $favoriteRemoveLabel = __('dd::global.favorite_remove'); $canAddToCart = $forSale && $isOrderable && $availabilityCtaType ==='cart' && $productOrderId > 0; $canRequestProduct = $forSale && $isOrderable && $availabilityCtaType ==='request' && $productOrderId > 0; $managerEntries = []; $managerId = (int)evo()->getLoginUserID('mgr'); $isManager = $managerId > 0; $isWholesaler = !empty($_SESSION['webSellerId']); $cardMetaSku = trim((string)($item['sku'] ?? '')); $hasStaffVariants = ($isManager || $isWholesaler) && count((array)($item['children'] ?? [])) > 1; $cardMetaId = $hasStaffVariants ? $productOrderId : (int)($item['id'] ?? 0); $cardShowMetaId = $cardMetaId > 0 && \EvolutionCMS\Ddaudio\Services\Storefront\StorefrontProductIdVisibility::visible(); $hasKitComposition = !empty($item['stock']['data']['sku']['related']) && $productOrderId > 0; $canViewKitComposition = \EvolutionCMS\Ddaudio\Helpers\DevHelper::isManager() && $hasKitComposition; $hasCardNuances = ($isManager || $isWholesaler) && !empty($item['nuances']['full']); $cardMarginPercent = ($isManager || $isWholesaler) ? trim((string)($item['marginality']['percent'] ?? '')) : ''; $canPrintLabels = \EvolutionCMS\Ddaudio\Helpers\DevHelper::isManager() && $productOrderId > 0; $showCardManagerTools = ($isManager && $cardMetaId > 0) || $canViewKitComposition || $canPrintLabels || $hasCardNuances || $cardMarginPercent !==''; // "від" the lowest retail price; managers and wholesalers also see each variant's SKU and stock. $cardVariantLabel = trim((string)($item['variant_option_name'] ?? '')) ?: __('dd::storefront.variants'); $cardVariantOptions = []; if (count((array)($item['children'] ?? [])) > 1) { foreach ((array)($item['children'] ?? []) as $child) { if (!is_array($child) || (int)($child['id'] ?? 0) <= 0) { continue; } $child = StorefrontDeliveryTerms::forProduct($child); $childId = (int)$child['id']; $childBadge = (array)($child['availabilityBadge'] ?? []); $childStockCount = (int)($child['inStockCount'] ?? 0); $childCta = (array)($childBadge['cta'] ?? []); $childCtaType = (string)($childCta['type'] ?? ($childStockCount > 0 ? 'cart' : 'request')); $childOrderable = (bool)($childBadge['is_orderable'] ?? ($childStockCount > 0)); $childUnavailableForDomain = (Domain::isEuMarket() && (int)($child['not_sale_europe'] ?? 0) === 1) || (int)($child['not_sale_' . Domain::siteKey()] ?? 0) === 1 || (int)($child['not_sale'] ?? 0) === 1; $childForSale = (int)($child['hidemenu'] ?? 0) === 0 && !$childUnavailableForDomain; $childAvailability = trim((string)($childBadge['label'] ?? '')); if ($childAvailability ==='') { $childAvailability = trim(strip_tags((string)($child['inStock'] ?? ''))); } $childPrice = $child['price'] ?? ''; if (!$childForSale) { $childPrice = (int)($child['hidemenu'] ?? 0) === 1 ? __('dd::global.out_of_production') : __('dd::global.not_sale'); } $childPreorder = (array)($child['preorder'] ?? []); $childPreorder['hint'] = \EvolutionCMS\Ddaudio\Services\Preorder\PreorderHintText::for($childPreorder); $childPresentation = StorefrontPricePresentation::forProduct($child, $isAuthenticatedCustomer); $childOldPrice = $childForSale ? ($childPresentation['old_price'] ?? '') : ''; $childBonusAmount = $showCardLoyaltyBonus && $childForSale && is_numeric($childPrice) ? CustomerLoyaltyProgram::purchaseBonusAmount((float)$childPrice) : 0; $cardVariantOptions[] = [ 'id' => $childId, 'title' => trim((string)($child['pagetitle'] ?? '')) ?: '#' . $childId, 'url' => trim((string)($child['alias'] ?? '')) !=='' ? (string)$child['alias'] . '.html' : $url, 'price' => (string)$childPrice, 'oldPrice' => (string)$childOldPrice, 'discountPercent' => $childForSale ? $childPresentation['discount_percent'] : 0, 'clientPercent' => $childForSale ? $childPresentation['client_percent'] : 0, 'clientBasePrice' => $childForSale ? $childPresentation['client_base_price'] : null, 'delivery' => StorefrontCardTerms::forProduct($child), 'dispatch' => \EvolutionCMS\Ddaudio\Services\Product\StorefrontCardDispatch::forProduct(array_replace($child, ['forSale' => $childForSale])), 'promoUntil' => $childForSale ? $childPresentation['promo_until'] : '', 'priceRefreshRequired' => $childForSale && $childPresentation['refresh_required'], 'bonusAmount' => $childBonusAmount, 'forSale' => $childForSale, // ProductListItemPresenter turns the category TV into 'categories' and drops 'category'. 'freeShipping' => StorefrontFreeShipping::forProduct([ 'price' => $childPrice, 'forSale' => $childForSale, 'categories' => $item['categories'] ?? [], 'category' => $item['category'] ?? '', ]), 'isMarkdown' => \EvolutionCMS\Ddaudio\Services\Product\StorefrontMarkdown::forProduct($child), 'availability' => $childAvailability, 'stockCount' => $childStockCount, 'sku' => trim((string)($child['sku'] ?? '')), 'sku2' => trim((string)($child['sku2'] ?? '')), 'canCart' => $childForSale && $childOrderable && $childCtaType ==='cart', 'canRequest' => $childForSale && $childOrderable && $childCtaType ==='request', 'preorder' => $childPreorder, ]; } } $hasCardVariants = count($cardVariantOptions) > 1; // it); the card it returns has that variant chosen. Until then, as for retail, nothing is chosen. $staffVariantChosen = $hasStaffVariants && $hasCardVariants && !empty($item['variantChosen']); $cardVariantPending = $hasCardVariants && !$staffVariantChosen; $minimumCardVariantPrice = collect($cardVariantOptions) ->filter(static fn(array $option): bool => $option['forSale'] && is_numeric($option['price'])) ->min(static fn(array $option): float => (float)$option['price']); $variantHasCartAction = $cardVariantPending && collect($cardVariantOptions)->contains(static fn(array $option): bool => $option['canCart']); $variantHasRequestAction = $cardVariantPending && collect($cardVariantOptions)->contains(static fn(array $option): bool => $option['canRequest']); $loyaltyBonusPrice = $cardVariantPending && $minimumCardVariantPrice !== null ? $minimumCardVariantPrice : $price; $loyaltyBonusEligible = $cardVariantPending ? $minimumCardVariantPrice !== null : $forSale; $freeShipping = StorefrontFreeShipping::forProduct([ 'price' => $loyaltyBonusPrice, 'forSale' => $loyaltyBonusEligible, 'categories' => $item['categories'] ?? [], 'category' => $item['category'] ?? '', ]); $loyaltyBonusAmount = $showCardLoyaltyBonus && $loyaltyBonusEligible && is_numeric($loyaltyBonusPrice) ? CustomerLoyaltyProgram::purchaseBonusAmount((float)$loyaltyBonusPrice) : 0; $loyaltyBonusTemplate = __('dd::product.loyalty_bonus_amount', [ 'amount' => '__AMOUNT__', 'currency' => $currencyLabel, ]); $wholesaleStock = (array)($ite @php use EvolutionCMS\Ddaudio\Facades\Domain; use EvolutionCMS\Ddaudio\Services\CustomerLoyaltyProgram; use EvolutionCMS\Ddaudio\Services\Product\StorefrontPricePresentation; use EvolutionCMS\Ddaudio\Services\Product\StorefrontFreeShipping; use EvolutionCMS\Ddaudio\Services\Product\StorefrontCardTerms; use EvolutionCMS\Ddaudio\Services\Product\StorefrontDeliveryTerms; $item = StorefrontDeliveryTerms::forProduct($item); $url = (string)($item['url'] ?? '#'); $title = trim((string)($item['pagetitle'] ?? '')); $normalizeImage = static function (mixed $image): string { $image = is_string($image) ? trim($image) : ''; return $image ==='' || preg_match('~^(?:https?:)?//|^data:|^/~i', $image) ? $image : '/' . ltrim($image, '/'); }; $sliderLarge = array_values((array)($item['sliderLarge'] ?? [])); $largeImages = []; $images = collect(array_values((array)($item['slider'] ?? []))) ->map(static fn(mixed $image, int $index): array => [$normalizeImage($image), $normalizeImage($sliderLarge[$index] ?? '')]) ->filter(static fn(array $pair): bool => $pair[0] !=='') ->unique(static fn(array $pair): string => $pair[0]) ->take(4) ->values() ->map(static function (array $pair) use (&$largeImages): string { if ($pair[1] !=='') { $largeImages[$pair[0]] = $pair[1]; } return $pair[0]; }) ->all(); $imageSrcset = static fn(string $image): string => isset($largeImages[$image]) ? $image . ' 320w, ' . $largeImages[$image] . ' 920w' : ''; $imageSizes ='(max-width: 575px) 100vw, (max-width: 1199px) 50vw, 360px'; if ($images === [] && is_string($item['image'] ?? null) && trim($item['image']) !=='') { $fallbackImage = trim($item['image']); $images[] = preg_match('~^(?:https?:)?//|^data:|^/~i', $fallbackImage) ? $fallbackImage : '/' . ltrim($fallbackImage, '/'); } $image = (string)($images[0] ?? ''); /* Фото немає — сірий логотип замість картинки товару. Клас is-placeholder не дає заглушці розтягнутись на всю плитку (див. card-product.scss). */ $imageIsPlaceholder = $image ===''; if ($imageIsPlaceholder) { $image = rtrim($storefrontThemeUrl, '/') . '/img/icons/logo-gray.svg'; } $hasImageSlider = count($images) > 1; $labels = (array)($item['labels'] ?? []); $rating = (array)($item['rating'] ?? []); $ratingValue = (float)($rating['rating'] ?? 0); $ratingTotal = 5.0; // total is the sum of votes, not the scale maximum. $ratingVotes = max(0, (int)($rating['votes'] ?? 0)); $ratingFilled = max(0, min(5, (int)round($ratingValue))); $currency = strtoupper((string)($_SESSION['currency'] ?? 'UAH')); $currencyLabel = \EvolutionCMS\Ddaudio\Support\StorefrontMoney::label($currency); $forSale = (bool)($item['forSale'] ?? false); $price = $item['price'] ?? null; $oldPrice = $item['price_old'] ?? null; $isAuthenticatedCustomer = (bool)evo()->getLoginUserID('web') && empty($_SESSION['webSellerId']); $pricePresentation = StorefrontPricePresentation::forProduct($item, $isAuthenticatedCustomer); $oldPrice = $pricePresentation['old_price']; $discountPercent = $pricePresentation['discount_percent']; $clientDiscountPercent = $pricePresentation['client_percent']; $isClientOnlyDiscount = $clientDiscountPercent > 0; $promoUntil = $pricePresentation['promo_until']; $formatCardAmount = static fn(float|int $amount): string => \EvolutionCMS\Ddaudio\Support\StorefrontMoney::format($amount); $showCardLoyaltyBonus = empty($_SESSION['webSellerId']) && Domain::isCountry('UA'); $stockCount = (int)($item['inStockCount'] ?? 0); $availabilityBadge = (array)($item['availabilityBadge'] ?? []); $availability = trim((string)($availabilityBadge['label'] ?? '')); $availabilityCta = (array)($availabilityBadge['cta'] ?? []); $availabilityCtaType = (string)($availabilityCta['type'] ?? ($stockCount > 0 ? 'cart' : 'request')); $isOrderable = (bool)($availabilityBadge['is_orderable'] ?? ($stockCount > 0)); $cardTerms = StorefrontCardTerms::forProduct($item); $productOrderId = (int)($item['productId'] ?? 0); $favoriteProductId = (int)($item['id'] ?? $productOrderId); if ((int)($item['template'] ?? 0) === 32) { $favoriteProductId = (int)($item['parent'] ?? $favoriteProductId); } $favoriteIds = array_map('intval', (array)($_SESSION['favorite'] ?? [])); $isFavorite = $favoriteProductId > 0 && in_array($favoriteProductId, $favoriteIds, true); $favoriteAddLabel = __('dd::global.favorite_add'); $favoriteRemoveLabel = __('dd::global.favorite_remove'); $canAddToCart = $forSale && $isOrderable && $availabilityCtaType ==='cart' && $productOrderId > 0; $canRequestProduct = $forSale && $isOrderable && $availabilityCtaType ==='request' && $productOrderId > 0; $managerEntries = []; $managerId = (int)evo()->getLoginUserID('mgr'); $isManager = $managerId > 0; $isWholesaler = !empty($_SESSION['webSellerId']); $cardMetaSku = trim((string)($item['sku'] ?? '')); $hasStaffVariants = ($isManager || $isWholesaler) && count((array)($item['children'] ?? [])) > 1; $cardMetaId = $hasStaffVariants ? $productOrderId : (int)($item['id'] ?? 0); $cardShowMetaId = $cardMetaId > 0 && \EvolutionCMS\Ddaudio\Services\Storefront\StorefrontProductIdVisibility::visible(); $hasKitComposition = !empty($item['stock']['data']['sku']['related']) && $productOrderId > 0; $canViewKitComposition = \EvolutionCMS\Ddaudio\Helpers\DevHelper::isManager() && $hasKitComposition; $hasCardNuances = ($isManager || $isWholesaler) && !empty($item['nuances']['full']); $cardMarginPercent = ($isManager || $isWholesaler) ? trim((string)($item['marginality']['percent'] ?? '')) : ''; $canPrintLabels = \EvolutionCMS\Ddaudio\Helpers\DevHelper::isManager() && $productOrderId > 0; $showCardManagerTools = ($isManager && $cardMetaId > 0) || $canViewKitComposition || $canPrintLabels || $hasCardNuances || $cardMarginPercent !==''; // "від" the lowest retail price; managers and wholesalers also see each variant's SKU and stock. $cardVariantLabel = trim((string)($item['variant_option_name'] ?? '')) ?: __('dd::storefront.variants'); $cardVariantOptions = []; if (count((array)($item['children'] ?? [])) > 1) { foreach ((array)($item['children'] ?? []) as $child) { if (!is_array($child) || (int)($child['id'] ?? 0) <= 0) { continue; } $child = StorefrontDeliveryTerms::forProduct($child); $childId = (int)$child['id']; $childBadge = (array)($child['availabilityBadge'] ?? []); $childStockCount = (int)($child['inStockCount'] ?? 0); $childCta = (array)($childBadge['cta'] ?? []); $childCtaType = (string)($childCta['type'] ?? ($childStockCount > 0 ? 'cart' : 'request')); $childOrderable = (bool)($childBadge['is_orderable'] ?? ($childStockCount > 0)); $childUnavailableForDomain = (Domain::isEuMarket() && (int)($child['not_sale_europe'] ?? 0) === 1) || (int)($child['not_sale_' . Domain::siteKey()] ?? 0) === 1 || (int)($child['not_sale'] ?? 0) === 1; $childForSale = (int)($child['hidemenu'] ?? 0) === 0 && !$childUnavailableForDomain; $childAvailability = trim((string)($childBadge['label'] ?? '')); if ($childAvailability ==='') { $childAvailability = trim(strip_tags((string)($child['inStock'] ?? ''))); } $childPrice = $child['price'] ?? ''; if (!$childForSale) { $childPrice = (int)($child['hidemenu'] ?? 0) === 1 ? __('dd::global.out_of_production') : __('dd::global.not_sale'); } $childPreorder = (array)($child['preorder'] ?? []); $childPreorder['hint'] = \EvolutionCMS\Ddaudio\Services\Preorder\PreorderHintText::for($childPreorder); $childPresentation = StorefrontPricePresentation::forProduct($child, $isAuthenticatedCustomer); $childOldPrice = $childForSale ? ($childPresentation['old_price'] ?? '') : ''; $childBonusAmount = $showCardLoyaltyBonus && $childForSale && is_numeric($childPrice) ? CustomerLoyaltyProgram::purchaseBonusAmount((float)$childPrice) : 0; $cardVariantOptions[] = [ 'id' => $childId, 'title' => trim((string)($child['pagetitle'] ?? '')) ?: '#' . $childId, 'url' => trim((string)($child['alias'] ?? '')) !=='' ? (string)$child['alias'] . '.html' : $url, 'price' => (string)$childPrice, 'oldPrice' => (string)$childOldPrice, 'discountPercent' => $childForSale ? $childPresentation['discount_percent'] : 0, 'clientPercent' => $childForSale ? $childPresentation['client_percent'] : 0, 'clientBasePrice' => $childForSale ? $childPresentation['client_base_price'] : null, 'delivery' => StorefrontCardTerms::forProduct($child), 'dispatch' => \EvolutionCMS\Ddaudio\Services\Product\StorefrontCardDispatch::forProduct(array_replace($child, ['forSale' => $childForSale])), 'promoUntil' => $childForSale ? $childPresentation['promo_until'] : '', 'priceRefreshRequired' => $childForSale && $childPresentation['refresh_required'], 'bonusAmount' => $childBonusAmount, 'forSale' => $childForSale, // ProductListItemPresenter turns the category TV into 'categories' and drops 'category'. 'freeShipping' => StorefrontFreeShipping::forProduct([ 'price' => $childPrice, 'forSale' => $childForSale, 'categories' => $item['categories'] ?? [], 'category' => $item['category'] ?? '', ]), 'isMarkdown' => \EvolutionCMS\Ddaudio\Services\Product\StorefrontMarkdown::forProduct($child), 'availability' => $childAvailability, 'stockCount' => $childStockCount, 'sku' => trim((string)($child['sku'] ?? '')), 'sku2' => trim((string)($child['sku2'] ?? '')), 'canCart' => $childForSale && $childOrderable && $childCtaType ==='cart', 'canRequest' => $childForSale && $childOrderable && $childCtaType ==='request', 'preorder' => $childPreorder, ]; } } $hasCardVariants = count($cardVariantOptions) > 1; // it); the card it returns has that variant chosen. Until then, as for retail, nothing is chosen. $staffVariantChosen = $hasStaffVariants && $hasCardVariants && !empty($item['variantChosen']); $cardVariantPending = $hasCardVariants && !$staffVariantChosen; $minimumCardVariantPrice = collect($cardVariantOptions) ->filter(static fn(array $option): bool => $option['forSale'] && is_numeric($option['price'])) ->min(static fn(array $option): float => (float)$option['price']); $variantHasCartAction = $cardVariantPending && collect($cardVariantOptions)->contains(static fn(array $option): bool => $option['canCart']); $variantHasRequestAction = $cardVariantPending && collect($cardVariantOptions)->contains(static fn(array $option): bool => $option['canRequest']); $loyaltyBonusPrice = $cardVariantPending && $minimumCardVariantPrice !== null ? $minimumCardVariantPrice : $price; $loyaltyBonusEligible = $cardVariantPending ? $minimumCardVariantPrice !== null : $forSale; $freeShipping = StorefrontFreeShipping::forProduct([ 'price' => $loyaltyBonusPrice, 'forSale' => $loyaltyBonusEligible, 'categories' => $item['categories'] ?? [], 'category' => $item['category'] ?? '', ]); $loyaltyBonusAmount = $showCardLoyaltyBonus && $loyaltyBonusEligible && is_numeric($loyaltyBonusPrice) ? CustomerLoyaltyProgram::purchaseBonusAmount((float)$loyaltyBonusPrice) : 0; $loyaltyBonusTemplate = __('dd::product.loyalty_bonus_amount', [ 'amount' => '__AMOUNT__', 'currency' => $currencyLabel, ]); $wholesaleStock = (array)($ite
Descargar Productos: 240 / 771