Compare commits

..

12 Commits

Author SHA1 Message Date
e1b91ebbfd Release v4.8.0 2025-11-17 15:11:14 +01:00
2139b53fb0 Merge branch '449-scope-picker-form-label' into 'master'
Remove the label if there is only one scope and no scope picking field is displayed.

Closes #449

See merge request Chill-Projet/chill-bundles!911
2025-11-17 10:48:16 +00:00
a43181d60d Remove the label if there is only one scope and no scope picking field is displayed. 2025-11-17 10:48:15 +00:00
04bc1c5de8 Merge branch '463-update-calendar-with-accepted-invites' into 'master'
Update calendar with accepted invites

Closes #463

See merge request Chill-Projet/chill-bundles!921
2025-11-14 14:22:59 +00:00
0a07d68b6d Merge branch '461-calendar-items-clickable' into 'master'
Resolve "Rendre le rendez-vous clicable dans la page "mes rendez-vous""

Closes #461

See merge request Chill-Projet/chill-bundles!919
2025-11-14 14:08:04 +00:00
fccd29e3c7 Resolve "Rendre le rendez-vous clicable dans la page "mes rendez-vous"" 2025-11-14 14:08:03 +00:00
274ee94196 Merge branch '420-localisation-variable' into 'master'
Ajouter une variable de localisation aux utilisateurs

Closes #420

See merge request Chill-Projet/chill-bundles!904
2025-11-14 13:52:33 +00:00
799d04142e Ajouter une variable de localisation aux utilisateurs 2025-11-14 13:52:33 +00:00
dfe8d8b0bf Merge branch 'accessibility/improve-login-page' into 'master'
Improve accessibility on the login page

See merge request Chill-Projet/chill-bundles!922
2025-11-14 10:16:08 +00:00
82f347b93a Improve accessibility on the login page 2025-11-14 10:16:08 +00:00
635efd6f1d Update calendar with accepted invites 2025-11-12 17:01:09 +01:00
869880d8f3 Revert "Display calendar items linked to person within search results"
This reverts commit f7ea7e4dbf.
2025-11-12 13:08:54 +01:00
41 changed files with 521 additions and 140 deletions

View File

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

9
.changes/v4.8.0.md Normal file
View 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.

View File

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

View File

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

View File

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

View File

@@ -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']]);

View File

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

View File

@@ -14,13 +14,13 @@ div.calendar-list {
ul.calendar-list {
display: inline-block;
}
}
.calendar-list__global {
display: inline-block;
padding: 0.2rem;
min-width: 2rem;
border: 1px solid var(--bs-chill-blue);
border-radius: 0.25rem;
text-align: center;
& > a.calendar-list__global {
display: inline-block;
padding: 0.2rem;
min-width: 2rem;
border: 1px solid var(--bs-chill-blue);
border-radius: 0.25rem;
text-align: center;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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']);

View File

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

View File

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

View 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;
}
}

View File

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

View File

@@ -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.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.fr.md.twig')
->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.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.fr.md.twig')
->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,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -866,29 +866,6 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
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
{
if (null !== $this->centerCurrent) {

View File

@@ -189,43 +189,7 @@
{% endfor %}
</li>
{% 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>
</div>
<div class="item-col">
<ul class="record_actions">
{% if options['customButtons']['before'] is defined %}
{{ options['customButtons']['before'] }}

View File

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

View File

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

View File

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

View File

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