From 723b4aaad61890847140a8ee7ebd297f7678321d Mon Sep 17 00:00:00 2001 From: livca-smile <48252963+livca-smile@users.noreply.github.com> Date: Fri, 3 May 2024 18:27:21 +0200 Subject: [PATCH] 2.0.x (#71) - Add modal map to product page search - Fix show product offer price if shop has been selected, even in Retail mode --- .../Catalog/Product/Retailer/Availability.php | 51 +++++- CHANGELOG.md | 8 +- Helper/Config.php | 29 +++ Model/Layer/Filter/Price.php | 2 + Plugin/ContextPlugin.php | 6 +- Plugin/ProductPlugin.php | 34 +++- Ui/Component/Offer/Listing/DataProvider.php | 2 +- etc/adminhtml/system.xml | 9 + etc/config.xml | 3 + i18n/en_US.csv | 1 + i18n/fr_FR.csv | 1 + view/frontend/layout/catalog_product_view.xml | 11 +- .../product/view/retailer/availability.phtml | 21 ++- view/frontend/web/css/source/_module.less | 170 ++++++++++-------- .../web/js/retailer/product-availability.js | 81 +++++++-- 15 files changed, 327 insertions(+), 102 deletions(-) create mode 100644 Helper/Config.php diff --git a/Block/Catalog/Product/Retailer/Availability.php b/Block/Catalog/Product/Retailer/Availability.php index 11db07a..c320821 100644 --- a/Block/Catalog/Product/Retailer/Availability.php +++ b/Block/Catalog/Product/Retailer/Availability.php @@ -8,6 +8,7 @@ use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Block\Product\Context; use Magento\Catalog\Model\Product as ProductModel; +use Magento\Directory\Model\Region; use Magento\Framework\DataObject\IdentityInterface; use Magento\Framework\Registry; use Magento\Framework\View\Element\Template; @@ -20,6 +21,10 @@ use Smile\Retailer\Api\Data\RetailerExtensionInterface; use Smile\Retailer\Api\Data\RetailerInterface; use Smile\Retailer\Model\ResourceModel\Retailer\CollectionFactory as RetailerCollectionFactory; +use Smile\RetailerOffer\Helper\Config as HelperConfig; +use Smile\StoreLocator\Helper\Data; +use Smile\StoreLocator\Helper\Schedule; +use Smile\StoreLocator\Model\Retailer\ScheduleManagement; /** * Block rendering availability in store for a given product. @@ -32,13 +37,21 @@ class Availability extends Template implements IdentityInterface protected Registry $coreRegistry; protected ?array $storeOffers = null; + /** + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ public function __construct( Context $context, protected ProductRepositoryInterface $productRepository, protected OfferManagement $offerManagement, protected RetailerCollectionFactory $retailerCollectionFactory, protected AddressFormatter $addressFormatter, + protected Region $region, + protected HelperConfig $helperConfig, MapProviderInterface $mapProvider, + protected ScheduleManagement $scheduleManagement, + protected Schedule $scheduleHelper, + protected Data $storeLocatorHelper, array $data = [] ) { $this->map = $mapProvider->getMap(); @@ -59,13 +72,27 @@ public function getJsLayout() $jsLayout['components']['catalog-product-retailer-availability']['productId'] = $this->getProduct()->getId(); $jsLayout['components']['catalog-product-retailer-availability']['storeOffers'] = $this->getStoreOffers(); - $jsLayout['components']['catalog-product-retailer-availability']['children']['geocoder']['provider'] = - $this->map->getIdentifier(); + $jsLayout['components']['catalog-product-retailer-availability']['searchPlaceholderText'] = $this + ->helperConfig->getSearchPlaceholder(); + + // smile-geocoder child + $jsLayout['components']['catalog-product-retailer-availability']['children']['geocoder']['provider'] + = $this->map->getIdentifier(); $jsLayout['components']['catalog-product-retailer-availability']['children']['geocoder'] = array_merge( $jsLayout['components']['catalog-product-retailer-availability']['children']['geocoder'], $this->map->getConfig() ); + // smile-map child + $jsLayout['components']['catalog-product-retailer-availability']['children']['map']['provider'] = $this->map + ->getIdentifier(); + $jsLayout['components']['catalog-product-retailer-availability']['children']['map']['markers'] + = $this->getStoreOffers(); + $jsLayout['components']['catalog-product-retailer-availability']['children']['map'] = array_merge( + $jsLayout['components']['catalog-product-retailer-availability']['children']['map'], + $this->map->getConfig() + ); + return json_encode($jsLayout); } @@ -76,7 +103,9 @@ public function getJsLayout() */ public function getIdentities(): array { - $identities = $this->getProduct()->getIdentities(); + /** @var ProductModel $product */ + $product = $this->getProduct(); + $identities = $product->getIdentities(); foreach ($this->getStoreOffers() as $offer) { if (isset($offer[OfferInterface::OFFER_ID])) { @@ -123,16 +152,32 @@ protected function getStoreOffers(): array /** @var RetailerExtensionInterface $retailerExtensionInterface */ $retailerExtensionInterface = $retailer->getExtensionAttributes(); $address = $retailerExtensionInterface->getAddress(); + $regionName = $this->region->load($address->getRegionId())->getName() ?: null; $offer = [ 'sellerId' => (int) $retailer->getId(), 'name' => $retailer->getName(), 'address' => $this->addressFormatter->formatAddress($address, AddressFormatter::FORMAT_ONELINE), + 'postCode' => $address->getPostcode(), + 'region' => $regionName, + 'city' => $address->getCity(), 'latitude' => $address->getCoordinates()->getLatitude(), 'longitude' => $address->getCoordinates()->getLongitude(), 'setStoreData' => $this->getSetStorePostData($retailer), 'isAvailable' => false, + 'url' => $this->storeLocatorHelper->getRetailerUrl($retailer), ]; + // phpcs:disable Magento2.Performance.ForeachArrayMerge.ForeachArrayMerge + $offer['schedule'] = array_merge( + $this->scheduleHelper->getConfig(), + [ + 'calendar' => $this->scheduleManagement->getCalendar($retailer), + 'openingHours' => $this->scheduleManagement->getWeekOpeningHours($retailer), + 'specialOpeningHours' => $retailerExtensionInterface->getSpecialOpeningHours(), + ] + ); + // phpcs:enable + if (isset($offerByRetailer[(int) $retailer->getId()])) { $offer['isAvailable'] = (bool) $offerByRetailer[(int) $retailer->getId()]->isAvailable(); $offer[OfferInterface::OFFER_ID] = $offerByRetailer[(int) $retailer->getId()]->getId(); diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e437d2..325a910 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,13 @@ All notable changes to this project will be documented in this file. -## [2.0.1] - 2023-09-20 +## [2.0.2] - 2024-05-03 +[2.0.2]: https://github.com/Smile-SA/magento2-module-retailer-offer/compare/2.0.1...2.0.2 + +- Add modal map to product page search +- Fix show product offer price if shop has been selected, even in Retail mode + +## [2.0.1] - 2023-10-05 [2.0.1]: https://github.com/Smile-SA/magento2-module-retailer-offer/compare/2.0.0...2.0.1 - Fix CategoryPlugin closure return type diff --git a/Helper/Config.php b/Helper/Config.php new file mode 100644 index 0000000..5c6c56e --- /dev/null +++ b/Helper/Config.php @@ -0,0 +1,29 @@ +scopeConfig->getValue($path, $scope); + } + + /** + * Get placeholder for search input of store_locator, default: City, Zipcode, Address, ... + */ + public function getSearchPlaceholder(): string + { + return (string) $this->getConfigByPath(self::SEARCH_PLACEHOLDER_XML_PATH) ?: 'City, Zipcode, Address ...'; + } +} diff --git a/Model/Layer/Filter/Price.php b/Model/Layer/Filter/Price.php index c1fdb63..effc1a3 100644 --- a/Model/Layer/Filter/Price.php +++ b/Model/Layer/Filter/Price.php @@ -92,6 +92,7 @@ public function apply(RequestInterface $request) if ($filter) { $this->dataProvider->setInterval($filter); + // @phpstan-ignore-next-line $priorFilters = $this->dataProvider->getPriorFilters($filterParams); if ($priorFilters) { $this->dataProvider->setPriorIntervals($priorFilters); @@ -103,6 +104,7 @@ public function apply(RequestInterface $request) $this->addQueryFilter($fromValue, $toValue); $this->getLayer()->getState()->addFilter( + // @phpstan-ignore-next-line $this->_createItem($this->_renderRangeLabel(empty($fromValue) ? 0 : $fromValue, $toValue), $filter) ); } diff --git a/Plugin/ContextPlugin.php b/Plugin/ContextPlugin.php index 0efb35c..b9a8cc1 100644 --- a/Plugin/ContextPlugin.php +++ b/Plugin/ContextPlugin.php @@ -34,11 +34,13 @@ public function aroundDispatch( RequestInterface $request ): mixed { - if ($this->settingsHelper->isDriveMode()) { + // show product offer price if shop has been selected, even in Retail mode + if ($this->settingsHelper->isDriveMode() || $this->currentStore->getRetailer()) { // Set a default value to have common vary for all customers without any chosen retailer. $retailerId = 'default'; - if ($this->currentStore->getRetailer() && $this->currentStore->getRetailer()->getId()) { + if ($this->currentStore->getRetailer() + && $this->currentStore->getRetailer()->getId()) { $retailerId = $this->currentStore->getRetailer()->getId(); } diff --git a/Plugin/ProductPlugin.php b/Plugin/ProductPlugin.php index d520aaa..84891c5 100644 --- a/Plugin/ProductPlugin.php +++ b/Plugin/ProductPlugin.php @@ -6,16 +6,36 @@ use Closure; use Magento\Catalog\Model\Product; +use Smile\Retailer\Api\Data\RetailerInterface; use Smile\RetailerOffer\Helper\Offer; use Smile\RetailerOffer\Helper\Settings; +use Smile\StoreLocator\CustomerData\CurrentStore; /** * Replace is in stock native filter on layer. */ class ProductPlugin { - public function __construct(private Offer $offerHelper, private Settings $settingsHelper) + + public function __construct( + private Offer $offerHelper, + private Settings $settingsHelper, + protected CurrentStore $currentStore + ) { + } + + /** + * Retrieve current retailer. + */ + private function getRetailer(): ?RetailerInterface { + $retailer = null; + if ($this->currentStore->getRetailer() && $this->currentStore->getRetailer()->getId()) { + /** @var RetailerInterface $retailer */ + $retailer = $this->currentStore->getRetailer(); + } + + return $retailer; } /** @@ -27,7 +47,8 @@ public function aroundIsAvailable(Product $product, Closure $proceed): bool { $isAvailable = $proceed(); - if ($this->settingsHelper->useStoreOffers()) { + // show product availability if shop has been selected, even in Retail mode + if ($this->settingsHelper->useStoreOffers() || $this->getRetailer()) { $isAvailable = false; $offer = $this->offerHelper->getCurrentOffer($product); @@ -46,7 +67,8 @@ public function aroundGetPrice(Product $product, Closure $proceed): mixed { $price = $proceed(); - if ($this->settingsHelper->useStoreOffers()) { + // show product offer price if shop has been selected, even in Retail mode + if ($this->settingsHelper->useStoreOffers() || $this->getRetailer()) { $offer = $this->offerHelper->getCurrentOffer($product); if ($offer && $offer->getPrice()) { @@ -66,7 +88,8 @@ public function aroundGetSpecialPrice(Product $product, Closure $proceed): mixed { $price = $proceed(); - if ($this->settingsHelper->useStoreOffers()) { + // show product offer price if shop has been selected, even in Retail mode + if ($this->settingsHelper->useStoreOffers() || $this->getRetailer()) { $offer = $this->offerHelper->getCurrentOffer($product); if ($offer && $offer->getSpecialPrice()) { @@ -84,7 +107,8 @@ public function aroundGetFinalPrice(Product $product, Closure $proceed, mixed $q { $price = $proceed($qty); - if ($this->settingsHelper->useStoreOffers()) { + // show product offer price if shop has been selected, even in Retail mode + if ($this->settingsHelper->useStoreOffers() || $this->getRetailer()) { $offer = $this->offerHelper->getCurrentOffer($product); if ($offer) { diff --git a/Ui/Component/Offer/Listing/DataProvider.php b/Ui/Component/Offer/Listing/DataProvider.php index d226903..de9959e 100644 --- a/Ui/Component/Offer/Listing/DataProvider.php +++ b/Ui/Component/Offer/Listing/DataProvider.php @@ -47,7 +47,7 @@ public function addField($field, $alias = null): void /** * @inheritdoc */ - public function addFilter(Filter $filter): mixed + public function addFilter(Filter $filter): void { if (isset($this->addFilterStrategies[$filter->getField()])) { $this->addFilterStrategies[$filter->getField()] diff --git a/etc/adminhtml/system.xml b/etc/adminhtml/system.xml index 79ec74f..7186199 100644 --- a/etc/adminhtml/system.xml +++ b/etc/adminhtml/system.xml @@ -9,6 +9,7 @@ Magento_Backend::config_smile_retailersuite_retailer_base_settings + Drive mode : the customer will only see the catalog of the chosen retailer in Front Office. Retail mode : the customer will browse the Web catalog by default. @@ -20,6 +21,14 @@ Magento\Config\Model\Config\Source\Yesno + + + + + + default: City, Zipcode, Address ... + + diff --git a/etc/config.xml b/etc/config.xml index eb8b628..5b2fecf 100644 --- a/etc/config.xml +++ b/etc/config.xml @@ -6,6 +6,9 @@ 0 1 + + City, Zipcode, Address ... + diff --git a/i18n/en_US.csv b/i18n/en_US.csv index ebac202..6b8cac9 100644 --- a/i18n/en_US.csv +++ b/i18n/en_US.csv @@ -61,3 +61,4 @@ ID,ID Search,Search "Retailer Offers","Retailer Offers" "Not Selected","Not Selected" +"City, Zipcode, Address ...","City, Zipcode, Address ..." diff --git a/i18n/fr_FR.csv b/i18n/fr_FR.csv index eed4e4b..838a90e 100644 --- a/i18n/fr_FR.csv +++ b/i18n/fr_FR.csv @@ -61,3 +61,4 @@ ID,ID Search,Search "Retailer Offers","Offres Magasin" "Not Selected","Non Sélectionné" +"City, Zipcode, Address ...","Ville, code postal, addresse ..." diff --git a/view/frontend/layout/catalog_product_view.xml b/view/frontend/layout/catalog_product_view.xml index bece482..098f832 100644 --- a/view/frontend/layout/catalog_product_view.xml +++ b/view/frontend/layout/catalog_product_view.xml @@ -14,13 +14,16 @@ Smile_RetailerOffer/js/retailer/product-availability Smile_RetailerOffer/retailer/product/store-list + Find a store : + Search + Geolocalize me + 25000 smile-geocoder - Find a store : - City, Zipcode, Department, ... - Search - 25000 + + + smile-map diff --git a/view/frontend/templates/product/view/retailer/availability.phtml b/view/frontend/templates/product/view/retailer/availability.phtml index fe5da3f..f323790 100644 --- a/view/frontend/templates/product/view/retailer/availability.phtml +++ b/view/frontend/templates/product/view/retailer/availability.phtml @@ -22,13 +22,13 @@ use Smile\RetailerOffer\Block\Catalog\Product\Retailer\Availability; "modal":true } }'> -
-
+
+
-
+
@@ -39,6 +39,13 @@ use Smile\RetailerOffer\Block\Catalog\Product\Retailer\Availability;
+
+
+ +
+
@@ -48,6 +55,12 @@ use Smile\RetailerOffer\Block\Catalog\Product\Retailer\Availability;
  • + +
    +
    +
    +
    +
    @@ -64,7 +77,7 @@ use Smile\RetailerOffer\Block\Catalog\Product\Retailer\Availability;
    - +
    diff --git a/view/frontend/web/css/source/_module.less b/view/frontend/web/css/source/_module.less index 6176e4f..6e6a6df 100644 --- a/view/frontend/web/css/source/_module.less +++ b/view/frontend/web/css/source/_module.less @@ -1,103 +1,129 @@ // // Shop details on product page // _____________________________________________ - -.catalog-product-retailer-availability-content { - .current-store { - .inStock { - .lib-message-icon-inner(success); +& when (@media-common = true) { + .catalog-product-retailer-availability-content { + .current-store { + .inStock { + .lib-message-icon-inner(success); + } + .outOfStock { + .lib-message-icon-inner(error); + } } - .outOfStock { - .lib-message-icon-inner(error); + + .store-name-value { + font-weight: bold; } } - .store-name-value { - font-weight: bold; + .product-store-availability { + border-bottom: 1px solid @color-gray-light5; + padding-bottom: 15px; + color: @color-gray34; } -} -.product-store-availability { - border-bottom: 1px solid @color-gray-light5; - padding-bottom: 15px; - color: @color-gray34; -} + .catalog-product-stores-availability-content { + display: none; -.catalog-product-stores-availability-content { - .fulltext-search-wrapper { - .form { - display: flex; + .fulltext-search-wrapper { + .form { + display: flex; - .field { - min-width: 30%; - } + .field { + min-width: 30%; + } - button { - margin-left: 10px; + .actions-toolbar { + display: flex; + } + + button { + margin-left: 10px; + } } } - } -} -.store-offers-list { - ul { - list-style-type: none; - margin: 10px 0 0; - padding: 0; - max-height: 420px; - overflow-y: scroll; - - li.result-item { - margin: 0; - padding: 5px; - border-top: 1px solid #d8d8d8; - display: flex; - align-items: center; - flex-direction: row; - flex-wrap: wrap; - justify-content: center; - - ::after { - clear: both; - content: ''; - display: table; - } + .store-view-map .map { + max-width: 100%; + height: 0; + z-index: 1; + display: inline-block; + margin-bottom: 20px; + width: 100%; + } + } - .store { - float:left; - min-width: 50%; - } + .store-offers-list { + ul { + list-style-type: none; + margin: 10px 0 0; + padding: 0; + max-height: 215px; + overflow-y: scroll; - .availability { - float:left; - .available { - .lib-message-icon-inner(success); - margin-bottom: 0; + li.result-item { + margin: 0; + padding: 5px; + border-top: 1px solid #d8d8d8; + display: flex; + align-items: center; + flex-direction: row; + flex-wrap: wrap; + justify-content: center; + + ::after { + clear: both; + content: ''; + display: table; } - .unavailable { - .lib-message-icon-inner(error); - margin-bottom: 0; + .store { + float:left; + min-width: 50%; } - p { - min-width: 80px; + .availability { + float:left; + .available { + .lib-message-icon-inner(success); + margin-bottom: 0; + } + + .unavailable { + .lib-message-icon-inner(error); + margin-bottom: 0; + } + + p { + min-width: 80px; + } } - } - .actions { - margin-left:auto; - } + .actions { + margin-left:auto; + } - .breaker { - clear:both; + .breaker { + clear:both; + } } } } -} -.catalog-product-retailer-availability .showavailability { - cursor: pointer; + .catalog-product-retailer-availability .showavailability { + cursor: pointer; + } + + .modal-popup { + .catalog-product-stores-availability-content { + display: block; + } + + .store-view-map .map { + height: 400px; + } + } } // diff --git a/view/frontend/web/js/retailer/product-availability.js b/view/frontend/web/js/retailer/product-availability.js index 5a114bd..69f1837 100644 --- a/view/frontend/web/js/retailer/product-availability.js +++ b/view/frontend/web/js/retailer/product-availability.js @@ -5,9 +5,11 @@ define([ 'ko', 'uiRegistry', 'Smile_Map/js/model/markers', + 'leaflet', 'smile-storelocator-store-collection', - 'mage/translate' - ], function ($, Component, storage, ko, registry, Markers, StoreCollection) { + 'mage/translate', + 'jquery/ui' + ], function ($, Component, storage, ko, registry, Markers, L, StoreCollection) { "use strict"; @@ -26,23 +28,25 @@ define([ }); this.storeOffers = ko.observable(offers.getList()); this.displayedOffers = ko.observable(offers.getList()); - this.initGeocoderBinding(); + this.observe(['fulltextSearch']); }, /** - * Init the geocoding component binding + * Search Shop per words */ - initGeocoderBinding: function() { + onSearchOffers: function() { registry.get(this.name + '.geocoder', function (geocoder) { this.geocoder = geocoder; - geocoder.currentResult.subscribe(function (result) { + this.geocoder.fulltextSearch(this.fulltextSearch()); + this.geocoder.currentResult.subscribe(function (result) { if (result && result.location) { - var offers = geocoder.filterMarkersListByPositionRadius(this.storeOffers(), result.location); - this.displayedOffers(offers); - } else { - this.displayedOffers(this.storeOffers); + this.findPositionSuccess( + {coords: {latitude: result.location.lat, longitude: result.location.lng}}, + this.fulltextSearch() + ); } }.bind(this)); + this.geocoder.onSearch(); }.bind(this)); }, @@ -99,6 +103,63 @@ define([ } return result; + }, + + /** + * Geolocalize me button action + */ + geolocalizeMe: function() { + registry.get(this.name + '.geocoder', function (geocoder) { + this.geocoder = geocoder; + this.geocoder.geolocalize(this.findPositionSuccess.bind(this)) + }.bind(this)); + }, + + /** + * Action on geolocation success + */ + findPositionSuccess: function(position) { + if (position.coords && position.coords.latitude && position.coords.longitude) { + registry.get(this.name + '.map', function (map) { + this.map = map; + this.map.applyPosition(position); + this.map.addMarkerWithMyPosition(position); + }.bind(this)); + + this.updateDisplayedOffers(); + } + }, + + /** + * Update list of displayed offers + */ + updateDisplayedOffers: function () { + registry.get(this.name + '.map', function (map) { + this.map = map; + this.map.refreshDisplayedMarkers(); + this.displayedOffers(this.map.displayedMarkers()); + }.bind(this)); + }, + + /** + * Load modal function to set moveend event on map + * + * @returns {string} + */ + loadRetailerAvailabilityModal : function () { + let self = this; + registry.get(this.name + '.map', function (map) { + this.map = map; + + // Update map if geolocation is ON and customer already click to geolocalize button + if (navigator.geolocation && window.location.search === '' && window.location.hash.length > 1) { + self.geolocalizeMe(); + } + + // Refresh moveen trigger + this.map.map.off('moveend'); + this.map.map.on('moveend', self.updateDisplayedOffers.bind(self)); + }.bind(this)); } }); });