Calendar: allow to subscribe/unsubscribe to personal events + improve info about invitations/subscriptions

pull/5266/head
Angel Fernando Quiroz Campos 2 years ago
parent b1afcb0251
commit 007514dd66
  1. 19
      assets/css/scss/_calendar.scss
  2. 2
      assets/css/scss/index.scss
  3. 17
      assets/vue/components/ccalendarevent/CCalendarEventInfo.vue
  4. 32
      assets/vue/components/ccalendarevent/CalendarEventInvitationsInfo.vue
  5. 69
      assets/vue/components/ccalendarevent/CalendarEventSubscriptionsInfo.vue
  6. 38
      assets/vue/composables/calendar/calendarEvent.js
  7. 6
      assets/vue/services/baseService.js
  8. 5
      assets/vue/services/resourceLinkService.js
  9. 41
      assets/vue/views/ccalendarevent/CCalendarEventList.vue
  10. 2
      src/CoreBundle/ApiResource/CalendarEvent.php
  11. 9
      src/CoreBundle/DataTransformer/CalendarEventTransformer.php
  12. 1
      src/CoreBundle/Entity/ResourceLink.php
  13. 6
      src/CoreBundle/Entity/ResourceNode.php

@ -0,0 +1,19 @@
.calendar-event-info {
@apply flex flex-col space-y-4;
.invitations-info {
@apply space-y-2;
&__title {
@apply text-gray-50 mb-3;
}
&__item {
@apply flex text-body-2 flex-row justify-between;
p {
@apply first:font-semibold;
}
}
}
}

@ -59,7 +59,7 @@
@import 'layout/main_container'; @import 'layout/main_container';
@import "admin_index"; @import "admin_index";
@import "calendar";
@import "course_home"; @import "course_home";
@import "documents"; @import "documents";

@ -1,5 +1,5 @@
<template> <template>
<div class="flex flex-col space-y-4"> <div class="calendar-event-info">
<h5 v-text="event.title" /> <h5 v-text="event.title" />
<p v-text="abbreviatedDatetime(event.startDate)" /> <p v-text="abbreviatedDatetime(event.startDate)" />
@ -13,15 +13,20 @@
<div v-html="event.content" /> <div v-html="event.content" />
<div v-if="allowCollectiveInvitations && type.invitation === event.invitationType"> <CalendarEventSubscriptionsInfo
<h6 v-t="'Invitees'" /> v-if="type.subscription === event.invitationType"
:event="event"
/>
<CalendarEventInvitationsInfo
v-else-if="type.invitation === event.invitationType"
:event="event"
/>
<ShowLinks <ShowLinks
v-else
:item="event" :item="event"
:show-status="false" :show-status="false"
/> />
</div> </div>
</div>
</template> </template>
<script setup> <script setup>
@ -29,6 +34,8 @@ import { useFormatDate } from "../../composables/formatDate"
import ShowLinks from "../resource_links/ShowLinks" import ShowLinks from "../resource_links/ShowLinks"
import { useCalendarInvitations } from "../../composables/calendar/calendarInvitations" import { useCalendarInvitations } from "../../composables/calendar/calendarInvitations"
import { type } from "../../constants/entity/ccalendarevent" import { type } from "../../constants/entity/ccalendarevent"
import CalendarEventSubscriptionsInfo from "./CalendarEventSubscriptionsInfo.vue"
import CalendarEventInvitationsInfo from "./CalendarEventInvitationsInfo.vue"
const { abbreviatedDatetime } = useFormatDate() const { abbreviatedDatetime } = useFormatDate()
const { allowCollectiveInvitations } = useCalendarInvitations() const { allowCollectiveInvitations } = useCalendarInvitations()

@ -0,0 +1,32 @@
<script setup>
import ShowLinks from "../resource_links/ShowLinks.vue"
defineProps({
event: {
type: Object,
required: true,
},
})
</script>
<template>
<div class="invitations-info">
<h6
v-t="'Invitations'"
class="invitations-info__title"
/>
<div
v-if="event.resourceLinkListFromEntity.length"
class="invitations-info__item"
>
<p v-t="'Invitees'" />
<div>
<ShowLinks
:item="event"
:show-status="false"
/>
</div>
</div>
</div>
</template>

@ -0,0 +1,69 @@
<script setup>
import ShowLinks from "../resource_links/ShowLinks.vue"
import { subscriptionVisibility } from "../../constants/entity/ccalendarevent"
defineProps({
event: {
type: Object,
required: true,
},
})
</script>
<template>
<div class="invitations-info">
<h6
v-t="'Subscriptions'"
class="invitations-info__title"
/>
<div class="invitations-info__item">
<p v-t="'Allow subscriptions'" />
<p
v-if="subscriptionVisibility.no === event.subscriptionVisibility"
v-text="'No'"
/>
<p
v-else-if="subscriptionVisibility.all === event.subscriptionVisibility"
v-text="'All system users'"
/>
<p
v-else-if="subscriptionVisibility.class === event.subscriptionVisibility"
v-text="'Users inside the class'"
/>
<p
v-if="subscriptionVisibility.class === event.subscriptionVisibility"
v-text="event.subscriptionItemTitle"
/>
</div>
<div
v-if="event.maxAttendees"
class="invitations-info__item"
>
<p v-t="'Maximum number of subscriptions'" />
<p v-text="event.maxAttendees" />
</div>
<div
v-if="event.maxAttendees"
class="invitations-info__item"
>
<p v-t="'Subscriptions count'" />
<p v-text="event.resourceLinkListFromEntity.length" />
</div>
<div
v-if="event.resourceLinkListFromEntity.length"
class="invitations-info__item"
>
<p v-t="'Subscribers'" />
<div>
<ShowLinks
:item="event"
:show-status="false"
/>
</div>
</div>
</div>
</template>

@ -1,9 +1,13 @@
import { type } from "../../constants/entity/ccalendarevent" import { type, subscriptionVisibility } from "../../constants/entity/ccalendarevent"
export function useCalendarEvent() { export function useCalendarEvent() {
return { return {
findUserLink, findUserLink,
isEditableByUser, isEditableByUser,
isSubscribeable,
canSubscribeToEvent,
allowSubscribeToEvent,
allowUnsubscribeToEvent,
} }
} }
@ -36,3 +40,35 @@ function isEditableByUser(event, userId) {
return false return false
} }
function isSubscribeable(event) {
if (type.subscription !== event.invitationType) {
return false
}
return subscriptionVisibility.no !== event.subscriptionVisibility
}
/**
* @param {Object} event
* @returns {boolean}
*/
function canSubscribeToEvent(event) {
return event.resourceLinkListFromEntity.length < event.maxAttendees || 0 === event.maxAttendees
}
function allowSubscribeToEvent(event) {
if (!isSubscribeable(event)) {
return false
}
return canSubscribeToEvent(event)
}
function allowUnsubscribeToEvent(event, userId) {
if (!isSubscribeable(event)) {
return false
}
return !!findUserLink(event, userId)
}

@ -8,6 +8,8 @@ async function find(iri) {
return await api.get(iri) return await api.get(iri)
} }
export { async function post(params) {
find return await api.post("/api/resource_links", params)
} }
export { find, post }

@ -0,0 +1,5 @@
import { post } from "./baseService"
export default {
post,
}

@ -44,6 +44,7 @@
v-model:visible="dialogShow" v-model:visible="dialogShow"
:header="t('Event')" :header="t('Event')"
:modal="true" :modal="true"
:style="{ width: '30rem' }"
> >
<CCalendarEventInfo :event="item" /> <CCalendarEventInfo :event="item" />
@ -54,6 +55,22 @@
type="black" type="black"
@click="dialogShow = false" @click="dialogShow = false"
/> />
<BaseButton
v-if="allowToUnsubscribe"
:label="t('Unsubscribe')"
type="black"
icon="join-group"
@click="unsubscribeToEvent"
/>
<BaseButton
v-else-if="allowToSubscribe"
:label="t('Subscribe')"
type="black"
icon="join-group"
@click="subscribeToEvent"
/>
<BaseButton <BaseButton
:label="t('Delete')" :label="t('Delete')"
icon="delete" icon="delete"
@ -130,6 +147,7 @@ import { storeToRefs } from "pinia"
import CalendarSectionHeader from "../../components/ccalendarevent/CalendarSectionHeader.vue" import CalendarSectionHeader from "../../components/ccalendarevent/CalendarSectionHeader.vue"
import { useCalendarActionButtons } from "../../composables/calendar/calendarActionButtons" import { useCalendarActionButtons } from "../../composables/calendar/calendarActionButtons"
import { useCalendarEvent } from "../../composables/calendar/calendarEvent" import { useCalendarEvent } from "../../composables/calendar/calendarEvent"
import resourceLinkService from "../../services/resourceLinkService"
const store = useStore() const store = useStore()
const confirm = useConfirm() const confirm = useConfirm()
@ -141,12 +159,14 @@ const { abbreviatedDatetime } = useFormatDate()
const { showAddButton } = useCalendarActionButtons() const { showAddButton } = useCalendarActionButtons()
const { isEditableByUser } = useCalendarEvent() const { isEditableByUser, allowSubscribeToEvent, allowUnsubscribeToEvent } = useCalendarEvent()
const item = ref({}) const item = ref({})
const dialog = ref(false) const dialog = ref(false)
const dialogShow = ref(false) const dialogShow = ref(false)
const allowToEdit = ref(false) const allowToEdit = ref(false)
const allowToSubscribe = ref(false)
const allowToUnsubscribe = ref(false)
const currentUser = computed(() => store.getters["security/getUser"]) const currentUser = computed(() => store.getters["security/getUser"])
const { t } = useI18n() const { t } = useI18n()
@ -248,6 +268,8 @@ const calendarOptions = ref({
item.value["parentResourceNodeId"] = event.extendedProps.resourceNode.creator.id item.value["parentResourceNodeId"] = event.extendedProps.resourceNode.creator.id
allowToEdit.value = isEditableByUser(item.value, currentUser.value.id) allowToEdit.value = isEditableByUser(item.value, currentUser.value.id)
allowToSubscribe.value = !allowToEdit.value && allowSubscribeToEvent(item.value)
allowToUnsubscribe.value = !allowToEdit.value && allowUnsubscribeToEvent(item.value, currentUser.value.id)
dialogShow.value = true dialogShow.value = true
}, },
@ -309,6 +331,23 @@ function confirmDelete() {
}) })
} }
async function subscribeToEvent() {
try {
await resourceLinkService.post({
resourceNode: item.value.resourceNode["@id"],
user: currentUser.value["@id"],
visibility: RESOURCE_LINK_PUBLISHED,
})
allowToSubscribe.value = false
allowToUnsubscribe.value = true
} catch (e) {
console.error(e)
}
}
async function unsubscribeToEvent() {}
const isLoading = computed(() => store.getters["ccalendarevent/isLoading"]) const isLoading = computed(() => store.getters["ccalendarevent/isLoading"])
const createForm = ref(null) const createForm = ref(null)

@ -37,6 +37,8 @@ class CalendarEvent extends AbstractResource
#[Groups(['calendar_event:read'])] #[Groups(['calendar_event:read'])]
public ?int $subscriptionItemId = null, public ?int $subscriptionItemId = null,
#[Groups(['calendar_event:read'])] #[Groups(['calendar_event:read'])]
public ?string $subscriptionItemTitle = null,
#[Groups(['calendar_event:read'])]
public int $maxAttendees = 0, public int $maxAttendees = 0,
#[Groups(['calendar_event:read'])] #[Groups(['calendar_event:read'])]
public ?ResourceNode $resourceNode = null, public ?ResourceNode $resourceNode = null,

@ -10,6 +10,7 @@ use ApiPlatform\Core\DataTransformer\DataTransformerInterface;
use Chamilo\CoreBundle\ApiResource\CalendarEvent; use Chamilo\CoreBundle\ApiResource\CalendarEvent;
use Chamilo\CoreBundle\Entity\Session; use Chamilo\CoreBundle\Entity\Session;
use Chamilo\CoreBundle\Entity\SessionRelCourse; use Chamilo\CoreBundle\Entity\SessionRelCourse;
use Chamilo\CoreBundle\Repository\Node\UsergroupRepository;
use Chamilo\CourseBundle\Entity\CCalendarEvent; use Chamilo\CourseBundle\Entity\CCalendarEvent;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Routing\RouterInterface; use Symfony\Component\Routing\RouterInterface;
@ -18,6 +19,7 @@ class CalendarEventTransformer implements DataTransformerInterface
{ {
public function __construct( public function __construct(
private readonly RouterInterface $router, private readonly RouterInterface $router,
private readonly UsergroupRepository $usergroupRepository,
) {} ) {}
public function transform($object, string $to, array $context = []): object public function transform($object, string $to, array $context = []): object
@ -40,6 +42,12 @@ class CalendarEventTransformer implements DataTransformerInterface
$object->setResourceLinkListFromEntity(); $object->setResourceLinkListFromEntity();
$subscriptionItemTitle = null;
if (CCalendarEvent::SUBSCRIPTION_VISIBILITY_CLASS == $object->getSubscriptionVisibility()) {
$subscriptionItemTitle = $this->usergroupRepository->find($object->getSubscriptionItemId())?->getTitle();
}
return new CalendarEvent( return new CalendarEvent(
'calendar_event_'.$object->getIid(), 'calendar_event_'.$object->getIid(),
$object->getTitle(), $object->getTitle(),
@ -52,6 +60,7 @@ class CalendarEventTransformer implements DataTransformerInterface
$object->isCollective(), $object->isCollective(),
$object->getSubscriptionVisibility(), $object->getSubscriptionVisibility(),
$object->getSubscriptionItemId(), $object->getSubscriptionItemId(),
$subscriptionItemTitle,
$object->getMaxAttendees(), $object->getMaxAttendees(),
$object->getResourceNode(), $object->getResourceNode(),
$object->getResourceLinkListFromEntity(), $object->getResourceLinkListFromEntity(),

@ -259,6 +259,7 @@ class ResourceLink implements Stringable
public function setResourceNode(ResourceNode $resourceNode): self public function setResourceNode(ResourceNode $resourceNode): self
{ {
$this->resourceNode = $resourceNode; $this->resourceNode = $resourceNode;
$this->resourceTypeGroup = $resourceNode->getResourceType()->getId();
return $this; return $this;
} }

@ -469,10 +469,8 @@ class ResourceNode implements Stringable
public function addResourceLink(ResourceLink $link): self public function addResourceLink(ResourceLink $link): self
{ {
$link $link->setResourceNode($this);
->setResourceNode($this)
->setResourceTypeGroup($this->resourceType->getId())
;
$this->resourceLinks->add($link); $this->resourceLinks->add($link);
return $this; return $this;

Loading…
Cancel
Save