Merge pull request #5168 from christianbeeznest/ofaj-21101-7

Social: Improve user profile interface with extra information display - refs BT#21101
pull/5169/head
christianbeeznest 2 years ago committed by GitHub
commit 93cd0311ec
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 32
      assets/css/scss/_social.scss
  2. 124
      assets/vue/components/social/UserProfileCard.vue
  3. 119
      src/CoreBundle/Controller/SocialController.php
  4. 22
      src/CoreBundle/Entity/ExtraField.php
  5. 3
      src/CoreBundle/Entity/TrackEOnline.php
  6. 29
      src/CoreBundle/Repository/ExtraFieldOptionsRepository.php
  7. 38
      src/CoreBundle/Repository/ExtraFieldRepository.php
  8. 187
      src/CoreBundle/Repository/Node/UserRepository.php

@ -745,3 +745,35 @@
margin-left: 5px;
vertical-align: middle;
}
.user-profile-card {
.extra-info-container {
margin-top: 0;
background: #f5f5f5;
padding: 0;
border-radius: 0.5rem;
text-align: left;
}
.extra-info-list {
list-style: none;
padding: 0;
}
.extra-info-list dt {
font-weight: bold;
}
.extra-info-list dd {
margin: 0;
margin-bottom: 0.5rem;
}
.language-target {
margin-top: 1rem;
}
.p-card .p-card-body {
padding: 1px;
}
}

@ -1,58 +1,120 @@
<template>
<BaseCard plain>
<div class="p-4 text-center">
<img
:src="user.illustrationUrl"
class="mb-4 w-24 h-24 mx-auto rounded-full"
alt="Profile picture"
/>
<div class="text-xl font-bold">{{ user.fullName }}</div>
<div class="text-lg">{{ user.username }}</div>
<div class="p-4 text-center user-profile-card">
<img :src="user.illustrationUrl" class="mb-4 w-24 h-24 mx-auto rounded-full" alt="Profile picture" />
<div v-if="visibility.firstname && visibility.lastname" class="text-xl font-bold">{{ user.fullName }}</div>
<div v-if="visibility.language && languageInfo">
<template v-if="flagIconExists(languageInfo.code)">
<i :class="`mdi mdi-flag-${languageInfo.code.toLowerCase()}`"></i>
</template>
<template v-else>
{{ languageInfo.code }}
</template>
</div>
<div class="mt-4">
<p class="flex items-center justify-center mb-2">
<i class="mdi mdi-email-outline mr-2"></i>
{{ user.email }}
<p v-if="showFullProfile" class="flex items-center justify-center mb-2">
<a v-if="visibility.email && user.email" :href="'/resources/messages/new'" class="flex items-center justify-center mb-2">
<i class="mdi mdi-email-outline mr-2"></i> {{ user.email }}
</a>
</p>
<p class="flex items-center justify-center mb-2">
<i class="mdi mdi-card-account-details-outline mr-2"></i>
{{ t('Business card') }}
<p v-if="vCardUserLink" class="flex items-center justify-center mb-2">
<a :href="vCardUserLink" target="_blank" class="flex items-center justify-center">
<i class="mdi mdi-card-account-details-outline mr-2"></i> {{ t('Business card') }}
</a>
</p>
<p class="flex items-center justify-center mb-2">
<i class="mdi mdi-skype mr-2"></i>
Skype
<p v-if="user.skype" class="flex items-center justify-center mb-2">
<i class="mdi mdi-skype mr-2"></i> Skype: {{ user.skype }}
</p>
<p class="flex items-center justify-center">
<i class="mdi mdi-linkedin mr-2"></i>
LinkedIn
<p v-if="user.linkedin" class="flex items-center justify-center">
<i class="mdi mdi-linkedin mr-2"></i> LinkedIn: {{ user.linkedin }}
</p>
</div>
<BaseButton
v-if="isCurrentUser"
:label="t('Edit profile')"
type="primary"
class="mt-4"
@click="editProfile"
icon="edit"
/>
<hr />
<div v-if="extraInfo && extraInfo.length > 0" class="extra-info-container">
<dl class="extra-info-list">
<template v-for="item in extraInfo" :key="item.variable">
<div v-if="item.value">
<dt v-if="item.variable !== 'langue_cible'">{{ item.label }}:</dt>
<dd v-if="item.variable !== 'langue_cible'">{{ item.value }}</dd>
<div v-if="item.variable === 'langue_cible'" class="language-target">
<i v-if="flagIconExists(item.value)" :class="`flag-icon flag-icon-${item.value.toLowerCase()}`"></i>
</div>
</div>
</template>
</dl>
</div>
<div v-if="chatEnabled && isUserOnline && !userOnlyInChat">
<button @click="chatWith(user.id, user.fullName, user.isOnline, user.illustrationUrl)">
{{ t('Chat') }} ({{ t('Online') }})
</button>
</div>
<Divider />
<BaseButton v-if="isCurrentUser || isAdmin" :label="t('Edit profile')" type="primary" class="mt-4" @click="editProfile" icon="edit" />
</div>
</BaseCard>
</template>
<script setup>
import { computed, inject, onMounted, ref, watch } from "vue"
import { computed, inject, onMounted, ref, watchEffect } from "vue"
import { useStore } from 'vuex'
import BaseCard from "../basecomponents/BaseCard.vue"
import BaseButton from "../basecomponents/BaseButton.vue"
import { useI18n } from "vue-i18n"
import { useRoute } from "vue-router"
import Divider from 'primevue/divider'
import axios from "axios"
const { t } = useI18n()
const store = useStore()
const route = useRoute()
const user = inject('social-user')
const isCurrentUser = inject('is-current-user')
const isAdmin = ref(false)
const extraInfo = ref([])
const chatEnabled = ref(true)
const isUserOnline = ref(false)
const userOnlyInChat = ref(false)
const showFullProfile = computed(() => isCurrentUser.value || isAdmin.value)
const languageInfo = ref(null)
const vCardUserLink = ref('')
const visibility = ref({})
watchEffect(() => {
if (user.value && user.value.id) {
fetchUserProfile(user.value.id)
}
})
const editProfile = () => {
window.location = "/account/edit"
}
async function fetchUserProfile(userId) {
try {
const response = await axios.get(`/social-network/user-profile/${userId}`)
const data = response.data
languageInfo.value = data.language
vCardUserLink.value = data.vCardUserLink
visibility.value = data.visibility
extraInfo.value = data.extraFields
isUserOnline.value = data.isUserOnline
userOnlyInChat.value = data.userOnlyInChat
chatEnabled.value = data.chatEnabled
} catch (error) {
console.error('Error fetching user profile data:', error)
}
}
function flagIconExists(code) {
const mdiFlagIcons = ['us', 'fr', 'de', 'es', 'it', 'pl']
return mdiFlagIcons.includes(code.toLowerCase())
}
function chatWith(userId, completeName, isOnline, avatarSmall) {
}
isAdmin.value = user.value.role === 'admin'
</script>

@ -6,12 +6,16 @@ declare(strict_types=1);
namespace Chamilo\CoreBundle\Controller;
use Chamilo\CoreBundle\Entity\ExtraField;
use Chamilo\CoreBundle\Entity\User;
use Chamilo\CoreBundle\Repository\ExtraFieldOptionsRepository;
use Chamilo\CoreBundle\Repository\ExtraFieldRepository;
use Chamilo\CoreBundle\Repository\LanguageRepository;
use Chamilo\CoreBundle\Repository\LegalRepository;
use Chamilo\CoreBundle\Repository\Node\IllustrationRepository;
use Chamilo\CoreBundle\Repository\Node\UsergroupRepository;
use Chamilo\CoreBundle\Repository\Node\UserRepository;
use Chamilo\CoreBundle\Repository\TrackEOnlineRepository;
use Chamilo\CoreBundle\Serializer\UserToJsonNormalizer;
use Chamilo\CoreBundle\Settings\SettingsManager;
use Chamilo\CourseBundle\Repository\CForumThreadRepository;
@ -327,4 +331,119 @@ class SocialController extends AbstractController
return $this->json(['invitedUsers' => $invitedUsersList]);
}
#[Route('/user-profile/{userId}', name: 'chamilo_core_social_user_profile')]
public function getUserProfile(
int $userId,
SettingsManager $settingsManager,
LanguageRepository $languageRepository,
UserRepository $userRepository,
RequestStack $requestStack,
TrackEOnlineRepository $trackOnlineRepository,
ExtraFieldRepository $extraFieldRepository,
ExtraFieldOptionsRepository $extraFieldOptionsRepository
): JsonResponse {
$user = $userRepository->find($userId);
if (!$user) {
return $this->json(['error' => 'User not found'], Response::HTTP_NOT_FOUND);
}
$baseUrl = $requestStack->getCurrentRequest()->getBaseUrl();
$profileFieldsVisibilityJson = $settingsManager->getSetting('profile.profile_fields_visibility');
$profileFieldsVisibility = json_decode($profileFieldsVisibilityJson, true)['options'] ?? [];
$vCardUserLink = $profileFieldsVisibility['vcard'] ?? true ? $baseUrl.'/main/social/vcard_export.php?userId='.intval($userId) : '';
$languageInfo = null;
if ($profileFieldsVisibility['language'] ?? true) {
$language = $languageRepository->findByIsoCode($user->getLocale());
if ($language) {
$languageInfo = [
'label' => $language->getOriginalName(),
'value' => $language->getEnglishName(),
'code' => $language->getIsocode(),
];
}
}
$isUserOnline = $trackOnlineRepository->isUserOnline($userId);
$userOnlyInChat = $this->checkUserStatus($userId, $userRepository);
$extraFields = $this->getExtraFieldBlock($userId, $userRepository, $settingsManager, $extraFieldRepository, $extraFieldOptionsRepository);
$response = [
'vCardUserLink' => $vCardUserLink,
'language' => $languageInfo,
'visibility' => $profileFieldsVisibility,
'isUserOnline' => $isUserOnline,
'userOnlyInChat' => $userOnlyInChat,
'extraFields' => $extraFields,
];
return $this->json($response);
}
private function getExtraFieldBlock(
int $userId,
UserRepository $userRepository,
SettingsManager $settingsManager,
ExtraFieldRepository $extraFieldRepository,
ExtraFieldOptionsRepository $extraFieldOptionsRepository
): array {
$user = $userRepository->find($userId);
if (!$user) {
return [];
}
$fieldVisibilityConfig = $settingsManager->getSetting('profile.profile_fields_visibility');
$fieldVisibility = $fieldVisibilityConfig ? json_decode($fieldVisibilityConfig, true)['options'] : [];
$extraUserData = $userRepository->getExtraUserData($userId);
$extraFieldsFormatted = [];
foreach ($extraUserData as $key => $value) {
$fieldVariable = str_replace('extra_', '', $key);
$extraField = $extraFieldRepository->getHandlerFieldInfoByFieldVariable($fieldVariable, ExtraField::USER_FIELD_TYPE);
if (!$extraField || !isset($fieldVisibility[$fieldVariable]) || !$fieldVisibility[$fieldVariable]) {
continue;
}
$fieldValue = is_array($value) ? implode(', ', $value) : $value;
switch ($extraField['type']) {
case ExtraField::FIELD_TYPE_RADIO:
case ExtraField::FIELD_TYPE_SELECT:
$extraFieldOptions = $extraFieldOptionsRepository->getFieldOptionByFieldAndOption($extraField['id'], $fieldValue, ExtraField::USER_FIELD_TYPE);
if (!empty($extraFieldOptions)) {
$optionTexts = array_map(function ($option) {
return $option['display_text'];
}, $extraFieldOptions);
$fieldValue = implode(', ', $optionTexts);
}
break;
case ExtraField::FIELD_TYPE_GEOLOCALIZATION_COORDINATES:
case ExtraField::FIELD_TYPE_GEOLOCALIZATION:
$geoData = explode('::', $fieldValue);
$locationName = $geoData[0];
$coordinates = $geoData[1] ?? '';
$fieldValue = $locationName;
break;
}
$extraFieldsFormatted[] = [
'variable' => $fieldVariable,
'label' => $extraField['display_text'],
'value' => $fieldValue,
];
}
return $extraFieldsFormatted;
}
private function checkUserStatus(int $userId, UserRepository $userRepository): bool
{
$userStatus = $userRepository->getExtraUserDataByField($userId, 'user_chat_status');
return !empty($userStatus) && isset($userStatus['user_chat_status']) && (int) $userStatus['user_chat_status'] === 1;
}
}

@ -48,6 +48,28 @@ class ExtraField
public const TRACK_EXERCISE_FIELD_TYPE = 18;
public const PORTFOLIO_TYPE = 19;
public const LP_VIEW_TYPE = 20;
public const USER_FIELD_TYPE_RADIO = 3;
public const USER_FIELD_TYPE_SELECT_MULTIPLE = 5;
public const USER_FIELD_TYPE_TAG = 10;
public const FIELD_TYPE_TEXT = 1;
public const FIELD_TYPE_TEXTAREA = 2;
public const FIELD_TYPE_RADIO = 3;
public const FIELD_TYPE_SELECT = 4;
public const FIELD_TYPE_SELECT_MULTIPLE = 5;
public const FIELD_TYPE_DATE = 6;
public const FIELD_TYPE_DATETIME = 7;
public const FIELD_TYPE_DOUBLE_SELECT = 8;
public const FIELD_TYPE_TAG = 10;
public const FIELD_TYPE_SOCIAL_PROFILE = 12;
public const FIELD_TYPE_CHECKBOX = 13;
public const FIELD_TYPE_INTEGER = 15;
public const FIELD_TYPE_FILE_IMAGE = 16;
public const FIELD_TYPE_FLOAT = 17;
public const FIELD_TYPE_FILE = 18;
public const FIELD_TYPE_GEOLOCALIZATION = 24;
public const FIELD_TYPE_GEOLOCALIZATION_COORDINATES = 25;
#[Groups(['extra_field:read'])]
#[ORM\Column(name: 'id', type: 'integer')]
#[ORM\Id]

@ -6,6 +6,7 @@ declare(strict_types=1);
namespace Chamilo\CoreBundle\Entity;
use Chamilo\CoreBundle\Repository\TrackEOnlineRepository;
use DateTime;
use Doctrine\ORM\Mapping as ORM;
@ -16,7 +17,7 @@ use Doctrine\ORM\Mapping as ORM;
#[ORM\Index(name: 'course', columns: ['c_id'])]
#[ORM\Index(name: 'login_user_id', columns: ['login_user_id'])]
#[ORM\Index(name: 'session_id', columns: ['session_id'])]
#[ORM\Entity]
#[ORM\Entity(repositoryClass: TrackEOnlineRepository::class)]
class TrackEOnline
{
#[ORM\Column(name: 'login_id', type: 'integer')]

@ -23,7 +23,7 @@ class ExtraFieldOptionsRepository extends ServiceEntityRepository
*
* @return array
*/
public function findSecondaryOptions(ExtraFieldOptions $option)
public function findSecondaryOptions(ExtraFieldOptions $option): array
{
$qb = $this->createQueryBuilder('so');
$qb
@ -41,4 +41,31 @@ class ExtraFieldOptionsRepository extends ServiceEntityRepository
->getResult()
;
}
public function getFieldOptionByFieldAndOption(int $fieldId, string $optionValue, int $itemType): array
{
$qb = $this->createQueryBuilder('o');
$qb->innerJoin('o.field', 'f')
->where('o.field = :fieldId')
->andWhere('o.value = :optionValue')
->andWhere('f.itemType = :itemType')
->setParameters([
'fieldId' => $fieldId,
'optionValue' => $optionValue,
'itemType' => $itemType,
]);
$result = $qb->getQuery()->getResult();
$options = [];
foreach ($result as $option) {
$options[] = [
'id' => $option->getId(),
'value' => $option->getValue(),
'display_text' => $option->getDisplayText(),
];
}
return $options;
}
}

@ -7,6 +7,7 @@ declare(strict_types=1);
namespace Chamilo\CoreBundle\Repository;
use Chamilo\CoreBundle\Entity\ExtraField;
use Chamilo\CoreBundle\Entity\ExtraFieldOptions;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
@ -20,7 +21,7 @@ class ExtraFieldRepository extends ServiceEntityRepository
/**
* @return ExtraField[]
*/
public function getExtraFields(int $type)
public function getExtraFields(int $type): array
{
$qb = $this->createQueryBuilder('f');
$qb
@ -34,4 +35,39 @@ class ExtraFieldRepository extends ServiceEntityRepository
return $qb->getQuery()->getResult();
}
public function getHandlerFieldInfoByFieldVariable(string $variable, int $itemType): bool|array
{
$extraField = $this->findOneBy([
'variable' => $variable,
'itemType' => $itemType,
]);
if (!$extraField) {
return false;
}
$fieldInfo = [
'id' => $extraField->getId(),
'variable' => $extraField->getVariable(),
'display_text' => $extraField->getDisplayText(),
'type' => $extraField->getValueType(),
'options' => [],
];
$options = $this->_em->getRepository(ExtraFieldOptions::class)->findBy([
'field' => $extraField,
]);
foreach ($options as $option) {
$fieldInfo['options'][$option->getId()] = [
'id' => $option->getId(),
'value' => $option->getValue(),
'display_text' => $option->getDisplayText(),
'option_order' => $option->getOptionOrder(),
];
}
return $fieldInfo;
}
}

@ -8,6 +8,8 @@ namespace Chamilo\CoreBundle\Repository\Node;
use Chamilo\CoreBundle\Entity\AccessUrl;
use Chamilo\CoreBundle\Entity\Course;
use Chamilo\CoreBundle\Entity\ExtraField;
use Chamilo\CoreBundle\Entity\ExtraFieldValues;
use Chamilo\CoreBundle\Entity\Message;
use Chamilo\CoreBundle\Entity\ResourceNode;
use Chamilo\CoreBundle\Entity\Session;
@ -273,37 +275,6 @@ class UserRepository extends ResourceRepository implements PasswordUpgraderInter
return $query->getResult();
}
/*
public function getTeachers()
{
$queryBuilder = $this->repository->createQueryBuilder('u');
// Selecting course info.
$queryBuilder
->select('u')
->where('u.groups.id = :groupId')
->setParameter('groupId', 1);
$query = $queryBuilder->getQuery();
return $query->execute();
}*/
/*public function getUsers($group)
{
$queryBuilder = $this->repository->createQueryBuilder('u');
// Selecting course info.
$queryBuilder
->select('u')
->where('u.groups = :groupId')
->setParameter('groupId', $group);
$query = $queryBuilder->getQuery();
return $query->execute();
}*/
/**
* Get the coaches for a course within a session.
*
@ -332,68 +303,6 @@ class UserRepository extends ResourceRepository implements PasswordUpgraderInter
return $qb->getQuery()->getResult();
}
/**
* Get course user relationship based in the course_rel_user table.
*
* @return array
*/
/*public function getCourses(User $user)
{
$qb = $this->createQueryBuilder('user');
// Selecting course info.
$qb->select('c');
// Loading User.
//$qb->from('Chamilo\CoreBundle\Entity\User', 'u');
// Selecting course
$qb->innerJoin('Chamilo\CoreBundle\Entity\Course', 'c');
//@todo check app settings
//$qb->add('orderBy', 'u.lastname ASC');
$wherePart = $qb->expr()->andx();
// Get only users subscribed to this course
$wherePart->add($qb->expr()->eq('user.userId', $user->getUserId()));
$qb->where($wherePart);
$query = $qb->getQuery();
return $query->execute();
}
public function getTeachers()
{
$qb = $this->createQueryBuilder('u');
// Selecting course info.
$qb
->select('u')
->where('u.groups.id = :groupId')
->setParameter('groupId', 1);
$query = $qb->getQuery();
return $query->execute();
}*/
/*public function getUsers($group)
{
$qb = $this->createQueryBuilder('u');
// Selecting course info.
$qb
->select('u')
->where('u.groups = :groupId')
->setParameter('groupId', $group);
$query = $qb->getQuery();
return $query->execute();
}*/
/**
* Get the sessions admins for a user.
*
@ -726,4 +635,96 @@ class UserRepository extends ResourceRepository implements PasswordUpgraderInter
return $query->getResult();
}
public function getExtraUserData(int $userId, bool $prefix = false, bool $allVisibility = true, bool $splitMultiple = false, ?int $fieldFilter = null): array
{
$qb = $this->getEntityManager()->createQueryBuilder();
// Start building the query
$qb->select('ef.id', 'ef.variable as fvar', 'ef.valueType as type', 'efv.fieldValue as fval', 'ef.defaultValue as fval_df')
->from(ExtraField::class, 'ef')
->leftJoin(ExtraFieldValues::class, 'efv', Join::WITH, 'efv.field = ef.id AND efv.itemId = :userId')
->where('ef.itemType = :itemType')
->setParameter('userId', $userId)
->setParameter('itemType', ExtraField::USER_FIELD_TYPE);
// Apply visibility filters
if (!$allVisibility) {
$qb->andWhere('ef.visibleToSelf = true');
}
// Apply field filter if provided
if (null !== $fieldFilter) {
$qb->andWhere('ef.id = :fieldFilter')
->setParameter('fieldFilter', $fieldFilter);
}
// Order by field order
$qb->orderBy('ef.fieldOrder', 'ASC');
// Execute the query
$results = $qb->getQuery()->getResult();
// Process results
$extraData = [];
foreach ($results as $row) {
$value = $row['fval'] ?? $row['fval_df'];
// Handle multiple values if necessary
if ($splitMultiple && in_array($row['type'], [ExtraField::USER_FIELD_TYPE_SELECT_MULTIPLE], true)) {
$value = explode(';', $value);
}
// Handle prefix if needed
$key = $prefix ? 'extra_' . $row['fvar'] : $row['fvar'];
// Special handling for certain field types
if ($row['type'] == ExtraField::USER_FIELD_TYPE_TAG) {
// Implement your logic to handle tags
} elseif ($row['type'] == ExtraField::USER_FIELD_TYPE_RADIO && $prefix) {
$extraData[$key][$key] = $value;
} else {
$extraData[$key] = $value;
}
}
return $extraData;
}
public function getExtraUserDataByField(int $userId, string $fieldVariable, bool $allVisibility = true): array
{
$qb = $this->getEntityManager()->createQueryBuilder();
$qb->select('e.id, e.variable, e.valueType, v.fieldValue')
->from(ExtraFieldValues::class, 'v')
->innerJoin('v.field', 'e')
->where('v.itemId = :userId')
->andWhere('e.variable = :fieldVariable')
->andWhere('e.itemType = :itemType')
->setParameters([
'userId' => $userId,
'fieldVariable' => $fieldVariable,
'itemType' => ExtraField::USER_FIELD_TYPE,
]);
if (!$allVisibility) {
$qb->andWhere('e.visibleToSelf = true');
}
$qb->orderBy('e.fieldOrder', 'ASC');
$result = $qb->getQuery()->getResult();
$extraData = [];
foreach ($result as $row) {
$value = $row['fieldValue'];
if (ExtraField::USER_FIELD_TYPE_SELECT_MULTIPLE == $row['valueType']) {
$value = explode(';', $row['fieldValue']);
}
$extraData[$row['variable']] = $value;
}
return $extraData;
}
}

Loading…
Cancel
Save