mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-09-18 20:54:59 +00:00
Compare commits
12 Commits
Author | SHA1 | Date | |
---|---|---|---|
ab98f3a102
|
|||
7516e68d77 | |||
7b60b7a8af | |||
d984dec7db
|
|||
46a4dedab8 | |||
db98519e65 | |||
c39637180a
|
|||
15f9409bc8 | |||
5b90d23367 | |||
c48625d1cd | |||
1195b54a68 | |||
2a280b814f |
8
.changes/v4.4.0.md
Normal file
8
.changes/v4.4.0.md
Normal 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
3
.changes/v4.4.1.md
Normal 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
3
.changes/v4.4.2.md
Normal 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
|
17
CHANGELOG.md
17
CHANGELOG.md
@@ -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
|
||||
|
@@ -55,5 +55,6 @@
|
||||
</dl>
|
||||
|
||||
{% endblock %}
|
||||
{% block content_view_actions_duplicate_link %}{% endblock %}
|
||||
{% endembed %}
|
||||
{% endblock %}
|
||||
|
@@ -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"
|
||||
|
@@ -23,6 +23,8 @@ See the document: Voir le document
|
||||
|
||||
document:
|
||||
Any title: Aucun titre
|
||||
replace: Remplacer
|
||||
Add: Ajouter un document
|
||||
|
||||
generic_doc:
|
||||
filter:
|
||||
|
@@ -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 }) }}">
|
||||
|
@@ -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">
|
||||
|
@@ -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 }) }}">
|
||||
|
@@ -94,6 +94,7 @@
|
||||
|
||||
|
||||
{% endblock crud_content_view_details %}
|
||||
{% block content_view_actions_duplicate_link %}{% endblock %}
|
||||
|
||||
{% block content_view_actions_back %}
|
||||
<li class="cancel">
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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']);
|
||||
}
|
||||
}
|
||||
}
|
@@ -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(),
|
||||
]);
|
||||
}
|
||||
}
|
@@ -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(),
|
||||
]));
|
||||
}
|
||||
}
|
@@ -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
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
@@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
@@ -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
|
||||
|
@@ -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) %}
|
||||
|
@@ -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 %}
|
||||
|
@@ -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 %}
|
||||
|
@@ -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 %}
|
||||
|
@@ -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'));
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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');
|
||||
|
||||
|
@@ -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 l’utilisateur
|
||||
$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 n’est 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();
|
||||
}
|
||||
}
|
||||
}
|
@@ -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'));
|
||||
}
|
||||
}
|
||||
|
@@ -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())
|
||||
|
@@ -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: ~
|
||||
|
@@ -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()]);
|
||||
}
|
||||
|
||||
|
@@ -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
|
||||
{
|
||||
|
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
@@ -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;
|
||||
|
@@ -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'],
|
||||
])
|
||||
|
@@ -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)
|
||||
|
@@ -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() ?? "";
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@@ -84,7 +84,7 @@ export interface AccompanyingPeriodWorkEvaluationDocument {
|
||||
}
|
||||
|
||||
export interface AccompanyingPeriodWork {
|
||||
id: number;
|
||||
id?: number;
|
||||
accompanyingPeriod?: AccompanyingPeriod;
|
||||
accompanyingPeriodWorkEvaluations: AccompanyingPeriodWorkEvaluation[];
|
||||
createdAt?: string;
|
||||
|
@@ -972,7 +972,7 @@ div#workEditor {
|
||||
font-size: 85%;
|
||||
}
|
||||
|
||||
i.fa {
|
||||
& > i.fa {
|
||||
padding: 0.25rem;
|
||||
color: $white;
|
||||
|
||||
|
@@ -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>
|
@@ -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>
|
@@ -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>
|
@@ -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"
|
||||
>
|
||||
|
||||
</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>
|
File diff suppressed because it is too large
Load Diff
@@ -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>
|
@@ -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);
|
||||
},
|
||||
|
@@ -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`,
|
||||
);
|
||||
};
|
||||
|
@@ -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>
|
@@ -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>
|
@@ -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);
|
||||
|
@@ -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>
|
||||
|
@@ -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 %}
|
||||
|
@@ -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) }}
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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"
|
||||
|
@@ -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 %}
|
||||
|
Reference in New Issue
Block a user