mirror of
				https://gitlab.com/Chill-Projet/chill-bundles.git
				synced 2025-10-31 17:28:23 +00:00 
			
		
		
		
	Merge branch 'signature-app/OP630-user-group-in-workflows' into 'signature-app-master'
Implements feature to send a workfllow to a group of users See merge request Chill-Projet/chill-bundles!744
This commit is contained in:
		| @@ -0,0 +1,28 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * 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. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\MainBundle\Controller; | ||||
|  | ||||
| use Chill\MainBundle\CRUD\Controller\CRUDController; | ||||
| use Chill\MainBundle\Pagination\PaginatorInterface; | ||||
| use Symfony\Component\HttpFoundation\Request; | ||||
|  | ||||
| class UserGroupAdminController extends CRUDController | ||||
| { | ||||
|     protected function orderQuery(string $action, $query, Request $request, PaginatorInterface $paginator) | ||||
|     { | ||||
|         $query->addSelect('JSON_EXTRACT(e.label, :lang) AS HIDDEN labeli18n') | ||||
|             ->setParameter('lang', $request->getLocale()); | ||||
|         $query->addOrderBy('labeli18n', 'ASC'); | ||||
|  | ||||
|         return $query; | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,16 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * 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. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\MainBundle\Controller; | ||||
|  | ||||
| use Chill\MainBundle\CRUD\Controller\ApiController; | ||||
|  | ||||
| class UserGroupApiController extends ApiController {} | ||||
							
								
								
									
										177
									
								
								src/Bundle/ChillMainBundle/Controller/UserGroupController.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										177
									
								
								src/Bundle/ChillMainBundle/Controller/UserGroupController.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,177 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * 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. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\MainBundle\Controller; | ||||
|  | ||||
| use Chill\MainBundle\Entity\User; | ||||
| use Chill\MainBundle\Entity\UserGroup; | ||||
| use Chill\MainBundle\Form\Type\PickUserDynamicType; | ||||
| use Chill\MainBundle\Pagination\PaginatorFactoryInterface; | ||||
| use Chill\MainBundle\Repository\UserGroupRepositoryInterface; | ||||
| use Chill\MainBundle\Routing\ChillUrlGeneratorInterface; | ||||
| use Chill\MainBundle\Security\Authorization\UserGroupVoter; | ||||
| use Chill\MainBundle\Templating\Entity\ChillEntityRenderManagerInterface; | ||||
| use Doctrine\ORM\EntityManagerInterface; | ||||
| use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter; | ||||
| use Symfony\Component\Form\Extension\Core\Type\FormType; | ||||
| use Symfony\Component\Form\FormFactoryInterface; | ||||
| use Symfony\Component\Form\FormInterface; | ||||
| use Symfony\Component\HttpFoundation\RedirectResponse; | ||||
| use Symfony\Component\HttpFoundation\Request; | ||||
| use Symfony\Component\HttpFoundation\Response; | ||||
| use Symfony\Component\HttpFoundation\Session\Session; | ||||
| use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; | ||||
| use Symfony\Component\Routing\Annotation\Route; | ||||
| use Symfony\Component\Security\Core\Security; | ||||
| use Symfony\Component\Translation\TranslatableMessage; | ||||
| use Twig\Environment; | ||||
|  | ||||
| /** | ||||
|  * Controller to see and manage user groups. | ||||
|  */ | ||||
| final readonly class UserGroupController | ||||
| { | ||||
|     public function __construct( | ||||
|         private UserGroupRepositoryInterface $userGroupRepository, | ||||
|         private Security $security, | ||||
|         private PaginatorFactoryInterface $paginatorFactory, | ||||
|         private Environment $twig, | ||||
|         private FormFactoryInterface $formFactory, | ||||
|         private ChillUrlGeneratorInterface $chillUrlGenerator, | ||||
|         private EntityManagerInterface $objectManager, | ||||
|         private ChillEntityRenderManagerInterface $chillEntityRenderManager, | ||||
|     ) {} | ||||
|  | ||||
|     #[Route('/{_locale}/main/user-groups/my', name: 'chill_main_user_groups_my')] | ||||
|     public function myUserGroups(): Response | ||||
|     { | ||||
|         if (!$this->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(); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,68 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * 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. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\MainBundle\DataFixtures\ORM; | ||||
|  | ||||
| use Chill\MainBundle\Entity\User; | ||||
| use Chill\MainBundle\Entity\UserGroup; | ||||
| use Doctrine\Bundle\FixturesBundle\Fixture; | ||||
| use Doctrine\Bundle\FixturesBundle\FixtureGroupInterface; | ||||
| use Doctrine\Persistence\ObjectManager; | ||||
|  | ||||
| class LoadUserGroup extends Fixture implements FixtureGroupInterface | ||||
| { | ||||
|     public static function getGroups(): array | ||||
|     { | ||||
|         return ['user-group']; | ||||
|     } | ||||
|  | ||||
|     public function load(ObjectManager $manager) | ||||
|     { | ||||
|         $centerASocial = $manager->getRepository(User::class)->findOneBy(['username' => 'center a_social']); | ||||
|         $centerBSocial = $manager->getRepository(User::class)->findOneBy(['username' => 'center b_social']); | ||||
|         $multiCenter = $manager->getRepository(User::class)->findOneBy(['username' => 'multi_center']); | ||||
|         $administrativeA = $manager->getRepository(User::class)->findOneBy(['username' => 'center a_administrative']); | ||||
|         $administrativeB = $manager->getRepository(User::class)->findOneBy(['username' => 'center b_administrative']); | ||||
|  | ||||
|         $level1 = $this->generateLevelGroup('Niveau 1', '#eec84aff', '#000000ff', 'level'); | ||||
|         $level1->addUser($centerASocial)->addUser($centerBSocial); | ||||
|         $manager->persist($level1); | ||||
|  | ||||
|         $level2 = $this->generateLevelGroup('Niveau 2', ' #e2793dff', '#000000ff', 'level'); | ||||
|         $level2->addUser($multiCenter); | ||||
|         $manager->persist($level2); | ||||
|  | ||||
|         $level3 = $this->generateLevelGroup('Niveau 3', ' #df4949ff', '#000000ff', 'level'); | ||||
|         $level3->addUser($multiCenter); | ||||
|         $manager->persist($level3); | ||||
|  | ||||
|         $tss = $this->generateLevelGroup('Travailleur sociaux', '#43b29dff', '#000000ff', ''); | ||||
|         $tss->addUser($multiCenter)->addUser($centerASocial)->addUser($centerBSocial); | ||||
|         $manager->persist($tss); | ||||
|         $admins = $this->generateLevelGroup('Administratif', '#334d5cff', '#000000ff', ''); | ||||
|         $admins->addUser($administrativeA)->addUser($administrativeB); | ||||
|         $manager->persist($admins); | ||||
|  | ||||
|         $manager->flush(); | ||||
|     } | ||||
|  | ||||
|     private function generateLevelGroup(string $title, string $backgroundColor, string $foregroundColor, string $excludeKey): UserGroup | ||||
|     { | ||||
|         $userGroup = new UserGroup(); | ||||
|  | ||||
|         return $userGroup | ||||
|             ->setLabel(['fr' => $title]) | ||||
|             ->setBackgroundColor($backgroundColor) | ||||
|             ->setForegroundColor($foregroundColor) | ||||
|             ->setExcludeKey($excludeKey) | ||||
|         ; | ||||
|     } | ||||
| } | ||||
| @@ -24,6 +24,8 @@ use Chill\MainBundle\Controller\LocationTypeController; | ||||
| use Chill\MainBundle\Controller\NewsItemController; | ||||
| use Chill\MainBundle\Controller\RegroupmentController; | ||||
| use Chill\MainBundle\Controller\UserController; | ||||
| use Chill\MainBundle\Controller\UserGroupAdminController; | ||||
| use Chill\MainBundle\Controller\UserGroupApiController; | ||||
| use Chill\MainBundle\Controller\UserJobApiController; | ||||
| use Chill\MainBundle\Controller\UserJobController; | ||||
| use Chill\MainBundle\DependencyInjection\Widget\Factory\WidgetFactoryInterface; | ||||
| @@ -59,6 +61,7 @@ use Chill\MainBundle\Entity\LocationType; | ||||
| use Chill\MainBundle\Entity\NewsItem; | ||||
| use Chill\MainBundle\Entity\Regroupment; | ||||
| use Chill\MainBundle\Entity\User; | ||||
| use Chill\MainBundle\Entity\UserGroup; | ||||
| use Chill\MainBundle\Entity\UserJob; | ||||
| use Chill\MainBundle\Form\CenterType; | ||||
| use Chill\MainBundle\Form\CivilityType; | ||||
| @@ -68,6 +71,7 @@ use Chill\MainBundle\Form\LocationFormType; | ||||
| use Chill\MainBundle\Form\LocationTypeType; | ||||
| use Chill\MainBundle\Form\NewsItemType; | ||||
| use Chill\MainBundle\Form\RegroupmentType; | ||||
| use Chill\MainBundle\Form\UserGroupType; | ||||
| use Chill\MainBundle\Form\UserJobType; | ||||
| use Chill\MainBundle\Form\UserType; | ||||
| use Misd\PhoneNumberBundle\Doctrine\DBAL\Types\PhoneNumberType; | ||||
| @@ -353,6 +357,28 @@ class ChillMainExtension extends Extension implements | ||||
|     { | ||||
|         $container->prependExtensionConfig('chill_main', [ | ||||
|             'cruds' => [ | ||||
|                 [ | ||||
|                     'class' => UserGroup::class, | ||||
|                     'controller' => UserGroupAdminController::class, | ||||
|                     'name' => 'admin_user_group', | ||||
|                     'base_path' => '/admin/main/user-group', | ||||
|                     'base_role' => 'ROLE_ADMIN', | ||||
|                     'form_class' => UserGroupType::class, | ||||
|                     'actions' => [ | ||||
|                         'index' => [ | ||||
|                             'role' => 'ROLE_ADMIN', | ||||
|                             'template' => '@ChillMain/UserGroup/index.html.twig', | ||||
|                         ], | ||||
|                         'new' => [ | ||||
|                             'role' => 'ROLE_ADMIN', | ||||
|                             'template' => '@ChillMain/UserGroup/new.html.twig', | ||||
|                         ], | ||||
|                         'edit' => [ | ||||
|                             'role' => 'ROLE_ADMIN', | ||||
|                             'template' => '@ChillMain/UserGroup/edit.html.twig', | ||||
|                         ], | ||||
|                     ], | ||||
|                 ], | ||||
|                 [ | ||||
|                     'class' => UserJob::class, | ||||
|                     'controller' => UserJobController::class, | ||||
| @@ -803,6 +829,21 @@ class ChillMainExtension extends Extension implements | ||||
|                         ], | ||||
|                     ], | ||||
|                 ], | ||||
|                 [ | ||||
|                     'class' => UserGroup::class, | ||||
|                     'controller' => UserGroupApiController::class, | ||||
|                     'name' => 'user-group', | ||||
|                     'base_path' => '/api/1.0/main/user-group', | ||||
|                     'base_role' => 'ROLE_USER', | ||||
|                     'actions' => [ | ||||
|                         '_index' => [ | ||||
|                             'methods' => [ | ||||
|                                 Request::METHOD_GET => true, | ||||
|                                 Request::METHOD_HEAD => true, | ||||
|                             ], | ||||
|                         ], | ||||
|                     ], | ||||
|                 ], | ||||
|             ], | ||||
|         ]); | ||||
|     } | ||||
|   | ||||
							
								
								
									
										222
									
								
								src/Bundle/ChillMainBundle/Entity/UserGroup.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										222
									
								
								src/Bundle/ChillMainBundle/Entity/UserGroup.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,222 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * 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. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\MainBundle\Entity; | ||||
|  | ||||
| use Doctrine\Common\Collections\ArrayCollection; | ||||
| use Doctrine\Common\Collections\Collection; | ||||
| use Doctrine\Common\Collections\Criteria; | ||||
| use Doctrine\Common\Collections\Order; | ||||
| use Doctrine\Common\Collections\ReadableCollection; | ||||
| use Doctrine\Common\Collections\Selectable; | ||||
| use Doctrine\ORM\Mapping as ORM; | ||||
| use Symfony\Component\Serializer\Annotation\DiscriminatorMap; | ||||
|  | ||||
| #[ORM\Entity] | ||||
| #[ORM\Table(name: 'chill_main_user_group')] | ||||
| // this discriminator key is required for automated denormalization | ||||
| #[DiscriminatorMap('type', mapping: ['user_group' => UserGroup::class])] | ||||
| class UserGroup | ||||
| { | ||||
|     #[ORM\Id] | ||||
|     #[ORM\GeneratedValue] | ||||
|     #[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER, nullable: false)] | ||||
|     private ?int $id = null; | ||||
|  | ||||
|     #[ORM\Column(type: \Doctrine\DBAL\Types\Types::BOOLEAN, nullable: false, options: ['default' => true])] | ||||
|     private bool $active = true; | ||||
|  | ||||
|     #[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, nullable: false, options: ['default' => '[]'])] | ||||
|     private array $label = []; | ||||
|  | ||||
|     /** | ||||
|      * @var Collection<int, User>&Selectable<int, User> | ||||
|      */ | ||||
|     #[ORM\ManyToMany(targetEntity: User::class)] | ||||
|     #[ORM\JoinTable(name: 'chill_main_user_group_user')] | ||||
|     private Collection&Selectable $users; | ||||
|  | ||||
|     /** | ||||
|      * @var Collection<int, User>&Selectable<int, User> | ||||
|      */ | ||||
|     #[ORM\ManyToMany(targetEntity: User::class)] | ||||
|     #[ORM\JoinTable(name: 'chill_main_user_group_user_admin')] | ||||
|     private Collection&Selectable $adminUsers; | ||||
|  | ||||
|     #[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false, options: ['default' => '#ffffffff'])] | ||||
|     private string $backgroundColor = '#ffffffff'; | ||||
|  | ||||
|     #[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false, options: ['default' => '#000000ff'])] | ||||
|     private string $foregroundColor = '#000000ff'; | ||||
|  | ||||
|     /** | ||||
|      * Groups with same exclude key are mutually exclusive: adding one in a many-to-one relationship | ||||
|      * will exclude others. | ||||
|      * | ||||
|      * An empty string means "no exclusion" | ||||
|      */ | ||||
|     #[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false, options: ['default' => ''])] | ||||
|     private string $excludeKey = ''; | ||||
|  | ||||
|     public function __construct() | ||||
|     { | ||||
|         $this->adminUsers = new ArrayCollection(); | ||||
|         $this->users = new ArrayCollection(); | ||||
|     } | ||||
|  | ||||
|     public function isActive(): bool | ||||
|     { | ||||
|         return $this->active; | ||||
|     } | ||||
|  | ||||
|     public function setActive(bool $active): self | ||||
|     { | ||||
|         $this->active = $active; | ||||
|  | ||||
|         return $this; | ||||
|     } | ||||
|  | ||||
|     public function addAdminUser(User $user): self | ||||
|     { | ||||
|         if (!$this->adminUsers->contains($user)) { | ||||
|             $this->adminUsers[] = $user; | ||||
|         } | ||||
|  | ||||
|         return $this; | ||||
|     } | ||||
|  | ||||
|     public function removeAdminUser(User $user): self | ||||
|     { | ||||
|         $this->adminUsers->removeElement($user); | ||||
|  | ||||
|         return $this; | ||||
|     } | ||||
|  | ||||
|     public function addUser(User $user): self | ||||
|     { | ||||
|         if (!$this->users->contains($user)) { | ||||
|             $this->users[] = $user; | ||||
|         } | ||||
|  | ||||
|         return $this; | ||||
|     } | ||||
|  | ||||
|     public function removeUser(User $user): self | ||||
|     { | ||||
|         if ($this->users->contains($user)) { | ||||
|             $this->users->removeElement($user); | ||||
|         } | ||||
|  | ||||
|         return $this; | ||||
|     } | ||||
|  | ||||
|     public function getId(): ?int | ||||
|     { | ||||
|         return $this->id; | ||||
|     } | ||||
|  | ||||
|     public function getLabel(): array | ||||
|     { | ||||
|         return $this->label; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @return Selectable<int, User>&Collection<int, User> | ||||
|      */ | ||||
|     public function getUsers(): Collection&Selectable | ||||
|     { | ||||
|         return $this->users; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @return Selectable<int, User>&Collection<int, User> | ||||
|      */ | ||||
|     public function getAdminUsers(): Collection&Selectable | ||||
|     { | ||||
|         return $this->adminUsers; | ||||
|     } | ||||
|  | ||||
|     public function getForegroundColor(): string | ||||
|     { | ||||
|         return $this->foregroundColor; | ||||
|     } | ||||
|  | ||||
|     public function getExcludeKey(): string | ||||
|     { | ||||
|         return $this->excludeKey; | ||||
|     } | ||||
|  | ||||
|     public function getBackgroundColor(): string | ||||
|     { | ||||
|         return $this->backgroundColor; | ||||
|     } | ||||
|  | ||||
|     public function setForegroundColor(string $foregroundColor): self | ||||
|     { | ||||
|         $this->foregroundColor = $foregroundColor; | ||||
|  | ||||
|         return $this; | ||||
|     } | ||||
|  | ||||
|     public function setBackgroundColor(string $backgroundColor): self | ||||
|     { | ||||
|         $this->backgroundColor = $backgroundColor; | ||||
|  | ||||
|         return $this; | ||||
|     } | ||||
|  | ||||
|     public function setExcludeKey(string $excludeKey): self | ||||
|     { | ||||
|         $this->excludeKey = $excludeKey; | ||||
|  | ||||
|         return $this; | ||||
|     } | ||||
|  | ||||
|     public function setLabel(array $label): self | ||||
|     { | ||||
|         $this->label = $label; | ||||
|  | ||||
|         return $this; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Checks if the current object is an instance of the UserGroup class. | ||||
|      * | ||||
|      * In use in twig template, to discriminate when there an object can be polymorphic. | ||||
|      * | ||||
|      * @return bool returns true if the current object is an instance of UserGroup, false otherwise | ||||
|      */ | ||||
|     public function isUserGroup(): bool | ||||
|     { | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     public function contains(User $user): bool | ||||
|     { | ||||
|         return $this->users->contains($user); | ||||
|     } | ||||
|  | ||||
|     public function getUserListByLabelAscending(): ReadableCollection | ||||
|     { | ||||
|         $criteria = Criteria::create(); | ||||
|         $criteria->orderBy(['label' => Order::Ascending]); | ||||
|  | ||||
|         return $this->getUsers()->matching($criteria); | ||||
|     } | ||||
|  | ||||
|     public function getAdminUserListByLabelAscending(): ReadableCollection | ||||
|     { | ||||
|         $criteria = Criteria::create(); | ||||
|         $criteria->orderBy(['label' => Order::Ascending]); | ||||
|  | ||||
|         return $this->getAdminUsers()->matching($criteria); | ||||
|     } | ||||
| } | ||||
| @@ -442,18 +442,18 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface | ||||
|             $newStep->addCcUser($user); | ||||
|         } | ||||
|  | ||||
|         foreach ($transitionContextDTO->futureDestUsers as $user) { | ||||
|         foreach ($transitionContextDTO->getFutureDestUsers() as $user) { | ||||
|             $newStep->addDestUser($user); | ||||
|         } | ||||
|  | ||||
|         foreach ($transitionContextDTO->getFutureDestUserGroups() as $userGroup) { | ||||
|             $newStep->addDestUserGroup($userGroup); | ||||
|         } | ||||
|  | ||||
|         if (null !== $transitionContextDTO->futureUserSignature) { | ||||
|             $newStep->addDestUser($transitionContextDTO->futureUserSignature); | ||||
|         } | ||||
|  | ||||
|         foreach ($transitionContextDTO->futureDestEmails as $email) { | ||||
|             $newStep->addDestEmail($email); | ||||
|         } | ||||
|  | ||||
|         if (null !== $transitionContextDTO->futureUserSignature) { | ||||
|             new EntityWorkflowStepSignature($newStep, $transitionContextDTO->futureUserSignature); | ||||
|         } else { | ||||
|   | ||||
| @@ -12,6 +12,7 @@ declare(strict_types=1); | ||||
| namespace Chill\MainBundle\Entity\Workflow; | ||||
|  | ||||
| use Chill\MainBundle\Entity\User; | ||||
| use Chill\MainBundle\Entity\UserGroup; | ||||
| use Doctrine\Common\Collections\ArrayCollection; | ||||
| use Doctrine\Common\Collections\Collection; | ||||
| use Doctrine\ORM\Mapping as ORM; | ||||
| @@ -48,6 +49,13 @@ class EntityWorkflowStep | ||||
|     #[ORM\JoinTable(name: 'chill_main_workflow_entity_step_user')] | ||||
|     private Collection $destUser; | ||||
|  | ||||
|     /** | ||||
|      * @var Collection<int, UserGroup> | ||||
|      */ | ||||
|     #[ORM\ManyToMany(targetEntity: UserGroup::class)] | ||||
|     #[ORM\JoinTable(name: 'chill_main_workflow_entity_step_user_group')] | ||||
|     private Collection $destUserGroups; | ||||
|  | ||||
|     /** | ||||
|      * @var Collection<int, User> | ||||
|      */ | ||||
| @@ -108,6 +116,7 @@ class EntityWorkflowStep | ||||
|     { | ||||
|         $this->ccUser = new ArrayCollection(); | ||||
|         $this->destUser = new ArrayCollection(); | ||||
|         $this->destUserGroups = new ArrayCollection(); | ||||
|         $this->destUserByAccessKey = new ArrayCollection(); | ||||
|         $this->signatures = new ArrayCollection(); | ||||
|         $this->holdsOnStep = new ArrayCollection(); | ||||
| @@ -123,6 +132,9 @@ class EntityWorkflowStep | ||||
|         return $this; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @deprecated | ||||
|      */ | ||||
|     public function addDestEmail(string $email): self | ||||
|     { | ||||
|         if (!\in_array($email, $this->destEmail, true)) { | ||||
| @@ -141,6 +153,22 @@ class EntityWorkflowStep | ||||
|         return $this; | ||||
|     } | ||||
|  | ||||
|     public function addDestUserGroup(UserGroup $userGroup): self | ||||
|     { | ||||
|         if (!$this->destUserGroups->contains($userGroup)) { | ||||
|             $this->destUserGroups[] = $userGroup; | ||||
|         } | ||||
|  | ||||
|         return $this; | ||||
|     } | ||||
|  | ||||
|     public function removeDestUserGroup(UserGroup $userGroup): self | ||||
|     { | ||||
|         $this->destUserGroups->removeElement($userGroup); | ||||
|  | ||||
|         return $this; | ||||
|     } | ||||
|  | ||||
|     public function addDestUserByAccessKey(User $user): self | ||||
|     { | ||||
|         if (!$this->destUserByAccessKey->contains($user) && !$this->destUser->contains($user)) { | ||||
| @@ -178,7 +206,9 @@ class EntityWorkflowStep | ||||
|  | ||||
|     /** | ||||
|      * get all the users which are allowed to apply a transition: those added manually, and | ||||
|      * those added automatically bu using an access key. | ||||
|      * those added automatically by using an access key. | ||||
|      * | ||||
|      * This method exclude the users associated with user groups | ||||
|      * | ||||
|      * @psalm-suppress DuplicateArrayKey | ||||
|      */ | ||||
| @@ -192,6 +222,14 @@ class EntityWorkflowStep | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @return Collection<int, UserGroup> | ||||
|      */ | ||||
|     public function getDestUserGroups(): Collection | ||||
|     { | ||||
|         return $this->destUserGroups; | ||||
|     } | ||||
|  | ||||
|     public function getCcUser(): Collection | ||||
|     { | ||||
|         return $this->ccUser; | ||||
| @@ -207,6 +245,11 @@ class EntityWorkflowStep | ||||
|         return $this->currentStep; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @return array<string> | ||||
|      * | ||||
|      * @deprecated | ||||
|      */ | ||||
|     public function getDestEmail(): array | ||||
|     { | ||||
|         return $this->destEmail; | ||||
|   | ||||
| @@ -12,6 +12,8 @@ declare(strict_types=1); | ||||
| namespace Chill\MainBundle\Form\Type\DataTransformer; | ||||
|  | ||||
| use Chill\MainBundle\Entity\User; | ||||
| use Chill\MainBundle\Entity\UserGroup; | ||||
| use Chill\MainBundle\Serializer\Normalizer\DiscriminatedObjectDenormalizer; | ||||
| use Chill\PersonBundle\Entity\Person; | ||||
| use Chill\ThirdPartyBundle\Entity\ThirdParty; | ||||
| use Symfony\Component\Form\DataTransformerInterface; | ||||
| @@ -74,15 +76,23 @@ class EntityToJsonTransformer implements DataTransformerInterface | ||||
|             'user' => User::class, | ||||
|             'person' => Person::class, | ||||
|             'thirdparty' => ThirdParty::class, | ||||
|             'user_group' => UserGroup::class, | ||||
|             'user_group_or_user' => DiscriminatedObjectDenormalizer::TYPE, | ||||
|             default => throw new \UnexpectedValueException('This type is not supported'), | ||||
|         }; | ||||
|  | ||||
|         $context = [AbstractNormalizer::GROUPS => ['read']]; | ||||
|  | ||||
|         if ('user_group_or_user' === $this->type) { | ||||
|             $context[DiscriminatedObjectDenormalizer::ALLOWED_TYPES] = [UserGroup::class, User::class]; | ||||
|         } | ||||
|  | ||||
|         return | ||||
|             $this->denormalizer->denormalize( | ||||
|                 ['type' => $item['type'], 'id' => $item['id']], | ||||
|                 $class, | ||||
|                 'json', | ||||
|                 [AbstractNormalizer::GROUPS => ['read']], | ||||
|                 $context, | ||||
|             ); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -24,6 +24,13 @@ use Symfony\Component\Serializer\SerializerInterface; | ||||
|  | ||||
| /** | ||||
|  * Pick user dymically, using vuejs module "AddPerson". | ||||
|  * | ||||
|  * Possible options: | ||||
|  * | ||||
|  * - `multiple`: pick one or more users | ||||
|  * - `suggested`: a list of suggested users | ||||
|  * - `as_id`: only the id will be set in the returned data | ||||
|  * - `submit_on_adding_new_entity`: the browser will immediately submit the form when new users are checked | ||||
|  */ | ||||
| class PickUserDynamicType extends AbstractType | ||||
| { | ||||
|   | ||||
| @@ -0,0 +1,68 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * 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. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\MainBundle\Form\Type; | ||||
|  | ||||
| use Chill\MainBundle\Form\Type\DataTransformer\EntityToJsonTransformer; | ||||
| use Symfony\Component\Form\AbstractType; | ||||
| use Symfony\Component\Form\FormBuilderInterface; | ||||
| use Symfony\Component\Form\FormInterface; | ||||
| use Symfony\Component\Form\FormView; | ||||
| use Symfony\Component\OptionsResolver\OptionsResolver; | ||||
| use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; | ||||
| use Symfony\Component\Serializer\Normalizer\NormalizerInterface; | ||||
| use Symfony\Component\Serializer\SerializerInterface; | ||||
|  | ||||
| /** | ||||
|  * Entity which picks a user **or** a user group. | ||||
|  */ | ||||
| final class PickUserGroupOrUserDynamicType extends AbstractType | ||||
| { | ||||
|     public function __construct(private readonly DenormalizerInterface $denormalizer, private readonly SerializerInterface $serializer, private readonly NormalizerInterface $normalizer) {} | ||||
|  | ||||
|     public function buildForm(FormBuilderInterface $builder, array $options) | ||||
|     { | ||||
|         $builder->addViewTransformer(new EntityToJsonTransformer($this->denormalizer, $this->serializer, $options['multiple'], 'user_group_or_user')); | ||||
|     } | ||||
|  | ||||
|     public function buildView(FormView $view, FormInterface $form, array $options) | ||||
|     { | ||||
|         $view->vars['multiple'] = $options['multiple']; | ||||
|         $view->vars['types'] = ['user-group', 'user']; | ||||
|         $view->vars['uniqid'] = uniqid('pick_usergroup_dyn'); | ||||
|         $view->vars['suggested'] = []; | ||||
|         $view->vars['as_id'] = true === $options['as_id'] ? '1' : '0'; | ||||
|         $view->vars['submit_on_adding_new_entity'] = true === $options['submit_on_adding_new_entity'] ? '1' : '0'; | ||||
|  | ||||
|         foreach ($options['suggested'] as $userGroup) { | ||||
|             $view->vars['suggested'][] = $this->normalizer->normalize($userGroup, 'json', ['groups' => 'read']); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public function configureOptions(OptionsResolver $resolver) | ||||
|     { | ||||
|         $resolver | ||||
|             ->setDefault('multiple', false) | ||||
|             ->setAllowedTypes('multiple', ['bool']) | ||||
|             ->setDefault('compound', false) | ||||
|             ->setDefault('suggested', []) | ||||
|             // if set to true, only the id will be set inside the content. The denormalization will not work. | ||||
|             ->setDefault('as_id', false) | ||||
|             ->setAllowedTypes('as_id', ['bool']) | ||||
|             ->setDefault('submit_on_adding_new_entity', false) | ||||
|             ->setAllowedTypes('submit_on_adding_new_entity', ['bool']); | ||||
|     } | ||||
|  | ||||
|     public function getBlockPrefix() | ||||
|     { | ||||
|         return 'pick_entity_dynamic'; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										58
									
								
								src/Bundle/ChillMainBundle/Form/UserGroupType.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								src/Bundle/ChillMainBundle/Form/UserGroupType.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,58 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * 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. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\MainBundle\Form; | ||||
|  | ||||
| use Chill\MainBundle\Form\Type\PickUserDynamicType; | ||||
| use Chill\MainBundle\Form\Type\TranslatableStringFormType; | ||||
| use Symfony\Component\Form\AbstractType; | ||||
| use Symfony\Component\Form\Extension\Core\Type\ColorType; | ||||
| use Symfony\Component\Form\Extension\Core\Type\TextType; | ||||
| use Symfony\Component\Form\FormBuilderInterface; | ||||
|  | ||||
| class UserGroupType extends AbstractType | ||||
| { | ||||
|     public function buildForm(FormBuilderInterface $builder, array $options) | ||||
|     { | ||||
|         $builder | ||||
|             ->add('label', TranslatableStringFormType::class, [ | ||||
|                 'label' => 'user_group.Label', | ||||
|                 'required' => true, | ||||
|             ]) | ||||
|             ->add('active') | ||||
|             ->add('backgroundColor', ColorType::class, [ | ||||
|                 'label' => 'user_group.BackgroundColor', | ||||
|             ]) | ||||
|             ->add('foregroundColor', ColorType::class, [ | ||||
|                 'label' => 'user_group.ForegroundColor', | ||||
|             ]) | ||||
|             ->add('excludeKey', TextType::class, [ | ||||
|                 'label' => 'user_group.ExcludeKey', | ||||
|                 'help' => 'user_group.ExcludeKeyHelp', | ||||
|                 'required' => false, | ||||
|                 'empty_data' => '', | ||||
|             ]) | ||||
|             ->add('users', PickUserDynamicType::class, [ | ||||
|                 'label' => 'user_group.Users', | ||||
|                 'multiple' => true, | ||||
|                 'required' => false, | ||||
|                 'empty_data' => [], | ||||
|             ]) | ||||
|             ->add('adminUsers', PickUserDynamicType::class, [ | ||||
|                 'label' => 'user_group.adminUsers', | ||||
|                 'multiple' => true, | ||||
|                 'required' => false, | ||||
|                 'empty_data' => [], | ||||
|                 'help' => 'user_group.adminUsersHelp', | ||||
|             ]) | ||||
|         ; | ||||
|     } | ||||
| } | ||||
| @@ -12,20 +12,17 @@ declare(strict_types=1); | ||||
| namespace Chill\MainBundle\Form; | ||||
|  | ||||
| use Chill\MainBundle\Entity\Workflow\EntityWorkflow; | ||||
| use Chill\MainBundle\Form\Type\ChillCollectionType; | ||||
| use Chill\MainBundle\Form\Type\ChillTextareaType; | ||||
| use Chill\MainBundle\Form\Type\PickUserDynamicType; | ||||
| use Chill\MainBundle\Form\Type\PickUserGroupOrUserDynamicType; | ||||
| use Chill\MainBundle\Templating\TranslatableStringHelperInterface; | ||||
| use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO; | ||||
| use Chill\PersonBundle\Form\Type\PickPersonDynamicType; | ||||
| use Symfony\Component\Form\AbstractType; | ||||
| use Symfony\Component\Form\Extension\Core\Type\ChoiceType; | ||||
| use Symfony\Component\Form\Extension\Core\Type\EmailType; | ||||
| use Symfony\Component\Form\FormBuilderInterface; | ||||
| use Symfony\Component\OptionsResolver\OptionsResolver; | ||||
| use Symfony\Component\Validator\Constraints\Callback; | ||||
| use Symfony\Component\Validator\Constraints\Email; | ||||
| use Symfony\Component\Validator\Constraints\NotBlank; | ||||
| use Symfony\Component\Validator\Constraints\NotNull; | ||||
| use Symfony\Component\Validator\Context\ExecutionContextInterface; | ||||
| use Symfony\Component\Workflow\Registry; | ||||
| @@ -156,7 +153,7 @@ class WorkflowStepType extends AbstractType | ||||
|                 'label' => 'workflow.signature_zone.user signature', | ||||
|                 'multiple' => false, | ||||
|             ]) | ||||
|             ->add('futureDestUsers', PickUserDynamicType::class, [ | ||||
|             ->add('futureDestUsers', PickUserGroupOrUserDynamicType::class, [ | ||||
|                 'label' => 'workflow.dest for next steps', | ||||
|                 'multiple' => true, | ||||
|                 'empty_data' => '[]', | ||||
| @@ -169,21 +166,6 @@ class WorkflowStepType extends AbstractType | ||||
|                 'suggested' => $options['suggested_users'], | ||||
|                 'empty_data' => '[]', | ||||
|                 'attr' => ['class' => 'future-cc-users'], | ||||
|             ]) | ||||
|             ->add('futureDestEmails', ChillCollectionType::class, [ | ||||
|                 'label' => 'workflow.dest by email', | ||||
|                 'help' => 'workflow.dest by email help', | ||||
|                 'allow_add' => true, | ||||
|                 'entry_type' => EmailType::class, | ||||
|                 'button_add_label' => 'workflow.Add an email', | ||||
|                 'button_remove_label' => 'workflow.Remove an email', | ||||
|                 'empty_collection_explain' => 'workflow.Any email', | ||||
|                 'entry_options' => [ | ||||
|                     'constraints' => [ | ||||
|                         new NotNull(), new NotBlank(), new Email(), | ||||
|                     ], | ||||
|                     'label' => 'Email', | ||||
|                 ], | ||||
|             ]); | ||||
|  | ||||
|         $builder | ||||
| @@ -222,9 +204,8 @@ class WorkflowStepType extends AbstractType | ||||
|                                 } | ||||
|                             } | ||||
|                             $destUsers = $step->futureDestUsers; | ||||
|                             $destEmails = $step->futureDestEmails; | ||||
|  | ||||
|                             if (!$toFinal && [] === $destUsers && [] === $destEmails) { | ||||
|                             if (!$toFinal && [] === $destUsers) { | ||||
|                                 $context | ||||
|                                     ->buildViolation('workflow.You must add at least one dest user or email') | ||||
|                                     ->atPath('future_dest_users') | ||||
|   | ||||
							
								
								
									
										133
									
								
								src/Bundle/ChillMainBundle/Repository/UserGroupRepository.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										133
									
								
								src/Bundle/ChillMainBundle/Repository/UserGroupRepository.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,133 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * 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. | ||||
|  */ | ||||
|  | ||||
| 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 class UserGroupRepository implements UserGroupRepositoryInterface, LocaleAwareInterface | ||||
| { | ||||
|     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); | ||||
|     } | ||||
|  | ||||
|     public function findAll(): array | ||||
|     { | ||||
|         return $this->repository->findAll(); | ||||
|     } | ||||
|  | ||||
|     public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array | ||||
|     { | ||||
|         return $this->repository->findBy($criteria, $orderBy, $limit, $offset); | ||||
|     } | ||||
|  | ||||
|     public function findOneBy(array $criteria): ?UserGroup | ||||
|     { | ||||
|         return $this->repository->findOneBy($criteria); | ||||
|     } | ||||
|  | ||||
|     public function getClassName(): string | ||||
|     { | ||||
|         return UserGroup::class; | ||||
|     } | ||||
|  | ||||
|     public function provideSearchApiQuery(string $pattern, string $lang, string $selectKey = 'user-group'): SearchApiQuery | ||||
|     { | ||||
|         $query = new SearchApiQuery(); | ||||
|         $query | ||||
|             ->setSelectKey($selectKey) | ||||
|             ->setSelectJsonbMetadata("jsonb_build_object('id', ug.id)") | ||||
|             ->setSelectPertinence('3 + SIMILARITY(LOWER(UNACCENT(?)), ug.label->>?) + CASE WHEN (EXISTS(SELECT 1 FROM unnest(string_to_array(label->>?, \' \')) AS t WHERE LOWER(t) LIKE \'%\' || LOWER(UNACCENT(?)) || \'%\')) THEN 100 ELSE 0 END', [$pattern, $lang, $lang, $pattern]) | ||||
|             ->setFromClause('chill_main_user_group AS ug') | ||||
|             ->setWhereClauses(' | ||||
|                 ug.active AND ( | ||||
|                 SIMILARITY(LOWER(UNACCENT(?)), ug.label->>?) > 0.15 | ||||
|                 OR ug.label->>? LIKE \'%\' || LOWER(UNACCENT(?)) || \'%\') | ||||
|             ', [$pattern, $lang, $pattern, $lang]); | ||||
|  | ||||
|         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; | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,32 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * 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. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\MainBundle\Repository; | ||||
|  | ||||
| use Chill\MainBundle\Entity\User; | ||||
| use Chill\MainBundle\Entity\UserGroup; | ||||
| use Chill\MainBundle\Search\SearchApiQuery; | ||||
| use Doctrine\Persistence\ObjectRepository; | ||||
|  | ||||
| /** | ||||
|  * @template-extends ObjectRepository<UserGroup> | ||||
|  */ | ||||
| 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; | ||||
| } | ||||
| @@ -12,6 +12,7 @@ declare(strict_types=1); | ||||
| namespace Chill\MainBundle\Repository\Workflow; | ||||
|  | ||||
| use Chill\MainBundle\Entity\User; | ||||
| use Chill\MainBundle\Entity\UserGroup; | ||||
| use Chill\MainBundle\Entity\Workflow\EntityWorkflow; | ||||
| use Chill\MainBundle\Entity\Workflow\EntityWorkflowStep; | ||||
| use Doctrine\ORM\EntityManagerInterface; | ||||
| @@ -264,6 +265,7 @@ class EntityWorkflowRepository implements ObjectRepository | ||||
|                 $qb->expr()->orX( | ||||
|                     $qb->expr()->isMemberOf(':user', 'step.destUser'), | ||||
|                     $qb->expr()->isMemberOf(':user', 'step.destUserByAccessKey'), | ||||
|                     $qb->expr()->exists(sprintf('SELECT 1 FROM %s ug WHERE ug MEMBER OF step.destUserGroups AND :user MEMBER OF ug.users', UserGroup::class)) | ||||
|                 ), | ||||
|                 $qb->expr()->isNull('step.transitionAfter'), | ||||
|                 $qb->expr()->eq('step.isFinal', "'FALSE'") | ||||
|   | ||||
| @@ -12,6 +12,7 @@ declare(strict_types=1); | ||||
| namespace Chill\MainBundle\Repository\Workflow; | ||||
|  | ||||
| use Chill\MainBundle\Entity\User; | ||||
| use Chill\MainBundle\Entity\UserGroup; | ||||
| use Chill\MainBundle\Entity\Workflow\EntityWorkflowStep; | ||||
| use Doctrine\ORM\EntityManagerInterface; | ||||
| use Doctrine\ORM\EntityRepository; | ||||
| @@ -65,7 +66,11 @@ readonly class EntityWorkflowStepRepository implements ObjectRepository | ||||
|  | ||||
|         $qb->where( | ||||
|             $qb->expr()->andX( | ||||
|                 $qb->expr()->isMemberOf(':user', 'e.destUser'), | ||||
|                 $qb->expr()->orX( | ||||
|                     $qb->expr()->isMemberOf(':user', 'e.destUser'), | ||||
|                     $qb->expr()->isMemberOf(':user', 'e.destUserByAccessKey'), | ||||
|                     $qb->expr()->exists(sprintf('SELECT 1 FROM %s ug WHERE ug MEMBER OF e.destUserGroups AND :user MEMBER OF ug.users', UserGroup::class)) | ||||
|                 ), | ||||
|                 $qb->expr()->isNull('e.transitionAt'), | ||||
|                 $qb->expr()->eq('e.isFinal', ':bool'), | ||||
|             ) | ||||
|   | ||||
| @@ -233,7 +233,7 @@ div.wrap-header { | ||||
|         } | ||||
|         &:last-child {} | ||||
|  | ||||
|         div.wh-col { | ||||
|         & > div.wh-col { | ||||
|             &:first-child { | ||||
|                 flex-grow: 0; flex-shrink: 1; flex-basis: auto; | ||||
|             } | ||||
|   | ||||
| @@ -44,7 +44,9 @@ function loadDynamicPicker(element) { | ||||
|                 ':suggested="notPickedSuggested" ' + | ||||
|                 ':label="label" ' + | ||||
|                 '@addNewEntity="addNewEntity" ' + | ||||
|                 '@removeEntity="removeEntity"></pick-entity>', | ||||
|                 '@removeEntity="removeEntity" ' + | ||||
|                 '@addNewEntityProcessEnded="addNewEntityProcessEnded"' + | ||||
|               '></pick-entity>', | ||||
|             components: { | ||||
|                 PickEntity, | ||||
|             }, | ||||
| @@ -97,10 +99,11 @@ function loadDynamicPicker(element) { | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|  | ||||
|                     if (this.submit_on_adding_new_entity) { | ||||
|                         input.form.submit(); | ||||
|                     } | ||||
|                 }, | ||||
|                 addNewEntityProcessEnded() { | ||||
|                   if (this.submit_on_adding_new_entity) { | ||||
|                     input.form.submit(); | ||||
|                   } | ||||
|                 }, | ||||
|                 removeEntity({entity}) { | ||||
|                     if (-1 === this.suggested.findIndex(e => e.type === entity.type && e.id === entity.id)) { | ||||
|   | ||||
| @@ -1,164 +1,181 @@ | ||||
| export interface DateTime { | ||||
|   datetime: string; | ||||
|   datetime8601: string | ||||
|     datetime: string; | ||||
|     datetime8601: string; | ||||
| } | ||||
|  | ||||
| export interface Civility { | ||||
|   id: number; | ||||
|   // TODO | ||||
|     id: number; | ||||
|     // TODO | ||||
| } | ||||
|  | ||||
| export interface Job { | ||||
|   id: number; | ||||
|   type: "user_job"; | ||||
|   label: { | ||||
|     "fr": string; // could have other key. How to do that in ts ? | ||||
|   } | ||||
|     id: number; | ||||
|     type: "user_job"; | ||||
|     label: { | ||||
|         fr: string; // could have other key. How to do that in ts ? | ||||
|     }; | ||||
| } | ||||
|  | ||||
| export interface Center { | ||||
|   id: number; | ||||
|   type: "center"; | ||||
|   name: string; | ||||
|     id: number; | ||||
|     type: "center"; | ||||
|     name: string; | ||||
| } | ||||
|  | ||||
| export interface Scope { | ||||
|   id: number; | ||||
|   type: "scope"; | ||||
|   name: { | ||||
|     "fr": string | ||||
|   } | ||||
|     id: number; | ||||
|     type: "scope"; | ||||
|     name: { | ||||
|         fr: string; | ||||
|     }; | ||||
| } | ||||
|  | ||||
| export interface ResultItem<T> { | ||||
|     result: T; | ||||
|     relevance: number; | ||||
| } | ||||
|  | ||||
| export interface User { | ||||
|   type: "user"; | ||||
|   id: number; | ||||
|   username: string; | ||||
|   text: string; | ||||
|   text_without_absence: string; | ||||
|   email: string; | ||||
|   user_job: Job; | ||||
|   label: string; | ||||
|   // todo: mainCenter; mainJob; etc.. | ||||
|     type: "user"; | ||||
|     id: number; | ||||
|     username: string; | ||||
|     text: string; | ||||
|     text_without_absence: string; | ||||
|     email: string; | ||||
|     user_job: Job; | ||||
|     label: string; | ||||
|     // todo: mainCenter; mainJob; etc.. | ||||
| } | ||||
|  | ||||
| export interface UserGroup { | ||||
|     type: "user_group"; | ||||
|     id: number; | ||||
|     label: TranslatableString; | ||||
|     backgroundColor: string; | ||||
|     foregroundColor: string; | ||||
|     excludeKey: string; | ||||
|     text: string; | ||||
| } | ||||
|  | ||||
| export type UserGroupOrUser = User | UserGroup; | ||||
|  | ||||
| export interface UserAssociatedInterface { | ||||
|   type: "user"; | ||||
|   id: number; | ||||
| }; | ||||
|  | ||||
| export type TranslatableString = { | ||||
|   fr?: string; | ||||
|   nl?: string; | ||||
|     type: "user"; | ||||
|     id: number; | ||||
| } | ||||
|  | ||||
| export type TranslatableString = { | ||||
|     fr?: string; | ||||
|     nl?: string; | ||||
| }; | ||||
|  | ||||
| export interface Postcode { | ||||
|   id: number; | ||||
|   name: string; | ||||
|   code: string; | ||||
|   center: Point; | ||||
|     id: number; | ||||
|     name: string; | ||||
|     code: string; | ||||
|     center: Point; | ||||
| } | ||||
|  | ||||
| export type Point = { | ||||
|   type: "Point"; | ||||
|   coordinates: [lat: number, lon: number]; | ||||
| } | ||||
|     type: "Point"; | ||||
|     coordinates: [lat: number, lon: number]; | ||||
| }; | ||||
|  | ||||
| export interface Country { | ||||
|   id: number; | ||||
|   name: TranslatableString; | ||||
|   code: string; | ||||
|     id: number; | ||||
|     name: TranslatableString; | ||||
|     code: string; | ||||
| } | ||||
|  | ||||
| export type AddressRefStatus = 'match'|'to_review'|'reviewed'; | ||||
| export type AddressRefStatus = "match" | "to_review" | "reviewed"; | ||||
|  | ||||
| export interface Address { | ||||
|   type: "address"; | ||||
|   address_id: number; | ||||
|   text: string; | ||||
|   street: string; | ||||
|   streetNumber: string; | ||||
|   postcode: Postcode; | ||||
|   country: Country; | ||||
|   floor: string | null; | ||||
|   corridor: string | null; | ||||
|   steps: string | null; | ||||
|   flat: string | null; | ||||
|   buildingName: string | null; | ||||
|   distribution: string | null; | ||||
|   extra: string | null; | ||||
|   confidential: boolean; | ||||
|   lines: string[]; | ||||
|   addressReference: AddressReference | null; | ||||
|   validFrom: DateTime; | ||||
|   validTo: DateTime | null; | ||||
|   point: Point | null; | ||||
|   refStatus: AddressRefStatus; | ||||
|   isNoAddress: boolean; | ||||
|     type: "address"; | ||||
|     address_id: number; | ||||
|     text: string; | ||||
|     street: string; | ||||
|     streetNumber: string; | ||||
|     postcode: Postcode; | ||||
|     country: Country; | ||||
|     floor: string | null; | ||||
|     corridor: string | null; | ||||
|     steps: string | null; | ||||
|     flat: string | null; | ||||
|     buildingName: string | null; | ||||
|     distribution: string | null; | ||||
|     extra: string | null; | ||||
|     confidential: boolean; | ||||
|     lines: string[]; | ||||
|     addressReference: AddressReference | null; | ||||
|     validFrom: DateTime; | ||||
|     validTo: DateTime | null; | ||||
|     point: Point | null; | ||||
|     refStatus: AddressRefStatus; | ||||
|     isNoAddress: boolean; | ||||
| } | ||||
|  | ||||
| export interface AddressWithPoint extends Address { | ||||
|   point: Point | ||||
|     point: Point; | ||||
| } | ||||
|  | ||||
| export interface AddressReference { | ||||
|   id: number; | ||||
|   createdAt: DateTime | null; | ||||
|   deletedAt: DateTime | null; | ||||
|   municipalityCode: string; | ||||
|   point: Point; | ||||
|   postcode: Postcode; | ||||
|   refId: string; | ||||
|   source: string; | ||||
|   street: string; | ||||
|   streetNumber: string; | ||||
|   updatedAt: DateTime | null; | ||||
|     id: number; | ||||
|     createdAt: DateTime | null; | ||||
|     deletedAt: DateTime | null; | ||||
|     municipalityCode: string; | ||||
|     point: Point; | ||||
|     postcode: Postcode; | ||||
|     refId: string; | ||||
|     source: string; | ||||
|     street: string; | ||||
|     streetNumber: string; | ||||
|     updatedAt: DateTime | null; | ||||
| } | ||||
|  | ||||
| export interface SimpleGeographicalUnit { | ||||
|   id: number; | ||||
|   layerId: number; | ||||
|   unitName: string; | ||||
|   unitRefId: string; | ||||
|     id: number; | ||||
|     layerId: number; | ||||
|     unitName: string; | ||||
|     unitRefId: string; | ||||
| } | ||||
|  | ||||
| export interface GeographicalUnitLayer { | ||||
|   id: number; | ||||
|   name: TranslatableString; | ||||
|   refId: string; | ||||
|     id: number; | ||||
|     name: TranslatableString; | ||||
|     refId: string; | ||||
| } | ||||
|  | ||||
| export interface Location { | ||||
|   type: "location"; | ||||
|   id: number; | ||||
|   active: boolean; | ||||
|   address: Address | null; | ||||
|   availableForUsers: boolean; | ||||
|   createdAt: DateTime | null; | ||||
|   createdBy: User | null; | ||||
|   updatedAt: DateTime | null; | ||||
|   updatedBy: User | null; | ||||
|   email: string | null | ||||
|   name: string; | ||||
|   phonenumber1: string | null; | ||||
|   phonenumber2: string | null; | ||||
|   locationType: LocationType; | ||||
|     type: "location"; | ||||
|     id: number; | ||||
|     active: boolean; | ||||
|     address: Address | null; | ||||
|     availableForUsers: boolean; | ||||
|     createdAt: DateTime | null; | ||||
|     createdBy: User | null; | ||||
|     updatedAt: DateTime | null; | ||||
|     updatedBy: User | null; | ||||
|     email: string | null; | ||||
|     name: string; | ||||
|     phonenumber1: string | null; | ||||
|     phonenumber2: string | null; | ||||
|     locationType: LocationType; | ||||
| } | ||||
|  | ||||
| export interface LocationAssociated { | ||||
|   type: "location"; | ||||
|   id: number; | ||||
|     type: "location"; | ||||
|     id: number; | ||||
| } | ||||
|  | ||||
| export interface LocationType { | ||||
|   type: "location-type"; | ||||
|   id: number; | ||||
|   active: boolean; | ||||
|   addressRequired: "optional" | "required"; | ||||
|   availableForUsers: boolean; | ||||
|   editableByUsers: boolean; | ||||
|   contactData: "optional" | "required"; | ||||
|   title: TranslatableString; | ||||
|     type: "location-type"; | ||||
|     id: number; | ||||
|     active: boolean; | ||||
|     addressRequired: "optional" | "required"; | ||||
|     availableForUsers: boolean; | ||||
|     editableByUsers: boolean; | ||||
|     contactData: "optional" | "required"; | ||||
|     title: TranslatableString; | ||||
| } | ||||
|  | ||||
| export interface NewsItemType { | ||||
|   | ||||
| @@ -62,7 +62,7 @@ export default { | ||||
|             required: false, | ||||
|         } | ||||
|     }, | ||||
|     emits: ['addNewEntity', 'removeEntity'], | ||||
|     emits: ['addNewEntity', 'removeEntity', 'addNewEntityProcessEnded'], | ||||
|     components: { | ||||
|         AddPersons, | ||||
|     }, | ||||
| @@ -121,6 +121,7 @@ export default { | ||||
|           ); | ||||
|           this.$refs.addPersons.resetSearch(); // to cast child method | ||||
|           modal.showModal = false; | ||||
|           this.$emit('addNewEntityProcessEnded'); | ||||
|       }, | ||||
|       removeEntity(entity) { | ||||
|           if (!this.$props.removableIfSet) { | ||||
|   | ||||
| @@ -0,0 +1,26 @@ | ||||
| <script setup lang="ts"> | ||||
| import {UserGroup} from "../../../types"; | ||||
| import {computed} from "vue"; | ||||
|  | ||||
| interface UserGroupRenderBoxProps { | ||||
|     userGroup: UserGroup; | ||||
| } | ||||
|  | ||||
| const props = defineProps<UserGroupRenderBoxProps>(); | ||||
|  | ||||
| const styles = computed<{color: string, "background-color": string}>(() => { | ||||
|     return { | ||||
|         color: props.userGroup.foregroundColor, | ||||
|         "background-color": props.userGroup.backgroundColor, | ||||
|     } | ||||
| }); | ||||
|  | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|     <span class="badge-user-group" :style="styles">{{ userGroup.label.fr }}</span> | ||||
| </template> | ||||
|  | ||||
| <style scoped lang="scss"> | ||||
|  | ||||
| </style> | ||||
| @@ -0,0 +1 @@ | ||||
| <span class="badge-user-group" style="color: {{ user_group.foregroundColor }}; background-color: {{  user_group.backgroundColor }};">{{ user_group.label|localize_translatable_string }}</span> | ||||
| @@ -60,7 +60,7 @@ | ||||
|                                     data-bs-toggle="dropdown" | ||||
|                                     aria-haspopup="true" | ||||
|                                     aria-expanded="false"> | ||||
|                                     <a href="#" class="more">{{ app.request.locale | capitalize }}</a> | ||||
|                                     {{ app.request.locale | capitalize }} | ||||
|                                 </a> | ||||
|                                 <div class="dropdown-menu dropdown-menu-end" | ||||
|                                     aria-labelledby="menu-languages"> | ||||
|   | ||||
| @@ -0,0 +1,21 @@ | ||||
| {% extends '@ChillMain/CRUD/Admin/index.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 %} | ||||
|     {% include('@ChillMain/CRUD/_edit_title.html.twig') %} | ||||
| {% endblock %} | ||||
|  | ||||
| {% block admin_content %} | ||||
|     {% embed '@ChillMain/CRUD/_edit_content.html.twig' %} | ||||
|         {% block content_form_actions_save_and_show %}{% endblock %} | ||||
|     {% endembed %} | ||||
| {% endblock admin_content %} | ||||
| @@ -0,0 +1,86 @@ | ||||
| {% extends '@ChillMain/CRUD/Admin/index.html.twig' %} | ||||
|  | ||||
| {% block admin_content %} | ||||
|     {% embed '@ChillMain/CRUD/_index.html.twig' %} | ||||
|  | ||||
|         {% block table_entities %} | ||||
|             <div class="flex-table"> | ||||
|                 {% for entity in entities %} | ||||
|                     <div class="item-bloc"> | ||||
|                         <div class="item-row"> | ||||
|                             <div class="wrap-header"> | ||||
|                                 <div class="wh-row"> | ||||
|                                     <div class="wh-col"> | ||||
|                                         {{ entity|chill_entity_render_box }} | ||||
|                                     </div> | ||||
|                                     <div class="wh-col"> | ||||
|                                         {%- if not entity.active -%} | ||||
|                                             <div> | ||||
|                                                 <span class="badge bg-danger">{{ 'user_group.inactive'|trans }}</span> | ||||
|                                             </div>  | ||||
|                                         {%- endif -%} | ||||
|                                         <div>{{ 'user_group.with_count_users'|trans({'count': entity.users|length}) }}</div> | ||||
|                                     </div> | ||||
|                                 </div> | ||||
|                             </div> | ||||
|                         </div> | ||||
|                         <div class="item-row separator"> | ||||
|                             <div class="wrap-list"> | ||||
|                                 <div class="wl-row"> | ||||
|                                     <div class="wl-col title"> | ||||
|                                         <strong>{{ 'user_group.with_users'|trans }}</strong> | ||||
|                                     </div> | ||||
|                                     <div class="wl-col list"> | ||||
|                                         {% for user in entity.userListByLabelAscending %} | ||||
|                                             <p class="wl-item"> | ||||
|                                             <span class="badge-user"> | ||||
|                                                 {{ user|chill_entity_render_box }} | ||||
|                                             </span> | ||||
|                                             </p> | ||||
|                                         {% else %} | ||||
|                                             <p class="wl-item chill-no-data-statement">{{ 'user_group.no_users'|trans }}</p> | ||||
|                                         {% endfor %} | ||||
|                                     </div> | ||||
|                                 </div> | ||||
|                             </div> | ||||
|                         </div> | ||||
|                         <div class="item-row separator"> | ||||
|                             <div class="wrap-list"> | ||||
|                                 <div class="wl-row"> | ||||
|                                     <div class="wl-col title"> | ||||
|                                         <strong>{{ 'user_group.adminUsers'|trans }}</strong> | ||||
|                                     </div> | ||||
|                                     <div class="wl-col list"> | ||||
|                                         {% for user in entity.adminUserListByLabelAscending %} | ||||
|                                             <p class="wl-item"> | ||||
|                                             <span class="badge-user"> | ||||
|                                                 {{ user|chill_entity_render_box }} | ||||
|                                             </span> | ||||
|                                             </p> | ||||
|                                         {% else %} | ||||
|                                             <p class="wl-item chill-no-data-statement">{{ 'user_group.no_admin_users'|trans }}</p> | ||||
|                                         {% endfor %} | ||||
|                                     </div> | ||||
|                                 </div> | ||||
|                             </div> | ||||
|                         </div> | ||||
|                         <div class="item-row separator"> | ||||
|                             <ul class="record_actions slim"> | ||||
|                                 <li> | ||||
|                                     <a href="{{ chill_path_add_return_path('chill_crud_admin_user_group_edit', {'id': entity.id}) }}" class="btn btn-edit"></a> | ||||
|                                 </li> | ||||
|                             </ul> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 {% endfor %} | ||||
|             </div> | ||||
|         {% endblock %} | ||||
|  | ||||
|         {% block actions_before %} | ||||
|             <li class='cancel'> | ||||
|                 <a href="{{ path('chill_main_admin_central') }}" class="btn btn-cancel">{{'Back to the admin'|trans }}</a> | ||||
|             </li> | ||||
|         {% endblock %} | ||||
|  | ||||
|     {% endembed %} | ||||
| {% endblock %} | ||||
| @@ -0,0 +1,160 @@ | ||||
| {% extends '@ChillMain/layout.html.twig' %} | ||||
|  | ||||
| {% block css %} | ||||
|     {{ parent() }} | ||||
|     {{ encore_entry_link_tags('mod_pickentity_type') }} | ||||
|  | ||||
|     <style type="text/css"> | ||||
|         form.remove { | ||||
|             display: inline-block; | ||||
|             padding: 1px; | ||||
|             border: 1px solid transparent; | ||||
|             border-radius: 4px; | ||||
|         } | ||||
|         form:hover { | ||||
|             animation-duration: 0.5s; | ||||
|             animation-name: onHover; | ||||
|             animation-iteration-count: 1; | ||||
|             border: 1px solid #dee2e6; | ||||
|             border-radius: 4px; | ||||
|         } | ||||
|         form.remove button.remove { | ||||
|             display: inline; | ||||
|             background-color: unset; | ||||
|             border: none; | ||||
|             color: var(--bs-chill-red); | ||||
|         } | ||||
|  | ||||
|         @keyframes onHover { | ||||
|            from { | ||||
|                border: 1px solid transparent; | ||||
|            } | ||||
|            to { | ||||
|                border: 1px solid #dee2e6; | ||||
|            } | ||||
|         } | ||||
|     </style> | ||||
| {% endblock %} | ||||
|  | ||||
| {% block js %} | ||||
|     {{ parent() }} | ||||
|     {{ encore_entry_script_tags('mod_pickentity_type') }} | ||||
| {% endblock %} | ||||
|  | ||||
| {% block title 'user_group.my_groups'|trans %} | ||||
|  | ||||
| {% block content %} | ||||
|     <h1>{{ block('title') }}</h1> | ||||
|  | ||||
|     {% if paginator.totalItems == 0 %} | ||||
|         <p>{{ 'user_group.no_user_groups'|trans }}</p> | ||||
|     {% else %} | ||||
|         <div class="flex-table"> | ||||
|             {% for entity in groups %} | ||||
|                 <div class="item-bloc"> | ||||
|                     <div class="item-row"> | ||||
|                         <div class="wrap-header"> | ||||
|                             <div class="wh-row"> | ||||
|                                 <div class="wh-col"> | ||||
|                                     {{ entity|chill_entity_render_box }} | ||||
|                                 </div> | ||||
|                                 <div class="wh-col"> | ||||
|                                     {%- if not entity.active -%} | ||||
|                                         <div> | ||||
|                                             <span class="badge bg-danger">{{ 'user_group.inactive'|trans }}</span> | ||||
|                                         </div>  | ||||
|                                     {%- endif -%} | ||||
|                                     <div>{{ 'user_group.with_count_users'|trans({'count': entity.users|length}) }}</div> | ||||
|                                 </div> | ||||
|                             </div> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                     <div class="item-row separator"> | ||||
|                         <div class="wrap-list"> | ||||
|                             <div class="wl-row"> | ||||
|                                 <div class="wl-col title"> | ||||
|                                     <strong>{{ 'user_group.with_users'|trans }}</strong> | ||||
|                                 </div> | ||||
|                                 <div class="wl-col list"> | ||||
|                                     {% if entity.users.contains(app.user) %} | ||||
|                                         {% if is_granted('CHILL_MAIN_USER_GROUP_APPEND_TO_GROUP', entity) %} | ||||
|                                             <form class="remove" method="POST" action="{{ chill_path_add_return_path('chill_main_user_groups_remove_user', {'id': entity.id, 'userId': app.user.id}) }}"> | ||||
|                                                 <p class="wl-item"> | ||||
|                                                     {{ 'user_group.me'|trans }} | ||||
|                                                     <button class="remove" type="submit"><i class="fa fa-times"></i></button> | ||||
|                                                 </p> | ||||
|                                             </form> | ||||
|                                         {% else %} | ||||
|                                             <p class="wl-item"> | ||||
|                                                 {% if entity.users|length > 1 %}{{ 'user_group.me_and'|trans }}{% else %}{{ 'user_group.me_only'|trans }}{% endif %} | ||||
|                                             </p> | ||||
|                                         {% 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) %} | ||||
|                                                 <form class="remove" method="POST" action="{{ chill_path_add_return_path('chill_main_user_groups_remove_user', {'id': entity.id, 'userId': user.id}) }}"> | ||||
|                                                     <p class="wl-item"> | ||||
|                                                         <span class="badge-user"> | ||||
|                                                             {{ user|chill_entity_render_box }} | ||||
|                                                         </span> | ||||
|                                                         <button class="remove" type="submit"><i class="fa fa-times"></i></button> | ||||
|                                                     </p> | ||||
|                                                 </form> | ||||
|                                             {% else %} | ||||
|                                                 <p class="wl-item"> | ||||
|                                                     <span class="badge-user"> | ||||
|                                                         {{ user|chill_entity_render_box }} | ||||
|                                                     </span> | ||||
|                                                 </p> | ||||
|                                             {% endif %} | ||||
|                                         {% endif %} | ||||
|                                     {% endfor %} | ||||
|                                 </div> | ||||
|                             </div> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                     {% if entity.adminUsers|length > 0 %} | ||||
|                         <div class="item-row separator"> | ||||
|                             <div class="wrap-list"> | ||||
|                                 <div class="wl-row"> | ||||
|                                     <div class="wl-col title"> | ||||
|                                         <strong>{{ 'user_group.adminUsers'|trans }}</strong> | ||||
|                                     </div> | ||||
|                                     <div class="wl-col list"> | ||||
|                                         {% if entity.adminUsers.contains(app.user) %} | ||||
|                                             <p class="wl-item">{% if entity.adminUsers|length > 1 %}{{ 'user_group.me_and'|trans }}{% else %}{{ 'user_group.me_only'|trans }}{% endif %}</p> | ||||
|                                         {% endif %} | ||||
|                                         {% for user in entity.adminUserListByLabelAscending %} | ||||
|                                             {% if user is not same as app.user %} | ||||
|                                                 <p class="wl-item"> | ||||
|                                                     <span class="badge-user"> | ||||
|                                                         {{ user|chill_entity_render_box }} | ||||
|                                                     </span> | ||||
|                                                 </p> | ||||
|                                             {% endif %} | ||||
|                                         {% endfor %} | ||||
|                                     </div> | ||||
|                                 </div> | ||||
|                             </div> | ||||
|                         </div> | ||||
|                     {% endif -%} | ||||
|                     {%- set form = forms.offsetGet(entity) %} | ||||
|                     {%- if form is not null -%} | ||||
|                         <div class="item-row separator"> | ||||
|                             <ul class="record_actions slim"> | ||||
|                                 <li> | ||||
|                                     {{- form_start(form) -}} | ||||
|                                     {{- form_widget(form.users) -}} | ||||
|                                     {{- form_end(form) -}} | ||||
|                                 </li> | ||||
|                             </ul> | ||||
|                         </div> | ||||
|                     {%- endif %} | ||||
|                 </div> | ||||
|             {% endfor %} | ||||
|         </div> | ||||
|  | ||||
|         {{ chill_pagination(paginator) }} | ||||
|     {% endif %} | ||||
| {% endblock %} | ||||
| @@ -0,0 +1,21 @@ | ||||
| {% extends '@ChillMain/CRUD/Admin/index.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 %} | ||||
|     {% include('@ChillMain/CRUD/_new_title.html.twig') %} | ||||
| {% endblock %} | ||||
|  | ||||
| {% block admin_content %} | ||||
|     {% embed '@ChillMain/CRUD/_new_content.html.twig' %} | ||||
|         {% block content_form_actions_save_and_show %}{% endblock %} | ||||
|     {% endembed %} | ||||
| {% endblock admin_content %} | ||||
| @@ -82,17 +82,26 @@ | ||||
|         {{ form_row(transition_form.futureCcUsers) }} | ||||
|         {{ form_errors(transition_form.futureCcUsers) }} | ||||
|         </div> | ||||
|         <div id="future-dest-emails"> | ||||
|         {{ form_row(transition_form.futureDestEmails) }} | ||||
|         {{ form_errors(transition_form.futureDestEmails) }} | ||||
|         </div> | ||||
|     </div> | ||||
|  | ||||
|     <p>{{ form_label(transition_form.comment) }}</p> | ||||
|  | ||||
|     {{ form_widget(transition_form.comment) }} | ||||
|  | ||||
|     <ul class="record_actions"> | ||||
|     <ul class="record_actions sticky-form-buttons"> | ||||
|         {% if entity_workflow.isOnHoldByUser(app.user)  %} | ||||
|             <li> | ||||
|                 <a class="btn btn-misc" href="{{ path('chill_main_workflow_remove_hold', {'id': entity_workflow.currentStep.id }) }}"><i class="fa fa-hourglass"></i> | ||||
|                     {{ 'workflow.Remove hold'|trans }} | ||||
|                 </a> | ||||
|             </li> | ||||
|         {% else %} | ||||
|             <li> | ||||
|                 <a class="btn btn-misc" href="{{ path('chill_main_workflow_on_hold', {'id': entity_workflow.id}) }}"><i class="fa fa-hourglass"></i> | ||||
|                     {{ 'workflow.Put on hold'|trans }} | ||||
|                 </a> | ||||
|             </li> | ||||
|         {% endif %} | ||||
|         <li> | ||||
|             <button type="submit" class="btn btn-save">{{ 'Save'|trans }}</button> | ||||
|         </li> | ||||
| @@ -115,15 +124,6 @@ | ||||
|                 </ul> | ||||
|             {% endif %} | ||||
|  | ||||
|             {% if entity_workflow.currentStep.destEmail|length > 0 %} | ||||
|                 <p><b>{{ 'workflow.An access key was also sent to those addresses'|trans }} :</b></p> | ||||
|                 <ul> | ||||
|                     {% for e in entity_workflow.currentStep.destEmail -%} | ||||
|                         <li><a href="mailto:{{ e|escape('html_attr') }}">{{ e }}</a></li> | ||||
|                     {%- endfor %} | ||||
|                 </ul> | ||||
|             {% endif %} | ||||
|  | ||||
|             {% if entity_workflow.currentStep.destUserByAccessKey|length > 0 %} | ||||
|                 <p><b>{{ 'workflow.Those users are also granted to apply a transition by using an access key'|trans }} :</b></p> | ||||
|                 <ul> | ||||
|   | ||||
| @@ -13,22 +13,22 @@ | ||||
|                         {{ 'workflow.No transitions'|trans }} | ||||
|                     </div> | ||||
|                 {% else %} | ||||
|  | ||||
|                     <div class="item-col"> | ||||
|                         {% if step.previous is not null and step.previous.freezeAfter == true %} | ||||
|                             <i class="fa fa-snowflake-o fa-sm me-1" title="{{ 'workflow.Freezed'|trans }}"></i> | ||||
|                         {% endif %} | ||||
|                         {% if loop.last %} | ||||
|                             {% if entity_workflow.isOnHoldAtCurrentStep %} | ||||
|                                 {% for hold in step.holdsOnStep %} | ||||
|                                     <span class="badge bg-success rounded-pill" title="{{ 'workflow.On hold by'|trans({'by': hold.byUser|chill_entity_render_string})|escape('html_attr') }}">{{ 'workflow.On hold by'|trans({'by': hold.byUser|chill_entity_render_string}) }}</span> | ||||
|                                 {% endfor %} | ||||
|                             {% endif %} | ||||
|                         {% endif %} | ||||
|                     </div> | ||||
|                     <div class="item-col flex-column align-items-end"> | ||||
|                         <div class="decided"> | ||||
|                             {{ place_label }} | ||||
|                         </div> | ||||
|                         {# | ||||
|                         <div class="decided"> | ||||
|                         <i class="fa fa-times fa-fw text-danger"></i> | ||||
|                         Refusé | ||||
|                         </div> | ||||
|                         #} | ||||
|                     </div> | ||||
|                 {% endif %} | ||||
|  | ||||
| @@ -71,19 +71,33 @@ | ||||
|                     </blockquote> | ||||
|                 </div> | ||||
|             {% endif %} | ||||
|             {% if loop.last and step.allDestUser|length > 0 %} | ||||
|             {% if not loop.last and step.signatures|length > 0 %} | ||||
|                 <div class="separator"> | ||||
|                     <div> | ||||
|                         <p><b>{{ 'workflow.signature_required_title'|trans({'nb_signatures': step.signatures|length}) }} :</b></p> | ||||
|                         <div> | ||||
|                             {{ include('@ChillMain/Workflow/_signature_list.html.twig', {'signatures': step.signatures, is_small: true }) }} | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             {% endif %} | ||||
|             {% if loop.last and not step.isFinal %} | ||||
|                 <div class="item-row separator"> | ||||
|                     <div> | ||||
|                         {% if step.destUser|length > 0 %} | ||||
|                         {% if step.destUser|length > 0 or step.destUserGroups|length > 0 %} | ||||
|                             <p><b>{{ 'workflow.Users allowed to apply transition'|trans }} : </b></p> | ||||
|                             <ul> | ||||
|                                 {% for u in step.destUser %} | ||||
|                                     <li>{{ u|chill_entity_render_box({'at_date': step.previous.transitionAt}) }} | ||||
|                                         {% if entity_workflow.isOnHoldAtCurrentStep %} | ||||
|                                     <li> | ||||
|                                         <span class="badge-user">{{ u|chill_entity_render_box({'at_date': step.previous.transitionAt}) }}</span> | ||||
|                                         {% if step.isOnHoldByUser(u) %} | ||||
|                                             <span class="badge bg-success rounded-pill" title="{{ 'workflow.On hold'|trans|escape('html_attr') }}">{{ 'workflow.On hold'|trans }}</span> | ||||
|                                         {% endif %} | ||||
|                                     </li> | ||||
|                                 {% endfor %} | ||||
|                                 {% for u in step.destUserGroups %} | ||||
|                                     <li>{{ u|chill_entity_render_box({'at_date': step.previous.transitionAt}) }}</li> | ||||
|                                 {% endfor %} | ||||
|                             </ul> | ||||
|                         {% endif %} | ||||
|  | ||||
| @@ -115,6 +129,10 @@ | ||||
|                         {% endif %} | ||||
|                     </div> | ||||
|                 </div> | ||||
|                 {% if step.signatures|length > 0 %} | ||||
|                     <p><b>{{ 'workflow.signature_required_title'|trans({'nb_signatures': step.signatures|length}) }} :</b></p> | ||||
|                     {{ include('@ChillMain/Workflow/_signature_list.html.twig', {'signatures': step.signatures, is_small: true }) }} | ||||
|                 {% endif %} | ||||
|             {% endif %} | ||||
|         </div> | ||||
|  | ||||
|   | ||||
| @@ -1,54 +1,3 @@ | ||||
| <h2>{{ 'workflow.signature_zone.title'|trans }}</h2> | ||||
|  | ||||
| <div class="container"> | ||||
|     {% for s in signatures %} | ||||
|         <div class="row row-hover align-items-center"> | ||||
|                 <div class="col-sm-12 col-md-5"> | ||||
|                     {% if s.signerKind == 'person' %} | ||||
|                         {% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with { | ||||
|                             action: 'show', displayBadge: true, | ||||
|                             targetEntity: { name: 'person', id: s.signer.id }, | ||||
|                             buttonText: s.signer|chill_entity_render_string, | ||||
|                             isDead: s.signer.deathDate is not null | ||||
|                         } %} | ||||
|                     {% else %} | ||||
|                         {% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with { | ||||
|                             action: 'show', displayBadge: true, | ||||
|                             targetEntity: { name: 'user', id: s.signer.id }, | ||||
|                             buttonText: s.signer|chill_entity_render_string, | ||||
|                         } %} | ||||
|                     {% endif %} | ||||
|                 </div> | ||||
|                 <div class="col-sm-12 col-md-7 text-end"> | ||||
|                         {% if s.isSigned %} | ||||
|                             <span class="text-end">{{ 'workflow.signature.signed_statement'|trans({ 'datetime' : s.stateDate }) }}</span> | ||||
|                         {% elseif s.isCanceled %} | ||||
|                             <span class="text-end">{{ 'workflow.signature.canceled_statement'|trans({ 'datetime' : s.stateDate }) }}</span> | ||||
|                         {% elseif s.isRejected%} | ||||
|                             <span class="text-end">{{ 'workflow.signature.rejected_statement'|trans({ 'datetime' : s.stateDate }) }}</span> | ||||
|                         {% else %} | ||||
|                             {% if (is_granted('CHILL_MAIN_ENTITY_WORKFLOW_SIGNATURE_CANCEL', s) or is_granted('CHILL_MAIN_ENTITY_WORKFLOW_SIGNATURE_SIGN', s)) %} | ||||
|                                 <ul class="record_actions slim"> | ||||
|                                     {% if is_granted('CHILL_MAIN_ENTITY_WORKFLOW_SIGNATURE_REJECT', s) %} | ||||
|                                         <li> | ||||
|                                             <a class="btn btn-remove" href="{{ chill_path_add_return_path('chill_main_workflow_signature_reject', { 'id': s.id}) }}">{{ 'workflow.signature_zone.button_reject'|trans }}</a> | ||||
|                                         </li> | ||||
|                                     {% endif %} | ||||
|                                     {% if is_granted('CHILL_MAIN_ENTITY_WORKFLOW_SIGNATURE_CANCEL', s) %} | ||||
|                                         <li> | ||||
|                                             <a class="btn btn-misc" href="{{ chill_path_add_return_path('chill_main_workflow_signature_cancel', { 'id': s.id}) }}">{{ 'workflow.signature_zone.button_cancel'|trans }}</a> | ||||
|                                         </li> | ||||
|                                     {% endif %} | ||||
|                                     {% if is_granted('CHILL_MAIN_ENTITY_WORKFLOW_SIGNATURE_SIGN', s) %} | ||||
|                                         <li> | ||||
|                                             <a class="btn btn-misc" href="{{ chill_path_add_return_path('chill_main_workflow_signature_metadata', { 'signature_id': s.id}) }}"><i class="fa fa-pencil-square-o"></i> {{ 'workflow.signature_zone.button_sign'|trans }}</a> | ||||
|                                         </li> | ||||
|                                     {% endif %} | ||||
|                                 </ul> | ||||
|                             {% endif %} | ||||
|                         {% endif %} | ||||
|                 </div> | ||||
|         </div> | ||||
|     {% endfor %} | ||||
| </div> | ||||
| <h2>{{ 'workflow.signature_required_title'|trans({'nb_signatures': signatures|length}) }}</h2> | ||||
|  | ||||
| {{ include('@ChillMain/Workflow/_signature_list.html.twig') }} | ||||
|   | ||||
| @@ -0,0 +1,52 @@ | ||||
| <div class="container"> | ||||
|     {% for s in signatures %} | ||||
|         <div class="row row-hover align-items-center"> | ||||
|             <div class="col-sm-12 col-md-5"> | ||||
|                 {% if s.signerKind == 'person' %} | ||||
|                     {% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with { | ||||
|                         action: 'show', displayBadge: true, | ||||
|                         targetEntity: { name: 'person', id: s.signer.id }, | ||||
|                         buttonText: s.signer|chill_entity_render_string, | ||||
|                         isDead: s.signer.deathDate is not null | ||||
|                     } %} | ||||
|                 {% else %} | ||||
|                     {% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with { | ||||
|                         action: 'show', displayBadge: true, | ||||
|                         targetEntity: { name: 'user', id: s.signer.id }, | ||||
|                         buttonText: s.signer|chill_entity_render_string, | ||||
|                     } %} | ||||
|                 {% endif %} | ||||
|             </div> | ||||
|             <div class="col-sm-12 col-md-7 text-end"> | ||||
|                 {% if s.isSigned %} | ||||
|                     <span class="text-end">{{ 'workflow.signature.signed_statement'|trans({ 'datetime' : s.stateDate }) }}</span> | ||||
|                 {% elseif s.isCanceled %} | ||||
|                     <span class="text-end">{{ 'workflow.signature.canceled_statement'|trans({ 'datetime' : s.stateDate }) }}</span> | ||||
|                 {% elseif s.isRejected%} | ||||
|                     <span class="text-end">{{ 'workflow.signature.rejected_statement'|trans({ 'datetime' : s.stateDate }) }}</span> | ||||
|                 {% else %} | ||||
|                     {% if (is_granted('CHILL_MAIN_ENTITY_WORKFLOW_SIGNATURE_CANCEL', s) or is_granted('CHILL_MAIN_ENTITY_WORKFLOW_SIGNATURE_SIGN', s)) %} | ||||
|                         <ul class="record_actions slim {% if is_small|default(false) %}small{% endif %}"> | ||||
|                             {% if is_granted('CHILL_MAIN_ENTITY_WORKFLOW_SIGNATURE_REJECT', s) %} | ||||
|                                 <li> | ||||
|                                     <a class="btn btn-remove" href="{{ chill_path_add_return_path('chill_main_workflow_signature_reject', { 'id': s.id}) }}">{{ 'workflow.signature_zone.button_reject'|trans }}</a> | ||||
|                                 </li> | ||||
|                             {% endif %} | ||||
|                             {% if is_granted('CHILL_MAIN_ENTITY_WORKFLOW_SIGNATURE_CANCEL', s) %} | ||||
|                                 <li> | ||||
|                                     <a class="btn btn-misc" href="{{ chill_path_add_return_path('chill_main_workflow_signature_cancel', { 'id': s.id}) }}">{{ 'workflow.signature_zone.button_cancel'|trans }}</a> | ||||
|                                 </li> | ||||
|                             {% endif %} | ||||
|                             {% if is_granted('CHILL_MAIN_ENTITY_WORKFLOW_SIGNATURE_SIGN', s) %} | ||||
|                                 <li> | ||||
|                                     <a class="btn btn-misc" href="{{ chill_path_add_return_path('chill_main_workflow_signature_metadata', { 'signature_id': s.id}) }}"><i class="fa fa-pencil-square-o"></i> {{ 'workflow.signature_zone.button_sign'|trans }}</a> | ||||
|                                 </li> | ||||
|                             {% endif %} | ||||
|                         </ul> | ||||
|                     {% endif %} | ||||
|                 {% endif %} | ||||
|             </div> | ||||
|         </div> | ||||
|     {% endfor %} | ||||
| </div> | ||||
|  | ||||
| @@ -62,26 +62,12 @@ | ||||
|     <section class="step my-4">{% include '@ChillMain/Workflow/_follow.html.twig' %}</section> | ||||
|     {% if signatures|length > 0 %} | ||||
|         <section class="step my-4">{% include '@ChillMain/Workflow/_signature.html.twig' %}</section> | ||||
|     {% else %} | ||||
|         <section class="step my-4">{% include '@ChillMain/Workflow/_decision.html.twig' %}</section> | ||||
|     {% endif %} | ||||
|     <section class="step my-4">{% include '@ChillMain/Workflow/_decision.html.twig' %}</section>{# | ||||
|     <section class="step my-4">{% include '@ChillMain/Workflow/_comment.html.twig' %}</section> #} | ||||
|     {# <section class="step my-4">{% include '@ChillMain/Workflow/_comment.html.twig' %}</section> #} | ||||
|     <section class="step my-4">{% include '@ChillMain/Workflow/_history.html.twig' %}</section> | ||||
|  | ||||
|     <ul class="record_actions sticky-form-buttons"> | ||||
|         {% if entity_workflow.isOnHoldByUser(app.user)  %} | ||||
|             <li> | ||||
|                 <a class="btn btn-misc" href="{{ path('chill_main_workflow_remove_hold', {'id': entity_workflow.currentStep.id }) }}"><i class="fa fa-hourglass"></i> | ||||
|                     {{ 'workflow.Remove hold'|trans }} | ||||
|                 </a> | ||||
|             </li> | ||||
|         {% else %} | ||||
|             <li> | ||||
|                 <a class="btn btn-misc" href="{{ path('chill_main_workflow_on_hold', {'id': entity_workflow.id}) }}"><i class="fa fa-hourglass"></i> | ||||
|                     {{ 'workflow.Put on hold'|trans }} | ||||
|                 </a> | ||||
|             </li> | ||||
|         {% endif %} | ||||
|     </ul> | ||||
|  | ||||
| </div> | ||||
| {% endblock %} | ||||
|   | ||||
| @@ -1,15 +0,0 @@ | ||||
| Madame, Monsieur, | ||||
|  | ||||
| Un suivi "{{ workflow.text }}" a atteint une nouvelle étape: {{ workflow.text }}. | ||||
|  | ||||
| Titre du workflow: "{{ entityTitle }}". | ||||
|  | ||||
| Vous êtes invité·e à valider cette étape. Pour obtenir un accès, vous pouvez cliquer sur le lien suivant: | ||||
|  | ||||
| {{ absolute_url(path('chill_main_workflow_grant_access_by_key', {'id': entity_workflow.currentStep.id, 'accessKey': entity_workflow.currentStep.accessKey, '_locale': fr})) }} | ||||
|  | ||||
| Dès que vous aurez cliqué une fois sur le lien, vous serez autorisé à valider cette étape. | ||||
|  | ||||
| Notez que vous devez disposer d'un compte utilisateur valide dans Chill. | ||||
|  | ||||
| Cordialement, | ||||
| @@ -1 +0,0 @@ | ||||
| Un suivi {{ workflow.text }} demande votre attention: {{ entityTitle }} | ||||
| @@ -49,19 +49,19 @@ | ||||
|  | ||||
|                             {% for flashMessage in app.session.flashbag.get('success') %} | ||||
|                                 <div class="alert alert-success flash_message"> | ||||
|                                     <span>{{ flashMessage|raw }}</span> | ||||
|                                     <span>{{ flashMessage|trans }}</span> | ||||
|                                 </div> | ||||
|                             {% endfor %} | ||||
|  | ||||
|                             {% for flashMessage in app.session.flashbag.get('error') %} | ||||
|                                 <div class="alert alert-danger flash_message"> | ||||
|                                     <span>{{ flashMessage|raw }}</span> | ||||
|                                     <span>{{ flashMessage|trans }}</span> | ||||
|                                 </div> | ||||
|                             {% endfor %} | ||||
|  | ||||
|                             {% for flashMessage in app.session.flashbag.get('notice') %} | ||||
|                                 <div class="alert alert-warning flash_message"> | ||||
|                                     <span>{{ flashMessage|raw }}</span> | ||||
|                                     <span>{{ flashMessage|trans }}</span> | ||||
|                                 </div> | ||||
|                             {% endfor %} | ||||
|  | ||||
|   | ||||
| @@ -27,7 +27,7 @@ final readonly class ChillUrlGenerator implements ChillUrlGeneratorInterface | ||||
|     { | ||||
|         $uri = $this->requestStack->getCurrentRequest()->getRequestUri(); | ||||
|  | ||||
|         return $this->urlGenerator->generate($name, [$parameters, 'returnPath' => $uri], $referenceType); | ||||
|         return $this->urlGenerator->generate($name, [...$parameters, 'returnPath' => $uri], $referenceType); | ||||
|     } | ||||
|  | ||||
|     public function returnPathOr(string $name, array $parameters = [], int $referenceType = UrlGeneratorInterface::ABSOLUTE_PATH): string | ||||
|   | ||||
| @@ -67,6 +67,10 @@ class AdminUserMenuBuilder implements LocalMenuBuilderInterface | ||||
|             'route' => 'chill_crud_admin_user_index', | ||||
|         ])->setExtras(['order' => 1040]); | ||||
|  | ||||
|         $menu->addChild('crud.admin_user_group.index.title', [ | ||||
|             'route' => 'chill_crud_admin_user_group_index', | ||||
|         ])->setExtras(['order' => 1042]); | ||||
|  | ||||
|         $menu->addChild('User jobs', [ | ||||
|             'route' => 'chill_crud_admin_user_job_index', | ||||
|         ])->setExtras(['order' => 1050]); | ||||
|   | ||||
| @@ -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]), | ||||
|   | ||||
| @@ -0,0 +1,59 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * 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. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\MainBundle\Search\Entity; | ||||
|  | ||||
| use Chill\MainBundle\Repository\UserGroupRepositoryInterface; | ||||
| use Chill\MainBundle\Search\SearchApiInterface; | ||||
| use Chill\MainBundle\Search\SearchApiQuery; | ||||
| use Symfony\Contracts\Translation\LocaleAwareInterface; | ||||
|  | ||||
| /** | ||||
|  * Provide search api for user group. | ||||
|  */ | ||||
| class SearchUserGroupApiProvider implements SearchApiInterface, LocaleAwareInterface | ||||
| { | ||||
|     private string $locale; | ||||
|  | ||||
|     public function __construct(private readonly UserGroupRepositoryInterface $userGroupRepository) {} | ||||
|  | ||||
|     public function setLocale(string $locale): void | ||||
|     { | ||||
|         $this->locale = $locale; | ||||
|     } | ||||
|  | ||||
|     public function getLocale(): string | ||||
|     { | ||||
|         return $this->locale; | ||||
|     } | ||||
|  | ||||
|     public function getResult(string $key, array $metadata, float $pertinence) | ||||
|     { | ||||
|         return $this->userGroupRepository->find($metadata['id']); | ||||
|     } | ||||
|  | ||||
|     public function prepare(array $metadatas): void {} | ||||
|  | ||||
|     public function provideQuery(string $pattern, array $parameters): SearchApiQuery | ||||
|     { | ||||
|         return $this->userGroupRepository->provideSearchApiQuery($pattern, $this->getLocale(), 'user-group'); | ||||
|     } | ||||
|  | ||||
|     public function supportsResult(string $key, array $metadatas): bool | ||||
|     { | ||||
|         return 'user-group' === $key; | ||||
|     } | ||||
|  | ||||
|     public function supportsTypes(string $pattern, array $types, array $parameters): bool | ||||
|     { | ||||
|         return in_array('user-group', $types, true); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,46 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * 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. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\MainBundle\Security\Authorization; | ||||
|  | ||||
| use Chill\MainBundle\Entity\User; | ||||
| use Chill\MainBundle\Entity\UserGroup; | ||||
| use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; | ||||
| use Symfony\Component\Security\Core\Authorization\Voter\Voter; | ||||
| use Symfony\Component\Security\Core\Security; | ||||
|  | ||||
| final class UserGroupVoter extends Voter | ||||
| { | ||||
|     public const APPEND_TO_GROUP = 'CHILL_MAIN_USER_GROUP_APPEND_TO_GROUP'; | ||||
|  | ||||
|     public function __construct(private readonly Security $security) {} | ||||
|  | ||||
|     protected function supports(string $attribute, $subject) | ||||
|     { | ||||
|         return self::APPEND_TO_GROUP === $attribute && $subject instanceof UserGroup; | ||||
|     } | ||||
|  | ||||
|     protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token) | ||||
|     { | ||||
|         /* @var UserGroup $subject */ | ||||
|         if ($this->security->isGranted('ROLE_ADMIN')) { | ||||
|             return true; | ||||
|         } | ||||
|  | ||||
|         $user = $this->security->getUser(); | ||||
|  | ||||
|         if (!$user instanceof User) { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         return $subject->getAdminUsers()->contains($user); | ||||
|     } | ||||
| } | ||||
| @@ -47,7 +47,7 @@ class DiscriminatedObjectDenormalizer implements ContextAwareDenormalizerInterfa | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         throw new RuntimeException(sprintf('Could not find any denormalizer for those ALLOWED_TYPES: %s', \implode(', ', $context[self::ALLOWED_TYPES]))); | ||||
|         throw new RuntimeException(sprintf('Could not find any denormalizer for those ALLOWED_TYPES: %s', \implode(', ', $context[self::ALLOWED_TYPES])), previous: $lastException ?? null); | ||||
|     } | ||||
|  | ||||
|     public function supportsDenormalization($data, $type, $format = null, array $context = []) | ||||
|   | ||||
| @@ -0,0 +1,37 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * 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. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\MainBundle\Serializer\Normalizer; | ||||
|  | ||||
| use Chill\MainBundle\Entity\UserGroup; | ||||
| use Chill\MainBundle\Repository\UserGroupRepositoryInterface; | ||||
| use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; | ||||
|  | ||||
| class UserGroupDenormalizer implements DenormalizerInterface | ||||
| { | ||||
|     public function __construct(private readonly UserGroupRepositoryInterface $userGroupRepository) {} | ||||
|  | ||||
|     public function denormalize($data, string $type, ?string $format = null, array $context = []): ?UserGroup | ||||
|     { | ||||
|         return $this->userGroupRepository->find($data['id']); | ||||
|     } | ||||
|  | ||||
|     public function supportsDenormalization($data, string $type, ?string $format = null): bool | ||||
|     { | ||||
|         return UserGroup::class === $type | ||||
|             && 'json' === $format | ||||
|             && is_array($data) | ||||
|             && array_key_exists('id', $data) | ||||
|             && 'user_group' === ($data['type'] ?? false) | ||||
|             && 2 === count(array_keys($data)) | ||||
|         ; | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,41 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * 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. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\MainBundle\Serializer\Normalizer; | ||||
|  | ||||
| use Chill\MainBundle\Entity\UserGroup; | ||||
| use Chill\MainBundle\Templating\Entity\UserGroupRenderInterface; | ||||
| use Symfony\Component\Serializer\Normalizer\NormalizerInterface; | ||||
|  | ||||
| class UserGroupNormalizer implements NormalizerInterface | ||||
| { | ||||
|     public function __construct(private readonly UserGroupRenderInterface $userGroupRender) {} | ||||
|  | ||||
|     public function normalize($object, ?string $format = null, array $context = []) | ||||
|     { | ||||
|         /* @var UserGroup $object */ | ||||
|  | ||||
|         return [ | ||||
|             'type' => 'user_group', | ||||
|             'id' => $object->getId(), | ||||
|             'label' => $object->getLabel(), | ||||
|             'backgroundColor' => $object->getBackgroundColor(), | ||||
|             'foregroundColor' => $object->getForegroundColor(), | ||||
|             'excludeKey' => $object->getExcludeKey(), | ||||
|             'text' => $this->userGroupRender->renderString($object, []), | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     public function supportsNormalization($data, ?string $format = null) | ||||
|     { | ||||
|         return $data instanceof UserGroup; | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,38 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * 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. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\MainBundle\Templating\Entity; | ||||
|  | ||||
| use Chill\MainBundle\Entity\UserGroup; | ||||
| use Chill\MainBundle\Templating\TranslatableStringHelperInterface; | ||||
| use Twig\Environment; | ||||
|  | ||||
| final readonly class UserGroupRender implements UserGroupRenderInterface | ||||
| { | ||||
|     public function __construct(private TranslatableStringHelperInterface $translatableStringHelper, private Environment $environment) {} | ||||
|  | ||||
|     public function renderBox($entity, array $options): string | ||||
|     { | ||||
|         /* @var $entity UserGroup */ | ||||
|         return $this->environment->render('@ChillMain/Entity/user_group.html.twig', ['user_group' => $entity]); | ||||
|     } | ||||
|  | ||||
|     public function renderString($entity, array $options): string | ||||
|     { | ||||
|         /* @var $entity UserGroup */ | ||||
|         return (string) $this->translatableStringHelper->localize($entity->getLabel()); | ||||
|     } | ||||
|  | ||||
|     public function supports(object $entity, array $options): bool | ||||
|     { | ||||
|         return $entity instanceof UserGroup; | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,14 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * 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. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\MainBundle\Templating\Entity; | ||||
|  | ||||
| interface UserGroupRenderInterface extends ChillEntityRenderInterface {} | ||||
| @@ -11,6 +11,7 @@ declare(strict_types=1); | ||||
|  | ||||
| namespace ChillMainBundle\Tests\Repository; | ||||
|  | ||||
| use Chill\MainBundle\Entity\User; | ||||
| use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository; | ||||
| use Doctrine\ORM\EntityManagerInterface; | ||||
| use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; | ||||
| @@ -43,4 +44,14 @@ class EntityWorkflowRepositoryTest extends KernelTestCase | ||||
|  | ||||
|         self::assertIsArray($actual, 'check that the query is successful'); | ||||
|     } | ||||
|  | ||||
|     public function testCountQueryByDest(): void | ||||
|     { | ||||
|         $repository = new EntityWorkflowRepository($this->em); | ||||
|         $user = $this->em->createQuery(sprintf('SELECT u FROM %s u', User::class)) | ||||
|             ->setMaxResults(1)->getSingleResult(); | ||||
|         $actual = $repository->countByDest($user); | ||||
|  | ||||
|         self::assertIsInt($actual); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,44 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * 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. | ||||
|  */ | ||||
|  | ||||
| namespace ChillMainBundle\Tests\Repository; | ||||
|  | ||||
| use Chill\MainBundle\Entity\User; | ||||
| use Chill\MainBundle\Repository\Workflow\EntityWorkflowStepRepository; | ||||
| use Doctrine\ORM\EntityManagerInterface; | ||||
| use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; | ||||
|  | ||||
| /** | ||||
|  * @internal | ||||
|  * | ||||
|  * @coversNothing | ||||
|  */ | ||||
| class EntityWorkflowStepRepositoryTest extends KernelTestCase | ||||
| { | ||||
|     private EntityManagerInterface $entityManager; | ||||
|  | ||||
|     protected function setUp(): void | ||||
|     { | ||||
|         self::bootKernel(); | ||||
|         $this->entityManager = self::getContainer()->get(EntityManagerInterface::class); | ||||
|     } | ||||
|  | ||||
|     public function testCountUnreadByUser(): void | ||||
|     { | ||||
|         $repository = new EntityWorkflowStepRepository($this->entityManager); | ||||
|         $user = $this->entityManager->createQuery(sprintf('SELECT u FROM %s u', User::class)) | ||||
|             ->setMaxResults(1)->getSingleResult(); | ||||
|  | ||||
|         $actual = $repository->countUnreadByUser($user); | ||||
|  | ||||
|         self::assertIsInt($actual); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,50 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * 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. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\MainBundle\Tests\Repository; | ||||
|  | ||||
| use Chill\MainBundle\Repository\UserGroupRepository; | ||||
| use Doctrine\ORM\EntityManagerInterface; | ||||
| use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; | ||||
|  | ||||
| /** | ||||
|  * @internal | ||||
|  * | ||||
|  * @coversNothing | ||||
|  */ | ||||
| class UserGroupRepositoryTest extends KernelTestCase | ||||
| { | ||||
|     private EntityManagerInterface $entityManager; | ||||
|  | ||||
|     protected function setUp(): void | ||||
|     { | ||||
|         self::bootKernel(); | ||||
|         $this->entityManager = static::getContainer()->get(EntityManagerInterface::class); | ||||
|  | ||||
|     } | ||||
|  | ||||
|     public function testProvideSearchApiQuery(): void | ||||
|     { | ||||
|         $repository = new UserGroupRepository($this->entityManager); | ||||
|  | ||||
|         $apiQuery = $repository->provideSearchApiQuery('trav', 'fr'); | ||||
|  | ||||
|         // test that the query does works | ||||
|         $sql = $apiQuery->buildQuery(); | ||||
|         $params = $apiQuery->buildParameters(); | ||||
|  | ||||
|         $result = $this->entityManager->getConnection()->executeQuery($sql, $params); | ||||
|         $results = $result->fetchAllAssociative(); | ||||
|  | ||||
|         self::assertIsArray($results); | ||||
|  | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,62 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * 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. | ||||
|  */ | ||||
|  | ||||
| namespace Serializer\Normalizer; | ||||
|  | ||||
| use Chill\MainBundle\Entity\UserGroup; | ||||
| use Chill\MainBundle\Repository\UserGroupRepositoryInterface; | ||||
| use Chill\MainBundle\Serializer\Normalizer\UserGroupDenormalizer; | ||||
| use PHPUnit\Framework\TestCase; | ||||
|  | ||||
| /** | ||||
|  * @internal | ||||
|  * | ||||
|  * @coversNothing | ||||
|  */ | ||||
| class UserGroupDenormalizerTest extends TestCase | ||||
| { | ||||
|     /** | ||||
|      * @throws \PHPUnit\Framework\MockObject\Exception | ||||
|      * | ||||
|      * @dataProvider provideSupportsDenormalization | ||||
|      */ | ||||
|     public function testSupportsDenormalization($data, string $type, bool $expected): void | ||||
|     { | ||||
|         $repository = $this->createMock(UserGroupRepositoryInterface::class); | ||||
|         $denormalizer = new UserGroupDenormalizer($repository); | ||||
|  | ||||
|         $actual = $denormalizer->supportsDenormalization($data, $type, 'json'); | ||||
|  | ||||
|         self::assertSame($expected, $actual); | ||||
|     } | ||||
|  | ||||
|     public static function provideSupportsDenormalization(): iterable | ||||
|     { | ||||
|         yield [['type' => 'user_group', 'id' => 10], UserGroup::class, true]; | ||||
|         yield [['type' => 'person', 'id' => 10], UserGroup::class, false]; | ||||
|         yield [['type' => 'user_group', 'id' => 10], \stdClass::class, false]; | ||||
|     } | ||||
|  | ||||
|     public function testDenormalize(): void | ||||
|     { | ||||
|         $repository = $this->createMock(UserGroupRepositoryInterface::class); | ||||
|         $repository->expects($this->once()) | ||||
|             ->method('find') | ||||
|             ->with(10) | ||||
|             ->willReturn($userGroup = new UserGroup()); | ||||
|  | ||||
|         $denormalizer = new UserGroupDenormalizer($repository); | ||||
|  | ||||
|         $actual = $denormalizer->denormalize(['type' => 'user_group', 'id' => 10], UserGroup::class, 'json'); | ||||
|  | ||||
|         self::assertSame($userGroup, $actual); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,56 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * 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. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\MainBundle\Tests\Serializer\Normalizer; | ||||
|  | ||||
| use Chill\MainBundle\Entity\UserGroup; | ||||
| use Chill\MainBundle\Serializer\Normalizer\UserGroupNormalizer; | ||||
| use Chill\MainBundle\Templating\Entity\UserGroupRenderInterface; | ||||
| use PHPUnit\Framework\TestCase; | ||||
| use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; | ||||
|  | ||||
| /** | ||||
|  * @internal | ||||
|  * | ||||
|  * @coversNothing | ||||
|  */ | ||||
| class UserGroupNormalizerTest extends TestCase | ||||
| { | ||||
|     public function testNormalize() | ||||
|     { | ||||
|         $userGroup = new UserGroup(); | ||||
|         $userGroup | ||||
|             ->setLabel(['fr' => 'test']) | ||||
|             ->setExcludeKey('top') | ||||
|             ->setForegroundColor('#123456') | ||||
|             ->setBackgroundColor('#456789'); | ||||
|  | ||||
|         $entityRender = $this->createMock(UserGroupRenderInterface::class); | ||||
|         $entityRender->expects($this->once()) | ||||
|             ->method('renderString') | ||||
|             ->with($userGroup, []) | ||||
|             ->willReturn('text'); | ||||
|  | ||||
|         $normalizer = new UserGroupNormalizer($entityRender); | ||||
|  | ||||
|         $actual = $normalizer->normalize($userGroup, 'json', [AbstractNormalizer::GROUPS => ['read']]); | ||||
|  | ||||
|         self::assertEqualsCanonicalizing([ | ||||
|             'type' => 'user_group', | ||||
|             'text' => 'text', | ||||
|             'label' => ['fr' => 'test'], | ||||
|             'excludeKey' => 'top', | ||||
|             'foregroundColor' => '#123456', | ||||
|             'backgroundColor' => '#456789', | ||||
|             'id' => null, | ||||
|         ], $actual); | ||||
|     } | ||||
| } | ||||
| @@ -44,7 +44,6 @@ class EntityWorkflowMarkingStoreTest extends TestCase | ||||
|         $dto = new WorkflowTransitionContextDTO($workflow); | ||||
|         $dto->futureCcUsers[] = $user1 = new User(); | ||||
|         $dto->futureDestUsers[] = $user2 = new User(); | ||||
|         $dto->futureDestEmails[] = $email = 'test@example.com'; | ||||
|  | ||||
|         $markingStore->setMarking($workflow, new Marking(['foo' => 1]), [ | ||||
|             'context' => $dto, | ||||
| @@ -55,7 +54,6 @@ class EntityWorkflowMarkingStoreTest extends TestCase | ||||
|  | ||||
|         $currentStep = $workflow->getCurrentStep(); | ||||
|         self::assertEquals('foo', $currentStep->getCurrentStep()); | ||||
|         self::assertContains($email, $currentStep->getDestEmail()); | ||||
|         self::assertContains($user1, $currentStep->getCcUser()); | ||||
|         self::assertContains($user2, $currentStep->getDestUser()); | ||||
|  | ||||
|   | ||||
| @@ -12,6 +12,7 @@ declare(strict_types=1); | ||||
| namespace Chill\MainBundle\Tests\Workflow\EventSubscriber; | ||||
|  | ||||
| use Chill\MainBundle\Entity\User; | ||||
| use Chill\MainBundle\Entity\UserGroup; | ||||
| use Chill\MainBundle\Entity\Workflow\EntityWorkflow; | ||||
| use Chill\MainBundle\Security\Authorization\EntityWorkflowTransitionVoter; | ||||
| use Chill\MainBundle\Templating\Entity\UserRender; | ||||
| @@ -145,6 +146,11 @@ class EntityWorkflowGuardTransitionTest extends TestCase | ||||
|         yield [self::buildEntityWorkflow([new User()]), 'transition1', null, false, 'd9e39a18-704c-11ef-b235-8fe0619caee7']; | ||||
|         yield [self::buildEntityWorkflow([$user = new User()]), 'transition3', $user, false, '5b6b95e0-704d-11ef-a5a9-4b6fc11a8eeb']; | ||||
|         yield [self::buildEntityWorkflow([$user = new User()]), 'transition3', $user, true, '5b6b95e0-704d-11ef-a5a9-4b6fc11a8eeb']; | ||||
|  | ||||
|         $userGroup = new UserGroup(); | ||||
|         $userGroup->addUser(new User()); | ||||
|  | ||||
|         yield [self::buildEntityWorkflow([$userGroup]), 'transition1', new User(), false, 'f3eeb57c-7532-11ec-9495-e7942a2ac7bc']; | ||||
|     } | ||||
|  | ||||
|     public static function provideValidTransition(): iterable | ||||
| @@ -159,6 +165,10 @@ class EntityWorkflowGuardTransitionTest extends TestCase | ||||
|         // transition allowed thanks to permission "apply all transitions" | ||||
|         yield [self::buildEntityWorkflow([new User()]), 'transition1', new User(), true, 'step1']; | ||||
|         yield [self::buildEntityWorkflow([new User()]), 'transition2', new User(), true, 'step2']; | ||||
|  | ||||
|         $userGroup = new UserGroup(); | ||||
|         $userGroup->addUser($u = new User()); | ||||
|         yield [self::buildEntityWorkflow([$userGroup]), 'transition1', $u, false, 'step1']; | ||||
|     } | ||||
|  | ||||
|     public static function buildEntityWorkflow(array $futureDestUsers): EntityWorkflow | ||||
|   | ||||
| @@ -13,6 +13,7 @@ namespace Chill\MainBundle\Tests\Workflow\EventSubscriber; | ||||
|  | ||||
| use Chill\MainBundle\Entity\Notification; | ||||
| use Chill\MainBundle\Entity\User; | ||||
| use Chill\MainBundle\Entity\UserGroup; | ||||
| use Chill\MainBundle\Entity\Workflow\EntityWorkflow; | ||||
| use Chill\MainBundle\Entity\Workflow\EntityWorkflowStep; | ||||
| use Chill\MainBundle\Workflow\EventSubscriber\NotificationOnTransition; | ||||
| @@ -20,8 +21,6 @@ use Chill\MainBundle\Workflow\Helper\MetadataExtractor; | ||||
| use Doctrine\ORM\EntityManagerInterface; | ||||
| use PHPUnit\Framework\TestCase; | ||||
| use Prophecy\Argument; | ||||
| use Prophecy\Call\Call; | ||||
| use Prophecy\Exception\Prediction\FailedPredictionException; | ||||
| use Prophecy\PhpUnit\ProphecyTrait; | ||||
| use Symfony\Component\Security\Core\Security; | ||||
| use Symfony\Component\Workflow\Event\Event; | ||||
| @@ -57,29 +56,23 @@ final class NotificationOnTransitionTest extends TestCase | ||||
|         $id->setValue($entityWorkflow, 1); | ||||
|  | ||||
|         $step = new EntityWorkflowStep(); | ||||
|         $userGroup = (new UserGroup())->addUser($userInGroup = new User())->addUser($dest); | ||||
|         $entityWorkflow->addStep($step); | ||||
|         $step->addDestUser($dest) | ||||
|         $step | ||||
|             ->addDestUser($dest) | ||||
|             ->addDestUserGroup($userGroup) | ||||
|             ->setCurrentStep('to_state'); | ||||
|  | ||||
|         $em = $this->prophesize(EntityManagerInterface::class); | ||||
|         $em->persist(Argument::type(Notification::class))->should( | ||||
|             static function ($args) use ($dest) { | ||||
|                 /** @var Call[] $args */ | ||||
|                 if (1 !== \count($args)) { | ||||
|                     throw new FailedPredictionException('no notification sent'); | ||||
|                 } | ||||
|  | ||||
|                 $notification = $args[0]->getArguments()[0]; | ||||
|  | ||||
|                 if (!$notification instanceof Notification) { | ||||
|                     throw new FailedPredictionException('persist is not a notification'); | ||||
|                 } | ||||
|  | ||||
|                 if (!$notification->getAddressees()->contains($dest)) { | ||||
|                     throw new FailedPredictionException('the dest is not notified'); | ||||
|                 } | ||||
|             } | ||||
|         ); | ||||
|         // we check that both notification has been persisted once, | ||||
|         // eliminating doublons | ||||
|         $em->persist(Argument::that( | ||||
|             fn ($notificationCandidate) => $notificationCandidate instanceof Notification && $notificationCandidate->getAddressees()->contains($dest) | ||||
|         ))->shouldBeCalledOnce(); | ||||
|         $em->persist(Argument::that( | ||||
|             fn ($notificationCandidate) => $notificationCandidate instanceof Notification && $notificationCandidate->getAddressees()->contains($userInGroup) | ||||
|         ))->shouldBeCalledOnce(); | ||||
|  | ||||
|         $engine = $this->prophesize(\Twig\Environment::class); | ||||
|         $engine->render(Argument::type('string'), Argument::type('array')) | ||||
|   | ||||
| @@ -0,0 +1,31 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * 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. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\MainBundle\Validation\Constraint; | ||||
|  | ||||
| use Symfony\Component\Validator\Constraint; | ||||
|  | ||||
| #[\Attribute(\Attribute::TARGET_PROPERTY)] | ||||
| class UserGroupDoNotExclude extends Constraint | ||||
| { | ||||
|     public string $message = 'The groups {{ excluded_groups }} do exclude themselves. Please choose one between them'; | ||||
|     public string $code = 'e16c8226-0090-11ef-8560-f7239594db09'; | ||||
|  | ||||
|     public function getTargets() | ||||
|     { | ||||
|         return [self::PROPERTY_CONSTRAINT]; | ||||
|     } | ||||
|  | ||||
|     public function validatedBy() | ||||
|     { | ||||
|         return \Chill\MainBundle\Validation\Validator\UserGroupDoNotExclude::class; | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,69 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * 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. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\MainBundle\Validation\Validator; | ||||
|  | ||||
| use Chill\MainBundle\Entity\UserGroup; | ||||
| use Chill\MainBundle\Templating\TranslatableStringHelperInterface; | ||||
| use Symfony\Component\Validator\Constraint; | ||||
| use Symfony\Component\Validator\ConstraintValidator; | ||||
| use Symfony\Component\Validator\Exception\UnexpectedTypeException; | ||||
| use Symfony\Component\Validator\Exception\UnexpectedValueException; | ||||
|  | ||||
| final class UserGroupDoNotExclude extends ConstraintValidator | ||||
| { | ||||
|     public function __construct(private readonly TranslatableStringHelperInterface $translatableStringHelper) {} | ||||
|  | ||||
|     public function validate($value, Constraint $constraint) | ||||
|     { | ||||
|         if (!$constraint instanceof \Chill\MainBundle\Validation\Constraint\UserGroupDoNotExclude) { | ||||
|             throw new UnexpectedTypeException($constraint, UserGroupDoNotExclude::class); | ||||
|         } | ||||
|  | ||||
|         if (null === $value) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         if (!is_iterable($value)) { | ||||
|             throw new UnexpectedValueException($value, 'iterable'); | ||||
|         } | ||||
|  | ||||
|         $groups = []; | ||||
|  | ||||
|         foreach ($value as $gr) { | ||||
|             if ($gr instanceof UserGroup) { | ||||
|                 $groups[$gr->getExcludeKey()][] = $gr; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         foreach ($groups as $excludeKey => $groupByKey) { | ||||
|             if ('' === $excludeKey) { | ||||
|                 continue; | ||||
|             } | ||||
|  | ||||
|             if (1 < count($groupByKey)) { | ||||
|                 $excludedGroups = implode( | ||||
|                     ', ', | ||||
|                     array_map( | ||||
|                         fn (UserGroup $group) => $this->translatableStringHelper->localize($group->getLabel()), | ||||
|                         $groupByKey | ||||
|                     ) | ||||
|                 ); | ||||
|  | ||||
|                 $this->context | ||||
|                     ->buildViolation($constraint->message) | ||||
|                     ->setCode($constraint->code) | ||||
|                     ->setParameters(['excluded_groups' => $excludedGroups]) | ||||
|                     ->addViolation(); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -12,6 +12,7 @@ declare(strict_types=1); | ||||
| namespace Chill\MainBundle\Workflow\Counter; | ||||
|  | ||||
| use Chill\MainBundle\Entity\User; | ||||
| use Chill\MainBundle\Entity\UserGroup; | ||||
| use Chill\MainBundle\Entity\Workflow\EntityWorkflow; | ||||
| use Chill\MainBundle\Repository\Workflow\EntityWorkflowStepRepository; | ||||
| use Chill\MainBundle\Templating\UI\NotificationCounterInterface; | ||||
| @@ -82,6 +83,18 @@ final readonly class WorkflowByUserCounter implements NotificationCounterInterfa | ||||
|         foreach ($step->getDestUser() as $user) { | ||||
|             $keys[] = self::generateCacheKeyWorkflowByUser($user); | ||||
|         } | ||||
|         foreach ($step->getDestUserGroups()->reduce( | ||||
|             function (array $accumulator, UserGroup $userGroup) { | ||||
|                 foreach ($userGroup->getUsers() as $user) { | ||||
|                     $accumulator[] = $user; | ||||
|                 } | ||||
|  | ||||
|                 return $accumulator; | ||||
|             }, | ||||
|             [] | ||||
|         ) as $user) { | ||||
|             $keys[] = self::generateCacheKeyWorkflowByUser($user); | ||||
|         } | ||||
|  | ||||
|         if ([] !== $keys) { | ||||
|             $this->cacheItemPool->deleteItems($keys); | ||||
|   | ||||
| @@ -87,6 +87,17 @@ class EntityWorkflowGuardTransition implements EventSubscriberInterface | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         if (!$user instanceof User) { | ||||
|             $event->addTransitionBlocker( | ||||
|                 new TransitionBlocker( | ||||
|                     'workflow.Only regular user can apply a transition', | ||||
|                     '04fb4f76-7c0e-11ef-afc3-877bad7b0fe7' | ||||
|                 ) | ||||
|             ); | ||||
|  | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         // for users | ||||
|         if (!in_array('only-dest', $systemTransitions, true)) { | ||||
|             $event->addTransitionBlocker( | ||||
| @@ -108,6 +119,13 @@ class EntityWorkflowGuardTransition implements EventSubscriberInterface | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             // we give a second chance, searching for the presence of the user within userGroups | ||||
|             foreach ($entityWorkflow->getCurrentStep()->getDestUserGroups() as $userGroup) { | ||||
|                 if ($userGroup->contains($user)) { | ||||
|                     return; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             $event->addTransitionBlocker(new TransitionBlocker( | ||||
|                 'workflow.You are not allowed to apply a transition on this workflow. Only those users are allowed: %users%', | ||||
|                 'f3eeb57c-7532-11ec-9495-e7942a2ac7bc', | ||||
|   | ||||
| @@ -13,6 +13,7 @@ namespace Chill\MainBundle\Workflow\EventSubscriber; | ||||
|  | ||||
| use Chill\MainBundle\Entity\Notification; | ||||
| use Chill\MainBundle\Entity\User; | ||||
| use Chill\MainBundle\Entity\UserGroup; | ||||
| use Chill\MainBundle\Entity\Workflow\EntityWorkflow; | ||||
| use Chill\MainBundle\Workflow\Helper\MetadataExtractor; | ||||
| use Doctrine\ORM\EntityManagerInterface; | ||||
| @@ -69,7 +70,18 @@ class NotificationOnTransition implements EventSubscriberInterface | ||||
|             // the dests for the current step | ||||
|             $entityWorkflow->getCurrentStep()->getDestUser()->toArray(), | ||||
|             // the cc users for the current step | ||||
|             $entityWorkflow->getCurrentStep()->getCcUser()->toArray() | ||||
|             $entityWorkflow->getCurrentStep()->getCcUser()->toArray(), | ||||
|             // the users within groups | ||||
|             $entityWorkflow->getCurrentStep()->getDestUserGroups()->reduce( | ||||
|                 function (array $accumulator, UserGroup $userGroup) { | ||||
|                     foreach ($userGroup->getUsers() as $user) { | ||||
|                         $accumulator[] = $user; | ||||
|                     } | ||||
|  | ||||
|                     return $accumulator; | ||||
|                 }, | ||||
|                 [] | ||||
|             ), | ||||
|         ) as $dest) { | ||||
|             $dests[spl_object_hash($dest)] = $dest; | ||||
|         } | ||||
|   | ||||
| @@ -1,59 +0,0 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * 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. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\MainBundle\Workflow\EventSubscriber; | ||||
|  | ||||
| use Chill\MainBundle\Entity\Workflow\EntityWorkflowStep; | ||||
| use Chill\MainBundle\Workflow\EntityWorkflowManager; | ||||
| use Chill\MainBundle\Workflow\Helper\MetadataExtractor; | ||||
| use Symfony\Component\Mailer\MailerInterface; | ||||
| use Symfony\Component\Mime\Email; | ||||
| use Symfony\Component\Workflow\Registry; | ||||
|  | ||||
| class SendAccessKeyEventSubscriber | ||||
| { | ||||
|     public function __construct( | ||||
|         private readonly \Twig\Environment $engine, | ||||
|         private readonly MetadataExtractor $metadataExtractor, | ||||
|         private readonly Registry $registry, | ||||
|         private readonly EntityWorkflowManager $entityWorkflowManager, | ||||
|         private readonly MailerInterface $mailer, | ||||
|     ) {} | ||||
|  | ||||
|     public function postPersist(EntityWorkflowStep $step): void | ||||
|     { | ||||
|         $entityWorkflow = $step->getEntityWorkflow(); | ||||
|  | ||||
|         $place = $this->metadataExtractor->buildArrayPresentationForPlace($entityWorkflow); | ||||
|         $workflow = $this->metadataExtractor->buildArrayPresentationForWorkflow( | ||||
|             $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName()) | ||||
|         ); | ||||
|         $handler = $this->entityWorkflowManager->getHandler($entityWorkflow); | ||||
|  | ||||
|         foreach ($step->getDestEmail() as $emailAddress) { | ||||
|             $context = [ | ||||
|                 'entity_workflow' => $entityWorkflow, | ||||
|                 'dest' => $emailAddress, | ||||
|                 'place' => $place, | ||||
|                 'workflow' => $workflow, | ||||
|                 'entityTitle' => $handler->getEntityTitle($entityWorkflow), | ||||
|             ]; | ||||
|  | ||||
|             $email = new Email(); | ||||
|             $email | ||||
|                 ->addTo($emailAddress) | ||||
|                 ->subject($this->engine->render('@ChillMain/Workflow/workflow_send_access_key_title.fr.txt.twig', $context)) | ||||
|                 ->text($this->engine->render('@ChillMain/Workflow/workflow_send_access_key.fr.txt.twig', $context)); | ||||
|  | ||||
|             $this->mailer->send($email); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -12,6 +12,7 @@ declare(strict_types=1); | ||||
| namespace Chill\MainBundle\Workflow; | ||||
|  | ||||
| use Chill\MainBundle\Entity\User; | ||||
| use Chill\MainBundle\Entity\UserGroup; | ||||
| use Chill\MainBundle\Entity\Workflow\EntityWorkflow; | ||||
| use Chill\PersonBundle\Entity\Person; | ||||
| use Symfony\Component\Validator\Constraints as Assert; | ||||
| @@ -24,34 +25,23 @@ use Symfony\Component\Workflow\Transition; | ||||
| class WorkflowTransitionContextDTO | ||||
| { | ||||
|     /** | ||||
|      * a list of future dest users for the next steps. | ||||
|      * a list of future dest users or user groups for the next step. | ||||
|      * | ||||
|      * This is in used in order to let controller inform who will be the future users which will validate | ||||
|      * the next step. This is necessary to perform some computation about the next users, before they are | ||||
|      * associated to the entity EntityWorkflowStep. | ||||
|      * | ||||
|      * @var array|User[] | ||||
|      * @var list<User|UserGroup> | ||||
|      */ | ||||
|     public array $futureDestUsers = []; | ||||
|  | ||||
|     /** | ||||
|      * a list of future cc users for the next steps. | ||||
|      * a list of future cc users for the next step. | ||||
|      * | ||||
|      * @var array|User[] | ||||
|      */ | ||||
|     public array $futureCcUsers = []; | ||||
|  | ||||
|     /** | ||||
|      * a list of future dest emails for the next steps. | ||||
|      * | ||||
|      * This is in used in order to let controller inform who will be the future emails which will validate | ||||
|      * the next step. This is necessary to perform some computation about the next emails, before they are | ||||
|      * associated to the entity EntityWorkflowStep. | ||||
|      * | ||||
|      * @var array|string[] | ||||
|      */ | ||||
|     public array $futureDestEmails = []; | ||||
|  | ||||
|     /** | ||||
|      * A list of future @see{Person} with will sign the next step. | ||||
|      * | ||||
| @@ -72,6 +62,22 @@ class WorkflowTransitionContextDTO | ||||
|         public EntityWorkflow $entityWorkflow, | ||||
|     ) {} | ||||
|  | ||||
|     /** | ||||
|      * @return list<User> | ||||
|      */ | ||||
|     public function getFutureDestUsers(): array | ||||
|     { | ||||
|         return array_values(array_filter($this->futureDestUsers, fn (User|UserGroup $user) => $user instanceof User)); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @return list<UserGroup> | ||||
|      */ | ||||
|     public function getFutureDestUserGroups(): array | ||||
|     { | ||||
|         return array_values(array_filter($this->futureDestUsers, fn (User|UserGroup $user) => $user instanceof UserGroup)); | ||||
|     } | ||||
|  | ||||
|     #[Assert\Callback()] | ||||
|     public function validateCCUserIsNotInDest(ExecutionContextInterface $context, $payload): void | ||||
|     { | ||||
|   | ||||
| @@ -5,8 +5,8 @@ info: | ||||
|     title: "Chill api" | ||||
|     description: "Api documentation for chill. Currently, work in progress" | ||||
| servers: | ||||
|     - url: "/api" | ||||
|       description: "Your current dev server" | ||||
|     -   url: "/api" | ||||
|         description: "Your current dev server" | ||||
|  | ||||
| components: | ||||
|     schemas: | ||||
| @@ -29,6 +29,42 @@ components: | ||||
|                     type: string | ||||
|                 text: | ||||
|                     type: string | ||||
|         UserById: | ||||
|             type: object | ||||
|             properties: | ||||
|                 id: | ||||
|                     type: integer | ||||
|                 type: | ||||
|                     type: string | ||||
|                     enum: | ||||
|                         - user | ||||
|         UserGroup: | ||||
|             type: object | ||||
|             properties: | ||||
|                 id: | ||||
|                     type: integer | ||||
|                 type: | ||||
|                     type: string | ||||
|                     enum: | ||||
|                         - user_group | ||||
|                 label: | ||||
|                     type: object | ||||
|                     additionalProperties: true | ||||
|                 backgroundColor: | ||||
|                     type: string | ||||
|                 foregroundColor: | ||||
|                     type: string | ||||
|                 exclusionKey: | ||||
|                     type: string | ||||
|         UserGroupById: | ||||
|             type: object | ||||
|             properties: | ||||
|                 id: | ||||
|                     type: integer | ||||
|                 type: | ||||
|                     type: string | ||||
|                     enum: | ||||
|                         - user_group | ||||
|         Center: | ||||
|             type: object | ||||
|             properties: | ||||
| @@ -181,25 +217,26 @@ paths: | ||||
|                 The results are ordered by relevance, from the most to the lowest relevant. | ||||
|  | ||||
|             parameters: | ||||
|                 - name: q | ||||
|                   in: query | ||||
|                   required: true | ||||
|                   description: the pattern to search | ||||
|                   schema: | ||||
|                       type: string | ||||
|                 - name: type[] | ||||
|                   in: query | ||||
|                   required: true | ||||
|                   description: the type entities amongst the search is performed | ||||
|                   schema: | ||||
|                       type: array | ||||
|                       items: | ||||
|                           type: string | ||||
|                           enum: | ||||
|                               - person | ||||
|                               - thirdparty | ||||
|                               - user | ||||
|                               - household | ||||
|                 -   name: q | ||||
|                     in: query | ||||
|                     required: true | ||||
|                     description: the pattern to search | ||||
|                     schema: | ||||
|                         type: string | ||||
|                 -   name: type[] | ||||
|                     in: query | ||||
|                     required: true | ||||
|                     description: the type entities amongst the search is performed | ||||
|                     schema: | ||||
|                         type: array | ||||
|                         items: | ||||
|                             type: string | ||||
|                             enum: | ||||
|                                 - person | ||||
|                                 - thirdparty | ||||
|                                 - user | ||||
|                                 - household | ||||
|                                 - user-group | ||||
|             responses: | ||||
|                 200: | ||||
|                     description: "OK" | ||||
| @@ -236,7 +273,7 @@ paths: | ||||
|                                     minItems: 2 | ||||
|                                     maxItems: 2 | ||||
|                                 postcode: | ||||
|                                     $ref: "#/components/schemas/PostalCode" | ||||
|                                     $ref: '#/components/schemas/PostalCode' | ||||
|                                 steps: | ||||
|                                     type: string | ||||
|                                 street: | ||||
| @@ -260,14 +297,14 @@ paths: | ||||
|                 - address | ||||
|             summary: Return an address by id | ||||
|             parameters: | ||||
|                 - name: id | ||||
|                   in: path | ||||
|                   required: true | ||||
|                   description: The address id | ||||
|                   schema: | ||||
|                       type: integer | ||||
|                       format: integer | ||||
|                       minimum: 1 | ||||
|                 -   name: id | ||||
|                     in: path | ||||
|                     required: true | ||||
|                     description: The address id | ||||
|                     schema: | ||||
|                         type: integer | ||||
|                         format: integer | ||||
|                         minimum: 1 | ||||
|             responses: | ||||
|                 200: | ||||
|                     description: "ok" | ||||
| @@ -284,14 +321,14 @@ paths: | ||||
|                 - address | ||||
|             summary: patch an address | ||||
|             parameters: | ||||
|                 - name: id | ||||
|                   in: path | ||||
|                   required: true | ||||
|                   description: The address id | ||||
|                   schema: | ||||
|                       type: integer | ||||
|                       format: integer | ||||
|                       minimum: 1 | ||||
|                 -   name: id | ||||
|                     in: path | ||||
|                     required: true | ||||
|                     description: The address id | ||||
|                     schema: | ||||
|                         type: integer | ||||
|                         format: integer | ||||
|                         minimum: 1 | ||||
|             requestBody: | ||||
|                 required: true | ||||
|                 content: | ||||
| @@ -349,14 +386,14 @@ paths: | ||||
|                 - address | ||||
|             summary: Duplicate an existing address | ||||
|             parameters: | ||||
|                 - name: id | ||||
|                   in: path | ||||
|                   required: true | ||||
|                   description: The address id that will be duplicated | ||||
|                   schema: | ||||
|                       type: integer | ||||
|                       format: integer | ||||
|                       minimum: 1 | ||||
|                 -   name: id | ||||
|                     in: path | ||||
|                     required: true | ||||
|                     description: The address id that will be duplicated | ||||
|                     schema: | ||||
|                         type: integer | ||||
|                         format: integer | ||||
|                         minimum: 1 | ||||
|             responses: | ||||
|                 200: | ||||
|                     description: "ok" | ||||
| @@ -375,12 +412,12 @@ paths: | ||||
|                 - address | ||||
|             summary: Return a list of all reference addresses | ||||
|             parameters: | ||||
|                 - in: query | ||||
|                   name: postal_code | ||||
|                   required: false | ||||
|                   schema: | ||||
|                       type: integer | ||||
|                   description: The id of a postal code to filter the reference addresses | ||||
|                 -   in: query | ||||
|                     name: postal_code | ||||
|                     required: false | ||||
|                     schema: | ||||
|                         type: integer | ||||
|                     description: The id of a postal code to filter the reference addresses | ||||
|             responses: | ||||
|                 200: | ||||
|                     description: "ok" | ||||
| @@ -390,14 +427,14 @@ paths: | ||||
|                 - address | ||||
|             summary: Return a reference address by id | ||||
|             parameters: | ||||
|                 - name: id | ||||
|                   in: path | ||||
|                   required: true | ||||
|                   description: The reference address id | ||||
|                   schema: | ||||
|                       type: integer | ||||
|                       format: integer | ||||
|                       minimum: 1 | ||||
|                 -   name: id | ||||
|                     in: path | ||||
|                     required: true | ||||
|                     description: The reference address id | ||||
|                     schema: | ||||
|                         type: integer | ||||
|                         format: integer | ||||
|                         minimum: 1 | ||||
|             responses: | ||||
|                 200: | ||||
|                     description: "ok" | ||||
| @@ -417,20 +454,20 @@ paths: | ||||
|                 - search | ||||
|             summary: Return a reference address by id | ||||
|             parameters: | ||||
|                 - name: id | ||||
|                   in: path | ||||
|                   required: true | ||||
|                   description: The reference address id | ||||
|                   schema: | ||||
|                       type: integer | ||||
|                       format: integer | ||||
|                       minimum: 1 | ||||
|                 - name: q | ||||
|                   in: query | ||||
|                   required: true | ||||
|                   description: The search pattern | ||||
|                   schema: | ||||
|                       type: string | ||||
|                 -   name: id | ||||
|                     in: path | ||||
|                     required: true | ||||
|                     description: The reference address id | ||||
|                     schema: | ||||
|                         type: integer | ||||
|                         format: integer | ||||
|                         minimum: 1 | ||||
|                 -   name: q | ||||
|                     in: query | ||||
|                     required: true | ||||
|                     description: The search pattern | ||||
|                     schema: | ||||
|                         type: string | ||||
|             responses: | ||||
|                 200: | ||||
|                     description: "ok" | ||||
| @@ -450,12 +487,12 @@ paths: | ||||
|                 - address | ||||
|             summary: Return a list of all postal-code | ||||
|             parameters: | ||||
|                 - in: query | ||||
|                   name: country | ||||
|                   required: false | ||||
|                   schema: | ||||
|                       type: integer | ||||
|                   description: The id of a country to filter the postal code | ||||
|                 -   in: query | ||||
|                     name: country | ||||
|                     required: false | ||||
|                     schema: | ||||
|                         type: integer | ||||
|                     description: The id of a country to filter the postal code | ||||
|             responses: | ||||
|                 200: | ||||
|                     description: "ok" | ||||
| @@ -494,14 +531,14 @@ paths: | ||||
|                 - address | ||||
|             summary: Return a postal code by id | ||||
|             parameters: | ||||
|                 - name: id | ||||
|                   in: path | ||||
|                   required: true | ||||
|                   description: The postal code id | ||||
|                   schema: | ||||
|                       type: integer | ||||
|                       format: integer | ||||
|                       minimum: 1 | ||||
|                 -   name: id | ||||
|                     in: path | ||||
|                     required: true | ||||
|                     description: The postal code id | ||||
|                     schema: | ||||
|                         type: integer | ||||
|                         format: integer | ||||
|                         minimum: 1 | ||||
|             responses: | ||||
|                 200: | ||||
|                     description: "ok" | ||||
| @@ -521,18 +558,18 @@ paths: | ||||
|                 - search | ||||
|             summary: Search a postal code | ||||
|             parameters: | ||||
|                 - name: q | ||||
|                   in: query | ||||
|                   required: true | ||||
|                   description: The search pattern | ||||
|                   schema: | ||||
|                       type: string | ||||
|                 - name: country | ||||
|                   in: query | ||||
|                   required: false | ||||
|                   description: The country id | ||||
|                   schema: | ||||
|                       type: integer | ||||
|                 -   name: q | ||||
|                     in: query | ||||
|                     required: true | ||||
|                     description: The search pattern | ||||
|                     schema: | ||||
|                         type: string | ||||
|                 -   name: country | ||||
|                     in: query | ||||
|                     required: false | ||||
|                     description: The country id | ||||
|                     schema: | ||||
|                         type: integer | ||||
|             responses: | ||||
|                 200: | ||||
|                     description: "ok" | ||||
| @@ -559,14 +596,14 @@ paths: | ||||
|                 - address | ||||
|             summary: Return a country by id | ||||
|             parameters: | ||||
|                 - name: id | ||||
|                   in: path | ||||
|                   required: true | ||||
|                   description: The country id | ||||
|                   schema: | ||||
|                       type: integer | ||||
|                       format: integer | ||||
|                       minimum: 1 | ||||
|                 -   name: id | ||||
|                     in: path | ||||
|                     required: true | ||||
|                     description: The country id | ||||
|                     schema: | ||||
|                         type: integer | ||||
|                         format: integer | ||||
|                         minimum: 1 | ||||
|             responses: | ||||
|                 200: | ||||
|                     description: "ok" | ||||
| @@ -609,14 +646,14 @@ paths: | ||||
|                 - user | ||||
|             summary: Return a user by id | ||||
|             parameters: | ||||
|                 - name: id | ||||
|                   in: path | ||||
|                   required: true | ||||
|                   description: The user id | ||||
|                   schema: | ||||
|                       type: integer | ||||
|                       format: integer | ||||
|                       minimum: 1 | ||||
|                 -   name: id | ||||
|                     in: path | ||||
|                     required: true | ||||
|                     description: The user id | ||||
|                     schema: | ||||
|                         type: integer | ||||
|                         format: integer | ||||
|                         minimum: 1 | ||||
|             responses: | ||||
|                 200: | ||||
|                     description: "ok" | ||||
| @@ -646,14 +683,14 @@ paths: | ||||
|                 - scope | ||||
|             summary: return a list of scopes | ||||
|             parameters: | ||||
|                 - name: id | ||||
|                   in: path | ||||
|                   required: true | ||||
|                   description: The scope id | ||||
|                   schema: | ||||
|                       type: integer | ||||
|                       format: integer | ||||
|                       minimum: 1 | ||||
|                 -   name: id | ||||
|                     in: path | ||||
|                     required: true | ||||
|                     description: The scope id | ||||
|                     schema: | ||||
|                         type: integer | ||||
|                         format: integer | ||||
|                         minimum: 1 | ||||
|             responses: | ||||
|                 200: | ||||
|                     description: "ok" | ||||
| @@ -721,14 +758,14 @@ paths: | ||||
|                 - location | ||||
|             summary: Return the given location | ||||
|             parameters: | ||||
|                 - name: id | ||||
|                   in: path | ||||
|                   required: true | ||||
|                   description: The location id | ||||
|                   schema: | ||||
|                       type: integer | ||||
|                       format: integer | ||||
|                       minimum: 1 | ||||
|                 -   name: id | ||||
|                     in: path | ||||
|                     required: true | ||||
|                     description: The location id | ||||
|                     schema: | ||||
|                         type: integer | ||||
|                         format: integer | ||||
|                         minimum: 1 | ||||
|             responses: | ||||
|                 200: | ||||
|                     description: "ok" | ||||
| @@ -788,14 +825,14 @@ paths: | ||||
|                 - notification | ||||
|             summary: mark a notification as read | ||||
|             parameters: | ||||
|                 - name: id | ||||
|                   in: path | ||||
|                   required: true | ||||
|                   description: The notification id | ||||
|                   schema: | ||||
|                       type: integer | ||||
|                       format: integer | ||||
|                       minimum: 1 | ||||
|                 -   name: id | ||||
|                     in: path | ||||
|                     required: true | ||||
|                     description: The notification id | ||||
|                     schema: | ||||
|                         type: integer | ||||
|                         format: integer | ||||
|                         minimum: 1 | ||||
|             responses: | ||||
|                 202: | ||||
|                     description: "accepted" | ||||
| @@ -807,14 +844,14 @@ paths: | ||||
|                 - notification | ||||
|             summary: mark a notification as unread | ||||
|             parameters: | ||||
|                 - name: id | ||||
|                   in: path | ||||
|                   required: true | ||||
|                   description: The notification id | ||||
|                   schema: | ||||
|                       type: integer | ||||
|                       format: integer | ||||
|                       minimum: 1 | ||||
|                 -   name: id | ||||
|                     in: path | ||||
|                     required: true | ||||
|                     description: The notification id | ||||
|                     schema: | ||||
|                         type: integer | ||||
|                         format: integer | ||||
|                         minimum: 1 | ||||
|             responses: | ||||
|                 202: | ||||
|                     description: "accepted" | ||||
| @@ -846,7 +883,7 @@ paths: | ||||
|                                     type: array | ||||
|                                     items: | ||||
|                                         type: integer | ||||
|                                         example: [1, 2, 3] # Example array of IDs | ||||
|                                         example: [ 1, 2, 3 ] # Example array of IDs | ||||
|             responses: | ||||
|                 "202": | ||||
|                     description: Notifications marked as unread successfully | ||||
| @@ -934,6 +971,22 @@ paths: | ||||
|                             schema: | ||||
|                                 type: array | ||||
|                                 items: | ||||
|                                     $ref: "#/components/schemas/NewsItem" | ||||
|                                     $ref: '#/components/schemas/NewsItem' | ||||
|                 403: | ||||
|                     description: "Unauthorized" | ||||
|     /1.0/main/user-group.json: | ||||
|         get: | ||||
|             tags: | ||||
|                 - user-group | ||||
|             summary: Return a list of users-groups | ||||
|             responses: | ||||
|                 200: | ||||
|                     description: "ok" | ||||
|                     content: | ||||
|                         application/json: | ||||
|                             schema: | ||||
|                                 type: array | ||||
|                                 items: | ||||
|                                     $ref: '#/components/schemas/UserGroup' | ||||
|                 403: | ||||
|                     description: "Unauthorized" | ||||
|   | ||||
| @@ -42,17 +42,6 @@ services: | ||||
|         arguments: | ||||
|             $handlers: !tagged_iterator chill_main.workflow_handler | ||||
|  | ||||
|     Chill\MainBundle\Workflow\EventSubscriber\SendAccessKeyEventSubscriber: | ||||
|         autoconfigure: true | ||||
|         autowire: true | ||||
|         tags: | ||||
|             - | ||||
|                 name: 'doctrine.orm.entity_listener' | ||||
|                 event: 'postPersist' | ||||
|                 entity: 'Chill\MainBundle\Entity\Workflow\EntityWorkflowStep' | ||||
|                 # set the 'lazy' option to TRUE to only instantiate listeners when they are used | ||||
|                 lazy: true | ||||
|  | ||||
|     # other stuffes | ||||
|  | ||||
|     chill.main.helper.translatable_string: | ||||
|   | ||||
| @@ -56,6 +56,10 @@ services: | ||||
|  | ||||
|     Chill\MainBundle\Templating\Entity\UserRender: ~ | ||||
|  | ||||
|     Chill\MainBundle\Templating\Entity\UserGroupRender: ~ | ||||
|     Chill\MainBundle\Templating\Entity\UserGroupRenderInterface: | ||||
|         alias: Chill\MainBundle\Templating\Entity\UserGroupRender | ||||
|  | ||||
|     Chill\MainBundle\Templating\Listing\: | ||||
|         resource: './../../Templating/Listing' | ||||
|  | ||||
|   | ||||
| @@ -3,6 +3,9 @@ services: | ||||
|         autowire: true | ||||
|         autoconfigure: true | ||||
|  | ||||
|     Chill\MainBundle\Validation\: | ||||
|         resource: '../../Validation' | ||||
|  | ||||
|     chill_main.validator_user_circle_consistency: | ||||
|         class: Chill\MainBundle\Validator\Constraints\Entity\UserCircleConsistencyValidator | ||||
|         arguments: | ||||
|   | ||||
| @@ -0,0 +1,41 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * 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. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\Migrations\Main; | ||||
|  | ||||
| use Doctrine\DBAL\Schema\Schema; | ||||
| use Doctrine\Migrations\AbstractMigration; | ||||
|  | ||||
| final class Version20240416145021 extends AbstractMigration | ||||
| { | ||||
|     public function getDescription(): string | ||||
|     { | ||||
|         return 'Create tables for user_group'; | ||||
|     } | ||||
|  | ||||
|     public function up(Schema $schema): void | ||||
|     { | ||||
|         $this->addSql('CREATE SEQUENCE chill_main_user_group_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); | ||||
|         $this->addSql('CREATE TABLE chill_main_user_group (id INT NOT NULL, label JSON DEFAULT \'[]\' NOT NULL, PRIMARY KEY(id))'); | ||||
|         $this->addSql('CREATE TABLE chill_main_user_group_user (usergroup_id INT NOT NULL, user_id INT NOT NULL, PRIMARY KEY(usergroup_id, user_id))'); | ||||
|         $this->addSql('CREATE INDEX IDX_1E07F044D2112630 ON chill_main_user_group_user (usergroup_id)'); | ||||
|         $this->addSql('CREATE INDEX IDX_1E07F044A76ED395 ON chill_main_user_group_user (user_id)'); | ||||
|         $this->addSql('ALTER TABLE chill_main_user_group_user ADD CONSTRAINT FK_1E07F044D2112630 FOREIGN KEY (usergroup_id) REFERENCES chill_main_user_group (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); | ||||
|         $this->addSql('ALTER TABLE chill_main_user_group_user ADD CONSTRAINT FK_1E07F044A76ED395 FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); | ||||
|     } | ||||
|  | ||||
|     public function down(Schema $schema): void | ||||
|     { | ||||
|         $this->addSql('DROP SEQUENCE chill_main_user_group_id_seq'); | ||||
|         $this->addSql('DROP TABLE chill_main_user_group_user'); | ||||
|         $this->addSql('DROP TABLE chill_main_user_group'); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,41 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * 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. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\Migrations\Main; | ||||
|  | ||||
| use Doctrine\DBAL\Schema\Schema; | ||||
| use Doctrine\Migrations\AbstractMigration; | ||||
|  | ||||
| final class Version20240422091752 extends AbstractMigration | ||||
| { | ||||
|     public function getDescription(): string | ||||
|     { | ||||
|         return 'Add colors and exclude string to user groups'; | ||||
|     } | ||||
|  | ||||
|     public function up(Schema $schema): void | ||||
|     { | ||||
|         $this->addSql('ALTER TABLE chill_main_user_group ADD backgroundColor TEXT DEFAULT \'#ffffffff\' NOT NULL'); | ||||
|         $this->addSql('ALTER TABLE chill_main_user_group ADD foregroundColor TEXT DEFAULT \'#000000ff\' NOT NULL'); | ||||
|         $this->addSql('ALTER TABLE chill_main_user_group ADD excludeKey TEXT DEFAULT \'\' NOT NULL'); | ||||
|         $this->addSql('ALTER INDEX idx_1e07f044d2112630 RENAME TO IDX_738BC82BD2112630'); | ||||
|         $this->addSql('ALTER INDEX idx_1e07f044a76ed395 RENAME TO IDX_738BC82BA76ED395'); | ||||
|     } | ||||
|  | ||||
|     public function down(Schema $schema): void | ||||
|     { | ||||
|         $this->addSql('ALTER TABLE chill_main_user_group DROP backgroundColor'); | ||||
|         $this->addSql('ALTER TABLE chill_main_user_group DROP foregroundColor'); | ||||
|         $this->addSql('ALTER TABLE chill_main_user_group DROP excludeKey'); | ||||
|         $this->addSql('ALTER INDEX idx_738bc82bd2112630 RENAME TO idx_1e07f044d2112630'); | ||||
|         $this->addSql('ALTER INDEX idx_738bc82ba76ed395 RENAME TO idx_1e07f044a76ed395'); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,39 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * 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. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\Migrations\Main; | ||||
|  | ||||
| use Doctrine\DBAL\Schema\Schema; | ||||
| use Doctrine\Migrations\AbstractMigration; | ||||
|  | ||||
| final class Version20240926132856 extends AbstractMigration | ||||
| { | ||||
|     public function getDescription(): string | ||||
|     { | ||||
|         return 'Add a relation between entityworkflow step and user groups'; | ||||
|     } | ||||
|  | ||||
|     public function up(Schema $schema): void | ||||
|     { | ||||
|         $this->addSql('CREATE TABLE chill_main_workflow_entity_step_user_group (entityworkflowstep_id INT NOT NULL, usergroup_id INT NOT NULL, PRIMARY KEY(entityworkflowstep_id, usergroup_id))'); | ||||
|         $this->addSql('CREATE INDEX IDX_AB433F907E6AF9D4 ON chill_main_workflow_entity_step_user_group (entityworkflowstep_id)'); | ||||
|         $this->addSql('CREATE INDEX IDX_AB433F90D2112630 ON chill_main_workflow_entity_step_user_group (usergroup_id)'); | ||||
|         $this->addSql('ALTER TABLE chill_main_workflow_entity_step_user_group ADD CONSTRAINT FK_AB433F907E6AF9D4 FOREIGN KEY (entityworkflowstep_id) REFERENCES chill_main_workflow_entity_step (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); | ||||
|         $this->addSql('ALTER TABLE chill_main_workflow_entity_step_user_group ADD CONSTRAINT FK_AB433F90D2112630 FOREIGN KEY (usergroup_id) REFERENCES chill_main_user_group (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); | ||||
|     } | ||||
|  | ||||
|     public function down(Schema $schema): void | ||||
|     { | ||||
|         $this->addSql('ALTER TABLE chill_main_workflow_entity_step_user_group DROP CONSTRAINT FK_AB433F907E6AF9D4'); | ||||
|         $this->addSql('ALTER TABLE chill_main_workflow_entity_step_user_group DROP CONSTRAINT FK_AB433F90D2112630'); | ||||
|         $this->addSql('DROP TABLE chill_main_workflow_entity_step_user_group'); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,41 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * 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. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\Migrations\Main; | ||||
|  | ||||
| use Doctrine\DBAL\Schema\Schema; | ||||
| use Doctrine\Migrations\AbstractMigration; | ||||
|  | ||||
| final class Version20240927095751 extends AbstractMigration | ||||
| { | ||||
|     public function getDescription(): string | ||||
|     { | ||||
|         return 'Add admin users and active on user groups'; | ||||
|     } | ||||
|  | ||||
|     public function up(Schema $schema): void | ||||
|     { | ||||
|         $this->addSql('CREATE TABLE chill_main_user_group_user_admin (usergroup_id INT NOT NULL, user_id INT NOT NULL, PRIMARY KEY(usergroup_id, user_id))'); | ||||
|         $this->addSql('CREATE INDEX IDX_DAD75036D2112630 ON chill_main_user_group_user_admin (usergroup_id)'); | ||||
|         $this->addSql('CREATE INDEX IDX_DAD75036A76ED395 ON chill_main_user_group_user_admin (user_id)'); | ||||
|         $this->addSql('ALTER TABLE chill_main_user_group_user_admin ADD CONSTRAINT FK_DAD75036D2112630 FOREIGN KEY (usergroup_id) REFERENCES chill_main_user_group (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); | ||||
|         $this->addSql('ALTER TABLE chill_main_user_group_user_admin ADD CONSTRAINT FK_DAD75036A76ED395 FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); | ||||
|         $this->addSql('ALTER TABLE chill_main_user_group ADD active BOOLEAN DEFAULT true NOT NULL'); | ||||
|     } | ||||
|  | ||||
|     public function down(Schema $schema): void | ||||
|     { | ||||
|         $this->addSql('ALTER TABLE chill_main_user_group_user_admin DROP CONSTRAINT FK_DAD75036D2112630'); | ||||
|         $this->addSql('ALTER TABLE chill_main_user_group_user_admin DROP CONSTRAINT FK_DAD75036A76ED395'); | ||||
|         $this->addSql('DROP TABLE chill_main_user_group_user_admin'); | ||||
|         $this->addSql('ALTER TABLE chill_main_user_group DROP active'); | ||||
|     } | ||||
| } | ||||
| @@ -5,6 +5,17 @@ years_old: >- | ||||
|         other {# ans} | ||||
|     } | ||||
|  | ||||
| user_group: | ||||
|     with_count_users: >- | ||||
|         {count, plural, | ||||
|             =0 {Aucun membre} | ||||
|             one {1 utilisateur} | ||||
|             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: >- | ||||
|         {nb, plural, | ||||
| @@ -47,9 +58,15 @@ workflow: | ||||
|         } | ||||
|     signature: | ||||
|         signed_statement: 'Signature appliquée le {datetime, date, short} à {datetime, time, short}' | ||||
|         rejected_statement: 'Signature rejectée le {datetime, date, short} à {datetime, time, short}' | ||||
|         rejected_statement: 'Signature rejetée le {datetime, date, short} à {datetime, time, short}' | ||||
|         canceled_statement: 'Signature annulée le {datetime, date, short} à {datetime, time, short}' | ||||
|  | ||||
|     On hold by: En attente par {by} | ||||
|     signature_required_title: >- | ||||
|         {nb_signatures, plural, | ||||
|             =0 {Aucune signature demandée} | ||||
|             one {Signature demandée} | ||||
|             other {Signatures demandées} | ||||
|         } | ||||
|  | ||||
| duration: | ||||
|     minute: >- | ||||
|   | ||||
| @@ -51,6 +51,25 @@ user: | ||||
|         no job: Pas de métier assigné | ||||
|         no scope: Pas de cercle assigné | ||||
|  | ||||
| user_group: | ||||
|     inactive: Inactif | ||||
|     with_users: Membres | ||||
|     no_users: Aucun utilisateur associé | ||||
|     no_admin_users: Aucun administrateur | ||||
|     Label: Nom du groupe | ||||
|     BackgroundColor: Couleur de fond du badge | ||||
|     ForegroundColor: Couleur de la police du badge | ||||
|     ExcludeKey: Clé d'exclusion | ||||
|     ExcludeKeyHelp: Lorsque cela est pertinent, les groupes comportant la même clé d'exclusion s'excluent mutuellement. | ||||
|     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 | ||||
|  | ||||
| Edit: Modifier | ||||
| @@ -395,6 +414,12 @@ crud: | ||||
|             add_new: Créer | ||||
|         title_new: Nouveau métier | ||||
|         title_edit: Modifier un métier | ||||
|     admin_user_group: | ||||
|         index: | ||||
|             title: Groupes d'utilisateurs | ||||
|             add_new: Créer | ||||
|         title_edit: Modifier un groupe d'utilisateur | ||||
|         title_new: Nouveau groupe utilisateur | ||||
|     main_location_type: | ||||
|         index: | ||||
|             title: Liste des types de localisations | ||||
|   | ||||
| @@ -3,8 +3,10 @@ | ||||
| */ | ||||
|  | ||||
| span.badge-user, | ||||
| span.badge-user-group, | ||||
| span.badge-person, | ||||
| span.badge-thirdparty { | ||||
|     margin: 0.2rem 0.1rem; | ||||
|     display: inline-block; | ||||
|     padding: 0 0.5em !important; | ||||
|     background-color: $white; | ||||
| @@ -18,6 +20,11 @@ span.badge-thirdparty { | ||||
|     } | ||||
| } | ||||
|  | ||||
| span.badge-user-group { | ||||
|     font-weight: 600; | ||||
|     border-width: 0px; | ||||
| } | ||||
|  | ||||
| span.badge-user { | ||||
|     border-bottom-width: 1px; | ||||
|     &.system { | ||||
| @@ -231,6 +238,10 @@ div[class*='budget-'] { | ||||
|         background-color: $chill-ll-gray; | ||||
|         color: $chill-blue; | ||||
|     } | ||||
|     &.bg-user-group { | ||||
|         background-color: $chill-l-gray; | ||||
|         color: $chill-blue; | ||||
|     } | ||||
|     &.bg-confidential { | ||||
|         background-color: $chill-ll-gray; | ||||
|         color: $chill-red; | ||||
|   | ||||
| @@ -27,6 +27,11 @@ | ||||
|             v-bind:item="item"> | ||||
|          </suggestion-user> | ||||
|  | ||||
|         <suggestion-user-group | ||||
|             v-if="item.result.type === 'user_group'" | ||||
|             v-bind:item="item"> | ||||
|         ></suggestion-user-group> | ||||
|  | ||||
|          <suggestion-household | ||||
|                v-if="item.result.type === 'household'" | ||||
|                v-bind:item="item"> | ||||
| @@ -41,6 +46,7 @@ import SuggestionPerson from './TypePerson'; | ||||
| import SuggestionThirdParty from './TypeThirdParty'; | ||||
| import SuggestionUser from './TypeUser'; | ||||
| import SuggestionHousehold from './TypeHousehold'; | ||||
| import SuggestionUserGroup from './TypeUserGroup'; | ||||
|  | ||||
| export default { | ||||
|    name: 'PersonSuggestion', | ||||
| @@ -49,6 +55,7 @@ export default { | ||||
|       SuggestionThirdParty, | ||||
|       SuggestionUser, | ||||
|       SuggestionHousehold, | ||||
|       SuggestionUserGroup, | ||||
|    }, | ||||
|    props: [ | ||||
|       'item', | ||||
|   | ||||
| @@ -0,0 +1,30 @@ | ||||
| <script setup lang="ts"> | ||||
|  | ||||
| import {ResultItem, UserGroup} from "../../../../../../ChillMainBundle/Resources/public/types"; | ||||
| import BadgeEntity from "ChillMainAssets/vuejs/_components/BadgeEntity.vue"; | ||||
| import UserRenderBoxBadge from "ChillMainAssets/vuejs/_components/Entity/UserRenderBoxBadge.vue"; | ||||
| import UserGroupRenderBox from "ChillMainAssets/vuejs/_components/Entity/UserGroupRenderBox.vue"; | ||||
|  | ||||
| interface TypeUserGroupProps { | ||||
|   item: ResultItem<UserGroup>; | ||||
| } | ||||
|  | ||||
| const props = defineProps<TypeUserGroupProps>(); | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <div class="container user-group-container"> | ||||
|     <div class="user-group-identification"> | ||||
|       <user-group-render-box :user-group="props.item.result"></user-group-render-box> | ||||
|     </div> | ||||
|   </div> | ||||
|   <div class="right_actions"> | ||||
|     <span class="badge rounded-pill bg-user-group"> | ||||
|         Groupe d'utilisateur | ||||
|     </span> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <style scoped lang="scss"> | ||||
|  | ||||
| </style> | ||||
		Reference in New Issue
	
	Block a user