Compare commits

..

10 Commits

37 changed files with 451 additions and 191 deletions

View File

@@ -257,10 +257,12 @@ class Activity implements AccompanyingPeriodLinkedWithSocialIssuesEntityInterfac
/**
* Add a social issue.
*
* Note: the social issue consistency (the fact that only youngest social issues
* Note: the social issue consistency (the fact that only yougest social issues
* are kept) is processed by an entity listener:
*
* @see{\Chill\PersonBundle\AccompanyingPeriod\SocialIssueConsistency\AccompanyingPeriodSocialIssueConsistencyEntityListener}
*
* @return $this
*/
public function addSocialIssue(SocialIssue $socialIssue): self
{
@@ -268,10 +270,6 @@ class Activity implements AccompanyingPeriodLinkedWithSocialIssuesEntityInterfac
$this->socialIssues[] = $socialIssue;
}
if ($this->getAccompanyingPeriod() !== null) {
$this->getAccompanyingPeriod()->addSocialIssue($socialIssue);
}
return $this;
}
@@ -552,10 +550,6 @@ class Activity implements AccompanyingPeriodLinkedWithSocialIssuesEntityInterfac
{
$this->accompanyingPeriod = $accompanyingPeriod;
foreach ($this->getSocialIssues() as $issue) {
$this->accompanyingPeriod->addSocialIssue($issue);
}
return $this;
}

View File

@@ -22,7 +22,6 @@ use Chill\MainBundle\Entity\User;
use DateTimeImmutable;
use LogicException;
use Psr\Log\LoggerInterface;
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use function array_key_exists;
@@ -75,18 +74,9 @@ class MapCalendarToUser
public function getDefaultUserCalendar(string $idOrUserPrincipalName): ?array
{
try {
$value = $this->machineHttpClient->request('GET', "users/{$idOrUserPrincipalName}/calendars", [
'query' => ['$filter' => 'isDefaultCalendar eq true'],
])->toArray()['value'];
} catch (ClientExceptionInterface $e) {
$this->logger->error('[MapCalendarToUser] Error while listing calendars for a user', [
'http_status_code' => $e->getResponse()->getStatusCode(),
'id_user' => $idOrUserPrincipalName,
]);
return null;
}
return $value[0] ?? null;
}

View File

@@ -34,7 +34,6 @@ use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Security;
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
@@ -65,8 +64,6 @@ class MSGraphRemoteCalendarConnector implements RemoteCalendarConnectorInterface
private OnBehalfOfUserHttpClient $userHttpClient;
private Security $security;
public function __construct(
CalendarRepository $calendarRepository,
CalendarRangeRepository $calendarRangeRepository,
@@ -77,8 +74,7 @@ class MSGraphRemoteCalendarConnector implements RemoteCalendarConnectorInterface
OnBehalfOfUserHttpClient $userHttpClient,
RemoteEventConverter $remoteEventConverter,
TranslatorInterface $translator,
UrlGeneratorInterface $urlGenerator,
Security $security
UrlGeneratorInterface $urlGenerator
) {
$this->calendarRepository = $calendarRepository;
$this->calendarRangeRepository = $calendarRangeRepository;
@@ -90,7 +86,6 @@ class MSGraphRemoteCalendarConnector implements RemoteCalendarConnectorInterface
$this->translator = $translator;
$this->urlGenerator = $urlGenerator;
$this->userHttpClient = $userHttpClient;
$this->security = $security;
}
public function countEventsForUser(User $user, DateTimeImmutable $startDate, DateTimeImmutable $endDate): int
@@ -138,24 +133,6 @@ class MSGraphRemoteCalendarConnector implements RemoteCalendarConnectorInterface
public function isReady(): bool
{
$user = $this->security->getUser();
if (!$user instanceof User) {
// this is not a user from chill. This is not the role of this class to
// restrict access, so we will just say that we do not have to do anything more
// here...
return true;
}
if (null === $this->mapCalendarToUser->getUserId($user)) {
// this user is not mapped with remote calendar. The user will have to wait for
// the next calendar subscription iteration
$this->logger->debug('mark user ready for msgraph calendar as he does not have any mapping', [
'userId' => $user->getId(),
]);
return true;
}
return $this->tokenStorage->hasToken();
}

View File

@@ -14,6 +14,7 @@ namespace Chill\MainBundle\Controller;
use Chill\MainBundle\Entity\Notification;
use Chill\MainBundle\Entity\NotificationComment;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
use Chill\MainBundle\Form\NotificationCommentType;
use Chill\MainBundle\Form\NotificationType;
use Chill\MainBundle\Notification\Exception\NotificationHandlerNotFound;
@@ -21,6 +22,7 @@ use Chill\MainBundle\Notification\NotificationHandlerManager;
use Chill\MainBundle\Pagination\PaginatorFactory;
use Chill\MainBundle\Repository\NotificationRepository;
use Chill\MainBundle\Repository\UserRepository;
use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository;
use Chill\MainBundle\Security\Authorization\NotificationVoter;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
@@ -68,7 +70,8 @@ class NotificationController extends AbstractController
NotificationHandlerManager $notificationHandlerManager,
PaginatorFactory $paginatorFactory,
TranslatorInterface $translator,
UserRepository $userRepository
UserRepository $userRepository,
EntityWorkflowRepository $entityWorkflowRepository
) {
$this->em = $em;
$this->logger = $logger;
@@ -79,6 +82,7 @@ class NotificationController extends AbstractController
$this->paginatorFactory = $paginatorFactory;
$this->translator = $translator;
$this->userRepository = $userRepository;
$this->entityWorkflowRepository = $entityWorkflowRepository;
}
/**
@@ -345,6 +349,7 @@ class NotificationController extends AbstractController
'appendCommentForm' => isset($appendCommentForm) ? $appendCommentForm->createView() : null,
'editedCommentForm' => isset($editedCommentForm) ? $editedCommentForm->createView() : null,
'editedCommentId' => $commentId ?? null,
'notificationCc' => $this->isNotificationCc($notification),
]);
// we mark the notification as read after having computed the response
@@ -364,6 +369,21 @@ class NotificationController extends AbstractController
];
}
private function isNotificationCc(Notification $notification): bool
{
$notificationCc = false;
if ($notification->getRelatedEntityClass() === EntityWorkflow::class) {
$relatedEntity = $this->entityWorkflowRepository->findOneBy(['id' => $notification->getRelatedEntityId()]);
if ($relatedEntity->getCurrentStepCreatedBy() !== $this->security->getUser()) {
$notificationCc = true;
}
}
return $notificationCc;
}
private function itemsForTemplate(array $notifications): array
{
$templateData = [];
@@ -373,6 +393,7 @@ class NotificationController extends AbstractController
'template' => $this->notificationHandlerManager->getTemplate($notification),
'template_data' => $this->notificationHandlerManager->getTemplateData($notification),
'notification' => $notification,
'isNotificationCc' => $this->isNotificationCc($notification),
];
}

View File

@@ -93,6 +93,45 @@ class WorkflowApiController
);
}
/**
* Return a list of workflow which are waiting an action for the user.
*
* @Route("/api/1.0/main/workflow/my-cc", methods={"GET"})
*/
public function myWorkflowCc(Request $request): JsonResponse
{
if (!$this->security->isGranted('ROLE_USER') || !$this->security->getUser() instanceof User) {
throw new AccessDeniedException();
}
$total = $this->entityWorkflowRepository->countByCc($this->security->getUser());
if ($request->query->getBoolean('countOnly', false)) {
return new JsonResponse(
$this->serializer->serialize(new Counter($total), 'json'),
JsonResponse::HTTP_OK,
[],
true
);
}
$paginator = $this->paginatorFactory->create($total);
$workflows = $this->entityWorkflowRepository->findByCc(
$this->security->getUser(),
['id' => 'DESC'],
$paginator->getItemsPerPage(),
$paginator->getCurrentPageFirstItemNumber()
);
return new JsonResponse(
$this->serializer->serialize(new Collection($workflows, $paginator), 'json', ['groups' => ['read']]),
JsonResponse::HTTP_OK,
[],
true
);
}
/**
* @Route("/api/1.0/main/workflow/{id}/subscribe", methods={"POST"})
*/

View File

@@ -224,6 +224,33 @@ class WorkflowController extends AbstractController
);
}
/**
* @Route("/{_locale}/main/workflow/list/cc", name="chill_main_workflow_list_cc")
*/
public function myWorkflowsCc(Request $request): Response
{
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED');
$total = $this->entityWorkflowRepository->countByDest($this->getUser());
$paginator = $this->paginatorFactory->create($total);
$workflows = $this->entityWorkflowRepository->findByCc(
$this->getUser(),
['createdAt' => 'DESC'],
$paginator->getItemsPerPage(),
$paginator->getCurrentPageFirstItemNumber()
);
return $this->render(
'@ChillMain/Workflow/list.html.twig',
[
'workflows' => $this->buildHandler($workflows),
'paginator' => $paginator,
'step' => 'cc',
]
);
}
/**
* @Route("/{_locale}/main/workflow/list/dest", name="chill_main_workflow_list_dest")
*/
@@ -318,6 +345,7 @@ class WorkflowController extends AbstractController
}
// 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();

View File

@@ -41,6 +41,13 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
use TrackUpdateTrait;
/**
* a list of future cc users for the next steps.
*
* @var array|User[]
*/
public array $futureCcUsers = [];
/**
* a list of future dest emails for the next steps.
*

View File

@@ -32,6 +32,12 @@ class EntityWorkflowStep
*/
private string $accessKey;
/**
* @ORM\ManyToMany(targetEntity=User::class)
* @ORM\JoinTable(name="chill_main_workflow_entity_step_cc_user")
*/
private Collection $ccUser;
/**
* @ORM\Column(type="text", options={"default": ""})
*/
@@ -114,11 +120,21 @@ class EntityWorkflowStep
public function __construct()
{
$this->ccUser = new ArrayCollection();
$this->destUser = new ArrayCollection();
$this->destUserByAccessKey = new ArrayCollection();
$this->accessKey = bin2hex(openssl_random_pseudo_bytes(32));
}
public function addCcUser(User $user): self
{
if (!$this->ccUser->contains($user)) {
$this->ccUser[] = $user;
}
return $this;
}
public function addDestEmail(string $email): self
{
if (!in_array($email, $this->destEmail, true)) {
@@ -167,6 +183,11 @@ class EntityWorkflowStep
);
}
public function getCcUser(): Collection
{
return $this->ccUser;
}
public function getComment(): string
{
return $this->comment;
@@ -261,6 +282,13 @@ class EntityWorkflowStep
return true;
}
public function removeCcUser(User $user): self
{
$this->ccUser->removeElement($user);
return $this;
}
public function removeDestEmail(string $email): self
{
$this->destEmail = array_filter($this->destEmail, static function (string $existing) use ($email) {

View File

@@ -155,6 +155,12 @@ class WorkflowStepType extends AbstractType
'multiple' => true,
'mapped' => false,
])
->add('future_cc_users', PickUserDynamicType::class, [
'label' => 'workflow.cc for next steps',
'multiple' => true,
'mapped' => false,
'required' => false,
])
->add('future_dest_emails', ChillCollectionType::class, [
'label' => 'workflow.dest by email',
'help' => 'workflow.dest by email help',

View File

@@ -74,7 +74,9 @@ class NotificationTwigExtensionRuntime implements RuntimeExtensionInterface
}
return $environment->render('@ChillMain/Notification/extension_list_notifications_for.html.twig', [
'notifications' => $notifications, 'appendCommentForms' => $appendCommentForms,
'notifications' => $notifications,
'appendCommentForms' => $appendCommentForms,
'notificationCc' => false,
]);
}
}

View File

@@ -27,6 +27,13 @@ class EntityWorkflowRepository implements ObjectRepository
$this->repository = $entityManager->getRepository(EntityWorkflow::class);
}
public function countByCc(User $user): int
{
$qb = $this->buildQueryByCc($user)->select('count(ew)');
return (int) $qb->getQuery()->getSingleScalarResult();
}
public function countByDest(User $user): int
{
$qb = $this->buildQueryByDest($user)->select('count(ew)');
@@ -103,6 +110,19 @@ class EntityWorkflowRepository implements ObjectRepository
return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
}
public function findByCc(User $user, ?array $orderBy = null, $limit = null, $offset = null): array
{
$qb = $this->buildQueryByCc($user)->select('ew');
foreach ($orderBy as $key => $sort) {
$qb->addOrderBy('ew.' . $key, $sort);
}
$qb->setMaxResults($limit)->setFirstResult($offset);
return $qb->getQuery()->getResult();
}
public function findByDest(User $user, ?array $orderBy = null, $limit = null, $offset = null): array
{
$qb = $this->buildQueryByDest($user)->select('ew');
@@ -165,6 +185,25 @@ class EntityWorkflowRepository implements ObjectRepository
return EntityWorkflow::class;
}
private function buildQueryByCc(User $user): QueryBuilder
{
$qb = $this->repository->createQueryBuilder('ew');
$qb->join('ew.steps', 'step');
$qb->where(
$qb->expr()->andX(
$qb->expr()->isMemberOf(':user', 'step.ccUser'),
$qb->expr()->isNull('step.transitionAfter'),
$qb->expr()->eq('step.isFinal', "'FALSE'")
)
);
$qb->setParameter('user', $user);
return $qb;
}
private function buildQueryByDest(User $user): QueryBuilder
{
$qb = $this->repository->createQueryBuilder('ew');

View File

@@ -1,88 +1,25 @@
<template>
<div class="alert alert-light">{{ $t('my_workflows.description') }}</div>
<span v-if="noResults" class="chill-no-data-statement">{{ $t('no_data') }}</span>
<tab-table v-else>
<template v-slot:thead>
<th scope="col">{{ $t('Object_workflow') }}</th>
<th scope="col">{{ $t('Step') }}</th>
<th scope="col">{{ $t('concerned_users') }}</th>
<th scope="col"></th>
</template>
<template v-slot:tbody>
<tr v-for="(w, i) in workflows.results" :key="`workflow-${i}`">
<td>{{ w.title }}</td>
<td>
<div class="workflow">
<div class="breadcrumb">
<i class="fa fa-circle me-1 text-chill-yellow mx-2"></i>
<span class="mx-2">{{ getStep(w) }}</span>
</div>
</div>
</td>
<td v-if="w.datas.persons !== null">
<span v-for="p in w.datas.persons" class="me-1" :key="p.id">
<on-the-fly
:type="p.type"
:id="p.id"
:buttonText="p.textAge"
:displayBadge="'true' === 'true'"
action="show">
</on-the-fly>
</span>
</td>
<td>
<a class="btn btn-sm btn-show" :href="getUrl(w)">
{{ $t('show_entity', { entity: $t('the_workflow') }) }}
</a>
</td>
</tr>
</template>
</tab-table>
<my-workflows-table :workflows="workflows" />
<div class="alert alert-light">{{ $t('my_workflows.description_cc') }}</div>
<my-workflows-table :workflows="workflowsCc" />
</template>
<script>
import { mapState, mapGetters } from "vuex";
import TabTable from "./TabTable";
import OnTheFly from 'ChillMainAssets/vuejs/OnTheFly/components/OnTheFly';
import { mapState } from "vuex";
import MyWorkflowsTable from './MyWorkflowsTable.vue';
export default {
name: "MyWorkflows",
components: {
TabTable,
OnTheFly
MyWorkflowsTable
},
computed: {
...mapState([
'workflows',
'workflowsCc',
]),
...mapGetters([
'isWorkflowsLoaded',
]),
noResults() {
if (!this.isWorkflowsLoaded) {
return false;
} else {
return this.workflows.count === 0;
}
},
},
methods: {
getUrl(w) {
return `/fr/main/workflow/${w.id}/show`;
},
getStep(w) {
const lastStep = w.steps.length - 1
return w.steps[lastStep].currentStep.text;
}
},
}
</script>
<style scoped>
span.outdated {
font-weight: bold;
color: var(--bs-warning);
}
</style>

View File

@@ -0,0 +1,83 @@
<template>
<span v-if="hasNoResults(workflows)" class="chill-no-data-statement">{{ $t('no_data') }}</span>
<tab-table v-else>
<template v-slot:thead>
<th scope="col">{{ $t('Object_workflow') }}</th>
<th scope="col">{{ $t('Step') }}</th>
<th scope="col">{{ $t('concerned_users') }}</th>
<th scope="col"></th>
</template>
<template v-slot:tbody>
<tr v-for="(w, i) in workflows.results" :key="`workflow-${i}`">
<td>{{ w.title }}</td>
<td>
<div class="workflow">
<div class="breadcrumb">
<i class="fa fa-circle me-1 text-chill-yellow mx-2"></i>
<span class="mx-2">{{ getStep(w) }}</span>
</div>
</div>
</td>
<td v-if="w.datas.persons !== null">
<span v-for="p in w.datas.persons" class="me-1" :key="p.id">
<on-the-fly
:type="p.type"
:id="p.id"
:buttonText="p.textAge"
:displayBadge="'true' === 'true'"
action="show">
</on-the-fly>
</span>
</td>
<td>
<a class="btn btn-sm btn-show" :href="getUrl(w)">
{{ $t('show_entity', { entity: $t('the_workflow') }) }}
</a>
</td>
</tr>
</template>
</tab-table>
</template>
<script>
import { mapGetters } from "vuex";
import TabTable from "./TabTable";
import OnTheFly from 'ChillMainAssets/vuejs/OnTheFly/components/OnTheFly';
export default {
name: "MyWorkflows",
components: {
TabTable,
OnTheFly
},
props: ['workflows'],
computed: {
...mapGetters([
'isWorkflowsLoaded',
]),
},
methods: {
hasNoResults(workflows) {
if (!this.isWorkflowsLoaded) {
return false;
} else {
return workflows.count === 0;
}
},
getUrl(w) {
return `/fr/main/workflow/${w.id}/show`;
},
getStep(w) {
const lastStep = w.steps.length - 1
return w.steps[lastStep].currentStep.text;
}
},
}
</script>
<style scoped>
span.outdated {
font-weight: bold;
color: var(--bs-warning);
}
</style>

View File

@@ -24,7 +24,8 @@ const appMessages = {
},
my_workflows: {
tab: "Mes workflows",
description: "Liste des workflows en attente d'une action."
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",

View File

@@ -22,6 +22,7 @@ const store = createStore({
accompanyingCourses: {},
notifications: {},
workflows: {},
workflowsCc: {},
errorMsg: [],
loading: false
},
@@ -87,6 +88,9 @@ const store = createStore({
addWorkflows(state, workflows) {
state.workflows = workflows;
},
addWorkflowsCc(state, workflows) {
state.workflowsCc = workflows;
},
setLoading(state, bool) {
state.loading = bool;
},
@@ -195,17 +199,23 @@ const store = createStore({
case 'MyWorkflows':
if (!getters.isWorflowsLoaded) {
commit('setLoading', true);
const url = '/api/1.0/main/workflow/my';
makeFetch('GET', url)
makeFetch('GET', '/api/1.0/main/workflow/my')
.then((response) => {
console.log('workflows', response)
commit('addWorkflows', response);
makeFetch('GET', '/api/1.0/main/workflow/my-cc')
.then((response) => {
commit('addWorkflowsCc', response);
commit('setLoading', false);
})
.catch((error) => {
commit('catchError', error);
throw error;
});
})
.catch((error) => {
commit('catchError', error);
throw error;
});
}
break;
default:

View File

@@ -30,11 +30,27 @@
{% endif %}
{% if c.notification.addressees|length > 0 %}
<li class="notification-to">
{% if c.notification_cc is defined %}
{% if c.notification_cc %}
<span class="item-key">
<abbr title="{{ 'notification.sent_cc'|trans }}">
{{ 'notification.cc'|trans }} :
</abbr>
</span>
{% else %}
<span class="item-key">
<abbr title="{{ 'notification.sent_to'|trans }}">
{{ 'notification.to'|trans }} :
</abbr>
</span>
{% endif %}
{% else %}
<span class="item-key">
<abbr title="{{ 'notification.sent_to'|trans }}">
{{ 'notification.to'|trans }} :
</abbr>
</span>
{% endif %}
{% for a in c.notification.addressees %}
<span class="badge-user">
{{ a|chill_entity_render_string }}

View File

@@ -6,6 +6,7 @@
'full_content': true,
'fold_item': true,
'action_button': true,
'notification_cc': notificationCc,
} %}{#
#}
{% endfor %}

View File

@@ -50,7 +50,8 @@
{% for data in datas %}
{% set notification = data.notification %}
{% include 'ChillMainBundle:Notification:_list_item.html.twig' with {
'fold_item': true
'fold_item': true,
'notification_cc': data.isNotificationCc
} %}
{% endfor %}
</div>

View File

@@ -27,7 +27,8 @@
},
'action_button': false,
'full_content': true,
'fold_item': false
'fold_item': false,
'notification_cc': notificationCc
} %}
</div>

View File

@@ -65,6 +65,8 @@
<div id="futureDests">
{{ form_row(transition_form.future_dest_users) }}
{{ form_row(transition_form.future_cc_users) }}
{{ form_row(transition_form.future_dest_emails) }}
</div>

View File

@@ -81,6 +81,15 @@
</ul>
{% endif %}
{% if step.ccUser|length > 0 %}
<p><b>{{ 'workflow.Users put in Cc'|trans }}&nbsp;: </b></p>
<ul>
{% for u in step.ccUser %}
<li>{{ u|chill_entity_render_box }}</li>
{% endfor %}
</ul>
{% endif %}
{% if entity_workflow.currentStep.destEmail|length > 0 %}
<p><b>{{ 'workflow.An access key was also sent to those addresses'|trans }}&nbsp;:</b></p>
<ul>

View File

@@ -7,7 +7,7 @@
{% endblock %}
{% block content %}
<div class="col-10 workflow">
<div class="col-12 workflow">
<h1 class="mb-5">{{ block('title') }}</h1>
@@ -25,6 +25,12 @@
{{ 'workflow.dest'|trans }}
</a>
</li>
<li class="nav-item">
<a href="{{ path('chill_main_workflow_list_cc') }}"
class="nav-link {% if step == 'cc' %}active{% endif %}">
{{ 'workflow.cc'|trans }}
</a>
</li>
<li class="nav-item">
<a href="{{ path('chill_main_workflow_list_previous_without_reaction') }}"
class="nav-link {% if step == 'previous_without_reaction' %}active{% endif %}">

View File

@@ -15,6 +15,12 @@
{% for d in step.destUser %}{{ d|chill_entity_render_string }}{% 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 }}{% if not loop.last %}, {% endif %}{% endfor %}
</b>
</li>
{% else %}
<li>
<span class="item-key">{{ 'workflow.Created by'|trans ~ ' : ' }}</span>

View File

@@ -50,6 +50,10 @@ class EntityWorkflowTransitionEventSubscriber implements EventSubscriberInterfac
/** @var EntityWorkflow $entityWorkflow */
$entityWorkflow = $event->getSubject();
foreach ($entityWorkflow->futureCcUsers as $user) {
$entityWorkflow->getCurrentStep()->addCcUser($user);
}
foreach ($entityWorkflow->futureDestUsers as $user) {
$entityWorkflow->getCurrentStep()->addDestUser($user);
}

View File

@@ -85,7 +85,9 @@ class NotificationOnTransition implements EventSubscriberInterface
// the subscriber to final, only if final
$entityWorkflow->isFinal() ? $entityWorkflow->getSubscriberToFinal()->toArray() : [],
// the dests for the current step
$entityWorkflow->getCurrentStep()->getDestUser()->toArray()
$entityWorkflow->getCurrentStep()->getDestUser()->toArray(),
// the cc users for the current step
$entityWorkflow->getCurrentStep()->getCcUser()->toArray()
) as $dest) {
$dests[spl_object_hash($dest)] = $dest;
}

View File

@@ -826,4 +826,20 @@ paths:
$ref: '#/components/schemas/Workflow'
403:
description: "Unauthorized"
/1.0/main/workflow/my-cc:
get:
tags:
- workflow
summary: Return a list of workflows for which user was notified in Cc
responses:
200:
description: "ok"
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Workflow'
403:
description: "Unauthorized"

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\Migrations\Main;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20230321134155 extends AbstractMigration
{
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE chill_main_workflow_entity_step_cc_user DROP CONSTRAINT FK_9FC79037E6AF9D4');
$this->addSql('ALTER TABLE chill_main_workflow_entity_step_cc_user DROP CONSTRAINT FK_9FC7903A76ED395');
$this->addSql('DROP TABLE chill_main_workflow_entity_step_cc_user');
}
public function getDescription(): string
{
return 'Add cc User to workflow step';
}
public function up(Schema $schema): void
{
$this->addSql('CREATE TABLE chill_main_workflow_entity_step_cc_user (entityworkflowstep_id INT NOT NULL, user_id INT NOT NULL, PRIMARY KEY(entityworkflowstep_id, user_id))');
$this->addSql('CREATE INDEX IDX_9FC79037E6AF9D4 ON chill_main_workflow_entity_step_cc_user (entityworkflowstep_id)');
$this->addSql('CREATE INDEX IDX_9FC7903A76ED395 ON chill_main_workflow_entity_step_cc_user (user_id)');
$this->addSql('ALTER TABLE chill_main_workflow_entity_step_cc_user ADD CONSTRAINT FK_9FC79037E6AF9D4 FOREIGN KEY (entityworkflowstep_id) REFERENCES chill_main_workflow_entity_step (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE chill_main_workflow_entity_step_cc_user ADD CONSTRAINT FK_9FC7903A76ED395 FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
}
}

View File

@@ -447,6 +447,7 @@ workflow:
Created by: Créé par
My decision: Ma décision
Next step: Prochaine étape
cc for next steps: Utilisateurs en copie
dest for next steps: Utilisateurs qui valideront la prochaine étape
Freeze: Geler
Freezed: Gelé
@@ -465,12 +466,14 @@ workflow:
Document (n°%doc%): "Document (n°%doc%)"
Work (n°%w%): "Action d'accompagnement (n°%w%)"
subscribed: Workflows suivis
cc: Workflows dont je suis en copie
dest: Workflows en attente d'action
you subscribed to all steps: Vous recevrez une notification à chaque étape
you subscribed to final step: Vous recevrez une notification à l'étape finale
Current step: Étape actuelle
Comment on last change: Commentaire à la transition précédente
Users allowed to apply transition: Utilisateurs pouvant valider cette étape
Users put in Cc: Utilisateurs mis en copie
Workflow deleted with success: Le workflow a été supprimé
Delete workflow ?: Supprimer le workflow ?
Are you sure you want to delete this workflow ?: Êtes-vous sûr·e de vouloir supprimer ce workflow ?
@@ -487,6 +490,7 @@ workflow:
Previous transitionned: Anciens workflows
Previous workflow transitionned help: Workflows où vous avez exécuté une action.
For: Pour
Cc: Cc
You must select a next step, pick another decision if no next steps are available: Il faut une prochaine étape. Choissisez une autre décision si nécessaire.
An access key was also sent to those addresses: Un lien d'accès a été envoyé à ces adresses
Those users are also granted to apply a transition by using an access key: Ces utilisateurs ont obtenu l'accès grâce au lien reçu par email
@@ -521,7 +525,9 @@ notification:
list: Notifications
Sent: Envoyé
to: À
cc: Cc
sent_to: Destinataire(s)
sent_cc: En copie
from: De
received_from: Expéditeur
you were notified by %sender%: Vous avez été notifié par %sender%

View File

@@ -373,6 +373,7 @@ workflow:
Created by: Créé par
My decision: Ma décision
Next step: Prochaine étape
cc for next steps: Utilisateurs en copie
dest for next steps: Utilisateurs qui valideront la prochaine étape
Freeze: Geler
Freezed: Gelé
@@ -392,11 +393,13 @@ workflow:
Work (n°%w%): "Action d'accompagnement (n°%w%)"
subscribed: Workflows suivis
dest: Workflows en attente d'action
cc: Workflows dont je suis en copie
you subscribed to all steps: Vous recevrez une notification à chaque étape
you subscribed to final step: Vous recevrez une notification à l'étape finale
Current step: Étape actuelle
Comment on last change: Commentaire à la transition précédente
Users allowed to apply transition: Utilisateurs pouvant valider cette étape
Users put in Cc: Utilisateurs mis en copie
Workflow deleted with success: Le workflow a été supprimé
Delete workflow ?: Supprimer le workflow ?
Are you sure you want to delete this workflow ?: Êtes-vous sûr·e de vouloir supprimer ce workflow ?
@@ -414,6 +417,7 @@ workflow:
Previous transitionned: Anciens workflows
Previous workflow transitionned help: Workflows où vous avez exécuté une action.
For: Pour
Cc: Cc
Subscribe final: Recevoir une notification à l'étape finale
@@ -442,7 +446,9 @@ notification:
list: Notifications
Sent: Envoyé
to: À
cc: Cc
sent_to: Destinataire(s)
sent_cc: En copie
from: De
received_from: Expéditeur
you were notified by %sender%: Vous avez été notifié par %sender%

View File

@@ -76,17 +76,8 @@ class SimilarPersonMatcher
$qb->select('p')
->from(Person::class, 'p')
->join('p.centerHistory', 'center_history')
->where('SIMILARITY(p.fullnameCanonical, UNACCENT(LOWER(:fullName))) >= :precision')
->andWhere($qb->expr()->in('center_history.center', ':centers'))
->andWhere($qb->expr()->andX(
$qb->expr()->lte('center_history.startDate', 'CURRENT_DATE()'),
$qb->expr()->orX(
$qb->expr()->isNull('center_history.endDate'),
$qb->expr()->gt('center_history.endDate', 'CURRENT_DATE()')
)
))
;
->andWhere($qb->expr()->in('p.center', ':centers'));
$qb
->setParameter('fullName', $this->personRender->renderString($person, []))
@@ -126,6 +117,7 @@ class SimilarPersonMatcher
$qb->setParameter('fullName', $this->personRender->renderString($person, []));
}
dump($qb->getQuery());
return $qb->getQuery()->getResult();
}
}

View File

@@ -54,7 +54,7 @@ class AccompanyingPeriodValidityValidator extends ConstraintValidator
$activities = $this->activityRepository->findBy(['accompanyingPeriod' => $period]);
foreach ($activities as $activity) {
$socialIssues = array_merge($socialIssues, $activity->getSocialIssues()->toArray());
$socialIssues = $activity->getSocialIssues()->toArray();
}
foreach ($period->getWorks() as $work) {
@@ -64,7 +64,7 @@ class AccompanyingPeriodValidityValidator extends ConstraintValidator
$socialIssuesByKey = [];
foreach ($socialIssues as $si) {
$socialIssuesByKey[spl_object_hash($si)] = $si;
$socialIssuesByKey[$si->getId()] = $si;
}
$periodIssuesWithAncestors = [];
@@ -75,7 +75,7 @@ class AccompanyingPeriodValidityValidator extends ConstraintValidator
$periodIssuesWithAncestors,
array_map(
static function (SocialIssue $si) {
return spl_object_hash($si);
return $si->getId();
},
$si->getAncestors(true)
)

View File

@@ -11,15 +11,12 @@ declare(strict_types=1);
namespace Chill\ReportBundle\Controller;
use Chill\DocStoreBundle\Security\Authorization\PersonDocumentVoter;
use Chill\MainBundle\Pagination\PaginatorFactory;
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Chill\MainBundle\Security\Resolver\CenterResolverManagerInterface;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Privacy\PrivacyEvent;
use Chill\ReportBundle\Entity\Report;
use Chill\ReportBundle\Form\ReportType;
use Chill\ReportBundle\Security\Authorization\ReportVoter;
use DateTime;
use RuntimeException;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
@@ -28,7 +25,7 @@ use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Role\Role;
use Symfony\Component\Security\Core\Security;
use function count;
/**
@@ -51,25 +48,17 @@ class ReportController extends AbstractController
*/
protected $paginator;
private CenterResolverManagerInterface $centerResolverManager;
private Security $security;
/**
* ReportController constructor.
*/
public function __construct(
EventDispatcherInterface $eventDispatcher,
AuthorizationHelper $authorizationHelper,
PaginatorFactory $paginator,
CenterResolverManagerInterface $centerResolverManager,
Security $security
PaginatorFactory $paginator
) {
$this->eventDispatcher = $eventDispatcher;
$this->authorizationHelper = $authorizationHelper;
$this->paginator = $paginator;
$this->centerResolverManager = $centerResolverManager;
$this->security = $security;
}
/**
@@ -131,7 +120,7 @@ class ReportController extends AbstractController
->trans('The form is not valid. The report has not been created !')
);
return $this->render('@ChillReport/Report/new.html.twig', [
return $this->render('ChillReportBundle:Report:new.html.twig', [
'entity' => $entity,
'form' => $form->createView(),
'person' => $person,
@@ -151,7 +140,7 @@ class ReportController extends AbstractController
$em = $this->getDoctrine()->getManager();
/** @var Report $report */
$report = $em->getRepository(Report::class)->find($report_id);
$report = $em->getRepository('ChillReportBundle:Report')->find($report_id);
if (!$report) {
throw $this->createNotFoundException(
@@ -200,7 +189,7 @@ class ReportController extends AbstractController
$cFGroup = $em->getRepository(\Chill\CustomFieldsBundle\Entity\CustomFieldsGroup::class)->find($cf_group_id);
$reports = $em->getRepository('ChillReportBundle:Report')->findByCFGroup($cFGroup);
$response = $this->render('@ChillReport/Report/export.csv.twig', [
$response = $this->render('ChillReportBundle:Report:export.csv.twig', [
'reports' => $reports,
'cf_group' => $cFGroup,
]);
@@ -229,9 +218,9 @@ class ReportController extends AbstractController
$reachableScopes = $this->authorizationHelper
->getReachableScopes(
$this->security->getUser(),
ReportVoter::SEE,
$this->centerResolverManager->resolveCenters($person)
$this->getUser(),
new Role('CHILL_REPORT_SEE'),
$person->getCenter()
);
$total = $em
@@ -309,7 +298,7 @@ class ReportController extends AbstractController
$form = $this->createCreateForm($entity, $person, $cFGroup);
return $this->render('@ChillReport/Report/new.html.twig', [
return $this->render('ChillReportBundle:Report:new.html.twig', [
'entity' => $entity,
'form' => $form->createView(),
'person' => $person,
@@ -526,7 +515,7 @@ class ReportController extends AbstractController
$person = $em->getRepository(\Chill\PersonBundle\Entity\Person::class)->find($person_id);
$entity = $em->getRepository(Report::class)->find($report_id);
$entity = $em->getRepository('ChillReportBundle:Report')->find($report_id);
if (!$entity || !$person) {
throw $this->createNotFoundException(
@@ -543,7 +532,7 @@ class ReportController extends AbstractController
]);
$this->eventDispatcher->dispatch(PrivacyEvent::PERSON_PRIVACY_EVENT, $event);
return $this->render('@ChillReport/Report/view.html.twig', [
return $this->render('ChillReportBundle:Report:view.html.twig', [
'entity' => $entity,
'person' => $person,
]);
@@ -589,8 +578,8 @@ class ReportController extends AbstractController
),
'method' => 'PUT',
'cFGroup' => $entity->getCFGroup(),
//'role' => ReportVoter::UPDATE,
//'center' => $this->centerResolverManager->resolveCenters($entity->getPerson()),
'role' => new Role('CHILL_REPORT_UPDATE'),
'center' => $entity->getPerson()->getCenter(),
]);
}
}

View File

@@ -74,15 +74,15 @@ class ReportType extends AbstractType
'group' => $options['cFGroup'], ]
);
//$this->appendScopeChoices(
// $builder,
// $options['role'],
// $options['center'],
// $this->user,
// $this->authorizationHelper,
// $this->translatableStringHelper,
// $this->om
//);
$this->appendScopeChoices(
$builder,
$options['role'],
$options['center'],
$this->user,
$this->authorizationHelper,
$this->translatableStringHelper,
$this->om
);
}
public function configureOptions(OptionsResolver $resolver)
@@ -97,7 +97,7 @@ class ReportType extends AbstractType
$resolver->setAllowedTypes('cFGroup', 'Chill\CustomFieldsBundle\Entity\CustomFieldsGroup');
//$this->appendScopeChoicesOptions($resolver);
$this->appendScopeChoicesOptions($resolver);
}
/**

View File

@@ -25,9 +25,7 @@
{{ form_row(edit_form.user) }}
{{ form_row(edit_form.date) }}
{#
{{ form_row(edit_form.scope) }}
#}
{{ form_row(edit_form.cFData) }}
{{ form_widget(edit_form) }}

View File

@@ -1,5 +1,7 @@
services:
Chill\ReportBundle\Controller\ReportController:
autowire: true
autoconfigure: true
arguments:
$eventDispatcher: '@Symfony\Component\EventDispatcher\EventDispatcherInterface'
$authorizationHelper: '@Chill\MainBundle\Security\Authorization\AuthorizationHelper'
$paginator: '@Chill\MainBundle\Pagination\PaginatorFactory'
tags: ['controller.service_arguments']

View File

@@ -805,9 +805,12 @@ class ThirdParty implements TrackCreationInterface, TrackUpdateInterface
return $this;
}
public function setProfession(?string $profession): self
/**
* @return $this
*/
public function setProfession(string $profession): self
{
$this->profession = (string) $profession;
$this->profession = $profession;
return $this;
}

View File

@@ -110,7 +110,6 @@ class ThirdPartyType extends AbstractType
->add('profession', TextType::class, [
'label' => 'thirdparty.Profession',
'required' => false,
'empty_data' => '',
])
->add('contactDataAnonymous', CheckboxType::class, [
'required' => false,