Merge branch 'master' into 'issue428_person_resource_ameliorations'

# Conflicts:
#   CHANGELOG.md
This commit is contained in:
LenaertsJ 2022-01-31 17:50:08 +00:00
commit 2c566bb21c
79 changed files with 2597 additions and 477 deletions

View File

@ -11,6 +11,16 @@ and this project adheres to
## Unreleased ## Unreleased
<!-- write down unreleased development here --> <!-- write down unreleased development here -->
* renommer "dossier numéro" en "parcours numéro" dans les résultats de recherche
* renomme date de début en date d'ouverture dans le formulaire parcours
## Test releases
### test release 2021-01-31
[fast_actions] improve fast-actions buttons override mechanism, fix https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/413
[homepage widget] add vue homepage_widget with asynchone loading, give a global view resume of the user concerned actions, notifications, etc.
* [person] accompanying course: optimisation: do not fetch some resources for the banner (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/409) * [person] accompanying course: optimisation: do not fetch some resources for the banner (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/409)
* [person] accompanying course: close modal when edit participation (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/420) * [person] accompanying course: close modal when edit participation (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/420)
* [person] accompanying course: treat validation error when editing on-the-fly entities (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/420) * [person] accompanying course: treat validation error when editing on-the-fly entities (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/420)
@ -22,9 +32,9 @@ and this project adheres to
* [person] age added to renderstring + renderbox/ vue component created to display person text (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/389) * [person] age added to renderstring + renderbox/ vue component created to display person text (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/389)
* [household member editor] allow to push to existing household * [household member editor] allow to push to existing household
* [person_resource]: Onthefly button added to view person/thirdparty and badge differentiation for a contact-thirdparty (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/428) * [person_resource]: Onthefly button added to view person/thirdparty and badge differentiation for a contact-thirdparty (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/428)
* [workflow][notification] improve how notifications and workflows are 'attached' to entities: contextual list, counter, buttons and vue modal
## Test releases
### test release 2021-01-28 ### test release 2021-01-28
@ -40,7 +50,6 @@ and this project adheres to
### test release 2021-01-26 ### test release 2021-01-26
>>>>>>> origin/master
* [parcours] comments truncated if too long + link added (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/406) * [parcours] comments truncated if too long + link added (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/406)
* [person]: possibility to add person resources (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/382) * [person]: possibility to add person resources (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/382)
* [person ressources]: module added * [person ressources]: module added

View File

@ -80,11 +80,6 @@ parameters:
count: 1 count: 1
path: src/Bundle/ChillPersonBundle/Form/ChoiceLoader/PersonChoiceLoader.php path: src/Bundle/ChillPersonBundle/Form/ChoiceLoader/PersonChoiceLoader.php
-
message: "#^Foreach overwrites \\$action with its value variable\\.$#"
count: 1
path: src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkRepository.php
- -
message: "#^Foreach overwrites \\$action with its value variable\\.$#" message: "#^Foreach overwrites \\$action with its value variable\\.$#"
count: 1 count: 1

View File

@ -30,36 +30,6 @@ parameters:
count: 2 count: 2
path: src/Bundle/ChillPersonBundle/Household/MembersEditorFactory.php path: src/Bundle/ChillPersonBundle/Household/MembersEditorFactory.php
-
message: "#^Parameter \\$action of method Chill\\\\PersonBundle\\\\Repository\\\\AccompanyingPeriod\\\\AccompanyingPeriodWorkRepository\\:\\:buildQueryBySocialActionWithDescendants\\(\\) has invalid type Chill\\\\PersonBundle\\\\Repository\\\\AccompanyingPeriod\\\\SocialAction\\.$#"
count: 1
path: src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkRepository.php
-
message: "#^Parameter \\$action of method Chill\\\\PersonBundle\\\\Repository\\\\AccompanyingPeriod\\\\AccompanyingPeriodWorkRepository\\:\\:countBySocialActionWithDescendants\\(\\) has invalid type Chill\\\\PersonBundle\\\\Repository\\\\AccompanyingPeriod\\\\SocialAction\\.$#"
count: 1
path: src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkRepository.php
-
message: "#^Undefined variable\\: \\$action$#"
count: 1
path: src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkRepository.php
-
message: "#^Undefined variable\\: \\$limit$#"
count: 1
path: src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkRepository.php
-
message: "#^Undefined variable\\: \\$offset$#"
count: 1
path: src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkRepository.php
-
message: "#^Undefined variable\\: \\$orderBy$#"
count: 1
path: src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkRepository.php
- -
message: "#^Variable variables are not allowed\\.$#" message: "#^Variable variables are not allowed\\.$#"
count: 4 count: 4

View File

@ -400,11 +400,6 @@ parameters:
count: 1 count: 1
path: src/Bundle/ChillPersonBundle/Form/Type/PersonPhoneType.php path: src/Bundle/ChillPersonBundle/Form/Type/PersonPhoneType.php
-
message: "#^Method Chill\\\\PersonBundle\\\\Repository\\\\AccompanyingPeriod\\\\AccompanyingPeriodWorkRepository\\:\\:buildQueryBySocialActionWithDescendants\\(\\) has invalid return type Chill\\\\PersonBundle\\\\Repository\\\\AccompanyingPeriod\\\\QueryBuilder\\.$#"
count: 1
path: src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkRepository.php
- -
message: "#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#" message: "#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#"
count: 3 count: 3

View File

@ -34,6 +34,8 @@ p.date-label {
font-size: 18pt; font-size: 18pt;
} }
div.dashboard, div.dashboard,
h4.badge-title,
h3.badge-title,
h2.badge-title { h2.badge-title {
ul.list-content { ul.list-content {
font-size: 70%; font-size: 70%;

View File

@ -2,10 +2,12 @@
{% if is_granted('CHILL_ACTIVITY_SEE_DETAILS', activity) %} {% if is_granted('CHILL_ACTIVITY_SEE_DETAILS', activity) %}
{% if no_action is not defined or no_action == false %} {% if no_action is not defined or no_action == false %}
<li> <li>
<a class="btn btn-notify" href="{{ chill_path_add_return_path('chill_main_notification_create', { <a class="btn btn-misc" href="{{ chill_path_add_return_path('chill_main_notification_create', {
'entityClass': 'Chill\\ActivityBundle\\Entity\\Activity', 'entityClass': 'Chill\\ActivityBundle\\Entity\\Activity',
'entityId': activity.id 'entityId': activity.id
}) }}">{{ 'notification.Notify'|trans }}</a> }) }}">
<i class="fa fa-paper-plane fa-fw"></i>
{{ 'notification.Notify'|trans }}</a>
</li> </li>
{% endif %} {% endif %}
{% if context == 'person' and activity.accompanyingPeriod is not empty %} {% if context == 'person' and activity.accompanyingPeriod is not empty %}

View File

@ -177,6 +177,13 @@
</div> </div>
</div> </div>
<div class="notification notification-list">
{% set notifications = chill_list_notifications('Chill\\ActivityBundle\\Entity\\Activity', entity.id) %}
{% if notifications is not empty %}
{{ notifications|raw }}
{% endif %}
</div>
{% set person_id = null %} {% set person_id = null %}
{% if person %} {% if person %}
{% set person_id = person.id %} {% set person_id = person.id %}
@ -193,18 +200,21 @@
{{ 'Back to the list'|trans }} {{ 'Back to the list'|trans }}
</a> </a>
</li> </li>
{% if is_granted('CHILL_ACTIVITY_UPDATE', entity) %} <li>
<li> <a class="btn btn-notify" href="{{ chill_path_add_return_path('chill_main_notification_create', {'entityClass': 'Chill\\ActivityBundle\\Entity\\Activity', 'entityId': entity.id}) }}">
<a class="btn btn-update" href="{{ path('chill_activity_activity_edit', { 'id': entity.id, 'person_id': person_id, 'accompanying_period_id': accompanying_course_id }) }}"> {{ 'notification.Notify'|trans }}
{{ 'Edit'|trans }}
</a> </a>
</li> </li>
{% if is_granted('CHILL_ACTIVITY_UPDATE', entity) %}
<li>
<a href="{{ path('chill_activity_activity_edit', { 'id': entity.id, 'person_id': person_id, 'accompanying_period_id': accompanying_course_id }) }}"
class="btn btn-update">{{ 'Edit'|trans }}</a>
</li>
{% endif %} {% endif %}
{% if is_granted('CHILL_ACTIVITY_DELETE', entity) %} {% if is_granted('CHILL_ACTIVITY_DELETE', entity) %}
<li> <li>
<a href="{{ path('chill_activity_activity_delete', { 'id': entity.id, 'person_id' : person_id, 'accompanying_period_id': accompanying_course_id } ) }}" class="btn btn-delete"> <a href="{{ path('chill_activity_activity_delete', { 'id': entity.id, 'person_id' : person_id, 'accompanying_period_id': accompanying_course_id } ) }}"
{{ 'Delete'|trans }} class="btn btn-delete" title="{{ 'Delete'|trans }}"></a>
</a>
</li> </li>
{% endif %} {% endif %}
</ul> </ul>

View File

@ -25,19 +25,5 @@
{% endblock content %} {% endblock content %}
{% block block_post_menu %} {% block block_post_menu %}
<div class="post-menu pt-4"> <div class="post-menu pt-4"></div>
<div class="d-grid gap-2">
<a class="btn btn-primary" href="{{ chill_path_add_return_path('chill_main_notification_create', {'entityClass': 'Chill\\ActivityBundle\\Entity\\Activity', 'entityId': entity.id}) }}">
<i class="fa fa-paper-plane fa-fw"></i>
{{ 'notification.Notify'|trans }}
</a>
</div>
{% set notifications = chill_list_notifications('Chill\\ActivityBundle\\Entity\\Activity', entity.id) %}
{% if notifications is not empty %}
{{ notifications|raw }}
{% endif %}
</div>
{% endblock %} {% endblock %}

View File

@ -28,8 +28,7 @@
<div class="post-menu pt-4"> <div class="post-menu pt-4">
<div class="d-grid gap-2"> <div class="d-grid gap-2">
<a class="btn btn-primary" href="{{ chill_path_add_return_path('chill_main_notification_create', {'entityClass': 'Chill\\ActivityBundle\\Entity\\Activity', 'entityId': entity.id}) }}"> <a class="btn btn-notify" href="{{ chill_path_add_return_path('chill_main_notification_create', {'entityClass': 'Chill\\ActivityBundle\\Entity\\Activity', 'entityId': entity.id}) }}">
<i class="fa fa-paper-plane fa-fw"></i>
{{ 'notification.Notify'|trans }} {{ 'notification.Notify'|trans }}
</a> </a>
</div> </div>

View File

@ -21,24 +21,28 @@
</div> </div>
</div> </div>
{% set freezed = false %}
{% for step in entity_workflow.stepsChained %}
{% if loop.last %}
{% if step.previous is not null and step.previous.freezeAfter == true %}
{% set freezed = true %}
{% endif %}
{% endif %}
{% endfor %}
{% if display_action is defined and display_action == true %} {% if display_action is defined and display_action == true %}
<ul class="record_actions"> <ul class="record_actions">
<li> <li>
{{ m.download_button(document.object, document.title) }} {{ m.download_button(document.object, document.title) }}
</li> </li>
<li> <li>
{% if not freezed %}
{# {% set button = {
data-button is optional ! 'changeIcon': 'fa-unlock',
OPTIONS: } %}{#
'changeIcon' string
'changeClass' string 'changeClass' string
'noText' boolean 'noText' boolean
#}
#}{% set button = {
'changeIcon': 'fa-unlock',
} %}
{# vue component #} {# vue component #}
<span <span
data-module="wopi-link" data-module="wopi-link"
@ -47,6 +51,11 @@
data-doc-type="{{ document.object.type|e('html_attr') }}" data-doc-type="{{ document.object.type|e('html_attr') }}"
data-button="{{ button|json_encode }}" data-button="{{ button|json_encode }}"
></span> ></span>
{% else %}
<a class="btn btn-update change-icon disabled" href="#" title="{{ 'workflow.freezed document'|trans }}">
<i class="fa fa-lock me-2"></i>{{ 'Update document'|trans }}
</a>
{% endif %}
</li> </li>
</ul> </ul>
{% endif %} {% endif %}

View File

@ -56,17 +56,18 @@
</a> </a>
</li> </li>
{% endif %} {% endif %}
{% set workflows_frame = chill_entity_workflow_list('Chill\\DocStoreBundle\\Entity\\AccompanyingCourseDocument', document.id) %}
{% if workflows_frame is not empty %}
<li>
{{ workflows_frame|raw }}
</li>
{% endif %}
</ul> </ul>
</div> </div>
{% endblock %} {% endblock %}
{% block block_post_menu %} {% block block_post_menu %}
<div class="post-menu pt-4"> <div class="post-menu pt-4"></div>
{% set workflows_frame = chill_entity_workflow_list('Chill\\DocStoreBundle\\Entity\\AccompanyingCourseDocument', document.id) %}
{% if workflows_frame is not empty %}
{{ workflows_frame|raw }}
{% endif %}
</div>
{% endblock %} {% endblock %}
{% block js %} {% block js %}

View File

@ -13,13 +13,19 @@ namespace Chill\MainBundle\Controller;
use Chill\MainBundle\Entity\Notification; use Chill\MainBundle\Entity\Notification;
use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Pagination\PaginatorFactory;
use Chill\MainBundle\Repository\NotificationRepository;
use Chill\MainBundle\Security\Authorization\NotificationVoter; use Chill\MainBundle\Security\Authorization\NotificationVoter;
use Chill\MainBundle\Serializer\Model\Collection;
use Chill\MainBundle\Serializer\Model\Counter;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use RuntimeException; use RuntimeException;
use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Exception\AccessDeniedException; use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Core\Security; use Symfony\Component\Security\Core\Security;
use Symfony\Component\Serializer\SerializerInterface;
use UnexpectedValueException; use UnexpectedValueException;
/** /**
@ -29,12 +35,26 @@ class NotificationApiController
{ {
private EntityManagerInterface $entityManager; private EntityManagerInterface $entityManager;
private NotificationRepository $notificationRepository;
private PaginatorFactory $paginatorFactory;
private Security $security; private Security $security;
public function __construct(EntityManagerInterface $entityManager, Security $security) private SerializerInterface $serializer;
{
public function __construct(
EntityManagerInterface $entityManager,
NotificationRepository $notificationRepository,
PaginatorFactory $paginatorFactory,
Security $security,
SerializerInterface $serializer
) {
$this->entityManager = $entityManager; $this->entityManager = $entityManager;
$this->notificationRepository = $notificationRepository;
$this->paginatorFactory = $paginatorFactory;
$this->security = $security; $this->security = $security;
$this->serializer = $serializer;
} }
/** /**
@ -53,6 +73,37 @@ class NotificationApiController
return $this->markAs('unread', $notification); return $this->markAs('unread', $notification);
} }
/**
* @Route("/my/unread")
*/
public function myUnreadNotifications(Request $request): JsonResponse
{
$total = $this->notificationRepository->countUnreadByUser($this->security->getUser());
if ($request->query->getBoolean('countOnly')) {
return new JsonResponse(
$this->serializer->serialize(new Counter($total), 'json', ['groups' => ['read']]),
JsonResponse::HTTP_OK,
[],
true
);
}
$paginator = $this->paginatorFactory->create($total);
$notifications = $this->notificationRepository->findUnreadByUser(
$this->security->getUser(),
$paginator->getItemsPerPage(),
$paginator->getCurrentPageFirstItemNumber()
);
$collection = new Collection($notifications, $paginator);
return new JsonResponse(
$this->serializer->serialize($collection, 'json', ['groups' => ['read']]),
JsonResponse::HTTP_OK,
[],
true
);
}
private function markAs(string $target, Notification $notification): JsonResponse private function markAs(string $target, Notification $notification): JsonResponse
{ {
if (!$this->security->isGranted(NotificationVoter::NOTIFICATION_TOGGLE_READ_STATUS, $notification)) { if (!$this->security->isGranted(NotificationVoter::NOTIFICATION_TOGGLE_READ_STATUS, $notification)) {

View File

@ -25,6 +25,7 @@ use Iterator;
use RuntimeException; use RuntimeException;
use Symfony\Component\Serializer\Annotation as Serializer; use Symfony\Component\Serializer\Annotation as Serializer;
use function count; use function count;
use function is_array;
/** /**
* @ORM\Entity * @ORM\Entity
@ -51,7 +52,6 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
* @ORM\Id * @ORM\Id
* @ORM\GeneratedValue * @ORM\GeneratedValue
* @ORM\Column(type="integer") * @ORM\Column(type="integer")
* @Serializer\Groups({"read"})
*/ */
private ?int $id = null; private ?int $id = null;
@ -73,6 +73,11 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
*/ */
private Collection $steps; private Collection $steps;
/**
* @var null|array|EntityWorkflowStep[]
*/
private ?array $stepsChainedCache = null;
/** /**
* @ORM\ManyToMany(targetEntity=User::class) * @ORM\ManyToMany(targetEntity=User::class)
* @ORM\JoinTable(name="chill_main_workflow_entity_subscriber_to_final") * @ORM\JoinTable(name="chill_main_workflow_entity_subscriber_to_final")
@ -130,10 +135,6 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
if (!$this->steps->contains($step)) { if (!$this->steps->contains($step)) {
$this->steps[] = $step; $this->steps[] = $step;
$step->setEntityWorkflow($this); $step->setEntityWorkflow($this);
if ($this->isFinalize()) {
$step->setFinalizeAfter(true);
}
} }
return $this; return $this;
@ -254,27 +255,33 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
public function getStepsChained(): array public function getStepsChained(): array
{ {
if (is_array($this->stepsChainedCache)) {
return $this->stepsChainedCache;
}
$iterator = $this->steps->getIterator(); $iterator = $this->steps->getIterator();
$previous = $next = $current = null; $current = null;
$steps = []; $steps = [];
$iterator->rewind(); $iterator->rewind();
while ($iterator->valid()) { do {
$previous = $current; $previous = $current;
$steps[] = $current = $iterator->current(); $current = $iterator->current();
$steps[] = $current;
$current->setPrevious($previous); $current->setPrevious($previous);
$iterator->next(); $iterator->next();
if ($iterator->valid()) { if ($iterator->valid()) {
$next = $iterator->current(); $current->setNext($iterator->current());
} else { } else {
$next = null; $current->setNext(null);
} }
} while ($iterator->valid());
$current->setNext($next); $this->stepsChainedCache = $steps;
}
return $steps; return $steps;
} }
@ -309,7 +316,7 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
return $this->workflowName; return $this->workflowName;
} }
public function isFinalize(): bool public function isFinal(): bool
{ {
$steps = $this->getStepsChained(); $steps = $this->getStepsChained();
@ -321,7 +328,7 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
/** @var EntityWorkflowStep $last */ /** @var EntityWorkflowStep $last */
$last = end($steps); $last = end($steps);
return $last->getPrevious()->isFinalizeAfter(); return $last->isFinal();
} }
public function isFreeze(): bool public function isFreeze(): bool

View File

@ -53,11 +53,6 @@ class EntityWorkflowStep
*/ */
private ?EntityWorkflow $entityWorkflow = null; private ?EntityWorkflow $entityWorkflow = null;
/**
* @ORM\Column(type="boolean", options={"default": false})
*/
private bool $finalizeAfter = false;
/** /**
* @ORM\Column(type="boolean", options={"default": false}) * @ORM\Column(type="boolean", options={"default": false})
*/ */
@ -70,6 +65,11 @@ class EntityWorkflowStep
*/ */
private ?int $id = null; private ?int $id = null;
/**
* @ORM\Column(type="boolean", options={"default": false})
*/
private bool $isFinal = false;
/** /**
* filled by @see{EntityWorkflow::getStepsChained}. * filled by @see{EntityWorkflow::getStepsChained}.
*/ */
@ -187,9 +187,9 @@ class EntityWorkflowStep
return $this->transitionByEmail; return $this->transitionByEmail;
} }
public function isFinalizeAfter(): bool public function isFinal(): bool
{ {
return $this->finalizeAfter; return $this->isFinal;
} }
public function isFreezeAfter(): bool public function isFreezeAfter(): bool
@ -244,16 +244,16 @@ class EntityWorkflowStep
return $this; return $this;
} }
public function setFinalizeAfter(bool $finalizeAfter): EntityWorkflowStep public function setFreezeAfter(bool $freezeAfter): EntityWorkflowStep
{ {
$this->finalizeAfter = $finalizeAfter; $this->freezeAfter = $freezeAfter;
return $this; return $this;
} }
public function setFreezeAfter(bool $freezeAfter): EntityWorkflowStep public function setIsFinal(bool $isFinal): EntityWorkflowStep
{ {
$this->freezeAfter = $freezeAfter; $this->isFinal = $isFinal;
return $this; return $this;
} }

View File

@ -15,6 +15,7 @@ use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStep; use Chill\MainBundle\Entity\Workflow\EntityWorkflowStep;
use Chill\MainBundle\Form\Type\ChillTextareaType; use Chill\MainBundle\Form\Type\ChillTextareaType;
use Chill\MainBundle\Form\Type\PickUserDynamicType; use Chill\MainBundle\Form\Type\PickUserDynamicType;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Chill\MainBundle\Workflow\EntityWorkflowManager; use Chill\MainBundle\Workflow\EntityWorkflowManager;
use LogicException; use LogicException;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
@ -24,6 +25,7 @@ use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Workflow\Registry; use Symfony\Component\Workflow\Registry;
use Symfony\Component\Workflow\Transition; use Symfony\Component\Workflow\Transition;
use function array_key_exists;
class WorkflowStepType extends AbstractType class WorkflowStepType extends AbstractType
{ {
@ -31,10 +33,13 @@ class WorkflowStepType extends AbstractType
private Registry $registry; private Registry $registry;
public function __construct(EntityWorkflowManager $entityWorkflowManager, Registry $registry) private TranslatableStringHelperInterface $translatableStringHelper;
public function __construct(EntityWorkflowManager $entityWorkflowManager, Registry $registry, TranslatableStringHelperInterface $translatableStringHelper)
{ {
$this->entityWorkflowManager = $entityWorkflowManager; $this->entityWorkflowManager = $entityWorkflowManager;
$this->registry = $registry; $this->registry = $registry;
$this->translatableStringHelper = $translatableStringHelper;
} }
public function buildForm(FormBuilderInterface $builder, array $options) public function buildForm(FormBuilderInterface $builder, array $options)
@ -42,6 +47,7 @@ class WorkflowStepType extends AbstractType
/** @var \Chill\MainBundle\Entity\Workflow\EntityWorkflow $entityWorkflow */ /** @var \Chill\MainBundle\Entity\Workflow\EntityWorkflow $entityWorkflow */
$entityWorkflow = $options['entity_workflow']; $entityWorkflow = $options['entity_workflow'];
$handler = $this->entityWorkflowManager->getHandler($entityWorkflow); $handler = $this->entityWorkflowManager->getHandler($entityWorkflow);
$workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
if (true === $options['transition']) { if (true === $options['transition']) {
if (null === $options['entity_workflow']) { if (null === $options['entity_workflow']) {
@ -53,20 +59,49 @@ class WorkflowStepType extends AbstractType
->getEnabledTransitions($entityWorkflow); ->getEnabledTransitions($entityWorkflow);
$choices = array_combine( $choices = array_combine(
array_map(static function (Transition $transition) { return $transition->getName(); }, $transitions), array_map(
static function (Transition $transition) {
return $transition->getName();
},
$transitions
),
$transitions $transitions
); );
$builder $builder
->add('transition', ChoiceType::class, [ ->add('transition', ChoiceType::class, [
'label' => 'workflow.Transition', 'label' => 'workflow.Transition to apply',
'mapped' => false, 'mapped' => false,
'multiple' => false, 'multiple' => false,
'expanded' => true, 'expanded' => true,
'choices' => $choices, 'choices' => $choices,
'choice_label' => static function (Transition $transition) { 'choice_label' => function (Transition $transition) use ($workflow) {
return implode(', ', $transition->getTos()); $meta = $workflow->getMetadataStore()->getTransitionMetadata($transition);
},
if (array_key_exists('label', $meta)) {
return $this->translatableStringHelper->localize($meta['label']);
}
return $transition->getName();
},
'choice_attr' => static function (Transition $transition) use ($workflow) {
$toFinal = true;
foreach ($transition->getTos() as $to) {
$meta = $workflow->getMetadataStore()->getPlaceMetadata($to);
if (
!array_key_exists('isFinal', $meta) || false === $meta['isFinal']
) {
$toFinal = false;
}
}
return [
'data-is-transition' => 'data-is-transition',
'data-to-final' => $toFinal ? '1' : '0',
];
},
]) ])
->add('future_dest_users', PickUserDynamicType::class, [ ->add('future_dest_users', PickUserDynamicType::class, [
'label' => 'workflow.dest for next steps', 'label' => 'workflow.dest for next steps',
@ -88,11 +123,6 @@ class WorkflowStepType extends AbstractType
} }
$builder $builder
->add('finalizeAfter', CheckboxType::class, [
'required' => false,
'label' => 'workflow.Finalize',
'help' => 'workflow.The workflow will be finalized',
])
->add('comment', ChillTextareaType::class, [ ->add('comment', ChillTextareaType::class, [
'required' => false, 'required' => false,
'label' => 'Comment', 'label' => 'Comment',

View File

@ -11,17 +11,27 @@ declare(strict_types=1);
namespace Chill\MainBundle\Notification\Templating; namespace Chill\MainBundle\Notification\Templating;
use Chill\MainBundle\Entity\NotificationComment;
use Chill\MainBundle\Form\NotificationCommentType;
use Chill\MainBundle\Notification\NotificationPresence; use Chill\MainBundle\Notification\NotificationPresence;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Twig\Environment; use Twig\Environment;
use Twig\Extension\RuntimeExtensionInterface; use Twig\Extension\RuntimeExtensionInterface;
class NotificationTwigExtensionRuntime implements RuntimeExtensionInterface class NotificationTwigExtensionRuntime implements RuntimeExtensionInterface
{ {
private FormFactoryInterface $formFactory;
private NotificationPresence $notificationPresence; private NotificationPresence $notificationPresence;
public function __construct(NotificationPresence $notificationPresence) private UrlGeneratorInterface $urlGenerator;
public function __construct(FormFactoryInterface $formFactory, NotificationPresence $notificationPresence, UrlGeneratorInterface $urlGenerator)
{ {
$this->formFactory = $formFactory;
$this->notificationPresence = $notificationPresence; $this->notificationPresence = $notificationPresence;
$this->urlGenerator = $urlGenerator;
} }
public function counterNotificationFor(Environment $environment, string $relatedEntityClass, int $relatedEntityId, array $options = []): string public function counterNotificationFor(Environment $environment, string $relatedEntityClass, int $relatedEntityId, array $options = []): string
@ -47,8 +57,24 @@ class NotificationTwigExtensionRuntime implements RuntimeExtensionInterface
return ''; return '';
} }
$appendCommentForms = [];
foreach ($notifications as $notification) {
$appendComment = new NotificationComment();
$appendCommentForms[$notification->getId()] = $this->formFactory->create(
NotificationCommentType::class,
$appendComment,
[
'action' => $this->urlGenerator->generate(
'chill_main_notification_show',
['id' => $notification->getId()]
),
]
)->createView();
}
return $environment->render('@ChillMain/Notification/extension_list_notifications_for.html.twig', [ return $environment->render('@ChillMain/Notification/extension_list_notifications_for.html.twig', [
'notifications' => $notifications, 'notifications' => $notifications, 'appendCommentForms' => $appendCommentForms,
]); ]);
} }
} }

View File

@ -193,6 +193,29 @@ final class NotificationRepository implements ObjectRepository
return $this->repository->findOneBy($criteria, $orderBy); return $this->repository->findOneBy($criteria, $orderBy);
} }
/**
* @return array|Notification[]
*/
public function findUnreadByUser(User $user, int $limit = 20, int $offset = 0): array
{
$rsm = new Query\ResultSetMappingBuilder($this->em);
$rsm->addRootEntityFromClassMetadata(Notification::class, 'cmn');
$sql = 'SELECT ' . $rsm->generateSelectClause(['cmn' => 'cmn']) . ' ' .
'FROM chill_main_notification cmn ' .
'WHERE ' .
'EXISTS (select 1 FROM chill_main_notification_addresses_unread cmnau WHERE cmnau.user_id = :userId and cmnau.notification_id = cmn.id) ' .
'ORDER BY cmn.date DESC ' .
'LIMIT :limit OFFSET :offset';
$nq = $this->em->createNativeQuery($sql, $rsm)
->setParameter('userId', $user->getId())
->setParameter('limit', $limit)
->setParameter('offset', $offset);
return $nq->getResult();
}
public function getClassName() public function getClassName()
{ {
return Notification::class; return Notification::class;

View File

@ -474,6 +474,7 @@ div.workflow {
// Override bootstrap popover styles // Override bootstrap popover styles
div.popover { div.popover {
box-shadow: 0 0 10px -5px $dark; box-shadow: 0 0 10px -5px $dark;
z-index: 9999;
.popover-arrow {} .popover-arrow {}
.popover-header {} .popover-header {}
.popover-body {} .popover-body {}

View File

@ -21,7 +21,7 @@ $chill-theme-buttons: (
"download": $gray-300, "download": $gray-300,
"cancel": $gray-300, "cancel": $gray-300,
"choose": $gray-300, "choose": $gray-300,
"notify": $gray-300, "notify": $chill-blue,
"search": $gray-300, "search": $gray-300,
"unlink": $chill-red, "unlink": $chill-red,
"tpchild": $chill-pink, "tpchild": $chill-pink,
@ -136,3 +136,18 @@ $chill-theme-buttons: (
.btn-sm, .btn-group-sm > .btn { .btn-sm, .btn-group-sm > .btn {
min-width: 36px; min-width: 36px;
} }
// Homepage special fast action buttons
div.sticky-buttons {
position: fixed;
bottom: 3em;
right: 2em;
.btn-circle {
width: 50px; height: 50px;
border-radius: 50%;
text-align: center;
padding: 0.45rem 0.7rem;
display: block;
margin-bottom: 0.5rem;
}
}

View File

@ -1,49 +1,34 @@
import { createApp } from "vue"; import { createApp } from "vue";
import PickWorkflowVue from 'ChillMainAssets/vuejs/_components/EntityWorkflow/PickWorkflow.vue'; import ListWorkflowModalVue from 'ChillMainAssets/vuejs/_components/EntityWorkflow/ListWorkflowModal.vue';
import ListWorkflowVue from 'ChillMainAssets/vuejs/_components/EntityWorkflow/ListWorkflow.vue'; import { _createI18n } from "ChillMainAssets/vuejs/_js/i18n";
// pick workflow const i18n = _createI18n({});
document.querySelectorAll('[data-pick-workflow]')
.forEach(function(el) {
const app = {
components: {
PickWorkflowVue
},
template:
'<pick-workflow-vue ' +
':relatedEntityClass="relatedEntityClass" ' +
':relatedEntityId="relatedEntityId" ' +
':workflowsAvailables="workflowsAvailables" ' +
'></pick-workflow-vue>',
data() {
return {
relatedEntityClass: el.dataset.relatedEntityClass,
relatedEntityId: Number.parseInt(el.dataset.relatedEntityId),
workflowsAvailables: JSON.parse(el.dataset.workflowsAvailables),
}
}
};
createApp(app).mount(el);
})
;
// list workflow // list workflow
document.querySelectorAll('[data-list-workflows]') document.querySelectorAll('[data-list-workflows]')
.forEach(function (el) { .forEach(function (el) {
const app = { const app = {
components: { components: {
ListWorkflowVue, ListWorkflowModalVue,
}, },
template: template:
'<list-workflow-vue ' + '<list-workflow-modal-vue ' +
':workflows="workflows" ' + ':workflows="workflows" ' +
'></list-workflow-vue>', ':allowCreate="allowCreate" ' +
':relatedEntityClass="relatedEntityClass" ' +
':relatedEntityId="relatedEntityId" ' +
':workflowsAvailables="workflowsAvailables" ' +
'></list-workflow-modal-vue>',
data() { data() {
return { return {
workflows: JSON.parse(el.dataset.workflows), workflows: JSON.parse(el.dataset.workflows),
allowCreate: el.dataset.allowCreate === "1",
relatedEntityClass: el.dataset.relatedEntityClass,
relatedEntityId: Number.parseInt(el.dataset.relatedEntityId),
workflowsAvailables: JSON.parse(el.dataset.workflowsAvailables),
} }
} }
}; };
createApp(app).mount(el); createApp(app).use(i18n).mount(el);
}) })
; ;

View File

@ -0,0 +1,16 @@
import { createApp } from 'vue';
import { _createI18n } from 'ChillMainAssets/vuejs/_js/i18n';
import { appMessages } from 'ChillMainAssets/vuejs/HomepageWidget/js/i18n';
import { store } from 'ChillMainAssets/vuejs/HomepageWidget/js/store';
import App from 'ChillMainAssets/vuejs/HomepageWidget/App';
const i18n = _createI18n(appMessages);
const app = createApp({
template: `<app></app>`,
})
.use(store)
.use(i18n)
.component('app', App)
.mount('#homepage_widget')
;

View File

@ -0,0 +1,140 @@
<template>
<h2>{{ $t('main_title') }}</h2>
<ul class="nav nav-tabs">
<li class="nav-item">
<a class="nav-link"
:class="{'active': activeTab === 'MyCustoms'}"
@click="selectTab('MyCustoms')">
<i class="fa fa-dashboard"></i>
</a>
</li>
<li class="nav-item">
<a class="nav-link"
:class="{'active': activeTab === 'MyNotifications'}"
@click="selectTab('MyNotifications')">
{{ $t('my_notifications.tab') }}
<tab-counter :count="state.notifications.count"></tab-counter>
</a>
</li>
<li class="nav-item">
<a class="nav-link"
:class="{'active': activeTab === 'MyAccompanyingCourses'}"
@click="selectTab('MyAccompanyingCourses')">
{{ $t('my_accompanying_courses.tab') }}
<tab-counter :count="state.accompanyingCourses.count"></tab-counter>
</a>
</li>
<li class="nav-item">
<a class="nav-link"
:class="{'active': activeTab === 'MyWorks'}"
@click="selectTab('MyWorks')">
{{ $t('my_works.tab') }}
<tab-counter :count="state.works.count"></tab-counter>
</a>
</li>
<li class="nav-item">
<a class="nav-link"
:class="{'active': activeTab === 'MyEvaluations'}"
@click="selectTab('MyEvaluations')">
{{ $t('my_evaluations.tab') }}
<tab-counter :count="state.evaluations.count"></tab-counter>
</a>
</li>
<li class="nav-item">
<a class="nav-link"
:class="{'active': activeTab === 'MyTasks'}"
@click="selectTab('MyTasks')">
{{ $t('my_tasks.tab') }}
<tab-counter :count="state.tasks.warning.count"></tab-counter>
<tab-counter :count="state.tasks.alert.count"></tab-counter>
</a>
</li>
<li class="nav-item loading ms-auto py-2" v-if="loading">
<i class="fa fa-circle-o-notch fa-spin fa-lg text-chill-gray" :title="$t('loading')"></i>
</li>
</ul>
<div class="my-4">
<my-customs
v-if="activeTab === 'MyCustoms'">
</my-customs>
<my-works
v-else-if="activeTab === 'MyWorks'">
</my-works>
<my-evaluations
v-else-if="activeTab === 'MyEvaluations'">
</my-evaluations>
<my-tasks
v-else-if="activeTab === 'MyTasks'">
</my-tasks>
<my-accompanying-courses
v-else-if="activeTab === 'MyAccompanyingCourses'">
</my-accompanying-courses>
<my-notifications
v-else-if="activeTab === 'MyNotifications'">
</my-notifications>
</div>
</template>
<script>
import MyCustoms from './MyCustoms';
import MyWorks from './MyWorks';
import MyEvaluations from './MyEvaluations';
import MyTasks from './MyTasks';
import MyAccompanyingCourses from './MyAccompanyingCourses';
import MyNotifications from './MyNotifications';
import TabCounter from './TabCounter';
import { mapState } from "vuex";
export default {
name: "App",
components: {
MyCustoms,
MyWorks,
MyEvaluations,
MyTasks,
MyAccompanyingCourses,
MyNotifications,
TabCounter,
},
data() {
return {
activeTab: 'MyCustoms'
}
},
computed: {
...mapState([
'loading',
]),
// just to see all in devtool :
...mapState({
state: (state) => state,
}),
},
methods: {
selectTab(tab) {
this.$store.dispatch('getByTab', { tab: tab });
this.activeTab = tab;
}
},
mounted() {
for (const m of [
'MyNotifications',
'MyAccompanyingCourses',
'MyWorks',
'MyEvaluations',
'MyTasks',
]) {
this.$store.dispatch('getByTab', { tab: m, param: "countOnly=1" });
}
}
}
</script>
<style scoped>
a.nav-link {
cursor: pointer;
}
</style>

View File

@ -0,0 +1,60 @@
<template>
<div class="alert alert-light">{{ $t('my_accompanying_courses.description') }}</div>
<span v-if="noResults" class="chill-no-data-statement">{{ $t('no_data') }}</span>
<tab-table v-else>
<template v-slot:thead>
<th scope="col">id</th>
<th scope="col">Ouvert le</th>
<th scope="col">Usagers concernés</th>
<th scope="col"></th>
</template>
<template v-slot:tbody>
<tr v-for="(c, i) in accompanyingCourses.results" :key="`course-${i}`">
<td>{{ c.id}}</td>
<td>{{ $d(c.openingDate.datetime, 'long') }}</td>
<td>{{ c.participations.length }}</td>
<td>
<a class="btn btn-sm btn-show" :href="getUrl(c)">
{{ $t('show_entity', { entity: $t('the_course') }) }}
</a>
</td>
</tr>
</template>
</tab-table>
</template>
<script>
import { mapState, mapGetters } from "vuex";
import TabTable from "./TabTable";
export default {
name: "MyAccompanyingCourses",
components: {
TabTable
},
computed: {
...mapState([
'accompanyingCourses',
]),
...mapGetters([
'isAccompanyingCoursesLoaded',
]),
noResults() {
if (!this.isAccompanyingCoursesLoaded) {
return false;
} else {
return this.accompanyingCourses.count === 0;
}
},
},
methods: {
getUrl(c) {
return `/fr/parcours/${c.id}`
}
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,77 @@
<template>
<span v-if="noResults" class="chill-no-data-statement">{{ $t('no_dashboard') }}</span>
<div v-else id="dashboards" class="row g-3" data-masonry='{"percentPosition": true }'>
<div class="mbloc col col-sm-6 col-lg-4">
<div class="custom1">
<ul class="list-unstyled">
<li v-if="counter.notifications > 0">
<b>{{ counter.notifications }}</b> {{ $t('counter.unread_notifications') }}
</li>
<li v-if="counter.accompanyingCourses > 0">
<b>{{ counter.accompanyingCourses }}</b> {{ $t('counter.assignated_courses') }}
</li>
<li v-if="counter.works > 0">
<b>{{ counter.works }}</b> {{ $t('counter.assignated_actions') }}
</li>
<li v-if="counter.evaluations > 0">
<b>{{ counter.evaluations }}</b> {{ $t('counter.assignated_evaluations') }}
</li>
<li v-if="counter.tasksAlert > 0">
<b>{{ counter.tasksAlert }}</b> {{ $t('counter.alert_tasks') }}
</li>
<li v-if="counter.tasksWarning > 0">
<b>{{ counter.tasksWarning }}</b> {{ $t('counter.warning_tasks') }}
</li>
</ul>
</div>
</div>
<!--
<div class="mbloc col col-sm-6 col-lg-4">
<div class="custom2">
Mon dashboard personnalisé
</div>
</div>
<div class="mbloc col col-sm-6 col-lg-4">
<div class="custom3">
Mon dashboard personnalisé
</div>
</div>
<div class="mbloc col col-sm-6 col-lg-4">
<div class="custom4">
Mon dashboard personnalisé
</div>
</div>
-->
</div>
</template>
<script>
import { mapGetters } from "vuex";
import Masonry from 'masonry-layout/masonry';
export default {
name: "MyCustoms",
computed: {
...mapGetters(['counter']),
noResults() {
return false
},
},
mounted() {
const elem = document.querySelector('#dashboards');
const masonry = new Masonry(elem, {});
}
}
</script>
<style scoped>
div.custom4,
div.custom3,
div.custom2 {
font-style: italic;
color: var(--bs-chill-gray);
}
</style>

View File

@ -0,0 +1,57 @@
<template>
<div class="alert alert-light">{{ $t('my_evaluations.description') }}</div>
<span v-if="noResults" class="chill-no-data-statement">{{ $t('no_data') }}</span>
<tab-table v-else>
<template v-slot:thead>
<th scope="col">id</th>
<th scope="col"></th>
</template>
<template v-slot:tbody>
<tr v-for="(e, i) in evaluations.results" :key="`evaluation-${i}`">
<td>{{ e.id}}</td>
<td>
<a class="btn btn-sm btn-show" :href="getUrl(e)">
{{ $t('show_entity', { entity: $t('the_evaluation') }) }}
</a>
</td>
</tr>
</template>
</tab-table>
</template>
<script>
import { mapState, mapGetters } from "vuex";
import TabTable from "./TabTable";
export default {
name: "MyEvaluations",
components: {
TabTable
},
computed: {
...mapState([
'evaluations',
]),
...mapGetters([
'isEvaluationsLoaded',
]),
noResults() {
if (!this.isEvaluationsLoaded) {
return false;
} else {
return this.evaluations.count === 0;
}
}
},
methods: {
getUrl(e) {
let anchor = '#evaluations';
return `/fr/person/accompanying-period/work/${e.id}/edit${anchor}`
}
},
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,96 @@
<template>
<div class="alert alert-light">{{ $t('my_notifications.description') }}</div>
<span v-if="noResults" class="chill-no-data-statement">{{ $t('no_data') }}</span>
<tab-table v-else>
<template v-slot:thead>
<th scope="col">{{ $t('Date') }}</th>
<th scope="col">{{ $t('Subject') }}</th>
<th scope="col">{{ $t('From') }}</th>
<th scope="col"></th>
</template>
<template v-slot:tbody>
<tr v-for="(n, i) in notifications.results" :key="`notify-${i}`">
<td>{{ $d(n.date.datetime, 'long') }}</td>
<td>
<span class="unread">
<i class="fa fa-envelope-o"></i>
<a :href="getNotificationUrl(n)">{{ n.title }}</a>
</span>
</td>
<td>{{ n.sender.text }}</td>
<td>
<a class="btn btn-sm btn-show"
:href="getEntityUrl(n)">
{{ $t('show_entity', { entity: getEntityName(n) }) }}
</a>
</td>
</tr>
</template>
</tab-table>
</template>
<script>
import { mapState, mapGetters } from "vuex";
import TabTable from "./TabTable";
import { appMessages } from 'ChillMainAssets/vuejs/HomepageWidget/js/i18n';
export default {
name: "MyNotifications",
components: {
TabTable
},
computed: {
...mapState([
'notifications',
]),
...mapGetters([
'isNotificationsLoaded',
]),
noResults() {
if (!this.isNotificationsLoaded) {
return false;
} else {
return this.notifications.count === 0;
}
}
},
methods: {
getNotificationUrl(n) {
return `/fr/notification/${n.id}/show`
},
getEntityName(n) {
switch (n.relatedEntityClass) {
case 'Chill\\ActivityBundle\\Entity\\Activity':
return appMessages.fr.the_activity;
case 'Chill\\PersonBundle\\Entity\\AccompanyingPeriod':
return appMessages.fr.the_course;
default:
throw 'notification type unknown';
}
},
getEntityUrl(n) {
switch (n.relatedEntityClass) {
case 'Chill\\ActivityBundle\\Entity\\Activity':
return `/fr/activity/${n.relatedEntityId}/show`
case 'Chill\\PersonBundle\\Entity\\AccompanyingPeriod':
return `/fr/parcours/${n.relatedEntityId}`
default:
throw 'notification type unknown';
}
}
}
}
</script>
<style lang="scss" scoped>
span.unread {
font-weight: bold;
i {
margin-right: 0.5em;
}
a {
text-decoration: unset;
}
}
</style>

View File

@ -0,0 +1,85 @@
<template>
<div class="alert alert-light">{{ $t('my_tasks.description_alert') }}</div>
<span v-if="noResultsWarning" class="chill-no-data-statement">{{ $t('no_data') }}</span>
<tab-table v-else>
<template v-slot:thead>
<th scope="col">id</th>
<th scope="col"></th>
</template>
<template v-slot:tbody>
<tr v-for="(t, i) in tasks.warning" :key="`task-warning-${i}`">
<td>{{ t.id}}</td>
<td>
<a class="btn btn-sm btn-show" :href="getUrl(t)">
{{ $t('show_entity', { entity: $t('the_task') }) }}
</a>
</td>
</tr>
</template>
</tab-table>
<div class="alert alert-light">{{ $t('my_tasks.description_warning') }}</div>
<span v-if="noResultsAlert" class="chill-no-data-statement">{{ $t('no_data') }}</span>
<tab-table v-else>
<template v-slot:thead>
<th scope="col">id</th>
<th scope="col"></th>
</template>
<template v-slot:tbody>
<tr v-for="(t, i) in tasks.alert" :key="`task-alert-${i}`">
<td>{{ t.id}}</td>
<td>
<a class="btn btn-sm btn-show" :href="getUrl(t)">
{{ $t('show_entity', { entity: $t('the_task') }) }}
</a>
</td>
</tr>
</template>
</tab-table>
</template>
<script>
import { mapState, mapGetters } from "vuex";
import TabTable from "./TabTable";
export default {
name: "MyTasks",
components: {
TabTable
},
computed: {
...mapState([
'tasks',
]),
...mapGetters([
'isTasksWarningLoaded',
'isTasksAlertLoaded',
]),
noResultsAlert() {
if (!this.isTasksAlertLoaded) {
return false;
} else {
return this.tasks.alert.count === 0;
}
},
noResultsWarning() {
if (!this.isTasksWarningLoaded) {
return false;
} else {
return this.tasks.warning.count === 0;
}
}
},
methods: {
getUrl(t) {
return `/fr/task/single-task/${t.id}/show`
}
},
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,79 @@
<template>
<div class="accompanying_course_work">
<div class="alert alert-light">{{ $t('my_works.description') }}</div>
<span v-if="noResults" class="chill-no-data-statement">{{ $t('no_data') }}</span>
<tab-table v-else>
<template v-slot:thead>
<th scope="col">{{ $t('StartDate') }}</th>
<th scope="col">{{ $t('SocialAction') }}</th>
<th scope="col"></th>
</template>
<template v-slot:tbody>
<tr v-for="(w, i) in works.results" :key="`works-${i}`">
<td>{{ $d(w.startDate.datetime, 'short') }}</td>
<td>
<h4 class="badge-title">
<span class="title_label"></span>
<span class="title_action">
{{ w.socialAction.text }}
</span>
</h4>
</td>
<td>
<div class="btn-group" role="group" aria-label="Actions">
<a class="btn btn-sm btn-update" :href="getUrl(w)">
{{ $t('show_entity', { entity: $t('the_action') }) }}
</a>
<a class="btn btn-sm btn-show" :href="getUrl(w.accompanyingPeriod)">
{{ $t('show_entity', { entity: $t('the_course') }) }}
</a>
</div>
</td>
</tr>
</template>
</tab-table>
</div>
</template>
<script>
import { mapState, mapGetters } from "vuex";
import TabTable from "./TabTable";
export default {
name: "MyWorks",
components: {
TabTable
},
computed: {
...mapState([
'works',
]),
...mapGetters([
'isWorksLoaded',
]),
noResults() {
if (!this.isWorksLoaded) {
return false;
} else {
return this.works.count === 0;
}
}
},
methods: {
getUrl(e) {
switch (e.type) {
case 'accompanying_period_work':
return `/fr/person/accompanying-period/work/${e.id}/edit`
case 'accompanying_period':
return `/fr/parcours/${e.id}`
default:
throw 'entity type unknown';
}
}
},
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,18 @@
<template>
<span v-if="isCounterAvailable"
class="badge rounded-pill bg-danger counter">
{{ count }}
</span>
</template>
<script>
export default {
name: "TabCounter",
props: ['count'],
computed: {
isCounterAvailable() {
return (typeof this.count !== 'undefined' && this.count > 0 )
}
}
}
</script>

View File

@ -0,0 +1,21 @@
<template>
<table class="table table-striped table-hover">
<thead>
<tr>
<slot name="thead"></slot>
</tr>
</thead>
<tbody>
<slot name="tbody"></slot>
</tbody>
</table>
</template>
<script>
export default {
name: "TabTable",
props: []
}
</script>
<style scoped></style>

View File

@ -0,0 +1,54 @@
const appMessages = {
fr: {
main_title: "Vue d'ensemble",
my_works: {
tab: "Mes actions",
description: "Liste des actions d'accompagnement dont je suis référent et qui arrivent à échéance.",
},
my_evaluations: {
tab: "Mes évaluations",
description: "Liste des évaluations dont je suis référent et qui arrivent à échéance.",
},
my_tasks: {
tab: "Mes tâches",
description_alert: "Liste des tâches auxquelles je suis assigné et dont la date de rappel est dépassée.",
description_warning: "Liste des tâches auxquelles je suis assigné et dont la date d'échéance est dépassée.",
},
my_accompanying_courses: {
tab: "Mes parcours",
description: "Liste des parcours d'accompagnement que l'on vient de m'attribuer.",
},
my_notifications: {
tab: "Mes notifications",
description: "Liste des notifications reçues et non lues.",
},
Date: "Date",
From: "De",
Subject: "Objet",
Entity: "Associé à",
show_entity: "Voir {entity}",
the_activity: "l'échange",
the_course: "le parcours",
the_action: "l'action",
the_evaluation: "l'évaluation",
the_task: "la tâche",
StartDate: "Date d'ouverture",
SocialAction: "Action d'accompagnement",
no_data: "Aucun résultats",
no_dashboard: "Pas de tableaux de bord",
counter: {
unread_notifications: "notifications non lues",
assignated_courses: "parcours récents assignés",
assignated_actions: "actions assignées",
assignated_evaluations: "évaluations assignées",
alert_tasks: "tâches en rappel",
warning_tasks: "tâches à échéances",
}
}
};
Object.assign(appMessages.fr);
export {
appMessages
};

View File

@ -0,0 +1,200 @@
import 'es6-promise/auto';
import { createStore } from 'vuex';
import { makeFetch } from "ChillMainAssets/lib/api/apiMethods";
import MyCustoms from "../MyCustoms";
import MyWorks from "../MyWorks";
import MyEvaluations from "../MyEvaluations";
import MyTasks from "../MyTasks";
import MyAccompanyingCourses from "../MyAccompanyingCourses";
import MyNotifications from "../MyNotifications";
const debug = process.env.NODE_ENV !== 'production';
const isEmpty = (obj) => {
return obj
&& Object.keys(obj).length <= 1
&& Object.getPrototypeOf(obj) === Object.prototype;
};
const store = createStore({
strict: debug,
state: {
works: {},
evaluations: {},
tasks: {
warning: {},
alert: {}
},
accompanyingCourses: {},
notifications: {},
errorMsg: [],
loading: false
},
getters: {
isWorksLoaded(state) {
return !isEmpty(state.works);
},
isEvaluationsLoaded(state) {
return !isEmpty(state.evaluations);
},
isTasksWarningLoaded(state) {
return !isEmpty(state.tasks.warning);
},
isTasksAlertLoaded(state) {
return !isEmpty(state.tasks.alert);
},
isAccompanyingCoursesLoaded(state) {
return !isEmpty(state.accompanyingCourses);
},
isNotificationsLoaded(state) {
return !isEmpty(state.notifications);
},
counter(state) {
return {
works: state.works.count,
evaluations: state.evaluations.count,
tasksWarning: state.tasks.warning.count,
tasksAlert: state.tasks.alert.count,
accompanyingCourses: state.accompanyingCourses.count,
notifications: state.notifications.count,
}
}
},
mutations: {
addWorks(state, works) {
console.log('addWorks', works);
state.works = works;
},
addEvaluations(state, evaluations) {
console.log('addEvaluations', evaluations);
state.evaluations = evaluations;
},
addTasksWarning(state, tasks) {
console.log('addTasksWarning', tasks);
state.tasks.warning = tasks;
},
addTasksAlert(state, tasks) {
console.log('addTasksAlert', tasks);
state.tasks.alert = tasks;
},
addCourses(state, courses) {
console.log('addCourses', courses);
state.accompanyingCourses = courses;
},
addNotifications(state, notifications) {
console.log('addNotifications', notifications);
state.notifications = notifications;
},
setLoading(state, bool) {
state.loading = bool;
},
catchError(state, error) {
state.errorMsg.push(error);
}
},
actions: {
getByTab({ commit, getters }, { tab, param }) {
switch (tab) {
case 'MyCustoms':
break;
case 'MyWorks':
if (!getters.isWorksLoaded) {
commit('setLoading', true);
const url = `/api/1.0/person/accompanying-period/work/my-near-end${'?'+ param}`;
makeFetch('GET', url)
.then((response) => {
commit('addWorks', response);
commit('setLoading', false);
})
.catch((error) => {
commit('catchError', error);
throw error;
})
;
}
break;
case 'MyEvaluations':
if (!getters.isEvaluationsLoaded) {
commit('setLoading', true);
const url = `/api/1.0/person/accompanying-period/work/evaluation/my-near-end${'?'+ param}`;
makeFetch('GET', url)
.then((response) => {
commit('addEvaluations', response);
commit('setLoading', false);
})
.catch((error) => {
commit('catchError', error);
throw error;
})
;
}
break;
case 'MyTasks':
if (!(getters.isTasksWarningLoaded && getters.isTasksAlertLoaded)) {
commit('setLoading', true);
const
urlWarning = `/api/1.0/task/single-task/list/my?f[q]=&f[checkboxes][status][]=warning&f[checkboxes][states][]=new&f[checkboxes][states][]=in_progress${'&'+ param}`,
urlAlert = `/api/1.0/task/single-task/list/my?f[q]=&f[checkboxes][status][]=alert&f[checkboxes][states][]=new&f[checkboxes][states][]=in_progress${'&'+ param}`
;
makeFetch('GET', urlWarning)
.then((response) => {
commit('addTasksWarning', response);
commit('setLoading', false);
})
.catch((error) => {
commit('catchError', error);
throw error;
})
;
makeFetch('GET', urlAlert)
.then((response) => {
commit('addTasksAlert', response);
commit('setLoading', false);
})
.catch((error) => {
commit('catchError', error);
throw error;
})
;
}
break;
case 'MyAccompanyingCourses':
if (!getters.isAccompanyingCoursesLoaded) {
commit('setLoading', true);
const url = `/api/1.0/person/accompanying-course/list/by-recent-attributions${'?'+ param}`;
makeFetch('GET', url)
.then((response) => {
commit('addCourses', response);
commit('setLoading', false);
})
.catch((error) => {
commit('catchError', error);
throw error;
})
;
}
break;
case 'MyNotifications':
if (!getters.isNotificationsLoaded) {
commit('setLoading', true);
const url = `/api/1.0/main/notification/my/unread${'?'+ param}`;
makeFetch('GET', url)
.then((response) => {
commit('addNotifications', response);
commit('setLoading', false);
})
.catch((error) => {
commit('catchError', error);
throw error;
})
;
}
break;
default:
throw 'tab '+ tab;
}
}
},
});
export { store };

View File

@ -1,26 +1,86 @@
<template> <template>
<div class="list-group my-2 workflow workflow-box"> <div class="flex-table workflow" id="workflow-list">
<div class="list-group-item"> <div v-for="(w, i) in workflows" :key="`workflow-${i}`"
<h4>Workflow associés</h4> class="item-bloc">
<div>
<div class="item-row col">
<h2>Workflow</h2>
<div class="flex-grow-1 ms-3 h3">
<div class="visually-hidden">
{{ w.relatedEntityClass }}
{{ w.relatedEntityId }}
</div>
</div>
</div>
<div class="breadcrumb">
<template v-for="(step, j) in w.steps" :key="`step-${j}`">
<span class="mx-2"
tabindex="0"
data-bs-trigger="focus hover"
data-bs-toggle="popover"
data-bs-placement="bottom"
data-bs-custom-class="workflow-transition"
:title="getPopTitle(step)"
:data-bs-content="getPopContent(step)">
<i v-if="step.currentStep.name === 'initial'"
class="fa fa-circle me-1 text-chill-yellow">
</i>
<i v-if="step.isFreezed"
class="fa fa-snowflake-o fa-sm me-1">
</i>
{{ step.currentStep.text }}
</span>
<span v-if="j !== Object.keys(w.steps).length - 1">
</span>
</template>
</div>
</div>
<div class="item-row">
<div class="item-col flex-grow-1">
<p v-if="isUserSubscribedToStep(w)">
<i class="fa fa-check fa-fw"></i>
{{ $t('you_subscribed_to_all_steps') }}
</p>
<p v-if="isUserSubscribedToFinal(w)">
<i class="fa fa-check fa-fw"></i>
{{ $t('you_subscribed_to_final_step') }}
</p>
</div>
<div class="item-col">
<ul class="record_actions">
<li>
<a :href="goToUrl(w)" class="btn btn-sm btn-show" :title="$t('action.show')"></a>
</li>
</ul>
</div>
</div>
</div>
</div> </div>
<div class="list-group-item" v-for="w in workflows">
{{ w.id }}
<ul class="record_actions">
<li>
<a class="btn btn-sm btn-outline-primary"
title="voir"
:href="goToUrl(w)">
<i class="fa fa-eye fa-fw"></i>
</a>
</li>
</ul>
</div>
</div>
</template> </template>
<script> <script>
import Popover from 'bootstrap/js/src/popover';
const i18n = {
messages: {
fr: {
you_subscribed_to_all_steps: "Vous recevrez une notification à chaque étape",
you_subscribed_to_final_step: "Vous recevrez une notification à l'étape finale",
by: "Par",
at: "Le"
}
}
}
export default { export default {
name: "ListWorkflow", name: "ListWorkflow",
i18n: i18n,
props: { props: {
workflows: { workflows: {
type: Array, type: Array,
@ -30,7 +90,42 @@ export default {
methods: { methods: {
goToUrl(w) { goToUrl(w) {
return `/fr/main/workflow/${w.id}/show`; return `/fr/main/workflow/${w.id}/show`;
} },
getPopTitle(step) {
if (step.transitionPrevious != null) {
let freezed = step.isFreezed ? `<i class="fa fa-snowflake-o fa-sm me-1"></i>` : ``;
return `${freezed}${step.currentStep.text}`;
}
},
getPopContent(step) {
if (step.transitionPrevious != null) {
return `<ul class="small_in_title">
<li><span class="item-key">${i18n.messages.fr.by} : </span><b>${step.transitionPreviousBy.text}</b></li>
<li><span class="item-key">${i18n.messages.fr.at} : </span><b>${this.formatDate(step.transitionPreviousAt.datetime)}</b></li>
</ul>`
;
}
},
formatDate(datetime) {
return datetime.split('T')[0] +' '+ datetime.split('T')[1].substring(0,5)
},
isUserSubscribedToStep(w) {
// todo
return false;
},
isUserSubscribedToFinal(w) {
// todo
return false;
},
},
mounted() {
const triggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]'));
const popoverList = triggerList.map(function (el) {
//console.log('popover', el)
return new Popover(el, {
html: true,
});
});
} }
} }
</script> </script>

View File

@ -0,0 +1,111 @@
<template>
<button v-if="hasWorkflow"
class="btn btn-primary"
@click="openModal">
<b>{{ countWorkflows }}</b>
<template v-if="countWorkflows > 1">{{ $t('workflows') }}</template>
<template v-else>{{ $t('workflow') }}</template>
</button>
<pick-workflow v-else-if="allowCreate"
:relatedEntityClass="this.relatedEntityClass"
:relatedEntityId="this.relatedEntityId"
:workflowsAvailables="workflowsAvailables"
></pick-workflow>
<teleport to="body">
<modal v-if="modal.showModal"
:modalDialogClass="modal.modalDialogClass"
@close="modal.showModal = false">
<template v-slot:header>
<h2 class="modal-title">{{ $t('workflow_list') }}</h2>
</template>
<template v-slot:body>
<list-workflow-vue
:workflows="workflows"
></list-workflow-vue>
</template>
<template v-slot:footer>
<pick-workflow v-if="allowCreate"
:relatedEntityClass="this.relatedEntityClass"
:relatedEntityId="this.relatedEntityId"
:workflowsAvailables="workflowsAvailables"
></pick-workflow>
</template>
</modal>
</teleport>
</template>
<script>
import Modal from 'ChillMainAssets/vuejs/_components/Modal';
import PickWorkflow from 'ChillMainAssets/vuejs/_components/EntityWorkflow/PickWorkflow.vue';
import ListWorkflowVue from 'ChillMainAssets/vuejs/_components/EntityWorkflow/ListWorkflow.vue';
export default {
name: "ListWorkflowModal",
components: {
Modal,
PickWorkflow,
ListWorkflowVue
},
props: {
workflows: {
type: Array,
required: true,
},
allowCreate: {
type: Boolean,
required: true,
},
relatedEntityClass: {
type: String,
required: true,
},
relatedEntityId: {
type: Number,
required: false,
},
workflowsAvailables: {
type: Array,
required: true,
}
},
data() {
return {
modal: {
showModal: false,
modalDialogClass: "modal-dialog-scrollable modal-xl"
},
}
},
computed: {
countWorkflows() {
return this.workflows.length;
},
hasWorkflow() {
return this.countWorkflows > 0;
}
},
methods: {
openModal() {
this.modal.showModal = true;
},
},
i18n: {
messages: {
fr: {
workflow_list: "Liste des workflows associés",
workflow: " workflow associé",
workflows: " workflows associés",
}
}
}
}
</script>
<style scoped></style>

View File

@ -0,0 +1,3 @@
<div class="sticky-buttons">
{# Override this file to add fast actions buttons #}
</div>

View File

@ -0,0 +1,15 @@
<div class="col-10 mt-5">
{# vue component #}
<div id="homepage_widget"></div>
{% include '@ChillMain/Homepage/fast_actions.html.twig' %}
</div>
{% block css %}
{{ encore_entry_link_tags('page_homepage_widget') }}
{% endblock %}
{% block js %}
{{ encore_entry_script_tags('page_homepage_widget') }}
{% endblock %}

View File

@ -0,0 +1,77 @@
{% import '@ChillPerson/AccompanyingCourse/Comment/macro_showItem.html.twig' as m %}
{% macro recordAction(comment) %}
{% if is_granted('CHILL_MAIN_NOTIFICATION_COMMENT_EDIT', comment) %}
<li>
<a href="{{ chill_path_forward_return_path('chill_main_notification_show', {
'_fragment': 'comment-' ~ comment.id,
'edit': comment.id,
'id': comment.notification.id
}) }}" class="btn btn-edit" title="{{ 'Edit'|trans }}"
></a>
</li>
{% endif %}
{% endmacro %}
<div class="notification-comment-list my-5">
<h2 class="chill-blue">{{ 'notification.comments_list'|trans }}</h2>
{% if notification.comments|length > 0 %}
<div class="flex-table">
{% for comment in notification.comments %}
{% if editedCommentForm is null or editedCommentId != comment.id %}
{{ m.show_comment(comment, {
'recordAction': _self.recordAction(comment)
}) }}
{% else %}
<div class="item-bloc">
<div class="item-row row">
<a id="comment-{{ comment.id }}"></a>
{{ form_start(editedCommentForm) }}
{{ form_errors(editedCommentForm) }}
{{ form_widget(editedCommentForm.content) }}
<input type="hidden" name="form" value="edit" />
<ul class="record_actions">
<li class="cancel">
<a href="{{ chill_path_forward_return_path('chill_main_notification_show', {
'_fragment': 'comment-' ~ comment.id,
'id': notification.id }) }}" class="btn btn-cancel">
{{ 'cancel'|trans }}
</a>
</li>
<li>
<button class="btn btn-save" type="submit">{{ 'Save'|trans }}</button>
</li>
</ul>
{{ form_end(editedCommentForm) }}
</div>
</div>
{% endif %}
{% endfor %}
</div>
{% else %}
<span class="chill-no-data-statement">{{ 'No comments'|trans }}</span>
{% endif %}
{% if appendCommentForm is not null %}
<div class="new-comment my-5">
<h2 class="chill-blue mb-4">{{ 'Write a new comment'|trans }}</h2>
{{ form_start(appendCommentForm) }}
{{ form_errors(appendCommentForm) }}
{{ form_widget(appendCommentForm.content) }}
<input type="hidden" name="form" value="append" />
<ul class="record_actions">
<li>
<button class="btn btn-create" type="submit">{{ 'notification.append_comment'|trans }}</button>
</li>
</ul>
{{ form_end(appendCommentForm) }}
</div>
{% endif %}
</div>

View File

@ -52,9 +52,11 @@
{% macro content(c) %} {% macro content(c) %}
<div class="item-row separator"> <div class="item-row separator">
<div class="mx-3 flex-grow-1"> {% if c.data is defined %}
{% include c.data.template with c.data.template_data %} <div class="mx-3 flex-grow-1">
</div> {% include c.data.template with c.data.template_data %}
</div>
{% endif %}
</div> </div>
<div class="item-row"> <div class="item-row">
<div class="notification-content"> <div class="notification-content">
@ -68,34 +70,44 @@
</div> </div>
{% if c.action_button is not defined or c.action_button != false %} {% if c.action_button is not defined or c.action_button != false %}
<div class="item-row separator"> <div class="item-row separator">
<ul class="record_actions"> <div class="item-col item-meta">
<li>
{# Vue component #} {# TODO twig extension to count comments #}
<span class="notification_toggle_read_status" <div class="comment-counter visually-hidden">
data-notification-id="{{ c.notification.id }}" <span>x commentaires</span>
data-notification-current-is-read="{{ c.notification.isReadBy(app.user) }}" </div>
data-container="notification-status"
></span> </div>
</li> <div class="item-col">
{% if is_granted('CHILL_MAIN_NOTIFICATION_UPDATE', c.notification) %} <ul class="record_actions">
<li> <li>
<a href="{{ chill_path_add_return_path('chill_main_notification_edit', {'id': c.notification.id}) }}" {# Vue component #}
class="btn btn-edit" title="{{ 'Edit'|trans }}"></a> <span class="notification_toggle_read_status"
data-notification-id="{{ c.notification.id }}"
data-notification-current-is-read="{{ c.notification.isReadBy(app.user) }}"
data-container="notification-status"
></span>
</li> </li>
{% endif %} {% if is_granted('CHILL_MAIN_NOTIFICATION_UPDATE', c.notification) %}
{% if is_granted('CHILL_MAIN_NOTIFICATION_SEE', c.notification) %} <li>
<li> <a href="{{ chill_path_add_return_path('chill_main_notification_edit', {'id': c.notification.id}) }}"
<a href="{{ chill_path_add_return_path('chill_main_notification_show', {'id': c.notification.id}) }}" class="btn btn-edit" title="{{ 'Edit'|trans }}"></a>
class="btn {% if not c.notification.isSystem %}btn-show change-icon{% else %}btn-misc{% endif %}" title="{{ 'notification.see_comments_thread'|trans }}"> </li>
{% if not c.notification.isSystem() %} {% endif %}
<i class="fa fa-comment"></i> {% if is_granted('CHILL_MAIN_NOTIFICATION_SEE', c.notification) %}
{% else %} <li>
{{ 'Read more'|trans }} <a href="{{ chill_path_add_return_path('chill_main_notification_show', {'id': c.notification.id}) }}"
{% endif %} class="btn {% if not c.notification.isSystem %}btn-show change-icon{% else %}btn-misc{% endif %}" title="{{ 'notification.see_comments_thread'|trans }}">
</a> {% if not c.notification.isSystem() %}
</li> <i class="fa fa-comment"></i>
{% endif %} {% else %}
</ul> {{ 'Read more'|trans }}
{% endif %}
</a>
</li>
{% endif %}
</ul>
</div>
</div> </div>
{% endif %} {% endif %}
{% endmacro %} {% endmacro %}
@ -107,15 +119,19 @@
<button type="button" class="accordion-button collapsed" <button type="button" class="accordion-button collapsed"
data-bs-toggle="collapse" data-bs-target="#flush-collapse-{{ notification.id }}" data-bs-toggle="collapse" data-bs-target="#flush-collapse-{{ notification.id }}"
aria-expanded="false" aria-controls="flush-collapse-{{ notification.id }}"> aria-expanded="false" aria-controls="flush-collapse-{{ notification.id }}">
{{ _self.title(_context) }} {{ _self.title(_context) }}
</button> </button>
{{ _self.header(_context) }} {{ _self.header(_context) }}
</div> </div>
<div id="flush-collapse-{{ notification.id }}" <div id="flush-collapse-{{ notification.id }}"
class="accordion-collapse collapse" class="accordion-collapse collapse"
aria-labelledby="flush-heading-{{ notification.id }}" aria-labelledby="flush-heading-{{ notification.id }}"
data-bs-parent="#notification-fold"> data-bs-parent="#notification-fold">
{{ _self.content(_context) }} {{ _self.content(_context) }}
</div> </div>
{% else %} {% else %}
{{ _self.title(_context) }} {{ _self.title(_context) }}

View File

@ -21,6 +21,8 @@
{{ form_row(form.title, { 'label': 'notification.subject'|trans }) }} {{ form_row(form.title, { 'label': 'notification.subject'|trans }) }}
{{ form_row(form.addressees, { 'label': 'notification.sent_to'|trans }) }} {{ form_row(form.addressees, { 'label': 'notification.sent_to'|trans }) }}
{% include handler.template(notification) with handler.templateData(notification) %}
<div class="mb-3 row"> <div class="mb-3 row">
<label class="col-form-label col-sm-4" for="notification_message">{{ form_label(form.message) }}</label> <label class="col-form-label col-sm-4" for="notification_message">{{ form_label(form.message) }}</label>
<div class="col-12"> <div class="col-12">
@ -28,8 +30,6 @@
</div> </div>
</div> </div>
{% include handler.template(notification) with handler.templateData(notification) %}
{{ form_end(form) }} {{ form_end(form) }}
<ul class="record_actions sticky-form-buttons"> <ul class="record_actions sticky-form-buttons">

View File

@ -1,46 +1,12 @@
<div class="list-group my-2 notification notification-box"> <h1 class="mt-5"><a id="notification-list"></a>{{ 'notification.Notifications'|trans }}</h1>
<div class="list-group-item">
<h4>{{ 'notification.Sent'|trans }}</h4> <div class="flex-table accordion accordion-flush" id="notification-fold">
</div>
{# TODO pagination or limit #}
{% for notification in notifications %} {% for notification in notifications %}
<div class="list-group-item notification-status {% if notification.isReadBy(app.user) %}read{% else %}unread{% endif %}"> {% include 'ChillMainBundle:Notification:_list_item.html.twig' with {
'full_content': true,
{% if not notification.isSystem %} 'fold_item': true,
{% if notification.sender == app.user %} 'action_button': true,
} %}{#
<h6 class="notification-title"> #}
<abbr title="{{ 'Le ' ~ notification.date|format_date('long') ~ '\n' ~ notification.title }}">
{{ notification.date|format_datetime('short','short') }}
</abbr>
{# Vue component #}
<span class="notification_toggle_read_status"
data-notification-id="{{ notification.id }}"
data-notification-current-is-read="{{ notification.isReadBy(app.user) }}"
data-container="notification-status"
data-show-button-url="{{ chill_path_add_return_path('chill_main_notification_show', {'id': notification.id}, false) }}"
data-button-class="btn-outline-primary"
data-button-text="false"
></span>
</h6>
{% if notification.addressees|length > 0 %}
<abbr title="{{ 'notification.sent_to'|trans }}">{{ 'notification.to'|trans }}:</abbr>
{% endif %}
{% for a in notification.addressees %}
<span class="badge-user">
{{ a|chill_entity_render_string }}
</span>
{% endfor %}
{% else %}
<div>{{ 'notification.you were notified by %sender%'|trans({'%sender%': notification.sender|chill_entity_render_string }) }}</div>
{% endif %}
{% else %}
<div>{{ 'notification.you were notified by system'|trans }}</div>
{% endif %}
</div>
{% endfor %} {% endfor %}
</div> </div>

View File

@ -14,21 +14,6 @@
{{ encore_entry_link_tags('mod_notification_toggle_read_status') }} {{ encore_entry_link_tags('mod_notification_toggle_read_status') }}
{% endblock %} {% endblock %}
{% import '@ChillPerson/AccompanyingCourse/Comment/macro_showItem.html.twig' as m %}
{% macro recordAction(comment) %}
{% if is_granted('CHILL_MAIN_NOTIFICATION_COMMENT_EDIT', comment) %}
<li>
<a href="{{ chill_path_forward_return_path('chill_main_notification_show', {
'_fragment': 'comment-' ~ comment.id,
'edit': comment.id,
'id': comment.notification.id
}) }}" class="btn btn-edit" title="{{ 'Edit'|trans }}"
></a>
</li>
{% endif %}
{% endmacro %}
{% block content %} {% block content %}
<div class="col-10 notification notification-show"> <div class="col-10 notification notification-show">
@ -41,70 +26,12 @@
'template_data': handler.getTemplateData(notification) 'template_data': handler.getTemplateData(notification)
}, },
'action_button': false, 'action_button': false,
'full_content': true 'full_content': true,
'fold_item': false
} %} } %}
</div> </div>
<div class="notification-comment-list my-5"> {% include 'ChillMainBundle:Notification:_item_comments.html.twig' %}
<h2 class="chill-blue">{{ 'notification.comments_list'|trans }}</h2>
{% if notification.comments|length > 0 %}
<div class="flex-table">
{% for comment in notification.comments %}
{% if editedCommentForm is null or editedCommentId != comment.id %}
{{ m.show_comment(comment, {
'recordAction': _self.recordAction(comment)
}) }}
{% else %}
<div class="item-bloc">
<div class="item-row row">
<a id="comment-{{ comment.id }}"></a>
{{ form_start(editedCommentForm) }}
{{ form_errors(editedCommentForm) }}
{{ form_widget(editedCommentForm.content) }}
<input type="hidden" name="form" value="edit" />
<ul class="record_actions">
<li class="cancel">
<a href="{{ chill_path_forward_return_path('chill_main_notification_show', {
'_fragment': 'comment-' ~ comment.id,
'id': notification.id }) }}" class="btn btn-cancel">
{{ 'cancel'|trans }}
</a>
</li>
<li>
<button class="btn btn-save" type="submit">{{ 'Save'|trans }}</button>
</li>
</ul>
{{ form_end(editedCommentForm) }}
</div>
</div>
{% endif %}
{% endfor %}
</div>
{% endif %}
{% if appendCommentForm is not null %}
<div class="new-comment my-5">
<h2 class="chill-blue mb-4">{{ 'Write a new comment'|trans }}</h2>
{{ form_start(appendCommentForm) }}
{{ form_errors(appendCommentForm) }}
{{ form_widget(appendCommentForm.content) }}
<input type="hidden" name="form" value="append" />
<ul class="record_actions">
<li>
<button class="btn btn-create" type="submit">{{ 'notification.append_comment'|trans }}</button>
</li>
</ul>
{{ form_end(appendCommentForm) }}
</div>
{% endif %}
</div>
<ul class="record_actions sticky-form-buttons"> <ul class="record_actions sticky-form-buttons">
<li class="cancel"> <li class="cancel">

View File

@ -5,10 +5,6 @@
{{ form_row(transition_form.transition) }} {{ form_row(transition_form.transition) }}
<div id="finalizeAfter">
{{ form_row(transition_form.finalizeAfter) }}
</div>
{% if transition_form.freezeAfter is defined %} {% if transition_form.freezeAfter is defined %}
{{ form_row(transition_form.freezeAfter) }} {{ form_row(transition_form.freezeAfter) }}
{% endif %} {% endif %}
@ -31,7 +27,7 @@
{% else %} {% else %}
<div class="alert alert-chill-yellow"> <div class="alert alert-chill-yellow">
{% if entity_workflow.currentStep.isFinalizeAfter %} {% if entity_workflow.currentStep.isFinal %}
<p>{{ 'workflow.This workflow is finalized'|trans }}</p> <p>{{ 'workflow.This workflow is finalized'|trans }}</p>
{% else %} {% else %}
<p>{{ 'workflow.You are not allowed to apply a transition on this workflow'|trans }}</p> <p>{{ 'workflow.You are not allowed to apply a transition on this workflow'|trans }}</p>

View File

@ -1,13 +1,13 @@
{% set acl = "0" %}
{% if is_granted('CHILL_MAIN_WORKFLOW_CREATE', blank_workflow) %} {% if is_granted('CHILL_MAIN_WORKFLOW_CREATE', blank_workflow) %}
{# vue component #} {% set acl = "1" %}
<div data-pick-workflow="1"
data-related-entity-class="{{ blank_workflow.relatedEntityClass }}"
data-related-entity-id="{{ blank_workflow.relatedEntityId }}"
data-workflows-availables="{{ workflows_availables|json_encode()|e('html_attr') }}"
></div>
{% endif %} {% endif %}
{% if entity_workflows|length > 0 %} {# vue component #}
{# vue component #} <div data-list-workflows="1"
<div data-list-workflows="1" data-workflows="{{ entity_workflows_json|json_encode|e('html_attr') }}"></div> data-workflows="{{ entity_workflows_json|json_encode|e('html_attr') }}"
{% endif %} data-allow-create="{{ acl }}"
data-related-entity-class="{{ blank_workflow.relatedEntityClass }}"
data-related-entity-id="{{ blank_workflow.relatedEntityId }}"
data-workflows-availables="{{ workflows_availables|json_encode()|e('html_attr') }}"
></div>

View File

@ -2,30 +2,42 @@
<div class="flex-table"> <div class="flex-table">
{% for step in entity_workflow.stepsChained %} {% for step in entity_workflow.stepsChained %}
{% set place_labels = workflow_metadata(entity_workflow, 'label', step.currentStep) %}
{% set place_label = place_labels is null ? step.currentStep : place_labels|localize_translatable_string %}
<div class="item-bloc {{ 'bloc' ~ step.id }} {% if loop.first %}initial{% endif %}"> <div class="item-bloc {{ 'bloc' ~ step.id }} {% if loop.first %}initial{% endif %}">
<div class="item-row"> <div class="item-row">
{% if loop.first and step.next is null %} {% if loop.first and step.next is null %}
<div class="item-col"> <div class="item-col">
{{ 'workflow.No transitions'|trans }} {{ 'workflow.No transitions'|trans }}
</div> </div>
{% else %}
<div class="item-col">
{% if step.previous is not null and step.previous.freezeAfter == true %}
<i class="fa fa-snowflake-o fa-sm me-1" title="{{ 'workflow.Freezed'|trans }}"></i>
{% endif %}
</div>
<div class="item-col flex-column align-items-end">
<div class="decided">
{{ place_label }}
</div>
{#
<div class="decided">
<i class="fa fa-times fa-fw text-danger"></i>
Refusé
</div>
#}
</div>
{% endif %} {% endif %}
<div class="item-col flex-column align-items-end">
<div class="decided">
{% if not loop.first %}
<i class="fa fa-check fa-fw text-success"></i>
{% endif %}
{{ step.currentStep }}
</div>
{#
<div class="decided">
<i class="fa fa-times fa-fw text-danger"></i>
Refusé
</div>
#}
</div>
</div> </div>
{% if step.next is not null %} {% if step.next is not null %}
{% set transition = chill_workflow_transition_by_string(step.entityWorkflow, step.transitionAfter) %}
{% set transition_labels = workflow_metadata(step.entityWorkflow, 'label', transition) %}
{% set transition_label = transition_labels is null ? step.transitionAfter : transition_labels|localize_translatable_string %}
{% set forward = workflow_metadata(step.entityWorkflow, 'isForward', transition) %}
<div class="item-row separator"> <div class="item-row separator">
<div class="item-col" style="width: inherit;"> <div class="item-col" style="width: inherit;">
{% if step.transitionBy is not null %} {% if step.transitionBy is not null %}
@ -40,7 +52,12 @@
<div class="item-col flex-column align-items-end"> <div class="item-col flex-column align-items-end">
<div class="to-decision"> <div class="to-decision">
<i class="fa fa-share fa-fw text-secondary" title="transféré"></i> <i class="fa fa-share fa-fw text-secondary" title="transféré"></i>
{{ step.next.currentStep }} {% if forward %}
<i class="fa fa-check fa-fw text-success"></i>
{% else %}
<i class="fa fa-times fa-fw text-danger"></i>
{% endif %}
{{ transition_label }}
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,41 +1,62 @@
{% macro popoverContent(step) %} {% macro popoverContent(step) %}
<ul class="small_in_title"> <ul class="small_in_title">
<li> {% if step.previous is not null %}
<span class="item-key">{{ 'By'|trans ~ ' : ' }}</span> <li>
<b>{{ step.transitionBy|chill_entity_render_box }}</b> <span class="item-key">{{ 'By'|trans ~ ' : ' }}</span>
</li> <b>{{ step.previous.transitionBy|chill_entity_render_box }}</b>
<li> </li>
<span class="item-key">{{ 'Le'|trans ~ ' : ' }}</span> <li>
<b>{{ step.transitionAt|format_datetime('short', 'short') }}</b> <span class="item-key">{{ 'Le'|trans ~ ' : ' }}</span>
</li> <b>{{ step.previous.transitionAt|format_datetime('short', 'short') }}</b>
</li>
{% else %}
<li>
<span class="item-key">{{ 'workflow.Created by'|trans ~ ' : ' }}</span>
<b>{{ step.entityWorkflow.createdBy|chill_entity_render_box }}</b>
</li>
<li>
<span class="item-key">{{ 'Le'|trans ~ ' : ' }}</span>
<b>{{ step.entityWorkflow.createdAt|format_datetime('short', 'short') }}</b>
</li>
{% endif %}
</ul> </ul>
{% endmacro %} {% endmacro %}
{% macro popoverTitle(step) %}
{% if step.previous is not null and step.previous.freezeAfter == true %}
<i class="fa fa-snowflake-o fa-sm me-1" title="{{ 'workflow.Freezed'|trans }}"></i>
{% endif %}
{% if step.previous is not null %}
{% set transition = chill_workflow_transition_by_string(step.entityWorkflow, step.previous.transitionAfter) %}
{% set labels = workflow_metadata(step.entityWorkflow, 'label', transition) %}
{% set label = labels is null ? step.previous.transitionAfter : labels|localize_translatable_string %}
{{ label }}
{% endif %}
{% endmacro %}
{% macro breadcrumb(_ctx) %} {% macro breadcrumb(_ctx) %}
<div class="breadcrumb"> <div class="breadcrumb">
{% for step in _ctx.entity_workflow.stepsChained %} {% for step in _ctx.entity_workflow.stepsChained %}
{% if step.previous is null %} {% set labels = workflow_metadata(_ctx.entity_workflow, 'label', step.currentStep) %}
{# {% set label = labels is null ? step.currentStep : labels|localize_translatable_string %}
{% set popContent = "Point de départ du workflow" %} {% set popTitle = _self.popoverTitle(step) %}
{{ dump(step) }} {% set popContent = _self.popoverContent(step) %}
#}
{% set popContent = _self.popoverContent(step) %}
{% else %}
{% set popContent = _self.popoverContent(step.previous) %}
{% endif %}
<span class="mx-2" <span class="mx-2"
tabindex="0" tabindex="0"
data-bs-trigger="focus hover" data-bs-trigger="focus hover"
data-bs-toggle="popover" data-bs-toggle="popover"
data-bs-placement="bottom" data-bs-placement="bottom"
data-bs-custom-class="workflow-transition" data-bs-custom-class="workflow-transition"
title="{{ step.currentStep }}" title="{{ popTitle|e('html_attr') }}"
data-bs-content="{{ popContent|e('html_attr') }}" data-bs-content="{{ popContent|e('html_attr') }}"
> >
{% if step.currentStep == 'initial' %} {% if step.currentStep == 'initial' %}
<i class="fa fa-circle me-1 text-chill-yellow"></i> <i class="fa fa-circle me-1 text-chill-yellow"></i>
{% endif %} {% endif %}
{{ step.currentStep }} {% if step.previous is not null and step.previous.freezeAfter == true %}
<i class="fa fa-snowflake-o fa-sm me-1" title="{{ 'workflow.Freezed'|trans }}"></i>
{% endif %}
{{ label }}
</span> </span>
{% if not loop.last %} {% if not loop.last %}

View File

@ -60,26 +60,26 @@
{% endif %} {% endif %}
{% block content %} {% block content %}
<div class="col-8 main_search"> <div class="col-8 main_search">
<h2>{{ 'Search'|trans }}</h2> <h2>{{ 'Search'|trans }}</h2>
<form action="{{ path('chill_main_search') }}" method="get"> <form action="{{ path('chill_main_search') }}" method="get">
<input class="form-control form-control-lg" name="q" type="search" placeholder="{{ 'Search persons, ...'|trans }}" /> <input class="form-control form-control-lg" name="q" type="search" placeholder="{{ 'Search persons, ...'|trans }}" />
<center> <center>
<button type="submit" class="btn btn-lg btn-warning mt-3"> <button type="submit" class="btn btn-lg btn-warning mt-3">
<i class="fa fa-fw fa-search"></i> {{ 'Search'|trans }} <i class="fa fa-fw fa-search"></i> {{ 'Search'|trans }}
</button> </button>
<a class="btn btn-lg btn-misc mt-3" href="{{ path('chill_main_advanced_search_list') }}"> <a class="btn btn-lg btn-misc mt-3" href="{{ path('chill_main_advanced_search_list') }}">
<i class="fa fa-fw fa-search"></i> {{ 'Advanced search'|trans }} <i class="fa fa-fw fa-search"></i> {{ 'Advanced search'|trans }}
</a> </a>
</center> </center>
</form> </form>
</div> </div>
<div class="col-8">
<a href="{{ path('chill_crud_aside_activity_new', {'type' : 7, 'duration' : '600', 'note' : 'Pas des remarques' }) }}"><div class="bloc btn btn-success btn-md btn-block">Appel téléphonique</div></a> {# DISABLED {{ chill_widget('homepage', {} ) }} #}
</div>
{% include '@ChillMain/Homepage/index.html.twig' %}
{{ chill_widget('homepage', {} ) }}
{% endblock %} {% endblock %}
</div> </div>

View File

@ -0,0 +1,41 @@
<?php
/**
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Chill\MainBundle\Serializer\Model;
use JsonSerializable;
class Counter implements JsonSerializable
{
private int $counter;
public function __construct(?int $counter)
{
$this->counter = $counter;
}
public function getCounter(): ?int
{
return $this->counter;
}
public function jsonSerialize()
{
return ['count' => $this->counter];
}
public function setCounter(?int $counter): Counter
{
$this->counter = $counter;
return $this;
}
}

View File

@ -0,0 +1,59 @@
<?php
/**
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Chill\MainBundle\Serializer\Normalizer;
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
use Chill\MainBundle\Workflow\Helper\MetadataExtractor;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Workflow\Registry;
class EntityWorkflowNormalizer implements NormalizerInterface, NormalizerAwareInterface
{
use NormalizerAwareTrait;
private MetadataExtractor $metadataExtractor;
private Registry $registry;
public function __construct(MetadataExtractor $metadataExtractor, Registry $registry)
{
$this->metadataExtractor = $metadataExtractor;
$this->registry = $registry;
}
/**
* @param EntityWorkflow $object
*
* @return array
*/
public function normalize($object, ?string $format = null, array $context = [])
{
$workflow = $this->registry->get($object, $object->getWorkflowName());
return [
'type' => 'entity_workflow',
'id' => $object->getId(),
'relatedEntityClass' => $object->getRelatedEntityClass(),
'relatedEntityId' => $object->getRelatedEntityId(),
'workflow' => $this->metadataExtractor->buildArrayPresentationForWorkflow($workflow),
'currentStep' => $this->normalizer->normalize($object->getCurrentStep(), $format, $context),
'steps' => $this->normalizer->normalize($object->getStepsChained(), $format, $context),
];
}
public function supportsNormalization($data, ?string $format = null): bool
{
return $data instanceof EntityWorkflow && 'json' === $format;
}
}

View File

@ -0,0 +1,87 @@
<?php
/**
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Chill\MainBundle\Serializer\Normalizer;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStep;
use Chill\MainBundle\Workflow\Helper\MetadataExtractor;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
class EntityWorkflowStepNormalizer implements NormalizerAwareInterface, NormalizerInterface
{
use NormalizerAwareTrait;
private MetadataExtractor $metadataExtractor;
public function __construct(MetadataExtractor $metadataExtractor)
{
$this->metadataExtractor = $metadataExtractor;
}
/**
* @param EntityWorkflowStep $object
*/
public function normalize($object, ?string $format = null, array $context = []): array
{
$data = [
'type' => 'entity_workflow_step',
'id' => $object->getId(),
'comment' => $object->getComment(),
'currentStep' => $this->metadataExtractor->buildArrayPresentationForPlace($object->getEntityWorkflow(), $object),
'isFinal' => $object->isFinal(),
'isFreezed' => false,
'isFinalized' => false,
'transitionPrevious' => null,
'transitionAfter' => null,
'previousId' => null,
'nextId' => null,
'transitionPreviousBy' => null,
'transitionPreviousAt' => null,
];
if (null !== $previous = $object->getPrevious()) {
$data['transitionPrevious'] = $this->metadataExtractor
->buildArrayPresentationForTransition($object->getEntityWorkflow(), $object->getPrevious()->getTransitionAfter());
$data['previousId'] = $previous->getId();
$data['isFreezed'] = $previous->isFreezeAfter();
$data['transitionPreviousBy'] = $this->normalizer->normalize(
$previous->getTransitionBy(),
$format,
$context
);
$data['transitionPreviousAt'] = $this->normalizer->normalize(
$previous->getTransitionAt(),
$format,
$context
);
}
if (null !== $next = $object->getNext()) {
$data['nextId'] = $next->getId();
}
if (null !== $object->getTransitionAfter()) {
$data['transitionAfter'] = $this->metadataExtractor->buildArrayPresentationForTransition(
$object->getEntityWorkflow(),
$object->getTransitionAfter()
);
}
return $data;
}
public function supportsNormalization($data, ?string $format = null): bool
{
return $data instanceof EntityWorkflowStep && 'json' === $format;
}
}

View File

@ -0,0 +1,71 @@
<?php
/**
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Chill\MainBundle\Serializer\Normalizer;
use ArrayObject;
use Chill\MainBundle\Entity\Notification;
use Chill\MainBundle\Notification\NotificationHandlerManager;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
class NotificationNormalizer implements NormalizerAwareInterface, NormalizerInterface
{
use NormalizerAwareTrait;
private EntityManagerInterface $entityManager;
private NotificationHandlerManager $notificationHandlerManager;
private Security $security;
public function __construct(NotificationHandlerManager $notificationHandlerManager, EntityManagerInterface $entityManager, Security $security)
{
$this->notificationHandlerManager = $notificationHandlerManager;
$this->entityManager = $entityManager;
$this->security = $security;
}
/**
* @param Notification $object
*
* @return array|ArrayObject|bool|float|int|string|void|null
*/
public function normalize($object, ?string $format = null, array $context = [])
{
dump($object);
$entity = $this->entityManager
->getRepository($object->getRelatedEntityClass())
->find($object->getRelatedEntityId());
return [
'type' => 'notification',
'id' => $object->getId(),
'addressees' => $this->normalizer->normalize($object->getAddressees(), $format, $context),
'date' => $this->normalizer->normalize($object->getDate(), $format, $context),
'isRead' => $object->isReadBy($this->security->getUser()),
'message' => $object->getMessage(),
'relatedEntityClass' => $object->getRelatedEntityClass(),
'relatedEntityId' => $object->getRelatedEntityId(),
'sender' => $this->normalizer->normalize($object->getSender(), $format, $context),
'title' => $object->getTitle(),
'entity' => null !== $entity ? $this->normalizer->normalize($entity, $format, $context) : null,
];
}
public function supportsNormalization($data, ?string $format = null)
{
return $data instanceof Notification && 'json' === $format;
}
}

View File

@ -21,6 +21,7 @@ use Symfony\Component\Security\Core\Security;
use Symfony\Component\Workflow\Event\Event; use Symfony\Component\Workflow\Event\Event;
use Symfony\Component\Workflow\Event\GuardEvent; use Symfony\Component\Workflow\Event\GuardEvent;
use Symfony\Component\Workflow\TransitionBlocker; use Symfony\Component\Workflow\TransitionBlocker;
use function array_key_exists;
class EntityWorkflowTransitionEventSubscriber implements EventSubscriberInterface class EntityWorkflowTransitionEventSubscriber implements EventSubscriberInterface
{ {
@ -44,6 +45,7 @@ class EntityWorkflowTransitionEventSubscriber implements EventSubscriberInterfac
{ {
return [ return [
'workflow.transition' => 'onTransition', 'workflow.transition' => 'onTransition',
'workflow.completed' => 'onCompleted',
'workflow.guard' => [ 'workflow.guard' => [
['guardEntityWorkflow', 0], ['guardEntityWorkflow', 0],
], ],
@ -59,7 +61,7 @@ class EntityWorkflowTransitionEventSubscriber implements EventSubscriberInterfac
/** @var EntityWorkflow $entityWorkflow */ /** @var EntityWorkflow $entityWorkflow */
$entityWorkflow = $event->getSubject(); $entityWorkflow = $event->getSubject();
if ($entityWorkflow->isFinalize()) { if ($entityWorkflow->isFinal()) {
$event->addTransitionBlocker( $event->addTransitionBlocker(
new TransitionBlocker( new TransitionBlocker(
'workflow.The workflow is finalized', 'workflow.The workflow is finalized',
@ -88,7 +90,25 @@ class EntityWorkflowTransitionEventSubscriber implements EventSubscriberInterfac
} }
} }
public function onTransition(Event $event) public function onCompleted(Event $event): void
{
if (!$event->getSubject() instanceof EntityWorkflow) {
return;
}
/** @var EntityWorkflow $entityWorkflow */
$entityWorkflow = $event->getSubject();
$step = $entityWorkflow->getCurrentStep();
$placeMetadata = $event->getWorkflow()->getMetadataStore()
->getPlaceMetadata($step->getCurrentStep());
if (array_key_exists('isFinal', $placeMetadata) && true === $placeMetadata['isFinal']) {
$step->setIsFinal(true);
}
}
public function onTransition(Event $event): void
{ {
if (!$event->getSubject() instanceof EntityWorkflow) { if (!$event->getSubject() instanceof EntityWorkflow) {
return; return;

View File

@ -61,7 +61,7 @@ class NotificationOnTransition implements EventSubscriberInterface
$dests = array_merge( $dests = array_merge(
$entityWorkflow->getSubscriberToStep()->toArray(), $entityWorkflow->getSubscriberToStep()->toArray(),
$entityWorkflow->isFinalize() ? $entityWorkflow->getSubscriberToFinal()->toArray() : [], $entityWorkflow->isFinal() ? $entityWorkflow->getSubscriberToFinal()->toArray() : [],
$entityWorkflow->getCurrentStep()->getDestUser()->toArray() $entityWorkflow->getCurrentStep()->getDestUser()->toArray()
); );

View File

@ -12,6 +12,7 @@ declare(strict_types=1);
namespace Chill\MainBundle\Workflow\Helper; namespace Chill\MainBundle\Workflow\Helper;
use Chill\MainBundle\Entity\Workflow\EntityWorkflow; use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStep;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface; use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Symfony\Component\Workflow\Registry; use Symfony\Component\Workflow\Registry;
use Symfony\Component\Workflow\WorkflowInterface; use Symfony\Component\Workflow\WorkflowInterface;
@ -51,16 +52,37 @@ class MetadataExtractor
return $workflowsList; return $workflowsList;
} }
public function buildArrayPresentationForPlace(EntityWorkflow $entityWorkflow): array public function buildArrayPresentationForPlace(EntityWorkflow $entityWorkflow, ?EntityWorkflowStep $step = null): array
{ {
$workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName()); $workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
$step ??= $entityWorkflow->getCurrentStep();
$markingMetadata = $workflow->getMetadataStore()->getPlaceMetadata($entityWorkflow->getCurrentStep()->getCurrentStep()); $markingMetadata = $workflow->getMetadataStore()->getPlaceMetadata($step->getCurrentStep());
$text = array_key_exists('label', $markingMetadata) ? $text = array_key_exists('label', $markingMetadata) ?
$this->translatableStringHelper->localize($markingMetadata['label']) : $entityWorkflow->getCurrentStep()->getCurrentStep(); $this->translatableStringHelper->localize($markingMetadata['label']) : $step->getCurrentStep();
return ['name' => $entityWorkflow->getCurrentStep()->getCurrentStep(), 'text' => $text]; return ['name' => $step->getCurrentStep(), 'text' => $text];
}
public function buildArrayPresentationForTransition(EntityWorkflow $entityWorkflow, string $transitionName): array
{
$workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
$transitions = $workflow->getDefinition()->getTransitions();
foreach ($transitions as $transition) {
if ($transition->getName() === $transitionName) {
break;
}
}
$metadata = $workflow->getMetadataStore()->getTransitionMetadata($transition);
return [
'name' => $transition->getName(),
'text' => array_key_exists('label', $metadata) ?
$this->translatableStringHelper->localize($metadata['label']) : $transition->getName(),
'isForward' => $metadata['isForward'] ?? null,
];
} }
public function buildArrayPresentationForWorkflow(WorkflowInterface $workflow): array public function buildArrayPresentationForWorkflow(WorkflowInterface $workflow): array

View File

@ -24,6 +24,10 @@ class WorkflowTwigExtension extends AbstractExtension
[WorkflowTwigExtensionRuntime::class, 'listWorkflows'], [WorkflowTwigExtensionRuntime::class, 'listWorkflows'],
['needs_environment' => true, 'is_safe' => ['html']] ['needs_environment' => true, 'is_safe' => ['html']]
), ),
new TwigFunction(
'chill_workflow_transition_by_string',
[WorkflowTwigExtensionRuntime::class, 'getTransitionByString']
),
]; ];
} }
} }

View File

@ -17,6 +17,7 @@ use Chill\MainBundle\Workflow\EntityWorkflowManager;
use Chill\MainBundle\Workflow\Helper\MetadataExtractor; use Chill\MainBundle\Workflow\Helper\MetadataExtractor;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Workflow\Registry; use Symfony\Component\Workflow\Registry;
use Symfony\Component\Workflow\Transition;
use Twig\Environment; use Twig\Environment;
use Twig\Extension\RuntimeExtensionInterface; use Twig\Extension\RuntimeExtensionInterface;
@ -46,6 +47,20 @@ class WorkflowTwigExtensionRuntime implements RuntimeExtensionInterface
$this->normalizer = $normalizer; $this->normalizer = $normalizer;
} }
public function getTransitionByString(EntityWorkflow $entityWorkflow, string $key): ?Transition
{
$workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
$transitions = $workflow->getDefinition()->getTransitions();
foreach ($transitions as $transition) {
if ($transition->getName() === $key) {
return $transition;
}
}
return null;
}
public function listWorkflows(Environment $environment, string $relatedEntityClass, int $relatedEntityId, array $options = []): string public function listWorkflows(Environment $environment, string $relatedEntityClass, int $relatedEntityId, array $options = []): string
{ {
$blankEntityWorkflow = new EntityWorkflow(); $blankEntityWorkflow = new EntityWorkflow();

View File

@ -53,6 +53,7 @@ module.exports = function(encore, entries)
encore.addEntry('page_login', __dirname + '/Resources/public/page/login/index.js'); encore.addEntry('page_login', __dirname + '/Resources/public/page/login/index.js');
encore.addEntry('page_location', __dirname + '/Resources/public/page/location/index.js'); encore.addEntry('page_location', __dirname + '/Resources/public/page/location/index.js');
encore.addEntry('page_workflow_show', __dirname + '/Resources/public/page/workflow-show/index.js'); encore.addEntry('page_workflow_show', __dirname + '/Resources/public/page/workflow-show/index.js');
encore.addEntry('page_homepage_widget', __dirname + '/Resources/public/page/homepage_widget/index.js');
buildCKEditor(encore); buildCKEditor(encore);

View File

@ -0,0 +1,33 @@
<?php
/**
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Chill\Migrations\Main;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20220128211748 extends AbstractMigration
{
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE chill_main_workflow_entity_step RENAME COLUMN isFinal TO finalizeAfter;');
}
public function getDescription(): string
{
return 'rename workflow entity step from finalizeAfter to isFinal';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE chill_main_workflow_entity_step RENAME COLUMN finalizeAfter TO isFinal;');
}
}

View File

@ -370,9 +370,11 @@ Workflow history: Historique de la décision
workflow: workflow:
Created by: Créé par Created by: Créé par
Transition: Prochaine étape Transition to apply: Ma décision
dest for next steps: Utilisateurs qui valideront la prochaine étape dest for next steps: Utilisateurs qui valideront la prochaine étape
Freeze: Geler Freeze: Geler
Freezed: Gelé
freezed document: Le document est gelé
The associated element will be freezed: L'élément associé sera gelé et ne pourra plus être modifié après cette décision. The associated element will be freezed: L'élément associé sera gelé et ne pourra plus être modifié après cette décision.
Finalize: Étape finale Finalize: Étape finale
The workflow will be finalized: Le suivi est clôturé lors de cette décision. The workflow will be finalized: Le suivi est clôturé lors de cette décision.
@ -386,8 +388,8 @@ workflow:
Evaluation (n°%eval%): "Évaluation (n°%eval%)" Evaluation (n°%eval%): "Évaluation (n°%eval%)"
Document (n°%doc%): "Document (n°%doc%)" Document (n°%doc%): "Document (n°%doc%)"
Work (n°%w%): "Action d'accompagnement (n°%w%)" Work (n°%w%): "Action d'accompagnement (n°%w%)"
subscribed: Souscrit subscribed: Workflows suivis
dest: Destinataire de l'étape finale dest: Workflows en attente d'action
you subscribed to all steps: Vous recevrez une notification à chaque étape you subscribed to all steps: Vous recevrez une notification à chaque étape
you subscribed to final step: Vous recevrez une notification à l'étape finale you subscribed to final step: Vous recevrez une notification à l'étape finale
@ -396,6 +398,7 @@ Subscribe all steps: Recevoir une notification à chaque étape
notification: notification:
Notification: Notification Notification: Notification
Notifications: Notifications
My own notifications: Mes notifications My own notifications: Mes notifications
Notify: Envoyer une notification Notify: Envoyer une notification
Send: Envoyer Send: Envoyer

View File

@ -13,7 +13,9 @@ namespace Chill\PersonBundle\Controller;
use Chill\MainBundle\CRUD\Controller\ApiController; use Chill\MainBundle\CRUD\Controller\ApiController;
use Chill\MainBundle\Entity\Scope; use Chill\MainBundle\Entity\Scope;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Serializer\Model\Collection; use Chill\MainBundle\Serializer\Model\Collection;
use Chill\MainBundle\Serializer\Model\Counter;
use Chill\PersonBundle\AccompanyingPeriod\Suggestion\ReferralsSuggestionInterface; use Chill\PersonBundle\AccompanyingPeriod\Suggestion\ReferralsSuggestionInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork; use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork;
@ -23,8 +25,11 @@ use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Entity\SocialWork\SocialIssue; use Chill\PersonBundle\Entity\SocialWork\SocialIssue;
use Chill\PersonBundle\Privacy\AccompanyingPeriodPrivacyEvent; use Chill\PersonBundle\Privacy\AccompanyingPeriodPrivacyEvent;
use Chill\PersonBundle\Repository\AccompanyingPeriodACLAwareRepository; use Chill\PersonBundle\Repository\AccompanyingPeriodACLAwareRepository;
use Chill\PersonBundle\Repository\AccompanyingPeriodRepository;
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter; use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter;
use Chill\ThirdPartyBundle\Entity\ThirdParty; use Chill\ThirdPartyBundle\Entity\ThirdParty;
use DateInterval;
use DateTimeImmutable;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter; use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Exception\BadRequestException; use Symfony\Component\HttpFoundation\Exception\BadRequestException;
@ -32,13 +37,14 @@ use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Serializer\Exception\RuntimeException; use Symfony\Component\Serializer\Exception\RuntimeException;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Validator\ConstraintViolationList; use Symfony\Component\Validator\ConstraintViolationList;
use Symfony\Component\Validator\ConstraintViolationListInterface; use Symfony\Component\Validator\ConstraintViolationListInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface; use Symfony\Component\Validator\Validator\ValidatorInterface;
use Symfony\Component\Workflow\Registry; use Symfony\Component\Workflow\Registry;
use function array_values; use function array_values;
use function count; use function count;
@ -46,6 +52,8 @@ final class AccompanyingCourseApiController extends ApiController
{ {
private AccompanyingPeriodACLAwareRepository $accompanyingPeriodACLAwareRepository; private AccompanyingPeriodACLAwareRepository $accompanyingPeriodACLAwareRepository;
private AccompanyingPeriodRepository $accompanyingPeriodRepository;
private EventDispatcherInterface $eventDispatcher; private EventDispatcherInterface $eventDispatcher;
private ReferralsSuggestionInterface $referralAvailable; private ReferralsSuggestionInterface $referralAvailable;
@ -55,17 +63,19 @@ final class AccompanyingCourseApiController extends ApiController
private ValidatorInterface $validator; private ValidatorInterface $validator;
public function __construct( public function __construct(
EventDispatcherInterface $eventDispatcher, AccompanyingPeriodRepository $accompanyingPeriodRepository,
ValidatorInterface $validator,
Registry $registry,
AccompanyingPeriodACLAwareRepository $accompanyingPeriodACLAwareRepository, AccompanyingPeriodACLAwareRepository $accompanyingPeriodACLAwareRepository,
ReferralsSuggestionInterface $referralAvailable EventDispatcherInterface $eventDispatcher,
ReferralsSuggestionInterface $referralAvailable,
Registry $registry,
ValidatorInterface $validator
) { ) {
$this->eventDispatcher = $eventDispatcher; $this->accompanyingPeriodRepository = $accompanyingPeriodRepository;
$this->validator = $validator;
$this->registry = $registry;
$this->accompanyingPeriodACLAwareRepository = $accompanyingPeriodACLAwareRepository; $this->accompanyingPeriodACLAwareRepository = $accompanyingPeriodACLAwareRepository;
$this->eventDispatcher = $eventDispatcher;
$this->referralAvailable = $referralAvailable; $this->referralAvailable = $referralAvailable;
$this->registry = $registry;
$this->validator = $validator;
} }
public function commentApi($id, Request $request, string $_format): Response public function commentApi($id, Request $request, string $_format): Response
@ -99,6 +109,57 @@ final class AccompanyingCourseApiController extends ApiController
]); ]);
} }
/**
* @Route("/api/1.0/person/accompanying-course/list/by-recent-attributions")
*/
public function findMyRecentCourseAttribution(Request $request): JsonResponse
{
$this->denyAccessUnlessGranted('ROLE_USER');
$user = $this->getUser();
if (!$user instanceof User) {
throw new AccessDeniedException();
}
$since = (new DateTimeImmutable('now'))->sub(new DateInterval('P15D'));
$total = $this->accompanyingPeriodRepository->countByRecentUserHistory($user, $since);
if ($request->query->getBoolean('countOnly', false)) {
return new JsonResponse(
$this->getSerializer()->serialize(new Counter($total), 'json'),
JsonResponse::HTTP_OK,
[],
true
);
}
$paginator = $this->getPaginatorFactory()->create($total);
if (0 === $total) {
return new JsonResponse(
$this->getSerializer()->serialize(new Collection([], $paginator), 'json'),
JsonResponse::HTTP_OK,
[],
true
);
}
$courses = $this->accompanyingPeriodRepository->findByRecentUserHistory(
$user,
$since,
$paginator->getItemsPerPage(),
$paginator->getCurrentPageFirstItemNumber()
);
return new JsonResponse(
$this->getSerializer()->serialize(new Collection($courses, $paginator), 'json', ['groups' => ['read']]),
JsonResponse::HTTP_OK,
[],
true
);
}
/** /**
* @ParamConverter("person", options={"id": "person_id"}) * @ParamConverter("person", options={"id": "person_id"})
*/ */

View File

@ -12,10 +12,54 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Controller; namespace Chill\PersonBundle\Controller;
use Chill\MainBundle\CRUD\Controller\ApiController; use Chill\MainBundle\CRUD\Controller\ApiController;
use Chill\MainBundle\Serializer\Model\Collection;
use Chill\MainBundle\Serializer\Model\Counter;
use Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkRepository;
use DateInterval;
use DateTimeImmutable;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
class AccompanyingCourseWorkApiController extends ApiController class AccompanyingCourseWorkApiController extends ApiController
{ {
private AccompanyingPeriodWorkRepository $accompanyingPeriodWorkRepository;
public function __construct(AccompanyingPeriodWorkRepository $accompanyingPeriodWorkRepository)
{
$this->accompanyingPeriodWorkRepository = $accompanyingPeriodWorkRepository;
}
/**
* @Route("/api/1.0/person/accompanying-period/work/my-near-end")
*/
public function myWorksNearEndDate(Request $request): JsonResponse
{
$since = (new DateTimeImmutable('now'))
->sub(new DateInterval('P' . $request->query->getInt('since', 15) . 'D'));
$until = (new DateTimeImmutable('now'))
->add(new DateInterval('P' . $request->query->getInt('since', 15) . 'D'));
$total = $this->accompanyingPeriodWorkRepository
->countNearEndDateByUser($this->getUser(), $since, $until);
if ($request->query->getBoolean('countOnly', false)) {
return $this->json(
new Counter($total),
JsonResponse::HTTP_OK,
[],
['groups' => ['read']]
);
}
$paginator = $this->getPaginatorFactory()->create($total);
$works = $this->accompanyingPeriodWorkRepository
->findNearEndDateByUser($this->getUser(), $since, $until, $paginator->getItemsPerPage(), $paginator->getCurrentPageFirstItemNumber());
$collection = new Collection($works, $paginator);
return $this->json($collection, 200, [], ['groups' => ['read']]);
}
protected function getContextForSerialization(string $action, Request $request, string $_format, $entity): array protected function getContextForSerialization(string $action, Request $request, string $_format, $entity): array
{ {
switch ($action) { switch ($action) {

View File

@ -15,11 +15,15 @@ use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepository; use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepository;
use Chill\MainBundle\Pagination\PaginatorFactory; use Chill\MainBundle\Pagination\PaginatorFactory;
use Chill\MainBundle\Serializer\Model\Collection; use Chill\MainBundle\Serializer\Model\Collection;
use Chill\MainBundle\Serializer\Model\Counter;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluation; use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluation;
use Chill\PersonBundle\Entity\SocialWork\Evaluation; use Chill\PersonBundle\Entity\SocialWork\Evaluation;
use Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationRepository;
use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\SerializerInterface; use Symfony\Component\Serializer\SerializerInterface;
@ -28,20 +32,28 @@ use function in_array;
class AccompanyingPeriodWorkEvaluationApiController class AccompanyingPeriodWorkEvaluationApiController
{ {
private AccompanyingPeriodWorkEvaluationRepository $accompanyingPeriodWorkEvaluationRepository;
private DocGeneratorTemplateRepository $docGeneratorTemplateRepository; private DocGeneratorTemplateRepository $docGeneratorTemplateRepository;
private PaginatorFactory $paginatorFactory; private PaginatorFactory $paginatorFactory;
private Security $security;
private SerializerInterface $serializer; private SerializerInterface $serializer;
public function __construct( public function __construct(
AccompanyingPeriodWorkEvaluationRepository $accompanyingPeriodWorkEvaluationRepository,
DocGeneratorTemplateRepository $docGeneratorTemplateRepository, DocGeneratorTemplateRepository $docGeneratorTemplateRepository,
SerializerInterface $serializer, SerializerInterface $serializer,
PaginatorFactory $paginatorFactory PaginatorFactory $paginatorFactory,
Security $security
) { ) {
$this->accompanyingPeriodWorkEvaluationRepository = $accompanyingPeriodWorkEvaluationRepository;
$this->docGeneratorTemplateRepository = $docGeneratorTemplateRepository; $this->docGeneratorTemplateRepository = $docGeneratorTemplateRepository;
$this->serializer = $serializer; $this->serializer = $serializer;
$this->paginatorFactory = $paginatorFactory; $this->paginatorFactory = $paginatorFactory;
$this->security = $security;
} }
/** /**
@ -76,4 +88,39 @@ class AccompanyingPeriodWorkEvaluationApiController
] ]
), JsonResponse::HTTP_OK, [], true); ), JsonResponse::HTTP_OK, [], true);
} }
/**
* @Route("/api/1.0/person/accompanying-period/work/evaluation/my-near-end")
*/
public function myWorksNearEndDate(Request $request): JsonResponse
{
$total = $this->accompanyingPeriodWorkEvaluationRepository
->countNearMaxDateByUser($this->security->getUser());
if ($request->query->getBoolean('countOnly', false)) {
return new JsonResponse(
$this->serializer->serialize(new Counter($total), 'json', ['groups' => 'read']),
JsonResponse::HTTP_OK,
[],
true
);
}
$paginator = $this->paginatorFactory->create($total);
$works = $this->accompanyingPeriodWorkEvaluationRepository
->findNearMaxDateByUser(
$this->security->getUser(),
$paginator->getItemsPerPage(),
$paginator->getCurrentPageFirstItemNumber()
);
$collection = new Collection($works, $paginator);
return new JsonResponse(
$this->serializer->serialize($collection, 'json', ['groups' => 'read']),
JsonResponse::HTTP_OK,
[],
true
);
}
} }

View File

@ -26,6 +26,7 @@ use Chill\PersonBundle\Entity\AccompanyingPeriod\ClosingMotive;
use Chill\PersonBundle\Entity\AccompanyingPeriod\Comment; use Chill\PersonBundle\Entity\AccompanyingPeriod\Comment;
use Chill\PersonBundle\Entity\AccompanyingPeriod\Origin; use Chill\PersonBundle\Entity\AccompanyingPeriod\Origin;
use Chill\PersonBundle\Entity\AccompanyingPeriod\Resource; use Chill\PersonBundle\Entity\AccompanyingPeriod\Resource;
use Chill\PersonBundle\Entity\AccompanyingPeriod\UserHistory;
use Chill\PersonBundle\Entity\SocialWork\SocialIssue; use Chill\PersonBundle\Entity\SocialWork\SocialIssue;
use Chill\PersonBundle\Validator\Constraints\AccompanyingPeriod\AccompanyingPeriodValidity; use Chill\PersonBundle\Validator\Constraints\AccompanyingPeriod\AccompanyingPeriodValidity;
use Chill\PersonBundle\Validator\Constraints\AccompanyingPeriod\ParticipationOverlap; use Chill\PersonBundle\Validator\Constraints\AccompanyingPeriod\ParticipationOverlap;
@ -338,6 +339,14 @@ class AccompanyingPeriod implements
*/ */
private ?User $user = null; private ?User $user = null;
/**
* @ORM\OneToMany(targetEntity=UserHistory::class, mappedBy="accompanyingPeriod", orphanRemoval=true,
* cascade={"persist", "remove"})
*
* @var Collection|UserHistory[]
*/
private Collection $userHistories;
/** /**
* Temporary field, which is filled when the user is changed. * Temporary field, which is filled when the user is changed.
* *
@ -370,6 +379,7 @@ class AccompanyingPeriod implements
$this->comments = new ArrayCollection(); $this->comments = new ArrayCollection();
$this->works = new ArrayCollection(); $this->works = new ArrayCollection();
$this->resources = new ArrayCollection(); $this->resources = new ArrayCollection();
$this->userHistories = new ArrayCollection();
} }
/** /**
@ -1214,10 +1224,20 @@ class AccompanyingPeriod implements
return $this; return $this;
} }
public function setUser(User $user): self public function setUser(?User $user): self
{ {
if ($this->user !== $user) { if ($this->user !== $user) {
$this->userPrevious = $this->user; $this->userPrevious = $this->user;
foreach ($this->userHistories as $history) {
if (null === $history->getEndDate()) {
$history->setEndDate(new DateTimeImmutable('now'));
}
}
if (null !== $user) {
$this->userHistories->add(new UserHistory($this, $user));
}
} }
$this->user = $user; $this->user = $user;

View File

@ -0,0 +1,96 @@
<?php
/**
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
use Chill\MainBundle\Doctrine\Model\TrackCreationTrait;
use Chill\MainBundle\Entity\User;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use DateTimeImmutable;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
* @ORM\Table("chill_person_accompanying_period_user_history")
*/
class UserHistory implements TrackCreationInterface
{
use TrackCreationTrait;
/**
* @ORM\ManyToOne(targetEntity=AccompanyingPeriod::class, inversedBy="userHistories")
* @ORM\JoinColumn(nullable=false)
*/
private ?AccompanyingPeriod $accompanyingPeriod;
/**
* @ORM\Column(type="datetime_immutable", nullable=true, options={"default": null})
*/
private ?DateTimeImmutable $endDate = null;
/**
* @ORM\Id
* @ORM\Column(name="id", type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*/
private ?int $id = null;
/**
* @ORM\Column(type="datetime_immutable", nullable=false)
*/
private DateTimeImmutable $startDate;
/**
* @ORM\ManyToOne(targetEntity=User::class)
* @ORM\JoinColumn(nullable=false)
*/
private User $user;
public function __construct(AccompanyingPeriod $accompanyingPeriod, User $user, ?DateTimeImmutable $startDate = null)
{
$this->startDate = $startDate ?? new DateTimeImmutable('now');
$this->accompanyingPeriod = $accompanyingPeriod;
$this->user = $user;
}
public function getAccompanyingPeriod(): AccompanyingPeriod
{
return $this->accompanyingPeriod;
}
public function getEndDate(): ?DateTimeImmutable
{
return $this->endDate;
}
public function getId(): ?int
{
return $this->id;
}
public function getStartDate(): DateTimeImmutable
{
return $this->startDate;
}
public function getUser(): User
{
return $this->user;
}
public function setEndDate(?DateTimeImmutable $endDate): UserHistory
{
$this->endDate = $endDate;
return $this;
}
}

View File

@ -11,9 +11,12 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Repository\AccompanyingPeriod; namespace Chill\PersonBundle\Repository\AccompanyingPeriod;
use Chill\MainBundle\Entity\User;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluation; use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluation;
use DateTimeImmutable;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository; use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\ObjectRepository; use Doctrine\Persistence\ObjectRepository;
class AccompanyingPeriodWorkEvaluationRepository implements ObjectRepository class AccompanyingPeriodWorkEvaluationRepository implements ObjectRepository
@ -25,6 +28,12 @@ class AccompanyingPeriodWorkEvaluationRepository implements ObjectRepository
$this->repository = $entityManager->getRepository(AccompanyingPeriodWorkEvaluation::class); $this->repository = $entityManager->getRepository(AccompanyingPeriodWorkEvaluation::class);
} }
public function countNearMaxDateByUser(User $user): int
{
return $this->buildQueryNearMaxDateByUser($user)
->select('count(e)')->getQuery()->getSingleScalarResult();
}
public function find($id): ?AccompanyingPeriodWorkEvaluation public function find($id): ?AccompanyingPeriodWorkEvaluation
{ {
return $this->repository->find($id); return $this->repository->find($id);
@ -39,8 +48,8 @@ class AccompanyingPeriodWorkEvaluationRepository implements ObjectRepository
} }
/** /**
* @param null|mixed $limit * @param int $limit
* @param null|mixed $offset * @param int $offset
* *
* @return array|AccompanyingPeriodWorkEvaluation[] * @return array|AccompanyingPeriodWorkEvaluation[]
*/ */
@ -49,13 +58,45 @@ class AccompanyingPeriodWorkEvaluationRepository implements ObjectRepository
return $this->repository->findBy($criteria, $orderBy, $limit, $offset); return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
} }
public function findOneBy(array $criteria): ?AccompanyingPeriodWorkEvaluation public function findNearMaxDateByUser(User $user, int $limit = 20, int $offset = 0): array
{ {
return $this->findOneBy($criteria); return $this->buildQueryNearMaxDateByUser($user)
->select('e')
->setFirstResult($offset)
->setMaxResults($limit)
->getQuery()
->getResult();
} }
public function getClassName() public function findOneBy(array $criteria): ?AccompanyingPeriodWorkEvaluation
{
return $this->repository->findOneBy($criteria);
}
public function getClassName(): string
{ {
return AccompanyingPeriodWorkEvaluation::class; return AccompanyingPeriodWorkEvaluation::class;
} }
private function buildQueryNearMaxDateByUser(User $user): QueryBuilder
{
$qb = $this->repository->createQueryBuilder('e');
$qb
->join('e.accompanyingPeriodWork', 'work')
->join('work.accompanyingPeriod', 'period')
->where(
$qb->expr()->andX(
$qb->expr()->eq('period.user', ':user'),
$qb->expr()->isNull('e.endDate'),
$qb->expr()->gte(':now', $qb->expr()->diff('e.maxDate', 'e.warningInterval'))
)
)
->setParameters([
'user' => $user,
'now' => new DateTimeImmutable('now'),
]);
return $qb;
}
} }

View File

@ -11,10 +11,14 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Repository\AccompanyingPeriod; namespace Chill\PersonBundle\Repository\AccompanyingPeriod;
use Chill\MainBundle\Entity\User;
use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork; use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork;
use Chill\PersonBundle\Entity\SocialWork\SocialAction;
use DateTimeImmutable;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository; use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\ObjectRepository; use Doctrine\Persistence\ObjectRepository;
final class AccompanyingPeriodWorkRepository implements ObjectRepository final class AccompanyingPeriodWorkRepository implements ObjectRepository
@ -41,6 +45,12 @@ final class AccompanyingPeriodWorkRepository implements ObjectRepository
->getSingleScalarResult(); ->getSingleScalarResult();
} }
public function countNearEndDateByUser(User $user, DateTimeImmutable $since, DateTimeImmutable $until): int
{
return $this->buildQueryNearEndDateByUser($user, $since, $until)
->select('count(w)')->getQuery()->getSingleScalarResult();
}
public function find($id): ?AccompanyingPeriodWork public function find($id): ?AccompanyingPeriodWork
{ {
return $this->repository->find($id); return $this->repository->find($id);
@ -68,6 +78,16 @@ final class AccompanyingPeriodWorkRepository implements ObjectRepository
return $this->repository->findByAccompanyingPeriod($period, $orderBy, $limit, $offset); return $this->repository->findByAccompanyingPeriod($period, $orderBy, $limit, $offset);
} }
public function findNearEndDateByUser(User $user, DateTimeImmutable $since, DateTimeImmutable $until, int $limit = 20, int $offset = 0): array
{
return $this->buildQueryNearEndDateByUser($user, $since, $until)
->select('w')
->setFirstResult($offset)
->setMaxResults($limit)
->getQuery()
->getResult();
}
public function findOneBy(array $criteria): ?AccompanyingPeriodWork public function findOneBy(array $criteria): ?AccompanyingPeriodWork
{ {
return $this->repository->findOneBy($criteria); return $this->repository->findOneBy($criteria);
@ -78,22 +98,6 @@ final class AccompanyingPeriodWorkRepository implements ObjectRepository
return AccompanyingPeriodWork::class; return AccompanyingPeriodWork::class;
} }
public function toDelete()
{
$qb = $this->buildQueryBySocialActionWithDescendants($action);
$qb->select('g');
foreach ($orderBy as $sort => $order) {
$qb->addOrderBy('g.' . $sort, $order);
}
return $qb
->setMaxResults($limit)
->setFirstResult($offset)
->getQuery()
->getResult();
}
private function buildQueryBySocialActionWithDescendants(SocialAction $action): QueryBuilder private function buildQueryBySocialActionWithDescendants(SocialAction $action): QueryBuilder
{ {
$actions = $action->getDescendantsWithThis(); $actions = $action->getDescendantsWithThis();
@ -103,12 +107,34 @@ final class AccompanyingPeriodWorkRepository implements ObjectRepository
$orx = $qb->expr()->orX(); $orx = $qb->expr()->orX();
$i = 0; $i = 0;
foreach ($actions as $action) { foreach ($actions as $a) {
$orx->add(":action_{$i} MEMBER OF g.socialActions"); $orx->add(":action_{$i} MEMBER OF g.socialActions");
$qb->setParameter("action_{$i}", $action); $qb->setParameter("action_{$i}", $a);
} }
$qb->where($orx); $qb->where($orx);
return $qb; return $qb;
} }
private function buildQueryNearEndDateByUser(User $user, DateTimeImmutable $since, DateTimeImmutable $until): QueryBuilder
{
$qb = $this->repository->createQueryBuilder('w');
$qb
->join('w.accompanyingPeriod', 'period')
->where(
$qb->expr()->andX(
$qb->expr()->eq('period.user', ':user'),
$qb->expr()->gte('w.endDate', ':since'),
$qb->expr()->lte('w.startDate', ':until')
)
)
->setParameters([
'user' => $user,
'since' => $since,
'until' => $until,
]);
return $qb;
}
} }

View File

@ -11,7 +11,9 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Repository; namespace Chill\PersonBundle\Repository;
use Chill\MainBundle\Entity\User;
use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\AccompanyingPeriod;
use DateTimeImmutable;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository; use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\QueryBuilder; use Doctrine\ORM\QueryBuilder;
@ -26,6 +28,13 @@ final class AccompanyingPeriodRepository implements ObjectRepository
$this->repository = $entityManager->getRepository(AccompanyingPeriod::class); $this->repository = $entityManager->getRepository(AccompanyingPeriod::class);
} }
public function countByRecentUserHistory(User $user, DateTimeImmutable $since): int
{
$qb = $this->buildQueryByRecentUserHistory($user, $since);
return $qb->select('count(a)')->getQuery()->getSingleScalarResult();
}
public function countBy(array $criteria): int public function countBy(array $criteria): int
{ {
return $this->repository->count($criteria); return $this->repository->count($criteria);
@ -54,6 +63,21 @@ final class AccompanyingPeriodRepository implements ObjectRepository
return $this->repository->findBy($criteria, $orderBy, $limit, $offset); return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
} }
/**
* @return array|AccompanyingPeriod[]
*/
public function findByRecentUserHistory(User $user, DateTimeImmutable $since, ?int $limit = 20, ?int $offset = 0): array
{
$qb = $this->buildQueryByRecentUserHistory($user, $since);
return $qb->select('a')
->distinct(true)
->getQuery()
->setMaxResults($limit)
->setFirstResult($offset)
->getResult();
}
public function findOneBy(array $criteria): ?AccompanyingPeriod public function findOneBy(array $criteria): ?AccompanyingPeriod
{ {
return $this->findOneBy($criteria); return $this->findOneBy($criteria);
@ -63,4 +87,19 @@ final class AccompanyingPeriodRepository implements ObjectRepository
{ {
return AccompanyingPeriod::class; return AccompanyingPeriod::class;
} }
private function buildQueryByRecentUserHistory(User $user, DateTimeImmutable $since): QueryBuilder
{
$qb = $this->repository->createQueryBuilder('a');
$qb
->join('a.userHistories', 'userHistory')
->where($qb->expr()->eq('a.user', ':user'))
->andWhere($qb->expr()->gte('userHistory.startDate', ':since'))
->andWhere($qb->expr()->isNull('userHistory.endDate'))
->setParameter('user', $user)
->setParameter('since', $since);
return $qb;
}
} }

View File

@ -80,6 +80,8 @@ div.dashboard {
} }
} }
div.dashboard, div.dashboard,
h4.badge-title,
h3.badge-title,
h2.badge-title { h2.badge-title {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@ -128,6 +130,8 @@ ul.columns { // XS:1 SM:2 MD:1 LG:2 XL:2 XXL:2
/// dashboard_like_badge in AccompanyingCourse Work list Page /// dashboard_like_badge in AccompanyingCourse Work list Page
div[class*='accompanying_course_work'] { div[class*='accompanying_course_work'] {
div.dashboard, div.dashboard,
h4.badge-title,
h3.badge-title,
h2.badge-title { h2.badge-title {
span.title_label { span.title_label {
// Calculate same color then border:groove // Calculate same color then border:groove
@ -143,6 +147,8 @@ div[class*='accompanying_course_work'] {
/// dashboard_like_badge in Activities on resume page /// dashboard_like_badge in Activities on resume page
div[class*='activity-'] { div[class*='activity-'] {
div.dashboard, div.dashboard,
h4.badge-title,
h3.badge-title,
h2.badge-title { h2.badge-title {
span.title_label { span.title_label {
// Calculate same color then border:groove // Calculate same color then border:groove

View File

@ -152,8 +152,8 @@ const appMessages = {
not_valid: "Sélectionnez un métier du référent" not_valid: "Sélectionnez un métier du référent"
}, },
startdate: { startdate: {
change: "Modifier la date de début", change: "Date d'ouverture",
date: "Date de début", date: "Date d'ouverture",
}, },
// catch errors // catch errors
'Error while updating AccompanyingPeriod Course.': "Erreur du serveur lors de la mise à jour du parcours d'accompagnement.", 'Error while updating AccompanyingPeriod Course.': "Erreur du serveur lors de la mise à jour du parcours d'accompagnement.",

View File

@ -244,16 +244,15 @@
</div> </div>
<ul class="record_actions sticky-form-buttons"> <ul class="record_actions sticky-form-buttons">
<!--
FAIT REPETER tout le template de App.vue plusieurs fois
<li> <li>
<pick-workflow <list-workflow-modal
:workflows="this.work.workflows"
:allowCreate="true"
relatedEntityClass="Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork" relatedEntityClass="Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork"
:relatedEntityId="this.work.id" :relatedEntityId="this.work.id"
:workflows="this.workflows" :workflowsAvailables="this.work.workflows_availables"
></pick-workflow> ></list-workflow-modal>
</li> </li>
-->
<li v-if="!isPosting"> <li v-if="!isPosting">
<button class="btn btn-save" @click="submit"> <button class="btn btn-save" @click="submit">
@ -282,6 +281,7 @@ import AddressRenderBox from 'ChillMainAssets/vuejs/_components/Entity/AddressRe
import ThirdPartyRenderBox from 'ChillThirdPartyAssets/vuejs/_components/Entity/ThirdPartyRenderBox.vue'; import ThirdPartyRenderBox from 'ChillThirdPartyAssets/vuejs/_components/Entity/ThirdPartyRenderBox.vue';
import PickTemplate from 'ChillDocGeneratorAssets/vuejs/_components/PickTemplate.vue'; import PickTemplate from 'ChillDocGeneratorAssets/vuejs/_components/PickTemplate.vue';
import OnTheFly from 'ChillMainAssets/vuejs/OnTheFly/components/OnTheFly.vue'; import OnTheFly from 'ChillMainAssets/vuejs/OnTheFly/components/OnTheFly.vue';
import ListWorkflowModal from 'ChillMainAssets/vuejs/_components/EntityWorkflow/ListWorkflowModal.vue';
import PickWorkflow from 'ChillMainAssets/vuejs/_components/EntityWorkflow/PickWorkflow.vue'; import PickWorkflow from 'ChillMainAssets/vuejs/_components/EntityWorkflow/PickWorkflow.vue';
import PersonText from 'ChillPersonAssets/vuejs/_components/Entity/PersonText.vue'; import PersonText from 'ChillPersonAssets/vuejs/_components/Entity/PersonText.vue';
@ -331,9 +331,11 @@ export default {
AddressRenderBox, AddressRenderBox,
ThirdPartyRenderBox, ThirdPartyRenderBox,
PickTemplate, PickTemplate,
ListWorkflowModal,
OnTheFly,
PickWorkflow, PickWorkflow,
OnTheFly, OnTheFly,
PersonText, PersonText,
}, },
i18n, i18n,
data() { data() {

View File

@ -1,5 +1,6 @@
<template> <template>
<div> <div>
<a id="evaluations"></a>
<div class="item-title" :title="evaluation.id || 'no id yet'"> <div class="item-title" :title="evaluation.id || 'no id yet'">
<span>{{ evaluation.evaluation.title.fr }}</span> <span>{{ evaluation.evaluation.title.fr }}</span>
</div> </div>
@ -9,12 +10,16 @@
<ul class="record_actions"> <ul class="record_actions">
<li v-if="evaluation.workflows_availables.length > 0"> <li v-if="evaluation.workflows_availables.length > 0">
<pick-workflow
<list-workflow-modal
:workflows="evaluation.workflows"
:allowCreate="true"
relatedEntityClass="faked" relatedEntityClass="faked"
:relatedEntityId="evaluation.id" :relatedEntityId="evaluation.id"
:workflowsAvailables="evaluation.workflows_availables" :workflowsAvailables="evaluation.workflows_availables"
@goToGenerateWorkflow="goToGenerateWorkflow" @goToGenerateWorkflow="goToGenerateWorkflow"
></pick-workflow> ></list-workflow-modal>
</li> </li>
<li> <li>
<a class="btn btn-delete" @click="modal.showModal = true" :title="$t('action.delete')"></a> <a class="btn btn-delete" @click="modal.showModal = true" :title="$t('action.delete')"></a>
@ -43,7 +48,7 @@
<script> <script>
import FormEvaluation from './FormEvaluation.vue'; import FormEvaluation from './FormEvaluation.vue';
import Modal from 'ChillMainAssets/vuejs/_components/Modal'; import Modal from 'ChillMainAssets/vuejs/_components/Modal';
import PickWorkflow from 'ChillMainAssets/vuejs/_components/EntityWorkflow/PickWorkflow.vue'; import ListWorkflowModal from 'ChillMainAssets/vuejs/_components/EntityWorkflow/ListWorkflowModal.vue';
import {buildLinkCreate} from 'ChillMainAssets/lib/entity-workflow/api.js'; import {buildLinkCreate} from 'ChillMainAssets/lib/entity-workflow/api.js';
const i18n = { const i18n = {
@ -72,7 +77,7 @@ export default {
components: { components: {
FormEvaluation, FormEvaluation,
Modal, Modal,
PickWorkflow, ListWorkflowModal,
}, },
props: ['evaluation'], props: ['evaluation'],
i18n, i18n,

View File

@ -77,7 +77,7 @@ export default {
}; };
</script> </script>
<style lang="scss" scoped> <style lang="scss">
div.results { div.results {
div.list-item { div.list-item {
padding: 0.4em 0.8em; padding: 0.4em 0.8em;

View File

@ -4,7 +4,7 @@
<span class="name"> <span class="name">
{{ item.result.text }}&nbsp; {{ item.result.text }}&nbsp;
</span> </span>
<span class="location"> <span class="location">
<template v-if="hasAddress"> <template v-if="hasAddress">
{{ getAddress.text }} - {{ getAddress.text }} -
{{ getAddress.postcode.name }} {{ getAddress.postcode.name }}
@ -89,5 +89,13 @@ export default {
font-variant: all-small-caps; font-variant: all-small-caps;
} }
} }
.tparty-identification {
span:not(.name) {
margin-left: 0.5em;
opacity: 0.5;
font-size: 90%;
font-style: italic;
}
}
} }
</style> </style>

View File

@ -185,6 +185,24 @@
</div> </div>
{% endif %} {% endif %}
<div class="mbloc col col-sm-6 col-lg-4">
<div class="notification-counter">
<h4 class="item-key">{{ 'notification.Notifications'|trans }}</h4>
{% set notif_counter = chill_count_notifications('Chill\\PersonBundle\\Entity\\AccompanyingPeriod', accompanyingCourse.id) %}
{% if notif_counter.total > 0 %}
<div class="my-2">
<a href="#notification-list">
{{ chill_counter_notifications('Chill\\PersonBundle\\Entity\\AccompanyingPeriod', accompanyingCourse.id) }}
</a>
</div>
{% endif %}
<div class="d-grid gap-2">
<a class="btn btn-notify" href="{{ chill_path_add_return_path('chill_main_notification_create', {'entityClass': 'Chill\\PersonBundle\\Entity\\AccompanyingPeriod', 'entityId': accompanyingCourse.id}) }}">
{{ 'notification.Notify'|trans }}
</a>
</div>
</div>
</div>
</div> </div>
<div class="social-actions my-4"> <div class="social-actions my-4">
@ -211,17 +229,15 @@
</div> </div>
{% endblock %} {% endblock %}
<div class="notification notification-list">
{% set notifications = chill_list_notifications('Chill\\PersonBundle\\Entity\\AccompanyingPeriod', accompanyingCourse.id) %}
{% if notifications is not empty %}
{{ notifications|raw }}
{% endif %}
</div>
</div> </div>
{% endblock %} {% endblock %}
{% block block_post_menu %} {% block block_post_menu %}
<div class="post-menu pt-4"> <div class="post-menu pt-4"></div>
<div class="d-grid gap-2">
<a class="btn btn-primary" href="{{ chill_path_add_return_path('chill_main_notification_create', {'entityClass': 'Chill\\PersonBundle\\Entity\\AccompanyingPeriod', 'entityId': accompanyingCourse.id}) }}">
<i class="fa fa-paper-plane fa-fw"></i>
{{ 'notification.Notify'|trans }}
</a>
</div>
{{ chill_list_notifications('Chill\\PersonBundle\\Entity\\AccompanyingPeriod', accompanyingCourse.id) }}
</div>
{% endblock %} {% endblock %}

View File

@ -16,12 +16,7 @@
{% endblock %} {% endblock %}
{% block block_post_menu %} {% block block_post_menu %}
<div class="post-menu pt-4"> <div class="post-menu pt-4"></div>
{% set workflows_frame = chill_entity_workflow_list('Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWork', work.id) %}
{% if workflows_frame is not empty %}
{{ workflows_frame|raw }}
{% endif %}
</div>
{% endblock %} {% endblock %}
{% block js %} {% block js %}

View File

@ -82,7 +82,7 @@
<div class="wl-col title"> <div class="wl-col title">
<h3 class="courseid mb-2"> <h3 class="courseid mb-2">
<i class="fa fa-random fa-fw"></i> <i class="fa fa-random fa-fw"></i>
{{ 'File number'|trans }} {{ acp.id }} {{ 'Course number'|trans }} {{ acp.id }}
</h3> </h3>
</div> </div>
<div class="wl-col list"> <div class="wl-col list">

View File

@ -0,0 +1,51 @@
<?php
/**
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Chill\Migrations\Person;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20220128133039 extends AbstractMigration
{
public function down(Schema $schema): void
{
$this->addSql('DROP SEQUENCE chill_person_accompanying_period_user_history_id_seq CASCADE');
$this->addSql('DROP TABLE chill_person_accompanying_period_user_history');
}
public function getDescription(): string
{
return 'Add table for tracking user history on accompanying period';
}
public function up(Schema $schema): void
{
$this->addSql('CREATE SEQUENCE chill_person_accompanying_period_user_history_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
$this->addSql('CREATE TABLE chill_person_accompanying_period_user_history (id INT NOT NULL, user_id INT, endDate TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, startDate TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, accompanyingPeriod_id INT, PRIMARY KEY(id))');
$this->addSql('CREATE INDEX IDX_6C258C49D7FA8EF0 ON chill_person_accompanying_period_user_history (accompanyingPeriod_id)');
$this->addSql('CREATE INDEX IDX_6C258C49A76ED395 ON chill_person_accompanying_period_user_history (user_id)');
$this->addSql('COMMENT ON COLUMN chill_person_accompanying_period_user_history.endDate IS \'(DC2Type:datetime_immutable)\'');
$this->addSql('COMMENT ON COLUMN chill_person_accompanying_period_user_history.startDate IS \'(DC2Type:datetime_immutable)\'');
$this->addSql('ALTER TABLE chill_person_accompanying_period_user_history ADD CONSTRAINT FK_6C258C49D7FA8EF0 FOREIGN KEY (accompanyingPeriod_id) REFERENCES chill_person_accompanying_period (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE chill_person_accompanying_period_user_history ADD CONSTRAINT FK_6C258C49A76ED395 FOREIGN KEY (user_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE chill_person_accompanying_period_user_history ADD CHECK (startdate <= enddate)');
$this->addSql('INSERT INTO chill_person_accompanying_period_user_history (id, user_id, accompanyingperiod_id, startDate, endDate) ' .
'SELECT nextval(\'chill_person_accompanying_period_user_history_id_seq\'), user_id, id, openingDate, null FROM chill_person_accompanying_period WHERE user_id IS NOT NULL');
$this->addSql('ALTER TABLE chill_person_accompanying_period_user_history ADD createdAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL;');
$this->addSql('ALTER TABLE chill_person_accompanying_period_user_history ADD createdBy_id INT DEFAULT NULL;');
$this->addSql('ALTER TABLE chill_person_accompanying_period_user_history ALTER user_id SET NOT NULL;');
$this->addSql('ALTER TABLE chill_person_accompanying_period_user_history ALTER accompanyingperiod_id SET NOT NULL;');
$this->addSql('COMMENT ON COLUMN chill_person_accompanying_period_user_history.createdAt IS \'(DC2Type:datetime_immutable)\';');
$this->addSql('ALTER TABLE chill_person_accompanying_period_user_history ADD CONSTRAINT FK_6C258C493174800F FOREIGN KEY (createdBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE;');
$this->addSql('CREATE INDEX IDX_6C258C493174800F ON chill_person_accompanying_period_user_history (createdBy_id);');
}
}

View File

@ -81,7 +81,7 @@ Married: Marié(e)
'Family information': Famille 'Family information': Famille
'Contact information': 'Informations de contact' 'Contact information': 'Informations de contact'
'Administrative information': Administratif 'Administrative information': Administratif
File number: Dossier Course number: Parcours
Civility: Civilité Civility: Civilité
choose civility: -- choose civility: --
All genders: tous les genres All genders: tous les genres

View File

@ -13,6 +13,8 @@ namespace Chill\TaskBundle\Controller;
use Chill\MainBundle\Pagination\PaginatorFactory; use Chill\MainBundle\Pagination\PaginatorFactory;
use Chill\MainBundle\Security\Resolver\CenterResolverDispatcherInterface; use Chill\MainBundle\Security\Resolver\CenterResolverDispatcherInterface;
use Chill\MainBundle\Serializer\Model\Collection;
use Chill\MainBundle\Serializer\Model\Counter;
use Chill\MainBundle\Templating\Listing\FilterOrderHelper; use Chill\MainBundle\Templating\Listing\FilterOrderHelper;
use Chill\MainBundle\Templating\Listing\FilterOrderHelperFactoryInterface; use Chill\MainBundle\Templating\Listing\FilterOrderHelperFactoryInterface;
use Chill\MainBundle\Timeline\TimelineBuilder; use Chill\MainBundle\Timeline\TimelineBuilder;
@ -31,6 +33,8 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormFactoryInterface; use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\HttpFoundation\Exception\BadRequestException;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
@ -431,10 +435,15 @@ final class SingleTaskController extends AbstractController
* @return Response * @return Response
* @Route( * @Route(
* "/{_locale}/task/single-task/list/my", * "/{_locale}/task/single-task/list/my",
* name="chill_task_singletask_my_tasks" * name="chill_task_singletask_my_tasks",
* defaults={"_format": "html"}
* )
* @Route(
* "/api/1.0/task/single-task/list/my",
* defaults={"_format": "json"}
* ) * )
*/ */
public function myTasksAction() public function myTasksAction(string $_format, Request $request)
{ {
$this->denyAccessUnlessGranted('ROLE_USER'); $this->denyAccessUnlessGranted('ROLE_USER');
@ -447,6 +456,13 @@ final class SingleTaskController extends AbstractController
$filterOrder->getQueryString(), $filterOrder->getQueryString(),
$flags $flags
); );
if ('json' === $_format && $request->query->getBoolean('countOnly')) {
return $this->json(
new Counter($nb),
);
}
$paginator = $this->paginatorFactory->create($nb); $paginator = $this->paginatorFactory->create($nb);
$tasks = $this->singleTaskAclAwareRepository->findByCurrentUsersTasks( $tasks = $this->singleTaskAclAwareRepository->findByCurrentUsersTasks(
$filterOrder->getQueryString(), $filterOrder->getQueryString(),
@ -459,11 +475,27 @@ final class SingleTaskController extends AbstractController
] ]
); );
return $this->render('@ChillTask/SingleTask/List/index_my_tasks.html.twig', [ switch ($_format) {
'tasks' => $tasks, case 'html':
'paginator' => $paginator, return $this->render('@ChillTask/SingleTask/List/index_my_tasks.html.twig', [
'filter_order' => $filterOrder, 'tasks' => $tasks,
]); 'paginator' => $paginator,
'filter_order' => $filterOrder,
]);
case 'json':
$collection = new Collection($tasks, $paginator);
return $this->json(
$collection,
JsonResponse::HTTP_OK,
[],
['groups' => ['read']]
);
default:
throw new BadRequestException("format not supported: {$_format}");
}
} }
/** /**

View File

@ -18,6 +18,7 @@ use Chill\MainBundle\Entity\User;
use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\Person; use Chill\PersonBundle\Entity\Person;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation as Serializer;
use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Constraints as Assert;
use function array_fill_keys; use function array_fill_keys;
@ -27,6 +28,9 @@ use function array_keys;
* AbstractTask. * AbstractTask.
* *
* @ORM\MappedSuperclass * @ORM\MappedSuperclass
* @Serializer\DiscriminatorMap(typeProperty="type", mapping={
* "single_task": SingleTask::class
* })
*/ */
abstract class AbstractTask implements HasCenterInterface, HasScopeInterface abstract class AbstractTask implements HasCenterInterface, HasScopeInterface
{ {
@ -35,6 +39,7 @@ abstract class AbstractTask implements HasCenterInterface, HasScopeInterface
* @ORM\ManyToOne( * @ORM\ManyToOne(
* targetEntity="\Chill\MainBundle\Entity\User" * targetEntity="\Chill\MainBundle\Entity\User"
* ) * )
* @Serializer\Groups({"read"})
*/ */
private $assignee; private $assignee;
@ -49,12 +54,14 @@ abstract class AbstractTask implements HasCenterInterface, HasScopeInterface
/** /**
* @var bool * @var bool
* @ORM\Column(name="closed", type="boolean", options={ "default": false }) * @ORM\Column(name="closed", type="boolean", options={ "default": false })
* @Serializer\Groups({"read"})
*/ */
private $closed = false; private $closed = false;
/** /**
* @var AccompanyingPeriod * @var AccompanyingPeriod
* @ORM\ManyToOne(targetEntity="\Chill\PersonBundle\Entity\AccompanyingPeriod") * @ORM\ManyToOne(targetEntity="\Chill\PersonBundle\Entity\AccompanyingPeriod")
* @Serializer\Groups({"read"})
*/ */
private $course; private $course;
@ -62,6 +69,7 @@ abstract class AbstractTask implements HasCenterInterface, HasScopeInterface
* @var json * @var json
* *
* @ORM\Column(name="current_states", type="json") * @ORM\Column(name="current_states", type="json")
* @Serializer\Groups({"read"})
*/ */
private $currentStates = []; private $currentStates = [];
@ -69,6 +77,7 @@ abstract class AbstractTask implements HasCenterInterface, HasScopeInterface
* @var string * @var string
* *
* @ORM\Column(name="description", type="text") * @ORM\Column(name="description", type="text")
* @Serializer\Groups({"read"})
*/ */
private $description = ''; private $description = '';
@ -77,6 +86,7 @@ abstract class AbstractTask implements HasCenterInterface, HasScopeInterface
* @ORM\ManyToOne( * @ORM\ManyToOne(
* targetEntity="\Chill\PersonBundle\Entity\Person" * targetEntity="\Chill\PersonBundle\Entity\Person"
* ) * )
* @Serializer\Groups({"read"})
*/ */
private $person; private $person;
@ -85,6 +95,7 @@ abstract class AbstractTask implements HasCenterInterface, HasScopeInterface
* *
* @ORM\Column(name="title", type="text") * @ORM\Column(name="title", type="text")
* @Assert\NotBlank * @Assert\NotBlank
* @Serializer\Groups({"read"})
*/ */
private $title = ''; private $title = '';
@ -92,6 +103,7 @@ abstract class AbstractTask implements HasCenterInterface, HasScopeInterface
* @var string * @var string
* *
* @ORM\Column(name="type", type="string", length=255) * @ORM\Column(name="type", type="string", length=255)
* @Serializer\Groups({"read"})
*/ */
private $type; private $type;