mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-10-14 01:09:41 +00:00
Compare commits
47 Commits
375-notifi
...
285-cancel
Author | SHA1 | Date | |
---|---|---|---|
111f72d996 | |||
67c29728f3 | |||
0f62ddc3d6 | |||
db3833190e | |||
fbb4e8c784 | |||
a41414a460 | |||
a3c1aa4679 | |||
99b801fb8a | |||
4087911262 | |||
d6a50888e0 | |||
705f3b6f76 | |||
6f8b25f457 | |||
ffef8b70a3 | |||
43a3d5c902 | |||
197b17fa76 | |||
229d62e676 | |||
b51a7cfc57 | |||
5d40948a63 | |||
9c1df94796 | |||
24eb220e90 | |||
9a262a3f43 | |||
b421e0b0af | |||
20b82ddbfc | |||
a568d3c5a2 | |||
260a42c35a | |||
5b4f47bfaa | |||
60706ae75b | |||
b561b12213 | |||
f4369f0e3c | |||
afa755f5a5 | |||
cb2308f808 | |||
432650e081 | |||
4215a90b0e | |||
24daae6ee5 | |||
5ae26a46eb | |||
aec32b9bf8 | |||
5b59bef990 | |||
e70de780a7 | |||
a546ec1e6e | |||
a5417855f2 | |||
2234093971 | |||
be499bee07 | |||
55faeaec39 | |||
ac24df3f3c | |||
64dc458844 | |||
5f57b5eae3 | |||
95de2c863d |
@@ -13,6 +13,7 @@ namespace Chill\CalendarBundle\Controller;
|
||||
|
||||
use Chill\CalendarBundle\Entity\Calendar;
|
||||
use Chill\CalendarBundle\Form\CalendarType;
|
||||
use Chill\CalendarBundle\Form\CancelType;
|
||||
use Chill\CalendarBundle\RemoteCalendar\Connector\RemoteCalendarConnectorInterface;
|
||||
use Chill\CalendarBundle\Repository\CalendarACLAwareRepositoryInterface;
|
||||
use Chill\CalendarBundle\Security\Voter\CalendarVoter;
|
||||
@@ -30,6 +31,7 @@ use Chill\PersonBundle\Repository\PersonRepository;
|
||||
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter;
|
||||
use Chill\PersonBundle\Security\Authorization\PersonVoter;
|
||||
use Chill\ThirdPartyBundle\Entity\ThirdParty;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use http\Exception\UnexpectedValueException;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
@@ -60,6 +62,7 @@ class CalendarController extends AbstractController
|
||||
private readonly UserRepositoryInterface $userRepository,
|
||||
private readonly TranslatorInterface $translator,
|
||||
private readonly \Doctrine\Persistence\ManagerRegistry $managerRegistry,
|
||||
private readonly EntityManagerInterface $em,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -111,6 +114,55 @@ class CalendarController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route(path: '/{_locale}/calendar/calendar/{id}/cancel', name: 'chill_calendar_calendar_cancel')]
|
||||
public function cancelAction(Calendar $calendar, Request $request): Response
|
||||
{
|
||||
// Deal with sms being sent or not
|
||||
// Communicate cancellation with the remote calendar.
|
||||
|
||||
$this->denyAccessUnlessGranted(CalendarVoter::EDIT, $calendar);
|
||||
|
||||
[$person, $accompanyingPeriod] = [$calendar->getPerson(), $calendar->getAccompanyingPeriod()];
|
||||
|
||||
$form = $this->createForm(CancelType::class, $calendar);
|
||||
$form->add('submit', SubmitType::class);
|
||||
|
||||
if ($accompanyingPeriod instanceof AccompanyingPeriod) {
|
||||
$view = '@ChillCalendar/Calendar/cancelCalendarByAccompanyingCourse.html.twig';
|
||||
$redirectRoute = $this->generateUrl('chill_calendar_calendar_list_by_period', ['id' => $accompanyingPeriod->getId()]);
|
||||
} elseif ($person instanceof Person) {
|
||||
$view = '@ChillCalendar/Calendar/cancelCalendarByPerson.html.twig';
|
||||
$redirectRoute = $this->generateUrl('chill_calendar_calendar_list_by_person', ['id' => $person->getId()]);
|
||||
} else {
|
||||
throw new \RuntimeException('nor person or accompanying period');
|
||||
}
|
||||
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
|
||||
$this->logger->notice('A calendar event has been cancelled', [
|
||||
'by_user' => $this->getUser()->getUsername(),
|
||||
'calendar_id' => $calendar->getId(),
|
||||
]);
|
||||
|
||||
$calendar->setStatus($calendar::STATUS_CANCELED);
|
||||
$calendar->setSmsStatus($calendar::SMS_CANCEL_PENDING);
|
||||
$this->em->flush();
|
||||
|
||||
$this->addFlash('success', $this->translator->trans('chill_calendar.calendar_canceled'));
|
||||
|
||||
return new RedirectResponse($redirectRoute);
|
||||
}
|
||||
|
||||
return $this->render($view, [
|
||||
'calendar' => $calendar,
|
||||
'form' => $form->createView(),
|
||||
'accompanyingCourse' => $accompanyingPeriod,
|
||||
'person' => $person,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit a calendar item.
|
||||
*/
|
||||
|
@@ -35,7 +35,7 @@ class LoadCancelReason extends Fixture implements FixtureGroupInterface
|
||||
$arr = [
|
||||
['name' => CancelReason::CANCELEDBY_USER],
|
||||
['name' => CancelReason::CANCELEDBY_PERSON],
|
||||
['name' => CancelReason::CANCELEDBY_DONOTCOUNT],
|
||||
['name' => CancelReason::CANCELEDBY_OTHER],
|
||||
];
|
||||
|
||||
foreach ($arr as $a) {
|
||||
|
@@ -18,14 +18,14 @@ use Doctrine\ORM\Mapping as ORM;
|
||||
#[ORM\Table(name: 'chill_calendar.cancel_reason')]
|
||||
class CancelReason
|
||||
{
|
||||
final public const CANCELEDBY_DONOTCOUNT = 'CANCELEDBY_DONOTCOUNT';
|
||||
final public const CANCELEDBY_OTHER = 'CANCELEDBY_OTHER';
|
||||
|
||||
final public const CANCELEDBY_PERSON = 'CANCELEDBY_PERSON';
|
||||
|
||||
final public const CANCELEDBY_USER = 'CANCELEDBY_USER';
|
||||
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::BOOLEAN)]
|
||||
private ?bool $active = null;
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::BOOLEAN, options: ['default' => true])]
|
||||
private bool $active = true;
|
||||
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::STRING, length: 255)]
|
||||
private ?string $canceledBy = null;
|
||||
|
@@ -15,7 +15,7 @@ use Chill\CalendarBundle\Entity\CancelReason;
|
||||
use Chill\MainBundle\Form\Type\TranslatableStringFormType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
@@ -28,7 +28,14 @@ class CancelReasonType extends AbstractType
|
||||
->add('active', CheckboxType::class, [
|
||||
'required' => false,
|
||||
])
|
||||
->add('canceledBy', TextType::class);
|
||||
->add('canceledBy', ChoiceType::class, [
|
||||
'choices' => [
|
||||
'chill_calendar.canceled_by.user' => CancelReason::CANCELEDBY_USER,
|
||||
'chill_calendar.canceled_by.person' => CancelReason::CANCELEDBY_PERSON,
|
||||
'chill_calendar.canceled_by.other' => CancelReason::CANCELEDBY_OTHER,
|
||||
],
|
||||
'required' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
|
42
src/Bundle/ChillCalendarBundle/Form/CancelType.php
Normal file
42
src/Bundle/ChillCalendarBundle/Form/CancelType.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?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\CalendarBundle\Form;
|
||||
|
||||
use Chill\CalendarBundle\Entity\Calendar;
|
||||
use Chill\CalendarBundle\Entity\CancelReason;
|
||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class CancelType extends AbstractType
|
||||
{
|
||||
public function __construct(private readonly TranslatableStringHelperInterface $translatableStringHelper) {}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$builder->add('cancelReason', EntityType::class, [
|
||||
'class' => CancelReason::class,
|
||||
'required' => true,
|
||||
'choice_label' => fn (CancelReason $cancelReason) => $this->translatableStringHelper->localize($cancelReason->getName()),
|
||||
]);
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => Calendar::class,
|
||||
|
||||
]);
|
||||
}
|
||||
}
|
@@ -21,6 +21,7 @@ namespace Chill\CalendarBundle\Messenger\Doctrine;
|
||||
use Chill\CalendarBundle\Entity\Calendar;
|
||||
use Chill\CalendarBundle\Messenger\Message\CalendarMessage;
|
||||
use Chill\CalendarBundle\Messenger\Message\CalendarRemovedMessage;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Doctrine\ORM\Event\PostPersistEventArgs;
|
||||
use Doctrine\ORM\Event\PostRemoveEventArgs;
|
||||
use Doctrine\ORM\Event\PostUpdateEventArgs;
|
||||
@@ -31,6 +32,17 @@ class CalendarEntityListener
|
||||
{
|
||||
public function __construct(private readonly MessageBusInterface $messageBus, private readonly Security $security) {}
|
||||
|
||||
private function getAuthenticatedUser(): User
|
||||
{
|
||||
$user = $this->security->getUser();
|
||||
|
||||
if (!$user instanceof User) {
|
||||
throw new \LogicException('Expected an instance of User.');
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
public function postPersist(Calendar $calendar, PostPersistEventArgs $args): void
|
||||
{
|
||||
if (!$calendar->preventEnqueueChanges) {
|
||||
@@ -38,7 +50,7 @@ class CalendarEntityListener
|
||||
new CalendarMessage(
|
||||
$calendar,
|
||||
CalendarMessage::CALENDAR_PERSIST,
|
||||
$this->security->getUser()
|
||||
$this->getAuthenticatedUser()
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -50,7 +62,7 @@ class CalendarEntityListener
|
||||
$this->messageBus->dispatch(
|
||||
new CalendarRemovedMessage(
|
||||
$calendar,
|
||||
$this->security->getUser()
|
||||
$this->getAuthenticatedUser()
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -58,12 +70,19 @@ class CalendarEntityListener
|
||||
|
||||
public function postUpdate(Calendar $calendar, PostUpdateEventArgs $args): void
|
||||
{
|
||||
if (!$calendar->preventEnqueueChanges) {
|
||||
if ($calendar->getStatus() === $calendar::STATUS_CANCELED) {
|
||||
$this->messageBus->dispatch(
|
||||
new CalendarRemovedMessage(
|
||||
$calendar,
|
||||
$this->getAuthenticatedUser()
|
||||
)
|
||||
);
|
||||
} elseif (!$calendar->preventEnqueueChanges) {
|
||||
$this->messageBus->dispatch(
|
||||
new CalendarMessage(
|
||||
$calendar,
|
||||
CalendarMessage::CALENDAR_UPDATE,
|
||||
$this->security->getUser()
|
||||
$this->getAuthenticatedUser()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@@ -70,6 +70,8 @@ class CalendarRemovedMessage
|
||||
|
||||
public function getRemoteId(): string
|
||||
{
|
||||
dump($this->remoteId);
|
||||
|
||||
return $this->remoteId;
|
||||
}
|
||||
}
|
||||
|
@@ -1,5 +1,6 @@
|
||||
services:
|
||||
Chill\CalendarBundle\Controller\:
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
resource: '../../../Controller'
|
||||
tags: ['controller.service_arguments']
|
||||
|
@@ -9,9 +9,20 @@
|
||||
<div class="item-row main">
|
||||
<div class="item-col">
|
||||
<div class="wrap-header">
|
||||
<div class="wl-row">
|
||||
{% if calendar.status == 'canceled' %}
|
||||
<div class="badge rounded-pill bg-danger">
|
||||
<span>{{ 'chill_calendar.canceled'|trans }}: </span>
|
||||
<span>{{ calendar.cancelReason.name|localize_translatable_string }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="wl-row">
|
||||
<div class="wl-col title">
|
||||
<p class="date-label">
|
||||
{% if calendar.status == 'canceled' %}
|
||||
<del>
|
||||
{% endif %}
|
||||
{% if context == 'person' and calendar.context == 'accompanying_period' %}
|
||||
<a href="{{ chill_path_add_return_path('chill_person_accompanying_course_index', {'accompanying_period_id': calendar.accompanyingPeriod.id}) }}" style="text-decoration: none;">
|
||||
<span class="badge bg-primary">
|
||||
@@ -19,6 +30,9 @@
|
||||
</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if calendar.status == 'canceled' %}
|
||||
<del>
|
||||
{% endif %}
|
||||
{% if calendar.endDate.diff(calendar.startDate).days >= 1 %}
|
||||
{{ calendar.startDate|format_datetime('short', 'short') }}
|
||||
- {{ calendar.endDate|format_datetime('short', 'short') }}
|
||||
@@ -26,13 +40,15 @@
|
||||
{{ calendar.startDate|format_datetime('short', 'short') }}
|
||||
- {{ calendar.endDate|format_datetime('none', 'short') }}
|
||||
{% endif %}
|
||||
</p>
|
||||
{% if calendar.status == 'canceled' %}
|
||||
</del>
|
||||
{% endif %}
|
||||
|
||||
<div class="duration short-message">
|
||||
<i class="fa fa-fw fa-hourglass-end"></i>
|
||||
{{ calendar.duration|date('%H:%I') }}
|
||||
{% if false == calendar.sendSMS or null == calendar.sendSMS %}
|
||||
<!-- no sms will be send -->
|
||||
<!-- no sms will be sent -->
|
||||
{% else %}
|
||||
{% if calendar.smsStatus == 'sms_sent' %}
|
||||
<span title="{{ 'SMS already sent'|trans }}" class="badge bg-info">
|
||||
@@ -63,8 +79,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if calendar.comment.comment is not empty
|
||||
or calendar.users|length > 0
|
||||
{% if calendar.users|length > 0
|
||||
or calendar.thirdParties|length > 0
|
||||
or calendar.users|length > 0 %}
|
||||
<div class="item-row details separator">
|
||||
@@ -103,12 +118,13 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="item-row separator column">
|
||||
<div>
|
||||
|
||||
{{ include('@ChillCalendar/Calendar/_documents.twig.html') }}
|
||||
{% if calendar.documents is not empty %}
|
||||
<div class="item-row separator column">
|
||||
<div>
|
||||
{{ include('@ChillCalendar/Calendar/_documents.twig.html') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if calendar.activity is not null %}
|
||||
<div class="item-row separator">
|
||||
@@ -151,7 +167,7 @@
|
||||
|
||||
<div class="item-row separator">
|
||||
<ul class="record_actions">
|
||||
{% if is_granted('CHILL_CALENDAR_DOC_EDIT', calendar) %}
|
||||
{% if is_granted('CHILL_CALENDAR_DOC_EDIT', calendar) and calendar.status is not constant('STATUS_CANCELED', calendar) %}
|
||||
{% if templates|length == 0 %}
|
||||
<li>
|
||||
<a class="btn btn-create"
|
||||
@@ -191,6 +207,7 @@
|
||||
or
|
||||
(calendar.context == 'person' and is_granted('CHILL_ACTIVITY_CREATE', calendar.person))
|
||||
)
|
||||
and calendar.status is not constant('STATUS_CANCELED', calendar)
|
||||
%}
|
||||
<li>
|
||||
<a class="btn btn-create"
|
||||
@@ -213,12 +230,18 @@
|
||||
class="btn btn-show "></a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if is_granted('CHILL_CALENDAR_CALENDAR_EDIT', calendar) %}
|
||||
|
||||
{% if is_granted('CHILL_CALENDAR_CALENDAR_EDIT', calendar) and calendar.status is not constant('STATUS_CANCELED', calendar) %}
|
||||
<li>
|
||||
<a href="{{ chill_path_add_return_path('chill_calendar_calendar_edit', { 'id': calendar.id }) }}"
|
||||
class="btn btn-update "></a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{ chill_path_add_return_path('chill_calendar_calendar_cancel', { 'id': calendar.id } ) }}"
|
||||
class="btn btn-remove">{{ 'Cancel'|trans }}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if is_granted('CHILL_CALENDAR_CALENDAR_DELETE', calendar) %}
|
||||
<li>
|
||||
<a href="{{ chill_path_add_return_path('chill_calendar_calendar_delete', { 'id': calendar.id } ) }}"
|
||||
|
@@ -0,0 +1,29 @@
|
||||
{% extends "@ChillPerson/AccompanyingCourse/layout.html.twig" %}
|
||||
|
||||
{% set activeRouteKey = 'chill_calendar_calendar_list' %}
|
||||
|
||||
{% block title 'chill_calendar.cancel_calendar_item'|trans %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
{{ form_start(form) }}
|
||||
|
||||
{{ form_row(form.cancelReason) }}
|
||||
|
||||
<ul class="record_actions sticky-form-buttons">
|
||||
<li class="save">
|
||||
<a
|
||||
class="btn btn-cancel"
|
||||
href="{{ chill_return_path_or('chill_calendar_calendar_list', { 'id': accompanyingCourse.id } )}}"
|
||||
>
|
||||
{{ 'Cancel'|trans|chill_return_path_label }}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
{{ form_widget(form.submit, { 'attr' : { 'class': 'btn btn-save' }, 'label': 'Save' } ) }}
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
{{ form_end(form) }}
|
||||
|
||||
{% endblock %}
|
@@ -0,0 +1,29 @@
|
||||
{% extends "@ChillPerson/Person/layout.html.twig" %}
|
||||
|
||||
{% set activeRouteKey = 'chill_calendar_calendar_list' %}
|
||||
|
||||
{% block title 'chill_calendar.cancel_calendar_item'|trans %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
{{ form_start(form) }}
|
||||
|
||||
{{ form_row(form.cancelReason) }}
|
||||
|
||||
<ul class="record_actions sticky-form-buttons">
|
||||
<li class="save">
|
||||
<a
|
||||
class="btn btn-cancel"
|
||||
href="{{ chill_return_path_or('chill_calendar_calendar_list', { 'id': person.id } )}}"
|
||||
>
|
||||
{{ 'Cancel'|trans|chill_return_path_label }}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
{{ form_widget(form.submit, { 'attr' : { 'class': 'btn btn-save' }, 'label': 'Save' } ) }}
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
{{ form_end(form) }}
|
||||
|
||||
{% endblock %}
|
@@ -5,7 +5,7 @@
|
||||
{% block table_entities_thead_tr %}
|
||||
<th>{{ 'Id'|trans }}</th>
|
||||
<th>{{ 'Name'|trans }}</th>
|
||||
<th>{{ 'canceledBy'|trans }}</th>
|
||||
<th>{{ 'Canceled by'|trans }}</th>
|
||||
<th>{{ 'active'|trans }}</th>
|
||||
<th> </th>
|
||||
{% endblock %}
|
||||
@@ -40,4 +40,4 @@
|
||||
</li>
|
||||
{% endblock %}
|
||||
{% endembed %}
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
@@ -19,6 +19,7 @@ declare(strict_types=1);
|
||||
namespace Chill\CalendarBundle\Service\ShortMessageNotification;
|
||||
|
||||
use Chill\CalendarBundle\Entity\Calendar;
|
||||
use Chill\CalendarBundle\Entity\CancelReason;
|
||||
use libphonenumber\PhoneNumberFormat;
|
||||
use libphonenumber\PhoneNumberUtil;
|
||||
use Symfony\Component\Notifier\Message\SmsMessage;
|
||||
@@ -57,7 +58,7 @@ class DefaultShortMessageForCalendarBuilder implements ShortMessageForCalendarBu
|
||||
$this->phoneUtil->format($person->getMobilenumber(), PhoneNumberFormat::E164),
|
||||
$this->engine->render('@ChillCalendar/CalendarShortMessage/short_message.txt.twig', ['calendar' => $calendar]),
|
||||
);
|
||||
} elseif (Calendar::SMS_CANCEL_PENDING === $calendar->getSmsStatus()) {
|
||||
} elseif (Calendar::SMS_CANCEL_PENDING === $calendar->getSmsStatus() && (null === $calendar->getCancelReason() || CancelReason::CANCELEDBY_PERSON !== $calendar->getCancelReason()->getCanceledBy())) {
|
||||
$toUsers[] = new SmsMessage(
|
||||
$this->phoneUtil->format($person->getMobilenumber(), PhoneNumberFormat::E164),
|
||||
$this->engine->render('@ChillCalendar/CalendarShortMessage/short_message_canceled.txt.twig', ['calendar' => $calendar]),
|
||||
|
@@ -31,8 +31,7 @@ Will send SMS: Un SMS de rappel sera envoyé
|
||||
Will not send SMS: Aucun SMS de rappel ne sera envoyé
|
||||
SMS already sent: Un SMS a été envoyé
|
||||
|
||||
canceledBy: supprimé par
|
||||
Canceled by: supprimé par
|
||||
Canceled by: Annulé par
|
||||
Calendar configuration: Gestion des rendez-vous
|
||||
|
||||
crud:
|
||||
@@ -44,6 +43,14 @@ crud:
|
||||
title_edit: Modifier le motif d'annulation
|
||||
|
||||
chill_calendar:
|
||||
canceled: Annulé
|
||||
cancel_reason: Raison d'annulation
|
||||
cancel_calendar_item: Annuler rendez-vous
|
||||
calendar_canceled: Le rendez-vous a été annulé
|
||||
canceled_by:
|
||||
user: Utilisateur
|
||||
person: Usager
|
||||
other: Autre
|
||||
Document: Document d'un rendez-vous
|
||||
form:
|
||||
The main user is mandatory. He will organize the appointment.: L'utilisateur principal est obligatoire. Il est l'organisateur de l'événement.
|
||||
|
@@ -80,8 +80,6 @@ export default {
|
||||
return appMessages.fr.the_evaluation_document;
|
||||
case "Chill\\MainBundle\\Entity\\Workflow\\EntityWorkflow":
|
||||
return appMessages.fr.the_workflow;
|
||||
case "Chill\\TaskBundle\\Entity\\SingleTask":
|
||||
return appMessages.fr.the_task;
|
||||
default:
|
||||
throw "notification type unknown";
|
||||
}
|
||||
@@ -98,8 +96,6 @@ export default {
|
||||
return `/fr/person/accompanying-period/work/evaluation/document/${n.relatedEntityId}/show`;
|
||||
case "Chill\\MainBundle\\Entity\\Workflow\\EntityWorkflow":
|
||||
return `/fr/main/workflow/${n.relatedEntityId}/show`;
|
||||
case "Chill\\TaskBundle\\Entity\\SingleTask":
|
||||
return `/fr/task/single-task/${n.relatedEntityId}/show`;
|
||||
default:
|
||||
throw "notification type unknown";
|
||||
}
|
||||
|
@@ -37,7 +37,7 @@ class NotificationNormalizer implements NormalizerAwareInterface, NormalizerInte
|
||||
->find($object->getRelatedEntityId());
|
||||
|
||||
return [
|
||||
'type' => $object->getType(),
|
||||
'type' => 'notification',
|
||||
'id' => $object->getId(),
|
||||
'addressees' => $this->normalizer->normalize($object->getAddressees(), $format, $context),
|
||||
'date' => $this->normalizer->normalize($object->getDate(), $format, $context),
|
||||
|
@@ -13,6 +13,7 @@ namespace Chill\TaskBundle\Controller;
|
||||
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Pagination\PaginatorFactory;
|
||||
use Chill\MainBundle\Security\Resolver\CenterResolverDispatcherInterface;
|
||||
use Chill\MainBundle\Serializer\Model\Collection;
|
||||
use Chill\MainBundle\Serializer\Model\Counter;
|
||||
use Chill\MainBundle\Templating\Listing\FilterOrderHelper;
|
||||
@@ -22,7 +23,6 @@ use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Chill\PersonBundle\Privacy\PrivacyEvent;
|
||||
use Chill\TaskBundle\Entity\SingleTask;
|
||||
use Chill\TaskBundle\Event\AssignTaskEvent;
|
||||
use Chill\TaskBundle\Event\TaskEvent;
|
||||
use Chill\TaskBundle\Event\UI\UIEvent;
|
||||
use Chill\TaskBundle\Form\SingleTaskType;
|
||||
@@ -48,6 +48,7 @@ use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
final class SingleTaskController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly CenterResolverDispatcherInterface $centerResolverDispatcher,
|
||||
private readonly PaginatorFactory $paginatorFactory,
|
||||
private readonly SingleTaskAclAwareRepositoryInterface $singleTaskAclAwareRepository,
|
||||
private readonly TranslatorInterface $translator,
|
||||
@@ -168,9 +169,6 @@ final class SingleTaskController extends AbstractController
|
||||
->setForm($this->setCreateForm($task, TaskVoter::UPDATE));
|
||||
$this->eventDispatcher->dispatch($event, UIEvent::EDIT_FORM);
|
||||
|
||||
// To keep track of specific assignee change
|
||||
$initialAssignee = $task->getAssignee();
|
||||
|
||||
$form = $event->getForm();
|
||||
|
||||
$form->handleRequest($request);
|
||||
@@ -180,13 +178,6 @@ final class SingleTaskController extends AbstractController
|
||||
$em = $this->managerRegistry->getManager();
|
||||
$em->persist($task);
|
||||
|
||||
if ($initialAssignee !== $task->getAssignee()) {
|
||||
$this->eventDispatcher->dispatch(
|
||||
new AssignTaskEvent($task, $initialAssignee),
|
||||
AssignTaskEvent::PERSIST
|
||||
);
|
||||
}
|
||||
|
||||
$em->flush();
|
||||
|
||||
$this->addFlash('success', $this->translator
|
||||
@@ -534,13 +525,6 @@ final class SingleTaskController extends AbstractController
|
||||
|
||||
$this->eventDispatcher->dispatch(new TaskEvent($task), TaskEvent::PERSIST);
|
||||
|
||||
if (null !== $task->getAssignee()) {
|
||||
$this->eventDispatcher->dispatch(
|
||||
new AssignTaskEvent($task, null),
|
||||
AssignTaskEvent::PERSIST
|
||||
);
|
||||
}
|
||||
|
||||
$em->flush();
|
||||
|
||||
$this->addFlash('success', $this->translator->trans('The task is created'));
|
||||
|
@@ -42,7 +42,6 @@ class ChillTaskExtension extends Extension implements PrependExtensionInterface
|
||||
$loader->load('services/timeline.yaml');
|
||||
$loader->load('services/fixtures.yaml');
|
||||
$loader->load('services/form.yaml');
|
||||
$loader->load('services/notification.yaml');
|
||||
}
|
||||
|
||||
public function prepend(ContainerBuilder $container)
|
||||
|
@@ -1,41 +0,0 @@
|
||||
<?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\TaskBundle\Event;
|
||||
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\TaskBundle\Entity\SingleTask;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
class AssignTaskEvent extends Event
|
||||
{
|
||||
final public const PERSIST = 'chill_task.assign_task';
|
||||
|
||||
public function __construct(
|
||||
private readonly SingleTask $task,
|
||||
private readonly ?User $initialAssignee,
|
||||
) {}
|
||||
|
||||
public function getTask(): SingleTask
|
||||
{
|
||||
return $this->task;
|
||||
}
|
||||
|
||||
public function getInitialAssignee(): ?User
|
||||
{
|
||||
return $this->initialAssignee;
|
||||
}
|
||||
|
||||
public function hasAssigneeChanged(): bool
|
||||
{
|
||||
return $this->initialAssignee !== $this->task->getAssignee();
|
||||
}
|
||||
}
|
@@ -1,66 +0,0 @@
|
||||
<?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\TaskBundle\Event;
|
||||
|
||||
use Chill\MainBundle\Entity\Notification;
|
||||
use Chill\TaskBundle\Entity\SingleTask;
|
||||
use Chill\TaskBundle\Notification\AssignTaskNotificationFlagProvider;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
||||
readonly class TaskAssignEventSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(
|
||||
private EntityManagerInterface $entityManager,
|
||||
private \Twig\Environment $engine,
|
||||
) {}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
AssignTaskEvent::PERSIST => ['onTaskAssigned', 0],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a notification when a user is assigned to a task.
|
||||
* Only triggers when the assignee actually changes.
|
||||
*/
|
||||
public function onTaskAssigned(AssignTaskEvent $event): void
|
||||
{
|
||||
if (!$event->hasAssigneeChanged()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$task = $event->getTask();
|
||||
$assignedUser = $task->getAssignee();
|
||||
|
||||
$title = $task->getTitle();
|
||||
|
||||
$context = [
|
||||
'task' => $task,
|
||||
'assignedUser' => $assignedUser,
|
||||
'title' => $title,
|
||||
];
|
||||
|
||||
$notification = new Notification();
|
||||
$notification
|
||||
->setRelatedEntityId($task->getId())
|
||||
->setRelatedEntityClass(SingleTask::class)
|
||||
->setTitle($this->engine->render('@ChillTask/Notification/task_assignment_notification_title.txt.twig', $context))
|
||||
->setMessage($this->engine->render('@ChillTask/Notification/task_assignment_notification_content.txt.twig', $context))
|
||||
->addAddressee($assignedUser)
|
||||
->setType(AssignTaskNotificationFlagProvider::FLAG);
|
||||
|
||||
$this->entityManager->persist($notification);
|
||||
}
|
||||
}
|
@@ -1,31 +0,0 @@
|
||||
<?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\TaskBundle\Notification;
|
||||
|
||||
use Chill\MainBundle\Notification\FlagProviders\NotificationFlagProviderInterface;
|
||||
use Symfony\Component\Translation\TranslatableMessage;
|
||||
use Symfony\Contracts\Translation\TranslatableInterface;
|
||||
|
||||
class AssignTaskNotificationFlagProvider implements NotificationFlagProviderInterface
|
||||
{
|
||||
public const FLAG = 'task-assign-notif';
|
||||
|
||||
public function getFlag(): string
|
||||
{
|
||||
return self::FLAG;
|
||||
}
|
||||
|
||||
public function getLabel(): TranslatableInterface
|
||||
{
|
||||
return new TranslatableMessage('notification.flags.task_assign');
|
||||
}
|
||||
}
|
@@ -1,69 +0,0 @@
|
||||
<?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\TaskBundle\Notification;
|
||||
|
||||
use Chill\MainBundle\Entity\Notification;
|
||||
use Chill\MainBundle\Notification\NotificationHandlerInterface;
|
||||
use Chill\TaskBundle\Entity\SingleTask;
|
||||
use Chill\TaskBundle\Repository\SingleTaskRepository;
|
||||
use Symfony\Component\Translation\TranslatableMessage;
|
||||
use Symfony\Contracts\Translation\TranslatableInterface;
|
||||
|
||||
final readonly class TaskNotificationHandler implements NotificationHandlerInterface
|
||||
{
|
||||
public function __construct(private SingleTaskRepository $taskRepository) {}
|
||||
|
||||
public function getTemplate(Notification $notification, array $options = []): string
|
||||
{
|
||||
return '@ChillTask/SingleTask/showInNotification.html.twig';
|
||||
}
|
||||
|
||||
public function getTemplateData(Notification $notification, array $options = []): array
|
||||
{
|
||||
return [
|
||||
'notification' => $notification,
|
||||
'task' => $this->taskRepository->find($notification->getRelatedEntityId()),
|
||||
];
|
||||
}
|
||||
|
||||
public function supports(Notification $notification, array $options = []): bool
|
||||
{
|
||||
return SingleTask::class === $notification->getRelatedEntityClass();
|
||||
}
|
||||
|
||||
public function getTitle(Notification $notification, array $options = []): TranslatableInterface
|
||||
{
|
||||
if (null === $task = $this->getRelatedEntity($notification)) {
|
||||
return new TranslatableMessage('task.deleted');
|
||||
}
|
||||
|
||||
return new TranslatableMessage('notification.task.title %title%', ['title' => $task->getTitle()]);
|
||||
}
|
||||
|
||||
public function getAssociatedPersons(Notification $notification, array $options = []): array
|
||||
{
|
||||
if (null === $task = $this->getRelatedEntity($notification)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (null !== $task->getCourse()) {
|
||||
return $task->getCourse()->getParticipations()->getValues();
|
||||
}
|
||||
|
||||
return [$task->getPerson()];
|
||||
}
|
||||
|
||||
public function getRelatedEntity(Notification $notification): ?object
|
||||
{
|
||||
return $this->taskRepository->find($notification->getRelatedEntityId());
|
||||
}
|
||||
}
|
@@ -1,15 +0,0 @@
|
||||
{{ assignedUser.label }},
|
||||
|
||||
{{ 'notification.email.task_assigned'|trans({}, null, assignedUser.getLocale) }}
|
||||
|
||||
{{ 'notification.email.title_label'|trans({}, null, assignedUser.getLocale) }} "{{ task.title }}".
|
||||
{% if task.endDate %}
|
||||
|
||||
{{ 'notification.email.deadline'|trans({'%date%': task.endDate|format_date('long')}, null, assignedUser.getLocale) }}
|
||||
{% endif %}
|
||||
|
||||
{{ 'notification.email.view_task'|trans({}, null, assignedUser.getLocale) }}
|
||||
|
||||
{{ absolute_url(path('chill_task_single_task_show', {'id': task.id, '_locale': assignedUser.getLocale})) }}
|
||||
|
||||
{{ 'notification.email.regards'|trans({}, null, assignedUser.getLocale) }},
|
@@ -1,3 +0,0 @@
|
||||
{{ 'notification.email.title'|trans({}, null, assignedUser.getLocale) }}
|
||||
|
||||
|
@@ -18,14 +18,14 @@
|
||||
<div>
|
||||
{% if task.person is not null %}
|
||||
<span class="chill-task-list__row__person">
|
||||
{% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with {
|
||||
targetEntity: { name: 'person', id: task.person.id },
|
||||
action: 'show',
|
||||
displayBadge: true,
|
||||
buttonText: task.person|chill_entity_render_string,
|
||||
isDead: task.person.deathdate is not null
|
||||
} %}
|
||||
</span>
|
||||
{% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with {
|
||||
targetEntity: { name: 'person', id: task.person.id },
|
||||
action: 'show',
|
||||
displayBadge: true,
|
||||
buttonText: task.person|chill_entity_render_string,
|
||||
isDead: task.person.deathdate is not null
|
||||
} %}
|
||||
</span>
|
||||
{% elseif task.course is not null %}
|
||||
<div style="margin-bottom: 1rem;">
|
||||
{% for part in task.course.currentParticipations %}
|
||||
|
@@ -110,5 +110,4 @@
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</ul></div>
|
||||
|
@@ -1,14 +0,0 @@
|
||||
{% macro recordAction(task) %}
|
||||
<li>
|
||||
<a href="{{ path('chill_person_accompanying_course_index', { 'task_id': task }) }}"
|
||||
class="btn btn-show" title="{{ 'See task'|trans }}"></a>
|
||||
</li>
|
||||
{% endmacro %}
|
||||
|
||||
{% if task is not null %}
|
||||
{# <div>Todo : display task? </div>#}
|
||||
{% else %}
|
||||
<div class="alert alert-warning border-warning border-1">
|
||||
{{ 'You are getting a notification for a task which does not exist any more'|trans }}
|
||||
</div>
|
||||
{% endif %}
|
@@ -1,138 +0,0 @@
|
||||
<?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\TaskBundle\Tests\EventSubscriber;
|
||||
|
||||
use Chill\MainBundle\Entity\Notification;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\TaskBundle\Entity\SingleTask;
|
||||
use Chill\TaskBundle\Event\AssignTaskEvent;
|
||||
use Chill\TaskBundle\Event\TaskAssignEventSubscriber;
|
||||
use Chill\TaskBundle\Notification\AssignTaskNotificationFlagProvider;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Twig\Environment;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class TaskAssignEventSubscriberTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
private ObjectProphecy $entityManager;
|
||||
private ObjectProphecy $twig;
|
||||
private TaskAssignEventSubscriber $subscriber;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->entityManager = $this->prophesize(EntityManagerInterface::class);
|
||||
$this->twig = $this->prophesize(Environment::class);
|
||||
$this->subscriber = new TaskAssignEventSubscriber(
|
||||
$this->entityManager->reveal(),
|
||||
$this->twig->reveal()
|
||||
);
|
||||
}
|
||||
|
||||
private function setEntityId(object $entity, int $id): void
|
||||
{
|
||||
$reflection = new \ReflectionClass($entity);
|
||||
$property = $reflection->getProperty('id');
|
||||
$property->setAccessible(true);
|
||||
$property->setValue($entity, $id);
|
||||
}
|
||||
|
||||
public function testOnTaskAssignedCreatesNotificationWhenAssigneeChanges(): void
|
||||
{
|
||||
// Arrange
|
||||
$initialAssignee = new User();
|
||||
$newAssignee = new User();
|
||||
|
||||
$task = new SingleTask();
|
||||
$task->setTitle('Test Task');
|
||||
$task->setAssignee($newAssignee);
|
||||
$this->setEntityId($task, 123);
|
||||
|
||||
$event = new AssignTaskEvent($task, $initialAssignee);
|
||||
|
||||
$this->twig->render('@ChillTask/Notification/task_assignment_notification_title.txt.twig', Argument::type('array'))
|
||||
->shouldBeCalledOnce()
|
||||
->willReturn('Notification Title');
|
||||
|
||||
$this->twig->render('@ChillTask/Notification/task_assignment_notification_content.txt.twig', Argument::type('array'))
|
||||
->shouldBeCalledOnce()
|
||||
->willReturn('Notification Content');
|
||||
|
||||
$this->entityManager->persist(Argument::type(Notification::class))
|
||||
->shouldBeCalledOnce();
|
||||
|
||||
// Act
|
||||
$this->subscriber->onTaskAssigned($event);
|
||||
}
|
||||
|
||||
public function testOnTaskAssignedDoesNothingWhenAssigneeDoesNotChange(): void
|
||||
{
|
||||
// Arrange
|
||||
$assignee = new User();
|
||||
|
||||
$task = new SingleTask();
|
||||
$task->setTitle('Test Task');
|
||||
$task->setAssignee($assignee);
|
||||
|
||||
$event = new AssignTaskEvent($task, $assignee);
|
||||
|
||||
$this->twig->render(Argument::any(), Argument::any())->shouldNotBeCalled();
|
||||
$this->entityManager->persist(Argument::any())->shouldNotBeCalled();
|
||||
|
||||
// Act
|
||||
$this->subscriber->onTaskAssigned($event);
|
||||
}
|
||||
|
||||
public function testNotificationHasCorrectProperties(): void
|
||||
{
|
||||
// Arrange
|
||||
$initialAssignee = new User();
|
||||
$newAssignee = new User();
|
||||
|
||||
$task = new SingleTask();
|
||||
$task->setTitle('Important Task');
|
||||
$task->setAssignee($newAssignee);
|
||||
$this->setEntityId($task, 456);
|
||||
|
||||
$event = new AssignTaskEvent($task, $initialAssignee);
|
||||
|
||||
$this->twig->render(Argument::any(), Argument::any())->willReturn('Test Content');
|
||||
|
||||
// Capture the persisted notification
|
||||
$persistedNotification = null;
|
||||
$this->entityManager->persist(Argument::type(Notification::class))
|
||||
->shouldBeCalledOnce()
|
||||
->will(function ($args) use (&$persistedNotification) {
|
||||
$persistedNotification = $args[0];
|
||||
});
|
||||
|
||||
// Act
|
||||
$this->subscriber->onTaskAssigned($event);
|
||||
|
||||
// Assert
|
||||
$this->assertInstanceOf(Notification::class, $persistedNotification);
|
||||
$this->assertEquals($task->getId(), $persistedNotification->getRelatedEntityId());
|
||||
$this->assertEquals(SingleTask::class, $persistedNotification->getRelatedEntityClass());
|
||||
$this->assertEquals(AssignTaskNotificationFlagProvider::FLAG, $persistedNotification->getType());
|
||||
$this->assertEquals('Test Content', $persistedNotification->getTitle());
|
||||
$this->assertEquals('Test Content', $persistedNotification->getMessage());
|
||||
}
|
||||
}
|
@@ -1,13 +1,7 @@
|
||||
services:
|
||||
_defaults:
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
|
||||
Chill\TaskBundle\Event\Lifecycle\TaskLifecycleEvent:
|
||||
arguments:
|
||||
$em: '@Doctrine\ORM\EntityManagerInterface'
|
||||
$tokenStorage: '@Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface'
|
||||
tags:
|
||||
- { name: kernel.event_subscriber }
|
||||
|
||||
Chill\TaskBundle\Event\TaskAssignEventSubscriber: ~
|
||||
|
@@ -1,7 +0,0 @@
|
||||
services:
|
||||
_defaults:
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
|
||||
Chill\TaskBundle\Notification\TaskNotificationHandler: ~
|
||||
Chill\TaskBundle\Notification\AssignTaskNotificationFlagProvider: ~
|
@@ -116,16 +116,3 @@ CHILL_TASK_TASK_UPDATE: Modifier une tâche
|
||||
CHILL_TASK_TASK_CREATE_FOR_COURSE: Créer une tâche pour un parcours
|
||||
CHILL_TASK_TASK_CREATE_FOR_PERSON: Créer une tâche pour un usager
|
||||
|
||||
notification:
|
||||
task:
|
||||
title %title%: "Tâche: title"
|
||||
flags:
|
||||
task_assign: Lorsqu'un autre utilisateur m'assigne à une tâche.
|
||||
email:
|
||||
title: "Une tâche demande votre attention"
|
||||
task_assigned: "Une tâche vous a été assignée."
|
||||
title_label: "Titre de la tâche:"
|
||||
deadline: "Vous êtes invités à accomplir cette tâche avant le %date%"
|
||||
view_task: "Vous pouvez visualiser la tâche sur cette page:"
|
||||
regards: "Cordialement"
|
||||
|
||||
|
Reference in New Issue
Block a user