multiFetch = $multiFetch; } public function getSeller(int $sellerId): ?array { if (!($seller = $this->getSellers()[$sellerId] ?? false)) { return null; } if (findModule(\Modules::COMPONENTS) && !findModule(\Modules::SELLERS, \Modules::SUB_SMARTY_SELLERS) && isset($seller['photos'])) { $seller['thumbnails'] = array_map(function ($photo) { return new Thumbnail((string) $photo['id'], $photo['description'], ''.$photo['dateUpdated']->getTimestamp()); }, $seller['photos']); } return $seller; } public function getSellers(bool $force = false): array { if ($this->sellers && $force === false) { return $this->sellers; } $qb = $this->getBaseQueryBuilder(); $sellers = []; foreach ($qb->execute() as $item) { $sellers[$item['id']] = $this->prepareSeller($item); } $this->multiFetchSellersData($sellers); return $this->sellers = $sellers; } public function isSellerOrderingAllowed(array $seller): bool { return ($seller['data']['ordering_disabled'] ?? 'N') === 'N'; } public function sellersGroupBy(array $sellers, string $fieldPath): array { $result = []; foreach ($sellers as $seller) { $path = explode('/', $fieldPath); $groupByField = $seller[array_shift($path)] ?? null; foreach ($path as $field) { if (empty($groupByField[$field])) { $groupByField = null; } $groupByField = &$groupByField[$field]; } $result[$groupByField ?: ''][$seller['id']] = $seller; } // seradim abecedne uksort($result, fn ($a, $b) => StringUtil::slugify($a) <=> StringUtil::slugify($b)); return $result; } // Find closest seller by latitude and longitude public function getClosestSeller(float $latitude, float $longitude): ?array { $seller = $this->getBaseQueryBuilder() ->addSelect('( 6371 * acos(cos(radians(:latitude)) * cos(radians(X(position))) * cos(radians(Y(position)) - radians(:longitude)) + sin(radians(:latitude)) * sin(radians(X(position)))) ) AS distance') ->andWhere('position IS NOT NULL AND position != ""') ->addParameters( [ 'latitude' => $latitude, 'longitude' => $longitude, ] ) ->orderBy('distance', 'ASC') ->setMaxResults(1) ->execute()->fetch(); if (!$seller) { return null; } return $this->prepareSeller($seller); } public function getSellersOrderedByClosestToPosition(float $latitude, float $longitude): ?array { $sellers = $this->getSellers(); foreach ($sellers as &$seller) { if ($seller['x'] && $seller['y']) { $theta = $longitude - $seller['y']; $dist = sin(deg2rad($latitude)) * sin(deg2rad($seller['x'])) + cos(deg2rad($latitude)) * cos(deg2rad($seller['x'])) * cos(deg2rad($theta)); $dist = acos($dist); $dist = rad2deg($dist); $miles = $dist * 60 * 1.1515; // vzdálenost v KM $seller['distance'] = $miles * 1.609344; } else { $seller['distance'] = 0; } } usort($sellers, fn ($a, $b) => $a['distance'] - $b['distance']); return $sellers; } public function prepareSeller(array $seller): array { $seller['id'] = (int) $seller['id']; $seller['blocks'] = []; $seller['data'] = array_replace_recursive( json_decode($seller['data'] ?: '', true) ?: [], json_decode($seller['data_translation'] ?? '', true) ?: [], ); $seller['is_ordering_disabled'] = !$this->isSellerOrderingAllowed($seller); $seller['opening_hours'] = $this->getOpeningHours($seller, new \DateTime()); $seller['flags'] = array_filter(explodeFlags($seller['flags'] ?: ''), fn ($k) => !empty($k), ARRAY_FILTER_USE_KEY); return $seller; } /** Vratí informaci, zda je prodejna už zavřená */ public function isSellerClosed(array $seller, int $hourReserve = 0, ?\DateTime $date = null): bool { // pripravim si datum $date = $date ?: new \DateTime(); // zkontroluju, zda je vyplnena hodina zavreni pro konkretni den if (!($closeTime = $this->getClosingTime($seller, $date, 1))) { return true; } $closeTimeParts = explode(':', $closeTime); $hour = (int) $closeTimeParts[0]; $minute = (int) ($closeTimeParts[1] ?? 0); $closeDate = (clone $date)->setTime($hour - $hourReserve, $minute); // zkontroluju, zda neni uz zavreno if ($date >= $closeDate) { return true; } return false; } /** * Vrátí nejbližší datum, kdy je prodejna otevřená * * $returnNullOnNoOpenDate - pokud se mi v celem tydnu nepodari najit zadny datum, tak vrati `null` - znamena to, ze * u ty prodejny pravdepodobne neni nastavena oteviraci doba */ public function getClosestOpenDate(array $seller, int $hourReserve = 0, bool $returnNullOnNoOpenDate = false): ?\DateTime { $loop = 0; $date = new \DateTime(); $found = false; do { if ($isClosed = $this->isSellerClosed($seller, $hourReserve, $date)) { $date->add(new \DateInterval('P1D')); $date->setTime(0, 0); } if (!$isClosed) { $found = true; } $loop++; } while ($isClosed && $loop <= 7); if ($returnNullOnNoOpenDate && !$found) { return null; } return $date; } public function loadSellersDeliveryInfoByPurchaseState(PurchaseState $purchaseState, array &$sellers): void { $productPieces = []; $products = []; foreach ($purchaseState->getProducts() as $product) { $products[$product->getIdProduct()] = $products[$product->getIdProduct()] ?? null; if ($product->getIdVariation()) { $products[$product->getIdProduct()][] = $product->getIdVariation(); } $key = $product->getIdProduct().($product->getIdVariation() ? '/'.$product->getIdVariation() : ''); $productPieces[$key] = ($productPieces[$key] ?? 0) + $product->getPieces(); } $this->multiFetch->fetchSellersInStore($sellers, $products); $this->loadSellersDeliveryDate($sellers, $productPieces); } public function loadSellersDeliveryDate(array &$sellers, array $products = []): void { // pokus o nejaky obecny nacteni datumu doruceni pro prodejny foreach ($sellers as &$seller) { $deliveryDate = new \DateTime(); $availability = self::AVAILABILITY_IN_STORE; $deliveryDateIncrement = 0; foreach ($seller['products'] ?? [] as $key => $item) { $pieces = $products[$key] ?? 1; // pokud nejaky produkt neni v dostatecnem mnozstvi na prodejne, ale je skladem jinde if ($item['in_store'] < $pieces) { $missingPieces = $pieces - $item['in_store']; // pokud je v dostatecnem mnozstvi skladem jinde if (($item['in_store_main'] + $item['in_store_other']) >= $missingPieces) { // mam dostatek kusu skladem, ale nejsou na aktualni prodejne, takze je budu muset zavest $deliveryDateIncrement = 2; $availability = min($availability, self::AVAILABILITY_PARTIALLY_IN_STORE); } else { // nemam dostatek kusu skladem $availability = self::AVAILABILITY_NOT_IN_STORE; } } } // pokud je datum vyzvednuti dnes, ale prodejna za chvili zavira, tak bych mel dat datum az na dalsi den if (!$deliveryDateIncrement) { if ($this->isSellerClosed($seller, 1)) { $deliveryDate = new \DateTime('tomorrow'); } } $seller['deliveryDate'] = $deliveryDate->add(new \DateInterval('P'.$deliveryDateIncrement.'D')); $seller['availability'] = $availability; } } /** Default fetches for sellers */ protected function multiFetchSellersData(array &$sellers): void { $this->multiFetch->fetchSellersBlocks($sellers); $this->multiFetch->fetchSellersPhotos($sellers); } protected function getBaseQueryBuilder(): QueryBuilder { return sqlQueryBuilder() ->select('se.*, X(se.position) as x, Y(se.position) as y') ->from('sellers', 'se') ->andWhere(Operator::equals(['se.figure' => 'Y'])) ->andWhere(Translation::coalesceTranslatedFields( translationClass: SellersTranslation::class, columns: function ($columns) { $columns['data'] = 'data_translation'; return $columns; })); } /** * Fetches the specific times (opening hours, closing hours, break start, break end) of a seller for a particular date. * * @param int $valueIndex represents the type of time interval to retrieve (0 for opening time, 1 for closing time, 2 for break starting time, 3 for break ending time) */ public function getClosingTime(array $seller, \DateTime $date, int $valueIndex): ?string { $openingHours = $this->getOpeningHours($seller, $date); return $openingHours[$date->format('N')][$valueIndex] ?? null; } /** * Gets the opening hours of the seller, evaluates extra opening hours for a particular date. */ public function getOpeningHours(array $seller, \DateTime $date): array { if (($seller['data']['extra_opening_hours']['toggle'] ?? 'N') === 'N') { return $seller['data']['opening_hours'] ?? []; } $startDateString = empty($seller['data']['extra_opening_hours']['date_from']) ? '01.01.1970 00:00:00' : $seller['data']['extra_opening_hours']['date_from']; $endDateString = empty($seller['data']['extra_opening_hours']['date_to']) ? '01.01.2070 00:00:00' : $seller['data']['extra_opening_hours']['date_to']; $startDate = new \DateTime($startDateString); $endDate = new \DateTime($endDateString); if ($date >= $startDate && $date <= $endDate) { $openingHours = $seller['data']['extra_opening_hours']; } else { $openingHours = $seller['data']['opening_hours']; } return $openingHours ?? []; } }