diff --git a/.gitignore b/.gitignore index 38d06d315..ebdc16e56 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ composer composer.phar composer.lock docs/build/ +node_modules/* .php_cs.cache ###> symfony/framework-bundle ### diff --git a/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/LocationFilter.php b/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/LocationFilter.php new file mode 100644 index 000000000..ed591e957 --- /dev/null +++ b/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/LocationFilter.php @@ -0,0 +1,74 @@ +translatableStringHelper = $translatableStringHelper; + } + + public function addRole(): ?string + { + return null; + } + + public function alterQuery(QueryBuilder $qb, $data) + { + $qb->andWhere( + $qb->expr()->in('activity.location', ':location') + ); + + $qb->setParameter('location', $data['accepted_location']); + } + + public function applyOn(): string + { + return Declarations::ACTIVITY_ACP; + } + + public function buildForm(FormBuilderInterface $builder) + { + $builder->add('accepted_location', PickUserLocationType::class, [ + 'multiple' => true, + 'label' => 'pick location' + ]); + } + + public function describeAction($data, $format = 'string'): array + { + $locations = []; + + foreach ($data['accepted_location'] as $location) { + $locations[] = $location->getName(); + } + + return ['Filtered activity by location: only %locations%', [ + '%locations%' => implode(', ', $locations), + ]]; + } + + public function getTitle(): string + { + return 'Filter activity by location'; + } +} diff --git a/src/Bundle/ChillActivityBundle/Export/Filter/PersonFilters/ActivityReasonFilter.php b/src/Bundle/ChillActivityBundle/Export/Filter/PersonFilters/ActivityReasonFilter.php index 5634358c7..c55d579e4 100644 --- a/src/Bundle/ChillActivityBundle/Export/Filter/PersonFilters/ActivityReasonFilter.php +++ b/src/Bundle/ChillActivityBundle/Export/Filter/PersonFilters/ActivityReasonFilter.php @@ -50,7 +50,7 @@ class ActivityReasonFilter implements ExportElementValidatedInterface, FilterInt { $where = $qb->getDQLPart('where'); $join = $qb->getDQLPart('join'); - $clause = $qb->expr()->in('reasons', ':selected_activity_reasons'); + $clause = $qb->expr()->in('actreasons', ':selected_activity_reasons'); if (!in_array('actreasons', $qb->getAllAliases(), true)) { $qb->join('activity.reasons', 'actreasons'); @@ -77,6 +77,7 @@ class ActivityReasonFilter implements ExportElementValidatedInterface, FilterInt 'class' => ActivityReason::class, 'choice_label' => fn (ActivityReason $reason) => $this->translatableStringHelper->localize($reason->getName()), 'group_by' => fn (ActivityReason $reason) => $this->translatableStringHelper->localize($reason->getCategory()->getName()), + 'attr' => ['class' => 'select2 '], 'multiple' => true, 'expanded' => false, ]); diff --git a/src/Bundle/ChillActivityBundle/Menu/AdminMenuBuilder.php b/src/Bundle/ChillActivityBundle/Menu/AdminMenuBuilder.php index 5e10c9a14..71a9e3c2a 100644 --- a/src/Bundle/ChillActivityBundle/Menu/AdminMenuBuilder.php +++ b/src/Bundle/ChillActivityBundle/Menu/AdminMenuBuilder.php @@ -36,7 +36,6 @@ final class AdminMenuBuilder implements LocalMenuBuilderInterface ->setAttribute('class', 'list-group-item-header') ->setExtras([ 'order' => 5000, - 'icons' => ['exchange'], ]); $menu->addChild('Activity Reasons', [ diff --git a/src/Bundle/ChillActivityBundle/Resources/views/Activity/show.html.twig b/src/Bundle/ChillActivityBundle/Resources/views/Activity/show.html.twig index 49e71bfad..1ec0ab274 100644 --- a/src/Bundle/ChillActivityBundle/Resources/views/Activity/show.html.twig +++ b/src/Bundle/ChillActivityBundle/Resources/views/Activity/show.html.twig @@ -156,7 +156,7 @@
- {{ entity.privateComment.comments[userId] }} + {{ entity.privateComment.comments[userId]|chill_markdown_to_html }}
@@ -168,11 +168,11 @@ {% if entity.documents|length > 0 %} {% else %} - {{ 'Any document found'|trans }} + {{ 'No document found'|trans }} {% endif %} {% endif %} diff --git a/src/Bundle/ChillActivityBundle/Resources/views/Activity/showAccompanyingCourse.html.twig b/src/Bundle/ChillActivityBundle/Resources/views/Activity/showAccompanyingCourse.html.twig index fbd7b20b4..3486f47bc 100644 --- a/src/Bundle/ChillActivityBundle/Resources/views/Activity/showAccompanyingCourse.html.twig +++ b/src/Bundle/ChillActivityBundle/Resources/views/Activity/showAccompanyingCourse.html.twig @@ -8,12 +8,14 @@ {{ parent() }} {{ encore_entry_script_tags('mod_notification_toggle_read_status') }} {{ encore_entry_script_tags('mod_async_upload') }} + {{ encore_entry_script_tags('mod_document_action_buttons_group') }} {% endblock %} {% block css %} {{ parent() }} {{ encore_entry_link_tags('mod_notification_toggle_read_status') }} {{ encore_entry_link_tags('mod_async_upload') }} + {{ encore_entry_link_tags('mod_document_action_buttons_group') }} {% endblock %} {% import 'ChillActivityBundle:ActivityReason:macro.html.twig' as m %} diff --git a/src/Bundle/ChillActivityBundle/Resources/views/Activity/showPerson.html.twig b/src/Bundle/ChillActivityBundle/Resources/views/Activity/showPerson.html.twig index 5776eddb5..43a8eb86b 100644 --- a/src/Bundle/ChillActivityBundle/Resources/views/Activity/showPerson.html.twig +++ b/src/Bundle/ChillActivityBundle/Resources/views/Activity/showPerson.html.twig @@ -7,13 +7,13 @@ {% block js %} {{ parent() }} {{ encore_entry_script_tags('mod_notification_toggle_read_status') }} - {{ encore_entry_link_tags('mod_async_upload') }} + {{ encore_entry_script_tags('mod_document_action_buttons_group') }} {% endblock %} {% block css %} {{ parent() }} {{ encore_entry_link_tags('mod_notification_toggle_read_status') }} - {{ encore_entry_link_tags('mod_async_upload') }} + {{ encore_entry_link_tags('mod_document_action_buttons_group') }} {% endblock %} {% import 'ChillActivityBundle:ActivityReason:macro.html.twig' as m %} diff --git a/src/Bundle/ChillActivityBundle/config/services/export.yaml b/src/Bundle/ChillActivityBundle/config/services/export.yaml index 16addd2a3..09817d80e 100644 --- a/src/Bundle/ChillActivityBundle/config/services/export.yaml +++ b/src/Bundle/ChillActivityBundle/config/services/export.yaml @@ -79,6 +79,11 @@ services: tags: - { name: chill.export_filter, alias: 'accompanyingcourse_activitytype_filter' } + chill.activity.export.location_filter: + class: Chill\ActivityBundle\Export\Filter\ACPFilters\LocationFilter + tags: + - { name: chill.export_filter, alias: 'activity_location_filter' } + chill.activity.export.locationtype_filter: class: Chill\ActivityBundle\Export\Filter\ACPFilters\LocationTypeFilter tags: diff --git a/src/Bundle/ChillActivityBundle/translations/messages.fr.yml b/src/Bundle/ChillActivityBundle/translations/messages.fr.yml index 20d72e7b3..461137454 100644 --- a/src/Bundle/ChillActivityBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillActivityBundle/translations/messages.fr.yml @@ -252,6 +252,8 @@ Activity reasons for those activities: Sujets de ces activités Filter by activity type: Filtrer les activités par type +Filter activity by location: Filtrer les activités par localisation +'Filtered activity by location: only %locations%': "Filtré par localisation: uniquement %locations%" Filter activity by locationtype: Filtrer les activités par type de localisation 'Filtered activity by locationtype: only %types%': "Filtré par type de localisation: uniquement %types%" Accepted locationtype: Types de localisation diff --git a/src/Bundle/ChillAsideActivityBundle/src/Export/Aggregator/ByActivityTypeAggregator.php b/src/Bundle/ChillAsideActivityBundle/src/Export/Aggregator/ByActivityTypeAggregator.php index 6c91c9336..32418e3c3 100644 --- a/src/Bundle/ChillAsideActivityBundle/src/Export/Aggregator/ByActivityTypeAggregator.php +++ b/src/Bundle/ChillAsideActivityBundle/src/Export/Aggregator/ByActivityTypeAggregator.php @@ -53,19 +53,15 @@ class ByActivityTypeAggregator implements AggregatorInterface public function getLabels($key, array $values, $data) { - $this->asideActivityCategoryRepository->findBy(['id' => $values]); - return function ($value): string { if ('_header' === $value) { return 'export.aggregator.Aside activity type'; } - if (null === $value) { + if (null === $value || null === $t = $this->asideActivityCategoryRepository->find($value)) { return ''; } - $t = $this->asideActivityCategoryRepository->find($value); - return $this->translatableStringHelper->localize($t->getTitle()); }; } diff --git a/src/Bundle/ChillAsideActivityBundle/src/Export/Aggregator/ByUserJobAggregator.php b/src/Bundle/ChillAsideActivityBundle/src/Export/Aggregator/ByUserJobAggregator.php new file mode 100644 index 000000000..d2fe7c2f2 --- /dev/null +++ b/src/Bundle/ChillAsideActivityBundle/src/Export/Aggregator/ByUserJobAggregator.php @@ -0,0 +1,89 @@ +userJobRepository = $userJobRepository; + $this->translatableStringHelper = $translatableStringHelper; + } + + public function addRole(): ?string + { + return null; + } + + public function alterQuery(QueryBuilder $qb, $data) + { + if (!in_array('aside_user', $qb->getAllAliases(), true)) { + $qb->leftJoin('aside.agent', 'aside_user'); + } + + $qb + ->addSelect('IDENTITY(aside_user.userJob) AS aside_activity_user_job_aggregator') + ->addGroupBy('aside_activity_user_job_aggregator'); + } + + public function applyOn() + { + return Declarations::ASIDE_ACTIVITY_TYPE; + } + + public function buildForm(FormBuilderInterface $builder) + { + // nothing to add in the form + } + + public function getLabels($key, array $values, $data) + { + return function ($value): string { + if ('_header' === $value) { + return 'Users \'s job'; + } + + if (null === $value || '' === $value) { + return ''; + } + + $j = $this->userJobRepository->find($value); + + return $this->translatableStringHelper->localize( + $j->getLabel() + ); + }; + } + + public function getQueryKeys($data): array + { + return ['aside_activity_user_job_aggregator']; + } + + public function getTitle() + { + return 'export.aggregator.Aggregate by user job'; + } +} diff --git a/src/Bundle/ChillAsideActivityBundle/src/Export/Aggregator/ByUserScopeAggregator.php b/src/Bundle/ChillAsideActivityBundle/src/Export/Aggregator/ByUserScopeAggregator.php new file mode 100644 index 000000000..6c06ee756 --- /dev/null +++ b/src/Bundle/ChillAsideActivityBundle/src/Export/Aggregator/ByUserScopeAggregator.php @@ -0,0 +1,89 @@ +scopeRepository = $scopeRepository; + $this->translatableStringHelper = $translatableStringHelper; + } + + public function addRole(): ?string + { + return null; + } + + public function alterQuery(QueryBuilder $qb, $data) + { + if (!in_array('aside_user', $qb->getAllAliases(), true)) { + $qb->leftJoin('aside.agent', 'aside_user'); + } + + $qb + ->addSelect('IDENTITY(aside_user.mainScope) AS aside_activity_user_scope_aggregator') + ->addGroupBy('aside_activity_user_scope_aggregator'); + } + + public function applyOn() + { + return Declarations::ASIDE_ACTIVITY_TYPE; + } + + public function buildForm(FormBuilderInterface $builder) + { + // nothing to add in the form + } + + public function getLabels($key, array $values, $data) + { + return function ($value): string { + if ('_header' === $value) { + return 'Users \'s scope'; + } + + if (null === $value || '' === $value) { + return ''; + } + + $s = $this->scopeRepository->find($value); + + return $this->translatableStringHelper->localize( + $s->getName() + ); + }; + } + + public function getQueryKeys($data): array + { + return ['aside_activity_user_scope_aggregator']; + } + + public function getTitle() + { + return 'export.aggregator.Aggregate by user scope'; + } +} diff --git a/src/Bundle/ChillAsideActivityBundle/src/Export/Export/AvgAsideActivityDuration.php b/src/Bundle/ChillAsideActivityBundle/src/Export/Export/AvgAsideActivityDuration.php new file mode 100644 index 000000000..f3db629cb --- /dev/null +++ b/src/Bundle/ChillAsideActivityBundle/src/Export/Export/AvgAsideActivityDuration.php @@ -0,0 +1,102 @@ +repository = $repository; + } + + public function buildForm(FormBuilderInterface $builder) + { + } + + public function getAllowedFormattersTypes(): array + { + return [FormatterInterface::TYPE_TABULAR]; + } + + public function getDescription(): string + { + return 'export.Average aside activities duration'; + } + + public function getGroup(): string + { + return 'export.Exports of aside activities'; + } + + public function getLabels($key, array $values, $data) + { + if ('export_avg_aside_activity_duration' !== $key) { + throw new LogicException("the key {$key} is not used by this export"); + } + + return static fn ($value) => '_header' === $value ? 'Average duration aside activities' : $value; + } + + public function getQueryKeys($data): array + { + return ['export_avg_aside_activity_duration']; + } + + public function getResult($query, $data) + { + return $query->getQuery()->getResult(Query::HYDRATE_SCALAR); + } + + public function getTitle(): string + { + return 'export.Average aside activities duration'; + } + + public function getType(): string + { + return Declarations::ASIDE_ACTIVITY_TYPE; + } + + public function initiateQuery(array $requiredModifiers, array $acl, array $data = []) + { + $qb = $this->repository->createQueryBuilder('aside'); + + $qb + ->select('AVG(aside.duration) as export_avg_aside_activity_duration') + ->andWhere($qb->expr()->isNotNull('aside.duration')); + + return $qb; + } + + public function requiredRole(): string + { + return AsideActivityVoter::STATS; + } + + public function supportsModifiers(): array + { + return [Declarations::ASIDE_ACTIVITY_TYPE]; + } +} diff --git a/src/Bundle/ChillAsideActivityBundle/src/Export/Export/CountAsideActivity.php b/src/Bundle/ChillAsideActivityBundle/src/Export/Export/CountAsideActivity.php index c3e99f129..87aad1659 100644 --- a/src/Bundle/ChillAsideActivityBundle/src/Export/Export/CountAsideActivity.php +++ b/src/Bundle/ChillAsideActivityBundle/src/Export/Export/CountAsideActivity.php @@ -11,12 +11,12 @@ declare(strict_types=1); namespace Chill\AsideActivityBundle\Export\Export; +use Chill\AsideActivityBundle\Export\Declarations; use Chill\AsideActivityBundle\Repository\AsideActivityRepository; use Chill\AsideActivityBundle\Security\AsideActivityVoter; use Chill\MainBundle\Export\ExportInterface; use Chill\MainBundle\Export\FormatterInterface; use Chill\MainBundle\Export\GroupedExportInterface; -use ChillAsideActivityBundle\Export\Declarations; use Doctrine\ORM\Query; use LogicException; use Symfony\Component\Form\FormBuilderInterface; @@ -100,6 +100,8 @@ class CountAsideActivity implements ExportInterface, GroupedExportInterface public function supportsModifiers(): array { - return []; + return [ + Declarations::ASIDE_ACTIVITY_TYPE, + ]; } } diff --git a/src/Bundle/ChillAsideActivityBundle/src/Export/Export/ListAsideActivity.php b/src/Bundle/ChillAsideActivityBundle/src/Export/Export/ListAsideActivity.php new file mode 100644 index 000000000..bf370f71b --- /dev/null +++ b/src/Bundle/ChillAsideActivityBundle/src/Export/Export/ListAsideActivity.php @@ -0,0 +1,236 @@ +em = $em; + $this->dateTimeHelper = $dateTimeHelper; + $this->userHelper = $userHelper; + $this->scopeRepository = $scopeRepository; + $this->centerRepository = $centerRepository; + $this->asideActivityCategoryRepository = $asideActivityCategoryRepository; + $this->categoryRender = $categoryRender; + $this->translatableStringHelper = $translatableStringHelper; + } + + public function buildForm(FormBuilderInterface $builder) + { + } + + public function getAllowedFormattersTypes() + { + return [FormatterInterface::TYPE_LIST]; + } + + public function getDescription() + { + return 'export.aside_activity.List of aside activities'; + } + + public function getTitle() + { + return 'export.aside_activity.List of aside activities'; + } + + public function getGroup(): string + { + return 'export.Exports of aside activities'; + } + + public function getLabels($key, array $values, $data) + { + switch ($key) { + case 'id': + case 'note': + return function ($value) use ($key) { + if ('_header' === $value) { + return 'export.aside_activity.' . $key; + } + + return $value ?? ''; + }; + case 'duration': + return function ($value) use ($key) { + if ('_header' === $value) { + return 'export.aside_activity.' . $key; + } + + if (null === $value) { + return ''; + } + + if ($value instanceof \DateTimeInterface) { + return $value->format('H:i:s'); + } + + return $value; + }; + + case 'createdAt': + case 'updatedAt': + case 'date': + return $this->dateTimeHelper->getLabel('export.aside_activity.'.$key); + + case 'agent_id': + case 'creator_id': + return $this->userHelper->getLabel($key, $values, 'export.aside_activity.' . $key); + + case 'aside_activity_type': + return function ($value) { + if ('_header' === $value) { + return 'export.aside_activity.aside_activity_type'; + } + + if (null === $value || '' === $value || null === $c = $this->asideActivityCategoryRepository->find($value)) { + return ''; + } + + return $this->categoryRender->renderString($c, []); + }; + + case 'main_scope': + return function ($value) { + if ('_header' === $value) { + return 'export.aside_activity.main_scope'; + } + + if (null === $value || '' === $value || null === $c = $this->scopeRepository->find($value)) { + return ''; + } + + return $this->translatableStringHelper->localize($c->getName()); + }; + + case 'main_center': + return function ($value) { + if ('_header' === $value) { + return 'export.aside_activity.main_center'; + } + + /** @var Center $c */ + if (null === $value || '' === $value || null === $c = $this->centerRepository->find($value)) { + return ''; + } + + return $c->getName(); + }; + + default: + throw new \LogicException('this key is not supported : ' . $key); + } + } + + public function getQueryKeys($data) + { + return [ + 'id', + 'createdAt', + 'updatedAt', + 'agent_id', + 'creator_id', + 'main_scope', + 'main_center', + 'aside_activity_type', + 'date', + 'duration', + 'note' + ]; + } + + /** + * @param QueryBuilder $query + * @param array $data + */ + public function getResult($query, $data): array + { + return $query->getQuery()->getResult(AbstractQuery::HYDRATE_ARRAY); + } + + public function getType(): string + { + return Declarations::ASIDE_ACTIVITY_TYPE; + } + + public function initiateQuery(array $requiredModifiers, array $acl, array $data = []) + { + $qb = $this->em->createQueryBuilder() + ->from(AsideActivity::class, 'aside') + ->leftJoin('aside.agent', 'agent') + ; + + $qb + ->addSelect('aside.id AS id') + ->addSelect('aside.createdAt AS createdAt') + ->addSelect('aside.updatedAt AS updatedAt') + ->addSelect('IDENTITY(aside.agent) AS agent_id') + ->addSelect('IDENTITY(aside.createdBy) AS creator_id') + ->addSelect('IDENTITY(agent.mainScope) AS main_scope') + ->addSelect('IDENTITY(agent.mainCenter) AS main_center') + ->addSelect('IDENTITY(aside.type) AS aside_activity_type') + ->addSelect('aside.date') + ->addSelect('aside.duration') + ->addSelect('aside.note') + ; + + return $qb; + } + + public function requiredRole(): string + { + return AsideActivityVoter::STATS; + } + + public function supportsModifiers() + { + return [Declarations::ASIDE_ACTIVITY_TYPE]; + } +} diff --git a/src/Bundle/ChillAsideActivityBundle/src/Export/Export/SumAsideActivityDuration.php b/src/Bundle/ChillAsideActivityBundle/src/Export/Export/SumAsideActivityDuration.php new file mode 100644 index 000000000..af17a2591 --- /dev/null +++ b/src/Bundle/ChillAsideActivityBundle/src/Export/Export/SumAsideActivityDuration.php @@ -0,0 +1,102 @@ +repository = $repository; + } + + public function buildForm(FormBuilderInterface $builder) + { + } + + public function getAllowedFormattersTypes(): array + { + return [FormatterInterface::TYPE_TABULAR]; + } + + public function getDescription(): string + { + return 'export.Sum aside activities duration'; + } + + public function getGroup(): string + { + return 'export.Exports of aside activities'; + } + + public function getLabels($key, array $values, $data) + { + if ('export_sum_aside_activity_duration' !== $key) { + throw new LogicException("the key {$key} is not used by this export"); + } + + return static fn ($value) => '_header' === $value ? 'Sum duration aside activities' : $value; + } + + public function getQueryKeys($data): array + { + return ['export_sum_aside_activity_duration']; + } + + public function getResult($query, $data) + { + return $query->getQuery()->getResult(Query::HYDRATE_SCALAR); + } + + public function getTitle(): string + { + return 'export.Sum aside activities duration'; + } + + public function getType(): string + { + return Declarations::ASIDE_ACTIVITY_TYPE; + } + + public function initiateQuery(array $requiredModifiers, array $acl, array $data = []) + { + $qb = $this->repository + ->createQueryBuilder('aside'); + + $qb->select('SUM(aside.duration) as export_sum_aside_activity_duration') + ->andWhere($qb->expr()->isNotNull('aside.duration')); + + return $qb; + } + + public function requiredRole(): string + { + return AsideActivityVoter::STATS; + } + + public function supportsModifiers(): array + { + return [Declarations::ASIDE_ACTIVITY_TYPE]; + } +} diff --git a/src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByActivityTypeFilter.php b/src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByActivityTypeFilter.php index 85b327795..3ad8e0e93 100644 --- a/src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByActivityTypeFilter.php +++ b/src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByActivityTypeFilter.php @@ -80,8 +80,8 @@ class ByActivityTypeFilter implements FilterInterface public function describeAction($data, $format = 'string'): array { $types = array_map( - fn (AsideActivityCategory $t): string => $this->translatableStringHelper->localize($t->getName()), - $this->asideActivityTypeRepository->findBy(['id' => $data['types']->toArray()]) + fn (AsideActivityCategory $t): string => $this->translatableStringHelper->localize($t->getTitle()), + $data['types']->toArray() ); return ['export.filter.Filtered by aside activity type: only %type%', [ diff --git a/src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByDateFilter.php b/src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByDateFilter.php index 099c87fc0..2d49b3d57 100644 --- a/src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByDateFilter.php +++ b/src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByDateFilter.php @@ -46,25 +46,18 @@ class ByDateFilter implements FilterInterface public function alterQuery(QueryBuilder $qb, $data) { - $where = $qb->getDQLPart('where'); $clause = $qb->expr()->between( 'aside.date', ':date_from', ':date_to' ); - if ($where instanceof Andx) { - $where->add($clause); - } else { - $where = $qb->expr()->andX($clause); - } + $qb->andWhere($clause); - $qb->add('where', $where); $qb->setParameter( 'date_from', $this->rollingDateConverter->convert($data['date_from']) - ); - $qb->setParameter( + )->setParameter( 'date_to', $this->rollingDateConverter->convert($data['date_to']) ); diff --git a/src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByUserFilter.php b/src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByUserFilter.php new file mode 100644 index 000000000..c2a3b4c54 --- /dev/null +++ b/src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByUserFilter.php @@ -0,0 +1,75 @@ +userRender = $userRender; + } + + public function addRole(): ?string + { + return null; + } + + public function alterQuery(QueryBuilder $qb, $data) + { + $clause = $qb->expr()->in('aside.agent', ':users'); + + $qb + ->andWhere($clause) + ->setParameter('users', $data['accepted_users']); + } + + public function applyOn(): string + { + return Declarations::ASIDE_ACTIVITY_TYPE; + } + + public function buildForm(FormBuilderInterface $builder) + { + $builder->add('accepted_users', PickUserDynamicType::class, [ + 'multiple' => true, + 'label' => 'Creators', + ]); + } + + public function describeAction($data, $format = 'string'): array + { + $users = []; + + foreach ($data['accepted_users'] as $u) { + $users[] = $this->userRender->renderString($u, []); + } + + return ['export.filter.Filtered aside activity by user: only %users%', [ + '%users%' => implode(', ', $users), + ]]; + } + + public function getTitle(): string + { + return 'export.filter.Filter aside activity by user'; + } +} diff --git a/src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByUserJobFilter.php b/src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByUserJobFilter.php new file mode 100644 index 000000000..86194b123 --- /dev/null +++ b/src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByUserJobFilter.php @@ -0,0 +1,81 @@ +translatableStringHelper = $translatableStringHelper; + } + + public function addRole(): ?string + { + return null; + } + + public function alterQuery(QueryBuilder $qb, $data) + { + $qb + ->andWhere( + $qb->expr()->exists( + 'SELECT 1 FROM ' . AsideActivity::class . ' aside_activity_user_job_filter_act + JOIN aside_activity_user_job_filter_act.agent aside_activity_user_job_filter_user WHERE aside_activity_user_job_filter_user.userJob IN (:aside_activity_user_job_filter_jobs) AND aside_activity_user_job_filter_act = aside' + ) + ) + ->setParameter('aside_activity_user_job_filter_jobs', $data['jobs']); + } + + public function applyOn() + { + return Declarations::ASIDE_ACTIVITY_TYPE; + } + + public function buildForm(FormBuilderInterface $builder) + { + $builder->add('jobs', EntityType::class, [ + 'class' => UserJob::class, + 'choice_label' => fn (UserJob $j) => $this->translatableStringHelper->localize($j->getLabel()), + 'multiple' => true, + 'expanded' => true, + ]); + } + + public function describeAction($data, $format = 'string') + { + return ['export.filter.Filtered aside activities by user jobs: only %jobs%', [ + '%jobs%' => implode( + ', ', + array_map( + fn (UserJob $job) => $this->translatableStringHelper->localize($job->getLabel()), + $data['jobs']->toArray() + ) + ), + ]]; + } + + public function getTitle() + { + return 'export.filter.Filter by user jobs'; + } +} diff --git a/src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByUserScopeFilter.php b/src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByUserScopeFilter.php new file mode 100644 index 000000000..4342e11eb --- /dev/null +++ b/src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByUserScopeFilter.php @@ -0,0 +1,88 @@ +scopeRepository = $scopeRepository; + $this->translatableStringHelper = $translatableStringHelper; + } + + public function addRole(): ?string + { + return null; + } + + public function alterQuery(QueryBuilder $qb, $data) + { + $qb + ->andWhere( + $qb->expr()->exists( + 'SELECT 1 FROM ' . AsideActivity::class . ' aside_activity_user_scope_filter_act + JOIN aside_activity_user_scope_filter_act.agent aside_activity_user_scope_filter_user WHERE aside_activity_user_scope_filter_user.mainScope IN (:aside_activity_user_scope_filter_scopes) AND aside_activity_user_scope_filter_act = aside ' + ) + ) + ->setParameter('aside_activity_user_scope_filter_scopes', $data['scopes']); + } + + public function applyOn() + { + return Declarations::ASIDE_ACTIVITY_TYPE; + } + + public function buildForm(FormBuilderInterface $builder) + { + $builder->add('scopes', EntityType::class, [ + 'class' => Scope::class, + 'choices' => $this->scopeRepository->findAllActive(), + 'choice_label' => fn (Scope $s) => $this->translatableStringHelper->localize($s->getName()), + 'multiple' => true, + 'expanded' => true, + ]); + } + + public function describeAction($data, $format = 'string') + { + return ['export.filter.Filtered aside activities by user scope: only %scopes%', [ + '%scopes%' => implode( + ', ', + array_map( + fn (Scope $s) => $this->translatableStringHelper->localize($s->getName()), + $data['scopes']->toArray() + ) + ), + ]]; + } + + public function getTitle() + { + return 'export.filter.Filter by user scope'; + } +} diff --git a/src/Bundle/ChillAsideActivityBundle/src/Form/AsideActivityFormType.php b/src/Bundle/ChillAsideActivityBundle/src/Form/AsideActivityFormType.php index 5c438bd0a..c02e53c23 100644 --- a/src/Bundle/ChillAsideActivityBundle/src/Form/AsideActivityFormType.php +++ b/src/Bundle/ChillAsideActivityBundle/src/Form/AsideActivityFormType.php @@ -12,8 +12,7 @@ declare(strict_types=1); namespace Chill\AsideActivityBundle\Form; use Chill\AsideActivityBundle\Entity\AsideActivity; -use Chill\AsideActivityBundle\Entity\AsideActivityCategory; -use Chill\AsideActivityBundle\Templating\Entity\CategoryRender; +use Chill\AsideActivityBundle\Form\Type\PickAsideActivityCategoryType; use Chill\MainBundle\Form\Type\ChillDateType; use Chill\MainBundle\Form\Type\ChillTextareaType; use Chill\MainBundle\Form\Type\PickUserDynamicType; @@ -21,8 +20,6 @@ use DateInterval; use DateTime; use DateTimeImmutable; use DateTimeZone; -use Doctrine\ORM\EntityRepository; -use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToTimestampTransformer; @@ -37,20 +34,16 @@ use function in_array; final class AsideActivityFormType extends AbstractType { - private CategoryRender $categoryRender; - private TokenStorageInterface $storage; private array $timeChoices; public function __construct( ParameterBagInterface $parameterBag, - TokenStorageInterface $storage, - CategoryRender $categoryRender + TokenStorageInterface $storage ) { $this->timeChoices = $parameterBag->get('chill_aside_activity.form.time_duration'); $this->storage = $storage; - $this->categoryRender = $categoryRender; } public function buildForm(FormBuilderInterface $builder, array $options) @@ -81,28 +74,10 @@ final class AsideActivityFormType extends AbstractType 'required' => true, ] ) - ->add( - 'type', - EntityType::class, - [ - 'label' => 'Type', - 'required' => true, - 'class' => AsideActivityCategory::class, - 'placeholder' => 'Choose the activity category', - 'query_builder' => static function (EntityRepository $er) { - $qb = $er->createQueryBuilder('ac'); - $qb->where($qb->expr()->eq('ac.isActive', 'TRUE')) - ->addOrderBy('ac.ordering', 'ASC'); - - return $qb; - }, - 'choice_label' => function (AsideActivityCategory $asideActivityCategory) { - $options = []; - - return $this->categoryRender->renderString($asideActivityCategory, $options); - }, - ] - ) + ->add('type', PickAsideActivityCategoryType::class, [ + 'label' => 'Type', + 'required' => true, + ]) ->add('duration', ChoiceType::class, $durationTimeOptions) ->add('note', ChillTextareaType::class, [ 'label' => 'Note', diff --git a/src/Bundle/ChillAsideActivityBundle/src/Form/Type/PickAsideActivityCategoryType.php b/src/Bundle/ChillAsideActivityBundle/src/Form/Type/PickAsideActivityCategoryType.php new file mode 100644 index 000000000..3ee392517 --- /dev/null +++ b/src/Bundle/ChillAsideActivityBundle/src/Form/Type/PickAsideActivityCategoryType.php @@ -0,0 +1,57 @@ +categoryRender = $categoryRender; + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver + ->setDefaults([ + 'class' => AsideActivityCategory::class, + 'placeholder' => 'Choose the activity category', + 'query_builder' => static function (EntityRepository $er) { + $qb = $er->createQueryBuilder('ac'); + $qb->where($qb->expr()->eq('ac.isActive', 'TRUE')) + ->addOrderBy('ac.ordering', 'ASC'); + + return $qb; + }, + 'choice_label' => function (AsideActivityCategory $asideActivityCategory) { + $options = []; + + return $this->categoryRender->renderString($asideActivityCategory, $options); + }, + 'attr' => ['class' => 'select2'], + ]); + } + + public function getParent(): string + { + return EntityType::class; + } +} diff --git a/src/Bundle/ChillAsideActivityBundle/src/Resources/views/asideActivity/_delete.html.twig b/src/Bundle/ChillAsideActivityBundle/src/Resources/views/asideActivity/_delete.html.twig index 2f30b14cf..79c2e6919 100644 --- a/src/Bundle/ChillAsideActivityBundle/src/Resources/views/asideActivity/_delete.html.twig +++ b/src/Bundle/ChillAsideActivityBundle/src/Resources/views/asideActivity/_delete.html.twig @@ -1,4 +1,4 @@ -
+ {% block crud_content_header %}

{{ ('crud.'~crud_name~'.title_delete')|trans({ '%as_string%': 'Aside Activity' }) }}

{% endblock crud_content_header %} @@ -27,4 +27,4 @@ {{ form_end(form) }} -
+ diff --git a/src/Bundle/ChillAsideActivityBundle/src/Resources/views/asideActivity/index.html.twig b/src/Bundle/ChillAsideActivityBundle/src/Resources/views/asideActivity/index.html.twig index 4dd12e042..5ffc73684 100644 --- a/src/Bundle/ChillAsideActivityBundle/src/Resources/views/asideActivity/index.html.twig +++ b/src/Bundle/ChillAsideActivityBundle/src/Resources/views/asideActivity/index.html.twig @@ -87,5 +87,5 @@ {% endif %} - + {% endblock %} diff --git a/src/Bundle/ChillAsideActivityBundle/src/Resources/views/asideActivity/new.html.twig b/src/Bundle/ChillAsideActivityBundle/src/Resources/views/asideActivity/new.html.twig index 09acf9859..99ff217b1 100644 --- a/src/Bundle/ChillAsideActivityBundle/src/Resources/views/asideActivity/new.html.twig +++ b/src/Bundle/ChillAsideActivityBundle/src/Resources/views/asideActivity/new.html.twig @@ -1,4 +1,4 @@ -{% extends '@ChillMain/Admin/layout.html.twig' %} +{% extends '@ChillMain/layout.html.twig' %} {% block js %} {{ parent() }} @@ -14,7 +14,7 @@ {% include('@ChillMain/CRUD/_new_title.html.twig') %} {% endblock %} -{% block admin_content %} +{% block content %} {% embed '@ChillMain/CRUD/_new_content.html.twig' %} {% block content_form_actions_save_and_show %}{% endblock %} {% endembed %} diff --git a/src/Bundle/ChillAsideActivityBundle/src/config/services.yaml b/src/Bundle/ChillAsideActivityBundle/src/config/services.yaml index 2a7c30d7c..34bb6da33 100644 --- a/src/Bundle/ChillAsideActivityBundle/src/config/services.yaml +++ b/src/Bundle/ChillAsideActivityBundle/src/config/services.yaml @@ -20,33 +20,3 @@ services: resource: "../Controller" autowire: true autoconfigure: true - - - ## Exports - - # indicators - Chill\AsideActivityBundle\Export\Export\CountAsideActivity: - autowire: true - autoconfigure: true - tags: - - { name: chill.export, alias: count_asideactivity } - - # filters - Chill\AsideActivityBundle\Export\Filter\ByDateFilter: - autowire: true - autoconfigure: true - tags: - - { name: chill.export_filter, alias: asideactivity_bydate_filter } - - Chill\AsideActivityBundle\Export\Filter\ByActivityTypeFilter: - autowire: true - autoconfigure: true - tags: - - { name: chill.export_filter, alias: asideactivity_activitytype_filter } - - # aggregators - Chill\AsideActivityBundle\Export\Aggregator\ByActivityTypeAggregator: - autowire: true - autoconfigure: true - tags: - - { name: chill.export_aggregator, alias: asideactivity_activitytype_aggregator } diff --git a/src/Bundle/ChillAsideActivityBundle/src/config/services/export.yaml b/src/Bundle/ChillAsideActivityBundle/src/config/services/export.yaml index 1b6b05e1c..a29413e15 100644 --- a/src/Bundle/ChillAsideActivityBundle/src/config/services/export.yaml +++ b/src/Bundle/ChillAsideActivityBundle/src/config/services/export.yaml @@ -3,11 +3,23 @@ services: autowire: true autoconfigure: true + Chill\AsideActivityBundle\Export\Export\ListAsideActivity: + tags: + - { name: chill.export, alias: 'list_aside_activity' } + ## Indicators Chill\AsideActivityBundle\Export\Export\CountAsideActivity: tags: - { name: chill.export, alias: 'count_aside_activity' } + Chill\AsideActivityBundle\Export\Export\SumAsideActivityDuration: + tags: + - { name: chill.export, alias: 'sum_aside_activity_duration' } + + Chill\AsideActivityBundle\Export\Export\AvgAsideActivityDuration: + tags: + - { name: chill.export, alias: 'avg_aside_activity_duration' } + ## Filters chill.aside_activity.export.date_filter: class: Chill\AsideActivityBundle\Export\Filter\ByDateFilter @@ -19,9 +31,34 @@ services: tags: - { name: chill.export_filter, alias: 'aside_activity_type_filter' } + chill.aside_activity.export.user_job_filter: + class: Chill\AsideActivityBundle\Export\Filter\ByUserJobFilter + tags: + - { name: chill.export_filter, alias: 'aside_activity_user_job_filter' } + + chill.aside_activity.export.user_scope_filter: + class: Chill\AsideActivityBundle\Export\Filter\ByUserScopeFilter + tags: + - { name: chill.export_filter, alias: 'aside_activity_user_scope_filter' } + + chill.aside_activity.export.user_filter: + class: Chill\AsideActivityBundle\Export\Filter\ByUserFilter + tags: + - { name: chill.export_filter, alias: 'aside_activity_user_filter' } + ## Aggregators chill.aside_activity.export.type_aggregator: class: Chill\AsideActivityBundle\Export\Aggregator\ByActivityTypeAggregator tags: - - { name: chill.export_aggregator, alias: activity_type_aggregator } \ No newline at end of file + - { name: chill.export_aggregator, alias: activity_type_aggregator } + + chill.aside_activity.export.user_job_aggregator: + class: Chill\AsideActivityBundle\Export\Aggregator\ByUserJobAggregator + tags: + - { name: chill.export_aggregator, alias: aside_activity_user_job_aggregator } + + chill.aside_activity.export.user_scope_aggregator: + class: Chill\AsideActivityBundle\Export\Aggregator\ByUserScopeAggregator + tags: + - { name: chill.export_aggregator, alias: aside_activity_user_scope_aggregator } diff --git a/src/Bundle/ChillAsideActivityBundle/src/translations/messages.fr.yml b/src/Bundle/ChillAsideActivityBundle/src/translations/messages.fr.yml index 35a4d6a22..0c807479f 100644 --- a/src/Bundle/ChillAsideActivityBundle/src/translations/messages.fr.yml +++ b/src/Bundle/ChillAsideActivityBundle/src/translations/messages.fr.yml @@ -29,16 +29,16 @@ location: Lieu # Crud crud: - aside_activity: - title_view: Détail de l'activité annexe - title_new: Nouvelle activité annexe - title_edit: Édition d'une activité annexe - title_delete: Supprimer une activité annexe - button_delete: Supprimer - confirm_message_delete: Êtes-vous sûr de vouloir supprimer cette activité annexe? - aside_activity_category: - title_new: Nouvelle catégorie d'activité annexe - title_edit: Édition d'une catégorie de type d'activité + aside_activity: + title_view: Détail de l'activité annexe + title_new: Nouvelle activité annexe + title_edit: Édition d'une activité annexe + title_delete: Supprimer une activité annexe + button_delete: Supprimer + confirm_message_delete: Êtes-vous sûr de vouloir supprimer cette activité annexe? + aside_activity_category: + title_new: Nouvelle catégorie d'activité annexe + title_edit: Édition d'une catégorie de type d'activité #forms Create a new aside activity type: Nouvelle categorie d'activité annexe @@ -169,18 +169,43 @@ Aside activity configuration: Configuration des activités annexes # exports export: - Exports of aside activities: Exports des activités annexes - Count aside activities: Nombre d'activités annexes - Count aside activities by various parameters.: Compte le nombre d'activités annexes selon divers critères - filter: - Filter by aside activity date: Filtrer les activités annexes par date - Filter by aside activity type: Filtrer les activités annexes par type d'activité - 'Filtered by aside activity type: only %type%': "Filtré par type d'activité annexe: uniquement %type%" - This date should be after the date given in "Implied in an aside activity after this date" field: Cette date devrait être postérieure à la date donnée dans le champ "activités annexes après cette date" - Aside activities after this date: Actvitités annexes après cette date - Aside activities before this date: Actvitités annexes avant cette date - aggregator: - Group by aside activity type: Grouper les activités annexes par type d'activité - Aside activity type: Type d'activité annexe + aside_activity: + List of aside activities: Liste des activités annexes + createdAt: Création + updatedAt: Dernière mise à jour + agent_id: Utilisateur + creator_id: Créateur + main_scope: Service principal de l'utilisateur + main_center: Centre principal de l'utilisteur + aside_activity_type: Catégorie d'activité annexe + date: Date + duration: Durée + note: Note + Exports of aside activities: Exports des activités annexes + Count aside activities: Nombre d'activités annexes + Count aside activities by various parameters.: Compte le nombre d'activités annexes selon divers critères + Average aside activities duration: Durée moyenne des activités annexes + Sum aside activities duration: Durée des activités annexes + filter: + Filter by aside activity date: Filtrer les activités annexes par date + Filter by aside activity type: Filtrer les activités annexes par type d'activité + 'Filtered by aside activity type: only %type%': "Filtré par type d'activité annexe: uniquement %type%" + Filtered by aside activities between %dateFrom% and %dateTo%: Filtré par date d'activité annexe, entre %dateFrom% et %dateTo% + This date should be after the date given in "Implied in an aside activity after this date" field: Cette date devrait être postérieure à la date donnée dans le champ "activités annexes après cette date" + Aside activities after this date: Actvitités annexes après cette date + Aside activities before this date: Actvitités annexes avant cette date + 'Filtered aside activity by user: only %users%': "Filtré par utilisateur: uniquement %users%" + Filter aside activity by user: Filtrer par utilisateur + 'Filtered aside activities by user jobs: only %jobs%': "Filtré par métier des utilisateurs: uniquement %jobs%" + Filter by user jobs: Filtrer les activités annexes par métier des utilisateurs + 'Filtered aside activities by user scope: only %scopes%': "Filtré par service des utilisateur: uniquement %scopes%" + Filter by user scope: Filtrer les activités annexes par service d'utilisateur + aggregator: + Group by aside activity type: Grouper les activités annexes par type d'activité + Aside activity type: Type d'activité annexe + Aggregate by user job: Grouper les activités annexes par métier des utilisateurs + Aggregate by user scope: Grouper les activités annexes par service des utilisateurs +# ROLES +CHILL_ASIDE_ACTIVITY_STATS: Statistiques pour les activités annexes diff --git a/src/Bundle/ChillBudgetBundle/Config/ConfigRepository.php b/src/Bundle/ChillBudgetBundle/Config/ConfigRepository.php deleted file mode 100644 index f28a83a4d..000000000 --- a/src/Bundle/ChillBudgetBundle/Config/ConfigRepository.php +++ /dev/null @@ -1,102 +0,0 @@ -resources = $resources; - $this->charges = $charges; - } - - public function getChargesKeys(bool $onlyActive = false): array - { - return array_map(static function ($element) { - return $element['key']; - }, $this->getCharges($onlyActive)); - } - - /** - * @return array where keys are the resource'key and label the ressource label - */ - public function getChargesLabels(bool $onlyActive = false) - { - $charges = []; - - foreach ($this->getCharges($onlyActive) as $definition) { - $charges[$definition['key']] = $this->normalizeLabel($definition['labels']); - } - - return $charges; - } - - public function getResourcesKeys(bool $onlyActive = false): array - { - return array_map(static function ($element) { - return $element['key']; - }, $this->getResources($onlyActive)); - } - - /** - * @return array where keys are the resource'key and label the ressource label - */ - public function getResourcesLabels(bool $onlyActive = false) - { - $resources = []; - - foreach ($this->getResources($onlyActive) as $definition) { - $resources[$definition['key']] = $this->normalizeLabel($definition['labels']); - } - - return $resources; - } - - private function getCharges(bool $onlyActive = false): array - { - return $onlyActive ? - array_filter($this->charges, static function ($el) { - return $el['active']; - }) - : $this->charges; - } - - private function getResources(bool $onlyActive = false): array - { - return $onlyActive ? - array_filter($this->resources, static function ($el) { - return $el['active']; - }) - : $this->resources; - } - - private function normalizeLabel($labels) - { - $normalizedLabels = []; - - foreach ($labels as $labelDefinition) { - $normalizedLabels[$labelDefinition['lang']] = $labelDefinition['label']; - } - - return $normalizedLabels; - } -} diff --git a/src/Bundle/ChillBudgetBundle/DependencyInjection/ChillBudgetExtension.php b/src/Bundle/ChillBudgetBundle/DependencyInjection/ChillBudgetExtension.php index ed7fd1ae9..57976f846 100644 --- a/src/Bundle/ChillBudgetBundle/DependencyInjection/ChillBudgetExtension.php +++ b/src/Bundle/ChillBudgetBundle/DependencyInjection/ChillBudgetExtension.php @@ -35,7 +35,6 @@ class ChillBudgetExtension extends Extension implements PrependExtensionInterfac $config = $this->processConfiguration($configuration, $configs); $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__ . '/../config')); - $loader->load('services/config.yaml'); $loader->load('services/form.yaml'); $loader->load('services/repository.yaml'); $loader->load('services/security.yaml'); diff --git a/src/Bundle/ChillBudgetBundle/Entity/AbstractElement.php b/src/Bundle/ChillBudgetBundle/Entity/AbstractElement.php index 73060f40b..f12ba3d72 100644 --- a/src/Bundle/ChillBudgetBundle/Entity/AbstractElement.php +++ b/src/Bundle/ChillBudgetBundle/Entity/AbstractElement.php @@ -75,7 +75,7 @@ abstract class AbstractElement /** * @ORM\Column(name="type", type="string", length=255) */ - private string $type; + private string $type = ''; /*Getters and Setters */ diff --git a/src/Bundle/ChillBudgetBundle/Form/ChargeType.php b/src/Bundle/ChillBudgetBundle/Form/ChargeType.php index 3dc2e230c..3356057cf 100644 --- a/src/Bundle/ChillBudgetBundle/Form/ChargeType.php +++ b/src/Bundle/ChillBudgetBundle/Form/ChargeType.php @@ -11,7 +11,6 @@ declare(strict_types=1); namespace Chill\BudgetBundle\Form; -use Chill\BudgetBundle\Config\ConfigRepository; use Chill\BudgetBundle\Entity\Charge; use Chill\BudgetBundle\Entity\ChargeKind; use Chill\BudgetBundle\Repository\ChargeKindRepository; @@ -25,13 +24,9 @@ use Symfony\Component\Form\Extension\Core\Type\TextareaType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Contracts\Translation\TranslatorInterface; -use function array_flip; -use function asort; class ChargeType extends AbstractType { - protected ConfigRepository $configRepository; - protected TranslatableStringHelperInterface $translatableStringHelper; private ChargeKindRepository $repository; @@ -39,12 +34,10 @@ class ChargeType extends AbstractType private TranslatorInterface $translator; public function __construct( - ConfigRepository $configRepository, TranslatableStringHelperInterface $translatableStringHelper, ChargeKindRepository $repository, TranslatorInterface $translator ) { - $this->configRepository = $configRepository; $this->translatableStringHelper = $translatableStringHelper; $this->repository = $repository; $this->translator = $translator; @@ -116,19 +109,4 @@ class ChargeType extends AbstractType { return 'chill_budgetbundle_charge'; } - - private function getTypes() - { - $charges = $this->configRepository - ->getChargesLabels(true); - - // rewrite labels to filter in language - foreach ($charges as $key => $labels) { - $charges[$key] = $this->translatableStringHelper->localize($labels); - } - - asort($charges); - - return array_flip($charges); - } } diff --git a/src/Bundle/ChillBudgetBundle/Form/ResourceType.php b/src/Bundle/ChillBudgetBundle/Form/ResourceType.php index 106f50ad1..fd859217a 100644 --- a/src/Bundle/ChillBudgetBundle/Form/ResourceType.php +++ b/src/Bundle/ChillBudgetBundle/Form/ResourceType.php @@ -11,7 +11,6 @@ declare(strict_types=1); namespace Chill\BudgetBundle\Form; -use Chill\BudgetBundle\Config\ConfigRepository; use Chill\BudgetBundle\Entity\Resource; use Chill\BudgetBundle\Entity\ResourceKind; use Chill\BudgetBundle\Repository\ResourceKindRepository; @@ -24,12 +23,9 @@ use Symfony\Component\Form\Extension\Core\Type\TextareaType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Contracts\Translation\TranslatorInterface; -use function array_flip; class ResourceType extends AbstractType { - protected ConfigRepository $configRepository; - protected TranslatableStringHelperInterface $translatableStringHelper; private ResourceKindRepository $repository; @@ -37,12 +33,10 @@ class ResourceType extends AbstractType private TranslatorInterface $translator; public function __construct( - ConfigRepository $configRepository, TranslatableStringHelperInterface $translatableStringHelper, ResourceKindRepository $repository, TranslatorInterface $translator ) { - $this->configRepository = $configRepository; $this->translatableStringHelper = $translatableStringHelper; $this->repository = $repository; $this->translator = $translator; @@ -98,19 +92,4 @@ class ResourceType extends AbstractType { return 'chill_budgetbundle_resource'; } - - private function getTypes() - { - $resources = $this->configRepository - ->getResourcesLabels(true); - - // rewrite labels to filter in language - foreach ($resources as $key => $labels) { - $resources[$key] = $this->translatableStringHelper->localize($labels); - } - - asort($resources); - - return array_flip($resources); - } } diff --git a/src/Bundle/ChillBudgetBundle/Repository/ChargeKindRepository.php b/src/Bundle/ChillBudgetBundle/Repository/ChargeKindRepository.php index e170a362a..10d02749a 100644 --- a/src/Bundle/ChillBudgetBundle/Repository/ChargeKindRepository.php +++ b/src/Bundle/ChillBudgetBundle/Repository/ChargeKindRepository.php @@ -14,9 +14,8 @@ namespace Chill\BudgetBundle\Repository; use Chill\BudgetBundle\Entity\ChargeKind; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityRepository; -use Doctrine\Persistence\ObjectRepository; -class ChargeKindRepository implements ObjectRepository +final class ChargeKindRepository implements ChargeKindRepositoryInterface { private EntityRepository $repository; @@ -50,7 +49,8 @@ class ChargeKindRepository implements ObjectRepository ->where($qb->expr()->eq('c.isActive', 'true')) ->orderBy('c.ordering', 'ASC') ->getQuery() - ->getResult(); + ->getResult() + ; } /** @@ -77,6 +77,11 @@ class ChargeKindRepository implements ObjectRepository return $this->repository->findOneBy($criteria); } + public function findOneByKind(string $kind): ?ChargeKind + { + return $this->repository->findOneBy(['kind' => $kind]); + } + public function getClassName(): string { return ChargeKind::class; diff --git a/src/Bundle/ChillBudgetBundle/Repository/ChargeKindRepositoryInterface.php b/src/Bundle/ChillBudgetBundle/Repository/ChargeKindRepositoryInterface.php new file mode 100644 index 000000000..5099a5674 --- /dev/null +++ b/src/Bundle/ChillBudgetBundle/Repository/ChargeKindRepositoryInterface.php @@ -0,0 +1,49 @@ +where($qb->expr()->eq('r.isActive', 'true')) ->orderBy('r.ordering', 'ASC') ->getQuery() - ->getResult(); + ->getResult() + ; } /** @@ -77,6 +77,11 @@ class ResourceKindRepository implements ObjectRepository return $this->repository->findOneBy($criteria); } + public function findOneByKind(string $kind): ?ResourceKind + { + return $this->repository->findOneBy(['kind' => $kind]); + } + public function getClassName(): string { return ResourceKind::class; diff --git a/src/Bundle/ChillBudgetBundle/Repository/ResourceKindRepositoryInterface.php b/src/Bundle/ChillBudgetBundle/Repository/ResourceKindRepositoryInterface.php new file mode 100644 index 000000000..658a87a3d --- /dev/null +++ b/src/Bundle/ChillBudgetBundle/Repository/ResourceKindRepositoryInterface.php @@ -0,0 +1,49 @@ +andWhere('c.startDate < :date') // TODO: there is a misconception here, the end date must be lower or null. startDate are never null //->andWhere('c.startDate < :date OR c.startDate IS NULL'); -; + ; if (null !== $sort) { $qb->orderBy($sort); diff --git a/src/Bundle/ChillBudgetBundle/Service/Summary/SummaryBudget.php b/src/Bundle/ChillBudgetBundle/Service/Summary/SummaryBudget.php index f02d2e64a..2f401f1ec 100644 --- a/src/Bundle/ChillBudgetBundle/Service/Summary/SummaryBudget.php +++ b/src/Bundle/ChillBudgetBundle/Service/Summary/SummaryBudget.php @@ -11,45 +11,52 @@ declare(strict_types=1); namespace Chill\BudgetBundle\Service\Summary; -use Chill\BudgetBundle\Config\ConfigRepository; +use Chill\BudgetBundle\Entity\ChargeKind; +use Chill\BudgetBundle\Entity\ResourceKind; +use Chill\BudgetBundle\Repository\ChargeKindRepository; +use Chill\BudgetBundle\Repository\ChargeKindRepositoryInterface; +use Chill\BudgetBundle\Repository\ResourceKindRepository; +use Chill\BudgetBundle\Repository\ResourceKindRepositoryInterface; use Chill\MainBundle\Templating\TranslatableStringHelperInterface; use Chill\PersonBundle\Entity\Household\Household; use Chill\PersonBundle\Entity\Person; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Query\ResultSetMapping; use LogicException; +use RuntimeException; use function count; /** * Helps to find a summary of the budget: the sum of resources and charges. */ -class SummaryBudget implements SummaryBudgetInterface +final class SummaryBudget implements SummaryBudgetInterface { - private const QUERY_CHARGE_BY_HOUSEHOLD = 'select SUM(amount) AS sum, string_agg(comment, \'|\') AS comment, type FROM chill_budget.charge WHERE (person_id IN (_ids_) OR household_id = ?) AND NOW() BETWEEN startdate AND COALESCE(enddate, \'infinity\'::timestamp) GROUP BY type'; + private const QUERY_CHARGE_BY_HOUSEHOLD = 'select SUM(amount) AS sum, string_agg(comment, \'|\') AS comment, charge_id AS kind_id FROM chill_budget.charge WHERE (person_id IN (_ids_) OR household_id = ?) AND NOW() BETWEEN startdate AND COALESCE(enddate, \'infinity\'::timestamp) GROUP BY charge_id'; - private const QUERY_CHARGE_BY_PERSON = 'select SUM(amount) AS sum, string_agg(comment, \'|\') AS comment, type FROM chill_budget.charge WHERE person_id = ? AND NOW() BETWEEN startdate AND COALESCE(enddate, \'infinity\'::timestamp) GROUP BY type'; + private const QUERY_CHARGE_BY_PERSON = 'select SUM(amount) AS sum, string_agg(comment, \'|\') AS comment, charge_id AS kind_id FROM chill_budget.charge WHERE person_id = ? AND NOW() BETWEEN startdate AND COALESCE(enddate, \'infinity\'::timestamp) GROUP BY charge_id'; - private const QUERY_RESOURCE_BY_HOUSEHOLD = 'select SUM(amount) AS sum, string_agg(comment, \'|\') AS comment, type FROM chill_budget.resource WHERE (person_id IN (_ids_) OR household_id = ?) AND NOW() BETWEEN startdate AND COALESCE(enddate, \'infinity\'::timestamp) GROUP BY type'; + private const QUERY_RESOURCE_BY_HOUSEHOLD = 'select SUM(amount) AS sum, string_agg(comment, \'|\') AS comment, resource_id AS kind_id FROM chill_budget.resource WHERE (person_id IN (_ids_) OR household_id = ?) AND NOW() BETWEEN startdate AND COALESCE(enddate, \'infinity\'::timestamp) GROUP BY resource_id'; - private const QUERY_RESOURCE_BY_PERSON = 'select SUM(amount) AS sum, string_agg(comment, \'|\') AS comment, type FROM chill_budget.resource WHERE person_id = ? AND NOW() BETWEEN startdate AND COALESCE(enddate, \'infinity\'::timestamp) GROUP BY type'; + private const QUERY_RESOURCE_BY_PERSON = 'select SUM(amount) AS sum, string_agg(comment, \'|\') AS comment, resource_id AS kind_id FROM chill_budget.resource WHERE person_id = ? AND NOW() BETWEEN startdate AND COALESCE(enddate, \'infinity\'::timestamp) GROUP BY resource_id'; - private array $chargeLabels; - - private ConfigRepository $configRepository; + private ChargeKindRepositoryInterface $chargeKindRepository; private EntityManagerInterface $em; - private array $resourcesLabels; + private ResourceKindRepositoryInterface $resourceKindRepository; private TranslatableStringHelperInterface $translatableStringHelper; - public function __construct(EntityManagerInterface $em, ConfigRepository $configRepository, TranslatableStringHelperInterface $translatableStringHelper) - { + public function __construct( + EntityManagerInterface $em, + TranslatableStringHelperInterface $translatableStringHelper, + ResourceKindRepositoryInterface $resourceKindRepository, + ChargeKindRepositoryInterface $chargeKindRepository + ) { $this->em = $em; - $this->configRepository = $configRepository; - $this->chargeLabels = $configRepository->getChargesLabels(); - $this->resourcesLabels = $configRepository->getResourcesLabels(); $this->translatableStringHelper = $translatableStringHelper; + $this->resourceKindRepository = $resourceKindRepository; + $this->chargeKindRepository = $chargeKindRepository; } public function getSummaryForHousehold(?Household $household): array @@ -112,7 +119,7 @@ class SummaryBudget implements SummaryBudgetInterface $rsm = new ResultSetMapping(); $rsm ->addScalarResult('sum', 'sum') - ->addScalarResult('type', 'type') + ->addScalarResult('kind_id', 'kind_id') ->addScalarResult('comment', 'comment'); return $rsm; @@ -120,51 +127,62 @@ class SummaryBudget implements SummaryBudgetInterface private function getEmptyChargeArray(): array { - $keys = $this->configRepository->getChargesKeys(); - $labels = $this->chargeLabels; + $keys = array_map(static fn (ChargeKind $kind) => $kind->getKind(), $this->chargeKindRepository->findAll()); - return array_combine($keys, array_map(function ($i) use ($labels) { - return ['sum' => 0.0, 'label' => $this->translatableStringHelper->localize($labels[$i]), 'comment' => '']; + return array_combine($keys, array_map(function ($kind) { + return ['sum' => 0.0, 'label' => $this->translatableStringHelper->localize($this->chargeKindRepository->findOneByKind($kind)->getName()), 'comment' => '']; }, $keys)); } private function getEmptyResourceArray(): array { - $keys = $this->configRepository->getResourcesKeys(); - $labels = $this->resourcesLabels; + $keys = array_map(static fn (ResourceKind $kind) => $kind->getKind(), $this->resourceKindRepository->findAll()); - return array_combine($keys, array_map(function ($i) use ($labels) { - return ['sum' => 0.0, 'label' => $this->translatableStringHelper->localize($labels[$i]), 'comment' => '']; + return array_combine($keys, array_map(function ($kind) { + return ['sum' => 0.0, 'label' => $this->translatableStringHelper->localize($this->resourceKindRepository->findOneByKind($kind)->getName()), 'comment' => '']; }, $keys)); } private function rowToArray(array $rows, string $kind): array { + $result = []; + switch ($kind) { case 'charge': - $label = $this->chargeLabels; + foreach ($rows as $row) { + $chargeKind = $this->chargeKindRepository->find($row['kind_id']); - break; + if (null === $chargeKind) { + throw new RuntimeException('charge kind not found: ' . $row['kind_id']); + } + $result[$chargeKind->getKind()] = [ + 'sum' => (float) $row['sum'], + 'label' => $this->translatableStringHelper->localize($chargeKind->getName()), + 'comment' => (string) $row['comment'], + ]; + } + + return $result; case 'resource': - $label = $this->resourcesLabels; + foreach ($rows as $row) { + $resourceKind = $this->resourceKindRepository->find($row['kind_id']); - break; + if (null === $resourceKind) { + throw new RuntimeException('charge kind not found: ' . $row['kind_id']); + } + + $result[$resourceKind->getKind()] = [ + 'sum' => (float) $row['sum'], + 'label' => $this->translatableStringHelper->localize($resourceKind->getName()), + 'comment' => (string) $row['comment'], + ]; + } + + return $result; default: throw new LogicException(); } - - $result = []; - - foreach ($rows as $row) { - $result[$row['type']] = [ - 'sum' => (float) $row['sum'], - 'label' => $this->translatableStringHelper->localize($label[$row['type']]), - 'comment' => (string) $row['comment'], - ]; - } - - return $result; } } diff --git a/src/Bundle/ChillBudgetBundle/Templating/Twig.php b/src/Bundle/ChillBudgetBundle/Templating/Twig.php deleted file mode 100644 index b4395f375..000000000 --- a/src/Bundle/ChillBudgetBundle/Templating/Twig.php +++ /dev/null @@ -1,65 +0,0 @@ -configRepository = $configRepository; - $this->translatableStringHelper = $translatableStringHelper; - } - - public function displayLink($link, $family) - { - switch ($family) { - case 'resource': - return $this->translatableStringHelper->localize( - $this->configRepository->getResourcesLabels()[$link] - ); - - case 'charge': - return $this->translatableStringHelper->localize( - $this->configRepository->getChargesLabels()[$link] - ); - - default: - throw new UnexpectedValueException("This family of element: {$family} is not " - . "supported. Supported families are 'resource', 'charge'"); - } - } - - public function getFilters() - { - return [ - new TwigFilter('budget_element_type_display', [$this, 'displayLink'], ['is_safe' => ['html']]), - ]; - } -} diff --git a/src/Bundle/ChillBudgetBundle/Tests/Service/Summary/SummaryBudgetTest.php b/src/Bundle/ChillBudgetBundle/Tests/Service/Summary/SummaryBudgetTest.php new file mode 100644 index 000000000..cf8c00efe --- /dev/null +++ b/src/Bundle/ChillBudgetBundle/Tests/Service/Summary/SummaryBudgetTest.php @@ -0,0 +1,158 @@ +prophesize(AbstractQuery::class); + $queryCharges->getResult()->willReturn([ + [ + 'sum' => 250.0, + 'comment' => '', + 'kind_id' => 1, // kind: rental + ], + ]); + $queryCharges->setParameters(Argument::type('array')) + ->will(function ($args, $query) { + return $query; + }) + ; + + $queryResources = $this->prophesize(AbstractQuery::class); + $queryResources->getResult()->willReturn([ + [ + 'sum' => 1500.0, + 'comment' => '', + 'kind_id' => 2, // kind: 'salary', + ], + ]); + $queryResources->setParameters(Argument::type('array')) + ->will(function ($args, $query) { + return $query; + }) + ; + + $em = $this->prophesize(EntityManagerInterface::class); + $em->createNativeQuery(Argument::type('string'), Argument::type(Query\ResultSetMapping::class)) + ->will(function ($args) use ($queryResources, $queryCharges) { + if (false !== strpos($args[0], 'chill_budget.resource')) { + return $queryResources->reveal(); + } + if (false !== strpos($args[0], 'chill_budget.charge')) { + return $queryCharges->reveal(); + } + throw new \RuntimeException('this query does not have a stub counterpart: '.$args[0]); + }) + ; + + $chargeRepository = $this->prophesize(ChargeKindRepositoryInterface::class); + $chargeRepository->findAll()->willReturn([ + $rental = (new ChargeKind())->setKind('rental')->setName(['fr' => 'Rental']), + $other = (new ChargeKind())->setKind('other')->setName(['fr' => 'Other']), + ]); + $chargeRepository->find(1)->willReturn($rental); + $chargeRepository->findOneByKind('rental')->willReturn($rental); + $chargeRepository->findOneByKind('other')->willReturn($other); + + $resourceRepository = $this->prophesize(ResourceKindRepositoryInterface::class); + $resourceRepository->findAll()->willReturn([ + $salary = (new ResourceKind())->setKind('salary')->setName(['fr' => 'Salary']), + $misc = (new ResourceKind())->setKind('misc')->setName(['fr' => 'Misc']), + ]); + $resourceRepository->find(2)->willReturn($salary); + $resourceRepository->findOneByKind('salary')->willReturn($salary); + $resourceRepository->findOneByKind('misc')->willReturn($misc); + + $translatableStringHelper = $this->prophesize(TranslatableStringHelperInterface::class); + $translatableStringHelper->localize(Argument::type('array'))->will(function ($arg) { + return $arg[0]['fr']; + }); + + $person = new Person(); + $personReflection = new \ReflectionClass($person); + $personIdReflection = $personReflection->getProperty('id'); + $personIdReflection->setAccessible(true); + $personIdReflection->setValue($person, 1); + + $household = new Household(); + $householdReflection = new \ReflectionClass($household); + $householdId = $householdReflection->getProperty('id'); + $householdId->setAccessible(true); + $householdId->setValue($household, 1); + $householdMember = (new HouseholdMember())->setPerson($person) + ->setStartDate(new \DateTimeImmutable('1 month ago')) + ; + $household->addMember($householdMember); + + $summaryBudget = new SummaryBudget( + $em->reveal(), + $translatableStringHelper->reveal(), + $resourceRepository->reveal(), + $chargeRepository->reveal() + ); + + $summary = $summaryBudget->getSummaryForPerson($person); + $summaryForHousehold = $summaryBudget->getSummaryForHousehold($household); + + // we check the structure for the summary. The structure is the same for household + // and persons + + $expected = [ + 'charges' => [ + 'rental' => ['sum' => 250.0, 'comment' => '', 'label' => 'Rental'], + 'other' => ['sum' => 0.0, 'comment' => '', 'label' => 'Other'], + ], + 'resources' => [ + 'salary' => ['sum' => 1500.0, 'comment' => '', 'label' => 'Salary'], + 'misc' => ['sum' => 0.0, 'comment' => '', 'label' => 'Misc'], + ], + ]; + + foreach ([$summaryForHousehold, $summary] as $summary) { + $this->assertIsArray($summary); + $this->assertEqualsCanonicalizing(['charges', 'resources'], array_keys($summary)); + $this->assertEqualsCanonicalizing(['rental', 'other'], array_keys($summary['charges'])); + $this->assertEqualsCanonicalizing(['salary', 'misc'], array_keys($summary['resources'])); + + foreach ($expected as $resCha => $contains) { + foreach ($contains as $kind => $row) { + $this->assertEqualsCanonicalizing($row, $summary[$resCha][$kind]); + } + } + } + } +} diff --git a/src/Bundle/ChillBudgetBundle/config/services/config.yaml b/src/Bundle/ChillBudgetBundle/config/services/config.yaml deleted file mode 100644 index 21136db2f..000000000 --- a/src/Bundle/ChillBudgetBundle/config/services/config.yaml +++ /dev/null @@ -1,5 +0,0 @@ -services: - Chill\BudgetBundle\Config\ConfigRepository: - arguments: - $resources: '%chill_budget.resources%' - $charges: '%chill_budget.charges%' diff --git a/src/Bundle/ChillBudgetBundle/migrations/Version20230201131008.php b/src/Bundle/ChillBudgetBundle/migrations/Version20230201131008.php new file mode 100644 index 000000000..7131c31f2 --- /dev/null +++ b/src/Bundle/ChillBudgetBundle/migrations/Version20230201131008.php @@ -0,0 +1,37 @@ +addSql('COMMENT ON COLUMN chill_budget.resource_type.tags IS \'(DC2Type:json)\''); + $this->addSql('COMMENT ON COLUMN chill_budget.charge_type.tags IS \'(DC2Type:json)\''); + } +} diff --git a/src/Bundle/ChillBudgetBundle/translations/messages.fr.yml b/src/Bundle/ChillBudgetBundle/translations/messages.fr.yml index 5fd8fa6f6..8337dff87 100644 --- a/src/Bundle/ChillBudgetBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillBudgetBundle/translations/messages.fr.yml @@ -77,6 +77,13 @@ The balance: Différence entre ressources et charges Valid since %startDate% until %endDate%: Valide depuis le %startDate% jusqu'au %endDate% Valid since %startDate%: Valide depuis le %startDate% +# ROLES +Budget elements: Budget +CHILL_BUDGET_ELEMENT_CREATE: Créer une ressource/charge +CHILL_BUDGET_ELEMENT_DELETE: Supprimer une ressource/charge +CHILL_BUDGET_ELEMENT_SEE: Voir les ressources/charges +CHILL_BUDGET_ELEMENT_UPDATE: Modifier une ressource/charge + ## admin crud: diff --git a/src/Bundle/ChillCalendarBundle/Menu/AdminMenuBuilder.php b/src/Bundle/ChillCalendarBundle/Menu/AdminMenuBuilder.php index ad00609e2..1658029a7 100644 --- a/src/Bundle/ChillCalendarBundle/Menu/AdminMenuBuilder.php +++ b/src/Bundle/ChillCalendarBundle/Menu/AdminMenuBuilder.php @@ -39,7 +39,6 @@ class AdminMenuBuilder implements LocalMenuBuilderInterface ->setAttribute('class', 'list-group-item-header') ->setExtras([ 'order' => 6000, - 'icons' => ['calendar'], ]); $menu->addChild('Cancel reason', [ diff --git a/src/Bundle/ChillCalendarBundle/Resources/views/Calendar/_documents.twig.html b/src/Bundle/ChillCalendarBundle/Resources/views/Calendar/_documents.twig.html index a568ec009..499fb0a83 100644 --- a/src/Bundle/ChillCalendarBundle/Resources/views/Calendar/_documents.twig.html +++ b/src/Bundle/ChillCalendarBundle/Resources/views/Calendar/_documents.twig.html @@ -3,7 +3,7 @@ {% import "@ChillDocStore/Macro/macro.html.twig" as m %} {% import "@ChillDocStore/Macro/macro_mimeicon.html.twig" as mm %} -
+
diff --git a/src/Bundle/ChillCustomFieldsBundle/Menu/AdminMenuBuilder.php b/src/Bundle/ChillCustomFieldsBundle/Menu/AdminMenuBuilder.php index 7ac745b12..db63ad6ac 100644 --- a/src/Bundle/ChillCustomFieldsBundle/Menu/AdminMenuBuilder.php +++ b/src/Bundle/ChillCustomFieldsBundle/Menu/AdminMenuBuilder.php @@ -39,7 +39,6 @@ class AdminMenuBuilder implements LocalMenuBuilderInterface ->setAttribute('class', 'list-group-item-header') ->setExtras([ 'order' => 4500, - 'icons' => ['plus'], ]); $menu->addChild('Custom fields group', [ diff --git a/src/Bundle/ChillDocGeneratorBundle/Controller/DocGeneratorTemplateController.php b/src/Bundle/ChillDocGeneratorBundle/Controller/DocGeneratorTemplateController.php index e54c4ae34..d618f758a 100644 --- a/src/Bundle/ChillDocGeneratorBundle/Controller/DocGeneratorTemplateController.php +++ b/src/Bundle/ChillDocGeneratorBundle/Controller/DocGeneratorTemplateController.php @@ -272,8 +272,9 @@ final class DocGeneratorTemplateController extends AbstractController } if ($isTest && isset($form) && $form['show_data']->getData()) { - // very ugly hack... - dd($context->getData($template, $entity, $contextGenerationData)); + return $this->render('@ChillDocGenerator/Generator/debug_value.html.twig', [ + 'datas' => json_encode($context->getData($template, $entity, $contextGenerationData), JSON_PRETTY_PRINT) + ]); } try { diff --git a/src/Bundle/ChillDocGeneratorBundle/Resources/views/Generator/debug_value.html.twig b/src/Bundle/ChillDocGeneratorBundle/Resources/views/Generator/debug_value.html.twig new file mode 100644 index 000000000..c08a05c96 --- /dev/null +++ b/src/Bundle/ChillDocGeneratorBundle/Resources/views/Generator/debug_value.html.twig @@ -0,0 +1,8 @@ + + + {{ 'Doc generator debug'|trans }} + + +
{{ datas }}
+ + diff --git a/src/Bundle/ChillDocStoreBundle/Menu/AdminMenuBuilder.php b/src/Bundle/ChillDocStoreBundle/Menu/AdminMenuBuilder.php index 6848ed65a..466c3bace 100644 --- a/src/Bundle/ChillDocStoreBundle/Menu/AdminMenuBuilder.php +++ b/src/Bundle/ChillDocStoreBundle/Menu/AdminMenuBuilder.php @@ -39,7 +39,6 @@ class AdminMenuBuilder implements LocalMenuBuilderInterface ->setAttribute('class', 'list-group-item-header') ->setExtras([ 'order' => 4000, - 'icons' => ['file-pdf-o'], ]); $menu->addChild('Document category list', [ diff --git a/src/Bundle/ChillDocStoreBundle/Resources/public/module/document_action_buttons_group/index.ts b/src/Bundle/ChillDocStoreBundle/Resources/public/module/document_action_buttons_group/index.ts new file mode 100644 index 000000000..ec1d50a86 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Resources/public/module/document_action_buttons_group/index.ts @@ -0,0 +1,35 @@ +import {_createI18n} from "../../../../../ChillMainBundle/Resources/public/vuejs/_js/i18n"; +import DocumentActionButtonsGroup from "../../vuejs/DocumentActionButtonsGroup.vue"; +import {createApp} from "vue"; +import {StoredObject} from "../../types"; + +const i18n = _createI18n({}); + +window.addEventListener('DOMContentLoaded', function (e) { + document.querySelectorAll('div[data-download-buttons]').forEach((el) => { + const app = createApp({ + components: {DocumentActionButtonsGroup}, + data() { + + const datasets = el.dataset as { + filename: string, + canEdit: string, + storedObject: string, + small: string, + }; + + const + storedObject = JSON.parse(datasets.storedObject), + filename = datasets.filename, + canEdit = datasets.canEdit === '1', + small = datasets.small === '1' + ; + + return { storedObject, filename, canEdit, small }; + }, + template: '', + }); + + app.use(i18n).mount(el); + }) +}); diff --git a/src/Bundle/ChillDocStoreBundle/Resources/public/types.ts b/src/Bundle/ChillDocStoreBundle/Resources/public/types.ts new file mode 100644 index 000000000..918526117 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Resources/public/types.ts @@ -0,0 +1,25 @@ +import {DateTime} from "../../../ChillMainBundle/Resources/public/types"; + +export interface StoredObject { + id: number, + + /** + * filename of the object in the object storage + */ + filename: string, + creationDate: DateTime, + datas: object, + iv: number[], + keyInfos: object, + title: string, + type: string, + uuid: string +} + +/** + * Function executed by the WopiEditButton component. + */ +export type WopiEditButtonExecutableBeforeLeaveFunction = { + (): Promise +} + diff --git a/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentActionButtonsGroup.vue b/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentActionButtonsGroup.vue new file mode 100644 index 000000000..60b368cd3 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentActionButtonsGroup.vue @@ -0,0 +1,63 @@ + + + + + diff --git a/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/README.md b/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/README.md new file mode 100644 index 000000000..2d10dace8 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/README.md @@ -0,0 +1,5 @@ +# About buttons and components available + +## DocumentActionButtonsGroup + +This is an component to use to render a group of button with actions linked to a document. diff --git a/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/StoredObjectButton/ConvertButton.vue b/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/StoredObjectButton/ConvertButton.vue new file mode 100644 index 000000000..aa99a223f --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/StoredObjectButton/ConvertButton.vue @@ -0,0 +1,46 @@ + + + diff --git a/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/StoredObjectButton/DownloadButton.vue b/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/StoredObjectButton/DownloadButton.vue new file mode 100644 index 000000000..ca6a0f618 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/StoredObjectButton/DownloadButton.vue @@ -0,0 +1,53 @@ + + + diff --git a/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/StoredObjectButton/WopiEditButton.vue b/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/StoredObjectButton/WopiEditButton.vue new file mode 100644 index 000000000..d68f60f86 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/StoredObjectButton/WopiEditButton.vue @@ -0,0 +1,44 @@ + + + + + + diff --git a/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/StoredObjectButton/helpers.ts b/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/StoredObjectButton/helpers.ts new file mode 100644 index 000000000..d82efb111 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/StoredObjectButton/helpers.ts @@ -0,0 +1,179 @@ + +const MIMES_EDIT = new Set([ + 'application/vnd.ms-powerpoint', + 'application/vnd.ms-excel', + 'application/vnd.oasis.opendocument.text', + 'application/vnd.oasis.opendocument.text-flat-xml', + 'application/vnd.oasis.opendocument.spreadsheet', + 'application/vnd.oasis.opendocument.spreadsheet-flat-xml', + 'application/vnd.oasis.opendocument.presentation', + 'application/vnd.oasis.opendocument.presentation-flat-xml', + 'application/vnd.oasis.opendocument.graphics', + 'application/vnd.oasis.opendocument.graphics-flat-xml', + 'application/vnd.oasis.opendocument.chart', + 'application/msword', + 'application/vnd.ms-excel', + 'application/vnd.ms-powerpoint', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'application/vnd.ms-word.document.macroEnabled.12', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'application/vnd.ms-excel.sheet.binary.macroEnabled.12', + 'application/vnd.ms-excel.sheet.macroEnabled.12', + 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + 'application/vnd.ms-powerpoint.presentation.macroEnabled.12', + 'application/x-dif-document', + 'text/spreadsheet', + 'text/csv', + 'application/x-dbase', + 'text/rtf', + 'text/plain', + 'application/vnd.openxmlformats-officedocument.presentationml.slideshow', +]); + + + +const MIMES_VIEW = new Set([ + ...MIMES_EDIT, + [ + 'image/svg+xml', + 'application/vnd.sun.xml.writer', + 'application/vnd.sun.xml.calc', + 'application/vnd.sun.xml.impress', + 'application/vnd.sun.xml.draw', + 'application/vnd.sun.xml.writer.global', + 'application/vnd.sun.xml.writer.template', + 'application/vnd.sun.xml.calc.template', + 'application/vnd.sun.xml.impress.template', + 'application/vnd.sun.xml.draw.template', + 'application/vnd.oasis.opendocument.text-master', + 'application/vnd.oasis.opendocument.text-template', + 'application/vnd.oasis.opendocument.text-master-template', + 'application/vnd.oasis.opendocument.spreadsheet-template', + 'application/vnd.oasis.opendocument.presentation-template', + 'application/vnd.oasis.opendocument.graphics-template', + 'application/vnd.ms-word.template.macroEnabled.12', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.template', + 'application/vnd.ms-excel.template.macroEnabled.12', + 'application/vnd.openxmlformats-officedocument.presentationml.template', + 'application/vnd.ms-powerpoint.template.macroEnabled.12', + 'application/vnd.wordperfect', + 'application/x-aportisdoc', + 'application/x-hwp', + 'application/vnd.ms-works', + 'application/x-mswrite', + 'application/vnd.lotus-1-2-3', + 'image/cgm', + 'image/vnd.dxf', + 'image/x-emf', + 'image/x-wmf', + 'application/coreldraw', + 'application/vnd.visio2013', + 'application/vnd.visio', + 'application/vnd.ms-visio.drawing', + 'application/x-mspublisher', + 'application/x-sony-bbeb', + 'application/x-gnumeric', + 'application/macwriteii', + 'application/x-iwork-numbers-sffnumbers', + 'application/vnd.oasis.opendocument.text-web', + 'application/x-pagemaker', + 'application/x-fictionbook+xml', + 'application/clarisworks', + 'image/x-wpg', + 'application/x-iwork-pages-sffpages', + 'application/x-iwork-keynote-sffkey', + 'application/x-abiword', + 'image/x-freehand', + 'application/vnd.sun.xml.chart', + 'application/x-t602', + 'image/bmp', + 'image/png', + 'image/gif', + 'image/tiff', + 'image/jpg', + 'image/jpeg', + 'application/pdf', + ] +]) + +function is_extension_editable(mimeType: string): boolean { + return MIMES_EDIT.has(mimeType); +} + +function is_extension_viewable(mimeType: string): boolean { + return MIMES_VIEW.has(mimeType); +} + +function build_convert_link(uuid: string) { + return `/chill/wopi/convert/${uuid}`; +} + +function build_download_info_link(object_name: string) { + return `/asyncupload/temp_url/generate/GET?object_name=${object_name}`; +} + +function build_wopi_editor_link(uuid: string, returnPath?: string) { + if (returnPath === undefined) { + returnPath = window.location.pathname + window.location.search + window.location.hash; + } + + return `/chill/wopi/edit/${uuid}?returnPath=` + encodeURIComponent(returnPath); +} + +function download_doc(url: string): Promise { + return window.fetch(url).then(r => { + if (r.ok) { + return r.blob() + } + + throw new Error('Could not download document'); + }); +} + +async function download_and_decrypt_doc(urlGenerator: string, keyData: JsonWebKey, iv: Uint8Array): Promise +{ + const algo = 'AES-CBC'; + // get an url to download the object + const downloadInfoResponse = await window.fetch(urlGenerator); + + if (!downloadInfoResponse.ok) { + throw new Error("error while downloading url " + downloadInfoResponse.status + " " + downloadInfoResponse.statusText); + } + + const downloadInfo = await downloadInfoResponse.json() as {url: string}; + const rawResponse = await window.fetch(downloadInfo.url); + + if (!rawResponse.ok) { + throw new Error("error while downloading raw file " + rawResponse.status + " " + rawResponse.statusText); + } + + if (iv.length === 0) { + return rawResponse.blob(); + } + + const rawBuffer = await rawResponse.arrayBuffer(); + + try { + const key = await window.crypto.subtle + .importKey('jwk', keyData, { name: algo }, false, ['decrypt']); + const decrypted = await window.crypto.subtle + .decrypt({ name: algo, iv: iv }, key, rawBuffer); + + return Promise.resolve(new Blob([decrypted])); + } catch (e) { + console.error('get error while keys and decrypt operations'); + console.error(e); + + throw e; + } +} + +export { + build_convert_link, + build_download_info_link, + build_wopi_editor_link, + download_and_decrypt_doc, + download_doc, + is_extension_editable, + is_extension_viewable, +}; diff --git a/src/Bundle/ChillDocStoreBundle/Resources/views/AccompanyingCourseDocument/_workflow.html.twig b/src/Bundle/ChillDocStoreBundle/Resources/views/AccompanyingCourseDocument/_workflow.html.twig index 299183cca..58d4903b4 100644 --- a/src/Bundle/ChillDocStoreBundle/Resources/views/AccompanyingCourseDocument/_workflow.html.twig +++ b/src/Bundle/ChillDocStoreBundle/Resources/views/AccompanyingCourseDocument/_workflow.html.twig @@ -6,7 +6,7 @@ {{ 'workflow.Document deleted'|trans }} {% else %} -
+
@@ -22,7 +22,6 @@ {{ document.description }} {% endif %} -
@@ -47,21 +46,8 @@ {% endif %}
  • - {{ m.download_button(document.object, document.title) }} + {{ document.object|chill_document_button_group(document.title, not freezed) }}
  • - {% if chill_document_is_editable(document.object) %} - {% if not freezed %} -
  • - {{ document.object|chill_document_edit_button({'title': document.title|e('html') }) }} -
  • - {% else %} -
  • - - {{ 'Update document'|trans }} - -
  • - {% endif %} - {% endif %} {% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_SEE', document) and document.course != null %}
  • diff --git a/src/Bundle/ChillDocStoreBundle/Resources/views/AccompanyingCourseDocument/index.html.twig b/src/Bundle/ChillDocStoreBundle/Resources/views/AccompanyingCourseDocument/index.html.twig index df18efa71..7a013260c 100644 --- a/src/Bundle/ChillDocStoreBundle/Resources/views/AccompanyingCourseDocument/index.html.twig +++ b/src/Bundle/ChillDocStoreBundle/Resources/views/AccompanyingCourseDocument/index.html.twig @@ -8,16 +8,16 @@ {% block js %} {{ parent() }} - {{ encore_entry_script_tags('mod_async_upload') }} {{ encore_entry_script_tags('mod_docgen_picktemplate') }} {{ encore_entry_script_tags('mod_entity_workflow_pick') }} + {{ encore_entry_script_tags('mod_document_action_buttons_group') }} {% endblock %} {% block css %} {{ parent() }} - {{ encore_entry_link_tags('mod_async_upload') }} {{ encore_entry_link_tags('mod_docgen_picktemplate') }} {{ encore_entry_link_tags('mod_entity_workflow_pick') }} + {{ encore_entry_link_tags('mod_document_action_buttons_group') }} {% endblock %} {% block content %} diff --git a/src/Bundle/ChillDocStoreBundle/Resources/views/AccompanyingCourseDocument/show.html.twig b/src/Bundle/ChillDocStoreBundle/Resources/views/AccompanyingCourseDocument/show.html.twig index 45ed3988b..3c62451a9 100644 --- a/src/Bundle/ChillDocStoreBundle/Resources/views/AccompanyingCourseDocument/show.html.twig +++ b/src/Bundle/ChillDocStoreBundle/Resources/views/AccompanyingCourseDocument/show.html.twig @@ -14,6 +14,7 @@ {{ parent() }} {{ encore_entry_link_tags('mod_async_upload') }} {{ encore_entry_link_tags('mod_entity_workflow_pick') }} + {{ encore_entry_link_tags('mod_document_action_buttons_group') }} {% endblock %} {% block content %} @@ -61,13 +62,8 @@
  • {% endif %}
  • - {{ m.download_button(document.object, document.title) }} + {{ document.object|chill_document_button_group(document.title) }}
  • - {% if chill_document_is_editable(document.object) %} -
  • - {{ document.object|chill_document_edit_button }} -
  • - {% endif %} {% set workflows_frame = chill_entity_workflow_list('Chill\\DocStoreBundle\\Entity\\AccompanyingCourseDocument', document.id) %} {% if workflows_frame is not empty %}
  • @@ -86,4 +82,5 @@ {{ parent() }} {{ encore_entry_script_tags('mod_async_upload') }} {{ encore_entry_script_tags('mod_entity_workflow_pick') }} + {{ encore_entry_script_tags('mod_document_action_buttons_group') }} {% endblock %} diff --git a/src/Bundle/ChillDocStoreBundle/Resources/views/Button/button_group.html.twig b/src/Bundle/ChillDocStoreBundle/Resources/views/Button/button_group.html.twig new file mode 100644 index 000000000..f83cafd51 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Resources/views/Button/button_group.html.twig @@ -0,0 +1,7 @@ +{%- import "@ChillDocStore/Macro/macro.html.twig" as m -%} +
    diff --git a/src/Bundle/ChillDocStoreBundle/Resources/views/List/list_item.html.twig b/src/Bundle/ChillDocStoreBundle/Resources/views/List/list_item.html.twig index 3963b0715..4cbcb7bef 100644 --- a/src/Bundle/ChillDocStoreBundle/Resources/views/List/list_item.html.twig +++ b/src/Bundle/ChillDocStoreBundle/Resources/views/List/list_item.html.twig @@ -53,15 +53,10 @@
  • - {% if chill_document_is_editable(document.object) %} -
  • - {{ document.object|chill_document_edit_button }} -
  • - {% endif %} {% endif %} {% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_SEE_DETAILS', document) %}
  • - {{ m.download_button(document.object, document.title) }} + {{ document.object|chill_document_button_group(document.title, is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_UPDATE', document)) }}
  • @@ -80,15 +75,10 @@
  • - {% if chill_document_is_editable(document.object) %} -
  • - {{ document.object|chill_document_edit_button }} -
  • - {% endif %} {% endif %} {% if is_granted('CHILL_PERSON_DOCUMENT_SEE_DETAILS', document) %}
  • - {{ m.download_button(document.object, document.title) }} + {{ document.object|chill_document_button_group(document.title, is_granted('CHILL_PERSON_DOCUMENT_UPDATE', document)) }}
  • diff --git a/src/Bundle/ChillDocStoreBundle/Resources/views/Macro/macro.html.twig b/src/Bundle/ChillDocStoreBundle/Resources/views/Macro/macro.html.twig index 886544b1e..199a86c15 100644 --- a/src/Bundle/ChillDocStoreBundle/Resources/views/Macro/macro.html.twig +++ b/src/Bundle/ChillDocStoreBundle/Resources/views/Macro/macro.html.twig @@ -2,14 +2,47 @@ {% if storedObject is null %} {% else %} - {{ 'Download'|trans }} {% endif %} {% endmacro %} + +{% macro download_button_small(storedObject, filename = null) %} + {% if storedObject is null %} + + {% else %} + + {{ 'Download'|trans }} + {% endif %} +{% endmacro %} + +{% macro download_button_group(storedObject, canEdit = true, filename = null, options = {}) %} + {% if storedObject is null %} + + {% else %} +
    + {% endif %} +{% endmacro %} diff --git a/src/Bundle/ChillDocStoreBundle/Resources/views/Macro/macro_mimeicon.html.twig b/src/Bundle/ChillDocStoreBundle/Resources/views/Macro/macro_mimeicon.html.twig index a9bc07e2f..7079a0d94 100644 --- a/src/Bundle/ChillDocStoreBundle/Resources/views/Macro/macro_mimeicon.html.twig +++ b/src/Bundle/ChillDocStoreBundle/Resources/views/Macro/macro_mimeicon.html.twig @@ -48,8 +48,8 @@ {% endif %} {% endfor %} - + {% endmacro %} \ No newline at end of file diff --git a/src/Bundle/ChillDocStoreBundle/Resources/views/PersonDocument/index.html.twig b/src/Bundle/ChillDocStoreBundle/Resources/views/PersonDocument/index.html.twig index 5dc359fa8..8d201605e 100644 --- a/src/Bundle/ChillDocStoreBundle/Resources/views/PersonDocument/index.html.twig +++ b/src/Bundle/ChillDocStoreBundle/Resources/views/PersonDocument/index.html.twig @@ -27,16 +27,16 @@ {% block js %} {{ parent() }} - {{ encore_entry_script_tags('mod_async_upload') }} {{ encore_entry_script_tags('mod_docgen_picktemplate') }} {{ encore_entry_script_tags('mod_entity_workflow_pick') }} + {{ encore_entry_script_tags('mod_document_action_buttons_group') }} {% endblock %} {% block css %} {{ parent() }} - {{ encore_entry_link_tags('mod_async_upload') }} {{ encore_entry_link_tags('mod_docgen_picktemplate') }} {{ encore_entry_link_tags('mod_entity_workflow_pick') }} + {{ encore_entry_link_tags('mod_document_action_buttons_group') }} {% endblock %} {% block content %} diff --git a/src/Bundle/ChillDocStoreBundle/Resources/views/PersonDocument/show.html.twig b/src/Bundle/ChillDocStoreBundle/Resources/views/PersonDocument/show.html.twig index b26aa4b8b..c276e067e 100644 --- a/src/Bundle/ChillDocStoreBundle/Resources/views/PersonDocument/show.html.twig +++ b/src/Bundle/ChillDocStoreBundle/Resources/views/PersonDocument/show.html.twig @@ -24,7 +24,11 @@ {% block title %}{{ 'Detail of document of %name%'|trans({ '%name%': person|chill_entity_render_string } ) }}{% endblock %} {% block js %} - {{ encore_entry_script_tags('mod_async_upload') }} + {{ encore_entry_script_tags('mod_document_action_buttons_group') }} +{% endblock %} + +{% block css %} + {{ encore_entry_link_tags('mod_document_action_buttons_group') }} {% endblock %} {% block content %} @@ -70,6 +74,10 @@
  • {% endif %} +
  • + {{ document.object|chill_document_button_group(document.title, is_granted('CHILL_PERSON_DOCUMENT_UPDATE', document)) }} +
  • + {% if is_granted('CHILL_PERSON_DOCUMENT_UPDATE', document) %}
  • @@ -77,16 +85,4 @@
  • {% endif %} - -
  • - {{ m.download_button(document.object, document.title) }} -
  • - - {% if chill_document_is_editable(document.object) %} -
  • - {{ document.object|chill_document_edit_button }} -
  • - {% endif %} - - {# {{ include('ChillDocStoreBundle:PersonDocument:_delete_form.html.twig') }} #} {% endblock %} diff --git a/src/Bundle/ChillDocStoreBundle/Templating/WopiEditTwigExtension.php b/src/Bundle/ChillDocStoreBundle/Templating/WopiEditTwigExtension.php index 43dc28f15..754c3fb3c 100644 --- a/src/Bundle/ChillDocStoreBundle/Templating/WopiEditTwigExtension.php +++ b/src/Bundle/ChillDocStoreBundle/Templating/WopiEditTwigExtension.php @@ -24,6 +24,10 @@ class WopiEditTwigExtension extends AbstractExtension 'needs_environment' => true, 'is_safe' => ['html'], ]), + new TwigFilter('chill_document_button_group', [WopiEditTwigExtensionRuntime::class, 'renderButtonGroup'], [ + 'needs_environment' => true, + 'is_safe' => ['html'], + ]), ]; } diff --git a/src/Bundle/ChillDocStoreBundle/Templating/WopiEditTwigExtensionRuntime.php b/src/Bundle/ChillDocStoreBundle/Templating/WopiEditTwigExtensionRuntime.php index f53d6336b..bae436b0a 100644 --- a/src/Bundle/ChillDocStoreBundle/Templating/WopiEditTwigExtensionRuntime.php +++ b/src/Bundle/ChillDocStoreBundle/Templating/WopiEditTwigExtensionRuntime.php @@ -13,6 +13,8 @@ namespace Chill\DocStoreBundle\Templating; use ChampsLibres\WopiLib\Contract\Service\Discovery\DiscoveryInterface; use Chill\DocStoreBundle\Entity\StoredObject; +use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; use Twig\Environment; use Twig\Extension\RuntimeExtensionInterface; @@ -112,20 +114,53 @@ final class WopiEditTwigExtensionRuntime implements RuntimeExtensionInterface 'application/pdf', ]; + private const DEFAULT_OPTIONS_TEMPLATE_BUTTON_GROUP = [ + 'small' => false, + ]; + private const TEMPLATE = '@ChillDocStore/Button/wopi_edit_document.html.twig'; + private const TEMPLATE_BUTTON_GROUP = '@ChillDocStore/Button/button_group.html.twig'; + private DiscoveryInterface $discovery; - public function __construct(DiscoveryInterface $discovery) + private NormalizerInterface $normalizer; + + public function __construct(DiscoveryInterface $discovery, NormalizerInterface $normalizer) { $this->discovery = $discovery; + $this->normalizer = $normalizer; } + /** + * return true if the document is editable. + * + * **NOTE**: as the Vue button does have similar test, this is not required if in use with + * the dedicated Vue component (GroupDownloadButton.vue, WopiEditButton.vue) + */ public function isEditable(StoredObject $document): bool { return in_array($document->getType(), self::SUPPORTED_MIMES, true); } + /** + * @param array{small: boolean} $options + * + * @throws \Twig\Error\LoaderError + * @throws \Twig\Error\RuntimeError + * @throws \Twig\Error\SyntaxError + */ + public function renderButtonGroup(Environment $environment, StoredObject $document, ?string $title = null, bool $canEdit = true, array $options = []): string + { + return $environment->render(self::TEMPLATE_BUTTON_GROUP, [ + 'document' => $document, + 'document_json' => $this->normalizer->normalize($document, 'json', [AbstractNormalizer::GROUPS => ['read']]), + 'title' => $title, + 'can_edit' => $canEdit, + 'options' => array_merge($options, self::DEFAULT_OPTIONS_TEMPLATE_BUTTON_GROUP), + ]); + } + public function renderEditButton(Environment $environment, StoredObject $document, ?array $options = null): string { return $environment->render(self::TEMPLATE, [ diff --git a/src/Bundle/ChillDocStoreBundle/chill.webpack.config.js b/src/Bundle/ChillDocStoreBundle/chill.webpack.config.js index c9b2b8877..3499fcf55 100644 --- a/src/Bundle/ChillDocStoreBundle/chill.webpack.config.js +++ b/src/Bundle/ChillDocStoreBundle/chill.webpack.config.js @@ -4,4 +4,5 @@ module.exports = function(encore) ChillDocStoreAssets: __dirname + '/Resources/public' }); encore.addEntry('mod_async_upload', __dirname + '/Resources/public/module/async_upload/index.js'); + encore.addEntry('mod_document_action_buttons_group', __dirname + '/Resources/public/module/document_action_buttons_group/index'); }; diff --git a/src/Bundle/ChillDocStoreBundle/translations/messages.fr.yml b/src/Bundle/ChillDocStoreBundle/translations/messages.fr.yml index 400e37236..b658dbbee 100644 --- a/src/Bundle/ChillDocStoreBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillDocStoreBundle/translations/messages.fr.yml @@ -14,7 +14,7 @@ Edit attributes: Modifier les propriétés du document Existing document: Document existant No document to download: Aucun document à télécharger 'Choose a document category': Choisissez une catégorie de document -Any document found: Aucun document trouvé +No document found: Aucun document trouvé The document is successfully registered: Le document est enregistré The document is successfully updated: Le document est mis à jour Any description: Aucune description @@ -66,3 +66,11 @@ online_edit_document: Éditer en ligne workflow: Document deleted: Document supprimé + +# ROLES +accompanyingCourseDocument: Documents dans les parcours d'accompagnement +CHILL_ACCOMPANYING_COURSE_DOCUMENT_CREATE: Créer un document +CHILL_ACCOMPANYING_COURSE_DOCUMENT_DELETE: Supprimer un document +CHILL_ACCOMPANYING_COURSE_DOCUMENT_SEE: Voir les documents +CHILL_ACCOMPANYING_COURSE_DOCUMENT_SEE_DETAILS: Voir les détails d'un document +CHILL_ACCOMPANYING_COURSE_DOCUMENT_UPDATE: Modifier un document \ No newline at end of file diff --git a/src/Bundle/ChillMainBundle/Controller/LocationController.php b/src/Bundle/ChillMainBundle/Controller/LocationController.php index f3c4db082..232c9c6cf 100644 --- a/src/Bundle/ChillMainBundle/Controller/LocationController.php +++ b/src/Bundle/ChillMainBundle/Controller/LocationController.php @@ -28,7 +28,7 @@ class LocationController extends CRUDController protected function customizeQuery(string $action, Request $request, $query): void { - $query->where('e.availableForUsers = true'); //TODO not working + $query->where('e.availableForUsers = "TRUE"'); } protected function orderQuery(string $action, $query, Request $request, PaginatorInterface $paginator) diff --git a/src/Bundle/ChillMainBundle/Controller/ScopeApiController.php b/src/Bundle/ChillMainBundle/Controller/ScopeApiController.php new file mode 100644 index 000000000..85041f0f3 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Controller/ScopeApiController.php @@ -0,0 +1,25 @@ +andWhere($query->expr()->eq('e.active', "'TRUE'")); + } + } +} diff --git a/src/Bundle/ChillMainBundle/Controller/UserApiController.php b/src/Bundle/ChillMainBundle/Controller/UserApiController.php index b29fe7c8e..da873e118 100644 --- a/src/Bundle/ChillMainBundle/Controller/UserApiController.php +++ b/src/Bundle/ChillMainBundle/Controller/UserApiController.php @@ -12,6 +12,7 @@ declare(strict_types=1); namespace Chill\MainBundle\Controller; use Chill\MainBundle\CRUD\Controller\ApiController; +use Chill\MainBundle\Pagination\PaginatorInterface; use Doctrine\ORM\QueryBuilder; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; @@ -70,4 +71,13 @@ class UserApiController extends ApiController $query->andWhere($query->expr()->eq('e.enabled', "'TRUE'")); } } + + /** + * @param mixed $query + * @param mixed $_format + */ + protected function orderQuery(string $action, $query, Request $request, PaginatorInterface $paginator, $_format) + { + return $query->orderBy('e.label', 'ASC'); + } } diff --git a/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php b/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php index 23a4bee89..69546016a 100644 --- a/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php +++ b/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php @@ -655,6 +655,7 @@ class ChillMainExtension extends Extension implements ], [ 'class' => \Chill\MainBundle\Entity\Scope::class, + 'controller' => \Chill\MainBundle\Controller\ScopeApiController::class, 'name' => 'scope', 'base_path' => '/api/1.0/main/scope', 'base_role' => 'ROLE_USER', diff --git a/src/Bundle/ChillMainBundle/Export/ExportInterface.php b/src/Bundle/ChillMainBundle/Export/ExportInterface.php index be43ad47a..79b59d241 100644 --- a/src/Bundle/ChillMainBundle/Export/ExportInterface.php +++ b/src/Bundle/ChillMainBundle/Export/ExportInterface.php @@ -83,9 +83,9 @@ interface ExportInterface extends ExportElementInterface * * @param string $key The column key, as added in the query * @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` + * @param mixed $data The data from the export's form (as defined in `buildForm`) * - * @return Closure 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 pure-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); diff --git a/src/Bundle/ChillMainBundle/Export/Helper/AggregateStringHelper.php b/src/Bundle/ChillMainBundle/Export/Helper/AggregateStringHelper.php new file mode 100644 index 000000000..6266ce86f --- /dev/null +++ b/src/Bundle/ChillMainBundle/Export/Helper/AggregateStringHelper.php @@ -0,0 +1,36 @@ +add('name', TranslatableStringFormType::class); + ->add('name', TranslatableStringFormType::class) + ->add('active', ChoiceType::class, [ + 'choices' => [ + 'Active' => true, + 'Inactive' => false, + ], ]); } /** diff --git a/src/Bundle/ChillMainBundle/Resources/public/chill/chillmain.scss b/src/Bundle/ChillMainBundle/Resources/public/chill/chillmain.scss index aa1d24844..3c9fc8601 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/chill/chillmain.scss +++ b/src/Bundle/ChillMainBundle/Resources/public/chill/chillmain.scss @@ -221,12 +221,8 @@ footer.footer { */ div.admin { - flex-direction: row-reverse; div.vertical-menu { - font-size: 0.9em; - .list-group-item { - padding: 0.3rem 0.7rem; - } + .list-group-item {} } } @@ -307,12 +303,12 @@ table.table-bordered { /// meta-data div.createdBy, div.updatedBy, -div.metadata { +.metadata { span.user, span.date { text-decoration: underline dotted; } } -div.metadata { +.metadata { font-size: smaller; color: $gray-600; span.user, span.date { @@ -368,6 +364,19 @@ div#flashMessages { } } +/// unbullet lists +ul.unbullet { + list-style-type: none; + padding-left: 0; +} +/// libellé +span.dt { + font-size: 90%; + font-weight: bolder; + background-color: var(--bs-chill-light-gray); +} + + /* * SPECIFIC RULES */ diff --git a/src/Bundle/ChillMainBundle/Resources/public/chill/scss/render_box.scss b/src/Bundle/ChillMainBundle/Resources/public/chill/scss/render_box.scss index 4d0bfcf8e..19da5aed3 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/chill/scss/render_box.scss +++ b/src/Bundle/ChillMainBundle/Resources/public/chill/scss/render_box.scss @@ -106,18 +106,5 @@ section.chill-entity { // used for comment-embeddable &.entity-comment-embeddable { width: 100%; - - /* already defined !! - div.metadata { - font-size: smaller; - color: $gray-600; - span.user, span.date { - text-decoration: underline dotted; - &:hover { - color: $gray-700; - } - } - } - */ } } diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress.vue index 30a240938..21ab5e3d0 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress.vue +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress.vue @@ -48,33 +48,35 @@ -
    - - - - - - -
    + @@ -118,40 +120,42 @@ -
    - - - - - - -
    + @@ -192,32 +196,34 @@ -
    - - - - diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/EditPane.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/EditPane.vue index 4ab117395..c0e42a7b1 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/EditPane.vue +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/EditPane.vue @@ -187,6 +187,7 @@ div.address-form { div#address_map { height: 400px; width: 100%; + z-index: 1; } } diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/ShowPane.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/ShowPane.vue index bc246d542..5048815e2 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/ShowPane.vue +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/ShowPane.vue @@ -1,6 +1,6 @@