diff --git a/src/Bundle/ChillMainBundle/Controller/UserGroupController.php b/src/Bundle/ChillMainBundle/Controller/UserGroupController.php new file mode 100644 index 000000000..f1786206c --- /dev/null +++ b/src/Bundle/ChillMainBundle/Controller/UserGroupController.php @@ -0,0 +1,177 @@ +security->isGranted('ROLE_USER')) { + throw new AccessDeniedHttpException(); + } + + $user = $this->security->getUser(); + + if (!$user instanceof User) { + throw new AccessDeniedHttpException(); + } + + $nb = $this->userGroupRepository->countByUser($user); + $paginator = $this->paginatorFactory->create($nb); + + $groups = $this->userGroupRepository->findByUser($user, true, $paginator->getItemsPerPage(), $paginator->getCurrentPageFirstItemNumber()); + $forms = new \SplObjectStorage(); + + foreach ($groups as $group) { + $forms->attach($group, $this->createFormAppendUserForGroup($group)?->createView()); + } + + return new Response($this->twig->render('@ChillMain/UserGroup/my_user_groups.html.twig', [ + 'groups' => $groups, + 'paginator' => $paginator, + 'forms' => $forms, + ])); + } + + #[Route('/{_locale}/main/user-groups/{id}/append', name: 'chill_main_user_groups_append_users')] + public function appendUsersToGroup(UserGroup $userGroup, Request $request, Session $session): Response + { + if (!$this->security->isGranted(UserGroupVoter::APPEND_TO_GROUP, $userGroup)) { + throw new AccessDeniedHttpException(); + } + + $form = $this->createFormAppendUserForGroup($userGroup); + + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + foreach ($form['users']->getData() as $user) { + $userGroup->addUser($user); + + $session->getFlashBag()->add( + 'success', + new TranslatableMessage( + 'user_group.user_added', + [ + 'user_group' => $this->chillEntityRenderManager->renderString($userGroup, []), + 'user' => $this->chillEntityRenderManager->renderString($user, []), + ] + ) + ); + } + + $this->objectManager->flush(); + + return new RedirectResponse( + $this->chillUrlGenerator->returnPathOr('chill_main_user_groups_my') + ); + } + if ($form->isSubmitted()) { + $errors = []; + foreach ($form->getErrors() as $error) { + $errors[] = $error->getMessage(); + } + + return new Response(implode(', ', $errors)); + } + + return new RedirectResponse( + $this->chillUrlGenerator->returnPathOr('chill_main_user_groups_my') + ); + } + + /** + * @ParamConverter("user", class=User::class, options={"id" = "userId"}) + */ + #[Route('/{_locale}/main/user-group/{id}/user/{userId}/remove', name: 'chill_main_user_groups_remove_user')] + public function removeUserToGroup(UserGroup $userGroup, User $user, Session $session): Response + { + if (!$this->security->isGranted(UserGroupVoter::APPEND_TO_GROUP, $userGroup)) { + throw new AccessDeniedHttpException(); + } + + $userGroup->removeUser($user); + $this->objectManager->flush(); + + $session->getFlashBag()->add( + 'success', + new TranslatableMessage( + 'user_group.user_removed', + [ + 'user_group' => $this->chillEntityRenderManager->renderString($userGroup, []), + 'user' => $this->chillEntityRenderManager->renderString($user, []), + ] + ) + ); + + return new RedirectResponse( + $this->chillUrlGenerator->returnPathOr('chill_main_user_groups_my') + ); + } + + private function createFormAppendUserForGroup(UserGroup $group): ?FormInterface + { + if (!$this->security->isGranted(UserGroupVoter::APPEND_TO_GROUP, $group)) { + return null; + } + + $builder = $this->formFactory->createBuilder(FormType::class, ['users' => []], [ + 'action' => $this->chillUrlGenerator->generateWithReturnPath('chill_main_user_groups_append_users', ['id' => $group->getId()]), + ]); + $builder->add('users', PickUserDynamicType::class, [ + 'submit_on_adding_new_entity' => true, + 'label' => 'user_group.append_users', + 'mapped' => false, + 'multiple' => true, + ]); + + return $builder->getForm(); + } +} diff --git a/src/Bundle/ChillMainBundle/Repository/UserGroupRepository.php b/src/Bundle/ChillMainBundle/Repository/UserGroupRepository.php index 637846526..e1c54442d 100644 --- a/src/Bundle/ChillMainBundle/Repository/UserGroupRepository.php +++ b/src/Bundle/ChillMainBundle/Repository/UserGroupRepository.php @@ -11,20 +11,34 @@ declare(strict_types=1); namespace Chill\MainBundle\Repository; +use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\UserGroup; use Chill\MainBundle\Search\SearchApiQuery; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityRepository; +use Symfony\Contracts\Translation\LocaleAwareInterface; -final readonly class UserGroupRepository implements UserGroupRepositoryInterface +final class UserGroupRepository implements UserGroupRepositoryInterface, LocaleAwareInterface { - private EntityRepository $repository; + private readonly EntityRepository $repository; + + private string $locale; public function __construct(EntityManagerInterface $em) { $this->repository = $em->getRepository(UserGroup::class); } + public function setLocale(string $locale): void + { + $this->locale = $locale; + } + + public function getLocale(): string + { + return $this->locale; + } + public function find($id): ?UserGroup { return $this->repository->find($id); @@ -66,4 +80,54 @@ final readonly class UserGroupRepository implements UserGroupRepositoryInterface return $query; } + + public function findByUser(User $user, bool $onlyActive = true, ?int $limit = null, ?int $offset = null): array + { + $qb = $this->buildQueryByUser($user, $onlyActive); + + if (null !== $limit) { + $qb->setMaxResults($limit); + } + + if (null !== $offset) { + $qb->setFirstResult($offset); + } + + // ordering thing + $qb->addSelect('JSON_EXTRACT(ug.label, :lang) AS HIDDEN label_ordering') + ->addOrderBy('label_ordering', 'ASC') + ->setParameter('lang', $this->getLocale()); + + return $qb->getQuery()->getResult(); + } + + public function countByUser(User $user, bool $onlyActive = true): int + { + $qb = $this->buildQueryByUser($user, $onlyActive); + $qb->select('count(ug)'); + + return $qb->getQuery()->getSingleScalarResult(); + } + + private function buildQueryByUser(User $user, bool $onlyActive): \Doctrine\ORM\QueryBuilder + { + $qb = $this->repository->createQueryBuilder('ug'); + $qb->where( + $qb->expr()->orX( + $qb->expr()->isMemberOf(':user', 'ug.users'), + $qb->expr()->isMemberOf(':user', 'ug.adminUsers') + ) + ); + + $qb->setParameter('user', $user); + + if ($onlyActive) { + $qb->andWhere( + $qb->expr()->eq('ug.active', ':active') + ); + $qb->setParameter('active', true); + } + + return $qb; + } } diff --git a/src/Bundle/ChillMainBundle/Repository/UserGroupRepositoryInterface.php b/src/Bundle/ChillMainBundle/Repository/UserGroupRepositoryInterface.php index 9512e7b25..c3a729ad4 100644 --- a/src/Bundle/ChillMainBundle/Repository/UserGroupRepositoryInterface.php +++ b/src/Bundle/ChillMainBundle/Repository/UserGroupRepositoryInterface.php @@ -11,6 +11,7 @@ declare(strict_types=1); namespace Chill\MainBundle\Repository; +use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\UserGroup; use Chill\MainBundle\Search\SearchApiQuery; use Doctrine\Persistence\ObjectRepository; @@ -24,4 +25,8 @@ interface UserGroupRepositoryInterface extends ObjectRepository * Provide a SearchApiQuery for searching amongst user groups. */ public function provideSearchApiQuery(string $pattern, string $lang, string $selectKey = 'user-group'): SearchApiQuery; + + public function findByUser(User $user, bool $onlyActive = true, ?int $limit = null, ?int $offset = null): array; + + public function countByUser(User $user, bool $onlyActive = true): int; } diff --git a/src/Bundle/ChillMainBundle/Resources/views/UserGroup/my_user_groups.html.twig b/src/Bundle/ChillMainBundle/Resources/views/UserGroup/my_user_groups.html.twig new file mode 100644 index 000000000..7f13c1618 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/views/UserGroup/my_user_groups.html.twig @@ -0,0 +1,160 @@ +{% extends '@ChillMain/layout.html.twig' %} + +{% block css %} + {{ parent() }} + {{ encore_entry_link_tags('mod_pickentity_type') }} + + +{% endblock %} + +{% block js %} + {{ parent() }} + {{ encore_entry_script_tags('mod_pickentity_type') }} +{% endblock %} + +{% block title 'user_group.my_groups'|trans %} + +{% block content %} +

{{ block('title') }}

+ + {% if paginator.totalItems == 0 %} +

{{ 'user_group.no_user_groups'|trans }}

+ {% else %} +
+ {% for entity in groups %} +
+
+
+
+
+ {{ entity|chill_entity_render_box }} +
+
+ {%- if not entity.active -%} +
+ {{ 'user_group.inactive'|trans }} +
  + {%- endif -%} +
{{ 'user_group.with_count_users'|trans({'count': entity.users|length}) }}
+
+
+
+
+
+
+
+
+ {{ 'user_group.with_users'|trans }} +
+
+ {% if entity.users.contains(app.user) %} + {% if is_granted('CHILL_MAIN_USER_GROUP_APPEND_TO_GROUP', entity) %} +
+

+ {{ 'user_group.me'|trans }} + +

+
+ {% else %} +

+ {% if entity.users|length > 1 %}{{ 'user_group.me_and'|trans }}{% else %}{{ 'user_group.me_only'|trans }}{% endif %} +

+ {% endif %} + {% endif %} + {% for user in entity.userListByLabelAscending %} + {% if user is not same as app.user %} + {% if is_granted('CHILL_MAIN_USER_GROUP_APPEND_TO_GROUP', entity) %} +
+

+ + {{ user|chill_entity_render_box }} + + +

+
+ {% else %} +

+ + {{ user|chill_entity_render_box }} + +

+ {% endif %} + {% endif %} + {% endfor %} +
+
+
+
+ {% if entity.adminUsers|length > 0 %} +
+
+
+
+ {{ 'user_group.adminUsers'|trans }} +
+
+ {% if entity.adminUsers.contains(app.user) %} +

{% if entity.adminUsers|length > 1 %}{{ 'user_group.me_and'|trans }}{% else %}{{ 'user_group.me_only'|trans }}{% endif %}

+ {% endif %} + {% for user in entity.adminUserListByLabelAscending %} + {% if user is not same as app.user %} +

+ + {{ user|chill_entity_render_box }} + +

+ {% endif %} + {% endfor %} +
+
+
+
+ {% endif -%} + {%- set form = forms.offsetGet(entity) %} + {%- if form is not null -%} +
+
    +
  • + {{- form_start(form) -}} + {{- form_widget(form.users) -}} + {{- form_end(form) -}} +
  • +
+
+ {%- endif %} +
+ {% endfor %} +
+ + {{ chill_pagination(paginator) }} + {% endif %} +{% endblock %} diff --git a/src/Bundle/ChillMainBundle/Resources/views/layout.html.twig b/src/Bundle/ChillMainBundle/Resources/views/layout.html.twig index bef5c9abd..30f84c855 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/layout.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/layout.html.twig @@ -49,19 +49,19 @@ {% for flashMessage in app.session.flashbag.get('success') %}
- {{ flashMessage|raw }} + {{ flashMessage|trans }}
{% endfor %} {% for flashMessage in app.session.flashbag.get('error') %}
- {{ flashMessage|raw }} + {{ flashMessage|trans }}
{% endfor %} {% for flashMessage in app.session.flashbag.get('notice') %}
- {{ flashMessage|raw }} + {{ flashMessage|trans }}
{% endfor %} diff --git a/src/Bundle/ChillMainBundle/Routing/MenuBuilder/UserMenuBuilder.php b/src/Bundle/ChillMainBundle/Routing/MenuBuilder/UserMenuBuilder.php index 3427face3..228e2c967 100644 --- a/src/Bundle/ChillMainBundle/Routing/MenuBuilder/UserMenuBuilder.php +++ b/src/Bundle/ChillMainBundle/Routing/MenuBuilder/UserMenuBuilder.php @@ -60,7 +60,6 @@ class UserMenuBuilder implements LocalMenuBuilderInterface $nbNotifications = $this->notificationByUserCounter->countUnreadByUser($user); - // TODO add an icon? How exactly? For example a clock icon... $menu ->addChild($this->translator->trans('absence.Set absence date'), [ 'route' => 'chill_main_user_absence_index', @@ -69,6 +68,14 @@ class UserMenuBuilder implements LocalMenuBuilderInterface 'order' => -8_888_888, ]); + $menu + ->addChild($this->translator->trans('user_group.my_groups'), [ + 'route' => 'chill_main_user_groups_my', + ]) + ->setExtras([ + 'order' => -7_777_777, + ]); + $menu ->addChild( $this->translator->trans('notification.My notifications with counter', ['nb' => $nbNotifications]), diff --git a/src/Bundle/ChillMainBundle/Security/Authorization/UserGroupVoter.php b/src/Bundle/ChillMainBundle/Security/Authorization/UserGroupVoter.php new file mode 100644 index 000000000..7256c2567 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Security/Authorization/UserGroupVoter.php @@ -0,0 +1,46 @@ +security->isGranted('ROLE_ADMIN')) { + return true; + } + + $user = $this->security->getUser(); + + if (!$user instanceof User) { + return false; + } + + return $subject->getAdminUsers()->contains($user); + } +} diff --git a/src/Bundle/ChillMainBundle/translations/messages+intl-icu.fr.yaml b/src/Bundle/ChillMainBundle/translations/messages+intl-icu.fr.yaml index ecfd0f5c8..79205862a 100644 --- a/src/Bundle/ChillMainBundle/translations/messages+intl-icu.fr.yaml +++ b/src/Bundle/ChillMainBundle/translations/messages+intl-icu.fr.yaml @@ -13,6 +13,8 @@ user_group: many {# utilisateurs} other {# utilisateurs} } + user_removed: L'utilisateur {user} est enlevé du groupe {user_group} avec succès + user_added: L'utilisateur {user} est ajouté groupe {user_group} avec succès notification: My notifications with counter: >- diff --git a/src/Bundle/ChillMainBundle/translations/messages.fr.yml b/src/Bundle/ChillMainBundle/translations/messages.fr.yml index 484e72a41..bec5e1adb 100644 --- a/src/Bundle/ChillMainBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillMainBundle/translations/messages.fr.yml @@ -53,7 +53,7 @@ user: user_group: inactive: Inactif - with_users: Associé aux utilisateurs + with_users: Membres no_users: Aucun utilisateur associé no_admin_users: Aucun administrateur Label: Nom du groupe @@ -64,6 +64,11 @@ user_group: Users: Membres du groupe adminUsers: Administrateurs du groupe adminUsersHelp: Les administrateurs du groupe peuvent ajouter ou retirer des membres dans le groupe. + my_groups: Mes groupes + me_and: Moi et + me_only: Uniquement moi + me: Moi + append_users: Ajouter des utilisateurs inactive: inactif