mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-09-27 00:55:01 +00:00
Merge branch 'calendar/finalization' into chill_amli
This commit is contained in:
@@ -21,6 +21,7 @@ use Chill\ActivityBundle\Repository\ActivityTypeRepository;
|
||||
use Chill\ActivityBundle\Security\Authorization\ActivityVoter;
|
||||
use Chill\MainBundle\Entity\Embeddable\CommentEmbeddable;
|
||||
use Chill\MainBundle\Repository\LocationRepository;
|
||||
use Chill\MainBundle\Repository\UserRepositoryInterface;
|
||||
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Chill\PersonBundle\Privacy\PrivacyEvent;
|
||||
@@ -70,6 +71,8 @@ final class ActivityController extends AbstractController
|
||||
|
||||
private ThirdPartyRepository $thirdPartyRepository;
|
||||
|
||||
private UserRepositoryInterface $userRepository;
|
||||
|
||||
public function __construct(
|
||||
ActivityACLAwareRepositoryInterface $activityACLAwareRepository,
|
||||
ActivityTypeRepository $activityTypeRepository,
|
||||
@@ -82,7 +85,8 @@ final class ActivityController extends AbstractController
|
||||
EntityManagerInterface $entityManager,
|
||||
EventDispatcherInterface $eventDispatcher,
|
||||
LoggerInterface $logger,
|
||||
SerializerInterface $serializer
|
||||
SerializerInterface $serializer,
|
||||
UserRepositoryInterface $userRepository
|
||||
) {
|
||||
$this->activityACLAwareRepository = $activityACLAwareRepository;
|
||||
$this->activityTypeRepository = $activityTypeRepository;
|
||||
@@ -96,6 +100,7 @@ final class ActivityController extends AbstractController
|
||||
$this->eventDispatcher = $eventDispatcher;
|
||||
$this->logger = $logger;
|
||||
$this->serializer = $serializer;
|
||||
$this->userRepository = $userRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -366,7 +371,7 @@ final class ActivityController extends AbstractController
|
||||
if ($request->query->has('activityData')) {
|
||||
$activityData = $request->query->get('activityData');
|
||||
|
||||
if (array_key_exists('durationTime', $activityData)) {
|
||||
if (array_key_exists('durationTime', $activityData) && $activityType->getDurationTimeVisible() > 0) {
|
||||
$durationTimeInMinutes = $activityData['durationTime'];
|
||||
$hours = floor($durationTimeInMinutes / 60);
|
||||
$minutes = $durationTimeInMinutes % 60;
|
||||
@@ -385,26 +390,36 @@ final class ActivityController extends AbstractController
|
||||
}
|
||||
}
|
||||
|
||||
if (array_key_exists('personsId', $activityData)) {
|
||||
if (array_key_exists('personsId', $activityData) && $activityType->getPersonsVisible() > 0) {
|
||||
foreach ($activityData['personsId'] as $personId) {
|
||||
$concernedPerson = $this->personRepository->find($personId);
|
||||
$entity->addPerson($concernedPerson);
|
||||
}
|
||||
}
|
||||
|
||||
if (array_key_exists('professionalsId', $activityData)) {
|
||||
if (array_key_exists('professionalsId', $activityData) && $activityType->getThirdPartiesVisible() > 0) {
|
||||
foreach ($activityData['professionalsId'] as $professionalsId) {
|
||||
$professional = $this->thirdPartyRepository->find($professionalsId);
|
||||
$entity->addThirdParty($professional);
|
||||
}
|
||||
}
|
||||
|
||||
if (array_key_exists('location', $activityData)) {
|
||||
if (array_key_exists('usersId', $activityData) && $activityType->getUsersVisible() > 0) {
|
||||
foreach ($activityData['usersId'] as $userId) {
|
||||
$user = $this->userRepository->find($userId);
|
||||
|
||||
if (null !== $user) {
|
||||
$entity->addUser($user);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (array_key_exists('location', $activityData) && $activityType->getLocationVisible() > 0) {
|
||||
$location = $this->locationRepository->find($activityData['location']);
|
||||
$entity->setLocation($location);
|
||||
}
|
||||
|
||||
if (array_key_exists('comment', $activityData)) {
|
||||
if (array_key_exists('comment', $activityData) && $activityType->getCommentVisible() > 0) {
|
||||
$comment = new CommentEmbeddable();
|
||||
$comment->setComment($activityData['comment']);
|
||||
$comment->setUserId($this->getUser()->getid());
|
||||
|
@@ -17,7 +17,7 @@ const getLocations = () => fetchResults('/api/1.0/main/location.json');
|
||||
|
||||
const getLocationTypes = () => fetchResults('/api/1.0/main/location-type.json');
|
||||
|
||||
const getUserCurrentLocation =
|
||||
const getUserCurrentLocation =
|
||||
() => fetch('/api/1.0/main/user-current-location.json')
|
||||
.then(response => {
|
||||
if (response.ok) { return response.json(); }
|
||||
@@ -35,6 +35,13 @@ const getLocationTypeByDefaultFor = (entity) => {
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Post a location
|
||||
*
|
||||
* **NOTE**: also in use for Calendar
|
||||
* @param body
|
||||
* @returns {Promise<T>}
|
||||
*/
|
||||
const postLocation = (body) => {
|
||||
const url = `/api/1.0/main/location.json`;
|
||||
return fetch(url, {
|
||||
|
@@ -55,7 +55,7 @@ const makeAccompanyingPeriodLocation = (locationType, store) => {
|
||||
|
||||
export default function prepareLocations(store) {
|
||||
|
||||
// find the locations
|
||||
// find the locations
|
||||
let allLocations = getLocations().then(
|
||||
(results) => {
|
||||
store.commit('addAvailableLocationGroup', {
|
||||
@@ -111,7 +111,7 @@ export default function prepareLocations(store) {
|
||||
if (window.default_location_id) {
|
||||
for (let group of store.state.availableLocations) {
|
||||
let location = group.locations.find((l) => l.id === window.default_location_id);
|
||||
if (location !== undefined & store.state.activity.location === null) {
|
||||
if (location !== undefined && store.state.activity.location === null) {
|
||||
store.dispatch('updateLocation', location);
|
||||
break;
|
||||
}
|
||||
|
@@ -1,3 +1,14 @@
|
||||
{#
|
||||
WARNING: this file is in use in both ActivityBundle and CalendarBundle.
|
||||
|
||||
Take care when editing this file.
|
||||
|
||||
Maybe should we think about abstracting this file a bit more ? Moving it to PersonBundle ?
|
||||
#}
|
||||
{% if context == 'calendar_accompanyingCourse' %}
|
||||
{% import "@ChillCalendar/_invite.html.twig" as invite %}
|
||||
{% endif %}
|
||||
|
||||
{% macro href(pathname, key, value) %}
|
||||
{% set parms = { (key): value } %}
|
||||
{{ path(pathname, parms) }}
|
||||
@@ -18,7 +29,7 @@
|
||||
{% endmacro %}
|
||||
|
||||
{% set blocks = [] %}
|
||||
{% if entity.activityType.personsVisible %}
|
||||
{% if context == 'calendar_accompanyingCourse' or entity.activityType.personsVisible %}
|
||||
{% if context == 'person' %}
|
||||
{% set blocks = blocks|merge([{
|
||||
'title': 'Others persons'|trans,
|
||||
@@ -43,7 +54,7 @@
|
||||
}]) %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if entity.activityType.thirdPartiesVisible %}
|
||||
{% if context == 'calendar_accompanyingCourse' or entity.activityType.thirdPartiesVisible %}
|
||||
{% set blocks = blocks|merge([{
|
||||
'title': 'Third parties'|trans,
|
||||
'items': entity.thirdParties,
|
||||
@@ -52,7 +63,7 @@
|
||||
'key' : 'id',
|
||||
}]) %}
|
||||
{% endif %}
|
||||
{% if entity.activityType.usersVisible %}
|
||||
{% if context == 'calendar_accompanyingCourse' or entity.activityType.usersVisible %}
|
||||
{% set blocks = blocks|merge([{
|
||||
'title': 'Users concerned'|trans,
|
||||
'items': entity.users,
|
||||
@@ -132,6 +143,12 @@
|
||||
{% if bloc.type == 'user' %}
|
||||
<span class="badge-user">
|
||||
{{ item|chill_entity_render_box({'render': 'raw', 'addAltNames': false }) }}
|
||||
{%- if context == 'calendar_accompanyingCourse' %}
|
||||
{% set invite = entity.inviteForUser(item) %}
|
||||
{% if invite is not null %}
|
||||
{{ invite.invite_span(invite) }}
|
||||
{% endif %}
|
||||
{%- endif -%}
|
||||
</span>
|
||||
{% else %}
|
||||
{{ _self.insert_onthefly(bloc.type, item) }}
|
||||
|
@@ -11,8 +11,16 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\CalendarBundle;
|
||||
|
||||
use Chill\CalendarBundle\RemoteCalendar\DependencyInjection\RemoteCalendarCompilerPass;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\HttpKernel\Bundle\Bundle;
|
||||
|
||||
class ChillCalendarBundle extends Bundle
|
||||
{
|
||||
public function build(ContainerBuilder $container)
|
||||
{
|
||||
parent::build($container);
|
||||
|
||||
$container->addCompilerPass(new RemoteCalendarCompilerPass());
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\CalendarBundle\Command;
|
||||
|
||||
use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\MachineTokenStorage;
|
||||
use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Helper\FormatterHelper;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Question\ConfirmationQuestion;
|
||||
use TheNetworg\OAuth2\Client\Provider\Azure;
|
||||
|
||||
class AzureGrantAdminConsentAndAcquireToken extends Command
|
||||
{
|
||||
private Azure $azure;
|
||||
|
||||
private ClientRegistry $clientRegistry;
|
||||
|
||||
private MachineTokenStorage $machineTokenStorage;
|
||||
|
||||
public function __construct(Azure $azure, ClientRegistry $clientRegistry, MachineTokenStorage $machineTokenStorage)
|
||||
{
|
||||
parent::__construct('chill:calendar:msgraph-grant-admin-consent');
|
||||
|
||||
$this->azure = $azure;
|
||||
$this->clientRegistry = $clientRegistry;
|
||||
$this->machineTokenStorage = $machineTokenStorage;
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
/** @var FormatterHelper $formatter */
|
||||
$formatter = $this->getHelper('formatter');
|
||||
$this->azure->scope = ['https://graph.microsoft.com/.default'];
|
||||
$authorizationUrl = explode('?', $this->azure->getAuthorizationUrl(['prompt' => 'admin_consent']));
|
||||
|
||||
// replace the first part by the admin consent authorization url
|
||||
$authorizationUrl[0] = strtr('https://login.microsoftonline.com/{tenant}/adminconsent', ['{tenant}' => $this->azure->tenant]);
|
||||
|
||||
$output->writeln('Go to the url');
|
||||
$output->writeln(implode('?', $authorizationUrl));
|
||||
$output->writeln('Authenticate as admin, and grant admin consent');
|
||||
|
||||
// not necessary ?
|
||||
$helper = $this->getHelper('question');
|
||||
$question = new ConfirmationQuestion('Access granted ?');
|
||||
|
||||
if (!$helper->ask($input, $output, $question)) {
|
||||
$messages = ['No problem, we will wait for you', 'Grant access and come back here'];
|
||||
$output->writeln($formatter->formatBlock($messages, 'warning'));
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
$token = $this->machineTokenStorage->getToken();
|
||||
|
||||
$messages = ['Token acquired!', 'We could acquire a machine token successfully'];
|
||||
$output->writeln($formatter->formatBlock($messages, 'success'));
|
||||
|
||||
$output->writeln('Token information:');
|
||||
$output->writeln($token->getToken());
|
||||
$output->writeln('Expires at: ' . $token->getExpires());
|
||||
$output->writeln('To inspect the token content, go to https://jwt.ms/#access_token=' . urlencode($token->getToken()));
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
@@ -0,0 +1,164 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\CalendarBundle\Command;
|
||||
|
||||
use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\EventsOnUserSubscriptionCreator;
|
||||
use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\MapCalendarToUser;
|
||||
use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\MSGraphUserRepository;
|
||||
use DateInterval;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class MapAndSubscribeUserCalendarCommand extends Command
|
||||
{
|
||||
private EntityManagerInterface $em;
|
||||
|
||||
private EventsOnUserSubscriptionCreator $eventsOnUserSubscriptionCreator;
|
||||
|
||||
private LoggerInterface $logger;
|
||||
|
||||
private MapCalendarToUser $mapCalendarToUser;
|
||||
|
||||
private MSGraphUserRepository $userRepository;
|
||||
|
||||
public function __construct(
|
||||
EntityManagerInterface $em,
|
||||
EventsOnUserSubscriptionCreator $eventsOnUserSubscriptionCreator,
|
||||
LoggerInterface $logger,
|
||||
MapCalendarToUser $mapCalendarToUser,
|
||||
MSGraphUserRepository $userRepository
|
||||
) {
|
||||
parent::__construct('chill:calendar:msgraph-user-map-subscribe');
|
||||
|
||||
$this->em = $em;
|
||||
$this->eventsOnUserSubscriptionCreator = $eventsOnUserSubscriptionCreator;
|
||||
$this->logger = $logger;
|
||||
$this->mapCalendarToUser = $mapCalendarToUser;
|
||||
$this->userRepository = $userRepository;
|
||||
}
|
||||
|
||||
public function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$this->logger->info(__CLASS__ . ' execute command');
|
||||
|
||||
$limit = 50;
|
||||
$offset = 0;
|
||||
/** @var DateInterval $interval the interval before the end of the expiration */
|
||||
$interval = new DateInterval('P1D');
|
||||
$expiration = (new DateTimeImmutable('now'))->add(new DateInterval($input->getOption('subscription-duration')));
|
||||
$total = $this->userRepository->countByMostOldSubscriptionOrWithoutSubscriptionOrData($interval);
|
||||
$created = 0;
|
||||
$renewed = 0;
|
||||
|
||||
$this->logger->info(__CLASS__ . ' the number of user to get - renew', [
|
||||
'total' => $total,
|
||||
'expiration' => $expiration->format(DateTimeImmutable::ATOM),
|
||||
]);
|
||||
|
||||
while ($offset < ($total - 1)) {
|
||||
$users = $this->userRepository->findByMostOldSubscriptionOrWithoutSubscriptionOrData(
|
||||
$interval,
|
||||
$limit,
|
||||
$offset
|
||||
);
|
||||
|
||||
foreach ($users as $user) {
|
||||
if (!$this->mapCalendarToUser->hasUserId($user)) {
|
||||
$this->mapCalendarToUser->writeMetadata($user);
|
||||
}
|
||||
|
||||
if ($this->mapCalendarToUser->hasUserId($user)) {
|
||||
// we first try to renew an existing subscription, if any.
|
||||
// if not, or if it fails, we try to create a new one
|
||||
if ($this->mapCalendarToUser->hasActiveSubscription($user)) {
|
||||
$this->logger->debug(__CLASS__ . ' renew a subscription for', [
|
||||
'userId' => $user->getId(),
|
||||
'username' => $user->getUsernameCanonical(),
|
||||
]);
|
||||
|
||||
['secret' => $secret, 'id' => $id, 'expiration' => $expirationTs]
|
||||
= $this->eventsOnUserSubscriptionCreator->renewSubscriptionForUser($user, $expiration);
|
||||
$this->mapCalendarToUser->writeSubscriptionMetadata($user, $expirationTs, $id, $secret);
|
||||
|
||||
if (0 !== $expirationTs) {
|
||||
++$renewed;
|
||||
} else {
|
||||
$this->logger->warning(__CLASS__ . ' could not renew subscription for a user', [
|
||||
'userId' => $user->getId(),
|
||||
'username' => $user->getUsernameCanonical(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
if (!$this->mapCalendarToUser->hasActiveSubscription($user)) {
|
||||
$this->logger->debug(__CLASS__ . ' create a subscription for', [
|
||||
'userId' => $user->getId(),
|
||||
'username' => $user->getUsernameCanonical(),
|
||||
]);
|
||||
|
||||
['secret' => $secret, 'id' => $id, 'expiration' => $expirationTs]
|
||||
= $this->eventsOnUserSubscriptionCreator->createSubscriptionForUser($user, $expiration);
|
||||
$this->mapCalendarToUser->writeSubscriptionMetadata($user, $expirationTs, $id, $secret);
|
||||
|
||||
if (0 !== $expirationTs) {
|
||||
++$created;
|
||||
} else {
|
||||
$this->logger->warning(__CLASS__ . ' could not create subscription for a user', [
|
||||
'userId' => $user->getId(),
|
||||
'username' => $user->getUsernameCanonical(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
++$offset;
|
||||
}
|
||||
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
}
|
||||
|
||||
$this->logger->warning(__CLASS__ . ' process executed', [
|
||||
'created' => $created,
|
||||
'renewed' => $renewed,
|
||||
]);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
protected function configure()
|
||||
{
|
||||
parent::configure();
|
||||
|
||||
$this
|
||||
->setDescription('MSGraph: collect user metadata and create subscription on events for users')
|
||||
->addOption(
|
||||
'renew-before-end-interval',
|
||||
'r',
|
||||
InputOption::VALUE_OPTIONAL,
|
||||
'delay before renewing subscription',
|
||||
'P1D'
|
||||
)
|
||||
->addOption(
|
||||
'subscription-duration',
|
||||
's',
|
||||
InputOption::VALUE_OPTIONAL,
|
||||
'duration for the subscription',
|
||||
'PT4230M'
|
||||
);
|
||||
}
|
||||
}
|
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\CalendarBundle\Command;
|
||||
|
||||
use Chill\CalendarBundle\Service\ShortMessageNotification\BulkCalendarShortMessageSender;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class SendShortMessageOnEligibleCalendar extends Command
|
||||
{
|
||||
private BulkCalendarShortMessageSender $messageSender;
|
||||
|
||||
public function __construct(BulkCalendarShortMessageSender $messageSender)
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
$this->messageSender = $messageSender;
|
||||
}
|
||||
|
||||
public function getName()
|
||||
{
|
||||
return 'chill:calendar:send-short-messages';
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$this->messageSender->sendBulkMessageToEligibleCalendars();
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
@@ -0,0 +1,200 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\CalendarBundle\Command;
|
||||
|
||||
use Chill\CalendarBundle\Entity\Calendar;
|
||||
use Chill\CalendarBundle\Service\ShortMessageNotification\ShortMessageForCalendarBuilderInterface;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Phonenumber\PhoneNumberHelperInterface;
|
||||
use Chill\MainBundle\Repository\UserRepositoryInterface;
|
||||
use Chill\MainBundle\Service\ShortMessage\ShortMessageTransporterInterface;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Chill\PersonBundle\Repository\PersonRepository;
|
||||
use DateInterval;
|
||||
use DateTimeImmutable;
|
||||
use libphonenumber\PhoneNumber;
|
||||
use libphonenumber\PhoneNumberFormat;
|
||||
use libphonenumber\PhoneNumberType;
|
||||
use libphonenumber\PhoneNumberUtil;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Helper\QuestionHelper;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Question\ConfirmationQuestion;
|
||||
use Symfony\Component\Console\Question\Question;
|
||||
use UnexpectedValueException;
|
||||
use function count;
|
||||
|
||||
class SendTestShortMessageOnCalendarCommand extends Command
|
||||
{
|
||||
private ShortMessageForCalendarBuilderInterface $messageForCalendarBuilder;
|
||||
|
||||
private PersonRepository $personRepository;
|
||||
|
||||
private PhoneNumberHelperInterface $phoneNumberHelper;
|
||||
|
||||
private PhoneNumberUtil $phoneNumberUtil;
|
||||
|
||||
private ShortMessageTransporterInterface $transporter;
|
||||
|
||||
private UserRepositoryInterface $userRepository;
|
||||
|
||||
public function __construct(
|
||||
PersonRepository $personRepository,
|
||||
PhoneNumberUtil $phoneNumberUtil,
|
||||
PhoneNumberHelperInterface $phoneNumberHelper,
|
||||
ShortMessageForCalendarBuilderInterface $messageForCalendarBuilder,
|
||||
ShortMessageTransporterInterface $transporter,
|
||||
UserRepositoryInterface $userRepository
|
||||
) {
|
||||
parent::__construct();
|
||||
|
||||
$this->personRepository = $personRepository;
|
||||
$this->phoneNumberUtil = $phoneNumberUtil;
|
||||
$this->phoneNumberHelper = $phoneNumberHelper;
|
||||
$this->messageForCalendarBuilder = $messageForCalendarBuilder;
|
||||
$this->transporter = $transporter;
|
||||
$this->userRepository = $userRepository;
|
||||
}
|
||||
|
||||
public function getName()
|
||||
{
|
||||
return 'chill:calendar:test-send-short-message';
|
||||
}
|
||||
|
||||
protected function configure()
|
||||
{
|
||||
$this->setDescription('Test sending a SMS for a dummy calendar appointment');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$calendar = new Calendar();
|
||||
$calendar->setSendSMS(true);
|
||||
|
||||
/** @var QuestionHelper $helper */
|
||||
$helper = $this->getHelper('question');
|
||||
|
||||
// start date
|
||||
$question = new Question('When will start the appointment ? (default: "1 hour") ', '1 hour');
|
||||
$startDate = new DateTimeImmutable($helper->ask($input, $output, $question));
|
||||
|
||||
if (false === $startDate) {
|
||||
throw new UnexpectedValueException('could not create a date with this date and time');
|
||||
}
|
||||
|
||||
$calendar->setStartDate($startDate);
|
||||
|
||||
// end date
|
||||
$question = new Question('How long will last the appointment ? (default: "PT30M") ', 'PT30M');
|
||||
$interval = new DateInterval($helper->ask($input, $output, $question));
|
||||
|
||||
if (false === $interval) {
|
||||
throw new UnexpectedValueException('could not create the interval');
|
||||
}
|
||||
|
||||
$calendar->setEndDate($calendar->getStartDate()->add($interval));
|
||||
|
||||
// a person
|
||||
$question = new Question('Who will participate ? Give an id for a person. ');
|
||||
$question
|
||||
->setValidator(function ($answer): Person {
|
||||
if (!is_numeric($answer)) {
|
||||
throw new UnexpectedValueException('the answer must be numeric');
|
||||
}
|
||||
|
||||
if (0 >= (int) $answer) {
|
||||
throw new UnexpectedValueException('the answer must be greater than zero');
|
||||
}
|
||||
|
||||
$person = $this->personRepository->find((int) $answer);
|
||||
|
||||
if (null === $person) {
|
||||
throw new UnexpectedValueException('The person is not found');
|
||||
}
|
||||
|
||||
return $person;
|
||||
});
|
||||
|
||||
$person = $helper->ask($input, $output, $question);
|
||||
$calendar->addPerson($person);
|
||||
|
||||
// a main user
|
||||
$question = new Question('Who will be the main user ? Give an id for a user. ');
|
||||
$question
|
||||
->setValidator(function ($answer): User {
|
||||
if (!is_numeric($answer)) {
|
||||
throw new UnexpectedValueException('the answer must be numeric');
|
||||
}
|
||||
|
||||
if (0 >= (int) $answer) {
|
||||
throw new UnexpectedValueException('the answer must be greater than zero');
|
||||
}
|
||||
|
||||
$user = $this->userRepository->find((int) $answer);
|
||||
|
||||
if (null === $user) {
|
||||
throw new UnexpectedValueException('The user is not found');
|
||||
}
|
||||
|
||||
return $user;
|
||||
});
|
||||
|
||||
$user = $helper->ask($input, $output, $question);
|
||||
$calendar->setMainUser($user);
|
||||
|
||||
// phonenumber
|
||||
$phonenumberFormatted = null !== $person->getMobilenumber() ?
|
||||
$this->phoneNumberUtil->format($person->getMobilenumber(), PhoneNumberFormat::E164) : '';
|
||||
$question = new Question(
|
||||
sprintf('To which number are we going to send this fake message ? (default to: %s)', $phonenumberFormatted),
|
||||
$phonenumberFormatted
|
||||
);
|
||||
|
||||
$question->setNormalizer(function ($answer): PhoneNumber {
|
||||
if (null === $answer) {
|
||||
throw new UnexpectedValueException('The person is not found');
|
||||
}
|
||||
|
||||
$phone = $this->phoneNumberUtil->parse($answer, 'BE');
|
||||
|
||||
if (!$this->phoneNumberUtil->isPossibleNumberForType($phone, PhoneNumberType::MOBILE)) {
|
||||
throw new UnexpectedValueException('Phone number si not a mobile');
|
||||
}
|
||||
|
||||
return $phone;
|
||||
});
|
||||
|
||||
$phone = $helper->ask($input, $output, $question);
|
||||
|
||||
$question = new ConfirmationQuestion('really send the message to the phone ?');
|
||||
$reallySend = (bool) $helper->ask($input, $output, $question);
|
||||
|
||||
$messages = $this->messageForCalendarBuilder->buildMessageForCalendar($calendar);
|
||||
|
||||
if (0 === count($messages)) {
|
||||
$output->writeln('no message to send to this user');
|
||||
}
|
||||
|
||||
foreach ($messages as $key => $message) {
|
||||
$output->writeln("The short message for SMS {$key} will be: ");
|
||||
$output->writeln($message->getContent());
|
||||
$message->setPhoneNumber($phone);
|
||||
|
||||
if ($reallySend) {
|
||||
$this->transporter->send($message);
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
@@ -11,11 +11,73 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\CalendarBundle\Controller;
|
||||
|
||||
use Chill\CalendarBundle\Repository\CalendarRepository;
|
||||
use Chill\MainBundle\CRUD\Controller\ApiController;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Serializer\Model\Collection;
|
||||
use DateTimeImmutable;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
class CalendarAPIController extends ApiController
|
||||
{
|
||||
private CalendarRepository $calendarRepository;
|
||||
|
||||
public function __construct(CalendarRepository $calendarRepository)
|
||||
{
|
||||
$this->calendarRepository = $calendarRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/api/1.0/calendar/calendar/by-user/{id}.{_format}",
|
||||
* name="chill_api_single_calendar_list_by-user",
|
||||
* requirements={"_format": "json"}
|
||||
* )
|
||||
*/
|
||||
public function listByUser(User $user, Request $request, string $_format): JsonResponse
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_USER');
|
||||
|
||||
if (!$request->query->has('dateFrom')) {
|
||||
throw new BadRequestHttpException('You must provide a dateFrom parameter');
|
||||
}
|
||||
|
||||
if (false === $dateFrom = DateTimeImmutable::createFromFormat(
|
||||
DateTimeImmutable::ATOM,
|
||||
$request->query->get('dateFrom')
|
||||
)) {
|
||||
throw new BadRequestHttpException('dateFrom not parsable');
|
||||
}
|
||||
|
||||
if (!$request->query->has('dateTo')) {
|
||||
throw new BadRequestHttpException('You must provide a dateTo parameter');
|
||||
}
|
||||
|
||||
if (false === $dateTo = DateTimeImmutable::createFromFormat(
|
||||
DateTimeImmutable::ATOM,
|
||||
$request->query->get('dateTo')
|
||||
)) {
|
||||
throw new BadRequestHttpException('dateTo not parsable');
|
||||
}
|
||||
|
||||
$total = $this->calendarRepository->countByUser($user, $dateFrom, $dateTo);
|
||||
$paginator = $this->getPaginatorFactory()->create($total);
|
||||
$ranges = $this->calendarRepository->findByUser(
|
||||
$user,
|
||||
$dateFrom,
|
||||
$dateTo,
|
||||
$paginator->getItemsPerPage(),
|
||||
$paginator->getCurrentPageFirstItemNumber()
|
||||
);
|
||||
|
||||
$collection = new Collection($ranges, $paginator);
|
||||
|
||||
return $this->json($collection, Response::HTTP_OK, [], ['groups' => ['calendar:light']]);
|
||||
}
|
||||
|
||||
protected function customizeQuery(string $action, Request $request, $qb): void
|
||||
{
|
||||
if ($request->query->has('main_user')) {
|
||||
|
@@ -13,53 +13,67 @@ namespace Chill\CalendarBundle\Controller;
|
||||
|
||||
use Chill\CalendarBundle\Entity\Calendar;
|
||||
use Chill\CalendarBundle\Form\CalendarType;
|
||||
use Chill\CalendarBundle\RemoteCalendar\Connector\RemoteCalendarConnectorInterface;
|
||||
use Chill\CalendarBundle\Repository\CalendarACLAwareRepositoryInterface;
|
||||
use Chill\CalendarBundle\Repository\CalendarRepository;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Pagination\PaginatorFactory;
|
||||
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
|
||||
use Chill\MainBundle\Repository\UserRepository;
|
||||
use Chill\MainBundle\Templating\Listing\FilterOrderHelper;
|
||||
use Chill\MainBundle\Templating\Listing\FilterOrderHelperFactoryInterface;
|
||||
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Chill\ThirdPartyBundle\Entity\ThirdParty;
|
||||
use DateTimeImmutable;
|
||||
use Exception;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||
use Symfony\Component\Form\Form;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
|
||||
class CalendarController extends AbstractController
|
||||
{
|
||||
protected AuthorizationHelper $authorizationHelper;
|
||||
|
||||
protected EventDispatcherInterface $eventDispatcher;
|
||||
|
||||
protected LoggerInterface $logger;
|
||||
|
||||
protected PaginatorFactory $paginator;
|
||||
|
||||
protected SerializerInterface $serializer;
|
||||
private CalendarACLAwareRepositoryInterface $calendarACLAwareRepository;
|
||||
|
||||
private CalendarRepository $calendarRepository;
|
||||
|
||||
private FilterOrderHelperFactoryInterface $filterOrderHelperFactory;
|
||||
|
||||
private LoggerInterface $logger;
|
||||
|
||||
private PaginatorFactory $paginator;
|
||||
|
||||
private RemoteCalendarConnectorInterface $remoteCalendarConnector;
|
||||
|
||||
private SerializerInterface $serializer;
|
||||
|
||||
private UserRepository $userRepository;
|
||||
|
||||
public function __construct(
|
||||
EventDispatcherInterface $eventDispatcher,
|
||||
AuthorizationHelper $authorizationHelper,
|
||||
CalendarRepository $calendarRepository,
|
||||
CalendarACLAwareRepositoryInterface $calendarACLAwareRepository,
|
||||
FilterOrderHelperFactoryInterface $filterOrderHelperFactory,
|
||||
LoggerInterface $logger,
|
||||
SerializerInterface $serializer,
|
||||
PaginatorFactory $paginator,
|
||||
CalendarRepository $calendarRepository
|
||||
RemoteCalendarConnectorInterface $remoteCalendarConnector,
|
||||
SerializerInterface $serializer,
|
||||
UserRepository $userRepository
|
||||
) {
|
||||
$this->eventDispatcher = $eventDispatcher;
|
||||
$this->authorizationHelper = $authorizationHelper;
|
||||
$this->logger = $logger;
|
||||
$this->serializer = $serializer;
|
||||
$this->paginator = $paginator;
|
||||
$this->calendarRepository = $calendarRepository;
|
||||
$this->calendarACLAwareRepository = $calendarACLAwareRepository;
|
||||
$this->filterOrderHelperFactory = $filterOrderHelperFactory;
|
||||
$this->logger = $logger;
|
||||
$this->paginator = $paginator;
|
||||
$this->remoteCalendarConnector = $remoteCalendarConnector;
|
||||
$this->serializer = $serializer;
|
||||
$this->userRepository = $userRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -67,12 +81,13 @@ class CalendarController extends AbstractController
|
||||
*
|
||||
* @Route("/{_locale}/calendar/{id}/delete", name="chill_calendar_calendar_delete")
|
||||
*/
|
||||
public function deleteAction(Request $request, int $id)
|
||||
public function deleteAction(Request $request, Calendar $entity)
|
||||
{
|
||||
$view = null;
|
||||
$em = $this->getDoctrine()->getManager();
|
||||
|
||||
[$user, $accompanyingPeriod] = $this->getEntity($request);
|
||||
$accompanyingPeriod = $entity->getAccompanyingPeriod();
|
||||
$user = null; // TODO legacy code ? remove it ?
|
||||
|
||||
if ($accompanyingPeriod instanceof AccompanyingPeriod) {
|
||||
$view = '@ChillCalendar/Calendar/confirm_deleteByAccompanyingCourse.html.twig';
|
||||
@@ -80,14 +95,7 @@ class CalendarController extends AbstractController
|
||||
$view = '@ChillCalendar/Calendar/confirm_deleteByUser.html.twig';
|
||||
}
|
||||
|
||||
/** @var Calendar $entity */
|
||||
$entity = $em->getRepository(\Chill\CalendarBundle\Entity\Calendar::class)->find($id);
|
||||
|
||||
if (!$entity) {
|
||||
throw $this->createNotFoundException('Unable to find Calendar entity.');
|
||||
}
|
||||
|
||||
$form = $this->createDeleteForm($id, $user, $accompanyingPeriod);
|
||||
$form = $this->createDeleteForm($entity->getId(), $user, $accompanyingPeriod);
|
||||
|
||||
if ($request->getMethod() === Request::METHOD_DELETE) {
|
||||
$form->handleRequest($request);
|
||||
@@ -106,7 +114,7 @@ class CalendarController extends AbstractController
|
||||
|
||||
$params = $this->buildParamsToUrl($user, $accompanyingPeriod);
|
||||
|
||||
return $this->redirectToRoute('chill_calendar_calendar_list', $params);
|
||||
return $this->redirectToRoute('chill_calendar_calendar_list_by_period', $params);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,8 +134,12 @@ class CalendarController extends AbstractController
|
||||
*
|
||||
* @Route("/{_locale}/calendar/calendar/{id}/edit", name="chill_calendar_calendar_edit")
|
||||
*/
|
||||
public function editAction(int $id, Request $request): Response
|
||||
public function editAction(Calendar $entity, Request $request): Response
|
||||
{
|
||||
if (!$this->remoteCalendarConnector->isReady()) {
|
||||
return $this->remoteCalendarConnector->getMakeReadyResponse($request->getUri());
|
||||
}
|
||||
|
||||
$view = null;
|
||||
$em = $this->getDoctrine()->getManager();
|
||||
|
||||
@@ -136,35 +148,28 @@ class CalendarController extends AbstractController
|
||||
if ($accompanyingPeriod instanceof AccompanyingPeriod) {
|
||||
$view = '@ChillCalendar/Calendar/editByAccompanyingCourse.html.twig';
|
||||
} elseif ($user instanceof User) {
|
||||
throw new Exception('to analyze');
|
||||
$view = '@ChillCalendar/Calendar/editByUser.html.twig';
|
||||
}
|
||||
|
||||
$entity = $em->getRepository(\Chill\CalendarBundle\Entity\Calendar::class)->find($id);
|
||||
|
||||
if (!$entity) {
|
||||
throw $this->createNotFoundException('Unable to find Calendar entity.');
|
||||
}
|
||||
|
||||
$form = $this->createForm(CalendarType::class, $entity, [
|
||||
'accompanyingPeriod' => $accompanyingPeriod,
|
||||
])->handleRequest($request);
|
||||
$form = $this->createForm(CalendarType::class, $entity);
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$em->persist($entity);
|
||||
$em->flush();
|
||||
|
||||
$this->addFlash('success', $this->get('translator')->trans('Success : calendar item updated!'));
|
||||
|
||||
$params = $this->buildParamsToUrl($user, $accompanyingPeriod);
|
||||
|
||||
return $this->redirectToRoute('chill_calendar_calendar_list', $params);
|
||||
return $this->redirectToRoute('chill_calendar_calendar_list_by_period', $params);
|
||||
}
|
||||
|
||||
if ($form->isSubmitted() && !$form->isValid()) {
|
||||
$this->addFlash('error', $this->get('translator')->trans('This form contains errors'));
|
||||
}
|
||||
|
||||
$deleteForm = $this->createDeleteForm($id, $user, $accompanyingPeriod);
|
||||
$deleteForm = $this->createDeleteForm($entity->getId(), $user, $accompanyingPeriod);
|
||||
|
||||
if (null === $view) {
|
||||
throw $this->createNotFoundException('Template not found');
|
||||
@@ -177,7 +182,7 @@ class CalendarController extends AbstractController
|
||||
'form' => $form->createView(),
|
||||
'delete_form' => $deleteForm->createView(),
|
||||
'accompanyingCourse' => $accompanyingPeriod,
|
||||
'user' => $user,
|
||||
// 'user' => $user,
|
||||
'entity_json' => $entity_array,
|
||||
]);
|
||||
}
|
||||
@@ -185,45 +190,53 @@ class CalendarController extends AbstractController
|
||||
/**
|
||||
* Lists all Calendar entities.
|
||||
*
|
||||
* @Route("/{_locale}/calendar/calendar/", name="chill_calendar_calendar_list")
|
||||
* @Route("/{_locale}/calendar/calendar/by-period/{id}", name="chill_calendar_calendar_list_by_period")
|
||||
*/
|
||||
public function listAction(Request $request): Response
|
||||
public function listActionByCourse(AccompanyingPeriod $accompanyingPeriod): Response
|
||||
{
|
||||
$view = null;
|
||||
$filterOrder = $this->buildListFilterOrder();
|
||||
['from' => $from, 'to' => $to] = $filterOrder->getDateRangeData('startDate');
|
||||
|
||||
[$user, $accompanyingPeriod] = $this->getEntity($request);
|
||||
$total = $this->calendarACLAwareRepository
|
||||
->countByAccompanyingPeriod($accompanyingPeriod, $from, $to);
|
||||
$paginator = $this->paginator->create($total);
|
||||
$calendarItems = $this->calendarACLAwareRepository->findByAccompanyingPeriod(
|
||||
$accompanyingPeriod,
|
||||
$from,
|
||||
$to,
|
||||
['startDate' => 'DESC'],
|
||||
$paginator->getCurrentPageFirstItemNumber(),
|
||||
$paginator->getItemsPerPage()
|
||||
);
|
||||
|
||||
if ($user instanceof User) {
|
||||
$calendarItems = $this->calendarRepository->findByUser($user);
|
||||
return $this->render('@ChillCalendar/Calendar/listByAccompanyingCourse.html.twig', [
|
||||
'calendarItems' => $calendarItems,
|
||||
'accompanyingCourse' => $accompanyingPeriod,
|
||||
'paginator' => $paginator,
|
||||
'filterOrder' => $filterOrder,
|
||||
]);
|
||||
}
|
||||
|
||||
$view = '@ChillCalendar/Calendar/listByUser.html.twig';
|
||||
/**
|
||||
* @Route("/{_locale}/calendar/calendar/my", name="chill_calendar_calendar_list_my")
|
||||
*/
|
||||
public function myCalendar(Request $request): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_USER');
|
||||
|
||||
return $this->render($view, [
|
||||
'calendarItems' => $calendarItems,
|
||||
'user' => $user,
|
||||
]);
|
||||
if (!$this->remoteCalendarConnector->isReady()) {
|
||||
return $this->remoteCalendarConnector->getMakeReadyResponse($request->getUri());
|
||||
}
|
||||
|
||||
if ($accompanyingPeriod instanceof AccompanyingPeriod) {
|
||||
$total = $this->calendarRepository->countByAccompanyingPeriod($accompanyingPeriod);
|
||||
$paginator = $this->paginator->create($total);
|
||||
$calendarItems = $this->calendarRepository->findBy(
|
||||
['accompanyingPeriod' => $accompanyingPeriod],
|
||||
['startDate' => 'DESC'],
|
||||
$paginator->getItemsPerPage(),
|
||||
$paginator->getCurrentPageFirstItemNumber()
|
||||
);
|
||||
|
||||
$view = '@ChillCalendar/Calendar/listByAccompanyingCourse.html.twig';
|
||||
|
||||
return $this->render($view, [
|
||||
'calendarItems' => $calendarItems,
|
||||
'accompanyingCourse' => $accompanyingPeriod,
|
||||
'paginator' => $paginator,
|
||||
]);
|
||||
if (!$this->getUser() instanceof User) {
|
||||
throw new UnauthorizedHttpException('you are not an user');
|
||||
}
|
||||
|
||||
throw new Exception('Unable to list actions.');
|
||||
$view = '@ChillCalendar/Calendar/listByUser.html.twig';
|
||||
|
||||
return $this->render($view, [
|
||||
'user' => $this->getUser(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -233,6 +246,10 @@ class CalendarController extends AbstractController
|
||||
*/
|
||||
public function newAction(Request $request): Response
|
||||
{
|
||||
if (!$this->remoteCalendarConnector->isReady()) {
|
||||
return $this->remoteCalendarConnector->getMakeReadyResponse($request->getUri());
|
||||
}
|
||||
|
||||
$view = null;
|
||||
$em = $this->getDoctrine()->getManager();
|
||||
|
||||
@@ -246,8 +263,10 @@ class CalendarController extends AbstractController
|
||||
// }
|
||||
|
||||
$entity = new Calendar();
|
||||
$entity->setUser($this->getUser());
|
||||
$entity->setStatus($entity::STATUS_VALID);
|
||||
|
||||
if ($request->query->has('mainUser')) {
|
||||
$entity->setMainUser($this->userRepository->find($request->query->getInt('mainUser')));
|
||||
}
|
||||
|
||||
// if ($user instanceof User) {
|
||||
// $entity->setPerson($user);
|
||||
@@ -257,9 +276,8 @@ class CalendarController extends AbstractController
|
||||
$entity->setAccompanyingPeriod($accompanyingPeriod);
|
||||
}
|
||||
|
||||
$form = $this->createForm(CalendarType::class, $entity, [
|
||||
'accompanyingPeriod' => $accompanyingPeriod,
|
||||
])->handleRequest($request);
|
||||
$form = $this->createForm(CalendarType::class, $entity);
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$em->persist($entity);
|
||||
@@ -269,7 +287,7 @@ class CalendarController extends AbstractController
|
||||
|
||||
$params = $this->buildParamsToUrl($user, $accompanyingPeriod);
|
||||
|
||||
return $this->redirectToRoute('chill_calendar_calendar_list', $params);
|
||||
return $this->redirectToRoute('chill_calendar_calendar_list_by_period', $params);
|
||||
}
|
||||
|
||||
if ($form->isSubmitted() && !$form->isValid()) {
|
||||
@@ -349,7 +367,7 @@ class CalendarController extends AbstractController
|
||||
'professionalsId' => $professionalsId,
|
||||
'date' => $entity->getStartDate()->format('Y-m-d'),
|
||||
'durationTime' => $durationTimeInMinutes,
|
||||
'location' => $entity->getLocation()->getId(),
|
||||
'location' => $entity->getLocation() ? $entity->getLocation()->getId() : null,
|
||||
'comment' => $entity->getComment()->getComment(),
|
||||
];
|
||||
|
||||
@@ -362,6 +380,58 @@ class CalendarController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/{_locale}/calendar/calendar/{id}/to-activity", name="chill_calendar_calendar_to_activity")
|
||||
*/
|
||||
public function toActivity(Request $request, Calendar $calendar): RedirectResponse
|
||||
{
|
||||
$personsId = array_map(
|
||||
static fn (Person $p): int => $p->getId(),
|
||||
$calendar->getPersons()->toArray()
|
||||
);
|
||||
|
||||
$professionalsId = array_map(
|
||||
static fn (ThirdParty $thirdParty): ?int => $thirdParty->getId(),
|
||||
$calendar->getProfessionals()->toArray()
|
||||
);
|
||||
|
||||
$usersId = array_map(
|
||||
static fn (User $user): ?int => $user->getId(),
|
||||
array_merge($calendar->getUsers()->toArray(), [$calendar->getMainUser()])
|
||||
);
|
||||
|
||||
$durationTime = $calendar->getEndDate()->diff($calendar->getStartDate());
|
||||
$durationTimeInMinutes = $durationTime->days * 1440 + $durationTime->h * 60 + $durationTime->i;
|
||||
|
||||
$activityData = [
|
||||
'calendarId' => $calendar->getId(),
|
||||
'personsId' => $personsId,
|
||||
'professionalsId' => $professionalsId,
|
||||
'usersId' => $usersId,
|
||||
'date' => $calendar->getStartDate()->format('Y-m-d'),
|
||||
'durationTime' => $durationTimeInMinutes,
|
||||
'location' => $calendar->getLocation() ? $calendar->getLocation()->getId() : null,
|
||||
'comment' => $calendar->getComment()->getComment(),
|
||||
];
|
||||
|
||||
return $this->redirectToRoute(
|
||||
'chill_activity_activity_new',
|
||||
[
|
||||
'accompanying_period_id' => $calendar->getAccompanyingPeriod()->getId(),
|
||||
'activityData' => $activityData,
|
||||
'returnPath' => $request->query->get('returnPath', null),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
private function buildListFilterOrder(): FilterOrderHelper
|
||||
{
|
||||
$filterOrder = $this->filterOrderHelperFactory->create(self::class);
|
||||
$filterOrder->addDateRange('startDate', null, new DateTimeImmutable('3 days ago'), null);
|
||||
|
||||
return $filterOrder->build();
|
||||
}
|
||||
|
||||
private function buildParamsToUrl(?User $user, ?AccompanyingPeriod $accompanyingPeriod): array
|
||||
{
|
||||
$params = [];
|
||||
@@ -371,7 +441,7 @@ class CalendarController extends AbstractController
|
||||
}
|
||||
|
||||
if (null !== $accompanyingPeriod) {
|
||||
$params['accompanying_period_id'] = $accompanyingPeriod->getId();
|
||||
$params['id'] = $accompanyingPeriod->getId();
|
||||
}
|
||||
|
||||
return $params;
|
||||
|
@@ -11,38 +11,72 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\CalendarBundle\Controller;
|
||||
|
||||
use Chill\CalendarBundle\Repository\CalendarRangeRepository;
|
||||
use Chill\MainBundle\CRUD\Controller\ApiController;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Serializer\Model\Collection;
|
||||
use DateTimeImmutable;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
|
||||
use function count;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
class CalendarRangeAPIController extends ApiController
|
||||
{
|
||||
/**
|
||||
* @Route("/api/1.0/calendar/calendar-range-available.{_format}", name="chill_api_single_calendar_range_available")
|
||||
*/
|
||||
public function availableRanges(Request $request, string $_format): JsonResponse
|
||||
private CalendarRangeRepository $calendarRangeRepository;
|
||||
|
||||
public function __construct(CalendarRangeRepository $calendarRangeRepository)
|
||||
{
|
||||
$em = $this->getDoctrine()->getManager();
|
||||
$this->calendarRangeRepository = $calendarRangeRepository;
|
||||
}
|
||||
|
||||
$sql = 'SELECT c FROM ChillCalendarBundle:CalendarRange c
|
||||
WHERE NOT EXISTS (SELECT cal.id FROM ChillCalendarBundle:Calendar cal WHERE cal.calendarRange = c.id)';
|
||||
/**
|
||||
* @Route("/api/1.0/calendar/calendar-range-available/{id}.{_format}",
|
||||
* name="chill_api_single_calendar_range_available",
|
||||
* requirements={"_format": "json"}
|
||||
* )
|
||||
*/
|
||||
public function availableRanges(User $user, Request $request, string $_format): JsonResponse
|
||||
{
|
||||
//return new JsonResponse(['ok' => true], 200, [], false);
|
||||
$this->denyAccessUnlessGranted('ROLE_USER');
|
||||
|
||||
if ($request->query->has('user')) {
|
||||
$user = $request->query->get('user');
|
||||
$sql = $sql . ' AND c.user = :user';
|
||||
$query = $em->createQuery($sql)
|
||||
->setParameter('user', $user);
|
||||
} else {
|
||||
$query = $em->createQuery($sql);
|
||||
if (!$request->query->has('dateFrom')) {
|
||||
throw new BadRequestHttpException('You must provide a dateFrom parameter');
|
||||
}
|
||||
|
||||
$results = $query->getResult();
|
||||
if (false === $dateFrom = DateTimeImmutable::createFromFormat(
|
||||
DateTimeImmutable::ATOM,
|
||||
$request->query->get('dateFrom')
|
||||
)) {
|
||||
throw new BadRequestHttpException('dateFrom not parsable');
|
||||
}
|
||||
|
||||
return $this->json(['count' => count($results), 'results' => $results], Response::HTTP_OK, [], ['groups' => ['read']]);
|
||||
//TODO use also the paginator, eg return $this->serializeCollection('get', $request, $_format, $paginator, $results);
|
||||
if (!$request->query->has('dateTo')) {
|
||||
throw new BadRequestHttpException('You must provide a dateTo parameter');
|
||||
}
|
||||
|
||||
if (false === $dateTo = DateTimeImmutable::createFromFormat(
|
||||
DateTimeImmutable::ATOM,
|
||||
$request->query->get('dateTo')
|
||||
)) {
|
||||
throw new BadRequestHttpException('dateTo not parsable');
|
||||
}
|
||||
|
||||
$total = $this->calendarRangeRepository->countByAvailableRangesForUser($user, $dateFrom, $dateTo);
|
||||
$paginator = $this->getPaginatorFactory()->create($total);
|
||||
$ranges = $this->calendarRangeRepository->findByAvailableRangesForUser(
|
||||
$user,
|
||||
$dateFrom,
|
||||
$dateTo,
|
||||
$paginator->getItemsPerPage(),
|
||||
$paginator->getCurrentPageFirstItemNumber()
|
||||
);
|
||||
|
||||
$collection = new Collection($ranges, $paginator);
|
||||
|
||||
return $this->json($collection, Response::HTTP_OK, [], ['groups' => ['read']]);
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\CalendarBundle\Controller;
|
||||
|
||||
use Chill\CalendarBundle\Entity\Calendar;
|
||||
use Chill\CalendarBundle\Entity\Invite;
|
||||
use Chill\CalendarBundle\Messenger\Message\InviteUpdateMessage;
|
||||
use Chill\CalendarBundle\Security\Voter\InviteVoter;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use function in_array;
|
||||
|
||||
class InviteApiController
|
||||
{
|
||||
private EntityManagerInterface $entityManager;
|
||||
|
||||
private MessageBusInterface $messageBus;
|
||||
|
||||
private Security $security;
|
||||
|
||||
public function __construct(EntityManagerInterface $entityManager, MessageBusInterface $messageBus, Security $security)
|
||||
{
|
||||
$this->entityManager = $entityManager;
|
||||
$this->messageBus = $messageBus;
|
||||
$this->security = $security;
|
||||
}
|
||||
|
||||
/**
|
||||
* Give an answer to a calendar invite.
|
||||
*
|
||||
* @Route("/api/1.0/calendar/calendar/{id}/answer/{answer}.json", methods={"post"})
|
||||
*/
|
||||
public function answer(Calendar $calendar, string $answer): Response
|
||||
{
|
||||
$user = $this->security->getUser();
|
||||
|
||||
if (!$user instanceof User) {
|
||||
throw new AccessDeniedHttpException('not a regular user');
|
||||
}
|
||||
|
||||
if (null === $invite = $calendar->getInviteForUser($user)) {
|
||||
throw new AccessDeniedHttpException('not invited to this calendar');
|
||||
}
|
||||
|
||||
if (!$this->security->isGranted(InviteVoter::ANSWER, $invite)) {
|
||||
throw new AccessDeniedHttpException('not allowed to answer on this invitation');
|
||||
}
|
||||
|
||||
if (!in_array($answer, Invite::STATUSES, true)) {
|
||||
throw new BadRequestHttpException('answer not valid');
|
||||
}
|
||||
|
||||
$invite->setStatus($answer);
|
||||
$this->entityManager->flush();
|
||||
|
||||
$this->messageBus->dispatch(new InviteUpdateMessage($invite, $this->security->getUser()));
|
||||
|
||||
return new JsonResponse(null, Response::HTTP_ACCEPTED, [], false);
|
||||
}
|
||||
}
|
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\CalendarBundle\Controller;
|
||||
|
||||
use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\OnBehalfOfUserTokenStorage;
|
||||
use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
|
||||
use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use TheNetworg\OAuth2\Client\Provider\Azure;
|
||||
use TheNetworg\OAuth2\Client\Token\AccessToken;
|
||||
|
||||
class RemoteCalendarConnectAzureController
|
||||
{
|
||||
private ClientRegistry $clientRegistry;
|
||||
|
||||
private OnBehalfOfUserTokenStorage $MSGraphTokenStorage;
|
||||
|
||||
public function __construct(
|
||||
ClientRegistry $clientRegistry,
|
||||
OnBehalfOfUserTokenStorage $MSGraphTokenStorage
|
||||
) {
|
||||
$this->clientRegistry = $clientRegistry;
|
||||
$this->MSGraphTokenStorage = $MSGraphTokenStorage;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/{_locale}/connect/azure", name="chill_calendar_remote_connect_azure")
|
||||
*/
|
||||
public function connectAzure(Request $request): Response
|
||||
{
|
||||
$request->getSession()->set('azure_return_path', $request->query->get('returnPath', '/'));
|
||||
|
||||
return $this->clientRegistry
|
||||
->getClient('azure') // key used in config/packages/knpu_oauth2_client.yaml
|
||||
->redirect(['https://graph.microsoft.com/.default', 'offline_access'], []);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/connect/azure/check", name="chill_calendar_remote_connect_azure_check")
|
||||
*/
|
||||
public function connectAzureCheck(Request $request): Response
|
||||
{
|
||||
/** @var Azure $client */
|
||||
$client = $this->clientRegistry->getClient('azure');
|
||||
|
||||
try {
|
||||
/** @var AccessToken $token */
|
||||
$token = $client->getAccessToken([]);
|
||||
|
||||
$this->MSGraphTokenStorage->setToken($token);
|
||||
} catch (IdentityProviderException $e) {
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return new RedirectResponse($request->getSession()->remove('azure_return_path', '/'));
|
||||
}
|
||||
}
|
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\CalendarBundle\Controller;
|
||||
|
||||
use Chill\CalendarBundle\Messenger\Message\MSGraphChangeNotificationMessage;
|
||||
use JsonException;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use const JSON_THROW_ON_ERROR;
|
||||
|
||||
class RemoteCalendarMSGraphSyncController
|
||||
{
|
||||
private MessageBusInterface $messageBus;
|
||||
|
||||
public function __construct(MessageBusInterface $messageBus)
|
||||
{
|
||||
$this->messageBus = $messageBus;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/public/incoming-hook/calendar/msgraph/events/{userId}", name="chill_calendar_remote_msgraph_incoming_webhook_events",
|
||||
* methods={"POST"})
|
||||
*/
|
||||
public function webhookCalendarReceiver(int $userId, Request $request): Response
|
||||
{
|
||||
if ($request->query->has('validationToken')) {
|
||||
return new Response($request->query->get('validationToken'), Response::HTTP_OK, [
|
||||
'content-type' => 'text/plain',
|
||||
]);
|
||||
}
|
||||
|
||||
try {
|
||||
$body = json_decode($request->getContent(), true, 512, JSON_THROW_ON_ERROR);
|
||||
} catch (JsonException $e) {
|
||||
throw new BadRequestHttpException('could not decode json', $e);
|
||||
}
|
||||
|
||||
$this->messageBus->dispatch(new MSGraphChangeNotificationMessage($body, $userId));
|
||||
|
||||
return new Response('', Response::HTTP_ACCEPTED);
|
||||
}
|
||||
}
|
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\CalendarBundle\Controller;
|
||||
|
||||
use Chill\CalendarBundle\RemoteCalendar\Connector\RemoteCalendarConnectorInterface;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Pagination\PaginatorFactory;
|
||||
use Chill\MainBundle\Serializer\Model\Collection;
|
||||
use DateTimeImmutable;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
use function count;
|
||||
|
||||
/**
|
||||
* Contains method to get events (Calendar) from remote calendar.
|
||||
*/
|
||||
class RemoteCalendarProxyController
|
||||
{
|
||||
private PaginatorFactory $paginatorFactory;
|
||||
|
||||
private RemoteCalendarConnectorInterface $remoteCalendarConnector;
|
||||
|
||||
private SerializerInterface $serializer;
|
||||
|
||||
public function __construct(PaginatorFactory $paginatorFactory, RemoteCalendarConnectorInterface $remoteCalendarConnector, SerializerInterface $serializer)
|
||||
{
|
||||
$this->paginatorFactory = $paginatorFactory;
|
||||
$this->remoteCalendarConnector = $remoteCalendarConnector;
|
||||
$this->serializer = $serializer;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("api/1.0/calendar/proxy/calendar/by-user/{id}/events")
|
||||
*/
|
||||
public function listEventForCalendar(User $user, Request $request): Response
|
||||
{
|
||||
if (!$request->query->has('dateFrom')) {
|
||||
throw new BadRequestHttpException('You must provide a dateFrom parameter');
|
||||
}
|
||||
|
||||
if (false === $dateFrom = DateTimeImmutable::createFromFormat(
|
||||
DateTimeImmutable::ATOM,
|
||||
$request->query->get('dateFrom')
|
||||
)) {
|
||||
throw new BadRequestHttpException('dateFrom not parsable');
|
||||
}
|
||||
|
||||
if (!$request->query->has('dateTo')) {
|
||||
throw new BadRequestHttpException('You must provide a dateTo parameter');
|
||||
}
|
||||
|
||||
if (false === $dateTo = DateTimeImmutable::createFromFormat(
|
||||
DateTimeImmutable::ATOM,
|
||||
$request->query->get('dateTo')
|
||||
)) {
|
||||
throw new BadRequestHttpException('dateTo not parsable');
|
||||
}
|
||||
|
||||
$total = $this->remoteCalendarConnector->countEventsForUser($user, $dateFrom, $dateTo);
|
||||
$paginator = $this->paginatorFactory->create($total);
|
||||
|
||||
if (0 === $total) {
|
||||
return new JsonResponse(
|
||||
$this->serializer->serialize(new Collection([], $paginator), 'json'),
|
||||
JsonResponse::HTTP_OK,
|
||||
[],
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
$events = $this->remoteCalendarConnector->listEventsForUser(
|
||||
$user,
|
||||
$dateFrom,
|
||||
$dateTo,
|
||||
$paginator->getCurrentPageFirstItemNumber(),
|
||||
$paginator->getItemsPerPage()
|
||||
);
|
||||
|
||||
// in some case, we cannot paginate: we have to fetch all the items at once. We must avoid
|
||||
// further requests by forcing the number of items returned.
|
||||
if (count($events) > $paginator->getItemsPerPage()) {
|
||||
$paginator->setItemsPerPage(count($events));
|
||||
}
|
||||
|
||||
$collection = new Collection($events, $paginator);
|
||||
|
||||
return new JsonResponse(
|
||||
$this->serializer->serialize($collection, 'json', ['groups' => ['read']]),
|
||||
JsonResponse::HTTP_OK,
|
||||
[],
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
@@ -12,12 +12,18 @@ declare(strict_types=1);
|
||||
namespace Chill\CalendarBundle\DataFixtures\ORM;
|
||||
|
||||
use Chill\CalendarBundle\Entity\CalendarRange;
|
||||
use Chill\MainBundle\Entity\Address;
|
||||
use Chill\MainBundle\Entity\Country;
|
||||
use Chill\MainBundle\Entity\Location;
|
||||
use Chill\MainBundle\Entity\LocationType;
|
||||
use Chill\MainBundle\Entity\PostalCode;
|
||||
use Chill\MainBundle\Repository\UserRepository;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\Bundle\FixturesBundle\Fixture;
|
||||
use Doctrine\Bundle\FixturesBundle\FixtureGroupInterface;
|
||||
use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
|
||||
use Doctrine\Persistence\ObjectManager;
|
||||
use libphonenumber\PhoneNumberUtil;
|
||||
|
||||
class LoadCalendarRange extends Fixture implements FixtureGroupInterface, OrderedFixtureInterface
|
||||
{
|
||||
@@ -49,6 +55,24 @@ class LoadCalendarRange extends Fixture implements FixtureGroupInterface, Ordere
|
||||
|
||||
$users = $this->userRepository->findAll();
|
||||
|
||||
$location = (new Location())
|
||||
->setAddress($address = new Address())
|
||||
->setName('Centre A')
|
||||
->setEmail('centreA@test.chill.social')
|
||||
->setLocationType($type = new LocationType())
|
||||
->setPhonenumber1(PhoneNumberUtil::getInstance()->parse('+3287653812'));
|
||||
$type->setTitle('Service');
|
||||
$address->setStreet('Rue des Épaules')->setStreetNumber('14')
|
||||
->setPostcode($postCode = new PostalCode());
|
||||
$postCode->setCode('4145')->setName('Houte-Si-Plout')->setCountry(
|
||||
($country = new Country())->setName(['fr' => 'Pays'])->setCountryCode('ZZ')
|
||||
);
|
||||
$manager->persist($country);
|
||||
$manager->persist($postCode);
|
||||
$manager->persist($address);
|
||||
$manager->persist($type);
|
||||
$manager->persist($location);
|
||||
|
||||
$days = [
|
||||
'2021-08-23',
|
||||
'2021-08-24',
|
||||
@@ -58,6 +82,8 @@ class LoadCalendarRange extends Fixture implements FixtureGroupInterface, Ordere
|
||||
'2021-08-31',
|
||||
'2021-09-01',
|
||||
'2021-09-02',
|
||||
(new DateTimeImmutable('tomorrow'))->format('Y-m-d'),
|
||||
(new DateTimeImmutable('today'))->format('Y-m-d'),
|
||||
];
|
||||
|
||||
$hours = [
|
||||
@@ -76,7 +102,8 @@ class LoadCalendarRange extends Fixture implements FixtureGroupInterface, Ordere
|
||||
$calendarRange = (new CalendarRange())
|
||||
->setUser($u)
|
||||
->setStartDate($startEvent)
|
||||
->setEndDate($endEvent);
|
||||
->setEndDate($endEvent)
|
||||
->setLocation($location);
|
||||
|
||||
$manager->persist($calendarRange);
|
||||
}
|
||||
|
@@ -36,6 +36,15 @@ class ChillCalendarExtension extends Extension implements PrependExtensionInterf
|
||||
$loader->load('services/fixtures.yml');
|
||||
$loader->load('services/form.yml');
|
||||
$loader->load('services/event.yml');
|
||||
$loader->load('services/remote_calendar.yaml');
|
||||
|
||||
$container->setParameter('chill_calendar', $config);
|
||||
|
||||
if ($config['short_messages']['enabled']) {
|
||||
$container->setParameter('chill_calendar.short_messages', $config['short_messages']);
|
||||
} else {
|
||||
$container->setParameter('chill_calendar.short_messages', null);
|
||||
}
|
||||
}
|
||||
|
||||
public function prepend(ContainerBuilder $container)
|
||||
|
@@ -26,9 +26,22 @@ class Configuration implements ConfigurationInterface
|
||||
$treeBuilder = new TreeBuilder('chill_calendar');
|
||||
$rootNode = $treeBuilder->getRootNode('chill_calendar');
|
||||
|
||||
// Here you should define the parameters that are allowed to
|
||||
// configure your bundle. See the documentation linked above for
|
||||
// more information on that topic.
|
||||
$rootNode
|
||||
->children()
|
||||
->arrayNode('short_messages')
|
||||
->canBeDisabled()
|
||||
->children()->end()
|
||||
->end() // end for short_messages
|
||||
->arrayNode('remote_calendars_sync')->canBeEnabled()
|
||||
->children()
|
||||
->arrayNode('microsoft_graph')->canBeEnabled()
|
||||
->children()
|
||||
->end() // end of machine_access_token
|
||||
->end() // end of microsoft_graph children
|
||||
->end() // end of array microsoft_graph
|
||||
->end() // end of children's remote_calendars_sync
|
||||
->end() // end of array remote_calendars_sync
|
||||
->end();
|
||||
|
||||
return $treeBuilder;
|
||||
}
|
||||
|
@@ -12,7 +12,10 @@ declare(strict_types=1);
|
||||
namespace Chill\CalendarBundle\Entity;
|
||||
|
||||
use Chill\ActivityBundle\Entity\Activity;
|
||||
use Chill\CalendarBundle\Repository\CalendarRepository;
|
||||
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
|
||||
use Chill\MainBundle\Doctrine\Model\TrackCreationTrait;
|
||||
use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
|
||||
use Chill\MainBundle\Doctrine\Model\TrackUpdateTrait;
|
||||
use Chill\MainBundle\Entity\Embeddable\CommentEmbeddable;
|
||||
use Chill\MainBundle\Entity\Embeddable\PrivateCommentEmbeddable;
|
||||
use Chill\MainBundle\Entity\Location;
|
||||
@@ -20,33 +23,71 @@ use Chill\MainBundle\Entity\User;
|
||||
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Chill\ThirdPartyBundle\Entity\ThirdParty;
|
||||
use DateInterval;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\Common\Collections\Criteria;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use LogicException;
|
||||
use Symfony\Component\Serializer\Annotation as Serializer;
|
||||
use Symfony\Component\Serializer\Annotation\Groups;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
use Symfony\Component\Validator\Constraints\NotBlank;
|
||||
|
||||
use Symfony\Component\Validator\Constraints\Range;
|
||||
use Symfony\Component\Validator\Mapping\ClassMetadata;
|
||||
|
||||
use function in_array;
|
||||
|
||||
/**
|
||||
* @ORM\Table(name="chill_calendar.calendar")
|
||||
* @ORM\Entity(repositoryClass=CalendarRepository::class)
|
||||
* @ORM\Table(
|
||||
* name="chill_calendar.calendar",
|
||||
* uniqueConstraints={@ORM\UniqueConstraint(name="idx_calendar_remote", columns={"remoteId"}, options={"where": "remoteId <> ''"})}
|
||||
* )
|
||||
* @ORM\Entity
|
||||
*/
|
||||
class Calendar
|
||||
class Calendar implements TrackCreationInterface, TrackUpdateInterface
|
||||
{
|
||||
use RemoteCalendarTrait;
|
||||
|
||||
use TrackCreationTrait;
|
||||
|
||||
use TrackUpdateTrait;
|
||||
|
||||
public const SMS_CANCEL_PENDING = 'sms_cancel_pending';
|
||||
|
||||
public const SMS_PENDING = 'sms_pending';
|
||||
|
||||
public const SMS_SENT = 'sms_sent';
|
||||
|
||||
public const STATUS_CANCELED = 'canceled';
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
public const STATUS_MOVED = 'moved';
|
||||
|
||||
public const STATUS_VALID = 'valid';
|
||||
|
||||
/**
|
||||
* @ORM\ManyToOne(targetEntity="Chill\PersonBundle\Entity\AccompanyingPeriod")
|
||||
* @Groups({"read"})
|
||||
* a list of invite which have been added during this session.
|
||||
*
|
||||
* @var array|Invite[]
|
||||
*/
|
||||
public array $newInvites = [];
|
||||
|
||||
/**
|
||||
* a list of invite which have been removed during this session.
|
||||
*
|
||||
* @var array|Invite[]
|
||||
*/
|
||||
public array $oldInvites = [];
|
||||
|
||||
public ?CalendarRange $previousCalendarRange = null;
|
||||
|
||||
public ?User $previousMainUser = null;
|
||||
|
||||
/**
|
||||
* @ORM\ManyToOne(targetEntity="Chill\PersonBundle\Entity\AccompanyingPeriod", inversedBy="calendars")
|
||||
*/
|
||||
private AccompanyingPeriod $accompanyingPeriod;
|
||||
|
||||
@@ -56,7 +97,8 @@ class Calendar
|
||||
private ?Activity $activity = null;
|
||||
|
||||
/**
|
||||
* @ORM\ManyToOne(targetEntity="CalendarRange", inversedBy="calendars")
|
||||
* @ORM\OneToOne(targetEntity="CalendarRange", inversedBy="calendar")
|
||||
* @Serializer\Groups({"calendar:read", "read"})
|
||||
*/
|
||||
private ?CalendarRange $calendarRange = null;
|
||||
|
||||
@@ -67,13 +109,14 @@ class Calendar
|
||||
|
||||
/**
|
||||
* @ORM\Embedded(class=CommentEmbeddable::class, columnPrefix="comment_")
|
||||
* @Serializer\Groups({"calendar:read"})
|
||||
* @Serializer\Groups({"calendar:read", "read"})
|
||||
*/
|
||||
private CommentEmbeddable $comment;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="datetimetz_immutable")
|
||||
* @Serializer\Groups({"calendar:read"})
|
||||
* @ORM\Column(type="datetime_immutable", nullable=false)
|
||||
* @Serializer\Groups({"calendar:read", "read", "calendar:light"})
|
||||
* @Assert\NotNull(message="calendar.An end date is required")
|
||||
*/
|
||||
private ?DateTimeImmutable $endDate = null;
|
||||
|
||||
@@ -81,38 +124,43 @@ class Calendar
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue
|
||||
* @ORM\Column(type="integer")
|
||||
* @Serializer\Groups({"calendar:read"})
|
||||
* @Serializer\Groups({"calendar:read", "read", "calendar:light"})
|
||||
*/
|
||||
private ?int $id;
|
||||
private ?int $id = null;
|
||||
|
||||
/**
|
||||
* @ORM\ManyToMany(
|
||||
* targetEntity="Invite",
|
||||
* cascade={"persist", "remove", "merge", "detach"})
|
||||
* @ORM\OneToMany(
|
||||
* targetEntity=Invite::class,
|
||||
* mappedBy="calendar",
|
||||
* orphanRemoval=true,
|
||||
* cascade={"persist", "remove", "merge", "detach"}
|
||||
* )
|
||||
* @ORM\JoinTable(name="chill_calendar.calendar_to_invites")
|
||||
* @Groups({"read"})
|
||||
* @Serializer\Groups({"read"})
|
||||
*/
|
||||
private Collection $invites;
|
||||
|
||||
/**
|
||||
* @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\Location")
|
||||
* @groups({"read"})
|
||||
* @Serializer\Groups({"read"})
|
||||
* @Assert\NotNull(message="calendar.A location is required")
|
||||
*/
|
||||
private ?Location $location = null;
|
||||
|
||||
/**
|
||||
* @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\User")
|
||||
* @Serializer\Groups({"calendar:read"})
|
||||
* @Serializer\Groups({"calendar:read", "read", "calendar:light"})
|
||||
* @Serializer\Context(normalizationContext={"read"}, groups={"calendar:light"})
|
||||
* @Assert\NotNull(message="calendar.A main user is mandatory")
|
||||
*/
|
||||
private ?User $mainUser;
|
||||
private ?User $mainUser = null;
|
||||
|
||||
/**
|
||||
* @ORM\ManyToMany(
|
||||
* targetEntity="Chill\PersonBundle\Entity\Person",
|
||||
* cascade={"persist", "remove", "merge", "detach"})
|
||||
* @ORM\ManyToMany(targetEntity="Chill\PersonBundle\Entity\Person", inversedBy="calendars")
|
||||
* @ORM\JoinTable(name="chill_calendar.calendar_to_persons")
|
||||
* @Groups({"read"})
|
||||
* @Serializer\Groups({"calendar:read"})
|
||||
* @Serializer\Groups({"calendar:read", "read", "calendar:light"})
|
||||
* @Serializer\Context(normalizationContext={"read"}, groups={"calendar:light"})
|
||||
* @Assert\Count(min=1, minMessage="calendar.At least {{ limit }} person is required.")
|
||||
*/
|
||||
private Collection $persons;
|
||||
|
||||
@@ -123,37 +171,37 @@ class Calendar
|
||||
private PrivateCommentEmbeddable $privateComment;
|
||||
|
||||
/**
|
||||
* @ORM\ManyToMany(
|
||||
* targetEntity="Chill\ThirdPartyBundle\Entity\ThirdParty",
|
||||
* cascade={"persist", "remove", "merge", "detach"})
|
||||
* @ORM\ManyToMany(targetEntity="Chill\ThirdPartyBundle\Entity\ThirdParty")
|
||||
* @ORM\JoinTable(name="chill_calendar.calendar_to_thirdparties")
|
||||
* @Groups({"read"})
|
||||
* @Serializer\Groups({"calendar:read"})
|
||||
* @Serializer\Groups({"calendar:read", "read", "calendar:light"})
|
||||
* @Serializer\Context(normalizationContext={"read"}, groups={"calendar:light"})
|
||||
*/
|
||||
private Collection $professionals;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="boolean", nullable=true)
|
||||
*/
|
||||
private ?bool $sendSMS;
|
||||
private ?bool $sendSMS = false;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="datetimetz_immutable")
|
||||
* @Serializer\Groups({"calendar:read"})
|
||||
* @ORM\Column(type="text", nullable=false, options={"default": Calendar::SMS_PENDING})
|
||||
*/
|
||||
private string $smsStatus = self::SMS_PENDING;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="datetime_immutable", nullable=false)
|
||||
* @Serializer\Groups({"calendar:read", "read", "calendar:light"})
|
||||
* @Serializer\Context(normalizationContext={"read"}, groups={"calendar:light"})
|
||||
* @Assert\NotNull(message="calendar.A start date is required")
|
||||
*/
|
||||
private ?DateTimeImmutable $startDate = null;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="string", length=255)
|
||||
* @ORM\Column(type="string", length=255, nullable=false, options={"default": "valid"})
|
||||
* @Serializer\Groups({"calendar:read", "read", "calendar:light"})
|
||||
* @Serializer\Context(normalizationContext={"read"}, groups={"calendar:light"})
|
||||
*/
|
||||
private ?string $status = null;
|
||||
|
||||
/**
|
||||
* @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\User")
|
||||
* @Groups({"read"})
|
||||
* @Serializer\Groups({"calendar:read"})
|
||||
*/
|
||||
private ?User $user = null;
|
||||
private string $status = self::STATUS_VALID;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
@@ -164,28 +212,41 @@ class Calendar
|
||||
$this->invites = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function addInvite(?Invite $invite): self
|
||||
/**
|
||||
* @internal Use {@link (Calendar::addUser)} instead
|
||||
*/
|
||||
public function addInvite(Invite $invite): self
|
||||
{
|
||||
if (null !== $invite) {
|
||||
$this->invites[] = $invite;
|
||||
if ($invite->getCalendar() instanceof Calendar && $invite->getCalendar() !== $this) {
|
||||
throw new LogicException('Not allowed to move an invitation to another Calendar');
|
||||
}
|
||||
|
||||
$this->invites[] = $invite;
|
||||
$this->newInvites[] = $invite;
|
||||
|
||||
$invite->setCalendar($this);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addPerson(?Person $person): self
|
||||
public function addPerson(Person $person): self
|
||||
{
|
||||
if (null !== $person) {
|
||||
$this->persons[] = $person;
|
||||
}
|
||||
$this->persons[] = $person;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addProfessional(?ThirdParty $professional): self
|
||||
public function addProfessional(ThirdParty $professional): self
|
||||
{
|
||||
if (null !== $professional) {
|
||||
$this->professionals[] = $professional;
|
||||
$this->professionals[] = $professional;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addUser(User $user): self
|
||||
{
|
||||
if (!$this->getUsers()->contains($user) && $this->getMainUser() !== $user) {
|
||||
$this->addInvite((new Invite())->setUser($user));
|
||||
}
|
||||
|
||||
return $this;
|
||||
@@ -216,6 +277,15 @@ class Calendar
|
||||
return $this->comment;
|
||||
}
|
||||
|
||||
public function getDuration(): ?DateInterval
|
||||
{
|
||||
if ($this->getStartDate() === null || $this->getEndDate() === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->getStartDate()->diff($this->getEndDate());
|
||||
}
|
||||
|
||||
public function getEndDate(): ?DateTimeImmutable
|
||||
{
|
||||
return $this->endDate;
|
||||
@@ -226,6 +296,21 @@ class Calendar
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getInviteForUser(User $user): ?Invite
|
||||
{
|
||||
$criteria = Criteria::create();
|
||||
$criteria->where(Criteria::expr()->eq('user', $user));
|
||||
|
||||
$matchings = $this->invites
|
||||
->matching($criteria);
|
||||
|
||||
if (1 === $matchings->count()) {
|
||||
return $matchings->first();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection|Invite[]
|
||||
*/
|
||||
@@ -304,6 +389,11 @@ class Calendar
|
||||
return $this->sendSMS;
|
||||
}
|
||||
|
||||
public function getSmsStatus(): string
|
||||
{
|
||||
return $this->smsStatus;
|
||||
}
|
||||
|
||||
public function getStartDate(): ?DateTimeImmutable
|
||||
{
|
||||
return $this->startDate;
|
||||
@@ -319,14 +409,35 @@ class Calendar
|
||||
return $this->getProfessionals();
|
||||
}
|
||||
|
||||
public function getUser(): ?User
|
||||
/**
|
||||
* @return Collection|User[]
|
||||
* @Serializer\Groups({"calendar:read", "read"})
|
||||
*/
|
||||
public function getUsers(): Collection
|
||||
{
|
||||
return $this->user;
|
||||
return $this->getInvites()->map(static function (Invite $i) { return $i->getUser(); });
|
||||
}
|
||||
|
||||
public function getusers(): Collection
|
||||
public function hasCalendarRange(): bool
|
||||
{
|
||||
return $this->getInvites(); //TODO get users of the invite
|
||||
return null !== $this->calendarRange;
|
||||
}
|
||||
|
||||
public function hasLocation(): bool
|
||||
{
|
||||
return null !== $this->getLocation();
|
||||
}
|
||||
|
||||
/**
|
||||
* return true if the user is invited.
|
||||
*/
|
||||
public function isInvited(User $user): bool
|
||||
{
|
||||
if ($this->getMainUser() === $user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->getUsers()->contains($user);
|
||||
}
|
||||
|
||||
public static function loadValidatorMetadata(ClassMetadata $metadata): void
|
||||
@@ -343,9 +454,15 @@ class Calendar
|
||||
]));
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal Use {@link (Calendar::removeUser)} instead
|
||||
*/
|
||||
public function removeInvite(Invite $invite): self
|
||||
{
|
||||
$this->invites->removeElement($invite);
|
||||
if ($this->invites->removeElement($invite)) {
|
||||
$invite->setCalendar(null);
|
||||
$this->oldInvites[] = $invite;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
@@ -364,6 +481,20 @@ class Calendar
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeUser(User $user): self
|
||||
{
|
||||
if (!$this->getUsers()->contains($user)) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$invite = $this->invites
|
||||
->filter(static function (Invite $invite) use ($user) { return $invite->getUser() === $user; })
|
||||
->first();
|
||||
$this->removeInvite($invite);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setAccompanyingPeriod(?AccompanyingPeriod $accompanyingPeriod): self
|
||||
{
|
||||
$this->accompanyingPeriod = $accompanyingPeriod;
|
||||
@@ -380,8 +511,20 @@ class Calendar
|
||||
|
||||
public function setCalendarRange(?CalendarRange $calendarRange): self
|
||||
{
|
||||
if ($this->calendarRange !== $calendarRange) {
|
||||
$this->previousCalendarRange = $this->calendarRange;
|
||||
|
||||
if (null !== $this->previousCalendarRange) {
|
||||
$this->previousCalendarRange->setCalendar(null);
|
||||
}
|
||||
}
|
||||
|
||||
$this->calendarRange = $calendarRange;
|
||||
|
||||
if ($this->calendarRange instanceof CalendarRange) {
|
||||
$this->calendarRange->setCalendar($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -415,7 +558,12 @@ class Calendar
|
||||
|
||||
public function setMainUser(?User $mainUser): self
|
||||
{
|
||||
if ($this->mainUser !== $mainUser) {
|
||||
$this->previousMainUser = $this->mainUser;
|
||||
}
|
||||
|
||||
$this->mainUser = $mainUser;
|
||||
$this->removeUser($mainUser);
|
||||
|
||||
return $this;
|
||||
}
|
||||
@@ -434,6 +582,13 @@ class Calendar
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setSmsStatus(string $smsStatus): self
|
||||
{
|
||||
$this->smsStatus = $smsStatus;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setStartDate(DateTimeImmutable $startDate): self
|
||||
{
|
||||
$this->startDate = $startDate;
|
||||
@@ -445,12 +600,9 @@ class Calendar
|
||||
{
|
||||
$this->status = $status;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setUser(?User $user): self
|
||||
{
|
||||
$this->user = $user;
|
||||
if (self::STATUS_CANCELED === $status && $this->getSmsStatus() === self::SMS_SENT) {
|
||||
$this->setSmsStatus(self::SMS_CANCEL_PENDING);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
@@ -11,29 +11,41 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\CalendarBundle\Entity;
|
||||
|
||||
use Chill\CalendarBundle\Repository\CalendarRangeRepository;
|
||||
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
|
||||
use Chill\MainBundle\Doctrine\Model\TrackCreationTrait;
|
||||
use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
|
||||
use Chill\MainBundle\Doctrine\Model\TrackUpdateTrait;
|
||||
use Chill\MainBundle\Entity\Location;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Serializer\Annotation\Groups;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* @ORM\Table(name="chill_calendar.calendar_range")
|
||||
* @ORM\Entity(repositoryClass=CalendarRangeRepository::class)
|
||||
* @ORM\Table(
|
||||
* name="chill_calendar.calendar_range",
|
||||
* uniqueConstraints={@ORM\UniqueConstraint(name="idx_calendar_range_remote", columns={"remoteId"}, options={"where": "remoteId <> ''"})}
|
||||
* )
|
||||
* @ORM\Entity
|
||||
*/
|
||||
class CalendarRange
|
||||
class CalendarRange implements TrackCreationInterface, TrackUpdateInterface
|
||||
{
|
||||
/**
|
||||
* @ORM\OneToMany(targetEntity=Calendar::class,
|
||||
* mappedBy="calendarRange")
|
||||
*/
|
||||
private Collection $calendars;
|
||||
use RemoteCalendarTrait;
|
||||
|
||||
use TrackCreationTrait;
|
||||
|
||||
use TrackUpdateTrait;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="datetimetz_immutable")
|
||||
* @groups({"read", "write"})
|
||||
* @ORM\OneToOne(targetEntity=Calendar::class, mappedBy="calendarRange")
|
||||
*/
|
||||
private ?Calendar $calendar = null;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="datetime_immutable", nullable=false)
|
||||
* @Groups({"read", "write", "calendar:read"})
|
||||
* @Assert\NotNull
|
||||
*/
|
||||
private ?DateTimeImmutable $endDate = null;
|
||||
|
||||
@@ -41,27 +53,35 @@ class CalendarRange
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue
|
||||
* @ORM\Column(type="integer")
|
||||
* @groups({"read"})
|
||||
* @Groups({"read"})
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="datetimetz_immutable")
|
||||
* @groups({"read", "write"})
|
||||
* @ORM\ManyToOne(targetEntity=Location::class)
|
||||
* @ORM\JoinColumn(nullable=false)
|
||||
* @Groups({"read", "write", "calendar:read"})
|
||||
* @Assert\NotNull
|
||||
*/
|
||||
private ?Location $location;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="datetime_immutable", nullable=false)
|
||||
* @groups({"read", "write", "calendar:read"})
|
||||
* @Assert\NotNull
|
||||
*/
|
||||
private ?DateTimeImmutable $startDate = null;
|
||||
|
||||
/**
|
||||
* @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\User")
|
||||
* @groups({"read", "write"})
|
||||
* @Groups({"read", "write", "calendar:read"})
|
||||
* @Assert\NotNull
|
||||
*/
|
||||
private ?User $user = null;
|
||||
|
||||
//TODO Lieu
|
||||
|
||||
public function __construct()
|
||||
public function getCalendar(): ?Calendar
|
||||
{
|
||||
$this->calendars = new ArrayCollection();
|
||||
return $this->calendar;
|
||||
}
|
||||
|
||||
public function getEndDate(): ?DateTimeImmutable
|
||||
@@ -74,6 +94,11 @@ class CalendarRange
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getLocation(): ?Location
|
||||
{
|
||||
return $this->location;
|
||||
}
|
||||
|
||||
public function getStartDate(): ?DateTimeImmutable
|
||||
{
|
||||
return $this->startDate;
|
||||
@@ -84,6 +109,14 @@ class CalendarRange
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal use {@link (Calendar::setCalendarRange)} instead
|
||||
*/
|
||||
public function setCalendar(?Calendar $calendar): void
|
||||
{
|
||||
$this->calendar = $calendar;
|
||||
}
|
||||
|
||||
public function setEndDate(DateTimeImmutable $endDate): self
|
||||
{
|
||||
$this->endDate = $endDate;
|
||||
@@ -91,6 +124,13 @@ class CalendarRange
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setLocation(?Location $location): self
|
||||
{
|
||||
$this->location = $location;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setStartDate(DateTimeImmutable $startDate): self
|
||||
{
|
||||
$this->startDate = $startDate;
|
||||
|
@@ -11,39 +11,90 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\CalendarBundle\Entity;
|
||||
|
||||
use Chill\CalendarBundle\Repository\InviteRepository;
|
||||
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
|
||||
use Chill\MainBundle\Doctrine\Model\TrackCreationTrait;
|
||||
use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
|
||||
use Chill\MainBundle\Doctrine\Model\TrackUpdateTrait;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use LogicException;
|
||||
use Symfony\Component\Serializer\Annotation as Serializer;
|
||||
|
||||
/**
|
||||
* @ORM\Table(name="chill_calendar.invite")
|
||||
* @ORM\Entity(repositoryClass=InviteRepository::class)
|
||||
* An invitation for another user to a Calendar.
|
||||
*
|
||||
* The event/calendar in the user may have a different id than the mainUser. We add then fields to store the
|
||||
* remote id of this event in the remote calendar.
|
||||
*
|
||||
* @ORM\Table(
|
||||
* name="chill_calendar.invite",
|
||||
* uniqueConstraints={@ORM\UniqueConstraint(name="idx_calendar_invite_remote", columns={"remoteId"}, options={"where": "remoteId <> ''"})}
|
||||
* )
|
||||
* @ORM\Entity
|
||||
*/
|
||||
class Invite
|
||||
class Invite implements TrackUpdateInterface, TrackCreationInterface
|
||||
{
|
||||
use RemoteCalendarTrait;
|
||||
|
||||
use TrackCreationTrait;
|
||||
|
||||
use TrackUpdateTrait;
|
||||
|
||||
public const ACCEPTED = 'accepted';
|
||||
|
||||
public const DECLINED = 'declined';
|
||||
|
||||
public const PENDING = 'pending';
|
||||
|
||||
/**
|
||||
* all statuses in one const.
|
||||
*/
|
||||
public const STATUSES = [
|
||||
self::ACCEPTED,
|
||||
self::DECLINED,
|
||||
self::PENDING,
|
||||
self::TENTATIVELY_ACCEPTED,
|
||||
];
|
||||
|
||||
public const TENTATIVELY_ACCEPTED = 'tentative';
|
||||
|
||||
/**
|
||||
* @ORM\ManyToOne(targetEntity=Calendar::class, inversedBy="invites")
|
||||
*/
|
||||
private ?Calendar $calendar = null;
|
||||
|
||||
/**
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue
|
||||
* @ORM\Column(type="integer")
|
||||
* @Serializer\Groups(groups={"calendar:read", "read"})
|
||||
*/
|
||||
private $id;
|
||||
private ?int $id = null;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="json")
|
||||
* @ORM\Column(type="text", nullable=false, options={"default": "pending"})
|
||||
* @Serializer\Groups(groups={"calendar:read", "read"})
|
||||
*/
|
||||
private array $status = [];
|
||||
private string $status = self::PENDING;
|
||||
|
||||
/**
|
||||
* @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\User")
|
||||
* @ORM\JoinColumn(nullable=false)
|
||||
* @Serializer\Groups(groups={"calendar:read", "read"})
|
||||
*/
|
||||
private User $user;
|
||||
private ?User $user = null;
|
||||
|
||||
public function getCalendar(): ?Calendar
|
||||
{
|
||||
return $this->calendar;
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getStatus(): ?array
|
||||
public function getStatus(): string
|
||||
{
|
||||
return $this->status;
|
||||
}
|
||||
@@ -53,7 +104,15 @@ class Invite
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
public function setStatus(array $status): self
|
||||
/**
|
||||
* @internal use Calendar::addInvite instead
|
||||
*/
|
||||
public function setCalendar(?Calendar $calendar): void
|
||||
{
|
||||
$this->calendar = $calendar;
|
||||
}
|
||||
|
||||
public function setStatus(string $status): self
|
||||
{
|
||||
$this->status = $status;
|
||||
|
||||
@@ -62,6 +121,10 @@ class Invite
|
||||
|
||||
public function setUser(?User $user): self
|
||||
{
|
||||
if ($user instanceof User && $this->user instanceof User && $user !== $this->user) {
|
||||
throw new LogicException('Not allowed to associate an invite to a different user');
|
||||
}
|
||||
|
||||
$this->user = $user;
|
||||
|
||||
return $this;
|
||||
|
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\CalendarBundle\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
trait RemoteCalendarTrait
|
||||
{
|
||||
/**
|
||||
* If true, the changes won't be enqueued to remote.
|
||||
*
|
||||
* This is required to prevent update loop: a persist trigger an event creation on remote,
|
||||
* which in turn change remoteId and, in turn, trigger an update change, ...
|
||||
*/
|
||||
public bool $preventEnqueueChanges = false;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="json", options={"default": "[]"}, nullable=false)
|
||||
*/
|
||||
private array $remoteAttributes = [];
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="text", options={"default": ""}, nullable=false)
|
||||
*/
|
||||
private string $remoteId = '';
|
||||
|
||||
public function addRemoteAttributes(array $remoteAttributes): self
|
||||
{
|
||||
$this->remoteAttributes = array_merge($this->remoteAttributes, $remoteAttributes);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getRemoteAttributes(): array
|
||||
{
|
||||
return $this->remoteAttributes;
|
||||
}
|
||||
|
||||
public function getRemoteId(): string
|
||||
{
|
||||
return $this->remoteId;
|
||||
}
|
||||
|
||||
public function hasRemoteId(): bool
|
||||
{
|
||||
return '' !== $this->remoteId;
|
||||
}
|
||||
|
||||
public function setRemoteId(string $remoteId): self
|
||||
{
|
||||
$this->remoteId = $remoteId;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
@@ -12,18 +12,16 @@ declare(strict_types=1);
|
||||
namespace Chill\CalendarBundle\Form;
|
||||
|
||||
use Chill\CalendarBundle\Entity\Calendar;
|
||||
use Chill\CalendarBundle\Entity\CalendarRange;
|
||||
use Chill\CalendarBundle\Entity\CancelReason;
|
||||
use Chill\CalendarBundle\Entity\Invite;
|
||||
use Chill\MainBundle\Entity\Location;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\CalendarBundle\Form\DataTransformer\IdToCalendarRangeDataTransformer;
|
||||
use Chill\MainBundle\Form\DataTransformer\IdToLocationDataTransformer;
|
||||
use Chill\MainBundle\Form\DataTransformer\IdToUserDataTransformer;
|
||||
use Chill\MainBundle\Form\DataTransformer\IdToUsersDataTransformer;
|
||||
use Chill\MainBundle\Form\Type\CommentType;
|
||||
use Chill\MainBundle\Form\Type\PrivateCommentType;
|
||||
use Chill\MainBundle\Templating\TranslatableStringHelper;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Chill\ThirdPartyBundle\Entity\ThirdParty;
|
||||
use Chill\PersonBundle\Form\DataTransformer\PersonsToIdDataTransformer;
|
||||
use Chill\ThirdPartyBundle\Form\DataTransformer\ThirdPartiesToIdDataTransformer;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\Persistence\ObjectManager;
|
||||
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\CallbackTransformer;
|
||||
@@ -34,16 +32,32 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class CalendarType extends AbstractType
|
||||
{
|
||||
protected ObjectManager $om;
|
||||
private IdToCalendarRangeDataTransformer $calendarRangeDataTransformer;
|
||||
|
||||
protected TranslatableStringHelper $translatableStringHelper;
|
||||
private IdToLocationDataTransformer $idToLocationDataTransformer;
|
||||
|
||||
private IdToUserDataTransformer $idToUserDataTransformer;
|
||||
|
||||
private IdToUsersDataTransformer $idToUsersDataTransformer;
|
||||
|
||||
private ThirdPartiesToIdDataTransformer $partiesToIdDataTransformer;
|
||||
|
||||
private PersonsToIdDataTransformer $personsToIdDataTransformer;
|
||||
|
||||
public function __construct(
|
||||
TranslatableStringHelper $translatableStringHelper,
|
||||
ObjectManager $om
|
||||
PersonsToIdDataTransformer $personsToIdDataTransformer,
|
||||
IdToUserDataTransformer $idToUserDataTransformer,
|
||||
IdToUsersDataTransformer $idToUsersDataTransformer,
|
||||
IdToLocationDataTransformer $idToLocationDataTransformer,
|
||||
ThirdPartiesToIdDataTransformer $partiesToIdDataTransformer,
|
||||
IdToCalendarRangeDataTransformer $idToCalendarRangeDataTransformer
|
||||
) {
|
||||
$this->translatableStringHelper = $translatableStringHelper;
|
||||
$this->om = $om;
|
||||
$this->personsToIdDataTransformer = $personsToIdDataTransformer;
|
||||
$this->idToUserDataTransformer = $idToUserDataTransformer;
|
||||
$this->idToUsersDataTransformer = $idToUsersDataTransformer;
|
||||
$this->idToLocationDataTransformer = $idToLocationDataTransformer;
|
||||
$this->partiesToIdDataTransformer = $partiesToIdDataTransformer;
|
||||
$this->calendarRangeDataTransformer = $idToCalendarRangeDataTransformer;
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
@@ -64,7 +78,6 @@ class CalendarType extends AbstractType
|
||||
// },
|
||||
// ])
|
||||
->add('sendSMS', ChoiceType::class, [
|
||||
'required' => false,
|
||||
'choices' => [
|
||||
'Oui' => true,
|
||||
'Non' => false,
|
||||
@@ -73,36 +86,26 @@ class CalendarType extends AbstractType
|
||||
]);
|
||||
|
||||
$builder->add('mainUser', HiddenType::class);
|
||||
$builder->get('mainUser')
|
||||
->addModelTransformer(new CallbackTransformer(
|
||||
static function (?User $user): int {
|
||||
if (null !== $user) {
|
||||
$res = $user->getId();
|
||||
} else {
|
||||
$res = -1; //TODO cannot be null in any ways...
|
||||
}
|
||||
|
||||
return $res;
|
||||
},
|
||||
function (?int $userId): User {
|
||||
return $this->om->getRepository(user::class)->findOneBy(['id' => (int) $userId]);
|
||||
}
|
||||
));
|
||||
$builder->get('mainUser')->addModelTransformer($this->idToUserDataTransformer);
|
||||
|
||||
$builder->add('startDate', HiddenType::class);
|
||||
$builder->get('startDate')
|
||||
->addModelTransformer(new CallbackTransformer(
|
||||
static function (?DateTimeImmutable $dateTimeImmutable): string {
|
||||
if (null !== $dateTimeImmutable) {
|
||||
$res = date_format($dateTimeImmutable, 'Y-m-d H:i:s');
|
||||
$res = date_format($dateTimeImmutable, DateTimeImmutable::ATOM);
|
||||
} else {
|
||||
$res = '';
|
||||
}
|
||||
|
||||
return $res;
|
||||
},
|
||||
static function (?string $dateAsString): DateTimeImmutable {
|
||||
return new DateTimeImmutable($dateAsString);
|
||||
static function (?string $dateAsString): ?DateTimeImmutable {
|
||||
if ('' === $dateAsString || null === $dateAsString) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return DateTimeImmutable::createFromFormat(DateTimeImmutable::ATOM, $dateAsString);
|
||||
}
|
||||
));
|
||||
|
||||
@@ -111,115 +114,41 @@ class CalendarType extends AbstractType
|
||||
->addModelTransformer(new CallbackTransformer(
|
||||
static function (?DateTimeImmutable $dateTimeImmutable): string {
|
||||
if (null !== $dateTimeImmutable) {
|
||||
$res = date_format($dateTimeImmutable, 'Y-m-d H:i:s');
|
||||
$res = date_format($dateTimeImmutable, DateTimeImmutable::ATOM);
|
||||
} else {
|
||||
$res = '';
|
||||
}
|
||||
|
||||
return $res;
|
||||
},
|
||||
static function (?string $dateAsString): DateTimeImmutable {
|
||||
return new DateTimeImmutable($dateAsString);
|
||||
static function (?string $dateAsString): ?DateTimeImmutable {
|
||||
if ('' === $dateAsString || null === $dateAsString) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return DateTimeImmutable::createFromFormat(DateTimeImmutable::ATOM, $dateAsString);
|
||||
}
|
||||
));
|
||||
|
||||
$builder->add('persons', HiddenType::class);
|
||||
$builder->get('persons')
|
||||
->addModelTransformer(new CallbackTransformer(
|
||||
static function (iterable $personsAsIterable): string {
|
||||
$personIds = [];
|
||||
|
||||
foreach ($personsAsIterable as $value) {
|
||||
$personIds[] = $value->getId();
|
||||
}
|
||||
|
||||
return implode(',', $personIds);
|
||||
},
|
||||
function (?string $personsAsString): array {
|
||||
return array_map(
|
||||
fn (string $id): ?Person => $this->om->getRepository(Person::class)->findOneBy(['id' => (int) $id]),
|
||||
explode(',', $personsAsString)
|
||||
);
|
||||
}
|
||||
));
|
||||
->addModelTransformer($this->personsToIdDataTransformer);
|
||||
|
||||
$builder->add('professionals', HiddenType::class);
|
||||
$builder->get('professionals')
|
||||
->addModelTransformer(new CallbackTransformer(
|
||||
static function (iterable $thirdpartyAsIterable): string {
|
||||
$thirdpartyIds = [];
|
||||
->addModelTransformer($this->partiesToIdDataTransformer);
|
||||
|
||||
foreach ($thirdpartyAsIterable as $value) {
|
||||
$thirdpartyIds[] = $value->getId();
|
||||
}
|
||||
|
||||
return implode(',', $thirdpartyIds);
|
||||
},
|
||||
function (?string $thirdpartyAsString): array {
|
||||
return array_map(
|
||||
fn (string $id): ?ThirdParty => $this->om->getRepository(ThirdParty::class)->findOneBy(['id' => (int) $id]),
|
||||
explode(',', $thirdpartyAsString)
|
||||
);
|
||||
}
|
||||
));
|
||||
$builder->add('users', HiddenType::class);
|
||||
$builder->get('users')
|
||||
->addModelTransformer($this->idToUsersDataTransformer);
|
||||
|
||||
$builder->add('calendarRange', HiddenType::class);
|
||||
$builder->get('calendarRange')
|
||||
->addModelTransformer(new CallbackTransformer(
|
||||
static function (?CalendarRange $calendarRange): ?int {
|
||||
if (null !== $calendarRange) {
|
||||
$res = $calendarRange->getId();
|
||||
} else {
|
||||
$res = -1;
|
||||
}
|
||||
|
||||
return $res;
|
||||
},
|
||||
function (?string $calendarRangeId): ?CalendarRange {
|
||||
if (null !== $calendarRangeId) {
|
||||
$res = $this->om->getRepository(CalendarRange::class)->findOneBy(['id' => (int) $calendarRangeId]);
|
||||
} else {
|
||||
$res = null;
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
));
|
||||
->addModelTransformer($this->calendarRangeDataTransformer);
|
||||
|
||||
$builder->add('location', HiddenType::class)
|
||||
->get('location')
|
||||
->addModelTransformer(new CallbackTransformer(
|
||||
static function (?Location $location): string {
|
||||
if (null === $location) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $location->getId();
|
||||
},
|
||||
function (?string $id): ?Location {
|
||||
return $this->om->getRepository(Location::class)->findOneBy(['id' => (int) $id]);
|
||||
}
|
||||
));
|
||||
|
||||
$builder->add('invites', HiddenType::class);
|
||||
$builder->get('invites')
|
||||
->addModelTransformer(new CallbackTransformer(
|
||||
static function (iterable $usersAsIterable): string {
|
||||
$userIds = [];
|
||||
|
||||
foreach ($usersAsIterable as $value) {
|
||||
$userIds[] = $value->getId();
|
||||
}
|
||||
|
||||
return implode(',', $userIds);
|
||||
},
|
||||
function (?string $usersAsString): array {
|
||||
return array_map(
|
||||
fn (string $id): ?Invite => $this->om->getRepository(Invite::class)->findOneBy(['id' => (int) $id]),
|
||||
explode(',', $usersAsString)
|
||||
);
|
||||
}
|
||||
));
|
||||
->addModelTransformer($this->idToLocationDataTransformer);
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
@@ -227,14 +156,11 @@ class CalendarType extends AbstractType
|
||||
$resolver->setDefaults([
|
||||
'data_class' => Calendar::class,
|
||||
]);
|
||||
|
||||
$resolver
|
||||
->setRequired(['accompanyingPeriod'])
|
||||
->setAllowedTypes('accompanyingPeriod', [\Chill\PersonBundle\Entity\AccompanyingPeriod::class, 'null']);
|
||||
}
|
||||
|
||||
public function getBlockPrefix()
|
||||
{
|
||||
return 'chill_calendarbundle_calendar';
|
||||
// as the js share some hardcoded items from activity, we have to rewrite block prefix
|
||||
return 'chill_activitybundle_activity';
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\CalendarBundle\Form\DataTransformer;
|
||||
|
||||
use Chill\CalendarBundle\Repository\CalendarRangeRepository;
|
||||
use Chill\MainBundle\Form\DataTransformer\IdToEntityDataTransformer;
|
||||
|
||||
class IdToCalendarRangeDataTransformer extends IdToEntityDataTransformer
|
||||
{
|
||||
public function __construct(CalendarRangeRepository $repository)
|
||||
{
|
||||
parent::__construct($repository, false);
|
||||
}
|
||||
}
|
@@ -11,44 +11,37 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\CalendarBundle\Menu;
|
||||
|
||||
use Chill\CalendarBundle\Security\Voter\CalendarVoter;
|
||||
use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
|
||||
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
|
||||
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
||||
use Knp\Menu\MenuItem;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class AccompanyingCourseMenuBuilder implements LocalMenuBuilderInterface
|
||||
{
|
||||
protected AuthorizationHelper $authorizationHelper;
|
||||
|
||||
protected TokenStorageInterface $tokenStorage;
|
||||
|
||||
protected TranslatorInterface $translator;
|
||||
|
||||
private Security $security;
|
||||
|
||||
public function __construct(
|
||||
TokenStorageInterface $tokenStorage,
|
||||
AuthorizationHelper $authorizationHelper,
|
||||
Security $security,
|
||||
TranslatorInterface $translator
|
||||
) {
|
||||
$this->security = $security;
|
||||
$this->translator = $translator;
|
||||
$this->authorizationHelper = $authorizationHelper;
|
||||
$this->tokenStorage = $tokenStorage;
|
||||
}
|
||||
|
||||
public function buildMenu($menuId, MenuItem $menu, array $parameters)
|
||||
{
|
||||
$period = $parameters['accompanyingCourse'];
|
||||
|
||||
if (AccompanyingPeriod::STEP_DRAFT !== $period->getStep()) {
|
||||
/*
|
||||
if ($this->security->isGranted(CalendarVoter::SEE, $period)) {
|
||||
$menu->addChild($this->translator->trans('Calendar'), [
|
||||
'route' => 'chill_calendar_calendar_list',
|
||||
'route' => 'chill_calendar_calendar_list_by_period',
|
||||
'routeParameters' => [
|
||||
'accompanying_period_id' => $period->getId(),
|
||||
'id' => $period->getId(),
|
||||
], ])
|
||||
->setExtras(['order' => 35]);
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -58,7 +58,7 @@ class UserMenuBuilder implements LocalMenuBuilderInterface
|
||||
|
||||
if ($this->authorizationChecker->isGranted('ROLE_USER')) {
|
||||
$menu->addChild('My calendar list', [
|
||||
'route' => 'chill_calendar_calendar_list',
|
||||
'route' => 'chill_calendar_calendar_list_my',
|
||||
])
|
||||
->setExtras([
|
||||
'order' => 9,
|
||||
|
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\CalendarBundle\Messenger\Doctrine;
|
||||
|
||||
use Chill\CalendarBundle\Entity\Calendar;
|
||||
use Chill\CalendarBundle\Messenger\Message\CalendarMessage;
|
||||
use Chill\CalendarBundle\Messenger\Message\CalendarRemovedMessage;
|
||||
use Doctrine\ORM\Event\LifecycleEventArgs;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
|
||||
class CalendarEntityListener
|
||||
{
|
||||
private MessageBusInterface $messageBus;
|
||||
|
||||
private Security $security;
|
||||
|
||||
public function __construct(MessageBusInterface $messageBus, Security $security)
|
||||
{
|
||||
$this->messageBus = $messageBus;
|
||||
$this->security = $security;
|
||||
}
|
||||
|
||||
public function postPersist(Calendar $calendar, LifecycleEventArgs $args): void
|
||||
{
|
||||
if (!$calendar->preventEnqueueChanges) {
|
||||
$this->messageBus->dispatch(
|
||||
new CalendarMessage(
|
||||
$calendar,
|
||||
CalendarMessage::CALENDAR_PERSIST,
|
||||
$this->security->getUser()
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function postRemove(Calendar $calendar, LifecycleEventArgs $args): void
|
||||
{
|
||||
if (!$calendar->preventEnqueueChanges) {
|
||||
$this->messageBus->dispatch(
|
||||
new CalendarRemovedMessage(
|
||||
$calendar,
|
||||
$this->security->getUser()
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function postUpdate(Calendar $calendar, LifecycleEventArgs $args): void
|
||||
{
|
||||
if (!$calendar->preventEnqueueChanges) {
|
||||
$this->messageBus->dispatch(
|
||||
new CalendarMessage(
|
||||
$calendar,
|
||||
CalendarMessage::CALENDAR_UPDATE,
|
||||
$this->security->getUser()
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\CalendarBundle\Messenger\Doctrine;
|
||||
|
||||
use Chill\CalendarBundle\Entity\CalendarRange;
|
||||
use Chill\CalendarBundle\Messenger\Message\CalendarRangeMessage;
|
||||
use Chill\CalendarBundle\Messenger\Message\CalendarRangeRemovedMessage;
|
||||
use Doctrine\ORM\Event\LifecycleEventArgs;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
|
||||
class CalendarRangeEntityListener
|
||||
{
|
||||
private MessageBusInterface $messageBus;
|
||||
|
||||
private Security $security;
|
||||
|
||||
public function __construct(MessageBusInterface $messageBus, Security $security)
|
||||
{
|
||||
$this->messageBus = $messageBus;
|
||||
$this->security = $security;
|
||||
}
|
||||
|
||||
public function postPersist(CalendarRange $calendarRange, LifecycleEventArgs $eventArgs): void
|
||||
{
|
||||
if (!$calendarRange->preventEnqueueChanges) {
|
||||
$this->messageBus->dispatch(
|
||||
new CalendarRangeMessage(
|
||||
$calendarRange,
|
||||
CalendarRangeMessage::CALENDAR_RANGE_PERSIST,
|
||||
$this->security->getUser()
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function postRemove(CalendarRange $calendarRange, LifecycleEventArgs $eventArgs): void
|
||||
{
|
||||
if (!$calendarRange->preventEnqueueChanges) {
|
||||
$this->messageBus->dispatch(
|
||||
new CalendarRangeRemovedMessage(
|
||||
$calendarRange,
|
||||
$this->security->getUser()
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function postUpdate(CalendarRange $calendarRange, LifecycleEventArgs $eventArgs): void
|
||||
{
|
||||
if (!$calendarRange->preventEnqueueChanges) {
|
||||
$this->messageBus->dispatch(
|
||||
new CalendarRangeMessage(
|
||||
$calendarRange,
|
||||
CalendarRangeMessage::CALENDAR_RANGE_UPDATE,
|
||||
$this->security->getUser()
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\CalendarBundle\Messenger\Handler;
|
||||
|
||||
use Chill\CalendarBundle\Messenger\Message\CalendarRangeRemovedMessage;
|
||||
use Chill\CalendarBundle\RemoteCalendar\Connector\RemoteCalendarConnectorInterface;
|
||||
use Chill\MainBundle\Repository\UserRepository;
|
||||
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
|
||||
{
|
||||
private RemoteCalendarConnectorInterface $remoteCalendarConnector;
|
||||
|
||||
private UserRepository $userRepository;
|
||||
|
||||
public function __construct(RemoteCalendarConnectorInterface $remoteCalendarConnector, UserRepository $userRepository)
|
||||
{
|
||||
$this->remoteCalendarConnector = $remoteCalendarConnector;
|
||||
$this->userRepository = $userRepository;
|
||||
}
|
||||
|
||||
public function __invoke(CalendarRangeRemovedMessage $calendarRangeRemovedMessage)
|
||||
{
|
||||
$this->remoteCalendarConnector->removeCalendarRange(
|
||||
$calendarRangeRemovedMessage->getRemoteId(),
|
||||
$calendarRangeRemovedMessage->getRemoteAttributes(),
|
||||
$this->userRepository->find($calendarRangeRemovedMessage->getCalendarRangeUserId())
|
||||
);
|
||||
}
|
||||
}
|
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\CalendarBundle\Messenger\Handler;
|
||||
|
||||
use Chill\CalendarBundle\Messenger\Message\CalendarRangeMessage;
|
||||
use Chill\CalendarBundle\RemoteCalendar\Connector\RemoteCalendarConnectorInterface;
|
||||
use Chill\CalendarBundle\Repository\CalendarRangeRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
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
|
||||
{
|
||||
private CalendarRangeRepository $calendarRangeRepository;
|
||||
|
||||
private EntityManagerInterface $entityManager;
|
||||
|
||||
private RemoteCalendarConnectorInterface $remoteCalendarConnector;
|
||||
|
||||
public function __construct(
|
||||
CalendarRangeRepository $calendarRangeRepository,
|
||||
RemoteCalendarConnectorInterface $remoteCalendarConnector,
|
||||
EntityManagerInterface $entityManager
|
||||
) {
|
||||
$this->calendarRangeRepository = $calendarRangeRepository;
|
||||
$this->remoteCalendarConnector = $remoteCalendarConnector;
|
||||
$this->entityManager = $entityManager;
|
||||
}
|
||||
|
||||
public function __invoke(CalendarRangeMessage $calendarRangeMessage): void
|
||||
{
|
||||
$range = $this->calendarRangeRepository->find($calendarRangeMessage->getCalendarRangeId());
|
||||
|
||||
if (null === $range) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->remoteCalendarConnector->syncCalendarRange($range);
|
||||
$range->preventEnqueueChanges = true;
|
||||
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
}
|
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\CalendarBundle\Messenger\Handler;
|
||||
|
||||
use Chill\CalendarBundle\Messenger\Message\CalendarRemovedMessage;
|
||||
use Chill\CalendarBundle\RemoteCalendar\Connector\RemoteCalendarConnectorInterface;
|
||||
use Chill\CalendarBundle\Repository\CalendarRangeRepository;
|
||||
use Chill\MainBundle\Repository\UserRepositoryInterface;
|
||||
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
|
||||
|
||||
/**
|
||||
* Handle the deletion of calendar.
|
||||
*
|
||||
* @AsMessageHandler
|
||||
*/
|
||||
class CalendarRemoveHandler implements MessageHandlerInterface
|
||||
{
|
||||
private CalendarRangeRepository $calendarRangeRepository;
|
||||
|
||||
private RemoteCalendarConnectorInterface $remoteCalendarConnector;
|
||||
|
||||
private UserRepositoryInterface $userRepository;
|
||||
|
||||
public function __construct(RemoteCalendarConnectorInterface $remoteCalendarConnector, CalendarRangeRepository $calendarRangeRepository, UserRepositoryInterface $userRepository)
|
||||
{
|
||||
$this->remoteCalendarConnector = $remoteCalendarConnector;
|
||||
$this->calendarRangeRepository = $calendarRangeRepository;
|
||||
$this->userRepository = $userRepository;
|
||||
}
|
||||
|
||||
public function __invoke(CalendarRemovedMessage $message)
|
||||
{
|
||||
if (null !== $message->getAssociatedCalendarRangeId()) {
|
||||
$associatedRange = $this->calendarRangeRepository->find($message->getAssociatedCalendarRangeId());
|
||||
} else {
|
||||
$associatedRange = null;
|
||||
}
|
||||
|
||||
$this->remoteCalendarConnector->removeCalendar(
|
||||
$message->getRemoteId(),
|
||||
$message->getRemoteAttributes(),
|
||||
$this->userRepository->find($message->getCalendarUserId()),
|
||||
$associatedRange
|
||||
);
|
||||
}
|
||||
}
|
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\CalendarBundle\Messenger\Handler;
|
||||
|
||||
use Chill\CalendarBundle\Entity\CalendarRange;
|
||||
use Chill\CalendarBundle\Entity\Invite;
|
||||
use Chill\CalendarBundle\Messenger\Message\CalendarMessage;
|
||||
use Chill\CalendarBundle\RemoteCalendar\Connector\RemoteCalendarConnectorInterface;
|
||||
use Chill\CalendarBundle\Repository\CalendarRangeRepository;
|
||||
use Chill\CalendarBundle\Repository\CalendarRepository;
|
||||
use Chill\CalendarBundle\Repository\InviteRepository;
|
||||
use Chill\MainBundle\Repository\UserRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
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
|
||||
{
|
||||
private RemoteCalendarConnectorInterface $calendarConnector;
|
||||
|
||||
private CalendarRangeRepository $calendarRangeRepository;
|
||||
|
||||
private CalendarRepository $calendarRepository;
|
||||
|
||||
private EntityManagerInterface $entityManager;
|
||||
|
||||
private InviteRepository $inviteRepository;
|
||||
|
||||
private UserRepository $userRepository;
|
||||
|
||||
public function __construct(
|
||||
CalendarRangeRepository $calendarRangeRepository,
|
||||
CalendarRepository $calendarRepository,
|
||||
EntityManagerInterface $entityManager,
|
||||
InviteRepository $inviteRepository,
|
||||
RemoteCalendarConnectorInterface $calendarConnector,
|
||||
UserRepository $userRepository
|
||||
) {
|
||||
$this->calendarConnector = $calendarConnector;
|
||||
$this->calendarRepository = $calendarRepository;
|
||||
$this->calendarRangeRepository = $calendarRangeRepository;
|
||||
$this->entityManager = $entityManager;
|
||||
$this->userRepository = $userRepository;
|
||||
$this->inviteRepository = $inviteRepository;
|
||||
}
|
||||
|
||||
public function __invoke(CalendarMessage $calendarMessage)
|
||||
{
|
||||
$calendar = $this->calendarRepository->find($calendarMessage->getCalendarId());
|
||||
|
||||
if (null === $calendar) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (null !== $calendarMessage->getPreviousCalendarRangeId()) {
|
||||
$previousCalendarRange = $this->calendarRangeRepository
|
||||
->find($calendarMessage->getPreviousCalendarRangeId());
|
||||
} else {
|
||||
$previousCalendarRange = null;
|
||||
}
|
||||
|
||||
if (null !== $calendarMessage->getPreviousMainUserId()) {
|
||||
$previousMainUser = $this->userRepository
|
||||
->find($calendarMessage->getPreviousMainUserId());
|
||||
} else {
|
||||
$previousMainUser = null;
|
||||
}
|
||||
|
||||
$newInvites = array_filter(
|
||||
array_map(
|
||||
function ($id) { return $this->inviteRepository->find($id); },
|
||||
$calendarMessage->getNewInvitesIds(),
|
||||
),
|
||||
static function (?Invite $invite) { return null !== $invite; }
|
||||
);
|
||||
|
||||
$this->calendarConnector->syncCalendar(
|
||||
$calendar,
|
||||
$calendarMessage->getAction(),
|
||||
$previousCalendarRange,
|
||||
$previousMainUser,
|
||||
$calendarMessage->getOldInvites(),
|
||||
$newInvites
|
||||
);
|
||||
|
||||
$calendar->preventEnqueueChanges = true;
|
||||
|
||||
if ($calendar->hasCalendarRange()) {
|
||||
$calendar->getCalendarRange()->preventEnqueueChanges = true;
|
||||
}
|
||||
|
||||
if ($previousCalendarRange instanceof CalendarRange) {
|
||||
$previousCalendarRange->preventEnqueueChanges = true;
|
||||
}
|
||||
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
}
|
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\CalendarBundle\Messenger\Handler;
|
||||
|
||||
use Chill\CalendarBundle\Messenger\Message\InviteUpdateMessage;
|
||||
use Chill\CalendarBundle\RemoteCalendar\Connector\RemoteCalendarConnectorInterface;
|
||||
use Chill\CalendarBundle\Repository\InviteRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
|
||||
|
||||
/**
|
||||
* Sync the local invitation to the remote calendar.
|
||||
*
|
||||
* @AsMessageHandler
|
||||
*/
|
||||
class InviteUpdateHandler implements MessageHandlerInterface
|
||||
{
|
||||
private EntityManagerInterface $em;
|
||||
|
||||
private InviteRepository $inviteRepository;
|
||||
|
||||
private RemoteCalendarConnectorInterface $remoteCalendarConnector;
|
||||
|
||||
public function __construct(EntityManagerInterface $em, InviteRepository $inviteRepository, RemoteCalendarConnectorInterface $remoteCalendarConnector)
|
||||
{
|
||||
$this->em = $em;
|
||||
$this->inviteRepository = $inviteRepository;
|
||||
$this->remoteCalendarConnector = $remoteCalendarConnector;
|
||||
}
|
||||
|
||||
public function __invoke(InviteUpdateMessage $inviteUpdateMessage): void
|
||||
{
|
||||
if (null === $invite = $this->inviteRepository->find($inviteUpdateMessage->getInviteId())) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->remoteCalendarConnector->syncInvite($invite);
|
||||
|
||||
$this->em->flush();
|
||||
}
|
||||
}
|
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\CalendarBundle\Messenger\Handler;
|
||||
|
||||
use Chill\CalendarBundle\Messenger\Message\MSGraphChangeNotificationMessage;
|
||||
use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\MapCalendarToUser;
|
||||
use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\RemoteToLocalSync\CalendarRangeSyncer;
|
||||
use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\RemoteToLocalSync\CalendarSyncer;
|
||||
use Chill\CalendarBundle\Repository\CalendarRangeRepository;
|
||||
use Chill\CalendarBundle\Repository\CalendarRepository;
|
||||
use Chill\MainBundle\Repository\UserRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
|
||||
|
||||
/**
|
||||
* Handle notification of changes made by users directly on Outlook calendar.
|
||||
*
|
||||
* @AsMessageHandler
|
||||
*/
|
||||
class MSGraphChangeNotificationHandler implements MessageHandlerInterface
|
||||
{
|
||||
private CalendarRangeRepository $calendarRangeRepository;
|
||||
|
||||
private CalendarRangeSyncer $calendarRangeSyncer;
|
||||
|
||||
private CalendarRepository $calendarRepository;
|
||||
|
||||
private CalendarSyncer $calendarSyncer;
|
||||
|
||||
private EntityManagerInterface $em;
|
||||
|
||||
private LoggerInterface $logger;
|
||||
|
||||
private MapCalendarToUser $mapCalendarToUser;
|
||||
|
||||
private UserRepository $userRepository;
|
||||
|
||||
public function __construct(
|
||||
CalendarRangeRepository $calendarRangeRepository,
|
||||
CalendarRangeSyncer $calendarRangeSyncer,
|
||||
CalendarRepository $calendarRepository,
|
||||
CalendarSyncer $calendarSyncer,
|
||||
EntityManagerInterface $em,
|
||||
LoggerInterface $logger,
|
||||
MapCalendarToUser $mapCalendarToUser,
|
||||
UserRepository $userRepository
|
||||
) {
|
||||
$this->calendarRangeRepository = $calendarRangeRepository;
|
||||
$this->calendarRangeSyncer = $calendarRangeSyncer;
|
||||
$this->calendarRepository = $calendarRepository;
|
||||
$this->calendarSyncer = $calendarSyncer;
|
||||
$this->em = $em;
|
||||
$this->logger = $logger;
|
||||
$this->mapCalendarToUser = $mapCalendarToUser;
|
||||
$this->userRepository = $userRepository;
|
||||
}
|
||||
|
||||
public function __invoke(MSGraphChangeNotificationMessage $changeNotificationMessage): void
|
||||
{
|
||||
$user = $this->userRepository->find($changeNotificationMessage->getUserId());
|
||||
|
||||
if (null === $user) {
|
||||
$this->logger->warning(__CLASS__ . ' notification concern non-existent user, skipping');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($changeNotificationMessage->getContent()['value'] as $notification) {
|
||||
$secret = $this->mapCalendarToUser->getSubscriptionSecret($user);
|
||||
|
||||
if ($secret !== ($notification['clientState'] ?? -1)) {
|
||||
$this->logger->warning(__CLASS__ . ' could not validate secret, skipping');
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$remoteId = $notification['resourceData']['id'];
|
||||
|
||||
// is this a calendar range ?
|
||||
if (null !== $calendarRange = $this->calendarRangeRepository->findOneBy(['remoteId' => $remoteId])) {
|
||||
$this->calendarRangeSyncer->handleCalendarRangeSync($calendarRange, $notification, $user);
|
||||
$this->em->flush();
|
||||
} elseif (null !== $calendar = $this->calendarRepository->findOneBy(['remoteId' => $remoteId])) {
|
||||
$this->calendarSyncer->handleCalendarSync($calendar, $notification, $user);
|
||||
$this->em->flush();
|
||||
} else {
|
||||
$this->logger->info(__CLASS__ . ' id not found in any calendar nor calendar range');
|
||||
}
|
||||
}
|
||||
|
||||
$this->em->flush();
|
||||
}
|
||||
}
|
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\CalendarBundle\Messenger\Message;
|
||||
|
||||
use Chill\CalendarBundle\Entity\Calendar;
|
||||
use Chill\CalendarBundle\Entity\Invite;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
|
||||
class CalendarMessage
|
||||
{
|
||||
public const CALENDAR_PERSIST = 'CHILL_CALENDAR_CALENDAR_PERSIST';
|
||||
|
||||
public const CALENDAR_UPDATE = 'CHILL_CALENDAR_CALENDAR_UPDATE';
|
||||
|
||||
private string $action;
|
||||
|
||||
private int $byUserId;
|
||||
|
||||
private int $calendarId;
|
||||
|
||||
private array $newInvitesIds = [];
|
||||
|
||||
/**
|
||||
* @var array<array{inviteId: int, userId: int, userEmail: int, userLabel: string}>
|
||||
*/
|
||||
private array $oldInvites = [];
|
||||
|
||||
private ?int $previousCalendarRangeId = null;
|
||||
|
||||
private ?int $previousMainUserId = null;
|
||||
|
||||
public function __construct(
|
||||
Calendar $calendar,
|
||||
string $action,
|
||||
User $byUser
|
||||
) {
|
||||
$this->calendarId = $calendar->getId();
|
||||
$this->byUserId = $byUser->getId();
|
||||
$this->action = $action;
|
||||
$this->previousCalendarRangeId = null !== $calendar->previousCalendarRange ?
|
||||
$calendar->previousCalendarRange->getId() : null;
|
||||
$this->previousMainUserId = null !== $calendar->previousMainUser ?
|
||||
$calendar->previousMainUser->getId() : null;
|
||||
$this->newInvitesIds = array_map(static fn (Invite $i) => $i->getId(), $calendar->newInvites);
|
||||
$this->oldInvites = array_map(static function (Invite $i) {
|
||||
return [
|
||||
'inviteId' => $i->getId(),
|
||||
'userId' => $i->getUser()->getId(),
|
||||
'userEmail' => $i->getUser()->getEmail(),
|
||||
'userLabel' => $i->getUser()->getLabel(),
|
||||
];
|
||||
}, $calendar->oldInvites);
|
||||
}
|
||||
|
||||
public function getAction(): string
|
||||
{
|
||||
return $this->action;
|
||||
}
|
||||
|
||||
public function getByUserId(): ?int
|
||||
{
|
||||
return $this->byUserId;
|
||||
}
|
||||
|
||||
public function getCalendarId(): ?int
|
||||
{
|
||||
return $this->calendarId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array|int[]|null[]
|
||||
*/
|
||||
public function getNewInvitesIds(): array
|
||||
{
|
||||
return $this->newInvitesIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<array{inviteId: int, userId: int, userEmail: int, userLabel: string}>
|
||||
*/
|
||||
public function getOldInvites(): array
|
||||
{
|
||||
return $this->oldInvites;
|
||||
}
|
||||
|
||||
public function getPreviousCalendarRangeId(): ?int
|
||||
{
|
||||
return $this->previousCalendarRangeId;
|
||||
}
|
||||
|
||||
public function getPreviousMainUserId(): ?int
|
||||
{
|
||||
return $this->previousMainUserId;
|
||||
}
|
||||
}
|
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\CalendarBundle\Messenger\Message;
|
||||
|
||||
use Chill\CalendarBundle\Entity\CalendarRange;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
|
||||
class CalendarRangeMessage
|
||||
{
|
||||
public const CALENDAR_RANGE_PERSIST = 'CHILL_CALENDAR_CALENDAR_RANGE_PERSIST';
|
||||
|
||||
public const CALENDAR_RANGE_UPDATE = 'CHILL_CALENDAR_CALENDAR_RANGE_UPDATE';
|
||||
|
||||
private string $action;
|
||||
|
||||
private ?int $byUserId = null;
|
||||
|
||||
private int $calendarRangeId;
|
||||
|
||||
public function __construct(CalendarRange $calendarRange, string $action, ?User $byUser)
|
||||
{
|
||||
$this->action = $action;
|
||||
$this->calendarRangeId = $calendarRange->getId();
|
||||
|
||||
if (null !== $byUser) {
|
||||
$this->byUserId = $byUser->getId();
|
||||
}
|
||||
}
|
||||
|
||||
public function getAction(): string
|
||||
{
|
||||
return $this->action;
|
||||
}
|
||||
|
||||
public function getByUserId(): ?int
|
||||
{
|
||||
return $this->byUserId;
|
||||
}
|
||||
|
||||
public function getCalendarRangeId(): ?int
|
||||
{
|
||||
return $this->calendarRangeId;
|
||||
}
|
||||
}
|
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\CalendarBundle\Messenger\Message;
|
||||
|
||||
use Chill\CalendarBundle\Entity\CalendarRange;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
|
||||
class CalendarRangeRemovedMessage
|
||||
{
|
||||
private ?int $byUserId = null;
|
||||
|
||||
private int $calendarRangeUserId;
|
||||
|
||||
private array $remoteAttributes;
|
||||
|
||||
private string $remoteId;
|
||||
|
||||
public function __construct(CalendarRange $calendarRange, ?User $byUser)
|
||||
{
|
||||
$this->remoteId = $calendarRange->getRemoteId();
|
||||
$this->remoteAttributes = $calendarRange->getRemoteAttributes();
|
||||
$this->calendarRangeUserId = $calendarRange->getUser()->getId();
|
||||
|
||||
if (null !== $byUser) {
|
||||
$this->byUserId = $byUser->getId();
|
||||
}
|
||||
}
|
||||
|
||||
public function getByUserId(): ?int
|
||||
{
|
||||
return $this->byUserId;
|
||||
}
|
||||
|
||||
public function getCalendarRangeUserId(): ?int
|
||||
{
|
||||
return $this->calendarRangeUserId;
|
||||
}
|
||||
|
||||
public function getRemoteAttributes(): array
|
||||
{
|
||||
return $this->remoteAttributes;
|
||||
}
|
||||
|
||||
public function getRemoteId(): string
|
||||
{
|
||||
return $this->remoteId;
|
||||
}
|
||||
}
|
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\CalendarBundle\Messenger\Message;
|
||||
|
||||
use Chill\CalendarBundle\Entity\Calendar;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
|
||||
class CalendarRemovedMessage
|
||||
{
|
||||
private ?int $associatedCalendarRangeId = null;
|
||||
|
||||
private ?int $byUserId = null;
|
||||
|
||||
private int $calendarUserId;
|
||||
|
||||
private array $remoteAttributes;
|
||||
|
||||
private string $remoteId;
|
||||
|
||||
public function __construct(Calendar $calendar, ?User $byUser)
|
||||
{
|
||||
$this->remoteId = $calendar->getRemoteId();
|
||||
$this->remoteAttributes = $calendar->getRemoteAttributes();
|
||||
$this->calendarUserId = $calendar->getMainUser()->getId();
|
||||
|
||||
if ($calendar->hasCalendarRange()) {
|
||||
$this->associatedCalendarRangeId = $calendar->getCalendarRange()->getId();
|
||||
}
|
||||
|
||||
if (null !== $byUser) {
|
||||
$this->byUserId = $byUser->getId();
|
||||
}
|
||||
}
|
||||
|
||||
public function getAssociatedCalendarRangeId(): ?int
|
||||
{
|
||||
return $this->associatedCalendarRangeId;
|
||||
}
|
||||
|
||||
public function getByUserId(): ?int
|
||||
{
|
||||
return $this->byUserId;
|
||||
}
|
||||
|
||||
public function getCalendarUserId(): ?int
|
||||
{
|
||||
return $this->calendarUserId;
|
||||
}
|
||||
|
||||
public function getRemoteAttributes(): array
|
||||
{
|
||||
return $this->remoteAttributes;
|
||||
}
|
||||
|
||||
public function getRemoteId(): string
|
||||
{
|
||||
return $this->remoteId;
|
||||
}
|
||||
}
|
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\CalendarBundle\Messenger\Message;
|
||||
|
||||
use Chill\CalendarBundle\Entity\Invite;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
|
||||
class InviteUpdateMessage
|
||||
{
|
||||
private int $byUserId;
|
||||
|
||||
private int $inviteId;
|
||||
|
||||
public function __construct(Invite $invite, User $byUser)
|
||||
{
|
||||
$this->inviteId = $invite->getId();
|
||||
$this->byUserId = $byUser->getId();
|
||||
}
|
||||
|
||||
public function getByUserId(): int
|
||||
{
|
||||
return $this->byUserId;
|
||||
}
|
||||
|
||||
public function getInviteId(): int
|
||||
{
|
||||
return $this->inviteId;
|
||||
}
|
||||
}
|
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\CalendarBundle\Messenger\Message;
|
||||
|
||||
class MSGraphChangeNotificationMessage
|
||||
{
|
||||
private array $content;
|
||||
|
||||
private int $userId;
|
||||
|
||||
public function __construct(array $content, int $userId)
|
||||
{
|
||||
$this->content = $content;
|
||||
$this->userId = $userId;
|
||||
}
|
||||
|
||||
public function getContent(): array
|
||||
{
|
||||
return $this->content;
|
||||
}
|
||||
|
||||
public function getUserId(): int
|
||||
{
|
||||
return $this->userId;
|
||||
}
|
||||
}
|
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph;
|
||||
|
||||
use Chill\MainBundle\Entity\Address;
|
||||
use Chill\MainBundle\Templating\Entity\AddressRender;
|
||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||
|
||||
class AddressConverter
|
||||
{
|
||||
private AddressRender $addressRender;
|
||||
|
||||
private TranslatableStringHelperInterface $translatableStringHelper;
|
||||
|
||||
public function __construct(AddressRender $addressRender, TranslatableStringHelperInterface $translatableStringHelper)
|
||||
{
|
||||
$this->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' => '',
|
||||
];
|
||||
}
|
||||
}
|
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph;
|
||||
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use DateTimeImmutable;
|
||||
use LogicException;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
|
||||
|
||||
/**
|
||||
* Create a subscription for a user.
|
||||
*/
|
||||
class EventsOnUserSubscriptionCreator
|
||||
{
|
||||
private LoggerInterface $logger;
|
||||
|
||||
private MachineHttpClient $machineHttpClient;
|
||||
|
||||
private MapCalendarToUser $mapCalendarToUser;
|
||||
|
||||
private UrlGeneratorInterface $urlGenerator;
|
||||
|
||||
public function __construct(
|
||||
LoggerInterface $logger,
|
||||
MachineHttpClient $machineHttpClient,
|
||||
MapCalendarToUser $mapCalendarToUser,
|
||||
UrlGeneratorInterface $urlGenerator
|
||||
) {
|
||||
$this->logger = $logger;
|
||||
$this->machineHttpClient = $machineHttpClient;
|
||||
$this->mapCalendarToUser = $mapCalendarToUser;
|
||||
$this->urlGenerator = $urlGenerator;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ClientExceptionInterface
|
||||
* @throws \Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface
|
||||
* @throws \Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface
|
||||
* @throws \Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface
|
||||
* @throws \Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface
|
||||
*
|
||||
* @return array<secret: string, id: string, expiration: int>
|
||||
*/
|
||||
public function createSubscriptionForUser(User $user, DateTimeImmutable $expiration): array
|
||||
{
|
||||
if (null === $userId = $this->mapCalendarToUser->getUserId($user)) {
|
||||
throw new LogicException('no user id');
|
||||
}
|
||||
|
||||
$subscription = [
|
||||
'changeType' => 'deleted,updated',
|
||||
'notificationUrl' => $this->urlGenerator->generate(
|
||||
'chill_calendar_remote_msgraph_incoming_webhook_events',
|
||||
['userId' => $user->getId()],
|
||||
UrlGeneratorInterface::ABSOLUTE_URL
|
||||
),
|
||||
'resource' => "/users/{$userId}/calendar/events",
|
||||
'clientState' => $secret = base64_encode(openssl_random_pseudo_bytes(92, $cstrong)),
|
||||
'expirationDateTime' => $expiration->format(DateTimeImmutable::ATOM),
|
||||
];
|
||||
|
||||
try {
|
||||
$subs = $this->machineHttpClient->request(
|
||||
'POST',
|
||||
'/v1.0/subscriptions',
|
||||
[
|
||||
'json' => $subscription,
|
||||
]
|
||||
)->toArray();
|
||||
} catch (ClientExceptionInterface $e) {
|
||||
$this->logger->error('could not create subscription for user events', [
|
||||
'body' => $e->getResponse()->getContent(false),
|
||||
]);
|
||||
|
||||
return ['secret' => '', 'id' => '', 'expiration' => 0];
|
||||
}
|
||||
|
||||
return ['secret' => $secret, 'id' => $subs['id'], 'expiration' => $expiration->getTimestamp()];
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ClientExceptionInterface
|
||||
* @throws \Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface
|
||||
* @throws \Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface
|
||||
* @throws \Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface
|
||||
* @throws \Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface
|
||||
*
|
||||
* @return array<secret: string, id: string, expiration: int>
|
||||
*/
|
||||
public function renewSubscriptionForUser(User $user, DateTimeImmutable $expiration): array
|
||||
{
|
||||
if (null === $userId = $this->mapCalendarToUser->getUserId($user)) {
|
||||
throw new LogicException('no user id');
|
||||
}
|
||||
|
||||
if (null === $subscriptionId = $this->mapCalendarToUser->getActiveSubscriptionId($user)) {
|
||||
throw new LogicException('no user id');
|
||||
}
|
||||
|
||||
$subscription = [
|
||||
'expirationDateTime' => $expiration->format(DateTimeImmutable::ATOM),
|
||||
];
|
||||
|
||||
try {
|
||||
$subs = $this->machineHttpClient->request(
|
||||
'PATCH',
|
||||
"/v1.0/subscriptions/{$subscriptionId}",
|
||||
[
|
||||
'json' => $subscription,
|
||||
]
|
||||
)->toArray();
|
||||
} catch (ClientExceptionInterface $e) {
|
||||
$this->logger->error('could not patch subscription for user events', [
|
||||
'body' => $e->getResponse()->getContent(false),
|
||||
]);
|
||||
|
||||
return ['secret' => '', 'id' => '', 'expiration' => 0];
|
||||
}
|
||||
|
||||
return ['secret' => $subs['clientState'], 'id' => $subs['id'], 'expiration' => $expiration->getTimestamp()];
|
||||
}
|
||||
}
|
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph;
|
||||
|
||||
use Chill\MainBundle\Entity\Location;
|
||||
|
||||
class LocationConverter
|
||||
{
|
||||
private AddressConverter $addressConverter;
|
||||
|
||||
public function __construct(AddressConverter $addressConverter)
|
||||
{
|
||||
$this->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;
|
||||
}
|
||||
}
|
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph;
|
||||
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use DateInterval;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\Query\ResultSetMapping;
|
||||
use Doctrine\ORM\Query\ResultSetMappingBuilder;
|
||||
use function strtr;
|
||||
|
||||
/**
|
||||
* Contains classes and methods for fetching users with some calendar metadatas.
|
||||
*/
|
||||
class MSGraphUserRepository
|
||||
{
|
||||
private const MOST_OLD_SUBSCRIPTION_OR_ANY_MS_GRAPH = <<<'SQL'
|
||||
select
|
||||
{select}
|
||||
from users u
|
||||
where
|
||||
NOT attributes ?? 'msgraph'
|
||||
OR NOT attributes->'msgraph' ?? 'subscription_events_expiration'
|
||||
OR (attributes->'msgraph' ?? 'subscription_events_expiration' AND (attributes->'msgraph'->>'subscription_events_expiration')::int < EXTRACT(EPOCH FROM (NOW() + :interval::interval)))
|
||||
LIMIT :limit OFFSET :offset
|
||||
;
|
||||
SQL;
|
||||
|
||||
private EntityManagerInterface $entityManager;
|
||||
|
||||
public function __construct(EntityManagerInterface $entityManager)
|
||||
{
|
||||
$this->entityManager = $entityManager;
|
||||
}
|
||||
|
||||
public function countByMostOldSubscriptionOrWithoutSubscriptionOrData(DateInterval $interval): int
|
||||
{
|
||||
$rsm = new ResultSetMapping();
|
||||
$rsm->addScalarResult('c', 'c');
|
||||
|
||||
$sql = strtr(self::MOST_OLD_SUBSCRIPTION_OR_ANY_MS_GRAPH, [
|
||||
'{select}' => 'COUNT(u) AS c',
|
||||
'LIMIT :limit OFFSET :offset' => '',
|
||||
]);
|
||||
|
||||
return $this->entityManager->createNativeQuery($sql, $rsm)->setParameters([
|
||||
'interval' => $interval,
|
||||
])->getSingleScalarResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array|User[]
|
||||
*/
|
||||
public function findByMostOldSubscriptionOrWithoutSubscriptionOrData(DateInterval $interval, int $limit = 50, int $offset = 0): array
|
||||
{
|
||||
$rsm = new ResultSetMappingBuilder($this->entityManager);
|
||||
$rsm->addRootEntityFromClassMetadata(User::class, 'u');
|
||||
|
||||
return $this->entityManager->createNativeQuery(
|
||||
strtr(self::MOST_OLD_SUBSCRIPTION_OR_ANY_MS_GRAPH, ['{select}' => $rsm->generateSelectClause()]),
|
||||
$rsm
|
||||
)->setParameters([
|
||||
'interval' => $interval,
|
||||
'limit' => $limit,
|
||||
'offset' => $offset,
|
||||
])->getResult();
|
||||
}
|
||||
}
|
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph;
|
||||
|
||||
use League\OAuth2\Client\Tool\BearerAuthorizationTrait;
|
||||
use LogicException;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
use Symfony\Contracts\HttpClient\ResponseInterface;
|
||||
use Symfony\Contracts\HttpClient\ResponseStreamInterface;
|
||||
|
||||
class MachineHttpClient implements HttpClientInterface
|
||||
{
|
||||
use BearerAuthorizationTrait;
|
||||
|
||||
private HttpClientInterface $decoratedClient;
|
||||
|
||||
private MachineTokenStorage $machineTokenStorage;
|
||||
|
||||
/**
|
||||
* @param HttpClientInterface $decoratedClient
|
||||
*/
|
||||
public function __construct(MachineTokenStorage $machineTokenStorage, ?HttpClientInterface $decoratedClient = null)
|
||||
{
|
||||
$this->decoratedClient = $decoratedClient ?? \Symfony\Component\HttpClient\HttpClient::create();
|
||||
$this->machineTokenStorage = $machineTokenStorage;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface
|
||||
* @throws LogicException if method is not supported
|
||||
*/
|
||||
public function request(string $method, string $url, array $options = []): ResponseInterface
|
||||
{
|
||||
$options['headers'] = array_merge(
|
||||
$options['headers'] ?? [],
|
||||
$this->getAuthorizationHeaders($this->machineTokenStorage->getToken())
|
||||
);
|
||||
$options['base_uri'] = 'https://graph.microsoft.com/v1.0/';
|
||||
|
||||
switch ($method) {
|
||||
case 'GET':
|
||||
case 'HEAD':
|
||||
case 'DELETE':
|
||||
$options['headers']['Accept'] = 'application/json';
|
||||
|
||||
break;
|
||||
|
||||
case 'POST':
|
||||
case 'PUT':
|
||||
case 'PATCH':
|
||||
$options['headers']['Content-Type'] = 'application/json';
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new LogicException("Method not supported: {$method}");
|
||||
}
|
||||
|
||||
return $this->decoratedClient->request($method, $url, $options);
|
||||
}
|
||||
|
||||
public function stream($responses, ?float $timeout = null): ResponseStreamInterface
|
||||
{
|
||||
return $this->decoratedClient->stream($responses, $timeout);
|
||||
}
|
||||
}
|
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph;
|
||||
|
||||
use Chill\MainBundle\Redis\ChillRedis;
|
||||
use League\OAuth2\Client\Token\AccessTokenInterface;
|
||||
use TheNetworg\OAuth2\Client\Provider\Azure;
|
||||
use TheNetworg\OAuth2\Client\Token\AccessToken;
|
||||
|
||||
class MachineTokenStorage
|
||||
{
|
||||
private const KEY = 'msgraph_access_token';
|
||||
|
||||
private ?AccessTokenInterface $accessToken = null;
|
||||
|
||||
private Azure $azure;
|
||||
|
||||
private ChillRedis $chillRedis;
|
||||
|
||||
public function __construct(Azure $azure, ChillRedis $chillRedis)
|
||||
{
|
||||
$this->azure = $azure;
|
||||
$this->chillRedis = $chillRedis;
|
||||
}
|
||||
|
||||
public function getToken(): AccessTokenInterface
|
||||
{
|
||||
if (null === $this->accessToken || $this->accessToken->hasExpired()) {
|
||||
$this->accessToken = $this->azure->getAccessToken('client_credentials', [
|
||||
'scope' => 'https://graph.microsoft.com/.default',
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->accessToken;
|
||||
}
|
||||
|
||||
public function storeToken(AccessToken $token): void
|
||||
{
|
||||
$this->chillRedis->set(self::KEY, serialize($token));
|
||||
}
|
||||
}
|
@@ -0,0 +1,193 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph;
|
||||
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use DateTimeImmutable;
|
||||
use LogicException;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
use function array_key_exists;
|
||||
|
||||
/**
|
||||
* Write metadata to user, which allow to find his default calendar.
|
||||
*/
|
||||
class MapCalendarToUser
|
||||
{
|
||||
public const EXPIRATION_SUBSCRIPTION_EVENT = 'subscription_events_expiration';
|
||||
|
||||
public const ID_SUBSCRIPTION_EVENT = 'subscription_events_id';
|
||||
|
||||
public const METADATA_KEY = 'msgraph';
|
||||
|
||||
public const SECRET_SUBSCRIPTION_EVENT = 'subscription_events_secret';
|
||||
|
||||
private LoggerInterface $logger;
|
||||
|
||||
private HttpClientInterface $machineHttpClient;
|
||||
|
||||
public function __construct(
|
||||
HttpClientInterface $machineHttpClient,
|
||||
LoggerInterface $logger
|
||||
) {
|
||||
$this->machineHttpClient = $machineHttpClient;
|
||||
$this->logger = $logger;
|
||||
}
|
||||
|
||||
public function getActiveSubscriptionId(User $user): string
|
||||
{
|
||||
if (!array_key_exists(self::METADATA_KEY, $user->getAttributes())) {
|
||||
throw new LogicException('do not contains msgraph metadata');
|
||||
}
|
||||
|
||||
if (!array_key_exists(self::ID_SUBSCRIPTION_EVENT, $user->getAttributes()[self::METADATA_KEY])) {
|
||||
throw new LogicException('do not contains metadata for subscription id');
|
||||
}
|
||||
|
||||
return $user->getAttributes()[self::METADATA_KEY][self::ID_SUBSCRIPTION_EVENT];
|
||||
}
|
||||
|
||||
public function getCalendarId(User $user): ?string
|
||||
{
|
||||
if (null === $msKey = ($user->getAttributes()[self::METADATA_KEY] ?? null)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $msKey['defaultCalendarId'] ?? null;
|
||||
}
|
||||
|
||||
public function getDefaultUserCalendar(string $idOrUserPrincipalName): ?array
|
||||
{
|
||||
$value = $this->machineHttpClient->request('GET', "users/{$idOrUserPrincipalName}/calendars", [
|
||||
'query' => ['$filter' => 'isDefaultCalendar eq true'],
|
||||
])->toArray()['value'];
|
||||
|
||||
return $value[0] ?? null;
|
||||
}
|
||||
|
||||
public function getSubscriptionSecret(User $user): string
|
||||
{
|
||||
if (!array_key_exists(self::METADATA_KEY, $user->getAttributes())) {
|
||||
throw new LogicException('do not contains msgraph metadata');
|
||||
}
|
||||
|
||||
if (!array_key_exists(self::SECRET_SUBSCRIPTION_EVENT, $user->getAttributes()[self::METADATA_KEY])) {
|
||||
throw new LogicException('do not contains secret in msgraph');
|
||||
}
|
||||
|
||||
return $user->getAttributes()[self::METADATA_KEY][self::SECRET_SUBSCRIPTION_EVENT];
|
||||
}
|
||||
|
||||
public function getUserByEmail(string $email): ?array
|
||||
{
|
||||
$value = $this->machineHttpClient->request('GET', 'users', [
|
||||
'query' => ['$filter' => "mail eq '{$email}'"],
|
||||
])->toArray()['value'];
|
||||
|
||||
return $value[0] ?? null;
|
||||
}
|
||||
|
||||
public function getUserId(User $user): ?string
|
||||
{
|
||||
if (null === $msKey = ($user->getAttributes()[self::METADATA_KEY] ?? null)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $msKey['id'] ?? null;
|
||||
}
|
||||
|
||||
public function hasActiveSubscription(User $user): bool
|
||||
{
|
||||
if (!array_key_exists(self::METADATA_KEY, $user->getAttributes())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!array_key_exists(self::EXPIRATION_SUBSCRIPTION_EVENT, $user->getAttributes()[self::METADATA_KEY])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $user->getAttributes()[self::METADATA_KEY][self::EXPIRATION_SUBSCRIPTION_EVENT]
|
||||
>= (new DateTimeImmutable('now'))->getTimestamp();
|
||||
}
|
||||
|
||||
public function hasSubscriptionSecret(User $user): bool
|
||||
{
|
||||
if (!array_key_exists(self::METADATA_KEY, $user->getAttributes())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return array_key_exists(self::SECRET_SUBSCRIPTION_EVENT, $user->getAttributes()[self::METADATA_KEY]);
|
||||
}
|
||||
|
||||
public function hasUserId(User $user): bool
|
||||
{
|
||||
if (null === $user->getEmail() || '' === $user->getEmail()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!array_key_exists(self::METADATA_KEY, $user->getAttributes())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return array_key_exists('id', $user->getAttributes()[self::METADATA_KEY]);
|
||||
}
|
||||
|
||||
public function writeMetadata(User $user): User
|
||||
{
|
||||
if (null === $user->getEmail() OR '' === $user->getEmail()) {
|
||||
return $user;
|
||||
}
|
||||
|
||||
if (null === $userData = $this->getUserByEmail($user->getEmailCanonical())) {
|
||||
$this->logger->warning('[MapCalendarToUser] could not find user on msgraph', ['userId' => $user->getId(), 'email' => $user->getEmailCanonical()]);
|
||||
|
||||
return $this->writeNullData($user);
|
||||
}
|
||||
|
||||
if (null === $defaultCalendar = $this->getDefaultUserCalendar($userData['id'])) {
|
||||
$this->logger->warning('[MapCalendarToUser] could not find default calendar', ['userId' => $user->getId(), 'email' => $user->getEmailCanonical()]);
|
||||
|
||||
return $this->writeNullData($user);
|
||||
}
|
||||
|
||||
return $user->setAttributes([self::METADATA_KEY => [
|
||||
'id' => $userData['id'],
|
||||
'userPrincipalName' => $userData['userPrincipalName'],
|
||||
'defaultCalendarId' => $defaultCalendar['id'],
|
||||
]]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $expiration the expiration time as unix timestamp
|
||||
*/
|
||||
public function writeSubscriptionMetadata(
|
||||
User $user,
|
||||
int $expiration,
|
||||
?string $id = null,
|
||||
?string $secret = null
|
||||
): void {
|
||||
$user->setAttributeByDomain(self::METADATA_KEY, self::EXPIRATION_SUBSCRIPTION_EVENT, $expiration);
|
||||
|
||||
if (null !== $id) {
|
||||
$user->setAttributeByDomain(self::METADATA_KEY, self::ID_SUBSCRIPTION_EVENT, $id);
|
||||
}
|
||||
|
||||
if (null !== $secret) {
|
||||
$user->setAttributeByDomain(self::METADATA_KEY, self::SECRET_SUBSCRIPTION_EVENT, $secret);
|
||||
}
|
||||
}
|
||||
|
||||
private function writeNullData(User $user): User
|
||||
{
|
||||
return $user->unsetAttribute(self::METADATA_KEY);
|
||||
}
|
||||
}
|
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph;
|
||||
|
||||
use League\OAuth2\Client\Tool\BearerAuthorizationTrait;
|
||||
use LogicException;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
use Symfony\Contracts\HttpClient\ResponseInterface;
|
||||
use Symfony\Contracts\HttpClient\ResponseStreamInterface;
|
||||
|
||||
class OnBehalfOfUserHttpClient
|
||||
{
|
||||
use BearerAuthorizationTrait;
|
||||
|
||||
private HttpClientInterface $decoratedClient;
|
||||
|
||||
private OnBehalfOfUserTokenStorage $tokenStorage;
|
||||
|
||||
/**
|
||||
* @param HttpClientInterface $decoratedClient
|
||||
*/
|
||||
public function __construct(OnBehalfOfUserTokenStorage $tokenStorage, ?HttpClientInterface $decoratedClient = null)
|
||||
{
|
||||
$this->decoratedClient = $decoratedClient ?? \Symfony\Component\HttpClient\HttpClient::create();
|
||||
$this->tokenStorage = $tokenStorage;
|
||||
}
|
||||
|
||||
public function request(string $method, string $url, array $options = []): ResponseInterface
|
||||
{
|
||||
$options['headers'] = array_merge(
|
||||
$options['headers'] ?? [],
|
||||
$this->getAuthorizationHeaders($this->tokenStorage->getToken())
|
||||
);
|
||||
$options['base_uri'] = 'https://graph.microsoft.com/v1.0/';
|
||||
|
||||
switch ($method) {
|
||||
case 'GET':
|
||||
case 'HEAD':
|
||||
$options['headers']['Accept'] = 'application/json';
|
||||
|
||||
break;
|
||||
|
||||
case 'POST':
|
||||
case 'PUT':
|
||||
case 'PATCH':
|
||||
$options['headers']['Content-Type'] = 'application/json';
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new LogicException("Method not supported: {$method}");
|
||||
}
|
||||
|
||||
return $this->decoratedClient->request($method, $url, $options);
|
||||
}
|
||||
|
||||
public function stream($responses, ?float $timeout = null): ResponseStreamInterface
|
||||
{
|
||||
return $this->decoratedClient->stream($responses, $timeout);
|
||||
}
|
||||
}
|
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph;
|
||||
|
||||
use LogicException;
|
||||
use Symfony\Component\HttpFoundation\Session\SessionInterface;
|
||||
use TheNetworg\OAuth2\Client\Provider\Azure;
|
||||
use TheNetworg\OAuth2\Client\Token\AccessToken;
|
||||
|
||||
/**
|
||||
* Store token obtained on behalf of a User.
|
||||
*/
|
||||
class OnBehalfOfUserTokenStorage
|
||||
{
|
||||
public const MS_GRAPH_ACCESS_TOKEN = 'msgraph_access_token';
|
||||
|
||||
private Azure $azure;
|
||||
|
||||
private SessionInterface $session;
|
||||
|
||||
public function __construct(Azure $azure, SessionInterface $session)
|
||||
{
|
||||
$this->azure = $azure;
|
||||
$this->session = $session;
|
||||
}
|
||||
|
||||
public function getToken(): AccessToken
|
||||
{
|
||||
/** @var ?AccessToken $token */
|
||||
$token = $this->session->get(self::MS_GRAPH_ACCESS_TOKEN, null);
|
||||
|
||||
if (null === $token) {
|
||||
throw new LogicException('unexisting token');
|
||||
}
|
||||
|
||||
if ($token->hasExpired()) {
|
||||
$token = $this->azure->getAccessToken('refresh_token', [
|
||||
'refresh_token' => $token->getRefreshToken(),
|
||||
]);
|
||||
|
||||
$this->setToken($token);
|
||||
}
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
public function hasToken(): bool
|
||||
{
|
||||
return $this->session->has(self::MS_GRAPH_ACCESS_TOKEN);
|
||||
}
|
||||
|
||||
public function setToken(AccessToken $token): void
|
||||
{
|
||||
$this->session->set(self::MS_GRAPH_ACCESS_TOKEN, $token);
|
||||
}
|
||||
}
|
@@ -0,0 +1,277 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph;
|
||||
|
||||
use Chill\CalendarBundle\Entity\Calendar;
|
||||
use Chill\CalendarBundle\Entity\CalendarRange;
|
||||
use Chill\CalendarBundle\Entity\Invite;
|
||||
use Chill\CalendarBundle\RemoteCalendar\Model\RemoteEvent;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Convert Chill Calendar event to Remote MS Graph event, and MS Graph
|
||||
* event to RemoteEvent.
|
||||
*/
|
||||
class RemoteEventConverter
|
||||
{
|
||||
/**
|
||||
* valid when the remote string contains also a timezone, like in
|
||||
* lastModifiedDate.
|
||||
*/
|
||||
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?';
|
||||
|
||||
private DateTimeZone $defaultDateTimeZone;
|
||||
|
||||
private EngineInterface $engine;
|
||||
|
||||
private LocationConverter $locationConverter;
|
||||
|
||||
private LoggerInterface $logger;
|
||||
|
||||
private PersonRenderInterface $personRender;
|
||||
|
||||
private DateTimeZone $remoteDateTimeZone;
|
||||
|
||||
private 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();
|
||||
$this->remoteDateTimeZone = self::getRemoteTimeZone();
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform a CalendarRange into a representation suitable for storing into MSGraph.
|
||||
*
|
||||
* @return array an array representation for event in MS Graph
|
||||
*/
|
||||
public function calendarRangeToEvent(CalendarRange $calendarRange): array
|
||||
{
|
||||
return [
|
||||
'subject' => $this->translator->trans('remote_calendar.calendar_range_title'),
|
||||
'start' => [
|
||||
'dateTime' => $calendarRange->getStartDate()->setTimezone($this->remoteDateTimeZone)
|
||||
->format(self::REMOTE_DATE_FORMAT),
|
||||
'timeZone' => 'UTC',
|
||||
],
|
||||
'end' => [
|
||||
'dateTime' => $calendarRange->getEndDate()->setTimezone($this->remoteDateTimeZone)
|
||||
->format(self::REMOTE_DATE_FORMAT),
|
||||
'timeZone' => 'UTC',
|
||||
],
|
||||
'attendees' => [
|
||||
[
|
||||
'emailAddress' => [
|
||||
'address' => $calendarRange->getUser()->getEmailCanonical(),
|
||||
'name' => $calendarRange->getUser()->getLabel(),
|
||||
],
|
||||
],
|
||||
],
|
||||
'isReminderOn' => false,
|
||||
'location' => $this->locationConverter->locationToRemote($calendarRange->getLocation()),
|
||||
];
|
||||
}
|
||||
|
||||
public function calendarToEvent(Calendar $calendar): array
|
||||
{
|
||||
$result = array_merge(
|
||||
[
|
||||
'subject' => '[Chill] ' .
|
||||
implode(
|
||||
', ',
|
||||
$calendar->getPersons()->map(function (Person $p) {
|
||||
return $this->personRender->renderString($p, []);
|
||||
})->toArray()
|
||||
),
|
||||
'start' => [
|
||||
'dateTime' => $calendar->getStartDate()->setTimezone($this->remoteDateTimeZone)
|
||||
->format(self::REMOTE_DATE_FORMAT),
|
||||
'timeZone' => 'UTC',
|
||||
],
|
||||
'end' => [
|
||||
'dateTime' => $calendar->getEndDate()->setTimezone($this->remoteDateTimeZone)
|
||||
->format(self::REMOTE_DATE_FORMAT),
|
||||
'timeZone' => 'UTC',
|
||||
],
|
||||
'allowNewTimeProposals' => false,
|
||||
'transactionId' => 'calendar_' . $calendar->getId(),
|
||||
'body' => [
|
||||
'contentType' => 'text',
|
||||
'content' => $this->engine->render(
|
||||
'@ChillCalendar/MSGraph/calendar_event_body.html.twig',
|
||||
['calendar' => $calendar]
|
||||
),
|
||||
],
|
||||
'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
|
||||
{
|
||||
return [
|
||||
'attendees' => $calendar->getInvites()->map(
|
||||
function (Invite $i) {
|
||||
return $this->buildInviteToAttendee($i);
|
||||
}
|
||||
)->toArray(),
|
||||
];
|
||||
}
|
||||
|
||||
public function convertAvailabilityToRemoteEvent(array $event): RemoteEvent
|
||||
{
|
||||
$startDate =
|
||||
DateTimeImmutable::createFromFormat(self::REMOTE_DATE_FORMAT, $event['start']['dateTime'], $this->remoteDateTimeZone)
|
||||
->setTimezone($this->defaultDateTimeZone);
|
||||
$endDate =
|
||||
DateTimeImmutable::createFromFormat(self::REMOTE_DATE_FORMAT, $event['end']['dateTime'], $this->remoteDateTimeZone)
|
||||
->setTimezone($this->defaultDateTimeZone);
|
||||
|
||||
return new RemoteEvent(
|
||||
uniqid('generated_'),
|
||||
$this->translator->trans('remote_ms_graph.freebusy_statuses.' . $event['status']),
|
||||
'',
|
||||
$startDate,
|
||||
$endDate
|
||||
);
|
||||
}
|
||||
|
||||
public static function convertStringDateWithoutTimezone(string $date): DateTimeImmutable
|
||||
{
|
||||
$d = DateTimeImmutable::createFromFormat(
|
||||
self::REMOTE_DATETIME_WITHOUT_TZ_FORMAT,
|
||||
$date,
|
||||
self::getRemoteTimeZone()
|
||||
);
|
||||
|
||||
if (false === $d) {
|
||||
throw new RuntimeException("could not convert string date to datetime: {$date}");
|
||||
}
|
||||
|
||||
return $d->setTimezone((new DateTimeImmutable())->getTimezone());
|
||||
}
|
||||
|
||||
public static function convertStringDateWithTimezone(string $date): DateTimeImmutable
|
||||
{
|
||||
$d = DateTimeImmutable::createFromFormat(self::REMOTE_DATETIMEZONE_FORMAT, $date);
|
||||
|
||||
if (false === $d) {
|
||||
throw new RuntimeException("could not convert string date to datetime: {$date}");
|
||||
}
|
||||
|
||||
$d->setTimezone((new DateTimeImmutable())->getTimezone());
|
||||
|
||||
return $d;
|
||||
}
|
||||
|
||||
public function convertToRemote(array $event): RemoteEvent
|
||||
{
|
||||
$startDate =
|
||||
DateTimeImmutable::createFromFormat(self::REMOTE_DATE_FORMAT, $event['start']['dateTime'], $this->remoteDateTimeZone)
|
||||
->setTimezone($this->defaultDateTimeZone);
|
||||
$endDate =
|
||||
DateTimeImmutable::createFromFormat(self::REMOTE_DATE_FORMAT, $event['end']['dateTime'], $this->remoteDateTimeZone)
|
||||
->setTimezone($this->defaultDateTimeZone);
|
||||
|
||||
return new RemoteEvent(
|
||||
$event['id'],
|
||||
$event['subject'],
|
||||
'',
|
||||
$startDate,
|
||||
$endDate,
|
||||
$event['isAllDay']
|
||||
);
|
||||
}
|
||||
|
||||
public function getLastModifiedDate(array $event): DateTimeImmutable
|
||||
{
|
||||
$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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a string which format a DateTime to string. To be used in POST requests,.
|
||||
*/
|
||||
public static function getRemoteDateTimeSimpleFormat(): string
|
||||
{
|
||||
return 'Y-m-d\TH:i:s';
|
||||
}
|
||||
|
||||
public static function getRemoteTimeZone(): DateTimeZone
|
||||
{
|
||||
return new DateTimeZone('UTC');
|
||||
}
|
||||
|
||||
private function buildInviteToAttendee(Invite $invite): array
|
||||
{
|
||||
return [
|
||||
'emailAddress' => [
|
||||
'address' => $invite->getUser()->getEmail(),
|
||||
'name' => $invite->getUser()->getLabel(),
|
||||
],
|
||||
'type' => 'Required',
|
||||
];
|
||||
}
|
||||
}
|
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\RemoteToLocalSync;
|
||||
|
||||
use Chill\CalendarBundle\Entity\CalendarRange;
|
||||
use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\MachineHttpClient;
|
||||
use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\RemoteEventConverter;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use RuntimeException;
|
||||
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
class CalendarRangeSyncer
|
||||
{
|
||||
private EntityManagerInterface $em;
|
||||
|
||||
private LoggerInterface $logger;
|
||||
|
||||
private HttpClientInterface $machineHttpClient;
|
||||
|
||||
/**
|
||||
* @param MachineHttpClient $machineHttpClient
|
||||
*/
|
||||
public function __construct(
|
||||
EntityManagerInterface $em,
|
||||
LoggerInterface $logger,
|
||||
HttpClientInterface $machineHttpClient
|
||||
) {
|
||||
$this->em = $em;
|
||||
$this->logger = $logger;
|
||||
$this->machineHttpClient = $machineHttpClient;
|
||||
}
|
||||
|
||||
public function handleCalendarRangeSync(CalendarRange $calendarRange, array $notification, User $user): void
|
||||
{
|
||||
switch ($notification['changeType']) {
|
||||
case 'deleted':
|
||||
// test if the notification is not linked to a Calendar
|
||||
if (null !== $calendarRange->getCalendar()) {
|
||||
return;
|
||||
}
|
||||
$calendarRange->preventEnqueueChanges = true;
|
||||
|
||||
$this->logger->info(__CLASS__ . ' remove a calendar range because deleted on remote calendar');
|
||||
$this->em->remove($calendarRange);
|
||||
|
||||
break;
|
||||
|
||||
case 'updated':
|
||||
try {
|
||||
$new = $this->machineHttpClient->request(
|
||||
'GET',
|
||||
$notification['resource']
|
||||
)->toArray();
|
||||
} catch (ClientExceptionInterface $clientException) {
|
||||
$this->logger->warning(__CLASS__ . ' could not retrieve event from ms graph. Already deleted ?', [
|
||||
'calendarRangeId' => $calendarRange->getId(),
|
||||
'remoteEventId' => $notification['resource'],
|
||||
]);
|
||||
|
||||
throw $clientException;
|
||||
}
|
||||
|
||||
$lastModified = RemoteEventConverter::convertStringDateWithTimezone($new['lastModifiedDateTime']);
|
||||
|
||||
if ($calendarRange->getRemoteAttributes()['lastModifiedDateTime'] === $lastModified->getTimestamp()) {
|
||||
$this->logger->info(__CLASS__ . ' change key is equals. Source is probably a local update', [
|
||||
'calendarRangeId' => $calendarRange->getId(),
|
||||
'remoteEventId' => $notification['resource'],
|
||||
]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$startDate = RemoteEventConverter::convertStringDateWithoutTimezone($new['start']['dateTime']);
|
||||
$endDate = RemoteEventConverter::convertStringDateWithoutTimezone($new['end']['dateTime']);
|
||||
|
||||
$calendarRange
|
||||
->setStartDate($startDate)->setEndDate($endDate)
|
||||
->addRemoteAttributes([
|
||||
'lastModifiedDateTime' => $lastModified->getTimestamp(),
|
||||
'changeKey' => $new['changeKey'],
|
||||
])
|
||||
->preventEnqueueChanges = true;
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new RuntimeException('This changeType is not suppored: ' . $notification['changeType']);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,182 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\RemoteToLocalSync;
|
||||
|
||||
use Chill\CalendarBundle\Entity\Calendar;
|
||||
use Chill\CalendarBundle\Entity\Invite;
|
||||
use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\RemoteEventConverter;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Repository\UserRepositoryInterface;
|
||||
use LogicException;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use RuntimeException;
|
||||
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
use function in_array;
|
||||
|
||||
class CalendarSyncer
|
||||
{
|
||||
private LoggerInterface $logger;
|
||||
|
||||
private HttpClientInterface $machineHttpClient;
|
||||
|
||||
private UserRepositoryInterface $userRepository;
|
||||
|
||||
public function __construct(LoggerInterface $logger, HttpClientInterface $machineHttpClient, UserRepositoryInterface $userRepository)
|
||||
{
|
||||
$this->logger = $logger;
|
||||
$this->machineHttpClient = $machineHttpClient;
|
||||
$this->userRepository = $userRepository;
|
||||
}
|
||||
|
||||
public function handleCalendarSync(Calendar $calendar, array $notification, User $user): void
|
||||
{
|
||||
switch ($notification['changeType']) {
|
||||
case 'deleted':
|
||||
$this->handleDeleteCalendar($calendar, $notification, $user);
|
||||
|
||||
break;
|
||||
|
||||
case 'updated':
|
||||
$this->handleUpdateCalendar($calendar, $notification, $user);
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new RuntimeException('this change type is not supported: ' . $notification['changeType']);
|
||||
}
|
||||
}
|
||||
|
||||
private function handleDeleteCalendar(Calendar $calendar, array $notification, User $user): void
|
||||
{
|
||||
$calendar
|
||||
->setStatus(Calendar::STATUS_CANCELED)
|
||||
->setCalendarRange(null);
|
||||
$calendar->preventEnqueueChanges = true;
|
||||
}
|
||||
|
||||
private function handleUpdateCalendar(Calendar $calendar, array $notification, User $user): void
|
||||
{
|
||||
try {
|
||||
$new = $this->machineHttpClient->request(
|
||||
'GET',
|
||||
$notification['resource']
|
||||
)->toArray();
|
||||
} catch (ClientExceptionInterface $clientException) {
|
||||
$this->logger->warning(__CLASS__ . ' could not retrieve event from ms graph. Already deleted ?', [
|
||||
'calendarId' => $calendar->getId(),
|
||||
'remoteEventId' => $notification['resource'],
|
||||
]);
|
||||
|
||||
throw $clientException;
|
||||
}
|
||||
|
||||
if (false === $new['isOrganizer']) {
|
||||
return;
|
||||
}
|
||||
|
||||
$lastModified = RemoteEventConverter::convertStringDateWithTimezone(
|
||||
$new['lastModifiedDateTime']
|
||||
);
|
||||
|
||||
if ($calendar->getRemoteAttributes()['lastModifiedDateTime'] === $lastModified->getTimestamp()) {
|
||||
$this->logger->info(__CLASS__ . ' change key is equals. Source is probably a local update', [
|
||||
'calendarRangeId' => $calendar->getId(),
|
||||
'remoteEventId' => $notification['resource'],
|
||||
]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->syncAttendees($calendar, $new['attendees']);
|
||||
|
||||
$startDate = RemoteEventConverter::convertStringDateWithoutTimezone($new['start']['dateTime']);
|
||||
$endDate = RemoteEventConverter::convertStringDateWithoutTimezone($new['end']['dateTime']);
|
||||
|
||||
if ($startDate->getTimestamp() !== $calendar->getStartDate()->getTimestamp()) {
|
||||
$calendar->setStartDate($startDate)->setStatus(Calendar::STATUS_MOVED);
|
||||
}
|
||||
|
||||
if ($endDate->getTimestamp() !== $calendar->getEndDate()->getTimestamp()) {
|
||||
$calendar->setEndDate($endDate)->setStatus(Calendar::STATUS_MOVED);
|
||||
}
|
||||
|
||||
$calendar
|
||||
->addRemoteAttributes([
|
||||
'lastModifiedDateTime' => $lastModified->getTimestamp(),
|
||||
'changeKey' => $new['changeKey'],
|
||||
])
|
||||
->preventEnqueueChanges = true;
|
||||
}
|
||||
|
||||
private function syncAttendees(Calendar $calendar, array $attendees): void
|
||||
{
|
||||
$emails = [];
|
||||
|
||||
foreach ($attendees as $attendee) {
|
||||
$status = $attendee['status']['response'];
|
||||
|
||||
if ('organizer' === $status) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$email = $attendee['emailAddress']['address'];
|
||||
$emails[] = strtolower($email);
|
||||
$user = $this->userRepository->findOneByUsernameOrEmail($email);
|
||||
|
||||
if (null === $user) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$calendar->isInvited($user)) {
|
||||
$calendar->addUser($user);
|
||||
}
|
||||
|
||||
$invite = $calendar->getInviteForUser($user);
|
||||
|
||||
switch ($status) {
|
||||
// possible cases: none, organizer, tentativelyAccepted, accepted, declined, notResponded.
|
||||
case 'none':
|
||||
case 'notResponded':
|
||||
$invite->setStatus(Invite::PENDING);
|
||||
|
||||
break;
|
||||
|
||||
case 'tentativelyAccepted':
|
||||
$invite->setStatus(Invite::TENTATIVELY_ACCEPTED);
|
||||
|
||||
break;
|
||||
|
||||
case 'accepted':
|
||||
$invite->setStatus(Invite::ACCEPTED);
|
||||
|
||||
break;
|
||||
|
||||
case 'declined':
|
||||
$invite->setStatus(Invite::DECLINED);
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new LogicException('should not happens, not implemented: ' . $status);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($calendar->getUsers() as $user) {
|
||||
if (!in_array(strtolower($user->getEmailCanonical()), $emails, true)) {
|
||||
$calendar->removeUser($user);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,733 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\CalendarBundle\RemoteCalendar\Connector;
|
||||
|
||||
use Chill\CalendarBundle\Entity\Calendar;
|
||||
use Chill\CalendarBundle\Entity\CalendarRange;
|
||||
use Chill\CalendarBundle\Entity\Invite;
|
||||
use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\MapCalendarToUser;
|
||||
use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\OnBehalfOfUserHttpClient;
|
||||
use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\OnBehalfOfUserTokenStorage;
|
||||
use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\RemoteEventConverter;
|
||||
use Chill\CalendarBundle\Repository\CalendarRangeRepository;
|
||||
use Chill\CalendarBundle\Repository\CalendarRepository;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use DateTimeImmutable;
|
||||
use Exception;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
use function array_key_exists;
|
||||
use function count;
|
||||
|
||||
class MSGraphRemoteCalendarConnector implements RemoteCalendarConnectorInterface
|
||||
{
|
||||
private array $cacheScheduleTimeForUser = [];
|
||||
|
||||
private CalendarRangeRepository $calendarRangeRepository;
|
||||
|
||||
private CalendarRepository $calendarRepository;
|
||||
|
||||
private LoggerInterface $logger;
|
||||
|
||||
private HttpClientInterface $machineHttpClient;
|
||||
|
||||
private MapCalendarToUser $mapCalendarToUser;
|
||||
|
||||
private RemoteEventConverter $remoteEventConverter;
|
||||
|
||||
private OnBehalfOfUserTokenStorage $tokenStorage;
|
||||
|
||||
private TranslatorInterface $translator;
|
||||
|
||||
private UrlGeneratorInterface $urlGenerator;
|
||||
|
||||
private OnBehalfOfUserHttpClient $userHttpClient;
|
||||
|
||||
public function __construct(
|
||||
CalendarRepository $calendarRepository,
|
||||
CalendarRangeRepository $calendarRangeRepository,
|
||||
HttpClientInterface $machineHttpClient,
|
||||
MapCalendarToUser $mapCalendarToUser,
|
||||
LoggerInterface $logger,
|
||||
OnBehalfOfUserTokenStorage $tokenStorage,
|
||||
OnBehalfOfUserHttpClient $userHttpClient,
|
||||
RemoteEventConverter $remoteEventConverter,
|
||||
TranslatorInterface $translator,
|
||||
UrlGeneratorInterface $urlGenerator
|
||||
) {
|
||||
$this->calendarRepository = $calendarRepository;
|
||||
$this->calendarRangeRepository = $calendarRangeRepository;
|
||||
$this->machineHttpClient = $machineHttpClient;
|
||||
$this->mapCalendarToUser = $mapCalendarToUser;
|
||||
$this->logger = $logger;
|
||||
$this->remoteEventConverter = $remoteEventConverter;
|
||||
$this->tokenStorage = $tokenStorage;
|
||||
$this->translator = $translator;
|
||||
$this->urlGenerator = $urlGenerator;
|
||||
$this->userHttpClient = $userHttpClient;
|
||||
}
|
||||
|
||||
public function countEventsForUser(User $user, DateTimeImmutable $startDate, DateTimeImmutable $endDate): int
|
||||
{
|
||||
$userId = $this->mapCalendarToUser->getUserId($user);
|
||||
|
||||
if (null === $userId) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
try {
|
||||
$data = $this->userHttpClient->request(
|
||||
'GET',
|
||||
'users/' . $userId . '/calendarView',
|
||||
[
|
||||
'query' => [
|
||||
'startDateTime' => $startDate->format(DateTimeImmutable::ATOM),
|
||||
'endDateTime' => $endDate->format(DateTimeImmutable::ATOM),
|
||||
'$count' => 'true',
|
||||
'$top' => 0,
|
||||
],
|
||||
]
|
||||
)->toArray();
|
||||
} catch (ClientExceptionInterface $e) {
|
||||
if (403 === $e->getResponse()->getStatusCode()) {
|
||||
return count($this->getScheduleTimesForUser($user, $startDate, $endDate));
|
||||
}
|
||||
|
||||
$this->logger->error('Could not get list of event on MSGraph', [
|
||||
'error_code' => $e->getResponse()->getStatusCode(),
|
||||
'error' => $e->getResponse()->getInfo(),
|
||||
]);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
return $data['@odata.count'];
|
||||
}
|
||||
|
||||
public function getMakeReadyResponse(string $returnPath): Response
|
||||
{
|
||||
return new RedirectResponse($this->urlGenerator
|
||||
->generate('chill_calendar_remote_connect_azure', ['returnPath' => $returnPath]));
|
||||
}
|
||||
|
||||
public function isReady(): bool
|
||||
{
|
||||
return $this->tokenStorage->hasToken();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface
|
||||
* @throws \Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface
|
||||
* @throws \Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface
|
||||
* @throws \Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface
|
||||
*
|
||||
* @return array|\Chill\CalendarBundle\RemoteCalendar\Model\RemoteEvent[]
|
||||
*/
|
||||
public function listEventsForUser(User $user, DateTimeImmutable $startDate, DateTimeImmutable $endDate, ?int $offset = 0, ?int $limit = 50): array
|
||||
{
|
||||
$userId = $this->mapCalendarToUser->getUserId($user);
|
||||
|
||||
if (null === $userId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
$bareEvents = $this->userHttpClient->request(
|
||||
'GET',
|
||||
'users/' . $userId . '/calendarView',
|
||||
[
|
||||
'query' => [
|
||||
'startDateTime' => $startDate->format(DateTimeImmutable::ATOM),
|
||||
'endDateTime' => $endDate->format(DateTimeImmutable::ATOM),
|
||||
'$select' => 'id,subject,start,end,isAllDay',
|
||||
'$top' => $limit,
|
||||
'$skip' => $offset,
|
||||
],
|
||||
]
|
||||
)->toArray();
|
||||
|
||||
$ids = array_map(static function ($item) { return $item['id']; }, $bareEvents['value']);
|
||||
$existingIdsInRange = $this->calendarRangeRepository->findRemoteIdsPresent($ids);
|
||||
$existingIdsInCalendar = $this->calendarRepository->findRemoteIdsPresent($ids);
|
||||
|
||||
return array_values(
|
||||
array_map(
|
||||
function ($item) {
|
||||
return $this->remoteEventConverter->convertToRemote($item);
|
||||
},
|
||||
// filter all event to keep only the one not in range
|
||||
array_filter(
|
||||
$bareEvents['value'],
|
||||
static function ($item) use ($existingIdsInRange, $existingIdsInCalendar) {
|
||||
return ((!$existingIdsInRange[$item['id']]) ?? true) && ((!$existingIdsInCalendar[$item['id']]) ?? true);
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
||||
} catch (ClientExceptionInterface $e) {
|
||||
if (403 === $e->getResponse()->getStatusCode()) {
|
||||
return $this->getScheduleTimesForUser($user, $startDate, $endDate);
|
||||
}
|
||||
|
||||
$this->logger->error('Could not get list of event on MSGraph', [
|
||||
'error_code' => $e->getResponse()->getStatusCode(),
|
||||
'error' => $e->getResponse()->getInfo(),
|
||||
]);
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public function removeCalendar(string $remoteId, array $remoteAttributes, User $user, ?CalendarRange $associatedCalendarRange = null): void
|
||||
{
|
||||
if ('' === $remoteId) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->removeEvent($remoteId, $user);
|
||||
|
||||
if (null !== $associatedCalendarRange) {
|
||||
$this->syncCalendarRange($associatedCalendarRange);
|
||||
}
|
||||
}
|
||||
|
||||
public function removeCalendarRange(string $remoteId, array $remoteAttributes, User $user): void
|
||||
{
|
||||
if ('' === $remoteId) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->removeEvent($remoteId, $user);
|
||||
}
|
||||
|
||||
public function syncCalendar(Calendar $calendar, string $action, ?CalendarRange $previousCalendarRange, ?User $previousMainUser, ?array $oldInvites, ?array $newInvites): void
|
||||
{
|
||||
/*
|
||||
* cases to support:
|
||||
*
|
||||
* * a calendar range is created:
|
||||
* * create on remote
|
||||
* * if calendar range is associated: remove the range
|
||||
* * a Calendar change the CalendarRange:
|
||||
* * re-create the previous calendar range;
|
||||
* * remove the current calendar range
|
||||
* * a calendar change the mainUser
|
||||
* * cancel the calendar in the previous mainUser
|
||||
* * recreate the previous calendar range in the previousMainUser, if any
|
||||
* * delete the current calendar range in the current mainUser, if any
|
||||
* * create the calendar in the current mainUser
|
||||
*
|
||||
*/
|
||||
|
||||
if (!$calendar->hasRemoteId()) {
|
||||
$this->createCalendarOnRemote($calendar);
|
||||
} else {
|
||||
if (null !== $previousMainUser) {
|
||||
// cancel event in previousMainUserCalendar
|
||||
$this->cancelOnRemote(
|
||||
$calendar->getRemoteId(),
|
||||
$this->translator->trans('remote_ms_graph.cancel_event_because_main_user_is_%label%', ['%label%' => $calendar->getMainUser()]),
|
||||
$previousMainUser,
|
||||
'calendar_' . $calendar->getRemoteId()
|
||||
);
|
||||
$this->createCalendarOnRemote($calendar);
|
||||
} else {
|
||||
$this->patchCalendarOnRemote($calendar, $newInvites);
|
||||
}
|
||||
}
|
||||
|
||||
if ($calendar->hasCalendarRange() && $calendar->getCalendarRange()->hasRemoteId()) {
|
||||
$this->removeEvent(
|
||||
$calendar->getCalendarRange()->getRemoteId(),
|
||||
$calendar->getCalendarRange()->getUser()
|
||||
);
|
||||
|
||||
$calendar->getCalendarRange()
|
||||
->addRemoteAttributes([
|
||||
'lastModifiedDateTime' => null,
|
||||
'changeKey' => null,
|
||||
'previousId' => $calendar->getCalendarRange()->getRemoteId(),
|
||||
])
|
||||
->setRemoteId('');
|
||||
}
|
||||
|
||||
if (null !== $previousCalendarRange) {
|
||||
$this->createRemoteCalendarRange($previousCalendarRange);
|
||||
}
|
||||
}
|
||||
|
||||
public function syncCalendarRange(CalendarRange $calendarRange): void
|
||||
{
|
||||
if ($calendarRange->hasRemoteId()) {
|
||||
$this->updateRemoteCalendarRange($calendarRange);
|
||||
} else {
|
||||
$this->createRemoteCalendarRange($calendarRange);
|
||||
}
|
||||
}
|
||||
|
||||
public function syncInvite(Invite $invite): void
|
||||
{
|
||||
if ('' === $remoteId = $invite->getCalendar()->getRemoteId()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (null === $invite->getUser()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (null === $userId = $this->mapCalendarToUser->getUserId($invite->getUser())) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($invite->hasRemoteId()) {
|
||||
$remoteIdAttendeeCalendar = $invite->getRemoteId();
|
||||
} else {
|
||||
$remoteIdAttendeeCalendar = $this->findRemoteIdOnUserCalendar($invite->getCalendar(), $invite->getUser());
|
||||
$invite->setRemoteId($remoteIdAttendeeCalendar);
|
||||
}
|
||||
|
||||
switch ($invite->getStatus()) {
|
||||
case Invite::PENDING:
|
||||
return;
|
||||
|
||||
case Invite::ACCEPTED:
|
||||
$url = "/v1.0/users/{$userId}/calendar/events/{$remoteIdAttendeeCalendar}/accept";
|
||||
|
||||
break;
|
||||
|
||||
case Invite::TENTATIVELY_ACCEPTED:
|
||||
$url = "/v1.0/users/{$userId}/calendar/events/{$remoteIdAttendeeCalendar}/tentativelyAccept";
|
||||
|
||||
break;
|
||||
|
||||
case Invite::DECLINED:
|
||||
$url = "/v1.0/users/{$userId}/calendar/events/{$remoteIdAttendeeCalendar}/decline";
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Exception('not supported');
|
||||
}
|
||||
|
||||
try {
|
||||
$this->machineHttpClient->request(
|
||||
'POST',
|
||||
$url,
|
||||
['json' => ['sendResponse' => true]]
|
||||
);
|
||||
} catch (ClientExceptionInterface $e) {
|
||||
$this->logger->warning('could not update calendar range to remote', [
|
||||
'exception' => $e->getTraceAsString(),
|
||||
'content' => $e->getResponse()->getContent(),
|
||||
'calendarRangeId' => 'invite_' . $invite->getId(),
|
||||
]);
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
private function cancelOnRemote(string $remoteId, string $comment, User $user, string $identifier): void
|
||||
{
|
||||
$userId = $this->mapCalendarToUser->getUserId($user);
|
||||
|
||||
if (null === $userId) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->machineHttpClient->request(
|
||||
'POST',
|
||||
"users/{$userId}/calendar/events/{$remoteId}/cancel",
|
||||
[
|
||||
'json' => ['Comment' => $comment],
|
||||
]
|
||||
);
|
||||
} catch (ClientExceptionInterface $e) {
|
||||
$this->logger->warning('could not update calendar range to remote', [
|
||||
'exception' => $e->getTraceAsString(),
|
||||
'content' => $e->getResponse()->getContent(),
|
||||
'calendarRangeId' => $identifier,
|
||||
]);
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
private function createCalendarOnRemote(Calendar $calendar): void
|
||||
{
|
||||
$eventData = $this->remoteEventConverter->calendarToEvent($calendar);
|
||||
|
||||
[
|
||||
'id' => $id,
|
||||
'lastModifiedDateTime' => $lastModified,
|
||||
'changeKey' => $changeKey
|
||||
] = $this->createOnRemote($eventData, $calendar->getMainUser(), 'calendar_' . $calendar->getId());
|
||||
|
||||
if (null === $id) {
|
||||
return;
|
||||
}
|
||||
|
||||
$calendar
|
||||
->setRemoteId($id)
|
||||
->addRemoteAttributes([
|
||||
'lastModifiedDateTime' => $lastModified,
|
||||
'changeKey' => $changeKey,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $identifier an identifier for logging in case of something does not work
|
||||
*
|
||||
* @return array{?id: string, ?lastModifiedDateTime: int, ?changeKey: string}
|
||||
*/
|
||||
private function createOnRemote(array $eventData, User $user, string $identifier): array
|
||||
{
|
||||
$userId = $this->mapCalendarToUser->getUserId($user);
|
||||
|
||||
if (null === $userId) {
|
||||
$this->logger->warning('user does not have userId nor calendarId', [
|
||||
'user_id' => $user->getId(),
|
||||
'calendar_identifier' => $identifier,
|
||||
]);
|
||||
|
||||
return ['id' => null, 'lastModifiedDateTime' => null, 'changeKey' => null];
|
||||
}
|
||||
|
||||
try {
|
||||
$event = $this->machineHttpClient->request(
|
||||
'POST',
|
||||
'users/' . $userId . '/calendar/events',
|
||||
[
|
||||
'json' => $eventData,
|
||||
]
|
||||
)->toArray();
|
||||
} catch (ClientExceptionInterface $e) {
|
||||
$this->logger->warning('could not save calendar range to remote', [
|
||||
'exception' => $e->getTraceAsString(),
|
||||
'content' => $e->getResponse()->getContent(),
|
||||
'calendar_identifier' => $identifier,
|
||||
]);
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => $event['id'],
|
||||
'lastModifiedDateTime' => $this->remoteEventConverter->getLastModifiedDate($event)->getTimestamp(),
|
||||
'changeKey' => $event['changeKey'],
|
||||
];
|
||||
}
|
||||
|
||||
private function createRemoteCalendarRange(CalendarRange $calendarRange): void
|
||||
{
|
||||
$userId = $this->mapCalendarToUser->getUserId($calendarRange->getUser());
|
||||
|
||||
if (null === $userId) {
|
||||
$this->logger->warning('user does not have userId nor calendarId', [
|
||||
'user_id' => $calendarRange->getUser()->getId(),
|
||||
'calendar_range_id' => $calendarRange->getId(),
|
||||
]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$eventData = $this->remoteEventConverter->calendarRangeToEvent($calendarRange);
|
||||
|
||||
[
|
||||
'id' => $id,
|
||||
'lastModifiedDateTime' => $lastModified,
|
||||
'changeKey' => $changeKey
|
||||
] = $this->createOnRemote(
|
||||
$eventData,
|
||||
$calendarRange->getUser(),
|
||||
'calendar_range_' . $calendarRange->getId()
|
||||
);
|
||||
|
||||
$calendarRange->setRemoteId($id)
|
||||
->addRemoteAttributes([
|
||||
'lastModifiedDateTime' => $lastModified,
|
||||
'changeKey' => $changeKey,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* the remoteId is not the same across different user calendars. This method allow to find
|
||||
* the correct remoteId in another calendar.
|
||||
*
|
||||
* For achieving this, the iCalUid is used.
|
||||
*/
|
||||
private function findRemoteIdOnUserCalendar(Calendar $calendar, User $user): ?string
|
||||
{
|
||||
// find the icalUid on original user
|
||||
$event = $this->getOnRemote($calendar->getMainUser(), $calendar->getRemoteId());
|
||||
$userId = $this->mapCalendarToUser->getUserId($user);
|
||||
|
||||
if ('' === $iCalUid = ($event['iCalUId'] ?? '')) {
|
||||
throw new Exception('no iCalUid for this event');
|
||||
}
|
||||
|
||||
try {
|
||||
$events = $this->machineHttpClient->request(
|
||||
'GET',
|
||||
"/v1.0/users/{$userId}/calendar/events",
|
||||
[
|
||||
'query' => [
|
||||
'$select' => 'id',
|
||||
'$filter' => "iCalUId eq '{$iCalUid}'",
|
||||
],
|
||||
]
|
||||
)->toArray();
|
||||
} catch (ClientExceptionInterface $clientException) {
|
||||
throw $clientException;
|
||||
}
|
||||
|
||||
if (1 !== count($events['value'])) {
|
||||
throw new Exception('multiple events found with same iCalUid');
|
||||
}
|
||||
|
||||
return $events['value'][0]['id'];
|
||||
}
|
||||
|
||||
private function getOnRemote(User $user, string $remoteId): array
|
||||
{
|
||||
$userId = $this->mapCalendarToUser->getUserId($user);
|
||||
|
||||
if (null === $userId) {
|
||||
throw new Exception('no remote calendar for this user', [
|
||||
'user' => $user->getId(),
|
||||
'remoteId' => $remoteId,
|
||||
]);
|
||||
}
|
||||
|
||||
try {
|
||||
return $this->machineHttpClient->request(
|
||||
'GET',
|
||||
'users/' . $userId . '/calendar/events/' . $remoteId
|
||||
)->toArray();
|
||||
} catch (ClientExceptionInterface $e) {
|
||||
$this->logger->warning('Could not get event from calendar', [
|
||||
'remoteId' => $remoteId,
|
||||
]);
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
private function getScheduleTimesForUser(User $user, DateTimeImmutable $startDate, DateTimeImmutable $endDate): array
|
||||
{
|
||||
$userId = $this->mapCalendarToUser->getUserId($user);
|
||||
|
||||
if (array_key_exists($userId, $this->cacheScheduleTimeForUser)) {
|
||||
return $this->cacheScheduleTimeForUser[$userId];
|
||||
}
|
||||
|
||||
if (null === $userId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (null === $user->getEmailCanonical() || '' === $user->getEmailCanonical()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$body = [
|
||||
'schedules' => [$user->getEmailCanonical()],
|
||||
'startTime' => [
|
||||
'dateTime' => ($startDate->setTimezone(RemoteEventConverter::getRemoteTimeZone())->format(RemoteEventConverter::getRemoteDateTimeSimpleFormat())),
|
||||
'timeZone' => 'UTC',
|
||||
],
|
||||
'endTime' => [
|
||||
'dateTime' => ($endDate->setTimezone(RemoteEventConverter::getRemoteTimeZone())->format(RemoteEventConverter::getRemoteDateTimeSimpleFormat())),
|
||||
'timeZone' => 'UTC',
|
||||
],
|
||||
];
|
||||
|
||||
try {
|
||||
$response = $this->userHttpClient->request('POST', 'users/' . $userId . '/calendar/getSchedule', [
|
||||
'json' => $body,
|
||||
])->toArray();
|
||||
} catch (ClientExceptionInterface $e) {
|
||||
$this->logger->debug('Could not get schedule on MSGraph', [
|
||||
'error_code' => $e->getResponse()->getStatusCode(),
|
||||
'error' => $e->getResponse()->getInfo(),
|
||||
]);
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
$this->cacheScheduleTimeForUser[$userId] = array_map(
|
||||
function ($item) {
|
||||
return $this->remoteEventConverter->convertAvailabilityToRemoteEvent($item);
|
||||
},
|
||||
$response['value'][0]['scheduleItems']
|
||||
);
|
||||
|
||||
return $this->cacheScheduleTimeForUser[$userId];
|
||||
}
|
||||
|
||||
private function patchCalendarOnRemote(Calendar $calendar, array $newInvites): void
|
||||
{
|
||||
$eventDatas = [];
|
||||
$eventDatas[] = $this->remoteEventConverter->calendarToEvent($calendar);
|
||||
|
||||
if (0 < count($newInvites)) {
|
||||
// it seems that invitaiton are always send, even if attendee changes are mixed with other datas
|
||||
// $eventDatas[] = $this->remoteEventConverter->calendarToEventAttendeesOnly($calendar);
|
||||
}
|
||||
|
||||
foreach ($eventDatas as $eventData) {
|
||||
[
|
||||
'id' => $id,
|
||||
'lastModifiedDateTime' => $lastModified,
|
||||
'changeKey' => $changeKey
|
||||
] = $this->patchOnRemote(
|
||||
$calendar->getRemoteId(),
|
||||
$eventData,
|
||||
$calendar->getMainUser(),
|
||||
'calendar_' . $calendar->getId()
|
||||
);
|
||||
|
||||
$calendar->addRemoteAttributes([
|
||||
'lastModifiedDateTime' => $lastModified,
|
||||
'changeKey' => $changeKey,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $identifier an identifier for logging in case of something does not work
|
||||
*
|
||||
* @return array{?id: string, ?lastModifiedDateTime: int, ?changeKey: string}
|
||||
*/
|
||||
private function patchOnRemote(string $remoteId, array $eventData, User $user, string $identifier): array
|
||||
{
|
||||
$userId = $this->mapCalendarToUser->getUserId($user);
|
||||
|
||||
if (null === $userId) {
|
||||
$this->logger->warning('user does not have userId nor calendarId', [
|
||||
'user_id' => $user->getId(),
|
||||
'calendar_identifier' => $identifier,
|
||||
]);
|
||||
|
||||
return ['id' => null, 'lastModifiedDateTime' => null, 'changeKey' => null];
|
||||
}
|
||||
|
||||
try {
|
||||
$event = $this->machineHttpClient->request(
|
||||
'PATCH',
|
||||
'users/' . $userId . '/calendar/events/' . $remoteId,
|
||||
[
|
||||
'json' => $eventData,
|
||||
]
|
||||
)->toArray();
|
||||
} catch (ClientExceptionInterface $e) {
|
||||
$this->logger->warning('could not update calendar range to remote', [
|
||||
'exception' => $e->getTraceAsString(),
|
||||
'calendarRangeId' => $identifier,
|
||||
]);
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => $event['id'],
|
||||
'lastModifiedDateTime' => $this->remoteEventConverter->getLastModifiedDate($event)->getTimestamp(),
|
||||
'changeKey' => $event['changeKey'],
|
||||
];
|
||||
}
|
||||
|
||||
private function removeEvent($remoteId, User $user): void
|
||||
{
|
||||
$userId = $this->mapCalendarToUser->getUserId($user);
|
||||
|
||||
try {
|
||||
$this->machineHttpClient->request(
|
||||
'DELETE',
|
||||
'users/' . $userId . '/calendar/events/' . $remoteId
|
||||
);
|
||||
} catch (ClientExceptionInterface $e) {
|
||||
$this->logger->warning('could not remove event from calendar', [
|
||||
'event_remote_id' => $remoteId,
|
||||
'user_id' => $user->getId(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private function updateRemoteCalendarRange(CalendarRange $calendarRange): void
|
||||
{
|
||||
$userId = $this->mapCalendarToUser->getUserId($calendarRange->getUser());
|
||||
$calendarId = $this->mapCalendarToUser->getCalendarId($calendarRange->getUser());
|
||||
|
||||
if (null === $userId || null === $calendarId) {
|
||||
$this->logger->warning('user does not have userId nor calendarId', [
|
||||
'user_id' => $calendarRange->getUser()->getId(),
|
||||
'calendar_range_id' => $calendarRange->getId(),
|
||||
]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$event = $this->machineHttpClient->request(
|
||||
'GET',
|
||||
'users/' . $userId . '/calendar/events/' . $calendarRange->getRemoteId()
|
||||
)->toArray();
|
||||
} catch (ClientExceptionInterface $e) {
|
||||
$this->logger->warning('Could not get event from calendar', [
|
||||
'calendar_range_id' => $calendarRange->getId(),
|
||||
'calendar_range_remote_id' => $calendarRange->getRemoteId(),
|
||||
]);
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
if ($this->remoteEventConverter->getLastModifiedDate($event)->getTimestamp() > $calendarRange->getUpdatedAt()->getTimestamp()) {
|
||||
$this->logger->info('Skip updating as the lastModified date seems more fresh than the database one', [
|
||||
'calendar_range_id' => $calendarRange->getId(),
|
||||
'calendar_range_remote_id' => $calendarRange->getRemoteId(),
|
||||
'db_last_updated' => $calendarRange->getUpdatedAt()->getTimestamp(),
|
||||
'remote_last_updated' => $this->remoteEventConverter->getLastModifiedDate($event)->getTimestamp(),
|
||||
]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$eventData = $this->remoteEventConverter->calendarRangeToEvent($calendarRange);
|
||||
|
||||
try {
|
||||
$event = $this->machineHttpClient->request(
|
||||
'PATCH',
|
||||
'users/' . $userId . '/calendar/events/' . $calendarRange->getRemoteId(),
|
||||
[
|
||||
'json' => $eventData,
|
||||
]
|
||||
)->toArray();
|
||||
} catch (ClientExceptionInterface $e) {
|
||||
$this->logger->warning('could not update calendar range to remote', [
|
||||
'exception' => $e->getTraceAsString(),
|
||||
'calendarRangeId' => $calendarRange->getId(),
|
||||
]);
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$calendarRange
|
||||
->addRemoteAttributes([
|
||||
'lastModifiedDateTime' => $this->remoteEventConverter->getLastModifiedDate($event)->getTimestamp(),
|
||||
'changeKey' => $event['changeKey'],
|
||||
]);
|
||||
}
|
||||
}
|
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\CalendarBundle\RemoteCalendar\Connector;
|
||||
|
||||
use Chill\CalendarBundle\Entity\Calendar;
|
||||
use Chill\CalendarBundle\Entity\CalendarRange;
|
||||
use Chill\CalendarBundle\Entity\Invite;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use DateTimeImmutable;
|
||||
use LogicException;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class NullRemoteCalendarConnector implements RemoteCalendarConnectorInterface
|
||||
{
|
||||
public function countEventsForUser(User $user, DateTimeImmutable $startDate, DateTimeImmutable $endDate): int
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
public function getMakeReadyResponse(string $returnPath): Response
|
||||
{
|
||||
throw new LogicException('As this connector is always ready, this method should not be called');
|
||||
}
|
||||
|
||||
public function isReady(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function listEventsForUser(User $user, DateTimeImmutable $startDate, DateTimeImmutable $endDate, ?int $offset = 0, ?int $limit = 50): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function removeCalendar(string $remoteId, array $remoteAttributes, User $user, ?CalendarRange $associatedCalendarRange = null): void
|
||||
{
|
||||
}
|
||||
|
||||
public function removeCalendarRange(string $remoteId, array $remoteAttributes, User $user): void
|
||||
{
|
||||
}
|
||||
|
||||
public function syncCalendar(Calendar $calendar, string $action, ?CalendarRange $previousCalendarRange, ?User $previousMainUser, ?array $oldInvites, ?array $newInvites): void
|
||||
{
|
||||
}
|
||||
|
||||
public function syncCalendarRange(CalendarRange $calendarRange): void
|
||||
{
|
||||
}
|
||||
|
||||
public function syncInvite(Invite $invite): void
|
||||
{
|
||||
}
|
||||
}
|
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\CalendarBundle\RemoteCalendar\Connector;
|
||||
|
||||
use Chill\CalendarBundle\Entity\Calendar;
|
||||
use Chill\CalendarBundle\Entity\CalendarRange;
|
||||
use Chill\CalendarBundle\Entity\Invite;
|
||||
use Chill\CalendarBundle\RemoteCalendar\Model\RemoteEvent;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use DateTimeImmutable;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
interface RemoteCalendarConnectorInterface
|
||||
{
|
||||
public function countEventsForUser(User $user, DateTimeImmutable $startDate, DateTimeImmutable $endDate): int;
|
||||
|
||||
/**
|
||||
* Return a response, more probably a RedirectResponse, where the user
|
||||
* will be able to fullfill requirements to prepare this connector and
|
||||
* make it ready.
|
||||
*/
|
||||
public function getMakeReadyResponse(string $returnPath): Response;
|
||||
|
||||
/**
|
||||
* Return true if the connector is ready to act as a proxy for reading
|
||||
* remote calendars.
|
||||
*/
|
||||
public function isReady(): bool;
|
||||
|
||||
/**
|
||||
* @return array|RemoteEvent[]
|
||||
*/
|
||||
public function listEventsForUser(User $user, DateTimeImmutable $startDate, DateTimeImmutable $endDate, ?int $offset = 0, ?int $limit = 50): array;
|
||||
|
||||
public function removeCalendar(string $remoteId, array $remoteAttributes, User $user, ?CalendarRange $associatedCalendarRange = null): void;
|
||||
|
||||
public function removeCalendarRange(string $remoteId, array $remoteAttributes, User $user): void;
|
||||
|
||||
/**
|
||||
* @param array<array{inviteId: int, userId: int, userEmail: int, userLabel: string}> $oldInvites
|
||||
*/
|
||||
public function syncCalendar(Calendar $calendar, string $action, ?CalendarRange $previousCalendarRange, ?User $previousMainUser, ?array $oldInvites, ?array $newInvites): void;
|
||||
|
||||
public function syncCalendarRange(CalendarRange $calendarRange): void;
|
||||
|
||||
public function syncInvite(Invite $invite): void;
|
||||
}
|
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\CalendarBundle\RemoteCalendar\DependencyInjection;
|
||||
|
||||
use Chill\CalendarBundle\Command\AzureGrantAdminConsentAndAcquireToken;
|
||||
use Chill\CalendarBundle\Command\MapAndSubscribeUserCalendarCommand;
|
||||
use Chill\CalendarBundle\Controller\RemoteCalendarConnectAzureController;
|
||||
use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\MachineHttpClient;
|
||||
use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\MachineTokenStorage;
|
||||
use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraphRemoteCalendarConnector;
|
||||
use Chill\CalendarBundle\RemoteCalendar\Connector\NullRemoteCalendarConnector;
|
||||
use Chill\CalendarBundle\RemoteCalendar\Connector\RemoteCalendarConnectorInterface;
|
||||
use RuntimeException;
|
||||
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
use TheNetworg\OAuth2\Client\Provider\Azure;
|
||||
|
||||
class RemoteCalendarCompilerPass implements CompilerPassInterface
|
||||
{
|
||||
public function process(ContainerBuilder $container)
|
||||
{
|
||||
$config = $container->getParameter('chill_calendar');
|
||||
$connector = null;
|
||||
|
||||
if (!$config['remote_calendars_sync']['enabled']) {
|
||||
$connector = NullRemoteCalendarConnector::class;
|
||||
}
|
||||
|
||||
if ($config['remote_calendars_sync']['microsoft_graph']['enabled']) {
|
||||
$connector = MSGraphRemoteCalendarConnector::class;
|
||||
|
||||
$container->setAlias(HttpClientInterface::class . ' $machineHttpClient', MachineHttpClient::class);
|
||||
} else {
|
||||
// remove services which cannot be loaded
|
||||
$container->removeDefinition(MapAndSubscribeUserCalendarCommand::class);
|
||||
$container->removeDefinition(AzureGrantAdminConsentAndAcquireToken::class);
|
||||
$container->removeDefinition(RemoteCalendarConnectAzureController::class);
|
||||
$container->removeDefinition(MachineTokenStorage::class);
|
||||
$container->removeDefinition(MachineHttpClient::class);
|
||||
$container->removeDefinition(MSGraphRemoteCalendarConnector::class);
|
||||
}
|
||||
|
||||
if (!$container->hasAlias(Azure::class) && $container->hasDefinition('knpu.oauth2.client.azure')) {
|
||||
$container->setAlias(Azure::class, 'knpu.oauth2.provider.azure');
|
||||
}
|
||||
|
||||
if (null === $connector) {
|
||||
throw new RuntimeException('Could not configure remote calendar');
|
||||
}
|
||||
|
||||
foreach ([
|
||||
NullRemoteCalendarConnector::class,
|
||||
MSGraphRemoteCalendarConnector::class, ] as $serviceId) {
|
||||
if ($connector === $serviceId) {
|
||||
$container->getDefinition($serviceId)
|
||||
->setDecoratedService(RemoteCalendarConnectorInterface::class);
|
||||
} else {
|
||||
// keep the container lighter by removing definitions
|
||||
if ($container->hasDefinition($serviceId)) {
|
||||
$container->removeDefinition($serviceId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\CalendarBundle\RemoteCalendar\Model;
|
||||
|
||||
use DateTimeImmutable;
|
||||
use Symfony\Component\Serializer\Annotation as Serializer;
|
||||
|
||||
class RemoteEvent
|
||||
{
|
||||
public string $description;
|
||||
|
||||
/**
|
||||
* @Serializer\Groups({"read"})
|
||||
*/
|
||||
public DateTimeImmutable $endDate;
|
||||
|
||||
/**
|
||||
* @Serializer\Groups({"read"})
|
||||
*/
|
||||
public string $id;
|
||||
|
||||
/**
|
||||
* @Serializer\Groups({"read"})
|
||||
*/
|
||||
public bool $isAllDay;
|
||||
|
||||
/**
|
||||
* @Serializer\Groups({"read"})
|
||||
*/
|
||||
public DateTimeImmutable $startDate;
|
||||
|
||||
/**
|
||||
* @Serializer\Groups({"read"})
|
||||
*/
|
||||
public string $title;
|
||||
|
||||
public function __construct(string $id, string $title, string $description, DateTimeImmutable $startDate, DateTimeImmutable $endDate, bool $isAllDay = false)
|
||||
{
|
||||
$this->id = $id;
|
||||
$this->title = $title;
|
||||
$this->description = $description;
|
||||
$this->startDate = $startDate;
|
||||
$this->endDate = $endDate;
|
||||
$this->isAllDay = $isAllDay;
|
||||
}
|
||||
}
|
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\CalendarBundle\Repository;
|
||||
|
||||
use Chill\CalendarBundle\Entity\Calendar;
|
||||
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
|
||||
class CalendarACLAwareRepository implements CalendarACLAwareRepositoryInterface
|
||||
{
|
||||
private EntityManagerInterface $em;
|
||||
|
||||
public function __construct(EntityManagerInterface $em)
|
||||
{
|
||||
$this->em = $em;
|
||||
}
|
||||
|
||||
public function buildQueryByAccompanyingPeriod(AccompanyingPeriod $period, ?DateTimeImmutable $startDate, ?DateTimeImmutable $endDate): QueryBuilder
|
||||
{
|
||||
$qb = $this->em->createQueryBuilder();
|
||||
$qb->from(Calendar::class, 'c');
|
||||
|
||||
$andX = $qb->expr()->andX($qb->expr()->eq('c.accompanyingPeriod', ':period'));
|
||||
$qb->setParameter('period', $period);
|
||||
|
||||
if (null !== $startDate) {
|
||||
$andX->add($qb->expr()->gte('c.startDate', ':startDate'));
|
||||
$qb->setParameter('startDate', $startDate);
|
||||
}
|
||||
|
||||
if (null !== $endDate) {
|
||||
$andX->add($qb->expr()->lte('c.endDate', ':endDate'));
|
||||
$qb->setParameter('endDate', $endDate);
|
||||
}
|
||||
|
||||
$qb->where($andX);
|
||||
|
||||
return $qb;
|
||||
}
|
||||
|
||||
public function countByAccompanyingPeriod(AccompanyingPeriod $period, ?DateTimeImmutable $startDate, ?DateTimeImmutable $endDate): int
|
||||
{
|
||||
$qb = $this->buildQueryByAccompanyingPeriod($period, $startDate, $endDate)->select('count(c)');
|
||||
|
||||
return $qb->getQuery()->getSingleScalarResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array|Calendar[]
|
||||
*/
|
||||
public function findByAccompanyingPeriod(AccompanyingPeriod $period, ?DateTimeImmutable $startDate, ?DateTimeImmutable $endDate, ?array $orderBy = [], ?int $offset = null, ?int $limit = null): array
|
||||
{
|
||||
$qb = $this->buildQueryByAccompanyingPeriod($period, $startDate, $endDate)->select('c');
|
||||
|
||||
foreach ($orderBy as $sort => $order) {
|
||||
$qb->addOrderBy('c.' . $sort, $order);
|
||||
}
|
||||
|
||||
if (null !== $offset) {
|
||||
$qb->setFirstResult($offset);
|
||||
}
|
||||
|
||||
if (null !== $limit) {
|
||||
$qb->setMaxResults($limit);
|
||||
}
|
||||
|
||||
return $qb->getQuery()->getResult();
|
||||
}
|
||||
}
|
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\CalendarBundle\Repository;
|
||||
|
||||
use Chill\CalendarBundle\Entity\Calendar;
|
||||
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
||||
use DateTimeImmutable;
|
||||
|
||||
interface CalendarACLAwareRepositoryInterface
|
||||
{
|
||||
public function countByAccompanyingPeriod(AccompanyingPeriod $period, ?DateTimeImmutable $startDate, ?DateTimeImmutable $endDate): int;
|
||||
|
||||
/**
|
||||
* @return array|Calendar[]
|
||||
*/
|
||||
public function findByAccompanyingPeriod(AccompanyingPeriod $period, ?DateTimeImmutable $startDate, ?DateTimeImmutable $endDate, ?array $orderBy = [], ?int $offset = null, ?int $limit = null): array;
|
||||
}
|
@@ -12,48 +12,159 @@ declare(strict_types=1);
|
||||
namespace Chill\CalendarBundle\Repository;
|
||||
|
||||
use Chill\CalendarBundle\Entity\CalendarRange;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\ORM\Query\ResultSetMapping;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Doctrine\Persistence\ObjectRepository;
|
||||
use function count;
|
||||
|
||||
/**
|
||||
* @method CalendarRange|null find($id, $lockMode = null, $lockVersion = null)
|
||||
* @method CalendarRange|null findOneBy(array $criteria, array $orderBy = null)
|
||||
* @method CalendarRange[] findAll()
|
||||
* @method CalendarRange[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
|
||||
*/
|
||||
class CalendarRangeRepository extends ServiceEntityRepository
|
||||
class CalendarRangeRepository implements ObjectRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
private EntityManagerInterface $em;
|
||||
|
||||
private EntityRepository $repository;
|
||||
|
||||
public function __construct(EntityManagerInterface $entityManager)
|
||||
{
|
||||
parent::__construct($registry, CalendarRange::class);
|
||||
$this->em = $entityManager;
|
||||
$this->repository = $entityManager->getRepository(CalendarRange::class);
|
||||
}
|
||||
|
||||
// /**
|
||||
// * @return CalendarRange[] Returns an array of CalendarRange objects
|
||||
// */
|
||||
/*
|
||||
public function findByExampleField($value)
|
||||
public function countByAvailableRangesForUser(User $user, DateTimeImmutable $from, DateTimeImmutable $to): int
|
||||
{
|
||||
return $this->createQueryBuilder('c')
|
||||
->andWhere('c.exampleField = :val')
|
||||
->setParameter('val', $value)
|
||||
->orderBy('c.id', 'ASC')
|
||||
->setMaxResults(10)
|
||||
->getQuery()
|
||||
->getResult()
|
||||
;
|
||||
return $this->buildQueryAvailableRangesForUser($user, $from, $to)
|
||||
->select('COUNT(cr)')
|
||||
->getQuery()->getSingleScalarResult();
|
||||
}
|
||||
*/
|
||||
|
||||
/*
|
||||
public function findOneBySomeField($value): ?CalendarRange
|
||||
public function find($id): ?CalendarRange
|
||||
{
|
||||
return $this->createQueryBuilder('c')
|
||||
->andWhere('c.exampleField = :val')
|
||||
->setParameter('val', $value)
|
||||
->getQuery()
|
||||
->getOneOrNullResult()
|
||||
;
|
||||
return $this->repository->find($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array|CalendarRange[]
|
||||
*/
|
||||
public function findAll(): array
|
||||
{
|
||||
return $this->repository->findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array|CalendarRange[]
|
||||
*/
|
||||
public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null)
|
||||
{
|
||||
return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array|CalendarRange[]
|
||||
*/
|
||||
public function findByAvailableRangesForUser(
|
||||
User $user,
|
||||
DateTimeImmutable $from,
|
||||
DateTimeImmutable $to,
|
||||
?int $limit = null,
|
||||
?int $offset = null
|
||||
): array {
|
||||
$qb = $this->buildQueryAvailableRangesForUser($user, $from, $to);
|
||||
|
||||
if (null !== $limit) {
|
||||
$qb->setMaxResults($limit);
|
||||
}
|
||||
|
||||
if (null !== $offset) {
|
||||
$qb->setFirstResult($offset);
|
||||
}
|
||||
|
||||
return $qb->getQuery()->getResult();
|
||||
}
|
||||
|
||||
public function findOneBy(array $criteria): ?CalendarRange
|
||||
{
|
||||
return $this->repository->findOneBy($criteria);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a list of remote ids, return an array where
|
||||
* keys are the remoteIds, and value is a boolean, true if the
|
||||
* id is present in database.
|
||||
*
|
||||
* @param array<int, string>|list<string> $remoteIds
|
||||
*
|
||||
* @return array<string, bool>
|
||||
*/
|
||||
public function findRemoteIdsPresent(array $remoteIds): array
|
||||
{
|
||||
if (0 === count($remoteIds)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$sql = 'SELECT
|
||||
sq.remoteId as remoteid,
|
||||
EXISTS (SELECT 1 FROM chill_calendar.calendar_range cr WHERE cr.remoteId = sq.remoteId) AS present
|
||||
FROM
|
||||
(
|
||||
VALUES %remoteIds%
|
||||
) AS sq(remoteId);
|
||||
';
|
||||
|
||||
$remoteIdsStr = implode(
|
||||
', ',
|
||||
array_fill(0, count($remoteIds), '((?))')
|
||||
);
|
||||
|
||||
$rsm = new ResultSetMapping();
|
||||
$rsm
|
||||
->addScalarResult('remoteid', 'remoteId', Types::STRING)
|
||||
->addScalarResult('present', 'present', Types::BOOLEAN);
|
||||
|
||||
$rows = $this->em
|
||||
->createNativeQuery(
|
||||
strtr($sql, ['%remoteIds%' => $remoteIdsStr]),
|
||||
$rsm
|
||||
)
|
||||
->setParameters(array_values($remoteIds))
|
||||
->getResult();
|
||||
|
||||
$results = [];
|
||||
|
||||
foreach ($rows as $r) {
|
||||
$results[$r['remoteId']] = $r['present'];
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
public function getClassName(): string
|
||||
{
|
||||
return CalendarRange::class;
|
||||
}
|
||||
|
||||
private function buildQueryAvailableRangesForUser(User $user, DateTimeImmutable $from, DateTimeImmutable $to): QueryBuilder
|
||||
{
|
||||
$qb = $this->repository->createQueryBuilder('cr');
|
||||
|
||||
$qb->leftJoin('cr.calendar', 'calendar');
|
||||
|
||||
return $qb
|
||||
->where(
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->eq('cr.user', ':user'),
|
||||
$qb->expr()->gte('cr.startDate', ':startDate'),
|
||||
$qb->expr()->lte('cr.endDate', ':endDate'),
|
||||
$qb->expr()->isNull('calendar')
|
||||
)
|
||||
)
|
||||
->setParameters([
|
||||
'user' => $user,
|
||||
'startDate' => $from,
|
||||
'endDate' => $to,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
@@ -12,52 +12,215 @@ declare(strict_types=1);
|
||||
namespace Chill\CalendarBundle\Repository;
|
||||
|
||||
use Chill\CalendarBundle\Entity\Calendar;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Doctrine\ORM\Query\ResultSetMapping;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Doctrine\Persistence\ObjectRepository;
|
||||
use function count;
|
||||
|
||||
/**
|
||||
* @method Calendar|null find($id, $lockMode = null, $lockVersion = null)
|
||||
* @method Calendar|null findOneBy(array $criteria, array $orderBy = null)
|
||||
* @method Calendar[] findAll()
|
||||
* @method Calendar[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
|
||||
*/
|
||||
class CalendarRepository extends ServiceEntityRepository
|
||||
class CalendarRepository implements ObjectRepository
|
||||
{
|
||||
// private EntityRepository $repository;
|
||||
private EntityManagerInterface $em;
|
||||
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
private EntityRepository $repository;
|
||||
|
||||
public function __construct(EntityManagerInterface $entityManager)
|
||||
{
|
||||
parent::__construct($registry, Calendar::class);
|
||||
// $this->repository = $entityManager->getRepository(AccompanyingPeriodWork::class);
|
||||
$this->repository = $entityManager->getRepository(Calendar::class);
|
||||
$this->em = $entityManager;
|
||||
}
|
||||
|
||||
// /**
|
||||
// * @return Calendar[] Returns an array of Calendar objects
|
||||
// */
|
||||
/*
|
||||
public function findByExampleField($value)
|
||||
public function countByAccompanyingPeriod(AccompanyingPeriod $period): int
|
||||
{
|
||||
return $this->createQueryBuilder('c')
|
||||
->andWhere('c.exampleField = :val')
|
||||
->setParameter('val', $value)
|
||||
->orderBy('c.id', 'ASC')
|
||||
->setMaxResults(10)
|
||||
return $this->repository->count(['accompanyingPeriod' => $period]);
|
||||
}
|
||||
|
||||
public function countByUser(User $user, DateTimeImmutable $from, DateTimeImmutable $to): int
|
||||
{
|
||||
return $this->buildQueryByUser($user, $from, $to)
|
||||
->select('COUNT(c)')
|
||||
->getQuery()
|
||||
->getResult()
|
||||
;
|
||||
->getSingleScalarResult();
|
||||
}
|
||||
*/
|
||||
|
||||
/*
|
||||
public function findOneBySomeField($value): ?Calendar
|
||||
public function find($id): ?Calendar
|
||||
{
|
||||
return $this->createQueryBuilder('c')
|
||||
->andWhere('c.exampleField = :val')
|
||||
->setParameter('val', $value)
|
||||
->getQuery()
|
||||
->getOneOrNullResult()
|
||||
;
|
||||
return $this->repository->find($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array|Calendar[]
|
||||
*/
|
||||
public function findAll(): array
|
||||
{
|
||||
return $this->repository->findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array|Calendar[]
|
||||
*/
|
||||
public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array
|
||||
{
|
||||
return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array|Calendar[]
|
||||
*/
|
||||
public function findByAccompanyingPeriod(AccompanyingPeriod $period, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array
|
||||
{
|
||||
return $this->findBy(
|
||||
[
|
||||
'accompanyingPeriod' => $period,
|
||||
],
|
||||
$orderBy,
|
||||
$limit,
|
||||
$orderBy
|
||||
);
|
||||
}
|
||||
|
||||
public function findByNotificationAvailable(DateTimeImmutable $startDate, DateTimeImmutable $endDate, ?int $limit = null, ?int $offset = null): array
|
||||
{
|
||||
$qb = $this->queryByNotificationAvailable($startDate, $endDate)->select('c');
|
||||
|
||||
if (null !== $limit) {
|
||||
$qb->setMaxResults($limit);
|
||||
}
|
||||
|
||||
if (null !== $offset) {
|
||||
$qb->setFirstResult($offset);
|
||||
}
|
||||
|
||||
return $qb->getQuery()->getResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array|Calendar[]
|
||||
*/
|
||||
public function findByUser(User $user, DateTimeImmutable $from, DateTimeImmutable $to, ?int $limit = null, ?int $offset = null): array
|
||||
{
|
||||
$qb = $this->buildQueryByUser($user, $from, $to)->select('c');
|
||||
|
||||
if (null !== $limit) {
|
||||
$qb->setMaxResults($limit);
|
||||
}
|
||||
|
||||
if (null !== $offset) {
|
||||
$qb->setFirstResult($offset);
|
||||
}
|
||||
|
||||
return $qb->getQuery()->getResult();
|
||||
}
|
||||
|
||||
public function findOneBy(array $criteria): ?Calendar
|
||||
{
|
||||
return $this->repository->findOneBy($criteria);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a list of remote ids, return an array where
|
||||
* keys are the remoteIds, and value is a boolean, true if the
|
||||
* id is present in database.
|
||||
*
|
||||
* @param array<int, string>|list<string> $remoteIds
|
||||
*
|
||||
* @return array<string, bool>
|
||||
*/
|
||||
public function findRemoteIdsPresent(array $remoteIds): array
|
||||
{
|
||||
if (0 === count($remoteIds)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$remoteIdsStr = implode(
|
||||
', ',
|
||||
array_fill(0, count($remoteIds), '((?))')
|
||||
);
|
||||
|
||||
$sql = "SELECT
|
||||
sq.remoteId as remoteid,
|
||||
EXISTS (SELECT 1 FROM chill_calendar.calendar c WHERE c.remoteId = sq.remoteId) AS present
|
||||
FROM
|
||||
(
|
||||
VALUES {$remoteIdsStr}
|
||||
) AS sq(remoteId);
|
||||
";
|
||||
|
||||
$rsm = new ResultSetMapping();
|
||||
$rsm
|
||||
->addScalarResult('remoteid', 'remoteId', Types::STRING)
|
||||
->addScalarResult('present', 'present', Types::BOOLEAN);
|
||||
|
||||
$rows = $this->em
|
||||
->createNativeQuery(
|
||||
$sql,
|
||||
$rsm
|
||||
)
|
||||
->setParameters(array_values($remoteIds))
|
||||
->getResult();
|
||||
|
||||
$results = [];
|
||||
|
||||
foreach ($rows as $r) {
|
||||
$results[$r['remoteId']] = $r['present'];
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
public function getClassName()
|
||||
{
|
||||
return Calendar::class;
|
||||
}
|
||||
|
||||
private function buildQueryByUser(User $user, DateTimeImmutable $from, DateTimeImmutable $to): QueryBuilder
|
||||
{
|
||||
$qb = $this->repository->createQueryBuilder('c');
|
||||
|
||||
return $qb
|
||||
->where(
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->eq('c.mainUser', ':user'),
|
||||
$qb->expr()->gte('c.startDate', ':startDate'),
|
||||
$qb->expr()->lte('c.endDate', ':endDate'),
|
||||
)
|
||||
)
|
||||
->setParameters([
|
||||
'user' => $user,
|
||||
'startDate' => $from,
|
||||
'endDate' => $to,
|
||||
]);
|
||||
}
|
||||
|
||||
private function queryByNotificationAvailable(DateTimeImmutable $startDate, DateTimeImmutable $endDate): QueryBuilder
|
||||
{
|
||||
$qb = $this->repository->createQueryBuilder('c');
|
||||
|
||||
$qb->where(
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->eq('c.sendSMS', ':true'),
|
||||
$qb->expr()->gte('c.startDate', ':startDate'),
|
||||
$qb->expr()->lt('c.startDate', ':endDate'),
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->eq('c.smsStatus', ':pending'),
|
||||
$qb->expr()->eq('c.smsStatus', ':cancel_pending')
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
$qb->setParameters([
|
||||
'true' => true,
|
||||
'startDate' => $startDate,
|
||||
'endDate' => $endDate,
|
||||
'pending' => Calendar::SMS_PENDING,
|
||||
'cancel_pending' => Calendar::SMS_CANCEL_PENDING,
|
||||
]);
|
||||
|
||||
return $qb;
|
||||
}
|
||||
}
|
||||
|
@@ -12,48 +12,47 @@ declare(strict_types=1);
|
||||
namespace Chill\CalendarBundle\Repository;
|
||||
|
||||
use Chill\CalendarBundle\Entity\Invite;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\Persistence\ObjectRepository;
|
||||
|
||||
/**
|
||||
* @method Invite|null find($id, $lockMode = null, $lockVersion = null)
|
||||
* @method Invite|null findOneBy(array $criteria, array $orderBy = null)
|
||||
* @method Invite[] findAll()
|
||||
* @method Invite[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
|
||||
*/
|
||||
class InviteRepository extends ServiceEntityRepository
|
||||
class InviteRepository implements ObjectRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
private EntityRepository $entityRepository;
|
||||
|
||||
public function __construct(EntityManagerInterface $em)
|
||||
{
|
||||
parent::__construct($registry, Invite::class);
|
||||
$this->entityRepository = $em->getRepository(Invite::class);
|
||||
}
|
||||
|
||||
// /**
|
||||
// * @return Invite[] Returns an array of Invite objects
|
||||
// */
|
||||
/*
|
||||
public function findByExampleField($value)
|
||||
public function find($id): ?Invite
|
||||
{
|
||||
return $this->createQueryBuilder('i')
|
||||
->andWhere('i.exampleField = :val')
|
||||
->setParameter('val', $value)
|
||||
->orderBy('i.id', 'ASC')
|
||||
->setMaxResults(10)
|
||||
->getQuery()
|
||||
->getResult()
|
||||
;
|
||||
return $this->entityRepository->find($id);
|
||||
}
|
||||
*/
|
||||
|
||||
/*
|
||||
public function findOneBySomeField($value): ?Invite
|
||||
{
|
||||
return $this->createQueryBuilder('i')
|
||||
->andWhere('i.exampleField = :val')
|
||||
->setParameter('val', $value)
|
||||
->getQuery()
|
||||
->getOneOrNullResult()
|
||||
;
|
||||
}
|
||||
/**
|
||||
* @return array|Invite[]
|
||||
*/
|
||||
public function findAll(): array
|
||||
{
|
||||
return $this->entityRepository->findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array|Invite[]
|
||||
*/
|
||||
public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null)
|
||||
{
|
||||
return $this->entityRepository->findBy($criteria, $orderBy, $limit, $offset);
|
||||
}
|
||||
|
||||
public function findOneBy(array $criteria): ?Invite
|
||||
{
|
||||
return $this->entityRepository->findOneBy($criteria);
|
||||
}
|
||||
|
||||
public function getClassName(): string
|
||||
{
|
||||
return Invite::class;
|
||||
}
|
||||
}
|
||||
|
@@ -3,12 +3,41 @@ services:
|
||||
|
||||
Chill\CalendarBundle\Repository\:
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
resource: '../../Repository/'
|
||||
tags:
|
||||
- { name: 'doctrine.repository_service' }
|
||||
|
||||
Chill\CalendarBundle\Menu\:
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
resource: '../../Menu/'
|
||||
tags: ['chill.menu_builder']
|
||||
|
||||
Chill\CalendarBundle\Command\:
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
resource: '../../Command/'
|
||||
|
||||
Chill\CalendarBundle\Messenger\:
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
resource: '../../Messenger/'
|
||||
|
||||
Chill\CalendarBundle\Command\AzureGrantAdminConsentAndAcquireToken:
|
||||
autoconfigure: true
|
||||
autowire: true
|
||||
arguments:
|
||||
$azure: '@knpu.oauth2.provider.azure'
|
||||
tags: ['console.command']
|
||||
|
||||
Chill\CalendarBundle\Security\:
|
||||
autoconfigure: true
|
||||
autowire: true
|
||||
resource: '../../Security/'
|
||||
|
||||
Chill\CalendarBundle\Service\:
|
||||
autoconfigure: true
|
||||
autowire: true
|
||||
resource: '../../Service/'
|
||||
|
||||
Chill\CalendarBundle\Service\ShortMessageForCalendarBuilderInterface:
|
||||
alias: Chill\CalendarBundle\Service\DefaultShortMessageForCalendarBuider
|
||||
|
@@ -7,4 +7,37 @@ services:
|
||||
name: 'doctrine.orm.entity_listener'
|
||||
event: 'postPersist'
|
||||
entity: 'Chill\ActivityBundle\Entity\Activity'
|
||||
|
||||
|
||||
Chill\CalendarBundle\Messenger\Doctrine\CalendarRangeEntityListener:
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
tags:
|
||||
-
|
||||
name: 'doctrine.orm.entity_listener'
|
||||
event: 'postPersist'
|
||||
entity: 'Chill\CalendarBundle\Entity\CalendarRange'
|
||||
-
|
||||
name: 'doctrine.orm.entity_listener'
|
||||
event: 'postUpdate'
|
||||
entity: 'Chill\CalendarBundle\Entity\CalendarRange'
|
||||
-
|
||||
name: 'doctrine.orm.entity_listener'
|
||||
event: 'postRemove'
|
||||
entity: 'Chill\CalendarBundle\Entity\CalendarRange'
|
||||
|
||||
Chill\CalendarBundle\Messenger\Doctrine\CalendarEntityListener:
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
tags:
|
||||
-
|
||||
name: 'doctrine.orm.entity_listener'
|
||||
event: 'postPersist'
|
||||
entity: 'Chill\CalendarBundle\Entity\Calendar'
|
||||
-
|
||||
name: 'doctrine.orm.entity_listener'
|
||||
event: 'postUpdate'
|
||||
entity: 'Chill\CalendarBundle\Entity\Calendar'
|
||||
-
|
||||
name: 'doctrine.orm.entity_listener'
|
||||
event: 'postRemove'
|
||||
entity: 'Chill\CalendarBundle\Entity\Calendar'
|
||||
|
@@ -1,10 +1,6 @@
|
||||
---
|
||||
services:
|
||||
chill.calendar.form.type.calendar:
|
||||
class: Chill\CalendarBundle\Form\CalendarType
|
||||
arguments:
|
||||
- "@chill.main.helper.translatable_string"
|
||||
- "@doctrine.orm.entity_manager"
|
||||
|
||||
tags:
|
||||
- { name: form.type, alias: chill_calendarbundle_calendar }
|
||||
services:
|
||||
Chill\CalendarBundle\Form\:
|
||||
resource: './../../Form'
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
|
@@ -0,0 +1,15 @@
|
||||
services:
|
||||
_defaults:
|
||||
autoconfigure: true
|
||||
autowire: true
|
||||
|
||||
Chill\CalendarBundle\RemoteCalendar\Connector\RemoteCalendarConnectorInterface: ~
|
||||
|
||||
Chill\CalendarBundle\RemoteCalendar\Connector\NullRemoteCalendarConnector: ~
|
||||
|
||||
Chill\CalendarBundle\RemoteCalendar\Connector\MSGraphRemoteCalendarConnector: ~
|
||||
|
||||
Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\:
|
||||
resource: '../../RemoteCalendar/Connector/MSGraph/'
|
||||
|
||||
|
@@ -0,0 +1,33 @@
|
||||
|
||||
import { createApp } from 'vue';
|
||||
import Answer from 'ChillCalendarAssets/vuejs/Invite/Answer';
|
||||
import { _createI18n } from 'ChillMainAssets/vuejs/_js/i18n';
|
||||
|
||||
const i18n = _createI18n({});
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function (e) {
|
||||
console.log('dom loaded answer');
|
||||
document.querySelectorAll('div[invite-answer]').forEach(function (el) {
|
||||
console.log('element found', el);
|
||||
|
||||
const app = createApp({
|
||||
components: {
|
||||
Answer,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
status: el.dataset.status,
|
||||
calendarId: Number.parseInt(el.dataset.calendarId),
|
||||
}
|
||||
},
|
||||
template: '<answer :calendarId="calendarId" :status="status" @statusChanged="onStatusChanged"></answer>',
|
||||
methods: {
|
||||
onStatusChanged: function(newStatus) {
|
||||
this.$data.status = newStatus;
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
app.use(i18n).mount(el);
|
||||
});
|
||||
});
|
67
src/Bundle/ChillCalendarBundle/Resources/public/types.ts
Normal file
67
src/Bundle/ChillCalendarBundle/Resources/public/types.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import {EventInput} from '@fullcalendar/vue3';
|
||||
import {DateTime, Location, User, UserAssociatedInterface} from '../../../ChillMainBundle/Resources/public/types' ;
|
||||
import {Person} from "../../../ChillPersonBundle/Resources/public/types";
|
||||
|
||||
export interface CalendarRange {
|
||||
id: number;
|
||||
endDate: DateTime;
|
||||
startDate: DateTime;
|
||||
user: User;
|
||||
location: Location;
|
||||
createdAt: DateTime;
|
||||
createdBy: User;
|
||||
updatedAt: DateTime;
|
||||
updatedBy: User;
|
||||
}
|
||||
|
||||
export interface CalendarRangeCreate {
|
||||
user: UserAssociatedInterface;
|
||||
startDate: DateTime;
|
||||
endDate: DateTime;
|
||||
location: Location;
|
||||
}
|
||||
|
||||
export interface CalendarRangeEdit {
|
||||
startDate?: DateTime,
|
||||
endDate?: DateTime
|
||||
location?: Location;
|
||||
}
|
||||
|
||||
export interface Calendar {
|
||||
id: number;
|
||||
}
|
||||
|
||||
export interface CalendarLight {
|
||||
id: number;
|
||||
endDate: DateTime;
|
||||
startDate: DateTime;
|
||||
mainUser: User;
|
||||
persons: Person[];
|
||||
status: "valid" | "moved" | "canceled";
|
||||
}
|
||||
|
||||
export interface CalendarRemote {
|
||||
id: number;
|
||||
endDate: DateTime;
|
||||
startDate: DateTime;
|
||||
title: string;
|
||||
isAllDay: boolean;
|
||||
}
|
||||
|
||||
export type EventInputCalendarRange = EventInput & {
|
||||
id: string,
|
||||
userId: number,
|
||||
userLabel: string,
|
||||
calendarRangeId: number,
|
||||
locationId: number,
|
||||
locationName: string,
|
||||
start: string,
|
||||
end: string,
|
||||
is: "range"
|
||||
};
|
||||
|
||||
export function isEventInputCalendarRange(toBeDetermined: EventInputCalendarRange | EventInput): toBeDetermined is EventInputCalendarRange {
|
||||
return typeof toBeDetermined.is === "string" && toBeDetermined.is === "range";
|
||||
}
|
||||
|
||||
export {};
|
@@ -1,156 +1,280 @@
|
||||
<template>
|
||||
|
||||
<teleport to="#mainUser">
|
||||
|
||||
<h2 class="chill-red">Utilisateur principal</h2>
|
||||
<div>
|
||||
<div>
|
||||
<div v-if="null !== this.$store.getters.getMainUser">
|
||||
<calendar-active :user="this.$store.getters.getMainUser" ></calendar-active>
|
||||
</div>
|
||||
<pick-entity
|
||||
:multiple="false"
|
||||
:types="['user']"
|
||||
:uniqid="'main_user_calendar'"
|
||||
:picked="null !== this.$store.getters.getMainUser ? [this.$store.getters.getMainUser] : []"
|
||||
:removableIfSet="false"
|
||||
:displayPicked="false"
|
||||
@addNewEntity="setMainUser"
|
||||
></pick-entity>
|
||||
</div>
|
||||
</div>
|
||||
</teleport>
|
||||
|
||||
<concerned-groups></concerned-groups>
|
||||
|
||||
<teleport to="#schedule">
|
||||
<div class="row mb-3" v-if="activity.startDate !== null">
|
||||
<label class="col-form-label col-sm-4">Date</label>
|
||||
<div class="col-sm-8">
|
||||
{{ $d(activity.startDate, 'long') }} - {{ $d(activity.endDate, 'hoursOnly') }}
|
||||
<span v-if="activity.calendarRange === null">(Pas de plage de disponibilité sélectionnée)</span>
|
||||
<span v-else>(Une plage de disponibilité sélectionnée)</span>
|
||||
</div>
|
||||
</div>
|
||||
</teleport>
|
||||
|
||||
<location></location>
|
||||
<teleport to="#calendarControls">
|
||||
<calendar-user-selector
|
||||
v-bind:users="users"
|
||||
v-bind:calendarEvents="calendarEvents"
|
||||
v-bind:updateEventsSource="updateEventsSource"
|
||||
v-bind:showMyCalendar="showMyCalendar"
|
||||
v-bind:toggleMyCalendar="toggleMyCalendar"
|
||||
v-bind:toggleWeekends="toggleWeekends" >
|
||||
</calendar-user-selector>
|
||||
</teleport>
|
||||
<teleport to="#fullCalendar">
|
||||
<FullCalendar ref="fullCalendar" :options="calendarOptions">
|
||||
<template v-slot:eventContent='arg'>
|
||||
<b>{{ arg.timeText }}</b>
|
||||
<i> {{ arg.event.title }}</i>
|
||||
</template>
|
||||
</FullCalendar>
|
||||
</teleport>
|
||||
|
||||
|
||||
|
||||
<teleport to="#fullCalendar">
|
||||
<div class="calendar-actives">
|
||||
<template class="" v-for="u in getActiveUsers" :key="u.id">
|
||||
<calendar-active :user="u" :invite="this.$store.getters.getInviteForUser(u)"></calendar-active>
|
||||
</template>
|
||||
</div>
|
||||
<div class="display-options row justify-content-between" style="margin-top: 1rem;">
|
||||
<div class="col-sm-9 col-xs-12">
|
||||
<div class="input-group mb-3">
|
||||
<label class="input-group-text" for="slotDuration">Durée des créneaux</label>
|
||||
<select v-model="slotDuration" id="slotDuration" class="form-select">
|
||||
<option value="00:05:00">5 minutes</option>
|
||||
<option value="00:10:00">10 minutes</option>
|
||||
<option value="00:15:00">15 minutes</option>
|
||||
<option value="00:30:00">30 minutes</option>
|
||||
</select>
|
||||
<label class="input-group-text" for="slotMinTime">De</label>
|
||||
<select v-model="slotMinTime" id="slotMinTime" class="form-select">
|
||||
<option value="00:00:00">0h</option>
|
||||
<option value="01:00:00">1h</option>
|
||||
<option value="02:00:00">2h</option>
|
||||
<option value="03:00:00">3h</option>
|
||||
<option value="04:00:00">4h</option>
|
||||
<option value="05:00:00">5h</option>
|
||||
<option value="06:00:00">6h</option>
|
||||
<option value="07:00:00">7h</option>
|
||||
<option value="08:00:00">8h</option>
|
||||
<option value="09:00:00">9h</option>
|
||||
<option value="10:00:00">10h</option>
|
||||
<option value="11:00:00">11h</option>
|
||||
<option value="12:00:00">12h</option>
|
||||
</select>
|
||||
<label class="input-group-text" for="slotMaxTime">À</label>
|
||||
<select v-model="slotMaxTime" id="slotMaxTime" class="form-select">
|
||||
<option value="12:00:00">12h</option>
|
||||
<option value="13:00:00">13h</option>
|
||||
<option value="14:00:00">14h</option>
|
||||
<option value="15:00:00">15h</option>
|
||||
<option value="16:00:00">16h</option>
|
||||
<option value="17:00:00">17h</option>
|
||||
<option value="18:00:00">18h</option>
|
||||
<option value="19:00:00">19h</option>
|
||||
<option value="20:00:00">20h</option>
|
||||
<option value="21:00:00">21h</option>
|
||||
<option value="22:00:00">22h</option>
|
||||
<option value="23:00:00">23h</option>
|
||||
<option value="23:59:59">24h</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-3 col-xs-12">
|
||||
<div class="float-end">
|
||||
<div class="form-check input-group">
|
||||
<span class="input-group-text">
|
||||
<input id="showHideWE" class="mt-0" type="checkbox" v-model="hideWeekends">
|
||||
</span>
|
||||
<label for="showHideWE" class="form-check-label input-group-text">Week-ends</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<FullCalendar ref="fullCalendar" :options="calendarOptions">
|
||||
<template v-slot:eventContent='arg'>
|
||||
<span>
|
||||
<b v-if="arg.event.extendedProps.is === 'remote'">{{ arg.event.title}}</b>
|
||||
<b v-else-if="arg.event.extendedProps.is === 'range'">{{ arg.timeText }} {{ arg.event.extendedProps.locationName }} <small>{{ arg.event.extendedProps.userLabel }}</small></b>
|
||||
<b v-else-if="arg.event.extendedProps.is === 'current'">{{ arg.timeText }} {{ $t('current_selected')}} </b>
|
||||
<b v-else-if="arg.event.extendedProps.is === 'local'">{{ arg.event.title}}</b>
|
||||
<b v-else>{{ arg.timeText }} {{ $t('current_selected')}} </b>
|
||||
</span>
|
||||
</template>
|
||||
</FullCalendar>
|
||||
</teleport>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ConcernedGroups from 'ChillActivityAssets/vuejs/Activity/components/ConcernedGroups.vue';
|
||||
import Location from 'ChillActivityAssets/vuejs/Activity/components/Location.vue';
|
||||
import CalendarUserSelector from '../_components/CalendarUserSelector/CalendarUserSelector.vue';
|
||||
import '@fullcalendar/core/vdom'; // solves problem with Vite
|
||||
import frLocale from '@fullcalendar/core/locales/fr';
|
||||
import FullCalendar from '@fullcalendar/vue3';
|
||||
import dayGridPlugin from '@fullcalendar/daygrid';
|
||||
import interactionPlugin from '@fullcalendar/interaction';
|
||||
import timeGridPlugin from '@fullcalendar/timegrid';
|
||||
// import listPlugin from '@fullcalendar/list';
|
||||
import listPlugin from '@fullcalendar/list';
|
||||
import CalendarActive from './Components/CalendarActive';
|
||||
import PickEntity from 'ChillMainAssets/vuejs/PickEntity/PickEntity.vue';
|
||||
import {mapGetters, mapState} from "vuex";
|
||||
|
||||
export default {
|
||||
name: "App",
|
||||
components: {
|
||||
ConcernedGroups,
|
||||
Location,
|
||||
CalendarUserSelector,
|
||||
FullCalendar
|
||||
FullCalendar,
|
||||
CalendarActive,
|
||||
PickEntity,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
errorMsg: [],
|
||||
users: {
|
||||
loaded: [],
|
||||
selected: [],
|
||||
logged: null
|
||||
},
|
||||
calendarEvents: {
|
||||
loaded: [],
|
||||
selected: [],
|
||||
user: [],
|
||||
current: {
|
||||
events: [{
|
||||
title: 'plage prévue',
|
||||
start: window.startDate,
|
||||
end: window.endDate
|
||||
}],
|
||||
id: window.mainUser,
|
||||
color: '#bbbbbb'
|
||||
}
|
||||
},
|
||||
selectedEvent: null,
|
||||
previousSelectedEvent: null,
|
||||
previousSelectedEventColor: null,
|
||||
showMyCalendar: false,
|
||||
calendarOptions: {
|
||||
slotDuration: '00:05:00',
|
||||
slotMinTime: '09:00:00',
|
||||
slotMaxTime: '18:00:00',
|
||||
hideWeekEnds: true,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['getMainUser']),
|
||||
...mapState(['activity']),
|
||||
events() {
|
||||
return this.$store.getters.getEventSources;
|
||||
},
|
||||
calendarOptions() {
|
||||
return {
|
||||
locale: frLocale,
|
||||
plugins: [ dayGridPlugin, interactionPlugin, timeGridPlugin ],
|
||||
plugins: [dayGridPlugin, interactionPlugin, timeGridPlugin, dayGridPlugin, listPlugin],
|
||||
initialView: 'timeGridWeek',
|
||||
initialDate: window.startDate !== undefined ? window.startDate : new Date(),
|
||||
eventSource: [],
|
||||
initialDate: this.$store.getters.getInitialDate,
|
||||
eventSources: this.events,
|
||||
selectable: true,
|
||||
slotMinTime: this.slotMinTime,
|
||||
slotMaxTime: this.slotMaxTime,
|
||||
scrollTimeReset: false,
|
||||
datesSet: this.onDatesSet,
|
||||
select: this.onDateSelect,
|
||||
eventChange: this.onEventChange,
|
||||
eventClick: this.onEventClick,
|
||||
// eventMouseEnter: this.onEventMouseEnter,
|
||||
// eventMouseLeave: this.onEventMouseLeave,
|
||||
selectMirror: true,
|
||||
editable: true,
|
||||
weekends: false,
|
||||
weekends: !this.hideWeekEnds,
|
||||
headerToolbar: {
|
||||
left: 'prev,next today',
|
||||
center: 'title',
|
||||
right: 'dayGridMonth,timeGridWeek,timeGridDay,listMonth,listWeek,listDay'
|
||||
right: 'timeGridWeek,timeGridDay,listWeek',
|
||||
},
|
||||
views: {
|
||||
timeGrid: {
|
||||
slotEventOverlap: false,
|
||||
slotDuration: this.slotDuration,
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
getActiveUsers() {
|
||||
const users = [];
|
||||
for (const id of this.$store.state.currentView.users.keys()) {
|
||||
users.push(this.$store.getters.getUserDataById(id).user);
|
||||
}
|
||||
return users;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
init() {
|
||||
this.updateEventsSource();
|
||||
},
|
||||
toggleMyCalendar(value) {
|
||||
this.showMyCalendar = value;
|
||||
},
|
||||
toggleWeekends: function() {
|
||||
this.calendarOptions.weekends = !this.calendarOptions.weekends;
|
||||
},
|
||||
updateEventsSource() {
|
||||
this.calendarOptions.eventSources = [];
|
||||
this.calendarOptions.eventSources.push(...this.calendarEvents.selected);
|
||||
if (window.startDate !== undefined) {
|
||||
this.calendarOptions.eventSources.push(this.calendarEvents.current);
|
||||
}
|
||||
if (this.showMyCalendar) {
|
||||
this.calendarOptions.eventSources.push(this.calendarEvents.user);
|
||||
}
|
||||
},
|
||||
unSelectPreviousEvent(event) {
|
||||
if (event) {
|
||||
if (typeof event.setProp === 'function') {
|
||||
event.setProp('backgroundColor', this.previousSelectedEventColor);
|
||||
event.setProp('borderColor', this.previousSelectedEventColor);
|
||||
event.setProp('textColor','#444444');
|
||||
event.setProp('title','');
|
||||
setMainUser(user) {
|
||||
console.log('setMainUser APP', user);
|
||||
|
||||
if (user.id !== this.$store.getters.getMainUser && (
|
||||
this.$store.state.activity.calendarRange !== null
|
||||
|| this.$store.state.activity.startDate !== null
|
||||
|| this.$store.state.activity.endDate !== null
|
||||
)
|
||||
) {
|
||||
if (!window.confirm(this.$t('change_main_user_will_reset_event_data'))) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.$store.dispatch('setMainUser', user);
|
||||
this.$store.commit('showUserOnCalendar', {user, ranges: true, remotes: true});
|
||||
},
|
||||
removeMainUser(user) {
|
||||
console.log('removeMainUser APP', user);
|
||||
|
||||
window.alert(this.$t('main_user_is_mandatory'));
|
||||
return;
|
||||
},
|
||||
onDatesSet(event) {
|
||||
console.log('onDatesSet', event);
|
||||
this.$store.dispatch('setCurrentDatesView', {start: event.start, end: event.end});
|
||||
},
|
||||
onDateSelect(payload) {
|
||||
console.log(payload)
|
||||
this.unSelectPreviousEvent(this.selectedEvent);
|
||||
Object.assign(payload, {users: this.users});
|
||||
Object.assign(payload, {title: 'Choisir cette plage'}); //TODO does not display
|
||||
//payload.event.setProp('title', 'Choisir cette plage');
|
||||
this.$store.dispatch('createEvent', payload);
|
||||
console.log('onDateSelect', payload);
|
||||
|
||||
// show an alert if changing mainUser
|
||||
if ((this.$store.getters.getMainUser !== null
|
||||
&& this.$store.state.me.id !== this.$store.getters.getMainUser.id)
|
||||
|| this.$store.getters.getMainUser === null) {
|
||||
if (!window.confirm(this.$t('will_change_main_user_for_me'))) {
|
||||
return;
|
||||
} else {
|
||||
this.$store.commit('showUserOnCalendar', {user: this.$store.state.me, remotes: true, ranges: true})
|
||||
}
|
||||
}
|
||||
|
||||
this.$store.dispatch('setEventTimes', {start: payload.start, end: payload.end});
|
||||
},
|
||||
onEventChange(payload) {
|
||||
this.$store.dispatch('updateEvent', payload);
|
||||
console.log('onEventChange', payload);
|
||||
if (this.$store.state.activity.calendarRange !== null) {
|
||||
throw new Error("not allowed to edit a calendar associated with a calendar range");
|
||||
}
|
||||
this.$store.dispatch('setEventTimes', {start: payload.event.start, end: payload.event.end});
|
||||
},
|
||||
onEventClick(payload) {
|
||||
this.previousSelectedEvent = this.selectedEvent;
|
||||
this.previousSelectedEventColor = payload.event.extendedProps.sourceColor;
|
||||
this.selectedEvent = payload.event;
|
||||
this.unSelectPreviousEvent(this.previousSelectedEvent);
|
||||
payload.event.setProp('backgroundColor','#3788d8');
|
||||
payload.event.setProp('borderColor','#3788d8');
|
||||
payload.event.setProp('title', 'Choisir cette plage');
|
||||
payload.event.setProp('textColor','#ffffff');
|
||||
if (payload.event.extendedProps.is !== 'range') {
|
||||
// do nothing when clicking on remote
|
||||
return;
|
||||
}
|
||||
|
||||
// show an alert if changing mainUser
|
||||
if (this.$store.getters.getMainUser !== null
|
||||
&& payload.event.extendedProps.userId !== this.$store.getters.getMainUser.id) {
|
||||
if (!window.confirm(this.$t('this_calendar_range_will_change_main_user'))) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.$store.dispatch('associateCalendarToRange', {range: payload.event});
|
||||
},
|
||||
onEventMouseEnter(payload) {
|
||||
payload.event.setProp('borderColor','#444444');
|
||||
},
|
||||
onEventMouseLeave(payload) {
|
||||
payload.event.setProp('borderColor','#ffffff');
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.init();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.calendar-actives {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.display-options {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
/* for events which are range */
|
||||
.fc-event.isrange {
|
||||
border-width: 3px;
|
||||
}
|
||||
</style>
|
||||
|
@@ -0,0 +1,80 @@
|
||||
<template>
|
||||
<div :style="style" class="calendar-active">
|
||||
<span class="badge-user">
|
||||
{{ user.text }}
|
||||
<template v-if="invite !== null">
|
||||
<i v-if="invite.status === 'accepted'" class="fa fa-check"></i>
|
||||
<i v-else-if="invite.status === 'declined'" class="fa fa-times"></i>
|
||||
<i v-else-if="invite.status === 'pending'" class="fa fa-question-o"></i>
|
||||
<i v-else-if="invite.status === 'tentative'" class="fa fa-question"></i>
|
||||
<span v-else="">{{ invite.status }}</span>
|
||||
</template>
|
||||
</span>
|
||||
<span class="form-check-inline form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="flexSwitchCheckDefault" v-model="rangeShow">
|
||||
<label class="form-check-label" for="flexSwitchCheckDefault" title="Disponibilités"><i class="fa fa-calendar-check-o"></i></label>
|
||||
</span>
|
||||
<span class="form-check-inline form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="flexSwitchCheckDefault" v-model="remoteShow">
|
||||
<label class="form-check-label" for="flexSwitchCheckDefault" title="Agenda"><i class="fa fa-calendar"></i></label>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {mapGetters} from 'vuex';
|
||||
|
||||
export default {
|
||||
name: "CalendarActive",
|
||||
props: {
|
||||
user: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
invite: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: null,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
style() {
|
||||
return {
|
||||
backgroundColor: this.$store.getters.getUserData(this.user).mainColor,
|
||||
};
|
||||
},
|
||||
rangeShow: {
|
||||
set (value) {
|
||||
this.$store.commit('showUserOnCalendar', {user: this.user, ranges: value});
|
||||
},
|
||||
get() {
|
||||
return this.$store.getters.isRangeShownOnCalendarForUser(this.user);
|
||||
}
|
||||
},
|
||||
remoteShow: {
|
||||
set (value) {
|
||||
this.$store.commit('showUserOnCalendar', {user: this.user, remotes: value});
|
||||
},
|
||||
get() {
|
||||
return this.$store.getters.isRemoteShownOnCalendarForUser(this.user);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
.calendar-active {
|
||||
margin: 0 0.25rem 0.25rem 0;
|
||||
padding: 0.5rem;
|
||||
|
||||
border-radius: 0.5rem;
|
||||
|
||||
color: var(--bs-blue);
|
||||
|
||||
& > .badge-user {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,38 @@
|
||||
import {fetchResults} from '../../../../../ChillMainBundle/Resources/public/lib/api/apiMethods';
|
||||
import {datetimeToISO} from '../../../../../ChillMainBundle/Resources/public/chill/js/date';
|
||||
import {User} from '../../../../../ChillMainBundle/Resources/public/types';
|
||||
import {CalendarLight, CalendarRange, CalendarRemote} from '../../types';
|
||||
|
||||
// re-export whoami
|
||||
export {whoami} from "../../../../../ChillMainBundle/Resources/public/lib/api/user";
|
||||
|
||||
/**
|
||||
*
|
||||
* @param user
|
||||
* @param Date start
|
||||
* @param Date end
|
||||
* @return Promise
|
||||
*/
|
||||
export const fetchCalendarRangeForUser = (user: User, start: Date, end: Date): Promise<CalendarRange[]> => {
|
||||
const uri = `/api/1.0/calendar/calendar-range-available/${user.id}.json`;
|
||||
const dateFrom = datetimeToISO(start);
|
||||
const dateTo = datetimeToISO(end);
|
||||
|
||||
return fetchResults<CalendarRange>(uri, {dateFrom, dateTo});
|
||||
}
|
||||
|
||||
export const fetchCalendarRemoteForUser = (user: User, start: Date, end: Date): Promise<CalendarRemote[]> => {
|
||||
const uri = `/api/1.0/calendar/proxy/calendar/by-user/${user.id}/events`;
|
||||
const dateFrom = datetimeToISO(start);
|
||||
const dateTo = datetimeToISO(end);
|
||||
|
||||
return fetchResults<CalendarRemote>(uri, {dateFrom, dateTo});
|
||||
}
|
||||
|
||||
export const fetchCalendarLocalForUser = (user: User, start: Date, end: Date): Promise<CalendarLight[]> => {
|
||||
const uri = `/api/1.0/calendar/calendar/by-user/${user.id}.json`;
|
||||
const dateFrom = datetimeToISO(start);
|
||||
const dateTo = datetimeToISO(end);
|
||||
|
||||
return fetchResults<CalendarLight>(uri, {dateFrom, dateTo});
|
||||
}
|
@@ -0,0 +1,19 @@
|
||||
|
||||
const COLORS = [ /* from https://colorbrewer2.org/#type=qualitative&scheme=Set3&n=12 */
|
||||
'#8dd3c7',
|
||||
'#ffffb3',
|
||||
'#bebada',
|
||||
'#fb8072',
|
||||
'#80b1d3',
|
||||
'#fdb462',
|
||||
'#b3de69',
|
||||
'#fccde5',
|
||||
'#d9d9d9',
|
||||
'#bc80bd',
|
||||
'#ccebc5',
|
||||
'#ffed6f'
|
||||
];
|
||||
|
||||
export {
|
||||
COLORS,
|
||||
};
|
@@ -1,19 +1,25 @@
|
||||
import { personMessages } from 'ChillPersonAssets/vuejs/_js/i18n'
|
||||
import { calendarUserSelectorMessages } from '../_components/CalendarUserSelector/js/i18n';
|
||||
import { activityMessages } from 'ChillActivityAssets/vuejs/Activity/i18n';
|
||||
import {personMessages} from 'ChillPersonAssets/vuejs/_js/i18n'
|
||||
import {calendarUserSelectorMessages} from '../_components/CalendarUserSelector/js/i18n';
|
||||
import {activityMessages} from 'ChillActivityAssets/vuejs/Activity/i18n';
|
||||
|
||||
const appMessages = {
|
||||
fr: {
|
||||
choose_your_date: "Sélectionnez votre plage",
|
||||
activity: {
|
||||
add_persons: "Ajouter des personnes concernées",
|
||||
bloc_persons: "Usagers",
|
||||
bloc_persons_associated: "Usagers du parcours",
|
||||
bloc_persons_not_associated: "Tiers non-pro.",
|
||||
bloc_thirdparty: "Tiers professionnels",
|
||||
bloc_users: "T(M)S",
|
||||
}
|
||||
}
|
||||
fr: {
|
||||
choose_your_date: "Sélectionnez votre plage",
|
||||
activity: {
|
||||
add_persons: "Ajouter des personnes concernées",
|
||||
bloc_persons: "Usagers",
|
||||
bloc_persons_associated: "Usagers du parcours",
|
||||
bloc_persons_not_associated: "Tiers non-pro.",
|
||||
bloc_thirdparty: "Tiers professionnels",
|
||||
bloc_users: "T(M)S",
|
||||
},
|
||||
this_calendar_range_will_change_main_user: "Cette plage de disponibilité n'est pas celle de l'utilisateur principal. Si vous continuez, l'utilisateur principal sera adapté. Êtes-vous sûr·e ?",
|
||||
will_change_main_user_for_me: "Vous ne pouvez pas écrire dans le calendrier d'un autre utilisateur. Voulez-vous être l'utilisateur principal de ce rendez-vous ?",
|
||||
main_user_is_mandatory: "L'utilisateur principal est requis. Vous pouvez le modifier, mais pas le supprimer",
|
||||
change_main_user_will_reset_event_data: "Modifier l'utilisateur principal nécessite de choisir une autre plage de disponibilité ou un autre horaire. Ces informations seront perdues. Êtes-vous sûr·e de vouloir continuer ?",
|
||||
list_three_days: 'Liste 3 jours',
|
||||
current_selected: 'Rendez-vous fixé',
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(appMessages.fr, personMessages.fr);
|
||||
@@ -21,5 +27,5 @@ Object.assign(appMessages.fr, calendarUserSelectorMessages.fr);
|
||||
Object.assign(appMessages.fr, activityMessages.fr);
|
||||
|
||||
export {
|
||||
appMessages
|
||||
appMessages
|
||||
};
|
||||
|
@@ -1,228 +0,0 @@
|
||||
import 'es6-promise/auto';
|
||||
import { createStore } from 'vuex';
|
||||
import { postLocation } from 'ChillActivityAssets/vuejs/Activity/api';
|
||||
import {
|
||||
getLocations, getLocationTypeByDefaultFor,
|
||||
getUserCurrentLocation
|
||||
} from "../../../../../ChillActivityBundle/Resources/public/vuejs/Activity/api";
|
||||
|
||||
const debug = process.env.NODE_ENV !== 'production';
|
||||
|
||||
const addIdToValue = (string, id) => {
|
||||
let array = string ? string.split(',') : [];
|
||||
array.push(id.toString());
|
||||
let str = array.join();
|
||||
return str;
|
||||
};
|
||||
|
||||
const removeIdFromValue = (string, id) => {
|
||||
let array = string.split(',');
|
||||
array = array.filter(el => el !== id.toString());
|
||||
let str = array.join();
|
||||
return str;
|
||||
};
|
||||
|
||||
/*
|
||||
* Assign missing keys for the ConcernedGroups component
|
||||
*/
|
||||
const mapEntity = (entity) => {
|
||||
Object.assign(entity, {thirdParties: entity.professionals, users: entity.invites});
|
||||
return entity;
|
||||
};
|
||||
|
||||
const store = createStore({
|
||||
strict: debug,
|
||||
state: {
|
||||
activity: mapEntity(window.entity), // activity is the calendar entity actually
|
||||
currentEvent: null
|
||||
},
|
||||
getters: {
|
||||
suggestedEntities(state) {
|
||||
if (typeof(state.activity.accompanyingPeriod) === 'undefined') {
|
||||
return [];
|
||||
}
|
||||
const allEntities = [
|
||||
...store.getters.suggestedPersons,
|
||||
...store.getters.suggestedRequestor,
|
||||
...store.getters.suggestedUser,
|
||||
...store.getters.suggestedResources
|
||||
];
|
||||
const uniqueIds = [...new Set(allEntities.map(i => `${i.type}-${i.id}`))];
|
||||
return Array.from(uniqueIds, id => allEntities.filter(r => `${r.type}-${r.id}` === id)[0]);
|
||||
},
|
||||
suggestedPersons(state) {
|
||||
const existingPersonIds = state.activity.persons.map(p => p.id);
|
||||
return state.activity.accompanyingPeriod.participations
|
||||
.filter(p => p.endDate === null)
|
||||
.map(p => p.person)
|
||||
.filter(p => !existingPersonIds.includes(p.id))
|
||||
},
|
||||
suggestedRequestor(state) {
|
||||
const existingPersonIds = state.activity.persons.map(p => p.id);
|
||||
const existingThirdPartyIds = state.activity.thirdParties.map(p => p.id);
|
||||
return [state.activity.accompanyingPeriod.requestor]
|
||||
.filter(r =>
|
||||
(r.type === 'person' && !existingPersonIds.includes(r.id)) ||
|
||||
(r.type === 'thirdparty' && !existingThirdPartyIds.includes(r.id))
|
||||
);
|
||||
},
|
||||
suggestedUser(state) {
|
||||
const existingUserIds = state.activity.users.map(p => p.id);
|
||||
return [state.activity.accompanyingPeriod.user]
|
||||
.filter(
|
||||
u => !existingUserIds.includes(u.id)
|
||||
);
|
||||
},
|
||||
suggestedResources(state) {
|
||||
const resources = state.activity.accompanyingPeriod.resources;
|
||||
const existingPersonIds = state.activity.persons.map(p => p.id);
|
||||
const existingThirdPartyIds = state.activity.thirdParties.map(p => p.id);
|
||||
return state.activity.accompanyingPeriod.resources
|
||||
.map(r => r.resource)
|
||||
.filter(r =>
|
||||
(r.type === 'person' && !existingPersonIds.includes(r.id)) ||
|
||||
(r.type === 'thirdparty' && !existingThirdPartyIds.includes(r.id))
|
||||
);
|
||||
}
|
||||
},
|
||||
mutations: {
|
||||
|
||||
// ConcernedGroups
|
||||
addPersonsInvolved(state, payload) {
|
||||
//console.log('### mutation addPersonsInvolved', payload.result.type);
|
||||
switch (payload.result.type) {
|
||||
case 'person':
|
||||
state.activity.persons.push(payload.result);
|
||||
break;
|
||||
case 'thirdparty':
|
||||
state.activity.thirdParties.push(payload.result);
|
||||
break;
|
||||
case 'user':
|
||||
state.activity.users.push(payload.result);
|
||||
break;
|
||||
};
|
||||
},
|
||||
removePersonInvolved(state, payload) {
|
||||
//console.log('### mutation removePersonInvolved', payload.type);
|
||||
switch (payload.type) {
|
||||
case 'person':
|
||||
state.activity.persons = state.activity.persons.filter(person => person !== payload);
|
||||
break;
|
||||
case 'thirdparty':
|
||||
state.activity.thirdParties = state.activity.thirdParties.filter(thirdparty => thirdparty !== payload);
|
||||
break;
|
||||
case 'user':
|
||||
state.activity.users = state.activity.users.filter(user => user !== payload);
|
||||
break;
|
||||
};
|
||||
},
|
||||
// Calendar
|
||||
setEvents(state, payload) {
|
||||
console.log(payload)
|
||||
state.currentEvent = {start: payload.start, end: payload.end}
|
||||
},
|
||||
// Location
|
||||
updateLocation(state, value) {
|
||||
console.log('### mutation: updateLocation', value);
|
||||
state.activity.location = value;
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
addPersonsInvolved({ commit }, payload) {
|
||||
console.log('### action addPersonsInvolved', payload.result.type);
|
||||
switch (payload.result.type) {
|
||||
case 'person':
|
||||
let aPersons = document.getElementById("chill_calendarbundle_calendar_persons");
|
||||
aPersons.value = addIdToValue(aPersons.value, payload.result.id);
|
||||
break;
|
||||
case 'thirdparty':
|
||||
let aThirdParties = document.getElementById("chill_calendarbundle_calendar_professionals");
|
||||
aThirdParties.value = addIdToValue(aThirdParties.value, payload.result.id);
|
||||
break;
|
||||
case 'user':
|
||||
let aUsers = document.getElementById("chill_calendarbundle_calendar_invites");
|
||||
aUsers.value = addIdToValue(aUsers.value, payload.result.id);
|
||||
break;
|
||||
};
|
||||
commit('addPersonsInvolved', payload);
|
||||
},
|
||||
removePersonInvolved({ commit }, payload) {
|
||||
//console.log('### action removePersonInvolved', payload);
|
||||
switch (payload.type) {
|
||||
case 'person':
|
||||
let aPersons = document.getElementById("chill_calendarbundle_calendar_persons");
|
||||
aPersons.value = removeIdFromValue(aPersons.value, payload.id);
|
||||
break;
|
||||
case 'thirdparty':
|
||||
let aThirdParties = document.getElementById("chill_calendarbundle_calendar_professionals");
|
||||
aThirdParties.value = removeIdFromValue(aThirdParties.value, payload.id);
|
||||
break;
|
||||
case 'user':
|
||||
let aUsers = document.getElementById("chill_calendarbundle_calendar_invites");
|
||||
aUsers.value = removeIdFromValue(aUsers.value, payload.id);
|
||||
break;
|
||||
};
|
||||
commit('removePersonInvolved', payload);
|
||||
},
|
||||
|
||||
// Calendar
|
||||
createEvent({ commit }, payload) {
|
||||
console.log('### action createEvent', payload);
|
||||
let startDateInput = document.getElementById("chill_calendarbundle_calendar_startDate");
|
||||
startDateInput.value = payload.startStr;
|
||||
let endDateInput = document.getElementById("chill_calendarbundle_calendar_endDate");
|
||||
endDateInput.value = payload.endStr;
|
||||
let mainUserInput = document.getElementById("chill_calendarbundle_calendar_mainUser");
|
||||
mainUserInput.value = payload.users.logged.id;
|
||||
commit('setEvents', payload);
|
||||
},
|
||||
updateEvent({ commit }, payload) {
|
||||
console.log('### action updateEvent', payload);
|
||||
let startDateInput = document.getElementById("chill_calendarbundle_calendar_startDate");
|
||||
startDateInput.value = payload.event.start.toISOString();
|
||||
let endDateInput = document.getElementById("chill_calendarbundle_calendar_endDate");
|
||||
endDateInput.value = payload.event.end.toISOString();
|
||||
let calendarRangeInput = document.getElementById("chill_calendarbundle_calendar_calendarRange");
|
||||
calendarRangeInput.value = Number(payload.event.extendedProps.calendarRangeId);
|
||||
let mainUserInput = document.getElementById("chill_calendarbundle_calendar_mainUser");
|
||||
mainUserInput.value = Number(payload.event.source.id);
|
||||
commit('setEvents', payload);
|
||||
},
|
||||
|
||||
// Location
|
||||
updateLocation({ commit }, value) {
|
||||
console.log('### action: updateLocation', value);
|
||||
let hiddenLocation = document.getElementById("chill_calendarbundle_calendar_location");
|
||||
if (value.onthefly) {
|
||||
const body = {
|
||||
"type": "location",
|
||||
"name": value.name === '__AccompanyingCourseLocation__' ? null : value.name,
|
||||
"locationType": {
|
||||
"id": value.locationType.id,
|
||||
"type": "location-type"
|
||||
}
|
||||
};
|
||||
if (value.address.id) {
|
||||
Object.assign(body, {
|
||||
"address": {
|
||||
"id": value.address.id
|
||||
},
|
||||
})
|
||||
}
|
||||
postLocation(body)
|
||||
.then(
|
||||
location => hiddenLocation.value = location.id
|
||||
).catch(
|
||||
err => {
|
||||
console.log(err.message);
|
||||
}
|
||||
);
|
||||
} else {
|
||||
hiddenLocation.value = value.id;
|
||||
}
|
||||
commit("updateLocation", value);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export default store;
|
@@ -0,0 +1,241 @@
|
||||
import {
|
||||
addIdToValue,
|
||||
removeIdFromValue,
|
||||
} from './utils';
|
||||
import {
|
||||
fetchCalendarRangeForUser,
|
||||
fetchCalendarRemoteForUser,
|
||||
fetchCalendarLocalForUser,
|
||||
} from './../api';
|
||||
import {datetimeToISO} from 'ChillMainAssets/chill/js/date';
|
||||
import {postLocation} from 'ChillActivityAssets/vuejs/Activity/api';
|
||||
|
||||
/**
|
||||
* This will store a unique key for each value, and prevent to launch the same
|
||||
* request multiple times, when fetching user calendars.
|
||||
*
|
||||
* Actually, each time a user is added or removed, the methods "dateSet" is executed and this
|
||||
* sparkle a request by user to get the calendar data. When the calendar data is fetched, it is
|
||||
* immediatly added to the calendar which, in turn , launch the event dateSet and re-launch fetch
|
||||
* queries which has not yet ended. Storing the queries already executed prevent this loop.
|
||||
*
|
||||
* @type {Set<String>}
|
||||
*/
|
||||
const fetchings = new Set();
|
||||
|
||||
export default {
|
||||
setCurrentDatesView({commit, dispatch}, {start, end}) {
|
||||
commit('setCurrentDatesView', {start, end});
|
||||
|
||||
return dispatch('fetchCalendarEvents');
|
||||
},
|
||||
fetchCalendarEvents({state, getters, dispatch}) {
|
||||
if (state.currentView.start === null && state.currentView.end === null) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
let promises = [];
|
||||
for (const uid of state.currentView.users.keys()) {
|
||||
let unique = `${uid}, ${state.currentView.start.toISOString()}, ${state.currentView.end.toISOString()}`;
|
||||
|
||||
if (fetchings.has(unique)) {
|
||||
console.log('prevent from fetching for a user', unique);
|
||||
continue;
|
||||
}
|
||||
|
||||
fetchings.add(unique);
|
||||
|
||||
promises.push(
|
||||
dispatch(
|
||||
'fetchCalendarRangeForUser',
|
||||
{user: state.usersData.get(uid).user, start: state.currentView.start, end: state.currentView.end}
|
||||
)
|
||||
);
|
||||
promises.push(
|
||||
dispatch(
|
||||
'fetchCalendarRemotesForUser',
|
||||
{user: state.usersData.get(uid).user, start: state.currentView.start, end: state.currentView.end}
|
||||
)
|
||||
);
|
||||
promises.push(
|
||||
dispatch(
|
||||
'fetchCalendarLocalsForUser',
|
||||
{user: state.usersData.get(uid).user, start: state.currentView.start, end: state.currentView.end}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return Promise.all(promises);
|
||||
},
|
||||
fetchCalendarRangeForUser({commit, getters}, {user, start, end}) {
|
||||
if (!getters.isCalendarRangeLoadedForUser({user, start, end})) {
|
||||
return fetchCalendarRangeForUser(user, start, end).then((ranges) => {
|
||||
commit('addCalendarRangesForUser', {user, ranges, start, end});
|
||||
|
||||
return Promise.resolve();
|
||||
});
|
||||
}
|
||||
},
|
||||
fetchCalendarRemotesForUser({commit, getters}, {user, start, end}) {
|
||||
if (!getters.isCalendarRemoteLoadedForUser({user, start, end})) {
|
||||
return fetchCalendarRemoteForUser(user, start, end).then((remotes) => {
|
||||
commit('addCalendarRemotesForUser', {user, remotes, start, end});
|
||||
|
||||
return Promise.resolve();
|
||||
});
|
||||
}
|
||||
},
|
||||
fetchCalendarLocalsForUser({commit, getters}, {user, start, end}) {
|
||||
if (!getters.isCalendarRemoteLoadedForUser({user, start, end})) {
|
||||
return fetchCalendarLocalForUser(user, start, end).then((locals) => {
|
||||
commit('addCalendarLocalsForUser', {user, locals, start, end});
|
||||
|
||||
return Promise.resolve();
|
||||
});
|
||||
}
|
||||
},
|
||||
addPersonsInvolved({commit, dispatch}, payload) {
|
||||
console.log('### action addPersonsInvolved', payload.result.type);
|
||||
console.log('### action addPersonsInvolved payload result', payload.result);
|
||||
switch (payload.result.type) {
|
||||
case 'person':
|
||||
let aPersons = document.getElementById("chill_activitybundle_activity_persons");
|
||||
aPersons.value = addIdToValue(aPersons.value, payload.result.id);
|
||||
break;
|
||||
case 'thirdparty':
|
||||
let aThirdParties = document.getElementById("chill_activitybundle_activity_professionals");
|
||||
aThirdParties.value = addIdToValue(aThirdParties.value, payload.result.id);
|
||||
break;
|
||||
case 'user':
|
||||
let aUsers = document.getElementById("chill_activitybundle_activity_users");
|
||||
aUsers.value = addIdToValue(aUsers.value, payload.result.id);
|
||||
commit('showUserOnCalendar', {user: payload.result, ranges: false, remotes: true});
|
||||
dispatch('fetchCalendarEvents');
|
||||
break;
|
||||
}
|
||||
;
|
||||
commit('addPersonsInvolved', payload);
|
||||
},
|
||||
removePersonInvolved({commit}, payload) {
|
||||
//console.log('### action removePersonInvolved', payload);
|
||||
switch (payload.type) {
|
||||
case 'person':
|
||||
let aPersons = document.getElementById("chill_activitybundle_activity_persons");
|
||||
aPersons.value = removeIdFromValue(aPersons.value, payload.id);
|
||||
break;
|
||||
case 'thirdparty':
|
||||
let aThirdParties = document.getElementById("chill_activitybundle_activity_professionals");
|
||||
aThirdParties.value = removeIdFromValue(aThirdParties.value, payload.id);
|
||||
break;
|
||||
case 'user':
|
||||
let aUsers = document.getElementById("chill_activitybundle_activity_users");
|
||||
aUsers.value = removeIdFromValue(aUsers.value, payload.id);
|
||||
break;
|
||||
}
|
||||
;
|
||||
commit('removePersonInvolved', payload);
|
||||
},
|
||||
|
||||
// Calendar
|
||||
/**
|
||||
* set event startDate and endDate.
|
||||
*
|
||||
* if the mainUser is different from "me", it will replace the mainUser
|
||||
*
|
||||
* @param commit
|
||||
* @param state
|
||||
* @param getters
|
||||
* @param start
|
||||
* @param end
|
||||
*/
|
||||
setEventTimes({commit, state, getters}, {start, end}) {
|
||||
console.log('### action createEvent', {start, end});
|
||||
let startDateInput = document.getElementById("chill_activitybundle_activity_startDate");
|
||||
startDateInput.value = null !== start ? datetimeToISO(start) : '';
|
||||
let endDateInput = document.getElementById("chill_activitybundle_activity_endDate");
|
||||
endDateInput.value = null !== end ? datetimeToISO(end) : '';
|
||||
let calendarRangeInput = document.getElementById("chill_activitybundle_activity_calendarRange");
|
||||
calendarRangeInput.value = "";
|
||||
|
||||
if (getters.getMainUser === null || getters.getMainUser.id !== state.me.id) {
|
||||
let mainUserInput = document.getElementById("chill_activitybundle_activity_mainUser");
|
||||
mainUserInput.value = state.me.id;
|
||||
commit('setMainUser', state.me);
|
||||
}
|
||||
|
||||
commit('setEventTimes', {start, end});
|
||||
},
|
||||
associateCalendarToRange({state, commit, dispatch, getters}, {range}) {
|
||||
console.log('### action associateCAlendarToRange', range);
|
||||
let startDateInput = document.getElementById("chill_activitybundle_activity_startDate");
|
||||
startDateInput.value = null !== range ? datetimeToISO(range.start) : "";
|
||||
let endDateInput = document.getElementById("chill_activitybundle_activity_endDate");
|
||||
endDateInput.value = null !== range ? datetimeToISO(range.end) : "";
|
||||
let calendarRangeInput = document.getElementById("chill_activitybundle_activity_calendarRange");
|
||||
calendarRangeInput.value = null !== range ? Number(range.extendedProps.calendarRangeId) : "";
|
||||
|
||||
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);
|
||||
|
||||
// TODO: remove persons involved with this user
|
||||
}
|
||||
}
|
||||
|
||||
commit('associateCalendarToRange', {range});
|
||||
return Promise.resolve();
|
||||
},
|
||||
setMainUser({commit, dispatch, state}, mainUser) {
|
||||
console.log('setMainUser', mainUser);
|
||||
|
||||
let mainUserInput = document.getElementById("chill_activitybundle_activity_mainUser");
|
||||
mainUserInput.value = Number(mainUser.id);
|
||||
|
||||
return dispatch('associateCalendarToRange', { range: null }).then(() => {
|
||||
commit('setMainUser', mainUser);
|
||||
});
|
||||
},
|
||||
|
||||
// Location
|
||||
updateLocation({commit}, value) {
|
||||
console.log('### action: updateLocation', value);
|
||||
let hiddenLocation = document.getElementById("chill_activitybundle_activity_location");
|
||||
if (value.onthefly) {
|
||||
const body = {
|
||||
"type": "location",
|
||||
"name": value.name === '__AccompanyingCourseLocation__' ? null : value.name,
|
||||
"locationType": {
|
||||
"id": value.locationType.id,
|
||||
"type": "location-type"
|
||||
}
|
||||
};
|
||||
if (value.address.id) {
|
||||
Object.assign(body, {
|
||||
"address": {
|
||||
"id": value.address.id
|
||||
},
|
||||
})
|
||||
}
|
||||
postLocation(body)
|
||||
.then(
|
||||
location => hiddenLocation.value = location.id
|
||||
).catch(
|
||||
err => {
|
||||
console.log(err.message);
|
||||
}
|
||||
);
|
||||
} else {
|
||||
hiddenLocation.value = value.id;
|
||||
}
|
||||
commit("updateLocation", value);
|
||||
}
|
||||
}
|
@@ -0,0 +1,272 @@
|
||||
export default {
|
||||
/**
|
||||
* get the main user of the event/Calendar
|
||||
*
|
||||
* @param state
|
||||
* @returns {*|null}
|
||||
*/
|
||||
getMainUser(state) {
|
||||
return state.activity.mainUser || null;
|
||||
},
|
||||
/**
|
||||
* return the date of the event/Calendar
|
||||
*
|
||||
* @param state
|
||||
* @returns {Date}
|
||||
*/
|
||||
getEventDate(state) {
|
||||
if (null === state.activity.start) {
|
||||
return new Date();
|
||||
}
|
||||
throw 'transform date to object ?';
|
||||
},
|
||||
/**
|
||||
* Compute the event sources to show on the FullCalendar
|
||||
*
|
||||
* @param state
|
||||
* @param getters
|
||||
* @returns {[]}
|
||||
*/
|
||||
getEventSources(state, getters) {
|
||||
let sources = [];
|
||||
|
||||
// current calendar
|
||||
if (state.activity.startDate !== null && state.activity.endDate !== null) {
|
||||
const s = {
|
||||
id: 'current',
|
||||
events: [
|
||||
{
|
||||
title: "Rendez-vous",
|
||||
start: state.activity.startDate,
|
||||
end: state.activity.endDate,
|
||||
allDay: false,
|
||||
is: "current",
|
||||
classNames: ['iscurrent'],
|
||||
}
|
||||
],
|
||||
editable: state.activity.calendarRange === null,
|
||||
};
|
||||
|
||||
sources.push(s);
|
||||
}
|
||||
|
||||
for (const [userId, kinds] of state.currentView.users.entries()) {
|
||||
if (!state.usersData.has(userId)) {
|
||||
console.log('try to get events on a user which not exists', userId);
|
||||
continue;
|
||||
}
|
||||
|
||||
const userData = state.usersData.get(userId);
|
||||
|
||||
if (kinds.ranges && userData.calendarRanges.length > 0) {
|
||||
const s = {
|
||||
id: `ranges_${userId}`,
|
||||
events: userData.calendarRanges.filter(r => state.activity.calendarRange === null || r.calendarRangeId !== state.activity.calendarRange.calendarRangeId),
|
||||
color: userData.mainColor,
|
||||
classNames: ['isrange'],
|
||||
backgroundColor: 'white',
|
||||
textColor: 'black',
|
||||
editable: false,
|
||||
};
|
||||
|
||||
sources.push(s);
|
||||
}
|
||||
|
||||
if (kinds.remotes && userData.remotes.length > 0) {
|
||||
const s = {
|
||||
'id': `remote_${userId}`,
|
||||
events: userData.remotes,
|
||||
color: userData.mainColor,
|
||||
textColor: 'black',
|
||||
editable: false,
|
||||
};
|
||||
|
||||
sources.push(s);
|
||||
}
|
||||
|
||||
// if remotes is checked, we display also the locals calendars
|
||||
if (kinds.remotes && userData.locals.length > 0) {
|
||||
const s = {
|
||||
'id': `local_${userId}`,
|
||||
events: userData.locals.filter(l => l.originId !== state.activity.id),
|
||||
color: userData.mainColor,
|
||||
textColor: 'black',
|
||||
editable: false,
|
||||
};
|
||||
|
||||
sources.push(s);
|
||||
}
|
||||
}
|
||||
|
||||
return sources;
|
||||
},
|
||||
getInitialDate(state) {
|
||||
return state.activity.startDate;
|
||||
},
|
||||
getInviteForUser: (state) => (user) => {
|
||||
return state.activity.invites.find(i => i.user.id === user.id);
|
||||
},
|
||||
/**
|
||||
* get the user data for a specific user
|
||||
*
|
||||
* @param state
|
||||
* @returns {function(*): unknown}
|
||||
*/
|
||||
getUserData: (state) => (user) => {
|
||||
return state.usersData.get(user.id);
|
||||
},
|
||||
getUserDataById: (state) => (userId) => {
|
||||
return state.usersData.get(userId);
|
||||
},
|
||||
/**
|
||||
* return true if the user has an entry in userData
|
||||
*
|
||||
* @param state
|
||||
* @returns {function(*): boolean}
|
||||
*/
|
||||
hasUserData: (state) => (user) => {
|
||||
return state.usersData.has(user.id);
|
||||
},
|
||||
hasUserDataById: (state) => (userId) => {
|
||||
return state.usersData.has(userId);
|
||||
},
|
||||
/**
|
||||
* return true if there was a fetch query for event between this date (start and end),
|
||||
* those date are included.
|
||||
*
|
||||
* @param state
|
||||
* @param getters
|
||||
* @returns {(function({user: *, start: *, end: *}): (boolean))|*}
|
||||
*/
|
||||
isCalendarRangeLoadedForUser: (state, getters) => ({user, start, end}) => {
|
||||
if (!getters.hasUserData(user)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let interval of getters.getUserData(user).calendarRangesLoaded) {
|
||||
if (start >= interval.start && end <= interval.end) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
/**
|
||||
* return true if there was a fetch query for event between this date (start and end),
|
||||
* those date are included.
|
||||
*
|
||||
* @param state
|
||||
* @param getters
|
||||
* @returns {(function({user: *, start: *, end: *}): (boolean))|*}
|
||||
*/
|
||||
isCalendarRemoteLoadedForUser: (state, getters) => ({user, start, end}) => {
|
||||
if (!getters.hasUserData(user)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let interval of getters.getUserData(user).remotesLoaded) {
|
||||
if (start >= interval.start && end <= interval.end) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
/**
|
||||
* return true if the user ranges are shown on calendar
|
||||
*
|
||||
* @param state
|
||||
* @returns boolean
|
||||
*/
|
||||
isRangeShownOnCalendarForUser: (state) => (user) => {
|
||||
const k = state.currentView.users.get(user.id);
|
||||
if (typeof k === 'undefined') {
|
||||
console.error('try to determinate if calendar range is shown and user is not in currentView');
|
||||
return false;
|
||||
}
|
||||
|
||||
return k.ranges;
|
||||
},
|
||||
|
||||
/**
|
||||
* return true if the user remote is shown on calendar
|
||||
* @param state
|
||||
* @returns boolean
|
||||
*/
|
||||
isRemoteShownOnCalendarForUser: (state) => (user) => {
|
||||
const k = state.currentView.users.get(user.id);
|
||||
if (typeof k === 'undefined') {
|
||||
console.error('try to determinate if calendar range is shown and user is not in currentView');
|
||||
return false;
|
||||
}
|
||||
|
||||
return k.remotes;
|
||||
},
|
||||
|
||||
getLocationById: (state) => (id) => {
|
||||
for (let group of state.availableLocations) {
|
||||
console.log('group', group);
|
||||
const found = group.locations.find(l => l.id === id);
|
||||
if (typeof found !== "undefined") {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
suggestedEntities(state, getters) {
|
||||
if (typeof (state.activity.accompanyingPeriod) === 'undefined') {
|
||||
return [];
|
||||
}
|
||||
const allEntities = [
|
||||
...getters.suggestedPersons,
|
||||
...getters.suggestedRequestor,
|
||||
...getters.suggestedUser,
|
||||
...getters.suggestedResources
|
||||
];
|
||||
const uniqueIds = [...new Set(allEntities.map(i => `${i.type}-${i.id}`))];
|
||||
return Array.from(uniqueIds, id => allEntities.filter(r => `${r.type}-${r.id}` === id)[0]);
|
||||
},
|
||||
suggestedPersons(state) {
|
||||
const existingPersonIds = state.activity.persons.map(p => p.id);
|
||||
return state.activity.accompanyingPeriod.participations
|
||||
.filter(p => p.endDate === null)
|
||||
.map(p => p.person)
|
||||
.filter(p => !existingPersonIds.includes(p.id))
|
||||
},
|
||||
suggestedRequestor(state) {
|
||||
if (state.activity.accompanyingPeriod.requestor === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const existingPersonIds = state.activity.persons.map(p => p.id);
|
||||
const existingThirdPartyIds = state.activity.thirdParties.map(p => p.id);
|
||||
return [state.activity.accompanyingPeriod.requestor]
|
||||
.filter(r =>
|
||||
(r.type === 'person' && !existingPersonIds.includes(r.id)) ||
|
||||
(r.type === 'thirdparty' && !existingThirdPartyIds.includes(r.id))
|
||||
);
|
||||
},
|
||||
suggestedUser(state) {
|
||||
if (null === state.activity.users) {
|
||||
return [];
|
||||
}
|
||||
const existingUserIds = state.activity.users.map(p => p.id);
|
||||
return [state.activity.accompanyingPeriod.user]
|
||||
.filter(
|
||||
u => u !== null && !existingUserIds.includes(u.id)
|
||||
);
|
||||
},
|
||||
suggestedResources(state) {
|
||||
const resources = state.activity.accompanyingPeriod.resources;
|
||||
const existingPersonIds = state.activity.persons.map(p => p.id);
|
||||
const existingThirdPartyIds = state.activity.thirdParties.map(p => p.id);
|
||||
return state.activity.accompanyingPeriod.resources
|
||||
.map(r => r.resource)
|
||||
.filter(r =>
|
||||
(r.type === 'person' && !existingPersonIds.includes(r.id)) ||
|
||||
(r.type === 'thirdparty' && !existingThirdPartyIds.includes(r.id))
|
||||
);
|
||||
}
|
||||
}
|
@@ -0,0 +1,59 @@
|
||||
import 'es6-promise/auto';
|
||||
import { createStore } from 'vuex';
|
||||
import { postLocation } from 'ChillActivityAssets/vuejs/Activity/api';
|
||||
import getters from './getters';
|
||||
import actions from './actions';
|
||||
import mutations from './mutations';
|
||||
import { mapEntity } from './utils';
|
||||
import { whoami } from '../api';
|
||||
import prepareLocations from "ChillActivityAssets/vuejs/Activity/store.locations";
|
||||
|
||||
const debug = process.env.NODE_ENV !== 'production';
|
||||
|
||||
const store = createStore({
|
||||
strict: debug,
|
||||
state: {
|
||||
activity: mapEntity(window.entity), // activity is the calendar entity actually
|
||||
currentEvent: null,
|
||||
availableLocations: [],
|
||||
/**
|
||||
* the current user
|
||||
*/
|
||||
me: null,
|
||||
/**
|
||||
* store information about current view
|
||||
*/
|
||||
currentView: {
|
||||
start: null,
|
||||
end: null,
|
||||
users: new Map(),
|
||||
},
|
||||
/**
|
||||
* store a list of existing event, to avoid storing them twice
|
||||
*/
|
||||
existingEvents: new Set(),
|
||||
/**
|
||||
* store user data
|
||||
*/
|
||||
usersData: new Map(),
|
||||
},
|
||||
getters,
|
||||
mutations,
|
||||
actions,
|
||||
});
|
||||
|
||||
whoami().then(me => {
|
||||
store.commit('setWhoAmiI', me);
|
||||
});
|
||||
|
||||
if (null !== store.getters.getMainUser) {
|
||||
store.commit('showUserOnCalendar', {ranges: true, remotes: true, user: store.getters.getMainUser});
|
||||
}
|
||||
|
||||
for (let u of store.state.activity.users) {
|
||||
store.commit('showUserOnCalendar', {ranges: false, remotes: false, user: u});
|
||||
}
|
||||
|
||||
prepareLocations(store);
|
||||
|
||||
export default store;
|
@@ -0,0 +1,195 @@
|
||||
import {
|
||||
createUserData,
|
||||
calendarRangeToFullCalendarEvent,
|
||||
remoteToFullCalendarEvent,
|
||||
localsToFullCalendarEvent,
|
||||
} from './utils';
|
||||
|
||||
export default {
|
||||
setWhoAmiI(state, me) {
|
||||
state.me = me;
|
||||
},
|
||||
setCurrentDatesView(state, {start, end}) {
|
||||
state.currentView.start = start;
|
||||
state.currentView.end = end;
|
||||
},
|
||||
showUserOnCalendar(state, {user, ranges, remotes}) {
|
||||
if (!state.usersData.has(user.id)) {
|
||||
state.usersData.set(user.id, createUserData(user, state.usersData.size));
|
||||
}
|
||||
|
||||
const cur = state.currentView.users.get(user.id);
|
||||
|
||||
state.currentView.users.set(
|
||||
user.id,
|
||||
{
|
||||
ranges: typeof ranges !== 'undefined' ? ranges : cur.ranges,
|
||||
remotes: typeof remotes !== 'undefined' ? remotes : cur.remotes,
|
||||
}
|
||||
);
|
||||
},
|
||||
/**
|
||||
* Set the event start and end to the given start and end,
|
||||
* and remove eventually the calendar range.
|
||||
*
|
||||
* @param state
|
||||
* @param Date start
|
||||
* @param Date end
|
||||
*/
|
||||
setEventTimes(state, {start, end}) {
|
||||
state.activity.startDate = start;
|
||||
state.activity.endDate = end;
|
||||
state.activity.calendarRange = null;
|
||||
},
|
||||
/**
|
||||
* Set the event's start and end from the calendar range data,
|
||||
* and associate event to calendar range.
|
||||
*
|
||||
* @param state
|
||||
* @param range
|
||||
*/
|
||||
associateCalendarToRange(state, {range}) {
|
||||
console.log('associateCalendarToRange', range);
|
||||
|
||||
if (null === range) {
|
||||
state.activity.calendarRange = null;
|
||||
state.activity.startDate = null;
|
||||
state.activity.endDate = null;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('userId', range.extendedProps.userId);
|
||||
|
||||
const r = state.usersData.get(range.extendedProps.userId).calendarRanges
|
||||
.find(r => r.calendarRangeId === range.extendedProps.calendarRangeId);
|
||||
|
||||
if (typeof r === 'undefined') {
|
||||
throw Error('Could not find managed calendar range');
|
||||
}
|
||||
|
||||
console.log('range found', r);
|
||||
|
||||
state.activity.startDate = range.start;
|
||||
state.activity.endDate = range.end;
|
||||
state.activity.calendarRange = r;
|
||||
|
||||
console.log('activity', state.activity);
|
||||
},
|
||||
|
||||
setMainUser(state, user) {
|
||||
state.activity.mainUser = user;
|
||||
},
|
||||
|
||||
// ConcernedGroups
|
||||
addPersonsInvolved(state, payload) {
|
||||
//console.log('### mutation addPersonsInvolved', payload.result.type);
|
||||
switch (payload.result.type) {
|
||||
case 'person':
|
||||
state.activity.persons.push(payload.result);
|
||||
break;
|
||||
case 'thirdparty':
|
||||
state.activity.thirdParties.push(payload.result);
|
||||
break;
|
||||
case 'user':
|
||||
state.activity.users.push(payload.result);
|
||||
break;
|
||||
}
|
||||
;
|
||||
},
|
||||
removePersonInvolved(state, payload) {
|
||||
//console.log('### mutation removePersonInvolved', payload.type);
|
||||
switch (payload.type) {
|
||||
case 'person':
|
||||
state.activity.persons = state.activity.persons.filter(person => person !== payload);
|
||||
break;
|
||||
case 'thirdparty':
|
||||
state.activity.thirdParties = state.activity.thirdParties.filter(thirdparty => thirdparty !== payload);
|
||||
break;
|
||||
case 'user':
|
||||
state.activity.users = state.activity.users.filter(user => user !== payload);
|
||||
break;
|
||||
}
|
||||
;
|
||||
},
|
||||
/**
|
||||
* Add CalendarRange object for an user
|
||||
*
|
||||
* @param state
|
||||
* @param user
|
||||
* @param ranges
|
||||
* @param start
|
||||
* @param end
|
||||
*/
|
||||
addCalendarRangesForUser(state, {user, ranges, start, end}) {
|
||||
let userData;
|
||||
if (state.usersData.has(user.id)) {
|
||||
userData = state.usersData.get(user.id);
|
||||
} else {
|
||||
userData = createUserData(user, state.usersData.size);
|
||||
state.usersData.set(user.id, userData);
|
||||
}
|
||||
|
||||
const eventRanges = ranges
|
||||
.filter(r => !state.existingEvents.has(`range_${r.id}`))
|
||||
.map(r => {
|
||||
// add to existing ids
|
||||
state.existingEvents.add(`range_${r.id}`);
|
||||
return r;
|
||||
})
|
||||
.map(r => calendarRangeToFullCalendarEvent(r));
|
||||
|
||||
userData.calendarRanges = userData.calendarRanges.concat(eventRanges);
|
||||
userData.calendarRangesLoaded.push({start, end});
|
||||
},
|
||||
addCalendarRemotesForUser(state, {user, remotes, start, end}) {
|
||||
let userData;
|
||||
if (state.usersData.has(user.id)) {
|
||||
userData = state.usersData.get(user.id);
|
||||
} else {
|
||||
userData = createUserData(user, state.usersData.size);
|
||||
state.usersData.set(user.id, userData);
|
||||
}
|
||||
|
||||
const eventRemotes = remotes
|
||||
.filter(r => !state.existingEvents.has(`remote_${r.id}`))
|
||||
.map(r => {
|
||||
// add to existing ids
|
||||
state.existingEvents.add(`remote_${r.id}`);
|
||||
return r;
|
||||
})
|
||||
.map(r => remoteToFullCalendarEvent(r));
|
||||
|
||||
userData.remotes = userData.remotes.concat(eventRemotes);
|
||||
userData.remotesLoaded.push({start, end});
|
||||
},
|
||||
addCalendarLocalsForUser(state, {user, locals, start, end}) {
|
||||
let userData;
|
||||
if (state.usersData.has(user.id)) {
|
||||
userData = state.usersData.get(user.id);
|
||||
} else {
|
||||
userData = createUserData(user, state.usersData.size);
|
||||
state.usersData.set(user.id, userData);
|
||||
}
|
||||
|
||||
const eventRemotes = locals
|
||||
.filter(r => !state.existingEvents.has(`locals_${r.id}`))
|
||||
.map(r => {
|
||||
// add to existing ids
|
||||
state.existingEvents.add(`locals_${r.id}`);
|
||||
return r;
|
||||
})
|
||||
.map(r => localsToFullCalendarEvent(r));
|
||||
|
||||
userData.locals = userData.locals.concat(eventRemotes);
|
||||
userData.localsLoaded.push({start, end});
|
||||
},
|
||||
// Location
|
||||
updateLocation(state, value) {
|
||||
console.log('### mutation: updateLocation', value);
|
||||
state.activity.location = value;
|
||||
},
|
||||
addAvailableLocationGroup(state, group) {
|
||||
state.availableLocations.push(group);
|
||||
},
|
||||
};
|
@@ -0,0 +1,108 @@
|
||||
import {COLORS} from '../const';
|
||||
import {ISOToDatetime} from '../../../../../../ChillMainBundle/Resources/public/chill/js/date';
|
||||
import {DateTime, User} from '../../../../../../ChillMainBundle/Resources/public/types';
|
||||
import {CalendarLight, CalendarRange, CalendarRemote} from '../../../types';
|
||||
import type {EventInputCalendarRange} from '../../../types';
|
||||
import {EventInput} from '@fullcalendar/vue3';
|
||||
|
||||
export interface UserData {
|
||||
user: User,
|
||||
calendarRanges: CalendarRange[],
|
||||
calendarRangesLoaded: {}[],
|
||||
remotes: CalendarRemote[],
|
||||
remotesLoaded: {}[],
|
||||
locals: CalendarRemote[],
|
||||
localsLoaded: {}[],
|
||||
mainColor: string,
|
||||
}
|
||||
|
||||
export const addIdToValue = (string: string, id: number): string => {
|
||||
let array = string ? string.split(',') : [];
|
||||
array.push(id.toString());
|
||||
let str = array.join();
|
||||
return str;
|
||||
};
|
||||
|
||||
export const removeIdFromValue = (string: string, id: number) => {
|
||||
let array = string.split(',');
|
||||
array = array.filter(el => el !== id.toString());
|
||||
let str = array.join();
|
||||
return str;
|
||||
};
|
||||
|
||||
/*
|
||||
* Assign missing keys for the ConcernedGroups component
|
||||
*/
|
||||
export const mapEntity = (entity: EventInput): EventInput => {
|
||||
let calendar = { ...entity};
|
||||
Object.assign(calendar, {thirdParties: entity.professionals});
|
||||
|
||||
if (entity.startDate !== null ) {
|
||||
calendar.startDate = ISOToDatetime(entity.startDate.datetime);
|
||||
}
|
||||
if (entity.endDate !== null) {
|
||||
calendar.endDate = ISOToDatetime(entity.endDate.datetime);
|
||||
}
|
||||
|
||||
if (entity.calendarRange !== null) {
|
||||
calendar.calendarRange.calendarRangeId = entity.calendarRange.id;
|
||||
calendar.calendarRange.id = `range_${entity.calendarRange.id}`;
|
||||
}
|
||||
|
||||
return calendar;
|
||||
};
|
||||
|
||||
export const createUserData = (user: User, colorIndex: number): UserData => {
|
||||
const colorId = colorIndex % COLORS.length;
|
||||
console.log('colorId', colorId);
|
||||
return {
|
||||
user: user,
|
||||
calendarRanges: [],
|
||||
calendarRangesLoaded: [],
|
||||
remotes: [],
|
||||
remotesLoaded: [],
|
||||
locals: [],
|
||||
localsLoaded: [],
|
||||
mainColor: COLORS[colorId],
|
||||
}
|
||||
}
|
||||
|
||||
// TODO move this function to a more global namespace, as it is also in use in MyCalendarRange app
|
||||
export const calendarRangeToFullCalendarEvent = (entity: CalendarRange): EventInputCalendarRange => {
|
||||
return {
|
||||
id: `range_${entity.id}`,
|
||||
title: "(" + entity.user.text + ")",
|
||||
start: entity.startDate.datetime8601,
|
||||
end: entity.endDate.datetime8601,
|
||||
allDay: false,
|
||||
userId: entity.user.id,
|
||||
userLabel: entity.user.label,
|
||||
calendarRangeId: entity.id,
|
||||
locationId: entity.location.id,
|
||||
locationName: entity.location.name,
|
||||
is: 'range',
|
||||
};
|
||||
}
|
||||
|
||||
export const remoteToFullCalendarEvent = (entity: CalendarRemote): EventInput & {id: string} => {
|
||||
return {
|
||||
id: `range_${entity.id}`,
|
||||
title: entity.title,
|
||||
start: entity.startDate.datetime8601,
|
||||
end: entity.endDate.datetime8601,
|
||||
allDay: entity.isAllDay,
|
||||
is: 'remote',
|
||||
};
|
||||
}
|
||||
|
||||
export const localsToFullCalendarEvent = (entity: CalendarLight): EventInput & {id: string; originId: number;} => {
|
||||
return {
|
||||
id: `local_${entity.id}`,
|
||||
title: entity.persons.map(p => p.text).join(', '),
|
||||
originId: entity.id,
|
||||
start: entity.startDate.datetime8601,
|
||||
end: entity.endDate.datetime8601,
|
||||
allDay: false,
|
||||
is: 'local',
|
||||
};
|
||||
}
|
@@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<div class="btn-group" role="group">
|
||||
<button id="btnGroupDrop1" type="button" class="btn btn-misc dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<template v-if="status === Statuses.PENDING">
|
||||
<span class="fa fa-hourglass"></span> {{ $t('Give_an_answer')}}
|
||||
</template>
|
||||
<template v-else-if="status === Statuses.ACCEPTED">
|
||||
<span class="fa fa-check"></span> {{ $t('Accepted')}}
|
||||
</template>
|
||||
<template v-else-if="status === Statuses.DECLINED">
|
||||
<span class="fa fa-times"></span> {{ $t('Declined')}}
|
||||
</template>
|
||||
<template v-else-if="status === Statuses.TENTATIVELY_ACCEPTED">
|
||||
<span class="fa fa-question"></span> {{ $t('Tentative')}}
|
||||
</template>
|
||||
</button>
|
||||
<ul class="dropdown-menu" aria-labelledby="btnGroupDrop1">
|
||||
<li v-if="status !== Statuses.ACCEPTED"><a class="dropdown-item" @click="changeStatus(Statuses.ACCEPTED)"><i class="fa fa-check" aria-hidden="true"></i> {{ $t('Accept') }}</a></li>
|
||||
<li v-if="status !== Statuses.DECLINED"><a class="dropdown-item" @click="changeStatus(Statuses.DECLINED)"><i class="fa fa-times" aria-hidden="true"></i> {{ $t('Decline') }}</a></li>
|
||||
<li v-if="status !== Statuses.TENTATIVELY_ACCEPTED"><a class="dropdown-item" @click="changeStatus(Statuses.TENTATIVELY_ACCEPTED)"><i class="fa fa-question"></i> {{ $t('Tentatively_accept') }}</a></li>
|
||||
<li v-if="status !== Statuses.PENDING"><a class="dropdown-item" @click="changeStatus(Statuses.PENDING)"><i class="fa fa-hourglass-o"></i> {{ $t('Set_pending') }}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent, PropType} from 'vue';
|
||||
|
||||
const ACCEPTED = 'accepted';
|
||||
const DECLINED = 'declined';
|
||||
const PENDING = 'pending';
|
||||
const TENTATIVELY_ACCEPTED = 'tentative';
|
||||
|
||||
const i18n = {
|
||||
messages: {
|
||||
fr: {
|
||||
"Give_an_answer": "Répondre",
|
||||
"Accepted": "Accepté",
|
||||
"Declined": "Refusé",
|
||||
"Tentative": "Accepté provisoirement",
|
||||
"Accept": "Accepter",
|
||||
"Decline": "Refuser",
|
||||
"Tentatively_accept": "Accepter provisoirement",
|
||||
"Set_pending": "Ne pas répondre",
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
name: "Answer",
|
||||
i18n,
|
||||
props: {
|
||||
calendarId: { type: Number, required: true},
|
||||
status: {type: String as PropType<"accepted" | "declined" | "pending" | "tentative">, required: true},
|
||||
},
|
||||
emits: {
|
||||
statusChanged(payload: "accepted" | "declined" | "pending" | "tentative") {
|
||||
return true;
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
Statuses: {
|
||||
ACCEPTED,
|
||||
DECLINED,
|
||||
PENDING,
|
||||
TENTATIVELY_ACCEPTED,
|
||||
},
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
changeStatus: function (newStatus: "accepted" | "declined" | "pending" | "tentative") {
|
||||
console.log('changeStatus', newStatus);
|
||||
const url = `/api/1.0/calendar/calendar/${this.$props.calendarId}/answer/${newStatus}.json`;
|
||||
window.fetch(url, {
|
||||
method: 'POST',
|
||||
}).then((r: Response) => {
|
||||
if (!r.ok) {
|
||||
console.error('could not confirm answer', newStatus);
|
||||
return;
|
||||
}
|
||||
console.log('answer sent', newStatus);
|
||||
this.$emit('statusChanged', newStatus);
|
||||
});
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
|
@@ -1,468 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<h2 class="chill-red">{{ $t('edit_your_calendar_range') }}</h2>
|
||||
|
||||
<div class="form-check">
|
||||
<input type="checkbox" id="myCalendar" class="form-check-input" v-model="showMyCalendarWidget" />
|
||||
<label class="form-check-label" for="myCalendar">{{ $t('show_my_calendar') }}</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" id="weekends" class="form-check-input" @click="toggleWeekends" />
|
||||
<label class="form-check-label" for="weekends">{{ $t('show_weekends') }}</label>
|
||||
</div>
|
||||
|
||||
<FullCalendar ref="fullCalendar" :options="calendarOptions">
|
||||
<template v-slot:eventContent='arg' >
|
||||
<span class='calendarRangeItems'>
|
||||
<b v-if="arg.event.extendedProps.myCalendar" style="text-decoration: underline" >{{ arg.timeText }}</b>
|
||||
<b v-else-if="!arg.event.extendedProps.myCalendar && arg.event.extendedProps.toDelete" style="text-decoration: line-through red" >{{ arg.timeText }}</b>
|
||||
<b v-else >{{ arg.timeText }}</b>
|
||||
<i> {{ arg.event.title }}</i>
|
||||
<a v-if=!arg.event.extendedProps.myCalendar class="fa fa-fw fa-times"
|
||||
@click.prevent="onClickDelete(arg.event)">
|
||||
</a>
|
||||
</span>
|
||||
</template>
|
||||
</FullCalendar>
|
||||
<div>
|
||||
<ul class="record_actions">
|
||||
<li>
|
||||
<button class="btn btn-save" :disabled="!dirty"
|
||||
@click.prevent="onClickSave">
|
||||
{{ $t('action.save')}}
|
||||
</button>
|
||||
<span v-if="flag.loading" class="loading">
|
||||
<i class="fa fa-circle-o-notch fa-spin fa-fw"></i>
|
||||
<span class="sr-only">{{ $t('loading') }}</span>
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<button v-if="disableCopyDayButton" class="btn btn-action" disabled>
|
||||
{{ $t('copy_range_to_next_day')}}
|
||||
</button>
|
||||
<button v-else class="btn btn-action"
|
||||
@click.prevent="copyDay">
|
||||
{{ $t('copy_range_from_day')}} {{this.lastNewDate.toLocaleDateString()}} {{ $t('to_the_next_day')}}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<div v-if="newCalendarRanges.length > 0">
|
||||
<h4>{{ $t('new_range_to_save') }}</h4>
|
||||
<ul>
|
||||
<li v-for="i in newCalendarRanges" :key="i.start">
|
||||
{{ i.start.toLocaleString() }} - {{ i.end.toLocaleString() }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-if="updateCalendarRanges.length > 0">
|
||||
<h4>{{ $t('update_range_to_save') }}</h4>
|
||||
<ul>
|
||||
<li v-for="i in updateCalendarRanges" :key="i.start">
|
||||
{{ i.start.toLocaleString() }} - {{ i.end.toLocaleString() }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-if="deleteCalendarRanges.length > 0">
|
||||
<h4>{{ $t('delete_range_to_save') }}</h4>
|
||||
<ul>
|
||||
<li v-for="i in deleteCalendarRanges" :key="i.start">
|
||||
{{ i.start.toLocaleString() }} - {{ i.end.toLocaleString() }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<teleport to="body">
|
||||
<modal v-if="modal.showModal"
|
||||
:modalDialogClass="modal.modalDialogClass"
|
||||
@close="modal.showModal = false">
|
||||
|
||||
<template v-slot:header>
|
||||
<h2 class="modal-title">{{ this.renderEventDate() }}</h2>
|
||||
</template>
|
||||
|
||||
<template v-slot:body>
|
||||
<p>{{ $t('by')}} {{this.myCalendarClickedEvent.user.username }}</p>
|
||||
<p>{{ $t('main_user_concerned') }} : {{ this.myCalendarClickedEvent.mainUser.username }}</p>
|
||||
<p v-if="myCalendarClickedEvent.comment.length > 0" >{{ this.myCalendarClickedEvent.comment }}</p>
|
||||
</template>
|
||||
|
||||
<template v-slot:footer>
|
||||
<ul class="record_actions">
|
||||
<li>
|
||||
<a
|
||||
class="btn btn-show"
|
||||
:href=myCalendarEventShowLink() >
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
class="btn btn-update"
|
||||
:href=myCalendarEventUpdateLink() >
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
class="btn btn-delete"
|
||||
:href=myCalendarEventDeleteLink() >
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
</modal>
|
||||
</teleport>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import '@fullcalendar/core/vdom'; // solves problem with Vite
|
||||
import frLocale from '@fullcalendar/core/locales/fr';
|
||||
import FullCalendar from '@fullcalendar/vue3';
|
||||
import dayGridPlugin from '@fullcalendar/daygrid';
|
||||
import interactionPlugin from '@fullcalendar/interaction';
|
||||
import timeGridPlugin from '@fullcalendar/timegrid';
|
||||
import Modal from 'ChillMainAssets/vuejs/_components/Modal';
|
||||
import { deleteCalendarRange, fetchCalendar, fetchCalendarRangesByUser, patchCalendarRange, postCalendarRange } from '../_api/api';
|
||||
import { mapState } from 'vuex';
|
||||
|
||||
export default {
|
||||
name: "App",
|
||||
components: {
|
||||
FullCalendar,
|
||||
Modal
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
errorMsg: [],
|
||||
modal: {
|
||||
showModal: false,
|
||||
modalDialogClass: "modal-dialog-scrollable modal-m"
|
||||
},
|
||||
flag: {
|
||||
loading: false
|
||||
},
|
||||
userId: window.userId,
|
||||
showMyCalendar: true,
|
||||
myCalendarClickedEvent: null,
|
||||
calendarEvents: {
|
||||
userCalendar: null,
|
||||
userCalendarRange: null,
|
||||
new: {
|
||||
events: [],
|
||||
color: "#3788d8"
|
||||
}
|
||||
},
|
||||
lastNewDate: null,
|
||||
disableCopyDayButton: true,
|
||||
calendarOptions: {
|
||||
locale: frLocale,
|
||||
plugins: [ dayGridPlugin, interactionPlugin, timeGridPlugin ],
|
||||
initialView: 'timeGridWeek',
|
||||
initialDate: window.startDate !== undefined ? window.startDate : new Date(),
|
||||
eventSource: [],
|
||||
selectable: true,
|
||||
select: this.onDateSelect,
|
||||
eventChange: this.onEventChange,
|
||||
eventDrop: this.onEventDropOrResize,
|
||||
eventResize: this.onEventDropOrResize,
|
||||
eventClick: this.onEventClick,
|
||||
selectMirror: false,
|
||||
editable: true,
|
||||
weekends: false,
|
||||
headerToolbar: {
|
||||
left: 'prev,next today',
|
||||
center: 'title',
|
||||
right: 'dayGridMonth,timeGridWeek,timeGridDay'
|
||||
},
|
||||
},
|
||||
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
newCalendarRanges: state => state.newCalendarRanges,
|
||||
updateCalendarRanges: state => state.updateCalendarRanges,
|
||||
deleteCalendarRanges: state => state.deleteCalendarRanges,
|
||||
dirty: state => state.newCalendarRanges.length > 0 || state.updateCalendarRanges.length > 0 || state.deleteCalendarRanges.length > 0
|
||||
}),
|
||||
showMyCalendarWidget: {
|
||||
set(value) {
|
||||
this.toggleMyCalendar(value);
|
||||
this.updateEventsSource();
|
||||
},
|
||||
get() {
|
||||
return this.showMyCalendar;
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
init() {
|
||||
this.fetchData();
|
||||
},
|
||||
openModal() {
|
||||
this.modal.showModal = true;
|
||||
},
|
||||
myCalendarEventShowLink() {
|
||||
return `/fr/calendar/calendar/${this.myCalendarClickedEvent.id}/show?user_id=${ this.userId }`
|
||||
},
|
||||
myCalendarEventUpdateLink() {
|
||||
return `/fr/calendar/calendar/${this.myCalendarClickedEvent.id}/edit?user_id=${ this.userId }`
|
||||
},
|
||||
myCalendarEventDeleteLink() {
|
||||
return `/fr/calendar/calendar/${this.myCalendarClickedEvent.id}/delete?user_id=${ this.userId }`
|
||||
},
|
||||
resetCalendar() {
|
||||
this.fetchData();
|
||||
this.calendarEvents.new = {
|
||||
events: [],
|
||||
color: "#3788d8"
|
||||
};
|
||||
this.updateEventsSource();
|
||||
},
|
||||
fetchData() {
|
||||
this.flag.loading = true;
|
||||
fetchCalendarRangesByUser(this.userId).then(calendarRanges => new Promise((resolve, reject) => {
|
||||
let events = calendarRanges.results.map(i =>
|
||||
({
|
||||
start: i.startDate.datetime,
|
||||
end: i.endDate.datetime,
|
||||
calendarRangeId: i.id,
|
||||
toDelete: false
|
||||
})
|
||||
);
|
||||
let calendarRangeEvents = {
|
||||
events: events,
|
||||
borderColor: "#3788d8",
|
||||
backgroundColor: '#ffffff',
|
||||
textColor: '#444444',
|
||||
};
|
||||
this.calendarEvents.userCalendarRange = calendarRangeEvents;
|
||||
|
||||
fetchCalendar(this.userId).then(calendar => new Promise((resolve, reject) => {
|
||||
let events = calendar.results.map(i =>
|
||||
({
|
||||
myCalendar: true,
|
||||
calendarId: i.id,
|
||||
start: i.startDate.datetime,
|
||||
end: i.endDate.datetime,
|
||||
user: i.user,
|
||||
mainUser: i.mainUser,
|
||||
persons: i.persons,
|
||||
professionals: i.professionals,
|
||||
comment: i.comment
|
||||
})
|
||||
);
|
||||
let calendarEventsCurrentUser = {
|
||||
events: events,
|
||||
color: 'darkblue',
|
||||
id: 1000,
|
||||
editable: false
|
||||
};
|
||||
this.calendarEvents.userCalendar = calendarEventsCurrentUser;
|
||||
this.updateEventsSource();
|
||||
this.flag.loading = false;
|
||||
resolve();
|
||||
}));
|
||||
|
||||
resolve();
|
||||
}));
|
||||
},
|
||||
updateEventsSource() {
|
||||
this.calendarOptions.eventSources = [];
|
||||
this.calendarOptions.eventSources.push(this.calendarEvents.new);
|
||||
this.calendarOptions.eventSources.push(this.calendarEvents.userCalendarRange);
|
||||
if (this.showMyCalendar) {
|
||||
this.calendarOptions.eventSources.push(this.calendarEvents.userCalendar);
|
||||
}
|
||||
console.log(this.calendarOptions.eventSources);
|
||||
},
|
||||
toggleMyCalendar(value) {
|
||||
this.showMyCalendar = value;
|
||||
},
|
||||
toggleWeekends: function() {
|
||||
this.calendarOptions.weekends = !this.calendarOptions.weekends;
|
||||
},
|
||||
onDateSelect(payload) {
|
||||
let events = this.calendarEvents.new.events;
|
||||
events.push({
|
||||
start: payload.startStr,
|
||||
end: payload.endStr
|
||||
});
|
||||
this.calendarEvents.new = {
|
||||
events: events,
|
||||
borderColor: "#3788d8",
|
||||
backgroundColor: '#fffadf ',
|
||||
textColor: '#444444',
|
||||
};
|
||||
this.disableCopyDayButton = false;
|
||||
this.lastNewDate = new Date(payload.startStr);
|
||||
this.updateEventsSource();
|
||||
this.$store.dispatch('createRange', payload);
|
||||
},
|
||||
onEventChange(payload) {
|
||||
},
|
||||
onEventDropOrResize(payload) {
|
||||
payload.event.setProp('borderColor', '#3788d8');
|
||||
payload.event.setProp('backgroundColor', '#fffadf');
|
||||
payload.event.setProp('textColor', '#444444');
|
||||
this.$store.dispatch('updateRange', payload);
|
||||
},
|
||||
onEventClick(payload) {
|
||||
if (payload.event.extendedProps.myCalendar) {
|
||||
this.myCalendarClickedEvent = {
|
||||
id: payload.event.extendedProps.calendarId,
|
||||
start: payload.event.start,
|
||||
end: payload.event.end,
|
||||
user: payload.event.extendedProps.user,
|
||||
mainUser: payload.event.extendedProps.mainUser,
|
||||
persons: payload.event.extendedProps.persons,
|
||||
professionals: payload.event.extendedProps.professionals,
|
||||
comment: payload.event.extendedProps.comment
|
||||
};
|
||||
console.log(this.myCalendarClickedEvent)
|
||||
this.openModal();
|
||||
}
|
||||
},
|
||||
onClickSave(payload) {
|
||||
this.flag.loading = true;
|
||||
if (this.$store.state.newCalendarRanges.length > 0){
|
||||
Promise.all(this.$store.state.newCalendarRanges.map(cr => {
|
||||
postCalendarRange({
|
||||
user: {
|
||||
type: 'user',
|
||||
id: window.userId,
|
||||
},
|
||||
startDate: {
|
||||
datetime: `${cr.start.toISOString().split('.')[0]}+0000`, //should be like "2021-08-20T15:00:00+0200",
|
||||
},
|
||||
endDate: {
|
||||
datetime: `${cr.end.toISOString().split('.')[0]}+0000`, // TODO check if OK with time zone
|
||||
},
|
||||
})
|
||||
})
|
||||
).then((_r) => this.resetCalendar());
|
||||
|
||||
this.$store.dispatch('clearNewCalendarRanges', payload);
|
||||
}
|
||||
if (this.$store.state.updateCalendarRanges.length > 0){
|
||||
Promise.all(this.$store.state.updateCalendarRanges.map(cr => {
|
||||
patchCalendarRange(cr.id,
|
||||
{
|
||||
startDate: {
|
||||
datetime: `${cr.start.toISOString().split('.')[0]}+0000`, //should be like "2021-08-20T15:00:00+0200",
|
||||
},
|
||||
endDate: {
|
||||
datetime: `${cr.end.toISOString().split('.')[0]}+0000`, // TODO check if OK with time zone
|
||||
},
|
||||
})
|
||||
})
|
||||
).then((_r) => this.resetCalendar());
|
||||
this.$store.dispatch('clearUpdateCalendarRanges', payload);
|
||||
}
|
||||
if (this.$store.state.deleteCalendarRanges.length > 0){
|
||||
Promise.all(this.$store.state.deleteCalendarRanges.map(cr => {
|
||||
deleteCalendarRange(cr.id)
|
||||
})
|
||||
).then((_r) => this.resetCalendar());
|
||||
this.$store.dispatch('clearDeleteCalendarRanges', payload);
|
||||
}
|
||||
|
||||
},
|
||||
onClickDelete(payload) {
|
||||
if (payload.extendedProps.hasOwnProperty("calendarRangeId")) {
|
||||
if (payload.extendedProps.toDelete) {
|
||||
payload.setExtendedProp('toDelete', false)
|
||||
payload.setProp('borderColor', '#79bafc');
|
||||
this.$store.dispatch('removeFromDeleteRange', payload);
|
||||
} else {
|
||||
payload.setExtendedProp('toDelete', true)
|
||||
payload.setProp('borderColor', '#dddddd');
|
||||
this.$store.dispatch('deleteRange', payload);
|
||||
}
|
||||
} else {
|
||||
let newEvents = this.calendarEvents.new.events;
|
||||
let filterEvents = newEvents.filter((e) =>
|
||||
e.start !== payload.startStr && e.end !== payload.endStr
|
||||
);
|
||||
this.calendarEvents.new = {
|
||||
events: filterEvents,
|
||||
color: "#3788d8"
|
||||
};
|
||||
|
||||
this.$store.dispatch('removeNewCalendarRanges', payload);
|
||||
|
||||
this.updateEventsSource();
|
||||
}
|
||||
|
||||
},
|
||||
isSameDay(date1, date2) {
|
||||
return date1.getFullYear() === date2.getFullYear() &&
|
||||
date1.getMonth() === date2.getMonth() &&
|
||||
date1.getDate() === date2.getDate();
|
||||
},
|
||||
isFriday(date) {
|
||||
return date.getDay() === 5
|
||||
},
|
||||
copyDay(_payload) {
|
||||
console.log(this.calendarEvents.new);
|
||||
if (this.calendarEvents.new.events.length > 0) {
|
||||
// Create the copied events
|
||||
let increment = !this.calendarOptions.weekends && this.isFriday(this.lastNewDate) ? 24*60*60*1000*3 : 24*60*60*1000;
|
||||
let events = this.calendarEvents.new.events.filter(
|
||||
i => this.isSameDay(new Date(i.start), this.lastNewDate)).map(
|
||||
i => {
|
||||
let startDate = new Date(new Date(i.start).getTime() + increment);
|
||||
let endDate = new Date(new Date(i.end).getTime() + increment);
|
||||
return ({
|
||||
start: startDate.toISOString(),
|
||||
end: endDate.toISOString()
|
||||
})
|
||||
}
|
||||
);
|
||||
let copiedEvents = {
|
||||
events: events,
|
||||
color: "#3788d8"
|
||||
};
|
||||
console.log(copiedEvents);
|
||||
|
||||
// Add to the calendar
|
||||
let newEvents = this.calendarEvents.new.events;
|
||||
newEvents.push(...copiedEvents.events);
|
||||
this.calendarEvents.new = {
|
||||
events: newEvents,
|
||||
color: "#3788d8"
|
||||
};
|
||||
this.updateEventsSource();
|
||||
|
||||
// Set the last new date
|
||||
this.lastNewDate = new Date(copiedEvents.events[copiedEvents.events.length - 1].start);
|
||||
|
||||
// Dispatch in store for saving
|
||||
for (let i = 0; i < copiedEvents.events.length; i++) {
|
||||
let eventObj = {
|
||||
start: new Date(copiedEvents.events[i].start),
|
||||
end: new Date(copiedEvents.events[i].end)
|
||||
}
|
||||
this.$store.dispatch('createRange', eventObj);
|
||||
}
|
||||
|
||||
} else {
|
||||
console.log('no new events to copy-paste!')
|
||||
}
|
||||
},
|
||||
renderEventDate() {
|
||||
let start = this.myCalendarClickedEvent.start;
|
||||
let end = this.myCalendarClickedEvent.end;
|
||||
return start.getDate() === end.getDate() ?
|
||||
`${start.toLocaleDateString()}, ${start.toLocaleTimeString()} - ${end.toLocaleTimeString()}` :
|
||||
`${start.toLocaleString()} - ${end.toLocaleString()}`;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.init();
|
||||
}
|
||||
}
|
||||
</script>
|
@@ -0,0 +1,304 @@
|
||||
<template>
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<label class="form-label">Lieu des plages de disponibilités créées</label>
|
||||
<vue-multiselect
|
||||
v-model="pickedLocation"
|
||||
:options="locations"
|
||||
:label="'name'"
|
||||
:track-by="'id'"
|
||||
:selectLabel="'Presser \'Entrée\' pour choisir'"
|
||||
:selectedLabel="'Choisir'"
|
||||
:deselectLabel="'Presser \'Entrée\' pour enlever'"
|
||||
:placeholder="'Choisir'"
|
||||
></vue-multiselect>
|
||||
</div>
|
||||
</div>
|
||||
<div class="display-options row justify-content-between" style="margin-top: 1rem;">
|
||||
<div class="col-sm-9 col-xs-12">
|
||||
<div class="input-group mb-3">
|
||||
<label class="input-group-text" for="slotDuration">Durée des créneaux</label>
|
||||
<select v-model="slotDuration" id="slotDuration" class="form-select">
|
||||
<option value="00:05:00">5 minutes</option>
|
||||
<option value="00:10:00">10 minutes</option>
|
||||
<option value="00:15:00">15 minutes</option>
|
||||
<option value="00:30:00">30 minutes</option>
|
||||
</select>
|
||||
<label class="input-group-text" for="slotMinTime">De</label>
|
||||
<select v-model="slotMinTime" id="slotMinTime" class="form-select">
|
||||
<option value="00:00:00">0h</option>
|
||||
<option value="01:00:00">1h</option>
|
||||
<option value="02:00:00">2h</option>
|
||||
<option value="03:00:00">3h</option>
|
||||
<option value="04:00:00">4h</option>
|
||||
<option value="05:00:00">5h</option>
|
||||
<option value="06:00:00">6h</option>
|
||||
<option value="07:00:00">7h</option>
|
||||
<option value="08:00:00">8h</option>
|
||||
<option value="09:00:00">9h</option>
|
||||
<option value="10:00:00">10h</option>
|
||||
<option value="11:00:00">11h</option>
|
||||
<option value="12:00:00">12h</option>
|
||||
</select>
|
||||
<label class="input-group-text" for="slotMaxTime">À</label>
|
||||
<select v-model="slotMaxTime" id="slotMaxTime" class="form-select">
|
||||
<option value="12:00:00">12h</option>
|
||||
<option value="13:00:00">13h</option>
|
||||
<option value="14:00:00">14h</option>
|
||||
<option value="15:00:00">15h</option>
|
||||
<option value="16:00:00">16h</option>
|
||||
<option value="17:00:00">17h</option>
|
||||
<option value="18:00:00">18h</option>
|
||||
<option value="19:00:00">19h</option>
|
||||
<option value="20:00:00">20h</option>
|
||||
<option value="21:00:00">21h</option>
|
||||
<option value="22:00:00">22h</option>
|
||||
<option value="23:00:00">23h</option>
|
||||
<option value="23:59:59">24h</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-3 col-xs-12">
|
||||
<div class="float-end">
|
||||
<div class="form-check input-group">
|
||||
<span class="input-group-text">
|
||||
<input id="showHideWE" class="mt-0" type="checkbox" v-model="showWeekends">
|
||||
</span>
|
||||
<label for="showHideWE" class="form-check-label input-group-text">Week-ends</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<FullCalendar :options="calendarOptions" ref="calendarRef">
|
||||
<template v-slot:eventContent="arg: EventApi">
|
||||
<span :class="eventClasses(arg.event)">
|
||||
<b v-if="arg.event.extendedProps.is === 'remote'">{{ arg.event.title}}</b>
|
||||
<b v-else-if="arg.event.extendedProps.is === 'range'">{{ arg.timeText }} - {{ arg.event.extendedProps.locationName }}</b>
|
||||
<b v-else-if="arg.event.extendedProps.is === 'local'">{{ arg.event.title}}</b>
|
||||
<b v-else >no 'is'</b>
|
||||
<a v-if="arg.event.extendedProps.is === 'range'" class="fa fa-fw fa-times delete"
|
||||
@click.prevent="onClickDelete(arg.event)">
|
||||
</a>
|
||||
</span>
|
||||
</template>
|
||||
</FullCalendar>
|
||||
|
||||
<div id="copy-widget">
|
||||
<div class="container">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-sm-4 col-xs-12">
|
||||
<h6 class="chill-red">{{ $t('copy_range_from_to') }}</h6>
|
||||
</div>
|
||||
<div class="col-sm-3 col-xs-12">
|
||||
<input class="form-control" type="date" v-model="copyFrom" />
|
||||
</div>
|
||||
<div class="col-sm-1 col-xs-12" style="text-align: center; font-size: x-large;">
|
||||
<i class="fa fa-angle-double-right"></i>
|
||||
</div>
|
||||
<div class="col-sm-3 col-xs-12" >
|
||||
<input class="form-control" type="date" v-model="copyTo" />
|
||||
</div>
|
||||
<div class="col-sm-1">
|
||||
<button class="btn btn-action" @click="copyDay">
|
||||
{{ $t('copy_range') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- not directly seen, but include in a modal -->
|
||||
<edit-location ref="editLocation"></edit-location>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
import type {
|
||||
CalendarOptions,
|
||||
DatesSetArg,
|
||||
EventInput,
|
||||
EventInstance
|
||||
} from '@fullcalendar/vue3';
|
||||
import {reactive, computed, ref} from "vue";
|
||||
import {useStore} from "vuex";
|
||||
import {key} from './store';
|
||||
import '@fullcalendar/core/vdom'; // solves problem with Vite
|
||||
import FullCalendar from '@fullcalendar/vue3';
|
||||
import frLocale from '@fullcalendar/core/locales/fr';
|
||||
import interactionPlugin, {DropArg, EventResizeDoneArg} from "@fullcalendar/interaction";
|
||||
import timeGridPlugin from "@fullcalendar/timegrid";
|
||||
import {EventApi, DateSelectArg, EventDropArg, EventClickArg} from "@fullcalendar/core";
|
||||
import {ISOToDate} from "../../../../../ChillMainBundle/Resources/public/chill/js/date";
|
||||
import VueMultiselect from "vue-multiselect";
|
||||
import {Location} from "../../../../../ChillMainBundle/Resources/public/types";
|
||||
import EditLocation from "./Components/EditLocation.vue";
|
||||
import {useI18n} from "vue-i18n";
|
||||
|
||||
const store = useStore(key);
|
||||
|
||||
const {t} = useI18n();
|
||||
|
||||
const showWeekends = ref(false);
|
||||
const slotDuration = ref('00:05:00');
|
||||
const slotMinTime = ref('09:00:00');
|
||||
const slotMaxTime = ref('18:00:00');
|
||||
const copyFrom = ref<string | null>(null);
|
||||
const copyTo = ref<string | null>(null);
|
||||
const editLocation = ref<InstanceType<typeof EditLocation> | null>(null)
|
||||
|
||||
const baseOptions = ref<CalendarOptions>({
|
||||
locale: frLocale,
|
||||
plugins: [interactionPlugin, timeGridPlugin],
|
||||
initialView: 'timeGridWeek',
|
||||
initialDate: new Date(),
|
||||
scrollTimeReset: false,
|
||||
selectable: true,
|
||||
// when the dates are changes in the fullcalendar view OR when new events are added
|
||||
datesSet: onDatesSet,
|
||||
// when a date is selected
|
||||
select: onDateSelect,
|
||||
// when a event is resized
|
||||
eventResize: onEventDropOrResize,
|
||||
// when an event is moved
|
||||
eventDrop: onEventDropOrResize,
|
||||
// when an event si clicked
|
||||
eventClick: onEventClick,
|
||||
selectMirror: false,
|
||||
editable: true,
|
||||
headerToolbar: {
|
||||
left: 'prev,next today',
|
||||
center: 'title',
|
||||
right: 'timeGridWeek,timeGridDay'
|
||||
},
|
||||
});
|
||||
|
||||
const ranges = computed<EventInput[]>(() => {
|
||||
return store.state.calendarRanges.ranges;
|
||||
});
|
||||
|
||||
const locations = computed<Location[]>(() => {
|
||||
return store.state.locations.locations;
|
||||
});
|
||||
|
||||
const pickedLocation = computed<Location | null>({
|
||||
get(): Location | null {
|
||||
return store.state.locations.locationPicked || store.state.locations.currentLocation;
|
||||
},
|
||||
set(newLocation: Location | null): void {
|
||||
store.commit('locations/setLocationPicked', newLocation, {root: true});
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* return the show classes for the event
|
||||
* @param arg
|
||||
*/
|
||||
const eventClasses = function(arg: EventApi): object {
|
||||
return {'calendarRangeItems': true};
|
||||
}
|
||||
|
||||
/*
|
||||
// currently, all events are stored into calendarRanges, due to reactivity bug
|
||||
const remotes = computed<EventInput[]>(() => {
|
||||
return store.state.calendarRemotes.remotes;
|
||||
});
|
||||
|
||||
const sources = computed<EventSourceInput[]>(() => {
|
||||
const sources = [];
|
||||
|
||||
const rangeSource: EventSourceInput = {
|
||||
id: 'ranges',
|
||||
events: ranges.value,
|
||||
};
|
||||
|
||||
sources.push(rangeSource);
|
||||
|
||||
return sources;
|
||||
});
|
||||
*/
|
||||
|
||||
const calendarOptions = computed((): CalendarOptions => {
|
||||
return {
|
||||
...baseOptions.value,
|
||||
weekends: showWeekends.value,
|
||||
slotDuration: slotDuration.value,
|
||||
events: ranges.value,
|
||||
slotMinTime: slotMinTime.value,
|
||||
slotMaxTime: slotMaxTime.value,
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* launched when the calendar range date change
|
||||
*/
|
||||
function onDatesSet(event: DatesSetArg): void {
|
||||
store.dispatch('fullCalendar/setCurrentDatesView', {start: event.start, end: event.end});
|
||||
}
|
||||
|
||||
function onDateSelect(event: DateSelectArg): void {
|
||||
|
||||
if (null === pickedLocation.value) {
|
||||
window.alert("Indiquez une localisation avant de créer une période de disponibilité.");
|
||||
return;
|
||||
}
|
||||
|
||||
store.dispatch('calendarRanges/createRange', {start: event.start, end: event.end, location: pickedLocation.value});
|
||||
}
|
||||
|
||||
/**
|
||||
* When a calendar range is deleted
|
||||
*/
|
||||
function onClickDelete(event: EventApi): void {
|
||||
console.log('onClickDelete', event);
|
||||
|
||||
if (event.extendedProps.is !== 'range') {
|
||||
return;
|
||||
}
|
||||
|
||||
store.dispatch('calendarRanges/deleteRange', event.extendedProps.calendarRangeId);
|
||||
}
|
||||
|
||||
function onEventDropOrResize(payload: EventDropArg | EventResizeDoneArg) {
|
||||
if (payload.event.extendedProps.is !== 'range') {
|
||||
return;
|
||||
}
|
||||
const changedEvent = payload.event;
|
||||
|
||||
store.dispatch('calendarRanges/patchRangeTime', {
|
||||
calendarRangeId: payload.event.extendedProps.calendarRangeId,
|
||||
start: payload.event.start,
|
||||
end: payload.event.end,
|
||||
});
|
||||
};
|
||||
|
||||
function onEventClick(payload: EventClickArg): void {
|
||||
// @ts-ignore TS does not recognize the target. But it does exists.
|
||||
if (payload.jsEvent.target.classList.contains('delete')) {
|
||||
return;
|
||||
}
|
||||
if (payload.event.extendedProps.is !== 'range') {
|
||||
return;
|
||||
}
|
||||
|
||||
editLocation.value?.startEdit(payload.event);
|
||||
}
|
||||
|
||||
function copyDay() {
|
||||
if (null === copyFrom.value || null === copyTo.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
store.dispatch('calendarRanges/copyFromDayToAnotherDay', {from: ISOToDate(copyFrom.value), to: ISOToDate(copyTo.value)})
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
#copy-widget {
|
||||
position: sticky;
|
||||
bottom: 0px;
|
||||
background-color: white;
|
||||
z-index: 9999999999;
|
||||
padding: 0.25rem 0 0.25rem;
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,88 @@
|
||||
<template>
|
||||
<component :is="Teleport" to="body">
|
||||
<modal v-if="showModal"
|
||||
@close="closeModal">
|
||||
|
||||
<template v-slot:header>
|
||||
<h3>{{ 'Modifier le lieu' }}</h3>
|
||||
</template>
|
||||
|
||||
<template v-slot:body>
|
||||
<div>
|
||||
|
||||
</div>
|
||||
<label>Localisation</label>
|
||||
<vue-multiselect v-model="location" :options="locations" :label="'name'" :track-by="'id'"></vue-multiselect>
|
||||
</template>
|
||||
|
||||
<template v-slot:footer>
|
||||
<button class="btn btn-save" @click="saveAndClose">{{ 'Enregistrer' }}</button>
|
||||
</template>
|
||||
|
||||
</modal>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Modal from "../../../../../../ChillMainBundle/Resources/public/vuejs/_components/Modal.vue";
|
||||
import {computed, ref} from "vue";
|
||||
import {EventApi} from "@fullcalendar/vue3";
|
||||
import {useStore} from "vuex";
|
||||
import {key} from "../store";
|
||||
import {Location} from "../../../../../../ChillMainBundle/Resources/public/types";
|
||||
import VueMultiselect from "vue-multiselect";
|
||||
//import type {Teleport} from "vue";
|
||||
|
||||
// see https://github.com/vuejs/core/issues/2855
|
||||
import {
|
||||
Teleport as teleport_,
|
||||
TeleportProps,
|
||||
VNodeProps
|
||||
} from 'vue'
|
||||
|
||||
const Teleport = teleport_ as {
|
||||
new (): {
|
||||
$props: VNodeProps & TeleportProps
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const store = useStore(key);
|
||||
|
||||
const calendarRangeId = ref<number | null>(null);
|
||||
const location = ref<Location | null>(null);
|
||||
const showModal = ref(false);
|
||||
//const tele = ref<InstanceType<typeof Teleport> | null>(null);
|
||||
|
||||
const locations = computed<Location[]>(() => {
|
||||
return store.state.locations.locations;
|
||||
});
|
||||
|
||||
const startEdit = function(event: EventApi): void {
|
||||
console.log('startEditing', event);
|
||||
calendarRangeId.value = event.extendedProps.calendarRangeId;
|
||||
location.value = store.getters['locations/getLocationById'](event.extendedProps.locationId) || null;
|
||||
|
||||
console.log('new location value', location.value);
|
||||
console.log('calendar range id', calendarRangeId.value);
|
||||
showModal.value = true;
|
||||
}
|
||||
|
||||
const saveAndClose = function(e: Event): void {
|
||||
console.log('saveEditAndClose', e);
|
||||
|
||||
store.dispatch('calendarRanges/patchRangeLocation', {location: location.value, calendarRangeId: calendarRangeId.value})
|
||||
.then(_ => {showModal.value = false;})
|
||||
}
|
||||
|
||||
const closeModal = function(_: any): void {
|
||||
showModal.value = false;
|
||||
}
|
||||
|
||||
defineExpose({startEdit});
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
@@ -3,6 +3,8 @@ const appMessages = {
|
||||
edit_your_calendar_range: "Planifiez vos plages de disponibilités",
|
||||
show_my_calendar: "Afficher mon calendrier",
|
||||
show_weekends: "Afficher les week-ends",
|
||||
copy_range: "Copier",
|
||||
copy_range_from_to: "Copier les plages d'un jour à l'autre",
|
||||
copy_range_to_next_day: "Copier les plages du jour au jour suivant",
|
||||
copy_range_from_day: "Copier les plages du ",
|
||||
to_the_next_day: " au jour suivant",
|
||||
@@ -12,7 +14,13 @@ const appMessages = {
|
||||
update_range_to_save: "Plages à modifier",
|
||||
delete_range_to_save: "Plages à supprimer",
|
||||
by: "Par",
|
||||
main_user_concerned: "Utilisateur concerné"
|
||||
main_user_concerned: "Utilisateur concerné",
|
||||
dateFrom: "De",
|
||||
dateTo: "à",
|
||||
day: "Jour",
|
||||
week: "Semaine",
|
||||
month: "Mois",
|
||||
today: "Aujourd'hui",
|
||||
}
|
||||
}
|
||||
|
@@ -1,16 +0,0 @@
|
||||
import { createApp } from 'vue';
|
||||
import { _createI18n } from 'ChillMainAssets/vuejs/_js/i18n'
|
||||
import { appMessages } from './i18n'
|
||||
import store from './store'
|
||||
|
||||
import App from './App.vue';
|
||||
|
||||
const i18n = _createI18n(appMessages);
|
||||
|
||||
const app = createApp({
|
||||
template: `<app></app>`,
|
||||
})
|
||||
.use(store)
|
||||
.use(i18n)
|
||||
.component('app', App)
|
||||
.mount('#myCalendar');
|
@@ -0,0 +1,19 @@
|
||||
import { createApp } from 'vue';
|
||||
import { _createI18n } from '../../../../../ChillMainBundle/Resources/public/vuejs/_js/i18n'
|
||||
import { appMessages } from './i18n'
|
||||
import futureStore, {key} from './store/index'
|
||||
|
||||
import App2 from './App2.vue';
|
||||
import {useI18n} from "vue-i18n";
|
||||
|
||||
futureStore().then((store) => {
|
||||
const i18n = _createI18n(appMessages, true);
|
||||
|
||||
const app = createApp({
|
||||
template: `<app></app>`,
|
||||
})
|
||||
.use(store, key)
|
||||
.use(i18n)
|
||||
.component('app', App2)
|
||||
.mount('#myCalendar');
|
||||
});
|
@@ -1,89 +0,0 @@
|
||||
import 'es6-promise/auto';
|
||||
import { createStore } from 'vuex';
|
||||
import { postCalendarRange, patchCalendarRange, deleteCalendarRange } from '../_api/api';
|
||||
|
||||
const debug = process.env.NODE_ENV !== 'production';
|
||||
|
||||
const store = createStore({
|
||||
strict: debug,
|
||||
state: {
|
||||
newCalendarRanges: [],
|
||||
updateCalendarRanges: [],
|
||||
deleteCalendarRanges: []
|
||||
},
|
||||
mutations: {
|
||||
updateRange(state, payload) {
|
||||
state.updateCalendarRanges.push({
|
||||
id: payload.event.extendedProps.calendarRangeId,
|
||||
start: payload.event.start,
|
||||
end: payload.event.end
|
||||
});
|
||||
},
|
||||
addRange(state, payload) {
|
||||
state.newCalendarRanges.push({
|
||||
start: payload.start,
|
||||
end: payload.end
|
||||
});
|
||||
},
|
||||
deleteRange(state, payload) {
|
||||
state.deleteCalendarRanges.push({
|
||||
id: payload.extendedProps.calendarRangeId,
|
||||
start: payload.start,
|
||||
end: payload.end
|
||||
});
|
||||
},
|
||||
clearNewCalendarRanges(state) {
|
||||
state.newCalendarRanges = [];
|
||||
},
|
||||
clearUpdateCalendarRanges(state) {
|
||||
state.updateCalendarRanges = [];
|
||||
},
|
||||
clearDeleteCalendarRanges(state) {
|
||||
state.deleteCalendarRanges = [];
|
||||
},
|
||||
removeNewCalendarRanges(state, payload) {
|
||||
let filteredCollection = state.newCalendarRanges.filter(
|
||||
(e) => e.start.toString() !== payload.start.toString() && e.end.toString() !== payload.end.toString()
|
||||
)
|
||||
state.newCalendarRanges = filteredCollection;
|
||||
},
|
||||
removeFromDeleteRange(state, payload) {
|
||||
let filteredCollection = state.deleteCalendarRanges.filter(
|
||||
(e) => e.start.toString() !== payload.start.toString() && e.end.toString() !== payload.end.toString()
|
||||
)
|
||||
state.deleteCalendarRanges = filteredCollection;
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
createRange({ commit }, payload) {
|
||||
console.log('### action createRange', payload);
|
||||
commit('addRange', payload);
|
||||
},
|
||||
updateRange({ commit }, payload) {
|
||||
console.log('### action updateRange', payload);
|
||||
commit('updateRange', payload);
|
||||
},
|
||||
deleteRange({ commit }, payload) {
|
||||
console.log('### action deleteRange', payload);
|
||||
commit('deleteRange', payload);
|
||||
},
|
||||
clearNewCalendarRanges({ commit }, payload) {
|
||||
commit('clearNewCalendarRanges', payload);
|
||||
},
|
||||
clearUpdateCalendarRanges({ commit }, payload) {
|
||||
commit('clearUpdateCalendarRanges', payload);
|
||||
},
|
||||
clearDeleteCalendarRanges({ commit }, payload) {
|
||||
commit('clearDeleteCalendarRanges', payload);
|
||||
},
|
||||
removeNewCalendarRanges({ commit }, payload) {
|
||||
commit('removeNewCalendarRanges', payload);
|
||||
},
|
||||
removeFromDeleteRange({ commit }, payload) {
|
||||
commit('removeFromDeleteRange', payload);
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
export default store;
|
@@ -0,0 +1,50 @@
|
||||
import 'es6-promise/auto';
|
||||
import {Store, createStore} from 'vuex';
|
||||
import {InjectionKey} from "vue";
|
||||
import me, {MeState} from './modules/me';
|
||||
import fullCalendar, {FullCalendarState} from './modules/fullcalendar';
|
||||
import calendarRanges, {CalendarRangesState} from './modules/calendarRanges';
|
||||
import calendarRemotes, {CalendarRemotesState} from './modules/calendarRemotes';
|
||||
import {whoami} from "../../../../../../ChillMainBundle/Resources/public/lib/api/user";
|
||||
import {User} from '../../../../../../ChillMainBundle/Resources/public/types';
|
||||
import locations, {LocationState} from "./modules/location";
|
||||
import calendarLocals, {CalendarLocalsState} from "./modules/calendarLocals";
|
||||
|
||||
const debug = process.env.NODE_ENV !== 'production';
|
||||
|
||||
export interface State {
|
||||
calendarRanges: CalendarRangesState,
|
||||
calendarRemotes: CalendarRemotesState,
|
||||
calendarLocals: CalendarLocalsState,
|
||||
fullCalendar: FullCalendarState,
|
||||
me: MeState,
|
||||
locations: LocationState
|
||||
}
|
||||
|
||||
export const key: InjectionKey<Store<State>> = Symbol();
|
||||
|
||||
const futureStore = function(): Promise<Store<State>> {
|
||||
return whoami().then((user: User) => {
|
||||
const store = createStore<State>({
|
||||
strict: debug,
|
||||
modules: {
|
||||
me,
|
||||
fullCalendar,
|
||||
calendarRanges,
|
||||
calendarRemotes,
|
||||
calendarLocals,
|
||||
locations,
|
||||
},
|
||||
mutations: {}
|
||||
});
|
||||
|
||||
store.commit('me/setWhoAmi', user, {root: true});
|
||||
store.dispatch('locations/getLocations', null, {root: true}).then(_ => {
|
||||
return store.dispatch('locations/getCurrentLocation', null, {root: true});
|
||||
});
|
||||
|
||||
return Promise.resolve(store);
|
||||
});
|
||||
}
|
||||
|
||||
export default futureStore;
|
@@ -0,0 +1,95 @@
|
||||
import {State} from './../index';
|
||||
import {ActionContext, Module} from 'vuex';
|
||||
import {CalendarLight} from '../../../../types';
|
||||
import {fetchCalendarLocalForUser} from '../../../Calendar/api';
|
||||
import {EventInput} from '@fullcalendar/vue3';
|
||||
import {localsToFullCalendarEvent} from "../../../Calendar/store/utils";
|
||||
import {TransportExceptionInterface} from "../../../../../../../ChillMainBundle/Resources/public/lib/api/apiMethods";
|
||||
import {COLORS} from "../../../Calendar/const";
|
||||
|
||||
export interface CalendarLocalsState {
|
||||
locals: EventInput[],
|
||||
localsLoaded: {start: number, end: number}[],
|
||||
localsIndex: Set<string>,
|
||||
key: number
|
||||
}
|
||||
|
||||
type Context = ActionContext<CalendarLocalsState, State>;
|
||||
|
||||
export default <Module<CalendarLocalsState, State>> {
|
||||
namespaced: true,
|
||||
state: (): CalendarLocalsState => ({
|
||||
locals: [],
|
||||
localsLoaded: [],
|
||||
localsIndex: new Set<string>(),
|
||||
key: 0
|
||||
}),
|
||||
getters: {
|
||||
isLocalsLoaded: (state: CalendarLocalsState) => ({start, end}: {start: Date, end: Date}): boolean => {
|
||||
for (let range of state.localsLoaded) {
|
||||
if (start.getTime() === range.start && end.getTime() === range.end) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
},
|
||||
mutations: {
|
||||
addLocals(state: CalendarLocalsState, ranges: CalendarLight[]) {
|
||||
console.log('addLocals', ranges);
|
||||
|
||||
const toAdd = ranges
|
||||
.map(cr => localsToFullCalendarEvent(cr))
|
||||
.filter(r => !state.localsIndex.has(r.id));
|
||||
|
||||
toAdd.forEach((r) => {
|
||||
state.localsIndex.add(r.id);
|
||||
state.locals.push(r);
|
||||
});
|
||||
state.key = state.key + toAdd.length;
|
||||
},
|
||||
addLoaded(state: CalendarLocalsState, payload: {start: Date, end: Date}) {
|
||||
state.localsLoaded.push({start: payload.start.getTime(), end: payload.end.getTime()});
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
fetchLocals(ctx: Context, payload: {start: Date, end: Date}): Promise<null> {
|
||||
const start = payload.start;
|
||||
const end = payload.end;
|
||||
|
||||
if (ctx.rootGetters['me/getMe'] === null) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
if (ctx.getters.isLocalsLoaded({start, end})) {
|
||||
return Promise.resolve(ctx.getters.getRangeSource);
|
||||
}
|
||||
|
||||
ctx.commit('addLoaded', {
|
||||
start: start,
|
||||
end: end,
|
||||
});
|
||||
|
||||
return fetchCalendarLocalForUser(
|
||||
ctx.rootGetters['me/getMe'],
|
||||
start,
|
||||
end
|
||||
)
|
||||
.then((remotes: CalendarLight[]) => {
|
||||
// to be add when reactivity problem will be solve ?
|
||||
//ctx.commit('addRemotes', remotes);
|
||||
const inputs = remotes
|
||||
.map(cr => localsToFullCalendarEvent(cr))
|
||||
.map(cr => ({...cr, backgroundColor: COLORS[0], textColor: 'black', editable: false}))
|
||||
ctx.commit('calendarRanges/addExternals', inputs, {root: true});
|
||||
return Promise.resolve(null);
|
||||
})
|
||||
.catch((e: TransportExceptionInterface) => {
|
||||
console.error(e);
|
||||
|
||||
return Promise.resolve(null);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
@@ -0,0 +1,252 @@
|
||||
import {State} from './../index';
|
||||
import {ActionContext, Module} from 'vuex';
|
||||
import {CalendarRange, CalendarRangeCreate, CalendarRangeEdit, isEventInputCalendarRange} from "../../../../types";
|
||||
import {Location} from "../../../../../../../ChillMainBundle/Resources/public/types";
|
||||
import {fetchCalendarRangeForUser} from '../../../Calendar/api';
|
||||
import {calendarRangeToFullCalendarEvent} from '../../../Calendar/store/utils';
|
||||
import {EventInput} from '@fullcalendar/vue3';
|
||||
import {makeFetch} from "../../../../../../../ChillMainBundle/Resources/public/lib/api/apiMethods";
|
||||
import {
|
||||
datetimeToISO,
|
||||
dateToISO,
|
||||
ISOToDatetime
|
||||
} from "../../../../../../../ChillMainBundle/Resources/public/chill/js/date";
|
||||
import type {EventInputCalendarRange} from "../../../../types";
|
||||
|
||||
export interface CalendarRangesState {
|
||||
ranges: (EventInput | EventInputCalendarRange) [],
|
||||
rangesLoaded: { start: number, end: number }[],
|
||||
rangesIndex: Set<string>,
|
||||
key: number
|
||||
}
|
||||
|
||||
type Context = ActionContext<CalendarRangesState, State>;
|
||||
|
||||
export default <Module<CalendarRangesState, State>>{
|
||||
namespaced: true,
|
||||
state: (): CalendarRangesState => ({
|
||||
ranges: [],
|
||||
rangesLoaded: [],
|
||||
rangesIndex: new Set<string>(),
|
||||
key: 0
|
||||
}),
|
||||
getters: {
|
||||
isRangeLoaded: (state: CalendarRangesState) => ({start, end}: { start: Date, end: Date }): boolean => {
|
||||
for (let range of state.rangesLoaded) {
|
||||
if (start.getTime() === range.start && end.getTime() === range.end) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
getRangesOnDate: (state: CalendarRangesState) => (date: Date): EventInputCalendarRange[] => {
|
||||
const founds = [];
|
||||
const dateStr = <string>dateToISO(date);
|
||||
|
||||
for (let range of state.ranges) {
|
||||
if (isEventInputCalendarRange(range)
|
||||
&& range.start.startsWith(dateStr)
|
||||
) {
|
||||
founds.push(range);
|
||||
}
|
||||
}
|
||||
|
||||
return founds;
|
||||
},
|
||||
},
|
||||
mutations: {
|
||||
addRanges(state: CalendarRangesState, ranges: CalendarRange[]) {
|
||||
const toAdd = ranges
|
||||
.map(cr => calendarRangeToFullCalendarEvent(cr))
|
||||
.map(cr => ({
|
||||
...cr, backgroundColor: 'white', borderColor: '#3788d8',
|
||||
textColor: 'black'
|
||||
}))
|
||||
.filter(r => !state.rangesIndex.has(r.id));
|
||||
|
||||
toAdd.forEach((r) => {
|
||||
state.rangesIndex.add(r.id);
|
||||
state.ranges.push(r);
|
||||
});
|
||||
state.key = state.key + toAdd.length;
|
||||
},
|
||||
addExternals(state, externalEvents: (EventInput & { id: string })[]) {
|
||||
const toAdd = externalEvents
|
||||
.filter(r => !state.rangesIndex.has(r.id));
|
||||
|
||||
toAdd.forEach((r) => {
|
||||
state.rangesIndex.add(r.id);
|
||||
state.ranges.push(r);
|
||||
});
|
||||
state.key = state.key + toAdd.length;
|
||||
},
|
||||
addLoaded(state: CalendarRangesState, payload: { start: Date, end: Date }) {
|
||||
state.rangesLoaded.push({start: payload.start.getTime(), end: payload.end.getTime()});
|
||||
},
|
||||
addRange(state: CalendarRangesState, payload: CalendarRange) {
|
||||
const asEvent = calendarRangeToFullCalendarEvent(payload);
|
||||
state.ranges.push({...asEvent, backgroundColor: 'white', borderColor: '#3788d8', textColor: 'black'});
|
||||
state.rangesIndex.add(asEvent.id);
|
||||
state.key = state.key + 1;
|
||||
},
|
||||
removeRange(state: CalendarRangesState, calendarRangeId: number) {
|
||||
const found = state.ranges.find(r => r.calendarRangeId === calendarRangeId && r.is === "range");
|
||||
|
||||
if (found !== undefined) {
|
||||
state.ranges = state.ranges.filter(
|
||||
(r) => !(r.calendarRangeId === calendarRangeId && r.is === "range")
|
||||
);
|
||||
|
||||
if (typeof found.id === "string") { // should always be true
|
||||
state.rangesIndex.delete(found.id);
|
||||
}
|
||||
|
||||
state.key = state.key + 1;
|
||||
}
|
||||
},
|
||||
updateRange(state, range: CalendarRange) {
|
||||
const found = state.ranges.find(r => r.calendarRangeId === range.id && r.is === "range");
|
||||
const newEvent = calendarRangeToFullCalendarEvent(range);
|
||||
|
||||
if (found !== undefined) {
|
||||
found.start = newEvent.start;
|
||||
found.end = newEvent.end;
|
||||
found.locationId = range.location.id;
|
||||
found.locationName = range.location.name;
|
||||
}
|
||||
|
||||
state.key = state.key + 1;
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
fetchRanges(ctx: Context, payload: { start: Date, end: Date }): Promise<null> {
|
||||
const start = payload.start;
|
||||
const end = payload.end;
|
||||
|
||||
if (ctx.rootGetters['me/getMe'] === null) {
|
||||
return Promise.resolve(ctx.getters.getRangeSource);
|
||||
}
|
||||
|
||||
if (ctx.getters.isRangeLoaded({start, end})) {
|
||||
return Promise.resolve(ctx.getters.getRangeSource);
|
||||
}
|
||||
|
||||
ctx.commit('addLoaded', {
|
||||
start: start,
|
||||
end: end,
|
||||
});
|
||||
|
||||
return fetchCalendarRangeForUser(
|
||||
ctx.rootGetters['me/getMe'],
|
||||
start,
|
||||
end
|
||||
)
|
||||
.then((ranges: CalendarRange[]) => {
|
||||
ctx.commit('addRanges', ranges);
|
||||
return Promise.resolve(null);
|
||||
});
|
||||
},
|
||||
createRange(ctx, {start, end, location}: { start: Date, end: Date, location: Location }): Promise<null> {
|
||||
const url = `/api/1.0/calendar/calendar-range.json?`;
|
||||
|
||||
if (ctx.rootState.me.me === null) {
|
||||
throw new Error('user is currently null');
|
||||
}
|
||||
|
||||
const body = {
|
||||
user: {
|
||||
id: ctx.rootState.me.me.id,
|
||||
type: "user"
|
||||
},
|
||||
startDate: {
|
||||
datetime: datetimeToISO(start),
|
||||
},
|
||||
endDate: {
|
||||
datetime: datetimeToISO(end)
|
||||
},
|
||||
location: {
|
||||
id: location.id,
|
||||
type: "location"
|
||||
}
|
||||
} as CalendarRangeCreate;
|
||||
|
||||
return makeFetch<CalendarRangeCreate, CalendarRange>('POST', url, body)
|
||||
.then((newRange) => {
|
||||
|
||||
ctx.commit('addRange', newRange);
|
||||
|
||||
return Promise.resolve(null);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
|
||||
throw error;
|
||||
})
|
||||
},
|
||||
deleteRange(ctx, calendarRangeId: number) {
|
||||
const url = `/api/1.0/calendar/calendar-range/${calendarRangeId}.json`;
|
||||
|
||||
makeFetch<undefined, never>('DELETE', url)
|
||||
.then((_) => {
|
||||
ctx.commit('removeRange', calendarRangeId);
|
||||
});
|
||||
},
|
||||
patchRangeTime(ctx, {calendarRangeId, start, end}: {calendarRangeId: number, start: Date, end: Date}): Promise<null> {
|
||||
const url = `/api/1.0/calendar/calendar-range/${calendarRangeId}.json`;
|
||||
const body = {
|
||||
startDate: {
|
||||
datetime: datetimeToISO(start)
|
||||
},
|
||||
endDate: {
|
||||
datetime: datetimeToISO(end)
|
||||
},
|
||||
} as CalendarRangeEdit;
|
||||
|
||||
return makeFetch<CalendarRangeEdit, CalendarRange>('PATCH', url, body)
|
||||
.then((range) => {
|
||||
ctx.commit('updateRange', range);
|
||||
return Promise.resolve(null);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
return Promise.resolve(null);
|
||||
})
|
||||
},
|
||||
patchRangeLocation(ctx, {location, calendarRangeId}: {location: Location, calendarRangeId: number}): Promise<null> {
|
||||
const url = `/api/1.0/calendar/calendar-range/${calendarRangeId}.json`;
|
||||
const body = {
|
||||
location: {
|
||||
id: location.id,
|
||||
type: "location"
|
||||
},
|
||||
} as CalendarRangeEdit;
|
||||
|
||||
return makeFetch<CalendarRangeEdit, CalendarRange>('PATCH', url, body)
|
||||
.then((range) => {
|
||||
ctx.commit('updateRange', range);
|
||||
return Promise.resolve(null);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
return Promise.resolve(null);
|
||||
})
|
||||
},
|
||||
copyFromDayToAnotherDay(ctx, {from, to}: {from: Date, to: Date}): Promise<null> {
|
||||
const rangesToCopy: EventInputCalendarRange[] = ctx.getters['getRangesOnDate'](from);
|
||||
const promises = [];
|
||||
|
||||
for (let r of rangesToCopy) {
|
||||
let start = new Date(<Date>ISOToDatetime(r.start));
|
||||
start.setFullYear(to.getFullYear(), to.getMonth(), to.getDate())
|
||||
let end = new Date(<Date>ISOToDatetime(r.end));
|
||||
end.setFullYear(to.getFullYear(), to.getMonth(), to.getDate());
|
||||
let location = ctx.rootGetters['locations/getLocationById'](r.locationId);
|
||||
|
||||
promises.push(ctx.dispatch('createRange', {start, end, location}));
|
||||
}
|
||||
|
||||
return Promise.all(promises).then(_ => Promise.resolve(null));
|
||||
}
|
||||
}
|
||||
};
|
@@ -0,0 +1,95 @@
|
||||
import {State} from './../index';
|
||||
import {ActionContext, Module} from 'vuex';
|
||||
import {CalendarRemote} from '../../../../types';
|
||||
import {fetchCalendarRemoteForUser} from '../../../Calendar/api';
|
||||
import {EventInput, EventSource} from '@fullcalendar/vue3';
|
||||
import {remoteToFullCalendarEvent} from "../../../Calendar/store/utils";
|
||||
import {TransportExceptionInterface} from "../../../../../../../ChillMainBundle/Resources/public/lib/api/apiMethods";
|
||||
import {COLORS} from "../../../Calendar/const";
|
||||
|
||||
export interface CalendarRemotesState {
|
||||
remotes: EventInput[],
|
||||
remotesLoaded: {start: number, end: number}[],
|
||||
remotesIndex: Set<string>,
|
||||
key: number
|
||||
}
|
||||
|
||||
type Context = ActionContext<CalendarRemotesState, State>;
|
||||
|
||||
export default <Module<CalendarRemotesState, State>> {
|
||||
namespaced: true,
|
||||
state: (): CalendarRemotesState => ({
|
||||
remotes: [],
|
||||
remotesLoaded: [],
|
||||
remotesIndex: new Set<string>(),
|
||||
key: 0
|
||||
}),
|
||||
getters: {
|
||||
isRemotesLoaded: (state: CalendarRemotesState) => ({start, end}: {start: Date, end: Date}): boolean => {
|
||||
for (let range of state.remotesLoaded) {
|
||||
if (start.getTime() === range.start && end.getTime() === range.end) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
},
|
||||
mutations: {
|
||||
addRemotes(state: CalendarRemotesState, ranges: CalendarRemote[]) {
|
||||
console.log('addRemotes', ranges);
|
||||
|
||||
const toAdd = ranges
|
||||
.map(cr => remoteToFullCalendarEvent(cr))
|
||||
.filter(r => !state.remotesIndex.has(r.id));
|
||||
|
||||
toAdd.forEach((r) => {
|
||||
state.remotesIndex.add(r.id);
|
||||
state.remotes.push(r);
|
||||
});
|
||||
state.key = state.key + toAdd.length;
|
||||
},
|
||||
addLoaded(state: CalendarRemotesState, payload: {start: Date, end: Date}) {
|
||||
state.remotesLoaded.push({start: payload.start.getTime(), end: payload.end.getTime()});
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
fetchRemotes(ctx: Context, payload: {start: Date, end: Date}): Promise<null> {
|
||||
const start = payload.start;
|
||||
const end = payload.end;
|
||||
|
||||
if (ctx.rootGetters['me/getMe'] === null) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
if (ctx.getters.isRemotesLoaded({start, end})) {
|
||||
return Promise.resolve(ctx.getters.getRangeSource);
|
||||
}
|
||||
|
||||
ctx.commit('addLoaded', {
|
||||
start: start,
|
||||
end: end,
|
||||
});
|
||||
|
||||
return fetchCalendarRemoteForUser(
|
||||
ctx.rootGetters['me/getMe'],
|
||||
start,
|
||||
end
|
||||
)
|
||||
.then((remotes: CalendarRemote[]) => {
|
||||
// to be add when reactivity problem will be solve ?
|
||||
//ctx.commit('addRemotes', remotes);
|
||||
const inputs = remotes
|
||||
.map(cr => remoteToFullCalendarEvent(cr))
|
||||
.map(cr => ({...cr, backgroundColor: COLORS[0], textColor: 'black', editable: false}))
|
||||
ctx.commit('calendarRanges/addExternals', inputs, {root: true});
|
||||
return Promise.resolve(null);
|
||||
})
|
||||
.catch((e: TransportExceptionInterface) => {
|
||||
console.error(e);
|
||||
|
||||
return Promise.resolve(null);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
@@ -0,0 +1,56 @@
|
||||
import {State} from './../index';
|
||||
import {ActionContext} from 'vuex';
|
||||
|
||||
export interface FullCalendarState {
|
||||
currentView: {
|
||||
start: Date|null,
|
||||
end: Date|null,
|
||||
},
|
||||
key: number
|
||||
}
|
||||
|
||||
type Context = ActionContext<FullCalendarState, State>;
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state: (): FullCalendarState => ({
|
||||
currentView: {
|
||||
start: null,
|
||||
end: null,
|
||||
},
|
||||
key: 0,
|
||||
}),
|
||||
mutations: {
|
||||
setCurrentDatesView: function(state: FullCalendarState, payload: {start: Date, end: Date}): void {
|
||||
state.currentView.start = payload.start;
|
||||
state.currentView.end = payload.end;
|
||||
},
|
||||
increaseKey: function(state: FullCalendarState): void {
|
||||
state.key = state.key + 1;
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
setCurrentDatesView(ctx: Context, {start, end}: {start: Date|null, end: Date|null}): Promise<null> {
|
||||
console.log('dispatch setCurrentDatesView', {start, end});
|
||||
|
||||
if (ctx.state.currentView.start !== start || ctx.state.currentView.end !== end) {
|
||||
ctx.commit('setCurrentDatesView', {start, end});
|
||||
}
|
||||
|
||||
if (start !== null && end !== null) {
|
||||
|
||||
return Promise.all([
|
||||
ctx.dispatch('calendarRanges/fetchRanges', {start, end}, {root: true}).then(_ => Promise.resolve(null)),
|
||||
ctx.dispatch('calendarRemotes/fetchRemotes', {start, end}, {root: true}).then(_ => Promise.resolve(null)),
|
||||
ctx.dispatch('calendarLocals/fetchLocals', {start, end}, {root: true}).then(_ => Promise.resolve(null))
|
||||
]
|
||||
).then(_ => Promise.resolve(null));
|
||||
|
||||
} else {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -0,0 +1,62 @@
|
||||
import {Location} from "../../../../../../../ChillMainBundle/Resources/public/types";
|
||||
import {State} from '../index';
|
||||
import {Module} from 'vuex';
|
||||
import {getLocations} from "../../../../../../../ChillMainBundle/Resources/public/lib/api/locations";
|
||||
import {whereami} from "../../../../../../../ChillMainBundle/Resources/public/lib/api/user";
|
||||
|
||||
export interface LocationState {
|
||||
locations: Location[];
|
||||
locationPicked: Location | null;
|
||||
currentLocation: Location | null;
|
||||
}
|
||||
|
||||
export default <Module<LocationState, State>>{
|
||||
namespaced: true,
|
||||
state: (): LocationState => {
|
||||
return {
|
||||
locations: [],
|
||||
locationPicked: null,
|
||||
currentLocation: null,
|
||||
}
|
||||
},
|
||||
getters: {
|
||||
getLocationById: (state) => (id: number): Location|undefined => {
|
||||
return state.locations.find(l => l.id === id);
|
||||
},
|
||||
},
|
||||
mutations: {
|
||||
setLocations(state, locations): void {
|
||||
state.locations = locations;
|
||||
},
|
||||
setLocationPicked(state, location: Location | null): void {
|
||||
if (null === location) {
|
||||
state.locationPicked = null;
|
||||
return;
|
||||
}
|
||||
|
||||
state.locationPicked = state.locations.find(l => l.id === location.id) || null;
|
||||
},
|
||||
setCurrentLocation(state, location: Location | null): void {
|
||||
if (null === location) {
|
||||
state.currentLocation = null;
|
||||
return;
|
||||
}
|
||||
|
||||
state.currentLocation = state.locations.find(l => l.id === location.id) || null;
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
getLocations(ctx): Promise<void> {
|
||||
return getLocations().then(locations => {
|
||||
ctx.commit('setLocations', locations);
|
||||
return Promise.resolve();
|
||||
});
|
||||
},
|
||||
getCurrentLocation(ctx): Promise<void> {
|
||||
return whereami().then(location => {
|
||||
ctx.commit('setCurrentLocation', location);
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,29 @@
|
||||
import {State} from './../index';
|
||||
import {User} from '../../../../../../../ChillMainBundle/Resources/public/types';
|
||||
import {ActionContext} from 'vuex';
|
||||
|
||||
export interface MeState {
|
||||
me: User|null,
|
||||
}
|
||||
|
||||
type Context = ActionContext<MeState, State>;
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state: (): MeState => ({
|
||||
me: null,
|
||||
}),
|
||||
getters: {
|
||||
getMe: function(state: MeState): User|null {
|
||||
return state.me;
|
||||
},
|
||||
},
|
||||
mutations: {
|
||||
setWhoAmi(state: MeState, me: User) {
|
||||
state.me = me;
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
@@ -4,6 +4,7 @@
|
||||
* @returns {Promise} a promise containing all Calendar ranges objects
|
||||
*/
|
||||
const fetchCalendarRanges = () => {
|
||||
return Promise.resolve([]);
|
||||
const url = `/api/1.0/calendar/calendar-range-available.json`;
|
||||
return fetch(url)
|
||||
.then(response => {
|
||||
@@ -13,6 +14,7 @@ const fetchCalendarRanges = () => {
|
||||
};
|
||||
|
||||
const fetchCalendarRangesByUser = (userId) => {
|
||||
return Promise.resolve([]);
|
||||
const url = `/api/1.0/calendar/calendar-range-available.json?user=${userId}`;
|
||||
return fetch(url)
|
||||
.then(response => {
|
||||
@@ -27,6 +29,7 @@ const fetchCalendarRangesByUser = (userId) => {
|
||||
* @returns {Promise} a promise containing all Calendar objects
|
||||
*/
|
||||
const fetchCalendar = (mainUserId) => {
|
||||
return Promise.resolve([]);
|
||||
const url = `/api/1.0/calendar/calendar.json?main_user=${mainUserId}&item_per_page=1000`;
|
||||
return fetch(url)
|
||||
.then(response => {
|
||||
|
@@ -9,8 +9,8 @@
|
||||
{
|
||||
'title' : 'Remove calendar item'|trans,
|
||||
'confirm_question' : 'Are you sure you want to remove the calendar item?'|trans,
|
||||
'cancel_route' : 'chill_calendar_calendar_list',
|
||||
'cancel_parameters' : { 'accompanying_period_id' : accompanyingCourse.id, 'id' : calendar.id },
|
||||
'cancel_route' : 'chill_calendar_calendar_list_by_period',
|
||||
'cancel_parameters' : { 'id' : accompanyingCourse.id },
|
||||
'form' : delete_form
|
||||
} ) }}
|
||||
{% endblock %}
|
||||
|
@@ -3,9 +3,9 @@
|
||||
{{ form_start(form) }}
|
||||
{{ form_errors(form) }}
|
||||
|
||||
{%- if form.mainUser is defined -%}
|
||||
{{ form_row(form.mainUser) }}
|
||||
{% endif %}
|
||||
<div id="calendar"></div> {# <=== vue component #}
|
||||
|
||||
<div id="mainUser"></div> {# <=== vue component: mainUser #}
|
||||
|
||||
<h2 class="chill-red">{{ 'Concerned groups'|trans }}</h2>
|
||||
|
||||
@@ -26,6 +26,13 @@
|
||||
|
||||
<h2 class="chill-red">{{ 'Calendar data'|trans }}</h2>
|
||||
|
||||
<div id="schedule"></div>
|
||||
|
||||
{%- if form.location is defined -%}
|
||||
{{ form_row(form.location) }}
|
||||
<div id="location"></div>
|
||||
{% endif %}
|
||||
|
||||
{%- if form.startDate is defined -%}
|
||||
{{ form_row(form.startDate) }}
|
||||
{% endif %}
|
||||
@@ -38,10 +45,7 @@
|
||||
{{ form_row(form.calendarRange) }}
|
||||
{% endif %}
|
||||
|
||||
{%- if form.location is defined -%}
|
||||
{{ form_row(form.location) }}
|
||||
<div id="location"></div>
|
||||
{% endif %}
|
||||
<div id="fullCalendar"></div>
|
||||
|
||||
{%- if form.comment is defined -%}
|
||||
{{ form_row(form.comment) }}
|
||||
@@ -59,7 +63,6 @@
|
||||
<div id="calendarControls"></div>
|
||||
{% endif %}
|
||||
|
||||
<div id="fullCalendar"></div>
|
||||
|
||||
<ul class="record_actions sticky-form-buttons">
|
||||
<li class="cancel">
|
||||
@@ -68,7 +71,7 @@
|
||||
{%- if context == 'user' -%}
|
||||
href="{{ chill_return_path_or('chill_calendar_calendar_list', { 'user_id': user.id } )}}"
|
||||
{%- elseif context == 'accompanyingCourse' -%}
|
||||
href="{{ chill_return_path_or('chill_calendar_calendar_list', { 'accompanying_period_id': accompanyingCourse.id } )}}"
|
||||
href="{{ chill_return_path_or('chill_calendar_calendar_list_by_period', { 'id': accompanyingCourse.id } )}}"
|
||||
{%- endif -%}
|
||||
>
|
||||
{{ 'Cancel'|trans|chill_return_path_label }}
|
||||
@@ -76,7 +79,7 @@
|
||||
</li>
|
||||
<li>
|
||||
<button class="btn btn-create" type="submit">
|
||||
{{ 'Update'|trans }}
|
||||
{{ 'Save'|trans }}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
@@ -7,7 +7,6 @@
|
||||
{% block content %}
|
||||
<div class="calendar-edit">
|
||||
|
||||
<div id="calendar"></div> {# <=== vue component #}
|
||||
{% include 'ChillCalendarBundle:Calendar:edit.html.twig' with {'context': 'accompanyingCourse'} %}
|
||||
|
||||
</div>
|
||||
@@ -16,10 +15,6 @@
|
||||
{% block js %}
|
||||
{{ parent() }}
|
||||
<script type="text/javascript">
|
||||
window.addEventListener('DOMContentLoaded', function (e) {
|
||||
chill.displayAlertWhenLeavingModifiedForm('form[name="{{ form.vars.form.vars.name }}"]',
|
||||
'{{ "You are going to leave a page with unsubmitted data. Are you sure you want to leave ?"|trans }}');
|
||||
});
|
||||
window.entity = {{ entity_json|json_encode|raw }};
|
||||
window.startDate = {{ entity.startDate|date('Y-m-d H:i:s')|json_encode|raw }};
|
||||
window.endDate = {{ entity.endDate|date('Y-m-d H:i:s')|json_encode|raw }};
|
||||
|
@@ -7,11 +7,22 @@
|
||||
{% set user_id = null %}
|
||||
{% set accompanying_course_id = accompanyingCourse.id %}
|
||||
|
||||
{% block js %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_script_tags('mod_answer') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block css %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_link_tags('mod_answer') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h1>{{ 'Calendar list' |trans }}</h1>
|
||||
{{ filterOrder|chill_render_filter_order_helper }}
|
||||
|
||||
{% if calendarItems|length == 0 %}
|
||||
{% if calendarItems|length == 0 %}
|
||||
<p class="chill-no-data-statement">
|
||||
{{ "There is no calendar items."|trans }}
|
||||
<a href="{{ path('chill_calendar_calendar_new', {'user_id': user_id, 'accompanying_period_id': accompanying_course_id}) }}" class="btn btn-create button-small"></a>
|
||||
@@ -25,63 +36,35 @@
|
||||
<div class="item-bloc">
|
||||
<div class="item-row main">
|
||||
<div class="item-col">
|
||||
<div class="wrap-header">
|
||||
<div class="wl-row">
|
||||
<div class="wl-col title">
|
||||
{% if calendar.endDate.diff(calendar.startDate).days >= 1 %}
|
||||
<p class="date-label">{{ calendar.startDate|format_datetime('short', 'short') }} - {{ calendar.endDate|format_datetime('short', 'short') }}</p>
|
||||
{% else %}
|
||||
<p class="date-label">{{ calendar.startDate|format_datetime('short', 'short') }} - {{ calendar.endDate|format_datetime('none', 'short') }}</p>
|
||||
{% endif %}
|
||||
|
||||
{% if calendar.startDate and calendar.endDate %}
|
||||
{% if calendar.endDate.diff(calendar.startDate).days >= 1 %}
|
||||
<h3>{{ "From the day"|trans }} {{ calendar.startDate|format_datetime('medium', 'short') }} </h3>
|
||||
<h3>{{ "to the day"|trans }} {{ calendar.endDate|format_datetime('medium', 'short') }}</h3>
|
||||
{% else %}
|
||||
<h3>{{ calendar.startDate|format_date('full') }} </h3>
|
||||
<h3>{{ calendar.startDate|format_datetime('none', 'short', locale='fr') }} - {{ calendar.endDate|format_datetime('none', 'short', locale='fr') }}</h3>
|
||||
<div class="duration short-message">
|
||||
<p>
|
||||
<i class="fa fa-fw fa-hourglass-end"></i>
|
||||
{{ calendar.duration|date('%H:%I')}}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="duration">
|
||||
<p>
|
||||
<i class="fa fa-fw fa-hourglass-end"></i>
|
||||
{{ calendar.endDate.diff(calendar.startDate)|date("%H:%M")}}
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
<div class="item-col">
|
||||
<ul class="list-content">
|
||||
{% if calendar.mainUser is not empty %}
|
||||
<span class="badge-user">{{ calendar.mainUser|chill_entity_render_box }}</span>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="item-col">
|
||||
<ul class="list-content">
|
||||
{% if calendar.user %}
|
||||
<li>
|
||||
<b>{{ 'by'|trans }}{{ calendar.user.usernameCanonical }}</b>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if calendar.mainUser is not empty %}
|
||||
<li>
|
||||
<b>{{ 'main user concerned'|trans }}: {{ calendar.mainUser.usernameCanonical }}</b>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
</ul>
|
||||
<ul class="record_actions">
|
||||
<li>
|
||||
<a href="{{ path('chill_calendar_calendar_show', { 'id': calendar.id, 'user_id': user_id, 'accompanying_period_id': accompanying_course_id }) }}" class="btn btn-show "></a>
|
||||
</li>
|
||||
{# TOOD
|
||||
{% if is_granted('CHILL_ACTIVITY_UPDATE', calendar) %}
|
||||
#}
|
||||
<li>
|
||||
<a href="{{ path('chill_calendar_calendar_edit', { 'id': calendar.id, 'user_id': user_id, 'accompanying_period_id': accompanying_course_id }) }}" class="btn btn-update "></a>
|
||||
</li>
|
||||
{# TOOD
|
||||
{% endif %}
|
||||
{% if is_granted('CHILL_ACTIVITY_DELETE', calendar) %}
|
||||
#}
|
||||
<li>
|
||||
<a href="{{ path('chill_calendar_calendar_delete', { 'id': calendar.id, 'user_id' : user_id, 'accompanying_period_id': accompanying_course_id } ) }}" class="btn btn-delete "></a>
|
||||
</li>
|
||||
{#
|
||||
{% endif %}
|
||||
#}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{%
|
||||
@@ -90,23 +73,99 @@
|
||||
or calendar.thirdParties|length > 0
|
||||
or calendar.users|length > 0
|
||||
%}
|
||||
<div class="item-row details">
|
||||
<div class="item-row details separator">
|
||||
<div class="item-col">
|
||||
{% include 'ChillActivityBundle:Activity:concernedGroups.html.twig' with {
|
||||
'context': accompanyingCourse,
|
||||
'render': 'row',
|
||||
'context': 'calendar_accompanyingCourse',
|
||||
'render': 'wrap-list',
|
||||
'entity': calendar
|
||||
} %}
|
||||
</div>
|
||||
|
||||
{% if calendar.comment.comment is not empty %}
|
||||
<div class="item-col comment">
|
||||
{{ calendar.comment|chill_entity_render_box( { 'limit_lines': 3, 'metadata': false } ) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if calendar.comment.comment is not empty %}
|
||||
<div class="item-row details separator">
|
||||
<div class="item-col comment">
|
||||
{{ calendar.comment|chill_entity_render_box( { 'limit_lines': 3, 'metadata': false } ) }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if calendar.location is not empty %}
|
||||
<div class="item-row separator">
|
||||
<div>
|
||||
{% if calendar.location.address is not same as(null) and calendar.location.name is not empty %}<i class="fa fa-map-marker"></i>{% endif %}
|
||||
{% if calendar.location.name is not empty %}{{ calendar.location.name }}{% endif %}
|
||||
{% if calendar.location.address is not same as(null) %}{{ calendar.location.address|chill_entity_render_box({'multiline': false, 'with_picto': (calendar.location.name is empty)}) }}{% else %}<i class="fa fa-map-marker"></i>{% endif %}
|
||||
{% if calendar.location.phonenumber1 is not empty %}<i class="fa fa-phone"></i> {{ calendar.location.phonenumber1|chill_format_phonenumber }}{% endif %}
|
||||
{% if calendar.location.phonenumber2 is not empty %}<i class="fa fa-phone"></i> {{ calendar.location.phonenumber2|chill_format_phonenumber }}{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="item-row separator">
|
||||
<div>
|
||||
{% if false == calendar.sendSMS or null == calendar.sendSMS %}
|
||||
<span title="{{ 'Will not send SMS'|trans }}" class="fa-stack">
|
||||
<i class="fa fa-envelope fa-stack-1x"></i>
|
||||
<i class="fa fa-ban text-danger fa-stack-2x"></i>
|
||||
</span>
|
||||
{% else %}
|
||||
{% if calendar.smsStatus == 'sms_sent' %}
|
||||
<span title="{{ 'SMS already sent'|trans }}">
|
||||
<i class="fa fa-envelope"></i>
|
||||
<i class="fa fa-check"></i>
|
||||
</span>
|
||||
{% else %}
|
||||
<span title="{{ 'Will send SMS'|trans }}">
|
||||
<i class="fa fa-bell"></i>
|
||||
<i class="fa fa-envelope"></i>
|
||||
</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<ul class="record_actions">
|
||||
{% if is_granted('CHILL_ACTIVITY_CREATE', accompanyingCourse) %}
|
||||
<li>
|
||||
<a class="btn btn-create" href="{{ chill_path_add_return_path('chill_calendar_calendar_to_activity', { 'id': calendar.id }) }}">
|
||||
{{ 'Transform to activity'|trans }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if (calendar.isInvited(app.user)) %}
|
||||
{% set invite = calendar.inviteForUser(app.user) %}
|
||||
<li>
|
||||
<div invite-answer data-status="{{ invite.status|e('html_attr') }}" data-calendar-id="{{ calendar.id|e('html_attr') }}"></div>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if false %}
|
||||
<li>
|
||||
<a href="{{ chill_path_add_return_path('chill_calendar_calendar_show', { 'id': calendar.id, 'user_id': user_id, 'accompanying_period_id': accompanying_course_id }) }}" class="btn btn-show "></a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{# TOOD
|
||||
{% if is_granted('CHILL_ACTIVITY_UPDATE', calendar) %}
|
||||
#}
|
||||
<li>
|
||||
<a href="{{ chill_path_add_return_path('chill_calendar_calendar_edit', { 'id': calendar.id, 'user_id': user_id, 'accompanying_period_id': accompanying_course_id }) }}" class="btn btn-update "></a>
|
||||
</li>
|
||||
{# TOOD
|
||||
{% endif %}
|
||||
{% if is_granted('CHILL_ACTIVITY_DELETE', calendar) %}
|
||||
#}
|
||||
<li>
|
||||
<a href="{{ chill_path_add_return_path('chill_calendar_calendar_delete', { 'id': calendar.id, 'user_id' : user_id, 'accompanying_period_id': accompanying_course_id } ) }}" class="btn btn-delete "></a>
|
||||
</li>
|
||||
{#
|
||||
{% endif %}
|
||||
#}
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
@@ -118,10 +177,17 @@
|
||||
{% endif %}
|
||||
|
||||
|
||||
<ul class="record_actions">
|
||||
<ul class="record_actions sticky-form-buttons">
|
||||
{% if accompanyingCourse.user is not same as(null) %}
|
||||
<li>
|
||||
<a href="{{ path('chill_calendar_calendar_new', {'user_id': user_id, 'accompanying_period_id': accompanying_course_id, 'mainUser': accompanyingCourse.user.id }) }}" class="btn btn-create">
|
||||
{{ 'chill_calendar.Create for referrer'|trans }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li>
|
||||
<a href="{{ path('chill_calendar_calendar_new', {'user_id': user_id, 'accompanying_period_id': accompanying_course_id}) }}" class="btn btn-create">
|
||||
{{ 'Add a new calendar' | trans }}
|
||||
{{ 'Create'|trans }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user