mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-11-19 02:17:45 +00:00
Compare commits
34 Commits
ticket/64-
...
v4.8.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
e1b91ebbfd
|
|||
| 2139b53fb0 | |||
| a43181d60d | |||
| 04bc1c5de8 | |||
| 0a07d68b6d | |||
| fccd29e3c7 | |||
| 274ee94196 | |||
| 799d04142e | |||
| dfe8d8b0bf | |||
| 82f347b93a | |||
| 635efd6f1d | |||
| 869880d8f3 | |||
| f7ea7e4dbf | |||
| 0a58e05230 | |||
| 68c83223dd | |||
| c28bd22560 | |||
| a5ef2475fb | |||
| 86dd9bfb80 | |||
| c28670f0fd | |||
| 9e2c030224 | |||
| a706c6f337 | |||
| bc63b489ee | |||
| a4cfc6a178 | |||
| f75d1da3b1 | |||
|
b8b68e5e5a
|
|||
|
ae5ba67064
|
|||
|
bfe4dd3aec
|
|||
| 74c9eb5585 | |||
| f93c7e014f | |||
| e6a799abc4 | |||
| 68a0ef7115 | |||
| 1675c56f3d | |||
| 675e8450fc | |||
| 4ffd7034d0 |
@@ -1,7 +0,0 @@
|
||||
kind: DX
|
||||
body: |
|
||||
Send notifications log to dedicated channel, if it exists
|
||||
time: 2025-10-27T15:00:53.309372316+01:00
|
||||
custom:
|
||||
Issue: ""
|
||||
SchemaChange: No schema change
|
||||
@@ -1,6 +0,0 @@
|
||||
kind: Feature
|
||||
body: Add columns for comments linked to an activity in the activity list export
|
||||
time: 2025-10-29T15:25:10.493968528+01:00
|
||||
custom:
|
||||
Issue: "404"
|
||||
SchemaChange: No schema change
|
||||
@@ -1,6 +0,0 @@
|
||||
kind: Fixed
|
||||
body: 'Fix: display also social actions linked to parents of the selected social issue'
|
||||
time: 2025-10-29T12:43:55.008647232+01:00
|
||||
custom:
|
||||
Issue: "451"
|
||||
SchemaChange: No schema change
|
||||
@@ -1,6 +0,0 @@
|
||||
kind: Fixed
|
||||
body: 'Fix: export actions and their results in csv even when action does not have any goals attached to it.'
|
||||
time: 2025-10-29T14:38:36.195220844+01:00
|
||||
custom:
|
||||
Issue: "453"
|
||||
SchemaChange: No schema change
|
||||
@@ -1,6 +0,0 @@
|
||||
kind: Fixed
|
||||
body: Fix the possibility to delete a workflow
|
||||
time: 2025-11-04T13:51:08.113234488+01:00
|
||||
custom:
|
||||
Issue: ""
|
||||
SchemaChange: Drop or rename table or columns, or enforce new constraint that must be manually fixed
|
||||
@@ -1,6 +0,0 @@
|
||||
kind: UX
|
||||
body: Change the terms 'cercle' and 'centre' to 'service', and 'territoire' respectively.
|
||||
time: 2025-10-06T12:39:32.514056818+02:00
|
||||
custom:
|
||||
Issue: "425"
|
||||
SchemaChange: No schema change
|
||||
@@ -1,6 +0,0 @@
|
||||
kind: UX
|
||||
body: Improve the ux for selecting whether user wants to be notified of the final step of a workflow or all steps
|
||||
time: 2025-10-29T11:08:04.077020411+01:00
|
||||
custom:
|
||||
Issue: "542"
|
||||
SchemaChange: No schema change
|
||||
@@ -1,6 +0,0 @@
|
||||
kind: UX
|
||||
body: Expand timeSpent choices for evaluation document and translate them to user locale or fallback 'fr'
|
||||
time: 2025-10-30T18:09:19.373907522+01:00
|
||||
custom:
|
||||
Issue: ""
|
||||
SchemaChange: No schema change
|
||||
21
.changes/v4.7.0.md
Normal file
21
.changes/v4.7.0.md
Normal file
@@ -0,0 +1,21 @@
|
||||
## v4.7.0 - 2025-11-10
|
||||
### Feature
|
||||
* ([#385](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/385)) Create invitation list in user menu
|
||||
* ([#404](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/404)) Add columns for comments linked to an activity in the activity list export
|
||||
### Fixed
|
||||
* ([#451](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/451)) Fix: display also social actions linked to parents of the selected social issue
|
||||
* ([#453](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/453)) Fix: export actions and their results in csv even when action does not have any goals attached to it.
|
||||
* Fix the possibility to delete a workflow
|
||||
|
||||
**Schema Change**: Drop or rename table or columns, or enforce new constraint that must be manually fixed
|
||||
* ([#457](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/457)) Fix the fusion of thirdparty properties that are located in another schema than public for TO_ONE relations + add extra loop for MANY_TO_MANY relations where thirdparty is the source instead of the target
|
||||
* ([#428](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/428)) Fix suggestion of referrer when creating notification for accompanyingPeriodWorkDocument
|
||||
### DX
|
||||
* Send notifications log to dedicated channel, if it exists
|
||||
|
||||
### UX
|
||||
* ([#425](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/425)) Change the terms 'cercle' and 'centre' to 'service', and 'territoire' respectively.
|
||||
* ([#542](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/542)) Improve the ux for selecting whether user wants to be notified of the final step of a workflow or all steps
|
||||
* Expand timeSpent choices for evaluation document and translate them to user locale or fallback 'fr'
|
||||
* ([#455](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/455)) Change the order of display for results and objectives in the social work/action form
|
||||
* Wrap text when it is too long within badges
|
||||
9
.changes/v4.8.0.md
Normal file
9
.changes/v4.8.0.md
Normal file
@@ -0,0 +1,9 @@
|
||||
## v4.8.0 - 2025-11-17
|
||||
### Feature
|
||||
* ([#461](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/461)) Make a calendar item on the 'mes rendez-vous' page clickable. Clicking will navigate to the edit page of the calendar item.
|
||||
### Fixed
|
||||
* ([#463](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/463)) Display calendar items for which an invite was accepted on the mes rendez-vous page
|
||||
* Improve accessibility on login page
|
||||
|
||||
### UX
|
||||
* ([#449](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/449)) Remove the label if there is only one scope and no scope picking field is displayed.
|
||||
36
CHANGELOG.md
36
CHANGELOG.md
@@ -6,6 +6,38 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
|
||||
and is generated by [Changie](https://github.com/miniscruff/changie).
|
||||
|
||||
|
||||
## v4.8.0 - 2025-11-17
|
||||
### Feature
|
||||
* ([#461](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/461)) Make a calendar item on the 'mes rendez-vous' page clickable. Clicking will navigate to the edit page of the calendar item.
|
||||
### Fixed
|
||||
* ([#463](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/463)) Display calendar items for which an invite was accepted on the mes rendez-vous page
|
||||
* Improve accessibility on login page
|
||||
|
||||
### UX
|
||||
* ([#449](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/449)) Remove the label if there is only one scope and no scope picking field is displayed.
|
||||
|
||||
## v4.7.0 - 2025-11-10
|
||||
### Feature
|
||||
* ([#385](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/385)) Create invitation list in user menu
|
||||
* ([#404](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/404)) Add columns for comments linked to an activity in the activity list export
|
||||
### Fixed
|
||||
* ([#451](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/451)) Fix: display also social actions linked to parents of the selected social issue
|
||||
* ([#453](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/453)) Fix: export actions and their results in csv even when action does not have any goals attached to it.
|
||||
* Fix the possibility to delete a workflow
|
||||
|
||||
**Schema Change**: Drop or rename table or columns, or enforce new constraint that must be manually fixed
|
||||
* ([#457](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/457)) Fix the fusion of thirdparty properties that are located in another schema than public for TO_ONE relations + add extra loop for MANY_TO_MANY relations where thirdparty is the source instead of the target
|
||||
* ([#428](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/428)) Fix suggestion of referrer when creating notification for accompanyingPeriodWorkDocument
|
||||
### DX
|
||||
* Send notifications log to dedicated channel, if it exists
|
||||
|
||||
### UX
|
||||
* ([#425](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/425)) Change the terms 'cercle' and 'centre' to 'service', and 'territoire' respectively.
|
||||
* ([#542](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/542)) Improve the ux for selecting whether user wants to be notified of the final step of a workflow or all steps
|
||||
* Expand timeSpent choices for evaluation document and translate them to user locale or fallback 'fr'
|
||||
* ([#455](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/455)) Change the order of display for results and objectives in the social work/action form
|
||||
* Wrap text when it is too long within badges
|
||||
|
||||
## v4.6.1 - 2025-10-27
|
||||
### Fixed
|
||||
* Fix export case where no 'reason' is picked within the PersonHavingActivityBetweenDateFilter.php
|
||||
@@ -780,7 +812,7 @@ Fix color of Chill footer
|
||||
- ajout d'un filtre et regroupement par usager participant sur les échanges
|
||||
- ajout d'un regroupement: par type d'activité associé au parcours;
|
||||
- trie les filtre et regroupements par ordre alphabétique dans els exports
|
||||
- ajout d'un paramètre qui permet de désactiver le filtre par territoire dans les exports
|
||||
- ajout d'un paramètre qui permet de désactiver le filtre par centre dans les exports
|
||||
- correction de l'interface de date dans les filtres et regroupements "par statut du parcours à la date"
|
||||
|
||||
## v2.9.2 - 2023-10-17
|
||||
@@ -960,7 +992,7 @@ error when trying to reedit a saved export
|
||||
- ajout d'un regroupement par métier des intervenants sur un parcours;
|
||||
- ajout d'un regroupement par service des intervenants sur un parcours;
|
||||
- ajout d'un regroupement par utilisateur intervenant sur un parcours
|
||||
- ajout d'un regroupement "par territoire de l'usager";
|
||||
- ajout d'un regroupement "par centre de l'usager";
|
||||
- ajout d'un filtre "par métier intervenant sur un parcours";
|
||||
- ajout d'un filtre "par service intervenant sur un parcours";
|
||||
- création d'un rôle spécifique pour voir les parcours confidentiels (et séparer de celui de la liste qui permet de ré-assigner les parcours en lot);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
chill_main:
|
||||
available_languages: [ '%env(resolve:LOCALE)%', 'en' ]
|
||||
available_languages: [ '%env(resolve:LOCALE)%', 'en', 'nl' ]
|
||||
available_countries: ['BE', 'FR']
|
||||
top_banner:
|
||||
visible: false
|
||||
|
||||
@@ -88,8 +88,8 @@ class ActivityType extends AbstractType
|
||||
|
||||
if (null !== $options['data']->getPerson()) {
|
||||
$builder->add('scope', ScopePickerType::class, [
|
||||
'center' => $options['center'],
|
||||
'role' => ActivityVoter::CREATE === (string) $options['role'] ? ActivityVoter::CREATE_PERSON : (string) $options['role'],
|
||||
'center' => $options['center'],
|
||||
'required' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -43,11 +43,23 @@ export default {
|
||||
span.badge {
|
||||
@include badge_social($social-action-color);
|
||||
font-size: 95%;
|
||||
white-space: normal;
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
margin-bottom: 5px;
|
||||
margin-right: 1em;
|
||||
max-width: 100%; /* Adjust as needed */
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
text-align: left;
|
||||
line-height: 1.2em;
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
left: 11px;
|
||||
top: 0;
|
||||
margin: 0 0.3em 0 -0.75em;
|
||||
}
|
||||
position: relative;
|
||||
padding-left: 1.5em;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -43,7 +43,22 @@ export default {
|
||||
span.badge {
|
||||
@include badge_social($social-issue-color);
|
||||
font-size: 95%;
|
||||
white-space: normal;
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
margin-bottom: 5px;
|
||||
margin-right: 1em;
|
||||
text-align: left;
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
left: 11px;
|
||||
top: 0;
|
||||
margin: 0 0.3em 0 -0.75em;
|
||||
}
|
||||
position: relative;
|
||||
padding-left: 1.5em;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -12,6 +12,7 @@ declare(strict_types=1);
|
||||
namespace Chill\CalendarBundle\Controller;
|
||||
|
||||
use Chill\CalendarBundle\Repository\CalendarRepository;
|
||||
use Chill\CalendarBundle\Repository\InviteRepository;
|
||||
use Chill\MainBundle\CRUD\Controller\ApiController;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Serializer\Model\Collection;
|
||||
@@ -23,7 +24,10 @@ use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
class CalendarAPIController extends ApiController
|
||||
{
|
||||
public function __construct(private readonly CalendarRepository $calendarRepository) {}
|
||||
public function __construct(
|
||||
private readonly CalendarRepository $calendarRepository,
|
||||
private readonly InviteRepository $inviteRepository,
|
||||
) {}
|
||||
|
||||
#[Route(path: '/api/1.0/calendar/calendar/by-user/{id}.{_format}', name: 'chill_api_single_calendar_list_by-user', requirements: ['_format' => 'json'])]
|
||||
public function listByUser(User $user, Request $request, string $_format): JsonResponse
|
||||
@@ -52,16 +56,37 @@ class CalendarAPIController extends ApiController
|
||||
throw new BadRequestHttpException('dateTo not parsable');
|
||||
}
|
||||
|
||||
$total = $this->calendarRepository->countByUser($user, $dateFrom, $dateTo);
|
||||
$paginator = $this->getPaginatorFactory()->create($total);
|
||||
$ranges = $this->calendarRepository->findByUser(
|
||||
// Get calendar items where user is the main user
|
||||
$ownCalendars = $this->calendarRepository->findByUser(
|
||||
$user,
|
||||
$dateFrom,
|
||||
$dateTo,
|
||||
$paginator->getItemsPerPage(),
|
||||
$paginator->getCurrentPageFirstItemNumber()
|
||||
$dateTo
|
||||
);
|
||||
|
||||
// Get calendar items from accepted invites
|
||||
$acceptedInvites = $this->inviteRepository->findAcceptedInvitesByUserAndDateRange($user, $dateFrom, $dateTo);
|
||||
$inviteCalendars = array_map(fn ($invite) => $invite->getCalendar(), $acceptedInvites);
|
||||
|
||||
// Merge
|
||||
$allCalendars = array_merge($ownCalendars, $inviteCalendars);
|
||||
$uniqueCalendars = [];
|
||||
$seenIds = [];
|
||||
|
||||
foreach ($allCalendars as $calendar) {
|
||||
$id = $calendar->getId();
|
||||
if (!in_array($id, $seenIds, true)) {
|
||||
$seenIds[] = $id;
|
||||
$uniqueCalendars[] = $calendar;
|
||||
}
|
||||
}
|
||||
|
||||
$total = count($uniqueCalendars);
|
||||
$paginator = $this->getPaginatorFactory()->create($total);
|
||||
|
||||
$offset = $paginator->getCurrentPageFirstItemNumber();
|
||||
$limit = $paginator->getItemsPerPage();
|
||||
$ranges = array_slice($uniqueCalendars, $offset, $limit);
|
||||
|
||||
$collection = new Collection($ranges, $paginator);
|
||||
|
||||
return $this->json($collection, Response::HTTP_OK, [], ['groups' => ['calendar:light']]);
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
@@ -266,7 +318,7 @@ class CalendarController extends AbstractController
|
||||
}
|
||||
|
||||
if (!$this->getUser() instanceof User) {
|
||||
throw new UnauthorizedHttpException('you are not an user');
|
||||
throw new UnauthorizedHttpException('you are not a user');
|
||||
}
|
||||
|
||||
$view = '@ChillCalendar/Calendar/listByUser.html.twig';
|
||||
|
||||
@@ -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\CalendarBundle\Controller;
|
||||
|
||||
use Chill\CalendarBundle\Entity\Calendar;
|
||||
use Chill\CalendarBundle\Repository\InviteRepository;
|
||||
use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepositoryInterface;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Pagination\PaginatorFactory;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
class MyInvitationsController extends AbstractController
|
||||
{
|
||||
public function __construct(private readonly InviteRepository $inviteRepository, private readonly PaginatorFactory $paginator, private readonly DocGeneratorTemplateRepositoryInterface $docGeneratorTemplateRepository) {}
|
||||
|
||||
#[Route(path: '/{_locale}/calendar/invitations/my', name: 'chill_calendar_invitations_list_my')]
|
||||
public function myInvitations(Request $request): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_USER');
|
||||
|
||||
$user = $this->getUser();
|
||||
|
||||
if (!$user instanceof User) {
|
||||
throw new UnauthorizedHttpException('you are not a user');
|
||||
}
|
||||
|
||||
$total = count($this->inviteRepository->findBy(['user' => $user]));
|
||||
$paginator = $this->paginator->create($total);
|
||||
|
||||
$invitations = $this->inviteRepository->findBy(
|
||||
['user' => $user],
|
||||
['createdAt' => 'DESC'],
|
||||
$paginator->getItemsPerPage(),
|
||||
$paginator->getCurrentPageFirstItemNumber()
|
||||
);
|
||||
|
||||
$view = '@ChillCalendar/Invitations/listByUser.html.twig';
|
||||
|
||||
return $this->render($view, [
|
||||
'invitations' => $invitations,
|
||||
'paginator' => $paginator,
|
||||
'templates' => $this->docGeneratorTemplateRepository->findByEntity(Calendar::class),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -269,6 +269,11 @@ class Calendar implements TrackCreationInterface, TrackUpdateInterface, HasCente
|
||||
return $this->cancelReason;
|
||||
}
|
||||
|
||||
public function isCanceled(): bool
|
||||
{
|
||||
return null !== $this->cancelReason;
|
||||
}
|
||||
|
||||
public function getCenters(): ?iterable
|
||||
{
|
||||
return match ($this->getContext()) {
|
||||
|
||||
@@ -18,14 +18,14 @@ use Doctrine\ORM\Mapping as ORM;
|
||||
#[ORM\Table(name: 'chill_calendar.cancel_reason')]
|
||||
class CancelReason
|
||||
{
|
||||
final public const CANCELEDBY_DONOTCOUNT = 'CANCELEDBY_DONOTCOUNT';
|
||||
final public const CANCELEDBY_OTHER = 'CANCELEDBY_OTHER';
|
||||
|
||||
final public const CANCELEDBY_PERSON = 'CANCELEDBY_PERSON';
|
||||
|
||||
final public const CANCELEDBY_USER = 'CANCELEDBY_USER';
|
||||
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::BOOLEAN)]
|
||||
private ?bool $active = null;
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::BOOLEAN, options: ['default' => true])]
|
||||
private bool $active = true;
|
||||
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::STRING, length: 255)]
|
||||
private ?string $canceledBy = null;
|
||||
|
||||
@@ -15,7 +15,7 @@ use Chill\CalendarBundle\Entity\CancelReason;
|
||||
use Chill\MainBundle\Form\Type\TranslatableStringFormType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
@@ -28,7 +28,14 @@ class CancelReasonType extends AbstractType
|
||||
->add('active', CheckboxType::class, [
|
||||
'required' => false,
|
||||
])
|
||||
->add('canceledBy', TextType::class);
|
||||
->add('canceledBy', ChoiceType::class, [
|
||||
'choices' => [
|
||||
'chill_calendar.canceled_by.user' => CancelReason::CANCELEDBY_USER,
|
||||
'chill_calendar.canceled_by.person' => CancelReason::CANCELEDBY_PERSON,
|
||||
'chill_calendar.canceled_by.other' => CancelReason::CANCELEDBY_OTHER,
|
||||
],
|
||||
'required' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
|
||||
42
src/Bundle/ChillCalendarBundle/Form/CancelType.php
Normal file
42
src/Bundle/ChillCalendarBundle/Form/CancelType.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\CalendarBundle\Form;
|
||||
|
||||
use Chill\CalendarBundle\Entity\Calendar;
|
||||
use Chill\CalendarBundle\Entity\CancelReason;
|
||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class CancelType extends AbstractType
|
||||
{
|
||||
public function __construct(private readonly TranslatableStringHelperInterface $translatableStringHelper) {}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$builder->add('cancelReason', EntityType::class, [
|
||||
'class' => CancelReason::class,
|
||||
'required' => true,
|
||||
'choice_label' => fn (CancelReason $cancelReason) => $this->translatableStringHelper->localize($cancelReason->getName()),
|
||||
]);
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => Calendar::class,
|
||||
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,13 @@ class UserMenuBuilder implements LocalMenuBuilderInterface
|
||||
if ($this->security->isGranted('ROLE_USER')) {
|
||||
$menu->addChild('My calendar list', [
|
||||
'route' => 'chill_calendar_calendar_list_my',
|
||||
])
|
||||
->setExtras([
|
||||
'order' => 8,
|
||||
'icon' => 'tasks',
|
||||
]);
|
||||
$menu->addChild('invite.list.title', [
|
||||
'route' => 'chill_calendar_invitations_list_my',
|
||||
])
|
||||
->setExtras([
|
||||
'order' => 9,
|
||||
|
||||
@@ -21,6 +21,7 @@ namespace Chill\CalendarBundle\Messenger\Doctrine;
|
||||
use Chill\CalendarBundle\Entity\Calendar;
|
||||
use Chill\CalendarBundle\Messenger\Message\CalendarMessage;
|
||||
use Chill\CalendarBundle\Messenger\Message\CalendarRemovedMessage;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Doctrine\ORM\Event\PostPersistEventArgs;
|
||||
use Doctrine\ORM\Event\PostRemoveEventArgs;
|
||||
use Doctrine\ORM\Event\PostUpdateEventArgs;
|
||||
@@ -31,6 +32,17 @@ class CalendarEntityListener
|
||||
{
|
||||
public function __construct(private readonly MessageBusInterface $messageBus, private readonly Security $security) {}
|
||||
|
||||
private function getAuthenticatedUser(): User
|
||||
{
|
||||
$user = $this->security->getUser();
|
||||
|
||||
if (!$user instanceof User) {
|
||||
throw new \LogicException('Expected an instance of User.');
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
public function postPersist(Calendar $calendar, PostPersistEventArgs $args): void
|
||||
{
|
||||
if (!$calendar->preventEnqueueChanges) {
|
||||
@@ -38,7 +50,7 @@ class CalendarEntityListener
|
||||
new CalendarMessage(
|
||||
$calendar,
|
||||
CalendarMessage::CALENDAR_PERSIST,
|
||||
$this->security->getUser()
|
||||
$this->getAuthenticatedUser()
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -50,7 +62,7 @@ class CalendarEntityListener
|
||||
$this->messageBus->dispatch(
|
||||
new CalendarRemovedMessage(
|
||||
$calendar,
|
||||
$this->security->getUser()
|
||||
$this->getAuthenticatedUser()
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -58,12 +70,19 @@ class CalendarEntityListener
|
||||
|
||||
public function postUpdate(Calendar $calendar, PostUpdateEventArgs $args): void
|
||||
{
|
||||
if (!$calendar->preventEnqueueChanges) {
|
||||
if ($calendar->getStatus() === $calendar::STATUS_CANCELED) {
|
||||
$this->messageBus->dispatch(
|
||||
new CalendarRemovedMessage(
|
||||
$calendar,
|
||||
$this->getAuthenticatedUser()
|
||||
)
|
||||
);
|
||||
} elseif (!$calendar->preventEnqueueChanges) {
|
||||
$this->messageBus->dispatch(
|
||||
new CalendarMessage(
|
||||
$calendar,
|
||||
CalendarMessage::CALENDAR_UPDATE,
|
||||
$this->security->getUser()
|
||||
$this->getAuthenticatedUser()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -70,6 +70,8 @@ class CalendarRemovedMessage
|
||||
|
||||
public function getRemoteId(): string
|
||||
{
|
||||
dump($this->remoteId);
|
||||
|
||||
return $this->remoteId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,6 +191,7 @@ class CalendarRepository implements ObjectRepository
|
||||
$qb->expr()->eq('c.mainUser', ':user'),
|
||||
$qb->expr()->gte('c.startDate', ':startDate'),
|
||||
$qb->expr()->lte('c.endDate', ':endDate'),
|
||||
$qb->expr()->isNull('c.cancelReason'),
|
||||
)
|
||||
)
|
||||
->setParameters([
|
||||
|
||||
@@ -12,6 +12,7 @@ declare(strict_types=1);
|
||||
namespace Chill\CalendarBundle\Repository;
|
||||
|
||||
use Chill\CalendarBundle\Entity\Invite;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\Persistence\ObjectRepository;
|
||||
@@ -41,7 +42,7 @@ class InviteRepository implements ObjectRepository
|
||||
/**
|
||||
* @return array|Invite[]
|
||||
*/
|
||||
public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null)
|
||||
public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array
|
||||
{
|
||||
return $this->entityRepository->findBy($criteria, $orderBy, $limit, $offset);
|
||||
}
|
||||
@@ -51,6 +52,52 @@ class InviteRepository implements ObjectRepository
|
||||
return $this->entityRepository->findOneBy($criteria);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find accepted invites for a user within a date range.
|
||||
*
|
||||
* @return array|Invite[]
|
||||
*/
|
||||
public function findAcceptedInvitesByUserAndDateRange(User $user, \DateTimeImmutable $from, \DateTimeImmutable $to): array
|
||||
{
|
||||
return $this->buildAcceptedInviteByUserAndDateRangeQuery($user, $from, $to)
|
||||
->getQuery()
|
||||
->getResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Count accepted invites for a user within a date range.
|
||||
*/
|
||||
public function countAcceptedInvitesByUserAndDateRange(User $user, \DateTimeImmutable $from, \DateTimeImmutable $to): int
|
||||
{
|
||||
return $this->buildAcceptedInviteByUserAndDateRangeQuery($user, $from, $to)
|
||||
->select('COUNT(c)')
|
||||
->getQuery()
|
||||
->getSingleScalarResult();
|
||||
}
|
||||
|
||||
public function buildAcceptedInviteByUserAndDateRangeQuery(User $user, \DateTimeImmutable $from, \DateTimeImmutable $to)
|
||||
{
|
||||
$qb = $this->entityRepository->createQueryBuilder('i');
|
||||
|
||||
return $qb
|
||||
->join('i.calendar', 'c')
|
||||
->where(
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->eq('i.user', ':user'),
|
||||
$qb->expr()->eq('i.status', ':status'),
|
||||
$qb->expr()->gte('c.startDate', ':startDate'),
|
||||
$qb->expr()->lte('c.endDate', ':endDate'),
|
||||
$qb->expr()->isNull('c.cancelReason')
|
||||
)
|
||||
)
|
||||
->setParameters([
|
||||
'user' => $user,
|
||||
'status' => Invite::ACCEPTED,
|
||||
'startDate' => $from,
|
||||
'endDate' => $to,
|
||||
]);
|
||||
}
|
||||
|
||||
public function getClassName(): string
|
||||
{
|
||||
return Invite::class;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
services:
|
||||
Chill\CalendarBundle\Controller\:
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
resource: '../../../Controller'
|
||||
tags: ['controller.service_arguments']
|
||||
|
||||
@@ -108,9 +108,12 @@
|
||||
{{ formatDate(event.endStr, "time") }}:
|
||||
{{ event.extendedProps.locationName }}</b
|
||||
>
|
||||
<b v-else-if="event.extendedProps.is === 'local'">{{
|
||||
event.title
|
||||
}}</b>
|
||||
<a
|
||||
:href="calendarLink(event.id)"
|
||||
v-else-if="event.extendedProps.is === 'local'"
|
||||
>
|
||||
<b>{{ event.title }}</b>
|
||||
</a>
|
||||
<b v-else>no 'is'</b>
|
||||
<a
|
||||
v-if="event.extendedProps.is === 'range'"
|
||||
@@ -486,6 +489,12 @@ function copyWeek() {
|
||||
});
|
||||
}
|
||||
|
||||
const calendarLink = (calendarId: string) => {
|
||||
const idStr = calendarId.match(/_(\d+)$/)?.[1];
|
||||
|
||||
return `/fr/calendar/calendar/${idStr}/edit`;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
copyFromWeek.value = dateToISO(getMonday(0));
|
||||
copyToWeek.value = dateToISO(getMonday(1));
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
{# list used in context of person or accompanyingPeriod #}
|
||||
|
||||
{% if calendarItems|length > 0 %}
|
||||
<div class="flex-table list-records context-accompanyingCourse">
|
||||
|
||||
{% for calendar in calendarItems %}
|
||||
{# list used in context of person, accompanyingPeriod or user #}
|
||||
|
||||
<div class="item-bloc">
|
||||
<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 +25,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 +35,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">
|
||||
@@ -103,12 +114,13 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if calendar.documents is not empty %}
|
||||
<div class="item-row separator column">
|
||||
<div>
|
||||
|
||||
{{ include('@ChillCalendar/Calendar/_documents.twig.html') }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if calendar.activity is not null %}
|
||||
<div class="item-row separator">
|
||||
@@ -151,7 +163,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 +203,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"
|
||||
@@ -200,7 +213,7 @@
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if (calendar.isInvited(app.user)) %}
|
||||
{% if calendar.isInvited(app.user) and not calendar.isCanceled %}
|
||||
{% set invite = calendar.inviteForUser(app.user) %}
|
||||
<li>
|
||||
<div invite-answer data-status="{{ invite.status|e('html_attr') }}"
|
||||
@@ -213,12 +226,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-action"><i class="bi bi-x-circle"></i> {{ '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 } ) }}"
|
||||
@@ -230,11 +249,5 @@
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% if calendarItems|length < paginator.getTotalItems %}
|
||||
{{ chill_pagination(paginator) }}
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -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="cancel">
|
||||
<a
|
||||
class="btn btn-cancel"
|
||||
href="{{ chill_return_path_or('chill_calendar_calendar_list', { 'id': accompanyingCourse.id } )}}"
|
||||
>
|
||||
{{ 'Cancel'|trans|chill_return_path_label }}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
{{ form_widget(form.submit, { 'attr' : { 'class': 'btn btn-save' }, 'label': 'Save' } ) }}
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
{{ form_end(form) }}
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,29 @@
|
||||
{% extends "@ChillPerson/Person/layout.html.twig" %}
|
||||
|
||||
{% set activeRouteKey = 'chill_calendar_calendar_list' %}
|
||||
|
||||
{% block title 'chill_calendar.cancel_calendar_item'|trans %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
{{ form_start(form) }}
|
||||
|
||||
{{ form_row(form.cancelReason) }}
|
||||
|
||||
<ul class="record_actions sticky-form-buttons">
|
||||
<li class="cancel">
|
||||
<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 %}
|
||||
@@ -34,7 +34,18 @@
|
||||
{% endif %}
|
||||
</p>
|
||||
{% else %}
|
||||
{% if calendarItems|length > 0 %}
|
||||
<div class="flex-table list-records context-accompanyingCourse">
|
||||
{% for calendar in calendarItems %}
|
||||
{{ include('@ChillCalendar/Calendar/_list.html.twig', {context: 'accompanying_course'}) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if calendarItems|length < paginator.getTotalItems %}
|
||||
{{ chill_pagination(paginator) }}
|
||||
{% endif %}
|
||||
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<ul class="record_actions sticky-form-buttons">
|
||||
|
||||
@@ -33,7 +33,17 @@
|
||||
{% endif %}
|
||||
</p>
|
||||
{% else %}
|
||||
{% if calendarItems|length > 0 %}
|
||||
<div class="flex-table list-records context-person">
|
||||
{% for calendar in calendarItems %}
|
||||
{{ include ('@ChillCalendar/Calendar/_list.html.twig', {context: 'person'}) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if calendarItems|length < paginator.getTotalItems %}
|
||||
{{ chill_pagination(paginator) }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<ul class="record_actions sticky-form-buttons">
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
{% block table_entities_thead_tr %}
|
||||
<th>{{ 'Id'|trans }}</th>
|
||||
<th>{{ 'Name'|trans }}</th>
|
||||
<th>{{ 'canceledBy'|trans }}</th>
|
||||
<th>{{ 'Canceled by'|trans }}</th>
|
||||
<th>{{ 'active'|trans }}</th>
|
||||
<th> </th>
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
{% extends "@ChillMain/layout.html.twig" %}
|
||||
|
||||
{% set activeRouteKey = 'chill_calendar_invitations_list' %}
|
||||
|
||||
{% block title %}{{ 'invite.list.title'|trans }}{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h1>{{ 'invite.list.title'|trans }}</h1>
|
||||
|
||||
{% if invitations|length == 0 %}
|
||||
<p class="chill-no-data-statement">
|
||||
{{ "invite.list.none"|trans }}
|
||||
</p>
|
||||
{% else %}
|
||||
<div class="flex-table list-records">
|
||||
{% for invitation in invitations %}
|
||||
{% set calendar = invitation.getCalendar %}
|
||||
{{ include('@ChillCalendar/Calendar/_list.html.twig', {context: 'user'}) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if invitations|length < paginator.getTotalItems %}
|
||||
{{ chill_pagination(paginator) }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_script_tags('mod_answer') }}
|
||||
{{ encore_entry_script_tags('mod_document_action_buttons_group') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block css %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_link_tags('mod_answer') }}
|
||||
{{ encore_entry_link_tags('mod_document_action_buttons_group') }}
|
||||
{% endblock %}
|
||||
@@ -19,6 +19,7 @@ declare(strict_types=1);
|
||||
namespace Chill\CalendarBundle\Service\ShortMessageNotification;
|
||||
|
||||
use Chill\CalendarBundle\Entity\Calendar;
|
||||
use Chill\CalendarBundle\Entity\CancelReason;
|
||||
use libphonenumber\PhoneNumberFormat;
|
||||
use libphonenumber\PhoneNumberUtil;
|
||||
use Symfony\Component\Notifier\Message\SmsMessage;
|
||||
@@ -57,7 +58,7 @@ class DefaultShortMessageForCalendarBuilder implements ShortMessageForCalendarBu
|
||||
$this->phoneUtil->format($person->getMobilenumber(), PhoneNumberFormat::E164),
|
||||
$this->engine->render('@ChillCalendar/CalendarShortMessage/short_message.txt.twig', ['calendar' => $calendar]),
|
||||
);
|
||||
} elseif (Calendar::SMS_CANCEL_PENDING === $calendar->getSmsStatus()) {
|
||||
} elseif (Calendar::SMS_CANCEL_PENDING === $calendar->getSmsStatus() && (null === $calendar->getCancelReason() || CancelReason::CANCELEDBY_PERSON !== $calendar->getCancelReason()->getCanceledBy())) {
|
||||
$toUsers[] = new SmsMessage(
|
||||
$this->phoneUtil->format($person->getMobilenumber(), PhoneNumberFormat::E164),
|
||||
$this->engine->render('@ChillCalendar/CalendarShortMessage/short_message_canceled.txt.twig', ['calendar' => $calendar]),
|
||||
|
||||
@@ -0,0 +1,292 @@
|
||||
<?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\Tests\Controller;
|
||||
|
||||
use Chill\CalendarBundle\Controller\MyInvitationsController;
|
||||
use Chill\CalendarBundle\Entity\Calendar;
|
||||
use Chill\CalendarBundle\Entity\Invite;
|
||||
use Chill\CalendarBundle\Repository\InviteRepository;
|
||||
use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepositoryInterface;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Pagination\PaginatorFactory;
|
||||
use Chill\MainBundle\Pagination\PaginatorInterface;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
||||
use Twig\Environment;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
final class MyInvitationsControllerTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
private MyInvitationsController $controller;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
// Create prophecies for dependencies
|
||||
$inviteRepository = $this->prophesize(InviteRepository::class);
|
||||
$paginatorFactory = $this->prophesize(PaginatorFactory::class);
|
||||
$docGeneratorTemplateRepository = $this->prophesize(DocGeneratorTemplateRepositoryInterface::class);
|
||||
|
||||
// Create controller instance
|
||||
$this->controller = new MyInvitationsController(
|
||||
$inviteRepository->reveal(),
|
||||
$paginatorFactory->reveal(),
|
||||
$docGeneratorTemplateRepository->reveal()
|
||||
);
|
||||
|
||||
// Set up necessary services for AbstractController
|
||||
$authorizationChecker = $this->prophesize(AuthorizationCheckerInterface::class);
|
||||
$tokenStorage = $this->prophesize(TokenStorageInterface::class);
|
||||
$twig = $this->prophesize(Environment::class);
|
||||
|
||||
// Use reflection to set the container
|
||||
$reflection = new \ReflectionClass($this->controller);
|
||||
$containerProperty = $reflection->getParentClass()->getProperty('container');
|
||||
$containerProperty->setAccessible(true);
|
||||
|
||||
// Create a mock container
|
||||
$container = $this->prophesize(\Psr\Container\ContainerInterface::class);
|
||||
$container->has('security.authorization_checker')->willReturn(true);
|
||||
$container->get('security.authorization_checker')->willReturn($authorizationChecker->reveal());
|
||||
$container->has('security.token_storage')->willReturn(true);
|
||||
$container->get('security.token_storage')->willReturn($tokenStorage->reveal());
|
||||
$container->has('twig')->willReturn(true);
|
||||
$container->get('twig')->willReturn($twig->reveal());
|
||||
|
||||
$containerProperty->setValue($this->controller, $container->reveal());
|
||||
}
|
||||
|
||||
public function testMyInvitationsReturnsCorrectAmountOfInvitations(): void
|
||||
{
|
||||
// Create test user
|
||||
$user = new User();
|
||||
$user->setUsername('testuser');
|
||||
|
||||
// Create test invitations
|
||||
$invite1 = new Invite();
|
||||
$invite1->setUser($user);
|
||||
$invite1->setStatus(Invite::PENDING);
|
||||
|
||||
$invite2 = new Invite();
|
||||
$invite2->setUser($user);
|
||||
$invite2->setStatus(Invite::ACCEPTED);
|
||||
|
||||
$invite3 = new Invite();
|
||||
$invite3->setUser($user);
|
||||
$invite3->setStatus(Invite::DECLINED);
|
||||
|
||||
$allInvitations = [$invite1, $invite2, $invite3];
|
||||
$paginatedInvitations = [$invite1, $invite2]; // First page with 2 items per page
|
||||
|
||||
// Set up repository prophecies
|
||||
$inviteRepository = $this->prophesize(InviteRepository::class);
|
||||
$inviteRepository->findBy(['user' => $user])->willReturn($allInvitations);
|
||||
$inviteRepository->findBy(
|
||||
['user' => $user],
|
||||
['createdAt' => 'DESC'],
|
||||
2, // items per page
|
||||
0 // offset
|
||||
)->willReturn($paginatedInvitations);
|
||||
|
||||
// Set up paginator prophecies
|
||||
$paginator = $this->prophesize(PaginatorInterface::class);
|
||||
$paginator->getItemsPerPage()->willReturn(2);
|
||||
$paginator->getCurrentPageFirstItemNumber()->willReturn(0);
|
||||
|
||||
$paginatorFactory = $this->prophesize(PaginatorFactory::class);
|
||||
$paginatorFactory->create(3)->willReturn($paginator->reveal());
|
||||
|
||||
// Set up doc generator repository
|
||||
$docGeneratorTemplateRepository = $this->prophesize(DocGeneratorTemplateRepositoryInterface::class);
|
||||
$docGeneratorTemplateRepository->findByEntity(Calendar::class)->willReturn([]);
|
||||
|
||||
// Create controller with mocked dependencies
|
||||
$controller = new MyInvitationsController(
|
||||
$inviteRepository->reveal(),
|
||||
$paginatorFactory->reveal(),
|
||||
$docGeneratorTemplateRepository->reveal()
|
||||
);
|
||||
|
||||
// Set up authorization checker to return true for ROLE_USER
|
||||
$authorizationChecker = $this->prophesize(AuthorizationCheckerInterface::class);
|
||||
$authorizationChecker->isGranted('ROLE_USER', null)->willReturn(true);
|
||||
|
||||
// Set up token storage to return user
|
||||
$token = $this->prophesize(TokenInterface::class);
|
||||
$token->getUser()->willReturn($user);
|
||||
$tokenStorage = $this->prophesize(TokenStorageInterface::class);
|
||||
$tokenStorage->getToken()->willReturn($token->reveal());
|
||||
|
||||
// Set up twig to return a response
|
||||
$twig = $this->prophesize(Environment::class);
|
||||
$twig->render('@ChillCalendar/Invitations/listByUser.html.twig', [
|
||||
'invitations' => $paginatedInvitations,
|
||||
'paginator' => $paginator->reveal(),
|
||||
'templates' => [],
|
||||
])->willReturn('rendered content');
|
||||
|
||||
// Set up container
|
||||
$container = $this->prophesize(\Psr\Container\ContainerInterface::class);
|
||||
$container->has('security.authorization_checker')->willReturn(true);
|
||||
$container->get('security.authorization_checker')->willReturn($authorizationChecker->reveal());
|
||||
$container->has('security.token_storage')->willReturn(true);
|
||||
$container->get('security.token_storage')->willReturn($tokenStorage->reveal());
|
||||
$container->has('twig')->willReturn(true);
|
||||
$container->get('twig')->willReturn($twig->reveal());
|
||||
|
||||
// Use reflection to set the container
|
||||
$reflection = new \ReflectionClass($controller);
|
||||
$containerProperty = $reflection->getParentClass()->getProperty('container');
|
||||
$containerProperty->setAccessible(true);
|
||||
$containerProperty->setValue($controller, $container->reveal());
|
||||
|
||||
// Create request
|
||||
$request = new Request();
|
||||
|
||||
// Execute the action
|
||||
$response = $controller->myInvitations($request);
|
||||
|
||||
// Assert that response is successful
|
||||
self::assertInstanceOf(Response::class, $response);
|
||||
self::assertSame(200, $response->getStatusCode());
|
||||
self::assertSame('rendered content', $response->getContent());
|
||||
}
|
||||
|
||||
public function testMyInvitationsPageLoads(): void
|
||||
{
|
||||
// Create test user
|
||||
$user = new User();
|
||||
$user->setUsername('testuser');
|
||||
|
||||
// Set up repository prophecies - no invitations
|
||||
$inviteRepository = $this->prophesize(InviteRepository::class);
|
||||
$inviteRepository->findBy(['user' => $user])->willReturn([]);
|
||||
$inviteRepository->findBy(
|
||||
['user' => $user],
|
||||
['createdAt' => 'DESC'],
|
||||
20, // default items per page
|
||||
0 // offset
|
||||
)->willReturn([]);
|
||||
|
||||
// Set up paginator prophecies
|
||||
$paginator = $this->prophesize(PaginatorInterface::class);
|
||||
$paginator->getItemsPerPage()->willReturn(20);
|
||||
$paginator->getCurrentPageFirstItemNumber()->willReturn(0);
|
||||
|
||||
$paginatorFactory = $this->prophesize(PaginatorFactory::class);
|
||||
$paginatorFactory->create(0)->willReturn($paginator->reveal());
|
||||
|
||||
// Set up doc generator repository
|
||||
$docGeneratorTemplateRepository = $this->prophesize(DocGeneratorTemplateRepositoryInterface::class);
|
||||
$docGeneratorTemplateRepository->findByEntity(Calendar::class)->willReturn([]);
|
||||
|
||||
// Create controller with mocked dependencies
|
||||
$controller = new MyInvitationsController(
|
||||
$inviteRepository->reveal(),
|
||||
$paginatorFactory->reveal(),
|
||||
$docGeneratorTemplateRepository->reveal()
|
||||
);
|
||||
|
||||
// Set up authorization checker to return true for ROLE_USER
|
||||
$authorizationChecker = $this->prophesize(AuthorizationCheckerInterface::class);
|
||||
$authorizationChecker->isGranted('ROLE_USER', null)->willReturn(true);
|
||||
|
||||
// Set up token storage to return user
|
||||
$token = $this->prophesize(TokenInterface::class);
|
||||
$token->getUser()->willReturn($user);
|
||||
$tokenStorage = $this->prophesize(TokenStorageInterface::class);
|
||||
$tokenStorage->getToken()->willReturn($token->reveal());
|
||||
|
||||
// Set up twig to return a response
|
||||
$twig = $this->prophesize(Environment::class);
|
||||
$twig->render('@ChillCalendar/Invitations/listByUser.html.twig', [
|
||||
'invitations' => [],
|
||||
'paginator' => $paginator->reveal(),
|
||||
'templates' => [],
|
||||
])->willReturn('empty page content');
|
||||
|
||||
// Set up container
|
||||
$container = $this->prophesize(\Psr\Container\ContainerInterface::class);
|
||||
$container->has('security.authorization_checker')->willReturn(true);
|
||||
$container->get('security.authorization_checker')->willReturn($authorizationChecker->reveal());
|
||||
$container->has('security.token_storage')->willReturn(true);
|
||||
$container->get('security.token_storage')->willReturn($tokenStorage->reveal());
|
||||
$container->has('twig')->willReturn(true);
|
||||
$container->get('twig')->willReturn($twig->reveal());
|
||||
|
||||
// Use reflection to set the container
|
||||
$reflection = new \ReflectionClass($controller);
|
||||
$containerProperty = $reflection->getParentClass()->getProperty('container');
|
||||
$containerProperty->setAccessible(true);
|
||||
$containerProperty->setValue($controller, $container->reveal());
|
||||
|
||||
// Create request
|
||||
$request = new Request();
|
||||
|
||||
// Execute the action
|
||||
$response = $controller->myInvitations($request);
|
||||
|
||||
// Assert that page loads successfully
|
||||
self::assertInstanceOf(Response::class, $response);
|
||||
self::assertSame(200, $response->getStatusCode());
|
||||
self::assertSame('empty page content', $response->getContent());
|
||||
}
|
||||
|
||||
public function testMyInvitationsRequiresAuthentication(): void
|
||||
{
|
||||
// Create controller with minimal dependencies
|
||||
$inviteRepository = $this->prophesize(InviteRepository::class);
|
||||
$paginatorFactory = $this->prophesize(PaginatorFactory::class);
|
||||
$docGeneratorTemplateRepository = $this->prophesize(DocGeneratorTemplateRepositoryInterface::class);
|
||||
|
||||
$controller = new MyInvitationsController(
|
||||
$inviteRepository->reveal(),
|
||||
$paginatorFactory->reveal(),
|
||||
$docGeneratorTemplateRepository->reveal()
|
||||
);
|
||||
|
||||
// Set up authorization checker to return false for ROLE_USER
|
||||
$authorizationChecker = $this->prophesize(AuthorizationCheckerInterface::class);
|
||||
$authorizationChecker->isGranted('ROLE_USER')->willReturn(false);
|
||||
$authorizationChecker->isGranted('ROLE_USER', null)->willReturn(false);
|
||||
|
||||
// Set up container
|
||||
$container = $this->prophesize(\Psr\Container\ContainerInterface::class);
|
||||
$container->has('security.authorization_checker')->willReturn(true);
|
||||
$container->get('security.authorization_checker')->willReturn($authorizationChecker->reveal());
|
||||
|
||||
// Use reflection to set the container
|
||||
$reflection = new \ReflectionClass($controller);
|
||||
$containerProperty = $reflection->getParentClass()->getProperty('container');
|
||||
$containerProperty->setAccessible(true);
|
||||
$containerProperty->setValue($controller, $container->reveal());
|
||||
|
||||
// Create request
|
||||
$request = new Request();
|
||||
|
||||
// Expect AccessDeniedException
|
||||
$this->expectException(\Symfony\Component\Security\Core\Exception\AccessDeniedException::class);
|
||||
|
||||
// Execute the action
|
||||
$controller->myInvitations($request);
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
@@ -86,6 +93,9 @@ invite:
|
||||
declined: Refusé
|
||||
pending: En attente
|
||||
tentative: Accepté provisoirement
|
||||
list:
|
||||
none: Il n'y aucun invitation
|
||||
title: Mes invitations
|
||||
|
||||
# exports
|
||||
Exports of calendar: Exports des rendez-vous
|
||||
|
||||
@@ -20,4 +20,9 @@ use Doctrine\Persistence\ObjectRepository;
|
||||
interface DocGeneratorTemplateRepositoryInterface extends ObjectRepository
|
||||
{
|
||||
public function countByEntity(string $entity): int;
|
||||
|
||||
/**
|
||||
* @return array|DocGeneratorTemplate[]
|
||||
*/
|
||||
public function findByEntity(string $entity, ?int $start = 0, ?int $limit = 50): array;
|
||||
}
|
||||
|
||||
@@ -25,6 +25,8 @@ use Symfony\Component\Mailer\MailerInterface;
|
||||
use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
// use Symfony\Component\Translation\LocaleSwitcher;
|
||||
|
||||
/**
|
||||
* @see OnGenerationFailsTest for test suite
|
||||
*/
|
||||
@@ -40,6 +42,7 @@ final readonly class OnGenerationFails implements EventSubscriberInterface
|
||||
private StoredObjectRepositoryInterface $storedObjectRepository,
|
||||
private TranslatorInterface $translator,
|
||||
private UserRepositoryInterface $userRepository,
|
||||
// private LocaleSwitcher $localeSwitcher,
|
||||
) {}
|
||||
|
||||
public static function getSubscribedEvents()
|
||||
@@ -118,6 +121,25 @@ final readonly class OnGenerationFails implements EventSubscriberInterface
|
||||
return;
|
||||
}
|
||||
|
||||
// Implementation with LocaleSwitcher (commented out - to be activated after migration to sf7.2):
|
||||
/*
|
||||
$this->localeSwitcher->runWithLocale($creator->getLocale(), function () use ($message, $errors, $template, $creator) {
|
||||
$email = (new TemplatedEmail())
|
||||
->to($message->getSendResultToEmail())
|
||||
->subject($this->translator->trans('docgen.failure_email.The generation of a document failed'))
|
||||
->textTemplate('@ChillDocGenerator/Email/on_generation_failed_email.txt.twig')
|
||||
->context([
|
||||
'errors' => $errors,
|
||||
'template' => $template,
|
||||
'creator' => $creator,
|
||||
'stored_object_id' => $message->getDestinationStoredObjectId(),
|
||||
]);
|
||||
|
||||
$this->mailer->send($email);
|
||||
});
|
||||
*/
|
||||
|
||||
// Current implementation:
|
||||
$email = (new TemplatedEmail())
|
||||
->to($message->getSendResultToEmail())
|
||||
->subject($this->translator->trans('docgen.failure_email.The generation of a document failed'))
|
||||
|
||||
@@ -27,6 +27,8 @@ use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException;
|
||||
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
// use Symfony\Component\Translation\LocaleSwitcher;
|
||||
|
||||
/**
|
||||
* Handle the request of document generation.
|
||||
*/
|
||||
@@ -46,6 +48,7 @@ class RequestGenerationHandler implements MessageHandlerInterface
|
||||
private readonly MailerInterface $mailer,
|
||||
private readonly TranslatorInterface $translator,
|
||||
private readonly StoredObjectManagerInterface $storedObjectManager,
|
||||
// private readonly LocaleSwitcher $localeSwitcher,
|
||||
) {}
|
||||
|
||||
public function __invoke(RequestGenerationMessage $message)
|
||||
@@ -122,6 +125,30 @@ class RequestGenerationHandler implements MessageHandlerInterface
|
||||
|
||||
private function sendDataDump(StoredObject $destinationStoredObject, RequestGenerationMessage $message): void
|
||||
{
|
||||
// Implementation with LocaleSwitcher (commented out - to be activated after migration to sf7.2):
|
||||
// Note: This method sends emails to admin addresses, not user addresses, so locale switching may not be needed
|
||||
/*
|
||||
$this->localeSwitcher->runWithLocale('fr', function () use ($destinationStoredObject, $message) {
|
||||
// Get the content of the document
|
||||
$content = $this->storedObjectManager->read($destinationStoredObject);
|
||||
$filename = $destinationStoredObject->getFilename();
|
||||
$contentType = $destinationStoredObject->getType();
|
||||
|
||||
// Create the email with the document as an attachment
|
||||
$email = (new TemplatedEmail())
|
||||
->to($message->getSendResultToEmail())
|
||||
->textTemplate('@ChillDocGenerator/Email/send_data_dump_to_admin.txt.twig')
|
||||
->context([
|
||||
'filename' => $filename,
|
||||
])
|
||||
->subject($this->translator->trans('docgen.data_dump_email.subject'))
|
||||
->attach($content, $filename, $contentType);
|
||||
|
||||
$this->mailer->send($email);
|
||||
});
|
||||
*/
|
||||
|
||||
// Current implementation:
|
||||
// Get the content of the document
|
||||
$content = $this->storedObjectManager->read($destinationStoredObject);
|
||||
$filename = $destinationStoredObject->getFilename();
|
||||
|
||||
@@ -17,7 +17,6 @@ use Chill\DocStoreBundle\Entity\PersonDocument;
|
||||
use Chill\MainBundle\Form\Type\ChillDateType;
|
||||
use Chill\MainBundle\Form\Type\ChillTextareaType;
|
||||
use Chill\MainBundle\Form\Type\ScopePickerType;
|
||||
use Chill\MainBundle\Security\Resolver\CenterResolverDispatcher;
|
||||
use Chill\MainBundle\Security\Resolver\ScopeResolverDispatcher;
|
||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
@@ -30,7 +29,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class PersonDocumentType extends AbstractType
|
||||
{
|
||||
public function __construct(private readonly TranslatableStringHelperInterface $translatableStringHelper, private readonly ScopeResolverDispatcher $scopeResolverDispatcher, private readonly ParameterBagInterface $parameterBag, private readonly CenterResolverDispatcher $centerResolverDispatcher) {}
|
||||
public function __construct(private readonly TranslatableStringHelperInterface $translatableStringHelper, private readonly ScopeResolverDispatcher $scopeResolverDispatcher, private readonly ParameterBagInterface $parameterBag) {}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
@@ -57,8 +56,8 @@ class PersonDocumentType extends AbstractType
|
||||
|
||||
if ($isScopeConcerned && $this->parameterBag->get('chill_main')['acl']['form_show_scopes']) {
|
||||
$builder->add('scope', ScopePickerType::class, [
|
||||
'center' => $this->centerResolverDispatcher->resolveCenter($document),
|
||||
'role' => $options['role'],
|
||||
'subject' => $document,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Notification\NotificationFlagManager;
|
||||
use Chill\MainBundle\Validation\Constraint\PhonenumberConstraint;
|
||||
use libphonenumber\PhoneNumber;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
final class UpdateProfileCommand
|
||||
{
|
||||
@@ -23,11 +24,13 @@ final class UpdateProfileCommand
|
||||
public function __construct(
|
||||
#[PhonenumberConstraint]
|
||||
public ?PhoneNumber $phonenumber,
|
||||
#[Assert\Choice(choices: ['fr', 'nl'], message: 'Locale must be either "fr" or "nl"')]
|
||||
public string $locale = 'fr',
|
||||
) {}
|
||||
|
||||
public static function create(User $user, NotificationFlagManager $flagManager): self
|
||||
{
|
||||
$updateProfileCommand = new self($user->getPhonenumber());
|
||||
$updateProfileCommand = new self($user->getPhonenumber(), $user->getLocale());
|
||||
|
||||
foreach ($flagManager->getAllNotificationFlagProviders() as $provider) {
|
||||
$updateProfileCommand->setNotificationFlag(
|
||||
|
||||
@@ -18,6 +18,7 @@ final readonly class UpdateProfileCommandHandler
|
||||
public function updateProfile(User $user, UpdateProfileCommand $command): void
|
||||
{
|
||||
$user->setPhonenumber($command->phonenumber);
|
||||
$user->setLocale($command->locale);
|
||||
|
||||
foreach ($command->notificationFlags as $flag => $values) {
|
||||
$user->setNotificationImmediately($flag, $values['immediate_email']);
|
||||
|
||||
@@ -102,7 +102,6 @@ class CRUDController extends AbstractController
|
||||
Resolver::class => Resolver::class,
|
||||
SerializerInterface::class => SerializerInterface::class,
|
||||
FilterOrderHelperFactoryInterface::class => FilterOrderHelperFactoryInterface::class,
|
||||
ManagerRegistry::class => ManagerRegistry::class,
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -674,7 +673,7 @@ class CRUDController extends AbstractController
|
||||
|
||||
protected function getManagerRegistry(): ManagerRegistry
|
||||
{
|
||||
return $this->container->get(ManagerRegistry::class);
|
||||
return $this->container->get('doctrine');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -128,6 +128,12 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, nullable: false, options: ['default' => '[]', 'jsonb' => true])]
|
||||
private array $notificationFlags = [];
|
||||
|
||||
/**
|
||||
* User's preferred locale.
|
||||
*/
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::STRING, length: 5, nullable: false, options: ['default' => 'fr'])]
|
||||
private string $locale = 'fr';
|
||||
|
||||
/**
|
||||
* User constructor.
|
||||
*/
|
||||
@@ -716,7 +722,14 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
|
||||
|
||||
public function getLocale(): string
|
||||
{
|
||||
return 'fr';
|
||||
return $this->locale;
|
||||
}
|
||||
|
||||
public function setLocale(string $locale): self
|
||||
{
|
||||
$this->locale = $locale;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
#[Assert\Callback]
|
||||
|
||||
@@ -16,6 +16,7 @@ use Chill\MainBundle\Entity\Scope;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Form\DataMapper\ScopePickerDataMapper;
|
||||
use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface;
|
||||
use Chill\MainBundle\Security\Resolver\CenterResolverManagerInterface;
|
||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
@@ -32,65 +33,84 @@ use Symfony\Component\Security\Core\Security;
|
||||
* Allow to pick amongst available scope for the current
|
||||
* user.
|
||||
*
|
||||
* options :
|
||||
*
|
||||
* - `center`: the center of the entity
|
||||
* - `role` : the role of the user
|
||||
* Options:
|
||||
* - `role`: string, the role to check permissions for
|
||||
* - Either `subject`: object, entity to resolve centers from
|
||||
* - Or `center`: Center|array|null, the center(s) to check
|
||||
*/
|
||||
class ScopePickerType extends AbstractType
|
||||
{
|
||||
public function __construct(
|
||||
private readonly TranslatableStringHelperInterface $translatableStringHelper,
|
||||
private readonly AuthorizationHelperInterface $authorizationHelper,
|
||||
private readonly Security $security,
|
||||
private readonly TranslatableStringHelperInterface $translatableStringHelper,
|
||||
private readonly CenterResolverManagerInterface $centerResolverManager,
|
||||
) {}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$items = array_values(
|
||||
// Compute centers from subject
|
||||
$centers = $options['center'] ?? null;
|
||||
if (null === $centers && isset($options['subject'])) {
|
||||
$centers = $this->centerResolverManager->resolveCenters($options['subject']);
|
||||
}
|
||||
|
||||
if (null === $centers) {
|
||||
throw new \RuntimeException('Either "center" or "subject" must be provided');
|
||||
}
|
||||
|
||||
$reachableScopes = array_values(
|
||||
array_filter(
|
||||
$this->authorizationHelper->getReachableScopes(
|
||||
$this->security->getUser(),
|
||||
$options['role'],
|
||||
$options['center']
|
||||
$centers
|
||||
),
|
||||
static fn (Scope $s) => $s->isActive()
|
||||
)
|
||||
);
|
||||
|
||||
if (0 === \count($items)) {
|
||||
throw new \RuntimeException('no scopes are reachable. This form should not be shown to user');
|
||||
$builder->setAttribute('reachable_scopes_count', count($reachableScopes));
|
||||
|
||||
if (0 === count($reachableScopes)) {
|
||||
$builder->setAttribute('has_scopes', false);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (1 !== \count($items)) {
|
||||
$builder->setAttribute('has_scopes', true);
|
||||
|
||||
if (1 !== count($reachableScopes)) {
|
||||
$builder->add('scope', EntityType::class, [
|
||||
'class' => Scope::class,
|
||||
'placeholder' => 'Choose the circle',
|
||||
'choice_label' => fn (Scope $c) => $this->translatableStringHelper->localize($c->getName()),
|
||||
'choices' => $items,
|
||||
'choices' => $reachableScopes,
|
||||
]);
|
||||
$builder->setDataMapper(new ScopePickerDataMapper());
|
||||
} else {
|
||||
$builder->add('scope', HiddenType::class, [
|
||||
'data' => $items[0]->getId(),
|
||||
'data' => $reachableScopes[0]->getId(),
|
||||
]);
|
||||
$builder->setDataMapper(new ScopePickerDataMapper($items[0]));
|
||||
$builder->setDataMapper(new ScopePickerDataMapper($reachableScopes[0]));
|
||||
}
|
||||
}
|
||||
|
||||
public function buildView(FormView $view, FormInterface $form, array $options)
|
||||
{
|
||||
$view->vars['fullWidth'] = true;
|
||||
// display of label is handled by the EntityType
|
||||
$view->vars['label'] = false;
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver
|
||||
// create `center` option
|
||||
->setRequired('center')
|
||||
->setAllowedTypes('center', [Center::class, 'array', 'null'])
|
||||
// create ``role` option
|
||||
->setRequired('role')
|
||||
->setAllowedTypes('role', ['string']);
|
||||
->setAllowedTypes('role', ['string'])
|
||||
->setDefined('subject')
|
||||
->setAllowedTypes('subject', ['object'])
|
||||
->setDefined('center')
|
||||
->setAllowedTypes('center', [Center::class, 'array', 'null']);
|
||||
}
|
||||
}
|
||||
|
||||
43
src/Bundle/ChillMainBundle/Form/Type/UserLocaleType.php
Normal file
43
src/Bundle/ChillMainBundle/Form/Type/UserLocaleType.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\MainBundle\Form\Type;
|
||||
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Intl\Languages;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class UserLocaleType extends AbstractType
|
||||
{
|
||||
public function __construct(private readonly array $availableLanguages) {}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$choices = [];
|
||||
foreach ($this->availableLanguages as $languageCode) {
|
||||
$choices[Languages::getName($languageCode)] = $languageCode;
|
||||
}
|
||||
|
||||
$resolver->setDefaults([
|
||||
'choices' => $choices,
|
||||
'placeholder' => 'user.locale.placeholder',
|
||||
'required' => true,
|
||||
'label' => 'user.locale.label',
|
||||
'help' => 'user.locale.help',
|
||||
]);
|
||||
}
|
||||
|
||||
public function getParent(): string
|
||||
{
|
||||
return ChoiceType::class;
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ namespace Chill\MainBundle\Form;
|
||||
use Chill\MainBundle\Action\User\UpdateProfile\UpdateProfileCommand;
|
||||
use Chill\MainBundle\Form\Type\ChillPhoneNumberType;
|
||||
use Chill\MainBundle\Form\Type\NotificationFlagsType;
|
||||
use Chill\MainBundle\Form\Type\UserLocaleType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
@@ -26,6 +27,7 @@ class UpdateProfileType extends AbstractType
|
||||
->add('phonenumber', ChillPhoneNumberType::class, [
|
||||
'required' => false,
|
||||
])
|
||||
->add('locale', UserLocaleType::class)
|
||||
->add('notificationFlags', NotificationFlagsType::class)
|
||||
;
|
||||
}
|
||||
|
||||
@@ -24,6 +24,8 @@ use Symfony\Component\Mime\Email;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
// use Symfony\Component\Translation\LocaleSwitcher;
|
||||
|
||||
readonly class NotificationMailer
|
||||
{
|
||||
public function __construct(
|
||||
@@ -31,6 +33,7 @@ readonly class NotificationMailer
|
||||
private LoggerInterface $logger,
|
||||
private MessageBusInterface $messageBus,
|
||||
private TranslatorInterface $translator,
|
||||
// private LocaleSwitcher $localeSwitcher,
|
||||
) {}
|
||||
|
||||
public function postPersistComment(NotificationComment $comment, PostPersistEventArgs $eventArgs): void
|
||||
@@ -56,7 +59,7 @@ readonly class NotificationMailer
|
||||
$email
|
||||
->to($dest->getEmail())
|
||||
->subject('Re: '.$comment->getNotification()->getTitle())
|
||||
->textTemplate('@ChillMain/Notification/email_notification_comment_persist.fr.md.twig')
|
||||
->textTemplate('@ChillMain/Notification/email_notification_comment_persist.md.twig')
|
||||
->context([
|
||||
'comment' => $comment,
|
||||
'dest' => $dest,
|
||||
@@ -137,13 +140,53 @@ readonly class NotificationMailer
|
||||
return;
|
||||
}
|
||||
|
||||
// Implementation with LocaleSwitcher (commented out - to be activated after migration to sf7.2):
|
||||
/*
|
||||
$this->localeSwitcher->runWithLocale($addressee->getLocale(), function () use ($notification, $addressee) {
|
||||
if ($notification->isSystem()) {
|
||||
$email = new Email();
|
||||
$email->text($notification->getMessage());
|
||||
} else {
|
||||
$email = new TemplatedEmail();
|
||||
$email
|
||||
->textTemplate('@ChillMain/Notification/email_non_system_notification_content.fr.md.twig')
|
||||
->textTemplate('@ChillMain/Notification/email_non_system_notification_content.md.twig')
|
||||
->context([
|
||||
'notification' => $notification,
|
||||
'dest' => $addressee,
|
||||
]);
|
||||
}
|
||||
|
||||
$email
|
||||
->subject($notification->getTitle())
|
||||
->to($addressee->getEmail());
|
||||
|
||||
try {
|
||||
$this->mailer->send($email);
|
||||
$this->logger->info('[NotificationMailer] Email sent successfully', [
|
||||
'notification_id' => $notification->getId(),
|
||||
'addressee_email' => $addressee->getEmail(),
|
||||
'locale' => $addressee->getLocale(),
|
||||
]);
|
||||
} catch (TransportExceptionInterface $e) {
|
||||
$this->logger->warning('[NotificationMailer] Could not send an email notification', [
|
||||
'to' => $addressee->getEmail(),
|
||||
'notification_id' => $notification->getId(),
|
||||
'error_message' => $e->getMessage(),
|
||||
'error_trace' => $e->getTraceAsString(),
|
||||
]);
|
||||
throw $e;
|
||||
}
|
||||
});
|
||||
*/
|
||||
|
||||
// Current implementation:
|
||||
if ($notification->isSystem()) {
|
||||
$email = new Email();
|
||||
$email->text($notification->getMessage());
|
||||
} else {
|
||||
$email = new TemplatedEmail();
|
||||
$email
|
||||
->textTemplate('@ChillMain/Notification/email_non_system_notification_content.md.twig')
|
||||
->context([
|
||||
'notification' => $notification,
|
||||
'dest' => $addressee,
|
||||
@@ -182,9 +225,43 @@ readonly class NotificationMailer
|
||||
return;
|
||||
}
|
||||
|
||||
// Implementation with LocaleSwitcher (commented out - to be activated after migration to sf7.2):
|
||||
/*
|
||||
$this->localeSwitcher->runWithLocale($user->getLocale(), function () use ($user, $notifications) {
|
||||
$email = new TemplatedEmail();
|
||||
$email
|
||||
->htmlTemplate('@ChillMain/Notification/email_daily_digest.fr.md.twig')
|
||||
->htmlTemplate('@ChillMain/Notification/email_daily_digest.md.twig')
|
||||
->context([
|
||||
'user' => $user,
|
||||
'notifications' => $notifications,
|
||||
'notification_count' => count($notifications),
|
||||
])
|
||||
->subject($this->translator->trans('notification.Daily Notification Digest'))
|
||||
->to($user->getEmail());
|
||||
|
||||
try {
|
||||
$this->mailer->send($email);
|
||||
$this->logger->info('[NotificationMailer] Daily digest email sent successfully', [
|
||||
'user_email' => $user->getEmail(),
|
||||
'notification_count' => count($notifications),
|
||||
'locale' => $user->getLocale(),
|
||||
]);
|
||||
} catch (TransportExceptionInterface $e) {
|
||||
$this->logger->warning('[NotificationMailer] Could not send daily digest email', [
|
||||
'to' => $user->getEmail(),
|
||||
'notification_count' => count($notifications),
|
||||
'error_message' => $e->getMessage(),
|
||||
'error_trace' => $e->getTraceAsString(),
|
||||
]);
|
||||
throw $e;
|
||||
}
|
||||
});
|
||||
*/
|
||||
|
||||
// Current implementation:
|
||||
$email = new TemplatedEmail();
|
||||
$email
|
||||
->htmlTemplate('@ChillMain/Notification/email_daily_digest.md.twig')
|
||||
->context([
|
||||
'user' => $user,
|
||||
'notifications' => $notifications,
|
||||
@@ -222,7 +299,7 @@ readonly class NotificationMailer
|
||||
|
||||
$email = new TemplatedEmail();
|
||||
$email
|
||||
->textTemplate('@ChillMain/Notification/email_non_system_notification_content_to_email.fr.md.twig')
|
||||
->textTemplate('@ChillMain/Notification/email_non_system_notification_content_to_email.md.twig')
|
||||
->context([
|
||||
'notification' => $notification,
|
||||
'dest' => $emailAddress,
|
||||
|
||||
@@ -17,7 +17,7 @@ use Symfony\Component\Routing\RouterInterface;
|
||||
/**
|
||||
* Create paginator instances.
|
||||
*/
|
||||
final readonly class PaginatorFactory implements PaginatorFactoryInterface
|
||||
class PaginatorFactory implements PaginatorFactoryInterface
|
||||
{
|
||||
final public const DEFAULT_CURRENT_PAGE_KEY = 'page';
|
||||
|
||||
@@ -29,16 +29,16 @@ final readonly class PaginatorFactory implements PaginatorFactoryInterface
|
||||
/**
|
||||
* the request stack.
|
||||
*/
|
||||
private RequestStack $requestStack,
|
||||
private readonly RequestStack $requestStack,
|
||||
/**
|
||||
* the router and generator for url.
|
||||
*/
|
||||
private RouterInterface $router,
|
||||
private readonly RouterInterface $router,
|
||||
/**
|
||||
* the default item per page. This may be overriden by
|
||||
* the request or inside the paginator.
|
||||
*/
|
||||
private int $itemPerPage = 20,
|
||||
private readonly int $itemPerPage = 20,
|
||||
) {}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1 +1 @@
|
||||
<img class="logo" src="{{ asset('build/images/logo-chill-outil-accompagnement_white.png') }}">
|
||||
<img class="logo" alt="{{ 'login_page.logo_alt'|trans }}" src="{{ asset('build/images/logo-chill-outil-accompagnement_white.png') }}">
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<html lang="{{ app.request.locale }}">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>
|
||||
@@ -35,10 +35,10 @@
|
||||
|
||||
<form method="POST" action="{{ path('login_check') }}">
|
||||
<label for="_username">{{ 'Username'|trans }}</label>
|
||||
<input type="text" name="_username" value="{{ last_username }}" />
|
||||
<input type="text" name="_username" value="{{ last_username }}" id="_username" />
|
||||
<br/>
|
||||
<label for="_password">{{ 'Password'|trans }}</label>
|
||||
<input type="password" name="_password" />
|
||||
<input type="password" name="_password" id="_password" />
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}" />
|
||||
<br/>
|
||||
<button type="submit" name="login">{{ 'Login'|trans }}</button>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
Vous pouvez visualiser la notification et y répondre ici:
|
||||
|
||||
{{ absolute_url(path('chill_main_notification_show', {'_locale': 'fr', 'id': notification.id }, false)) }}
|
||||
{{ absolute_url(path('chill_main_notification_show', {'_locale': dest.locale, 'id': notification.id }, false)) }}
|
||||
|
||||
--
|
||||
Le logiciel Chill
|
||||
@@ -13,7 +13,7 @@ Commentaire:
|
||||
|
||||
Vous pouvez visualiser la notification et y répondre ici:
|
||||
|
||||
{{ absolute_url(path('chill_main_notification_show', {'_locale': 'fr', 'id': comment.notification.id }, false)) }}
|
||||
{{ absolute_url(path('chill_main_notification_show', {'_locale': dest.locale, 'id': comment.notification.id }, false)) }}
|
||||
|
||||
--
|
||||
Le logiciel Chill
|
||||
@@ -44,6 +44,7 @@
|
||||
<div>
|
||||
{{ form_start(form) }}
|
||||
{{ form_row(form.phonenumber) }}
|
||||
{{ form_row(form.locale) }}
|
||||
|
||||
<h2 class="mb-4">{{ 'user.profile.notification_preferences'|trans }}</h2>
|
||||
<table class="table table-striped align-middle">
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
{{ dest.label }},
|
||||
|
||||
Un suivi "{{ workflow.text }}" a atteint une nouvelle étape: {{ workflow.text }}
|
||||
{{ 'workflow.notification.content.new_step_reached'|trans({'%workflow%': workflow.text}) }}
|
||||
|
||||
Titre du workflow: "{{ title }}".
|
||||
{{ 'workflow.notification.content.workflow_title'|trans({'%title%': title}) }}
|
||||
{% if is_dest %}
|
||||
|
||||
Vous êtes invités à valider cette étape au plus tôt.
|
||||
{{ 'workflow.notification.content.validation_needed'|trans }}
|
||||
{% endif %}
|
||||
|
||||
|
||||
Vous pouvez visualiser le workflow sur cette page:
|
||||
{{ 'workflow.notification.content.view_workflow'|trans }}
|
||||
|
||||
{{ absolute_url(path('chill_main_workflow_show', {'id': entity_workflow.id, '_locale': 'fr'})) }}
|
||||
{{ absolute_url(path('chill_main_workflow_show', {'id': entity_workflow.id, '_locale': dest.locale|default('fr')})) }}
|
||||
|
||||
Cordialement,
|
||||
{{ 'workflow.notification.content.regards'|trans }}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{%- if is_dest -%}
|
||||
Un suivi {{ workflow.text }} demande votre attention: {{ title }}
|
||||
{{ 'workflow.notification.title.attention_needed'|trans({'%workflow%': workflow.text, '%title%': title}) }}
|
||||
{%- else -%}
|
||||
Un suivi {{ workflow.text }} a atteint une nouvelle étape: {{ place.text }}: {{ title }}
|
||||
{{ 'workflow.notification.title.new_step'|trans({'%workflow%': workflow.text, '%place%': place.text, '%title%': title}) }}
|
||||
{%- endif -%}
|
||||
|
||||
@@ -16,11 +16,13 @@ use Symfony\Bridge\Twig\Mime\TemplatedEmail;
|
||||
use Symfony\Component\Mailer\MailerInterface;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
|
||||
// use Symfony\Component\Translation\LocaleSwitcher;
|
||||
|
||||
class RecoverPasswordHelper
|
||||
{
|
||||
final public const RECOVER_PASSWORD_ROUTE = 'password_recover';
|
||||
|
||||
public function __construct(private readonly TokenManager $tokenManager, private readonly UrlGeneratorInterface $urlGenerator, private readonly MailerInterface $mailer) {}
|
||||
public function __construct(private readonly TokenManager $tokenManager, private readonly UrlGeneratorInterface $urlGenerator, private readonly MailerInterface $mailer/* , private readonly LocaleSwitcher $localeSwitcher */) {}
|
||||
|
||||
/**
|
||||
* @param bool $absolute
|
||||
@@ -53,6 +55,24 @@ class RecoverPasswordHelper
|
||||
throw new \UnexpectedValueException('No emaail associated to the user');
|
||||
}
|
||||
|
||||
// Implementation with LocaleSwitcher (commented out - to be activated after migration to sf7.2):
|
||||
/*
|
||||
$this->localeSwitcher->runWithLocale($user->getLocale(), function () use ($user, $expiration, $template, $templateParameters, $emailSubject, $additionalUrlParameters) {
|
||||
$email = (new TemplatedEmail())
|
||||
->subject($emailSubject)
|
||||
->to($user->getEmail())
|
||||
->textTemplate($template)
|
||||
->context([
|
||||
'user' => $user,
|
||||
'url' => $this->generateUrl($user, $expiration, true, $additionalUrlParameters),
|
||||
...$templateParameters,
|
||||
]);
|
||||
|
||||
$this->mailer->send($email);
|
||||
});
|
||||
*/
|
||||
|
||||
// Current implementation:
|
||||
$email = (new TemplatedEmail())
|
||||
->subject($emailSubject)
|
||||
->to($user->getEmail())
|
||||
|
||||
@@ -11,11 +11,11 @@ declare(strict_types=1);
|
||||
|
||||
namespace Form\Type;
|
||||
|
||||
use Chill\MainBundle\Entity\Center;
|
||||
use Chill\MainBundle\Entity\Scope;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Form\Type\ScopePickerType;
|
||||
use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface;
|
||||
use Chill\MainBundle\Security\Resolver\CenterResolverManagerInterface;
|
||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\Mapping\Builder\ClassMetadataBuilder;
|
||||
@@ -39,11 +39,11 @@ final class ScopePickerTypeTest extends TypeTestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
public function estBuildOneScopeIsSuccessful()
|
||||
public function testBuildOneScopeIsSuccessful()
|
||||
{
|
||||
$form = $this->factory->create(ScopePickerType::class, null, [
|
||||
'center' => new Center(),
|
||||
'role' => 'ONE_SCOPE',
|
||||
'center' => [],
|
||||
]);
|
||||
|
||||
$view = $form->createView();
|
||||
@@ -54,8 +54,8 @@ final class ScopePickerTypeTest extends TypeTestCase
|
||||
public function testBuildThreeScopesIsSuccessful()
|
||||
{
|
||||
$form = $this->factory->create(ScopePickerType::class, null, [
|
||||
'center' => new Center(),
|
||||
'role' => 'THREE_SCOPE',
|
||||
'center' => [],
|
||||
]);
|
||||
|
||||
$view = $form->createView();
|
||||
@@ -66,8 +66,8 @@ final class ScopePickerTypeTest extends TypeTestCase
|
||||
public function testBuildTwoScopesIsSuccessful()
|
||||
{
|
||||
$form = $this->factory->create(ScopePickerType::class, null, [
|
||||
'center' => new Center(),
|
||||
'role' => 'TWO_SCOPE',
|
||||
'center' => [],
|
||||
]);
|
||||
|
||||
$view = $form->createView();
|
||||
@@ -101,10 +101,13 @@ final class ScopePickerTypeTest extends TypeTestCase
|
||||
static fn ($args) => $args[0]['fr']
|
||||
);
|
||||
|
||||
$centerResolverManager = $this->prophesize(CenterResolverManagerInterface::class);
|
||||
|
||||
$type = new ScopePickerType(
|
||||
$translatableStringHelper->reveal(),
|
||||
$authorizationHelper->reveal(),
|
||||
$security->reveal(),
|
||||
$translatableStringHelper->reveal()
|
||||
$centerResolverManager->reveal()
|
||||
);
|
||||
|
||||
// add the mocks for creating EntityType
|
||||
|
||||
@@ -22,6 +22,8 @@ use Symfony\Component\Mailer\MailerInterface;
|
||||
use Symfony\Component\Workflow\Event\Event;
|
||||
use Symfony\Component\Workflow\Registry;
|
||||
|
||||
// use Symfony\Component\Translation\LocaleSwitcher;
|
||||
|
||||
final readonly class NotificationToUserGroupsOnTransition implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(
|
||||
@@ -31,6 +33,7 @@ final readonly class NotificationToUserGroupsOnTransition implements EventSubscr
|
||||
private MailerInterface $mailer,
|
||||
private EntityManagerInterface $entityManager,
|
||||
private EntityWorkflowManager $entityWorkflowManager,
|
||||
// private LocaleSwitcher $localeSwitcher,
|
||||
) {}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
@@ -87,6 +90,24 @@ final readonly class NotificationToUserGroupsOnTransition implements EventSubscr
|
||||
'title' => $title,
|
||||
];
|
||||
|
||||
// Implementation with LocaleSwitcher (commented out - to be activated after migration to sf7.2):
|
||||
// Note: This sends emails to user groups, not individual users, so locale switching may use default locale
|
||||
/*
|
||||
$this->localeSwitcher->runWithLocale('fr', function () use ($context, $userGroup) {
|
||||
$email = new TemplatedEmail();
|
||||
$email
|
||||
->htmlTemplate('@ChillMain/Workflow/workflow_notification_on_transition_completed_content_to_user_group.fr.txt.twig')
|
||||
->context($context)
|
||||
->subject(
|
||||
$this->engine->render('@ChillMain/Workflow/workflow_notification_on_transition_completed_title.fr.txt.twig', $context)
|
||||
)
|
||||
->to($userGroup->getEmail());
|
||||
|
||||
$this->mailer->send($email);
|
||||
});
|
||||
*/
|
||||
|
||||
// Current implementation:
|
||||
$email = new TemplatedEmail();
|
||||
$email
|
||||
->htmlTemplate('@ChillMain/Workflow/workflow_notification_on_transition_completed_content_to_user_group.fr.txt.twig')
|
||||
|
||||
@@ -12,6 +12,12 @@ services:
|
||||
tags:
|
||||
- { name: form.type, alias: translatable_string }
|
||||
|
||||
Chill\MainBundle\Form\Type\UserLocaleType:
|
||||
arguments:
|
||||
- "%chill_main.available_languages%"
|
||||
tags:
|
||||
- { name: form.type }
|
||||
|
||||
chill.main.form.type.select2choice:
|
||||
class: Chill\MainBundle\Form\Type\Select2ChoiceType
|
||||
tags:
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\Migrations\Main;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20251022140718 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Add locale field to users table for user language preferences';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE users ADD locale VARCHAR(5) DEFAULT \'fr\' NOT NULL');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE users DROP locale');
|
||||
}
|
||||
}
|
||||
@@ -56,6 +56,10 @@ user:
|
||||
no job: Pas de métier assigné
|
||||
no scope: Pas de service assigné
|
||||
notification_preferences: Préférences pour mes notifications
|
||||
locale:
|
||||
label: Langue de communication
|
||||
help: Langue utilisée pour les notifications par email et autres communications.
|
||||
placeholder: Choisissez une langue
|
||||
|
||||
user_group:
|
||||
inactive: Inactif
|
||||
@@ -668,6 +672,17 @@ workflow:
|
||||
reject_are_you_sure: Êtes-vous sûr de vouloir rejeter la signature de %signer%
|
||||
waiting_for: En attente de modification de l'état de la signature
|
||||
|
||||
notification:
|
||||
title:
|
||||
attention_needed: "Attention requise dans le workflow %workflow% pour %title%"
|
||||
new_step: "Nouvelle étape dans le workflow %workflow% (%place%) pour %title%"
|
||||
content:
|
||||
new_step_reached: "Une nouvelle étape a été atteinte dans le workflow %workflow%."
|
||||
workflow_title: "Titre du workflow : %title%"
|
||||
validation_needed: "Votre validation est nécessaire pour cette étape."
|
||||
view_workflow: "Vous pouvez consulter le workflow ici :"
|
||||
regards: "Cordialement,"
|
||||
|
||||
attachments:
|
||||
title: Pièces jointes
|
||||
no_attachment: Aucune pièce jointe
|
||||
@@ -747,7 +762,22 @@ notification:
|
||||
greeting: "Bonjour %user%"
|
||||
intro: "Vous avez reçu %notification_count% nouvelle(s) notification(s)."
|
||||
view_notification: "Vous pouvez visualiser la notification et y répondre ici:"
|
||||
signature: "Le logiciel Chill"
|
||||
signature: "L'équipe Chill"
|
||||
|
||||
daily_notifications: "{1}Vous avez 1 nouvelle notification.|]1,Inf[Vous avez %notification_count% nouvelles notifications."
|
||||
|
||||
docgen:
|
||||
failure_email:
|
||||
"The generation of a document failed": "La génération d'un document a échoué"
|
||||
"The generation of the document %template_name% failed": "La génération du document %template_name% a échoué"
|
||||
"Forward this email to your administrator for solving": "Transmettez cet email à votre administrateur pour résolution"
|
||||
"References": "Références"
|
||||
"The following errors were encoutered": "Les erreurs suivantes ont été rencontrées"
|
||||
data_dump_email:
|
||||
subject: "Export de données disponible"
|
||||
"Dear": "Cher utilisateur,"
|
||||
"data_dump_ready_and_attached": "Votre export de données est prêt et joint à cet email."
|
||||
"filename": "Nom du fichier : %filename%"
|
||||
|
||||
CHILL_MAIN_COMPOSE_EXPORT: Exécuter des exports et les sauvegarder
|
||||
CHILL_MAIN_GENERATE_SAVED_EXPORT: Exécuter et modifier des exports préalablement sauvegardés
|
||||
@@ -975,3 +1005,6 @@ multiselect:
|
||||
editor:
|
||||
switch_to_simple: Éditeur simple
|
||||
switch_to_complex: Éditeur riche
|
||||
|
||||
login_page:
|
||||
logo_alt: "Logo de Chill"
|
||||
|
||||
@@ -46,6 +46,14 @@ No title: Geen titel
|
||||
User profile: Mijn gebruikersprofiel
|
||||
Phonenumber successfully updated!: Telefoonnummer bijgewerkt!
|
||||
|
||||
user:
|
||||
locale:
|
||||
label: Communicatietaal
|
||||
help: Taal gebruikt voor e-mailmeldingen en andere communicatie.
|
||||
placeholder: Kies een taal
|
||||
choice:
|
||||
french: Français
|
||||
dutch: Nederlands
|
||||
|
||||
Edit: Bewerken
|
||||
Update: Updaten
|
||||
@@ -423,6 +431,17 @@ workflow:
|
||||
For: Pour
|
||||
Cc: Cc
|
||||
|
||||
notification:
|
||||
title:
|
||||
attention_needed: "Aandacht vereist in workflow %workflow% voor %title%"
|
||||
new_step: "Nieuwe stap in workflow %workflow% (%place%) voor %title%"
|
||||
content:
|
||||
new_step_reached: "Een nieuwe stap is bereikt in workflow %workflow%."
|
||||
workflow_title: "Workflow titel: %title%"
|
||||
validation_needed: "Uw validatie is nodig voor deze stap."
|
||||
view_workflow: "U kunt de workflow hier bekijken:"
|
||||
regards: "Met vriendelijke groeten,"
|
||||
|
||||
|
||||
Subscribe final: Recevoir une notification à l'étape finale
|
||||
Subscribe all steps: Recevoir une notification à chaque étape
|
||||
|
||||
@@ -52,20 +52,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- results which are not attached to an objective -->
|
||||
<div v-if="hasResultsForAction">
|
||||
<div class="results_without_objective">
|
||||
{{ $t("results_without_objective") }}
|
||||
</div>
|
||||
<div>
|
||||
<add-result
|
||||
:availableResults="resultsForAction"
|
||||
destination="action"
|
||||
></add-result>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- results which **are** attached to an objective -->
|
||||
<!-- 1. Goals with results that were already selected/saved to the entity -->
|
||||
<div v-for="g in goalsPicked" :key="g.goal.id">
|
||||
<div class="item-title" @click="removeGoal(g)">
|
||||
<span class="removable">{{
|
||||
@@ -76,6 +63,32 @@
|
||||
<add-result :goal="g.goal" destination="goal"></add-result>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 2. Results without objectives that were already selected/saved to the entity -->
|
||||
<div v-if="hasResultsForAction">
|
||||
<div
|
||||
class="results_without_objective"
|
||||
style="
|
||||
background: repeating-linear-gradient(
|
||||
45deg,
|
||||
#e6e6e6,
|
||||
#e6e6e6 10px,
|
||||
#f3f3f3 0,
|
||||
#f3f3f3 20px
|
||||
);
|
||||
"
|
||||
>
|
||||
{{ $t("results_without_objective") }}
|
||||
</div>
|
||||
<div>
|
||||
<add-result
|
||||
:availableResults="resultsForAction"
|
||||
destination="action"
|
||||
></add-result>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 3. Selector for objectives with results -->
|
||||
<div class="accordion" id="expandedSuggestions">
|
||||
<div
|
||||
v-if="availableForCheckGoal.length > 0"
|
||||
@@ -138,6 +151,8 @@
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 4. Selector for results without objectives is already included above in section 2 -->
|
||||
</div>
|
||||
|
||||
<div id="evaluations" class="action-row">
|
||||
|
||||
@@ -97,7 +97,7 @@
|
||||
@click="
|
||||
goToGenerateDocumentNotification(
|
||||
d,
|
||||
false,
|
||||
true,
|
||||
)
|
||||
"
|
||||
>
|
||||
|
||||
@@ -2,30 +2,6 @@
|
||||
# OPTIONS
|
||||
# - displayContent: [short|long] default: short
|
||||
#}
|
||||
{% if w.results|length > 0 %}
|
||||
|
||||
<table class="obj-res-eval">
|
||||
<thead>
|
||||
<th class="obj"><h4 class="title_label">{{ 'accompanying_course_work.goal'|trans }}</h4></th>
|
||||
<th class="res"><h4 class="title_label">{{ 'accompanying_course_work.results'|trans }}</h4></th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="obj">
|
||||
<p class="chill-no-data-statement">{{ 'accompanying_course_work.results without objective'|trans }}</p>
|
||||
</td>
|
||||
<td class="res">
|
||||
<ul class="result_list">
|
||||
{% for r in w.results %}
|
||||
<li>{{ r.title|localize_translatable_string }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
|
||||
{% if w.goals|length > 0 %}
|
||||
<table class="obj-res-eval">
|
||||
<thead>
|
||||
@@ -57,6 +33,31 @@
|
||||
</table>
|
||||
{% endif %}
|
||||
|
||||
{% if w.results|length > 0 %}
|
||||
|
||||
<table class="obj-res-eval">
|
||||
<thead>
|
||||
<th class="obj"><h4 class="title_label">{{ 'accompanying_course_work.goal'|trans }}</h4></th>
|
||||
<th class="res"><h4 class="title_label">{{ 'accompanying_course_work.results'|trans }}</h4></th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="obj">
|
||||
<p class="chill-no-data-statement">{{ 'accompanying_course_work.results without objective'|trans }}</p>
|
||||
</td>
|
||||
<td class="res">
|
||||
<ul class="result_list">
|
||||
{% for r in w.results %}
|
||||
<li>{{ r.title|localize_translatable_string }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% if w.accompanyingPeriodWorkEvaluations|length > 0 %}
|
||||
<table class="obj-res-eval">
|
||||
<thead>
|
||||
|
||||
@@ -168,9 +168,8 @@ final readonly class PersonContext implements PersonContextInterface
|
||||
|
||||
if ($this->isScopeNecessary($entity)) {
|
||||
$builder->add('scope', ScopePickerType::class, [
|
||||
'center' => $this->centerResolverManager->resolveCenters($entity),
|
||||
'role' => PersonDocumentVoter::CREATE,
|
||||
'label' => 'Scope',
|
||||
'subject' => $entity,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,11 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class SingleTaskType extends AbstractType
|
||||
{
|
||||
public function __construct(private readonly ParameterBagInterface $parameterBag, private readonly CenterResolverDispatcherInterface $centerResolverDispatcher, private readonly ScopeResolverDispatcher $scopeResolverDispatcher) {}
|
||||
public function __construct(
|
||||
private readonly ParameterBagInterface $parameterBag,
|
||||
private readonly CenterResolverDispatcherInterface $centerResolverDispatcher,
|
||||
private readonly ScopeResolverDispatcher $scopeResolverDispatcher,
|
||||
) {}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
@@ -64,8 +68,8 @@ class SingleTaskType extends AbstractType
|
||||
if ($isScopeConcerned && $this->parameterBag->get('chill_main')['acl']['form_show_scopes']) {
|
||||
$builder
|
||||
->add('circle', ScopePickerType::class, [
|
||||
'center' => $center,
|
||||
'role' => $options['role'],
|
||||
'subject' => $task,
|
||||
'required' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
{% block title 'Tasks for {{ name }}'|trans({ '{{ name }}' : person|chill_entity_render_string }) %}
|
||||
|
||||
{% block content %}
|
||||
<div class="col-md-10 col-xxl">
|
||||
<div class="task-list"">
|
||||
|
||||
<h1>{{ block('title') }}</h1>
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
{% endblock %}
|
||||
{% else %}
|
||||
{% block content %}
|
||||
<div class="col-md-10 col-xxl tasks">
|
||||
<div class="col-md-9 col-xxl tasks">
|
||||
{% include '@ChillTask/SingleTask/AccompanyingCourse/list.html.twig' %}
|
||||
|
||||
</div>
|
||||
|
||||
@@ -69,10 +69,11 @@ readonly class ThirdpartyMergeService
|
||||
if (($assoc['type'] & ClassMetadata::TO_ONE) !== 0) {
|
||||
$joinColumn = $meta->getSingleAssociationJoinColumnName($assoc['fieldName']);
|
||||
|
||||
$suffix = (ThirdParty::class === $assoc['sourceEntity']) ? 'chill_3party.' : '';
|
||||
$schema = $meta->getSchemaName();
|
||||
$prefix = null !== $schema && '' !== $schema ? $schema.'.' : '';
|
||||
|
||||
$queries[] = [
|
||||
'sql' => "UPDATE {$suffix}{$tableName} SET {$joinColumn} = :toKeep WHERE {$joinColumn} = :toDelete",
|
||||
'sql' => "UPDATE {$prefix}{$tableName} SET {$joinColumn} = :toKeep WHERE {$joinColumn} = :toDelete",
|
||||
'params' => ['toKeep' => $toKeep->getId(), 'toDelete' => $toDelete->getId()],
|
||||
];
|
||||
} elseif (ClassMetadata::MANY_TO_MANY === $assoc['type'] && isset($assoc['joinTable'])) {
|
||||
@@ -85,13 +86,36 @@ readonly class ThirdpartyMergeService
|
||||
];
|
||||
|
||||
$queries[] = [
|
||||
'sql' => "DELETE FROM {$joinTable} WHERE {$joinColumn} = :toDelete",
|
||||
'sql' => "DELETE FROM {$prefix}{$joinTable} WHERE {$joinColumn} = :toDelete",
|
||||
'params' => ['toDelete' => $toDelete->getId()],
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Also handle many-to-many where ThirdParty is the source
|
||||
$thirdPartyMeta = $this->em->getClassMetadata(ThirdParty::class);
|
||||
foreach ($thirdPartyMeta->getAssociationMappings() as $assoc) {
|
||||
if (ClassMetadata::MANY_TO_MANY === $assoc['type'] && isset($assoc['joinTable'])) {
|
||||
$joinTable = $assoc['joinTable']['name'];
|
||||
$prefix = null !== ($assoc['joinTable']['schema'] ?? null) ? $assoc['joinTable']['schema'].'.' : '';
|
||||
$joinColumn = $assoc['joinTable']['joinColumns'][0]['name']; // Note: joinColumns, not inverseJoinColumns
|
||||
|
||||
// Get the other column name to build proper duplicate check
|
||||
$otherColumn = $assoc['joinTable']['inverseJoinColumns'][0]['name'];
|
||||
|
||||
$queries[] = [
|
||||
'sql' => "UPDATE {$prefix}{$joinTable} SET {$joinColumn} = :toKeep WHERE {$joinColumn} = :toDelete AND NOT EXISTS (SELECT 1 FROM {$prefix}{$joinTable} AS t2 WHERE t2.{$joinColumn} = :toKeep AND t2.{$otherColumn} = {$prefix}{$joinTable}.{$otherColumn})",
|
||||
'params' => ['toKeep' => $toKeep->getId(), 'toDelete' => $toDelete->getId()],
|
||||
];
|
||||
|
||||
$queries[] = [
|
||||
'sql' => "DELETE FROM {$prefix}{$joinTable} WHERE {$joinColumn} = :toDelete",
|
||||
'params' => ['toDelete' => $toDelete->getId()],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $queries;
|
||||
}
|
||||
|
||||
@@ -102,10 +126,6 @@ readonly class ThirdpartyMergeService
|
||||
'sql' => 'UPDATE chill_3party.third_party SET parent_id = :toKeep WHERE parent_id = :toDelete',
|
||||
'params' => ['toKeep' => $toKeep->getId(), 'toDelete' => $toDelete->getId()],
|
||||
],
|
||||
[
|
||||
'sql' => 'UPDATE chill_3party.thirdparty_category SET thirdparty_id = :toKeep WHERE thirdparty_id = :toDelete AND NOT EXISTS (SELECT 1 FROM chill_3party.thirdparty_category WHERE thirdparty_id = :toKeep)',
|
||||
'params' => ['toKeep' => $toKeep->getId(), 'toDelete' => $toDelete->getId()],
|
||||
],
|
||||
[
|
||||
'sql' => 'DELETE FROM chill_3party.third_party WHERE id = :toDelete',
|
||||
'params' => ['toDelete' => $toDelete->getId()],
|
||||
|
||||
@@ -12,6 +12,7 @@ declare(strict_types=1);
|
||||
namespace Chill\ThirdPartyBundle\Tests\Service;
|
||||
|
||||
use Chill\ActivityBundle\Entity\Activity;
|
||||
use Chill\MainBundle\Entity\Center;
|
||||
use Chill\ThirdPartyBundle\Entity\ThirdParty;
|
||||
use Chill\ThirdPartyBundle\Entity\ThirdPartyCategory;
|
||||
use Chill\ThirdPartyBundle\Service\ThirdpartyMergeService;
|
||||
@@ -47,19 +48,20 @@ class ThirdpartyMergeServiceTest extends KernelTestCase
|
||||
$toDelete->setName('Thirdparty to delete');
|
||||
$this->em->persist($toDelete);
|
||||
|
||||
// Create a related entity with TO_ONE relation (thirdparty parent)
|
||||
// Create a related entity with TO_ONE relation (thirdparty parent) - tests schema handling for TO_ONE
|
||||
$relatedToOneEntity = new ThirdParty();
|
||||
$relatedToOneEntity->setName('RelatedToOne thirdparty');
|
||||
$relatedToOneEntity->setParent($toDelete);
|
||||
$this->em->persist($relatedToOneEntity);
|
||||
|
||||
// Create a related entity with TO_MANY relation (thirdparty category)
|
||||
// Create a related entity with MANY_TO_MANY relation (thirdparty category) - tests schema handling for MANY_TO_MANY where ThirdParty is target
|
||||
$thirdpartyCategory = new ThirdPartyCategory();
|
||||
$thirdpartyCategory->setName(['fr' => 'Thirdparty category']);
|
||||
$this->em->persist($thirdpartyCategory);
|
||||
$toDelete->addCategory($thirdpartyCategory);
|
||||
$this->em->persist($toDelete);
|
||||
|
||||
// Test MANY_TO_MANY relation from another bundle (Activity) - tests cross-bundle schema handling
|
||||
$activity = new Activity();
|
||||
$activity->setDate(new \DateTime());
|
||||
$activity->addThirdParty($toDelete);
|
||||
@@ -73,14 +75,55 @@ class ThirdpartyMergeServiceTest extends KernelTestCase
|
||||
$this->em->refresh($relatedToOneEntity);
|
||||
|
||||
// Check that references were updated
|
||||
$this->assertEquals($toKeep->getId(), $relatedToOneEntity->getParent()->getId(), 'The parent thirdparty was succesfully merged');
|
||||
|
||||
// Test TO_ONE relation in chill_3party schema was properly handled
|
||||
$this->assertEquals($toKeep->getId(), $relatedToOneEntity->getParent()->getId(), 'The parent thirdparty in chill_3party schema was successfully merged');
|
||||
|
||||
// Test MANY_TO_MANY relation in chill_3party schema was properly handled
|
||||
$updatedRelatedManyEntity = $this->em->find(ThirdPartyCategory::class, $thirdpartyCategory->getId());
|
||||
$this->assertContains($updatedRelatedManyEntity, $toKeep->getCategories(), 'The thirdparty category was found in the toKeep entity');
|
||||
$this->assertContains($updatedRelatedManyEntity, $toKeep->getCategories(), 'The thirdparty category in chill_3party schema was found in the toKeep entity');
|
||||
|
||||
// Test MANY_TO_MANY relation from different schema (Activity bundle) was properly handled
|
||||
$this->em->refresh($activity);
|
||||
$this->assertContains($toKeep, $activity->getThirdParties(), 'The activity relation from different schema was successfully merged');
|
||||
$this->assertNotContains($toDelete, $activity->getThirdParties(), 'The toDelete thirdparty was removed from activity relation');
|
||||
|
||||
// Check that toDelete was removed
|
||||
$this->em->clear();
|
||||
$deletedThirdParty = $this->em->find(ThirdParty::class, $toDelete->getId());
|
||||
$this->assertNull($deletedThirdParty);
|
||||
$this->assertNull($deletedThirdParty, 'The toDelete thirdparty was successfully removed');
|
||||
}
|
||||
|
||||
public function testMergeWithSharedCenterDoesNotCauseUniqueConstraintViolation(): void
|
||||
{
|
||||
// Create a center that will be shared by both thirdparties
|
||||
$sharedCenter = new Center();
|
||||
$sharedCenter->setName('Shared Center');
|
||||
$this->em->persist($sharedCenter);
|
||||
|
||||
// Create ThirdParty entities
|
||||
$toKeep = new ThirdParty();
|
||||
$toKeep->setName('Thirdparty to keep');
|
||||
$toKeep->addCenter($sharedCenter); // Both thirdparties linked to same center
|
||||
$this->em->persist($toKeep);
|
||||
|
||||
$toDelete = new ThirdParty();
|
||||
$toDelete->setName('Thirdparty to delete');
|
||||
$toDelete->addCenter($sharedCenter); // Both thirdparties linked to same center
|
||||
$this->em->persist($toDelete);
|
||||
|
||||
$this->em->flush();
|
||||
|
||||
// This should not throw a unique constraint violation
|
||||
$this->service->merge($toKeep, $toDelete);
|
||||
|
||||
// Verify that toKeep still has the shared center
|
||||
$this->em->refresh($toKeep);
|
||||
$this->assertContains($sharedCenter, $toKeep->getCenters(), 'The shared center is still linked to the kept thirdparty');
|
||||
|
||||
// Verify that toDelete was removed
|
||||
$this->em->clear();
|
||||
$deletedThirdParty = $this->em->find(ThirdParty::class, $toDelete->getId());
|
||||
$this->assertNull($deletedThirdParty, 'The toDelete thirdparty was successfully removed');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user