mirror of
				https://gitlab.com/Chill-Projet/chill-bundles.git
				synced 2025-10-31 01:08:26 +00:00 
			
		
		
		
	Compare commits
	
		
			46 Commits
		
	
	
		
			manage-tra
			...
			testing
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 801f799e45 | |||
| 98b8f3dcff | |||
| a333a0312a | |||
| 0288fd22cf | |||
| 1dd4398c43 | |||
| 6065680e1e | |||
| 88114e3ba6 | |||
| bf93c1ddb2 | |||
| 0d365e16e5 | |||
| 802ff20b5c | |||
| cdfe201574 | |||
| 43419f9f15 | |||
| 39896ea6e2 | |||
| ca62c3fd0b | |||
| b3b84c5dc0 | |||
| 6bdb3e9695 | |||
| 20e64e8768 | |||
| 4f4b3dbb44 | |||
| 1c3e6e0dba | |||
| e7ca81e057 | |||
| 63f9bd5548 | |||
| c8146ded17 | |||
| 17d2b795b4 | |||
| 7f30742fc3 | |||
| 56d9072abe | |||
| 7ccff61c25 | |||
| c04fd66163 | |||
| 145c1df313 | |||
| 7f9738975c | |||
| 3e63b4abf3 | |||
| 1485d1ce7a | |||
| 9687debb57 | |||
| 769504c497 | |||
| 811364e139 | |||
| 0e5f1b4ab9 | |||
| f7c11d3567 | |||
| 51544cfc48 | |||
| 659dff3d2c | |||
| deffc5e4db | |||
| 40ecaab5b4 | |||
| f7be53f790 | |||
| 6fb01b19ec | |||
| b8ecff4f08 | |||
| 4c340dd086 | |||
| 8f1955c536 | |||
| c9c15cdd56 | 
							
								
								
									
										5
									
								
								.changes/unreleased/Feature-20230707-123609.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								.changes/unreleased/Feature-20230707-123609.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| kind: Feature | ||||
| body: '[export] Add a list for people with their associated course' | ||||
| time: 2023-07-07T12:36:09.596469063+02:00 | ||||
| custom: | ||||
|   Issue: "125" | ||||
							
								
								
									
										6
									
								
								.changes/unreleased/Feature-20230707-124132.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.changes/unreleased/Feature-20230707-124132.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| kind: Feature | ||||
| body: '[export] Add ordering by person''s lastname or course opening date in list | ||||
|   which concerns accompanying course or peoples' | ||||
| time: 2023-07-07T12:41:32.112725962+02:00 | ||||
| custom: | ||||
|   Issue: "" | ||||
							
								
								
									
										5
									
								
								.changes/unreleased/Feature-20230711-150055.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								.changes/unreleased/Feature-20230711-150055.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| kind: Feature | ||||
| body: '[Export] allow to group activities by localisation' | ||||
| time: 2023-07-11T15:00:55.770070399+02:00 | ||||
| custom: | ||||
|   Issue: "128" | ||||
| @@ -30,6 +30,8 @@ kinds: | ||||
|         auto: patch | ||||
|     -   label: DX | ||||
|         auto: patch | ||||
|     -   label: UX | ||||
|         auto: patch | ||||
| newlines: | ||||
|     afterChangelogHeader: 1 | ||||
|     beforeChangelogVersion: 1 | ||||
|   | ||||
| @@ -18,11 +18,17 @@ use Chill\ActivityBundle\Repository\ActivityACLAwareRepositoryInterface; | ||||
| use Chill\ActivityBundle\Repository\ActivityRepository; | ||||
| use Chill\ActivityBundle\Repository\ActivityTypeCategoryRepository; | ||||
| use Chill\ActivityBundle\Repository\ActivityTypeRepositoryInterface; | ||||
| use Chill\ActivityBundle\Repository\ActivityUserJobRepository; | ||||
| use Chill\ActivityBundle\Security\Authorization\ActivityVoter; | ||||
| use Chill\MainBundle\Entity\Embeddable\CommentEmbeddable; | ||||
| use Chill\MainBundle\Entity\UserJob; | ||||
| use Chill\MainBundle\Pagination\PaginatorFactory; | ||||
| use Chill\MainBundle\Repository\LocationRepository; | ||||
| use Chill\MainBundle\Repository\UserRepositoryInterface; | ||||
| use Chill\MainBundle\Security\Resolver\CenterResolverManagerInterface; | ||||
| use Chill\MainBundle\Templating\Listing\FilterOrderHelper; | ||||
| use Chill\MainBundle\Templating\Listing\FilterOrderHelperFactoryInterface; | ||||
| use Chill\MainBundle\Templating\TranslatableStringHelperInterface; | ||||
| use Chill\PersonBundle\Entity\AccompanyingPeriod; | ||||
| use Chill\PersonBundle\Entity\Person; | ||||
| use Chill\PersonBundle\Privacy\PrivacyEvent; | ||||
| @@ -47,68 +53,26 @@ use function array_key_exists; | ||||
|  | ||||
| final class ActivityController extends AbstractController | ||||
| { | ||||
|     private AccompanyingPeriodRepository $accompanyingPeriodRepository; | ||||
|  | ||||
|     private ActivityACLAwareRepositoryInterface $activityACLAwareRepository; | ||||
|  | ||||
|     private ActivityRepository $activityRepository; | ||||
|  | ||||
|     private ActivityTypeCategoryRepository $activityTypeCategoryRepository; | ||||
|  | ||||
|     private ActivityTypeRepositoryInterface $activityTypeRepository; | ||||
|  | ||||
|     private CenterResolverManagerInterface $centerResolver; | ||||
|  | ||||
|     private EntityManagerInterface $entityManager; | ||||
|  | ||||
|     private EventDispatcherInterface $eventDispatcher; | ||||
|  | ||||
|     private LocationRepository $locationRepository; | ||||
|  | ||||
|     private LoggerInterface $logger; | ||||
|  | ||||
|     private PersonRepository $personRepository; | ||||
|  | ||||
|     private SerializerInterface $serializer; | ||||
|  | ||||
|     private ThirdPartyRepository $thirdPartyRepository; | ||||
|  | ||||
|     private TranslatorInterface $translator; | ||||
|  | ||||
|     private UserRepositoryInterface $userRepository; | ||||
|  | ||||
|     public function __construct( | ||||
|         ActivityACLAwareRepositoryInterface $activityACLAwareRepository, | ||||
|         ActivityTypeRepositoryInterface $activityTypeRepository, | ||||
|         ActivityTypeCategoryRepository $activityTypeCategoryRepository, | ||||
|         PersonRepository $personRepository, | ||||
|         ThirdPartyRepository $thirdPartyRepository, | ||||
|         LocationRepository $locationRepository, | ||||
|         ActivityRepository $activityRepository, | ||||
|         AccompanyingPeriodRepository $accompanyingPeriodRepository, | ||||
|         EntityManagerInterface $entityManager, | ||||
|         EventDispatcherInterface $eventDispatcher, | ||||
|         LoggerInterface $logger, | ||||
|         SerializerInterface $serializer, | ||||
|         UserRepositoryInterface $userRepository, | ||||
|         CenterResolverManagerInterface $centerResolver, | ||||
|         TranslatorInterface $translator | ||||
|         private readonly ActivityACLAwareRepositoryInterface $activityACLAwareRepository, | ||||
|         private readonly ActivityTypeRepositoryInterface $activityTypeRepository, | ||||
|         private readonly ActivityTypeCategoryRepository $activityTypeCategoryRepository, | ||||
|         private readonly PersonRepository $personRepository, | ||||
|         private readonly ThirdPartyRepository $thirdPartyRepository, | ||||
|         private readonly LocationRepository $locationRepository, | ||||
|         private readonly ActivityRepository $activityRepository, | ||||
|         private readonly AccompanyingPeriodRepository $accompanyingPeriodRepository, | ||||
|         private readonly EntityManagerInterface $entityManager, | ||||
|         private readonly EventDispatcherInterface $eventDispatcher, | ||||
|         private readonly LoggerInterface $logger, | ||||
|         private readonly SerializerInterface $serializer, | ||||
|         private readonly UserRepositoryInterface $userRepository, | ||||
|         private readonly CenterResolverManagerInterface $centerResolver, | ||||
|         private readonly TranslatorInterface $translator, | ||||
|         private readonly FilterOrderHelperFactoryInterface $filterOrderHelperFactory, | ||||
|         private readonly TranslatableStringHelperInterface $translatableStringHelper, | ||||
|         private readonly PaginatorFactory $paginatorFactory, | ||||
|     ) { | ||||
|         $this->activityACLAwareRepository = $activityACLAwareRepository; | ||||
|         $this->activityTypeRepository = $activityTypeRepository; | ||||
|         $this->activityTypeCategoryRepository = $activityTypeCategoryRepository; | ||||
|         $this->personRepository = $personRepository; | ||||
|         $this->thirdPartyRepository = $thirdPartyRepository; | ||||
|         $this->locationRepository = $locationRepository; | ||||
|         $this->activityRepository = $activityRepository; | ||||
|         $this->accompanyingPeriodRepository = $accompanyingPeriodRepository; | ||||
|         $this->entityManager = $entityManager; | ||||
|         $this->eventDispatcher = $eventDispatcher; | ||||
|         $this->logger = $logger; | ||||
|         $this->serializer = $serializer; | ||||
|         $this->userRepository = $userRepository; | ||||
|         $this->centerResolver = $centerResolver; | ||||
|         $this->translator = $translator; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -289,14 +253,31 @@ final class ActivityController extends AbstractController | ||||
|     { | ||||
|         $view = null; | ||||
|         $activities = []; | ||||
|         // TODO: add pagination | ||||
|  | ||||
|         [$person, $accompanyingPeriod] = $this->getEntity($request); | ||||
|         $filter = $this->buildFilterOrder($person ?? $accompanyingPeriod); | ||||
|  | ||||
|         $filterArgs = [ | ||||
|             'my_activities' => $filter->getSingleCheckboxData('my_activities'), | ||||
|             'types' => $filter->hasEntityChoice('activity_types') ? $filter->getEntityChoiceData('activity_types') : [], | ||||
|             'jobs' => $filter->hasEntityChoice('jobs') ? $filter->getEntityChoiceData('jobs') : [], | ||||
|             'before' => $filter->getDateRangeData('activity_date')['to'], | ||||
|             'after' => $filter->getDateRangeData('activity_date')['from'], | ||||
|         ]; | ||||
|  | ||||
|         if ($person instanceof Person) { | ||||
|             $this->denyAccessUnlessGranted(ActivityVoter::SEE, $person); | ||||
|             $count = $this->activityACLAwareRepository->countByPerson($person, ActivityVoter::SEE, $filterArgs); | ||||
|             $paginator = $this->paginatorFactory->create($count); | ||||
|             $activities = $this->activityACLAwareRepository | ||||
|                 ->findByPerson($person, ActivityVoter::SEE, 0, null, ['date' => 'DESC', 'id' => 'DESC']); | ||||
|                 ->findByPerson( | ||||
|                     $person, | ||||
|                     ActivityVoter::SEE, | ||||
|                     $paginator->getCurrentPageFirstItemNumber(), | ||||
|                     $paginator->getItemsPerPage(), | ||||
|                     ['date' => 'DESC', 'id' => 'DESC'], | ||||
|                     $filterArgs | ||||
|                 ); | ||||
|  | ||||
|             $event = new PrivacyEvent($person, [ | ||||
|                 'element_class' => Activity::class, | ||||
| @@ -308,10 +289,21 @@ final class ActivityController extends AbstractController | ||||
|         } elseif ($accompanyingPeriod instanceof AccompanyingPeriod) { | ||||
|             $this->denyAccessUnlessGranted(ActivityVoter::SEE, $accompanyingPeriod); | ||||
|  | ||||
|             $count = $this->activityACLAwareRepository->countByAccompanyingPeriod($accompanyingPeriod, ActivityVoter::SEE, $filterArgs); | ||||
|             $paginator = $this->paginatorFactory->create($count); | ||||
|             $activities = $this->activityACLAwareRepository | ||||
|                 ->findByAccompanyingPeriod($accompanyingPeriod, ActivityVoter::SEE, 0, null, ['date' => 'DESC', 'id' => 'DESC']); | ||||
|                 ->findByAccompanyingPeriod( | ||||
|                     $accompanyingPeriod, | ||||
|                     ActivityVoter::SEE, | ||||
|                     $paginator->getCurrentPageFirstItemNumber(), | ||||
|                     $paginator->getItemsPerPage(), | ||||
|                     ['date' => 'DESC', 'id' => 'DESC'], | ||||
|                     $filterArgs | ||||
|                 ); | ||||
|  | ||||
|             $view = 'ChillActivityBundle:Activity:listAccompanyingCourse.html.twig'; | ||||
|         } else { | ||||
|             throw new \LogicException("Unsupported"); | ||||
|         } | ||||
|  | ||||
|         return $this->render( | ||||
| @@ -320,10 +312,47 @@ final class ActivityController extends AbstractController | ||||
|                 'activities' => $activities, | ||||
|                 'person' => $person, | ||||
|                 'accompanyingCourse' => $accompanyingPeriod, | ||||
|                 'filter' => $filter, | ||||
|                 'paginator' => $paginator, | ||||
|             ] | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     private function buildFilterOrder(AccompanyingPeriod|Person $associated): FilterOrderHelper | ||||
|     { | ||||
|  | ||||
|         $filterBuilder = $this->filterOrderHelperFactory->create(self::class); | ||||
|         $types = $this->activityACLAwareRepository->findActivityTypeByAssociated($associated); | ||||
|         $jobs = $this->activityACLAwareRepository->findUserJobByAssociated($associated); | ||||
|  | ||||
|         $filterBuilder | ||||
|             ->addDateRange('activity_date', 'activity.date') | ||||
|             ->addSingleCheckbox('my_activities', 'activity_filter.My activities'); | ||||
|  | ||||
|         if (1 < count($types)) { | ||||
|             $filterBuilder | ||||
|                 ->addEntityChoice('activity_types', 'activity_filter.Types', \Chill\ActivityBundle\Entity\ActivityType::class, $types, [ | ||||
|                     'choice_label' => function (\Chill\ActivityBundle\Entity\ActivityType $activityType) { | ||||
|                         $text = match ($activityType->hasCategory()) { | ||||
|                             true => $this->translatableStringHelper->localize($activityType->getCategory()->getName()) . ' > ', | ||||
|                             false => '', | ||||
|                         }; | ||||
|  | ||||
|                         return $text . $this->translatableStringHelper->localize($activityType->getName()); | ||||
|                     } | ||||
|                 ]); | ||||
|         } | ||||
|  | ||||
|         if (1 < count($jobs)) { | ||||
|             $filterBuilder | ||||
|                 ->addEntityChoice('jobs', 'activity_filter.Jobs', UserJob::class, $jobs, [ | ||||
|                     'choice_label' => fn (UserJob $u) => $this->translatableStringHelper->localize($u->getLabel()) | ||||
|                 ]); | ||||
|         } | ||||
|  | ||||
|         return $filterBuilder->build(); | ||||
|     } | ||||
|  | ||||
|     public function newAction(Request $request): Response | ||||
|     { | ||||
|         $view = null; | ||||
|   | ||||
| @@ -0,0 +1,80 @@ | ||||
| <?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\ActivityBundle\Export\Aggregator; | ||||
|  | ||||
| use Chill\ActivityBundle\Export\Declarations; | ||||
| use Chill\ActivityBundle\Repository\ActivityTypeRepositoryInterface; | ||||
| use Chill\MainBundle\Export\AggregatorInterface; | ||||
| use Chill\MainBundle\Repository\LocationRepository; | ||||
| use Chill\MainBundle\Templating\TranslatableStringHelperInterface; | ||||
| use Closure; | ||||
| use Doctrine\ORM\QueryBuilder; | ||||
| use Symfony\Component\Form\FormBuilderInterface; | ||||
| use function in_array; | ||||
|  | ||||
| final readonly class ActivityLocationAggregator implements AggregatorInterface | ||||
| { | ||||
|     public const KEY = 'activity_location_aggregator'; | ||||
|  | ||||
|     public function addRole(): ?string | ||||
|     { | ||||
|         return null; | ||||
|     } | ||||
|  | ||||
|     public function alterQuery(QueryBuilder $qb, $data) | ||||
|     { | ||||
|         if (!in_array('actloc', $qb->getAllAliases(), true)) { | ||||
|             $qb->leftJoin('activity.location', 'actloc'); | ||||
|         } | ||||
|         $qb->addSelect(sprintf('actloc.name AS %s', self::KEY)); | ||||
|         $qb->addGroupBy(self::KEY); | ||||
|     } | ||||
|  | ||||
|     public function applyOn(): string | ||||
|     { | ||||
|         return Declarations::ACTIVITY; | ||||
|     } | ||||
|  | ||||
|     public function buildForm(FormBuilderInterface $builder) | ||||
|     { | ||||
|         // no form required for this aggregator | ||||
|     } | ||||
|     public function getFormDefaultData(): array | ||||
|     { | ||||
|         return []; | ||||
|     } | ||||
|  | ||||
|     public function getLabels($key, array $values, $data): Closure | ||||
|     { | ||||
|         return function ($value): string { | ||||
|             if ('_header' === $value) { | ||||
|                 return 'export.aggregator.activity.by_location.Activity Location'; | ||||
|             } | ||||
|  | ||||
|             if (null === $value || '' === $value) { | ||||
|                 return ''; | ||||
|             } | ||||
|  | ||||
|             return $value; | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     public function getQueryKeys($data): array | ||||
|     { | ||||
|         return [self::KEY]; | ||||
|     } | ||||
|  | ||||
|     public function getTitle() | ||||
|     { | ||||
|         return 'export.aggregator.activity.by_location.Title'; | ||||
|     } | ||||
| } | ||||
| @@ -18,67 +18,193 @@ use Chill\ActivityBundle\Security\Authorization\ActivityVoter; | ||||
| use Chill\MainBundle\Entity\Location; | ||||
| use Chill\MainBundle\Entity\LocationType; | ||||
| use Chill\MainBundle\Entity\Scope; | ||||
| use Chill\MainBundle\Security\Authorization\AuthorizationHelper; | ||||
| use Chill\MainBundle\Security\Resolver\CenterResolverDispatcherInterface; | ||||
| use Chill\MainBundle\Entity\User; | ||||
| use Chill\MainBundle\Entity\UserJob; | ||||
| use Chill\MainBundle\Security\Authorization\AuthorizationHelperForCurrentUserInterface; | ||||
| use Chill\MainBundle\Security\Resolver\CenterResolverManagerInterface; | ||||
| use Chill\PersonBundle\Entity\AccompanyingPeriod; | ||||
| use Chill\PersonBundle\Entity\Person; | ||||
| use Doctrine\DBAL\Types\Types; | ||||
| use Doctrine\ORM\AbstractQuery; | ||||
| use Doctrine\ORM\EntityManagerInterface; | ||||
| use Doctrine\ORM\NonUniqueResultException; | ||||
| use Doctrine\ORM\NoResultException; | ||||
| use Doctrine\ORM\Query\Expr\Join; | ||||
| use Doctrine\ORM\Query\ResultSetMappingBuilder; | ||||
| use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; | ||||
| use Symfony\Component\Security\Core\Role\Role; | ||||
| use Doctrine\ORM\QueryBuilder; | ||||
| use Symfony\Component\HttpFoundation\RequestStack; | ||||
| use Symfony\Component\Security\Core\Security; | ||||
|  | ||||
| use function count; | ||||
| use function in_array; | ||||
|  | ||||
| final class ActivityACLAwareRepository implements ActivityACLAwareRepositoryInterface | ||||
| final readonly class ActivityACLAwareRepository implements ActivityACLAwareRepositoryInterface | ||||
| { | ||||
|     private AuthorizationHelper $authorizationHelper; | ||||
|  | ||||
|     private CenterResolverDispatcherInterface $centerResolverDispatcher; | ||||
|  | ||||
|     private EntityManagerInterface $em; | ||||
|  | ||||
|     private ActivityRepository $repository; | ||||
|  | ||||
|     private Security $security; | ||||
|  | ||||
|     private TokenStorageInterface $tokenStorage; | ||||
|  | ||||
|     public function __construct( | ||||
|         AuthorizationHelper $authorizationHelper, | ||||
|         CenterResolverDispatcherInterface $centerResolverDispatcher, | ||||
|         TokenStorageInterface $tokenStorage, | ||||
|         ActivityRepository $repository, | ||||
|         EntityManagerInterface $em, | ||||
|         Security $security | ||||
|         private AuthorizationHelperForCurrentUserInterface $authorizationHelper, | ||||
|         private CenterResolverManagerInterface $centerResolverManager, | ||||
|         private ActivityRepository $repository, | ||||
|         private EntityManagerInterface $em, | ||||
|         private Security $security, | ||||
|         private RequestStack $requestStack, | ||||
|     ) { | ||||
|         $this->authorizationHelper = $authorizationHelper; | ||||
|         $this->centerResolverDispatcher = $centerResolverDispatcher; | ||||
|         $this->tokenStorage = $tokenStorage; | ||||
|         $this->repository = $repository; | ||||
|         $this->em = $em; | ||||
|         $this->security = $security; | ||||
|     } | ||||
|  | ||||
|     public function findByAccompanyingPeriod(AccompanyingPeriod $period, string $role, ?int $start = 0, ?int $limit = 1000, ?array $orderBy = []): array | ||||
|     /** | ||||
|      * @throws NonUniqueResultException | ||||
|      * @throws NoResultException | ||||
|      */ | ||||
|     public function countByAccompanyingPeriod(AccompanyingPeriod $period, string $role, array $filters = []): int | ||||
|     { | ||||
|         $user = $this->security->getUser(); | ||||
|         $center = $this->centerResolverDispatcher->resolveCenter($period); | ||||
|         $qb = $this->buildBaseQuery($filters); | ||||
|  | ||||
|         if (0 === count($orderBy)) { | ||||
|             $orderBy = ['date' => 'DESC']; | ||||
|         $qb | ||||
|             ->select('COUNT(a)') | ||||
|             ->andWhere('a.accompanyingPeriod = :period')->setParameter('period', $period); | ||||
|  | ||||
|         return $qb->getQuery()->getSingleScalarResult(); | ||||
|     } | ||||
|  | ||||
|     public function countByPerson(Person $person, string $role, array $filters = []): int | ||||
|     { | ||||
|         $qb = $this->buildBaseQuery($filters); | ||||
|  | ||||
|         $qb = $this->filterBaseQueryByPerson($qb, $person, $role); | ||||
|  | ||||
|         $qb->select('COUNT(a)'); | ||||
|  | ||||
|         return $qb->getQuery()->getSingleScalarResult(); | ||||
|     } | ||||
|  | ||||
|  | ||||
|     public function findByAccompanyingPeriod(AccompanyingPeriod $period, string $role, ?int $start = 0, ?int $limit = 1000, array $orderBy = ['date' => 'DESC'], array $filters = []): array | ||||
|     { | ||||
|         $qb = $this->buildBaseQuery($filters); | ||||
|  | ||||
|         $qb->andWhere('a.accompanyingPeriod = :period')->setParameter('period', $period); | ||||
|  | ||||
|         foreach ($orderBy as $field => $order) { | ||||
|             $qb->addOrderBy('a.' . $field, $order); | ||||
|         } | ||||
|  | ||||
|         $scopes = $this->authorizationHelper | ||||
|             ->getReachableCircles($user, $role, $center); | ||||
|         if (null !== $start) { | ||||
|             $qb->setFirstResult($start); | ||||
|         } | ||||
|         if (null !== $limit) { | ||||
|             $qb->setMaxResults($limit); | ||||
|         } | ||||
|  | ||||
|         return $this->em->getRepository(Activity::class) | ||||
|             ->findByAccompanyingPeriod($period, $scopes, true, $limit, $start, $orderBy); | ||||
|         return $qb->getQuery()->getResult(); | ||||
|     } | ||||
|  | ||||
|     public function buildBaseQuery(array $filters): QueryBuilder | ||||
|     { | ||||
|         $qb = $this->repository | ||||
|             ->createQueryBuilder('a') | ||||
|         ; | ||||
|  | ||||
|         if (($filters['my_activities'] ?? false) and ($user = $this->security->getUser()) instanceof User) { | ||||
|             $qb->andWhere( | ||||
|                 $qb->expr()->orX( | ||||
|                     'a.createdBy = :user', | ||||
|                     'a.user = :user', | ||||
|                     ':user MEMBER OF a.users' | ||||
|                 ) | ||||
|             )->setParameter('user', $user); | ||||
|         } | ||||
|  | ||||
|         if ([] !== ($types = $filters['types'] ?? [])) { | ||||
|             $qb->andWhere('a.activityType IN (:types)')->setParameter('types', $types); | ||||
|         } | ||||
|  | ||||
|         if ([] !== ($jobs = $filters['jobs'] ?? [])) { | ||||
|             $qb | ||||
|                 ->leftJoin('a.createdBy', 'creator') | ||||
|                 ->leftJoin('a.user', 'activity_u') | ||||
|                 ->andWhere( | ||||
|                     $qb->expr()->orX( | ||||
|                         'creator.userJob IN (:jobs)', | ||||
|                         'activity_u.userJob IN (:jobs)', | ||||
|                         'EXISTS (SELECT 1 FROM ' . User::class . ' activity_user WHERE activity_user MEMBER OF a.users AND activity_user.userJob IN (:jobs))' | ||||
|                     ) | ||||
|                 ) | ||||
|                 ->setParameter('jobs', $jobs); | ||||
|         } | ||||
|  | ||||
|         if (null !== ($after = $filters['after'] ?? null)) { | ||||
|             $qb->andWhere('a.date >= :after')->setParameter('after', $after); | ||||
|         } | ||||
|  | ||||
|         if (null !== ($before = $filters['before'] ?? null)) { | ||||
|             $qb->andWhere('a.date <= :before')->setParameter('before', $before); | ||||
|         } | ||||
|  | ||||
|         return $qb; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @param AccompanyingPeriod|Person $associated | ||||
|      * @return array<ActivityType> | ||||
|      */ | ||||
|     public function findActivityTypeByAssociated(AccompanyingPeriod|Person $associated): array | ||||
|     { | ||||
|         $in = $this->em->createQueryBuilder(); | ||||
|         $in | ||||
|             ->select('1') | ||||
|             ->from(Activity::class, 'a'); | ||||
|  | ||||
|         if ($associated instanceof Person) { | ||||
|             $in = $this->filterBaseQueryByPerson($in, $associated, ActivityVoter::SEE); | ||||
|         } else { | ||||
|             $in->andWhere('a.accompanyingPeriod = :period')->setParameter('period', $associated); | ||||
|         } | ||||
|  | ||||
|         // join between the embedded exist query and the main query | ||||
|         $in->andWhere('a.activityType = t'); | ||||
|  | ||||
|         $qb = $this->em->createQueryBuilder()->setParameters($in->getParameters()); | ||||
|         $qb | ||||
|             ->select('t') | ||||
|             ->from(ActivityType::class, 't') | ||||
|             ->where( | ||||
|                 $qb->expr()->exists($in->getDQL()) | ||||
|             ); | ||||
|  | ||||
|         return $qb->getQuery()->getResult(); | ||||
|     } | ||||
|  | ||||
|     public function findUserJobByAssociated(Person|AccompanyingPeriod $associated): array | ||||
|     { | ||||
|         $in = $this->em->createQueryBuilder(); | ||||
|         $in->select('IDENTITY(u.userJob)') | ||||
|             ->from(User::class, 'u') | ||||
|             ->join( | ||||
|                 Activity::class, | ||||
|                 'a', | ||||
|                 Join::WITH, | ||||
|                 'a.createdBy = u OR a.user = u OR u MEMBER OF a.users' | ||||
|             ); | ||||
|  | ||||
|         if ($associated instanceof Person) { | ||||
|             $in = $this->filterBaseQueryByPerson($in, $associated, ActivityVoter::SEE); | ||||
|         } else { | ||||
|             $in->andWhere('a.accompanyingPeriod = :associated'); | ||||
|             $in->setParameter('associated', $associated); | ||||
|         } | ||||
|  | ||||
|         $qb = $this->em->createQueryBuilder()->setParameters($in->getParameters()); | ||||
|  | ||||
|         $qb->select('ub', 'JSON_EXTRACT(ub.label, :lang) AS HIDDEN lang') | ||||
|             ->from(UserJob::class, 'ub') | ||||
|             ->where($qb->expr()->in('ub.id', $in->getDQL())) | ||||
|             ->setParameter('lang', $this->requestStack->getCurrentRequest()->getLocale()) | ||||
|             ->orderBy('lang') | ||||
|         ; | ||||
|  | ||||
|         return $qb->getQuery()->getResult(); | ||||
|     } | ||||
|  | ||||
|  | ||||
|     public function findByAccompanyingPeriodSimplified(AccompanyingPeriod $period, ?int $limit = 1000): array | ||||
|     { | ||||
|         $rsm = new ResultSetMappingBuilder($this->em); | ||||
| @@ -159,25 +285,73 @@ final class ActivityACLAwareRepository implements ActivityACLAwareRepositoryInte | ||||
|         return $nq->getResult(AbstractQuery::HYDRATE_ARRAY); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @param array $orderBy | ||||
|      * | ||||
|      * @return Activity[]|array | ||||
|      */ | ||||
|     public function findByPerson(Person $person, string $role, ?int $start = 0, ?int $limit = 1000, ?array $orderBy = []): array | ||||
|     public function findByPerson(Person $person, string $role, ?int $start = 0, ?int $limit = 1000, ?array $orderBy = [], array $filters = []): array | ||||
|     { | ||||
|         $user = $this->security->getUser(); | ||||
|         $center = $this->centerResolverDispatcher->resolveCenter($person); | ||||
|         $qb = $this->buildBaseQuery($filters); | ||||
|  | ||||
|         if (0 === count($orderBy)) { | ||||
|             $orderBy = ['date' => 'DESC']; | ||||
|         $qb = $this->filterBaseQueryByPerson($qb, $person, $role); | ||||
|  | ||||
|         foreach ($orderBy as $field => $direction) { | ||||
|             $qb->addOrderBy('a.' . $field, $direction); | ||||
|         } | ||||
|  | ||||
|         $reachableScopes = $this->authorizationHelper | ||||
|             ->getReachableCircles($user, $role, $center); | ||||
|         if (null !== $start) { | ||||
|             $qb->setFirstResult($start); | ||||
|         } | ||||
|         if (null !== $limit) { | ||||
|             $qb->setMaxResults($limit); | ||||
|         } | ||||
|  | ||||
|         return $this->em->getRepository(Activity::class) | ||||
|             ->findByPersonImplied($person, $reachableScopes, $orderBy, $limit, $start); | ||||
|         return $qb->getQuery()->getResult(); | ||||
|     } | ||||
|  | ||||
|     private function filterBaseQueryByPerson(QueryBuilder $qb, Person $person, string $role): QueryBuilder | ||||
|     { | ||||
|         $orX = $qb->expr()->orX(); | ||||
|         $counter = 0; | ||||
|         foreach ($this->centerResolverManager->resolveCenters($person) as $center) { | ||||
|             $scopes = $this->authorizationHelper->getReachableScopes($role, $center); | ||||
|  | ||||
|             if ([] === $scopes) { | ||||
|                 continue; | ||||
|             } | ||||
|  | ||||
|             $orX->add(sprintf('a.person = :person AND a.scope IN (:scopes_%d)', $counter)); | ||||
|             $qb->setParameter(sprintf('scopes_%d', $counter), $scopes); | ||||
|             $qb->setParameter('person', $person); | ||||
|             $counter++; | ||||
|         } | ||||
|  | ||||
|         foreach  ($person->getAccompanyingPeriodParticipations() as $participation) { | ||||
|             if (!$this->security->isGranted(ActivityVoter::SEE, $participation->getAccompanyingPeriod())) { | ||||
|                 continue; | ||||
|             } | ||||
|  | ||||
|             $and = $qb->expr()->andX( | ||||
|                 sprintf('a.accompanyingPeriod = :period_%d', $counter), | ||||
|                 sprintf('a.date >= :participation_start_%d', $counter) | ||||
|             ); | ||||
|  | ||||
|             $qb | ||||
|                 ->setParameter(sprintf('period_%d', $counter), $participation->getAccompanyingPeriod()) | ||||
|                 ->setParameter(sprintf('participation_start_%d', $counter), $participation->getStartDate()); | ||||
|  | ||||
|             if (null !== $participation->getEndDate()) { | ||||
|                 $and->add(sprintf('a.date < :participation_end_%d', $counter)); | ||||
|                 $qb | ||||
|                     ->setParameter(sprintf('participation_end_%d', $counter), $participation->getEndDate()); | ||||
|             } | ||||
|             $orX->add($and); | ||||
|             $counter++; | ||||
|         } | ||||
|  | ||||
|         if (0 === $orX->count()) { | ||||
|             $qb->andWhere('FALSE = TRUE'); | ||||
|         } else { | ||||
|             $qb->andWhere($orX); | ||||
|         } | ||||
|  | ||||
|         return $qb; | ||||
|     } | ||||
|  | ||||
|     public function queryTimelineIndexer(string $context, array $args = []): array | ||||
| @@ -226,7 +400,6 @@ final class ActivityACLAwareRepository implements ActivityACLAwareRepositoryInte | ||||
|  | ||||
|         // acls: | ||||
|         $reachableCenters = $this->authorizationHelper->getReachableCenters( | ||||
|             $this->tokenStorage->getToken()->getUser(), | ||||
|             ActivityVoter::SEE | ||||
|         ); | ||||
|  | ||||
| @@ -251,7 +424,7 @@ final class ActivityACLAwareRepository implements ActivityACLAwareRepositoryInte | ||||
|                 continue; | ||||
|             } | ||||
|             // we get all the reachable scopes for this center | ||||
|             $reachableScopes = $this->authorizationHelper->getReachableScopes($this->tokenStorage->getToken()->getUser(), ActivityVoter::SEE, $center); | ||||
|             $reachableScopes = $this->authorizationHelper->getReachableScopes(ActivityVoter::SEE, $center); | ||||
|             // we get the ids for those scopes | ||||
|             $reachablesScopesId = array_map( | ||||
|                 static fn (Scope $scope) => $scope->getId(), | ||||
|   | ||||
| @@ -11,15 +11,32 @@ declare(strict_types=1); | ||||
|  | ||||
| namespace Chill\ActivityBundle\Repository; | ||||
|  | ||||
| use Chill\ActivityBundle\Entity\Activity; | ||||
| use Chill\ActivityBundle\Entity\ActivityType; | ||||
| use Chill\MainBundle\Entity\UserJob; | ||||
| use Chill\PersonBundle\Entity\AccompanyingPeriod; | ||||
| use Chill\PersonBundle\Entity\Person; | ||||
|  | ||||
| interface ActivityACLAwareRepositoryInterface | ||||
| { | ||||
|     /** | ||||
|      * @return Activity[]|array | ||||
|      * Return all the activities associated to an accompanying period and that the user is allowed to apply the given role. | ||||
|      * | ||||
|      * | ||||
|      * @param array{my_activities?: bool, types?: array<ActivityType>, jobs?: array<UserJob>, after?: \DateTimeImmutable|null, before?: \DateTimeImmutable|null} $filters | ||||
|      * @return array<Activity> | ||||
|      */ | ||||
|     public function findByAccompanyingPeriod(AccompanyingPeriod $period, string $role, ?int $start = 0, ?int $limit = 1000, ?array $orderBy = []): array; | ||||
|     public function findByAccompanyingPeriod(AccompanyingPeriod $period, string $role, ?int $start = 0, ?int $limit = 1000, array $orderBy = ['date' => 'DESC'], array $filters = []): array; | ||||
|  | ||||
|     /** | ||||
|      * @param array{my_activities?: bool, types?: array<ActivityType>, jobs?: array<UserJob>, after?: \DateTimeImmutable|null, before?: \DateTimeImmutable|null} $filters | ||||
|      */ | ||||
|     public function countByAccompanyingPeriod(AccompanyingPeriod $period, string $role, array $filters = []): int; | ||||
|  | ||||
|     /** | ||||
|      * @param array{my_activities?: bool, types?: array<ActivityType>, jobs?: array<UserJob>, after?: \DateTimeImmutable|null, before?: \DateTimeImmutable|null} $filters | ||||
|      */ | ||||
|     public function countByPerson(Person $person, string $role, array $filters = []): int; | ||||
|  | ||||
|     /** | ||||
|      * Return a list of activities, simplified as array (not object). | ||||
| @@ -31,7 +48,28 @@ interface ActivityACLAwareRepositoryInterface | ||||
|     public function findByAccompanyingPeriodSimplified(AccompanyingPeriod $period, ?int $limit = 1000): array; | ||||
|  | ||||
|     /** | ||||
|      * @return Activity[]|array | ||||
|      * @param array{my_activities?: bool, types?: array<ActivityType>, jobs?: array<UserJob>, after?: \DateTimeImmutable|null, before?: \DateTimeImmutable|null} $filters | ||||
|      * @return array<Activity> | ||||
|      */ | ||||
|     public function findByPerson(Person $person, string $role, ?int $start = 0, ?int $limit = 1000, ?array $orderBy = []): array; | ||||
|     public function findByPerson(Person $person, string $role, ?int $start = 0, ?int $limit = 1000, array $orderBy = ['date' => 'DESC'], array $filters = []): array; | ||||
|  | ||||
|  | ||||
|     /** | ||||
|      * Return a list of the type for the activities associated to person or accompanying period | ||||
|      * | ||||
|      * @return array<ActivityType> | ||||
|      */ | ||||
|     public function findActivityTypeByAssociated(AccompanyingPeriod|Person $associated): array; | ||||
|  | ||||
|     /** | ||||
|      * Return a list of the user job for the activities associated to person or accompanying period | ||||
|      * | ||||
|      * Associated mean the job: | ||||
|      * - of the creator; | ||||
|      * - of the user (activity.user) | ||||
|      * - of all the users | ||||
|      * | ||||
|      * @return array<UserJob> | ||||
|      */ | ||||
|     public function findUserJobByAssociated(AccompanyingPeriod|Person $associated): array; | ||||
| } | ||||
|   | ||||
| @@ -11,9 +11,13 @@ declare(strict_types=1); | ||||
|  | ||||
| namespace Chill\ActivityBundle\Repository; | ||||
|  | ||||
| use Chill\ActivityBundle\Entity\Activity; | ||||
| use Chill\ActivityBundle\Entity\ActivityType; | ||||
| use Chill\PersonBundle\Entity\AccompanyingPeriod; | ||||
| use Chill\PersonBundle\Entity\Person; | ||||
| use Doctrine\ORM\EntityManagerInterface; | ||||
| use Doctrine\ORM\EntityRepository; | ||||
| use Doctrine\ORM\Query\Expr\Join; | ||||
|  | ||||
| final class ActivityTypeRepository implements ActivityTypeRepositoryInterface | ||||
| { | ||||
|   | ||||
| @@ -12,12 +12,14 @@ declare(strict_types=1); | ||||
| namespace Chill\ActivityBundle\Repository; | ||||
|  | ||||
| use Chill\ActivityBundle\Entity\ActivityType; | ||||
| use Chill\PersonBundle\Entity\AccompanyingPeriod; | ||||
| use Chill\PersonBundle\Entity\Person; | ||||
| use Doctrine\Persistence\ObjectRepository; | ||||
|  | ||||
| interface ActivityTypeRepositoryInterface extends ObjectRepository | ||||
| { | ||||
|     /** | ||||
|      * @return array|ActivityType[] | ||||
|      * @return array<ActivityType> | ||||
|      */ | ||||
|     public function findAllActive(): array; | ||||
| } | ||||
|   | ||||
| @@ -80,12 +80,15 @@ | ||||
|  | ||||
| <div class="context-{{ context }}"> | ||||
|  | ||||
|     {{ filter|chill_render_filter_order_helper }} | ||||
|  | ||||
|     {% if activities|length == 0 %} | ||||
|         <p class="chill-no-data-statement"> | ||||
|             {{ "There isn't any activities."|trans }} | ||||
|         </p> | ||||
|  | ||||
|     {% else %} | ||||
|  | ||||
|         <div class="flex-table activity-list"> | ||||
|             {% for activity in activities %} | ||||
|                 {% include 'ChillActivityBundle:Activity:_list_item.html.twig' with { | ||||
| @@ -96,4 +99,6 @@ | ||||
|         </div> | ||||
|     {% endif %} | ||||
|  | ||||
|     {{ chill_pagination(paginator) }} | ||||
|  | ||||
| </div> | ||||
|   | ||||
| @@ -0,0 +1,325 @@ | ||||
| <?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\ActivityBundle\Tests\Repository; | ||||
|  | ||||
| use Chill\ActivityBundle\Entity\ActivityType; | ||||
| use Chill\ActivityBundle\Repository\ActivityACLAwareRepository; | ||||
| use Chill\ActivityBundle\Repository\ActivityRepository; | ||||
| use Chill\ActivityBundle\Security\Authorization\ActivityVoter; | ||||
| use Chill\MainBundle\Entity\Center; | ||||
| use Chill\MainBundle\Entity\Scope; | ||||
| use Chill\MainBundle\Entity\User; | ||||
| use Chill\MainBundle\Entity\UserJob; | ||||
| use Chill\MainBundle\Security\Authorization\AuthorizationHelperForCurrentUserInterface; | ||||
| use Chill\MainBundle\Security\Resolver\CenterResolverManagerInterface; | ||||
| use Chill\PersonBundle\Entity\AccompanyingPeriod; | ||||
| use Chill\PersonBundle\Entity\Person; | ||||
| use Doctrine\ORM\EntityManagerInterface; | ||||
| use Prophecy\Argument; | ||||
| use Prophecy\PhpUnit\ProphecyTrait; | ||||
| use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; | ||||
| use Symfony\Component\HttpFoundation\Request; | ||||
| use Symfony\Component\HttpFoundation\RequestStack; | ||||
| use Symfony\Component\Security\Core\Security; | ||||
|  | ||||
| /** | ||||
|  * @internal | ||||
|  * @coversNothing | ||||
|  */ | ||||
| class ActivityACLAwareRepositoryTest extends KernelTestCase | ||||
| { | ||||
|     use ProphecyTrait; | ||||
|     private AuthorizationHelperForCurrentUserInterface $authorizationHelperForCurrentUser; | ||||
|  | ||||
|     private CenterResolverManagerInterface $centerResolverManager; | ||||
|  | ||||
|     private ActivityRepository $activityRepository; | ||||
|  | ||||
|     private EntityManagerInterface $entityManager; | ||||
|  | ||||
|     private Security $security; | ||||
|  | ||||
|     private RequestStack $requestStack; | ||||
|  | ||||
|     protected function setUp(): void | ||||
|     { | ||||
|         self::bootKernel(); | ||||
|  | ||||
|         $this->authorizationHelperForCurrentUser = self::$container->get(AuthorizationHelperForCurrentUserInterface::class); | ||||
|         $this->centerResolverManager = self::$container->get(CenterResolverManagerInterface::class); | ||||
|         $this->activityRepository = self::$container->get(ActivityRepository::class); | ||||
|         $this->entityManager = self::$container->get(EntityManagerInterface::class); | ||||
|         $this->security = self::$container->get(Security::class); | ||||
|  | ||||
|         $this->requestStack = $requestStack = new RequestStack(); | ||||
|         $request = $this->prophesize(Request::class); | ||||
|         $request->getLocale()->willReturn('fr'); | ||||
|         $request->getDefaultLocale()->willReturn('fr'); | ||||
|         $requestStack->push($request->reveal()); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @dataProvider provideDataFindByAccompanyingPeriod | ||||
|      */ | ||||
|     public function testFindByAccompanyingPeriod(AccompanyingPeriod $period, User $user, string $role, ?int $start = 0, ?int $limit = 1000, array $orderBy = ['date' => 'DESC'], array $filters = []): void | ||||
|     { | ||||
|         $security = $this->prophesize(Security::class); | ||||
|         $security->isGranted($role, $period)->willReturn(true); | ||||
|         $security->getUser()->willReturn($user); | ||||
|  | ||||
|         $repository = new ActivityACLAwareRepository( | ||||
|             $this->authorizationHelperForCurrentUser, | ||||
|             $this->centerResolverManager, | ||||
|             $this->activityRepository, | ||||
|             $this->entityManager, | ||||
|             $security->reveal(), | ||||
|             $this->requestStack | ||||
|         ); | ||||
|  | ||||
|         $actual = $repository->findByAccompanyingPeriod($period, $role, $start, $limit, $orderBy, $filters); | ||||
|  | ||||
|         self::assertIsArray($actual); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @dataProvider provideDataFindByAccompanyingPeriod | ||||
|      */ | ||||
|     public function testFindActivityTypeByAccompanyingPeriod(AccompanyingPeriod $period, User $user, string $role, ?int $start = 0, ?int $limit = 1000, array $orderBy = ['date' => 'DESC'], array $filters = []): void | ||||
|     { | ||||
|         $security = $this->prophesize(Security::class); | ||||
|         $security->isGranted($role, $period)->willReturn(true); | ||||
|         $security->getUser()->willReturn($user); | ||||
|  | ||||
|         $repository = new ActivityACLAwareRepository( | ||||
|             $this->authorizationHelperForCurrentUser, | ||||
|             $this->centerResolverManager, | ||||
|             $this->activityRepository, | ||||
|             $this->entityManager, | ||||
|             $security->reveal(), | ||||
|             $this->requestStack | ||||
|         ); | ||||
|  | ||||
|         $actual = $repository->findActivityTypeByAssociated($period); | ||||
|  | ||||
|         self::assertIsArray($actual); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @dataProvider provideDataFindByPerson | ||||
|      */ | ||||
|     public function testFindActivityTypeByPerson(Person $person, User $user, array $centers, array $scopes, string $role, ?int $start = 0, ?int $limit = 1000, ?array $orderBy = [], array $filters = []): void | ||||
|     { | ||||
|         $role = ActivityVoter::SEE; | ||||
|         $centerResolver = $this->prophesize(CenterResolverManagerInterface::class); | ||||
|         $centerResolver->resolveCenters($person)->willReturn($centers); | ||||
|  | ||||
|         $authorizationHelper = $this->prophesize(AuthorizationHelperForCurrentUserInterface::class); | ||||
|         $authorizationHelper->getReachableScopes($role, Argument::type(Center::class)) | ||||
|             ->willReturn($scopes); | ||||
|  | ||||
|         $security = $this->prophesize(Security::class); | ||||
|         $security->isGranted($role, Argument::type(AccompanyingPeriod::class))->willReturn(true); | ||||
|         $security->getUser()->willReturn($user); | ||||
|  | ||||
|         $repository = new ActivityACLAwareRepository( | ||||
|             $authorizationHelper->reveal(), | ||||
|             $centerResolver->reveal(), | ||||
|             $this->activityRepository, | ||||
|             $this->entityManager, | ||||
|             $security->reveal(), | ||||
|             $this->requestStack | ||||
|         ); | ||||
|  | ||||
|         $actual = $repository->findByPerson($person, $role, $start, $limit, $orderBy, $filters); | ||||
|  | ||||
|         self::assertIsArray($actual); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @dataProvider provideDataFindByPerson | ||||
|      */ | ||||
|     public function testFindByPerson(Person $person, User $user, array $centers, array $scopes, string $role, ?int $start = 0, ?int $limit = 1000, ?array $orderBy = [], array $filters = []): void | ||||
|     { | ||||
|         $centerResolver = $this->prophesize(CenterResolverManagerInterface::class); | ||||
|         $centerResolver->resolveCenters($person)->willReturn($centers); | ||||
|  | ||||
|         $authorizationHelper = $this->prophesize(AuthorizationHelperForCurrentUserInterface::class); | ||||
|         $authorizationHelper->getReachableScopes($role, Argument::type(Center::class)) | ||||
|             ->willReturn($scopes); | ||||
|  | ||||
|         $security = $this->prophesize(Security::class); | ||||
|         $security->isGranted($role, Argument::type(AccompanyingPeriod::class))->willReturn(true); | ||||
|         $security->getUser()->willReturn($user); | ||||
|  | ||||
|         $repository = new ActivityACLAwareRepository( | ||||
|             $authorizationHelper->reveal(), | ||||
|             $centerResolver->reveal(), | ||||
|             $this->activityRepository, | ||||
|             $this->entityManager, | ||||
|             $security->reveal(), | ||||
|             $this->requestStack | ||||
|         ); | ||||
|  | ||||
|         $actual = $repository->findByPerson($person, $role, $start, $limit, $orderBy, $filters); | ||||
|  | ||||
|         self::assertIsArray($actual); | ||||
|     } | ||||
|  | ||||
|     public function provideDataFindByPerson(): iterable | ||||
|     { | ||||
|         $this->setUp(); | ||||
|  | ||||
|         /** @var Person $person */ | ||||
|         if (null === $person = $this->entityManager->createQueryBuilder() | ||||
|             ->select('p')->from(Person::class, 'p')->setMaxResults(1) | ||||
|             ->getQuery()->getSingleResult()) { | ||||
|             throw new \RuntimeException("person not found"); | ||||
|         } | ||||
|  | ||||
|         /** @var AccompanyingPeriod $period1 */ | ||||
|         if (null === $period1 = $this->entityManager | ||||
|             ->createQueryBuilder() | ||||
|             ->select('a') | ||||
|             ->from(AccompanyingPeriod::class, 'a') | ||||
|             ->setMaxResults(1) | ||||
|             ->getQuery() | ||||
|             ->getSingleResult()) { | ||||
|             throw new \RuntimeException("no period found"); | ||||
|         } | ||||
|  | ||||
|         /** @var AccompanyingPeriod $period2 */ | ||||
|         if (null === $period2 = $this->entityManager | ||||
|             ->createQueryBuilder() | ||||
|             ->select('a') | ||||
|             ->from(AccompanyingPeriod::class, 'a') | ||||
|             ->where('a.id > :pid') | ||||
|             ->setParameter('pid', $period1->getId()) | ||||
|             ->setMaxResults(1) | ||||
|             ->getQuery() | ||||
|             ->getSingleResult()) { | ||||
|             throw new \RuntimeException("no second period found"); | ||||
|         } | ||||
|         // add a period | ||||
|         $period1->addPerson($person); | ||||
|         $period2->addPerson($person); | ||||
|         $period1->getParticipationsContainsPerson($person)->first()->setEndDate( | ||||
|             (new \DateTime('now'))->add(new \DateInterval('P1M')) | ||||
|         ); | ||||
|  | ||||
|         if ([] === $types = $this->entityManager | ||||
|             ->createQueryBuilder() | ||||
|             ->select('t') | ||||
|             ->from(ActivityType::class, 't') | ||||
|             ->setMaxResults(2) | ||||
|             ->getQuery() | ||||
|             ->getResult()) { | ||||
|             throw new \RuntimeException("no types"); | ||||
|         } | ||||
|  | ||||
|         if ([] === $jobs = $this->entityManager | ||||
|             ->createQueryBuilder() | ||||
|             ->select('j') | ||||
|             ->from(UserJob::class, 'j') | ||||
|             ->setMaxResults(2) | ||||
|             ->getQuery() | ||||
|             ->getResult() | ||||
|         ) { | ||||
|             throw new \RuntimeException("no jobs found"); | ||||
|         } | ||||
|  | ||||
|         if (null === $user = $this->entityManager | ||||
|             ->createQueryBuilder() | ||||
|             ->select('u') | ||||
|             ->from(User::class, 'u') | ||||
|             ->setMaxResults(1) | ||||
|             ->getQuery() | ||||
|             ->getSingleResult() | ||||
|         ) { | ||||
|             throw new \RuntimeException("no user found"); | ||||
|         } | ||||
|  | ||||
|         if ([] === $centers = $this->entityManager->createQueryBuilder() | ||||
|             ->select('c')->from(Center::class, 'c')->setMaxResults(2)->getQuery() | ||||
|             ->getResult()) { | ||||
|             throw new \RuntimeException("no centers found"); | ||||
|         } | ||||
|  | ||||
|         if ([] === $scopes = $this->entityManager->createQueryBuilder() | ||||
|             ->select('s')->from(Scope::class, 's')->setMaxResults(2)->getQuery() | ||||
|             ->getResult()) { | ||||
|             throw new \RuntimeException("no scopes found"); | ||||
|         } | ||||
|  | ||||
|         yield [$person, $user, $centers, $scopes, ActivityVoter::SEE, 0, 5, ['date' => 'DESC'], []]; | ||||
|         yield [$person, $user, $centers, $scopes, ActivityVoter::SEE, 0, 5, ['date' => 'DESC'], ['my_activities' => true]]; | ||||
|         yield [$person, $user, $centers, $scopes, ActivityVoter::SEE, 0, 5, ['date' => 'DESC'], ['types' => $types]]; | ||||
|         yield [$person, $user, $centers, $scopes, ActivityVoter::SEE, 0, 5, ['date' => 'DESC'], ['jobs' => $jobs]]; | ||||
|         yield [$person, $user, $centers, $scopes, ActivityVoter::SEE, 0, 5, ['date' => 'DESC'], ['after' => new \DateTimeImmutable('1 year ago')]]; | ||||
|         yield [$person, $user, $centers, $scopes, ActivityVoter::SEE, 0, 5, ['date' => 'DESC'], ['before' => new \DateTimeImmutable('1 year ago')]]; | ||||
|         yield [$person, $user, $centers, $scopes, ActivityVoter::SEE, 0, 5, ['date' => 'DESC'], ['after' => new \DateTimeImmutable('1 year ago'), 'before' => new \DateTimeImmutable('1 month ago')]]; | ||||
|     } | ||||
|  | ||||
|     public function provideDataFindByAccompanyingPeriod(): iterable | ||||
|     { | ||||
|         $this->setUp(); | ||||
|  | ||||
|         if (null === $period = $this->entityManager | ||||
|             ->createQueryBuilder() | ||||
|             ->select('a') | ||||
|             ->from(AccompanyingPeriod::class, 'a') | ||||
|             ->setMaxResults(1) | ||||
|             ->getQuery() | ||||
|             ->getSingleResult()) { | ||||
|             throw new \RuntimeException("no period found"); | ||||
|         } | ||||
|  | ||||
|         if ([] === $types = $this->entityManager | ||||
|             ->createQueryBuilder() | ||||
|             ->select('t') | ||||
|             ->from(ActivityType::class, 't') | ||||
|             ->setMaxResults(2) | ||||
|             ->getQuery() | ||||
|             ->getResult()) { | ||||
|             throw new \RuntimeException("no types"); | ||||
|         } | ||||
|  | ||||
|         if ([] === $jobs = $this->entityManager | ||||
|             ->createQueryBuilder() | ||||
|             ->select('j') | ||||
|             ->from(UserJob::class, 'j') | ||||
|             ->setMaxResults(2) | ||||
|             ->getQuery() | ||||
|             ->getResult() | ||||
|         ) { | ||||
|             throw new \RuntimeException("no jobs found"); | ||||
|         } | ||||
|  | ||||
|         if (null === $user = $this->entityManager | ||||
|             ->createQueryBuilder() | ||||
|             ->select('u') | ||||
|             ->from(User::class, 'u') | ||||
|             ->setMaxResults(1) | ||||
|             ->getQuery() | ||||
|             ->getSingleResult() | ||||
|         ) { | ||||
|             throw new \RuntimeException("no user found"); | ||||
|         } | ||||
|  | ||||
|         yield [$period, $user, ActivityVoter::SEE, 0, 10, ['date' => 'DESC'], []]; | ||||
|         yield [$period, $user, ActivityVoter::SEE, 0, 10, ['date' => 'DESC'], ['my_activities' => true]]; | ||||
|         yield [$period, $user, ActivityVoter::SEE, 0, 10, ['date' => 'DESC'], ['types' => $types]]; | ||||
|         yield [$period, $user, ActivityVoter::SEE, 0, 10, ['date' => 'DESC'], ['jobs' => $jobs]]; | ||||
|         yield [$period, $user, ActivityVoter::SEE, 0, 10, ['date' => 'DESC'], ['after' => new \DateTimeImmutable('1 year ago')]]; | ||||
|         yield [$period, $user, ActivityVoter::SEE, 0, 10, ['date' => 'DESC'], ['before' => new \DateTimeImmutable('1 year ago')]]; | ||||
|         yield [$period, $user, ActivityVoter::SEE, 0, 10, ['date' => 'DESC'], ['after' => new \DateTimeImmutable('1 year ago'), 'before' => new \DateTimeImmutable('1 month ago')]]; | ||||
|     } | ||||
| } | ||||
| @@ -148,6 +148,10 @@ services: | ||||
|         tags: | ||||
|             - { name: chill.export_aggregator, alias: activity_common_type_aggregator } | ||||
|  | ||||
|     Chill\ActivityBundle\Export\Aggregator\ActivityLocationAggregator: | ||||
|         tags: | ||||
|             - { name: chill.export_aggregator, alias: activity_common_location_aggregator } | ||||
|  | ||||
|     chill.activity.export.user_aggregator: | ||||
|         class: Chill\ActivityBundle\Export\Aggregator\ActivityUserAggregator | ||||
|         tags: | ||||
|   | ||||
| @@ -83,12 +83,20 @@ Third persons: Tiers non-pro. | ||||
| Others persons: Usagers | ||||
| Third parties: Tiers professionnels | ||||
| Users concerned: T(M)S | ||||
|  | ||||
| activity: | ||||
|     date: Date de l'échange | ||||
|     Insert a document: Insérer un document | ||||
|     Remove a document: Supprimer le document | ||||
|     comment: Commentaire | ||||
| No documents: Aucun document | ||||
|  | ||||
| # activity filter in list page | ||||
| activity_filter: | ||||
|     My activities: Mes échanges (où j'interviens) | ||||
|     Types: Par type d'échange | ||||
|     Jobs: Par métier impliqué | ||||
|  | ||||
| #timeline | ||||
| '%user% has done an %activity_type%': '%user% a effectué un échange de type "%activity_type%"' | ||||
|  | ||||
| @@ -378,6 +386,9 @@ export: | ||||
|                 is sent: envoyé | ||||
|                 is received: reçu | ||||
|                 Group activity by sentreceived: Grouper les échanges par envoyé / reçu | ||||
|             by_location: | ||||
|                 Activity Location: Localisation de l'échange | ||||
|                 Title: Grouper les échanges par localisation de l'échange | ||||
|  | ||||
| generic_doc: | ||||
|     filter: | ||||
|   | ||||
| @@ -37,7 +37,7 @@ class UserJob | ||||
|     protected ?int $id = null; | ||||
|  | ||||
|     /** | ||||
|      * @var array|string[]A | ||||
|      * @var array<string, string> | ||||
|      * @ORM\Column(name="label", type="json") | ||||
|      * @Serializer\Groups({"read", "docgen:read"}) | ||||
|      * @Serializer\Context({"is-translatable": true}, groups={"docgen:read"}) | ||||
|   | ||||
| @@ -97,7 +97,7 @@ interface ExportInterface extends ExportElementInterface | ||||
|      * @param mixed[] $values The values from the result. if there are duplicates, those might be given twice. Example: array('FR', 'BE', 'CZ', 'FR', 'BE', 'FR') | ||||
|      * @param mixed $data The data from the export's form (as defined in `buildForm`) | ||||
|      * | ||||
|      * @return callable(null|string|int|float|'_header' $value): string|int|\DateTimeInterface where the first argument is the value, and the function should return the label to show in the formatted file. Example : `function($countryCode) use ($countries) { return $countries[$countryCode]->getName(); }` | ||||
|      * @return (callable(null|string|int|float|'_header' $value): string|int|\DateTimeInterface) where the first argument is the value, and the function should return the label to show in the formatted file. Example : `function($countryCode) use ($countries) { return $countries[$countryCode]->getName(); }` | ||||
|      */ | ||||
|     public function getLabels($key, array $values, $data); | ||||
|  | ||||
|   | ||||
| @@ -13,6 +13,8 @@ namespace Chill\MainBundle\Form\Type\Listing; | ||||
|  | ||||
| use Chill\MainBundle\Form\Type\ChillDateType; | ||||
| use Chill\MainBundle\Templating\Listing\FilterOrderHelper; | ||||
| use Symfony\Bridge\Doctrine\Form\Type\EntityType; | ||||
| use Symfony\Component\Form\Extension\Core\Type\CheckboxType; | ||||
| use Symfony\Component\Form\Extension\Core\Type\ChoiceType; | ||||
| use Symfony\Component\Form\Extension\Core\Type\HiddenType; | ||||
| use Symfony\Component\Form\Extension\Core\Type\SearchType; | ||||
| @@ -27,13 +29,6 @@ use function count; | ||||
|  | ||||
| final class FilterOrderType extends \Symfony\Component\Form\AbstractType | ||||
| { | ||||
|     private RequestStack $requestStack; | ||||
|  | ||||
|     public function __construct(RequestStack $requestStack) | ||||
|     { | ||||
|         $this->requestStack = $requestStack; | ||||
|     } | ||||
|  | ||||
|     public function buildForm(FormBuilderInterface $builder, array $options) | ||||
|     { | ||||
|         /** @var FilterOrderHelper $helper */ | ||||
| @@ -43,22 +38,16 @@ final class FilterOrderType extends \Symfony\Component\Form\AbstractType | ||||
|             $builder->add('q', SearchType::class, [ | ||||
|                 'label' => false, | ||||
|                 'required' => false, | ||||
|                 'attr' => [ | ||||
|                     'placeholder' => 'filter_order.Search', | ||||
|                 ] | ||||
|             ]); | ||||
|         } | ||||
|  | ||||
|         $checkboxesBuilder = $builder->create('checkboxes', null, ['compound' => true]); | ||||
|  | ||||
|         foreach ($helper->getCheckboxes() as $name => $c) { | ||||
|             $choices = array_combine( | ||||
|                 array_map(static function ($c, $t) { | ||||
|                     if (null !== $t) { | ||||
|                         return $t; | ||||
|                     } | ||||
|  | ||||
|                     return $c; | ||||
|                 }, $c['choices'], $c['trans']), | ||||
|                 $c['choices'] | ||||
|             ); | ||||
|             $choices = self::buildCheckboxChoices($c['choices'], $c['trans']); | ||||
|  | ||||
|             $checkboxesBuilder->add($name, ChoiceType::class, [ | ||||
|                 'choices' => $choices, | ||||
| @@ -71,6 +60,25 @@ final class FilterOrderType extends \Symfony\Component\Form\AbstractType | ||||
|             $builder->add($checkboxesBuilder); | ||||
|         } | ||||
|  | ||||
|         if ([] !== $helper->getEntityChoices()) { | ||||
|             $entityChoicesBuilder = $builder->create('entity_choices', null, ['compound' => true]); | ||||
|  | ||||
|             foreach ($helper->getEntityChoices() as $key => [ | ||||
|                 'label' => $label, 'choices' => $choices, 'options' => $opts, 'class' => $class | ||||
|             ]) { | ||||
|                 $entityChoicesBuilder->add($key, EntityType::class, [ | ||||
|                     'label' => $label, | ||||
|                     'choices' => $choices, | ||||
|                     'class' => $class, | ||||
|                     'multiple' => true, | ||||
|                     'expanded' => true, | ||||
|                     ...$opts, | ||||
|                 ]); | ||||
|             } | ||||
|  | ||||
|             $builder->add($entityChoicesBuilder); | ||||
|         } | ||||
|  | ||||
|         if (0 < count($helper->getDateRanges())) { | ||||
|             $dateRangesBuilder = $builder->create('dateRanges', null, ['compound' => true]); | ||||
|  | ||||
| @@ -97,31 +105,31 @@ final class FilterOrderType extends \Symfony\Component\Form\AbstractType | ||||
|             $builder->add($dateRangesBuilder); | ||||
|         } | ||||
|  | ||||
|         foreach ($this->requestStack->getCurrentRequest()->query->getIterator() as $key => $value) { | ||||
|             switch ($key) { | ||||
|                 case 'q': | ||||
|                 case 'checkboxes' . $key: | ||||
|                 case $key . '_from': | ||||
|                 case $key . '_to': | ||||
|                     break; | ||||
|         if ([] !== $helper->getSingleCheckbox()) { | ||||
|             $singleCheckBoxBuilder = $builder->create('single_checkboxes', null, ['compound' => true]); | ||||
|  | ||||
|                 case 'page': | ||||
|                     $builder->add($key, HiddenType::class, [ | ||||
|                         'data' => 1, | ||||
|                     ]); | ||||
|  | ||||
|                     break; | ||||
|  | ||||
|                 default: | ||||
|                     $builder->add($key, HiddenType::class, [ | ||||
|                         'data' => $value, | ||||
|                     ]); | ||||
|  | ||||
|                     break; | ||||
|             foreach ($helper->getSingleCheckbox() as $name => ['label' => $label]) { | ||||
|                 $singleCheckBoxBuilder->add($name, CheckboxType::class, ['label' => $label, 'required' => false]); | ||||
|             } | ||||
|  | ||||
|             $builder->add($singleCheckBoxBuilder); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public static function buildCheckboxChoices(array $choices, array $trans = []): array | ||||
|     { | ||||
|         return array_combine( | ||||
|             array_map(static function ($c, $t) { | ||||
|                 if (null !== $t) { | ||||
|                     return $t; | ||||
|                 } | ||||
|  | ||||
|                 return $c; | ||||
|             }, $choices, $trans), | ||||
|             $choices | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     public function buildView(FormView $view, FormInterface $form, array $options) | ||||
|     { | ||||
|         /** @var FilterOrderHelper $helper */ | ||||
|   | ||||
| @@ -42,3 +42,7 @@ form { | ||||
|     font-weight: 700; | ||||
|     margin-bottom: .375em; | ||||
| } | ||||
|  | ||||
| .chill_filter_order { | ||||
|     background: $gray-100; | ||||
| } | ||||
| @@ -1,65 +1,120 @@ | ||||
| {{ form_start(form) }} | ||||
|     <div class="chill_filter_order container my-4"> | ||||
|         <div class="row"> | ||||
|             {% if form.vars.has_search_box %} | ||||
|               <div class="col-md-12"> | ||||
|                   <div class="input-group mb-3"> | ||||
|                       {{ form_widget(form.q)}} | ||||
|                       <button type="submit" class="btn btn-chill-l-gray"><i class="fa fa-search"></i></button> | ||||
|                   </div> | ||||
|               </div> | ||||
|             {% endif %} | ||||
|         </div> | ||||
|         {% if form.dateRanges is defined %} | ||||
|             {% if form.dateRanges|length > 0 %} | ||||
|                 {% for dateRangeName, _o in form.dateRanges %} | ||||
|                     <div class="row gx-2 justify-content-center"> | ||||
|                         {% if form.dateRanges[dateRangeName].vars.label is not same as(false) %} | ||||
|                         <div class="col-md-5"> | ||||
|                             {{ form_label(form.dateRanges[dateRangeName])}} | ||||
|                         </div> | ||||
|                         {% endif %} | ||||
|                         <div class="col-md-6"> | ||||
|                             <div class="input-group mb-3"> | ||||
|                                 <span class="input-group-text">{{ 'chill_calendar.From'|trans }}</span> | ||||
|                                 {{ form_widget(form.dateRanges[dateRangeName]['from']) }} | ||||
|                                 <span class="input-group-text">{{ 'chill_calendar.To'|trans }}</span> | ||||
|                                 {{ form_widget(form.dateRanges[dateRangeName]['to']) }} | ||||
|                             </div> | ||||
|                         </div> | ||||
|                         <div class="col-md-1"> | ||||
|                             <button type="submit" class="btn btn-misc"><i class="fa fa-filter"></i></button> | ||||
| <div class="accordion my-3" id="filterOrderAccordion"> | ||||
|     <h2 class="accordion-header" id="filterOrderHeading"> | ||||
|         <button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#filterOrderCollapse" aria-expanded="true" aria-controls="filterOrderCollapse"> | ||||
|             <strong><i class="fa fa-fw fa-filter"></i>Filtrer la liste</strong> | ||||
|         </button> | ||||
|     </h2> | ||||
|     <div class="accordion-collapse collapse" id="filterOrderCollapse" aria-labelledby="filterOrderHeading" data-bs-parent="#filterOrderAccordion"> | ||||
|         {% set btnSubmit = 0 %} | ||||
|         <div class="accordion-body chill_filter_order container-xxl p-5 py-2"> | ||||
|             <div class="row my-2"> | ||||
|                 {% if form.vars.has_search_box %} | ||||
|                     <div class="col-sm-12"> | ||||
|                         <div class="input-group"> | ||||
|                             {{ form_widget(form.q) }} | ||||
|                             <button type="submit" class="btn btn-misc"><i class="fa fa-search"></i></button> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 {% endfor %} | ||||
|                 {% endif %} | ||||
|             </div> | ||||
|             {% if form.dateRanges is defined %} | ||||
|                 {% set btnSubmit = 1 %} | ||||
|                 {% if form.dateRanges|length > 0 %} | ||||
|                     {% for dateRangeName, _o in form.dateRanges %} | ||||
|                         <div class="row my-2"> | ||||
|                             {% if form.dateRanges[dateRangeName].vars.label is not same as(false) %} | ||||
|                                 {{ form_label(form.dateRanges[dateRangeName])}} | ||||
|                             {% else %} | ||||
|                                 <div class="col-sm-4 col-form-label">{{ 'filter_order.By date'|trans }}</div> | ||||
|                             {% endif %} | ||||
|                             <div class="col-sm-8 pt-1"> | ||||
|                                 <div class="input-group"> | ||||
|                                     <span class="input-group-text">{{ 'chill_calendar.From'|trans }}</span> | ||||
|                                     {{ form_widget(form.dateRanges[dateRangeName]['from']) }} | ||||
|                                     <span class="input-group-text">{{ 'chill_calendar.To'|trans }}</span> | ||||
|                                     {{ form_widget(form.dateRanges[dateRangeName]['to']) }} | ||||
|                                 </div> | ||||
|                             </div> | ||||
|                         </div> | ||||
|                     {% endfor %} | ||||
|                 {% endif %} | ||||
|             {% endif %} | ||||
|         {% endif %} | ||||
|         {% if form.checkboxes is defined %} | ||||
|             {% if form.checkboxes|length > 0 %} | ||||
|                 {% for checkbox_name, options in form.checkboxes %} | ||||
|                     <div class="row gx-0"> | ||||
|                         <div class="col-md-12"> | ||||
|                             {% for c in form['checkboxes'][checkbox_name].children %} | ||||
|                                 <div class="form-check form-check-inline"> | ||||
|             {% if form.checkboxes is defined %} | ||||
|                 {% set btnSubmit = 1 %} | ||||
|                 {% if form.checkboxes|length > 0 %} | ||||
|                     {% for checkbox_name, options in form.checkboxes %} | ||||
|                         <div class="row my-2"> | ||||
|                             <div class="col-sm-4 col-form-label">{{ 'filter_order.By'|trans }}</div> | ||||
|                             <div class="col-sm-8 pt-2"> | ||||
|                                 {% for c in form['checkboxes'][checkbox_name].children %} | ||||
|                                     {{ form_widget(c) }} | ||||
|                                     {{ form_label(c) }} | ||||
|                                 </div> | ||||
|                             {% endfor %} | ||||
|                         </div> | ||||
|                     </div> | ||||
|                     {% if loop.last %} | ||||
|                         <div class="row gx-0"> | ||||
|                             <div class="col-md-12"> | ||||
|                                 <ul class="record_actions"> | ||||
|                                     <li> | ||||
|                                         <button type="submit" class="btn btn-misc"><i class="fa fa-filter"></i></button> | ||||
|                                     </li> | ||||
|                                 </ul> | ||||
|                                 {% endfor %} | ||||
|                             </div> | ||||
|                         </div> | ||||
|                     {% endif %} | ||||
|                     {% endfor %} | ||||
|                 {% endif %} | ||||
|             {% endif %} | ||||
|             {% if form.entity_choices is defined %} | ||||
|                 {% set btnSubmit = 1 %} | ||||
|                 {% if form.entity_choices |length > 0 %} | ||||
|                     {% for checkbox_name, options in form.entity_choices %} | ||||
|                         <div class="row my-2"> | ||||
|                             {% if form.entity_choices[checkbox_name].vars.label is not same as(false) %} | ||||
|                                 {{ form_label(form.entity_choices[checkbox_name])}} | ||||
|                             {% endif %} | ||||
|                             <div class="col-sm-8 pt-2"> | ||||
|                                 {% for c in form['entity_choices'][checkbox_name].children %} | ||||
|                                     {{ form_widget(c) }} | ||||
|                                     {{ form_label(c) }} | ||||
|                                 {% endfor %} | ||||
|                             </div> | ||||
|                         </div> | ||||
|                     {% endfor %} | ||||
|                 {% endif %} | ||||
|             {% endif %} | ||||
|             {% if form.single_checkboxes is defined %} | ||||
|                 {% set btnSubmit = 1 %} | ||||
|                 {% for name, _o in form.single_checkboxes %} | ||||
|                     <div class="row my-2"> | ||||
|                         <div class="col-sm-4 col-form-label">{{ 'filter_order.By'|trans }}</div> | ||||
|                         <div class="col-sm-8 pt-2"> | ||||
|                             {{ form_widget(form.single_checkboxes[name]) }} | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 {% endfor %} | ||||
|             {% endif %} | ||||
|         {% endif %} | ||||
|  | ||||
|             {% if btnSubmit == 1 %} | ||||
|                 <div class="row my-2"> | ||||
|                     <button type="submit" class="btn btn-sm btn-misc"><i class="fa fa-fw fa-filter"></i>{{ 'Filter'|trans }}</button> | ||||
|                 </div> | ||||
|             {% endif %} | ||||
|         </div> | ||||
|     </div> | ||||
|     {% if active|length > 0 %} | ||||
|         <div class="activeFilters mt-3"> | ||||
|             {% for f in active %} | ||||
|                 <span class="badge rounded-pill bg-secondary ms-1 {{ f.position }} {{ f.name }}"> | ||||
|                     {%- if f.label != '' %} | ||||
|                         <span class="text-dark">{{ f.label|trans }} : </span> | ||||
|                     {% endif -%} | ||||
|                     {%- if f.position == 'search_box' and f.value is not null %} | ||||
|                         <span class="text-dark">{{ 'filter_order.search_box'|trans ~ ' :' }}</span> | ||||
|                     {% endif -%} | ||||
|                     {{ f.value}}{# | ||||
|                 #}</span> | ||||
|             {% endfor %} | ||||
|         </div> | ||||
|     {% endif %} | ||||
|     <div> | ||||
|  | ||||
|     </div> | ||||
| </div> | ||||
|  | ||||
| {% for k,v in otherParameters %} | ||||
|     <input type="hidden" name="{{ k }}" value="{{ v }}" /> | ||||
| {% endfor %} | ||||
| {{ form_end(form) }} | ||||
|  | ||||
|   | ||||
| @@ -0,0 +1,84 @@ | ||||
| <?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\Listing; | ||||
|  | ||||
| use Symfony\Component\PropertyAccess\PropertyAccessorInterface; | ||||
| use Symfony\Component\PropertyAccess\PropertyPathInterface; | ||||
| use Symfony\Contracts\Translation\TranslatorInterface; | ||||
|  | ||||
| final readonly class FilterOrderGetActiveFilterHelper | ||||
| { | ||||
|     public function __construct( | ||||
|         private TranslatorInterface $translator, | ||||
|         private PropertyAccessorInterface $propertyAccessor, | ||||
|     ) { | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Return all the data required to display the active filters | ||||
|      * | ||||
|      * @param FilterOrderHelper $filterOrderHelper | ||||
|      * @return array<array{label: string, value: string, position: string, name: string}> | ||||
|      */ | ||||
|     public function getActiveFilters(FilterOrderHelper $filterOrderHelper): array | ||||
|     { | ||||
|         $result = []; | ||||
|  | ||||
|         if ($filterOrderHelper->hasSearchBox() && '' !== $filterOrderHelper->getQueryString()) { | ||||
|             $result[] = ['label' => '', 'value' => $filterOrderHelper->getQueryString(), 'position' => FilterOrderPositionEnum::SearchBox->value, 'name' => 'q']; | ||||
|         } | ||||
|  | ||||
|         foreach ($filterOrderHelper->getDateRanges() as $name => ['label' => $label]) { | ||||
|             $base = ['position' => FilterOrderPositionEnum::DateRange->value, 'name' => $name, 'label' => (string)$label]; | ||||
|  | ||||
|             if (null !== ($from = $filterOrderHelper->getDateRangeData($name)['from'] ?? null)) { | ||||
|                 $result[] = ['value' => $this->translator->trans('filter_order.by_date.From', ['from_date' => $from]), ...$base]; | ||||
|             } | ||||
|             if (null !== ($to = $filterOrderHelper->getDateRangeData($name)['to'] ?? null)) { | ||||
|                 $result[] = ['value' => $this->translator->trans('filter_order.by_date.To', ['to_date' => $to]), ...$base]; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         foreach ($filterOrderHelper->getCheckboxes() as $name => ['choices' => $choices, 'trans' => $trans]) { | ||||
|             $translatedChoice = array_combine($choices, [...$trans]); | ||||
|             foreach ($filterOrderHelper->getCheckboxData($name) as $keyChoice) { | ||||
|                 $result[] = ['value' => $this->translator->trans($translatedChoice[$keyChoice]), 'label' => '', 'position' => FilterOrderPositionEnum::Checkboxes->value, 'name' => $name]; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         foreach ($filterOrderHelper->getEntityChoices() as $name => ['label' => $label, 'class' => $class, 'choices' => $choices, 'options' => $options]) { | ||||
|             foreach ($filterOrderHelper->getEntityChoiceData($name) as $selected) { | ||||
|                 if (is_callable($options['choice_label'])) { | ||||
|                     $value = call_user_func($options['choice_label'], $selected); | ||||
|                 } elseif ($options['choice_label'] instanceof PropertyPathInterface || is_string($options['choice_label'])) { | ||||
|                     $value = $this->propertyAccessor->getValue($selected, $options['choice_label']); | ||||
|                 } else { | ||||
|                     if (!$selected instanceof \Stringable) { | ||||
|                         throw new \UnexpectedValueException(sprintf("we are not able to transform the value of %s to a string. Implements \\Stringable or add a 'choice_label' option to the filterFormBuilder", get_class($selected))); | ||||
|                     } | ||||
|  | ||||
|                     $value = (string)$selected; | ||||
|                 } | ||||
|  | ||||
|                 $result[] = ['value' => $this->translator->trans($value), 'label' => $label, 'position' => FilterOrderPositionEnum::EntityChoice->value, 'name' => $name]; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         foreach ($filterOrderHelper->getSingleCheckbox() as $name => ['label' => $label]) { | ||||
|             if (true === $filterOrderHelper->getSingleCheckboxData($name)) { | ||||
|                 $result[] = ['label' => '', 'value' => $this->translator->trans($label), 'position' => FilterOrderPositionEnum::SingleCheckbox->value, 'name' => $name]; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return $result; | ||||
|     } | ||||
| } | ||||
| @@ -17,47 +17,80 @@ use Symfony\Component\Form\FormFactoryInterface; | ||||
| use Symfony\Component\Form\FormInterface; | ||||
| use Symfony\Component\HttpFoundation\RequestStack; | ||||
|  | ||||
| use Symfony\Component\PropertyAccess\PropertyAccessor; | ||||
| use Symfony\Component\PropertyAccess\PropertyAccessorInterface; | ||||
| use Symfony\Component\PropertyAccess\PropertyPath; | ||||
| use Symfony\Component\PropertyAccess\PropertyPathInterface; | ||||
| use Symfony\Contracts\Translation\TranslatorInterface; | ||||
| use function array_merge; | ||||
| use function count; | ||||
|  | ||||
| class FilterOrderHelper | ||||
| final class FilterOrderHelper | ||||
| { | ||||
|     private array $checkboxes = []; | ||||
|  | ||||
|     /** | ||||
|      * @var array<string, array{label: string}> | ||||
|      */ | ||||
|     private array $singleCheckbox = []; | ||||
|  | ||||
|     private array $dateRanges = []; | ||||
|  | ||||
|     private FormFactoryInterface $formFactory; | ||||
|  | ||||
|     private ?string $formName = 'f'; | ||||
|     public const FORM_NAME = 'f'; | ||||
|  | ||||
|     private array $formOptions = []; | ||||
|  | ||||
|     private string $formType = FilterOrderType::class; | ||||
|  | ||||
|     private RequestStack $requestStack; | ||||
|  | ||||
|     private ?array $searchBoxFields = null; | ||||
|  | ||||
|     private ?array $submitted = null; | ||||
|  | ||||
|     /** | ||||
|      * @var array<string, array{label: string, choices: array, options: array}> | ||||
|      */ | ||||
|     private array $entityChoices = []; | ||||
|  | ||||
|  | ||||
|     public function __construct( | ||||
|         FormFactoryInterface $formFactory, | ||||
|         RequestStack $requestStack | ||||
|         private readonly FormFactoryInterface $formFactory, | ||||
|         private readonly RequestStack $requestStack, | ||||
|     ) { | ||||
|         $this->formFactory = $formFactory; | ||||
|         $this->requestStack = $requestStack; | ||||
|     } | ||||
|  | ||||
|     public function addCheckbox(string $name, array $choices, ?array $default = [], ?array $trans = []): self | ||||
|     public function addSingleCheckbox(string $name, string $label): self | ||||
|     { | ||||
|         $missing = count($choices) - count($trans) - 1; | ||||
|         $this->singleCheckbox[$name] = ['label' => $label]; | ||||
|  | ||||
|         return $this; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @param class-string $class | ||||
|      */ | ||||
|     public function addEntityChoice(string $name, string $class, string $label, array $choices, array $options = []): self | ||||
|     { | ||||
|         $this->entityChoices[$name] = ['label' => $label, 'class' => $class, 'choices' => $choices, 'options' => $options]; | ||||
|  | ||||
|         return $this; | ||||
|     } | ||||
|  | ||||
|     public function getEntityChoices(): array | ||||
|     { | ||||
|         return $this->entityChoices; | ||||
|     } | ||||
|  | ||||
|     public function addCheckbox(string $name, array $choices, ?array $default = [], ?array $trans = [], array $options = []): self | ||||
|     { | ||||
|         if ([] === $trans) { | ||||
|             $trans = $choices; | ||||
|         } | ||||
|  | ||||
|         $this->checkboxes[$name] = [ | ||||
|             'choices' => $choices, 'default' => $default, | ||||
|             'trans' => array_merge( | ||||
|                 $trans, | ||||
|                 0 < $missing ? | ||||
|                        array_fill(0, $missing, null) : [] | ||||
|             ), | ||||
|             'choices' => $choices, | ||||
|             'default' => $default, | ||||
|             'trans' => $trans, | ||||
|             ...$options, | ||||
|         ]; | ||||
|  | ||||
|         return $this; | ||||
| @@ -73,7 +106,7 @@ class FilterOrderHelper | ||||
|     public function buildForm(): FormInterface | ||||
|     { | ||||
|         return $this->formFactory | ||||
|             ->createNamed($this->formName, $this->formType, $this->getDefaultData(), array_merge([ | ||||
|             ->createNamed(self::FORM_NAME, $this->formType, $this->getDefaultData(), array_merge([ | ||||
|                 'helper' => $this, | ||||
|                 'method' => 'GET', | ||||
|                 'csrf_protection' => false, | ||||
| @@ -81,11 +114,36 @@ class FilterOrderHelper | ||||
|             ->handleRequest($this->requestStack->getCurrentRequest()); | ||||
|     } | ||||
|  | ||||
|     public function hasCheckboxData(string $name): bool | ||||
|     { | ||||
|         return array_key_exists($name, $this->checkboxes); | ||||
|     } | ||||
|  | ||||
|     public function getCheckboxData(string $name): array | ||||
|     { | ||||
|         return $this->getFormData()['checkboxes'][$name]; | ||||
|     } | ||||
|  | ||||
|     public function hasSingleCheckboxData(string $name): bool | ||||
|     { | ||||
|         return array_key_exists($name, $this->singleCheckbox); | ||||
|     } | ||||
|  | ||||
|     public function getSingleCheckboxData(string $name): ?bool | ||||
|     { | ||||
|         return $this->getFormData()['single_checkboxes'][$name]; | ||||
|     } | ||||
|  | ||||
|     public function hasEntityChoice(string $name): bool | ||||
|     { | ||||
|         return array_key_exists($name, $this->entityChoices); | ||||
|     } | ||||
|  | ||||
|     public function getEntityChoiceData($name): mixed | ||||
|     { | ||||
|         return $this->getFormData()['entity_choices'][$name]; | ||||
|     } | ||||
|  | ||||
|     public function getCheckboxes(): array | ||||
|     { | ||||
|         return $this->checkboxes; | ||||
| @@ -97,7 +155,20 @@ class FilterOrderHelper | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @return array<'to': DateTimeImmutable, 'from': DateTimeImmutable> | ||||
|      * @return array<string, array{label: string}> | ||||
|      */ | ||||
|     public function getSingleCheckbox(): array | ||||
|     { | ||||
|         return $this->singleCheckbox; | ||||
|     } | ||||
|  | ||||
|     public function hasDateRangeData(string $name): bool | ||||
|     { | ||||
|         return array_key_exists($name, $this->dateRanges); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @return array{to: ?DateTimeImmutable, from: ?DateTimeImmutable} | ||||
|      */ | ||||
|     public function getDateRangeData(string $name): array | ||||
|     { | ||||
| @@ -128,7 +199,12 @@ class FilterOrderHelper | ||||
|  | ||||
|     private function getDefaultData(): array | ||||
|     { | ||||
|         $r = []; | ||||
|         $r = [ | ||||
|             'checkboxes' => [], | ||||
|             'dateRanges' => [], | ||||
|             'single_checkboxes' => [], | ||||
|             'entity_choices' => [] | ||||
|         ]; | ||||
|  | ||||
|         if ($this->hasSearchBox()) { | ||||
|             $r['q'] = ''; | ||||
| @@ -143,6 +219,14 @@ class FilterOrderHelper | ||||
|             $r['dateRanges'][$name]['to'] = $defaults['to']; | ||||
|         } | ||||
|  | ||||
|         foreach ($this->singleCheckbox as $name => $c) { | ||||
|             $r['single_checkboxes'][$name] = false; | ||||
|         } | ||||
|  | ||||
|         foreach ($this->entityChoices as $name => $c) { | ||||
|             $r['entity_choices'][$name] = ($c['options']['multiple'] ?? true) ? [] : null; | ||||
|         } | ||||
|  | ||||
|         return $r; | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -14,6 +14,8 @@ namespace Chill\MainBundle\Templating\Listing; | ||||
| use DateTimeImmutable; | ||||
| use Symfony\Component\Form\FormFactoryInterface; | ||||
| use Symfony\Component\HttpFoundation\RequestStack; | ||||
| use Symfony\Component\PropertyAccess\PropertyAccessorInterface; | ||||
| use Symfony\Contracts\Translation\TranslatorInterface; | ||||
|  | ||||
| class FilterOrderHelperBuilder | ||||
| { | ||||
| @@ -27,14 +29,31 @@ class FilterOrderHelperBuilder | ||||
|  | ||||
|     private ?array $searchBoxFields = null; | ||||
|  | ||||
|     /** | ||||
|      * @var array<string, array{label: string}> | ||||
|      */ | ||||
|     private array $singleCheckboxes = []; | ||||
|  | ||||
|     /** | ||||
|      * @var array<string, array{label: string, class: class-string, choices: array, options: array}> | ||||
|      */ | ||||
|     private array $entityChoices = []; | ||||
|  | ||||
|     public function __construct( | ||||
|         FormFactoryInterface $formFactory, | ||||
|         RequestStack $requestStack | ||||
|         RequestStack $requestStack, | ||||
|     ) { | ||||
|         $this->formFactory = $formFactory; | ||||
|         $this->requestStack = $requestStack; | ||||
|     } | ||||
|  | ||||
|     public function addSingleCheckbox(string $name, string $label): self | ||||
|     { | ||||
|         $this->singleCheckboxes[$name] = ['label' => $label]; | ||||
|  | ||||
|         return $this; | ||||
|     } | ||||
|  | ||||
|     public function addCheckbox(string $name, array $choices, ?array $default = [], ?array $trans = []): self | ||||
|     { | ||||
|         $this->checkboxes[$name] = ['choices' => $choices, 'default' => $default, 'trans' => $trans]; | ||||
| @@ -42,6 +61,16 @@ class FilterOrderHelperBuilder | ||||
|         return $this; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @param class-string $class | ||||
|      */ | ||||
|     public function addEntityChoice(string $name, string $label, string $class, array $choices, ?array $options = []): self | ||||
|     { | ||||
|         $this->entityChoices[$name] = ['label' => $label, 'class' => $class, 'choices' => $choices, 'options' => $options]; | ||||
|  | ||||
|         return $this; | ||||
|     } | ||||
|  | ||||
|     public function addDateRange(string $name, ?string $label = null, ?DateTimeImmutable $from = null, ?DateTimeImmutable $to = null): self | ||||
|     { | ||||
|         $this->dateRanges[$name] = ['from' => $from, 'to' => $to, 'label' => $label]; | ||||
| @@ -60,7 +89,7 @@ class FilterOrderHelperBuilder | ||||
|     { | ||||
|         $helper = new FilterOrderHelper( | ||||
|             $this->formFactory, | ||||
|             $this->requestStack | ||||
|             $this->requestStack, | ||||
|         ); | ||||
|  | ||||
|         $helper->setSearchBox($this->searchBoxFields); | ||||
| @@ -75,6 +104,18 @@ class FilterOrderHelperBuilder | ||||
|             $helper->addCheckbox($name, $choices, $default, $trans); | ||||
|         } | ||||
|  | ||||
|         foreach ( | ||||
|             $this->singleCheckboxes as $name => ['label' => $label] | ||||
|         ) { | ||||
|             $helper->addSingleCheckbox($name, $label); | ||||
|         } | ||||
|  | ||||
|         foreach ( | ||||
|             $this->entityChoices as $name => ['label' => $label, 'class' => $class, 'choices' => $choices, 'options' => $options] | ||||
|         ) { | ||||
|             $helper->addEntityChoice($name, $class, $label, $choices, $options); | ||||
|         } | ||||
|  | ||||
|         foreach ( | ||||
|             $this->dateRanges as $name => [ | ||||
|                 'from' => $from, | ||||
|   | ||||
| @@ -13,6 +13,8 @@ namespace Chill\MainBundle\Templating\Listing; | ||||
|  | ||||
| use Symfony\Component\Form\FormFactoryInterface; | ||||
| use Symfony\Component\HttpFoundation\RequestStack; | ||||
| use Symfony\Component\PropertyAccess\PropertyAccessorInterface; | ||||
| use Symfony\Contracts\Translation\TranslatorInterface; | ||||
|  | ||||
| class FilterOrderHelperFactory implements FilterOrderHelperFactoryInterface | ||||
| { | ||||
| @@ -22,7 +24,7 @@ class FilterOrderHelperFactory implements FilterOrderHelperFactoryInterface | ||||
|  | ||||
|     public function __construct( | ||||
|         FormFactoryInterface $formFactory, | ||||
|         RequestStack $requestStack | ||||
|         RequestStack $requestStack, | ||||
|     ) { | ||||
|         $this->formFactory = $formFactory; | ||||
|         $this->requestStack = $requestStack; | ||||
|   | ||||
| @@ -0,0 +1,21 @@ | ||||
| <?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\Listing; | ||||
|  | ||||
| enum FilterOrderPositionEnum: string | ||||
| { | ||||
|     case SearchBox = 'search_box'; | ||||
|     case Checkboxes = 'checkboxes'; | ||||
|     case DateRange = 'date_range'; | ||||
|     case EntityChoice = 'entity_choice'; | ||||
|     case SingleCheckbox = 'single_checkbox'; | ||||
| } | ||||
| @@ -11,13 +11,24 @@ declare(strict_types=1); | ||||
|  | ||||
| namespace Chill\MainBundle\Templating\Listing; | ||||
|  | ||||
| use Chill\MainBundle\Pagination\PaginatorFactory; | ||||
| use Symfony\Component\HttpFoundation\RequestStack; | ||||
| use Twig\Environment; | ||||
| use Twig\Error\LoaderError; | ||||
| use Twig\Error\RuntimeError; | ||||
| use Twig\Error\SyntaxError; | ||||
| use Twig\Extension\AbstractExtension; | ||||
| use Twig\TwigFilter; | ||||
|  | ||||
| class Templating extends AbstractExtension | ||||
| { | ||||
|     public function getFilters() | ||||
|     public function __construct( | ||||
|         private readonly RequestStack $requestStack, | ||||
|         private readonly FilterOrderGetActiveFilterHelper $filterOrderGetActiveFilterHelper, | ||||
|     ) { | ||||
|     } | ||||
|  | ||||
|     public function getFilters(): array | ||||
|     { | ||||
|         return [ | ||||
|             new TwigFilter('chill_render_filter_order_helper', [$this, 'renderFilterOrderHelper'], [ | ||||
| @@ -26,16 +37,42 @@ class Templating extends AbstractExtension | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @throws SyntaxError | ||||
|      * @throws RuntimeError | ||||
|      * @throws LoaderError | ||||
|      */ | ||||
|     public function renderFilterOrderHelper( | ||||
|         Environment $environment, | ||||
|         FilterOrderHelper $helper, | ||||
|         ?string $template = '@ChillMain/FilterOrder/base.html.twig', | ||||
|         ?array $options = [] | ||||
|     ) { | ||||
|     ): string { | ||||
|         $otherParameters = []; | ||||
|  | ||||
|         foreach ($this->requestStack->getCurrentRequest()->query->getIterator() as $key => $value) { | ||||
|             switch ($key) { | ||||
|                 case FilterOrderHelper::FORM_NAME: | ||||
|                     break; | ||||
|  | ||||
|                 case PaginatorFactory::DEFAULT_CURRENT_PAGE_KEY: | ||||
|                     // when filtering, go back to page 1 | ||||
|                     $otherParameters[PaginatorFactory::DEFAULT_CURRENT_PAGE_KEY] = 1; | ||||
|  | ||||
|                     break; | ||||
|                 default: | ||||
|                     $otherParameters[$key] = $value; | ||||
|  | ||||
|                     break; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return $environment->render($template, [ | ||||
|             'helper' => $helper, | ||||
|             'active' => $this->filterOrderGetActiveFilterHelper->getActiveFilters($helper), | ||||
|             'form' => $helper->buildForm()->createView(), | ||||
|             'options' => $options, | ||||
|             'otherParameters' => $otherParameters, | ||||
|         ]); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -54,3 +54,12 @@ duration: | ||||
|             few {# minutes} | ||||
|             other {# minutes} | ||||
|         } | ||||
|  | ||||
| filter_order: | ||||
|     by_date: | ||||
|         From: Depuis le {from_date, date, long} | ||||
|         To: Jusqu'au {to_date, date, long} | ||||
|     By: Filtrer par | ||||
|     Search: Chercher dans la liste | ||||
|     By date: Filtrer par date | ||||
|     search_box: Filtrer par contenu | ||||
|   | ||||
| @@ -29,6 +29,7 @@ use Chill\PersonBundle\Entity\Household\PersonHouseholdAddress; | ||||
| use Chill\PersonBundle\Entity\Person\PersonCenterHistory; | ||||
| use Chill\PersonBundle\Entity\SocialWork\SocialIssue; | ||||
| use Chill\PersonBundle\Export\Declarations; | ||||
| use Chill\PersonBundle\Export\Helper\ListAccompanyingPeriodHelper; | ||||
| use Chill\PersonBundle\Repository\PersonRepository; | ||||
| use Chill\PersonBundle\Repository\SocialWork\SocialIssueRepository; | ||||
| use Chill\PersonBundle\Security\Authorization\PersonVoter; | ||||
| @@ -45,95 +46,13 @@ use Symfony\Component\Form\FormBuilderInterface; | ||||
| use Symfony\Contracts\Translation\TranslatorInterface; | ||||
| use function strlen; | ||||
|  | ||||
| class ListAccompanyingPeriod implements ListInterface, GroupedExportInterface | ||||
| final readonly class ListAccompanyingPeriod implements ListInterface, GroupedExportInterface | ||||
| { | ||||
|     private const FIELDS = [ | ||||
|         'id', | ||||
|         'step', | ||||
|         'stepSince', | ||||
|         'openingDate', | ||||
|         'closingDate', | ||||
|         'referrer', | ||||
|         'referrerSince', | ||||
|         'administrativeLocation', | ||||
|         'locationIsPerson', | ||||
|         'locationIsTemp', | ||||
|         'locationPersonName', | ||||
|         'locationPersonId', | ||||
|         'origin', | ||||
|         'closingMotive', | ||||
|         'confidential', | ||||
|         'emergency', | ||||
|         'intensity', | ||||
|         'job', | ||||
|         'isRequestorPerson', | ||||
|         'isRequestorThirdParty', | ||||
|         'requestorPerson', | ||||
|         'requestorPersonId', | ||||
|         'requestorThirdParty', | ||||
|         'requestorThirdPartyId', | ||||
|         'scopes', | ||||
|         'socialIssues', | ||||
|         'createdAt', | ||||
|         'createdBy', | ||||
|         'updatedAt', | ||||
|         'updatedBy', | ||||
|     ]; | ||||
|  | ||||
|     private ExportAddressHelper $addressHelper; | ||||
|  | ||||
|     private DateTimeHelper $dateTimeHelper; | ||||
|  | ||||
|     private EntityManagerInterface $entityManager; | ||||
|  | ||||
|     private PersonRenderInterface $personRender; | ||||
|  | ||||
|     private PersonRepository $personRepository; | ||||
|  | ||||
|     private RollingDateConverterInterface $rollingDateConverter; | ||||
|  | ||||
|     private SocialIssueRender $socialIssueRender; | ||||
|  | ||||
|     private SocialIssueRepository $socialIssueRepository; | ||||
|  | ||||
|     private ThirdPartyRender $thirdPartyRender; | ||||
|  | ||||
|     private ThirdPartyRepository $thirdPartyRepository; | ||||
|  | ||||
|     private TranslatableStringHelperInterface $translatableStringHelper; | ||||
|  | ||||
|     private TranslatorInterface $translator; | ||||
|  | ||||
|     private UserHelper $userHelper; | ||||
|  | ||||
|     public function __construct( | ||||
|         ExportAddressHelper $addressHelper, | ||||
|         DateTimeHelper $dateTimeHelper, | ||||
|         EntityManagerInterface $entityManager, | ||||
|         PersonRenderInterface $personRender, | ||||
|         PersonRepository $personRepository, | ||||
|         ThirdPartyRepository $thirdPartyRepository, | ||||
|         ThirdPartyRender $thirdPartyRender, | ||||
|         SocialIssueRepository $socialIssueRepository, | ||||
|         SocialIssueRender $socialIssueRender, | ||||
|         TranslatableStringHelperInterface $translatableStringHelper, | ||||
|         TranslatorInterface $translator, | ||||
|         RollingDateConverterInterface $rollingDateConverter, | ||||
|         UserHelper $userHelper | ||||
|         private EntityManagerInterface $entityManager, | ||||
|         private RollingDateConverterInterface $rollingDateConverter, | ||||
|         private ListAccompanyingPeriodHelper $listAccompanyingPeriodHelper, | ||||
|     ) { | ||||
|         $this->addressHelper = $addressHelper; | ||||
|         $this->dateTimeHelper = $dateTimeHelper; | ||||
|         $this->entityManager = $entityManager; | ||||
|         $this->personRender = $personRender; | ||||
|         $this->personRepository = $personRepository; | ||||
|         $this->socialIssueRender = $socialIssueRender; | ||||
|         $this->socialIssueRepository = $socialIssueRepository; | ||||
|         $this->thirdPartyRender = $thirdPartyRender; | ||||
|         $this->thirdPartyRepository = $thirdPartyRepository; | ||||
|         $this->translatableStringHelper = $translatableStringHelper; | ||||
|         $this->translator = $translator; | ||||
|         $this->rollingDateConverter = $rollingDateConverter; | ||||
|         $this->userHelper = $userHelper; | ||||
|     } | ||||
|  | ||||
|     public function buildForm(FormBuilderInterface $builder) | ||||
| @@ -169,141 +88,12 @@ class ListAccompanyingPeriod implements ListInterface, GroupedExportInterface | ||||
|  | ||||
|     public function getLabels($key, array $values, $data) | ||||
|     { | ||||
|         if (substr($key, 0, strlen('address_fields')) === 'address_fields') { | ||||
|             return $this->addressHelper->getLabel($key, $values, $data, 'address_fields'); | ||||
|         } | ||||
|  | ||||
|         switch ($key) { | ||||
|             case 'stepSince': | ||||
|             case 'openingDate': | ||||
|             case 'closingDate': | ||||
|             case 'referrerSince': | ||||
|             case 'createdAt': | ||||
|             case 'updatedAt': | ||||
|                 return $this->dateTimeHelper->getLabel('export.list.acp.' . $key); | ||||
|  | ||||
|             case 'origin': | ||||
|             case 'closingMotive': | ||||
|             case 'job': | ||||
|                 return function ($value) use ($key) { | ||||
|                     if ('_header' === $value) { | ||||
|                         return 'export.list.acp.' . $key; | ||||
|                     } | ||||
|  | ||||
|                     if (null === $value) { | ||||
|                         return ''; | ||||
|                     } | ||||
|  | ||||
|                     return $this->translatableStringHelper->localize(json_decode($value, true, 512, JSON_THROW_ON_ERROR)); | ||||
|                 }; | ||||
|  | ||||
|             case 'locationPersonName': | ||||
|             case 'requestorPerson': | ||||
|                 return function ($value) use ($key) { | ||||
|                     if ('_header' === $value) { | ||||
|                         return 'export.list.acp.' . $key; | ||||
|                     } | ||||
|  | ||||
|                     if (null === $value || null === $person = $this->personRepository->find($value)) { | ||||
|                         return ''; | ||||
|                     } | ||||
|  | ||||
|                     return $this->personRender->renderString($person, []); | ||||
|                 }; | ||||
|  | ||||
|             case 'requestorThirdParty': | ||||
|                 return function ($value) use ($key) { | ||||
|                     if ('_header' === $value) { | ||||
|                         return 'export.list.acp.' . $key; | ||||
|                     } | ||||
|  | ||||
|                     if (null === $value || null === $thirdparty = $this->thirdPartyRepository->find($value)) { | ||||
|                         return ''; | ||||
|                     } | ||||
|  | ||||
|                     return $this->thirdPartyRender->renderString($thirdparty, []); | ||||
|                 }; | ||||
|  | ||||
|             case 'scopes': | ||||
|                 return function ($value) use ($key) { | ||||
|                     if ('_header' === $value) { | ||||
|                         return 'export.list.acp.' . $key; | ||||
|                     } | ||||
|  | ||||
|                     if (null === $value) { | ||||
|                         return ''; | ||||
|                     } | ||||
|  | ||||
|                     return implode( | ||||
|                         '|', | ||||
|                         array_map( | ||||
|                             fn ($s) => $this->translatableStringHelper->localize($s), | ||||
|                             json_decode($value, true, 512, JSON_THROW_ON_ERROR) | ||||
|                         ) | ||||
|                     ); | ||||
|                 }; | ||||
|  | ||||
|             case 'socialIssues': | ||||
|                 return function ($value) use ($key) { | ||||
|                     if ('_header' === $value) { | ||||
|                         return 'export.list.acp.' . $key; | ||||
|                     } | ||||
|  | ||||
|                     if (null === $value) { | ||||
|                         return ''; | ||||
|                     } | ||||
|  | ||||
|                     return implode( | ||||
|                         '|', | ||||
|                         array_map( | ||||
|                             fn ($s) => $this->socialIssueRender->renderString($this->socialIssueRepository->find($s), []), | ||||
|                             json_decode($value, true, 512, JSON_THROW_ON_ERROR) | ||||
|                         ) | ||||
|                     ); | ||||
|                 }; | ||||
|  | ||||
|             case 'step': | ||||
|                 return fn ($value) => match ($value) { | ||||
|                     '_header' => 'export.list.acp.step', | ||||
|                     null => '', | ||||
|                     AccompanyingPeriod::STEP_DRAFT => $this->translator->trans('course.draft'), | ||||
|                     AccompanyingPeriod::STEP_CONFIRMED => $this->translator->trans('course.confirmed'), | ||||
|                     AccompanyingPeriod::STEP_CLOSED => $this->translator->trans('course.closed'), | ||||
|                     AccompanyingPeriod::STEP_CONFIRMED_INACTIVE_SHORT => $this->translator->trans('course.inactive_short'), | ||||
|                     AccompanyingPeriod::STEP_CONFIRMED_INACTIVE_LONG => $this->translator->trans('course.inactive_long'), | ||||
|                     default => $value, | ||||
|                 }; | ||||
|  | ||||
|             case 'intensity': | ||||
|                 return fn ($value) => match ($value) { | ||||
|                     '_header' => 'export.list.acp.intensity', | ||||
|                     null => '', | ||||
|                     AccompanyingPeriod::INTENSITY_OCCASIONAL => $this->translator->trans('occasional'), | ||||
|                     AccompanyingPeriod::INTENSITY_REGULAR => $this->translator->trans('regular'), | ||||
|                     default => $value, | ||||
|                 }; | ||||
|  | ||||
|             default: | ||||
|                 return static function ($value) use ($key) { | ||||
|                     if ('_header' === $value) { | ||||
|                         return 'export.list.acp.' . $key; | ||||
|                     } | ||||
|  | ||||
|                     if (null === $value) { | ||||
|                         return ''; | ||||
|                     } | ||||
|  | ||||
|                     return $value; | ||||
|                 }; | ||||
|         } | ||||
|         return $this->listAccompanyingPeriodHelper->getLabels($key, $values, $data); | ||||
|     } | ||||
|  | ||||
|     public function getQueryKeys($data) | ||||
|     { | ||||
|         return array_merge( | ||||
|             self::FIELDS, | ||||
|             $this->addressHelper->getKeys(ExportAddressHelper::F_ALL, 'address_fields') | ||||
|         ); | ||||
|         return $this->listAccompanyingPeriodHelper->getQueryKeys($data); | ||||
|     } | ||||
|  | ||||
|     public function getResult($query, $data) | ||||
| @@ -341,7 +131,12 @@ class ListAccompanyingPeriod implements ListInterface, GroupedExportInterface | ||||
|             ->setParameter('list_acp_step', AccompanyingPeriod::STEP_DRAFT) | ||||
|             ->setParameter('authorized_centers', $centers); | ||||
|  | ||||
|         $this->addSelectClauses($qb, $this->rollingDateConverter->convert($data['calc_date'])); | ||||
|         $this->listAccompanyingPeriodHelper->addSelectClauses($qb, $this->rollingDateConverter->convert($data['calc_date'])); | ||||
|  | ||||
|         $qb | ||||
|             ->addOrderBy('acp.openingDate') | ||||
|             ->addOrderBy('acp.closingDate') | ||||
|             ->addOrderBy('acp.id'); | ||||
|  | ||||
|         return $qb; | ||||
|     } | ||||
| @@ -357,91 +152,4 @@ class ListAccompanyingPeriod implements ListInterface, GroupedExportInterface | ||||
|             Declarations::ACP_TYPE, | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     private function addSelectClauses(QueryBuilder $qb, DateTimeImmutable $calcDate): void | ||||
|     { | ||||
|         // add the regular fields | ||||
|         foreach (['id', 'openingDate', 'closingDate', 'confidential', 'emergency', 'intensity', 'createdAt', 'updatedAt'] as $field) { | ||||
|             $qb->addSelect(sprintf('acp.%s AS %s', $field, $field)); | ||||
|         } | ||||
|  | ||||
|         // add the field which are simple association | ||||
|         foreach (['origin' => 'label', 'closingMotive' => 'name', 'job' => 'label', 'createdBy' => 'label', 'updatedBy' => 'label', 'administrativeLocation' => 'name'] as $entity => $field) { | ||||
|             $qb | ||||
|                 ->leftJoin(sprintf('acp.%s', $entity), "{$entity}_t") | ||||
|                 ->addSelect(sprintf('%s_t.%s AS %s', $entity, $field, $entity)); | ||||
|         } | ||||
|  | ||||
|         // step at date | ||||
|         $qb | ||||
|             ->addSelect('stepHistory.step AS step') | ||||
|             ->addSelect('stepHistory.startDate AS stepSince') | ||||
|             ->leftJoin('acp.stepHistories', 'stepHistory') | ||||
|             ->andWhere( | ||||
|                 $qb->expr()->andX( | ||||
|                     $qb->expr()->lte('stepHistory.startDate', ':calcDate'), | ||||
|                     $qb->expr()->orX($qb->expr()->isNull('stepHistory.endDate'), $qb->expr()->gt('stepHistory.endDate', ':calcDate')) | ||||
|                 ) | ||||
|             ); | ||||
|  | ||||
|         // referree at date | ||||
|         $qb | ||||
|             ->addSelect('referrer_t.label AS referrer') | ||||
|             ->addSelect('userHistory.startDate AS referrerSince') | ||||
|             ->leftJoin('acp.userHistories', 'userHistory') | ||||
|             ->leftJoin('userHistory.user', 'referrer_t') | ||||
|             ->andWhere( | ||||
|                 $qb->expr()->orX( | ||||
|                     $qb->expr()->isNull('userHistory'), | ||||
|                     $qb->expr()->andX( | ||||
|                         $qb->expr()->lte('userHistory.startDate', ':calcDate'), | ||||
|                         $qb->expr()->orX($qb->expr()->isNull('userHistory.endDate'), $qb->expr()->gt('userHistory.endDate', ':calcDate')) | ||||
|                     ) | ||||
|                 ) | ||||
|             ); | ||||
|  | ||||
|         // location of the acp | ||||
|         $qb | ||||
|             ->addSelect('CASE WHEN locationHistory.personLocation IS NOT NULL THEN 1 ELSE 0 END AS locationIsPerson') | ||||
|             ->addSelect('CASE WHEN locationHistory.personLocation IS NOT NULL THEN 0 ELSE 1 END AS locationIsTemp') | ||||
|             ->addSelect('IDENTITY(locationHistory.personLocation) AS locationPersonName') | ||||
|             ->addSelect('IDENTITY(locationHistory.personLocation) AS locationPersonId') | ||||
|             ->leftJoin('acp.locationHistories', 'locationHistory') | ||||
|             ->andWhere( | ||||
|                 $qb->expr()->orX( | ||||
|                     $qb->expr()->isNull('locationHistory'), | ||||
|                     $qb->expr()->andX( | ||||
|                         $qb->expr()->lte('locationHistory.startDate', ':calcDate'), | ||||
|                         $qb->expr()->orX($qb->expr()->isNull('locationHistory.endDate'), $qb->expr()->gt('locationHistory.endDate', ':calcDate')) | ||||
|                     ) | ||||
|                 ) | ||||
|             ) | ||||
|             ->leftJoin( | ||||
|                 PersonHouseholdAddress::class, | ||||
|                 'personAddress', | ||||
|                 Join::WITH, | ||||
|                 'locationHistory.personLocation = personAddress.person AND (personAddress.validFrom <= :calcDate AND (personAddress.validTo IS NULL OR personAddress.validTo > :calcDate))' | ||||
|             ) | ||||
|             ->leftJoin(Address::class, 'acp_address', Join::WITH, 'COALESCE(IDENTITY(locationHistory.addressLocation), IDENTITY(personAddress.address)) = acp_address.id'); | ||||
|  | ||||
|         $this->addressHelper->addSelectClauses(ExportAddressHelper::F_ALL, $qb, 'acp_address', 'address_fields'); | ||||
|  | ||||
|         // requestor | ||||
|         $qb | ||||
|             ->addSelect('CASE WHEN acp.requestorPerson IS NULL THEN 1 ELSE 0 END AS isRequestorPerson') | ||||
|             ->addSelect('CASE WHEN acp.requestorPerson IS NULL THEN 0 ELSE 1 END AS isRequestorThirdParty') | ||||
|             ->addSelect('IDENTITY(acp.requestorPerson) AS requestorPersonId') | ||||
|             ->addSelect('IDENTITY(acp.requestorThirdParty) AS requestorThirdPartyId') | ||||
|             ->addSelect('IDENTITY(acp.requestorPerson) AS requestorPerson') | ||||
|             ->addSelect('IDENTITY(acp.requestorThirdParty) AS requestorThirdParty'); | ||||
|  | ||||
|         $qb | ||||
|             // scopes | ||||
|             ->addSelect('(SELECT AGGREGATE(scope.name) FROM ' . Scope::class . ' scope WHERE scope MEMBER OF acp.scopes) AS scopes') | ||||
|             // social issues | ||||
|             ->addSelect('(SELECT AGGREGATE(socialIssue.id) FROM ' . SocialIssue::class . ' socialIssue WHERE socialIssue MEMBER OF acp.socialIssues) AS socialIssues'); | ||||
|  | ||||
|         // add parameter | ||||
|         $qb->setParameter('calcDate', $calcDate); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -35,7 +35,12 @@ use function count; | ||||
| use function in_array; | ||||
| use function strlen; | ||||
| 
 | ||||
| class ListPersonWithAccompanyingPeriod implements ExportElementValidatedInterface, ListInterface, GroupedExportInterface | ||||
| /** | ||||
|  * List the persons, having an accompanying period. | ||||
|  * | ||||
|  * Details of the accompanying period are not included | ||||
|  */ | ||||
| class ListPersonHavingAccompanyingPeriod implements ExportElementValidatedInterface, ListInterface, GroupedExportInterface | ||||
| { | ||||
|     private ExportAddressHelper $addressHelper; | ||||
| 
 | ||||
| @@ -185,6 +190,11 @@ class ListPersonWithAccompanyingPeriod implements ExportElementValidatedInterfac | ||||
| 
 | ||||
|         $this->listPersonHelper->addSelect($qb, $fields, $data['address_date']); | ||||
| 
 | ||||
|         $qb | ||||
|             ->addOrderBy('person.lastName') | ||||
|             ->addOrderBy('person.firstName') | ||||
|             ->addOrderBy('person.id'); | ||||
| 
 | ||||
|         return $qb; | ||||
|     } | ||||
| 
 | ||||
| @@ -0,0 +1,155 @@ | ||||
| <?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\PersonBundle\Export\Export; | ||||
|  | ||||
| use Chill\MainBundle\Export\ExportElementValidatedInterface; | ||||
| use Chill\MainBundle\Export\FormatterInterface; | ||||
| use Chill\MainBundle\Export\GroupedExportInterface; | ||||
| use Chill\MainBundle\Export\Helper\ExportAddressHelper; | ||||
| use Chill\MainBundle\Export\ListInterface; | ||||
| use Chill\MainBundle\Form\Type\ChillDateType; | ||||
| use Chill\MainBundle\Form\Type\PickRollingDateType; | ||||
| use Chill\MainBundle\Service\RollingDate\RollingDate; | ||||
| use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface; | ||||
| use Chill\PersonBundle\Entity\AccompanyingPeriod; | ||||
| use Chill\PersonBundle\Entity\Person; | ||||
| use Chill\PersonBundle\Entity\Person\PersonCenterHistory; | ||||
| use Chill\PersonBundle\Export\Declarations; | ||||
| use Chill\PersonBundle\Export\Helper\ListAccompanyingPeriodHelper; | ||||
| use Chill\PersonBundle\Export\Helper\ListPersonHelper; | ||||
| use Chill\PersonBundle\Security\Authorization\PersonVoter; | ||||
| use DateTimeImmutable; | ||||
| use Doctrine\ORM\AbstractQuery; | ||||
| use Doctrine\ORM\EntityManagerInterface; | ||||
| use Symfony\Component\Form\Extension\Core\Type\ChoiceType; | ||||
| use Symfony\Component\Form\FormBuilderInterface; | ||||
| use Symfony\Component\Validator\Constraints\Callback; | ||||
| use Symfony\Component\Validator\Context\ExecutionContextInterface; | ||||
| use function array_key_exists; | ||||
| use function count; | ||||
| use function in_array; | ||||
| use function strlen; | ||||
|  | ||||
| /** | ||||
|  * List the persons having an accompanying period, with the accompanying period details | ||||
|  * | ||||
|  */ | ||||
| final readonly class ListPersonWithAccompanyingPeriodDetails implements ListInterface, GroupedExportInterface | ||||
| { | ||||
|     public function __construct( | ||||
|         private ListPersonHelper $listPersonHelper, | ||||
|         private ListAccompanyingPeriodHelper $listAccompanyingPeriodHelper, | ||||
|         private EntityManagerInterface $entityManager, | ||||
|         private RollingDateConverterInterface $rollingDateConverter, | ||||
|     ) { | ||||
|     } | ||||
|  | ||||
|     public function buildForm(FormBuilderInterface $builder) | ||||
|     { | ||||
|         $builder->add('address_date', PickRollingDateType::class, [ | ||||
|             'label' => 'Data valid at this date', | ||||
|             'help' => 'Data regarding center, addresses, and so on will be computed at this date', | ||||
|         ]); | ||||
|     } | ||||
|     public function getFormDefaultData(): array | ||||
|     { | ||||
|         return ['address_date' => new RollingDate(RollingDate::T_TODAY)]; | ||||
|     } | ||||
|  | ||||
|     public function getAllowedFormattersTypes() | ||||
|     { | ||||
|         return [FormatterInterface::TYPE_LIST]; | ||||
|     } | ||||
|  | ||||
|     public function getDescription() | ||||
|     { | ||||
|         return 'export.list.person_with_acp.Create a list of people having an accompaying periods with details of period, according to various filters.'; | ||||
|     } | ||||
|  | ||||
|     public function getGroup(): string | ||||
|     { | ||||
|         return 'Exports of persons'; | ||||
|     } | ||||
|  | ||||
|     public function getLabels($key, array $values, $data) | ||||
|     { | ||||
|         if (in_array($key, $this->listPersonHelper->getAllKeys(), true)) { | ||||
|             return $this->listPersonHelper->getLabels($key, $values, $data); | ||||
|         } | ||||
|  | ||||
|         return $this->listAccompanyingPeriodHelper->getLabels($key, $values, $data); | ||||
|     } | ||||
|  | ||||
|     public function getQueryKeys($data) | ||||
|     { | ||||
|         return array_merge( | ||||
|             $this->listPersonHelper->getAllKeys(), | ||||
|             $this->listAccompanyingPeriodHelper->getQueryKeys($data), | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     public function getResult($query, $data) | ||||
|     { | ||||
|         return $query->getQuery()->getResult(AbstractQuery::HYDRATE_SCALAR); | ||||
|     } | ||||
|  | ||||
|     public function getTitle() | ||||
|     { | ||||
|         return 'export.list.person_with_acp.List peoples having an accompanying period with period details'; | ||||
|     } | ||||
|  | ||||
|     public function getType() | ||||
|     { | ||||
|         return Declarations::PERSON_TYPE; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * param array{fields: string[], address_date: DateTimeImmutable} $data. | ||||
|      */ | ||||
|     public function initiateQuery(array $requiredModifiers, array $acl, array $data = []) | ||||
|     { | ||||
|         $centers = array_map(static fn ($el) => $el['center'], $acl); | ||||
|  | ||||
|         $qb = $this->entityManager->createQueryBuilder(); | ||||
|  | ||||
|         $qb->from(Person::class, 'person') | ||||
|             ->join('person.accompanyingPeriodParticipations', 'acppart') | ||||
|             ->join('acppart.accompanyingPeriod', 'acp') | ||||
|             ->andWhere($qb->expr()->neq('acp.step', "'" . AccompanyingPeriod::STEP_DRAFT . "'")) | ||||
|             ->andWhere( | ||||
|                 $qb->expr()->exists( | ||||
|                     'SELECT 1 FROM ' . PersonCenterHistory::class . ' pch WHERE pch.person = person.id AND pch.center IN (:authorized_centers)' | ||||
|                 ) | ||||
|             )->setParameter('authorized_centers', $centers); | ||||
|  | ||||
|         $this->listPersonHelper->addSelect($qb, ListPersonHelper::FIELDS, $this->rollingDateConverter->convert($data['address_date'])); | ||||
|         $this->listAccompanyingPeriodHelper->addSelectClauses($qb, $this->rollingDateConverter->convert($data['address_date'])); | ||||
|  | ||||
|         $qb | ||||
|             ->addOrderBy('person.lastName') | ||||
|             ->addOrderBy('person.firstName') | ||||
|             ->addOrderBy('person.id') | ||||
|             ->addOrderBy('acp.id'); | ||||
|  | ||||
|         return $qb; | ||||
|     } | ||||
|  | ||||
|     public function requiredRole(): string | ||||
|     { | ||||
|         return PersonVoter::LISTS; | ||||
|     } | ||||
|  | ||||
|     public function supportsModifiers() | ||||
|     { | ||||
|         return [Declarations::PERSON_TYPE, Declarations::PERSON_IMPLIED_IN, Declarations::ACP_TYPE]; | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,317 @@ | ||||
| <?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\PersonBundle\Export\Helper; | ||||
|  | ||||
| use Chill\MainBundle\Entity\Address; | ||||
| use Chill\MainBundle\Entity\Scope; | ||||
| use Chill\MainBundle\Export\Helper\DateTimeHelper; | ||||
| use Chill\MainBundle\Export\Helper\ExportAddressHelper; | ||||
| use Chill\MainBundle\Templating\TranslatableStringHelperInterface; | ||||
| use Chill\PersonBundle\Entity\AccompanyingPeriod; | ||||
| use Chill\PersonBundle\Entity\Household\PersonHouseholdAddress; | ||||
| use Chill\PersonBundle\Entity\SocialWork\SocialIssue; | ||||
| use Chill\PersonBundle\Repository\PersonRepository; | ||||
| use Chill\PersonBundle\Repository\SocialWork\SocialIssueRepository; | ||||
| use Chill\PersonBundle\Templating\Entity\PersonRenderInterface; | ||||
| use Chill\PersonBundle\Templating\Entity\SocialIssueRender; | ||||
| use Chill\ThirdPartyBundle\Repository\ThirdPartyRepository; | ||||
| use Chill\ThirdPartyBundle\Templating\Entity\ThirdPartyRender; | ||||
| use Doctrine\ORM\Query\Expr\Join; | ||||
| use Doctrine\ORM\QueryBuilder; | ||||
| use Symfony\Contracts\Translation\TranslatorInterface; | ||||
|  | ||||
| final readonly class ListAccompanyingPeriodHelper | ||||
| { | ||||
|     public const FIELDS = [ | ||||
|         'acpId', | ||||
|         'step', | ||||
|         'stepSince', | ||||
|         'openingDate', | ||||
|         'closingDate', | ||||
|         'referrer', | ||||
|         'referrerSince', | ||||
|         'administrativeLocation', | ||||
|         'locationIsPerson', | ||||
|         'locationIsTemp', | ||||
|         'locationPersonName', | ||||
|         'locationPersonId', | ||||
|         'origin', | ||||
|         'closingMotive', | ||||
|         'confidential', | ||||
|         'emergency', | ||||
|         'intensity', | ||||
|         'job', | ||||
|         'isRequestorPerson', | ||||
|         'isRequestorThirdParty', | ||||
|         'requestorPerson', | ||||
|         'requestorPersonId', | ||||
|         'requestorThirdParty', | ||||
|         'requestorThirdPartyId', | ||||
|         'scopes', | ||||
|         'socialIssues', | ||||
|         'acpCreatedAt', | ||||
|         'acpCreatedBy', | ||||
|         'acpUpdatedAt', | ||||
|         'acpUpdatedBy', | ||||
|     ]; | ||||
|  | ||||
|     public function __construct( | ||||
|         private ExportAddressHelper $addressHelper, | ||||
|         private DateTimeHelper $dateTimeHelper, | ||||
|         private PersonRenderInterface $personRender, | ||||
|         private PersonRepository $personRepository, | ||||
|         private ThirdPartyRepository $thirdPartyRepository, | ||||
|         private ThirdPartyRender $thirdPartyRender, | ||||
|         private SocialIssueRepository $socialIssueRepository, | ||||
|         private SocialIssueRender $socialIssueRender, | ||||
|         private TranslatableStringHelperInterface $translatableStringHelper, | ||||
|         private TranslatorInterface $translator, | ||||
|     ) { | ||||
|     } | ||||
|  | ||||
|     public function getQueryKeys($data) | ||||
|     { | ||||
|         return array_merge( | ||||
|             ListAccompanyingPeriodHelper::FIELDS, | ||||
|             $this->addressHelper->getKeys(ExportAddressHelper::F_ALL, 'acp_address_fields') | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     public function getLabels($key, array $values, $data) | ||||
|     { | ||||
|         if (str_starts_with($key, 'acp_address_fields')) { | ||||
|             return $this->addressHelper->getLabel($key, $values, $data, 'acp_address_fields'); | ||||
|         } | ||||
|  | ||||
|         switch ($key) { | ||||
|             case 'stepSince': | ||||
|             case 'openingDate': | ||||
|             case 'closingDate': | ||||
|             case 'referrerSince': | ||||
|             case 'acpCreatedAt': | ||||
|             case 'acpUpdatedAt': | ||||
|                 return $this->dateTimeHelper->getLabel('export.list.acp.' . $key); | ||||
|  | ||||
|             case 'origin': | ||||
|             case 'closingMotive': | ||||
|             case 'job': | ||||
|                 return function ($value) use ($key) { | ||||
|                     if ('_header' === $value) { | ||||
|                         return 'export.list.acp.' . $key; | ||||
|                     } | ||||
|  | ||||
|                     if (null === $value) { | ||||
|                         return ''; | ||||
|                     } | ||||
|  | ||||
|                     return $this->translatableStringHelper->localize(json_decode($value, true, 512, JSON_THROW_ON_ERROR)); | ||||
|                 }; | ||||
|  | ||||
|             case 'locationPersonName': | ||||
|             case 'requestorPerson': | ||||
|                 return function ($value) use ($key) { | ||||
|                     if ('_header' === $value) { | ||||
|                         return 'export.list.acp.' . $key; | ||||
|                     } | ||||
|  | ||||
|                     if (null === $value || null === $person = $this->personRepository->find($value)) { | ||||
|                         return ''; | ||||
|                     } | ||||
|  | ||||
|                     return $this->personRender->renderString($person, []); | ||||
|                 }; | ||||
|  | ||||
|             case 'requestorThirdParty': | ||||
|                 return function ($value) use ($key) { | ||||
|                     if ('_header' === $value) { | ||||
|                         return 'export.list.acp.' . $key; | ||||
|                     } | ||||
|  | ||||
|                     if (null === $value || null === $thirdparty = $this->thirdPartyRepository->find($value)) { | ||||
|                         return ''; | ||||
|                     } | ||||
|  | ||||
|                     return $this->thirdPartyRender->renderString($thirdparty, []); | ||||
|                 }; | ||||
|  | ||||
|             case 'scopes': | ||||
|                 return function ($value) use ($key) { | ||||
|                     if ('_header' === $value) { | ||||
|                         return 'export.list.acp.' . $key; | ||||
|                     } | ||||
|  | ||||
|                     if (null === $value) { | ||||
|                         return ''; | ||||
|                     } | ||||
|  | ||||
|                     return implode( | ||||
|                         '|', | ||||
|                         array_map( | ||||
|                             fn ($s) => $this->translatableStringHelper->localize($s), | ||||
|                             json_decode($value, true, 512, JSON_THROW_ON_ERROR) | ||||
|                         ) | ||||
|                     ); | ||||
|                 }; | ||||
|  | ||||
|             case 'socialIssues': | ||||
|                 return function ($value) use ($key) { | ||||
|                     if ('_header' === $value) { | ||||
|                         return 'export.list.acp.' . $key; | ||||
|                     } | ||||
|  | ||||
|                     if (null === $value) { | ||||
|                         return ''; | ||||
|                     } | ||||
|  | ||||
|                     return implode( | ||||
|                         '|', | ||||
|                         array_map( | ||||
|                             fn ($s) => $this->socialIssueRender->renderString($this->socialIssueRepository->find($s), []), | ||||
|                             json_decode($value, true, 512, JSON_THROW_ON_ERROR) | ||||
|                         ) | ||||
|                     ); | ||||
|                 }; | ||||
|  | ||||
|             case 'step': | ||||
|                 return fn ($value) => match ($value) { | ||||
|                     '_header' => 'export.list.acp.step', | ||||
|                     null => '', | ||||
|                     AccompanyingPeriod::STEP_DRAFT => $this->translator->trans('course.draft'), | ||||
|                     AccompanyingPeriod::STEP_CONFIRMED => $this->translator->trans('course.confirmed'), | ||||
|                     AccompanyingPeriod::STEP_CLOSED => $this->translator->trans('course.closed'), | ||||
|                     AccompanyingPeriod::STEP_CONFIRMED_INACTIVE_SHORT => $this->translator->trans('course.inactive_short'), | ||||
|                     AccompanyingPeriod::STEP_CONFIRMED_INACTIVE_LONG => $this->translator->trans('course.inactive_long'), | ||||
|                     default => $value, | ||||
|                 }; | ||||
|  | ||||
|             case 'intensity': | ||||
|                 return fn ($value) => match ($value) { | ||||
|                     '_header' => 'export.list.acp.intensity', | ||||
|                     null => '', | ||||
|                     AccompanyingPeriod::INTENSITY_OCCASIONAL => $this->translator->trans('occasional'), | ||||
|                     AccompanyingPeriod::INTENSITY_REGULAR => $this->translator->trans('regular'), | ||||
|                     default => $value, | ||||
|                 }; | ||||
|  | ||||
|             default: | ||||
|                 return static function ($value) use ($key) { | ||||
|                     if ('_header' === $value) { | ||||
|                         return 'export.list.acp.' . $key; | ||||
|                     } | ||||
|  | ||||
|                     if (null === $value) { | ||||
|                         return ''; | ||||
|                     } | ||||
|  | ||||
|                     return $value; | ||||
|                 }; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public function addSelectClauses(QueryBuilder $qb, \DateTimeImmutable $calcDate): void | ||||
|     { | ||||
|         $qb->addSelect('acp.id AS acpId'); | ||||
|         $qb->addSelect('acp.createdAt AS acpCreatedAt'); | ||||
|         $qb->addSelect('acp.updatedAt AS acpUpdatedAt'); | ||||
|  | ||||
|         // add the regular fields | ||||
|         foreach (['openingDate', 'closingDate', 'confidential', 'emergency', 'intensity'] as $field) { | ||||
|             $qb->addSelect(sprintf('acp.%s AS %s', $field, $field)); | ||||
|         } | ||||
|  | ||||
|         // add the field which are simple association | ||||
|         $qb | ||||
|             ->leftJoin('acp.createdBy', "acp_created_by_t") | ||||
|             ->addSelect('acp_created_by_t.label AS acpCreatedBy'); | ||||
|         $qb | ||||
|             ->leftJoin('acp.updatedBy', "acp_updated_by_t") | ||||
|             ->addSelect('acp_updated_by_t.label AS acpUpdatedBy'); | ||||
|  | ||||
|         foreach (['origin' => 'label', 'closingMotive' => 'name', 'job' => 'label', 'administrativeLocation' => 'name'] as $entity => $field) { | ||||
|             $qb | ||||
|                 ->leftJoin(sprintf('acp.%s', $entity), "{$entity}_t") | ||||
|                 ->addSelect(sprintf('%s_t.%s AS %s', $entity, $field, $entity)); | ||||
|         } | ||||
|  | ||||
|         // step at date | ||||
|         $qb | ||||
|             ->addSelect('stepHistory.step AS step') | ||||
|             ->addSelect('stepHistory.startDate AS stepSince') | ||||
|             ->leftJoin('acp.stepHistories', 'stepHistory') | ||||
|             ->andWhere( | ||||
|                 $qb->expr()->andX( | ||||
|                     $qb->expr()->lte('stepHistory.startDate', ':calcDate'), | ||||
|                     $qb->expr()->orX($qb->expr()->isNull('stepHistory.endDate'), $qb->expr()->gt('stepHistory.endDate', ':calcDate')) | ||||
|                 ) | ||||
|             ); | ||||
|  | ||||
|         // referree at date | ||||
|         $qb | ||||
|             ->addSelect('referrer_t.label AS referrer') | ||||
|             ->addSelect('userHistory.startDate AS referrerSince') | ||||
|             ->leftJoin('acp.userHistories', 'userHistory') | ||||
|             ->leftJoin('userHistory.user', 'referrer_t') | ||||
|             ->andWhere( | ||||
|                 $qb->expr()->orX( | ||||
|                     $qb->expr()->isNull('userHistory'), | ||||
|                     $qb->expr()->andX( | ||||
|                         $qb->expr()->lte('userHistory.startDate', ':calcDate'), | ||||
|                         $qb->expr()->orX($qb->expr()->isNull('userHistory.endDate'), $qb->expr()->gt('userHistory.endDate', ':calcDate')) | ||||
|                     ) | ||||
|                 ) | ||||
|             ); | ||||
|  | ||||
|         // location of the acp | ||||
|         $qb | ||||
|             ->addSelect('CASE WHEN locationHistory.personLocation IS NOT NULL THEN 1 ELSE 0 END AS locationIsPerson') | ||||
|             ->addSelect('CASE WHEN locationHistory.personLocation IS NOT NULL THEN 0 ELSE 1 END AS locationIsTemp') | ||||
|             ->addSelect('IDENTITY(locationHistory.personLocation) AS locationPersonName') | ||||
|             ->addSelect('IDENTITY(locationHistory.personLocation) AS locationPersonId') | ||||
|             ->leftJoin('acp.locationHistories', 'locationHistory') | ||||
|             ->andWhere( | ||||
|                 $qb->expr()->orX( | ||||
|                     $qb->expr()->isNull('locationHistory'), | ||||
|                     $qb->expr()->andX( | ||||
|                         $qb->expr()->lte('locationHistory.startDate', ':calcDate'), | ||||
|                         $qb->expr()->orX($qb->expr()->isNull('locationHistory.endDate'), $qb->expr()->gt('locationHistory.endDate', ':calcDate')) | ||||
|                     ) | ||||
|                 ) | ||||
|             ) | ||||
|             ->leftJoin( | ||||
|                 PersonHouseholdAddress::class, | ||||
|                 'acpPersonAddress', | ||||
|                 Join::WITH, | ||||
|                 'locationHistory.personLocation = acpPersonAddress.person AND (acpPersonAddress.validFrom <= :calcDate AND (acpPersonAddress.validTo IS NULL OR acpPersonAddress.validTo > :calcDate))' | ||||
|             ) | ||||
|             ->leftJoin(Address::class, 'acp_address', Join::WITH, 'COALESCE(IDENTITY(locationHistory.addressLocation), IDENTITY(acpPersonAddress.address)) = acp_address.id'); | ||||
|  | ||||
|         $this->addressHelper->addSelectClauses(ExportAddressHelper::F_ALL, $qb, 'acp_address', 'acp_address_fields'); | ||||
|  | ||||
|         // requestor | ||||
|         $qb | ||||
|             ->addSelect('CASE WHEN acp.requestorPerson IS NULL THEN 1 ELSE 0 END AS isRequestorPerson') | ||||
|             ->addSelect('CASE WHEN acp.requestorPerson IS NULL THEN 0 ELSE 1 END AS isRequestorThirdParty') | ||||
|             ->addSelect('IDENTITY(acp.requestorPerson) AS requestorPersonId') | ||||
|             ->addSelect('IDENTITY(acp.requestorThirdParty) AS requestorThirdPartyId') | ||||
|             ->addSelect('IDENTITY(acp.requestorPerson) AS requestorPerson') | ||||
|             ->addSelect('IDENTITY(acp.requestorThirdParty) AS requestorThirdParty'); | ||||
|  | ||||
|         $qb | ||||
|             // scopes | ||||
|             ->addSelect('(SELECT AGGREGATE(scope.name) FROM ' . Scope::class . ' scope WHERE scope MEMBER OF acp.scopes) AS scopes') | ||||
|             // social issues | ||||
|             ->addSelect('(SELECT AGGREGATE(socialIssue.id) FROM ' . SocialIssue::class . ' socialIssue WHERE socialIssue MEMBER OF acp.socialIssues) AS socialIssues'); | ||||
|  | ||||
|         // add parameter | ||||
|         $qb->setParameter('calcDate', $calcDate); | ||||
|     } | ||||
| } | ||||
| @@ -11,6 +11,7 @@ declare(strict_types=1); | ||||
|  | ||||
| namespace Chill\PersonBundle\Export\Helper; | ||||
|  | ||||
| use Chill\MainBundle\Entity\Language; | ||||
| use Chill\MainBundle\Export\Helper\ExportAddressHelper; | ||||
| use Chill\MainBundle\Repository\CenterRepositoryInterface; | ||||
| use Chill\MainBundle\Repository\CivilityRepositoryInterface; | ||||
| @@ -42,7 +43,7 @@ use function strlen; | ||||
| class ListPersonHelper | ||||
| { | ||||
|     public const FIELDS = [ | ||||
|         'id', | ||||
|         'personId', | ||||
|         'civility', | ||||
|         'firstName', | ||||
|         'lastName', | ||||
| @@ -114,7 +115,26 @@ class ListPersonHelper | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @param array|value-of<self::FIELDS>[] $fields | ||||
|      * Those keys are the "direct" keys, which are created when we decide to use to list all the keys. | ||||
|      * | ||||
|      * This method must be used in `getKeys` instead of the `self::FIELDS` | ||||
|      * | ||||
|      * @return array<string> | ||||
|      */ | ||||
|     public function getAllKeys(): array | ||||
|     { | ||||
|         return [ | ||||
|             ...array_filter( | ||||
|                 ListPersonHelper::FIELDS, | ||||
|                 fn (string $key) => !in_array($key, ['address_fields', 'lifecycleUpdate'], true) | ||||
|             ), | ||||
|             ...$this->addressHelper->getKeys(ExportAddressHelper::F_ALL, 'address_fields'), | ||||
|             ...['createdAt', 'createdBy', 'updatedAt', 'updatedBy'], | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @param array<value-of<self::FIELDS>> $fields | ||||
|      */ | ||||
|     public function addSelect(QueryBuilder $qb, array $fields, DateTimeImmutable $computedDate): void | ||||
|     { | ||||
| @@ -124,6 +144,11 @@ class ListPersonHelper | ||||
|             } | ||||
|  | ||||
|             switch ($f) { | ||||
|                 case 'personId': | ||||
|                     $qb->addSelect('person.id AS personId'); | ||||
|  | ||||
|                     break; | ||||
|  | ||||
|                 case 'countryOfBirth': | ||||
|                 case 'nationality': | ||||
|                     $qb->addSelect(sprintf('IDENTITY(person.%s) as %s', $f, $f)); | ||||
| @@ -138,25 +163,7 @@ class ListPersonHelper | ||||
|                     break; | ||||
|  | ||||
|                 case 'spokenLanguages': | ||||
|                     $qb | ||||
|                         ->leftJoin('person.spokenLanguages', 'spokenLanguage') | ||||
|                         ->addSelect('AGGREGATE(spokenLanguage.id) AS spokenLanguages') | ||||
|                         ->addGroupBy('person'); | ||||
|  | ||||
|                     if (in_array('center', $fields, true)) { | ||||
|                         $qb->addGroupBy('center'); | ||||
|                     } | ||||
|  | ||||
|                     if (in_array('address_fields', $fields, true)) { | ||||
|                         $qb | ||||
|                             ->addGroupBy('address_fieldsid') | ||||
|                             ->addGroupBy('address_fieldscountry_t.id') | ||||
|                             ->addGroupBy('address_fieldspostcode_t.id'); | ||||
|                     } | ||||
|  | ||||
|                     if (in_array('household_id', $fields, true)) { | ||||
|                         $qb->addGroupBy('household_id'); | ||||
|                     } | ||||
|                     $qb->addSelect('(SELECT AGGREGATE(language.id) FROM ' . Language::class . ' language WHERE language MEMBER OF person.spokenLanguages) AS spokenLanguages'); | ||||
|  | ||||
|                     break; | ||||
|  | ||||
|   | ||||
| @@ -4,35 +4,27 @@ services: | ||||
|         autowire: true | ||||
|  | ||||
|     ## Indicators | ||||
|     chill.person.export.count_person: | ||||
|         class: Chill\PersonBundle\Export\Export\CountPerson | ||||
|         autowire: true | ||||
|         autoconfigure: true | ||||
|     Chill\PersonBundle\Export\Export\CountPerson: | ||||
|         tags: | ||||
|             - { name: chill.export, alias: count_person } | ||||
|  | ||||
|     chill.person.export.count_person_with_accompanying_course: | ||||
|         class: Chill\PersonBundle\Export\Export\CountPersonWithAccompanyingCourse | ||||
|         autowire: true | ||||
|         autoconfigure: true | ||||
|     Chill\PersonBundle\Export\Export\CountPersonWithAccompanyingCourse: | ||||
|         tags: | ||||
|             - { name: chill.export, alias: count_person_with_accompanying_course } | ||||
|  | ||||
|     Chill\PersonBundle\Export\Export\ListPerson: | ||||
|         autowire: true | ||||
|         autoconfigure: true | ||||
|         tags: | ||||
|             - { name: chill.export, alias: list_person } | ||||
|  | ||||
|     Chill\PersonBundle\Export\Export\ListPersonWithAccompanyingPeriod: | ||||
|         autowire: true | ||||
|         autoconfigure: true | ||||
|     Chill\PersonBundle\Export\Export\ListPersonHavingAccompanyingPeriod: | ||||
|         tags: | ||||
|             - { name: chill.export, alias: list_person_with_acp } | ||||
|  | ||||
|     Chill\PersonBundle\Export\Export\ListPersonWithAccompanyingPeriodDetails: | ||||
|         tags: | ||||
|             - { name: chill.export, alias: list_person_with_acp_details } | ||||
|  | ||||
|     Chill\PersonBundle\Export\Export\ListAccompanyingPeriod: | ||||
|         autowire: true | ||||
|         autoconfigure: true | ||||
|         tags: | ||||
|             - { name: chill.export, alias: list_acp } | ||||
|  | ||||
|   | ||||
| @@ -998,6 +998,8 @@ notification: | ||||
|     Notify referrer: Notifier le référent | ||||
|     Notify any: Notifier d'autres utilisateurs | ||||
|  | ||||
| personId: Identifiant de l'usager | ||||
|  | ||||
| export: | ||||
|     export: | ||||
|         acp_stats: | ||||
| @@ -1147,13 +1149,15 @@ export: | ||||
|     list: | ||||
|         person_with_acp: | ||||
|             List peoples having an accompanying period: Liste des usagers ayant un parcours d'accompagnement | ||||
|             List peoples having an accompanying period with period details: Liste des usagers concernés avec détail de chaque parcours | ||||
|             Create a list of people having an accompaying periods, according to various filters.: Génère une liste des usagers ayant un parcours d'accompagnement, selon différents critères liés au parcours ou à l'usager | ||||
|             Create a list of people having an accompaying periods with details of period, according to various filters.: Génère une liste des usagers ayant un parcours d'accompagnement, selon différents critères liés au parcours ou à l'usager. Ajoute les détails du parcours à la liste. | ||||
|         acp: | ||||
|             List of accompanying periods: Liste des parcours d'accompagnements | ||||
|             Generate a list of accompanying periods, filtered on different parameters.: Génère une liste des parcours d'accompagnement, filtrée sur différents paramètres. | ||||
|             Date of calculation for associated elements: Date de calcul des éléments associés | ||||
|             The associated referree, localisation, and other elements will be valid at this date: Les éléments associés, comme la localisation, le référent et d'autres éléments seront valides à cette date | ||||
|             id: Identifiant du parcours | ||||
|             acpId: Identifiant du parcours | ||||
|             openingDate: Date d'ouverture du parcours | ||||
|             closingDate: Date de fermeture du parcours | ||||
|             closingMotive: Motif de cloture | ||||
| @@ -1161,14 +1165,14 @@ export: | ||||
|             confidential: Confidentiel | ||||
|             emergency: Urgent | ||||
|             intensity: Intensité | ||||
|             createdAt: Créé le | ||||
|             updatedAt: Dernière mise à jour le | ||||
|             acpCreatedAt: Créé le | ||||
|             acpUpdatedAt: Dernière mise à jour le | ||||
|             acpOrigin: Origine du parcours | ||||
|             origin: Origine du parcours | ||||
|             acpClosingMotive: Motif de fermeture | ||||
|             acpJob: Métier du parcours | ||||
|             createdBy: Créé par | ||||
|             updatedBy: Dernière modification par | ||||
|             acpCreatedBy: Créé par | ||||
|             acpUpdatedBy: Dernière modification par | ||||
|             administrativeLocation: Location administrative | ||||
|             step: Etape | ||||
|             stepSince: Dernière modification de l'étape | ||||
| @@ -1176,7 +1180,7 @@ export: | ||||
|             referrerSince: Référent depuis le | ||||
|             locationIsPerson: Parcours localisé auprès d'un usager concerné | ||||
|             locationIsTemp: Parcours avec une localisation temporaire | ||||
|             acpLocationPersonName: Usager auprès duquel le parcours est localisé | ||||
|             locationPersonName: Usager auprès duquel le parcours est localisé | ||||
|             locationPersonId: Identifiant de l'usager auprès duquel le parcours est localisé | ||||
|             acpaddress_fieldscountry: Pays de l'adresse | ||||
|             isRequestorPerson: Le demandeur est-il un usager ? | ||||
|   | ||||
		Reference in New Issue
	
	Block a user