diff --git a/src/Bundle/ChillCalendarBundle/Entity/Calendar.php b/src/Bundle/ChillCalendarBundle/Entity/Calendar.php index 17dbbe7c5..f5d73e34c 100644 --- a/src/Bundle/ChillCalendarBundle/Entity/Calendar.php +++ b/src/Bundle/ChillCalendarBundle/Entity/Calendar.php @@ -413,6 +413,11 @@ class Calendar implements TrackCreationInterface, TrackUpdateInterface return null !== $this->calendarRange; } + public function hasLocation(): bool + { + return null !== $this->getLocation(); + } + /** * return true if the user is invited. */ diff --git a/src/Bundle/ChillCalendarBundle/Messenger/Handler/CalendarRangeRemoveToRemoteHandler.php b/src/Bundle/ChillCalendarBundle/Messenger/Handler/CalendarRangeRemoveToRemoteHandler.php index 0181195ed..8f8b724a5 100644 --- a/src/Bundle/ChillCalendarBundle/Messenger/Handler/CalendarRangeRemoveToRemoteHandler.php +++ b/src/Bundle/ChillCalendarBundle/Messenger/Handler/CalendarRangeRemoveToRemoteHandler.php @@ -18,6 +18,8 @@ use Symfony\Component\Messenger\Attribute\AsMessageHandler; use Symfony\Component\Messenger\Handler\MessageHandlerInterface; /** + * Remove a calendar range when it is removed from local calendar. + * * @AsMessageHandler */ class CalendarRangeRemoveToRemoteHandler implements MessageHandlerInterface diff --git a/src/Bundle/ChillCalendarBundle/Messenger/Handler/CalendarRangeToRemoteHandler.php b/src/Bundle/ChillCalendarBundle/Messenger/Handler/CalendarRangeToRemoteHandler.php index 97e666b60..60eb2ae23 100644 --- a/src/Bundle/ChillCalendarBundle/Messenger/Handler/CalendarRangeToRemoteHandler.php +++ b/src/Bundle/ChillCalendarBundle/Messenger/Handler/CalendarRangeToRemoteHandler.php @@ -19,6 +19,8 @@ use Symfony\Component\Messenger\Attribute\AsMessageHandler; use Symfony\Component\Messenger\Handler\MessageHandlerInterface; /** + * Write calendar range creation / update to the remote calendar. + * * @AsMessageHandler */ class CalendarRangeToRemoteHandler implements MessageHandlerInterface @@ -43,6 +45,10 @@ class CalendarRangeToRemoteHandler implements MessageHandlerInterface { $range = $this->calendarRangeRepository->find($calendarRangeMessage->getCalendarRangeId()); + if (null === $range) { + return; + } + $this->remoteCalendarConnector->syncCalendarRange($range); $range->preventEnqueueChanges = true; diff --git a/src/Bundle/ChillCalendarBundle/Messenger/Handler/CalendarToRemoteHandler.php b/src/Bundle/ChillCalendarBundle/Messenger/Handler/CalendarToRemoteHandler.php index 8780c3d3a..e0a4c30ee 100644 --- a/src/Bundle/ChillCalendarBundle/Messenger/Handler/CalendarToRemoteHandler.php +++ b/src/Bundle/ChillCalendarBundle/Messenger/Handler/CalendarToRemoteHandler.php @@ -24,6 +24,8 @@ use Symfony\Component\Messenger\Attribute\AsMessageHandler; use Symfony\Component\Messenger\Handler\MessageHandlerInterface; /** + * Write calendar creation / update to the remote calendar. + * * @AsMessageHandler */ class CalendarToRemoteHandler implements MessageHandlerInterface @@ -60,6 +62,10 @@ class CalendarToRemoteHandler implements MessageHandlerInterface { $calendar = $this->calendarRepository->find($calendarMessage->getCalendarId()); + if (null === $calendar) { + return; + } + if (null !== $calendarMessage->getPreviousCalendarRangeId()) { $previousCalendarRange = $this->calendarRangeRepository ->find($calendarMessage->getPreviousCalendarRangeId()); diff --git a/src/Bundle/ChillCalendarBundle/Messenger/Handler/InviteUpdateHandler.php b/src/Bundle/ChillCalendarBundle/Messenger/Handler/InviteUpdateHandler.php index ece0b6929..30888a05b 100644 --- a/src/Bundle/ChillCalendarBundle/Messenger/Handler/InviteUpdateHandler.php +++ b/src/Bundle/ChillCalendarBundle/Messenger/Handler/InviteUpdateHandler.php @@ -18,6 +18,8 @@ use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\Messenger\Handler\MessageHandlerInterface; /** + * Sync the local invitation to the remote calendar. + * * @AsMessageHandler */ class InviteUpdateHandler implements MessageHandlerInterface diff --git a/src/Bundle/ChillCalendarBundle/Messenger/Handler/MSGraphChangeNotificationHandler.php b/src/Bundle/ChillCalendarBundle/Messenger/Handler/MSGraphChangeNotificationHandler.php index bcc0a5bc9..744b1a301 100644 --- a/src/Bundle/ChillCalendarBundle/Messenger/Handler/MSGraphChangeNotificationHandler.php +++ b/src/Bundle/ChillCalendarBundle/Messenger/Handler/MSGraphChangeNotificationHandler.php @@ -23,7 +23,7 @@ use Psr\Log\LoggerInterface; use Symfony\Component\Messenger\Handler\MessageHandlerInterface; /** - * Handle notification of chagnes from MSGraph. + * Handle notification of changes made by users directly on Outlook calendar. * * @AsMessageHandler */ diff --git a/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/AddressConverter.php b/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/AddressConverter.php new file mode 100644 index 000000000..3d283ed9d --- /dev/null +++ b/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/AddressConverter.php @@ -0,0 +1,41 @@ +addressRender = $addressRender; + $this->translatableStringHelper = $translatableStringHelper; + } + + public function addressToRemote(Address $address): array + { + return [ + 'city' => $address->getPostcode()->getName(), + 'postalCode' => $address->getPostcode()->getCode(), + 'countryOrRegion' => $this->translatableStringHelper->localize($address->getPostcode()->getCountry()->getName()), + 'street' => $address->isNoAddress() ? '' : + implode(', ', $this->addressRender->renderLines($address, false, false)), + 'state' => '', + ]; + } +} diff --git a/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/LocationConverter.php b/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/LocationConverter.php new file mode 100644 index 000000000..bf34ff8bb --- /dev/null +++ b/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/LocationConverter.php @@ -0,0 +1,46 @@ +addressConverter = $addressConverter; + } + + public function locationToRemote(Location $location): array + { + $results = []; + + if ($location->hasAddress()) { + $results['address'] = $this->addressConverter->addressToRemote($location->getAddress()); + + if ($location->getAddress()->hasAddressReference() && $location->getAddress()->getAddressReference()->hasPoint()) { + $results['coordinates'] = [ + 'latitude' => $location->getAddress()->getAddressReference()->getPoint()->getLat(), + 'longitude' => $location->getAddress()->getAddressReference()->getPoint()->getLon(), + ]; + } + } + + if (null !== $location->getName()) { + $results['displayName'] = $location->getName(); + } + + return $results; + } +} diff --git a/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/RemoteEventConverter.php b/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/RemoteEventConverter.php index 63c768ff7..85b77a4e9 100644 --- a/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/RemoteEventConverter.php +++ b/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/RemoteEventConverter.php @@ -19,6 +19,7 @@ use Chill\PersonBundle\Entity\Person; use Chill\PersonBundle\Templating\Entity\PersonRenderInterface; use DateTimeImmutable; use DateTimeZone; +use Psr\Log\LoggerInterface; use RuntimeException; use Symfony\Component\Templating\EngineInterface; use Symfony\Contracts\Translation\TranslatorInterface; @@ -35,6 +36,11 @@ class RemoteEventConverter */ public const REMOTE_DATETIMEZONE_FORMAT = 'Y-m-d\\TH:i:s.u?P'; + /** + * Same as above, but sometimes the date is expressed with only 6 milliseconds. + */ + public const REMOTE_DATETIMEZONE_FORMAT_ALT = 'Y-m-d\\TH:i:s.uP'; + private const REMOTE_DATE_FORMAT = 'Y-m-d\TH:i:s.u0'; private const REMOTE_DATETIME_WITHOUT_TZ_FORMAT = 'Y-m-d\TH:i:s.u?'; @@ -43,15 +49,26 @@ class RemoteEventConverter private EngineInterface $engine; + private LocationConverter $locationConverter; + + private LoggerInterface $logger; + private PersonRenderInterface $personRender; private DateTimeZone $remoteDateTimeZone; private TranslatorInterface $translator; - public function __construct(EngineInterface $engine, PersonRenderInterface $personRender, TranslatorInterface $translator) - { + public function __construct( + EngineInterface $engine, + LocationConverter $locationConverter, + LoggerInterface $logger, + PersonRenderInterface $personRender, + TranslatorInterface $translator + ) { $this->engine = $engine; + $this->locationConverter = $locationConverter; + $this->logger = $logger; $this->translator = $translator; $this->personRender = $personRender; $this->defaultDateTimeZone = (new DateTimeImmutable())->getTimezone(); @@ -86,12 +103,13 @@ class RemoteEventConverter ], ], 'isReminderOn' => false, + 'location' => $this->locationConverter->locationToRemote($calendarRange->getLocation()), ]; } public function calendarToEvent(Calendar $calendar): array { - return array_merge( + $result = array_merge( [ 'subject' => '[Chill] ' . implode( @@ -120,9 +138,16 @@ class RemoteEventConverter ), ], 'responseRequested' => true, + 'isReminderOn' => false, ], $this->calendarToEventAttendeesOnly($calendar) ); + + if ($calendar->hasLocation()) { + $result['location'] = $this->locationConverter->locationToRemote($calendar->getLocation()); + } + + return $result; } public function calendarToEventAttendeesOnly(Calendar $calendar): array @@ -203,7 +228,27 @@ class RemoteEventConverter public function getLastModifiedDate(array $event): DateTimeImmutable { - return DateTimeImmutable::createFromFormat(self::REMOTE_DATETIMEZONE_FORMAT, $event['lastModifiedDateTime']); + $date = DateTimeImmutable::createFromFormat(self::REMOTE_DATETIMEZONE_FORMAT, $event['lastModifiedDateTime']); + + if (false === $date) { + $date = DateTimeImmutable::createFromFormat(self::REMOTE_DATETIMEZONE_FORMAT_ALT, $event['lastModifiedDateTime']); + } + + if (false === $date) { + $this->logger->error(self::class . ' Could not convert lastModifiedDate', [ + 'actual' => $event['lastModifiedDateTime'], + 'format' => self::REMOTE_DATETIMEZONE_FORMAT, + 'format_alt' => self::REMOTE_DATETIMEZONE_FORMAT_ALT, + ]); + + throw new RuntimeException(sprintf( + 'could not convert lastModifiedDate: %s, expected format: %s', + $event['lastModifiedDateTime'], + self::REMOTE_DATETIMEZONE_FORMAT . ' and ' . self::REMOTE_DATETIMEZONE_FORMAT_ALT + )); + } + + return $date; } /** diff --git a/src/Bundle/ChillCalendarBundle/RemoteCalendar/Model/RemoteEvent.php b/src/Bundle/ChillCalendarBundle/RemoteCalendar/Model/RemoteEvent.php index 5d2cdc1de..b96fa0a3b 100644 --- a/src/Bundle/ChillCalendarBundle/RemoteCalendar/Model/RemoteEvent.php +++ b/src/Bundle/ChillCalendarBundle/RemoteCalendar/Model/RemoteEvent.php @@ -28,6 +28,11 @@ class RemoteEvent */ public string $id; + /** + * @Serializer\Groups({"read"}) + */ + public bool $isAllDay; + /** * @Serializer\Groups({"read"}) */ @@ -38,11 +43,6 @@ class RemoteEvent */ public string $title; - /** - * @Serializer\Groups({"read"}) - */ - public bool $isAllDay; - public function __construct(string $id, string $title, string $description, DateTimeImmutable $startDate, DateTimeImmutable $endDate, bool $isAllDay = false) { $this->id = $id; diff --git a/src/Bundle/ChillCalendarBundle/Resources/public/vuejs/Calendar/store/actions.js b/src/Bundle/ChillCalendarBundle/Resources/public/vuejs/Calendar/store/actions.js index 1c3284325..fc3d6547a 100644 --- a/src/Bundle/ChillCalendarBundle/Resources/public/vuejs/Calendar/store/actions.js +++ b/src/Bundle/ChillCalendarBundle/Resources/public/vuejs/Calendar/store/actions.js @@ -157,15 +157,16 @@ export default { endDateInput.value = null !== range ? datetimeToISO(range.end) : ""; let calendarRangeInput = document.getElementById("chill_activitybundle_activity_calendarRange"); calendarRangeInput.value = null !== range ? Number(range.extendedProps.calendarRangeId) : ""; - let location = getters.getLocationById(range.extendedProps.locationId); - - if (null === location) { - console.error("location not found!", range.extendedProps.locationId); - } - - dispatch('updateLocation', location); if (null !== range) { + let location = getters.getLocationById(range.extendedProps.locationId); + + if (null === location) { + console.error("location not found!", range.extendedProps.locationId); + } + + dispatch('updateLocation', location); + const userId = range.extendedProps.userId; if (state.activity.mainUser !== null && state.activity.mainUser.id !== userId) { dispatch('setMainUser', state.usersData.get(userId).user); diff --git a/src/Bundle/ChillCalendarBundle/Resources/public/vuejs/MyCalendarRange/Components/EditLocation.vue b/src/Bundle/ChillCalendarBundle/Resources/public/vuejs/MyCalendarRange/Components/EditLocation.vue index 87c3529c0..ae184f6b4 100644 --- a/src/Bundle/ChillCalendarBundle/Resources/public/vuejs/MyCalendarRange/Components/EditLocation.vue +++ b/src/Bundle/ChillCalendarBundle/Resources/public/vuejs/MyCalendarRange/Components/EditLocation.vue @@ -1,7 +1,7 @@ diff --git a/src/Bundle/ChillCalendarBundle/Tests/RemoteCalendar/Connector/MSGraph/AddressConverterTest.php b/src/Bundle/ChillCalendarBundle/Tests/RemoteCalendar/Connector/MSGraph/AddressConverterTest.php new file mode 100644 index 000000000..d4fcbf46c --- /dev/null +++ b/src/Bundle/ChillCalendarBundle/Tests/RemoteCalendar/Connector/MSGraph/AddressConverterTest.php @@ -0,0 +1,67 @@ +setName(['fr' => 'Belgique']); + $postalCode = (new PostalCode())->setName('Houte-Si-Plout')->setCode('4122') + ->setCountry($country); + $address = (new Address())->setPostcode($postalCode)->setStreet("Rue de l'Église") + ->setStreetNumber('15B')->setBuildingName('Résidence de la Truite'); + + $actual = $this->buildAddressConverter()->addressToRemote($address); + + $this->assertArrayHasKey('city', $actual); + $this->assertStringContainsString($actual['city'], 'Houte-Si-Plout'); + $this->assertArrayHasKey('postalCode', $actual); + $this->assertStringContainsString($actual['postalCode'], '4122'); + $this->assertArrayHasKey('countryOrRegion', $actual); + $this->assertStringContainsString('Belgique', $actual['countryOrRegion']); + $this->assertArrayHasKey('street', $actual); + $this->assertStringContainsString('Rue de l\'Église', $actual['street']); + $this->assertStringContainsString('15B', $actual['street']); + $this->assertStringContainsString('Résidence de la Truite', $actual['street']); + } + + private function buildAddressConverter(): AddressConverter + { + $engine = $this->prophesize(EngineInterface::class); + $translatableStringHelper = $this->prophesize(TranslatableStringHelperInterface::class); + $translatableStringHelper->localize(Argument::type('array'))->will(static function ($args): string { + return ($args[0] ?? ['fr' => 'not provided'])['fr'] ?? 'not provided'; + }); + + $addressRender = new AddressRender($engine->reveal(), $translatableStringHelper->reveal()); + + return new AddressConverter($addressRender, $translatableStringHelper->reveal()); + } +} diff --git a/src/Bundle/ChillCalendarBundle/Tests/RemoteCalendar/Connector/MSGraph/LocationConverterTest.php b/src/Bundle/ChillCalendarBundle/Tests/RemoteCalendar/Connector/MSGraph/LocationConverterTest.php new file mode 100644 index 000000000..3ea81afdc --- /dev/null +++ b/src/Bundle/ChillCalendarBundle/Tests/RemoteCalendar/Connector/MSGraph/LocationConverterTest.php @@ -0,0 +1,79 @@ +setName('display')->setAddress($address = new Address()); + $address->setAddressReference($reference = new AddressReference()); + + $actual = $this->buildLocationConverter()->locationToRemote($location); + + $this->assertArrayHasKey('address', $actual); + $this->assertArrayNotHasKey('coordinates', $actual); + $this->assertArrayHasKey('displayName', $actual); + $this->assertEquals('display', $actual['displayName']); + } + + public function testConvertToRemoteWithAddressWithPoint(): void + { + $location = (new Location())->setName('display')->setAddress($address = new Address()); + $address->setAddressReference($reference = new AddressReference()); + $reference->setPoint($point = Point::fromLonLat(5.3134, 50.3134)); + + $actual = $this->buildLocationConverter()->locationToRemote($location); + + $this->assertArrayHasKey('address', $actual); + $this->assertArrayHasKey('coordinates', $actual); + $this->assertEquals(['latitude' => 50.3134, 'longitude' => 5.3134], $actual['coordinates']); + $this->assertArrayHasKey('displayName', $actual); + $this->assertEquals('display', $actual['displayName']); + } + + public function testConvertToRemoteWithoutAddressWithoutPoint(): void + { + $location = (new Location())->setName('display'); + + $actual = $this->buildLocationConverter()->locationToRemote($location); + + $this->assertArrayNotHasKey('address', $actual); + $this->assertArrayNotHasKey('coordinates', $actual); + $this->assertArrayHasKey('displayName', $actual); + $this->assertEquals('display', $actual['displayName']); + } + + private function buildLocationConverter(): LocationConverter + { + $addressConverter = $this->prophesize(AddressConverter::class); + $addressConverter->addressToRemote(Argument::type(Address::class))->willReturn(['street' => 'dummy']); + + return new LocationConverter($addressConverter->reveal()); + } +} diff --git a/src/Bundle/ChillMainBundle/Entity/Address.php b/src/Bundle/ChillMainBundle/Entity/Address.php index d3eee0b2a..d088a567d 100644 --- a/src/Bundle/ChillMainBundle/Entity/Address.php +++ b/src/Bundle/ChillMainBundle/Entity/Address.php @@ -359,6 +359,11 @@ class Address return $this->validTo; } + public function hasAddressReference(): bool + { + return null !== $this->getAddressReference(); + } + public function isNoAddress(): bool { return $this->getIsNoAddress(); diff --git a/src/Bundle/ChillMainBundle/Entity/AddressReference.php b/src/Bundle/ChillMainBundle/Entity/AddressReference.php index fc4339fe0..5d581efd4 100644 --- a/src/Bundle/ChillMainBundle/Entity/AddressReference.php +++ b/src/Bundle/ChillMainBundle/Entity/AddressReference.php @@ -168,6 +168,11 @@ class AddressReference return $this->updatedAt; } + public function hasPoint(): bool + { + return null !== $this->getPoint(); + } + public function setCreatedAt(?DateTimeImmutable $createdAt): self { $this->createdAt = $createdAt; diff --git a/src/Bundle/ChillMainBundle/Entity/Location.php b/src/Bundle/ChillMainBundle/Entity/Location.php index c1421586f..09f2f2140 100644 --- a/src/Bundle/ChillMainBundle/Entity/Location.php +++ b/src/Bundle/ChillMainBundle/Entity/Location.php @@ -180,6 +180,11 @@ class Location implements TrackCreationInterface, TrackUpdateInterface return $this->updatedBy; } + public function hasAddress(): bool + { + return null !== $this->getAddress(); + } + public function setActive(bool $active): self { $this->active = $active; diff --git a/src/Bundle/ChillMainBundle/Templating/Entity/AddressRender.php b/src/Bundle/ChillMainBundle/Templating/Entity/AddressRender.php index 3a97d0ef6..20f4adc8b 100644 --- a/src/Bundle/ChillMainBundle/Templating/Entity/AddressRender.php +++ b/src/Bundle/ChillMainBundle/Templating/Entity/AddressRender.php @@ -12,7 +12,7 @@ declare(strict_types=1); namespace Chill\MainBundle\Templating\Entity; use Chill\MainBundle\Entity\Address; -use Chill\MainBundle\Templating\TranslatableStringHelper; +use Chill\MainBundle\Templating\TranslatableStringHelperInterface; use Symfony\Component\Templating\EngineInterface; use function array_merge; @@ -33,9 +33,9 @@ class AddressRender implements ChillEntityRenderInterface private EngineInterface $templating; - private TranslatableStringHelper $translatableStringHelper; + private TranslatableStringHelperInterface $translatableStringHelper; - public function __construct(EngineInterface $templating, TranslatableStringHelper $translatableStringHelper) + public function __construct(EngineInterface $templating, TranslatableStringHelperInterface $translatableStringHelper) { $this->templating = $templating; $this->translatableStringHelper = $translatableStringHelper; @@ -65,7 +65,7 @@ class AddressRender implements ChillEntityRenderInterface * * @return string[] */ - public function renderLines($addr): array + public function renderLines(Address $addr, bool $includeCityLine = true, bool $includeCountry = true): array { $lines = []; @@ -75,14 +75,26 @@ class AddressRender implements ChillEntityRenderInterface $lines[] = $this->renderBuildingLine($addr); $lines[] = $this->renderStreetLine($addr); $lines[] = $this->renderDeliveryLine($addr); - $lines[] = $this->renderCityLine($addr); - $lines[] = $this->renderCountryLine($addr); + + if ($includeCityLine) { + $lines[] = $this->renderCityLine($addr); + } + + if ($includeCountry) { + $lines[] = $this->renderCountryLine($addr); + } } else { $lines[] = $this->renderBuildingLine($addr); $lines[] = $this->renderDeliveryLine($addr); $lines[] = $this->renderStreetLine($addr); - $lines[] = $this->renderCityLine($addr); - $lines[] = $this->renderCountryLine($addr); + + if ($includeCityLine) { + $lines[] = $this->renderCityLine($addr); + } + + if ($includeCountry) { + $lines[] = $this->renderCountryLine($addr); + } } }