feat(analytics): enhance analytics functionality with token validation and data extraction
All checks were successful
Build & Push Docker Image / build (push) Successful in 6m29s
All checks were successful
Build & Push Docker Image / build (push) Successful in 6m29s
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { BarChart3, Trash2, Clock, Users, MessageSquare, Video, Mic, ChevronDown, ChevronUp } from 'lucide-react';
|
||||
import { BarChart3, Trash2, Clock, Users, MessageSquare, Video, Mic, ChevronDown, ChevronUp, Hand, BarChart2 } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import api from '../services/api';
|
||||
import { useLanguage } from '../contexts/LanguageContext';
|
||||
@@ -20,14 +20,15 @@ export default function AnalyticsList({ analytics, onRefresh }) {
|
||||
});
|
||||
};
|
||||
|
||||
const formatDurationMs = (ms) => {
|
||||
if (!ms || ms <= 0) return '0m';
|
||||
const seconds = Math.floor(ms / 1000);
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const formatDurationSec = (sec) => {
|
||||
if (!sec || sec <= 0) return '0m';
|
||||
const minutes = Math.floor(sec / 60);
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const mins = minutes % 60;
|
||||
const secs = sec % 60;
|
||||
if (hours > 0) return `${hours}h ${mins}m`;
|
||||
return `${mins}m`;
|
||||
if (minutes > 0) return `${mins}m ${secs}s`;
|
||||
return `${secs}s`;
|
||||
};
|
||||
|
||||
const handleDelete = async (id) => {
|
||||
@@ -48,22 +49,32 @@ export default function AnalyticsList({ analytics, onRefresh }) {
|
||||
setExpanded(prev => ({ ...prev, [id]: !prev[id] }));
|
||||
};
|
||||
|
||||
// Extract user summary from BBB learning analytics data
|
||||
// Extract user summary from BBB learning analytics callback data
|
||||
// Payload: { meeting_id, data: { duration, start, finish, attendees: [{ name, moderator, duration, engagement: { chats, talks, raisehand, emojis, poll_votes, talk_time } }] } }
|
||||
const getUserSummary = (data) => {
|
||||
if (!data?.users) return [];
|
||||
const usersObj = typeof data.users === 'object' ? data.users : {};
|
||||
return Object.values(usersObj).map(u => ({
|
||||
name: u.name || '—',
|
||||
isModerator: u.isModerator || false,
|
||||
totalTime: u.registeredOn && u.leftOn ? u.leftOn - u.registeredOn : 0,
|
||||
talkTime: u.talk?.totalTime || 0,
|
||||
webcamTime: Array.isArray(u.webcams) ? u.webcams.reduce((acc, w) => acc + ((w.stoppedSharingAt || Date.now()) - (w.startedSharingAt || 0)), 0) : 0,
|
||||
messages: u.totalOfMessages || 0,
|
||||
emojis: Array.isArray(u.emojis) ? u.emojis.length : 0,
|
||||
raiseHand: Array.isArray(u.raiseHand) ? u.raiseHand.length : 0,
|
||||
const attendees = data?.data?.attendees;
|
||||
if (!Array.isArray(attendees)) return [];
|
||||
return attendees.map(a => ({
|
||||
name: a.name || '—',
|
||||
isModerator: !!a.moderator,
|
||||
duration: a.duration || 0,
|
||||
talkTime: a.engagement?.talk_time || 0,
|
||||
chats: a.engagement?.chats || 0,
|
||||
talks: a.engagement?.talks || 0,
|
||||
raiseHand: a.engagement?.raisehand || 0,
|
||||
emojis: a.engagement?.emojis || 0,
|
||||
pollVotes: a.engagement?.poll_votes || 0,
|
||||
}));
|
||||
};
|
||||
|
||||
const getMeetingSummary = (data) => ({
|
||||
duration: data?.data?.duration || 0,
|
||||
start: data?.data?.start || null,
|
||||
finish: data?.data?.finish || null,
|
||||
files: data?.data?.files || [],
|
||||
polls: data?.data?.polls || [],
|
||||
});
|
||||
|
||||
if (!analytics || analytics.length === 0) {
|
||||
return (
|
||||
<div className="text-center py-12">
|
||||
@@ -77,9 +88,10 @@ export default function AnalyticsList({ analytics, onRefresh }) {
|
||||
<div className="space-y-3">
|
||||
{analytics.map(entry => {
|
||||
const users = getUserSummary(entry.data);
|
||||
const meeting = getMeetingSummary(entry.data);
|
||||
const isExpanded = expanded[entry.id];
|
||||
const totalParticipants = users.length;
|
||||
const totalMessages = users.reduce((sum, u) => sum + u.messages, 0);
|
||||
const totalMessages = users.reduce((sum, u) => sum + u.chats, 0);
|
||||
|
||||
return (
|
||||
<div key={entry.id} className="card p-4">
|
||||
@@ -91,10 +103,14 @@ export default function AnalyticsList({ analytics, onRefresh }) {
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4 text-xs text-th-text-s">
|
||||
<div className="flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-th-text-s">
|
||||
<span className="flex items-center gap-1">
|
||||
<Clock size={12} />
|
||||
{formatDate(entry.createdAt)}
|
||||
{meeting.start ? formatDate(meeting.start) : formatDate(entry.createdAt)}
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<BarChart2 size={12} />
|
||||
{formatDurationSec(meeting.duration)}
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<Users size={12} />
|
||||
@@ -134,14 +150,17 @@ export default function AnalyticsList({ analytics, onRefresh }) {
|
||||
<tr className="text-left text-xs text-th-text-s border-b border-th-border">
|
||||
<th className="pb-2 pr-4 font-medium">{t('analytics.userName')}</th>
|
||||
<th className="pb-2 pr-4 font-medium">{t('analytics.role')}</th>
|
||||
<th className="pb-2 pr-4 font-medium">
|
||||
<span className="flex items-center gap-1"><Clock size={11} />{t('analytics.duration')}</span>
|
||||
</th>
|
||||
<th className="pb-2 pr-4 font-medium">
|
||||
<span className="flex items-center gap-1"><Mic size={11} />{t('analytics.talkTime')}</span>
|
||||
</th>
|
||||
<th className="pb-2 pr-4 font-medium">
|
||||
<span className="flex items-center gap-1"><Video size={11} />{t('analytics.webcamTime')}</span>
|
||||
<span className="flex items-center gap-1"><MessageSquare size={11} />{t('analytics.messages')}</span>
|
||||
</th>
|
||||
<th className="pb-2 pr-4 font-medium">
|
||||
<span className="flex items-center gap-1"><MessageSquare size={11} />{t('analytics.messages')}</span>
|
||||
<span className="flex items-center gap-1"><Hand size={11} />{t('analytics.raiseHand')}</span>
|
||||
</th>
|
||||
<th className="pb-2 font-medium">{t('analytics.reactions')}</th>
|
||||
</tr>
|
||||
@@ -159,10 +178,11 @@ export default function AnalyticsList({ analytics, onRefresh }) {
|
||||
{u.isModerator ? t('analytics.moderator') : t('analytics.viewer')}
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-2 pr-4 text-th-text-s">{formatDurationMs(u.talkTime)}</td>
|
||||
<td className="py-2 pr-4 text-th-text-s">{formatDurationMs(u.webcamTime)}</td>
|
||||
<td className="py-2 pr-4 text-th-text-s">{u.messages}</td>
|
||||
<td className="py-2 text-th-text-s">{u.emojis + u.raiseHand}</td>
|
||||
<td className="py-2 pr-4 text-th-text-s">{formatDurationSec(u.duration)}</td>
|
||||
<td className="py-2 pr-4 text-th-text-s">{formatDurationSec(u.talkTime)}</td>
|
||||
<td className="py-2 pr-4 text-th-text-s">{u.chats}</td>
|
||||
<td className="py-2 pr-4 text-th-text-s">{u.raiseHand}</td>
|
||||
<td className="py-2 text-th-text-s">{u.emojis}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
|
||||
Reference in New Issue
Block a user