Social: Add more social group UI enhancements and API integration - refs BT#21101

pull/5156/head
christianbeeznst 9 months ago
parent de7f67d310
commit c065f0e549
  1. 119
      assets/css/scss/_social.scss
  2. 8
      assets/vue/components/social/SocialGroupMenu.vue
  3. 4
      assets/vue/components/usergroup/GroupDiscussions.vue
  4. 45
      assets/vue/components/usergroup/GroupMembers.vue
  5. 13
      assets/vue/composables/useSocialInfo.js
  6. 6
      assets/vue/router/usergroup.js
  7. 112
      assets/vue/views/usergroup/Invite.vue
  8. 62
      src/CoreBundle/Controller/SocialController.php
  9. 39
      src/CoreBundle/DataProvider/GroupMembersDataProvider.php
  10. 8
      src/CoreBundle/Entity/Usergroup.php
  11. 31
      src/CoreBundle/Repository/Node/UserRepository.php
  12. 70
      src/CoreBundle/Repository/Node/UsergroupRepository.php

@ -626,3 +626,122 @@
border-top: none;
}
}
.invite-friends {
.invite-friends-container {
max-width: 600px;
margin: auto;
}
.invite-friends-header {
text-align: center;
margin-bottom: 20px;
}
.invite-friends-body {
display: flex;
justify-content: space-between;
margin-bottom: 20px;
}
.friends-list, .selected-friends-list {
width: 48%;
}
.list-header {
background-color: #f5f5f5;
padding: 10px;
border-radius: 5px;
}
.list-content {
border: 1px solid #ddd;
border-radius: 5px;
padding: 10px;
height: 300px;
overflow-y: auto;
}
.friend-entry {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
}
.friend-avatar {
border-radius: 50%;
width: 30px;
height: 30px;
margin-right: 10px;
}
.friend-info {
display: flex;
align-items: center;
}
.invite-btn, .remove-btn {
border: none;
background-color: #5cb85c;
color: white;
padding: 5px 10px;
border-radius: 5px;
cursor: pointer;
}
.remove-btn {
background-color: #d9534f;
}
.send-invites-btn {
width: 100%;
padding: 10px 20px;
background-color: #0275d8;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
.send-invites-btn:hover {
background-color: #025aa5;
}
.invited-users-container {
margin-top: 20px;
}
.invited-users-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 10px;
}
.user-card {
display: flex;
flex-direction: column;
align-items: center;
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
background-color: #f9f9f9;
}
.user-avatar {
border-radius: 50%;
width: 50px;
height: 50px;
margin-bottom: 10px;
}
.user-name {
text-align: center;
}
}
.admin-icon {
color: gold;
margin-left: 5px;
vertical-align: middle;
}

@ -13,12 +13,6 @@
{{ t("Home") }}
</router-link>
</li>
<li class="menu-item">
<router-link :to="{ name: '', params: { group_id: groupInfo.id } }">
<i class="mdi mdi-account-edit" aria-hidden="true"></i>
{{ t("Edit this group") }}
</router-link>
</li>
<li class="menu-item">
<router-link :to="{ name: '', params: { group_id: groupInfo.id } }">
<i class="mdi mdi-account-multiple-outline" aria-hidden="true"></i>
@ -26,7 +20,7 @@
</router-link>
</li>
<li class="menu-item">
<router-link :to="{ name: '', params: { group_id: groupInfo.id } }">
<router-link :to="{ name: 'UserGroupInvite', params: { group_id: groupInfo.id } }">
<i class="mdi mdi-account-plus" aria-hidden="true"></i>
{{ t("Invite friends") }}
</router-link>

@ -3,7 +3,7 @@
<div class="discussions-header">
<h2>Discussions</h2>
<a :href="threadCreationUrl" class="btn btn-primary create-thread-btn">
<i class="pi pi-plus"></i> Create thread
<i class="pi pi-plus"></i> {{ t("Create thread") }}
</a>
</div>
<div class="discussion-item" v-for="discussion in discussions" :key="discussion.id">
@ -11,7 +11,7 @@
<div class="discussion-title" v-html="discussion.title"></div>
<div class="discussion-details">
<i class="mdi mdi-message-reply-text icon"></i>
<span>{{ discussion.repliesCount }} Replies</span>
<span>{{ discussion.repliesCount }} {{ t("Replies") }}</span>
<i class="mdi mdi-clock-outline icon"></i>
<span>Created {{ new Date(discussion.sendDate).toLocaleDateString() }}</span>
</div>

@ -12,10 +12,13 @@
<div class="members-grid">
<div class="member-card" v-for="member in members" :key="member.id">
<div class="member-avatar">
<img v-if="member.image" :src="member.image" alt="Member avatar">
<img v-if="member.avatar" :src="member.avatar" alt="Member avatar">
<i v-else class="mdi mdi-account-circle-outline"></i>
</div>
<div class="member-name">{{ member.name }}</div>
<div class="member-name">
{{ member.name }}
<i v-if="member.isAdmin" class="mdi mdi-star-outline admin-icon"></i>
</div>
<div class="member-role" v-if="member.role">{{ member.role }}</div>
</div>
</div>
@ -23,24 +26,38 @@
</template>
<script setup>
import { ref, watchEffect } from 'vue'
import { ref, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import BaseButton from "../basecomponents/BaseButton.vue"
import axios from "axios"
const route = useRoute()
//const members = ref([])
const members = ref([
{ name: 'John Doe', role: 'Admin', avatar: '/path/to/avatar.jpg' },
{ name: 'Jane Doe', role: 'Miembro', avatar: '/path/to/avatar2.jpg' },
// ...
])
const members = ref([])
const groupId = ref(route.params.group_id)
const fetchMembers = async (groupId) => {
if (groupId.value) {
try {
const response = await axios.get(`/api/usergroups/${groupId.value}/members`)
members.value = response.data['hydra:member'].map(member => ({
id: member.id,
name: member.username,
role: member.relationType === 1 ? 'Admin' : 'Member',
avatar: null,
isAdmin: member.relationType === 1
}))
} catch (error) {
console.error('Error fetching group members:', error)
members.value = []
}
}
}
const editMembers = () => {
}
/*
watchEffect(() => {
const groupId = route.query.group_id
onMounted(() => {
if (groupId) {
// members.value = fetchMembers(groupId)
fetchMembers(groupId)
}
});*/
})
</script>

@ -19,7 +19,14 @@ export function useSocialInfo() {
if (groupId) {
try {
const response = await axios.get(`/api/usergroup/${groupId}`);
groupInfo.value = response.data;
const groupData = response.data;
const extractedId = groupData['@id'].split('/').pop();
groupInfo.value = {
...groupData,
id: extractedId
};
isGroup.value = true;
} catch (error) {
console.error("Error loading group:", error);
@ -50,9 +57,9 @@ export function useSocialInfo() {
onMounted(async () => {
try {
if (!route.params.group_id) {
//if (!route.params.group_id) {
await loadUser();
}
//}
if (route.params.group_id) {
await loadGroup(route.params.group_id);
}

@ -21,6 +21,12 @@ export default {
path: 'search',
component: () => import('../views/usergroup/Search.vue'),
props: (route) => ({ q: route.query.q })
},
{
name: 'UserGroupInvite',
path: 'invite/:group_id?',
component: () => import('../views/usergroup/Invite.vue'),
props: true
}
]
};

@ -0,0 +1,112 @@
<template>
<div class="invite-friends-container invite-friends">
<div class="invite-friends-header">
<h2>{{ t('Invite Friends to Group') }}</h2>
</div>
<div class="invite-friends-body">
<div class="friends-list">
<div class="list-header">
<h3>{{ t('Available Friends') }}</h3>
</div>
<div class="list-content">
<div class="friend-entry" v-for="friend in availableFriends" :key="friend.id">
<div class="friend-info">
<img :src="friend.avatar" alt="avatar" class="friend-avatar" />
<span class="friend-name">{{ friend.name }}</span>
</div>
<button @click="selectFriend(friend)" class="invite-btn">+</button>
</div>
</div>
</div>
<div class="selected-friends-list">
<div class="list-header">
<h3>{{ t('Selected Friends') }}</h3>
</div>
<div class="list-content">
<div class="friend-entry" v-for="friend in selectedFriends" :key="friend.id">
<div class="friend-info">
<img :src="friend.avatar" alt="avatar" class="friend-avatar" />
<span class="friend-name">{{ friend.name }}</span>
</div>
<button @click="removeFriend(friend)" class="remove-btn">-</button>
</div>
</div>
</div>
</div>
<div class="invite-friends-footer">
<button @click="sendInvitations" class="send-invites-btn">{{ t('Send Invitations') }}</button>
</div>
<div class="invited-friends-list mt-4">
<div class="list-header">
<h3>{{ t('Users Already Invited') }}</h3>
</div>
<div class="invited-users-grid mt-4">
<div class="user-card" v-for="user in invitedFriends" :key="user.id">
<img :src="user.avatar" alt="avatar" class="user-avatar" />
<span class="user-name">{{ user.name }}</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { inject, onMounted, ref, computed} from "vue"
import { useI18n } from 'vue-i18n'
import { useRoute } from 'vue-router'
import { useStore } from "vuex"
import axios from "axios"
const { t } = useI18n()
const route = useRoute()
const store = useStore()
const currentUser = computed(() => store.getters["security/getUser"])
const availableFriends = ref([])
const selectedFriends = ref([])
const invitedFriends = ref([])
const selectFriend = (friend) => {
availableFriends.value = availableFriends.value.filter((f) => f.id !== friend.id)
selectedFriends.value.push(friend)
}
const removeFriend = (friend) => {
selectedFriends.value = selectedFriends.value.filter((f) => f.id !== friend.id)
availableFriends.value.push(friend)
}
const loadAvailableFriends = async () => {
const groupId = route.params.group_id
const userId = currentUser.value.id
try {
const response = await axios.get(`/social-network/invite-friends/${userId}/${groupId}`)
availableFriends.value = response.data.friends
} catch (error) {
console.error('Error loading available friends:', error)
}
}
onMounted(() => {
loadAvailableFriends()
loadInvitedFriends()
})
const sendInvitations = async () => {
const groupId = route.params.group_id
const userIds = selectedFriends.value.map(friend => friend.id)
try {
await axios.post(`/social-network/add-users-to-group/${groupId}`, {
userIds,
})
console.log('Users added to group successfully!')
selectedFriends.value = []
loadInvitedFriends()
} catch (error) {
console.error('Error adding users to group:', error)
}
}
const loadInvitedFriends = async () => {
const groupId = route.params.group_id
try {
const response = await axios.get(`/social-network/group/${groupId}/invited-users`)
invitedFriends.value = response.data.invitedUsers
} catch (error) {
console.error('Error loading invited friends:', error)
}
}
</script>

@ -11,6 +11,7 @@ use Chamilo\CoreBundle\Entity\Usergroup;
use Chamilo\CoreBundle\Repository\LanguageRepository;
use Chamilo\CoreBundle\Repository\LegalRepository;
use Chamilo\CoreBundle\Repository\Node\CourseRepository;
use Chamilo\CoreBundle\Repository\Node\IllustrationRepository;
use Chamilo\CoreBundle\Repository\Node\UsergroupRepository;
use Chamilo\CoreBundle\Repository\Node\UserRepository;
use Chamilo\CoreBundle\Serializer\UserToJsonNormalizer;
@ -22,6 +23,7 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Email;
use Symfony\Component\Routing\Annotation\Route;
@ -272,21 +274,59 @@ class SocialController extends AbstractController
return $this->json(['go_to' => $goToLink]);
}
/**
* @Route("/upload-group-picture/{id}", name="chamilo_core_social_upload_group_picture", methods={"POST"})
*/
#[Route('/upload-group-picture/{id}', name: 'chamilo_core_social_upload_group_picture')]
public function uploadGroupPicture(Request $request, Usergroup $usergroup)
#[Route('/invite-friends/{userId}/{groupId}', name: 'chamilo_core_social_invite_friends')]
public function inviteFriends(int $userId, int $groupId, UserRepository $userRepository, UsergroupRepository $usergroupRepository, IllustrationRepository $illustrationRepository): JsonResponse
{
$file = $request->files->get('picture');
if ($file) {
$user = $userRepository->find($userId);
if (!$user) {
return $this->json(['error' => 'User not found'], Response::HTTP_NOT_FOUND);
}
$group = $usergroupRepository->find($groupId);
if (!$group) {
return $this->json(['error' => 'Group not found'], Response::HTTP_NOT_FOUND);
}
return $this->json([
'success' => true,
'message' => 'Imagen del grupo actualizada correctamente.',
]);
$friends = $userRepository->getFriendsNotInGroup($userId, $groupId);
$friendsList = array_map(function ($friend) use ($illustrationRepository) {
return [
'id' => $friend->getId(),
'name' => $friend->getFirstName() . ' ' . $friend->getLastName(),
'avatar' => $illustrationRepository->getIllustrationUrl($friend),
];
}, $friends);
return $this->json(['friends' => $friendsList]);
}
#[Route('/add-users-to-group/{groupId}', name: 'chamilo_core_social_add_users_to_group')]
public function addUsersToGroup(Request $request, int $groupId, UsergroupRepository $usergroupRepository): JsonResponse
{
$data = json_decode($request->getContent(), true);
$userIds = $data['userIds'] ?? [];
try {
$usergroupRepository->addUserToGroup($userIds, $groupId);
return $this->json(['success' => true, 'message' => 'Users added to group successfully.']);
} catch (\Exception $e) {
return $this->json(['success' => false, 'message' => 'An error occurred: ' . $e->getMessage()], Response::HTTP_BAD_REQUEST);
}
}
#[Route('/group/{groupId}/invited-users', name: 'chamilo_core_social_group_invited_users')]
public function groupInvitedUsers(int $groupId, UsergroupRepository $usergroupRepository, IllustrationRepository $illustrationRepository): JsonResponse
{
$invitedUsers = $usergroupRepository->getInvitedUsersByGroup($groupId);
$invitedUsersList = array_map(function ($user) use ($illustrationRepository) {
return [
'id' => $user['id'],
'name' => $user['username'],
// 'avatar' => $illustrationRepository->getIllustrationUrl($user),
];
}, $invitedUsers);
return $this->json(['invitedUsers' => $invitedUsersList]);
}
}

@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace Chamilo\CoreBundle\DataProvider;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface;
use Chamilo\CoreBundle\Entity\Usergroup;
use Doctrine\ORM\EntityManagerInterface;
final class GroupMembersDataProvider implements ProviderInterface
{
private EntityManagerInterface $entityManager;
public function __construct(EntityManagerInterface $entityManager)
{
$this->entityManager = $entityManager;
}
public function supports(Operation $operation, array $uriVariables = [], array $context = []): bool
{
return Usergroup::class === $operation->getClass() && 'get_group_members' === $operation->getName();
}
public function provide(Operation $operation, array $uriVariables = [], array $context = []): iterable
{
$groupId = $uriVariables['id'] ?? null;
if (null === $groupId) {
return [];
}
$usergroupRepository = $this->entityManager->getRepository(Usergroup::class);
$users = $usergroupRepository->getUsersByGroup((int)$groupId);
return $users;
}
}

@ -12,6 +12,7 @@ use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\Put;
use Chamilo\CoreBundle\DataProvider\GroupMembersDataProvider;
use Chamilo\CoreBundle\DataProvider\UsergroupDataProvider;
use Chamilo\CoreBundle\Repository\Node\UsergroupRepository;
use Chamilo\CoreBundle\State\UsergroupPostProcessor;
@ -61,6 +62,13 @@ use Symfony\Component\Validator\Constraints as Assert;
security: "is_granted('ROLE_USER')",
name: 'search_usergroups'
),
new GetCollection(
uriTemplate: '/usergroups/{id}/members',
normalizationContext: ['groups' => ['usergroup:read']],
security: "is_granted('ROLE_USER')",
name: 'get_group_members',
provider: GroupMembersDataProvider::class
),
new Post(
securityPostDenormalize: "is_granted('CREATE', object)",
processor: UsergroupPostProcessor::class

@ -14,6 +14,8 @@ use Chamilo\CoreBundle\Entity\Session;
use Chamilo\CoreBundle\Entity\TrackELogin;
use Chamilo\CoreBundle\Entity\TrackEOnline;
use Chamilo\CoreBundle\Entity\User;
use Chamilo\CoreBundle\Entity\Usergroup;
use Chamilo\CoreBundle\Entity\UsergroupRelUser;
use Chamilo\CoreBundle\Entity\UserRelUser;
use Chamilo\CoreBundle\Repository\ResourceRepository;
use Datetime;
@ -694,4 +696,33 @@ class UserRepository extends ResourceRepository implements PasswordUpgraderInter
return $qb;
}
public function getFriendsNotInGroup(int $userId, int $groupId)
{
$entityManager = $this->getEntityManager();
$subQueryBuilder = $entityManager->createQueryBuilder();
$subQuery = $subQueryBuilder
->select('IDENTITY(ugr.user)')
->from(UsergroupRelUser::class, 'ugr')
->where('ugr.usergroup = :subGroupId')
->andWhere('ugr.relationType IN (:subRelationTypes)')
->getDQL();
$queryBuilder = $entityManager->createQueryBuilder();
$query = $queryBuilder
->select('u')
->from(User::class, 'u')
->leftJoin('u.friendsWithMe', 'uruf')
->leftJoin('u.friends', 'urut')
->where('uruf.friend = :userId OR urut.user = :userId')
->andWhere($queryBuilder->expr()->notIn('u.id', $subQuery))
->setParameter('userId', $userId)
->setParameter('subGroupId', $groupId)
->setParameter('subRelationTypes', [Usergroup::GROUP_USER_PERMISSION_PENDING_INVITATION])
->getQuery();
return $query->getResult();
}
}

@ -6,7 +6,9 @@ declare(strict_types=1);
namespace Chamilo\CoreBundle\Repository\Node;
use Chamilo\CoreBundle\Entity\User;
use Chamilo\CoreBundle\Entity\Usergroup;
use Chamilo\CoreBundle\Entity\UsergroupRelUser;
use Chamilo\CoreBundle\Repository\ResourceRepository;
use Doctrine\Persistence\ManagerRegistry;
@ -134,6 +136,74 @@ class UsergroupRepository extends ResourceRepository
return $queryBuilder->getQuery()->getResult();
}
public function getUsersByGroup(int $groupID)
{
$qb = $this->createQueryBuilder('g')
->innerJoin('g.users', 'gu')
->innerJoin('gu.user', 'u')
->where('g.id = :groupID')
->setParameter('groupID', $groupID)
->andWhere('gu.relationType IN (:relationTypes)')
->setParameter('relationTypes', [
Usergroup::GROUP_USER_PERMISSION_ADMIN,
Usergroup::GROUP_USER_PERMISSION_READER,
Usergroup::GROUP_USER_PERMISSION_PENDING_INVITATION
])
->select('u.id, u.username, u.email, gu.relationType');
return $qb->getQuery()->getResult();
}
public function addUserToGroup(array $userIds, int $groupId): void
{
$group = $this->find($groupId);
if (!$group) {
throw new \Exception("Group not found");
}
foreach ($userIds as $userId) {
$user = $this->_em->getRepository(User::class)->find($userId);
if ($user) {
$groupRelUser = new UsergroupRelUser();
$groupRelUser->setUsergroup($group);
$groupRelUser->setUser($user);
$groupRelUser->setRelationType(Usergroup::GROUP_USER_PERMISSION_PENDING_INVITATION);
$this->_em->persist($groupRelUser);
}
}
$this->_em->flush();
}
public function getInvitedUsersByGroup(int $groupID)
{
$qb = $this->createQueryBuilder('g')
->innerJoin('g.users', 'gu')
->innerJoin('gu.user', 'u')
->where('g.id = :groupID')
->setParameter('groupID', $groupID)
->andWhere('gu.relationType = :relationType')
->setParameter('relationType', Usergroup::GROUP_USER_PERMISSION_PENDING_INVITATION)
->select('u.id, u.username, u.email, gu.relationType');
return $qb->getQuery()->getResult();
}
public function getInvitedUsers(int $groupId): array
{
$qb = $this->createQueryBuilder('g')
->innerJoin('g.users', 'rel')
->innerJoin('rel.user', 'u')
->where('g.id = :groupId')
->andWhere('rel.relationType = :relationType')
->setParameter('groupId', $groupId)
->setParameter('relationType', Usergroup::GROUP_USER_PERMISSION_PENDING_INVITATION)
->select('u');
return $qb->getQuery()->getResult();
}
/**
* Determines whether to use the multi-URL feature.
*

Loading…
Cancel
Save