diff --git a/public/typo3conf/ext/ep_products/Classes/BpnConnect/ApiClient.php b/public/typo3conf/ext/ep_products/Classes/BpnConnect/ApiClient.php new file mode 100644 index 00000000..4b674f59 --- /dev/null +++ b/public/typo3conf/ext/ep_products/Classes/BpnConnect/ApiClient.php @@ -0,0 +1,91 @@ +request('GET', 'api/v1/contingents/calendar', [ + 'query' => [ + 'hotelCode' => $hotelCode, + 'dateFrom' => $dateFrom->format('Y-m-d'), + 'dateTo' => $dateTo->format('Y-m-d'), + ], + ]); + + $this->assertStatusCode($response, [Response::HTTP_OK], 'GET', 'api/v1/contingents/calendar'); + + return $this->decodeResponse($response, 'GET', 'api/v1/contingents/calendar'); + } + + private function request(string $method, string $uri, array $options = []): ResponseInterface + { + try { + return $this->getHttpClient()->request($method, $uri, $options); + } catch (TransportExceptionInterface $e) { + throw new ApiException(sprintf('Bpn Connect API transport error for %s %s: %s', $method, $uri, $e->getMessage()), 0, $e); + } + } + + private function assertStatusCode(ResponseInterface $response, array $expectedStatusCodes, string $method, string $uri): void + { + try { + $statusCode = $response->getStatusCode(); + } catch (TransportExceptionInterface $e) { + throw new ApiException(sprintf('Bpn Connect API transport error while reading status for %s %s: %s', $method, $uri, $e->getMessage()), 0, $e); + } + + if (in_array($statusCode, $expectedStatusCodes, true)) { + return; + } + + throw new ApiException(sprintf('Bpn Connect API returned unexpected status %d for %s %s', $statusCode, $method, $uri), $statusCode); + } + + private function decodeResponse(ResponseInterface $response, string $method, string $uri): array + { + try { + $data = $response->toArray(false); + } catch (DecodingExceptionInterface $e) { + throw new ApiException(sprintf('Bpn Connect API returned invalid JSON for %s %s: %s', $method, $uri, $e->getMessage()), 0, $e); + } catch (TransportExceptionInterface $e) { + throw new ApiException(sprintf('Bpn Connect API transport error while reading body for %s %s: %s', $method, $uri, $e->getMessage()), 0, $e); + } + + if (!isset($data['data']) || !is_array($data['data'])) { + throw new ApiException(sprintf('Bpn Connect API returned a calendar payload without a data array for %s %s', $method, $uri)); + } + + return $data; + } + + private function getHttpClient(): HttpClientInterface + { + $config = GeneralUtility::makeInstance(ExtensionConfiguration::class)->get('ep_products'); + $baseUrl = trim((string)($config['bpnConnectApiBaseUrl'] ?? '')); + $apiKey = trim((string)($config['bpnConnectApiKey'] ?? '')); + + if ($baseUrl === '') { + throw new ApiException('Bpn Connect API base URL is not configured.'); + } + if ($apiKey === '') { + throw new ApiException('Bpn Connect API key is not configured.'); + } + + return HttpClient::createForBaseUri(rtrim($baseUrl, '/') . '/', [ + 'headers' => [ + 'X-API-KEY' => $apiKey, + ], + ]); + } +} diff --git a/public/typo3conf/ext/ep_products/Classes/BpnConnect/ApiException.php b/public/typo3conf/ext/ep_products/Classes/BpnConnect/ApiException.php new file mode 100644 index 00000000..86767028 --- /dev/null +++ b/public/typo3conf/ext/ep_products/Classes/BpnConnect/ApiException.php @@ -0,0 +1,7 @@ +contingentRepository = $contingentRepository; + $this->apiClient = $apiClient; } /** @@ -57,35 +55,89 @@ class AjaxCalendarController extends ActionController */ public function rangeAction(Hotel $hotel, $year = null, $month = null, $months = 1) { - [ $begin, $end ] = DateUtility::getDateRange($year, $month, $months); + $dateRange = DateUtility::getDateRange($year, $month, $months); + $period = $dateRange['period']; + $apiDateFrom = $dateRange['dateFrom']; + $apiDateTo = $dateRange['dateTo']; + $hotelCode = trim((string) $hotel->getCode()); - $factory = new Calendar(); - $factory->getEventManager()->addProvider('contingent', $this->contingentRepository); + $calendar = [ + 'meta' => [], + 'data' => [], + ]; - // Actual date range as requested - $range = new Range($begin, $end); - - // Date range for event selection extends the actual date range by one day - // before and after - $eventsRangeBegin = $range->getBegin()->sub(new \DateInterval('P1D')); - $eventsRangeEnd = $range->getEnd()->add(new \DateInterval('P1D')); - $eventsRange = new Range($eventsRangeBegin, $eventsRangeEnd); - $events = $factory->getEvents($eventsRange, [ 'hotel' => $hotel ]); - - $yearStart = $factory->getYear($range->getBegin()->format('Y')); - $yearEnd = null; - if ($range->getBegin()->format('Y') !== $range->getEnd()->format('Y')) { - $yearEnd = $factory->getYear($range->getEnd()->format('Y')); + if ($hotelCode === '') { + $calendar['error'] = true; + } else { + try { + $calendar = $this->apiClient->getCalendar( + $hotelCode, + new \DateTimeImmutable($apiDateFrom), + new \DateTimeImmutable($apiDateTo) + ); + } catch (\Throwable $e) { + $calendar['error'] = true; + } } $this->view->assignMultiple([ 'months' => $months, 'hotel' => $hotel, - 'range' => $range, - 'yearStart' => $yearStart, - 'yearEnd' => $yearEnd, - 'events' => $events, + 'calendar' => $calendar, + 'calendarMonths' => $this->buildCalendarMonths($period), ]); } + /** + * @return array> + */ + private function buildCalendarMonths(\League\Period\Period $period): array + { + $months = []; + $current = new \DateTimeImmutable($period->getStartDate()->format('Y-m-01 00:00:00')); + $lastDay = $period->getEndDate()->sub(new \DateInterval('P1D')); + $lastMonthStart = new \DateTimeImmutable($lastDay->format('Y-m-01 00:00:00')); + + while ($current <= $lastMonthStart) { + $months[] = $this->buildCalendarMonth($current); + $current = $current->modify('first day of next month'); + } + + return $months; + } + + /** + * @return array + */ + private function buildCalendarMonth(\DateTimeImmutable $monthStart): array + { + $firstOfMonth = $monthStart->setTime(0, 0, 0); + $lastOfMonth = $firstOfMonth->modify('last day of this month'); + $gridStart = $firstOfMonth->modify('monday this week'); + $gridEnd = $lastOfMonth->modify('sunday this week'); + + $weeks = []; + $week = []; + $current = $gridStart; + + while ($current <= $gridEnd) { + $week[] = [ + 'date' => $current, + 'inMonth' => $current->format('Y-m') === $firstOfMonth->format('Y-m'), + ]; + + if (count($week) === 7) { + $weeks[] = $week; + $week = []; + } + + $current = $current->add(new \DateInterval('P1D')); + } + + return [ + 'label' => $firstOfMonth->format('F Y'), + 'weeks' => $weeks, + ]; + } + } diff --git a/public/typo3conf/ext/ep_products/Classes/Utility/DateUtility.php b/public/typo3conf/ext/ep_products/Classes/Utility/DateUtility.php index 12286fb1..033aaa4c 100644 --- a/public/typo3conf/ext/ep_products/Classes/Utility/DateUtility.php +++ b/public/typo3conf/ext/ep_products/Classes/Utility/DateUtility.php @@ -2,6 +2,8 @@ namespace EP\EpProducts\Utility; +use League\Period\Period; + /*************************************************************** * * Copyright notice @@ -66,13 +68,15 @@ class DateUtility $fromMonth = $currentMonth; } - $begin = new \DateTime(); - $begin->setDate($fromYear, $fromMonth, 1); - $begin->setTime(0, 0, 0); - $end = clone $begin; - $end->add(new \DateInterval('P' . $months . 'M')); - $end->setTime(23, 59, 59); + $begin = new \DateTimeImmutable(); + $begin = $begin->setDate((int) $fromYear, (int) $fromMonth, 1)->setTime(0, 0, 0); + $end = $begin->add(new \DateInterval('P' . $months . 'M')); + $period = new Period($begin, $end); - return [ $begin, $end ]; + return [ + 'period' => $period, + 'dateFrom' => $period->getStartDate()->format('Y-m-d'), + 'dateTo' => $period->getEndDate()->sub(new \DateInterval('P1D'))->format('Y-m-d'), + ]; } } diff --git a/public/typo3conf/ext/ep_products/ext_conf_template.txt b/public/typo3conf/ext/ep_products/ext_conf_template.txt index 243ddffe..051caf9d 100644 --- a/public/typo3conf/ext/ep_products/ext_conf_template.txt +++ b/public/typo3conf/ext/ep_products/ext_conf_template.txt @@ -22,3 +22,8 @@ myEpApiUrlAuthorize = # cat=MyEpAPI; type=string; label=MyE&P API token URL myEpApiUrlToken = +# cat=MyEpAPI; type=string; label=Bpn Connect API base URL +bpnConnectApiBaseUrl = + +# cat=MyEpAPI; type=string; label=Bpn Connect API key +bpnConnectApiKey = diff --git a/public/typo3conf/ext/ep_theme/Classes/ViewHelpers/Calendar/ContingentViewHelper.php b/public/typo3conf/ext/ep_theme/Classes/ViewHelpers/Calendar/ContingentViewHelper.php index bdf910df..bf3a38ec 100644 --- a/public/typo3conf/ext/ep_theme/Classes/ViewHelpers/Calendar/ContingentViewHelper.php +++ b/public/typo3conf/ext/ep_theme/Classes/ViewHelpers/Calendar/ContingentViewHelper.php @@ -27,10 +27,7 @@ namespace EP\EpTheme\ViewHelpers\Calendar; * This copyright notice MUST APPEAR in all copies of the script! ***************************************************************/ -use CalendR\Event\Collection\Basic; -use CalendR\Period\Day; -use CalendR\Period\Month; -use EP\EpProducts\Domain\Model\Contingent; +use DateInterval; use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface; use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper; use TYPO3Fluid\Fluid\Core\ViewHelper\Traits\CompileWithRenderStatic; @@ -44,17 +41,16 @@ class ContingentViewHelper extends AbstractViewHelper */ protected $escapeOutput = false; - const LEVEL_GREEN = 1; - const LEVEL_YELLOW = 2; - const LEVEL_RED = 3; + private const LEVEL_GREEN = 1; + private const LEVEL_YELLOW = 2; + private const LEVEL_RED = 3; public function initializeArguments() { parent::initializeArguments(); - $this->registerArgument('month', Month::class, 'The month', true); - $this->registerArgument('day', Day::class, 'The day', true); - $this->registerArgument('events', Basic::class, 'The calendar events', true); + $this->registerArgument('day', \DateTimeInterface::class, 'The day', true); + $this->registerArgument('calendar', 'array', 'The calendar payload', true); } /** @@ -69,105 +65,129 @@ class ContingentViewHelper extends AbstractViewHelper RenderingContextInterface $renderingContext ) { - $month = $arguments['month']; $day = $arguments['day']; - $events = $arguments['events']; + $calendar = $arguments['calendar']; + $now = new \DateTimeImmutable('now'); + $dayDate = new \DateTimeImmutable($day->format('Y-m-d H:i:s'), $day->getTimezone()); + $dayDate = $dayDate->setTime(0, 0, 0); - if (!$month->includes($day)) { - return ''; - } - $now = new \DateTime('now'); - if ($now > $day->getBegin()) { + if (!empty($calendar['error'])) { $class = [ 'text-zinc-400' ]; - return static::getHtml($class, $day->format('d')); + return static::getHtml($class, $dayDate->format('d')); } - $contingents = $events->find($day); - if (count($contingents) === 0) { - $percentage = 100; - $level = static::LEVEL_GREEN; - } else { - $contingent = reset($contingents); - $percentage = $contingent->getPercentageAvailable(); - $status = $contingent->getStatus(); - $level = static::getOccupancyLevel($percentage, $status); - } - $previousDay = $day->getPrevious(); - $previousContingents = $events->find($previousDay); - if (count($previousContingents) === 0) { - $previousLevel = static::LEVEL_GREEN; - } else { - $previousContingent = reset($previousContingents); - $previousPercentage = $previousContingent->getPercentageAvailable(); - $previousStatus = $previousContingent->getStatus(); - $previousLevel = static::getOccupancyLevel($previousPercentage, $previousStatus); + + if ($now > $dayDate) { + $class = [ 'text-zinc-400' ]; + return static::getHtml($class, $dayDate->format('d')); } + $statusMap = static::getStatusMap($calendar); + $status = static::normalizeStatus($statusMap[$dayDate->format('Y-m-d')] ?? 'OK'); + $previousStatus = static::normalizeStatus($statusMap[$dayDate->sub(new DateInterval('P1D'))->format('Y-m-d')] ?? 'OK'); + $level = static::getOccupancyLevel($status); + $previousLevel = static::getOccupancyLevel($previousStatus); if ($previousLevel !== $level) { - $class = static::getTransientLabelClasses($previousLevel, $level); + $class = static::getTransientLabelClasses($previousStatus, $status); } else { - $class = static::getLabelClasses($percentage, $level); + $class = static::getLabelClasses($status); } - return static::getHtml($class, $day->format('d')); + return static::getHtml($class, $dayDate->format('d')); } /** - * @param int $percentage - * @param int $level - * * @return array */ - protected static function getLabelClasses($percentage, $level) + protected static function getLabelClasses(string $status): array { - $classes = static::getTransientLabelClasses($level); - $classes[] = 'available-' . $percentage; - return $classes; + return [ + 'block p-1 leading-none text-sm text-white', + static::getStatusClass($status), + ]; } /** - * @param int $levelFrom - * @param int|null $levelTo - * * @return array */ - protected static function getTransientLabelClasses($levelFrom, $levelTo = null) + protected static function getTransientLabelClasses(string $statusFrom, ?string $statusTo = null): array { $classes = [ 'block p-1 leading-none text-sm text-white' ]; - if ($levelFrom === static::LEVEL_RED) { - $class = 'availability-none'; - } elseif ($levelFrom === static::LEVEL_YELLOW) { - $class = 'availability-partial'; - } else { - $class = 'availability-full'; - } - if ($levelTo === static::LEVEL_RED) { - $class .= '-to-none'; - } elseif ($levelTo === static::LEVEL_YELLOW) { - $class .= '-to-partial'; - } elseif ($levelTo !== null) { - $class .= '-to-full'; + $class = static::getStatusClass($statusFrom); + if ($statusTo !== null) { + $class .= '-to-' . static::getStatusSuffix($statusTo); } $classes[] = $class; return $classes; } /** - * @param int $percentage - * @param int $status - * * @return int */ - protected static function getOccupancyLevel($percentage, $status) + protected static function getOccupancyLevel(string $status): int { - if ($percentage <= 20 || $status === Contingent::STATUS_BLOCKED) { + if ($status === 'BLOCKED') { return static::LEVEL_RED; } - if ($percentage <= 80 || $status === Contingent::STATUS_ONREQUEST) { + if ($status === 'ON_REQUEST') { return static::LEVEL_YELLOW; } return static::LEVEL_GREEN; } + /** + * @return array + */ + protected static function getStatusMap(array $calendar): array + { + $statusMap = []; + foreach (($calendar['data'] ?? []) as $row) { + if (!isset($row['date'])) { + continue; + } + $statusMap[$row['date']] = static::normalizeStatus((string)($row['status'] ?? 'OK')); + } + + return $statusMap; + } + + protected static function normalizeStatus(string $status): string + { + $status = strtoupper(trim($status)); + if ($status === 'ONREQUEST') { + return 'ON_REQUEST'; + } + + if (in_array($status, [ 'OK', 'ON_REQUEST', 'BLOCKED' ], true)) { + return $status; + } + + return 'OK'; + } + + protected static function getStatusClass(string $status): string + { + if ($status === 'BLOCKED') { + return 'availability-none'; + } + if ($status === 'ON_REQUEST') { + return 'availability-partial'; + } + + return 'availability-full'; + } + + protected static function getStatusSuffix(string $status): string + { + if ($status === 'BLOCKED') { + return 'none'; + } + if ($status === 'ON_REQUEST') { + return 'partial'; + } + + return 'full'; + } + /** * @param array $class * @param string $label diff --git a/public/typo3conf/ext/ep_theme/Classes/ViewHelpers/Calendar/IncludesViewHelper.php b/public/typo3conf/ext/ep_theme/Classes/ViewHelpers/Calendar/IncludesViewHelper.php deleted file mode 100644 index 961bb0ec..00000000 --- a/public/typo3conf/ext/ep_theme/Classes/ViewHelpers/Calendar/IncludesViewHelper.php +++ /dev/null @@ -1,54 +0,0 @@ -, dreipunktnull - * - * All rights reserved - * - * This script is part of the TYPO3 project. The TYPO3 project is - * free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 3 of the License, or - * (at your option) any later version. - * - * The GNU General Public License can be found at - * http://www.gnu.org/copyleft/gpl.html. - * - * This script is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * This copyright notice MUST APPEAR in all copies of the script! - ***************************************************************/ - -use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractConditionViewHelper; - -class IncludesViewHelper extends AbstractConditionViewHelper -{ - public function initializeArguments() - { - parent::initializeArguments(); - - $this->registerArgument('range', 'object', 'Range to check against', true); - $this->registerArgument('period', 'object', 'Period to check if it is included in range', true); - } - - /** - * @param array $arguments - * @return bool - */ - protected static function evaluateCondition($arguments = null) - { - /** @var \CalendR\Period\Range $range */ - $range = $arguments['range']; - /** @var \CalendR\Period\PeriodInterface $period */ - $period = $arguments['period']; - return $range->includes($period); - } -} diff --git a/public/typo3conf/ext/ep_theme/Resources/Private/Templates/AjaxCalendar/Range.html b/public/typo3conf/ext/ep_theme/Resources/Private/Templates/AjaxCalendar/Range.html index ece952c7..40151515 100644 --- a/public/typo3conf/ext/ep_theme/Resources/Private/Templates/AjaxCalendar/Range.html +++ b/public/typo3conf/ext/ep_theme/Resources/Private/Templates/AjaxCalendar/Range.html @@ -6,21 +6,17 @@
- - - + +
+ Der Kalender konnte aktuell nicht geladen werden. +
+ + +
- - - - - - - -
@@ -34,17 +30,27 @@ - +
- + + + + + +   + +
+
+ {month.label} +