From 51607572deb84a34dc5ab0db93aa82c6edc64e6e Mon Sep 17 00:00:00 2001 From: LenaertsJ Date: Fri, 7 Nov 2025 10:11:46 +0000 Subject: [PATCH] Create admin for motive --- .../unreleased/Feature-20250808-120802.yaml | 6 + .../unreleased/Feature-20251007-155945.yaml | 6 + .../unreleased/Feature-20251022-111552.yaml | 6 + .../unreleased/Fixed-20251106-161605.yaml | 6 + .../Controller/CalendarController.php | 54 ++- .../Controller/MyInvitationsController.php | 58 +++ .../DataFixtures/ORM/LoadCancelReason.php | 2 +- .../ChillCalendarBundle/Entity/Calendar.php | 5 + .../Entity/CancelReason.php | 6 +- .../Form/CancelReasonType.php | 11 +- .../ChillCalendarBundle/Form/CancelType.php | 42 +++ .../Menu/UserMenuBuilder.php | 7 + .../Doctrine/CalendarEntityListener.php | 27 +- .../Message/CalendarRemovedMessage.php | 2 + .../Repository/CalendarRepository.php | 1 + .../Repository/InviteRepository.php | 2 +- .../Resources/config/services/controller.yml | 1 + .../Resources/views/Calendar/_list.html.twig | 349 +++++++++--------- ...ncelCalendarByAccompanyingCourse.html.twig | 29 ++ .../Calendar/cancelCalendarByPerson.html.twig | 29 ++ .../listByAccompanyingCourse.html.twig | 13 +- .../views/Calendar/listByPerson.html.twig | 12 +- ...d.twig => short_message_canceled.txt.twig} | 0 .../views/CancelReason/index.html.twig | 4 +- .../views/Invitations/listByUser.html.twig | 40 ++ .../DefaultShortMessageForCalendarBuilder.php | 3 +- .../MyInvitationsControllerTest.php | 292 +++++++++++++++ .../translations/messages.fr.yml | 14 +- ...ocGeneratorTemplateRepositoryInterface.php | 5 + .../CRUD/Controller/CRUDController.php | 3 +- .../Pagination/PaginatorFactory.php | 8 +- .../components/DocumentsList.vue | 2 +- .../src/Controller/Admin/MotiveController.php | 54 +++ .../src/Controller/AdminController.php | 31 ++ .../ChillTicketExtension.php | 36 ++ .../ChillTicketBundle/src/Entity/Motive.php | 16 +- .../ChillTicketBundle/src/Form/MotiveType.php | 71 ++++ .../src/Menu/AdminMenuBuilder.php | 45 +++ .../ChillTicketBundle/src/MotiveDTO.php | 67 ++++ .../views/Admin/Motive/edit.html.twig | 15 + .../views/Admin/Motive/index.html.twig | 68 ++++ .../views/Admin/Motive/new.html.twig | 15 + .../views/Admin/Motive/view.html.twig | 68 ++++ .../src/Resources/views/Admin/index.html.twig | 13 + .../src/config/services.yaml | 3 + .../src/migrations/Version20251022081554.php | 34 ++ .../src/translations/messages+intl-icu.fr.yml | 33 ++ 47 files changed, 1418 insertions(+), 196 deletions(-) create mode 100644 .changes/unreleased/Feature-20250808-120802.yaml create mode 100644 .changes/unreleased/Feature-20251007-155945.yaml create mode 100644 .changes/unreleased/Feature-20251022-111552.yaml create mode 100644 .changes/unreleased/Fixed-20251106-161605.yaml create mode 100644 src/Bundle/ChillCalendarBundle/Controller/MyInvitationsController.php create mode 100644 src/Bundle/ChillCalendarBundle/Form/CancelType.php create mode 100644 src/Bundle/ChillCalendarBundle/Resources/views/Calendar/cancelCalendarByAccompanyingCourse.html.twig create mode 100644 src/Bundle/ChillCalendarBundle/Resources/views/Calendar/cancelCalendarByPerson.html.twig rename src/Bundle/ChillCalendarBundle/Resources/views/CalendarShortMessage/{short_message_canceled.twig => short_message_canceled.txt.twig} (100%) create mode 100644 src/Bundle/ChillCalendarBundle/Resources/views/Invitations/listByUser.html.twig create mode 100644 src/Bundle/ChillCalendarBundle/Tests/Controller/MyInvitationsControllerTest.php create mode 100644 src/Bundle/ChillTicketBundle/src/Controller/Admin/MotiveController.php create mode 100644 src/Bundle/ChillTicketBundle/src/Controller/AdminController.php create mode 100644 src/Bundle/ChillTicketBundle/src/Form/MotiveType.php create mode 100644 src/Bundle/ChillTicketBundle/src/Menu/AdminMenuBuilder.php create mode 100644 src/Bundle/ChillTicketBundle/src/MotiveDTO.php create mode 100644 src/Bundle/ChillTicketBundle/src/Resources/views/Admin/Motive/edit.html.twig create mode 100644 src/Bundle/ChillTicketBundle/src/Resources/views/Admin/Motive/index.html.twig create mode 100644 src/Bundle/ChillTicketBundle/src/Resources/views/Admin/Motive/new.html.twig create mode 100644 src/Bundle/ChillTicketBundle/src/Resources/views/Admin/Motive/view.html.twig create mode 100644 src/Bundle/ChillTicketBundle/src/Resources/views/Admin/index.html.twig create mode 100644 src/Bundle/ChillTicketBundle/src/migrations/Version20251022081554.php diff --git a/.changes/unreleased/Feature-20250808-120802.yaml b/.changes/unreleased/Feature-20250808-120802.yaml new file mode 100644 index 000000000..50d1eb8ba --- /dev/null +++ b/.changes/unreleased/Feature-20250808-120802.yaml @@ -0,0 +1,6 @@ +kind: Feature +body: Create invitation list in user menu +time: 2025-08-08T12:08:02.446361367+02:00 +custom: + Issue: "385" + SchemaChange: No schema change diff --git a/.changes/unreleased/Feature-20251007-155945.yaml b/.changes/unreleased/Feature-20251007-155945.yaml new file mode 100644 index 000000000..9b59e7ea5 --- /dev/null +++ b/.changes/unreleased/Feature-20251007-155945.yaml @@ -0,0 +1,6 @@ +kind: Feature +body: Admin interface for Motive entity +time: 2025-10-07T15:59:45.597029709+02:00 +custom: + Issue: "" + SchemaChange: No schema change diff --git a/.changes/unreleased/Feature-20251022-111552.yaml b/.changes/unreleased/Feature-20251022-111552.yaml new file mode 100644 index 000000000..058e40d82 --- /dev/null +++ b/.changes/unreleased/Feature-20251022-111552.yaml @@ -0,0 +1,6 @@ +kind: Feature +body: Add an admin interface for Motive entity +time: 2025-10-22T11:15:52.13937955+02:00 +custom: + Issue: "" + SchemaChange: Add columns or tables diff --git a/.changes/unreleased/Fixed-20251106-161605.yaml b/.changes/unreleased/Fixed-20251106-161605.yaml new file mode 100644 index 000000000..3962daf8a --- /dev/null +++ b/.changes/unreleased/Fixed-20251106-161605.yaml @@ -0,0 +1,6 @@ +kind: Fixed +body: Fix suggestion of referrer when creating notification for accompanyingPeriodWorkDocument +time: 2025-11-06T16:16:05.861813041+01:00 +custom: + Issue: "428" + SchemaChange: No schema change diff --git a/src/Bundle/ChillCalendarBundle/Controller/CalendarController.php b/src/Bundle/ChillCalendarBundle/Controller/CalendarController.php index 94db03b1f..6705d7bb7 100644 --- a/src/Bundle/ChillCalendarBundle/Controller/CalendarController.php +++ b/src/Bundle/ChillCalendarBundle/Controller/CalendarController.php @@ -13,6 +13,7 @@ namespace Chill\CalendarBundle\Controller; use Chill\CalendarBundle\Entity\Calendar; use Chill\CalendarBundle\Form\CalendarType; +use Chill\CalendarBundle\Form\CancelType; use Chill\CalendarBundle\RemoteCalendar\Connector\RemoteCalendarConnectorInterface; use Chill\CalendarBundle\Repository\CalendarACLAwareRepositoryInterface; use Chill\CalendarBundle\Security\Voter\CalendarVoter; @@ -30,6 +31,7 @@ use Chill\PersonBundle\Repository\PersonRepository; use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter; use Chill\PersonBundle\Security\Authorization\PersonVoter; use Chill\ThirdPartyBundle\Entity\ThirdParty; +use Doctrine\ORM\EntityManagerInterface; use http\Exception\UnexpectedValueException; use Psr\Log\LoggerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; @@ -60,6 +62,7 @@ class CalendarController extends AbstractController private readonly UserRepositoryInterface $userRepository, private readonly TranslatorInterface $translator, private readonly \Doctrine\Persistence\ManagerRegistry $managerRegistry, + private readonly EntityManagerInterface $em, ) {} /** @@ -111,6 +114,55 @@ class CalendarController extends AbstractController ]); } + #[Route(path: '/{_locale}/calendar/calendar/{id}/cancel', name: 'chill_calendar_calendar_cancel')] + public function cancelAction(Calendar $calendar, Request $request): Response + { + // Deal with sms being sent or not + // Communicate cancellation with the remote calendar. + + $this->denyAccessUnlessGranted(CalendarVoter::EDIT, $calendar); + + [$person, $accompanyingPeriod] = [$calendar->getPerson(), $calendar->getAccompanyingPeriod()]; + + $form = $this->createForm(CancelType::class, $calendar); + $form->add('submit', SubmitType::class); + + if ($accompanyingPeriod instanceof AccompanyingPeriod) { + $view = '@ChillCalendar/Calendar/cancelCalendarByAccompanyingCourse.html.twig'; + $redirectRoute = $this->generateUrl('chill_calendar_calendar_list_by_period', ['id' => $accompanyingPeriod->getId()]); + } elseif ($person instanceof Person) { + $view = '@ChillCalendar/Calendar/cancelCalendarByPerson.html.twig'; + $redirectRoute = $this->generateUrl('chill_calendar_calendar_list_by_person', ['id' => $person->getId()]); + } else { + throw new \RuntimeException('nor person or accompanying period'); + } + + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + + $this->logger->notice('A calendar event has been cancelled', [ + 'by_user' => $this->getUser()->getUsername(), + 'calendar_id' => $calendar->getId(), + ]); + + $calendar->setStatus($calendar::STATUS_CANCELED); + $calendar->setSmsStatus($calendar::SMS_CANCEL_PENDING); + $this->em->flush(); + + $this->addFlash('success', $this->translator->trans('chill_calendar.calendar_canceled')); + + return new RedirectResponse($redirectRoute); + } + + return $this->render($view, [ + 'calendar' => $calendar, + 'form' => $form->createView(), + 'accompanyingCourse' => $accompanyingPeriod, + 'person' => $person, + ]); + } + /** * Edit a calendar item. */ @@ -266,7 +318,7 @@ class CalendarController extends AbstractController } if (!$this->getUser() instanceof User) { - throw new UnauthorizedHttpException('you are not an user'); + throw new UnauthorizedHttpException('you are not a user'); } $view = '@ChillCalendar/Calendar/listByUser.html.twig'; diff --git a/src/Bundle/ChillCalendarBundle/Controller/MyInvitationsController.php b/src/Bundle/ChillCalendarBundle/Controller/MyInvitationsController.php new file mode 100644 index 000000000..7af5ac18f --- /dev/null +++ b/src/Bundle/ChillCalendarBundle/Controller/MyInvitationsController.php @@ -0,0 +1,58 @@ +denyAccessUnlessGranted('ROLE_USER'); + + $user = $this->getUser(); + + if (!$user instanceof User) { + throw new UnauthorizedHttpException('you are not a user'); + } + + $total = count($this->inviteRepository->findBy(['user' => $user])); + $paginator = $this->paginator->create($total); + + $invitations = $this->inviteRepository->findBy( + ['user' => $user], + ['createdAt' => 'DESC'], + $paginator->getItemsPerPage(), + $paginator->getCurrentPageFirstItemNumber() + ); + + $view = '@ChillCalendar/Invitations/listByUser.html.twig'; + + return $this->render($view, [ + 'invitations' => $invitations, + 'paginator' => $paginator, + 'templates' => $this->docGeneratorTemplateRepository->findByEntity(Calendar::class), + ]); + } +} diff --git a/src/Bundle/ChillCalendarBundle/DataFixtures/ORM/LoadCancelReason.php b/src/Bundle/ChillCalendarBundle/DataFixtures/ORM/LoadCancelReason.php index d7e552d5d..2a8e371e0 100644 --- a/src/Bundle/ChillCalendarBundle/DataFixtures/ORM/LoadCancelReason.php +++ b/src/Bundle/ChillCalendarBundle/DataFixtures/ORM/LoadCancelReason.php @@ -35,7 +35,7 @@ class LoadCancelReason extends Fixture implements FixtureGroupInterface $arr = [ ['name' => CancelReason::CANCELEDBY_USER], ['name' => CancelReason::CANCELEDBY_PERSON], - ['name' => CancelReason::CANCELEDBY_DONOTCOUNT], + ['name' => CancelReason::CANCELEDBY_OTHER], ]; foreach ($arr as $a) { diff --git a/src/Bundle/ChillCalendarBundle/Entity/Calendar.php b/src/Bundle/ChillCalendarBundle/Entity/Calendar.php index dad302193..c194c247e 100644 --- a/src/Bundle/ChillCalendarBundle/Entity/Calendar.php +++ b/src/Bundle/ChillCalendarBundle/Entity/Calendar.php @@ -269,6 +269,11 @@ class Calendar implements TrackCreationInterface, TrackUpdateInterface, HasCente return $this->cancelReason; } + public function isCanceled(): bool + { + return null !== $this->cancelReason; + } + public function getCenters(): ?iterable { return match ($this->getContext()) { diff --git a/src/Bundle/ChillCalendarBundle/Entity/CancelReason.php b/src/Bundle/ChillCalendarBundle/Entity/CancelReason.php index d4a2ed9a9..04192133c 100644 --- a/src/Bundle/ChillCalendarBundle/Entity/CancelReason.php +++ b/src/Bundle/ChillCalendarBundle/Entity/CancelReason.php @@ -18,14 +18,14 @@ use Doctrine\ORM\Mapping as ORM; #[ORM\Table(name: 'chill_calendar.cancel_reason')] class CancelReason { - final public const CANCELEDBY_DONOTCOUNT = 'CANCELEDBY_DONOTCOUNT'; + final public const CANCELEDBY_OTHER = 'CANCELEDBY_OTHER'; final public const CANCELEDBY_PERSON = 'CANCELEDBY_PERSON'; final public const CANCELEDBY_USER = 'CANCELEDBY_USER'; - #[ORM\Column(type: \Doctrine\DBAL\Types\Types::BOOLEAN)] - private ?bool $active = null; + #[ORM\Column(type: \Doctrine\DBAL\Types\Types::BOOLEAN, options: ['default' => true])] + private bool $active = true; #[ORM\Column(type: \Doctrine\DBAL\Types\Types::STRING, length: 255)] private ?string $canceledBy = null; diff --git a/src/Bundle/ChillCalendarBundle/Form/CancelReasonType.php b/src/Bundle/ChillCalendarBundle/Form/CancelReasonType.php index c0ac4ddb0..311d3ac02 100644 --- a/src/Bundle/ChillCalendarBundle/Form/CancelReasonType.php +++ b/src/Bundle/ChillCalendarBundle/Form/CancelReasonType.php @@ -15,7 +15,7 @@ use Chill\CalendarBundle\Entity\CancelReason; use Chill\MainBundle\Form\Type\TranslatableStringFormType; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\CheckboxType; -use Symfony\Component\Form\Extension\Core\Type\TextType; +use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -28,7 +28,14 @@ class CancelReasonType extends AbstractType ->add('active', CheckboxType::class, [ 'required' => false, ]) - ->add('canceledBy', TextType::class); + ->add('canceledBy', ChoiceType::class, [ + 'choices' => [ + 'chill_calendar.canceled_by.user' => CancelReason::CANCELEDBY_USER, + 'chill_calendar.canceled_by.person' => CancelReason::CANCELEDBY_PERSON, + 'chill_calendar.canceled_by.other' => CancelReason::CANCELEDBY_OTHER, + ], + 'required' => true, + ]); } public function configureOptions(OptionsResolver $resolver) diff --git a/src/Bundle/ChillCalendarBundle/Form/CancelType.php b/src/Bundle/ChillCalendarBundle/Form/CancelType.php new file mode 100644 index 000000000..ad41a6105 --- /dev/null +++ b/src/Bundle/ChillCalendarBundle/Form/CancelType.php @@ -0,0 +1,42 @@ +add('cancelReason', EntityType::class, [ + 'class' => CancelReason::class, + 'required' => true, + 'choice_label' => fn (CancelReason $cancelReason) => $this->translatableStringHelper->localize($cancelReason->getName()), + ]); + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'data_class' => Calendar::class, + + ]); + } +} diff --git a/src/Bundle/ChillCalendarBundle/Menu/UserMenuBuilder.php b/src/Bundle/ChillCalendarBundle/Menu/UserMenuBuilder.php index 3a062f7b8..672b53460 100644 --- a/src/Bundle/ChillCalendarBundle/Menu/UserMenuBuilder.php +++ b/src/Bundle/ChillCalendarBundle/Menu/UserMenuBuilder.php @@ -25,6 +25,13 @@ class UserMenuBuilder implements LocalMenuBuilderInterface if ($this->security->isGranted('ROLE_USER')) { $menu->addChild('My calendar list', [ 'route' => 'chill_calendar_calendar_list_my', + ]) + ->setExtras([ + 'order' => 8, + 'icon' => 'tasks', + ]); + $menu->addChild('invite.list.title', [ + 'route' => 'chill_calendar_invitations_list_my', ]) ->setExtras([ 'order' => 9, diff --git a/src/Bundle/ChillCalendarBundle/Messenger/Doctrine/CalendarEntityListener.php b/src/Bundle/ChillCalendarBundle/Messenger/Doctrine/CalendarEntityListener.php index 8f62fdcdb..2319fde99 100644 --- a/src/Bundle/ChillCalendarBundle/Messenger/Doctrine/CalendarEntityListener.php +++ b/src/Bundle/ChillCalendarBundle/Messenger/Doctrine/CalendarEntityListener.php @@ -21,6 +21,7 @@ namespace Chill\CalendarBundle\Messenger\Doctrine; use Chill\CalendarBundle\Entity\Calendar; use Chill\CalendarBundle\Messenger\Message\CalendarMessage; use Chill\CalendarBundle\Messenger\Message\CalendarRemovedMessage; +use Chill\MainBundle\Entity\User; use Doctrine\ORM\Event\PostPersistEventArgs; use Doctrine\ORM\Event\PostRemoveEventArgs; use Doctrine\ORM\Event\PostUpdateEventArgs; @@ -31,6 +32,17 @@ class CalendarEntityListener { public function __construct(private readonly MessageBusInterface $messageBus, private readonly Security $security) {} + private function getAuthenticatedUser(): User + { + $user = $this->security->getUser(); + + if (!$user instanceof User) { + throw new \LogicException('Expected an instance of User.'); + } + + return $user; + } + public function postPersist(Calendar $calendar, PostPersistEventArgs $args): void { if (!$calendar->preventEnqueueChanges) { @@ -38,7 +50,7 @@ class CalendarEntityListener new CalendarMessage( $calendar, CalendarMessage::CALENDAR_PERSIST, - $this->security->getUser() + $this->getAuthenticatedUser() ) ); } @@ -50,7 +62,7 @@ class CalendarEntityListener $this->messageBus->dispatch( new CalendarRemovedMessage( $calendar, - $this->security->getUser() + $this->getAuthenticatedUser() ) ); } @@ -58,12 +70,19 @@ class CalendarEntityListener public function postUpdate(Calendar $calendar, PostUpdateEventArgs $args): void { - if (!$calendar->preventEnqueueChanges) { + if ($calendar->getStatus() === $calendar::STATUS_CANCELED) { + $this->messageBus->dispatch( + new CalendarRemovedMessage( + $calendar, + $this->getAuthenticatedUser() + ) + ); + } elseif (!$calendar->preventEnqueueChanges) { $this->messageBus->dispatch( new CalendarMessage( $calendar, CalendarMessage::CALENDAR_UPDATE, - $this->security->getUser() + $this->getAuthenticatedUser() ) ); } diff --git a/src/Bundle/ChillCalendarBundle/Messenger/Message/CalendarRemovedMessage.php b/src/Bundle/ChillCalendarBundle/Messenger/Message/CalendarRemovedMessage.php index 53dcea28c..7d2e88fef 100644 --- a/src/Bundle/ChillCalendarBundle/Messenger/Message/CalendarRemovedMessage.php +++ b/src/Bundle/ChillCalendarBundle/Messenger/Message/CalendarRemovedMessage.php @@ -70,6 +70,8 @@ class CalendarRemovedMessage public function getRemoteId(): string { + dump($this->remoteId); + return $this->remoteId; } } diff --git a/src/Bundle/ChillCalendarBundle/Repository/CalendarRepository.php b/src/Bundle/ChillCalendarBundle/Repository/CalendarRepository.php index 3121854e5..e6f90f279 100644 --- a/src/Bundle/ChillCalendarBundle/Repository/CalendarRepository.php +++ b/src/Bundle/ChillCalendarBundle/Repository/CalendarRepository.php @@ -191,6 +191,7 @@ class CalendarRepository implements ObjectRepository $qb->expr()->eq('c.mainUser', ':user'), $qb->expr()->gte('c.startDate', ':startDate'), $qb->expr()->lte('c.endDate', ':endDate'), + $qb->expr()->isNull('c.cancelReason'), ) ) ->setParameters([ diff --git a/src/Bundle/ChillCalendarBundle/Repository/InviteRepository.php b/src/Bundle/ChillCalendarBundle/Repository/InviteRepository.php index 8778330f8..6cb34738c 100644 --- a/src/Bundle/ChillCalendarBundle/Repository/InviteRepository.php +++ b/src/Bundle/ChillCalendarBundle/Repository/InviteRepository.php @@ -41,7 +41,7 @@ class InviteRepository implements ObjectRepository /** * @return array|Invite[] */ - public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null) + public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array { return $this->entityRepository->findBy($criteria, $orderBy, $limit, $offset); } diff --git a/src/Bundle/ChillCalendarBundle/Resources/config/services/controller.yml b/src/Bundle/ChillCalendarBundle/Resources/config/services/controller.yml index a0457c5a8..cce562d7d 100644 --- a/src/Bundle/ChillCalendarBundle/Resources/config/services/controller.yml +++ b/src/Bundle/ChillCalendarBundle/Resources/config/services/controller.yml @@ -1,5 +1,6 @@ services: Chill\CalendarBundle\Controller\: autowire: true + autoconfigure: true resource: '../../../Controller' tags: ['controller.service_arguments'] diff --git a/src/Bundle/ChillCalendarBundle/Resources/views/Calendar/_list.html.twig b/src/Bundle/ChillCalendarBundle/Resources/views/Calendar/_list.html.twig index cff5c00cc..c827ca4f4 100644 --- a/src/Bundle/ChillCalendarBundle/Resources/views/Calendar/_list.html.twig +++ b/src/Bundle/ChillCalendarBundle/Resources/views/Calendar/_list.html.twig @@ -1,17 +1,23 @@ -{# list used in context of person or accompanyingPeriod #} +{# list used in context of person, accompanyingPeriod or user #} -{% if calendarItems|length > 0 %} -
- - {% for calendar in calendarItems %} - -
-
-
-
+
+
+
+
+
+ {% if calendar.status == 'canceled' %} +
+ {{ 'chill_calendar.canceled'|trans }}: + {{ calendar.cancelReason.name|localize_translatable_string }} +
+ {% endif %} +

+ {% if calendar.status == 'canceled' %} + + {% endif %} {% if context == 'person' and calendar.context == 'accompanying_period' %} @@ -19,6 +25,9 @@ {% endif %} + {% if calendar.status == 'canceled' %} + + {% endif %} {% if calendar.endDate.diff(calendar.startDate).days >= 1 %} {{ calendar.startDate|format_datetime('short', 'short') }} - {{ calendar.endDate|format_datetime('short', 'short') }} @@ -26,44 +35,46 @@ {{ calendar.startDate|format_datetime('short', 'short') }} - {{ calendar.endDate|format_datetime('none', 'short') }} {% endif %} -

- -
- - {{ calendar.duration|date('%H:%I') }} - {% if false == calendar.sendSMS or null == calendar.sendSMS %} - - {% else %} - {% if calendar.smsStatus == 'sms_sent' %} - - - - - {% else %} - - - - - {% endif %} + {% if calendar.status == 'canceled' %} + {% endif %} -
-
-
-
- -
-
    - {% if calendar.mainUser is not empty %} - {{ calendar.mainUser|chill_entity_render_box({'at_date': calendar.startDate}) }} +
    + + {{ calendar.duration|date('%H:%I') }} + {% if false == calendar.sendSMS or null == calendar.sendSMS %} + + {% else %} + {% if calendar.smsStatus == 'sms_sent' %} + + + + + {% else %} + + + + {% endif %} -
+ {% endif %}
+
- {% if calendar.comment.comment is not empty +
+
    + {% if calendar.mainUser is not empty %} + {{ calendar.mainUser|chill_entity_render_box({'at_date': calendar.startDate}) }} + {% endif %} +
+
+ +
+
+ + {% if calendar.comment.comment is not empty or calendar.users|length > 0 or calendar.thirdParties|length > 0 or calendar.users|length > 0 %} @@ -76,131 +87,133 @@ } %}
-
- {% endif %} +
+ {% endif %} + + {% if calendar.comment.comment is not empty %} +
+
+ {{ calendar.comment|chill_entity_render_box( { 'limit_lines': 3, 'metadata': false } ) }} +
+
+ {% endif %} + + {% if calendar.location is not empty %} +
+
+ {% if calendar.location.address is not same as(null) and calendar.location.name is not empty %} + {% 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 %} + {% endif %} + {% if calendar.location.phonenumber1 is not empty %} {{ calendar.location.phonenumber1|chill_format_phonenumber }}{% endif %} + {% if calendar.location.phonenumber2 is not empty %} {{ calendar.location.phonenumber2|chill_format_phonenumber }}{% endif %} +
+
+ {% endif %} + + {% if calendar.documents is not empty %} +
+
+ {{ include('@ChillCalendar/Calendar/_documents.twig.html') }} +
+
+ {% endif %} + + {% if calendar.activity is not null %} +
+
+
+
+

{{ 'Activity'|trans }}

+
+

+ + + {{ calendar.activity.type.name | localize_translatable_string }} + + {% if calendar.activity.emergency %} + {{ 'Emergency'|trans|upper }} + {% endif %} + +

+ +
    +
  • + + {{ 'Created by'|trans }} + {{ calendar.activity.createdBy|chill_entity_render_string({'at_date': calendar.activity.createdAt}) }}, {{ 'on'|trans }} {{ calendar.activity.createdAt|format_datetime('short', 'short') }} + +
  • + {% if is_granted('CHILL_ACTIVITY_SEE', calendar.activity) %} +
  • + +
  • + {% endif %} +
- {% if calendar.comment.comment is not empty %} -
-
- {{ calendar.comment|chill_entity_render_box( { 'limit_lines': 3, 'metadata': false } ) }}
- {% endif %} - - {% if calendar.location is not empty %} -
-
- {% if calendar.location.address is not same as(null) and calendar.location.name is not empty %} - {% 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 %} - {% endif %} - {% if calendar.location.phonenumber1 is not empty %} {{ calendar.location.phonenumber1|chill_format_phonenumber }}{% endif %} - {% if calendar.location.phonenumber2 is not empty %} {{ calendar.location.phonenumber2|chill_format_phonenumber }}{% endif %} -
-
- {% endif %} - -
-
- - {{ include('@ChillCalendar/Calendar/_documents.twig.html') }} -
+
+
+ {% endif %} - {% if calendar.activity is not null %} -
-
-
-
-

{{ 'Activity'|trans }}

-
-

- - - {{ calendar.activity.type.name | localize_translatable_string }} - - {% if calendar.activity.emergency %} - {{ 'Emergency'|trans|upper }} - {% endif %} - -

- -
    -
  • - - {{ 'Created by'|trans }} - {{ calendar.activity.createdBy|chill_entity_render_string({'at_date': calendar.activity.createdAt}) }}, {{ 'on'|trans }} {{ calendar.activity.createdAt|format_datetime('short', 'short') }} - -
  • - {% if is_granted('CHILL_ACTIVITY_SEE', calendar.activity) %} -
  • - -
  • - {% endif %} -
- -
-
+
+
-
+ + {% endif %} {% endif %} + {% if calendar.activity is null and ( + (calendar.context == 'accompanying_period' and is_granted('CHILL_ACTIVITY_CREATE', calendar.accompanyingPeriod)) + or + (calendar.context == 'person' and is_granted('CHILL_ACTIVITY_CREATE', calendar.person)) + ) + and calendar.status is not constant('STATUS_CANCELED', calendar) + %} +
  • + + {{ 'Transform to activity'|trans }} + +
  • + {% endif %} -
    -
      - {% if is_granted('CHILL_CALENDAR_DOC_EDIT', calendar) %} - {% if templates|length == 0 %} -
    • - - {{ 'chill_calendar.Add a document'|trans }} - -
    • - {% else %} -
    • - -
    • - {% endif %} - {% endif %} - {% if calendar.activity is null and ( - (calendar.context == 'accompanying_period' and is_granted('CHILL_ACTIVITY_CREATE', calendar.accompanyingPeriod)) - or - (calendar.context == 'person' and is_granted('CHILL_ACTIVITY_CREATE', calendar.person)) - ) - %} -
    • - - {{ 'Transform to activity'|trans }} - -
    • - {% endif %} - - {% if (calendar.isInvited(app.user)) %} + {% if calendar.isInvited(app.user) and not calendar.isCanceled %} {% set invite = calendar.inviteForUser(app.user) %}
    • {% endif %} - {% if is_granted('CHILL_CALENDAR_CALENDAR_EDIT', calendar) %} + + {% if is_granted('CHILL_CALENDAR_CALENDAR_EDIT', calendar) and calendar.status is not constant('STATUS_CANCELED', calendar) %}
    • +
    • + {{ 'Cancel'|trans }} +
    • {% endif %} + {% if is_granted('CHILL_CALENDAR_CALENDAR_DELETE', calendar) %}
    • -
    - -
    - {% endfor %} - - {% if calendarItems|length < paginator.getTotalItems %} - {{ chill_pagination(paginator) }} - {% endif %} -
    -{% endif %} + +
    + + diff --git a/src/Bundle/ChillCalendarBundle/Resources/views/Calendar/cancelCalendarByAccompanyingCourse.html.twig b/src/Bundle/ChillCalendarBundle/Resources/views/Calendar/cancelCalendarByAccompanyingCourse.html.twig new file mode 100644 index 000000000..2f6759725 --- /dev/null +++ b/src/Bundle/ChillCalendarBundle/Resources/views/Calendar/cancelCalendarByAccompanyingCourse.html.twig @@ -0,0 +1,29 @@ +{% extends "@ChillPerson/AccompanyingCourse/layout.html.twig" %} + +{% set activeRouteKey = 'chill_calendar_calendar_list' %} + +{% block title 'chill_calendar.cancel_calendar_item'|trans %} + +{% block content %} + + {{ form_start(form) }} + + {{ form_row(form.cancelReason) }} + + + + {{ form_end(form) }} + +{% endblock %} diff --git a/src/Bundle/ChillCalendarBundle/Resources/views/Calendar/cancelCalendarByPerson.html.twig b/src/Bundle/ChillCalendarBundle/Resources/views/Calendar/cancelCalendarByPerson.html.twig new file mode 100644 index 000000000..76196b23e --- /dev/null +++ b/src/Bundle/ChillCalendarBundle/Resources/views/Calendar/cancelCalendarByPerson.html.twig @@ -0,0 +1,29 @@ +{% extends "@ChillPerson/Person/layout.html.twig" %} + +{% set activeRouteKey = 'chill_calendar_calendar_list' %} + +{% block title 'chill_calendar.cancel_calendar_item'|trans %} + +{% block content %} + + {{ form_start(form) }} + + {{ form_row(form.cancelReason) }} + + + + {{ form_end(form) }} + +{% endblock %} diff --git a/src/Bundle/ChillCalendarBundle/Resources/views/Calendar/listByAccompanyingCourse.html.twig b/src/Bundle/ChillCalendarBundle/Resources/views/Calendar/listByAccompanyingCourse.html.twig index 7ce1003bc..96ddb3388 100644 --- a/src/Bundle/ChillCalendarBundle/Resources/views/Calendar/listByAccompanyingCourse.html.twig +++ b/src/Bundle/ChillCalendarBundle/Resources/views/Calendar/listByAccompanyingCourse.html.twig @@ -34,7 +34,18 @@ {% endif %}

    {% else %} - {{ include('@ChillCalendar/Calendar/_list.html.twig', {context: 'accompanying_course'}) }} + {% if calendarItems|length > 0 %} +
    + {% for calendar in calendarItems %} + {{ include('@ChillCalendar/Calendar/_list.html.twig', {context: 'accompanying_course'}) }} + {% endfor %} +
    + + {% if calendarItems|length < paginator.getTotalItems %} + {{ chill_pagination(paginator) }} + {% endif %} + + {% endif %} {% endif %}
      diff --git a/src/Bundle/ChillCalendarBundle/Resources/views/Calendar/listByPerson.html.twig b/src/Bundle/ChillCalendarBundle/Resources/views/Calendar/listByPerson.html.twig index 9e3b59d2a..dc44b721c 100644 --- a/src/Bundle/ChillCalendarBundle/Resources/views/Calendar/listByPerson.html.twig +++ b/src/Bundle/ChillCalendarBundle/Resources/views/Calendar/listByPerson.html.twig @@ -33,7 +33,17 @@ {% endif %}

      {% else %} - {{ include ('@ChillCalendar/Calendar/_list.html.twig', {context: 'person'}) }} + {% if calendarItems|length > 0 %} +
      + {% for calendar in calendarItems %} + {{ include ('@ChillCalendar/Calendar/_list.html.twig', {context: 'person'}) }} + {% endfor %} +
      + + {% if calendarItems|length < paginator.getTotalItems %} + {{ chill_pagination(paginator) }} + {% endif %} + {% endif %} {% endif %}
        diff --git a/src/Bundle/ChillCalendarBundle/Resources/views/CalendarShortMessage/short_message_canceled.twig b/src/Bundle/ChillCalendarBundle/Resources/views/CalendarShortMessage/short_message_canceled.txt.twig similarity index 100% rename from src/Bundle/ChillCalendarBundle/Resources/views/CalendarShortMessage/short_message_canceled.twig rename to src/Bundle/ChillCalendarBundle/Resources/views/CalendarShortMessage/short_message_canceled.txt.twig diff --git a/src/Bundle/ChillCalendarBundle/Resources/views/CancelReason/index.html.twig b/src/Bundle/ChillCalendarBundle/Resources/views/CancelReason/index.html.twig index 0668d8db5..bd99d6e6f 100644 --- a/src/Bundle/ChillCalendarBundle/Resources/views/CancelReason/index.html.twig +++ b/src/Bundle/ChillCalendarBundle/Resources/views/CancelReason/index.html.twig @@ -5,7 +5,7 @@ {% block table_entities_thead_tr %} {{ 'Id'|trans }} {{ 'Name'|trans }} - {{ 'canceledBy'|trans }} + {{ 'Canceled by'|trans }} {{ 'active'|trans }}   {% endblock %} @@ -40,4 +40,4 @@ {% endblock %} {% endembed %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/src/Bundle/ChillCalendarBundle/Resources/views/Invitations/listByUser.html.twig b/src/Bundle/ChillCalendarBundle/Resources/views/Invitations/listByUser.html.twig new file mode 100644 index 000000000..c7b7ecc86 --- /dev/null +++ b/src/Bundle/ChillCalendarBundle/Resources/views/Invitations/listByUser.html.twig @@ -0,0 +1,40 @@ +{% extends "@ChillMain/layout.html.twig" %} + +{% set activeRouteKey = 'chill_calendar_invitations_list' %} + +{% block title %}{{ 'invite.list.title'|trans }}{% endblock title %} + +{% block content %} + +

        {{ 'invite.list.title'|trans }}

        + + {% if invitations|length == 0 %} +

        + {{ "invite.list.none"|trans }} +

        + {% else %} +
        + {% for invitation in invitations %} + {% set calendar = invitation.getCalendar %} + {{ include('@ChillCalendar/Calendar/_list.html.twig', {context: 'user'}) }} + {% endfor %} +
        + + {% if invitations|length < paginator.getTotalItems %} + {{ chill_pagination(paginator) }} + {% endif %} + {% endif %} + +{% endblock %} + +{% block js %} + {{ parent() }} + {{ encore_entry_script_tags('mod_answer') }} + {{ encore_entry_script_tags('mod_document_action_buttons_group') }} +{% endblock %} + +{% block css %} + {{ parent() }} + {{ encore_entry_link_tags('mod_answer') }} + {{ encore_entry_link_tags('mod_document_action_buttons_group') }} +{% endblock %} diff --git a/src/Bundle/ChillCalendarBundle/Service/ShortMessageNotification/DefaultShortMessageForCalendarBuilder.php b/src/Bundle/ChillCalendarBundle/Service/ShortMessageNotification/DefaultShortMessageForCalendarBuilder.php index dfce0548c..bce9b1487 100644 --- a/src/Bundle/ChillCalendarBundle/Service/ShortMessageNotification/DefaultShortMessageForCalendarBuilder.php +++ b/src/Bundle/ChillCalendarBundle/Service/ShortMessageNotification/DefaultShortMessageForCalendarBuilder.php @@ -19,6 +19,7 @@ declare(strict_types=1); namespace Chill\CalendarBundle\Service\ShortMessageNotification; use Chill\CalendarBundle\Entity\Calendar; +use Chill\CalendarBundle\Entity\CancelReason; use libphonenumber\PhoneNumberFormat; use libphonenumber\PhoneNumberUtil; use Symfony\Component\Notifier\Message\SmsMessage; @@ -57,7 +58,7 @@ class DefaultShortMessageForCalendarBuilder implements ShortMessageForCalendarBu $this->phoneUtil->format($person->getMobilenumber(), PhoneNumberFormat::E164), $this->engine->render('@ChillCalendar/CalendarShortMessage/short_message.txt.twig', ['calendar' => $calendar]), ); - } elseif (Calendar::SMS_CANCEL_PENDING === $calendar->getSmsStatus()) { + } elseif (Calendar::SMS_CANCEL_PENDING === $calendar->getSmsStatus() && (null === $calendar->getCancelReason() || CancelReason::CANCELEDBY_PERSON !== $calendar->getCancelReason()->getCanceledBy())) { $toUsers[] = new SmsMessage( $this->phoneUtil->format($person->getMobilenumber(), PhoneNumberFormat::E164), $this->engine->render('@ChillCalendar/CalendarShortMessage/short_message_canceled.txt.twig', ['calendar' => $calendar]), diff --git a/src/Bundle/ChillCalendarBundle/Tests/Controller/MyInvitationsControllerTest.php b/src/Bundle/ChillCalendarBundle/Tests/Controller/MyInvitationsControllerTest.php new file mode 100644 index 000000000..32ed70456 --- /dev/null +++ b/src/Bundle/ChillCalendarBundle/Tests/Controller/MyInvitationsControllerTest.php @@ -0,0 +1,292 @@ +prophesize(InviteRepository::class); + $paginatorFactory = $this->prophesize(PaginatorFactory::class); + $docGeneratorTemplateRepository = $this->prophesize(DocGeneratorTemplateRepositoryInterface::class); + + // Create controller instance + $this->controller = new MyInvitationsController( + $inviteRepository->reveal(), + $paginatorFactory->reveal(), + $docGeneratorTemplateRepository->reveal() + ); + + // Set up necessary services for AbstractController + $authorizationChecker = $this->prophesize(AuthorizationCheckerInterface::class); + $tokenStorage = $this->prophesize(TokenStorageInterface::class); + $twig = $this->prophesize(Environment::class); + + // Use reflection to set the container + $reflection = new \ReflectionClass($this->controller); + $containerProperty = $reflection->getParentClass()->getProperty('container'); + $containerProperty->setAccessible(true); + + // Create a mock container + $container = $this->prophesize(\Psr\Container\ContainerInterface::class); + $container->has('security.authorization_checker')->willReturn(true); + $container->get('security.authorization_checker')->willReturn($authorizationChecker->reveal()); + $container->has('security.token_storage')->willReturn(true); + $container->get('security.token_storage')->willReturn($tokenStorage->reveal()); + $container->has('twig')->willReturn(true); + $container->get('twig')->willReturn($twig->reveal()); + + $containerProperty->setValue($this->controller, $container->reveal()); + } + + public function testMyInvitationsReturnsCorrectAmountOfInvitations(): void + { + // Create test user + $user = new User(); + $user->setUsername('testuser'); + + // Create test invitations + $invite1 = new Invite(); + $invite1->setUser($user); + $invite1->setStatus(Invite::PENDING); + + $invite2 = new Invite(); + $invite2->setUser($user); + $invite2->setStatus(Invite::ACCEPTED); + + $invite3 = new Invite(); + $invite3->setUser($user); + $invite3->setStatus(Invite::DECLINED); + + $allInvitations = [$invite1, $invite2, $invite3]; + $paginatedInvitations = [$invite1, $invite2]; // First page with 2 items per page + + // Set up repository prophecies + $inviteRepository = $this->prophesize(InviteRepository::class); + $inviteRepository->findBy(['user' => $user])->willReturn($allInvitations); + $inviteRepository->findBy( + ['user' => $user], + ['createdAt' => 'DESC'], + 2, // items per page + 0 // offset + )->willReturn($paginatedInvitations); + + // Set up paginator prophecies + $paginator = $this->prophesize(PaginatorInterface::class); + $paginator->getItemsPerPage()->willReturn(2); + $paginator->getCurrentPageFirstItemNumber()->willReturn(0); + + $paginatorFactory = $this->prophesize(PaginatorFactory::class); + $paginatorFactory->create(3)->willReturn($paginator->reveal()); + + // Set up doc generator repository + $docGeneratorTemplateRepository = $this->prophesize(DocGeneratorTemplateRepositoryInterface::class); + $docGeneratorTemplateRepository->findByEntity(Calendar::class)->willReturn([]); + + // Create controller with mocked dependencies + $controller = new MyInvitationsController( + $inviteRepository->reveal(), + $paginatorFactory->reveal(), + $docGeneratorTemplateRepository->reveal() + ); + + // Set up authorization checker to return true for ROLE_USER + $authorizationChecker = $this->prophesize(AuthorizationCheckerInterface::class); + $authorizationChecker->isGranted('ROLE_USER', null)->willReturn(true); + + // Set up token storage to return user + $token = $this->prophesize(TokenInterface::class); + $token->getUser()->willReturn($user); + $tokenStorage = $this->prophesize(TokenStorageInterface::class); + $tokenStorage->getToken()->willReturn($token->reveal()); + + // Set up twig to return a response + $twig = $this->prophesize(Environment::class); + $twig->render('@ChillCalendar/Invitations/listByUser.html.twig', [ + 'invitations' => $paginatedInvitations, + 'paginator' => $paginator->reveal(), + 'templates' => [], + ])->willReturn('rendered content'); + + // Set up container + $container = $this->prophesize(\Psr\Container\ContainerInterface::class); + $container->has('security.authorization_checker')->willReturn(true); + $container->get('security.authorization_checker')->willReturn($authorizationChecker->reveal()); + $container->has('security.token_storage')->willReturn(true); + $container->get('security.token_storage')->willReturn($tokenStorage->reveal()); + $container->has('twig')->willReturn(true); + $container->get('twig')->willReturn($twig->reveal()); + + // Use reflection to set the container + $reflection = new \ReflectionClass($controller); + $containerProperty = $reflection->getParentClass()->getProperty('container'); + $containerProperty->setAccessible(true); + $containerProperty->setValue($controller, $container->reveal()); + + // Create request + $request = new Request(); + + // Execute the action + $response = $controller->myInvitations($request); + + // Assert that response is successful + self::assertInstanceOf(Response::class, $response); + self::assertSame(200, $response->getStatusCode()); + self::assertSame('rendered content', $response->getContent()); + } + + public function testMyInvitationsPageLoads(): void + { + // Create test user + $user = new User(); + $user->setUsername('testuser'); + + // Set up repository prophecies - no invitations + $inviteRepository = $this->prophesize(InviteRepository::class); + $inviteRepository->findBy(['user' => $user])->willReturn([]); + $inviteRepository->findBy( + ['user' => $user], + ['createdAt' => 'DESC'], + 20, // default items per page + 0 // offset + )->willReturn([]); + + // Set up paginator prophecies + $paginator = $this->prophesize(PaginatorInterface::class); + $paginator->getItemsPerPage()->willReturn(20); + $paginator->getCurrentPageFirstItemNumber()->willReturn(0); + + $paginatorFactory = $this->prophesize(PaginatorFactory::class); + $paginatorFactory->create(0)->willReturn($paginator->reveal()); + + // Set up doc generator repository + $docGeneratorTemplateRepository = $this->prophesize(DocGeneratorTemplateRepositoryInterface::class); + $docGeneratorTemplateRepository->findByEntity(Calendar::class)->willReturn([]); + + // Create controller with mocked dependencies + $controller = new MyInvitationsController( + $inviteRepository->reveal(), + $paginatorFactory->reveal(), + $docGeneratorTemplateRepository->reveal() + ); + + // Set up authorization checker to return true for ROLE_USER + $authorizationChecker = $this->prophesize(AuthorizationCheckerInterface::class); + $authorizationChecker->isGranted('ROLE_USER', null)->willReturn(true); + + // Set up token storage to return user + $token = $this->prophesize(TokenInterface::class); + $token->getUser()->willReturn($user); + $tokenStorage = $this->prophesize(TokenStorageInterface::class); + $tokenStorage->getToken()->willReturn($token->reveal()); + + // Set up twig to return a response + $twig = $this->prophesize(Environment::class); + $twig->render('@ChillCalendar/Invitations/listByUser.html.twig', [ + 'invitations' => [], + 'paginator' => $paginator->reveal(), + 'templates' => [], + ])->willReturn('empty page content'); + + // Set up container + $container = $this->prophesize(\Psr\Container\ContainerInterface::class); + $container->has('security.authorization_checker')->willReturn(true); + $container->get('security.authorization_checker')->willReturn($authorizationChecker->reveal()); + $container->has('security.token_storage')->willReturn(true); + $container->get('security.token_storage')->willReturn($tokenStorage->reveal()); + $container->has('twig')->willReturn(true); + $container->get('twig')->willReturn($twig->reveal()); + + // Use reflection to set the container + $reflection = new \ReflectionClass($controller); + $containerProperty = $reflection->getParentClass()->getProperty('container'); + $containerProperty->setAccessible(true); + $containerProperty->setValue($controller, $container->reveal()); + + // Create request + $request = new Request(); + + // Execute the action + $response = $controller->myInvitations($request); + + // Assert that page loads successfully + self::assertInstanceOf(Response::class, $response); + self::assertSame(200, $response->getStatusCode()); + self::assertSame('empty page content', $response->getContent()); + } + + public function testMyInvitationsRequiresAuthentication(): void + { + // Create controller with minimal dependencies + $inviteRepository = $this->prophesize(InviteRepository::class); + $paginatorFactory = $this->prophesize(PaginatorFactory::class); + $docGeneratorTemplateRepository = $this->prophesize(DocGeneratorTemplateRepositoryInterface::class); + + $controller = new MyInvitationsController( + $inviteRepository->reveal(), + $paginatorFactory->reveal(), + $docGeneratorTemplateRepository->reveal() + ); + + // Set up authorization checker to return false for ROLE_USER + $authorizationChecker = $this->prophesize(AuthorizationCheckerInterface::class); + $authorizationChecker->isGranted('ROLE_USER')->willReturn(false); + $authorizationChecker->isGranted('ROLE_USER', null)->willReturn(false); + + // Set up container + $container = $this->prophesize(\Psr\Container\ContainerInterface::class); + $container->has('security.authorization_checker')->willReturn(true); + $container->get('security.authorization_checker')->willReturn($authorizationChecker->reveal()); + + // Use reflection to set the container + $reflection = new \ReflectionClass($controller); + $containerProperty = $reflection->getParentClass()->getProperty('container'); + $containerProperty->setAccessible(true); + $containerProperty->setValue($controller, $container->reveal()); + + // Create request + $request = new Request(); + + // Expect AccessDeniedException + $this->expectException(\Symfony\Component\Security\Core\Exception\AccessDeniedException::class); + + // Execute the action + $controller->myInvitations($request); + } +} diff --git a/src/Bundle/ChillCalendarBundle/translations/messages.fr.yml b/src/Bundle/ChillCalendarBundle/translations/messages.fr.yml index cfb2fe057..8717dcd9f 100644 --- a/src/Bundle/ChillCalendarBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillCalendarBundle/translations/messages.fr.yml @@ -31,8 +31,7 @@ Will send SMS: Un SMS de rappel sera envoyé Will not send SMS: Aucun SMS de rappel ne sera envoyé SMS already sent: Un SMS a été envoyé -canceledBy: supprimé par -Canceled by: supprimé par +Canceled by: Annulé par Calendar configuration: Gestion des rendez-vous crud: @@ -44,6 +43,14 @@ crud: title_edit: Modifier le motif d'annulation chill_calendar: + canceled: Annulé + cancel_reason: Raison d'annulation + cancel_calendar_item: Annuler rendez-vous + calendar_canceled: Le rendez-vous a été annulé + canceled_by: + user: Utilisateur + person: Usager + other: Autre Document: Document d'un rendez-vous form: The main user is mandatory. He will organize the appointment.: L'utilisateur principal est obligatoire. Il est l'organisateur de l'événement. @@ -86,6 +93,9 @@ invite: declined: Refusé pending: En attente tentative: Accepté provisoirement + list: + none: Il n'y aucun invitation + title: Mes invitations # exports Exports of calendar: Exports des rendez-vous diff --git a/src/Bundle/ChillDocGeneratorBundle/Repository/DocGeneratorTemplateRepositoryInterface.php b/src/Bundle/ChillDocGeneratorBundle/Repository/DocGeneratorTemplateRepositoryInterface.php index e5071e76a..f2e3cf629 100644 --- a/src/Bundle/ChillDocGeneratorBundle/Repository/DocGeneratorTemplateRepositoryInterface.php +++ b/src/Bundle/ChillDocGeneratorBundle/Repository/DocGeneratorTemplateRepositoryInterface.php @@ -20,4 +20,9 @@ use Doctrine\Persistence\ObjectRepository; interface DocGeneratorTemplateRepositoryInterface extends ObjectRepository { public function countByEntity(string $entity): int; + + /** + * @return array|DocGeneratorTemplate[] + */ + public function findByEntity(string $entity, ?int $start = 0, ?int $limit = 50): array; } diff --git a/src/Bundle/ChillMainBundle/CRUD/Controller/CRUDController.php b/src/Bundle/ChillMainBundle/CRUD/Controller/CRUDController.php index 19e9014da..ccba39848 100644 --- a/src/Bundle/ChillMainBundle/CRUD/Controller/CRUDController.php +++ b/src/Bundle/ChillMainBundle/CRUD/Controller/CRUDController.php @@ -102,7 +102,6 @@ class CRUDController extends AbstractController Resolver::class => Resolver::class, SerializerInterface::class => SerializerInterface::class, FilterOrderHelperFactoryInterface::class => FilterOrderHelperFactoryInterface::class, - ManagerRegistry::class => ManagerRegistry::class, ] ); } @@ -674,7 +673,7 @@ class CRUDController extends AbstractController protected function getManagerRegistry(): ManagerRegistry { - return $this->container->get(ManagerRegistry::class); + return $this->container->get('doctrine'); } /** diff --git a/src/Bundle/ChillMainBundle/Pagination/PaginatorFactory.php b/src/Bundle/ChillMainBundle/Pagination/PaginatorFactory.php index 9a809287b..fa4897224 100644 --- a/src/Bundle/ChillMainBundle/Pagination/PaginatorFactory.php +++ b/src/Bundle/ChillMainBundle/Pagination/PaginatorFactory.php @@ -17,7 +17,7 @@ use Symfony\Component\Routing\RouterInterface; /** * Create paginator instances. */ -final readonly class PaginatorFactory implements PaginatorFactoryInterface +class PaginatorFactory implements PaginatorFactoryInterface { final public const DEFAULT_CURRENT_PAGE_KEY = 'page'; @@ -29,16 +29,16 @@ final readonly class PaginatorFactory implements PaginatorFactoryInterface /** * the request stack. */ - private RequestStack $requestStack, + private readonly RequestStack $requestStack, /** * the router and generator for url. */ - private RouterInterface $router, + private readonly RouterInterface $router, /** * the default item per page. This may be overriden by * the request or inside the paginator. */ - private int $itemPerPage = 20, + private readonly int $itemPerPage = 20, ) {} /** diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourseWorkEdit/components/DocumentsList.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourseWorkEdit/components/DocumentsList.vue index 77fa443d4..7c9892de9 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourseWorkEdit/components/DocumentsList.vue +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourseWorkEdit/components/DocumentsList.vue @@ -97,7 +97,7 @@ @click=" goToGenerateDocumentNotification( d, - false, + true, ) " > diff --git a/src/Bundle/ChillTicketBundle/src/Controller/Admin/MotiveController.php b/src/Bundle/ChillTicketBundle/src/Controller/Admin/MotiveController.php new file mode 100644 index 000000000..891f82d07 --- /dev/null +++ b/src/Bundle/ChillTicketBundle/src/Controller/Admin/MotiveController.php @@ -0,0 +1,54 @@ +addOrderBy('e.ordering', 'ASC'); + + return parent::orderQuery($action, $query, $request, $paginator); + } + + protected function createFormFor(string $action, mixed $entity, ?string $formClass = null, array $formOptions = []): FormInterface + { + if (in_array($action, ['new', 'edit'], true) && $entity instanceof Motive) { + $dto = MotiveDTO::fromMotive($entity); + + return parent::createFormFor($action, $dto, $formClass, $formOptions); + } + + return parent::createFormFor($action, $entity, $formClass, $formOptions); + } + + protected function onFormValid(string $action, object $entity, FormInterface $form, Request $request): void + { + if (in_array($action, ['new', 'edit'], true) && $entity instanceof Motive) { + $dto = $form->getData(); + if ($dto instanceof MotiveDTO) { + $dto->applyToMotive($entity); + } + } + + parent::onFormValid($action, $entity, $form, $request); + } +} diff --git a/src/Bundle/ChillTicketBundle/src/Controller/AdminController.php b/src/Bundle/ChillTicketBundle/src/Controller/AdminController.php new file mode 100644 index 000000000..9bf5028c1 --- /dev/null +++ b/src/Bundle/ChillTicketBundle/src/Controller/AdminController.php @@ -0,0 +1,31 @@ +render('@ChillTicket/Admin/index.html.twig'); + } +} diff --git a/src/Bundle/ChillTicketBundle/src/DependencyInjection/ChillTicketExtension.php b/src/Bundle/ChillTicketBundle/src/DependencyInjection/ChillTicketExtension.php index 596cb655a..a990cfa16 100644 --- a/src/Bundle/ChillTicketBundle/src/DependencyInjection/ChillTicketExtension.php +++ b/src/Bundle/ChillTicketBundle/src/DependencyInjection/ChillTicketExtension.php @@ -11,8 +11,10 @@ declare(strict_types=1); namespace Chill\TicketBundle\DependencyInjection; +use Chill\TicketBundle\Controller\Admin\MotiveController; use Chill\TicketBundle\Controller\MotiveApiController; use Chill\TicketBundle\Entity\Motive; +use Chill\TicketBundle\Form\MotiveType; use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface; @@ -36,6 +38,7 @@ class ChillTicketExtension extends Extension implements PrependExtensionInterfac public function prepend(ContainerBuilder $container) { $this->prependApi($container); + $this->prependCruds($container); } private function prependApi(ContainerBuilder $container): void @@ -66,4 +69,37 @@ class ChillTicketExtension extends Extension implements PrependExtensionInterfac ], ]); } + + protected function prependCruds(ContainerBuilder $container): void + { + $container->prependExtensionConfig('chill_main', [ + 'cruds' => [ + [ + 'class' => Motive::class, + 'name' => 'motive', + 'base_path' => '/admin/ticket/motive', + 'form_class' => MotiveType::class, + 'controller' => MotiveController::class, + 'actions' => [ + 'index' => [ + 'template' => '@ChillTicket/Admin/Motive/index.html.twig', + 'role' => 'ROLE_ADMIN', + ], + 'new' => [ + 'role' => 'ROLE_ADMIN', + 'template' => '@ChillTicket/Admin/Motive/new.html.twig', + ], + 'view' => [ + 'role' => 'ROLE_ADMIN', + 'template' => '@ChillTicket/Admin/Motive/view.html.twig', + ], + 'edit' => [ + 'role' => 'ROLE_ADMIN', + 'template' => '@ChillTicket/Admin/Motive/edit.html.twig', + ], + ], + ], + ], + ]); + } } diff --git a/src/Bundle/ChillTicketBundle/src/Entity/Motive.php b/src/Bundle/ChillTicketBundle/src/Entity/Motive.php index 1be0fe47f..a22898b14 100644 --- a/src/Bundle/ChillTicketBundle/src/Entity/Motive.php +++ b/src/Bundle/ChillTicketBundle/src/Entity/Motive.php @@ -51,13 +51,15 @@ class Motive #[ORM\ManyToOne(targetEntity: Motive::class, inversedBy: 'children')] private ?Motive $parent = null; - /** * @var Collection&Selectable */ - #[ORM\OneToMany(targetEntity: Motive::class, mappedBy: 'parent')] + #[ORM\OneToMany(mappedBy: 'parent', targetEntity: Motive::class)] private Collection&Selectable $children; + #[ORM\Column(name: 'ordering', type: \Doctrine\DBAL\Types\Types::FLOAT, nullable: true, options: ['default' => '0.0'])] + private float $ordering = 0; + public function __construct() { $this->storedObjects = new ArrayCollection(); @@ -218,4 +220,14 @@ class Motive return $collection; } + + public function getOrdering(): float + { + return $this->ordering; + } + + public function setOrdering(float $ordering): void + { + $this->ordering = $ordering; + } } diff --git a/src/Bundle/ChillTicketBundle/src/Form/MotiveType.php b/src/Bundle/ChillTicketBundle/src/Form/MotiveType.php new file mode 100644 index 000000000..61251cb40 --- /dev/null +++ b/src/Bundle/ChillTicketBundle/src/Form/MotiveType.php @@ -0,0 +1,71 @@ +add('label', TranslatableStringFormType::class, [ + 'label' => 'Label', + 'required' => true, + ]) + ->add('active', CheckboxType::class, [ + 'label' => 'Active', + 'required' => false, + ]) + ->add('makeTicketEmergency', EnumType::class, [ + 'class' => EmergencyStatusEnum::class, + 'label' => 'emergency?', + 'required' => false, + 'placeholder' => 'Choose an option...', + ]) + ->add('supplementaryComments', ChillCollectionType::class, [ + 'entry_type' => TextType::class, + 'allow_add' => true, + 'allow_delete' => true, + 'by_reference' => true, + 'label' => 'Supplementary comments', + 'required' => false, + ]) + ->add('ordering', NumberType::class, [ + 'scale' => 4, + 'label' => 'Ordering', + ]) + ; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => MotiveDTO::class, + ]); + } + + public function getBlockPrefix(): string + { + return 'chill_ticketbundle_motive'; + } +} diff --git a/src/Bundle/ChillTicketBundle/src/Menu/AdminMenuBuilder.php b/src/Bundle/ChillTicketBundle/src/Menu/AdminMenuBuilder.php new file mode 100644 index 000000000..b12dab2bf --- /dev/null +++ b/src/Bundle/ChillTicketBundle/src/Menu/AdminMenuBuilder.php @@ -0,0 +1,45 @@ +authorizationChecker->isGranted('ROLE_ADMIN')) { + return; + } + + $menu->addChild('Tickets', [ + 'route' => 'chill_ticket_admin_index', + ]) + ->setAttribute('class', 'list-group-item-header') + ->setExtras([ + 'order' => 7500, + ]); + + $menu->addChild('admin.ticket.motive.menu', [ + 'route' => 'chill_crud_motive_index', + ])->setExtras(['order' => 7510]); + } + + public static function getMenuIds(): array + { + return ['admin_section', 'admin_ticket']; + } +} diff --git a/src/Bundle/ChillTicketBundle/src/MotiveDTO.php b/src/Bundle/ChillTicketBundle/src/MotiveDTO.php new file mode 100644 index 000000000..e84565ee2 --- /dev/null +++ b/src/Bundle/ChillTicketBundle/src/MotiveDTO.php @@ -0,0 +1,67 @@ +supplementaryComments->isEmpty()) { + $this->supplementaryComments = new ArrayCollection(); + } + } + + public static function fromMotive(Motive $motive): self + { + $supplementaryCommentsCollection = new ArrayCollection(); + + foreach ($motive->getSupplementaryComments() as $comment) { + $supplementaryCommentsCollection->add($comment['label']); + } + + return new self( + label: $motive->getLabel(), + active: $motive->isActive(), + supplementaryComments: $supplementaryCommentsCollection, + makeTicketEmergency: $motive->getMakeTicketEmergency(), + ordering: $motive->getOrdering(), + ); + } + + public function applyToMotive(Motive $motive): void + { + $motive->setLabel($this->label); + $motive->setActive($this->active); + $motive->setMakeTicketEmergency($this->makeTicketEmergency); + $motive->setOrdering($this->ordering); + + $supplementaryCommentsArray = []; + + foreach ($this->supplementaryComments as $supplementaryComment) { + $supplementaryCommentsArray[] = ['label' => $supplementaryComment]; + } + + $motive->setSupplementaryComment($supplementaryCommentsArray); + } +} diff --git a/src/Bundle/ChillTicketBundle/src/Resources/views/Admin/Motive/edit.html.twig b/src/Bundle/ChillTicketBundle/src/Resources/views/Admin/Motive/edit.html.twig new file mode 100644 index 000000000..73c005374 --- /dev/null +++ b/src/Bundle/ChillTicketBundle/src/Resources/views/Admin/Motive/edit.html.twig @@ -0,0 +1,15 @@ +{% extends '@ChillMain/CRUD/Admin/index.html.twig' %} + +{% block title %} + {% include('@ChillMain/CRUD/_edit_title.html.twig') %} +{% endblock %} + +{% block js %} +{{ parent() }} +{% endblock %} + +{% block admin_content %} + {% embed '@ChillMain/CRUD/_edit_content.html.twig' %} + {% block content_form_actions_save_and_view %}{% endblock %} + {% endembed %} +{% endblock %} diff --git a/src/Bundle/ChillTicketBundle/src/Resources/views/Admin/Motive/index.html.twig b/src/Bundle/ChillTicketBundle/src/Resources/views/Admin/Motive/index.html.twig new file mode 100644 index 000000000..fe3959309 --- /dev/null +++ b/src/Bundle/ChillTicketBundle/src/Resources/views/Admin/Motive/index.html.twig @@ -0,0 +1,68 @@ +{% extends '@ChillMain/Admin/layoutWithVerticalMenu.html.twig' %} + +{% block title %}{{ 'admin.motive.list.title'|trans }}{% endblock title %} + +{% block admin_content %} + +

        {{ 'admin.motive.list.title'|trans }}

        + + + + + + + + + + + + + + {% for entity in entities %} + + + + + + + + + {% endfor %} + +
        {{ 'Label'|trans }}{{ 'Active'|trans }}{{ 'emergency?'|trans }}{{ 'Ordering'|trans }}{{ 'Supplementary comments'|trans }} 
        {{ entity.label|localize_translatable_string }} + {%- if entity.isActive -%} + + {%- else -%} + + {%- endif -%} + + {%- if entity.makeTicketEmergency -%} + {{ entity.makeTicketEmergency.value|trans }} + {%- else -%} + - + {%- endif -%} + + {{ entity.ordering }} + + {{ entity.supplementaryComments|length }} + +
          +
        • + +
        • +
        • + +
        • +
        +
        + + {{ chill_pagination(paginator) }} + + +{% endblock %} diff --git a/src/Bundle/ChillTicketBundle/src/Resources/views/Admin/Motive/new.html.twig b/src/Bundle/ChillTicketBundle/src/Resources/views/Admin/Motive/new.html.twig new file mode 100644 index 000000000..428c3fa75 --- /dev/null +++ b/src/Bundle/ChillTicketBundle/src/Resources/views/Admin/Motive/new.html.twig @@ -0,0 +1,15 @@ +{% extends '@ChillMain/CRUD/Admin/index.html.twig' %} + +{% block title %} + {% include('@ChillMain/CRUD/_new_title.html.twig') %} +{% endblock %} + +{% block js %} +{{ parent() }} +{% endblock %} + +{% block admin_content %} + {% embed '@ChillMain/CRUD/_new_content.html.twig' %} + {% block content_form_actions_save_and_show %}{% endblock %} + {% endembed %} +{% endblock %} diff --git a/src/Bundle/ChillTicketBundle/src/Resources/views/Admin/Motive/view.html.twig b/src/Bundle/ChillTicketBundle/src/Resources/views/Admin/Motive/view.html.twig new file mode 100644 index 000000000..8ac3176ff --- /dev/null +++ b/src/Bundle/ChillTicketBundle/src/Resources/views/Admin/Motive/view.html.twig @@ -0,0 +1,68 @@ +{% extends '@ChillMain/Admin/layoutWithVerticalMenu.html.twig' %} + +{% block title %}{{ 'admin.motive.view.title'|trans }}{% endblock title %} + +{% block admin_content %} + +

        {{ 'admin.motive.view.title'|trans }}

        + + + + + + + + + + + + + + + + + + + + +
        {{ 'Id'|trans }}{{ entity.id }}
        {{ 'Label'|trans }}{{ entity.label|localize_translatable_string }}
        {{ 'Active'|trans }} + {%- if entity.isActive -%} + {{ 'Yes'|trans }} + {%- else -%} + {{ 'No'|trans }} + {%- endif -%} +
        {{ 'emergency?'|trans }} + {%- if entity.makeTicketEmergency -%} + {{ entity.makeTicketEmergency.value|trans }} + {%- else -%} + - + {%- endif -%} +
        + + {% if entity.supplementaryComments is not empty %} +

        {{ 'Supplementary comments'|trans }}

        +
        + {% for comment in entity.supplementaryComments %} +
        +
        +
        + {{ comment.label|raw }} +
        +
        +
        + {% endfor %} +
        + {% else %} +

        {{ 'Supplementary comments'|trans }}

        +

        {{ 'No supplementary comments'|trans }}

        + {% endif %} + + +{% endblock %} diff --git a/src/Bundle/ChillTicketBundle/src/Resources/views/Admin/index.html.twig b/src/Bundle/ChillTicketBundle/src/Resources/views/Admin/index.html.twig new file mode 100644 index 000000000..8a5ec98f0 --- /dev/null +++ b/src/Bundle/ChillTicketBundle/src/Resources/views/Admin/index.html.twig @@ -0,0 +1,13 @@ +{% extends "@ChillMain/Admin/layoutWithVerticalMenu.html.twig" %} + +{% block vertical_menu_content %} + {{ chill_menu('admin_ticket', { + 'layout': '@ChillMain/Admin/menu_admin_section.html.twig', + }) }} +{% endblock %} + +{% block layout_wvm_content %} + {% block admin_content %} +

        {{ 'Tickets configuration' |trans }}

        + {% endblock %} +{% endblock %} diff --git a/src/Bundle/ChillTicketBundle/src/config/services.yaml b/src/Bundle/ChillTicketBundle/src/config/services.yaml index 5e2b8e1b3..b7f6df2c2 100644 --- a/src/Bundle/ChillTicketBundle/src/config/services.yaml +++ b/src/Bundle/ChillTicketBundle/src/config/services.yaml @@ -46,3 +46,6 @@ services: Chill\TicketBundle\DataFixtures\: resource: '../DataFixtures/' + + Chill\TicketBundle\Form\: + resource: '../Form/' diff --git a/src/Bundle/ChillTicketBundle/src/migrations/Version20251022081554.php b/src/Bundle/ChillTicketBundle/src/migrations/Version20251022081554.php new file mode 100644 index 000000000..c024b334b --- /dev/null +++ b/src/Bundle/ChillTicketBundle/src/migrations/Version20251022081554.php @@ -0,0 +1,34 @@ +addSql('ALTER TABLE chill_ticket.motive ADD ordering DOUBLE PRECISION DEFAULT \'0.0\''); + $this->addSql('UPDATE chill_ticket.motive SET ordering = id * 100'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE chill_ticket.motive DROP ordering'); + } +} diff --git a/src/Bundle/ChillTicketBundle/src/translations/messages+intl-icu.fr.yml b/src/Bundle/ChillTicketBundle/src/translations/messages+intl-icu.fr.yml index ec46a9c98..a6436ea9f 100644 --- a/src/Bundle/ChillTicketBundle/src/translations/messages+intl-icu.fr.yml +++ b/src/Bundle/ChillTicketBundle/src/translations/messages+intl-icu.fr.yml @@ -151,3 +151,36 @@ chill_ticket: open_new_tab: "Ouvrir dans un nouvel onglet" iframe_not_supported: "Votre navigateur ne supporte pas les iframes." click_to_open_pdf: "Cliquez ici pour ouvrir le PDF" + +admin: + ticket: + motive: + menu: Motifs + motive: + list: + title: Liste des motifs + view: + title: Le motif + new: + title: Créer un motif +crud: + motive: + title_new: Nouveau motif + title_edit: Modifier le motif + new: + "Create a new motive": "Créer un nouveau motif" + +"Label": "Libellé" +"Active": "Actif" +"emergency?": "Urgent ?" +"Supplementary comments": "Commentaires supplémentaires" +"edit": "modifier" +"show": "voir" +"Yes": "Oui" +"No": "Non" +"Id": "ID" +"Date": "Date" +"User": "Utilisateur" +"No supplementary comments": "Aucun commentaire supplémentaire" +"Back to the list": "Retour à la liste" +"Edit": "Modifier"