Merge branch 'master' into ticket-app-master

# Conflicts:
#	docs/source/development/create-a-new-bundle.md
#	src/Bundle/ChillActivityBundle/Resources/public/vuejs/Activity/components/SocialIssuesAcc/CheckSocialAction.vue
#	src/Bundle/ChillActivityBundle/Resources/public/vuejs/Activity/components/SocialIssuesAcc/CheckSocialIssue.vue
#	src/Bundle/ChillCalendarBundle/Menu/UserMenuBuilder.php
#	src/Bundle/ChillCalendarBundle/Resources/views/Calendar/_list.html.twig
#	src/Bundle/ChillCalendarBundle/Resources/views/Calendar/cancelCalendarByAccompanyingCourse.html.twig
#	src/Bundle/ChillCalendarBundle/Resources/views/Calendar/cancelCalendarByPerson.html.twig
#	src/Bundle/ChillCalendarBundle/translations/messages.fr.yml
#	src/Bundle/ChillDocStoreBundle/Resources/public/types/index.ts
#	src/Bundle/ChillMainBundle/Resources/public/lib/api/apiMethods.ts
#	src/Bundle/ChillMainBundle/translations/messages.fr.yml
#	src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/Confirm.vue
This commit is contained in:
2025-12-05 17:04:49 +01:00
248 changed files with 11337 additions and 5040 deletions

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

@@ -47,6 +47,8 @@ class ChillCalendarExtension extends Extension implements PrependExtensionInterf
} else {
$container->setParameter('chill_calendar.short_messages', null);
}
$container->setParameter('chill_calendar.remote_calendar_dsn', $config['remote_calendar_dsn']);
}
public function prepend(ContainerBuilder $container)

View File

@@ -32,9 +32,10 @@ class Configuration implements ConfigurationInterface
->canBeDisabled()
->children()->end()
->end() // end for short_messages
->scalarNode('remote_calendar_dsn')->defaultValue('null://null')->cannotBeEmpty()->end()
->arrayNode('remote_calendars_sync')->canBeEnabled()
->children()
->arrayNode('microsoft_graph')->canBeEnabled()
->arrayNode('microsoft_graph')->canBeEnabled()->setDeprecated('chill-project/chill-bundles', '4.7.0', 'The child node %node% at path %path% is deprecated: use remote_calendar_dsn instead, with a "msgraph://default" value')
->children()
->end() // end of machine_access_token
->end() // end of microsoft_graph children

View File

@@ -107,6 +107,11 @@ class CalendarRange implements TrackCreationInterface, TrackUpdateInterface
return $this;
}
public function hasLocation(): bool
{
return null !== $this->location;
}
public function setLocation(?Location $location): self
{
$this->location = $location;

View File

@@ -11,32 +11,46 @@ declare(strict_types=1);
namespace Chill\CalendarBundle\Menu;
use Chill\CalendarBundle\Repository\InviteRepository;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
use Knp\Menu\MenuItem;
use Symfony\Component\Security\Core\Security;
use Symfony\Contracts\Translation\TranslatorInterface;
class UserMenuBuilder implements LocalMenuBuilderInterface
final readonly class UserMenuBuilder implements LocalMenuBuilderInterface
{
public function __construct(private readonly Security $security, public TranslatorInterface $translator) {}
public function __construct(
private Security $security,
private TranslatorInterface $translator,
private InviteRepository $inviteRepository,
) {}
public function buildMenu($menuId, MenuItem $menu, array $parameters)
public function buildMenu($menuId, MenuItem $menu, array $parameters): void
{
if ($this->security->isGranted('ROLE_USER')) {
$menu->addChild('My calendar list', [
'route' => 'chill_calendar_calendar_list_my',
])
->setExtras([
'order' => 8,
'icon' => 'tasks',
]);
$menu->addChild('invite.list.title', [
'route' => 'chill_calendar_invitations_list_my',
])
->setExtras([
'order' => 9,
'icon' => 'tasks',
]);
$user = $this->security->getUser();
if ($user instanceof User) {
$invitationsPending = $this->inviteRepository->countPendingInvitesByUser($user);
if ($this->security->isGranted('ROLE_USER')) {
$menu->addChild('My calendar list', [
'route' => 'chill_calendar_calendar_list_my',
])
->setExtras([
'order' => 8,
'icon' => 'tasks',
]);
$menu->addChild(
$this->translator->trans('invite.menu with counter', ['nb' => $invitationsPending]),
['route' => 'chill_calendar_invitations_list_my']
)
->setExtras([
'order' => 9,
'icon' => 'tasks',
'counter' => 0 < $invitationsPending ? $invitationsPending : null,
]);
}
}
}

View File

@@ -22,6 +22,7 @@ use Chill\CalendarBundle\Messenger\Message\CalendarRemovedMessage;
use Chill\CalendarBundle\RemoteCalendar\Connector\RemoteCalendarConnectorInterface;
use Chill\CalendarBundle\Repository\CalendarRangeRepository;
use Chill\MainBundle\Repository\UserRepositoryInterface;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
/**
@@ -31,7 +32,12 @@ use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
*/
class CalendarRemoveHandler implements MessageHandlerInterface
{
public function __construct(private readonly RemoteCalendarConnectorInterface $remoteCalendarConnector, private readonly CalendarRangeRepository $calendarRangeRepository, private readonly UserRepositoryInterface $userRepository) {}
public function __construct(
private readonly RemoteCalendarConnectorInterface $remoteCalendarConnector,
private readonly CalendarRangeRepository $calendarRangeRepository,
private readonly UserRepositoryInterface $userRepository,
private readonly EntityManagerInterface $entityManager,
) {}
public function __invoke(CalendarRemovedMessage $message)
{
@@ -47,5 +53,7 @@ class CalendarRemoveHandler implements MessageHandlerInterface
$this->userRepository->find($message->getCalendarUserId()),
$associatedRange
);
$this->entityManager->flush();
}
}

View File

@@ -21,42 +21,137 @@ namespace Chill\CalendarBundle\RemoteCalendar\Connector;
use Chill\CalendarBundle\Entity\Calendar;
use Chill\CalendarBundle\Entity\CalendarRange;
use Chill\CalendarBundle\Entity\Invite;
use Chill\CalendarBundle\Messenger\Message\CalendarMessage;
use Chill\CalendarBundle\RemoteCalendar\Model\RemoteEvent;
use Chill\MainBundle\Entity\User;
use Symfony\Component\HttpFoundation\Response;
/**
* Contract for connectors that synchronize Chill calendars with a remote
* calendar provider (for example Microsoft 365/Graph, Zimbra, ...).
*
* Implementations act as an adapter between Chill domain objects
* (Calendar, CalendarRange, Invite) and the remote provider API. They must:
* - expose a readiness flow for per-user authorization when applicable
* (see {@see getMakeReadyResponse()} and {@see isReady()});
* - list and count remote events in a time range for a given user;
* - mirror local lifecycle changes to the remote provider for calendars,
* calendar ranges (availability/busy blocks) and invites/attendees.
*
* Use {@see MSGraphRemoteCalendarConnector} as a reference implementation for
* expected behaviours, error handling and parameter semantics.
*/
interface RemoteCalendarConnectorInterface
{
public function countEventsForUser(User $user, \DateTimeImmutable $startDate, \DateTimeImmutable $endDate): int;
/**
* Return a response, more probably a RedirectResponse, where the user
* will be able to fullfill requirements to prepare this connector and
* make it ready.
* Returns a Response (typically a RedirectResponse) that lets the current
* user perform the steps required to make the connector usable (for
* example, OAuth consent or account linking). After completion, the user
* should be redirected back to the given path.
*/
public function getMakeReadyResponse(string $returnPath): Response;
/**
* Return true if the connector is ready to act as a proxy for reading
* remote calendars.
* Returns true when the connector is ready to access the remote provider
* on behalf of the current user (e.g. required tokens/consent exist).
*/
public function isReady(): bool;
/**
* Lists events from the remote provider for the given user and time range.
*
* Implementations should map provider-specific payloads to instances of
* {@see RemoteEvent}.
*
* @return array|RemoteEvent[]
*/
public function listEventsForUser(User $user, \DateTimeImmutable $startDate, \DateTimeImmutable $endDate, ?int $offset = 0, ?int $limit = 50): array;
/**
* Removes a calendar (single event) from the remote provider.
*
* **Note**: calendar (single event) which are canceled will appears in this
* method, and not in syncCalendar method.
*
* Parameters:
* - remoteId: the provider identifier of the remote event to delete. If
* empty, implementations should no-op.
* - remoteAttributes: provider-specific metadata previously stored with the
* local entity (e.g. change keys, etags) that can help perform safe
* concurrency checks when deleting. Implementations may ignore unknown
* keys.
* - user: the user in whose remote calendar the event lives and on whose
* behalf the deletion must be performed.
* - associatedCalendarRange: when provided, the implementation should
* update/synchronize the corresponding remote busy-time block after the
* event removal so that availability stays consistent.
*/
public function removeCalendar(string $remoteId, array $remoteAttributes, User $user, ?CalendarRange $associatedCalendarRange = null): void;
/**
* Removes a remote busy-time block (calendar range) identified by
* provider-specific id and attributes for the given user.
*
* Implementations should no-op if the id is empty.
*/
public function removeCalendarRange(string $remoteId, array $remoteAttributes, User $user): void;
/**
* Synchronizes a Calendar entity to the remote provider.
*
* Typical cases to support (see MSGraph implementation):
* - Creating the event on the remote calendar when it has no remote id.
* - Updating the existing remote event when details or attendees change.
* - Handling main user changes: cancel on the previous user's calendar,
* (re)create associated ranges where needed, then create on the new
* main user's calendar.
* - If the Calendar uses a CalendarRange that already exists remotely,
* implementations should remove/update that remote range when the event
* becomes the source of truth for busy times.
*
* The implementation should not expects to receive calendar which are canceled
* here.
*
* Parameters:
* - calendar: the domain Calendar to mirror remotely.
* - action: a hint about what triggered the sync; implementations should not rely
* solely on this value and must base decisions on the Calendar state.
* - previousCalendarRange: if the Calendar was previously attached to a
* different range, this contains the former range so it can be recreated
* remotely to preserve availability history when applicable.
* - previousMainUser: the former main user, when the main user changed;
* used to cancel the event in the previous user's calendar.
* - oldInvites: the attendee snapshot before the change. Each item is an
* array with keys: inviteId, userId, userEmail, userLabel.
* - newInvites: the attendee snapshot after the change, same shape as
* oldInvites. Implementations can compute diffs to add/remove attendees.
*
* The $action argument is a string tag indicating what happened to the
* calendar. It MUST be one of the constants defined on
* {@see CalendarMessage}:
* - {@see CalendarMessage::CALENDAR_PERSIST}
* - {@see CalendarMessage::CALENDAR_UPDATE}
*
* @param array<array{inviteId: int, userId: int, userEmail: int, userLabel: string}> $oldInvites
*
* @phpstan-param (CalendarMessage::CALENDAR_PERSIST|CalendarMessage::CALENDAR_UPDATE) $action
*/
public function syncCalendar(Calendar $calendar, string $action, ?CalendarRange $previousCalendarRange, ?User $previousMainUser, ?array $oldInvites, ?array $newInvites): void;
/**
* Creates or updates a remote busy-time block representing the provided
* CalendarRange. If the range has a remote id, it should be updated;
* otherwise it should be created remotely, and the range enriched with
* the new id/attributes by the caller.
*/
public function syncCalendarRange(CalendarRange $calendarRange): void;
/**
* Synchronizes a single Invite (attendee) change to the remote provider.
* Implementations may need to lookup the attendee's personal calendar to
* find provider-specific identifiers before patching the main event.
*/
public function syncInvite(Invite $invite): void;
}

View File

@@ -35,25 +35,46 @@ use TheNetworg\OAuth2\Client\Provider\Azure;
class RemoteCalendarCompilerPass implements CompilerPassInterface
{
private const ZIMBRA_CONNECTOR = 'Chill\ZimbraBundle\Calendar\Connector\ZimbraConnector';
private const MS_GRAPH_SERVICES_TO_REMOVE = [
MapAndSubscribeUserCalendarCommand::class,
AzureGrantAdminConsentAndAcquireToken::class,
RemoteCalendarConnectAzureController::class,
MachineTokenStorage::class,
MachineHttpClient::class,
MSGraphRemoteCalendarConnector::class,
MSUserAbsenceReaderInterface::class,
MSUserAbsenceSync::class,
];
public function process(ContainerBuilder $container)
{
$config = $container->getParameter('chill_calendar');
$config = $container->getParameter('chill_calendar.remote_calendar_dsn');
if (true === $container->getParameter('chill_calendar')['remote_calendars_sync']['microsoft_graph']['enabled']) {
$dsn = 'msgraph://default';
} else {
$dsn = $config;
}
if (true === $config['remote_calendars_sync']['microsoft_graph']['enabled']) {
$scheme = parse_url($dsn, PHP_URL_SCHEME);
if ('msgraph' === $scheme) {
$connector = MSGraphRemoteCalendarConnector::class;
$container->setAlias(HttpClientInterface::class.' $machineHttpClient', MachineHttpClient::class);
} else {
} elseif ('zimbra+http' === $scheme || 'zimbra+https' === $scheme) {
$connector = self::ZIMBRA_CONNECTOR;
foreach (self::MS_GRAPH_SERVICES_TO_REMOVE as $serviceId) {
$container->removeDefinition($serviceId);
}
} elseif ('null' === $scheme) {
$connector = NullRemoteCalendarConnector::class;
// remove services which cannot be loaded
$container->removeDefinition(MapAndSubscribeUserCalendarCommand::class);
$container->removeDefinition(AzureGrantAdminConsentAndAcquireToken::class);
$container->removeDefinition(RemoteCalendarConnectAzureController::class);
$container->removeDefinition(MachineTokenStorage::class);
$container->removeDefinition(MachineHttpClient::class);
$container->removeDefinition(MSGraphRemoteCalendarConnector::class);
$container->removeDefinition(MSUserAbsenceReaderInterface::class);
$container->removeDefinition(MSUserAbsenceSync::class);
foreach (self::MS_GRAPH_SERVICES_TO_REMOVE as $serviceId) {
$container->removeDefinition($serviceId);
}
} else {
throw new \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException('Unsupported remote calendar scheme: '.$scheme);
}
if (!$container->hasAlias(Azure::class) && $container->hasDefinition('knpu.oauth2.client.azure')) {
@@ -62,7 +83,9 @@ class RemoteCalendarCompilerPass implements CompilerPassInterface
foreach ([
NullRemoteCalendarConnector::class,
MSGraphRemoteCalendarConnector::class, ] as $serviceId) {
MSGraphRemoteCalendarConnector::class,
self::ZIMBRA_CONNECTOR,
] as $serviceId) {
if ($connector === $serviceId) {
$container->getDefinition($serviceId)
->setDecoratedService(RemoteCalendarConnectorInterface::class);

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,71 @@ 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 countPendingInvitesByUser(User $user): int
{
$qb = $this->entityRepository->createQueryBuilder('i');
$qb->select('COUNT(i)')
->where(
$qb->expr()->andX(
$qb->expr()->eq('i.user', ':user'),
$qb->expr()->eq('i.status', ':status')
)
)
->setParameters([
'user' => $user,
'status' => Invite::PENDING,
]);
return $qb->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

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

@@ -28,6 +28,9 @@
{% if calendar.status == 'canceled' %}
<del>
{% endif %}
{% if calendar.status == 'canceled' %}
<del>
{% endif %}
{% if calendar.endDate.diff(calendar.startDate).days >= 1 %}
{{ calendar.startDate|format_datetime('short', 'short') }}
- {{ calendar.endDate|format_datetime('short', 'short') }}
@@ -162,6 +165,7 @@
{% endif %}
<div class="item-row separator">
{% if show_record_actions is not defined or show_record_actions %}
<ul class="record_actions">
{% if is_granted('CHILL_CALENDAR_DOC_EDIT', calendar) and calendar.status is not constant('STATUS_CANCELED', calendar) %}
{% if templates|length == 0 %}
@@ -236,6 +240,10 @@
<a href="{{ chill_path_add_return_path('chill_calendar_calendar_cancel', { 'id': calendar.id } ) }}"
class="btn btn-action"><i class="bi bi-x-circle"></i> {{ 'Cancel'|trans }}</a>
</li>
<li>
<a href="{{ chill_path_add_return_path('chill_calendar_calendar_cancel', { 'id': calendar.id } ) }}"
class="btn btn-action"><i class="bi bi-x-circle"></i> {{ 'Cancel'|trans }}</a>
</li>
{% endif %}
{% if is_granted('CHILL_CALENDAR_CALENDAR_DELETE', calendar) %}
@@ -245,7 +253,7 @@
</li>
{% endif %}
</ul>
{% endif %}
</div>
</div>

View File

@@ -6,6 +6,10 @@
{% block content %}
<div class="flex-table list-records context-accompanyingCourse">
{{ include('@ChillCalendar/Calendar/_list.html.twig', {context: 'accompanying_course', show_record_actions: false}) }}
</div>
{{ form_start(form) }}
{{ form_row(form.cancelReason) }}

View File

@@ -6,6 +6,10 @@
{% block content %}
<div class="flex-table list-records context-person">
{{ include('@ChillCalendar/Calendar/_list.html.twig', {context: 'person', show_record_actions: false}) }}
</div>
{{ form_start(form) }}
{{ form_row(form.cancelReason) }}

View File

@@ -6,3 +6,11 @@ chill_calendar:
few {# rendez-vous sont ignorés par le filtre de date. Modifiez le filtre de date pour les voir apparaitre.}
other {# rendez-vous sont ignorés par le filtre de date. Modifiez le filtre de date pour les voir apparaitre.}
}
invite:
menu with counter: >-
{nb, plural,
=0 {Mes invitations}
one {# invitation}
few {# invitations}
other {# invitations}
}

View File

@@ -0,0 +1,8 @@
chill_calendar:
There are count ignored calendars by date filter: >-
{nbIgnored, plural,
=0 {Er zijn geen afspraken genegeerd door de datumfilter.}
one {Er is een afspraak genegeerd door de datumfilter. Wijzig de datumfilter om deze te laten verschijnen.}
few {# afspraken zijn genegeerd door de datumfilter. Wijzig de datumfilter om deze te laten verschijnen.}
other {# afspraken zijn genegeerd door de datumfilter. Wijzig de datumfilter om deze te laten verschijnen.}
}

View File

@@ -87,6 +87,8 @@ remote_ms_graph:
remote_calendar:
calendar_range_title: Plage de disponibilité Chill
# small type-hint in remote calendar to says that the appointment is created through an invitation, and not as main referrer
calendar_invite_statement_in_calendar: Par invitation
invite:
accepted: Accepté
@@ -96,6 +98,7 @@ invite:
list:
none: Il n'y aucun invitation
title: Mes invitations
number of invitations waiting: Invitations en attente
# exports
Exports of calendar: Exports des rendez-vous

View File

@@ -26,3 +26,149 @@ The calendar item has been successfully removed.: De afspraak is verwijdert
From the day: Vanaf
to the day: tot
Transform to activity: In activiteit omzetten
Create a new calendar in accompanying course: Afspraak aanmaken in het traject
Will send SMS: Er zal een herinnerings-sms worden verzonden
Will not send SMS: Er wordt geen herinnerings-sms verzonden
SMS already sent: Er is een sms verzonden
Canceled by: Geannuleerd door
Calendar configuration: Beheer van afspraken
crud:
calendar_cancel-reason:
index:
title: Lijst van annuleringsredenen
add_new: Nieuwe toevoegen
title_new: Nieuwe annuleringsreden
title_edit: Annuleringsreden bewerken
chill_calendar:
canceled: Geannuleerd
cancel_reason: Reden van annulering
cancel_calendar_item: Afspraak annuleren
calendar_canceled: De afspraak is geannuleerd
canceled_by:
user: Gebruiker
person: Gebruiker
other: Andere
Document: Document van een afspraak
form:
The main user is mandatory. He will organize the appointment.: De hoofdgebruiker is verplicht. Hij is de organisator van de gebeurtenis.
Create for referrer: Aanmaken voor de referent
start date filter: Begin van de afspraak
From: Van
To: Tot
Next calendars: Volgende afspraken
Add a document: Document toevoegen
Documents: Documenten
Create and add a document: Aanmaken en document toevoegen
Save and add a document: Opslaan en document toevoegen
Create for me: Afspraak aanmaken voor mezelf
Edit a document: Document bewerken
Document title: Titel
Document object: Document
Add a document from template: Document toevoegen vanaf sjabloon
Upload a document: Document uploaden
Remove a calendar document: Document van een afspraak verwijderen
Are you sure you want to remove the doc?: Weet u zeker dat u het gekoppelde document wilt verwijderen?
Document outdated: De datum en tijd van de afspraak zijn gewijzigd na het aanmaken van het document
remote_ms_graph:
freebusy_statuses:
busy: Bezet
free: Vrij
tentative: In afwachting van bevestiging
oof: Buiten kantoor
workingElsewhere: Werkt elders
unknown: Onbekend
cancel_event_because_main_user_is_%label%: De gebeurtenis is overgedragen aan gebruiker %label%
remote_calendar:
calendar_range_title: Chill-beschikbaarheidsperiode
invite:
accepted: Geaccepteerd
declined: Geweigerd
pending: In afwachting
tentative: Voorlopig geaccepteerd
list:
none: Er is geen uitnodiging
title: Mijn uitnodigingen
# exports
Exports of calendar: Exports van afspraken
Count calendars: Aantal afspraken
Count calendars by various parameters.: Telt het aantal afspraken op basis van verschillende parameters.
Average appointment duration: Gemiddelde duur van afspraken
Get the average of appointment duration according to various filters: Berekent het gemiddelde van de duur van afspraken op basis van verschillende parameters.
Sum of appointment durations: Som van de duur van afspraken
Get the sum of appointment durations according to various filters: Berekent de som van de duur van afspraken op basis van verschillende parameters.
'Filtered by agent: only %agents%': "Gefilterd op agenten: alleen %agents%"
Filter calendars by agent: Afspraken filteren op agenten
Filter calendars between certain dates: Afspraken filteren op datum van de afspraak
'Filtered by calendars between %dateFrom% and %dateTo%': 'Gefilterd op afspraken tussen %dateFrom% en %dateTo%'
'Filtered by calendar range: only %calendarRange%': 'Gefilterd op afspraken per beschikbaarheidsperiode: alleen de %calendarRange%'
Filter by calendar range: Filteren op afspraken binnen een beschikbaarheidsperiode of niet
Group calendars by agent: Afspraken groeperen op agent
Group calendars by location type: Afspraken groeperen op type locatie
Group calendars by location: Afspraken groeperen op afspraaklocatie
Group calendars by cancel reason: Afspraken groeperen op annuleringsreden
Group calendars by month and year: Afspraken groeperen op maand en jaar
Group calendars by urgency: Afspraken groeperen op urgent of niet
export:
aggregator.calendar:
agent_job:
Group calendars by agent job: Afspraken groeperen op beroep van de agent
agent_scope:
Group calendars by agent scope: Afspraken groeperen op dienst van de agent
filter.calendar:
agent_job:
Filter calendars by agent job: Afspraken filteren op beroepen van de agenten (hoofdgebruikers)
'Filtered by agent job: only %jobs%': 'Gefilterd op beroepen van de agenten (hoofdgebruikers): alleen de %jobs%'
agent_scope:
Filter calendars by agent scope: Afspraken filteren op diensten van de agenten (hoofdgebruikers)
'Filtered by agent scope: only %scopes%': 'Gefilterd op diensten van de agenten (hoofdgebruikers): alleen de diensten %scopes%'
Scope: Dienst
Job: Beroep
Location type: Type locatie
Location: Afspraaklocatie
by month and year: Per maand en jaar
is urgent: Urgent
is not urgent: Niet urgent
has calendar range: Binnen een beschikbaarheidsperiode?
Not made within a calendar range: Afspraak binnen een beschikbaarheidsperiode
Made within a calendar range: Afspraak buiten een beschikbaarheidsperiode
docgen:
calendar:
Base context for calendar: 'Afspraak: basiscontext'
A base context for generating document on calendar: Context voor het genereren van documenten op basis van afspraken
Track changes on datetime and warn user if date time is updated after the doc generation: Wijzigingen in het document volgen en gebruikers waarschuwen dat de datum en tijd zijn gewijzigd na het genereren van het document
Ask main person: Vragen om een gebruiker te kiezen uit de deelnemers aan de afspraken
Main person label: Label om de gebruiker te kiezen
Ask third party: Vragen om een derde te kiezen uit de deelnemers aan de afspraken
Third party label: Label om de derde te kiezen
Destinee: Geadresseerde
None: Geen keuze
title of the generated document: Titel van het gegenereerde document
CHILL_CALENDAR_CALENDAR_CREATE: Afspraken aanmaken
CHILL_CALENDAR_CALENDAR_EDIT: Afspraken bewerken
CHILL_CALENDAR_CALENDAR_DELETE: Afspraken verwijderen
CHILL_CALENDAR_CALENDAR_SEE: Afspraken bekijken
generic_doc:
filter:
keys:
accompanying_period_calendar_document: Document van afspraken van trajecten
person_calendar_document: Document van afspraken van de gebruiker

View File

@@ -0,0 +1,6 @@
calendar:
At least {{ limit }} person is required.: Minimaal {{ limit }} persoon is vereist voor deze afspraak
An end date is required: Geef een einddatum en -tijd op
A start date is required: Geef een startdatum en -tijd op
A location is required: Geef een locatie op
A main user is mandator: Geef een hoofdgebruiker op