mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-11-19 10:27:44 +00:00
Compare commits
15 Commits
462-displa
...
466-set-ma
| Author | SHA1 | Date | |
|---|---|---|---|
|
220d1fd928
|
|||
|
8878efdc47
|
|||
|
845a794b74
|
|||
|
e1b91ebbfd
|
|||
| 2139b53fb0 | |||
| a43181d60d | |||
| 04bc1c5de8 | |||
| 0a07d68b6d | |||
| fccd29e3c7 | |||
| 274ee94196 | |||
| 799d04142e | |||
| dfe8d8b0bf | |||
| 82f347b93a | |||
| 635efd6f1d | |||
| 869880d8f3 |
@@ -1,6 +0,0 @@
|
|||||||
kind: Feature
|
|
||||||
body: Display appointments (calendar items) linked to a person within the search results, like it was done for accompanying periods
|
|
||||||
time: 2025-11-12T13:00:16.844313583+01:00
|
|
||||||
custom:
|
|
||||||
Issue: "462"
|
|
||||||
SchemaChange: No schema change
|
|
||||||
7
.changes/unreleased/Fixed-20251118-140559.yaml
Normal file
7
.changes/unreleased/Fixed-20251118-140559.yaml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
kind: Fixed
|
||||||
|
body: |
|
||||||
|
Associate activity's creator as a participant by default, and retro-actively append the creator to each activity
|
||||||
|
time: 2025-11-18T14:05:59.904993123+01:00
|
||||||
|
custom:
|
||||||
|
Issue: "466"
|
||||||
|
SchemaChange: Add columns or tables
|
||||||
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.
|
||||||
10
CHANGELOG.md
10
CHANGELOG.md
@@ -6,6 +6,16 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
|
|||||||
and is generated by [Changie](https://github.com/miniscruff/changie).
|
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
|
## v4.7.0 - 2025-11-10
|
||||||
### Feature
|
### Feature
|
||||||
* ([#385](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/385)) Create invitation list in user menu
|
* ([#385](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/385)) Create invitation list in user menu
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
chill_main:
|
chill_main:
|
||||||
available_languages: [ '%env(resolve:LOCALE)%', 'en' ]
|
available_languages: [ '%env(resolve:LOCALE)%', 'en', 'nl' ]
|
||||||
available_countries: ['BE', 'FR']
|
available_countries: ['BE', 'FR']
|
||||||
top_banner:
|
top_banner:
|
||||||
visible: false
|
visible: false
|
||||||
|
|||||||
@@ -382,6 +382,7 @@ final class ActivityController extends AbstractController
|
|||||||
|
|
||||||
$entity = new Activity();
|
$entity = new Activity();
|
||||||
$entity->setUser($this->security->getUser());
|
$entity->setUser($this->security->getUser());
|
||||||
|
$entity->addUser($this->security->getUser());
|
||||||
|
|
||||||
if ($person instanceof Person) {
|
if ($person instanceof Person) {
|
||||||
$entity->setPerson($person);
|
$entity->setPerson($person);
|
||||||
|
|||||||
@@ -88,8 +88,8 @@ class ActivityType extends AbstractType
|
|||||||
|
|
||||||
if (null !== $options['data']->getPerson()) {
|
if (null !== $options['data']->getPerson()) {
|
||||||
$builder->add('scope', ScopePickerType::class, [
|
$builder->add('scope', ScopePickerType::class, [
|
||||||
'center' => $options['center'],
|
|
||||||
'role' => ActivityVoter::CREATE === (string) $options['role'] ? ActivityVoter::CREATE_PERSON : (string) $options['role'],
|
'role' => ActivityVoter::CREATE === (string) $options['role'] ? ActivityVoter::CREATE_PERSON : (string) $options['role'],
|
||||||
|
'center' => $options['center'],
|
||||||
'required' => true,
|
'required' => true,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
<?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\Activity;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migration fixing the automatic association of users to activities (exchanges).
|
||||||
|
*
|
||||||
|
* Originally, the user who created an exchange was not automatically associated
|
||||||
|
* to it (the "TMS" column), which led to incomplete data and biased statistics.
|
||||||
|
*
|
||||||
|
* This migration:
|
||||||
|
* - retroactively associates the creator of each exchange to the corresponding
|
||||||
|
* activity;
|
||||||
|
* - flags these backfilled associations with a temporary column so it is clear
|
||||||
|
* they were added by this data correction and can be safely cleaned up later.
|
||||||
|
*/
|
||||||
|
final class Version20251118124241 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Insert the creator of activity into the activity_user table';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE activity_user ADD COLUMN by_migration BOOL DEFAULT FALSE');
|
||||||
|
$this->addSql("COMMENT ON COLUMN activity_user.by_migration IS 'For backup purpose - can be safely deleted after a while. See migration \\Chill\\Migrations\\Activity\\Version20251118124241'");
|
||||||
|
|
||||||
|
$this->addSql('INSERT INTO activity_user (activity_id, user_id, by_migration)
|
||||||
|
SELECT id, user_id, true FROM activity
|
||||||
|
ON CONFLICT DO NOTHING');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE activity_user DROP COLUMN by_migration');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ declare(strict_types=1);
|
|||||||
namespace Chill\CalendarBundle\Controller;
|
namespace Chill\CalendarBundle\Controller;
|
||||||
|
|
||||||
use Chill\CalendarBundle\Repository\CalendarRepository;
|
use Chill\CalendarBundle\Repository\CalendarRepository;
|
||||||
|
use Chill\CalendarBundle\Repository\InviteRepository;
|
||||||
use Chill\MainBundle\CRUD\Controller\ApiController;
|
use Chill\MainBundle\CRUD\Controller\ApiController;
|
||||||
use Chill\MainBundle\Entity\User;
|
use Chill\MainBundle\Entity\User;
|
||||||
use Chill\MainBundle\Serializer\Model\Collection;
|
use Chill\MainBundle\Serializer\Model\Collection;
|
||||||
@@ -23,7 +24,10 @@ use Symfony\Component\Routing\Annotation\Route;
|
|||||||
|
|
||||||
class CalendarAPIController extends ApiController
|
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'])]
|
#[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
|
public function listByUser(User $user, Request $request, string $_format): JsonResponse
|
||||||
@@ -52,16 +56,37 @@ class CalendarAPIController extends ApiController
|
|||||||
throw new BadRequestHttpException('dateTo not parsable');
|
throw new BadRequestHttpException('dateTo not parsable');
|
||||||
}
|
}
|
||||||
|
|
||||||
$total = $this->calendarRepository->countByUser($user, $dateFrom, $dateTo);
|
// Get calendar items where user is the main user
|
||||||
$paginator = $this->getPaginatorFactory()->create($total);
|
$ownCalendars = $this->calendarRepository->findByUser(
|
||||||
$ranges = $this->calendarRepository->findByUser(
|
|
||||||
$user,
|
$user,
|
||||||
$dateFrom,
|
$dateFrom,
|
||||||
$dateTo,
|
$dateTo
|
||||||
$paginator->getItemsPerPage(),
|
|
||||||
$paginator->getCurrentPageFirstItemNumber()
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 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);
|
$collection = new Collection($ranges, $paginator);
|
||||||
|
|
||||||
return $this->json($collection, Response::HTTP_OK, [], ['groups' => ['calendar:light']]);
|
return $this->json($collection, Response::HTTP_OK, [], ['groups' => ['calendar:light']]);
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ declare(strict_types=1);
|
|||||||
namespace Chill\CalendarBundle\Repository;
|
namespace Chill\CalendarBundle\Repository;
|
||||||
|
|
||||||
use Chill\CalendarBundle\Entity\Invite;
|
use Chill\CalendarBundle\Entity\Invite;
|
||||||
|
use Chill\MainBundle\Entity\User;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Doctrine\ORM\EntityRepository;
|
use Doctrine\ORM\EntityRepository;
|
||||||
use Doctrine\Persistence\ObjectRepository;
|
use Doctrine\Persistence\ObjectRepository;
|
||||||
@@ -51,6 +52,52 @@ class InviteRepository implements ObjectRepository
|
|||||||
return $this->entityRepository->findOneBy($criteria);
|
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
|
public function getClassName(): string
|
||||||
{
|
{
|
||||||
return Invite::class;
|
return Invite::class;
|
||||||
|
|||||||
@@ -14,13 +14,13 @@ div.calendar-list {
|
|||||||
ul.calendar-list {
|
ul.calendar-list {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.calendar-list__global {
|
& > a.calendar-list__global {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 0.2rem;
|
padding: 0.2rem;
|
||||||
min-width: 2rem;
|
min-width: 2rem;
|
||||||
border: 1px solid var(--bs-chill-blue);
|
border: 1px solid var(--bs-chill-blue);
|
||||||
border-radius: 0.25rem;
|
border-radius: 0.25rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -108,9 +108,12 @@
|
|||||||
{{ formatDate(event.endStr, "time") }}:
|
{{ formatDate(event.endStr, "time") }}:
|
||||||
{{ event.extendedProps.locationName }}</b
|
{{ event.extendedProps.locationName }}</b
|
||||||
>
|
>
|
||||||
<b v-else-if="event.extendedProps.is === 'local'">{{
|
<a
|
||||||
event.title
|
:href="calendarLink(event.id)"
|
||||||
}}</b>
|
v-else-if="event.extendedProps.is === 'local'"
|
||||||
|
>
|
||||||
|
<b>{{ event.title }}</b>
|
||||||
|
</a>
|
||||||
<b v-else>no 'is'</b>
|
<b v-else>no 'is'</b>
|
||||||
<a
|
<a
|
||||||
v-if="event.extendedProps.is === 'range'"
|
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(() => {
|
onMounted(() => {
|
||||||
copyFromWeek.value = dateToISO(getMonday(0));
|
copyFromWeek.value = dateToISO(getMonday(0));
|
||||||
copyToWeek.value = dateToISO(getMonday(1));
|
copyToWeek.value = dateToISO(getMonday(1));
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ use Symfony\Component\Mailer\MailerInterface;
|
|||||||
use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent;
|
use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent;
|
||||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
|
|
||||||
|
// use Symfony\Component\Translation\LocaleSwitcher;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @see OnGenerationFailsTest for test suite
|
* @see OnGenerationFailsTest for test suite
|
||||||
*/
|
*/
|
||||||
@@ -40,6 +42,7 @@ final readonly class OnGenerationFails implements EventSubscriberInterface
|
|||||||
private StoredObjectRepositoryInterface $storedObjectRepository,
|
private StoredObjectRepositoryInterface $storedObjectRepository,
|
||||||
private TranslatorInterface $translator,
|
private TranslatorInterface $translator,
|
||||||
private UserRepositoryInterface $userRepository,
|
private UserRepositoryInterface $userRepository,
|
||||||
|
// private LocaleSwitcher $localeSwitcher,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public static function getSubscribedEvents()
|
public static function getSubscribedEvents()
|
||||||
@@ -118,6 +121,25 @@ final readonly class OnGenerationFails implements EventSubscriberInterface
|
|||||||
return;
|
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())
|
$email = (new TemplatedEmail())
|
||||||
->to($message->getSendResultToEmail())
|
->to($message->getSendResultToEmail())
|
||||||
->subject($this->translator->trans('docgen.failure_email.The generation of a document failed'))
|
->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\Component\Messenger\Handler\MessageHandlerInterface;
|
||||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
|
|
||||||
|
// use Symfony\Component\Translation\LocaleSwitcher;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle the request of document generation.
|
* Handle the request of document generation.
|
||||||
*/
|
*/
|
||||||
@@ -46,6 +48,7 @@ class RequestGenerationHandler implements MessageHandlerInterface
|
|||||||
private readonly MailerInterface $mailer,
|
private readonly MailerInterface $mailer,
|
||||||
private readonly TranslatorInterface $translator,
|
private readonly TranslatorInterface $translator,
|
||||||
private readonly StoredObjectManagerInterface $storedObjectManager,
|
private readonly StoredObjectManagerInterface $storedObjectManager,
|
||||||
|
// private readonly LocaleSwitcher $localeSwitcher,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function __invoke(RequestGenerationMessage $message)
|
public function __invoke(RequestGenerationMessage $message)
|
||||||
@@ -122,6 +125,30 @@ class RequestGenerationHandler implements MessageHandlerInterface
|
|||||||
|
|
||||||
private function sendDataDump(StoredObject $destinationStoredObject, RequestGenerationMessage $message): void
|
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
|
// Get the content of the document
|
||||||
$content = $this->storedObjectManager->read($destinationStoredObject);
|
$content = $this->storedObjectManager->read($destinationStoredObject);
|
||||||
$filename = $destinationStoredObject->getFilename();
|
$filename = $destinationStoredObject->getFilename();
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ use Chill\DocStoreBundle\Entity\PersonDocument;
|
|||||||
use Chill\MainBundle\Form\Type\ChillDateType;
|
use Chill\MainBundle\Form\Type\ChillDateType;
|
||||||
use Chill\MainBundle\Form\Type\ChillTextareaType;
|
use Chill\MainBundle\Form\Type\ChillTextareaType;
|
||||||
use Chill\MainBundle\Form\Type\ScopePickerType;
|
use Chill\MainBundle\Form\Type\ScopePickerType;
|
||||||
use Chill\MainBundle\Security\Resolver\CenterResolverDispatcher;
|
|
||||||
use Chill\MainBundle\Security\Resolver\ScopeResolverDispatcher;
|
use Chill\MainBundle\Security\Resolver\ScopeResolverDispatcher;
|
||||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||||
use Doctrine\ORM\EntityRepository;
|
use Doctrine\ORM\EntityRepository;
|
||||||
@@ -30,7 +29,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
|
|||||||
|
|
||||||
class PersonDocumentType extends AbstractType
|
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)
|
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']) {
|
if ($isScopeConcerned && $this->parameterBag->get('chill_main')['acl']['form_show_scopes']) {
|
||||||
$builder->add('scope', ScopePickerType::class, [
|
$builder->add('scope', ScopePickerType::class, [
|
||||||
'center' => $this->centerResolverDispatcher->resolveCenter($document),
|
|
||||||
'role' => $options['role'],
|
'role' => $options['role'],
|
||||||
|
'subject' => $document,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ use Chill\MainBundle\Entity\User;
|
|||||||
use Chill\MainBundle\Notification\NotificationFlagManager;
|
use Chill\MainBundle\Notification\NotificationFlagManager;
|
||||||
use Chill\MainBundle\Validation\Constraint\PhonenumberConstraint;
|
use Chill\MainBundle\Validation\Constraint\PhonenumberConstraint;
|
||||||
use libphonenumber\PhoneNumber;
|
use libphonenumber\PhoneNumber;
|
||||||
|
use Symfony\Component\Validator\Constraints as Assert;
|
||||||
|
|
||||||
final class UpdateProfileCommand
|
final class UpdateProfileCommand
|
||||||
{
|
{
|
||||||
@@ -23,11 +24,13 @@ final class UpdateProfileCommand
|
|||||||
public function __construct(
|
public function __construct(
|
||||||
#[PhonenumberConstraint]
|
#[PhonenumberConstraint]
|
||||||
public ?PhoneNumber $phonenumber,
|
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
|
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) {
|
foreach ($flagManager->getAllNotificationFlagProviders() as $provider) {
|
||||||
$updateProfileCommand->setNotificationFlag(
|
$updateProfileCommand->setNotificationFlag(
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ final readonly class UpdateProfileCommandHandler
|
|||||||
public function updateProfile(User $user, UpdateProfileCommand $command): void
|
public function updateProfile(User $user, UpdateProfileCommand $command): void
|
||||||
{
|
{
|
||||||
$user->setPhonenumber($command->phonenumber);
|
$user->setPhonenumber($command->phonenumber);
|
||||||
|
$user->setLocale($command->locale);
|
||||||
|
|
||||||
foreach ($command->notificationFlags as $flag => $values) {
|
foreach ($command->notificationFlags as $flag => $values) {
|
||||||
$user->setNotificationImmediately($flag, $values['immediate_email']);
|
$user->setNotificationImmediately($flag, $values['immediate_email']);
|
||||||
|
|||||||
@@ -128,6 +128,12 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
|
|||||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, nullable: false, options: ['default' => '[]', 'jsonb' => true])]
|
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, nullable: false, options: ['default' => '[]', 'jsonb' => true])]
|
||||||
private array $notificationFlags = [];
|
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.
|
* User constructor.
|
||||||
*/
|
*/
|
||||||
@@ -716,7 +722,14 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
|
|||||||
|
|
||||||
public function getLocale(): string
|
public function getLocale(): string
|
||||||
{
|
{
|
||||||
return 'fr';
|
return $this->locale;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setLocale(string $locale): self
|
||||||
|
{
|
||||||
|
$this->locale = $locale;
|
||||||
|
|
||||||
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Assert\Callback]
|
#[Assert\Callback]
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ use Chill\MainBundle\Entity\Scope;
|
|||||||
use Chill\MainBundle\Entity\User;
|
use Chill\MainBundle\Entity\User;
|
||||||
use Chill\MainBundle\Form\DataMapper\ScopePickerDataMapper;
|
use Chill\MainBundle\Form\DataMapper\ScopePickerDataMapper;
|
||||||
use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface;
|
use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface;
|
||||||
|
use Chill\MainBundle\Security\Resolver\CenterResolverManagerInterface;
|
||||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||||
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
||||||
use Symfony\Component\Form\AbstractType;
|
use Symfony\Component\Form\AbstractType;
|
||||||
@@ -32,65 +33,84 @@ use Symfony\Component\Security\Core\Security;
|
|||||||
* Allow to pick amongst available scope for the current
|
* Allow to pick amongst available scope for the current
|
||||||
* user.
|
* user.
|
||||||
*
|
*
|
||||||
* options :
|
* Options:
|
||||||
*
|
* - `role`: string, the role to check permissions for
|
||||||
* - `center`: the center of the entity
|
* - Either `subject`: object, entity to resolve centers from
|
||||||
* - `role` : the role of the user
|
* - Or `center`: Center|array|null, the center(s) to check
|
||||||
*/
|
*/
|
||||||
class ScopePickerType extends AbstractType
|
class ScopePickerType extends AbstractType
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
|
private readonly TranslatableStringHelperInterface $translatableStringHelper,
|
||||||
private readonly AuthorizationHelperInterface $authorizationHelper,
|
private readonly AuthorizationHelperInterface $authorizationHelper,
|
||||||
private readonly Security $security,
|
private readonly Security $security,
|
||||||
private readonly TranslatableStringHelperInterface $translatableStringHelper,
|
private readonly CenterResolverManagerInterface $centerResolverManager,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
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(
|
array_filter(
|
||||||
$this->authorizationHelper->getReachableScopes(
|
$this->authorizationHelper->getReachableScopes(
|
||||||
$this->security->getUser(),
|
$this->security->getUser(),
|
||||||
$options['role'],
|
$options['role'],
|
||||||
$options['center']
|
$centers
|
||||||
),
|
),
|
||||||
static fn (Scope $s) => $s->isActive()
|
static fn (Scope $s) => $s->isActive()
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (0 === \count($items)) {
|
$builder->setAttribute('reachable_scopes_count', count($reachableScopes));
|
||||||
throw new \RuntimeException('no scopes are reachable. This form should not be shown to user');
|
|
||||||
|
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, [
|
$builder->add('scope', EntityType::class, [
|
||||||
'class' => Scope::class,
|
'class' => Scope::class,
|
||||||
'placeholder' => 'Choose the circle',
|
'placeholder' => 'Choose the circle',
|
||||||
'choice_label' => fn (Scope $c) => $this->translatableStringHelper->localize($c->getName()),
|
'choice_label' => fn (Scope $c) => $this->translatableStringHelper->localize($c->getName()),
|
||||||
'choices' => $items,
|
'choices' => $reachableScopes,
|
||||||
]);
|
]);
|
||||||
$builder->setDataMapper(new ScopePickerDataMapper());
|
$builder->setDataMapper(new ScopePickerDataMapper());
|
||||||
} else {
|
} else {
|
||||||
$builder->add('scope', HiddenType::class, [
|
$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)
|
public function buildView(FormView $view, FormInterface $form, array $options)
|
||||||
{
|
{
|
||||||
$view->vars['fullWidth'] = true;
|
$view->vars['fullWidth'] = true;
|
||||||
|
// display of label is handled by the EntityType
|
||||||
|
$view->vars['label'] = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function configureOptions(OptionsResolver $resolver)
|
public function configureOptions(OptionsResolver $resolver)
|
||||||
{
|
{
|
||||||
$resolver
|
$resolver
|
||||||
// create `center` option
|
|
||||||
->setRequired('center')
|
|
||||||
->setAllowedTypes('center', [Center::class, 'array', 'null'])
|
|
||||||
// create ``role` option
|
|
||||||
->setRequired('role')
|
->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\Action\User\UpdateProfile\UpdateProfileCommand;
|
||||||
use Chill\MainBundle\Form\Type\ChillPhoneNumberType;
|
use Chill\MainBundle\Form\Type\ChillPhoneNumberType;
|
||||||
use Chill\MainBundle\Form\Type\NotificationFlagsType;
|
use Chill\MainBundle\Form\Type\NotificationFlagsType;
|
||||||
|
use Chill\MainBundle\Form\Type\UserLocaleType;
|
||||||
use Symfony\Component\Form\AbstractType;
|
use Symfony\Component\Form\AbstractType;
|
||||||
use Symfony\Component\Form\FormBuilderInterface;
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
@@ -26,6 +27,7 @@ class UpdateProfileType extends AbstractType
|
|||||||
->add('phonenumber', ChillPhoneNumberType::class, [
|
->add('phonenumber', ChillPhoneNumberType::class, [
|
||||||
'required' => false,
|
'required' => false,
|
||||||
])
|
])
|
||||||
|
->add('locale', UserLocaleType::class)
|
||||||
->add('notificationFlags', NotificationFlagsType::class)
|
->add('notificationFlags', NotificationFlagsType::class)
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ use Symfony\Component\Mime\Email;
|
|||||||
use Symfony\Component\Messenger\MessageBusInterface;
|
use Symfony\Component\Messenger\MessageBusInterface;
|
||||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
|
|
||||||
|
// use Symfony\Component\Translation\LocaleSwitcher;
|
||||||
|
|
||||||
readonly class NotificationMailer
|
readonly class NotificationMailer
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
@@ -31,6 +33,7 @@ readonly class NotificationMailer
|
|||||||
private LoggerInterface $logger,
|
private LoggerInterface $logger,
|
||||||
private MessageBusInterface $messageBus,
|
private MessageBusInterface $messageBus,
|
||||||
private TranslatorInterface $translator,
|
private TranslatorInterface $translator,
|
||||||
|
// private LocaleSwitcher $localeSwitcher,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function postPersistComment(NotificationComment $comment, PostPersistEventArgs $eventArgs): void
|
public function postPersistComment(NotificationComment $comment, PostPersistEventArgs $eventArgs): void
|
||||||
@@ -56,7 +59,7 @@ readonly class NotificationMailer
|
|||||||
$email
|
$email
|
||||||
->to($dest->getEmail())
|
->to($dest->getEmail())
|
||||||
->subject('Re: '.$comment->getNotification()->getTitle())
|
->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([
|
->context([
|
||||||
'comment' => $comment,
|
'comment' => $comment,
|
||||||
'dest' => $dest,
|
'dest' => $dest,
|
||||||
@@ -137,13 +140,53 @@ readonly class NotificationMailer
|
|||||||
return;
|
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.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()) {
|
if ($notification->isSystem()) {
|
||||||
$email = new Email();
|
$email = new Email();
|
||||||
$email->text($notification->getMessage());
|
$email->text($notification->getMessage());
|
||||||
} else {
|
} else {
|
||||||
$email = new TemplatedEmail();
|
$email = new TemplatedEmail();
|
||||||
$email
|
$email
|
||||||
->textTemplate('@ChillMain/Notification/email_non_system_notification_content.fr.md.twig')
|
->textTemplate('@ChillMain/Notification/email_non_system_notification_content.md.twig')
|
||||||
->context([
|
->context([
|
||||||
'notification' => $notification,
|
'notification' => $notification,
|
||||||
'dest' => $addressee,
|
'dest' => $addressee,
|
||||||
@@ -182,9 +225,43 @@ readonly class NotificationMailer
|
|||||||
return;
|
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.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 = new TemplatedEmail();
|
||||||
$email
|
$email
|
||||||
->htmlTemplate('@ChillMain/Notification/email_daily_digest.fr.md.twig')
|
->htmlTemplate('@ChillMain/Notification/email_daily_digest.md.twig')
|
||||||
->context([
|
->context([
|
||||||
'user' => $user,
|
'user' => $user,
|
||||||
'notifications' => $notifications,
|
'notifications' => $notifications,
|
||||||
@@ -222,7 +299,7 @@ readonly class NotificationMailer
|
|||||||
|
|
||||||
$email = new TemplatedEmail();
|
$email = new TemplatedEmail();
|
||||||
$email
|
$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([
|
->context([
|
||||||
'notification' => $notification,
|
'notification' => $notification,
|
||||||
'dest' => $emailAddress,
|
'dest' => $emailAddress,
|
||||||
|
|||||||
@@ -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/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#}
|
#}
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html lang="{{ app.request.locale }}">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<title>
|
<title>
|
||||||
@@ -35,10 +35,10 @@
|
|||||||
|
|
||||||
<form method="POST" action="{{ path('login_check') }}">
|
<form method="POST" action="{{ path('login_check') }}">
|
||||||
<label for="_username">{{ 'Username'|trans }}</label>
|
<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/>
|
<br/>
|
||||||
<label for="_password">{{ 'Password'|trans }}</label>
|
<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') }}" />
|
<input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}" />
|
||||||
<br/>
|
<br/>
|
||||||
<button type="submit" name="login">{{ 'Login'|trans }}</button>
|
<button type="submit" name="login">{{ 'Login'|trans }}</button>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
Vous pouvez visualiser la notification et y répondre ici:
|
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
|
Le logiciel Chill
|
||||||
@@ -13,7 +13,7 @@ Commentaire:
|
|||||||
|
|
||||||
Vous pouvez visualiser la notification et y répondre ici:
|
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
|
Le logiciel Chill
|
||||||
@@ -44,6 +44,7 @@
|
|||||||
<div>
|
<div>
|
||||||
{{ form_start(form) }}
|
{{ form_start(form) }}
|
||||||
{{ form_row(form.phonenumber) }}
|
{{ form_row(form.phonenumber) }}
|
||||||
|
{{ form_row(form.locale) }}
|
||||||
|
|
||||||
<h2 class="mb-4">{{ 'user.profile.notification_preferences'|trans }}</h2>
|
<h2 class="mb-4">{{ 'user.profile.notification_preferences'|trans }}</h2>
|
||||||
<table class="table table-striped align-middle">
|
<table class="table table-striped align-middle">
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
{{ dest.label }},
|
{{ 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 %}
|
{% if is_dest %}
|
||||||
|
|
||||||
Vous êtes invités à valider cette étape au plus tôt.
|
{{ 'workflow.notification.content.validation_needed'|trans }}
|
||||||
{% endif %}
|
{% 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 -%}
|
{%- if is_dest -%}
|
||||||
Un suivi {{ workflow.text }} demande votre attention: {{ title }}
|
{{ 'workflow.notification.title.attention_needed'|trans({'%workflow%': workflow.text, '%title%': title}) }}
|
||||||
{%- else -%}
|
{%- 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 -%}
|
{%- endif -%}
|
||||||
|
|||||||
@@ -16,11 +16,13 @@ use Symfony\Bridge\Twig\Mime\TemplatedEmail;
|
|||||||
use Symfony\Component\Mailer\MailerInterface;
|
use Symfony\Component\Mailer\MailerInterface;
|
||||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||||
|
|
||||||
|
// use Symfony\Component\Translation\LocaleSwitcher;
|
||||||
|
|
||||||
class RecoverPasswordHelper
|
class RecoverPasswordHelper
|
||||||
{
|
{
|
||||||
final public const RECOVER_PASSWORD_ROUTE = 'password_recover';
|
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
|
* @param bool $absolute
|
||||||
@@ -53,6 +55,24 @@ class RecoverPasswordHelper
|
|||||||
throw new \UnexpectedValueException('No emaail associated to the user');
|
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())
|
$email = (new TemplatedEmail())
|
||||||
->subject($emailSubject)
|
->subject($emailSubject)
|
||||||
->to($user->getEmail())
|
->to($user->getEmail())
|
||||||
|
|||||||
@@ -11,11 +11,11 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Form\Type;
|
namespace Form\Type;
|
||||||
|
|
||||||
use Chill\MainBundle\Entity\Center;
|
|
||||||
use Chill\MainBundle\Entity\Scope;
|
use Chill\MainBundle\Entity\Scope;
|
||||||
use Chill\MainBundle\Entity\User;
|
use Chill\MainBundle\Entity\User;
|
||||||
use Chill\MainBundle\Form\Type\ScopePickerType;
|
use Chill\MainBundle\Form\Type\ScopePickerType;
|
||||||
use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface;
|
use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface;
|
||||||
|
use Chill\MainBundle\Security\Resolver\CenterResolverManagerInterface;
|
||||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Doctrine\ORM\Mapping\Builder\ClassMetadataBuilder;
|
use Doctrine\ORM\Mapping\Builder\ClassMetadataBuilder;
|
||||||
@@ -39,11 +39,11 @@ final class ScopePickerTypeTest extends TypeTestCase
|
|||||||
{
|
{
|
||||||
use ProphecyTrait;
|
use ProphecyTrait;
|
||||||
|
|
||||||
public function estBuildOneScopeIsSuccessful()
|
public function testBuildOneScopeIsSuccessful()
|
||||||
{
|
{
|
||||||
$form = $this->factory->create(ScopePickerType::class, null, [
|
$form = $this->factory->create(ScopePickerType::class, null, [
|
||||||
'center' => new Center(),
|
|
||||||
'role' => 'ONE_SCOPE',
|
'role' => 'ONE_SCOPE',
|
||||||
|
'center' => [],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$view = $form->createView();
|
$view = $form->createView();
|
||||||
@@ -54,8 +54,8 @@ final class ScopePickerTypeTest extends TypeTestCase
|
|||||||
public function testBuildThreeScopesIsSuccessful()
|
public function testBuildThreeScopesIsSuccessful()
|
||||||
{
|
{
|
||||||
$form = $this->factory->create(ScopePickerType::class, null, [
|
$form = $this->factory->create(ScopePickerType::class, null, [
|
||||||
'center' => new Center(),
|
|
||||||
'role' => 'THREE_SCOPE',
|
'role' => 'THREE_SCOPE',
|
||||||
|
'center' => [],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$view = $form->createView();
|
$view = $form->createView();
|
||||||
@@ -66,8 +66,8 @@ final class ScopePickerTypeTest extends TypeTestCase
|
|||||||
public function testBuildTwoScopesIsSuccessful()
|
public function testBuildTwoScopesIsSuccessful()
|
||||||
{
|
{
|
||||||
$form = $this->factory->create(ScopePickerType::class, null, [
|
$form = $this->factory->create(ScopePickerType::class, null, [
|
||||||
'center' => new Center(),
|
|
||||||
'role' => 'TWO_SCOPE',
|
'role' => 'TWO_SCOPE',
|
||||||
|
'center' => [],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$view = $form->createView();
|
$view = $form->createView();
|
||||||
@@ -101,10 +101,13 @@ final class ScopePickerTypeTest extends TypeTestCase
|
|||||||
static fn ($args) => $args[0]['fr']
|
static fn ($args) => $args[0]['fr']
|
||||||
);
|
);
|
||||||
|
|
||||||
|
$centerResolverManager = $this->prophesize(CenterResolverManagerInterface::class);
|
||||||
|
|
||||||
$type = new ScopePickerType(
|
$type = new ScopePickerType(
|
||||||
|
$translatableStringHelper->reveal(),
|
||||||
$authorizationHelper->reveal(),
|
$authorizationHelper->reveal(),
|
||||||
$security->reveal(),
|
$security->reveal(),
|
||||||
$translatableStringHelper->reveal()
|
$centerResolverManager->reveal()
|
||||||
);
|
);
|
||||||
|
|
||||||
// add the mocks for creating EntityType
|
// 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\Event\Event;
|
||||||
use Symfony\Component\Workflow\Registry;
|
use Symfony\Component\Workflow\Registry;
|
||||||
|
|
||||||
|
// use Symfony\Component\Translation\LocaleSwitcher;
|
||||||
|
|
||||||
final readonly class NotificationToUserGroupsOnTransition implements EventSubscriberInterface
|
final readonly class NotificationToUserGroupsOnTransition implements EventSubscriberInterface
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
@@ -31,6 +33,7 @@ final readonly class NotificationToUserGroupsOnTransition implements EventSubscr
|
|||||||
private MailerInterface $mailer,
|
private MailerInterface $mailer,
|
||||||
private EntityManagerInterface $entityManager,
|
private EntityManagerInterface $entityManager,
|
||||||
private EntityWorkflowManager $entityWorkflowManager,
|
private EntityWorkflowManager $entityWorkflowManager,
|
||||||
|
// private LocaleSwitcher $localeSwitcher,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public static function getSubscribedEvents(): array
|
public static function getSubscribedEvents(): array
|
||||||
@@ -87,6 +90,24 @@ final readonly class NotificationToUserGroupsOnTransition implements EventSubscr
|
|||||||
'title' => $title,
|
'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 = new TemplatedEmail();
|
||||||
$email
|
$email
|
||||||
->htmlTemplate('@ChillMain/Workflow/workflow_notification_on_transition_completed_content_to_user_group.fr.txt.twig')
|
->htmlTemplate('@ChillMain/Workflow/workflow_notification_on_transition_completed_content_to_user_group.fr.txt.twig')
|
||||||
|
|||||||
@@ -12,6 +12,12 @@ services:
|
|||||||
tags:
|
tags:
|
||||||
- { name: form.type, alias: translatable_string }
|
- { 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:
|
chill.main.form.type.select2choice:
|
||||||
class: Chill\MainBundle\Form\Type\Select2ChoiceType
|
class: Chill\MainBundle\Form\Type\Select2ChoiceType
|
||||||
tags:
|
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 job: Pas de métier assigné
|
||||||
no scope: Pas de service assigné
|
no scope: Pas de service assigné
|
||||||
notification_preferences: Préférences pour mes notifications
|
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:
|
user_group:
|
||||||
inactive: Inactif
|
inactive: Inactif
|
||||||
@@ -668,6 +672,17 @@ workflow:
|
|||||||
reject_are_you_sure: Êtes-vous sûr de vouloir rejeter la signature de %signer%
|
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
|
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:
|
attachments:
|
||||||
title: Pièces jointes
|
title: Pièces jointes
|
||||||
no_attachment: Aucune pièce jointe
|
no_attachment: Aucune pièce jointe
|
||||||
@@ -747,7 +762,22 @@ notification:
|
|||||||
greeting: "Bonjour %user%"
|
greeting: "Bonjour %user%"
|
||||||
intro: "Vous avez reçu %notification_count% nouvelle(s) notification(s)."
|
intro: "Vous avez reçu %notification_count% nouvelle(s) notification(s)."
|
||||||
view_notification: "Vous pouvez visualiser la notification et y répondre ici:"
|
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_COMPOSE_EXPORT: Exécuter des exports et les sauvegarder
|
||||||
CHILL_MAIN_GENERATE_SAVED_EXPORT: Exécuter et modifier des exports préalablement sauvegardés
|
CHILL_MAIN_GENERATE_SAVED_EXPORT: Exécuter et modifier des exports préalablement sauvegardés
|
||||||
@@ -975,3 +1005,6 @@ multiselect:
|
|||||||
editor:
|
editor:
|
||||||
switch_to_simple: Éditeur simple
|
switch_to_simple: Éditeur simple
|
||||||
switch_to_complex: Éditeur riche
|
switch_to_complex: Éditeur riche
|
||||||
|
|
||||||
|
login_page:
|
||||||
|
logo_alt: "Logo de Chill"
|
||||||
|
|||||||
@@ -46,6 +46,14 @@ No title: Geen titel
|
|||||||
User profile: Mijn gebruikersprofiel
|
User profile: Mijn gebruikersprofiel
|
||||||
Phonenumber successfully updated!: Telefoonnummer bijgewerkt!
|
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
|
Edit: Bewerken
|
||||||
Update: Updaten
|
Update: Updaten
|
||||||
@@ -423,6 +431,17 @@ workflow:
|
|||||||
For: Pour
|
For: Pour
|
||||||
Cc: Cc
|
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 final: Recevoir une notification à l'étape finale
|
||||||
Subscribe all steps: Recevoir une notification à chaque étape
|
Subscribe all steps: Recevoir une notification à chaque étape
|
||||||
|
|||||||
@@ -866,29 +866,6 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
|
|||||||
return $this->calendars;
|
return $this->calendars;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get next calendars for this person.
|
|
||||||
* Only returns calendars where this person is in the persons collection and the person property is not null.
|
|
||||||
*
|
|
||||||
* @param int $limit Maximum number of calendars to return
|
|
||||||
*
|
|
||||||
* @return ReadableCollection<int, Calendar>
|
|
||||||
*/
|
|
||||||
public function getNextCalendarsForPerson(int $limit = 5): ReadableCollection
|
|
||||||
{
|
|
||||||
$today = new \DateTimeImmutable('today');
|
|
||||||
|
|
||||||
$filtered = $this->calendars->filter(
|
|
||||||
fn (Calendar $calendar) => $calendar->getStartDate() >= $today
|
|
||||||
&& $calendar->getPerson() === $this
|
|
||||||
);
|
|
||||||
|
|
||||||
$sorted = $filtered->toArray();
|
|
||||||
usort($sorted, fn ($a, $b) => $a->getStartDate() <=> $b->getStartDate());
|
|
||||||
|
|
||||||
return new ArrayCollection(array_slice($sorted, 0, $limit));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getCenter(): ?Center
|
public function getCenter(): ?Center
|
||||||
{
|
{
|
||||||
if (null !== $this->centerCurrent) {
|
if (null !== $this->centerCurrent) {
|
||||||
|
|||||||
@@ -189,43 +189,7 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% set calendars = [] %}
|
|
||||||
{% for c in person.getNextCalendarsForPerson(10) %}
|
|
||||||
{% if is_granted('CHILL_CALENDAR_CALENDAR_SEE', c) %}
|
|
||||||
{% set calendars = calendars|merge([c]) %}
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
{% if calendars|length > 0 %}
|
|
||||||
<div class="d-flex align-items-center justify-content-between flex-wrap gap-3 mt-3">
|
|
||||||
<h5 class="mb-0">{{ 'chill_calendar.Next calendars'|trans }}</h5>
|
|
||||||
|
|
||||||
<ul class="list-inline mb-0 d-flex flex-wrap align-items-center">
|
|
||||||
{% for c in calendars %}
|
|
||||||
<li class="list-inline-item">
|
|
||||||
{% if is_granted('CHILL_CALENDAR_CALENDAR_EDIT', c) %}
|
|
||||||
<a href="{{ chill_path_add_return_path('chill_calendar_calendar_edit', { id: c.id }) }}">
|
|
||||||
<span class="badge bg-secondary">
|
|
||||||
{{ c.startDate|format_datetime('long', 'short') }}
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
{% else %}
|
|
||||||
<span class="badge bg-secondary">
|
|
||||||
{{ c.startDate|format_datetime('long', 'short') }}
|
|
||||||
</span>
|
|
||||||
{% endif %}
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
{% if is_granted('CHILL_CALENDAR_CALENDAR_SEE', person) %}
|
|
||||||
<a href="{{ chill_path_add_return_path('chill_calendar_calendar_list_by_person', {'id': person.id}) }}" class="calendar-list__global"><i class="fa fa-list"></i></a>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
|
||||||
<div class="item-col">
|
|
||||||
<ul class="record_actions">
|
<ul class="record_actions">
|
||||||
{% if options['customButtons']['before'] is defined %}
|
{% if options['customButtons']['before'] is defined %}
|
||||||
{{ options['customButtons']['before'] }}
|
{{ options['customButtons']['before'] }}
|
||||||
|
|||||||
@@ -168,9 +168,8 @@ final readonly class PersonContext implements PersonContextInterface
|
|||||||
|
|
||||||
if ($this->isScopeNecessary($entity)) {
|
if ($this->isScopeNecessary($entity)) {
|
||||||
$builder->add('scope', ScopePickerType::class, [
|
$builder->add('scope', ScopePickerType::class, [
|
||||||
'center' => $this->centerResolverManager->resolveCenters($entity),
|
|
||||||
'role' => PersonDocumentVoter::CREATE,
|
'role' => PersonDocumentVoter::CREATE,
|
||||||
'label' => 'Scope',
|
'subject' => $entity,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,11 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
|
|||||||
|
|
||||||
class SingleTaskType extends AbstractType
|
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)
|
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']) {
|
if ($isScopeConcerned && $this->parameterBag->get('chill_main')['acl']['form_show_scopes']) {
|
||||||
$builder
|
$builder
|
||||||
->add('circle', ScopePickerType::class, [
|
->add('circle', ScopePickerType::class, [
|
||||||
'center' => $center,
|
|
||||||
'role' => $options['role'],
|
'role' => $options['role'],
|
||||||
|
'subject' => $task,
|
||||||
'required' => true,
|
'required' => true,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
{% block title 'Tasks for {{ name }}'|trans({ '{{ name }}' : person|chill_entity_render_string }) %}
|
{% block title 'Tasks for {{ name }}'|trans({ '{{ name }}' : person|chill_entity_render_string }) %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="col-md-10 col-xxl">
|
<div class="task-list"">
|
||||||
|
|
||||||
<h1>{{ block('title') }}</h1>
|
<h1>{{ block('title') }}</h1>
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,7 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{% block content %}
|
{% 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' %}
|
{% include '@ChillTask/SingleTask/AccompanyingCourse/list.html.twig' %}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user