mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-08-29 19:13:49 +00:00
Resolve merge with master
This commit is contained in:
@@ -96,7 +96,7 @@ class SearchController extends AbstractController
|
||||
return $this->render('@ChillMain/Search/choose_list.html.twig');
|
||||
}
|
||||
|
||||
#[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/search.{_format}', name: 'chill_main_search', requirements: ['_format' => 'html|json'], defaults: ['_format' => 'html'])]
|
||||
#[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/search.{_format}', name: 'chill_main_search', requirements: ['_format' => 'html|json', '_locale' => '[a-z]{1,3}'], defaults: ['_format' => 'html'])]
|
||||
public function searchAction(Request $request, mixed $_format)
|
||||
{
|
||||
$pattern = trim((string) $request->query->get('q', ''));
|
||||
|
@@ -12,7 +12,9 @@ declare(strict_types=1);
|
||||
namespace Chill\MainBundle\Controller;
|
||||
|
||||
use Chill\MainBundle\CRUD\Controller\CRUDController;
|
||||
use Chill\MainBundle\Entity\Center;
|
||||
use Chill\MainBundle\Entity\GroupCenter;
|
||||
use Chill\MainBundle\Entity\PermissionsGroup;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Form\Type\ComposedGroupCenterType;
|
||||
use Chill\MainBundle\Form\UserCurrentLocationType;
|
||||
@@ -64,10 +66,14 @@ class UserController extends CRUDController
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isValid()) {
|
||||
$groupCenter = $this->getPersistedGroupCenter(
|
||||
$form[self::FORM_GROUP_CENTER_COMPOSED]->getData()
|
||||
);
|
||||
$user->addGroupCenter($groupCenter);
|
||||
|
||||
$formData = $form[self::FORM_GROUP_CENTER_COMPOSED]->getData();
|
||||
$selectedCenters = $formData['center'];
|
||||
|
||||
foreach ($selectedCenters as $center) {
|
||||
$groupCenter = $this->getPersistedGroupCenter($center, $formData['permissionsgroup']);
|
||||
$user->addGroupCenter($groupCenter);
|
||||
}
|
||||
|
||||
if (0 === $this->validator->validate($user)->count()) {
|
||||
$em->flush();
|
||||
@@ -419,17 +425,21 @@ class UserController extends CRUDController
|
||||
}
|
||||
}
|
||||
|
||||
private function getPersistedGroupCenter(GroupCenter $groupCenter)
|
||||
private function getPersistedGroupCenter(Center $center, PermissionsGroup $permissionsGroup)
|
||||
{
|
||||
$em = $this->managerRegistry->getManager();
|
||||
|
||||
$groupCenterManaged = $em->getRepository(GroupCenter::class)
|
||||
->findOneBy([
|
||||
'center' => $groupCenter->getCenter(),
|
||||
'permissionsGroup' => $groupCenter->getPermissionsGroup(),
|
||||
'center' => $center,
|
||||
'permissionsGroup' => $permissionsGroup,
|
||||
]);
|
||||
|
||||
if (!$groupCenterManaged) {
|
||||
$groupCenter = new GroupCenter();
|
||||
$groupCenter->setCenter($center);
|
||||
$groupCenter->setPermissionsGroup($permissionsGroup);
|
||||
|
||||
$em->persist($groupCenter);
|
||||
|
||||
return $groupCenter;
|
||||
|
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\MainBundle\Controller;
|
||||
|
||||
use Chill\MainBundle\CRUD\Controller\CRUDController;
|
||||
use Chill\MainBundle\Pagination\PaginatorInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
class UserGroupAdminController extends CRUDController
|
||||
{
|
||||
protected function orderQuery(string $action, $query, Request $request, PaginatorInterface $paginator)
|
||||
{
|
||||
$query->addSelect('JSON_EXTRACT(e.label, :lang) AS HIDDEN labeli18n')
|
||||
->setParameter('lang', $request->getLocale());
|
||||
$query->addOrderBy('labeli18n', 'ASC');
|
||||
|
||||
return $query;
|
||||
}
|
||||
}
|
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\MainBundle\Controller;
|
||||
|
||||
use Chill\MainBundle\CRUD\Controller\ApiController;
|
||||
|
||||
class UserGroupApiController extends ApiController {}
|
177
src/Bundle/ChillMainBundle/Controller/UserGroupController.php
Normal file
177
src/Bundle/ChillMainBundle/Controller/UserGroupController.php
Normal file
@@ -0,0 +1,177 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\MainBundle\Controller;
|
||||
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Entity\UserGroup;
|
||||
use Chill\MainBundle\Form\Type\PickUserDynamicType;
|
||||
use Chill\MainBundle\Pagination\PaginatorFactoryInterface;
|
||||
use Chill\MainBundle\Repository\UserGroupRepositoryInterface;
|
||||
use Chill\MainBundle\Routing\ChillUrlGeneratorInterface;
|
||||
use Chill\MainBundle\Security\Authorization\UserGroupVoter;
|
||||
use Chill\MainBundle\Templating\Entity\ChillEntityRenderManagerInterface;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
|
||||
use Symfony\Component\Form\Extension\Core\Type\FormType;
|
||||
use Symfony\Component\Form\FormFactoryInterface;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\Session\Session;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Component\Translation\TranslatableMessage;
|
||||
use Twig\Environment;
|
||||
|
||||
/**
|
||||
* Controller to see and manage user groups.
|
||||
*/
|
||||
final readonly class UserGroupController
|
||||
{
|
||||
public function __construct(
|
||||
private UserGroupRepositoryInterface $userGroupRepository,
|
||||
private Security $security,
|
||||
private PaginatorFactoryInterface $paginatorFactory,
|
||||
private Environment $twig,
|
||||
private FormFactoryInterface $formFactory,
|
||||
private ChillUrlGeneratorInterface $chillUrlGenerator,
|
||||
private EntityManagerInterface $objectManager,
|
||||
private ChillEntityRenderManagerInterface $chillEntityRenderManager,
|
||||
) {}
|
||||
|
||||
#[Route('/{_locale}/main/user-groups/my', name: 'chill_main_user_groups_my')]
|
||||
public function myUserGroups(): Response
|
||||
{
|
||||
if (!$this->security->isGranted('ROLE_USER')) {
|
||||
throw new AccessDeniedHttpException();
|
||||
}
|
||||
|
||||
$user = $this->security->getUser();
|
||||
|
||||
if (!$user instanceof User) {
|
||||
throw new AccessDeniedHttpException();
|
||||
}
|
||||
|
||||
$nb = $this->userGroupRepository->countByUser($user);
|
||||
$paginator = $this->paginatorFactory->create($nb);
|
||||
|
||||
$groups = $this->userGroupRepository->findByUser($user, true, $paginator->getItemsPerPage(), $paginator->getCurrentPageFirstItemNumber());
|
||||
$forms = new \SplObjectStorage();
|
||||
|
||||
foreach ($groups as $group) {
|
||||
$forms->attach($group, $this->createFormAppendUserForGroup($group)?->createView());
|
||||
}
|
||||
|
||||
return new Response($this->twig->render('@ChillMain/UserGroup/my_user_groups.html.twig', [
|
||||
'groups' => $groups,
|
||||
'paginator' => $paginator,
|
||||
'forms' => $forms,
|
||||
]));
|
||||
}
|
||||
|
||||
#[Route('/{_locale}/main/user-groups/{id}/append', name: 'chill_main_user_groups_append_users')]
|
||||
public function appendUsersToGroup(UserGroup $userGroup, Request $request, Session $session): Response
|
||||
{
|
||||
if (!$this->security->isGranted(UserGroupVoter::APPEND_TO_GROUP, $userGroup)) {
|
||||
throw new AccessDeniedHttpException();
|
||||
}
|
||||
|
||||
$form = $this->createFormAppendUserForGroup($userGroup);
|
||||
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
foreach ($form['users']->getData() as $user) {
|
||||
$userGroup->addUser($user);
|
||||
|
||||
$session->getFlashBag()->add(
|
||||
'success',
|
||||
new TranslatableMessage(
|
||||
'user_group.user_added',
|
||||
[
|
||||
'user_group' => $this->chillEntityRenderManager->renderString($userGroup, []),
|
||||
'user' => $this->chillEntityRenderManager->renderString($user, []),
|
||||
]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$this->objectManager->flush();
|
||||
|
||||
return new RedirectResponse(
|
||||
$this->chillUrlGenerator->returnPathOr('chill_main_user_groups_my')
|
||||
);
|
||||
}
|
||||
if ($form->isSubmitted()) {
|
||||
$errors = [];
|
||||
foreach ($form->getErrors() as $error) {
|
||||
$errors[] = $error->getMessage();
|
||||
}
|
||||
|
||||
return new Response(implode(', ', $errors));
|
||||
}
|
||||
|
||||
return new RedirectResponse(
|
||||
$this->chillUrlGenerator->returnPathOr('chill_main_user_groups_my')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @ParamConverter("user", class=User::class, options={"id" = "userId"})
|
||||
*/
|
||||
#[Route('/{_locale}/main/user-group/{id}/user/{userId}/remove', name: 'chill_main_user_groups_remove_user')]
|
||||
public function removeUserToGroup(UserGroup $userGroup, User $user, Session $session): Response
|
||||
{
|
||||
if (!$this->security->isGranted(UserGroupVoter::APPEND_TO_GROUP, $userGroup)) {
|
||||
throw new AccessDeniedHttpException();
|
||||
}
|
||||
|
||||
$userGroup->removeUser($user);
|
||||
$this->objectManager->flush();
|
||||
|
||||
$session->getFlashBag()->add(
|
||||
'success',
|
||||
new TranslatableMessage(
|
||||
'user_group.user_removed',
|
||||
[
|
||||
'user_group' => $this->chillEntityRenderManager->renderString($userGroup, []),
|
||||
'user' => $this->chillEntityRenderManager->renderString($user, []),
|
||||
]
|
||||
)
|
||||
);
|
||||
|
||||
return new RedirectResponse(
|
||||
$this->chillUrlGenerator->returnPathOr('chill_main_user_groups_my')
|
||||
);
|
||||
}
|
||||
|
||||
private function createFormAppendUserForGroup(UserGroup $group): ?FormInterface
|
||||
{
|
||||
if (!$this->security->isGranted(UserGroupVoter::APPEND_TO_GROUP, $group)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$builder = $this->formFactory->createBuilder(FormType::class, ['users' => []], [
|
||||
'action' => $this->chillUrlGenerator->generateWithReturnPath('chill_main_user_groups_append_users', ['id' => $group->getId()]),
|
||||
]);
|
||||
$builder->add('users', PickUserDynamicType::class, [
|
||||
'submit_on_adding_new_entity' => true,
|
||||
'label' => 'user_group.append_users',
|
||||
'mapped' => false,
|
||||
'multiple' => true,
|
||||
]);
|
||||
|
||||
return $builder->getForm();
|
||||
}
|
||||
}
|
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\MainBundle\Controller;
|
||||
|
||||
use Chill\DocStoreBundle\Service\Signature\PDFSignatureZoneAvailable;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature;
|
||||
use Chill\MainBundle\Security\Authorization\EntityWorkflowStepSignatureVoter;
|
||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||
use Twig\Environment;
|
||||
|
||||
final readonly class WorkflowAddSignatureController
|
||||
{
|
||||
public function __construct(
|
||||
private EntityWorkflowManager $entityWorkflowManager,
|
||||
private PDFSignatureZoneAvailable $PDFSignatureZoneAvailable,
|
||||
private NormalizerInterface $normalizer,
|
||||
private Environment $twig,
|
||||
private UrlGeneratorInterface $urlGenerator,
|
||||
private Security $security,
|
||||
) {}
|
||||
|
||||
#[Route(path: '/{_locale}/main/workflow/signature/{id}/sign', name: 'chill_main_workflow_signature_add', methods: 'GET')]
|
||||
public function __invoke(EntityWorkflowStepSignature $signature, Request $request): Response
|
||||
{
|
||||
if (!$this->security->isGranted(EntityWorkflowStepSignatureVoter::SIGN, $signature)) {
|
||||
throw new AccessDeniedHttpException('not authorized to sign this step');
|
||||
}
|
||||
|
||||
$entityWorkflow = $signature->getStep()->getEntityWorkflow();
|
||||
|
||||
if (EntityWorkflowSignatureStateEnum::PENDING !== $signature->getState()) {
|
||||
if ($request->query->has('returnPath')) {
|
||||
return new RedirectResponse($request->query->get('returnPath'));
|
||||
}
|
||||
|
||||
return new RedirectResponse(
|
||||
$this->urlGenerator->generate('chill_main_workflow_show', ['id' => $entityWorkflow->getId()])
|
||||
);
|
||||
}
|
||||
|
||||
$storedObject = $this->entityWorkflowManager->getAssociatedStoredObject($entityWorkflow);
|
||||
if (null === $storedObject) {
|
||||
throw new NotFoundHttpException('No stored object found');
|
||||
}
|
||||
|
||||
$zones = $this->PDFSignatureZoneAvailable->getAvailableSignatureZones($entityWorkflow);
|
||||
|
||||
$signatureClient = [];
|
||||
$signatureClient['id'] = $signature->getId();
|
||||
$signatureClient['storedObject'] = $this->normalizer->normalize($storedObject, 'json');
|
||||
$signatureClient['zones'] = $zones;
|
||||
|
||||
return new Response(
|
||||
$this->twig->render(
|
||||
'@ChillMain/Workflow/signature_sign.html.twig',
|
||||
['signature' => $signatureClient]
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
@@ -13,23 +13,27 @@ namespace Chill\MainBundle\Controller;
|
||||
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowComment;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStep;
|
||||
use Chill\MainBundle\Form\EntityWorkflowCommentType;
|
||||
use Chill\MainBundle\Form\WorkflowSignatureMetadataType;
|
||||
use Chill\MainBundle\Form\WorkflowStepType;
|
||||
use Chill\MainBundle\Pagination\PaginatorFactory;
|
||||
use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository;
|
||||
use Chill\MainBundle\Repository\Workflow\EntityWorkflowStepSignatureRepository;
|
||||
use Chill\MainBundle\Security\Authorization\EntityWorkflowVoter;
|
||||
use Chill\MainBundle\Security\ChillSecurity;
|
||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
||||
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\NonUniqueResultException;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\Clock\ClockInterface;
|
||||
use Symfony\Component\Form\Extension\Core\Type\FormType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||
use Symfony\Component\Workflow\Registry;
|
||||
@@ -38,7 +42,19 @@ use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class WorkflowController extends AbstractController
|
||||
{
|
||||
public function __construct(private readonly EntityWorkflowManager $entityWorkflowManager, private readonly EntityWorkflowRepository $entityWorkflowRepository, private readonly ValidatorInterface $validator, private readonly PaginatorFactory $paginatorFactory, private readonly Registry $registry, private readonly EntityManagerInterface $entityManager, private readonly TranslatorInterface $translator, private readonly ChillSecurity $security, private readonly \Doctrine\Persistence\ManagerRegistry $managerRegistry) {}
|
||||
public function __construct(
|
||||
private readonly EntityWorkflowManager $entityWorkflowManager,
|
||||
private readonly EntityWorkflowRepository $entityWorkflowRepository,
|
||||
private readonly ValidatorInterface $validator,
|
||||
private readonly PaginatorFactory $paginatorFactory,
|
||||
private readonly Registry $registry,
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly TranslatorInterface $translator,
|
||||
private readonly ChillSecurity $security,
|
||||
private readonly \Doctrine\Persistence\ManagerRegistry $managerRegistry,
|
||||
private readonly ClockInterface $clock,
|
||||
private readonly EntityWorkflowStepSignatureRepository $entityWorkflowStepSignatureRepository,
|
||||
) {}
|
||||
|
||||
#[Route(path: '/{_locale}/main/workflow/create', name: 'chill_main_workflow_create')]
|
||||
public function create(Request $request): Response
|
||||
@@ -268,6 +284,9 @@ class WorkflowController extends AbstractController
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws NonUniqueResultException
|
||||
*/
|
||||
#[Route(path: '/{_locale}/main/workflow/{id}/show', name: 'chill_main_workflow_show')]
|
||||
public function show(EntityWorkflow $entityWorkflow, Request $request): Response
|
||||
{
|
||||
@@ -276,24 +295,17 @@ class WorkflowController extends AbstractController
|
||||
$handler = $this->entityWorkflowManager->getHandler($entityWorkflow);
|
||||
$workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
|
||||
$errors = [];
|
||||
$signatures = $entityWorkflow->getCurrentStep()->getSignatures();
|
||||
|
||||
if (\count($workflow->getEnabledTransitions($entityWorkflow)) > 0) {
|
||||
// possible transition
|
||||
|
||||
$usersInvolved = $entityWorkflow->getUsersInvolved();
|
||||
$currentUserFound = array_search($this->security->getUser(), $usersInvolved, true);
|
||||
|
||||
if (false !== $currentUserFound) {
|
||||
unset($usersInvolved[$currentUserFound]);
|
||||
}
|
||||
$stepDTO = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||
|
||||
$transitionForm = $this->createForm(
|
||||
WorkflowStepType::class,
|
||||
$entityWorkflow->getCurrentStep(),
|
||||
$stepDTO,
|
||||
[
|
||||
'transition' => true,
|
||||
'entity_workflow' => $entityWorkflow,
|
||||
'suggested_users' => $usersInvolved,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -310,12 +322,14 @@ class WorkflowController extends AbstractController
|
||||
throw $this->createAccessDeniedException(sprintf("not allowed to apply transition {$transition}: %s", implode(', ', $msgs)));
|
||||
}
|
||||
|
||||
// TODO symfony 5: add those "future" on context ($workflow->apply($entityWorkflow, $transition, $context)
|
||||
$entityWorkflow->futureCcUsers = $transitionForm['future_cc_users']->getData() ?? [];
|
||||
$entityWorkflow->futureDestUsers = $transitionForm['future_dest_users']->getData() ?? [];
|
||||
$entityWorkflow->futureDestEmails = $transitionForm['future_dest_emails']->getData() ?? [];
|
||||
$byUser = $this->security->getUser();
|
||||
|
||||
$workflow->apply($entityWorkflow, $transition);
|
||||
$workflow->apply($entityWorkflow, $transition, [
|
||||
'context' => $stepDTO,
|
||||
'byUser' => $byUser,
|
||||
'transition' => $transition,
|
||||
'transitionAt' => $this->clock->now(),
|
||||
]);
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
@@ -327,22 +341,6 @@ class WorkflowController extends AbstractController
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
$commentForm = $this->createForm(EntityWorkflowCommentType::class, $newComment = new EntityWorkflowComment());
|
||||
$commentForm->handleRequest($request);
|
||||
|
||||
if ($commentForm->isSubmitted() && $commentForm->isValid()) {
|
||||
$this->entityManager->persist($newComment);
|
||||
$this->entityManager->flush();
|
||||
|
||||
$this->addFlash('success', $this->translator->trans('workflow.Comment added'));
|
||||
|
||||
return $this->redirectToRoute('chill_main_workflow_show', ['id' => $entityWorkflow->getId()]);
|
||||
} elseif ($commentForm->isSubmitted() && !$commentForm->isValid()) {
|
||||
$this->addFlash('error', $this->translator->trans('This form contains errors'));
|
||||
}
|
||||
*/
|
||||
|
||||
return $this->render(
|
||||
'@ChillMain/Workflow/index.html.twig',
|
||||
[
|
||||
@@ -352,7 +350,7 @@ class WorkflowController extends AbstractController
|
||||
'transition_form' => isset($transitionForm) ? $transitionForm->createView() : null,
|
||||
'entity_workflow' => $entityWorkflow,
|
||||
'transition_form_errors' => $errors,
|
||||
// 'comment_form' => $commentForm->createView(),
|
||||
'signatures' => $signatures,
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -371,4 +369,65 @@ class WorkflowController extends AbstractController
|
||||
|
||||
return $lines;
|
||||
}
|
||||
|
||||
#[Route(path: '/{_locale}/main/workflow/signature/{signature_id}/metadata', name: 'chill_main_workflow_signature_metadata')]
|
||||
public function addSignatureMetadata(int $signature_id, Request $request): Response
|
||||
{
|
||||
$signature = $this->entityWorkflowStepSignatureRepository->find($signature_id);
|
||||
|
||||
if (null === $signature) {
|
||||
throw new NotFoundHttpException('signature not found');
|
||||
}
|
||||
|
||||
if ($signature->isSigned()) {
|
||||
$this->addFlash(
|
||||
'notice',
|
||||
$this->translator->trans('workflow.signature_zone.already_signed_alert')
|
||||
);
|
||||
|
||||
return $this->redirectToRoute('chill_main_workflow_show', ['id' => $signature->getStep()->getEntityWorkflow()->getId()]);
|
||||
}
|
||||
|
||||
if ($signature->getSigner() instanceof User) {
|
||||
return $this->redirectToRoute('chill_main_workflow_signature_add', [
|
||||
'id' => $signature_id,
|
||||
'returnPath' => $request->query->get('returnPath', null),
|
||||
]);
|
||||
}
|
||||
|
||||
$metadataForm = $this->createForm(WorkflowSignatureMetadataType::class);
|
||||
$metadataForm->add('submit', SubmitType::class, ['label' => $this->translator->trans('Save')]);
|
||||
|
||||
$metadataForm->handleRequest($request);
|
||||
|
||||
if ($metadataForm->isSubmitted() && $metadataForm->isValid()) {
|
||||
$data = $metadataForm->getData();
|
||||
|
||||
$signature->setSignatureMetadata(
|
||||
[
|
||||
'base_signer' => [
|
||||
'document_type' => $data['documentType'],
|
||||
'document_number' => $data['documentNumber'],
|
||||
'expiration_date' => $data['expirationDate'],
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$this->entityManager->persist($signature);
|
||||
$this->entityManager->flush();
|
||||
|
||||
return $this->redirectToRoute('chill_main_workflow_signature_add', [
|
||||
'id' => $signature_id,
|
||||
'returnPath' => $request->query->get('returnPath', null),
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->render(
|
||||
'@ChillMain/Workflow/signature_metadata.html.twig',
|
||||
[
|
||||
'metadata_form' => $metadataForm->createView(),
|
||||
'person' => $signature->getSigner(),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\MainBundle\Controller;
|
||||
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStep;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepHold;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Component\Workflow\Registry;
|
||||
|
||||
class WorkflowOnHoldController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly Security $security,
|
||||
private readonly Registry $registry,
|
||||
private readonly UrlGeneratorInterface $urlGenerator,
|
||||
) {}
|
||||
|
||||
#[Route(path: '/{_locale}/main/workflow/{id}/hold', name: 'chill_main_workflow_on_hold')]
|
||||
public function putOnHold(EntityWorkflow $entityWorkflow, Request $request): Response
|
||||
{
|
||||
$currentStep = $entityWorkflow->getCurrentStep();
|
||||
$currentUser = $this->security->getUser();
|
||||
|
||||
if (!$currentUser instanceof User) {
|
||||
throw new AccessDeniedHttpException('only user can put a workflow on hold');
|
||||
}
|
||||
|
||||
$workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
|
||||
$enabledTransitions = $workflow->getEnabledTransitions($entityWorkflow);
|
||||
|
||||
if (0 === count($enabledTransitions)) {
|
||||
throw new AccessDeniedHttpException('You are not allowed to apply any transitions to this workflow, therefore you cannot toggle the hold status.');
|
||||
}
|
||||
|
||||
$stepHold = new EntityWorkflowStepHold($currentStep, $currentUser);
|
||||
|
||||
$this->entityManager->persist($stepHold);
|
||||
$this->entityManager->flush();
|
||||
|
||||
return new RedirectResponse(
|
||||
$this->urlGenerator->generate(
|
||||
'chill_main_workflow_show',
|
||||
['id' => $entityWorkflow->getId()]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[Route(path: '/{_locale}/main/workflow/{id}/remove_hold', name: 'chill_main_workflow_remove_hold')]
|
||||
public function removeOnHold(EntityWorkflowStep $entityWorkflowStep): Response
|
||||
{
|
||||
$user = $this->security->getUser();
|
||||
|
||||
if (!$user instanceof User) {
|
||||
throw new AccessDeniedHttpException('only user can remove workflow on hold');
|
||||
}
|
||||
|
||||
if (!$entityWorkflowStep->isOnHoldByUser($user)) {
|
||||
throw new AccessDeniedHttpException('You are not allowed to remove workflow on hold');
|
||||
}
|
||||
|
||||
$hold = $entityWorkflowStep->getHoldsOnStep()->findFirst(fn (int $index, EntityWorkflowStepHold $entityWorkflowStepHold) => $user === $entityWorkflowStepHold->getByUser());
|
||||
|
||||
if (null === $hold) {
|
||||
// this should not happens...
|
||||
throw new NotFoundHttpException();
|
||||
}
|
||||
|
||||
$this->entityManager->remove($hold);
|
||||
$this->entityManager->flush();
|
||||
|
||||
return new RedirectResponse(
|
||||
$this->urlGenerator->generate(
|
||||
'chill_main_workflow_show',
|
||||
['id' => $entityWorkflowStep->getEntityWorkflow()->getId()]
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\MainBundle\Controller;
|
||||
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature;
|
||||
use Chill\MainBundle\Routing\ChillUrlGeneratorInterface;
|
||||
use Chill\MainBundle\Security\Authorization\EntityWorkflowStepSignatureVoter;
|
||||
use Chill\MainBundle\Workflow\SignatureStepStateChanger;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||
use Symfony\Component\Form\FormFactoryInterface;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Twig\Environment;
|
||||
|
||||
final readonly class WorkflowSignatureStateChangeController
|
||||
{
|
||||
public function __construct(
|
||||
private EntityManagerInterface $entityManager,
|
||||
private Security $security,
|
||||
private FormFactoryInterface $formFactory,
|
||||
private Environment $twig,
|
||||
private SignatureStepStateChanger $signatureStepStateChanger,
|
||||
private ChillUrlGeneratorInterface $chillUrlGenerator,
|
||||
) {}
|
||||
|
||||
#[Route('/{_locale}/main/workflow/signature/{id}/cancel', name: 'chill_main_workflow_signature_cancel')]
|
||||
public function cancelSignature(EntityWorkflowStepSignature $signature, Request $request): Response
|
||||
{
|
||||
return $this->markSignatureAction(
|
||||
$signature,
|
||||
$request,
|
||||
EntityWorkflowStepSignatureVoter::CANCEL,
|
||||
function (EntityWorkflowStepSignature $signature) {$this->signatureStepStateChanger->markSignatureAsCanceled($signature); },
|
||||
'@ChillMain/WorkflowSignature/cancel.html.twig',
|
||||
);
|
||||
}
|
||||
|
||||
#[Route('/{_locale}/main/workflow/signature/{id}/reject', name: 'chill_main_workflow_signature_reject')]
|
||||
public function rejectSignature(EntityWorkflowStepSignature $signature, Request $request): Response
|
||||
{
|
||||
return $this->markSignatureAction(
|
||||
$signature,
|
||||
$request,
|
||||
EntityWorkflowStepSignatureVoter::REJECT,
|
||||
function (EntityWorkflowStepSignature $signature) {$this->signatureStepStateChanger->markSignatureAsRejected($signature); },
|
||||
'@ChillMain/WorkflowSignature/reject.html.twig',
|
||||
);
|
||||
}
|
||||
|
||||
private function markSignatureAction(
|
||||
EntityWorkflowStepSignature $signature,
|
||||
Request $request,
|
||||
string $permissionAttribute,
|
||||
callable $markSignature,
|
||||
string $template,
|
||||
): Response {
|
||||
|
||||
if (!$this->security->isGranted($permissionAttribute, $signature)) {
|
||||
throw new AccessDeniedHttpException('not allowed to cancel this signature');
|
||||
}
|
||||
|
||||
$form = $this->formFactory->create();
|
||||
$form->add('confirm', SubmitType::class, ['label' => 'Confirm']);
|
||||
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$this->entityManager->wrapInTransaction(function () use ($signature, $markSignature) {
|
||||
$markSignature($signature);
|
||||
});
|
||||
|
||||
return new RedirectResponse(
|
||||
$this->chillUrlGenerator->returnPathOr('chill_main_workflow_show', ['id' => $signature->getStep()->getEntityWorkflow()->getId()])
|
||||
);
|
||||
}
|
||||
|
||||
return
|
||||
new Response(
|
||||
$this->twig->render(
|
||||
$template,
|
||||
['form' => $form->createView(), 'signature' => $signature]
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\MainBundle\Controller;
|
||||
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSend;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSendView;
|
||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
||||
use Chill\MainBundle\Workflow\Exception\HandlerWithPublicViewNotFoundException;
|
||||
use Chill\MainBundle\Workflow\Messenger\PostPublicViewMessage;
|
||||
use Chill\MainBundle\Workflow\Templating\EntityWorkflowViewMetadataDTO;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Clock\ClockInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Twig\Environment;
|
||||
|
||||
final readonly class WorkflowViewSendPublicController
|
||||
{
|
||||
public const LOG_PREFIX = '[workflow-view-send-public-controller] ';
|
||||
|
||||
public function __construct(
|
||||
private EntityManagerInterface $entityManager,
|
||||
private LoggerInterface $chillLogger,
|
||||
private EntityWorkflowManager $entityWorkflowManager,
|
||||
private ClockInterface $clock,
|
||||
private Environment $environment,
|
||||
private MessageBusInterface $messageBus,
|
||||
) {}
|
||||
|
||||
#[Route('/public/main/workflow/send/{uuid}/view/{verificationKey}', name: 'chill_main_workflow_send_view_public', methods: ['GET'])]
|
||||
public function __invoke(EntityWorkflowSend $workflowSend, string $verificationKey, Request $request): Response
|
||||
{
|
||||
if (50 < $workflowSend->getNumberOfErrorTrials()) {
|
||||
throw new AccessDeniedHttpException('number of trials exceeded, no more access allowed');
|
||||
}
|
||||
|
||||
if ($verificationKey !== $workflowSend->getPrivateToken()) {
|
||||
$this->chillLogger->info(self::LOG_PREFIX.'Invalid trial for this send', ['client_ip' => $request->getClientIp()]);
|
||||
$workflowSend->increaseErrorTrials();
|
||||
$this->entityManager->flush();
|
||||
|
||||
throw new AccessDeniedHttpException('invalid verification key');
|
||||
}
|
||||
|
||||
if ($this->clock->now() > $workflowSend->getExpireAt()) {
|
||||
return new Response(
|
||||
$this->environment->render('@ChillMain/Workflow/workflow_view_send_public_expired.html.twig'),
|
||||
409
|
||||
);
|
||||
}
|
||||
|
||||
if (100 < $workflowSend->getViews()->count()) {
|
||||
$this->chillLogger->info(self::LOG_PREFIX.'100 view reached, not allowed to see it again');
|
||||
throw new AccessDeniedHttpException('100 views reached, not allowed to see it again');
|
||||
}
|
||||
|
||||
try {
|
||||
$metadata = new EntityWorkflowViewMetadataDTO(
|
||||
$workflowSend->getViews()->count(),
|
||||
100 - $workflowSend->getViews()->count(),
|
||||
);
|
||||
$response = new Response(
|
||||
$this->entityWorkflowManager->renderPublicView($workflowSend, $metadata),
|
||||
);
|
||||
|
||||
$view = new EntityWorkflowSendView($workflowSend, $this->clock->now(), $request->getClientIp());
|
||||
$this->entityManager->persist($view);
|
||||
$this->messageBus->dispatch(new PostPublicViewMessage($view->getId()));
|
||||
$this->entityManager->flush();
|
||||
|
||||
return $response;
|
||||
} catch (HandlerWithPublicViewNotFoundException $e) {
|
||||
throw new \RuntimeException('Could not render the public view', previous: $e);
|
||||
}
|
||||
}
|
||||
}
|
@@ -19,25 +19,25 @@ use Doctrine\Persistence\ObjectManager;
|
||||
*/
|
||||
trait LoadAbstractNotificationsTrait
|
||||
{
|
||||
public function load(ObjectManager $manager)
|
||||
public function load(ObjectManager $manager): void
|
||||
{
|
||||
return;
|
||||
|
||||
foreach ($this->notifs as $notif) {
|
||||
$entityId = $this->getReference($notif['entityRef'])->getId();
|
||||
$entityId = $this->getReference($notif['entityRef'], Notification::class)->getId();
|
||||
|
||||
echo 'Adding notification for '.$notif['entityClass'].'(entity id:'.$entityId.")\n";
|
||||
|
||||
$newNotif = (new Notification())
|
||||
->setMessage($notif['message'])
|
||||
->setSender($this->getReference($notif['sender']))
|
||||
->setSender($this->getReference($notif['sender'], Notification::class))
|
||||
->setRelatedEntityClass($notif['entityClass'])
|
||||
->setRelatedEntityId($entityId)
|
||||
->setDate(new \DateTimeImmutable('now'))
|
||||
->setRead([]);
|
||||
|
||||
foreach ($notif['addressees'] as $addressee) {
|
||||
$newNotif->addAddressee($this->getReference($addressee));
|
||||
$newNotif->addAddressee($this->getReference($addressee, Notification::class));
|
||||
}
|
||||
|
||||
$manager->persist($newNotif);
|
||||
|
@@ -13,6 +13,7 @@ namespace Chill\MainBundle\DataFixtures\ORM;
|
||||
|
||||
use Chill\MainBundle\Doctrine\Model\Point;
|
||||
use Chill\MainBundle\Entity\AddressReference;
|
||||
use Chill\MainBundle\Entity\PostalCode;
|
||||
use Doctrine\Common\DataFixtures\AbstractFixture;
|
||||
use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
|
||||
use Doctrine\Persistence\ObjectManager;
|
||||
@@ -33,12 +34,12 @@ class LoadAddressReferences extends AbstractFixture implements ContainerAwareInt
|
||||
$this->faker = \Faker\Factory::create('fr_FR');
|
||||
}
|
||||
|
||||
public function getOrder()
|
||||
public function getOrder(): int
|
||||
{
|
||||
return 51;
|
||||
}
|
||||
|
||||
public function load(ObjectManager $manager)
|
||||
public function load(ObjectManager $manager): void
|
||||
{
|
||||
echo "loading some reference address... \n";
|
||||
|
||||
@@ -69,7 +70,8 @@ class LoadAddressReferences extends AbstractFixture implements ContainerAwareInt
|
||||
$ar->setStreetNumber((string) random_int(0, 199));
|
||||
$ar->setPoint($this->getRandomPoint());
|
||||
$ar->setPostcode($this->getReference(
|
||||
LoadPostalCodes::$refs[array_rand(LoadPostalCodes::$refs)]
|
||||
LoadPostalCodes::$refs[array_rand(LoadPostalCodes::$refs)],
|
||||
PostalCode::class
|
||||
));
|
||||
|
||||
$ar->setMunicipalityCode($ar->getPostcode()->getCode());
|
||||
|
@@ -31,12 +31,12 @@ class LoadCenters extends AbstractFixture implements OrderedFixtureInterface
|
||||
|
||||
public static $refs = [];
|
||||
|
||||
public function getOrder()
|
||||
public function getOrder(): int
|
||||
{
|
||||
return 100;
|
||||
}
|
||||
|
||||
public function load(ObjectManager $manager)
|
||||
public function load(ObjectManager $manager): void
|
||||
{
|
||||
foreach (static::$centers as $new) {
|
||||
$center = new Center();
|
||||
|
@@ -25,12 +25,12 @@ class LoadCountries extends AbstractFixture implements ContainerAwareInterface,
|
||||
{
|
||||
private ?ContainerInterface $container = null;
|
||||
|
||||
public function getOrder()
|
||||
public function getOrder(): int
|
||||
{
|
||||
return 20;
|
||||
}
|
||||
|
||||
public function load(ObjectManager $manager)
|
||||
public function load(ObjectManager $manager): void
|
||||
{
|
||||
echo "loading countries... \n";
|
||||
|
||||
|
@@ -38,12 +38,12 @@ class LoadGenders extends AbstractFixture implements OrderedFixtureInterface
|
||||
],
|
||||
];
|
||||
|
||||
public function getOrder()
|
||||
public function getOrder(): int
|
||||
{
|
||||
return 100;
|
||||
}
|
||||
|
||||
public function load(ObjectManager $manager)
|
||||
public function load(ObjectManager $manager): void
|
||||
{
|
||||
echo "loading genders... \n";
|
||||
|
||||
|
@@ -11,7 +11,9 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\MainBundle\DataFixtures\ORM;
|
||||
|
||||
use Chill\MainBundle\Entity\Center;
|
||||
use Chill\MainBundle\Entity\GroupCenter;
|
||||
use Chill\MainBundle\Entity\PermissionsGroup;
|
||||
use Doctrine\Common\DataFixtures\AbstractFixture;
|
||||
use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
|
||||
use Doctrine\Persistence\ObjectManager;
|
||||
@@ -20,18 +22,18 @@ class LoadGroupCenters extends AbstractFixture implements OrderedFixtureInterfac
|
||||
{
|
||||
public static $refs = [];
|
||||
|
||||
public function getOrder()
|
||||
public function getOrder(): int
|
||||
{
|
||||
return 500;
|
||||
}
|
||||
|
||||
public function load(ObjectManager $manager)
|
||||
public function load(ObjectManager $manager): void
|
||||
{
|
||||
foreach (LoadCenters::$refs as $centerRef) {
|
||||
foreach (LoadPermissionsGroup::$refs as $permissionGroupRef) {
|
||||
$GroupCenter = new GroupCenter();
|
||||
$GroupCenter->setCenter($this->getReference($centerRef));
|
||||
$GroupCenter->setPermissionsGroup($this->getReference($permissionGroupRef));
|
||||
$GroupCenter->setCenter($this->getReference($centerRef, Center::class));
|
||||
$GroupCenter->setPermissionsGroup($this->getReference($permissionGroupRef, PermissionsGroup::class));
|
||||
|
||||
$manager->persist($GroupCenter);
|
||||
|
||||
|
@@ -33,12 +33,12 @@ class LoadLanguages extends AbstractFixture implements ContainerAwareInterface,
|
||||
// This array contains regional code to not exclude
|
||||
private array $regionalVersionToInclude = ['ro_MD'];
|
||||
|
||||
public function getOrder()
|
||||
public function getOrder(): int
|
||||
{
|
||||
return 10;
|
||||
}
|
||||
|
||||
public function load(ObjectManager $manager)
|
||||
public function load(ObjectManager $manager): void
|
||||
{
|
||||
echo "loading languages... \n";
|
||||
|
||||
|
@@ -25,7 +25,7 @@ class LoadLocationType extends AbstractFixture implements ContainerAwareInterfac
|
||||
{
|
||||
private ?ContainerInterface $container = null;
|
||||
|
||||
public function getOrder()
|
||||
public function getOrder(): int
|
||||
{
|
||||
return 52;
|
||||
}
|
||||
|
@@ -12,6 +12,7 @@ declare(strict_types=1);
|
||||
namespace Chill\MainBundle\DataFixtures\ORM;
|
||||
|
||||
use Chill\MainBundle\Entity\PermissionsGroup;
|
||||
use Chill\MainBundle\Entity\RoleScope;
|
||||
use Doctrine\Common\DataFixtures\AbstractFixture;
|
||||
use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
|
||||
use Doctrine\Persistence\ObjectManager;
|
||||
@@ -47,19 +48,19 @@ class LoadPermissionsGroup extends AbstractFixture implements OrderedFixtureInte
|
||||
|
||||
public static $refs = [];
|
||||
|
||||
public function getOrder()
|
||||
public function getOrder(): int
|
||||
{
|
||||
return 400;
|
||||
}
|
||||
|
||||
public function load(ObjectManager $manager)
|
||||
public function load(ObjectManager $manager): void
|
||||
{
|
||||
foreach (static::$permissionGroup as $new) {
|
||||
$permissionGroup = new PermissionsGroup();
|
||||
$permissionGroup->setName($new['name']);
|
||||
|
||||
foreach ($new['role_scopes'] as $roleScopeRef) {
|
||||
$permissionGroup->addRoleScope($this->getReference($roleScopeRef));
|
||||
$permissionGroup->addRoleScope($this->getReference($roleScopeRef, RoleScope::class));
|
||||
}
|
||||
|
||||
$manager->persist($permissionGroup);
|
||||
|
@@ -325,12 +325,12 @@ class LoadPostalCodes extends AbstractFixture implements OrderedFixtureInterface
|
||||
85800,GIVRAND,FR,85100,46.6822701061,-1.8787272243,INSEE
|
||||
EOF;
|
||||
|
||||
public function getOrder()
|
||||
public function getOrder(): int
|
||||
{
|
||||
return 50;
|
||||
}
|
||||
|
||||
public function load(ObjectManager $manager)
|
||||
public function load(ObjectManager $manager): void
|
||||
{
|
||||
echo "loading postal codes... \n";
|
||||
$this->loadPostalCodeCSV($manager, self::$postalCodeBelgium, 'BE');
|
||||
@@ -365,7 +365,7 @@ class LoadPostalCodes extends AbstractFixture implements OrderedFixtureInterface
|
||||
$manager->persist($c);
|
||||
$ref = 'postal_code_'.$code[0];
|
||||
|
||||
if (!$this->hasReference($ref)) {
|
||||
if (!$this->hasReference($ref, PostalCode::class)) {
|
||||
$this->addReference($ref, $c);
|
||||
self::$refs[] = $ref;
|
||||
}
|
||||
|
@@ -12,6 +12,7 @@ declare(strict_types=1);
|
||||
namespace Chill\MainBundle\DataFixtures\ORM;
|
||||
|
||||
use Chill\MainBundle\Entity\RoleScope;
|
||||
use Chill\MainBundle\Entity\Scope;
|
||||
use Doctrine\Common\DataFixtures\AbstractFixture;
|
||||
use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
|
||||
use Doctrine\Persistence\ObjectManager;
|
||||
@@ -44,19 +45,19 @@ class LoadRoleScopes extends AbstractFixture implements OrderedFixtureInterface
|
||||
|
||||
public static $references = [];
|
||||
|
||||
public function getOrder()
|
||||
public function getOrder(): int
|
||||
{
|
||||
return 300;
|
||||
}
|
||||
|
||||
public function load(ObjectManager $manager)
|
||||
public function load(ObjectManager $manager): void
|
||||
{
|
||||
foreach (static::$permissions as $key => $permission) {
|
||||
foreach (LoadScopes::$references as $scopeReference) {
|
||||
$roleScope = new RoleScope();
|
||||
$roleScope->setRole($key)
|
||||
->setScope($this->getReference($scopeReference));
|
||||
$reference = 'role_scope_'.$key.'_'.$this->getReference($scopeReference)->getName()['en'];
|
||||
->setScope($this->getReference($scopeReference, Scope::class));
|
||||
$reference = 'role_scope_'.$key.'_'.$this->getReference($scopeReference, Scope::class)->getName()['en'];
|
||||
echo "Creating {$reference} \n";
|
||||
$this->addReference($reference, $roleScope);
|
||||
$manager->persist($roleScope);
|
||||
|
@@ -46,12 +46,12 @@ class LoadScopes extends AbstractFixture implements OrderedFixtureInterface
|
||||
],
|
||||
];
|
||||
|
||||
public function getOrder()
|
||||
public function getOrder(): int
|
||||
{
|
||||
return 200;
|
||||
}
|
||||
|
||||
public function load(ObjectManager $manager)
|
||||
public function load(ObjectManager $manager): void
|
||||
{
|
||||
foreach ($this->scopes as $new) {
|
||||
$scope = new \Chill\MainBundle\Entity\Scope();
|
||||
|
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\MainBundle\DataFixtures\ORM;
|
||||
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Entity\UserGroup;
|
||||
use Doctrine\Bundle\FixturesBundle\Fixture;
|
||||
use Doctrine\Bundle\FixturesBundle\FixtureGroupInterface;
|
||||
use Doctrine\Persistence\ObjectManager;
|
||||
|
||||
class LoadUserGroup extends Fixture implements FixtureGroupInterface
|
||||
{
|
||||
public static function getGroups(): array
|
||||
{
|
||||
return ['user-group'];
|
||||
}
|
||||
|
||||
public function load(ObjectManager $manager): void
|
||||
{
|
||||
$centerASocial = $manager->getRepository(User::class)->findOneBy(['username' => 'center a_social']);
|
||||
$centerBSocial = $manager->getRepository(User::class)->findOneBy(['username' => 'center b_social']);
|
||||
$multiCenter = $manager->getRepository(User::class)->findOneBy(['username' => 'multi_center']);
|
||||
$administrativeA = $manager->getRepository(User::class)->findOneBy(['username' => 'center a_administrative']);
|
||||
$administrativeB = $manager->getRepository(User::class)->findOneBy(['username' => 'center b_administrative']);
|
||||
|
||||
$level1 = $this->generateLevelGroup('Niveau 1', '#eec84aff', '#000000ff', 'level');
|
||||
$level1->addUser($centerASocial)->addUser($centerBSocial);
|
||||
$manager->persist($level1);
|
||||
|
||||
$level2 = $this->generateLevelGroup('Niveau 2', ' #e2793dff', '#000000ff', 'level');
|
||||
$level2->addUser($multiCenter);
|
||||
$manager->persist($level2);
|
||||
|
||||
$level3 = $this->generateLevelGroup('Niveau 3', ' #df4949ff', '#000000ff', 'level');
|
||||
$level3->addUser($multiCenter);
|
||||
$manager->persist($level3);
|
||||
|
||||
$tss = $this->generateLevelGroup('Travailleur sociaux', '#43b29dff', '#000000ff', '');
|
||||
$tss->addUser($multiCenter)->addUser($centerASocial)->addUser($centerBSocial);
|
||||
$manager->persist($tss);
|
||||
$admins = $this->generateLevelGroup('Administratif', '#334d5cff', '#000000ff', '');
|
||||
$admins->addUser($administrativeA)->addUser($administrativeB);
|
||||
$manager->persist($admins);
|
||||
|
||||
$manager->flush();
|
||||
}
|
||||
|
||||
private function generateLevelGroup(string $title, string $backgroundColor, string $foregroundColor, string $excludeKey): UserGroup
|
||||
{
|
||||
$userGroup = new UserGroup();
|
||||
|
||||
return $userGroup
|
||||
->setLabel(['fr' => $title])
|
||||
->setBackgroundColor($backgroundColor)
|
||||
->setForegroundColor($foregroundColor)
|
||||
->setExcludeKey($excludeKey)
|
||||
;
|
||||
}
|
||||
}
|
@@ -11,6 +11,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\MainBundle\DataFixtures\ORM;
|
||||
|
||||
use Chill\MainBundle\Entity\GroupCenter;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Doctrine\Common\DataFixtures\AbstractFixture;
|
||||
use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
|
||||
@@ -54,12 +55,12 @@ class LoadUsers extends AbstractFixture implements ContainerAwareInterface, Orde
|
||||
|
||||
private ?ContainerInterface $container = null;
|
||||
|
||||
public function getOrder()
|
||||
public function getOrder(): int
|
||||
{
|
||||
return 1000;
|
||||
}
|
||||
|
||||
public function load(ObjectManager $manager)
|
||||
public function load(ObjectManager $manager): void
|
||||
{
|
||||
foreach (self::$refs as $username => $params) {
|
||||
$user = new User();
|
||||
@@ -80,7 +81,7 @@ class LoadUsers extends AbstractFixture implements ContainerAwareInterface, Orde
|
||||
->setEmail(sprintf('%s@chill.social', \str_replace(' ', '', (string) $username)));
|
||||
|
||||
foreach ($params['groupCenterRefs'] as $groupCenterRef) {
|
||||
$user->addGroupCenter($this->getReference($groupCenterRef));
|
||||
$user->addGroupCenter($this->getReference($groupCenterRef, GroupCenter::class));
|
||||
}
|
||||
|
||||
echo 'Creating user '.$username."... \n";
|
||||
|
@@ -26,6 +26,8 @@ use Chill\MainBundle\Controller\LocationTypeController;
|
||||
use Chill\MainBundle\Controller\NewsItemController;
|
||||
use Chill\MainBundle\Controller\RegroupmentController;
|
||||
use Chill\MainBundle\Controller\UserController;
|
||||
use Chill\MainBundle\Controller\UserGroupAdminController;
|
||||
use Chill\MainBundle\Controller\UserGroupApiController;
|
||||
use Chill\MainBundle\Controller\UserJobApiController;
|
||||
use Chill\MainBundle\Controller\UserJobController;
|
||||
use Chill\MainBundle\DependencyInjection\Widget\Factory\WidgetFactoryInterface;
|
||||
@@ -62,6 +64,7 @@ use Chill\MainBundle\Entity\LocationType;
|
||||
use Chill\MainBundle\Entity\NewsItem;
|
||||
use Chill\MainBundle\Entity\Regroupment;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Entity\UserGroup;
|
||||
use Chill\MainBundle\Entity\UserJob;
|
||||
use Chill\MainBundle\Form\CenterType;
|
||||
use Chill\MainBundle\Form\CivilityType;
|
||||
@@ -72,6 +75,7 @@ use Chill\MainBundle\Form\LocationFormType;
|
||||
use Chill\MainBundle\Form\LocationTypeType;
|
||||
use Chill\MainBundle\Form\NewsItemType;
|
||||
use Chill\MainBundle\Form\RegroupmentType;
|
||||
use Chill\MainBundle\Form\UserGroupType;
|
||||
use Chill\MainBundle\Form\UserJobType;
|
||||
use Chill\MainBundle\Form\UserType;
|
||||
use Misd\PhoneNumberBundle\Doctrine\DBAL\Types\PhoneNumberType;
|
||||
@@ -357,6 +361,28 @@ class ChillMainExtension extends Extension implements
|
||||
{
|
||||
$container->prependExtensionConfig('chill_main', [
|
||||
'cruds' => [
|
||||
[
|
||||
'class' => UserGroup::class,
|
||||
'controller' => UserGroupAdminController::class,
|
||||
'name' => 'admin_user_group',
|
||||
'base_path' => '/admin/main/user-group',
|
||||
'base_role' => 'ROLE_ADMIN',
|
||||
'form_class' => UserGroupType::class,
|
||||
'actions' => [
|
||||
'index' => [
|
||||
'role' => 'ROLE_ADMIN',
|
||||
'template' => '@ChillMain/UserGroup/index.html.twig',
|
||||
],
|
||||
'new' => [
|
||||
'role' => 'ROLE_ADMIN',
|
||||
'template' => '@ChillMain/UserGroup/new.html.twig',
|
||||
],
|
||||
'edit' => [
|
||||
'role' => 'ROLE_ADMIN',
|
||||
'template' => '@ChillMain/UserGroup/edit.html.twig',
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'class' => UserJob::class,
|
||||
'controller' => UserJobController::class,
|
||||
@@ -844,6 +870,21 @@ class ChillMainExtension extends Extension implements
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'class' => UserGroup::class,
|
||||
'controller' => UserGroupApiController::class,
|
||||
'name' => 'user-group',
|
||||
'base_path' => '/api/1.0/main/user-group',
|
||||
'base_role' => 'ROLE_USER',
|
||||
'actions' => [
|
||||
'_index' => [
|
||||
'methods' => [
|
||||
Request::METHOD_GET => true,
|
||||
Request::METHOD_HEAD => true,
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
@@ -85,6 +85,29 @@ class Configuration implements ConfigurationInterface
|
||||
->end()
|
||||
->end()
|
||||
->end() // end of notifications
|
||||
->arrayNode('workflow_signature')
|
||||
->children()
|
||||
->arrayNode('base_signer')
|
||||
->children()
|
||||
->arrayNode('document_kinds')
|
||||
->arrayPrototype()
|
||||
->children()
|
||||
->scalarNode('key')->cannotBeEmpty()->end()
|
||||
->arrayNode('labels')
|
||||
->arrayPrototype()
|
||||
->children()
|
||||
->scalarNode('lang')->cannotBeEmpty()->end()
|
||||
->scalarNode('label')->cannotBeEmpty()->end()
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
->end() // end of workflow signature document types
|
||||
->arrayNode('phone_helper')
|
||||
->canBeUnset()
|
||||
->children()
|
||||
|
@@ -36,7 +36,7 @@ class Center implements HasCenterInterface, \Stringable
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::STRING, length: 255)]
|
||||
private string $name = '';
|
||||
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::BOOLEAN)]
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::BOOLEAN, options: ['default' => true])]
|
||||
private bool $isActive = true;
|
||||
|
||||
/**
|
||||
|
@@ -39,7 +39,7 @@ class Country
|
||||
* @var array<string, string>
|
||||
*/
|
||||
#[Groups(['read', 'docgen:read'])]
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON)]
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, options: ['default' => '[]', 'jsonb' => true])]
|
||||
#[Context(['is-translatable' => true], groups: ['docgen:read'])]
|
||||
private array $name = [];
|
||||
|
||||
|
@@ -30,7 +30,7 @@ class CronJobExecution
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER, nullable: true, options: ['default' => null])]
|
||||
private ?int $lastStatus = null;
|
||||
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, options: ['default' => "'{}'::jsonb", 'jsonb' => true])]
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, options: ['default' => '{}', 'jsonb' => true])]
|
||||
private array $lastExecutionData = [];
|
||||
|
||||
public function __construct(
|
||||
|
@@ -39,7 +39,7 @@ class DashboardConfigItem
|
||||
private ?User $user = null;
|
||||
|
||||
#[Serializer\Groups(['dashboardConfigItem:read'])]
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, options: ['default' => '[]', 'jsonb' => true])]
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, options: ['default' => '{}', 'jsonb' => true])]
|
||||
private array $metadata = [];
|
||||
|
||||
public function getId(): ?int
|
||||
|
@@ -21,13 +21,13 @@ use Symfony\Component\Validator\Constraints as Assert;
|
||||
#[ORM\Table(name: 'chill_main_gender')]
|
||||
class Gender
|
||||
{
|
||||
#[Serializer\Groups(['read'])]
|
||||
#[Serializer\Groups(['read', 'docgen:read'])]
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER)]
|
||||
private ?int $id = null;
|
||||
|
||||
#[Serializer\Groups(['read'])]
|
||||
#[Serializer\Groups(['read', 'docgen:read'])]
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON)]
|
||||
private array $label = [];
|
||||
|
||||
@@ -36,7 +36,7 @@ class Gender
|
||||
private bool $active = true;
|
||||
|
||||
#[Assert\NotNull(message: 'You must choose a gender translation')]
|
||||
#[Serializer\Groups(['read'])]
|
||||
#[Serializer\Groups(['read', 'docgen:read'])]
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::STRING, enumType: GenderEnum::class)]
|
||||
private GenderEnum $genderTranslation;
|
||||
|
||||
|
@@ -26,7 +26,7 @@ use Symfony\Component\Serializer\Annotation\DiscriminatorMap;
|
||||
class Location implements TrackCreationInterface, TrackUpdateInterface
|
||||
{
|
||||
#[Serializer\Groups(['read'])]
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::BOOLEAN, nullable: true)]
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::BOOLEAN, options: ['default' => true])]
|
||||
private bool $active = true;
|
||||
|
||||
#[Serializer\Groups(['read', 'write', 'docgen:read'])]
|
||||
|
@@ -34,7 +34,7 @@ class LocationType
|
||||
final public const STATUS_REQUIRED = 'required';
|
||||
|
||||
#[Serializer\Groups(['read'])]
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::BOOLEAN, nullable: true)]
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::BOOLEAN, options: ['default' => true])]
|
||||
private bool $active = true;
|
||||
|
||||
#[Serializer\Groups(['read'])]
|
||||
@@ -54,7 +54,7 @@ class LocationType
|
||||
private ?string $defaultFor = null;
|
||||
|
||||
#[Serializer\Groups(['read'])]
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::BOOLEAN)]
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::BOOLEAN, options: ['default' => true])]
|
||||
private bool $editableByUsers = true;
|
||||
|
||||
#[Serializer\Groups(['read', 'docgen:read'])]
|
||||
|
@@ -41,7 +41,7 @@ class Notification implements TrackUpdateInterface
|
||||
*
|
||||
* @var array|string[]
|
||||
*/
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON)]
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, options: ['default' => '[]', 'jsonb' => true])]
|
||||
private array $addressesEmails = [];
|
||||
|
||||
/**
|
||||
|
@@ -24,7 +24,7 @@ class PermissionsGroup
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON)]
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, options: ['default' => '[]', 'jsonb' => true])]
|
||||
private array $flags = [];
|
||||
|
||||
/**
|
||||
|
@@ -45,7 +45,7 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
|
||||
/**
|
||||
* Array where SAML attributes's data are stored.
|
||||
*/
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, nullable: false)]
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, nullable: false, options: ['default' => '[]', 'jsonb' => true])]
|
||||
private array $attributes = [];
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Civility::class)]
|
||||
@@ -64,7 +64,7 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
|
||||
private bool $enabled = true;
|
||||
|
||||
/**
|
||||
* @var Collection<int, \Chill\MainBundle\Entity\GroupCenter>
|
||||
* @var Collection<int, GroupCenter>
|
||||
*/
|
||||
#[ORM\ManyToMany(targetEntity: GroupCenter::class, inversedBy: 'users')]
|
||||
#[ORM\Cache(usage: 'NONSTRICT_READ_WRITE')]
|
||||
@@ -83,7 +83,7 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
|
||||
private ?Location $mainLocation = null;
|
||||
|
||||
/**
|
||||
* @var \Doctrine\Common\Collections\Collection<int, \Chill\MainBundle\Entity\User\UserScopeHistory>&Selectable
|
||||
* @var Collection<int, UserScopeHistory>&Selectable
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'user', targetEntity: UserScopeHistory::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
|
||||
private Collection&Selectable $scopeHistories;
|
||||
@@ -98,7 +98,7 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
|
||||
private ?string $salt = null;
|
||||
|
||||
/**
|
||||
* @var \Doctrine\Common\Collections\Collection<int, \Chill\MainBundle\Entity\User\UserJobHistory>&Selectable
|
||||
* @var Collection<int, UserJobHistory>&Selectable
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'user', targetEntity: UserJobHistory::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
|
||||
private Collection&Selectable $jobHistories;
|
||||
|
244
src/Bundle/ChillMainBundle/Entity/UserGroup.php
Normal file
244
src/Bundle/ChillMainBundle/Entity/UserGroup.php
Normal file
@@ -0,0 +1,244 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\MainBundle\Entity;
|
||||
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\Common\Collections\Criteria;
|
||||
use Doctrine\Common\Collections\Order;
|
||||
use Doctrine\Common\Collections\ReadableCollection;
|
||||
use Doctrine\Common\Collections\Selectable;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Serializer\Annotation\DiscriminatorMap;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
#[ORM\Entity]
|
||||
#[ORM\Table(name: 'chill_main_user_group')]
|
||||
// this discriminator key is required for automated denormalization
|
||||
#[DiscriminatorMap('type', mapping: ['user_group' => UserGroup::class])]
|
||||
class UserGroup
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER, nullable: false)]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::BOOLEAN, nullable: false, options: ['default' => true])]
|
||||
private bool $active = true;
|
||||
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, nullable: false, options: ['default' => '[]'])]
|
||||
private array $label = [];
|
||||
|
||||
/**
|
||||
* @var Collection<int, User>&Selectable<int, User>
|
||||
*/
|
||||
#[ORM\ManyToMany(targetEntity: User::class)]
|
||||
#[ORM\JoinTable(name: 'chill_main_user_group_user')]
|
||||
private Collection&Selectable $users;
|
||||
|
||||
/**
|
||||
* @var Collection<int, User>&Selectable<int, User>
|
||||
*/
|
||||
#[ORM\ManyToMany(targetEntity: User::class)]
|
||||
#[ORM\JoinTable(name: 'chill_main_user_group_user_admin')]
|
||||
private Collection&Selectable $adminUsers;
|
||||
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false, options: ['default' => '#ffffffff'])]
|
||||
private string $backgroundColor = '#ffffffff';
|
||||
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false, options: ['default' => '#000000ff'])]
|
||||
private string $foregroundColor = '#000000ff';
|
||||
|
||||
/**
|
||||
* Groups with same exclude key are mutually exclusive: adding one in a many-to-one relationship
|
||||
* will exclude others.
|
||||
*
|
||||
* An empty string means "no exclusion"
|
||||
*/
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false, options: ['default' => ''])]
|
||||
private string $excludeKey = '';
|
||||
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false, options: ['default' => ''])]
|
||||
#[Assert\Email]
|
||||
private string $email = '';
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->adminUsers = new ArrayCollection();
|
||||
$this->users = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function isActive(): bool
|
||||
{
|
||||
return $this->active;
|
||||
}
|
||||
|
||||
public function setActive(bool $active): self
|
||||
{
|
||||
$this->active = $active;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addAdminUser(User $user): self
|
||||
{
|
||||
if (!$this->adminUsers->contains($user)) {
|
||||
$this->adminUsers[] = $user;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeAdminUser(User $user): self
|
||||
{
|
||||
$this->adminUsers->removeElement($user);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addUser(User $user): self
|
||||
{
|
||||
if (!$this->users->contains($user)) {
|
||||
$this->users[] = $user;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeUser(User $user): self
|
||||
{
|
||||
if ($this->users->contains($user)) {
|
||||
$this->users->removeElement($user);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getLabel(): array
|
||||
{
|
||||
return $this->label;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Selectable<int, User>&Collection<int, User>
|
||||
*/
|
||||
public function getUsers(): Collection&Selectable
|
||||
{
|
||||
return $this->users;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Selectable<int, User>&Collection<int, User>
|
||||
*/
|
||||
public function getAdminUsers(): Collection&Selectable
|
||||
{
|
||||
return $this->adminUsers;
|
||||
}
|
||||
|
||||
public function getForegroundColor(): string
|
||||
{
|
||||
return $this->foregroundColor;
|
||||
}
|
||||
|
||||
public function getExcludeKey(): string
|
||||
{
|
||||
return $this->excludeKey;
|
||||
}
|
||||
|
||||
public function getBackgroundColor(): string
|
||||
{
|
||||
return $this->backgroundColor;
|
||||
}
|
||||
|
||||
public function setForegroundColor(string $foregroundColor): self
|
||||
{
|
||||
$this->foregroundColor = $foregroundColor;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setBackgroundColor(string $backgroundColor): self
|
||||
{
|
||||
$this->backgroundColor = $backgroundColor;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setExcludeKey(string $excludeKey): self
|
||||
{
|
||||
$this->excludeKey = $excludeKey;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setLabel(array $label): self
|
||||
{
|
||||
$this->label = $label;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getEmail(): string
|
||||
{
|
||||
return $this->email;
|
||||
}
|
||||
|
||||
public function setEmail(string $email): self
|
||||
{
|
||||
$this->email = $email;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function hasEmail(): bool
|
||||
{
|
||||
return '' !== $this->email;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current object is an instance of the UserGroup class.
|
||||
*
|
||||
* In use in twig template, to discriminate when there an object can be polymorphic.
|
||||
*
|
||||
* @return bool returns true if the current object is an instance of UserGroup, false otherwise
|
||||
*/
|
||||
public function isUserGroup(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function contains(User $user): bool
|
||||
{
|
||||
return $this->users->contains($user);
|
||||
}
|
||||
|
||||
public function getUserListByLabelAscending(): ReadableCollection
|
||||
{
|
||||
$criteria = Criteria::create();
|
||||
$criteria->orderBy(['label' => Order::Ascending]);
|
||||
|
||||
return $this->getUsers()->matching($criteria);
|
||||
}
|
||||
|
||||
public function getAdminUserListByLabelAscending(): ReadableCollection
|
||||
{
|
||||
$criteria = Criteria::create();
|
||||
$criteria->orderBy(['label' => Order::Ascending]);
|
||||
|
||||
return $this->getAdminUsers()->matching($criteria);
|
||||
}
|
||||
}
|
@@ -17,9 +17,10 @@ use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
|
||||
use Chill\MainBundle\Doctrine\Model\TrackUpdateTrait;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Workflow\Validator\EntityWorkflowCreation;
|
||||
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\Common\Collections\Order;
|
||||
use Doctrine\Common\Collections\Selectable;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Serializer\Annotation as Serializer;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
@@ -35,38 +36,9 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
|
||||
use TrackUpdateTrait;
|
||||
|
||||
/**
|
||||
* a list of future cc users for the next steps.
|
||||
*
|
||||
* @var array|User[]
|
||||
* @var Collection<int, EntityWorkflowComment>
|
||||
*/
|
||||
public array $futureCcUsers = [];
|
||||
|
||||
/**
|
||||
* a list of future dest emails for the next steps.
|
||||
*
|
||||
* This is in used in order to let controller inform who will be the future emails which will validate
|
||||
* the next step. This is necessary to perform some computation about the next emails, before they are
|
||||
* associated to the entity EntityWorkflowStep.
|
||||
*
|
||||
* @var array|string[]
|
||||
*/
|
||||
public array $futureDestEmails = [];
|
||||
|
||||
/**
|
||||
* a list of future dest users for the next steps.
|
||||
*
|
||||
* This is in used in order to let controller inform who will be the future users which will validate
|
||||
* the next step. This is necessary to perform some computation about the next users, before they are
|
||||
* associated to the entity EntityWorkflowStep.
|
||||
*
|
||||
* @var array|User[]
|
||||
*/
|
||||
public array $futureDestUsers = [];
|
||||
|
||||
/**
|
||||
* @var Collection<int, \Chill\MainBundle\Entity\Workflow\EntityWorkflowComment>
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: EntityWorkflowComment::class, mappedBy: 'entityWorkflow', orphanRemoval: true)]
|
||||
#[ORM\OneToMany(mappedBy: 'entityWorkflow', targetEntity: EntityWorkflowComment::class, orphanRemoval: true)]
|
||||
private Collection $comments;
|
||||
|
||||
#[ORM\Id]
|
||||
@@ -81,12 +53,12 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
|
||||
private int $relatedEntityId;
|
||||
|
||||
/**
|
||||
* @var Collection<int, EntityWorkflowStep>
|
||||
* @var Collection<int, EntityWorkflowStep>&Selectable<int, EntityWorkflowStep>
|
||||
*/
|
||||
#[Assert\Valid(traverse: true)]
|
||||
#[ORM\OneToMany(mappedBy: 'entityWorkflow', targetEntity: EntityWorkflowStep::class, cascade: ['persist'], orphanRemoval: true)]
|
||||
#[ORM\OneToMany(mappedBy: 'entityWorkflow', targetEntity: EntityWorkflowStep::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
|
||||
#[ORM\OrderBy(['transitionAt' => \Doctrine\Common\Collections\Criteria::ASC, 'id' => 'ASC'])]
|
||||
private Collection $steps;
|
||||
private Collection&Selectable $steps;
|
||||
|
||||
/**
|
||||
* @var array|EntityWorkflowStep[]|null
|
||||
@@ -271,7 +243,10 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
|
||||
throw new \RuntimeException();
|
||||
}
|
||||
|
||||
public function getSteps(): ArrayCollection|Collection
|
||||
/**
|
||||
* @return Selectable<int, EntityWorkflowStep>&Collection<int, EntityWorkflowStep>
|
||||
*/
|
||||
public function getSteps(): Collection&Selectable
|
||||
{
|
||||
return $this->steps;
|
||||
}
|
||||
@@ -346,7 +321,7 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
|
||||
}
|
||||
}
|
||||
|
||||
return $usersInvolved;
|
||||
return array_values($usersInvolved);
|
||||
}
|
||||
|
||||
public function getWorkflowName(): string
|
||||
@@ -367,8 +342,6 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
|
||||
|
||||
public function isFreeze(): bool
|
||||
{
|
||||
$steps = $this->getStepsChained();
|
||||
|
||||
foreach ($this->getStepsChained() as $step) {
|
||||
if ($step->isFreezeAfter()) {
|
||||
return true;
|
||||
@@ -378,6 +351,11 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
|
||||
return false;
|
||||
}
|
||||
|
||||
public function isOnHoldByUser(User $user): bool
|
||||
{
|
||||
return $this->getCurrentStep()->isOnHoldByUser($user);
|
||||
}
|
||||
|
||||
public function isUserSubscribedToFinal(User $user): bool
|
||||
{
|
||||
return $this->subscriberToFinal->contains($user);
|
||||
@@ -446,11 +424,55 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setStep(string $step): self
|
||||
{
|
||||
public function setStep(
|
||||
string $step,
|
||||
WorkflowTransitionContextDTO $transitionContextDTO,
|
||||
string $transition,
|
||||
\DateTimeImmutable $transitionAt,
|
||||
?User $byUser = null,
|
||||
): self {
|
||||
$previousStep = $this->getCurrentStep();
|
||||
|
||||
$previousStep
|
||||
->setComment($transitionContextDTO->comment)
|
||||
->setTransitionAfter($transition)
|
||||
->setTransitionAt($transitionAt)
|
||||
->setTransitionBy($byUser);
|
||||
|
||||
$newStep = new EntityWorkflowStep();
|
||||
$newStep->setCurrentStep($step);
|
||||
|
||||
foreach ($transitionContextDTO->futureCcUsers as $user) {
|
||||
$newStep->addCcUser($user);
|
||||
}
|
||||
|
||||
foreach ($transitionContextDTO->getFutureDestUsers() as $user) {
|
||||
$newStep->addDestUser($user);
|
||||
}
|
||||
|
||||
foreach ($transitionContextDTO->getFutureDestUserGroups() as $userGroup) {
|
||||
$newStep->addDestUserGroup($userGroup);
|
||||
}
|
||||
|
||||
if (null !== $transitionContextDTO->futureUserSignature) {
|
||||
$newStep->addDestUser($transitionContextDTO->futureUserSignature);
|
||||
}
|
||||
|
||||
if (null !== $transitionContextDTO->futureUserSignature) {
|
||||
new EntityWorkflowStepSignature($newStep, $transitionContextDTO->futureUserSignature);
|
||||
} else {
|
||||
foreach ($transitionContextDTO->futurePersonSignatures as $personSignature) {
|
||||
new EntityWorkflowStepSignature($newStep, $personSignature);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($transitionContextDTO->futureDestineeThirdParties as $thirdParty) {
|
||||
new EntityWorkflowSend($newStep, $thirdParty, $transitionAt->add(new \DateInterval('P30D')));
|
||||
}
|
||||
foreach ($transitionContextDTO->futureDestineeEmails as $email) {
|
||||
new EntityWorkflowSend($newStep, $email, $transitionAt->add(new \DateInterval('P30D')));
|
||||
}
|
||||
|
||||
// copy the freeze
|
||||
if ($this->isFreeze()) {
|
||||
$newStep->setFreezeAfter(true);
|
||||
@@ -476,4 +498,41 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
|
||||
|
||||
return $this->steps->get($this->steps->count() - 2);
|
||||
}
|
||||
|
||||
public function isOnHoldAtCurrentStep(): bool
|
||||
{
|
||||
return $this->getCurrentStep()->getHoldsOnStep()->count() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the workflow has become stale after a given date.
|
||||
*
|
||||
* This function checks the creation date and the transition states of the workflow steps.
|
||||
* A workflow is considered stale if:
|
||||
* - The creation date is before the given date and no transitions have occurred since the creation.
|
||||
* - Or if there are no transitions after the given date.
|
||||
*
|
||||
* @param \DateTimeImmutable $at the date to compare against the workflow's status
|
||||
*
|
||||
* @return bool true if the workflow is stale after the given date, false otherwise
|
||||
*/
|
||||
public function isStaledAt(\DateTimeImmutable $at): bool
|
||||
{
|
||||
// if there is no transition since the creation, then the workflow is staled
|
||||
if ('initial' === $this->getCurrentStep()->getCurrentStep()
|
||||
&& null === $this->getCurrentStep()->getTransitionAt()
|
||||
) {
|
||||
if (null === $this->getCreatedAt()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->getCreatedAt() < $at) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->getCurrentStepChained()->getPrevious()->getTransitionAt() < $at;
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,227 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\MainBundle\Entity\Workflow;
|
||||
|
||||
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
|
||||
use Chill\MainBundle\Doctrine\Model\TrackCreationTrait;
|
||||
use Chill\ThirdPartyBundle\Entity\ThirdParty;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Ramsey\Uuid\UuidInterface;
|
||||
use Random\Randomizer;
|
||||
|
||||
/**
|
||||
* An entity which stores then sending of a workflow's content to
|
||||
* some external entity.
|
||||
*/
|
||||
#[ORM\Entity]
|
||||
#[ORM\Table(name: 'chill_main_workflow_entity_send')]
|
||||
class EntityWorkflowSend implements TrackCreationInterface
|
||||
{
|
||||
use TrackCreationTrait;
|
||||
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column(type: Types::INTEGER)]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: ThirdParty::class)]
|
||||
#[ORM\JoinColumn(nullable: true)]
|
||||
private ?ThirdParty $destineeThirdParty = null;
|
||||
|
||||
#[ORM\Column(type: Types::TEXT, nullable: false, options: ['default' => ''])]
|
||||
private string $destineeEmail = '';
|
||||
|
||||
#[ORM\Column(type: 'uuid', unique: true, nullable: false)]
|
||||
private UuidInterface $uuid;
|
||||
|
||||
#[ORM\Column(type: Types::STRING, length: 255, nullable: false)]
|
||||
private string $privateToken;
|
||||
|
||||
#[ORM\Column(type: Types::INTEGER, nullable: false, options: ['default' => 0])]
|
||||
private int $numberOfErrorTrials = 0;
|
||||
|
||||
/**
|
||||
* @var Collection<int, EntityWorkflowSendView>
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'send', targetEntity: EntityWorkflowSendView::class, cascade: ['remove'])]
|
||||
private Collection $views;
|
||||
|
||||
public function __construct(
|
||||
#[ORM\ManyToOne(targetEntity: EntityWorkflowStep::class, inversedBy: 'sends')]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private EntityWorkflowStep $entityWorkflowStep,
|
||||
string|ThirdParty $destinee,
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: false)]
|
||||
private \DateTimeImmutable $expireAt,
|
||||
) {
|
||||
$this->uuid = Uuid::uuid4();
|
||||
$random = new Randomizer();
|
||||
$this->privateToken = bin2hex($random->getBytes(48));
|
||||
|
||||
$this->entityWorkflowStep->addSend($this);
|
||||
|
||||
if ($destinee instanceof ThirdParty) {
|
||||
$this->destineeThirdParty = $destinee;
|
||||
} else {
|
||||
$this->destineeEmail = $destinee;
|
||||
}
|
||||
|
||||
$this->views = new ArrayCollection();
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal use the @see{EntityWorkflowSendView}'s constructor instead
|
||||
*/
|
||||
public function addView(EntityWorkflowSendView $view): self
|
||||
{
|
||||
if (!$this->views->contains($view)) {
|
||||
$this->views->add($view);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDestineeEmail(): string
|
||||
{
|
||||
return $this->destineeEmail;
|
||||
}
|
||||
|
||||
public function getDestineeThirdParty(): ?ThirdParty
|
||||
{
|
||||
return $this->destineeThirdParty;
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getNumberOfErrorTrials(): int
|
||||
{
|
||||
return $this->numberOfErrorTrials;
|
||||
}
|
||||
|
||||
public function getPrivateToken(): string
|
||||
{
|
||||
return $this->privateToken;
|
||||
}
|
||||
|
||||
public function getEntityWorkflowStep(): EntityWorkflowStep
|
||||
{
|
||||
return $this->entityWorkflowStep;
|
||||
}
|
||||
|
||||
public function getEntityWorkflowStepChained(): ?EntityWorkflowStep
|
||||
{
|
||||
foreach ($this->getEntityWorkflowStep()->getEntityWorkflow()->getStepsChained() as $step) {
|
||||
if ($this->getEntityWorkflowStep() === $step) {
|
||||
return $step;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getUuid(): UuidInterface
|
||||
{
|
||||
return $this->uuid;
|
||||
}
|
||||
|
||||
public function getExpireAt(): \DateTimeImmutable
|
||||
{
|
||||
return $this->expireAt;
|
||||
}
|
||||
|
||||
public function getViews(): Collection
|
||||
{
|
||||
return $this->views;
|
||||
}
|
||||
|
||||
public function increaseErrorTrials(): void
|
||||
{
|
||||
$this->numberOfErrorTrials = $this->numberOfErrorTrials + 1;
|
||||
}
|
||||
|
||||
public function getDestinee(): string|ThirdParty
|
||||
{
|
||||
if (null !== $this->getDestineeThirdParty()) {
|
||||
return $this->getDestineeThirdParty();
|
||||
}
|
||||
|
||||
return $this->getDestineeEmail();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the kind of destinee based on whether the destinee is a thirdParty or an emailAddress.
|
||||
*
|
||||
* @return 'thirdParty'|'email' 'thirdParty' if the destinee is a third party, 'email' otherwise
|
||||
*/
|
||||
public function getDestineeKind(): string
|
||||
{
|
||||
if (null !== $this->getDestineeThirdParty()) {
|
||||
return 'thirdParty';
|
||||
}
|
||||
|
||||
return 'email';
|
||||
}
|
||||
|
||||
public function isViewed(): bool
|
||||
{
|
||||
return $this->views->count() > 0;
|
||||
}
|
||||
|
||||
public function isExpired(?\DateTimeImmutable $now = null): bool
|
||||
{
|
||||
return ($now ?? new \DateTimeImmutable('now')) >= $this->expireAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the most recent view.
|
||||
*
|
||||
* @return EntityWorkflowSendView|null returns the last view or null if there are no views
|
||||
*/
|
||||
public function getLastView(): ?EntityWorkflowSendView
|
||||
{
|
||||
$last = null;
|
||||
foreach ($this->views as $view) {
|
||||
if (null === $last) {
|
||||
$last = $view;
|
||||
} else {
|
||||
if ($view->getViewAt() > $last->getViewAt()) {
|
||||
$last = $view;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $last;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves an array of views grouped by their remote IP address.
|
||||
*
|
||||
* @return array<string, list<EntityWorkflowSendView>> an associative array where the keys are IP addresses and the values are arrays of views associated with those IPs
|
||||
*/
|
||||
public function getViewsByIp(): array
|
||||
{
|
||||
$views = [];
|
||||
|
||||
foreach ($this->getViews() as $view) {
|
||||
$views[$view->getRemoteIp()][] = $view;
|
||||
}
|
||||
|
||||
return $views;
|
||||
}
|
||||
}
|
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\MainBundle\Entity\Workflow;
|
||||
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
/**
|
||||
* Register the viewing action from an external destinee.
|
||||
*/
|
||||
#[ORM\Entity(readOnly: true)]
|
||||
#[ORM\Table(name: 'chill_main_workflow_entity_send_views')]
|
||||
class EntityWorkflowSendView
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column(type: Types::INTEGER)]
|
||||
private ?int $id = null;
|
||||
|
||||
public function __construct(
|
||||
#[ORM\ManyToOne(targetEntity: EntityWorkflowSend::class, inversedBy: 'views')]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private EntityWorkflowSend $send,
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE)]
|
||||
private \DateTimeInterface $viewAt,
|
||||
#[ORM\Column(type: Types::TEXT)]
|
||||
private string $remoteIp = '',
|
||||
) {
|
||||
$this->send->addView($this);
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getRemoteIp(): string
|
||||
{
|
||||
return $this->remoteIp;
|
||||
}
|
||||
|
||||
public function getSend(): EntityWorkflowSend
|
||||
{
|
||||
return $this->send;
|
||||
}
|
||||
|
||||
public function getViewAt(): \DateTimeInterface
|
||||
{
|
||||
return $this->viewAt;
|
||||
}
|
||||
}
|
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\MainBundle\Entity\Workflow;
|
||||
|
||||
enum EntityWorkflowSignatureStateEnum: string
|
||||
{
|
||||
case PENDING = 'pending';
|
||||
case SIGNED = 'signed';
|
||||
case REJECTED = 'rejected';
|
||||
case CANCELED = 'canceled';
|
||||
}
|
@@ -12,6 +12,7 @@ declare(strict_types=1);
|
||||
namespace Chill\MainBundle\Entity\Workflow;
|
||||
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Entity\UserGroup;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
@@ -48,6 +49,13 @@ class EntityWorkflowStep
|
||||
#[ORM\JoinTable(name: 'chill_main_workflow_entity_step_user')]
|
||||
private Collection $destUser;
|
||||
|
||||
/**
|
||||
* @var Collection<int, UserGroup>
|
||||
*/
|
||||
#[ORM\ManyToMany(targetEntity: UserGroup::class)]
|
||||
#[ORM\JoinTable(name: 'chill_main_workflow_entity_step_user_group')]
|
||||
private Collection $destUserGroups;
|
||||
|
||||
/**
|
||||
* @var Collection<int, User>
|
||||
*/
|
||||
@@ -55,6 +63,12 @@ class EntityWorkflowStep
|
||||
#[ORM\JoinTable(name: 'chill_main_workflow_entity_step_user_by_accesskey')]
|
||||
private Collection $destUserByAccessKey;
|
||||
|
||||
/**
|
||||
* @var Collection <int, EntityWorkflowStepSignature>
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'step', targetEntity: EntityWorkflowStepSignature::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
|
||||
private Collection $signatures;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: EntityWorkflow::class, inversedBy: 'steps')]
|
||||
private ?EntityWorkflow $entityWorkflow = null;
|
||||
|
||||
@@ -92,11 +106,27 @@ class EntityWorkflowStep
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, nullable: true)]
|
||||
private ?string $transitionByEmail = null;
|
||||
|
||||
/**
|
||||
* @var Collection<int, EntityWorkflowStepHold>
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'step', targetEntity: EntityWorkflowStepHold::class)]
|
||||
private Collection $holdsOnStep;
|
||||
|
||||
/**
|
||||
* @var Collection<int, EntityWorkflowSend>
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'entityWorkflowStep', targetEntity: EntityWorkflowSend::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
|
||||
private Collection $sends;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->ccUser = new ArrayCollection();
|
||||
$this->destUser = new ArrayCollection();
|
||||
$this->destUserGroups = new ArrayCollection();
|
||||
$this->destUserByAccessKey = new ArrayCollection();
|
||||
$this->signatures = new ArrayCollection();
|
||||
$this->holdsOnStep = new ArrayCollection();
|
||||
$this->sends = new ArrayCollection();
|
||||
$this->accessKey = bin2hex(openssl_random_pseudo_bytes(32));
|
||||
}
|
||||
|
||||
@@ -109,6 +139,9 @@ class EntityWorkflowStep
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
public function addDestEmail(string $email): self
|
||||
{
|
||||
if (!\in_array($email, $this->destEmail, true)) {
|
||||
@@ -127,6 +160,22 @@ class EntityWorkflowStep
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addDestUserGroup(UserGroup $userGroup): self
|
||||
{
|
||||
if (!$this->destUserGroups->contains($userGroup)) {
|
||||
$this->destUserGroups[] = $userGroup;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeDestUserGroup(UserGroup $userGroup): self
|
||||
{
|
||||
$this->destUserGroups->removeElement($userGroup);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addDestUserByAccessKey(User $user): self
|
||||
{
|
||||
if (!$this->destUserByAccessKey->contains($user) && !$this->destUser->contains($user)) {
|
||||
@@ -136,6 +185,39 @@ class EntityWorkflowStep
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal use @see{EntityWorkflowStepSignature}'s constructor instead
|
||||
*/
|
||||
public function addSignature(EntityWorkflowStepSignature $signature): self
|
||||
{
|
||||
if (!$this->signatures->contains($signature)) {
|
||||
$this->signatures[] = $signature;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal use @see{EntityWorkflowSend}'s constructor instead
|
||||
*/
|
||||
public function addSend(EntityWorkflowSend $send): self
|
||||
{
|
||||
if (!$this->sends->contains($send)) {
|
||||
$this->sends[] = $send;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeSignature(EntityWorkflowStepSignature $signature): self
|
||||
{
|
||||
if ($this->signatures->contains($signature)) {
|
||||
$this->signatures->removeElement($signature);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getAccessKey(): string
|
||||
{
|
||||
return $this->accessKey;
|
||||
@@ -143,7 +225,9 @@ class EntityWorkflowStep
|
||||
|
||||
/**
|
||||
* get all the users which are allowed to apply a transition: those added manually, and
|
||||
* those added automatically bu using an access key.
|
||||
* those added automatically by using an access key.
|
||||
*
|
||||
* This method exclude the users associated with user groups
|
||||
*
|
||||
* @psalm-suppress DuplicateArrayKey
|
||||
*/
|
||||
@@ -157,6 +241,14 @@ class EntityWorkflowStep
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, UserGroup>
|
||||
*/
|
||||
public function getDestUserGroups(): Collection
|
||||
{
|
||||
return $this->destUserGroups;
|
||||
}
|
||||
|
||||
public function getCcUser(): Collection
|
||||
{
|
||||
return $this->ccUser;
|
||||
@@ -172,6 +264,11 @@ class EntityWorkflowStep
|
||||
return $this->currentStep;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string>
|
||||
*
|
||||
* @deprecated
|
||||
*/
|
||||
public function getDestEmail(): array
|
||||
{
|
||||
return $this->destEmail;
|
||||
@@ -198,6 +295,22 @@ class EntityWorkflowStep
|
||||
return $this->entityWorkflow;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, EntityWorkflowStepSignature>
|
||||
*/
|
||||
public function getSignatures(): Collection
|
||||
{
|
||||
return $this->signatures;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, EntityWorkflowSend>
|
||||
*/
|
||||
public function getSends(): Collection
|
||||
{
|
||||
return $this->sends;
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
@@ -243,6 +356,17 @@ class EntityWorkflowStep
|
||||
return $this->freezeAfter;
|
||||
}
|
||||
|
||||
public function isOnHoldByUser(User $user): bool
|
||||
{
|
||||
foreach ($this->getHoldsOnStep() as $onHold) {
|
||||
if ($onHold->getByUser() === $user) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function isWaitingForTransition(): bool
|
||||
{
|
||||
if (null !== $this->transitionAfter) {
|
||||
@@ -377,6 +501,11 @@ class EntityWorkflowStep
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getHoldsOnStep(): Collection
|
||||
{
|
||||
return $this->holdsOnStep;
|
||||
}
|
||||
|
||||
#[Assert\Callback]
|
||||
public function validateOnCreation(ExecutionContextInterface $context, mixed $payload): void
|
||||
{
|
||||
@@ -396,4 +525,13 @@ class EntityWorkflowStep
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function addOnHold(EntityWorkflowStepHold $onHold): self
|
||||
{
|
||||
if (!$this->holdsOnStep->contains($onHold)) {
|
||||
$this->holdsOnStep->add($onHold);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\MainBundle\Entity\Workflow;
|
||||
|
||||
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
|
||||
use Chill\MainBundle\Doctrine\Model\TrackCreationTrait;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[ORM\Entity]
|
||||
#[ORM\Table('chill_main_workflow_entity_step_hold')]
|
||||
#[ORM\UniqueConstraint(name: 'chill_main_workflow_hold_unique_idx', columns: ['step_id', 'byUser_id'])]
|
||||
class EntityWorkflowStepHold implements TrackCreationInterface
|
||||
{
|
||||
use TrackCreationTrait;
|
||||
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER)]
|
||||
private ?int $id = null;
|
||||
|
||||
public function __construct(#[ORM\ManyToOne(targetEntity: EntityWorkflowStep::class)]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private EntityWorkflowStep $step, #[ORM\ManyToOne(targetEntity: User::class)]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private User $byUser)
|
||||
{
|
||||
$step->addOnHold($this);
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getStep(): EntityWorkflowStep
|
||||
{
|
||||
return $this->step;
|
||||
}
|
||||
|
||||
public function getByUser(): User
|
||||
{
|
||||
return $this->byUser;
|
||||
}
|
||||
}
|
@@ -0,0 +1,206 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\MainBundle\Entity\Workflow;
|
||||
|
||||
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
|
||||
use Chill\MainBundle\Doctrine\Model\TrackCreationTrait;
|
||||
use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
|
||||
use Chill\MainBundle\Doctrine\Model\TrackUpdateTrait;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[ORM\Entity]
|
||||
#[ORM\Table(name: 'chill_main_workflow_entity_step_signature')]
|
||||
class EntityWorkflowStepSignature implements TrackCreationInterface, TrackUpdateInterface
|
||||
{
|
||||
use TrackCreationTrait;
|
||||
use TrackUpdateTrait;
|
||||
|
||||
#[ORM\Id]
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER, unique: true)]
|
||||
#[ORM\GeneratedValue(strategy: 'AUTO')]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: User::class)]
|
||||
#[ORM\JoinColumn(nullable: true)]
|
||||
private ?User $userSigner = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Person::class)]
|
||||
#[ORM\JoinColumn(nullable: true)]
|
||||
private ?Person $personSigner = null;
|
||||
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::STRING, length: 50, nullable: false, enumType: EntityWorkflowSignatureStateEnum::class)]
|
||||
private EntityWorkflowSignatureStateEnum $state = EntityWorkflowSignatureStateEnum::PENDING;
|
||||
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIMETZ_IMMUTABLE, nullable: true, options: ['default' => null])]
|
||||
private ?\DateTimeImmutable $stateDate = null;
|
||||
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, nullable: false, options: ['default' => '[]'])]
|
||||
private array $signatureMetadata = [];
|
||||
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER, nullable: true, options: ['default' => null])]
|
||||
private ?int $zoneSignatureIndex = null;
|
||||
|
||||
public function __construct(
|
||||
#[ORM\ManyToOne(targetEntity: EntityWorkflowStep::class, inversedBy: 'signatures')]
|
||||
private EntityWorkflowStep $step,
|
||||
User|Person $signer,
|
||||
) {
|
||||
$this->step->addSignature($this);
|
||||
$this->setSigner($signer);
|
||||
}
|
||||
|
||||
private function setSigner(User|Person $signer): void
|
||||
{
|
||||
if ($signer instanceof User) {
|
||||
$this->userSigner = $signer;
|
||||
} else {
|
||||
$this->personSigner = $signer;
|
||||
}
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getStep(): EntityWorkflowStep
|
||||
{
|
||||
return $this->step;
|
||||
}
|
||||
|
||||
public function getSigner(): User|Person
|
||||
{
|
||||
if (null !== $this->userSigner) {
|
||||
return $this->userSigner;
|
||||
}
|
||||
|
||||
return $this->personSigner;
|
||||
}
|
||||
|
||||
public function getSignatureMetadata(): array
|
||||
{
|
||||
return $this->signatureMetadata;
|
||||
}
|
||||
|
||||
public function setSignatureMetadata(array $signatureMetadata): EntityWorkflowStepSignature
|
||||
{
|
||||
$this->signatureMetadata = $signatureMetadata;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getState(): EntityWorkflowSignatureStateEnum
|
||||
{
|
||||
return $this->state;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*
|
||||
* @internal You should not use this method directly, use @see{Chill\MainBundle\Workflow\SignatureStepStateChanger} instead
|
||||
*/
|
||||
public function setState(EntityWorkflowSignatureStateEnum $state): EntityWorkflowStepSignature
|
||||
{
|
||||
$this->state = $state;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getStateDate(): ?\DateTimeImmutable
|
||||
{
|
||||
return $this->stateDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*
|
||||
* @internal You should not use this method directly, use @see{Chill\MainBundle\Workflow\SignatureStepStateChanger} instead
|
||||
*/
|
||||
public function setStateDate(?\DateTimeImmutable $stateDate): EntityWorkflowStepSignature
|
||||
{
|
||||
$this->stateDate = $stateDate;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getZoneSignatureIndex(): ?int
|
||||
{
|
||||
return $this->zoneSignatureIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*
|
||||
* @internal You should not use this method directly, use @see{Chill\MainBundle\Workflow\SignatureStepStateChanger} instead
|
||||
*/
|
||||
public function setZoneSignatureIndex(?int $zoneSignatureIndex): EntityWorkflowStepSignature
|
||||
{
|
||||
$this->zoneSignatureIndex = $zoneSignatureIndex;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isSigned(): bool
|
||||
{
|
||||
return EntityWorkflowSignatureStateEnum::SIGNED == $this->getState();
|
||||
}
|
||||
|
||||
public function isPending(): bool
|
||||
{
|
||||
return EntityWorkflowSignatureStateEnum::PENDING == $this->getState();
|
||||
}
|
||||
|
||||
public function isCanceled(): bool
|
||||
{
|
||||
return EntityWorkflowSignatureStateEnum::CANCELED === $this->getState();
|
||||
}
|
||||
|
||||
public function isRejected(): bool
|
||||
{
|
||||
return EntityWorkflowSignatureStateEnum::REJECTED === $this->getState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether all signatures associated with a given workflow step are not pending.
|
||||
*
|
||||
* Iterates over each signature in the provided workflow step, and returns false if any signature
|
||||
* is found to be pending. If all signatures are not pending, returns true.
|
||||
*
|
||||
* @param EntityWorkflowStep $step the workflow step whose signatures are to be checked
|
||||
*
|
||||
* @return bool true if all signatures are not pending, false otherwise
|
||||
*/
|
||||
public static function isAllSignatureNotPendingForStep(EntityWorkflowStep $step): bool
|
||||
{
|
||||
foreach ($step->getSignatures() as $signature) {
|
||||
if ($signature->isPending()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 'person'|'user'
|
||||
*/
|
||||
public function getSignerKind(): string
|
||||
{
|
||||
if ($this->personSigner instanceof Person) {
|
||||
return 'person';
|
||||
}
|
||||
|
||||
return 'user';
|
||||
}
|
||||
}
|
@@ -38,13 +38,13 @@ class GenderType extends AbstractType
|
||||
'mapped' => true,
|
||||
'choice_label' => fn (GenderIconEnum $enum) => '<i class="'.strtolower($enum->value).'"></i>',
|
||||
'choice_value' => fn (?GenderIconEnum $enum) => null !== $enum ? $enum->value : null,
|
||||
'label' => 'gender.admin.Select Gender Icon',
|
||||
'label' => 'gender.admin.Select gender icon',
|
||||
'label_html' => true,
|
||||
])
|
||||
->add('genderTranslation', EnumType::class, [
|
||||
'class' => GenderEnum::class,
|
||||
'choice_label' => fn (GenderEnum $enum) => $enum->value,
|
||||
'label' => 'gender.admin.Select Gender Translation',
|
||||
'label' => 'gender.admin.Select gender translation',
|
||||
])
|
||||
->add('active', ChoiceType::class, [
|
||||
'choices' => [
|
||||
|
@@ -36,6 +36,7 @@ class ChillCollectionType extends AbstractType
|
||||
$view->vars['identifier'] = $options['identifier'];
|
||||
$view->vars['empty_collection_explain'] = $options['empty_collection_explain'];
|
||||
$view->vars['js_caller'] = $options['js_caller'];
|
||||
$view->vars['uniqid'] = uniqid();
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
|
@@ -13,36 +13,30 @@ namespace Chill\MainBundle\Form\Type;
|
||||
|
||||
use Chill\MainBundle\Entity\Center;
|
||||
use Chill\MainBundle\Entity\PermissionsGroup;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Chill\MainBundle\Repository\CenterRepository;
|
||||
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class ComposedGroupCenterType extends AbstractType
|
||||
{
|
||||
public function __construct(private readonly CenterRepository $centerRepository) {}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$centers = $this->centerRepository->findActive();
|
||||
|
||||
$builder->add('permissionsgroup', EntityType::class, [
|
||||
'class' => PermissionsGroup::class,
|
||||
'choice_label' => static fn (PermissionsGroup $group) => $group->getName(),
|
||||
])->add('center', EntityType::class, [
|
||||
'class' => Center::class,
|
||||
'query_builder' => static function (EntityRepository $er) {
|
||||
$qb = $er->createQueryBuilder('c');
|
||||
$qb->where($qb->expr()->eq('c.isActive', 'TRUE'))
|
||||
->orderBy('c.name', 'ASC');
|
||||
|
||||
return $qb;
|
||||
},
|
||||
])->add('center', ChoiceType::class, [
|
||||
'choices' => $centers,
|
||||
'choice_label' => fn (Center $center) => $center->getName(),
|
||||
'multiple' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefault('data_class', \Chill\MainBundle\Entity\GroupCenter::class);
|
||||
}
|
||||
|
||||
public function getBlockPrefix()
|
||||
{
|
||||
return 'composed_groupcenter';
|
||||
|
@@ -12,6 +12,8 @@ declare(strict_types=1);
|
||||
namespace Chill\MainBundle\Form\Type\DataTransformer;
|
||||
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Entity\UserGroup;
|
||||
use Chill\MainBundle\Serializer\Normalizer\DiscriminatedObjectDenormalizer;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Chill\ThirdPartyBundle\Entity\ThirdParty;
|
||||
use Symfony\Component\Form\DataTransformerInterface;
|
||||
@@ -26,10 +28,14 @@ class EntityToJsonTransformer implements DataTransformerInterface
|
||||
|
||||
public function reverseTransform($value)
|
||||
{
|
||||
if ('' === $value) {
|
||||
if (false === $this->multiple && '' === $value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($this->multiple && [] === $value) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$denormalized = json_decode((string) $value, true, 512, JSON_THROW_ON_ERROR);
|
||||
|
||||
if ($this->multiple) {
|
||||
@@ -74,15 +80,23 @@ class EntityToJsonTransformer implements DataTransformerInterface
|
||||
'user' => User::class,
|
||||
'person' => Person::class,
|
||||
'thirdparty' => ThirdParty::class,
|
||||
'user_group' => UserGroup::class,
|
||||
'user_group_or_user' => DiscriminatedObjectDenormalizer::TYPE,
|
||||
default => throw new \UnexpectedValueException('This type is not supported'),
|
||||
};
|
||||
|
||||
$context = [AbstractNormalizer::GROUPS => ['read']];
|
||||
|
||||
if ('user_group_or_user' === $this->type) {
|
||||
$context[DiscriminatedObjectDenormalizer::ALLOWED_TYPES] = [UserGroup::class, User::class];
|
||||
}
|
||||
|
||||
return
|
||||
$this->denormalizer->denormalize(
|
||||
['type' => $item['type'], 'id' => $item['id']],
|
||||
$class,
|
||||
'json',
|
||||
[AbstractNormalizer::GROUPS => ['read']],
|
||||
$context,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -18,16 +18,30 @@ use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Form\FormView;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
|
||||
/**
|
||||
* Pick user dymically, using vuejs module "AddPerson".
|
||||
*
|
||||
* Possible options:
|
||||
*
|
||||
* - `multiple`: pick one or more users
|
||||
* - `suggested`: a list of suggested users
|
||||
* - `suggest_myself`: append the current user to the list of suggested
|
||||
* - `as_id`: only the id will be set in the returned data
|
||||
* - `submit_on_adding_new_entity`: the browser will immediately submit the form when new users are checked
|
||||
*/
|
||||
class PickUserDynamicType extends AbstractType
|
||||
{
|
||||
public function __construct(private readonly DenormalizerInterface $denormalizer, private readonly SerializerInterface $serializer, private readonly NormalizerInterface $normalizer) {}
|
||||
public function __construct(
|
||||
private readonly DenormalizerInterface $denormalizer,
|
||||
private readonly SerializerInterface $serializer,
|
||||
private readonly NormalizerInterface $normalizer,
|
||||
private readonly Security $security,
|
||||
) {}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
@@ -46,6 +60,12 @@ class PickUserDynamicType extends AbstractType
|
||||
foreach ($options['suggested'] as $user) {
|
||||
$view->vars['suggested'][] = $this->normalizer->normalize($user, 'json', ['groups' => 'read']);
|
||||
}
|
||||
$user = $this->security->getUser();
|
||||
if ($user instanceof User) {
|
||||
if (true === $options['suggest_myself'] && !in_array($user, $options['suggested'], true)) {
|
||||
$view->vars['suggested'][] = $this->normalizer->normalize($user, 'json', ['groups' => 'read']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
@@ -54,6 +74,8 @@ class PickUserDynamicType extends AbstractType
|
||||
->setDefault('multiple', false)
|
||||
->setAllowedTypes('multiple', ['bool'])
|
||||
->setDefault('compound', false)
|
||||
->setDefault('suggest_myself', false)
|
||||
->setAllowedTypes('suggest_myself', ['bool'])
|
||||
->setDefault('suggested', [])
|
||||
// if set to true, only the id will be set inside the content. The denormalization will not work.
|
||||
->setDefault('as_id', false)
|
||||
|
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\MainBundle\Form\Type;
|
||||
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Form\Type\DataTransformer\EntityToJsonTransformer;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Form\FormView;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
|
||||
/**
|
||||
* Entity which picks a user **or** a user group.
|
||||
*/
|
||||
final class PickUserGroupOrUserDynamicType extends AbstractType
|
||||
{
|
||||
public function __construct(
|
||||
private readonly DenormalizerInterface $denormalizer,
|
||||
private readonly SerializerInterface $serializer,
|
||||
private readonly NormalizerInterface $normalizer,
|
||||
private readonly Security $security,
|
||||
) {}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$builder->addViewTransformer(new EntityToJsonTransformer($this->denormalizer, $this->serializer, $options['multiple'], 'user_group_or_user'));
|
||||
}
|
||||
|
||||
public function buildView(FormView $view, FormInterface $form, array $options)
|
||||
{
|
||||
$view->vars['multiple'] = $options['multiple'];
|
||||
$view->vars['types'] = ['user-group', 'user'];
|
||||
$view->vars['uniqid'] = uniqid('pick_usergroup_dyn');
|
||||
$view->vars['suggested'] = [];
|
||||
$view->vars['as_id'] = true === $options['as_id'] ? '1' : '0';
|
||||
$view->vars['submit_on_adding_new_entity'] = true === $options['submit_on_adding_new_entity'] ? '1' : '0';
|
||||
|
||||
foreach ($options['suggested'] as $userGroup) {
|
||||
$view->vars['suggested'][] = $this->normalizer->normalize($userGroup, 'json', ['groups' => 'read']);
|
||||
}
|
||||
$user = $this->security->getUser();
|
||||
if ($user instanceof User) {
|
||||
if (true === $options['suggest_myself'] && !in_array($user, $options['suggested'], true)) {
|
||||
$view->vars['suggested'][] = $this->normalizer->normalize($user, 'json', ['groups' => 'read']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver
|
||||
->setDefault('multiple', false)
|
||||
->setAllowedTypes('multiple', ['bool'])
|
||||
->setDefault('compound', false)
|
||||
->setDefault('suggested', [])
|
||||
->setDefault('suggest_myself', false)
|
||||
->setAllowedTypes('suggest_myself', ['bool'])
|
||||
// if set to true, only the id will be set inside the content. The denormalization will not work.
|
||||
->setDefault('as_id', false)
|
||||
->setAllowedTypes('as_id', ['bool'])
|
||||
->setDefault('submit_on_adding_new_entity', false)
|
||||
->setAllowedTypes('submit_on_adding_new_entity', ['bool']);
|
||||
}
|
||||
|
||||
public function getBlockPrefix()
|
||||
{
|
||||
return 'pick_entity_dynamic';
|
||||
}
|
||||
}
|
65
src/Bundle/ChillMainBundle/Form/UserGroupType.php
Normal file
65
src/Bundle/ChillMainBundle/Form/UserGroupType.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\MainBundle\Form;
|
||||
|
||||
use Chill\MainBundle\Form\Type\PickUserDynamicType;
|
||||
use Chill\MainBundle\Form\Type\TranslatableStringFormType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ColorType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\EmailType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
class UserGroupType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$builder
|
||||
->add('label', TranslatableStringFormType::class, [
|
||||
'label' => 'user_group.Label',
|
||||
'required' => true,
|
||||
])
|
||||
->add('active')
|
||||
->add('backgroundColor', ColorType::class, [
|
||||
'label' => 'user_group.BackgroundColor',
|
||||
])
|
||||
->add('foregroundColor', ColorType::class, [
|
||||
'label' => 'user_group.ForegroundColor',
|
||||
])
|
||||
->add('email', EmailType::class, [
|
||||
'label' => 'user_group.Email',
|
||||
'help' => 'user_group.EmailHelp',
|
||||
'empty_data' => '',
|
||||
'required' => false,
|
||||
])
|
||||
->add('excludeKey', TextType::class, [
|
||||
'label' => 'user_group.ExcludeKey',
|
||||
'help' => 'user_group.ExcludeKeyHelp',
|
||||
'required' => false,
|
||||
'empty_data' => '',
|
||||
])
|
||||
->add('users', PickUserDynamicType::class, [
|
||||
'label' => 'user_group.Users',
|
||||
'multiple' => true,
|
||||
'required' => false,
|
||||
'empty_data' => [],
|
||||
])
|
||||
->add('adminUsers', PickUserDynamicType::class, [
|
||||
'label' => 'user_group.adminUsers',
|
||||
'multiple' => true,
|
||||
'required' => false,
|
||||
'empty_data' => [],
|
||||
'help' => 'user_group.adminUsersHelp',
|
||||
])
|
||||
;
|
||||
}
|
||||
}
|
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\MainBundle\Form;
|
||||
|
||||
use Chill\MainBundle\Form\Type\ChillDateType;
|
||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
class WorkflowSignatureMetadataType extends AbstractType
|
||||
{
|
||||
public function __construct(private readonly ParameterBagInterface $parameterBag, private readonly TranslatableStringHelperInterface $translatableStringHelper) {}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$documentTypeChoices = $this->parameterBag->get('chill_main')['workflow_signature']['base_signer']['document_kinds'];
|
||||
|
||||
$choices = [];
|
||||
|
||||
foreach ($documentTypeChoices as $documentType) {
|
||||
$labels = [];
|
||||
|
||||
foreach ($documentType['labels'] as $label) {
|
||||
$labels[$label['lang']] = $label['label'];
|
||||
}
|
||||
|
||||
$localizedLabel = $this->translatableStringHelper->localize($labels);
|
||||
if (null !== $localizedLabel) {
|
||||
$choices[$localizedLabel] = $documentType['key'];
|
||||
}
|
||||
}
|
||||
|
||||
$builder
|
||||
->add('documentType', ChoiceType::class, [
|
||||
'label' => 'workflow.signature_zone.metadata.docType',
|
||||
'expanded' => false,
|
||||
'required' => true,
|
||||
'choices' => $choices,
|
||||
])
|
||||
->add('documentNumber', TextType::class, [
|
||||
'required' => true,
|
||||
'label' => 'workflow.signature_zone.metadata.docNumber',
|
||||
])
|
||||
->add('expirationDate', ChillDateType::class, [
|
||||
'required' => true,
|
||||
'input' => 'datetime_immutable',
|
||||
'label' => 'workflow.signature_zone.metadata.docExpiration',
|
||||
]);
|
||||
}
|
||||
}
|
@@ -12,232 +12,209 @@ declare(strict_types=1);
|
||||
namespace Chill\MainBundle\Form;
|
||||
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStep;
|
||||
use Chill\MainBundle\Form\Type\ChillCollectionType;
|
||||
use Chill\MainBundle\Form\Type\ChillTextareaType;
|
||||
use Chill\MainBundle\Form\Type\PickUserDynamicType;
|
||||
use Chill\MainBundle\Form\Type\PickUserGroupOrUserDynamicType;
|
||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
||||
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
|
||||
use Chill\PersonBundle\Form\Type\PickPersonDynamicType;
|
||||
use Chill\ThirdPartyBundle\Form\Type\PickThirdpartyDynamicType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\EmailType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Component\Validator\Constraints\Callback;
|
||||
use Symfony\Component\Validator\Constraints\Email;
|
||||
use Symfony\Component\Validator\Constraints\NotBlank;
|
||||
use Symfony\Component\Validator\Constraints\NotNull;
|
||||
use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||
use Symfony\Component\Workflow\Registry;
|
||||
use Symfony\Component\Workflow\Transition;
|
||||
|
||||
class WorkflowStepType extends AbstractType
|
||||
{
|
||||
public function __construct(private readonly EntityWorkflowManager $entityWorkflowManager, private readonly Registry $registry, private readonly TranslatableStringHelperInterface $translatableStringHelper) {}
|
||||
public function __construct(
|
||||
private readonly Registry $registry,
|
||||
private readonly TranslatableStringHelperInterface $translatableStringHelper,
|
||||
private readonly EntityWorkflowManager $entityWorkflowManager,
|
||||
) {}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
/** @var EntityWorkflow $entityWorkflow */
|
||||
$entityWorkflow = $options['entity_workflow'];
|
||||
$handler = $this->entityWorkflowManager->getHandler($entityWorkflow);
|
||||
$workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
|
||||
$place = $workflow->getMarking($entityWorkflow);
|
||||
$placeMetadata = $workflow->getMetadataStore()->getPlaceMetadata(array_keys($place->getPlaces())[0]);
|
||||
$suggestedUsers = $this->entityWorkflowManager->getSuggestedUsers($entityWorkflow);
|
||||
$suggestedThirdParties = $this->entityWorkflowManager->getSuggestedThirdParties($entityWorkflow);
|
||||
$suggestedPersons = $this->entityWorkflowManager->getSuggestedPersons($entityWorkflow);
|
||||
|
||||
if (true === $options['transition']) {
|
||||
if (null === $options['entity_workflow']) {
|
||||
throw new \LogicException('if transition is true, entity_workflow should be defined');
|
||||
}
|
||||
if (null === $options['entity_workflow']) {
|
||||
throw new \LogicException('if transition is true, entity_workflow should be defined');
|
||||
}
|
||||
|
||||
$transitions = $this->registry
|
||||
->get($options['entity_workflow'], $entityWorkflow->getWorkflowName())
|
||||
->getEnabledTransitions($entityWorkflow);
|
||||
$transitions = $this->registry
|
||||
->get($options['entity_workflow'], $entityWorkflow->getWorkflowName())
|
||||
->getEnabledTransitions($entityWorkflow);
|
||||
|
||||
$choices = array_combine(
|
||||
array_map(
|
||||
static fn (Transition $transition) => $transition->getName(),
|
||||
$transitions
|
||||
),
|
||||
$choices = array_combine(
|
||||
array_map(
|
||||
static fn (Transition $transition) => $transition->getName(),
|
||||
$transitions
|
||||
);
|
||||
),
|
||||
$transitions
|
||||
);
|
||||
|
||||
if (\array_key_exists('validationFilterInputLabels', $placeMetadata)) {
|
||||
$inputLabels = $placeMetadata['validationFilterInputLabels'];
|
||||
if (\array_key_exists('validationFilterInputLabels', $placeMetadata)) {
|
||||
$inputLabels = $placeMetadata['validationFilterInputLabels'];
|
||||
|
||||
$builder->add('transitionFilter', ChoiceType::class, [
|
||||
'multiple' => false,
|
||||
'label' => 'workflow.My decision',
|
||||
'choices' => [
|
||||
'forward' => 'forward',
|
||||
'backward' => 'backward',
|
||||
'neutral' => 'neutral',
|
||||
],
|
||||
'choice_label' => fn (string $key) => $this->translatableStringHelper->localize($inputLabels[$key]),
|
||||
'choice_attr' => static fn (string $key) => [
|
||||
$key => $key,
|
||||
],
|
||||
'mapped' => false,
|
||||
'expanded' => true,
|
||||
'data' => 'forward',
|
||||
]);
|
||||
}
|
||||
|
||||
$builder
|
||||
->add('transition', ChoiceType::class, [
|
||||
'label' => 'workflow.Next step',
|
||||
'mapped' => false,
|
||||
'multiple' => false,
|
||||
'expanded' => true,
|
||||
'choices' => $choices,
|
||||
'constraints' => [new NotNull()],
|
||||
'choice_label' => function (Transition $transition) use ($workflow) {
|
||||
$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;
|
||||
$isForward = 'neutral';
|
||||
|
||||
$metadata = $workflow->getMetadataStore()->getTransitionMetadata($transition);
|
||||
|
||||
if (\array_key_exists('isForward', $metadata)) {
|
||||
if ($metadata['isForward']) {
|
||||
$isForward = 'forward';
|
||||
} else {
|
||||
$isForward = 'backward';
|
||||
}
|
||||
}
|
||||
|
||||
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',
|
||||
'data-is-forward' => $isForward,
|
||||
];
|
||||
},
|
||||
])
|
||||
->add('future_dest_users', PickUserDynamicType::class, [
|
||||
'label' => 'workflow.dest for next steps',
|
||||
'multiple' => true,
|
||||
'mapped' => false,
|
||||
'suggested' => $options['suggested_users'],
|
||||
])
|
||||
->add('future_cc_users', PickUserDynamicType::class, [
|
||||
'label' => 'workflow.cc for next steps',
|
||||
'multiple' => true,
|
||||
'mapped' => false,
|
||||
'required' => false,
|
||||
'suggested' => $options['suggested_users'],
|
||||
])
|
||||
->add('future_dest_emails', ChillCollectionType::class, [
|
||||
'label' => 'workflow.dest by email',
|
||||
'help' => 'workflow.dest by email help',
|
||||
'mapped' => false,
|
||||
'allow_add' => true,
|
||||
'entry_type' => EmailType::class,
|
||||
'button_add_label' => 'workflow.Add an email',
|
||||
'button_remove_label' => 'workflow.Remove an email',
|
||||
'empty_collection_explain' => 'workflow.Any email',
|
||||
'entry_options' => [
|
||||
'constraints' => [
|
||||
new NotNull(), new NotBlank(), new Email(),
|
||||
],
|
||||
'label' => 'Email',
|
||||
],
|
||||
]);
|
||||
$builder->add('transitionFilter', ChoiceType::class, [
|
||||
'multiple' => false,
|
||||
'label' => 'workflow.My decision',
|
||||
'choices' => [
|
||||
'forward' => 'forward',
|
||||
'backward' => 'backward',
|
||||
'neutral' => 'neutral',
|
||||
],
|
||||
'choice_label' => fn (string $key) => $this->translatableStringHelper->localize($inputLabels[$key]),
|
||||
'choice_attr' => static fn (string $key) => [
|
||||
$key => $key,
|
||||
],
|
||||
'mapped' => false,
|
||||
'expanded' => true,
|
||||
'data' => 'forward',
|
||||
]);
|
||||
}
|
||||
|
||||
if (
|
||||
$handler->supportsFreeze($entityWorkflow)
|
||||
&& !$entityWorkflow->isFreeze()
|
||||
) {
|
||||
$builder
|
||||
->add('freezeAfter', CheckboxType::class, [
|
||||
'required' => false,
|
||||
'label' => 'workflow.Freeze',
|
||||
'help' => 'workflow.The associated element will be freezed',
|
||||
]);
|
||||
}
|
||||
$builder
|
||||
->add('transition', ChoiceType::class, [
|
||||
'label' => 'workflow.Next step',
|
||||
'multiple' => false,
|
||||
'expanded' => true,
|
||||
'choices' => $choices,
|
||||
'constraints' => [new NotNull()],
|
||||
'choice_label' => function (Transition $transition) use ($workflow) {
|
||||
$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;
|
||||
$isForward = 'neutral';
|
||||
$isSignature = [];
|
||||
$isSentExternal = false;
|
||||
|
||||
$metadata = $workflow->getMetadataStore()->getTransitionMetadata($transition);
|
||||
|
||||
if (\array_key_exists('isForward', $metadata)) {
|
||||
if ($metadata['isForward']) {
|
||||
$isForward = 'forward';
|
||||
} else {
|
||||
$isForward = 'backward';
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($transition->getTos() as $to) {
|
||||
$meta = $workflow->getMetadataStore()->getPlaceMetadata($to);
|
||||
|
||||
if (
|
||||
!\array_key_exists('isFinal', $meta) || false === $meta['isFinal']
|
||||
) {
|
||||
$toFinal = false;
|
||||
}
|
||||
|
||||
if (\array_key_exists('isSignature', $meta)) {
|
||||
$isSignature = $meta['isSignature'];
|
||||
}
|
||||
|
||||
$isSentExternal = $isSentExternal ? true : $meta['isSentExternal'] ?? false;
|
||||
}
|
||||
|
||||
return [
|
||||
'data-is-transition' => 'data-is-transition',
|
||||
'data-to-final' => $toFinal ? '1' : '0',
|
||||
'data-is-forward' => $isForward,
|
||||
'data-is-signature' => json_encode($isSignature),
|
||||
'data-is-sent-external' => $isSentExternal ? '1' : '0',
|
||||
];
|
||||
},
|
||||
])
|
||||
->add('isPersonOrUserSignature', ChoiceType::class, [
|
||||
'mapped' => false,
|
||||
'multiple' => false,
|
||||
'expanded' => true,
|
||||
'label' => 'workflow.signature_zone.type of signature',
|
||||
'choices' => [
|
||||
'workflow.signature_zone.persons' => 'person',
|
||||
'workflow.signature_zone.user' => 'user',
|
||||
],
|
||||
])
|
||||
->add('futurePersonSignatures', PickPersonDynamicType::class, [
|
||||
'label' => 'workflow.signature_zone.person signatures',
|
||||
'multiple' => true,
|
||||
'empty_data' => '[]',
|
||||
'suggested' => $suggestedPersons,
|
||||
])
|
||||
->add('futureUserSignature', PickUserDynamicType::class, [
|
||||
'label' => 'workflow.signature_zone.user signature',
|
||||
'multiple' => false,
|
||||
'suggest_myself' => true,
|
||||
'suggested' => $suggestedUsers,
|
||||
])
|
||||
->add('futureDestUsers', PickUserGroupOrUserDynamicType::class, [
|
||||
'label' => 'workflow.dest for next steps',
|
||||
'multiple' => true,
|
||||
'empty_data' => '[]',
|
||||
'suggested' => $suggestedUsers,
|
||||
'suggest_myself' => true,
|
||||
])
|
||||
->add('futureCcUsers', PickUserDynamicType::class, [
|
||||
'label' => 'workflow.cc for next steps',
|
||||
'multiple' => true,
|
||||
'required' => false,
|
||||
'suggested' => $suggestedUsers,
|
||||
'empty_data' => '[]',
|
||||
'attr' => ['class' => 'future-cc-users'],
|
||||
'suggest_myself' => true,
|
||||
])
|
||||
->add('futureDestineeEmails', ChillCollectionType::class, [
|
||||
'entry_type' => EmailType::class,
|
||||
'entry_options' => [
|
||||
'empty_data' => '',
|
||||
],
|
||||
'allow_add' => true,
|
||||
'allow_delete' => true,
|
||||
'delete_empty' => static fn (?string $email) => '' === $email || null === $email,
|
||||
'button_add_label' => 'workflow.transition_destinee_add_emails',
|
||||
'button_remove_label' => 'workflow.transition_destinee_remove_emails',
|
||||
'help' => 'workflow.transition_destinee_emails_help',
|
||||
'label' => 'workflow.transition_destinee_emails_label',
|
||||
])
|
||||
->add('futureDestineeThirdParties', PickThirdpartyDynamicType::class, [
|
||||
'label' => 'workflow.transition_destinee_third_party',
|
||||
'help' => 'workflow.transition_destinee_third_party_help',
|
||||
'multiple' => true,
|
||||
'empty_data' => [],
|
||||
'suggested' => $suggestedThirdParties,
|
||||
]);
|
||||
|
||||
$builder
|
||||
->add('comment', ChillTextareaType::class, [
|
||||
'required' => false,
|
||||
'label' => 'Comment',
|
||||
'empty_data' => '',
|
||||
]);
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver
|
||||
->setDefined('class')
|
||||
->setRequired('transition')
|
||||
->setAllowedTypes('transition', 'bool')
|
||||
->setDefault('data_class', WorkflowTransitionContextDTO::class)
|
||||
->setRequired('entity_workflow')
|
||||
->setAllowedTypes('entity_workflow', EntityWorkflow::class)
|
||||
->setDefault('suggested_users', [])
|
||||
->setDefault('constraints', [
|
||||
new Callback(
|
||||
function ($step, ExecutionContextInterface $context, $payload) {
|
||||
/** @var EntityWorkflowStep $step */
|
||||
$form = $context->getObject();
|
||||
$workflow = $this->registry->get($step->getEntityWorkflow(), $step->getEntityWorkflow()->getWorkflowName());
|
||||
$transition = $form['transition']->getData();
|
||||
$toFinal = true;
|
||||
|
||||
if (null === $transition) {
|
||||
$context
|
||||
->buildViolation('workflow.You must select a next step, pick another decision if no next steps are available');
|
||||
} else {
|
||||
foreach ($transition->getTos() as $to) {
|
||||
$meta = $workflow->getMetadataStore()->getPlaceMetadata($to);
|
||||
|
||||
if (
|
||||
!\array_key_exists('isFinal', $meta) || false === $meta['isFinal']
|
||||
) {
|
||||
$toFinal = false;
|
||||
}
|
||||
}
|
||||
$destUsers = $form['future_dest_users']->getData();
|
||||
$destEmails = $form['future_dest_emails']->getData();
|
||||
|
||||
if (!$toFinal && [] === $destUsers && [] === $destEmails) {
|
||||
$context
|
||||
->buildViolation('workflow.You must add at least one dest user or email')
|
||||
->atPath('future_dest_users')
|
||||
->addViolation();
|
||||
}
|
||||
}
|
||||
}
|
||||
),
|
||||
new Callback(
|
||||
function ($step, ExecutionContextInterface $context, $payload) {
|
||||
$form = $context->getObject();
|
||||
|
||||
foreach ($form->get('future_dest_users')->getData() as $u) {
|
||||
if (in_array($u, $form->get('future_cc_users')->getData(), true)) {
|
||||
$context
|
||||
->buildViolation('workflow.The user in cc cannot be a dest user in the same workflow step')
|
||||
->atPath('ccUsers')
|
||||
->addViolation();
|
||||
}
|
||||
}
|
||||
}
|
||||
),
|
||||
]);
|
||||
->setAllowedTypes('entity_workflow', EntityWorkflow::class);
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\MainBundle\Repository;
|
||||
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSendView;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Doctrine\Persistence\ObjectRepository;
|
||||
|
||||
/**
|
||||
* @template-implements ObjectRepository<EntityWorkflowSendView>
|
||||
*/
|
||||
class EntityWorkflowSendViewRepository implements ObjectRepository
|
||||
{
|
||||
private readonly ObjectRepository $repository;
|
||||
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
$this->repository = $registry->getRepository($this->getClassName());
|
||||
}
|
||||
|
||||
public function find($id): ?EntityWorkflowSendView
|
||||
{
|
||||
return $this->repository->find($id);
|
||||
}
|
||||
|
||||
public function findAll()
|
||||
{
|
||||
return $this->repository->findAll();
|
||||
}
|
||||
|
||||
public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array
|
||||
{
|
||||
return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
|
||||
}
|
||||
|
||||
public function findOneBy(array $criteria): ?EntityWorkflowSendView
|
||||
{
|
||||
return $this->repository->findOneBy($criteria);
|
||||
}
|
||||
|
||||
public function getClassName()
|
||||
{
|
||||
return EntityWorkflowSendView::class;
|
||||
}
|
||||
}
|
133
src/Bundle/ChillMainBundle/Repository/UserGroupRepository.php
Normal file
133
src/Bundle/ChillMainBundle/Repository/UserGroupRepository.php
Normal file
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\MainBundle\Repository;
|
||||
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Entity\UserGroup;
|
||||
use Chill\MainBundle\Search\SearchApiQuery;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Symfony\Contracts\Translation\LocaleAwareInterface;
|
||||
|
||||
final class UserGroupRepository implements UserGroupRepositoryInterface, LocaleAwareInterface
|
||||
{
|
||||
private readonly EntityRepository $repository;
|
||||
|
||||
private string $locale;
|
||||
|
||||
public function __construct(EntityManagerInterface $em)
|
||||
{
|
||||
$this->repository = $em->getRepository(UserGroup::class);
|
||||
}
|
||||
|
||||
public function setLocale(string $locale): void
|
||||
{
|
||||
$this->locale = $locale;
|
||||
}
|
||||
|
||||
public function getLocale(): string
|
||||
{
|
||||
return $this->locale;
|
||||
}
|
||||
|
||||
public function find($id): ?UserGroup
|
||||
{
|
||||
return $this->repository->find($id);
|
||||
}
|
||||
|
||||
public function findAll(): array
|
||||
{
|
||||
return $this->repository->findAll();
|
||||
}
|
||||
|
||||
public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array
|
||||
{
|
||||
return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
|
||||
}
|
||||
|
||||
public function findOneBy(array $criteria): ?UserGroup
|
||||
{
|
||||
return $this->repository->findOneBy($criteria);
|
||||
}
|
||||
|
||||
public function getClassName(): string
|
||||
{
|
||||
return UserGroup::class;
|
||||
}
|
||||
|
||||
public function provideSearchApiQuery(string $pattern, string $lang, string $selectKey = 'user-group'): SearchApiQuery
|
||||
{
|
||||
$query = new SearchApiQuery();
|
||||
$query
|
||||
->setSelectKey($selectKey)
|
||||
->setSelectJsonbMetadata("jsonb_build_object('id', ug.id)")
|
||||
->setSelectPertinence('3 + SIMILARITY(LOWER(UNACCENT(?)), ug.label->>?) + CASE WHEN (EXISTS(SELECT 1 FROM unnest(string_to_array(label->>?, \' \')) AS t WHERE LOWER(t) LIKE \'%\' || LOWER(UNACCENT(?)) || \'%\')) THEN 100 ELSE 0 END', [$pattern, $lang, $lang, $pattern])
|
||||
->setFromClause('chill_main_user_group AS ug')
|
||||
->setWhereClauses('
|
||||
ug.active AND (
|
||||
SIMILARITY(LOWER(UNACCENT(?)), ug.label->>?) > 0.15
|
||||
OR ug.label->>? LIKE \'%\' || LOWER(UNACCENT(?)) || \'%\')
|
||||
', [$pattern, $lang, $pattern, $lang]);
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
public function findByUser(User $user, bool $onlyActive = true, ?int $limit = null, ?int $offset = null): array
|
||||
{
|
||||
$qb = $this->buildQueryByUser($user, $onlyActive);
|
||||
|
||||
if (null !== $limit) {
|
||||
$qb->setMaxResults($limit);
|
||||
}
|
||||
|
||||
if (null !== $offset) {
|
||||
$qb->setFirstResult($offset);
|
||||
}
|
||||
|
||||
// ordering thing
|
||||
$qb->addSelect('JSON_EXTRACT(ug.label, :lang) AS HIDDEN label_ordering')
|
||||
->addOrderBy('label_ordering', 'ASC')
|
||||
->setParameter('lang', $this->getLocale());
|
||||
|
||||
return $qb->getQuery()->getResult();
|
||||
}
|
||||
|
||||
public function countByUser(User $user, bool $onlyActive = true): int
|
||||
{
|
||||
$qb = $this->buildQueryByUser($user, $onlyActive);
|
||||
$qb->select('count(ug)');
|
||||
|
||||
return $qb->getQuery()->getSingleScalarResult();
|
||||
}
|
||||
|
||||
private function buildQueryByUser(User $user, bool $onlyActive): \Doctrine\ORM\QueryBuilder
|
||||
{
|
||||
$qb = $this->repository->createQueryBuilder('ug');
|
||||
$qb->where(
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->isMemberOf(':user', 'ug.users'),
|
||||
$qb->expr()->isMemberOf(':user', 'ug.adminUsers')
|
||||
)
|
||||
);
|
||||
|
||||
$qb->setParameter('user', $user);
|
||||
|
||||
if ($onlyActive) {
|
||||
$qb->andWhere(
|
||||
$qb->expr()->eq('ug.active', ':active')
|
||||
);
|
||||
$qb->setParameter('active', true);
|
||||
}
|
||||
|
||||
return $qb;
|
||||
}
|
||||
}
|
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\MainBundle\Repository;
|
||||
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Entity\UserGroup;
|
||||
use Chill\MainBundle\Search\SearchApiQuery;
|
||||
use Doctrine\Persistence\ObjectRepository;
|
||||
|
||||
/**
|
||||
* @template-extends ObjectRepository<UserGroup>
|
||||
*/
|
||||
interface UserGroupRepositoryInterface extends ObjectRepository
|
||||
{
|
||||
/**
|
||||
* Provide a SearchApiQuery for searching amongst user groups.
|
||||
*/
|
||||
public function provideSearchApiQuery(string $pattern, string $lang, string $selectKey = 'user-group'): SearchApiQuery;
|
||||
|
||||
public function findByUser(User $user, bool $onlyActive = true, ?int $limit = null, ?int $offset = null): array;
|
||||
|
||||
public function countByUser(User $user, bool $onlyActive = true): int;
|
||||
}
|
@@ -261,7 +261,7 @@ final readonly class UserRepository implements UserRepositoryInterface
|
||||
* allows to make a first search amongst users based on role and center
|
||||
* and, then filter those users having some flags.
|
||||
*
|
||||
* @param \Chill\MainBundle\Entity\User[] $amongstUsers
|
||||
* @param User[] $amongstUsers
|
||||
*/
|
||||
public function findUsersHavingFlags($flag, array $amongstUsers = []): array
|
||||
{
|
||||
|
@@ -71,7 +71,7 @@ interface UserRepositoryInterface extends ObjectRepository
|
||||
* allows to make a first search amongst users based on role and center
|
||||
* and, then filter those users having some flags.
|
||||
*
|
||||
* @param \Chill\MainBundle\Entity\User[] $amongstUsers
|
||||
* @param User[] $amongstUsers
|
||||
*/
|
||||
public function findUsersHavingFlags(mixed $flag, array $amongstUsers = []): array;
|
||||
}
|
||||
|
@@ -12,7 +12,9 @@ declare(strict_types=1);
|
||||
namespace Chill\MainBundle\Repository\Workflow;
|
||||
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Entity\UserGroup;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStep;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
@@ -99,6 +101,24 @@ class EntityWorkflowRepository implements ObjectRepository
|
||||
return $this->repository->findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<EntityWorkflow>
|
||||
*/
|
||||
public function findByRelatedEntity($entityClass, $relatedEntityId): array
|
||||
{
|
||||
$qb = $this->repository->createQueryBuilder('w');
|
||||
|
||||
$query = $qb->where(
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->eq('w.relatedEntityClass', ':entity_class'),
|
||||
$qb->expr()->eq('w.relatedEntityId', ':entity_id'),
|
||||
)
|
||||
)->setParameter('entity_class', $entityClass)
|
||||
->setParameter('entity_id', $relatedEntityId);
|
||||
|
||||
return $query->getQuery()->getResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed|null $limit
|
||||
* @param mixed|null $offset
|
||||
@@ -180,6 +200,36 @@ class EntityWorkflowRepository implements ObjectRepository
|
||||
return $this->repository->findOneBy($criteria);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds workflows that are not finalized and are older than the specified date.
|
||||
*
|
||||
* @param \DateTimeImmutable $olderThanDate the date to compare against
|
||||
*
|
||||
* @return iterable<EntityWorkflow>
|
||||
*/
|
||||
public function findWorkflowsWithoutFinalStepAndOlderThan(\DateTimeImmutable $olderThanDate): iterable
|
||||
{
|
||||
$qb = $this->repository->createQueryBuilder('sw');
|
||||
|
||||
$qb->select('sw')
|
||||
// only the workflow which are not finalized
|
||||
->where(sprintf('NOT EXISTS (SELECT 1 FROM %s ews WHERE ews.isFinal = TRUE AND ews.entityWorkflow = sw.id)', EntityWorkflowStep::class))
|
||||
->andWhere(
|
||||
$qb->expr()->orX(
|
||||
// only the workflow where all the transitions are older than transitionAt
|
||||
sprintf(':olderThanDate > ALL (SELECT ews2.transitionAt FROM %s ews2 WHERE ews2.transitionAt IS NOT NULL AND ews2.entityWorkflow = sw)', EntityWorkflowStep::class),
|
||||
// or the workflow which have only the initial step, with no transition
|
||||
sprintf('1 = (SELECT COUNT(ews3.id) FROM %s ews3 WHERE ews3.currentStep = :initial AND ews3.transitionAt IS NULL AND ews3.entityWorkflow = sw)', EntityWorkflowStep::class),
|
||||
)
|
||||
)
|
||||
->andWhere('sw.createdAt < :olderThanDate')
|
||||
->setParameter('olderThanDate', $olderThanDate)
|
||||
->setParameter('initial', 'initial')
|
||||
;
|
||||
|
||||
return $qb->getQuery()->toIterable();
|
||||
}
|
||||
|
||||
public function getClassName(): string
|
||||
{
|
||||
return EntityWorkflow::class;
|
||||
@@ -212,7 +262,11 @@ class EntityWorkflowRepository implements ObjectRepository
|
||||
|
||||
$qb->where(
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->isMemberOf(':user', 'step.destUser'),
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->isMemberOf(':user', 'step.destUser'),
|
||||
$qb->expr()->isMemberOf(':user', 'step.destUserByAccessKey'),
|
||||
$qb->expr()->exists(sprintf('SELECT 1 FROM %s ug WHERE ug MEMBER OF step.destUserGroups AND :user MEMBER OF ug.users', UserGroup::class))
|
||||
),
|
||||
$qb->expr()->isNull('step.transitionAfter'),
|
||||
$qb->expr()->eq('step.isFinal', "'FALSE'")
|
||||
)
|
||||
|
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\MainBundle\Repository\Workflow;
|
||||
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
|
||||
interface EntityWorkflowSignatureACLAwareRepositoryInterface
|
||||
{
|
||||
/**
|
||||
* @return array<EntityWorkflowStepSignature>
|
||||
*/
|
||||
public function findByPersonAndPendingState(Person $person): array;
|
||||
}
|
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\MainBundle\Repository\Workflow;
|
||||
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStep;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepHold;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\ORM\NonUniqueResultException;
|
||||
use Doctrine\ORM\NoResultException;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @template-extends ServiceEntityRepository<EntityWorkflowStepHold>
|
||||
*/
|
||||
class EntityWorkflowStepHoldRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, EntityWorkflowStepHold::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find an EntityWorkflowStepHold by its ID.
|
||||
*/
|
||||
public function findById(int $id): ?EntityWorkflowStepHold
|
||||
{
|
||||
return $this->find($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all EntityWorkflowStepHold records.
|
||||
*
|
||||
* @return EntityWorkflowStepHold[]
|
||||
*/
|
||||
public function findAllHolds(): array
|
||||
{
|
||||
return $this->findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Find EntityWorkflowStepHold by a specific step.
|
||||
*
|
||||
* @return EntityWorkflowStepHold[]
|
||||
*/
|
||||
public function findByStep(EntityWorkflowStep $step): array
|
||||
{
|
||||
return $this->findBy(['step' => $step]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a single EntityWorkflowStepHold by step and user.
|
||||
*
|
||||
* @throws NonUniqueResultException
|
||||
*/
|
||||
public function findOneByStepAndUser(EntityWorkflowStep $step, User $user): ?EntityWorkflowStepHold
|
||||
{
|
||||
try {
|
||||
return $this->createQueryBuilder('e')
|
||||
->andWhere('e.step = :step')
|
||||
->andWhere('e.byUser = :user')
|
||||
->setParameter('step', $step)
|
||||
->setParameter('user', $user)
|
||||
->getQuery()
|
||||
->getSingleResult();
|
||||
} catch (NoResultException) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
@@ -12,15 +12,16 @@ declare(strict_types=1);
|
||||
namespace Chill\MainBundle\Repository\Workflow;
|
||||
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Entity\UserGroup;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStep;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Doctrine\Persistence\ObjectRepository;
|
||||
|
||||
class EntityWorkflowStepRepository implements ObjectRepository
|
||||
readonly class EntityWorkflowStepRepository implements ObjectRepository
|
||||
{
|
||||
private readonly EntityRepository $repository;
|
||||
private EntityRepository $repository;
|
||||
|
||||
public function __construct(EntityManagerInterface $entityManager)
|
||||
{
|
||||
@@ -65,7 +66,11 @@ class EntityWorkflowStepRepository implements ObjectRepository
|
||||
|
||||
$qb->where(
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->isMemberOf(':user', 'e.destUser'),
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->isMemberOf(':user', 'e.destUser'),
|
||||
$qb->expr()->isMemberOf(':user', 'e.destUserByAccessKey'),
|
||||
$qb->expr()->exists(sprintf('SELECT 1 FROM %s ug WHERE ug MEMBER OF e.destUserGroups AND :user MEMBER OF ug.users', UserGroup::class))
|
||||
),
|
||||
$qb->expr()->isNull('e.transitionAt'),
|
||||
$qb->expr()->eq('e.isFinal', ':bool'),
|
||||
)
|
||||
|
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\MainBundle\Repository\Workflow;
|
||||
|
||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum;
|
||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
|
||||
class EntityWorkflowStepSignatureACLAwareRepository implements EntityWorkflowSignatureACLAwareRepositoryInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly EntityWorkflowStepSignatureRepository $repository,
|
||||
private readonly EntityWorkflowManager $entityWorkflowManager,
|
||||
private readonly Security $security,
|
||||
) {}
|
||||
|
||||
public function findByPersonAndPendingState(Person $person): array
|
||||
{
|
||||
$signatures = $this->repository->findByPerson($person);
|
||||
|
||||
$filteredSignatures = [];
|
||||
|
||||
foreach ($signatures as $signature) {
|
||||
if (EntityWorkflowSignatureStateEnum::SIGNED === $signature->getState() || EntityWorkflowSignatureStateEnum::CANCELED === $signature->getState() || EntityWorkflowSignatureStateEnum::REJECTED === $signature->getState()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$workflow = $signature->getStep()->getEntityWorkflow();
|
||||
|
||||
$storedObject = $this->entityWorkflowManager->getAssociatedStoredObject($workflow);
|
||||
|
||||
if (null === $storedObject) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject)) {
|
||||
$filteredSignatures[] = $signature;
|
||||
}
|
||||
}
|
||||
|
||||
return $filteredSignatures;
|
||||
}
|
||||
}
|
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\MainBundle\Repository\Workflow;
|
||||
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @template-extends ServiceEntityRepository<EntityWorkflowStepSignature>
|
||||
*/
|
||||
class EntityWorkflowStepSignatureRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, EntityWorkflowStepSignature::class);
|
||||
}
|
||||
|
||||
public function findByPerson(Person $person): array
|
||||
{
|
||||
$qb = $this->createQueryBuilder('ewss');
|
||||
$qb->select('ewss');
|
||||
$qb->where($qb->expr()->eq('ewss.personSigner', ':person'));
|
||||
|
||||
$qb->setParameter('person', $person);
|
||||
|
||||
return $qb->getQuery()->getResult();
|
||||
}
|
||||
}
|
@@ -31,6 +31,8 @@
|
||||
// Specific templates
|
||||
@import './scss/notification';
|
||||
|
||||
@import './scss/hover.scss';
|
||||
|
||||
/*
|
||||
* BASE LAYOUT POSITION
|
||||
*/
|
||||
@@ -496,6 +498,7 @@ div.workflow {
|
||||
div.breadcrumb {
|
||||
display: initial;
|
||||
margin-bottom: 0;
|
||||
margin-right: .5rem;
|
||||
padding-right: 0.5em;
|
||||
background-color: tint-color($chill-yellow, 90%);
|
||||
border: 1px solid $chill-yellow;
|
||||
|
@@ -233,7 +233,7 @@ div.wrap-header {
|
||||
}
|
||||
&:last-child {}
|
||||
|
||||
div.wh-col {
|
||||
& > div.wh-col {
|
||||
&:first-child {
|
||||
flex-grow: 0; flex-shrink: 1; flex-basis: auto;
|
||||
}
|
||||
|
@@ -0,0 +1,11 @@
|
||||
|
||||
|
||||
.row.row-hover {
|
||||
padding: 0.3rem;
|
||||
|
||||
&:hover {
|
||||
background-color: $gray-100;
|
||||
border-top: 1px solid $gray-400;
|
||||
border-bottom: 1px solid $gray-400;
|
||||
}
|
||||
}
|
@@ -17,6 +17,10 @@ ul.record_actions {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
&.slim {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&.column {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
@@ -1,14 +0,0 @@
|
||||
const buildLinkCreate = function (
|
||||
workflowName,
|
||||
relatedEntityClass,
|
||||
relatedEntityId,
|
||||
) {
|
||||
let params = new URLSearchParams();
|
||||
params.set("entityClass", relatedEntityClass);
|
||||
params.set("entityId", relatedEntityId);
|
||||
params.set("workflow", workflowName);
|
||||
|
||||
return `/fr/main/workflow/create?` + params.toString();
|
||||
};
|
||||
|
||||
export { buildLinkCreate };
|
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Constructs a URL for creating a workflow link associated with a specific entity.
|
||||
*
|
||||
* @param {string} workflowName - The name of the workflow to associate.
|
||||
* @param {string} relatedEntityClass - The class/type of the related entity.
|
||||
* @param {number|undefined} relatedEntityId - The unique identifier of the related entity. Must be defined.
|
||||
*
|
||||
* @returns {string} The constructed URL containing query parameters based on the provided arguments.
|
||||
*
|
||||
* @throws {Error} If the related entity ID is undefined.
|
||||
*/
|
||||
export const buildLinkCreate = (workflowName: string, relatedEntityClass: string, relatedEntityId: number|undefined): string => {
|
||||
if (typeof (relatedEntityId) === 'undefined') {
|
||||
throw new Error("the related entity id is not set");
|
||||
}
|
||||
let params = new URLSearchParams();
|
||||
params.set('entityClass', relatedEntityClass);
|
||||
params.set('entityId', relatedEntityId.toString(10));
|
||||
params.set('workflow', workflowName);
|
||||
|
||||
return `/fr/main/workflow/create?`+params.toString();
|
||||
};
|
@@ -28,7 +28,13 @@
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
import "./collection.scss";
|
||||
import './collection.scss';
|
||||
|
||||
declare global {
|
||||
interface GlobalEventHandlersEventMap {
|
||||
'show-hide-show': CustomEvent<{id: number, froms: HTMLElement[], container: HTMLElement}>,
|
||||
}
|
||||
}
|
||||
|
||||
export class CollectionEventPayload {
|
||||
collection: HTMLUListElement;
|
||||
@@ -41,31 +47,27 @@ export class CollectionEventPayload {
|
||||
}
|
||||
|
||||
export const handleAdd = (button: any): void => {
|
||||
const form_name = button.dataset.collectionAddTarget,
|
||||
let
|
||||
form_name = button.dataset.collectionAddTarget,
|
||||
prototype = button.dataset.formPrototype,
|
||||
collection: HTMLUListElement | null = document.querySelector(
|
||||
'ul[data-collection-name="' + form_name + '"]',
|
||||
);
|
||||
collection: HTMLUListElement | null = document.querySelector('ul[data-collection-name="' + form_name + '"]');
|
||||
|
||||
if (collection === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const empty_explain: HTMLLIElement | null = collection.querySelector(
|
||||
"li[data-collection-empty-explain]",
|
||||
),
|
||||
entry = document.createElement("li"),
|
||||
counter = collection.querySelectorAll("li.entry").length, // Updated counter logic
|
||||
let
|
||||
empty_explain: HTMLLIElement | null = collection.querySelector('li[data-collection-empty-explain]'),
|
||||
entry = document.createElement('li'),
|
||||
counter = collection.querySelectorAll('li.entry').length, // Updated counter logic
|
||||
content = prototype.replace(/__name__/g, counter.toString()),
|
||||
event = new CustomEvent("collection-add-entry", {
|
||||
detail: new CollectionEventPayload(collection, entry),
|
||||
});
|
||||
event = new CustomEvent('collection-add-entry', {detail: new CollectionEventPayload(collection, entry)});
|
||||
|
||||
console.log(counter);
|
||||
console.log(content);
|
||||
console.log(counter)
|
||||
console.log(content)
|
||||
|
||||
entry.innerHTML = content;
|
||||
entry.classList.add("entry");
|
||||
entry.classList.add('entry');
|
||||
|
||||
if ("collectionRegular" in collection.dataset) {
|
||||
initializeRemove(collection, entry);
|
||||
@@ -79,10 +81,7 @@ export const handleAdd = (button: any): void => {
|
||||
window.dispatchEvent(event);
|
||||
};
|
||||
|
||||
const initializeRemove = (
|
||||
collection: HTMLUListElement,
|
||||
entry: HTMLLIElement,
|
||||
): void => {
|
||||
const initializeRemove = (collection: HTMLUListElement, entry: HTMLLIElement): void => {
|
||||
const button = buildRemoveButton(collection, entry);
|
||||
if (null === button) {
|
||||
return;
|
||||
@@ -90,24 +89,21 @@ const initializeRemove = (
|
||||
entry.appendChild(button);
|
||||
};
|
||||
|
||||
export const buildRemoveButton = (
|
||||
collection: HTMLUListElement,
|
||||
entry: HTMLLIElement,
|
||||
): HTMLButtonElement | null => {
|
||||
const button = document.createElement("button"),
|
||||
isPersisted = entry.dataset.collectionIsPersisted || "",
|
||||
content = collection.dataset.collectionButtonRemoveLabel || "",
|
||||
allowDelete = collection.dataset.collectionAllowDelete || "",
|
||||
event = new CustomEvent("collection-remove-entry", {
|
||||
detail: new CollectionEventPayload(collection, entry),
|
||||
});
|
||||
export const buildRemoveButton = (collection: HTMLUListElement, entry: HTMLLIElement): HTMLButtonElement|null => {
|
||||
|
||||
if (allowDelete === "0" && isPersisted === "1") {
|
||||
let
|
||||
button = document.createElement('button'),
|
||||
isPersisted = entry.dataset.collectionIsPersisted || '',
|
||||
content = collection.dataset.collectionButtonRemoveLabel || '',
|
||||
allowDelete = collection.dataset.collectionAllowDelete || '',
|
||||
event = new CustomEvent('collection-remove-entry', {detail: new CollectionEventPayload(collection, entry)});
|
||||
|
||||
if (allowDelete === '0' && isPersisted === '1') {
|
||||
return null;
|
||||
}
|
||||
button.classList.add("btn", "btn-delete", "remove-entry");
|
||||
button.classList.add('btn', 'btn-delete', 'remove-entry');
|
||||
button.textContent = content;
|
||||
button.addEventListener("click", (e: Event) => {
|
||||
button.addEventListener('click', (e: Event) => {
|
||||
e.preventDefault();
|
||||
entry.remove();
|
||||
collection.dispatchEvent(event);
|
||||
@@ -115,26 +111,36 @@ export const buildRemoveButton = (
|
||||
});
|
||||
|
||||
return button;
|
||||
};
|
||||
}
|
||||
|
||||
window.addEventListener("load", () => {
|
||||
const addButtons: NodeListOf<HTMLButtonElement> = document.querySelectorAll(
|
||||
"button[data-collection-add-target]",
|
||||
),
|
||||
collections: NodeListOf<HTMLUListElement> = document.querySelectorAll(
|
||||
"ul[data-collection-regular]",
|
||||
);
|
||||
const collectionsInit = new Set<string>;
|
||||
const buttonsInit = new Set<string>();
|
||||
|
||||
const initialize = function (target: Document|Element): void {
|
||||
let
|
||||
addButtons: NodeListOf<HTMLButtonElement> = document.querySelectorAll("button[data-collection-add-target]"),
|
||||
collections: NodeListOf<HTMLUListElement> = document.querySelectorAll("ul[data-collection-regular]");
|
||||
|
||||
for (let i = 0; i < addButtons.length; i++) {
|
||||
const addButton = addButtons[i];
|
||||
addButton.addEventListener("click", (e: Event) => {
|
||||
const uniqid = addButton.dataset.uniqid as string;
|
||||
if (buttonsInit.has(uniqid)) {
|
||||
continue;
|
||||
}
|
||||
buttonsInit.add(uniqid);
|
||||
addButton.addEventListener('click', (e: Event) => {
|
||||
e.preventDefault();
|
||||
handleAdd(e.target);
|
||||
});
|
||||
}
|
||||
for (let i = 0; i < collections.length; i++) {
|
||||
const entries: NodeListOf<HTMLLIElement> =
|
||||
collections[i].querySelectorAll(":scope > li");
|
||||
const collection = collections[i];
|
||||
const uniqid = collection.dataset.uniqid as string;
|
||||
if (collectionsInit.has(uniqid)) {
|
||||
continue;
|
||||
}
|
||||
collectionsInit.add(uniqid);
|
||||
let entries: NodeListOf<HTMLLIElement> = collection.querySelectorAll(':scope > li');
|
||||
for (let j = 0; j < entries.length; j++) {
|
||||
if (entries[j].dataset.collectionEmptyExplain === "1") {
|
||||
continue;
|
||||
@@ -142,4 +148,13 @@ window.addEventListener("load", () => {
|
||||
initializeRemove(collections[i], entries[j]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
initialize(document);
|
||||
});
|
||||
|
||||
window.addEventListener('show-hide-show', (event: CustomEvent<{id: number; container: HTMLElement; froms: HTMLElement[]}>) => {
|
||||
const container = event.detail.container as HTMLElement;
|
||||
initialize(container);
|
||||
})
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { createApp } from "vue";
|
||||
import PickEntity from "ChillMainAssets/vuejs/PickEntity/PickEntity.vue";
|
||||
import { _createI18n } from "ChillMainAssets/vuejs/_js/i18n";
|
||||
import { appMessages } from "ChillMainAssets/vuejs/PickEntity/i18n";
|
||||
import { createApp } from 'vue';
|
||||
import PickEntity from 'ChillMainAssets/vuejs/PickEntity/PickEntity.vue';
|
||||
import { _createI18n } from 'ChillMainAssets/vuejs/_js/i18n';
|
||||
import { appMessages } from 'ChillMainAssets/vuejs/PickEntity/i18n';
|
||||
|
||||
const i18n = _createI18n(appMessages);
|
||||
|
||||
@@ -9,169 +9,158 @@ let appsOnPage = new Map();
|
||||
let appsPerInput = new Map();
|
||||
|
||||
function loadDynamicPicker(element) {
|
||||
let apps = element.querySelectorAll('[data-module="pick-dynamic"]');
|
||||
|
||||
apps.forEach(function (el) {
|
||||
const isMultiple = parseInt(el.dataset.multiple) === 1,
|
||||
uniqId = el.dataset.uniqid,
|
||||
input = element.querySelector(
|
||||
'[data-input-uniqid="' + el.dataset.uniqid + '"]',
|
||||
),
|
||||
// the "picked" will always be an array, even if multiple is false
|
||||
picked = isMultiple
|
||||
? JSON.parse(input.value)
|
||||
: input.value === "[]" || input.value === ""
|
||||
? null
|
||||
: [JSON.parse(input.value)];
|
||||
(suggested = JSON.parse(el.dataset.suggested)),
|
||||
(as_id = parseInt(el.dataset.asId) === 1),
|
||||
(submit_on_adding_new_entity =
|
||||
parseInt(el.dataset.submitOnAddingNewEntity) === 1);
|
||||
label = el.dataset.label;
|
||||
let apps = element.querySelectorAll('[data-module="pick-dynamic"]');
|
||||
|
||||
if (!isMultiple) {
|
||||
if (input.value === "[]") {
|
||||
input.value = null;
|
||||
}
|
||||
}
|
||||
apps.forEach(function(el) {
|
||||
|
||||
const app = createApp({
|
||||
template:
|
||||
"<pick-entity " +
|
||||
':multiple="multiple" ' +
|
||||
':types="types" ' +
|
||||
':picked="picked" ' +
|
||||
':uniqid="uniqid" ' +
|
||||
':suggested="notPickedSuggested" ' +
|
||||
':label="label" ' +
|
||||
'@addNewEntity="addNewEntity" ' +
|
||||
'@removeEntity="removeEntity"></pick-entity>',
|
||||
components: {
|
||||
PickEntity,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
multiple: isMultiple,
|
||||
types: JSON.parse(el.dataset.types),
|
||||
picked: picked === null ? [] : picked,
|
||||
uniqid: el.dataset.uniqid,
|
||||
suggested,
|
||||
as_id,
|
||||
submit_on_adding_new_entity,
|
||||
label,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
notPickedSuggested() {
|
||||
const pickedIds = new Set();
|
||||
for (const p of this.picked) {
|
||||
pickedIds.add(`${p.type}${p.id}`);
|
||||
}
|
||||
return this.suggested.filter(
|
||||
(e) => !pickedIds.has(`${e.type}${e.id}`),
|
||||
);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
addNewEntity({ entity }) {
|
||||
if (this.multiple) {
|
||||
if (
|
||||
!this.picked.some((el) => {
|
||||
return el.type === entity.type && el.id === entity.id;
|
||||
})
|
||||
) {
|
||||
this.picked.push(entity);
|
||||
if (!as_id) {
|
||||
input.value = JSON.stringify(this.picked);
|
||||
} else {
|
||||
const ids = this.picked.map((el) => el.id);
|
||||
input.value = ids.join(",");
|
||||
}
|
||||
console.log(entity);
|
||||
const
|
||||
isMultiple = parseInt(el.dataset.multiple) === 1,
|
||||
uniqId = el.dataset.uniqid,
|
||||
input = element.querySelector('[data-input-uniqid="'+ el.dataset.uniqid +'"]'),
|
||||
// the "picked" will always be an array, even if multiple is false
|
||||
picked = isMultiple ?
|
||||
JSON.parse(input.value) : (
|
||||
(input.value === '[]' || input.value === '') ?
|
||||
null : [ JSON.parse(input.value) ]
|
||||
)
|
||||
suggested = JSON.parse(el.dataset.suggested),
|
||||
as_id = parseInt(el.dataset.asId) === 1,
|
||||
submit_on_adding_new_entity = parseInt(el.dataset.submitOnAddingNewEntity) === 1
|
||||
label = el.dataset.label;
|
||||
|
||||
if (!isMultiple) {
|
||||
if (input.value === '[]'){
|
||||
input.value = null;
|
||||
}
|
||||
} else {
|
||||
if (
|
||||
!this.picked.some((el) => {
|
||||
return el.type === entity.type && el.id === entity.id;
|
||||
})
|
||||
) {
|
||||
this.picked.splice(0, this.picked.length);
|
||||
this.picked.push(entity);
|
||||
if (!as_id) {
|
||||
input.value = JSON.stringify(this.picked[0]);
|
||||
} else {
|
||||
input.value = this.picked.map((el) => el.id);
|
||||
}
|
||||
|
||||
const app = createApp({
|
||||
template: '<pick-entity ' +
|
||||
':multiple="multiple" ' +
|
||||
':types="types" ' +
|
||||
':picked="picked" ' +
|
||||
':uniqid="uniqid" ' +
|
||||
':suggested="notPickedSuggested" ' +
|
||||
':label="label" ' +
|
||||
'@addNewEntity="addNewEntity" ' +
|
||||
'@removeEntity="removeEntity" ' +
|
||||
'@addNewEntityProcessEnded="addNewEntityProcessEnded"' +
|
||||
'></pick-entity>',
|
||||
components: {
|
||||
PickEntity,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
multiple: isMultiple,
|
||||
types: JSON.parse(el.dataset.types),
|
||||
picked: picked === null ? [] : picked,
|
||||
uniqid: el.dataset.uniqid,
|
||||
suggested,
|
||||
as_id,
|
||||
submit_on_adding_new_entity,
|
||||
label,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
notPickedSuggested() {
|
||||
const pickedIds = new Set();
|
||||
for (const p of this.picked) {
|
||||
pickedIds.add(`${p.type}${p.id}`);
|
||||
}
|
||||
return this.suggested.filter(e => !pickedIds.has(`${e.type}${e.id}`))
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
addNewEntity({entity}) {
|
||||
if (this.multiple) {
|
||||
if (!this.picked.some(el => {
|
||||
return el.type === entity.type && el.id === entity.id;
|
||||
})) {
|
||||
this.picked.push(entity);
|
||||
if (!as_id) {
|
||||
input.value = JSON.stringify(this.picked);
|
||||
} else {
|
||||
const ids = this.picked.map(el => el.id);
|
||||
input.value = ids.join(',');
|
||||
}
|
||||
console.log(entity)
|
||||
}
|
||||
} else {
|
||||
if (!this.picked.some(el => {
|
||||
return el.type === entity.type && el.id === entity.id;
|
||||
})) {
|
||||
this.picked.splice(0, this.picked.length);
|
||||
this.picked.push(entity);
|
||||
if (!as_id) {
|
||||
input.value = JSON.stringify(this.picked[0]);
|
||||
} else {
|
||||
input.value = this.picked.map(el => el.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
addNewEntityProcessEnded() {
|
||||
if (this.submit_on_adding_new_entity) {
|
||||
input.form.submit();
|
||||
}
|
||||
},
|
||||
removeEntity({entity}) {
|
||||
if (-1 === this.suggested.findIndex(e => e.type === entity.type && e.id === entity.id)) {
|
||||
this.suggested.push(entity);
|
||||
}
|
||||
this.picked = this.picked.filter(e => !(e.type === entity.type && e.id === entity.id));
|
||||
if (this.multiple) {
|
||||
input.value = JSON.stringify(this.picked);
|
||||
} else {
|
||||
input.value = "";
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
})
|
||||
.use(i18n)
|
||||
.mount(el);
|
||||
|
||||
if (this.submit_on_adding_new_entity) {
|
||||
input.form.submit();
|
||||
}
|
||||
},
|
||||
removeEntity({ entity }) {
|
||||
if (
|
||||
-1 ===
|
||||
this.suggested.findIndex(
|
||||
(e) => e.type === entity.type && e.id === entity.id,
|
||||
)
|
||||
) {
|
||||
this.suggested.push(entity);
|
||||
}
|
||||
this.picked = this.picked.filter(
|
||||
(e) => !(e.type === entity.type && e.id === entity.id),
|
||||
);
|
||||
if (this.multiple) {
|
||||
input.value = JSON.stringify(this.picked);
|
||||
} else {
|
||||
input.value = "";
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
.use(i18n)
|
||||
.mount(el);
|
||||
|
||||
appsOnPage.set(uniqId, app);
|
||||
appsPerInput.set(input.name, app);
|
||||
});
|
||||
appsOnPage.set(uniqId, app);
|
||||
appsPerInput.set(input.name, app);
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener("show-hide-show", function (e) {
|
||||
loadDynamicPicker(e.detail.container);
|
||||
|
||||
document.addEventListener('show-hide-show', function(e) {
|
||||
loadDynamicPicker(e.detail.container)
|
||||
});
|
||||
|
||||
document.addEventListener("show-hide-hide", function (e) {
|
||||
console.log("hiding event caught");
|
||||
e.detail.container
|
||||
.querySelectorAll('[data-module="pick-dynamic"]')
|
||||
.forEach((el) => {
|
||||
let uniqId = el.dataset.uniqid;
|
||||
if (appsOnPage.has(uniqId)) {
|
||||
appsOnPage.get(uniqId).unmount();
|
||||
appsOnPage.delete(uniqId);
|
||||
document.addEventListener('show-hide-hide', function(e) {
|
||||
console.log('hiding event caught')
|
||||
e.detail.container.querySelectorAll('[data-module="pick-dynamic"]').forEach((el) => {
|
||||
let uniqId = el.dataset.uniqid;
|
||||
if (appsOnPage.has(uniqId)) {
|
||||
appsOnPage.get(uniqId).unmount();
|
||||
appsOnPage.delete(uniqId);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
document.addEventListener('pick-entity-type-action', function (e) {
|
||||
console.log('pick entity event', e);
|
||||
if (!appsPerInput.has(e.detail.name)) {
|
||||
console.error('no app with this name');
|
||||
return;
|
||||
}
|
||||
const app = appsPerInput.get(e.detail.name);
|
||||
if (e.detail.action === 'add') {
|
||||
app.addNewEntity(e.detail.entity);
|
||||
} else if (e.detail.action === 'remove') {
|
||||
app.removeEntity(e.detail.entity);
|
||||
} else {
|
||||
console.error('action not supported: '+e.detail.action);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
document.addEventListener("pick-entity-type-action", function (e) {
|
||||
console.log("pick entity event", e);
|
||||
if (!appsPerInput.has(e.detail.name)) {
|
||||
console.error("no app with this name");
|
||||
return;
|
||||
}
|
||||
const app = appsPerInput.get(e.detail.name);
|
||||
if (e.detail.action === "add") {
|
||||
app.addNewEntity(e.detail.entity);
|
||||
} else if (e.detail.action === "remove") {
|
||||
app.removeEntity(e.detail.entity);
|
||||
} else {
|
||||
console.error("action not supported: " + e.detail.action);
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function (e) {
|
||||
loadDynamicPicker(document);
|
||||
});
|
||||
document.addEventListener('DOMContentLoaded', function(e) {
|
||||
loadDynamicPicker(document)
|
||||
})
|
||||
|
||||
window.loadDynamicPicker = loadDynamicPicker;
|
||||
|
||||
|
@@ -1,62 +1,174 @@
|
||||
import { ShowHide } from "ChillMainAssets/lib/show_hide/show_hide.js";
|
||||
import {ShowHide} from 'ChillMainAssets/lib/show_hide/show_hide.js';
|
||||
|
||||
window.addEventListener("DOMContentLoaded", function () {
|
||||
let divTransitions = document.querySelector("#transitions"),
|
||||
futureDestUsersContainer = document.querySelector("#futureDests");
|
||||
if (null !== divTransitions) {
|
||||
window.addEventListener('DOMContentLoaded', function() {
|
||||
const
|
||||
divTransitions = document.querySelector('#transitions'),
|
||||
futureDestUsersContainer = document.querySelector('#futureDests'),
|
||||
personSignatureField = document.querySelector('#person-signature-field'),
|
||||
userSignatureField = document.querySelector('#user-signature-field'),
|
||||
signatureTypeChoices = document.querySelector('#signature-type-choice'),
|
||||
personChoice = document.querySelector('#workflow_step_isPersonOrUserSignature_0'),
|
||||
userChoice = document.querySelector('#workflow_step_isPersonOrUserSignature_1'),
|
||||
signatureZone = document.querySelector('#signature-zone'),
|
||||
transitionFilterContainer = document.querySelector('#transitionFilter'),
|
||||
transitionsContainer = document.querySelector('#transitions'),
|
||||
sendExternalContainer = document.querySelector('#sendExternalContainer')
|
||||
;
|
||||
|
||||
// ShowHide instance for future dest users
|
||||
new ShowHide({
|
||||
debug: false,
|
||||
load_event: null,
|
||||
froms: [divTransitions],
|
||||
container: [futureDestUsersContainer],
|
||||
test: function (divs, arg2, arg3) {
|
||||
for (let div of divs) {
|
||||
for (let input of div.querySelectorAll("input")) {
|
||||
test: function(froms, event) {
|
||||
for (let transition of froms) {
|
||||
for (let input of transition.querySelectorAll('input')) {
|
||||
if (input.checked) {
|
||||
if (input.dataset.toFinal === "1") {
|
||||
if ('1' === input.dataset.toFinal) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ('1' === input.dataset.isSentExternal) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const inputData = JSON.parse(input.getAttribute('data-is-signature'))
|
||||
if (inputData.includes('person') || inputData.includes('user')) {
|
||||
return false;
|
||||
} else {
|
||||
personChoice.checked = false
|
||||
userChoice.checked = false
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let transitionFilterContainer = document.querySelector("#transitionFilter"),
|
||||
transitions = document.querySelector("#transitions");
|
||||
if (null !== transitionFilterContainer) {
|
||||
transitions.querySelectorAll(".form-check").forEach(function (row) {
|
||||
const isForward = row.querySelector("input").dataset.isForward;
|
||||
|
||||
new ShowHide({
|
||||
load_event: null,
|
||||
froms: [transitionFilterContainer],
|
||||
container: row,
|
||||
test: function (containers) {
|
||||
for (let container of containers) {
|
||||
for (let input of container.querySelectorAll("input")) {
|
||||
if (input.checked) {
|
||||
return isForward === input.value;
|
||||
// ShowHide instance for send external
|
||||
new ShowHide({
|
||||
debug: false,
|
||||
load_event: null,
|
||||
froms: [divTransitions],
|
||||
container: [sendExternalContainer],
|
||||
test: function(froms, event) {
|
||||
for (let transition of froms) {
|
||||
for (let input of transition.querySelectorAll('input')) {
|
||||
if (input.checked) {
|
||||
if ('1' === input.dataset.isSentExternal) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
toggle_callback: function (c, dir) {
|
||||
for (let div of c) {
|
||||
let input = div.querySelector("input");
|
||||
if ("hide" === dir) {
|
||||
input.checked = false;
|
||||
input.disabled = true;
|
||||
} else {
|
||||
input.disabled = false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
})
|
||||
|
||||
// ShowHide signature zone
|
||||
new ShowHide({
|
||||
debug: false,
|
||||
load_event: null,
|
||||
froms: [divTransitions],
|
||||
container: [signatureZone],
|
||||
test: function(froms, event) {
|
||||
for (let transition of froms) {
|
||||
for (let input of transition.querySelectorAll('input')) {
|
||||
if (input.checked) {
|
||||
const inputData = JSON.parse(input.getAttribute('data-is-signature'))
|
||||
if (inputData.includes('person') || inputData.includes('user')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ShowHides for personSignatureField or userSignatureField within signature zone
|
||||
new ShowHide({
|
||||
debug: false,
|
||||
froms: [signatureTypeChoices],
|
||||
container: [personSignatureField],
|
||||
test: function(froms, event) {
|
||||
for (let container of froms) {
|
||||
return personChoice.checked;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
});
|
||||
|
||||
new ShowHide({
|
||||
debug: false,
|
||||
froms: [signatureTypeChoices],
|
||||
container: [userSignatureField],
|
||||
test: function(froms, event) {
|
||||
for (let container of froms) {
|
||||
return userChoice.checked;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
});
|
||||
|
||||
if (null !== divTransitions) {
|
||||
new ShowHide({
|
||||
load_event: null,
|
||||
froms: [divTransitions],
|
||||
container: [futureDestUsersContainer],
|
||||
test: function(divs, arg2, arg3) {
|
||||
for (let div of divs) {
|
||||
for (let input of div.querySelectorAll('input')) {
|
||||
if (input.checked) {
|
||||
if (input.dataset.toFinal === "1") {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (null !== transitionFilterContainer) {
|
||||
transitionsContainer.querySelectorAll('.form-check').forEach(function(row) {
|
||||
|
||||
const isForward = row.querySelector('input').dataset.isForward;
|
||||
|
||||
new ShowHide({
|
||||
load_event: null,
|
||||
froms: [transitionFilterContainer],
|
||||
container: row,
|
||||
test: function (containers) {
|
||||
for (let container of containers) {
|
||||
for (let input of container.querySelectorAll('input')) {
|
||||
if (input.checked) {
|
||||
return isForward === input.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
toggle_callback: function (c, dir) {
|
||||
for (let div of c) {
|
||||
let input = div.querySelector('input');
|
||||
if ('hide' === dir) {
|
||||
input.checked = false;
|
||||
input.disabled = true;
|
||||
} else {
|
||||
input.disabled = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@@ -30,6 +30,11 @@ export interface Scope {
|
||||
};
|
||||
}
|
||||
|
||||
export interface ResultItem<T> {
|
||||
result: T;
|
||||
relevance: number;
|
||||
}
|
||||
|
||||
export interface User {
|
||||
type: "user";
|
||||
id: number;
|
||||
@@ -42,15 +47,27 @@ export interface User {
|
||||
// todo: mainCenter; mainJob; etc..
|
||||
}
|
||||
|
||||
export interface UserGroup {
|
||||
type: "user_group";
|
||||
id: number;
|
||||
label: TranslatableString;
|
||||
backgroundColor: string;
|
||||
foregroundColor: string;
|
||||
excludeKey: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export type UserGroupOrUser = User | UserGroup;
|
||||
|
||||
export interface UserAssociatedInterface {
|
||||
type: "user";
|
||||
id: number;
|
||||
}
|
||||
|
||||
export interface TranslatableString {
|
||||
export type TranslatableString = {
|
||||
fr?: string;
|
||||
nl?: string;
|
||||
}
|
||||
};
|
||||
|
||||
export interface Postcode {
|
||||
id: number;
|
||||
@@ -59,10 +76,10 @@ export interface Postcode {
|
||||
center: Point;
|
||||
}
|
||||
|
||||
export interface Point {
|
||||
export type Point = {
|
||||
type: "Point";
|
||||
coordinates: [lat: number, lon: number];
|
||||
}
|
||||
};
|
||||
|
||||
export interface Country {
|
||||
id: number;
|
||||
@@ -168,3 +185,8 @@ export interface NewsItemType {
|
||||
startDate: DateTime;
|
||||
endDate: DateTime | null;
|
||||
}
|
||||
|
||||
export interface WorkflowAvailable {
|
||||
name: string;
|
||||
text: string;
|
||||
}
|
||||
|
@@ -17,7 +17,9 @@
|
||||
</template>
|
||||
<template #tbody>
|
||||
<tr v-for="(w, i) in workflows.results" :key="`workflow-${i}`">
|
||||
<td>{{ w.title }}</td>
|
||||
<td>
|
||||
{{ w.title }}
|
||||
</td>
|
||||
<td>
|
||||
<div class="workflow">
|
||||
<div class="breadcrumb">
|
||||
@@ -26,6 +28,7 @@
|
||||
/>
|
||||
<span class="mx-2">{{ getStep(w) }}</span>
|
||||
</div>
|
||||
<span v-if="w.isOnHoldAtCurrentStep" class="badge bg-success rounded-pill">{{ $t('on_hold') }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td v-if="w.datas.persons !== null">
|
||||
|
@@ -1,88 +1,83 @@
|
||||
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 nouveaux parcours",
|
||||
description:
|
||||
"Liste des parcours d'accompagnement que l'on vient de m'attribuer depuis moins de 15 jours.",
|
||||
},
|
||||
my_notifications: {
|
||||
tab: "Mes nouvelles notifications",
|
||||
description: "Liste des notifications reçues et non lues.",
|
||||
},
|
||||
my_workflows: {
|
||||
tab: "Mes workflows",
|
||||
description: "Liste des workflows en attente d'une action.",
|
||||
description_cc: "Liste des workflows dont je suis en copie.",
|
||||
},
|
||||
opening_date: "Date d'ouverture",
|
||||
social_issues: "Problématiques sociales",
|
||||
concerned_persons: "Usagers concernés",
|
||||
max_date: "Date d'échéance",
|
||||
warning_date: "Date de rappel",
|
||||
evaluation: "Évaluation",
|
||||
task: "Tâche",
|
||||
Date: "Date",
|
||||
From: "Expéditeur",
|
||||
Subject: "Objet",
|
||||
Entity: "Associé à",
|
||||
Step: "Étape",
|
||||
concerned_users: "Usagers concernés",
|
||||
Object_workflow: "Objet du workflow",
|
||||
show_entity: "Voir {entity}",
|
||||
the_activity: "l'échange",
|
||||
the_course: "le parcours",
|
||||
the_action: "l'action",
|
||||
the_evaluation: "l'évaluation",
|
||||
the_evaluation_document: "le document",
|
||||
the_task: "la tâche",
|
||||
the_workflow: "le workflow",
|
||||
StartDate: "Date d'ouverture",
|
||||
SocialAction: "Action d'accompagnement",
|
||||
no_data: "Aucun résultats",
|
||||
no_dashboard: "Pas de tableaux de bord",
|
||||
counter: {
|
||||
unread_notifications:
|
||||
"{n} notification non lue | {n} notifications non lues",
|
||||
assignated_courses:
|
||||
"{n} parcours récent assigné | {n} parcours récents assignés",
|
||||
assignated_actions: "{n} action assignée | {n} actions assignées",
|
||||
assignated_evaluations:
|
||||
"{n} évaluation assignée | {n} évaluations assignées",
|
||||
alert_tasks: "{n} tâche en rappel | {n} tâches en rappel",
|
||||
warning_tasks: "{n} tâche à échéance | {n} tâches à échéance",
|
||||
},
|
||||
emergency: "Urgent",
|
||||
confidential: "Confidentiel",
|
||||
automatic_notification: "Notification automatique",
|
||||
widget: {
|
||||
news: {
|
||||
title: "Actualités",
|
||||
readMore: "Lire la suite",
|
||||
date: "Date",
|
||||
none: "Aucune actualité",
|
||||
},
|
||||
},
|
||||
},
|
||||
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 nouveaux parcours",
|
||||
description: "Liste des parcours d'accompagnement que l'on vient de m'attribuer depuis moins de 15 jours.",
|
||||
},
|
||||
my_notifications: {
|
||||
tab: "Mes nouvelles notifications",
|
||||
description: "Liste des notifications reçues et non lues.",
|
||||
},
|
||||
my_workflows: {
|
||||
tab: "Mes workflows",
|
||||
description: "Liste des workflows en attente d'une action.",
|
||||
description_cc: "Liste des workflows dont je suis en copie."
|
||||
},
|
||||
opening_date: "Date d'ouverture",
|
||||
social_issues: "Problématiques sociales",
|
||||
concerned_persons: "Usagers concernés",
|
||||
max_date: "Date d'échéance",
|
||||
warning_date: "Date de rappel",
|
||||
evaluation: "Évaluation",
|
||||
task: "Tâche",
|
||||
Date: "Date",
|
||||
From: "Expéditeur",
|
||||
Subject: "Objet",
|
||||
Entity: "Associé à",
|
||||
Step: "Étape",
|
||||
concerned_users: "Usagers concernés",
|
||||
Object_workflow: "Objet du workflow",
|
||||
on_hold: "En attente",
|
||||
show_entity: "Voir {entity}",
|
||||
the_activity: "l'échange",
|
||||
the_course: "le parcours",
|
||||
the_action: "l'action",
|
||||
the_evaluation: "l'évaluation",
|
||||
the_evaluation_document: "le document",
|
||||
the_task: "la tâche",
|
||||
the_workflow: "le workflow",
|
||||
StartDate: "Date d'ouverture",
|
||||
SocialAction: "Action d'accompagnement",
|
||||
no_data: "Aucun résultats",
|
||||
no_dashboard: "Pas de tableaux de bord",
|
||||
counter: {
|
||||
unread_notifications: "{n} notification non lue | {n} notifications non lues",
|
||||
assignated_courses: "{n} parcours récent assigné | {n} parcours récents assignés",
|
||||
assignated_actions: "{n} action assignée | {n} actions assignées",
|
||||
assignated_evaluations: "{n} évaluation assignée | {n} évaluations assignées",
|
||||
alert_tasks: "{n} tâche en rappel | {n} tâches en rappel",
|
||||
warning_tasks: "{n} tâche à échéance | {n} tâches à échéance",
|
||||
},
|
||||
emergency: "Urgent",
|
||||
confidential: "Confidentiel",
|
||||
automatic_notification: "Notification automatique",
|
||||
widget: {
|
||||
news: {
|
||||
title: "Actualités",
|
||||
readMore: "Lire la suite",
|
||||
date: "Date",
|
||||
none: "Aucune actualité"
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Object.assign(appMessages.fr);
|
||||
|
||||
export { appMessages };
|
||||
export {
|
||||
appMessages
|
||||
};
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<ul :class="listClasses" v-if="picked.length && displayPicked">
|
||||
<li v-for="p in picked" @click="removeEntity(p)" :key="p.type + p.id">
|
||||
<li v-for="p in picked" @click="removeEntity(p)" :key="p.type+p.id">
|
||||
<span class="chill_denomination">{{ p.text }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -9,17 +9,16 @@
|
||||
<add-persons
|
||||
:options="addPersonsOptions"
|
||||
:key="uniqid"
|
||||
:button-title="translatedListOfTypes"
|
||||
:modal-title="translatedListOfTypes"
|
||||
:buttonTitle="translatedListOfTypes"
|
||||
:modalTitle="translatedListOfTypes"
|
||||
ref="addPersons"
|
||||
@add-new-persons="addNewEntity"
|
||||
/>
|
||||
@addNewPersons="addNewEntity"
|
||||
>
|
||||
</add-persons>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="list-suggest add-items inline">
|
||||
<li v-for="s in suggested" :key="s.id" @click="addNewSuggested(s)">
|
||||
<span>{{ s.text }}</span>
|
||||
</li>
|
||||
<li v-for="s in suggested" :key="s.id" @click="addNewSuggested(s)"><span>{{ s.text }}</span></li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
@@ -50,91 +49,86 @@ export default {
|
||||
default: true,
|
||||
},
|
||||
displayPicked: {
|
||||
// display picked entities.
|
||||
type: Boolean,
|
||||
default: true,
|
||||
// display picked entities.
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
suggested: {
|
||||
type: Array,
|
||||
default: [],
|
||||
type: Array,
|
||||
default: []
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
}
|
||||
},
|
||||
emits: ["addNewEntity", "removeEntity"],
|
||||
emits: ['addNewEntity', 'removeEntity', 'addNewEntityProcessEnded'],
|
||||
components: {
|
||||
AddPersons,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
key: "",
|
||||
key: ''
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
addPersonsOptions() {
|
||||
return {
|
||||
uniq: !this.multiple,
|
||||
type: this.types,
|
||||
priority: null,
|
||||
button: {
|
||||
size: "btn-sm",
|
||||
class: "btn-submit",
|
||||
},
|
||||
};
|
||||
},
|
||||
translatedListOfTypes() {
|
||||
if (this.label !== "") {
|
||||
return this.label;
|
||||
}
|
||||
addPersonsOptions() {
|
||||
return {
|
||||
uniq: !this.multiple,
|
||||
type: this.types,
|
||||
priority: null,
|
||||
button: {
|
||||
size: 'btn-sm',
|
||||
class: 'btn-submit',
|
||||
},
|
||||
};
|
||||
},
|
||||
translatedListOfTypes() {
|
||||
if (this.label !== '') {
|
||||
return this.label;
|
||||
}
|
||||
|
||||
let trans = [];
|
||||
this.types.forEach((t) => {
|
||||
if (this.$props.multiple) {
|
||||
trans.push(appMessages.fr.pick_entity[t].toLowerCase());
|
||||
} else {
|
||||
trans.push(
|
||||
appMessages.fr.pick_entity[t + "_one"].toLowerCase(),
|
||||
);
|
||||
}
|
||||
});
|
||||
let trans = [];
|
||||
this.types.forEach(t => {
|
||||
if (this.$props.multiple) {
|
||||
trans.push(appMessages.fr.pick_entity[t].toLowerCase());
|
||||
} else {
|
||||
trans.push(appMessages.fr.pick_entity[t + '_one'].toLowerCase());
|
||||
}
|
||||
})
|
||||
|
||||
if (this.$props.multiple) {
|
||||
return (
|
||||
appMessages.fr.pick_entity.modal_title + trans.join(", ")
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
appMessages.fr.pick_entity.modal_title_one +
|
||||
trans.join(", ")
|
||||
);
|
||||
}
|
||||
},
|
||||
listClasses() {
|
||||
return {
|
||||
"list-suggest": true,
|
||||
"remove-items": this.$props.removableIfSet,
|
||||
};
|
||||
},
|
||||
if (this.$props.multiple) {
|
||||
return appMessages.fr.pick_entity.modal_title + trans.join(', ');
|
||||
} else {
|
||||
return appMessages.fr.pick_entity.modal_title_one + trans.join(', ');
|
||||
}
|
||||
},
|
||||
listClasses() {
|
||||
return {
|
||||
'list-suggest': true,
|
||||
'remove-items': this.$props.removableIfSet,
|
||||
};
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
addNewSuggested(entity) {
|
||||
this.$emit("addNewEntity", { entity: entity });
|
||||
},
|
||||
addNewEntity({ selected, modal }) {
|
||||
selected.forEach((item) => {
|
||||
this.$emit("addNewEntity", { entity: item.result });
|
||||
}, this);
|
||||
this.$refs.addPersons.resetSearch(); // to cast child method
|
||||
modal.showModal = false;
|
||||
},
|
||||
removeEntity(entity) {
|
||||
if (!this.$props.removableIfSet) {
|
||||
return;
|
||||
}
|
||||
this.$emit("removeEntity", { entity: entity });
|
||||
},
|
||||
addNewSuggested(entity) {
|
||||
this.$emit('addNewEntity', {entity: entity});
|
||||
},
|
||||
addNewEntity({ selected, modal }) {
|
||||
selected.forEach((item) => {
|
||||
this.$emit('addNewEntity', { entity: item.result});
|
||||
}, this
|
||||
);
|
||||
this.$refs.addPersons.resetSearch(); // to cast child method
|
||||
modal.showModal = false;
|
||||
this.$emit('addNewEntityProcessEnded');
|
||||
},
|
||||
removeEntity(entity) {
|
||||
if (!this.$props.removableIfSet) {
|
||||
return;
|
||||
}
|
||||
this.$emit('removeEntity',{ entity: entity });
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
@@ -0,0 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
import {UserGroup} from "../../../types";
|
||||
import {computed} from "vue";
|
||||
|
||||
interface UserGroupRenderBoxProps {
|
||||
userGroup: UserGroup;
|
||||
}
|
||||
|
||||
const props = defineProps<UserGroupRenderBoxProps>();
|
||||
|
||||
const styles = computed<{color: string, "background-color": string}>(() => {
|
||||
return {
|
||||
color: props.userGroup.foregroundColor,
|
||||
"background-color": props.userGroup.backgroundColor,
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span class="badge-user-group" :style="styles">{{ userGroup.label.fr }}</span>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
@@ -1,149 +1,140 @@
|
||||
<template>
|
||||
<div class="flex-table workflow" id="workflow-list">
|
||||
<div
|
||||
v-for="(w, i) in workflows"
|
||||
:key="`workflow-${i}`"
|
||||
class="item-bloc"
|
||||
>
|
||||
<div>
|
||||
<div class="item-row col">
|
||||
<h2>{{ w.title }}</h2>
|
||||
<div class="flex-grow-1 ms-3 h3">
|
||||
<div class="visually-hidden">
|
||||
{{ w.relatedEntityClass }}
|
||||
{{ w.relatedEntityId }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-table workflow" id="workflow-list">
|
||||
<div v-for="(w, i) in workflows" :key="`workflow-${i}`"
|
||||
class="item-bloc">
|
||||
|
||||
<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
|
||||
v-if="step.isFreezed"
|
||||
class="fa fa-snowflake-o fa-sm me-1"
|
||||
/>
|
||||
{{ step.currentStep.text }}
|
||||
</span>
|
||||
<span v-if="j !== Object.keys(w.steps).length - 1">
|
||||
→
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
<div>
|
||||
<div class="item-row col">
|
||||
<h2>{{ w.title }}</h2>
|
||||
<div class="flex-grow-1 ms-3 h3">
|
||||
<div class="visually-hidden">
|
||||
{{ w.relatedEntityClass }}
|
||||
{{ w.relatedEntityId }}
|
||||
</div>
|
||||
</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" />
|
||||
{{ $t("you_subscribed_to_all_steps") }}
|
||||
</p>
|
||||
<p v-if="isUserSubscribedToFinal(w)">
|
||||
<i class="fa fa-check fa-fw" />
|
||||
{{ $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')"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</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>
|
||||
<span v-if="w.isOnHoldAtCurrentStep" class="badge bg-success rounded-pill">{{ $t('on_hold') }}</span>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Popover from "bootstrap/js/src/popover";
|
||||
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",
|
||||
},
|
||||
},
|
||||
};
|
||||
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",
|
||||
on_hold: "En attente"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
name: "ListWorkflow",
|
||||
i18n: i18n,
|
||||
props: {
|
||||
workflows: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
goToUrl(w) {
|
||||
return `/fr/main/workflow/${w.id}/show`;
|
||||
},
|
||||
getPopTitle(step) {
|
||||
if (step.transitionPrevious != null) {
|
||||
//console.log(step.transitionPrevious.text);
|
||||
let freezed = step.isFreezed
|
||||
? `<i class="fa fa-snowflake-o fa-sm me-1"></i>`
|
||||
: ``;
|
||||
return `${freezed}${step.transitionPrevious.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() {
|
||||
// todo
|
||||
return false;
|
||||
},
|
||||
isUserSubscribedToFinal() {
|
||||
// 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,
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
name: "ListWorkflow",
|
||||
i18n: i18n,
|
||||
props: {
|
||||
workflows: {
|
||||
type: Array,
|
||||
required: true,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
goToUrl(w) {
|
||||
return `/fr/main/workflow/${w.id}/show`;
|
||||
},
|
||||
getPopTitle(step) {
|
||||
if (step.transitionPrevious != null) {
|
||||
//console.log(step.transitionPrevious.text);
|
||||
let freezed = step.isFreezed ? `<i class="fa fa-snowflake-o fa-sm me-1"></i>` : ``;
|
||||
return `${freezed}${step.transitionPrevious.text}`;
|
||||
}
|
||||
},
|
||||
getPopContent(step) {
|
||||
if (step.transitionPrevious != null) {
|
||||
if (step.transitionPreviousBy !== 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>`
|
||||
;
|
||||
} else {
|
||||
return `<ul class="small_in_title">
|
||||
<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>
|
||||
|
@@ -1,140 +1,126 @@
|
||||
<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
|
||||
:relatedEntityClass="this.relatedEntityClass"
|
||||
:relatedEntityId="this.relatedEntityId"
|
||||
:workflowsAvailables="workflowsAvailables"
|
||||
:preventDefaultMoveToGenerate="this.$props.preventDefaultMoveToGenerate"
|
||||
:goToGenerateWorkflowPayload="this.goToGenerateWorkflowPayload"
|
||||
:countExistingWorkflows="countWorkflows"
|
||||
@go-to-generate-workflow="goToGenerateWorkflow"
|
||||
@click-open-list="openModal"
|
||||
></pick-workflow>
|
||||
|
||||
<pick-workflow
|
||||
v-else-if="allowCreate"
|
||||
:related-entity-class="this.relatedEntityClass"
|
||||
:related-entity-id="this.relatedEntityId"
|
||||
:workflows-availables="workflowsAvailables"
|
||||
:prevent-default-move-to-generate="
|
||||
this.$props.preventDefaultMoveToGenerate
|
||||
"
|
||||
:go-to-generate-workflow-payload="this.goToGenerateWorkflowPayload"
|
||||
@go-to-generate-workflow="goToGenerateWorkflow"
|
||||
/>
|
||||
<teleport to="body">
|
||||
<modal v-if="modal.showModal"
|
||||
:modalDialogClass="modal.modalDialogClass"
|
||||
@close="modal.showModal = false">
|
||||
|
||||
<teleport to="body">
|
||||
<modal
|
||||
v-if="modal.showModal"
|
||||
:modal-dialog-class="modal.modalDialogClass"
|
||||
@close="modal.showModal = false"
|
||||
>
|
||||
<template #header>
|
||||
<h2 class="modal-title">
|
||||
{{ $t("workflow_list") }}
|
||||
</h2>
|
||||
</template>
|
||||
<template v-slot:header>
|
||||
<h2 class="modal-title">{{ $t('workflow_list') }}</h2>
|
||||
</template>
|
||||
|
||||
<template #body>
|
||||
<list-workflow-vue :workflows="workflows" />
|
||||
</template>
|
||||
<template v-slot:body>
|
||||
<list-workflow-vue
|
||||
:workflows="workflows"
|
||||
></list-workflow-vue>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<pick-workflow
|
||||
v-if="allowCreate"
|
||||
:related-entity-class="this.relatedEntityClass"
|
||||
:related-entity-id="this.relatedEntityId"
|
||||
:workflows-availables="workflowsAvailables"
|
||||
:prevent-default-move-to-generate="
|
||||
this.$props.preventDefaultMoveToGenerate
|
||||
"
|
||||
:go-to-generate-workflow-payload="
|
||||
this.goToGenerateWorkflowPayload
|
||||
"
|
||||
@go-to-generate-workflow="this.goToGenerateWorkflow"
|
||||
/>
|
||||
</template>
|
||||
</modal>
|
||||
</teleport>
|
||||
<template v-slot:footer>
|
||||
<pick-workflow v-if="allowCreate"
|
||||
:relatedEntityClass="this.relatedEntityClass"
|
||||
:relatedEntityId="this.relatedEntityId"
|
||||
:workflowsAvailables="workflowsAvailables"
|
||||
:preventDefaultMoveToGenerate="this.$props.preventDefaultMoveToGenerate"
|
||||
:goToGenerateWorkflowPayload="this.goToGenerateWorkflowPayload"
|
||||
:countExistingWorkflows="countWorkflows"
|
||||
:embedded-within-list-modal="true"
|
||||
@go-to-generate-workflow="this.goToGenerateWorkflow"
|
||||
></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";
|
||||
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,
|
||||
},
|
||||
emits: ["goToGenerateWorkflow"],
|
||||
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,
|
||||
},
|
||||
preventDefaultMoveToGenerate: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
goToGenerateWorkflowPayload: {
|
||||
required: false,
|
||||
default: {},
|
||||
},
|
||||
},
|
||||
name: "ListWorkflowModal",
|
||||
components: {
|
||||
Modal,
|
||||
PickWorkflow,
|
||||
ListWorkflowVue
|
||||
},
|
||||
emits: ['goToGenerateWorkflow'],
|
||||
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,
|
||||
},
|
||||
preventDefaultMoveToGenerate: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
goToGenerateWorkflowPayload: {
|
||||
required: false,
|
||||
default: {}
|
||||
},
|
||||
},
|
||||
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;
|
||||
},
|
||||
goToGenerateWorkflow(data) {
|
||||
console.log("go to generate workflow intercepted", data);
|
||||
this.$emit("goToGenerateWorkflow", data);
|
||||
},
|
||||
},
|
||||
i18n: {
|
||||
messages: {
|
||||
fr: {
|
||||
workflow_list: "Liste des workflows associés",
|
||||
workflow: " workflow associé",
|
||||
workflows: " workflows associés",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
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;
|
||||
},
|
||||
goToGenerateWorkflow(data) {
|
||||
console.log('go to generate workflow intercepted', data);
|
||||
this.$emit('goToGenerateWorkflow', data);
|
||||
}
|
||||
},
|
||||
i18n: {
|
||||
messages: {
|
||||
fr: {
|
||||
workflow_list: "Liste des workflows associés",
|
||||
workflow: " workflow associé",
|
||||
workflows: " workflows associés",
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
@@ -1,83 +1,107 @@
|
||||
<template>
|
||||
<template v-if="workflowsAvailables.length >= 1">
|
||||
<div class="dropdown d-grid gap-2">
|
||||
<button
|
||||
class="btn btn-primary dropdown-toggle"
|
||||
type="button"
|
||||
id="createWorkflowButton"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<template v-if="props.workflowsAvailables.length >= 1">
|
||||
<div v-if="countExistingWorkflows == 0 || embeddedWithinListModal" class="dropdown d-grid gap-2">
|
||||
<button class="btn btn-primary dropdown-toggle" type="button" id="createWorkflowButton" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
Créer un workflow
|
||||
</button>
|
||||
<ul class="dropdown-menu" aria-labelledby="createWorkflowButton">
|
||||
<li v-for="w in workflowsAvailables" :key="w.name">
|
||||
<a
|
||||
class="dropdown-item"
|
||||
:href="makeLink(w.name)"
|
||||
@click.prevent="goToGenerateWorkflow($event, w.name)"
|
||||
>{{ w.text }}</a
|
||||
>
|
||||
<li v-for="w in props.workflowsAvailables" :key="w.name">
|
||||
<button class="dropdown-item" type="button" @click.prevent="goToGenerateWorkflow($event, w.name)">{{ w.text }}</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div class="btn-group">
|
||||
<button @click="emit('clickOpenList')" class="btn btn-primary">
|
||||
<template v-if="countExistingWorkflows === 1">
|
||||
1 workflow associé
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ countExistingWorkflows }} workflows associés
|
||||
</template>
|
||||
</button>
|
||||
<button class="btn btn-primary dropdown-toggle dropdown-toggle-split" type="button" id="createWorkflowButton" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<span class="visually-hidden">Liste des workflows disponibles</span>
|
||||
</button>
|
||||
<ul class="dropdown-menu" aria-labelledby="createWorkflowButton">
|
||||
<li v-for="w in props.workflowsAvailables" :key="w.name">
|
||||
<button class="dropdown-item" type="button" @click.prevent="goToGenerateWorkflow($event, w.name)">{{ w.text }}</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div v-if="countExistingWorkflows > 0" class="dropdown d-grid gap-2">
|
||||
<button @click="emit('clickOpenList')" class="btn btn-primary" type="button" id="createWorkflowButton" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<template v-if="countExistingWorkflows === 1">
|
||||
1 workflow associé
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ countExistingWorkflows }} workflows associés
|
||||
</template>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { buildLinkCreate } from "ChillMainAssets/lib/entity-workflow/api.js";
|
||||
<script setup lang="ts">
|
||||
|
||||
export default {
|
||||
name: "PickWorkflow",
|
||||
props: {
|
||||
relatedEntityClass: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
relatedEntityId: {
|
||||
type: Number,
|
||||
required: false,
|
||||
},
|
||||
workflowsAvailables: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
preventDefaultMoveToGenerate: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
goToGenerateWorkflowPayload: {
|
||||
required: false,
|
||||
default: {},
|
||||
},
|
||||
},
|
||||
emits: ["goToGenerateWorkflow"],
|
||||
methods: {
|
||||
makeLink(workflowName) {
|
||||
return buildLinkCreate(
|
||||
workflowName,
|
||||
this.relatedEntityClass,
|
||||
this.relatedEntityId,
|
||||
);
|
||||
},
|
||||
goToGenerateWorkflow(event, workflowName) {
|
||||
console.log("goToGenerateWorkflow", event, workflowName);
|
||||
import {buildLinkCreate} from '../../../lib/entity-workflow/api';
|
||||
import {WorkflowAvailable} from "../../../types";
|
||||
|
||||
if (!this.$props.preventDefaultMoveToGenerate) {
|
||||
console.log("to go generate");
|
||||
window.location.assign(this.makeLink(workflowName));
|
||||
}
|
||||
interface PickWorkflowConfig {
|
||||
relatedEntityClass: string;
|
||||
/**
|
||||
* Represents the identifier of a related entity.
|
||||
* This variable can store either a numerical value representing the entity's ID or an undefined value
|
||||
* if the related entity is not specified or available, for instance when the entity is created within
|
||||
* the interface and the id will be available later
|
||||
*/
|
||||
relatedEntityId: number|undefined;
|
||||
workflowsAvailables: WorkflowAvailable[];
|
||||
preventDefaultMoveToGenerate: boolean;
|
||||
goToGenerateWorkflowPayload: object;
|
||||
countExistingWorkflows: number;
|
||||
/**
|
||||
* if true, this button will not present a splitted button
|
||||
*/
|
||||
embeddedWithinListModal: boolean;
|
||||
}
|
||||
|
||||
this.$emit("goToGenerateWorkflow", {
|
||||
event,
|
||||
workflowName,
|
||||
link: this.makeLink(workflowName),
|
||||
payload: this.goToGenerateWorkflowPayload,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
const props = withDefaults(defineProps<PickWorkflowConfig>(), {preventDefaultMoveToGenerate: false, goToGenerateWorkflowPayload: {}, allowCreateWorkflow: false});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'goToGenerateWorkflow', {event: MouseEvent, workflowName: string, isLinkValid: boolean, link: string, payload: object}): void;
|
||||
(e: 'clickOpenList'): void;
|
||||
}>();
|
||||
|
||||
const makeLink = (workflowName: string): string => buildLinkCreate(workflowName, props.relatedEntityClass, props.relatedEntityId);
|
||||
|
||||
const goToGenerateWorkflow = (event: MouseEvent, workflowName: string): void => {
|
||||
console.log('goToGenerateWorkflow', event, workflowName);
|
||||
let link = '';
|
||||
let isLinkValid = false;
|
||||
|
||||
try {
|
||||
link = makeLink(workflowName);
|
||||
isLinkValid = true;
|
||||
} catch (e) {
|
||||
console.info("could not generate link to create workflow, maybe the relatedEntityId is not yet known", e);
|
||||
}
|
||||
|
||||
if (!props.preventDefaultMoveToGenerate) {
|
||||
window.location.assign(link);
|
||||
}
|
||||
|
||||
emit('goToGenerateWorkflow', {event, workflowName, link, isLinkValid, payload: props.goToGenerateWorkflowPayload});
|
||||
}
|
||||
|
||||
const goToDuplicateRelatedEntity = (event: MouseEvent, workflowName: string): void => {
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
|
@@ -300,7 +300,96 @@
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h3><code>slim</code></h3>
|
||||
|
||||
<p>Ajouter <code>slim</code> enlève la marge inférieure. Permet un meilleur alignement horizontal dans une <code>row</code></p>
|
||||
|
||||
<div class="container">
|
||||
<div class="row row-hover">
|
||||
<div class="col-8">
|
||||
Some text, ul_record_actions sans slim
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<ul class="record_actions">
|
||||
<li><button class="btn">Some action</button></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row row-hover">
|
||||
<div class="col-8">
|
||||
Some text, ul_record_actions avec slim
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<ul class="record_actions slim">
|
||||
<li><button class="btn">Some action</button></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<xmp><a class="btn btn-submit">Text</a></xmp>
|
||||
Toutes les classes btn-* de bootstrap sont fonctionnelles
|
||||
|
||||
<h2>Hover</h2>
|
||||
|
||||
<p>Ajouter <code>.row-hover</code> sur une class <code>.row</code> provoque un changement de background au survol</p>
|
||||
|
||||
<div class="container">
|
||||
<div class="row row-hover align-items-center">
|
||||
<div class="col-sm-12 col-md-8">
|
||||
<span class="onthefly-container" data-target-name="person" data-target-id="329" data-action="show" data-button-text="Fatoumata Binta DIALLO (33 ans)" data-display-badge="true" data-v-app=""><a data-v-0c1a1125=""><span data-v-0c1a1125="" class="chill-entity entity-person badge-person">Fatoumata Binta DIALLO (33 ans)<!--v-if--></span></a><!--teleport start--><!--teleport end--></span>
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-4">
|
||||
<span class="text-end">A signé le 04/09/2024 à 13:55</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row row-hover align-items-center">
|
||||
<div class="col-sm-12 col-md-8">
|
||||
<span class="onthefly-container" data-target-name="person" data-target-id="330" data-action="show" data-button-text="Abdoulaye DIALLO (9 ans)" data-display-badge="true" data-v-app=""><a data-v-0c1a1125=""><span data-v-0c1a1125="" class="chill-entity entity-person badge-person">Abdoulaye DIALLO (9 ans)<!--v-if--></span></a><!--teleport start--><!--teleport end--></span>
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-4">
|
||||
<ul class="record_actions">
|
||||
<li>
|
||||
<a class="btn btn-misc" href="/fr/main/workflow/signature/6/metadata?returnPath=/fr/main/workflow/15/show"><i class="fa fa-pencil-square-o"></i> Signer</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row row-hover align-items-center">
|
||||
<div class="col-sm-12 col-md-8">
|
||||
<span class="onthefly-container" data-target-name="person" data-target-id="332" data-action="show" data-button-text="Mohamed DIALLO (44 ans)" data-display-badge="true" data-v-app=""><a data-v-0c1a1125=""><span data-v-0c1a1125="" class="chill-entity entity-person badge-person">Mohamed DIALLO (44 ans)<!--v-if--></span></a><!--teleport start--><!--teleport end--></span>
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-4">
|
||||
<span class="text-end">A signé le 04/09/2024 à 13:57</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row row-hover align-items-center">
|
||||
<div class="col-sm-12 col-md-8">
|
||||
<span class="onthefly-container" data-target-name="person" data-target-id="333" data-action="show" data-button-text="Fatou DIALLO (37 ans)" data-display-badge="true" data-v-app=""><a data-v-0c1a1125=""><span data-v-0c1a1125="" class="chill-entity entity-person badge-person">Fatou DIALLO (37 ans)<!--v-if--></span></a><!--teleport start--><!--teleport end--></span>
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-4">
|
||||
<ul class="record_actions">
|
||||
<li>
|
||||
<a class="btn btn-misc" href="/fr/main/workflow/signature/8/metadata?returnPath=/fr/main/workflow/15/show"><i class="fa fa-pencil-square-o"></i> Signer</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row row-hover align-items-center">
|
||||
<div class="col-sm-12 col-md-8">
|
||||
<span class="onthefly-container" data-target-name="person" data-target-id="334" data-action="show" data-button-text="Fanta DIALLO (7 ans)" data-display-badge="true" data-v-app=""><a data-v-0c1a1125=""><span data-v-0c1a1125="" class="chill-entity entity-person badge-person">Fanta DIALLO (7 ans)<!--v-if--></span></a><!--teleport start--><!--teleport end--></span>
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-4">
|
||||
<ul class="record_actions">
|
||||
<li>
|
||||
<a class="btn btn-misc" href="/fr/main/workflow/signature/9/metadata?returnPath=/fr/main/workflow/15/show"><i class="fa fa-pencil-square-o"></i> Signer</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
@@ -0,0 +1,22 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="description" content="SwaggerUI" />
|
||||
<title>SwaggerUI</title>
|
||||
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5.11.0/swagger-ui.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="swagger-ui"></div>
|
||||
<script src="https://unpkg.com/swagger-ui-dist@5.11.0/swagger-ui-bundle.js" crossorigin></script>
|
||||
<script>
|
||||
window.onload = () => {
|
||||
window.ui = SwaggerUIBundle({
|
||||
url: '/_dev/specs.yaml',
|
||||
dom_id: '#swagger-ui',
|
||||
});
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@@ -0,0 +1 @@
|
||||
<span class="badge-user-group" style="color: {{ user_group.foregroundColor }}; background-color: {{ user_group.backgroundColor }};">{{ user_group.label|localize_translatable_string }}</span>
|
@@ -163,6 +163,7 @@
|
||||
<div class="chill-collection">
|
||||
<ul class="list-entry"
|
||||
{{ form.vars.js_caller }}="{{ form.vars.js_caller }}"
|
||||
data-uniqid="{{ form.vars.uniqid|escape('html_attr') }}"
|
||||
data-collection-name="{{ form.vars.name|escape('html_attr') }}"
|
||||
data-collection-identifier="{{ form.vars.identifier|escape('html_attr') }}"
|
||||
data-collection-button-remove-label="{{ form.vars.button_remove_label|trans|e }}"
|
||||
@@ -184,6 +185,8 @@
|
||||
|
||||
{% if form.vars.allow_add == 1 %}
|
||||
<button class="add-entry btn btn-misc"
|
||||
type="button"
|
||||
data-uniqid="{{ form.vars.uniqid|escape('html_attr') }}"
|
||||
data-collection-add-target="{{ form.vars.name|escape('html_attr') }}"
|
||||
data-form-prototype="{{ ('<div>' ~ form_widget(form.vars.prototype) ~ '</div>')|escape('html_attr') }}" >
|
||||
<i class="fa fa-plus fa-fw"></i>
|
||||
|
@@ -60,7 +60,7 @@
|
||||
data-bs-toggle="dropdown"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false">
|
||||
<a href="#" class="more">{{ app.request.locale | capitalize }}</a>
|
||||
{{ app.request.locale | capitalize }}
|
||||
</a>
|
||||
<div class="dropdown-menu dropdown-menu-end"
|
||||
aria-labelledby="menu-languages">
|
||||
|
@@ -0,0 +1,21 @@
|
||||
{% extends '@ChillMain/CRUD/Admin/index.html.twig' %}
|
||||
|
||||
{% block css %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_link_tags('mod_pickentity_type') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_script_tags('mod_pickentity_type') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block title %}
|
||||
{% include('@ChillMain/CRUD/_edit_title.html.twig') %}
|
||||
{% endblock %}
|
||||
|
||||
{% block admin_content %}
|
||||
{% embed '@ChillMain/CRUD/_edit_content.html.twig' %}
|
||||
{% block content_form_actions_save_and_show %}{% endblock %}
|
||||
{% endembed %}
|
||||
{% endblock admin_content %}
|
@@ -0,0 +1,86 @@
|
||||
{% extends '@ChillMain/CRUD/Admin/index.html.twig' %}
|
||||
|
||||
{% block admin_content %}
|
||||
{% embed '@ChillMain/CRUD/_index.html.twig' %}
|
||||
|
||||
{% block table_entities %}
|
||||
<div class="flex-table">
|
||||
{% for entity in entities %}
|
||||
<div class="item-bloc">
|
||||
<div class="item-row">
|
||||
<div class="wrap-header">
|
||||
<div class="wh-row">
|
||||
<div class="wh-col">
|
||||
{{ entity|chill_entity_render_box }}
|
||||
</div>
|
||||
<div class="wh-col">
|
||||
{%- if not entity.active -%}
|
||||
<div>
|
||||
<span class="badge bg-danger">{{ 'user_group.inactive'|trans }}</span>
|
||||
</div>
|
||||
{%- endif -%}
|
||||
<div>{{ 'user_group.with_count_users'|trans({'count': entity.users|length}) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item-row separator">
|
||||
<div class="wrap-list">
|
||||
<div class="wl-row">
|
||||
<div class="wl-col title">
|
||||
<strong>{{ 'user_group.with_users'|trans }}</strong>
|
||||
</div>
|
||||
<div class="wl-col list">
|
||||
{% for user in entity.userListByLabelAscending %}
|
||||
<p class="wl-item">
|
||||
<span class="badge-user">
|
||||
{{ user|chill_entity_render_box }}
|
||||
</span>
|
||||
</p>
|
||||
{% else %}
|
||||
<p class="wl-item chill-no-data-statement">{{ 'user_group.no_users'|trans }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item-row separator">
|
||||
<div class="wrap-list">
|
||||
<div class="wl-row">
|
||||
<div class="wl-col title">
|
||||
<strong>{{ 'user_group.adminUsers'|trans }}</strong>
|
||||
</div>
|
||||
<div class="wl-col list">
|
||||
{% for user in entity.adminUserListByLabelAscending %}
|
||||
<p class="wl-item">
|
||||
<span class="badge-user">
|
||||
{{ user|chill_entity_render_box }}
|
||||
</span>
|
||||
</p>
|
||||
{% else %}
|
||||
<p class="wl-item chill-no-data-statement">{{ 'user_group.no_admin_users'|trans }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item-row separator">
|
||||
<ul class="record_actions slim">
|
||||
<li>
|
||||
<a href="{{ chill_path_add_return_path('chill_crud_admin_user_group_edit', {'id': entity.id}) }}" class="btn btn-edit"></a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block actions_before %}
|
||||
<li class='cancel'>
|
||||
<a href="{{ path('chill_main_admin_central') }}" class="btn btn-cancel">{{'Back to the admin'|trans }}</a>
|
||||
</li>
|
||||
{% endblock %}
|
||||
|
||||
{% endembed %}
|
||||
{% endblock %}
|
@@ -0,0 +1,160 @@
|
||||
{% extends '@ChillMain/layout.html.twig' %}
|
||||
|
||||
{% block css %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_link_tags('mod_pickentity_type') }}
|
||||
|
||||
<style type="text/css">
|
||||
form.remove {
|
||||
display: inline-block;
|
||||
padding: 1px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
}
|
||||
form:hover {
|
||||
animation-duration: 0.5s;
|
||||
animation-name: onHover;
|
||||
animation-iteration-count: 1;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 4px;
|
||||
}
|
||||
form.remove button.remove {
|
||||
display: inline;
|
||||
background-color: unset;
|
||||
border: none;
|
||||
color: var(--bs-chill-red);
|
||||
}
|
||||
|
||||
@keyframes onHover {
|
||||
from {
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
to {
|
||||
border: 1px solid #dee2e6;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_script_tags('mod_pickentity_type') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block title 'user_group.my_groups'|trans %}
|
||||
|
||||
{% block content %}
|
||||
<h1>{{ block('title') }}</h1>
|
||||
|
||||
{% if paginator.totalItems == 0 %}
|
||||
<p>{{ 'user_group.no_user_groups'|trans }}</p>
|
||||
{% else %}
|
||||
<div class="flex-table">
|
||||
{% for entity in groups %}
|
||||
<div class="item-bloc">
|
||||
<div class="item-row">
|
||||
<div class="wrap-header">
|
||||
<div class="wh-row">
|
||||
<div class="wh-col">
|
||||
{{ entity|chill_entity_render_box }}
|
||||
</div>
|
||||
<div class="wh-col">
|
||||
{%- if not entity.active -%}
|
||||
<div>
|
||||
<span class="badge bg-danger">{{ 'user_group.inactive'|trans }}</span>
|
||||
</div>
|
||||
{%- endif -%}
|
||||
<div>{{ 'user_group.with_count_users'|trans({'count': entity.users|length}) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item-row separator">
|
||||
<div class="wrap-list">
|
||||
<div class="wl-row">
|
||||
<div class="wl-col title">
|
||||
<strong>{{ 'user_group.with_users'|trans }}</strong>
|
||||
</div>
|
||||
<div class="wl-col list">
|
||||
{% if entity.users.contains(app.user) %}
|
||||
{% if is_granted('CHILL_MAIN_USER_GROUP_APPEND_TO_GROUP', entity) %}
|
||||
<form class="remove" method="POST" action="{{ chill_path_add_return_path('chill_main_user_groups_remove_user', {'id': entity.id, 'userId': app.user.id}) }}">
|
||||
<p class="wl-item">
|
||||
{{ 'user_group.me'|trans }}
|
||||
<button class="remove" type="submit"><i class="fa fa-times"></i></button>
|
||||
</p>
|
||||
</form>
|
||||
{% else %}
|
||||
<p class="wl-item">
|
||||
{% if entity.users|length > 1 %}{{ 'user_group.me_and'|trans }}{% else %}{{ 'user_group.me_only'|trans }}{% endif %}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% for user in entity.userListByLabelAscending %}
|
||||
{% if user is not same as app.user %}
|
||||
{% if is_granted('CHILL_MAIN_USER_GROUP_APPEND_TO_GROUP', entity) %}
|
||||
<form class="remove" method="POST" action="{{ chill_path_add_return_path('chill_main_user_groups_remove_user', {'id': entity.id, 'userId': user.id}) }}">
|
||||
<p class="wl-item">
|
||||
<span class="badge-user">
|
||||
{{ user|chill_entity_render_box }}
|
||||
</span>
|
||||
<button class="remove" type="submit"><i class="fa fa-times"></i></button>
|
||||
</p>
|
||||
</form>
|
||||
{% else %}
|
||||
<p class="wl-item">
|
||||
<span class="badge-user">
|
||||
{{ user|chill_entity_render_box }}
|
||||
</span>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if entity.adminUsers|length > 0 %}
|
||||
<div class="item-row separator">
|
||||
<div class="wrap-list">
|
||||
<div class="wl-row">
|
||||
<div class="wl-col title">
|
||||
<strong>{{ 'user_group.adminUsers'|trans }}</strong>
|
||||
</div>
|
||||
<div class="wl-col list">
|
||||
{% if entity.adminUsers.contains(app.user) %}
|
||||
<p class="wl-item">{% if entity.adminUsers|length > 1 %}{{ 'user_group.me_and'|trans }}{% else %}{{ 'user_group.me_only'|trans }}{% endif %}</p>
|
||||
{% endif %}
|
||||
{% for user in entity.adminUserListByLabelAscending %}
|
||||
{% if user is not same as app.user %}
|
||||
<p class="wl-item">
|
||||
<span class="badge-user">
|
||||
{{ user|chill_entity_render_box }}
|
||||
</span>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif -%}
|
||||
{%- set form = forms.offsetGet(entity) %}
|
||||
{%- if form is not null -%}
|
||||
<div class="item-row separator">
|
||||
<ul class="record_actions slim">
|
||||
<li>
|
||||
{{- form_start(form) -}}
|
||||
{{- form_widget(form.users) -}}
|
||||
{{- form_end(form) -}}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{%- endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{{ chill_pagination(paginator) }}
|
||||
{% endif %}
|
||||
{% endblock %}
|
@@ -0,0 +1,21 @@
|
||||
{% extends '@ChillMain/CRUD/Admin/index.html.twig' %}
|
||||
|
||||
{% block css %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_link_tags('mod_pickentity_type') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_script_tags('mod_pickentity_type') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block title %}
|
||||
{% include('@ChillMain/CRUD/_new_title.html.twig') %}
|
||||
{% endblock %}
|
||||
|
||||
{% block admin_content %}
|
||||
{% embed '@ChillMain/CRUD/_new_content.html.twig' %}
|
||||
{% block content_form_actions_save_and_show %}{% endblock %}
|
||||
{% endembed %}
|
||||
{% endblock admin_content %}
|
@@ -6,7 +6,7 @@
|
||||
{{ form_errors(transition_form) }}
|
||||
|
||||
{% set step = entity_workflow.currentStepChained %}
|
||||
{% set labels = workflow_metadata(entity_workflow, 'label', step.currentStep) %}
|
||||
{% set labels = workflow_metadata(entity_workflow, 'label', step.currentStep, entity_workflow.workflowName) %}
|
||||
{% set label = labels is null ? step.currentStep : labels|localize_translatable_string %}
|
||||
|
||||
<div class="card">
|
||||
@@ -58,24 +58,57 @@
|
||||
{{ form_row(transition_form.transition) }}
|
||||
</div>
|
||||
|
||||
{% if transition_form.freezeAfter is defined %}
|
||||
{{ form_row(transition_form.freezeAfter) }}
|
||||
{% endif %}
|
||||
<div id="signature-zone">
|
||||
<div id="signature-type-choice">
|
||||
{{ form_row(transition_form.isPersonOrUserSignature) }}
|
||||
{{ form_errors(transition_form.isPersonOrUserSignature) }}
|
||||
</div>
|
||||
<div id="user-signature-field">
|
||||
{{ form_row(transition_form.futureUserSignature) }}
|
||||
{{ form_errors(transition_form.futureUserSignature) }}
|
||||
</div>
|
||||
<div id="person-signature-field">
|
||||
{{ form_row(transition_form.futurePersonSignatures) }}
|
||||
{{ form_errors(transition_form.futurePersonSignatures) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="futureDests">
|
||||
{{ form_row(transition_form.future_dest_users) }}
|
||||
<div id="future-dest-users">
|
||||
{{ form_row(transition_form.futureDestUsers) }}
|
||||
{{ form_errors(transition_form.futureDestUsers) }}
|
||||
</div>
|
||||
<div id="future-cc-users">
|
||||
{{ form_row(transition_form.futureCcUsers) }}
|
||||
{{ form_errors(transition_form.futureCcUsers) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ form_row(transition_form.future_cc_users) }}
|
||||
|
||||
{{ form_row(transition_form.future_dest_emails) }}
|
||||
{{ form_errors(transition_form.future_dest_users) }}
|
||||
<div id="sendExternalContainer">
|
||||
{{ form_row(transition_form.futureDestineeThirdParties) }}
|
||||
{{ form_errors(transition_form.futureDestineeThirdParties) }}
|
||||
{{ form_row(transition_form.futureDestineeEmails) }}
|
||||
{{ form_errors(transition_form.futureDestineeEmails) }}
|
||||
</div>
|
||||
|
||||
<p>{{ form_label(transition_form.comment) }}</p>
|
||||
|
||||
{{ form_widget(transition_form.comment) }}
|
||||
|
||||
<ul class="record_actions">
|
||||
<ul class="record_actions sticky-form-buttons">
|
||||
{% if entity_workflow.isOnHoldByUser(app.user) %}
|
||||
<li>
|
||||
<a class="btn btn-misc" href="{{ path('chill_main_workflow_remove_hold', {'id': entity_workflow.currentStep.id }) }}"><i class="fa fa-hourglass"></i>
|
||||
{{ 'workflow.Remove hold'|trans }}
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li>
|
||||
<a class="btn btn-misc" href="{{ path('chill_main_workflow_on_hold', {'id': entity_workflow.id}) }}"><i class="fa fa-hourglass"></i>
|
||||
{{ 'workflow.Put on hold'|trans }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li>
|
||||
<button type="submit" class="btn btn-save">{{ 'Save'|trans }}</button>
|
||||
</li>
|
||||
@@ -98,15 +131,6 @@
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
{% if entity_workflow.currentStep.destEmail|length > 0 %}
|
||||
<p><b>{{ 'workflow.An access key was also sent to those addresses'|trans }} :</b></p>
|
||||
<ul>
|
||||
{% for e in entity_workflow.currentStep.destEmail -%}
|
||||
<li><a href="mailto:{{ e|escape('html_attr') }}">{{ e }}</a></li>
|
||||
{%- endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
{% if entity_workflow.currentStep.destUserByAccessKey|length > 0 %}
|
||||
<p><b>{{ 'workflow.Those users are also granted to apply a transition by using an access key'|trans }} :</b></p>
|
||||
<ul>
|
||||
|
@@ -1,8 +1,5 @@
|
||||
{% set acl = "0" %}
|
||||
{% if is_granted('CHILL_MAIN_WORKFLOW_CREATE', blank_workflow) %}
|
||||
{% set acl = "1" %}
|
||||
{% endif %}
|
||||
|
||||
{% set acl = "1" %}
|
||||
{# note: the list of available workflows already check that it is possible to create a given workflow #}
|
||||
{# vue component #}
|
||||
<div data-list-workflows="1"
|
||||
data-workflows="{{ entity_workflows_json|json_encode|e('html_attr') }}"
|
||||
|
@@ -2,7 +2,7 @@
|
||||
|
||||
<div class="flex-table">
|
||||
{% for step in entity_workflow.stepsChained %}
|
||||
{% set place_labels = workflow_metadata(entity_workflow, 'label', step.currentStep) %}
|
||||
{% set place_labels = workflow_metadata(entity_workflow, 'label', step.currentStep, entity_workflow.workflowName) %}
|
||||
{% 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 %}">
|
||||
@@ -13,38 +13,40 @@
|
||||
{{ 'workflow.No transitions'|trans }}
|
||||
</div>
|
||||
{% else %}
|
||||
|
||||
<div class="item-col">
|
||||
{% if step.previous is not null and step.previous.freezeAfter == true %}
|
||||
<i class="fa fa-snowflake-o fa-sm me-1" title="{{ 'workflow.Freezed'|trans }}"></i>
|
||||
{% endif %}
|
||||
{% if loop.last %}
|
||||
{% if entity_workflow.isOnHoldAtCurrentStep %}
|
||||
{% for hold in step.holdsOnStep %}
|
||||
<span class="badge bg-success rounded-pill" title="{{ 'workflow.On hold by'|trans({'by': hold.byUser|chill_entity_render_string})|escape('html_attr') }}">{{ 'workflow.On hold by'|trans({'by': hold.byUser|chill_entity_render_string}) }}</span>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="item-col flex-column align-items-end">
|
||||
<div class="decided">
|
||||
{{ place_label }}
|
||||
</div>
|
||||
{#
|
||||
<div class="decided">
|
||||
<i class="fa fa-times fa-fw text-danger"></i>
|
||||
Refusé
|
||||
</div>
|
||||
#}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
{% 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_labels = workflow_metadata(step.entityWorkflow, 'label', transition, step.entityWorkflow.workflowName) %}
|
||||
{% set transition_label = transition_labels is null ? step.transitionAfter : transition_labels|localize_translatable_string %}
|
||||
{% set forward = workflow_metadata(step.entityWorkflow, 'isForward', transition) %}
|
||||
{% set forward = workflow_metadata(step.entityWorkflow, 'isForward', transition, step.entityWorkflow.workflowName) %}
|
||||
<div class="item-row separator">
|
||||
<div class="item-col" style="width: inherit;">
|
||||
{% if step.transitionBy is not null %}
|
||||
<div>
|
||||
{{ step.transitionBy|chill_entity_render_box({'at_date': step.transitionAt}) }}
|
||||
{%- if step.transitionBy is not null -%}
|
||||
<span class="badge-user">{{ step.transitionBy|chill_entity_render_box({'at_date': step.transitionAt}) }}</span>
|
||||
{% else %}
|
||||
<span class="chill-no-data-statement">{{ 'workflow.Automated transition'|trans }}</span>
|
||||
{%- endif -%}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div>
|
||||
<span>{{ step.transitionAt|format_datetime('long', 'medium') }}</span>
|
||||
</div>
|
||||
@@ -69,13 +71,31 @@
|
||||
</blockquote>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if loop.last and step.allDestUser|length > 0 %}
|
||||
{% if not loop.last and step.signatures|length > 0 %}
|
||||
<div class="separator">
|
||||
<div>
|
||||
<p><b>{{ 'workflow.signatures_title'|trans({'nb_signatures': step.signatures|length}) }} :</b></p>
|
||||
<div>
|
||||
{{ include('@ChillMain/Workflow/_signature_list.html.twig', {'signatures': step.signatures, is_small: true }) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if loop.last and not step.isFinal %}
|
||||
<div class="item-row separator">
|
||||
<div>
|
||||
{% if step.destUser|length > 0 %}
|
||||
{% if step.destUser|length > 0 or step.destUserGroups|length > 0 %}
|
||||
<p><b>{{ 'workflow.Users allowed to apply transition'|trans }} : </b></p>
|
||||
<ul>
|
||||
{% for u in step.destUser %}
|
||||
<li>
|
||||
<span class="badge-user">{{ u|chill_entity_render_box({'at_date': step.previous.transitionAt}) }}</span>
|
||||
{% if step.isOnHoldByUser(u) %}
|
||||
<span class="badge bg-success rounded-pill" title="{{ 'workflow.On hold'|trans|escape('html_attr') }}">{{ 'workflow.On hold'|trans }}</span>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% for u in step.destUserGroups %}
|
||||
<li>{{ u|chill_entity_render_box({'at_date': step.previous.transitionAt}) }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
@@ -85,7 +105,7 @@
|
||||
<p><b>{{ 'workflow.Users put in Cc'|trans }} : </b></p>
|
||||
<ul>
|
||||
{% for u in step.ccUser %}
|
||||
<li>{{ u|chill_entity_render_box({'at_date': step.previous.transitionAt}) }}</li>
|
||||
<li><span class="badge-user">{{ u|chill_entity_render_box({'at_date': step.previous.transitionAt}) }}</span></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
@@ -103,12 +123,23 @@
|
||||
<p><b>{{ 'workflow.Those users are also granted to apply a transition by using an access key'|trans }} :</b></p>
|
||||
<ul>
|
||||
{% for u in step.destUserByAccessKey %}
|
||||
<li>{{ u|chill_entity_render_box({'at_date': step.previous.transitionAt}) }}</li>
|
||||
<li><span class="badge-user">{{ u|chill_entity_render_box({'at_date': step.previous.transitionAt}) }}</span></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% if step.signatures|length > 0 %}
|
||||
<p><b>{{ 'workflow.signatures_title'|trans({'nb_signatures': step.signatures|length}) }} :</b></p>
|
||||
{{ include('@ChillMain/Workflow/_signature_list.html.twig', {'signatures': step.signatures, is_small: true }) }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if step.sends|length > 0 %}
|
||||
<div>
|
||||
<p><b>{{ 'workflow.sent_through_secured_link'|trans }}</b></p>
|
||||
|
||||
{{ include('@ChillMain/Workflow/_send_views_list.html.twig', {'sends': step.sends}) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
|
@@ -1,11 +1,15 @@
|
||||
<h2>
|
||||
{{ 'workflow_'|trans }}
|
||||
</h2>
|
||||
{% if handler is not null %}
|
||||
<h2>
|
||||
{{ 'workflow_'|trans }}
|
||||
</h2>
|
||||
|
||||
<div class="item-row col" style="display: block;">
|
||||
{% include handler.template(entity_workflow) with handler.templateData(entity_workflow)|merge({
|
||||
'description': true,
|
||||
'breadcrumb': true,
|
||||
'add_classes': ''
|
||||
}) %}
|
||||
</div>
|
||||
<div class="item-row col" style="display: block;">
|
||||
{% include handler.template(entity_workflow) with handler.templateData(entity_workflow)|merge({
|
||||
'description': true,
|
||||
'breadcrumb': true,
|
||||
'add_classes': ''
|
||||
}) %}
|
||||
</div>
|
||||
{% else %}
|
||||
<h2>{{ 'workflow.deleted_title'|trans }}</h2>
|
||||
{% endif %}
|
||||
|
@@ -0,0 +1,35 @@
|
||||
<div class="container">
|
||||
{% for send in sends %}
|
||||
<div class="row row-hover align-items-center">
|
||||
<div class="col-sm-12 col-md-5">
|
||||
{% if send.destineeKind == 'thirdParty' %}
|
||||
{% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with {
|
||||
action: 'show', displayBadge: true,
|
||||
targetEntity: { name: 'thirdParty', id: send.destineeThirdParty.id },
|
||||
buttonText: send.destineeThirdParty|chill_entity_render_string,
|
||||
} %}
|
||||
{% else %}
|
||||
<a href="mailto:{{ send.destineeEmail }}">{{ send.destineeEmail }}</a>
|
||||
{% endif %}
|
||||
{% if not send.expired %}
|
||||
<p><small>{{ 'workflow.send_external_message.document_available_until'|trans({'expiration': send.expireAt}) }}</small></p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-7 text-end">
|
||||
<p><small>{{ 'workflow.external_views.number_of_views'|trans({'numberOfViews': send.views|length}) }}</small></p>
|
||||
{% if send.views|length > 0 %}
|
||||
<p><small>{{ 'workflow.external_views.last_view_at'|trans({'at': send.lastView.viewAt }) }}</small></p>
|
||||
<p>
|
||||
<small>
|
||||
<b>{{ 'workflow.public_views_by_ip'|trans }} :</b>
|
||||
{%- for ip, vs in send.viewsByIp -%}
|
||||
<span title="{% for v in vs %}{{ v.viewAt|format_datetime('short', 'short')|escape('html_attr') }}{% if not loop.last %}{{ ', '|escape('html_attr') }}{% endif %}{% endfor %}">{{ ip }} ({{ vs|length }}x)</span>
|
||||
{%- endfor -%}
|
||||
</small>
|
||||
</p>
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
@@ -0,0 +1,3 @@
|
||||
<h2>{{ 'workflow.signature_required_title'|trans({'nb_signatures': signatures|length}) }}</h2>
|
||||
|
||||
{{ include('@ChillMain/Workflow/_signature_list.html.twig') }}
|
@@ -0,0 +1,52 @@
|
||||
<div class="container">
|
||||
{% for s in signatures %}
|
||||
<div class="row row-hover align-items-center">
|
||||
<div class="col-sm-12 col-md-5">
|
||||
{% if s.signerKind == 'person' %}
|
||||
{% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with {
|
||||
action: 'show', displayBadge: true,
|
||||
targetEntity: { name: 'person', id: s.signer.id },
|
||||
buttonText: s.signer|chill_entity_render_string,
|
||||
isDead: s.signer.deathDate is not null
|
||||
} %}
|
||||
{% else %}
|
||||
{% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with {
|
||||
action: 'show', displayBadge: true,
|
||||
targetEntity: { name: 'user', id: s.signer.id },
|
||||
buttonText: s.signer|chill_entity_render_string,
|
||||
} %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-7 text-end">
|
||||
{% if s.isSigned %}
|
||||
<span class="text-end">{{ 'workflow.signature.signed_statement'|trans({ 'datetime' : s.stateDate }) }}</span>
|
||||
{% elseif s.isCanceled %}
|
||||
<span class="text-end">{{ 'workflow.signature.canceled_statement'|trans({ 'datetime' : s.stateDate }) }}</span>
|
||||
{% elseif s.isRejected%}
|
||||
<span class="text-end">{{ 'workflow.signature.rejected_statement'|trans({ 'datetime' : s.stateDate }) }}</span>
|
||||
{% else %}
|
||||
{% if (is_granted('CHILL_MAIN_ENTITY_WORKFLOW_SIGNATURE_CANCEL', s) or is_granted('CHILL_MAIN_ENTITY_WORKFLOW_SIGNATURE_SIGN', s)) %}
|
||||
<ul class="record_actions slim {% if is_small|default(false) %}small{% endif %}">
|
||||
{% if is_granted('CHILL_MAIN_ENTITY_WORKFLOW_SIGNATURE_REJECT', s) %}
|
||||
<li>
|
||||
<a class="btn btn-remove" href="{{ chill_path_add_return_path('chill_main_workflow_signature_reject', { 'id': s.id}) }}">{{ 'workflow.signature_zone.button_reject'|trans }}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if is_granted('CHILL_MAIN_ENTITY_WORKFLOW_SIGNATURE_CANCEL', s) %}
|
||||
<li>
|
||||
<a class="btn btn-misc" href="{{ chill_path_add_return_path('chill_main_workflow_signature_cancel', { 'id': s.id}) }}">{{ 'workflow.signature_zone.button_cancel'|trans }}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if is_granted('CHILL_MAIN_ENTITY_WORKFLOW_SIGNATURE_SIGN', s) %}
|
||||
<li>
|
||||
<a class="btn btn-misc" href="{{ chill_path_add_return_path('chill_main_workflow_signature_metadata', { 'signature_id': s.id}) }}"><i class="fa fa-pencil-square-o"></i> {{ 'workflow.signature_zone.button_sign'|trans }}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
@@ -1,12 +1,11 @@
|
||||
{% extends '@ChillMain/layout.html.twig' %}
|
||||
|
||||
{% block title %}
|
||||
{% block title -%}
|
||||
{{ 'Workflow'|trans }}
|
||||
{% endblock %}
|
||||
{%- endblock %}
|
||||
|
||||
{% block js %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_script_tags('mod_async_upload') }}
|
||||
{{ encore_entry_script_tags('mod_pickentity_type') }}
|
||||
{{ encore_entry_script_tags('mod_entity_workflow_subscribe') }}
|
||||
{{ encore_entry_script_tags('page_workflow_show') }}
|
||||
@@ -38,7 +37,10 @@
|
||||
<div class="mb-5">
|
||||
<h2>{{ handler.entityTitle(entity_workflow) }}</h2>
|
||||
|
||||
{{ macro.breadcrumb({'entity_workflow': entity_workflow}) }}
|
||||
{{ macro.breadcrumb(entity_workflow) }}
|
||||
{% if entity_workflow.isOnHoldAtCurrentStep %}
|
||||
<span class="badge bg-success rounded-pill" title="{{ 'workflow.On hold'|trans|escape('html_attr') }}">{{ 'workflow.On hold'|trans }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% include handler_template with handler_template_data|merge({'display_action': true }) %}
|
||||
@@ -57,18 +59,19 @@
|
||||
</section>
|
||||
|
||||
<section class="step my-4">{% include '@ChillMain/Workflow/_follow.html.twig' %}</section>
|
||||
<section class="step my-4">{% include '@ChillMain/Workflow/_decision.html.twig' %}</section>{#
|
||||
<section class="step my-4">{% include '@ChillMain/Workflow/_comment.html.twig' %}</section> #}
|
||||
{% if signatures|length > 0 %}
|
||||
<section class="step my-4">{% include '@ChillMain/Workflow/_signature.html.twig' %}</section>
|
||||
{% elseif entity_workflow.currentStep.sends|length > 0 %}
|
||||
<section class="step my-4">
|
||||
<h2>{{ 'workflow.external_views.title'|trans({'numberOfSends': entity_workflow.currentStep.sends|length }) }}</h2>
|
||||
{% include '@ChillMain/Workflow/_send_views_list.html.twig' with {'sends': entity_workflow.currentStep.sends} %}
|
||||
</section>
|
||||
{% else %}
|
||||
<section class="step my-4">{% include '@ChillMain/Workflow/_decision.html.twig' %}</section>
|
||||
{% endif %}
|
||||
{# <section class="step my-4">{% include '@ChillMain/Workflow/_comment.html.twig' %}</section> #}
|
||||
<section class="step my-4">{% include '@ChillMain/Workflow/_history.html.twig' %}</section>
|
||||
|
||||
{# useful ?
|
||||
<ul class="record_actions sticky-form-buttons">
|
||||
<li class="cancel">
|
||||
<a class="btn btn-cancel" href="{{ path('chill_main_workflow_list_dest') }}">
|
||||
{{ 'Back to the list'|trans }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
#}
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@@ -68,7 +68,10 @@
|
||||
|
||||
</button>
|
||||
<div>
|
||||
{{ macro.breadcrumb(l) }}
|
||||
{{ macro.breadcrumb(l.entity_workflow) }}
|
||||
{% if l.entity_workflow.isOnHoldAtCurrentStep %}
|
||||
<span class="badge bg-success rounded-pill" title="{{ 'workflow.On hold'|trans|escape('html_attr') }}">{{ 'workflow.On hold'|trans }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
@@ -3,24 +3,30 @@
|
||||
{% if step.previous is not null %}
|
||||
<li>
|
||||
<span class="item-key">{{ 'By'|trans ~ ' : ' }}</span>
|
||||
<b>{{ step.previous.transitionBy|chill_entity_render_box({'at_date': step.previous.transitionAt }) }}</b>
|
||||
<b>{% if step.previous.transitionBy is not null %}{{ step.previous.transitionBy|chill_entity_render_box({'at_date': step.previous.transitionAt }) }}{% else %}<span class="chill-no-data-statement">{{ 'workflow.Automated transition'|trans }}</span>{% endif %}</b>
|
||||
</li>
|
||||
<li>
|
||||
<span class="item-key">{{ 'Le'|trans ~ ' : ' }}</span>
|
||||
<b>{{ step.previous.transitionAt|format_datetime('short', 'short') }}</b>
|
||||
</li>
|
||||
<li>
|
||||
<span class="item-key">{{ 'workflow.For'|trans ~ ' : ' }}</span>
|
||||
<b>
|
||||
{% for d in step.destUser %}{{ d|chill_entity_render_string({'at_date': step.previous.transitionAt}) }}{% if not loop.last %}, {% endif %}{% endfor %}
|
||||
</b>
|
||||
</li>
|
||||
<li>
|
||||
<span class="item-key">{{ 'workflow.Cc'|trans ~ ' : ' }}</span>
|
||||
<b>
|
||||
{% for u in step.ccUser %}{{ u|chill_entity_render_string({'at_date': step.previous.transitionAt }) }}{% if not loop.last %}, {% endif %}{% endfor %}
|
||||
</b>
|
||||
</li>
|
||||
{% if step.destUser|length > 0 or step.destUserGroups|length > 0 %}
|
||||
<li>
|
||||
<span class="item-key">{{ 'workflow.For'|trans ~ ' : ' }}</span>
|
||||
<b>
|
||||
{% for d in step.destUser %}<span class="badge-user">{{ d|chill_entity_render_string({'at_date': step.previous.transitionAt}) }}</span>{% if not loop.last %}, {% endif -%}{% endfor -%}
|
||||
{%- if step.destUser|length > 0 and step.destUserGroups|length > 0 %}, {% endif -%}
|
||||
{%- for d in step.destUserGroups %}{{ d|chill_entity_render_box }}{% if not loop.last %}, {% endif %}{% endfor -%}
|
||||
</b>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if step.ccUser|length > 0 %}
|
||||
<li>
|
||||
<span class="item-key">{{ 'workflow.Cc'|trans ~ ' : ' }}</span>
|
||||
<b>
|
||||
{% for u in step.ccUser %}{{ u|chill_entity_render_string({'at_date': step.previous.transitionAt }) }}{% if not loop.last %}, {% endif %}{% endfor %}
|
||||
</b>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<li>
|
||||
<span class="item-key">{{ 'workflow.Created by'|trans ~ ' : ' }}</span>
|
||||
@@ -40,16 +46,16 @@
|
||||
{% 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 labels = workflow_metadata(step.entityWorkflow, 'label', transition, step.entityWorkflow.workflowName) %}
|
||||
{% set label = labels is null ? step.previous.transitionAfter : labels|localize_translatable_string %}
|
||||
{{ label }}
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro breadcrumb(_ctx) %}
|
||||
{% macro breadcrumb(entity_workflow) %}
|
||||
<div class="breadcrumb">
|
||||
{% for step in _ctx.entity_workflow.stepsChained %}
|
||||
{% set labels = workflow_metadata(_ctx.entity_workflow, 'label', step.currentStep) %}
|
||||
{% for step in entity_workflow.stepsChained %}
|
||||
{% set labels = workflow_metadata(entity_workflow, 'label', step.currentStep, entity_workflow.workflowName) %}
|
||||
{% set label = labels is null ? step.currentStep : labels|localize_translatable_string %}
|
||||
{% set popTitle = _self.popoverTitle(step) %}
|
||||
{% set popContent = _self.popoverContent(step) %}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user