mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2026-03-18 03:38:05 +00:00
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:
@@ -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']]);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) }}
|
||||
|
||||
@@ -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) }}
|
||||
|
||||
@@ -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}
|
||||
}
|
||||
|
||||
@@ -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.}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user