Merge pull request #14365 from JoVictorNunes/dashboard-user-details
feat(Dashboard): a new modal for displaying user details
This commit is contained in:
commit
ae840d00eb
@ -12,5 +12,6 @@
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div class="bg-gray-50" id="root"></div>
|
||||
<div id="modal-container"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -7,6 +7,8 @@ import {
|
||||
import { emojiConfigs } from './services/EmojiService';
|
||||
import Card from './components/Card';
|
||||
import UsersTable from './components/UsersTable';
|
||||
import UserDetails from './components/UserDetails/component';
|
||||
import { UserDetailsContext } from './components/UserDetails/context';
|
||||
import StatusTable from './components/StatusTable';
|
||||
import PollsTable from './components/PollsTable';
|
||||
import ErrorMessage from './components/ErrorMessage';
|
||||
@ -135,6 +137,19 @@ class App extends React.Component {
|
||||
return mostUsedEmojis.map(([emoji]) => icons[emoji]);
|
||||
}
|
||||
|
||||
updateModalUser() {
|
||||
const { user, dispatch, isOpen } = this.context;
|
||||
const { activitiesJson } = this.state;
|
||||
const { users } = activitiesJson;
|
||||
|
||||
if (isOpen && users[user.userKey]) {
|
||||
dispatch({
|
||||
type: 'changeUser',
|
||||
user: users[user.userKey],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fetchActivitiesJson() {
|
||||
const {
|
||||
learningDashboardAccessToken, meetingId, sessionToken, invalidSessionCount,
|
||||
@ -151,6 +166,7 @@ class App extends React.Component {
|
||||
lastUpdated: Date.now(),
|
||||
});
|
||||
document.title = `Learning Dashboard - ${json.name}`;
|
||||
this.updateModalUser();
|
||||
}).catch(() => {
|
||||
this.setState({ loading: false, invalidSessionCount: invalidSessionCount + 1 });
|
||||
});
|
||||
@ -168,6 +184,7 @@ class App extends React.Component {
|
||||
lastUpdated: Date.now(),
|
||||
});
|
||||
document.title = `Learning Dashboard - ${jsonData.name}`;
|
||||
this.updateModalUser();
|
||||
} else {
|
||||
// When meeting is ended the sessionToken stop working, check for new cookies
|
||||
this.setDashboardParams();
|
||||
@ -480,6 +497,7 @@ class App extends React.Component {
|
||||
{ tab === 'polling'
|
||||
? <PollsTable polls={activitiesJson.polls} allUsers={activitiesJson.users} />
|
||||
: null }
|
||||
<UserDetails dataJson={activitiesJson} />
|
||||
</div>
|
||||
</div>
|
||||
<hr className="my-8" />
|
||||
@ -525,4 +543,6 @@ class App extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
App.contextType = UserDetailsContext;
|
||||
|
||||
export default injectIntl(App);
|
||||
|
477
bbb-learning-dashboard/src/components/UserDetails/component.jsx
Normal file
477
bbb-learning-dashboard/src/components/UserDetails/component.jsx
Normal file
@ -0,0 +1,477 @@
|
||||
import React, { useContext, useEffect } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import {
|
||||
FormattedMessage, FormattedNumber, FormattedTime, injectIntl,
|
||||
} from 'react-intl';
|
||||
import { UserDetailsContext } from './context';
|
||||
import UserAvatar from '../UserAvatar';
|
||||
import { getSumOfTime, tsToHHmmss, getActivityScore } from '../../services/UserService';
|
||||
|
||||
const UserDatailsComponent = (props) => {
|
||||
const {
|
||||
isOpen, dispatch, user, dataJson, intl,
|
||||
} = props;
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (e) => {
|
||||
if (e.code === 'Escape') dispatch({ type: 'closeModal' });
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handler);
|
||||
return () => window.removeEventListener('keydown', handler);
|
||||
}, []);
|
||||
|
||||
const {
|
||||
createdOn, endedOn, polls, users,
|
||||
} = dataJson;
|
||||
|
||||
const currTime = () => new Date().getTime();
|
||||
|
||||
// Join and left times.
|
||||
const registeredTimes = Object.values(user.intIds).map((intId) => intId.registeredOn);
|
||||
const leftTimes = Object.values(user.intIds).map((intId) => intId.leftOn);
|
||||
const joinTime = Math.min(...registeredTimes);
|
||||
const leftTime = Math.max(...leftTimes);
|
||||
const isOnline = Object.values(user.intIds).some((intId) => intId.leftOn === 0);
|
||||
|
||||
// Used in the calculation of the online loader.
|
||||
const sessionDuration = (endedOn || currTime()) - createdOn;
|
||||
const userStartOffsetTime = ((joinTime - createdOn) * 100) / sessionDuration;
|
||||
const userEndOffsetTime = isOnline
|
||||
? 0
|
||||
: (((endedOn || currTime()) - leftTime) * 100) / sessionDuration;
|
||||
|
||||
const allUsers = () => Object.values(users || {}).filter((currUser) => !currUser.isModerator);
|
||||
const allUsersArr = allUsers();
|
||||
|
||||
// Here we count each poll vote in order to find out the most common answer.
|
||||
const pollVotesCount = Object.keys(polls || {}).reduce((prevPollVotesCount, pollId) => {
|
||||
const currPollVotesCount = { ...prevPollVotesCount };
|
||||
currPollVotesCount[pollId] = {};
|
||||
|
||||
if (polls[pollId].anonymous) {
|
||||
polls[pollId].anonymousAnswers.forEach((answer) => {
|
||||
const answerLowerCase = answer.toLowerCase();
|
||||
if (currPollVotesCount[pollId][answerLowerCase] === undefined) {
|
||||
currPollVotesCount[pollId][answerLowerCase] = 1;
|
||||
} else {
|
||||
currPollVotesCount[pollId][answerLowerCase] += 1;
|
||||
}
|
||||
});
|
||||
|
||||
return currPollVotesCount;
|
||||
}
|
||||
|
||||
allUsersArr.forEach((currUser) => {
|
||||
if (currUser.answers[pollId] !== undefined) {
|
||||
const userAnswers = Array.isArray(currUser.answers[pollId])
|
||||
? currUser.answers[pollId]
|
||||
: [currUser.answers[pollId]];
|
||||
|
||||
userAnswers.forEach((answer) => {
|
||||
const answerLowerCase = answer.toLowerCase();
|
||||
if (currPollVotesCount[pollId][answerLowerCase] === undefined) {
|
||||
currPollVotesCount[pollId][answerLowerCase] = 1;
|
||||
} else {
|
||||
currPollVotesCount[pollId][answerLowerCase] += 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return currPollVotesCount;
|
||||
}, {});
|
||||
|
||||
const usersTalkTime = allUsersArr.map((currUser) => currUser.talk.totalTime);
|
||||
const usersTotalOfMessages = allUsersArr.map((currUser) => currUser.totalOfMessages);
|
||||
const usersEmojis = allUsersArr.map((currUser) => currUser.emojis.filter((emoji) => emoji.name !== 'raiseHand').length);
|
||||
const usersRaiseHands = allUsersArr.map((currUser) => currUser.emojis.filter((emoji) => emoji.name === 'raiseHand').length);
|
||||
|
||||
const totalPolls = Object.values(polls || {}).length;
|
||||
|
||||
function getPointsOfTalk(u) {
|
||||
const maxTalkTime = Math.max(...usersTalkTime);
|
||||
if (maxTalkTime > 0) {
|
||||
return (u.talk.totalTime / maxTalkTime) * 2;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function getPointsOfChatting(u) {
|
||||
const maxMessages = Math.max(...usersTotalOfMessages);
|
||||
if (maxMessages > 0) {
|
||||
return (u.totalOfMessages / maxMessages) * 2;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function getPointsOfRaiseHand(u) {
|
||||
const maxRaiseHand = Math.max(...usersRaiseHands);
|
||||
const userRaiseHand = u.emojis.filter((emoji) => emoji.name === 'raiseHand').length;
|
||||
if (maxRaiseHand > 0) {
|
||||
return (userRaiseHand / maxRaiseHand) * 2;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function getPointsofEmoji(u) {
|
||||
const maxEmojis = Math.max(...usersEmojis);
|
||||
const userEmojis = u.emojis.filter((emoji) => emoji.name !== 'raiseHand').length;
|
||||
if (maxEmojis > 0) {
|
||||
return (userEmojis / maxEmojis) * 2;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function getPointsOfPolls(u) {
|
||||
if (totalPolls > 0) {
|
||||
return (Object.values(u.answers || {}).length / totalPolls) * 2;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
const talkTimeAverage = allUsersArr
|
||||
.map((currUser) => getPointsOfTalk(currUser))
|
||||
.reduce((prev, curr) => prev + curr, 0) / (allUsersArr.length || 1);
|
||||
|
||||
const messagesAverage = allUsersArr
|
||||
.map((currUser) => getPointsOfChatting(currUser))
|
||||
.reduce((prev, curr) => prev + curr, 0) / (allUsersArr.length || 1);
|
||||
|
||||
const emojisAverage = allUsersArr
|
||||
.map((currUser) => getPointsofEmoji(currUser))
|
||||
.reduce((prev, curr) => prev + curr, 0) / (allUsersArr.length || 1);
|
||||
|
||||
const raiseHandsAverage = allUsersArr
|
||||
.map((currUser) => getPointsOfRaiseHand(currUser))
|
||||
.reduce((prev, curr) => prev + curr, 0) / (allUsersArr.length || 1);
|
||||
|
||||
const pollsAverage = allUsersArr
|
||||
.map((currUser) => getPointsOfPolls(currUser))
|
||||
.reduce((prev, curr) => prev + curr, 0) / (allUsersArr.length || 1);
|
||||
|
||||
const activityPointsFunctions = {
|
||||
'Talk Time': getPointsOfTalk,
|
||||
Messages: getPointsOfChatting,
|
||||
Emojis: getPointsofEmoji,
|
||||
'Raise Hands': getPointsOfRaiseHand,
|
||||
'Poll Votes': getPointsOfPolls,
|
||||
};
|
||||
|
||||
const averages = {
|
||||
'Talk Time': talkTimeAverage,
|
||||
Messages: messagesAverage,
|
||||
Emojis: emojisAverage,
|
||||
'Raise Hands': raiseHandsAverage,
|
||||
'Poll Votes': pollsAverage,
|
||||
};
|
||||
|
||||
function renderPollItem(poll, answers) {
|
||||
const { anonymous: isAnonymous, question, pollId } = poll;
|
||||
const answersSorted = Object
|
||||
.entries(pollVotesCount[pollId])
|
||||
.sort(([, countA], [, countB]) => countB - countA);
|
||||
let mostCommonAnswer = answersSorted[0]?.[0];
|
||||
const mostCommonAnswerCount = answersSorted[0]?.[1];
|
||||
|
||||
if (mostCommonAnswer && mostCommonAnswerCount) {
|
||||
const hasDraw = answersSorted[1]?.[1] === mostCommonAnswerCount;
|
||||
if (hasDraw) mostCommonAnswer = null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-6 flex flex-row justify-between items-center">
|
||||
<div className="min-w-[40%] text-ellipsis">{question}</div>
|
||||
{ isAnonymous ? (
|
||||
<div
|
||||
className="min-w-[20%] grow text-center mx-3"
|
||||
>
|
||||
<span
|
||||
title={intl.formatMessage({
|
||||
id: 'app.learningDashboard.userDetails.anonymousAnswer',
|
||||
defaultMessage: 'Anonymous Poll',
|
||||
})}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-4 w-4 inline"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="min-w-[20%] grow text-center mx-3">{answers.map((answer) => <p title={answer} className="overflow-hidden text-ellipsis">{answer}</p>)}</div>
|
||||
) }
|
||||
<div
|
||||
className="min-w-[40%] text-ellipsis text-center overflow-hidden"
|
||||
title={mostCommonAnswer
|
||||
? `${String.fromCharCode(mostCommonAnswer.charCodeAt(0) - 32)}${mostCommonAnswer.substring(1)}`
|
||||
: null}
|
||||
>
|
||||
{ mostCommonAnswer
|
||||
? `${String.fromCharCode(mostCommonAnswer.charCodeAt(0) - 32)}${mostCommonAnswer.substring(1)}`
|
||||
: intl.formatMessage({
|
||||
id: 'app.learningDashboard.usersTable.notAvailable',
|
||||
defaultMessage: 'N/A',
|
||||
}) }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function renderActivityScoreItem(
|
||||
category, average, activityPoints, totalOfActivity,
|
||||
) {
|
||||
return (
|
||||
<div className="p-6 flex flex-row justify-between items-end">
|
||||
<div className="min-w-[20%] text-ellipsis overflow-hidden">{category}</div>
|
||||
<div className="min-w-[60%] grow text-center text-sm">
|
||||
<div className="mb-2">
|
||||
{ average >= 0
|
||||
? <FormattedNumber value={average} minimumFractionDigits="0" maximumFractionDigits="1" />
|
||||
: <FormattedMessage id="app.learningDashboard.usersTable.notAvailable" defaultMessage="N/A" /> }
|
||||
</div>
|
||||
<div className="rounded-2xl bg-gray-200 before:bg-gray-500 h-4 relative before:absolute before:top-[-50%] before:bottom-[-50%] before:w-[2px] before:left-[calc(50%-1px)] before:z-10">
|
||||
<div
|
||||
className="flex justify-end items-center text-white rounded-2xl ltr:bg-gradient-to-br rtl:bg-gradient-to-bl from-green-100 to-green-600 absolute inset-0"
|
||||
style={{
|
||||
width: `${(activityPoints / 2) * 100}%`,
|
||||
}}
|
||||
>
|
||||
{ totalOfActivity > 0
|
||||
? <span className="ltr:mr-4 rtl:ml-4">{category === 'Talk Time' ? tsToHHmmss(totalOfActivity) : totalOfActivity}</span>
|
||||
: null }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="min-w-[20%] text-sm text-ellipsis overflow-hidden text-right rtl:text-left">
|
||||
{ activityPoints >= 0
|
||||
? <FormattedNumber value={activityPoints} minimumFractionDigits="0" maximumFractionDigits="1" />
|
||||
: <FormattedMessage id="app.learningDashboard.usersTable.notAvailable" defaultMessage="N/A" /> }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function getUserAnswer(poll) {
|
||||
if (typeof user.answers[poll.pollId] !== 'undefined') {
|
||||
return Array.isArray(user.answers[poll.pollId])
|
||||
? user.answers[poll.pollId]
|
||||
: [user.answers[poll.pollId]];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 flex flex-row z-50">
|
||||
<div
|
||||
className="bg-black grow opacity-50"
|
||||
role="none"
|
||||
onClick={() => dispatch({ type: 'closeModal' })}
|
||||
/>
|
||||
<div className="overflow-auto w-full md:w-2/4 bg-gray-100 p-6">
|
||||
<div className="text-right rtl:text-left">
|
||||
<button
|
||||
onClick={() => dispatch({ type: 'closeModal' })}
|
||||
type="button"
|
||||
aria-label="Close user details modal"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-col items-center p-10">
|
||||
<div className="inline-block relative w-8 h-8 rounded-full">
|
||||
<UserAvatar user={user} />
|
||||
<div
|
||||
className="absolute inset-0 rounded-full shadow-inner"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
<p className="break-words text-center">{user.name}</p>
|
||||
</div>
|
||||
<div className="bg-white shadow rounded mb-4">
|
||||
<div className="p-6 text-lg flex items-center">
|
||||
<div className="p-2 rounded-full bg-pink-50 text-pink-500">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<p className="ltr:ml-2 rtl:mr-2"><FormattedMessage id="app.learningDashboard.usersTable.title" defaultMessage="Overview" /></p>
|
||||
</div>
|
||||
<div className="p-6 m-px bg-gray-100">
|
||||
<div className="h-6 relative before:bg-gray-500 before:absolute before:w-[10px] before:h-[10px] before:rounded-full before:left-0 before:top-[calc(50%-5px)] after:bg-gray-500 after:absolute after:w-[10px] after:h-[10px] after:rounded-full after:right-0 after:top-[calc(50%-5px)]">
|
||||
<div className="bg-gray-500 [--line-height:2px] h-[var(--line-height)] absolute top-[calc(50%-var(--line-height)/2)] left-[10px] right-[10px] rounded-2xl" />
|
||||
<div
|
||||
className="ltr:bg-gradient-to-br rtl:bg-gradient-to-bl from-green-100 to-green-600 absolute h-full rounded-2xl text-right rtl:text-left text-ellipsis overflow-hidden"
|
||||
style={{
|
||||
right: `calc(${document.dir === 'ltr' ? userEndOffsetTime : userStartOffsetTime}% + 10px)`,
|
||||
left: `calc(${document.dir === 'ltr' ? userStartOffsetTime : userEndOffsetTime}% + 10px)`,
|
||||
}}
|
||||
>
|
||||
<div className="mx-3 inline-block text-white">
|
||||
{ new Date(getSumOfTime(Object.values(user.intIds)))
|
||||
.toISOString()
|
||||
.substring(11, 19) }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row justify-between font-light text-gray-700">
|
||||
<div>
|
||||
<div><FormattedMessage id="app.learningDashboard.userDetails.startTime" defaultMessage="Start Time" /></div>
|
||||
<div>
|
||||
<FormattedTime value={createdOn} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="ltr:text-right rtl:text-left">
|
||||
<div><FormattedMessage id="app.learningDashboard.userDetails.endTime" defaultMessage="End Time" /></div>
|
||||
<div>
|
||||
{ endedOn === 0 ? (
|
||||
<span className="px-2 py-1 font-semibold leading-tight text-green-700 bg-green-100 rounded-full">
|
||||
<FormattedMessage id="app.learningDashboard.indicators.meetingStatusActive" defaultMessage="Active" />
|
||||
</span>
|
||||
) : (
|
||||
<FormattedTime value={endedOn} />
|
||||
) }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-6 flex flex-row justify-between text-gray-700">
|
||||
<div>
|
||||
<div className="text-gray-900 font-medium">
|
||||
{ new Date(getSumOfTime(Object.values(user.intIds)))
|
||||
.toISOString()
|
||||
.substring(11, 19) }
|
||||
</div>
|
||||
<div><FormattedMessage id="app.learningDashboard.indicators.duration" defaultMessage="Duration" /></div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium">
|
||||
<FormattedTime value={joinTime} />
|
||||
</div>
|
||||
<div><FormattedMessage id="app.learningDashboard.userDetails.joined" defaultMessage="Joined" /></div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium">
|
||||
{ isOnline ? (
|
||||
<span className="px-2 py-1 font-semibold leading-tight text-green-700 bg-green-100 rounded-full">
|
||||
<FormattedMessage id="app.learningDashboard.indicators.userStatusOnline" defaultMessage="Online" />
|
||||
</span>
|
||||
) : (
|
||||
<FormattedTime value={leftTime} />
|
||||
) }
|
||||
</div>
|
||||
<div className="px-2"><FormattedMessage id="app.learningDashboard.usersTable.left" defaultMessage="Left" /></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{ !user.isModerator && (
|
||||
<>
|
||||
<div className="bg-white shadow rounded mb-4 table w-full">
|
||||
<div className="p-6 text-lg flex items-center">
|
||||
<div className="p-2 rounded-full bg-green-200 text-green-500">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 3.055A9.001 9.001 0 1020.945 13H11V3.055z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20.488 9H15V3.512A9.025 9.025 0 0120.488 9z" />
|
||||
</svg>
|
||||
</div>
|
||||
<p className="ltr:ml-2 rtl:mr-2">
|
||||
<FormattedMessage id="app.learningDashboard.indicators.activityScore" defaultMessage="Activity Score" />
|
||||
:
|
||||
<span className="font-bold">
|
||||
<FormattedNumber value={getActivityScore(user, users, totalPolls)} minimumFractionDigits="0" maximumFractionDigits="1" />
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-6 py-2 m-px bg-gray-200 flex flex-row justify-between text-xs text-gray-700">
|
||||
<div className="min-w-[20%] text-ellipsis"><FormattedMessage id="app.learningDashboard.userDetails.category" defaultMessage="Category" /></div>
|
||||
<div className="grow text-center"><FormattedMessage id="app.learningDashboard.userDetails.average" defaultMessage="Average" /></div>
|
||||
<div className="min-w-[20%] text-ellipsis text-right rtl:text-left"><FormattedMessage id="app.learningDashboard.userDetails.activityPoints" defaultMessage="Activity Points" /></div>
|
||||
</div>
|
||||
{ ['Talk Time', 'Messages', 'Emojis', 'Raise Hands', 'Poll Votes'].map((category) => {
|
||||
let totalOfActivity = 0;
|
||||
|
||||
switch (category) {
|
||||
case 'Talk Time':
|
||||
totalOfActivity = user.talk.totalTime;
|
||||
break;
|
||||
case 'Messages':
|
||||
totalOfActivity = user.totalOfMessages;
|
||||
break;
|
||||
case 'Emojis':
|
||||
totalOfActivity = user.emojis.filter((emoji) => emoji.name !== 'raiseHand').length;
|
||||
break;
|
||||
case 'Raise Hands':
|
||||
totalOfActivity = user.emojis.filter((emoji) => emoji.name === 'raiseHand').length;
|
||||
break;
|
||||
case 'Poll Votes':
|
||||
totalOfActivity = Object.values(user.answers).length;
|
||||
break;
|
||||
default:
|
||||
}
|
||||
|
||||
return renderActivityScoreItem(
|
||||
category,
|
||||
averages[category],
|
||||
activityPointsFunctions[category](user),
|
||||
totalOfActivity,
|
||||
);
|
||||
}) }
|
||||
</div>
|
||||
<div className="bg-white shadow rounded">
|
||||
<div className="p-6 text-lg flex items-center">
|
||||
<div className="p-2 rounded-full bg-blue-100 text-blue-500">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01" />
|
||||
</svg>
|
||||
</div>
|
||||
<p className="ltr:ml-2 rtl:mr-2"><FormattedMessage id="app.learningDashboard.indicators.polls" defaultMessage="Polls" /></p>
|
||||
</div>
|
||||
<div className="p-6 py-2 m-px bg-gray-200 flex flex-row justify-between text-xs text-gray-700">
|
||||
<div className="min-w-[40%] text-ellipsis"><FormattedMessage id="app.learningDashboard.userDetails.poll" defaultMessage="Poll" /></div>
|
||||
<div className="grow text-center"><FormattedMessage id="app.learningDashboard.userDetails.response" defaultMessage="Response" /></div>
|
||||
<div className="min-w-[40%] text-ellipsis text-center"><FormattedMessage id="app.learningDashboard.userDetails.mostCommonAnswer" defaultMessage="Most Common Answer" /></div>
|
||||
</div>
|
||||
{ Object.values(polls || {})
|
||||
.map((poll) => renderPollItem(
|
||||
poll,
|
||||
getUserAnswer(poll),
|
||||
)) }
|
||||
</div>
|
||||
</>
|
||||
) }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const UserDetailsContainer = (props) => {
|
||||
const { isOpen, dispatch, user } = useContext(UserDetailsContext);
|
||||
|
||||
return ReactDOM.createPortal(
|
||||
<UserDatailsComponent
|
||||
{...{
|
||||
...props,
|
||||
isOpen,
|
||||
dispatch,
|
||||
user,
|
||||
}}
|
||||
/>,
|
||||
document.getElementById('modal-container'),
|
||||
);
|
||||
};
|
||||
|
||||
export default injectIntl(UserDetailsContainer);
|
@ -0,0 +1,39 @@
|
||||
import React, { useReducer } from 'react';
|
||||
|
||||
export const UserDetailsContext = React.createContext();
|
||||
|
||||
export const UserDetailsProvider = ({ children }) => {
|
||||
const reducer = (state, action) => {
|
||||
switch (action.type) {
|
||||
case 'changeUser':
|
||||
return {
|
||||
user: action.user,
|
||||
isOpen: true,
|
||||
};
|
||||
case 'closeModal':
|
||||
return {
|
||||
user: null,
|
||||
isOpen: false,
|
||||
};
|
||||
default:
|
||||
throw new Error();
|
||||
}
|
||||
};
|
||||
|
||||
const [state, dispatch] = useReducer(reducer, {
|
||||
user: null,
|
||||
isOpen: false,
|
||||
});
|
||||
|
||||
return (
|
||||
<UserDetailsContext.Provider
|
||||
value={{
|
||||
user: state.user,
|
||||
isOpen: state.isOpen,
|
||||
dispatch,
|
||||
}}
|
||||
>
|
||||
{ children }
|
||||
</UserDetailsContext.Provider>
|
||||
);
|
||||
};
|
@ -5,6 +5,7 @@ import {
|
||||
import { getUserEmojisSummary, emojiConfigs } from '../services/EmojiService';
|
||||
import { getActivityScore, getSumOfTime, tsToHHmmss } from '../services/UserService';
|
||||
import UserAvatar from './UserAvatar';
|
||||
import { UserDetailsContext } from './UserDetails/context';
|
||||
|
||||
function renderArrow(order = 'asc') {
|
||||
return (
|
||||
@ -36,6 +37,8 @@ class UsersTable extends React.Component {
|
||||
activityscoreOrder: 'desc',
|
||||
lastFieldClicked: 'userOrder',
|
||||
};
|
||||
|
||||
this.openUserModal = this.openUserModal.bind(this);
|
||||
}
|
||||
|
||||
toggleOrder(field) {
|
||||
@ -51,6 +54,15 @@ class UsersTable extends React.Component {
|
||||
if (tab === 'overview') this.setState({ lastFieldClicked: field });
|
||||
}
|
||||
|
||||
openUserModal(user) {
|
||||
const { dispatch } = this.context;
|
||||
|
||||
dispatch({
|
||||
type: 'changeUser',
|
||||
user,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
allUsers, totalOfActivityTime, totalOfPolls, tab,
|
||||
@ -231,9 +243,14 @@ class UsersTable extends React.Component {
|
||||
</div>
|
||||
|
||||
<div className="inline-block">
|
||||
<p className="font-semibold truncate xl:max-w-sm max-w-xs">
|
||||
<button
|
||||
className="leading-none border-0 p-0 m-0 bg-none font-semibold truncate xl:max-w-sm max-w-xs cursor-pointer rounded-md focus:outline-none focus:ring ring-offset-0 focus:ring-gray-500 focus:ring-opacity-50"
|
||||
type="button"
|
||||
onClick={() => this.openUserModal(user)}
|
||||
aria-label={`Open user details modal - ${user.name}`}
|
||||
>
|
||||
{user.name}
|
||||
</p>
|
||||
</button>
|
||||
{ Object.values(user.intIds || {}).map((intId, index) => (
|
||||
<>
|
||||
<p className="text-xs text-gray-600 dark:text-gray-400">
|
||||
@ -504,4 +521,6 @@ class UsersTable extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
UsersTable.contextType = UserDetailsContext;
|
||||
|
||||
export default injectIntl(UsersTable);
|
||||
|
@ -4,6 +4,7 @@ import './index.css';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
import App from './App';
|
||||
import reportWebVitals from './reportWebVitals';
|
||||
import { UserDetailsProvider } from './components/UserDetails/context';
|
||||
|
||||
const RTL_LANGUAGES = ['ar', 'dv', 'fa', 'he'];
|
||||
|
||||
@ -62,6 +63,8 @@ class Dashboard extends React.Component {
|
||||
|
||||
if (RTL_LANGUAGES.includes(intlLocale.substring(0, 2))) {
|
||||
document.body.parentNode.setAttribute('dir', 'rtl');
|
||||
} else {
|
||||
document.body.parentNode.setAttribute('dir', 'ltr');
|
||||
}
|
||||
}
|
||||
|
||||
@ -69,9 +72,11 @@ class Dashboard extends React.Component {
|
||||
const { intlLocale, intlMessages } = this.state;
|
||||
|
||||
return (
|
||||
<IntlProvider defaultLocale="en" locale={intlLocale} messages={intlMessages}>
|
||||
<App />
|
||||
</IntlProvider>
|
||||
<UserDetailsProvider>
|
||||
<IntlProvider defaultLocale="en" locale={intlLocale} messages={intlMessages}>
|
||||
<App />
|
||||
</IntlProvider>
|
||||
</UserDetailsProvider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -46,7 +46,7 @@ export function getActivityScore(user, allUsers, totalOfPolls) {
|
||||
|
||||
export function getSumOfTime(eventsArr) {
|
||||
return eventsArr.reduce((prevVal, elem) => {
|
||||
if ((elem.stoppedOn || elem.registeredOn) > 0) {
|
||||
if ((elem.stoppedOn || elem.leftOn) > 0) {
|
||||
return prevVal + ((elem.stoppedOn || elem.leftOn) - (elem.startedOn || elem.registeredOn));
|
||||
}
|
||||
return prevVal + (new Date().getTime() - (elem.startedOn || elem.registeredOn));
|
||||
|
@ -970,6 +970,16 @@
|
||||
"app.learningDashboard.indicators.timeline": "Timeline",
|
||||
"app.learningDashboard.indicators.activityScore": "Activity Score",
|
||||
"app.learningDashboard.indicators.duration": "Duration",
|
||||
"app.learningDashboard.userDetails.startTime": "Start Time",
|
||||
"app.learningDashboard.userDetails.endTime": "End Time",
|
||||
"app.learningDashboard.userDetails.joined": "Joined",
|
||||
"app.learningDashboard.userDetails.category": "Category",
|
||||
"app.learningDashboard.userDetails.average": "Average",
|
||||
"app.learningDashboard.userDetails.activityPoints": "Activity Points",
|
||||
"app.learningDashboard.userDetails.poll": "Poll",
|
||||
"app.learningDashboard.userDetails.response": "Response",
|
||||
"app.learningDashboard.userDetails.mostCommonAnswer": "Most Common Answer",
|
||||
"app.learningDashboard.userDetails.anonymousAnswer": "Anonymous Poll",
|
||||
"app.learningDashboard.usersTable.title": "Overview",
|
||||
"app.learningDashboard.usersTable.colOnline": "Online time",
|
||||
"app.learningDashboard.usersTable.colTalk": "Talk time",
|
||||
|
Loading…
Reference in New Issue
Block a user