Compare commits

...

61 Commits

Author SHA1 Message Date
111f72d996 Change free text field to choice field within cancel reason form 2025-09-29 19:27:15 +02:00
67c29728f3 Change if-logic in CalendarEntityListener.php
CalendarRemovedMessage should be triggered upon 'editing' the Calendar entity setting it's status to canceled. Otherwise trigger CalendarMessage instead
2025-09-29 19:26:40 +02:00
0f62ddc3d6 Change naming of CancelReason constant 2025-09-29 19:25:08 +02:00
db3833190e Fix translation of admin cancel reason column 2025-09-29 19:24:03 +02:00
fbb4e8c784 Add cancel reason within the cancel label 2025-09-29 19:22:59 +02:00
a41414a460 Fix dependencyInjection error for cancel reason controller implementing CRUDController 2025-09-29 19:19:12 +02:00
a3c1aa4679 Set isActive on cancelReason to true by default 2025-09-29 19:18:43 +02:00
99b801fb8a Remove double controller method after rebase on master 2025-09-29 17:04:22 +02:00
4087911262 Pipeline fixes 2025-09-29 17:01:49 +02:00
d6a50888e0 Use translation with capital letter 2025-09-29 17:01:00 +02:00
705f3b6f76 Only change sms status upon cancellation if sendSMS is true 2025-09-29 16:59:21 +02:00
6f8b25f457 Re-establish canceled tag and strikethrough 2025-09-29 16:59:20 +02:00
ffef8b70a3 work on syncing with remote calendar 2025-09-29 16:58:57 +02:00
43a3d5c902 Set the sms status to 'canceled' 2025-09-29 16:58:57 +02:00
197b17fa76 Use a different button type 2025-09-29 16:58:55 +02:00
229d62e676 Add translation, make reason required, and change template for form 2025-09-29 16:57:54 +02:00
b51a7cfc57 Fix form to submit cancel reason 2025-09-29 16:57:35 +02:00
5d40948a63 Add cancel reason form 2025-09-29 16:57:33 +02:00
9c1df94796 Initial setup of controller and templates 2025-09-29 16:57:07 +02:00
24eb220e90 Pipeline corrections 2025-09-29 16:55:39 +02:00
9a262a3f43 Adjust display of canceled calendar items in the list 2025-09-29 16:54:48 +02:00
b421e0b0af Add a logger notice when calendar is canceled 2025-09-29 16:54:21 +02:00
20b82ddbfc Set the sms status to 'canceled' 2025-09-29 16:54:21 +02:00
a568d3c5a2 Use a different button type 2025-09-29 16:54:20 +02:00
260a42c35a Only show cancel button with EDIT permission 2025-09-29 16:53:54 +02:00
5b4f47bfaa Add translation, make reason required, and change template for form 2025-09-29 16:53:19 +02:00
60706ae75b Fix form to submit cancel reason 2025-09-29 16:53:19 +02:00
b561b12213 Add cancel reason form 2025-09-29 16:53:17 +02:00
f4369f0e3c Initial setup of controller and templates 2025-09-29 16:52:27 +02:00
afa755f5a5 Add condition for preparing cancellation message 2025-09-29 16:51:29 +02:00
cb2308f808 template adjustments + add missing translations 2025-09-29 16:51:29 +02:00
432650e081 work on syncing with remote calendar 2025-09-29 16:51:29 +02:00
4215a90b0e Set the sms status to 'canceled' 2025-09-29 16:51:29 +02:00
24daae6ee5 Add translation, make reason required, and change template for form 2025-09-29 16:51:29 +02:00
5ae26a46eb Fix form to submit cancel reason 2025-09-29 16:51:29 +02:00
aec32b9bf8 Add cancel reason form 2025-09-29 16:51:29 +02:00
5b59bef990 Initial setup of controller and templates 2025-09-29 16:51:29 +02:00
e70de780a7 Pipeline corrections 2025-09-29 16:51:29 +02:00
a546ec1e6e Adjust display of canceled calendar items in the list 2025-09-29 16:51:29 +02:00
a5417855f2 Add a logger notice when calendar is canceled 2025-09-29 16:51:29 +02:00
2234093971 Set the sms status to 'canceled' 2025-09-29 16:51:29 +02:00
be499bee07 Use a different button type 2025-09-29 16:51:29 +02:00
55faeaec39 Only show cancel button with EDIT permission 2025-09-29 16:51:28 +02:00
ac24df3f3c Add translation, make reason required, and change template for form 2025-09-29 16:51:28 +02:00
64dc458844 Fix form to submit cancel reason 2025-09-29 16:51:28 +02:00
5f57b5eae3 Add cancel reason form 2025-09-29 16:51:28 +02:00
95de2c863d Initial setup of controller and templates 2025-09-29 16:51:28 +02:00
c8bb7575e7 Merge branch '426-increase_nb_chars_to_14_chill_password' into 'master'
#426 Increased the number of required characters when setting a new password in Chill

Closes #426

See merge request Chill-Projet/chill-bundles!883
2025-09-19 07:03:51 +00:00
juminet
80a3734171 #426 Increased the number of required characters when setting a new password in Chill 2025-09-19 07:03:51 +00:00
ab98f3a102 Release v4.4.2 2025-09-12 12:47:06 +02:00
7516e68d77 Merge branch 'fix/docgen-after-accp-work-refacto' into 'master'
Fix document generation and workflow generation do not work on accompanying period work documents

See merge request Chill-Projet/chill-bundles!880
2025-09-12 10:42:34 +00:00
7b60b7a8af Fix document generation and workflow generation do not work on accompanying period work documents 2025-09-12 10:42:34 +00:00
d984dec7db Release v4.4.1 2025-09-11 16:26:51 +02:00
46a4dedab8 Merge branch 'missing_commit_duplicate_evaluation' into 'master'
Fix translations and close button modal for duplicate evaluation document

See merge request Chill-Projet/chill-bundles!878
2025-09-11 14:21:05 +00:00
db98519e65 Fix translations and close button modal for duplicate evaluation document 2025-09-11 14:21:05 +00:00
c39637180a Release v4.4.0 2025-09-11 13:04:50 +02:00
15f9409bc8 Merge branch '369-duplicate-evaluation-document' into 'master'
Resolve "Dupliquer une document d'une évaluation vers une autre" + "Déplacer un document vers une autre évaluation"

Closes #369

See merge request Chill-Projet/chill-bundles!813
2025-09-11 11:01:16 +00:00
5b90d23367 Resolve "Dupliquer une document d'une évaluation vers une autre" + "Déplacer un document vers une autre évaluation" 2025-09-11 11:01:16 +00:00
c48625d1cd Merge branch 'bug/1607-the-user-preferences-for-notification-in-profile-are-not-shown-correctly' into 'master'
Resolve "user notification preferences are not displayed correctly"

See merge request Chill-Projet/chill-bundles!877
2025-09-10 16:28:45 +00:00
1195b54a68 Resolve "user notification preferences are not displayed correctly" 2025-09-10 16:28:45 +00:00
2a280b814f Refactor view templates: relocate 'merge' action block and standardize 'duplicate link' block handling 2025-09-09 17:36:46 +02:00
76 changed files with 2322 additions and 989 deletions

View File

@@ -0,0 +1,6 @@
kind: Fixed
body: Increased the number of required characters when setting a new password in Chill from 9 to 14 - GDPR compliance
time: 2025-09-18T11:40:44.858533536+02:00
custom:
Issue: "426"
SchemaChange: No schema change

8
.changes/v4.4.0.md Normal file
View File

@@ -0,0 +1,8 @@
## v4.4.0 - 2025-09-11
### Feature
* ([#359](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/359)) Allow the merge of two accompanying period works
* ([#369](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/369)) Duplication of a document to another accompanying period work evaluation
* ([#359](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/359)) Fusion of two accompanying period works
### Fixed
* Fix display of 'duplicate' and 'merge' buttons in CRUD templates
* Fix saving notification preferences in user's profile

3
.changes/v4.4.1.md Normal file
View File

@@ -0,0 +1,3 @@
## v4.4.1 - 2025-09-11
### Fixed
* fix translations in duplicate evaluation document modal and realign close modal button

3
.changes/v4.4.2.md Normal file
View File

@@ -0,0 +1,3 @@
## v4.4.2 - 2025-09-12
### Fixed
* Fix document generation and workflow generation do not work on accompanying period work documents

View File

@@ -6,6 +6,23 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
and is generated by [Changie](https://github.com/miniscruff/changie).
## v4.4.2 - 2025-09-12
### Fixed
* Fix document generation and workflow generation do not work on accompanying period work documents
## v4.4.1 - 2025-09-11
### Fixed
* fix translations in duplicate evaluation document modal and realign close modal button
## v4.4.0 - 2025-09-11
### Feature
* ([#359](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/359)) Allow the merge of two accompanying period works
* ([#369](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/369)) Duplication of a document to another accompanying period work evaluation
* ([#359](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/359)) Fusion of two accompanying period works
### Fixed
* Fix display of 'duplicate' and 'merge' buttons in CRUD templates
* Fix saving notification preferences in user's profile
## v4.3.0 - 2025-09-08
### Feature
* ([#409](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/409)) Add 45 and 60 min calendar ranges

View File

@@ -55,5 +55,6 @@
</dl>
{% endblock %}
{% block content_view_actions_duplicate_link %}{% endblock %}
{% endembed %}
{% endblock %}

View File

@@ -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.
*/

View File

@@ -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) {

View File

@@ -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;

View File

@@ -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)

View 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,
]);
}
}

View File

@@ -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()
)
);
}

View File

@@ -70,6 +70,8 @@ class CalendarRemovedMessage
public function getRemoteId(): string
{
dump($this->remoteId);
return $this->remoteId;
}
}

View File

@@ -1,5 +1,6 @@
services:
Chill\CalendarBundle\Controller\:
autowire: true
autoconfigure: true
resource: '../../../Controller'
tags: ['controller.service_arguments']

View File

@@ -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 } ) }}"

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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>&nbsp;</th>
{% endblock %}
@@ -40,4 +40,4 @@
</li>
{% endblock %}
{% endembed %}
{% endblock %}
{% endblock %}

View File

@@ -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]),

View File

@@ -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.

View File

@@ -4,6 +4,7 @@ import { StoredObject, StoredObjectVersion } from "../../types";
import DropFileWidget from "ChillDocStoreAssets/vuejs/DropFileWidget/DropFileWidget.vue";
import { computed, reactive } from "vue";
import { useToast } from "vue-toast-notification";
import { DOCUMENT_ADD, trans } from "translator";
interface DropFileConfig {
allowRemove: boolean;
@@ -75,11 +76,9 @@ function closeModal(): void {
@click="openModal"
class="btn btn-create"
>
Ajouter un document
</button>
<button v-else @click="openModal" class="btn btn-edit">
Remplacer le document
{{ trans(DOCUMENT_ADD) }}
</button>
<button v-else @click="openModal" class="btn btn-edit"></button>
<modal
v-if="state.showModal"
:modal-dialog-class="modalClasses"

View File

@@ -23,6 +23,8 @@ See the document: Voir le document
document:
Any title: Aucun titre
replace: Remplacer
Add: Ajouter un document
generic_doc:
filter:

View File

@@ -118,7 +118,7 @@
{{ entity.notes|chill_print_or_message("Aucune note", 'blockquote') }}
{% endblock crud_content_view_details %}
{% block content_view_actions_duplicate_link %}{% endblock %}
{% block content_view_actions_back %}
<li class="cancel">
<a class="btn btn-cancel" href="{{ chill_return_path_or('chill_job_report_index', { 'person': entity.person.id }) }}">

View File

@@ -46,6 +46,7 @@
</dd>
</dl>
{% endblock crud_content_view_details %}
{% block content_view_actions_duplicate_link %}{% endblock %}
{% block content_view_actions_back %}
<li class="cancel">

View File

@@ -206,6 +206,8 @@
</a>
</li>
{% endblock %}
{% block content_view_actions_duplicate_link %}{% endblock %}
{% block content_view_actions_after %}
<li>
<a class="btn btn-misc" href="{{ chill_return_path_or('chill_crud_immersion_bilan', { 'id': entity.id, 'person_id': entity.person.id }) }}">

View File

@@ -94,6 +94,7 @@
{% endblock crud_content_view_details %}
{% block content_view_actions_duplicate_link %}{% endblock %}
{% block content_view_actions_back %}
<li class="cancel">

View File

@@ -0,0 +1,64 @@
<?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\Action\User\UpdateProfile;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Notification\NotificationFlagManager;
use Chill\MainBundle\Validation\Constraint\PhonenumberConstraint;
use libphonenumber\PhoneNumber;
final class UpdateProfileCommand
{
public array $notificationFlags = [];
public function __construct(
#[PhonenumberConstraint]
public ?PhoneNumber $phonenumber,
) {}
public static function create(User $user, NotificationFlagManager $flagManager): self
{
$updateProfileCommand = new self($user->getPhonenumber());
foreach ($flagManager->getAllNotificationFlagProviders() as $provider) {
$updateProfileCommand->setNotificationFlag(
$provider->getFlag(),
User::NOTIF_FLAG_IMMEDIATE_EMAIL,
$user->isNotificationSendImmediately($provider->getFlag())
);
$updateProfileCommand->setNotificationFlag(
$provider->getFlag(),
User::NOTIF_FLAG_DAILY_DIGEST,
$user->isNotificationDailyDigest($provider->getFlag())
);
}
return $updateProfileCommand;
}
/**
* @param User::NOTIF_FLAG_IMMEDIATE_EMAIL|User::NOTIF_FLAG_DAILY_DIGEST $kind
*/
private function setNotificationFlag(string $type, string $kind, bool $value): void
{
if (!array_key_exists($type, $this->notificationFlags)) {
$this->notificationFlags[$type] = ['immediate_email' => true, 'daily_digest' => false];
}
$k = match ($kind) {
User::NOTIF_FLAG_IMMEDIATE_EMAIL => 'immediate_email',
User::NOTIF_FLAG_DAILY_DIGEST => 'daily_digest',
};
$this->notificationFlags[$type][$k] = $value;
}
}

View File

@@ -0,0 +1,27 @@
<?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\Action\User\UpdateProfile;
use Chill\MainBundle\Entity\User;
final readonly class UpdateProfileCommandHandler
{
public function updateProfile(User $user, UpdateProfileCommand $command): void
{
$user->setPhonenumber($command->phonenumber);
foreach ($command->notificationFlags as $flag => $values) {
$user->setNotificationImmediately($flag, $values['immediate_email']);
$user->setNotificationDailyDigest($flag, $values['daily_digest']);
}
}
}

View File

@@ -1,63 +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\MainBundle\Controller;
use Chill\MainBundle\Form\UserProfileType;
use Chill\MainBundle\Security\ChillSecurity;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Contracts\Translation\TranslatorInterface;
use Symfony\Component\Routing\Annotation\Route;
final class UserProfileController extends AbstractController
{
public function __construct(
private readonly TranslatorInterface $translator,
private readonly ChillSecurity $security,
private readonly \Doctrine\Persistence\ManagerRegistry $managerRegistry,
) {}
/**
* User profile that allows editing of phonenumber and visualization of certain data.
*/
#[Route(path: '/{_locale}/main/user/my-profile', name: 'chill_main_user_profile')]
public function __invoke(Request $request)
{
if (!$this->security->isGranted('ROLE_USER')) {
throw new AccessDeniedHttpException();
}
$user = $this->security->getUser();
$editForm = $this->createForm(UserProfileType::class, $user);
$editForm->get('notificationFlags')->setData($user->getNotificationFlags());
$editForm->handleRequest($request);
if ($editForm->isSubmitted() && $editForm->isValid()) {
$notificationFlagsData = $editForm->get('notificationFlags')->getData();
$user->setNotificationFlags($notificationFlagsData);
$em = $this->managerRegistry->getManager();
$em->flush();
$this->addFlash('success', $this->translator->trans('user.profile.Profile successfully updated!'));
return $this->redirectToRoute('chill_main_user_profile');
}
return $this->render('@ChillMain/User/profile.html.twig', [
'user' => $user,
'form' => $editForm->createView(),
]);
}
}

View File

@@ -0,0 +1,75 @@
<?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\Action\User\UpdateProfile\UpdateProfileCommand;
use Chill\MainBundle\Action\User\UpdateProfile\UpdateProfileCommandHandler;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Form\UpdateProfileType;
use Chill\MainBundle\Notification\NotificationFlagManager;
use Chill\MainBundle\Security\ChillSecurity;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Form\FormFactoryInterface;
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\Generator\UrlGeneratorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use Symfony\Component\Routing\Annotation\Route;
use Twig\Environment;
final readonly class UserUpdateProfileController
{
public function __construct(
private TranslatorInterface $translator,
private ChillSecurity $security,
private EntityManagerInterface $entityManager,
private NotificationFlagManager $notificationFlagManager,
private FormFactoryInterface $formFactory,
private UrlGeneratorInterface $urlGenerator,
private Environment $twig,
private UpdateProfileCommandHandler $updateProfileCommandHandler,
) {}
/**
* User profile that allows editing of phonenumber and visualization of certain data.
*/
#[Route(path: '/{_locale}/main/user/my-profile', name: 'chill_main_user_profile')]
public function __invoke(Request $request, Session $session)
{
if (!$this->security->isGranted('ROLE_USER')) {
throw new AccessDeniedHttpException();
}
$user = $this->security->getUser();
$command = UpdateProfileCommand::create($user, $this->notificationFlagManager);
$editForm = $this->formFactory->create(UpdateProfileType::class, $command);
$editForm->handleRequest($request);
if ($editForm->isSubmitted() && $editForm->isValid()) {
$this->updateProfileCommandHandler->updateProfile($user, $command);
$this->entityManager->flush();
$session->getFlashBag()->add('success', $this->translator->trans('user.profile.Profile successfully updated!'));
return new RedirectResponse($this->urlGenerator->generate('chill_main_user_profile'));
}
return new Response($this->twig->render('@ChillMain/User/profile.html.twig', [
'user' => $user,
'form' => $editForm->createView(),
]));
}
}

View File

@@ -652,42 +652,66 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
return true;
}
public function getNotificationFlags(): array
private function getNotificationFlagData(string $flag): array
{
return $this->notificationFlags;
}
public function setNotificationFlags(array $notificationFlags)
{
$this->notificationFlags = $notificationFlags;
}
public function getNotificationFlagData(string $flag): array
{
return $this->notificationFlags[$flag] ?? [];
}
public function setNotificationFlagData(string $flag, array $data): void
{
$this->notificationFlags[$flag] = $data;
return $this->notificationFlags[$flag] ?? [self::NOTIF_FLAG_IMMEDIATE_EMAIL];
}
public function isNotificationSendImmediately(string $type): bool
{
if ([] === $this->getNotificationFlagData($type) || in_array(User::NOTIF_FLAG_IMMEDIATE_EMAIL, $this->getNotificationFlagData($type), true)) {
return true;
return $this->isNotificationForElement($type, self::NOTIF_FLAG_IMMEDIATE_EMAIL);
}
public function setNotificationImmediately(string $type, bool $active): void
{
$this->setNotificationFlagElement($type, $active, self::NOTIF_FLAG_IMMEDIATE_EMAIL);
}
public function setNotificationDailyDigest(string $type, bool $active): void
{
$this->setNotificationFlagElement($type, $active, self::NOTIF_FLAG_DAILY_DIGEST);
}
/**
* @param self::NOTIF_FLAG_IMMEDIATE_EMAIL|self::NOTIF_FLAG_DAILY_DIGEST $kind
*/
private function setNotificationFlagElement(string $type, bool $active, string $kind): void
{
$notificationFlags = [...$this->notificationFlags];
$changed = false;
if (!isset($notificationFlags[$type])) {
$notificationFlags[$type] = [self::NOTIF_FLAG_IMMEDIATE_EMAIL];
$changed = true;
}
return false;
if ($active) {
if (!in_array($kind, $notificationFlags[$type], true)) {
$notificationFlags[$type] = [...$notificationFlags[$type], $kind];
$changed = true;
}
} else {
if (in_array($kind, $notificationFlags[$type], true)) {
$notificationFlags[$type] = array_values(
array_filter($notificationFlags[$type], static fn ($k) => $k !== $kind)
);
$changed = true;
}
}
if ($changed) {
$this->notificationFlags = [...$notificationFlags];
}
}
private function isNotificationForElement(string $type, string $kind): bool
{
return in_array($kind, $this->getNotificationFlagData($type), true);
}
public function isNotificationDailyDigest(string $type): bool
{
if (in_array(User::NOTIF_FLAG_DAILY_DIGEST, $this->getNotificationFlagData($type), true)) {
return true;
}
return false;
return $this->isNotificationForElement($type, self::NOTIF_FLAG_DAILY_DIGEST);
}
public function getLocale(): string

View File

@@ -1,75 +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\MainBundle\Form\DataMapper;
use Chill\MainBundle\Entity\User;
use Symfony\Component\Form\DataMapperInterface;
final readonly class NotificationFlagDataMapper implements DataMapperInterface
{
public function __construct(private array $notificationFlagProviders) {}
public function mapDataToForms($viewData, $forms): void
{
if (null === $viewData) {
$viewData = [];
}
$formsArray = iterator_to_array($forms);
foreach ($this->notificationFlagProviders as $flagProvider) {
$flag = $flagProvider->getFlag();
if (isset($formsArray[$flag])) {
$flagForm = $formsArray[$flag];
$immediateEmailChecked = in_array(User::NOTIF_FLAG_IMMEDIATE_EMAIL, $viewData[$flag] ?? [], true)
|| !array_key_exists($flag, $viewData);
$dailyEmailChecked = in_array(User::NOTIF_FLAG_DAILY_DIGEST, $viewData[$flag] ?? [], true);
if ($flagForm->has('immediate_email')) {
$flagForm->get('immediate_email')->setData($immediateEmailChecked);
}
if ($flagForm->has('daily_email')) {
$flagForm->get('daily_email')->setData($dailyEmailChecked);
}
}
}
}
public function mapFormsToData($forms, &$viewData): void
{
$formsArray = iterator_to_array($forms);
$viewData = [];
foreach ($this->notificationFlagProviders as $flagProvider) {
$flag = $flagProvider->getFlag();
if (isset($formsArray[$flag])) {
$flagForm = $formsArray[$flag];
$viewData[$flag] = [];
if (true === $flagForm['immediate_email']->getData()) {
$viewData[$flag][] = User::NOTIF_FLAG_IMMEDIATE_EMAIL;
}
if (true === $flagForm['daily_email']->getData()) {
$viewData[$flag][] = User::NOTIF_FLAG_DAILY_DIGEST;
}
if ([] === $viewData[$flag]) {
$viewData[$flag][] = User::NOTIF_FLAG_IMMEDIATE_EMAIL;
}
}
}
}
}

View File

@@ -11,11 +11,9 @@ declare(strict_types=1);
namespace Chill\MainBundle\Form\Type;
use Chill\MainBundle\Form\DataMapper\NotificationFlagDataMapper;
use Chill\MainBundle\Notification\NotificationFlagManager;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
@@ -30,27 +28,24 @@ class NotificationFlagsType extends AbstractType
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->setDataMapper(new NotificationFlagDataMapper($this->notificationFlagProviders));
foreach ($this->notificationFlagProviders as $flagProvider) {
$flag = $flagProvider->getFlag();
$builder->add($flag, FormType::class, [
$flagBuilder = $builder->create($flag, options: [
'label' => $flagProvider->getLabel(),
'required' => false,
'compound' => true,
]);
$builder->get($flag)
$flagBuilder
->add('immediate_email', CheckboxType::class, [
'label' => false,
'required' => false,
'mapped' => false,
])
->add('daily_email', CheckboxType::class, [
->add('daily_digest', CheckboxType::class, [
'label' => false,
'required' => false,
'mapped' => false,
])
;
$builder->add($flagBuilder);
}
}
@@ -58,6 +53,7 @@ class NotificationFlagsType extends AbstractType
{
$resolver->setDefaults([
'data_class' => null,
'compound' => true,
]);
}
}

View File

@@ -11,31 +11,29 @@ declare(strict_types=1);
namespace Chill\MainBundle\Form;
use Chill\MainBundle\Action\User\UpdateProfile\UpdateProfileCommand;
use Chill\MainBundle\Form\Type\ChillPhoneNumberType;
use Chill\MainBundle\Form\Type\NotificationFlagsType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class UserProfileType extends AbstractType
class UpdateProfileType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('phonenumber', ChillPhoneNumberType::class, [
'required' => false,
])
->add('notificationFlags', NotificationFlagsType::class, [
'label' => false,
'mapped' => false,
])
->add('notificationFlags', NotificationFlagsType::class)
;
}
public function configureOptions(OptionsResolver $resolver)
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => \Chill\MainBundle\Entity\User::class,
'data_class' => UpdateProfileCommand::class,
]);
}
}

View File

@@ -59,7 +59,7 @@ class UserPasswordType extends AbstractType
'invalid_message' => 'The password fields must match',
'constraints' => [
new Length([
'min' => 9,
'min' => 14,
'minMessage' => 'The password must be greater than {{ limit }} characters',
]),
new NotBlank(),

View File

@@ -84,6 +84,8 @@ const emits = defineEmits<{
}
.modal-header .close {
border-top-right-radius: 0.3rem;
margin-right: 0;
margin-left: auto;
}
/*
* The following styles are auto-applied to elements with

View File

@@ -44,17 +44,7 @@
{% endif %}
{% endif %}
{% endblock content_view_actions_duplicate_link %}
{% block content_view_actions_merge %}
<li>
<a href="{{ chill_path_add_return_path('chill_thirdparty_find_duplicate',
{ 'thirdparty_id': entity.id }) }}"
title="{{ 'Merge'|trans }}"
class="btn btn-misc">
<i class="bi bi-chevron-contract"></i>
{{ 'Merge'|trans }}
</a>
</li>
{% endblock %}
{% block content_view_actions_merge %}{% endblock %}
{% block content_view_actions_edit_link %}
{% if chill_crud_action_exists(crud_name, 'edit') %}
{% if is_granted(chill_crud_config('role', crud_name, 'edit'), entity) %}

View File

@@ -280,11 +280,17 @@
</div>
{% endblock %}
{% block pick_linked_entities_widget %}
<input type="hidden" {{ block('widget_attributes') }} {% if value is not empty %}value="{{ value|escape('html_attr') }}" {% endif %} data-input-uniqid="{{ form.vars['uniqid'] }}" />
<div data-input-uniqid="{{ form.vars['uniqid'] }}" data-module="pick-linked-entities" data-pick-entities-type="{{ form.vars['pick-entities-type'] }}"
></div>
{% block pick_linked_entities_widget %}
<input type="hidden" {{ block('widget_attributes') }}
{% if value is not empty %}value="{{ value|escape('html_attr') }}" {% endif %}
data-input-uniqid="{{ form.vars['uniqid'] }}"/>
<div
data-input-uniqid="{{ form.vars['uniqid'] }}"
data-module="pick-linked-entities"
data-pick-entities-type="{{ form.vars['pick-entities-type'] }}"
data-suggested="{{ form.vars['suggested']|json_encode|escape('html_attr') }}"
></div>
{% endblock %}
{% block pick_postal_code_widget %}

View File

@@ -5,7 +5,7 @@
role="button"
data-bs-toggle="dropdown"
aria-expanded="false">
<i class="fa fa-flash"></i>
<i class="bi bi-lightning-fill"></i>
</a>
<div class="dropdown-menu">
{% for menu in menus %}

View File

@@ -64,7 +64,7 @@
{{ form_widget(flag.immediate_email, {'label_attr': { 'class': 'checkbox-inline checkbox-switch'}}) }}
</td>
<td class="text-center">
{{ form_widget(flag.daily_email, {'label_attr': { 'class': 'checkbox-inline checkbox-switch'}}) }}
{{ form_widget(flag.daily_digest, {'label_attr': { 'class': 'checkbox-inline checkbox-switch'}}) }}
</td>
</tr>
{% endfor %}

View File

@@ -0,0 +1,85 @@
<?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 Action\User\UpdateProfile;
use Chill\MainBundle\Action\User\UpdateProfile\UpdateProfileCommand;
use Chill\MainBundle\Action\User\UpdateProfile\UpdateProfileCommandHandler;
use Chill\MainBundle\Entity\User;
use libphonenumber\PhoneNumber;
use PHPUnit\Framework\TestCase;
/**
* @internal
*
* @coversNothing
*/
final class UpdateProfileCommandHandlerTest extends TestCase
{
public function testUpdateProfileWithNullPhoneAndFlags(): void
{
$user = new User();
// Pre-set some flags to opposite values to check they are updated
$flag = 'tickets';
$user->setNotificationImmediately($flag, true);
$user->setNotificationDailyDigest($flag, true);
$command = new UpdateProfileCommand(null);
$command->notificationFlags = [
$flag => [
'immediate_email' => false,
'daily_digest' => false,
],
];
(new UpdateProfileCommandHandler())->updateProfile($user, $command);
self::assertNull($user->getPhonenumber(), 'Phone should be set to null');
self::assertFalse($user->isNotificationSendImmediately($flag));
self::assertFalse($user->isNotificationDailyDigest($flag));
}
public function testUpdateProfileWithPhoneAndMultipleFlags(): void
{
$user = new User();
$phone = new PhoneNumber();
$phone->setCountryCode(33); // France
$phone->setNationalNumber(612345678);
$command = new UpdateProfileCommand($phone);
$command->notificationFlags = [
'reports' => [
'immediate_email' => true,
'daily_digest' => false,
],
'activities' => [
'immediate_email' => false,
'daily_digest' => true,
],
];
(new UpdateProfileCommandHandler())->updateProfile($user, $command);
// Phone assigned
self::assertInstanceOf(PhoneNumber::class, $user->getPhonenumber());
self::assertSame(33, $user->getPhonenumber()->getCountryCode());
self::assertSame('612345678', (string) $user->getPhonenumber()->getNationalNumber());
// Flags applied
self::assertTrue($user->isNotificationSendImmediately('reports'));
self::assertFalse($user->isNotificationDailyDigest('reports'));
self::assertFalse($user->isNotificationSendImmediately('activities'));
self::assertTrue($user->isNotificationDailyDigest('activities'));
}
}

View File

@@ -0,0 +1,103 @@
<?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 Action\User\UpdateProfile;
use Chill\MainBundle\Action\User\UpdateProfile\UpdateProfileCommand;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Notification\FlagProviders\NotificationFlagProviderInterface;
use Chill\MainBundle\Notification\NotificationFlagManager;
use libphonenumber\PhoneNumber;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Translation\TranslatableMessage;
/**
* @internal
*
* @coversNothing
*/
final class UpdateProfileCommandTest extends TestCase
{
public function testCreateTransfersPhonenumberAndNotificationFlags(): void
{
$user = new User();
// set a phone number
$phone = new PhoneNumber();
$phone->setCountryCode(32); // Belgium
$phone->setNationalNumber(471234567);
$user->setPhonenumber($phone);
// configure notification flags on the user via helpers
$flagA = 'foo';
$flagB = 'bar';
// For tickets: immediate true, daily false
$user->setNotificationImmediately($flagA, true);
$user->setNotificationDailyDigest($flagA, false);
// For reports: immediate false, daily true
$user->setNotificationImmediately($flagB, false);
$user->setNotificationDailyDigest($flagB, true);
// a third flag not explicitly set to validate default behavior from User
$flagC = 'foobar'; // by default immediate-email is true, daily-digest is false per User::getNotificationFlagData
$manager = $this->createNotificationFlagManager([$flagA, $flagB, $flagC]);
$command = UpdateProfileCommand::create($user, $manager);
// phone number transferred
self::assertInstanceOf(PhoneNumber::class, $command->phonenumber);
self::assertSame($phone->getCountryCode(), $command->phonenumber->getCountryCode());
self::assertSame($phone->getNationalNumber(), $command->phonenumber->getNationalNumber());
// flags transferred consistently
self::assertArrayHasKey($flagA, $command->notificationFlags);
self::assertArrayHasKey($flagB, $command->notificationFlags);
self::assertArrayHasKey($flagC, $command->notificationFlags);
self::assertSame([
'immediate_email' => true,
'daily_digest' => false,
], $command->notificationFlags[$flagA]);
self::assertSame([
'immediate_email' => false,
'daily_digest' => true,
], $command->notificationFlags[$flagB]);
// default from User::getNotificationFlagData -> immediate true, daily false
self::assertSame([
'immediate_email' => true,
'daily_digest' => false,
], $command->notificationFlags[$flagC]);
}
private function createNotificationFlagManager(array $flags): NotificationFlagManager
{
$providers = array_map(fn (string $flag) => new class ($flag) implements NotificationFlagProviderInterface {
public function __construct(private readonly string $flag) {}
public function getFlag(): string
{
return $this->flag;
}
public function getLabel(): TranslatableMessage
{
return new TranslatableMessage($this->flag);
}
}, $flags);
return new NotificationFlagManager($providers);
}
}

View File

@@ -45,7 +45,7 @@ final class UserControllerTest extends WebTestCase
self::assertResponseIsSuccessful();
$username = 'Test_user'.uniqid();
$password = 'Password1234!';
$password = 'Password_1234!';
// Fill in the form and submit it
@@ -99,7 +99,7 @@ final class UserControllerTest extends WebTestCase
{
$client = $this->getClientAuthenticatedAsAdmin();
$crawler = $client->request('GET', "/fr/admin/user/{$userId}/edit_password");
$newPassword = '1234Password!';
$newPassword = '1234_Password!';
$form = $crawler->selectButton('Changer le mot de passe')->form([
'chill_mainbundle_user_password[new_password][first]' => $newPassword,

View File

@@ -96,11 +96,13 @@ final class NotificationTest extends KernelTestCase
$this->assertTrue($user->isNotificationSendImmediately($notification->getType()), 'Should return true when no notification flags are set, by default immediate email');
// immediate-email preference
$user->setNotificationFlags(['test_notification_type' => [User::NOTIF_FLAG_IMMEDIATE_EMAIL, User::NOTIF_FLAG_DAILY_DIGEST]]);
$user->setNotificationImmediately('test_notification_type', true);
$user->setNotificationDailyDigest('test_notification_type', true);
$this->assertTrue($user->isNotificationSendImmediately($notification->getType()), 'Should return true when preferences contain immediate-email');
// daily-email preference
$user->setNotificationFlags(['test_notification_type' => [User::NOTIF_FLAG_DAILY_DIGEST]]);
$user->setNotificationDailyDigest('test_notification_type', true);
$user->setNotificationImmediately('test_notification_type', false);
$this->assertFalse($user->isNotificationSendImmediately($notification->getType()), 'Should return false when preference is daily-email only');
$this->assertTrue($user->isNotificationDailyDigest($notification->getType()), 'Should return true when preference is daily-email');

View File

@@ -0,0 +1,82 @@
<?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\Tests\Entity;
use Chill\MainBundle\Entity\User;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
/**
* @internal
*
* @coversNothing
*/
class UserNotificationFlagsPersistenceTest extends KernelTestCase
{
public function testFlushPersistsNotificationFlagsChanges(): void
{
self::bootKernel();
$em = self::getContainer()->get('doctrine')->getManager();
$user = new User();
$user->setUsername('user_'.bin2hex(random_bytes(4)));
$user->setLabel('Test User');
$user->setPassword('secret');
// Étape 1: créer et persister lutilisateur
$em->persist($user);
$em->flush();
$id = $user->getId();
self::assertNotNull($id, 'User should have an ID after flush');
try {
// Sanity check: par défaut, pas de daily digest pour "alerts"
self::assertFalse($user->isNotificationDailyDigest('alerts'));
// Étape 2: activer le daily digest -> setNotificationFlagElement réassigne la propriété
$user->setNotificationDailyDigest('alerts', true);
$em->flush(); // persist le changement
$em->clear(); // simule un nouveau cycle de requête
// Étape 3: recharger depuis la base et vérifier la persistance
/** @var User $reloaded */
$reloaded = $em->find(User::class, $id);
self::assertNotNull($reloaded);
self::assertTrue(
$reloaded->isNotificationDailyDigest('alerts'),
'Daily digest flag should be persisted'
);
// Étape 4: modifier via setNotificationFlagData (remplacement du tableau)
// Cette méthode doit réassigner la propriété (copie -> réassignation)
$reloaded->setNotificationImmediately('alerts', true);
$reloaded->setNotificationDailyDigest('alerts', false);
$em->flush();
$em->clear();
/** @var User $reloaded2 */
$reloaded2 = $em->find(User::class, $id);
self::assertNotNull($reloaded2);
// Le daily digest nest plus actif, seul immediate-email est présent
self::assertFalse($reloaded2->isNotificationDailyDigest('alerts'));
self::assertTrue($reloaded2->isNotificationSendImmediately('alerts'));
} finally {
// Nettoyage
$managed = $em->find(User::class, $id);
if (null !== $managed) {
$em->remove($managed);
$em->flush();
}
$em->clear();
}
}
}

View File

@@ -99,4 +99,22 @@ class UserTest extends TestCase
$user->setAbsenceEnd(null);
self::assertFalse($user->isAbsent(), 'Should not be absent if start is null');
}
public function testSetNotification(): void
{
$user = new User();
self::assertTrue($user->isNotificationSendImmediately('dummy'));
self::assertFalse($user->isNotificationDailyDigest('dummy'));
$user->setNotificationImmediately('dummy', false);
self::assertFalse($user->isNotificationSendImmediately('dummy'));
$user->setNotificationDailyDigest('dummy', true);
self::assertTrue($user->isNotificationDailyDigest('dummy'));
$user->setNotificationImmediately('dummy', true);
self::assertTrue($user->isNotificationSendImmediately('dummy'));
self::assertTrue($user->isNotificationDailyDigest('dummy'));
}
}

View File

@@ -144,7 +144,7 @@ class NotificationMailerTest extends TestCase
$idProperty->setValue($user, 456);
// Set notification flags for the user
$user->setNotificationFlags(['test_notification_type' => [User::NOTIF_FLAG_IMMEDIATE_EMAIL]]);
$user->setNotificationImmediately('test_notification_type', true);
$messageBus = $this->createMock(MessageBusInterface::class);
$messageBus->expects($this->once())

View File

@@ -113,3 +113,5 @@ services:
Chill\MainBundle\Service\EntityInfo\ViewEntityInfoManager:
arguments:
$vienEntityInfoProviders: !tagged_iterator chill_main.entity_info_provider
Chill\MainBundle\Action\User\UpdateProfile\UpdateProfileCommandHandler: ~

View File

@@ -18,6 +18,7 @@ use Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkRepos
use Chill\PersonBundle\Service\AccompanyingPeriodWork\AccompanyingPeriodWorkMergeService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Translation\TranslatableMessage;
@@ -32,7 +33,7 @@ class AccompanyingPeriodWorkDuplicateController extends AbstractController
* @ParamConverter("accompanyingPeriodWork", options={"id": "acpw_id"})
*/
#[Route(path: '{_locale}/person/accompanying-period/work/{id}/assign-duplicate', name: 'chill_person_accompanying_period_work_assign_duplicate')]
public function assignDuplicate(AccompanyingPeriodWork $acpw, Request $request)
public function assignDuplicate(AccompanyingPeriodWork $acpw, Request $request): Response
{
$accompanyingPeriod = $acpw->getAccompanyingPeriod();
@@ -79,7 +80,7 @@ class AccompanyingPeriodWorkDuplicateController extends AbstractController
* @ParamConverter("acpw2", options={"id": "acpw2_id"})
*/
#[Route(path: '/{_locale}/person/{acpw1_id}/acpw-duplicate/{acpw2_id}/confirm', name: 'chill_person_acpw_duplicate_confirm')]
public function confirmAction(AccompanyingPeriodWork $acpw1, AccompanyingPeriodWork $acpw2, Request $request)
public function confirmAction(AccompanyingPeriodWork $acpw1, AccompanyingPeriodWork $acpw2, Request $request): Response
{
$accompanyingPeriod = $acpw1->getAccompanyingPeriod();
@@ -98,6 +99,7 @@ class AccompanyingPeriodWorkDuplicateController extends AbstractController
$session->getFlashBag()->add('success', new TranslatableMessage('acpw_duplicate.Successfully merged'));
}
return $this->redirectToRoute('chill_person_accompanying_period_work_show', ['id' => $acpw1->getId()]);
}

View File

@@ -12,6 +12,7 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Controller;
use Chill\MainBundle\Routing\ChillUrlGeneratorInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluation;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument;
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodWorkVoter;
use Chill\PersonBundle\Service\AccompanyingPeriodWorkEvaluationDocument\AccompanyingPeriodWorkEvaluationDocumentDuplicator;
@@ -24,15 +25,16 @@ use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\SerializerInterface;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
class AccompanyingPeriodWorkEvaluationDocumentDuplicateController
readonly class AccompanyingPeriodWorkEvaluationDocumentDuplicateController
{
public function __construct(
private readonly AccompanyingPeriodWorkEvaluationDocumentDuplicator $duplicator,
private readonly Security $security,
private readonly SerializerInterface $serializer,
private readonly EntityManagerInterface $entityManager,
private readonly ChillUrlGeneratorInterface $urlGenerator,
private AccompanyingPeriodWorkEvaluationDocumentDuplicator $duplicator,
private Security $security,
private SerializerInterface $serializer,
private EntityManagerInterface $entityManager,
private ChillUrlGeneratorInterface $urlGenerator,
) {}
#[Route('/api/1.0/person/accompanying-course-work-evaluation-document/{id}/duplicate', methods: ['POST'])]
@@ -56,6 +58,32 @@ class AccompanyingPeriodWorkEvaluationDocumentDuplicateController
);
}
/**
* @ParamConverter("document", options={"id": "document_id"})
* @ParamConverter("evaluation", options={"id": "evaluation_id"})
*/
#[Route('/api/1.0/person/accompanying-course-work-evaluation-document/{document_id}/evaluation/{evaluation_id}/duplicate', methods: ['POST'])]
public function duplicateToEvaluationApi(AccompanyingPeriodWorkEvaluationDocument $document, AccompanyingPeriodWorkEvaluation $evaluation): Response
{
$work = $evaluation->getAccompanyingPeriodWork();
if (!$this->security->isGranted(AccompanyingPeriodWorkVoter::UPDATE, $work)) {
throw new AccessDeniedHttpException('not allowed to edit this accompanying period work');
}
$duplicatedDocument = $this->duplicator->duplicateToEvaluation($document, $evaluation);
$this->entityManager->persist($duplicatedDocument);
$this->entityManager->persist($duplicatedDocument->getStoredObject());
$this->entityManager->persist($evaluation);
$this->entityManager->flush();
return new JsonResponse(
$this->serializer->serialize($duplicatedDocument, 'json', [AbstractNormalizer::GROUPS => ['read']]),
json: true
);
}
#[Route('/{_locale}/person/accompanying-course-work-evaluation-document/{id}/duplicate', name: 'chill_person_accompanying_period_work_evaluation_document_duplicate', methods: ['POST'])]
public function duplicate(AccompanyingPeriodWorkEvaluationDocument $document): Response
{

View File

@@ -0,0 +1,58 @@
<?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\PersonBundle\Controller;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluation;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument;
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodWorkVoter;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\SerializerInterface;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
readonly class AccompanyingPeriodWorkEvaluationDocumentMoveController
{
public function __construct(
private Security $security,
private SerializerInterface $serializer,
private EntityManagerInterface $entityManager,
) {}
/**
* @ParamConverter("document", options={"id": "document_id"})
* @ParamConverter("evaluation", options={"id": "evaluation_id"})
*/
#[Route('/api/1.0/person/accompanying-course-work-evaluation-document/{document_id}/evaluation/{evaluation_id}/move', methods: ['POST'])]
public function moveToEvaluationApi(AccompanyingPeriodWorkEvaluationDocument $document, AccompanyingPeriodWorkEvaluation $evaluation): Response
{
$work = $evaluation->getAccompanyingPeriodWork();
if (!$this->security->isGranted(AccompanyingPeriodWorkVoter::UPDATE, $work)) {
throw new AccessDeniedHttpException('not allowed to edit this accompanying period work');
}
$document->setAccompanyingPeriodWorkEvaluation($evaluation);
$this->entityManager->persist($document);
$this->entityManager->flush();
return new JsonResponse(
$this->serializer->serialize($document, 'json', [AbstractNormalizer::GROUPS => ['read']]),
json: true
);
}
}

View File

@@ -98,16 +98,6 @@ class AccompanyingPeriodWorkEvaluationDocument implements \Chill\MainBundle\Doct
public function setAccompanyingPeriodWorkEvaluation(?AccompanyingPeriodWorkEvaluation $accompanyingPeriodWorkEvaluation): AccompanyingPeriodWorkEvaluationDocument
{
// if an evaluation is already associated, we cannot change the association (removing the association,
// by setting a null value, is allowed.
if (
$this->accompanyingPeriodWorkEvaluation instanceof AccompanyingPeriodWorkEvaluation
&& $accompanyingPeriodWorkEvaluation instanceof AccompanyingPeriodWorkEvaluation
) {
if ($this->accompanyingPeriodWorkEvaluation !== $accompanyingPeriodWorkEvaluation) {
throw new \RuntimeException('It is not allowed to change the evaluation for a document');
}
}
$this->accompanyingPeriodWorkEvaluation = $accompanyingPeriodWorkEvaluation;
return $this;

View File

@@ -24,7 +24,7 @@ class FindAccompanyingPeriodWorkType extends AbstractType
{
$builder
->add('acpw', PickLinkedAccompanyingPeriodWorkType::class, [
'label' => 'Social action',
'label' => 'Accompanying period work',
'multiple' => false,
'accompanyingPeriod' => $options['accompanyingPeriod'],
])

View File

@@ -16,18 +16,26 @@ use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
class PickLinkedAccompanyingPeriodWorkType extends AbstractType
{
public function __construct(private readonly NormalizerInterface $normalizer) {}
public function buildView(FormView $view, FormInterface $form, array $options)
{
$view->vars['multiple'] = $options['multiple'];
$view->vars['types'] = ['acpw'];
$view->vars['uniqid'] = uniqid('pick_acpw_dyn');
$view->vars['suggested'] = [];
$view->vars['as_id'] = true === $options['as_id'] ? '1' : '0';
$view->vars['submit_on_adding_new_entity'] = false;
$view->vars['pick-entities-type'] = 'acpw';
$view->vars['attr']['data-accompanying-period-id'] = $options['accompanyingPeriod']->getId();
foreach ($options['suggested'] as $suggestion) {
$view->vars['suggested'][] = $this->normalizer->normalize($suggestion, 'json', ['groups' => 'read']);
}
}
public function configureOptions(OptionsResolver $resolver)
@@ -38,6 +46,7 @@ class PickLinkedAccompanyingPeriodWorkType extends AbstractType
->setDefault('multiple', false)
->setAllowedTypes('multiple', ['bool'])
->setDefault('compound', false)
->setDefault('suggested', [])
->setDefault('as_id', false)
->setAllowedTypes('as_id', ['bool'])
->setDefault('submit_on_adding_new_entity', false)

View File

@@ -41,7 +41,8 @@ document.addEventListener("DOMContentLoaded", () => {
methods: {
pickWork: function (payload: { work: AccompanyingPeriodWork }) {
console.log("payload", payload);
input.value = payload.work.id.toString();
input.value = payload.work.id?.toString() ?? "";
},
},
});

View File

@@ -84,7 +84,7 @@ export interface AccompanyingPeriodWorkEvaluationDocument {
}
export interface AccompanyingPeriodWork {
id: number;
id?: number;
accompanyingPeriod?: AccompanyingPeriod;
accompanyingPeriodWorkEvaluations: AccompanyingPeriodWorkEvaluation[];
createdAt?: string;

View File

@@ -972,7 +972,7 @@ div#workEditor {
font-size: 85%;
}
i.fa {
& > i.fa {
padding: 0.25rem;
color: $white;

View File

@@ -0,0 +1,31 @@
<template>
<div class="row mb-3">
<label class="col-sm-4 col-form-label visually-hidden">{{
trans(EVALUATION_PUBLIC_COMMENT)
}}</label>
<div class="col-sm-12">
<ckeditor
:editor="ClassicEditor"
:config="classicEditorConfig"
:placeholder="trans(EVALUATION_COMMENT_PLACEHOLDER)"
:value="comment"
@input="$emit('update:comment', $event)"
tag-name="textarea"
></ckeditor>
</div>
</div>
</template>
<script setup>
import { Ckeditor } from "@ckeditor/ckeditor5-vue";
import { ClassicEditor } from "ckeditor5";
import classicEditorConfig from "ChillMainAssets/module/ckeditor5/editor_config";
import {
EVALUATION_PUBLIC_COMMENT,
EVALUATION_COMMENT_PLACEHOLDER,
trans,
} from "translator";
defineProps(["comment"]);
defineEmits(["update:comment"]);
</script>

View File

@@ -0,0 +1,71 @@
<template>
<div class="row mb-3">
<label class="col-4 col-sm-2 col-md-4 col-lg-2 col-form-label">
{{ trans(EVALUATION_STARTDATE) }}
</label>
<div class="col-8 col-sm-4 col-md-8 col-lg-4">
<input
class="form-control form-control-sm"
type="date"
:value="startDate"
@input="$emit('update:startDate', $event.target.value)"
/>
</div>
<label class="col-4 col-sm-2 col-md-4 col-lg-2 col-form-label">
{{ trans(EVALUATION_ENDDATE) }}
</label>
<div class="col-8 col-sm-4 col-md-8 col-lg-4">
<input
class="form-control form-control-sm"
type="date"
:value="endDate"
@input="$emit('update:endDate', $event.target.value)"
/>
</div>
</div>
<div class="row mb-3">
<label class="col-4 col-sm-2 col-md-4 col-lg-2 col-form-label">
{{ trans(EVALUATION_MAXDATE) }}
</label>
<div class="col-8 col-sm-4 col-md-8 col-lg-4">
<input
class="form-control form-control-sm"
type="date"
:value="maxDate"
@input="$emit('update:maxDate', $event.target.value)"
/>
</div>
<label class="col-4 col-sm-2 col-md-4 col-lg-2 col-form-label">
{{ trans(EVALUATION_WARNING_INTERVAL) }}
</label>
<div class="col-8 col-sm-4 col-md-8 col-lg-4">
<input
class="form-control form-control-sm"
type="number"
:value="warningInterval"
@input="$emit('update:warningInterval', $event.target.value)"
/>
</div>
</div>
</template>
<script setup>
import {
EVALUATION_STARTDATE,
EVALUATION_ENDDATE,
EVALUATION_MAXDATE,
EVALUATION_WARNING_INTERVAL,
trans,
} from "translator";
defineProps(["startDate", "endDate", "maxDate", "warningInterval"]);
defineEmits([
"update:startDate",
"update:endDate",
"update:maxDate",
"update:warningInterval",
]);
</script>

View File

@@ -0,0 +1,76 @@
<template>
<div class="row mb-3">
<h6>{{ trans(EVALUATION_DOCUMENT_ADD) }} :</h6>
<pick-template
entityClass="Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluation"
:id="evaluation.id"
:templates="templates"
:preventDefaultMoveToGenerate="true"
@go-to-generate-document="submitBeforeGenerate"
>
<template v-slot:title>
<label class="col-form-label">{{
trans(EVALUATION_GENERATE_A_DOCUMENT)
}}</label>
</template>
</pick-template>
<div>
<label class="col-form-label">{{
trans(EVALUATION_DOCUMENT_UPLOAD)
}}</label>
<ul class="record_actions document-upload">
<li>
<drop-file-modal
:allow-remove="false"
@add-document="emit('addDocument', $event)"
></drop-file-modal>
</li>
</ul>
</div>
</div>
</template>
<script setup>
import PickTemplate from "ChillDocGeneratorAssets/vuejs/_components/PickTemplate.vue";
import DropFileModal from "ChillDocStoreAssets/vuejs/DropFileWidget/DropFileModal.vue";
import {
EVALUATION_DOCUMENT_ADD,
EVALUATION_DOCUMENT_UPLOAD,
EVALUATION_GENERATE_A_DOCUMENT,
trans,
} from "translator";
import { buildLink } from "ChillDocGeneratorAssets/lib/document-generator";
import { useStore } from "vuex";
const store = useStore();
const props = defineProps(["evaluation", "templates"]);
const emit = defineEmits(["addDocument"]);
async function submitBeforeGenerate({ template }) {
const callback = (data) => {
let evaluationId = data.accompanyingPeriodWorkEvaluations.find(
(e) => e.key === props.evaluation.key,
).id;
window.location.assign(
buildLink(
template,
evaluationId,
"Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWorkEvaluation",
),
);
};
return store.dispatch("submit", callback).catch((e) => {
console.log(e);
throw e;
});
}
</script>
<style scoped>
ul.document-upload {
justify-content: flex-start;
}
</style>

View File

@@ -0,0 +1,455 @@
<template>
<div class="row mb-3">
<h5>{{ trans(EVALUATION_DOCUMENTS) }} :</h5>
<div class="flex-table">
<div
class="item-bloc"
v-for="(d, i) in documents"
:key="d.id"
:class="[
parseInt(docAnchorId) === d.id ? 'bg-blink' : 'nothing',
]"
>
<div :id="'document_' + d.id" class="item-row">
<div class="input-group input-group-lg mb-3 row">
<label class="col-sm-3 col-form-label"
>Titre du document:</label
>
<div class="col-sm-9">
<input
class="form-control document-title"
type="text"
:value="d.title"
:id="d.id"
:data-key="i"
@input="$emit('inputDocumentTitle', $event)"
/>
</div>
</div>
</div>
<div class="item-row">
<div class="item-col item-meta">
<p v-if="d.createdBy" class="createdBy">
Créé par {{ d.createdBy.text }}<br />
Le
{{
$d(ISOToDatetime(d.createdAt.datetime), "long")
}}
</p>
</div>
</div>
<div class="item-row">
<div class="item-col">
<ul class="record_actions">
<li
v-if="
d.workflows_availables.length > 0 ||
d.workflows.length > 0
"
>
<list-workflow-modal
:workflows="d.workflows"
:allowCreate="true"
relatedEntityClass="Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument"
:relatedEntityId="d.id"
:workflowsAvailables="
d.workflows_availables
"
:preventDefaultMoveToGenerate="true"
:goToGenerateWorkflowPayload="{ doc: d }"
@go-to-generate-workflow="
goToGenerateWorkflowEvaluationDocument
"
></list-workflow-modal>
</li>
<li>
<button
v-if="AmIRefferer"
class="btn btn-notify"
@click="
$emit(
'goToGenerateNotification',
d,
false,
)
"
></button>
<template v-else>
<button
id="btnGroupNotifyButtons"
type="button"
class="btn btn-notify dropdown-toggle"
:title="
trans(EVALUATION_NOTIFICATION_SEND)
"
data-bs-toggle="dropdown"
aria-expanded="false"
>
&nbsp;
</button>
<ul
class="dropdown-menu"
aria-labelledby="btnGroupNotifyButtons"
>
<li>
<a
class="dropdown-item"
@click="
goToGenerateDocumentNotification(
d,
false,
)
"
>
{{
trans(
EVALUATION_NOTIFICATION_NOTIFY_REFERRER,
)
}}
</a>
</li>
<li>
<a
class="dropdown-item"
@click="
goToGenerateDocumentNotification(
d,
false,
)
"
>
{{
trans(
EVALUATION_NOTIFICATION_NOTIFY_ANY,
)
}}
</a>
</li>
</ul>
</template>
</li>
<li>
<document-action-buttons-group
:stored-object="d.storedObject"
:filename="d.title"
:can-edit="true"
:execute-before-leave="
submitBeforeLeaveToEditor
"
:davLink="
d.storedObject._links?.dav_link.href
"
:davLinkExpiration="
d.storedObject._links?.dav_link
.expiration
"
@on-stored-object-status-change="
$emit('statusDocumentChanged', $event)
"
></document-action-buttons-group>
</li>
<!--replace document-->
<li
v-if="
Number.isInteger(d.id) &&
d.storedObject._permissions.canEdit
"
>
<drop-file-modal
:existing-doc="d.storedObject"
:allow-remove="false"
@add-document="
(arg) =>
replaceDocument(
d,
arg.stored_object,
arg.stored_object_version,
)
"
></drop-file-modal>
</li>
<li v-if="Number.isInteger(d.id)">
<div class="duplicate-dropdown">
<button
class="btn btn-outline-primary dropdown-toggle"
type="button"
data-bs-toggle="dropdown"
aria-expanded="false"
>
<i class="bi bi-lightning-fill"></i>
</button>
<ul class="dropdown-menu">
<!--delete-->
<li v-if="d.workflows.length === 0">
<a
class="dropdown-item"
@click="
$emit('removeDocument', d)
"
>
<i
class="fa fa-trash-o"
aria-hidden="true"
></i>
{{
trans(
EVALUATION_DOCUMENT_DELETE,
)
}}
</a>
</li>
<!--duplicate document-->
<li>
<a
class="dropdown-item"
@click="
$emit(
'duplicateDocument',
d,
)
"
>
<i
class="fa fa-copy"
aria-hidden="true"
></i>
{{
trans(
EVALUATION_DOCUMENT_DUPLICATE_HERE,
)
}}
</a>
</li>
<li>
<a
class="dropdown-item"
@click="
prepareDocumentDuplicationToWork(
d,
)
"
>
<i
class="fa fa-copy"
aria-hidden="true"
></i>
{{
trans(
EVALUATION_DOCUMENT_DUPLICATE_TO_OTHER_EVALUATION,
)
}}</a
>
</li>
<!--move document-->
<li
v-if="
d.storedObject._permissions
.canEdit
"
>
<a
class="dropdown-item"
@click="
prepareDocumentMoveToWork(d)
"
>
<i
class="fa fa-arrows"
aria-hidden="true"
></i>
{{
trans(
EVALUATION_DOCUMENT_MOVE,
)
}}</a
>
</li>
</ul>
</div>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<AccompanyingPeriodWorkSelectorModal
v-if="showAccompanyingPeriodSelector"
v-model:selectedAcpw="selectedAcpw"
:accompanying-period-id="accompanyingPeriodId"
:is-evaluation-selector="true"
:ignore-accompanying-period-work-ids="[]"
@close-modal="showAccompanyingPeriodSelector = false"
@update:selectedEvaluation="selectedEvaluation = $event"
/>
</template>
<script setup>
import { ISOToDatetime } from "ChillMainAssets/chill/js/date";
import ListWorkflowModal from "ChillMainAssets/vuejs/_components/EntityWorkflow/ListWorkflowModal.vue";
import DocumentActionButtonsGroup from "ChillDocStoreAssets/vuejs/DocumentActionButtonsGroup.vue";
import DropFileModal from "ChillDocStoreAssets/vuejs/DropFileWidget/DropFileModal.vue";
import {
EVALUATION_NOTIFICATION_NOTIFY_REFERRER,
EVALUATION_NOTIFICATION_NOTIFY_ANY,
EVALUATION_NOTIFICATION_SEND,
EVALUATION_DOCUMENTS,
EVALUATION_DOCUMENT_MOVE,
EVALUATION_DOCUMENT_DELETE,
EVALUATION_DOCUMENT_DUPLICATE_HERE,
EVALUATION_DOCUMENT_DUPLICATE_TO_OTHER_EVALUATION,
trans,
} from "translator";
import { computed, ref, watch } from "vue";
import AccompanyingPeriodWorkSelectorModal from "ChillPersonAssets/vuejs/_components/AccompanyingPeriodWorkSelector/AccompanyingPeriodWorkSelectorModal.vue";
import { buildLinkCreate } from "ChillMainAssets/lib/entity-workflow/api";
import { buildLinkCreate as buildLinkCreateNotification } from "ChillMainAssets/lib/entity-notification/api";
import { useStore } from "vuex";
const props = defineProps([
"documents",
"docAnchorId",
"accompanyingPeriodId",
"evaluation",
]);
const emit = defineEmits([
"inputDocumentTitle",
"removeDocument",
"duplicateDocument",
"statusDocumentChanged",
"duplicateDocumentToWork",
]);
const store = useStore();
const showAccompanyingPeriodSelector = ref(false);
const selectedEvaluation = ref(null);
const selectedDocumentToDuplicate = ref(null);
const selectedDocumentToMove = ref(null);
const AmIRefferer = computed(() => {
return !(
store.state.work.accompanyingPeriod.user &&
store.state.me &&
store.state.work.accompanyingPeriod.user.id !== store.state.me.id
);
});
const prepareDocumentDuplicationToWork = (d) => {
selectedDocumentToDuplicate.value = d;
/** ensure selectedDocumentToMove is null */
selectedDocumentToMove.value = null;
showAccompanyingPeriodSelector.value = true;
};
const prepareDocumentMoveToWork = (d) => {
selectedDocumentToMove.value = d;
/** ensure selectedDocumentToDuplicate is null */
selectedDocumentToDuplicate.value = null;
showAccompanyingPeriodSelector.value = true;
};
watch(selectedEvaluation, (val) => {
if (selectedDocumentToDuplicate.value) {
emit("duplicateDocumentToEvaluation", {
evaluation: val,
document: selectedDocumentToDuplicate.value,
});
} else {
emit("moveDocumentToEvaluation", {
evaluationDest: val,
document: selectedDocumentToMove.value,
});
}
});
async function goToGenerateWorkflowEvaluationDocument({
workflowName,
payload,
}) {
const callback = (data) => {
let evaluation = data.accompanyingPeriodWorkEvaluations.find(
(e) => e.key === props.evaluation.key,
);
let updatedDocument = evaluation.documents.find(
(d) => d.key === payload.doc.key,
);
window.location.assign(
buildLinkCreate(
workflowName,
"Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWorkEvaluationDocument",
updatedDocument.id,
),
);
};
return store.dispatch("submit", callback).catch((e) => {
console.log(e);
throw e;
});
}
/**
* Replaces a document in the store with a new document.
*
* @param {Object} oldDocument - The document to be replaced.
* @param {StoredObject} storedObject - The stored object of the new document.
* @param {StoredObjectVersion} storedObjectVersion - The new version of the document
* @return {void}
*/
async function replaceDocument(oldDocument, storedObject, storedObjectVersion) {
let document = {
type: "accompanying_period_work_evaluation_document",
storedObject: storedObject,
title: oldDocument.title,
};
return store.commit("replaceDocument", {
key: props.evaluation.key,
document,
oldDocument: oldDocument,
stored_object_version: storedObjectVersion,
});
}
async function goToGenerateDocumentNotification(document, tos) {
const callback = (data) => {
let evaluation = data.accompanyingPeriodWorkEvaluations.find(
(e) => e.key === props.evaluation.key,
);
let updatedDocument = evaluation.documents.find(
(d) => d.key === document.key,
);
window.location.assign(
buildLinkCreateNotification(
"Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWorkEvaluationDocument",
updatedDocument.id,
tos === true
? store.state.work.accompanyingPeriod.user?.id
: null,
window.location.pathname +
window.location.search +
window.location.hash,
),
);
};
return store.dispatch("submit", callback).catch((e) => {
console.log(e);
throw e;
});
}
async function submitBeforeLeaveToEditor() {
console.log("submit beore edit 2");
// empty callback
const callback = () => null;
return store.dispatch("submit", callback).catch((e) => {
console.log(e);
throw e;
});
}
</script>

View File

@@ -0,0 +1,32 @@
<template>
<div class="row mb-3">
<label class="col-4 col-sm-2 col-md-4 col-lg-2 col-form-label">
{{ trans(EVALUATION_TIME_SPENT) }}
</label>
<div class="col-8 col-sm-4 col-md-8 col-lg-4">
<select
class="form-control form-control-sm"
:value="timeSpent"
@input="$emit('update:timeSpent', $event.target.value)"
>
<option disabled value="">
{{ trans(EVALUATION_TIME_SPENT) }}
</option>
<option
v-for="time in timeSpentChoices"
:value="time.value"
:key="time.value"
>
{{ time.text }}
</option>
</select>
</div>
</div>
</template>
<script setup>
import { EVALUATION_TIME_SPENT, trans } from "translator";
defineProps(["timeSpent", "timeSpentChoices"]);
defineEmits(["update:timeSpent"]);
</script>

View File

@@ -11,12 +11,13 @@ import { findSocialActionsBySocialIssue } from "ChillPersonAssets/vuejs/_api/Soc
import { create } from "ChillPersonAssets/vuejs/_api/AccompanyingCourseWork.js";
import { fetchResults, makeFetch } from "ChillMainAssets/lib/api/apiMethods.ts";
import { fetchTemplates } from "ChillDocGeneratorAssets/api/pickTemplate.js";
import { duplicate } from "../_api/accompanyingCourseWorkEvaluationDocument";
import {
duplicate,
duplicateDocumentToEvaluation,
moveDocumentToEvaluation,
} from "../_api/accompanyingCourseWorkEvaluationDocument";
const debug = process.env.NODE_ENV !== "production";
const evalFQDN = encodeURIComponent(
"Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWorkEvaluation",
);
const store = createStore({
strict: debug,
@@ -298,15 +299,47 @@ const store = createStore({
);
},
addDuplicatedDocument(state, { document, evaluation_key }) {
console.log("add duplicated document", document);
console.log("add duplicated dcuemnt - evaluation key", evaluation_key);
let evaluation = state.evaluationsPicked.find(
(e) => e.key === evaluation_key,
);
document.key = evaluation.documents.length + 1;
evaluation.documents.splice(0, 0, document);
},
addDuplicatedDocumentToEvaluation(state, { document, evaluation }) {
let evaluationDest = state.evaluationsPicked.find(
(e) => e.id === evaluation.id,
);
if (evaluationDest) {
console.log("add duplicated document to evaluation", evaluationDest);
document.key = evaluationDest.documents.length + 1;
evaluationDest.documents.splice(0, 0, document);
}
},
moveDocumentToEvaluation(
state,
{ evaluationInitial, evaluationDest, document },
) {
let evaluationA = state.evaluationsPicked.find(
(e) => e.id === evaluationInitial.id,
);
let evaluationB = state.evaluationsPicked.find(
(e) => e.id === evaluationDest.id,
);
if (evaluationB) {
// add document to chosen evaluation if evaluation is part of the same social work
document.key = evaluationB.documents.length + 1;
evaluationB.documents.splice(0, 0, document);
}
// remove document from original evaluation
const indexToRemove = evaluationA.documents.findIndex(
(doc) => doc.id === document.id,
);
if (indexToRemove !== -1) {
evaluationA.documents.splice(indexToRemove, 1);
}
},
/**
* Replaces a document in the state with a new document.
*
@@ -603,6 +636,44 @@ const store = createStore({
const newDoc = await duplicate(document.id);
commit("addDuplicatedDocument", { document: newDoc, evaluation_key });
},
async duplicateDocumentToEvaluation({ commit }, { document, evaluation }) {
try {
const newDoc = await duplicateDocumentToEvaluation(
document.id,
evaluation.id,
);
commit("addDuplicatedDocumentToEvaluation", {
document: newDoc,
evaluation,
});
return newDoc;
} catch (error) {
console.error("Failed to move document:", error);
throw error;
}
},
async moveDocumentToEvaluation(
{ commit },
{ evaluationInitial, evaluationDest, document },
) {
try {
const response = await moveDocumentToEvaluation(
document.id,
evaluationDest.id,
);
commit("moveDocumentToEvaluation", {
evaluationInitial,
evaluationDest,
document,
});
return response;
} catch (error) {
console.error("Failed to move document:", error);
throw error;
}
},
removeDocument({ commit }, payload) {
commit("removeDocument", payload);
},

View File

@@ -9,3 +9,23 @@ export const duplicate = async (
`/api/1.0/person/accompanying-course-work-evaluation-document/${id}/duplicate`,
);
};
export const duplicateDocumentToEvaluation = async (
document_id: number,
evaluation_id: number,
): Promise<AccompanyingPeriodWorkEvaluationDocument> => {
return makeFetch<null, AccompanyingPeriodWorkEvaluationDocument>(
"POST",
`/api/1.0/person/accompanying-course-work-evaluation-document/${document_id}/evaluation/${evaluation_id}/duplicate`,
);
};
export const moveDocumentToEvaluation = async (
document_id: number,
evaluation_id: number,
): Promise<AccompanyingPeriodWorkEvaluationDocument> => {
return makeFetch<null, AccompanyingPeriodWorkEvaluationDocument>(
"POST",
`/api/1.0/person/accompanying-course-work-evaluation-document/${document_id}/evaluation/${evaluation_id}/move`,
);
};

View File

@@ -0,0 +1,70 @@
<template>
<div class="container">
<div class="item-bloc">
<div class="item-row">
<h2 class="badge-title">
<span class="title_label"></span>
<span class="title_action">
<span>
{{ trans(EVALUATION) }}:
<span class="badge bg-light text-dark">
{{ eval?.evaluation?.title.fr }}
</span>
</span>
<ul class="small_in_title columns mt-1">
<li>
<span class="item-key">
{{
trans(
ACCOMPANYING_COURSE_WORK_START_DATE,
)
}}
:
</span>
<b>{{ formatDate(eval.startDate) }}</b>
</li>
<li v-if="eval.endDate">
<span class="item-key">
{{
trans(ACCOMPANYING_COURSE_WORK_END_DATE)
}}
:
</span>
<b>{{ formatDate(eval.endDate) }}</b>
</li>
</ul>
</span>
</h2>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import {
ACCOMPANYING_COURSE_WORK_END_DATE,
ACCOMPANYING_COURSE_WORK_START_DATE,
EVALUATION,
trans,
} from "translator";
import { ISOToDate } from "ChillMainAssets/chill/js/date";
import { DateTime } from "ChillMainAssets/types";
import { AccompanyingPeriodWorkEvaluation } from "../../../types";
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const props = defineProps<{ eval: AccompanyingPeriodWorkEvaluation }>();
const formatDate = (dateObject: DateTime) => {
if (dateObject) {
const parsedDate = ISOToDate(dateObject.datetime);
if (parsedDate) {
return new Intl.DateTimeFormat("default", {
dateStyle: "short",
}).format(parsedDate);
} else {
return "";
}
}
};
</script>

View File

@@ -0,0 +1,47 @@
<template>
<div class="results">
<div
v-for="evaluation in evaluations"
:key="evaluation.id"
class="list-item"
>
<label class="acpw-item">
<div>
<input
type="radio"
:value="evaluation"
v-model="selectedEvaluation"
name="item"
/>
</div>
<accompanying-period-work-evaluation-item :eval="evaluation" />
</label>
</div>
</div>
</template>
<script setup lang="ts">
import { AccompanyingPeriodWorkEvaluation } from "../../../types";
import { defineProps, ref, watch } from "vue";
import AccompanyingPeriodWorkEvaluationItem from "ChillPersonAssets/vuejs/_components/AccompanyingPeriodWorkSelector/AccompanyingPeriodWorkEvaluationItem.vue";
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const props = defineProps<{
evaluations: AccompanyingPeriodWorkEvaluation[];
}>();
const selectedEvaluation = ref<AccompanyingPeriodWorkEvaluation | null>(null);
// eslint-disable-next-line vue/valid-define-emits
const emit = defineEmits();
watch(selectedEvaluation, (newValue) => {
emit("update:selectedEvaluation", newValue);
});
</script>
<style>
.acpw-item {
display: flex;
}
</style>

View File

@@ -26,14 +26,24 @@ import AccompanyingPeriodWorkItem from "./AccompanyingPeriodWorkItem.vue";
import { AccompanyingPeriodWork } from "../../../types";
import { defineProps, ref, watch } from "vue";
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const props = defineProps<{
accompanyingPeriodWorks: AccompanyingPeriodWork[];
selectedAcpw?: AccompanyingPeriodWork | null;
}>();
const selectedAcpw = ref<AccompanyingPeriodWork | null>(null);
const selectedAcpw = ref<AccompanyingPeriodWork | null>(
props.selectedAcpw ?? null,
);
// eslint-disable-next-line vue/valid-define-emits
const emit = defineEmits();
const emit = defineEmits<{
"update:selectedAcpw": [value: AccompanyingPeriodWork | null];
}>();
watch(
() => props.selectedAcpw,
(val) => {
selectedAcpw.value = val ?? null;
},
);
watch(selectedAcpw, (newValue) => {
emit("update:selectedAcpw", newValue);

View File

@@ -1,6 +1,6 @@
<template>
<div>
<div class="row justify-content-end">
<div class="row justify-content-end" v-if="!isEvaluationSelector">
<div class="col-md-6 col-sm-10" v-if="selectedAcpw">
<ul class="list-suggest remove-items">
<li>
@@ -14,7 +14,7 @@
</div>
</div>
<ul class="record_actions">
<ul v-if="!showModal" class="record_actions">
<li>
<a class="btn btn-sm btn-create mt-3" @click="openModal">
{{ trans(ACPW_DUPLICATE_SELECT_ACCOMPANYING_PERIOD_WORK) }}
@@ -30,19 +30,21 @@
>
<template #header>
<h3>
{{
trans(
ACPW_DUPLICATE_SELECT_ACCOMPANYING_PERIOD_WORK,
)
}}
{{ getModalTitle() }}
</h3>
</template>
<template #body>
<accompanying-period-work-list
v-if="evaluations.length === 0"
:accompanying-period-works="accompanyingPeriodWorks"
v-model:selectedAcpw="selectedAcpw"
/>
<accompanying-period-work-evaluation-list
v-if="evaluations.length > 0"
:evaluations="evaluations"
v-model:selectedEvaluation="selectedEvaluation"
/>
</template>
<template #footer>
@@ -60,58 +62,116 @@
</template>
<script setup lang="ts">
import { onMounted, ref } from "vue";
import { ref, watch, onMounted } from "vue";
import Modal from "ChillMainAssets/vuejs/_components/Modal.vue";
import AccompanyingPeriodWorkList from "./AccompanyingPeriodWorkList.vue";
import { AccompanyingPeriodWork } from "../../../types";
import {
ACPW_DUPLICATE_SELECT_ACCOMPANYING_PERIOD_WORK,
CONFIRM,
trans,
ACPW_DUPLICATE_SELECT_ACCOMPANYING_PERIOD_WORK,
ACPW_DUPLICATE_SELECT_AN_EVALUATION,
CONFIRM,
} from "translator";
import { fetchResults } from "ChillMainAssets/lib/api/apiMethods";
interface AccompanyingPeriodWorkSelectorModalProps {
accompanyingPeriodId: number;
}
import AccompanyingPeriodWorkEvaluationList from "ChillPersonAssets/vuejs/_components/AccompanyingPeriodWorkSelector/AccompanyingPeriodWorkEvaluationList.vue";
import { AccompanyingPeriodWorkEvaluation } from "../../../types";
const selectedAcpw = ref<AccompanyingPeriodWork | null>(null);
const selectedEvaluation = ref<AccompanyingPeriodWorkEvaluation | null>(null);
const showModal = ref(false);
const accompanyingPeriodWorks = ref<AccompanyingPeriodWork[]>([]);
const props = defineProps<AccompanyingPeriodWorkSelectorModalProps>();
const evaluations = ref<AccompanyingPeriodWorkEvaluation[]>([]);
const props = defineProps<{
accompanyingPeriodId: string;
isEvaluationSelector: boolean;
ignoreAccompanyingPeriodWorkIds: number[];
}>();
const emit = defineEmits<{
pickWork: [payload: { work: AccompanyingPeriodWork | null }];
closeModal: [];
"update:selectedEvaluation": [evaluation: AccompanyingPeriodWorkEvaluation];
}>();
const getModalTitle = () =>
evaluations.value.length > 0
? trans(ACPW_DUPLICATE_SELECT_AN_EVALUATION)
: trans(ACPW_DUPLICATE_SELECT_ACCOMPANYING_PERIOD_WORK);
onMounted(() => {
if (props.accompanyingPeriodId) {
getAccompanyingPeriodWorks(props.accompanyingPeriodId);
getAccompanyingPeriodWorks(parseInt(props.accompanyingPeriodId));
} else {
console.error("No accompanyingperiod id was given");
}
showModal.value = true;
});
const getAccompanyingPeriodWorks = async (periodId: number) => {
const url = `/api/1.0/person/accompanying-course/${periodId}/works.json`;
try {
accompanyingPeriodWorks.value = await fetchResults(url);
} catch (error) {
console.log(error);
const accompanyingPeriodWorksFetched =
await fetchResults<AccompanyingPeriodWork>(url);
if (props.isEvaluationSelector) {
accompanyingPeriodWorks.value = accompanyingPeriodWorksFetched.filter(
(acpw: AccompanyingPeriodWork) =>
acpw.accompanyingPeriodWorkEvaluations.length > 0 &&
typeof acpw.id !== "undefined" &&
!props.ignoreAccompanyingPeriodWorkIds.includes(acpw.id),
);
} else {
accompanyingPeriodWorks.value = accompanyingPeriodWorksFetched;
}
/* makeFetch<number, AccompanyingPeriodWork[]>("GET", url)
.then((response) => {
accompanyingPeriodWorks.value = response;
})
.catch((error) => {
console.log(error);
});*/
};
const openModal = () => (showModal.value = true);
const closeModal = () => (showModal.value = false);
watch(selectedAcpw, (newValue) => {
const inputField = document.getElementById(
"find_accompanying_period_work_acpw",
) as HTMLInputElement;
if (inputField) {
inputField.value = String(newValue?.id || "");
}
/* if (!props.isEvaluationSelector) {
console.log("Emitting from watch:", { work: newValue });
emit("pickWork", { work: newValue });
}*/
});
const openModal = () => {
showModal.value = true;
};
const closeModal = () => {
showModal.value = false;
selectedEvaluation.value = null;
// selectedAcpw.value = null;
emit("closeModal");
};
const confirmSelection = () => {
emit("pickWork", { work: selectedAcpw.value });
closeModal();
selectedAcpw.value = selectedAcpw.value;
console.log("selectedAcpw", selectedAcpw.value);
if (!props.isEvaluationSelector) {
if (selectedAcpw.value) {
// only emit if something is actually selected!
emit("pickWork", { work: selectedAcpw.value });
closeModal();
}
// optionally show some error or warning if not selected
return;
}
if (selectedAcpw.value && props.isEvaluationSelector) {
evaluations.value =
selectedAcpw.value.accompanyingPeriodWorkEvaluations;
}
if (selectedEvaluation.value && props.isEvaluationSelector) {
// console.log('evaluation log in modal', selectedEvaluation.value)
emit("update:selectedEvaluation", selectedEvaluation.value);
closeModal();
}
};
</script>

View File

@@ -1,9 +1,9 @@
{%- macro details(w, accompanyingCourse, options) -%}
{% include '@ChillPerson/AccompanyingCourseWork/_item.html.twig' with {
'displayAction': false,
'displayAction': true,
'displayContent': 'short',
'displayFontSmall': true,
'itemBlocClass': '',
'displayNotification': false
'displayNotification': true
} %}
{% endmacro %}

View File

@@ -2,10 +2,10 @@
{% set activeRouteKey = 'chill_person_accompanying_period_work_assign_duplicate' %}
{% block title %}{{ 'Assign an accompanying period work duplicate' }}{% endblock %}
{% import '@ChillPerson/AccompanyingPeriodWorkDuplicate/_details.html.twig' as details %}
{% block title %}{{ 'Assign an accompanying period work duplicate' }}{% endblock %}
{% block content %}
<div class="person-duplicate">
@@ -18,7 +18,7 @@
</div>
</div>
<h3>{{ 'acpw_duplicate.Assign duplicate'|trans }}</h3>
<h1>{{ 'acpw_duplicate.Assign duplicate'|trans }}</h1>
{{ form_start(form) }}
{%- if form.acpw is defined -%}
{{ form_row(form.acpw) }}

View File

@@ -12,6 +12,7 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Service\AccompanyingPeriodWorkEvaluationDocument;
use Chill\DocStoreBundle\Service\StoredObjectDuplicate;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluation;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument;
use Symfony\Component\Clock\ClockInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
@@ -36,4 +37,17 @@ class AccompanyingPeriodWorkEvaluationDocumentDuplicator
return $newDocument;
}
public function duplicateToEvaluation(AccompanyingPeriodWorkEvaluationDocument $document, AccompanyingPeriodWorkEvaluation $evaluation): AccompanyingPeriodWorkEvaluationDocument
{
$newDocument = new AccompanyingPeriodWorkEvaluationDocument();
$newDocument
->setTitle($document->getTitle().' ('.$this->translator->trans('accompanying_course_evaluation_document.duplicated_at', ['at' => $this->clock->now()]).')')
->setStoredObject($this->storedObjectDuplicate->duplicate($document->getStoredObject()))
;
$evaluation->addDocument($newDocument);
return $newDocument;
}
}

View File

@@ -1993,3 +1993,33 @@ paths:
application/json:
schema:
type: object
/1.0/person/accompanying-course-work-evaluation-document/{document_id}/evaluation/{evaluation_id}/duplicate:
post:
tags:
- accompanying-course-work-evaluation-document
summary: Dupliate an an accompanying period work evaluation document to another evaluation
parameters:
- in: path
name: document_id
required: true
description: The document's id
schema:
type: integer
format: integer
minimum: 1
- in: path
name: evaluation_id
required: true
description: The evaluation's id
schema:
type: integer
format: integer
minimum: 1
responses:
200:
description: "OK"
content:
application/json:
schema:
type: object

View File

@@ -753,6 +753,42 @@ evaluation:
delay: Délai
notificationDelay: Délai de notification
url: Lien internet
title: Ecrire une évaluation
status: Statut
choose_a_status: Choisir un statut
startdate: Date d'ouverture
enddate: Date de fin
maxdate: Date d'échéance
warning_interval: Rappel (jours)
public_comment: Note publique
comment_placeholder: Commencez à écrire ...
generate_a_document: Générer un document
choose_a_template: Choisir un modèle
add_a_document: Ajouter un document
add: Ajouter une évaluation
time_spent: Temps de rédaction
select_time_spent: Indiquez le temps de rédaction
Documents: Documents
document_add: Générer ou téléverser un document
document_upload: Téléverser un document
document_title: Titre du document
template_title: Nom du template
browse: Ajouter un document
replace: Remplacer
download: Télécharger le fichier existant
notification_notify_referrer: Notifier le référent
notification_notify_any: Notifier d'autres utilisateurs
notification_send: Envoyer une notification
document:
edit: Modifier
delete: Supprimer
move: Déplacer
duplicate: Dupliquer
duplicate_here: Dupliquer ici
duplicate_to_other_evaluation: Dupliquer vers une autre évaluation
duplicate_success: Le document d'évaluation a été dupliqué
move_success: Le document d'évaluation a été déplacé
goal:
desactivationDate: Date de désactivation
@@ -777,7 +813,6 @@ relation:
reverseTitle: Deuxième membre
days: jours
months: mois
years: années
# specific to closing motive
@@ -1508,7 +1543,8 @@ entity_display_title:
acpw_duplicate:
title: Fusionner les actions d'accompagnement
description: Cette fusion conservera la date de début la plus ancienne, la date de fin la plus récente, toutes les évaluations, documents et workflows. Les agents traitants seront additionnés ainsi que les tiers intervenants. Les commentaires seront mis l'un à la suite de l'autre.
Select accompanying period work: Selectionner un action d'accompagnement
Select accompanying period work: Sélectionner une action d'accompagnement
Select an evaluation: Sélectionner une évaluation
Assign duplicate: Désigner un action d'accompagnement doublon
Accompanying period work to delete: Action d'accompagnement à supprimer
Accompanying period work to delete explanation: Cet action d'accompagnement sera supprimé.
@@ -1525,3 +1561,6 @@ my_parcours_filters:
parcours_intervening: Intervenant
is_open: Parcours ouverts
is_closed: Parcours clôturés
document_duplicate:
to_evaluation_success: "Le document a été dupliquer"

View File

@@ -152,6 +152,17 @@
{% endif %}
</dl>
{% endblock %}
{% block content_view_actions_merge %}
<li>
<a href="{{ chill_path_add_return_path('chill_thirdparty_find_duplicate',
{ 'thirdparty_id': entity.id }) }}"
title="{{ 'Merge'|trans }}"
class="btn btn-misc">
<i class="bi bi-chevron-contract"></i>
{{ 'Merge'|trans }}
</a>
</li>
{% endblock %}
{% block content_form_actions_delete %}{% endblock %}
{% block content_view_actions_duplicate_link %}{% endblock %}
{% endembed %}