import CancelIcon from '@mui/icons-material/Cancel' import CheckCircleIcon from '@mui/icons-material/CheckCircle' import CircleIcon from '@mui/icons-material/Circle' import { Cell, Pie, PieChart, Tooltip } from 'recharts' import { EventBusy, Group, Timeline, Toll } from '@mui/icons-material' import { Avatar, Box, Button, Card, Chip, Container, Divider, Grid, Link, Option, Select, Stack, Tab, TabList, Tabs, Typography, } from '@mui/joy' import React, { useEffect, useState } from 'react' import { useChores, useChoresHistory } from '../../queries/ChoreQueries' import { useCircleMembers, useUserProfile } from '../../queries/UserQueries.jsx' import { ChoresGrouper } from '../../utils/Chores' import { COLORS, TASK_COLOR } from '../../utils/Colors.jsx' import { resolvePhotoURL } from '../../utils/Helpers.jsx' import LoadingComponent from '../components/Loading' const groupByDate = history => { const aggregated = {} for (let i = 0; i < history.length; i++) { const item = history[i] const date = new Date(item.performedAt).toLocaleDateString() if (!aggregated[date]) { aggregated[date] = [] } aggregated[date].push(item) } return aggregated } const ChoreHistoryItem = ({ time, name, points, status }) => { const statusIcon = { completed: , missed: , pending: , } return ( {time} {statusIcon[status] ? statusIcon[status] : statusIcon['completed']} {name} {points && ( }> {`${points} points`} )} ) } const ChoreHistoryTimeline = ({ history }) => { const groupedHistory = groupByDate(history) return ( Activities Timeline {Object.entries(groupedHistory).map(([date, items]) => ( {new Date(date).toLocaleDateString([], { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric', })} {items.map(record => ( <> ))} ))} ) } const renderPieChart = (data, size, isPrimary, chartType = null) => { // Filter out items with zero or negative values const validData = data.filter(item => item.value > 0) if (validData.length === 0) { return ( No data available ) } // For primary charts, render chart and legend separately to control layout better if (isPrimary) { const chartSize = Math.min(size - 20, 220) // Reserve space and limit max size return ( {/* Chart Container */} 1 ? 2 : 0} cornerRadius={3} minAngle={5} > {validData.map((entry, index) => ( ))} { if (chartType === 'tasksTime' && props.payload.count) { return [`${value}h (${props.payload.count} times)`, name] } return [`${value}`, name] }} /> {/* Scrollable Legend Container */} {validData.map((entry, index) => ( } > {entry.label}: {entry.value} {chartType === 'tasksTime' && entry.count ? ` (${entry.count}x)` : ''} {chartType === 'labelsDuration' || chartType === 'tasksTime' ? 'h' : ''} ))} ) } // For small preview charts, keep it simple without legend return ( 1 ? 1 : 0} cornerRadius={2} > {validData.map((entry, index) => ( ))} ) } const USER_FILTER = (history, userId) => { if (userId === undefined || userId === 'all') return true return history.completedBy === userId } const UserActivites = () => { const { data: userProfile } = useUserProfile() const [tabValue, setTabValue] = React.useState(30) const [selectedHistory, setSelectedHistory] = React.useState([]) const [enrichedHistory, setEnrichedHistory] = React.useState([]) const [selectedChart, setSelectedChart] = React.useState('history') const [historyPieChartData, setHistoryPieChartData] = React.useState([]) const [choreDuePieChartData, setChoreDuePieChartData] = React.useState([]) const [choresPriorityChartData, setChoresPriorityChartData] = React.useState( [], ) const [choresLabelsChartData, setChoresLabelsChartData] = React.useState([]) const [choresLabelsDurationChartData, setChoresLabelsDurationChartData] = React.useState([]) const [tasksTimeChartData, setTasksTimeChartData] = React.useState([]) const [ choresAssigneeBreakdownChartData, setChoresAssigneeBreakdownChartData, ] = React.useState([]) const { data: choresData, isLoading: isChoresLoading } = useChores(true) const { data: choresHistory, isChoresHistoryLoading, handleLimitChange: refetchHistory, } = useChoresHistory(tabValue ? tabValue : 30, true) const { data: circleMembersData } = useCircleMembers() const [selectedUser, setSelectedUser] = React.useState('all') const [circleUsers, setCircleUsers] = useState([]) useEffect(() => { if (circleMembersData) { setCircleUsers(circleMembersData.res) } }, [circleMembersData]) useEffect(() => { if ( !isChoresHistoryLoading && !isChoresLoading && choresHistory && choresData?.res ) { const enrichedHistory = choresHistory.map(item => { const chore = choresData.res.find(chore => chore.id === item.choreId) return { ...item, choreName: chore?.name, } }) setEnrichedHistory(enrichedHistory) const filteredHistory = enrichedHistory.filter(h => USER_FILTER(h, selectedUser), ) setSelectedHistory(filteredHistory) setHistoryPieChartData(generateHistoryPieChartData(filteredHistory)) // Generate labels duration chart data when both chores and history are available setChoresLabelsDurationChartData( generateChoreLabelsWithDurationChartData( choresData.res, filteredHistory, ), ) // Generate tasks time chart data setTasksTimeChartData(generateTasksTimeChartData(filteredHistory)) } else { // Reset data when loading or no data setEnrichedHistory([]) setSelectedHistory([]) setHistoryPieChartData([]) setChoresLabelsDurationChartData([]) setTasksTimeChartData([]) } }, [ isChoresHistoryLoading, isChoresLoading, choresHistory, choresData?.res, selectedUser, ]) useEffect(() => { if (!isChoresLoading && choresData) { // Filter chores based on selected user const filteredChores = selectedUser === 'all' || selectedUser === undefined ? choresData.res : choresData.res.filter(chore => chore.assignedTo === selectedUser) const generateChorePriorityPieChartData = chores => { const groups = ChoresGrouper('priority', chores, null) return groups .map(group => { return { label: group.name, value: group.content.length, color: group.color, id: group.name, } }) .filter(item => item.value > 0) } const generateChoreLabelsChartData = chores => { const labelCounts = {} let unlabeledCount = 0 chores.forEach(chore => { if (chore.labelsV2 && chore.labelsV2.length > 0) { chore.labelsV2.forEach(label => { if (labelCounts[label.id]) { labelCounts[label.id].count++ } else { labelCounts[label.id] = { label: label.name, count: 1, color: label.color || TASK_COLOR.ANYTIME, id: label.id, } } }) } else { unlabeledCount++ } }) const result = Object.values(labelCounts) .map(item => ({ label: item.label, value: item.count, color: item.color, id: item.id, })) .filter(item => item.value > 0) .sort((a, b) => b.value - a.value) // Sort by count descending // Add unlabeled tasks if there are any if (unlabeledCount > 0) { result.push({ label: 'No Labels', value: unlabeledCount, color: TASK_COLOR.ANYTIME, id: 'unlabeled', }) } return result } const generateChoreAssigneeBreakdownChartData = chores => { const assigneeCounts = {} // Define a set of distinct colors for different assignees const assigneeColors = Object.values(COLORS) let colorIndex = 0 chores.forEach(chore => { const assignee = circleUsers.find( user => user.userId === chore.assignedTo, ) const assigneeName = assignee ? assignee.displayName : 'Unassigned' const assigneeId = chore.assignedTo || 'unassigned' if (assigneeCounts[assigneeId]) { assigneeCounts[assigneeId].count++ } else { assigneeCounts[assigneeId] = { label: assigneeName, count: 1, color: assigneeId === 'unassigned' ? TASK_COLOR.ANYTIME : assigneeColors[colorIndex % assigneeColors.length], id: assigneeId, } if (assigneeId !== 'unassigned') { colorIndex++ } } }) return Object.values(assigneeCounts) .map(item => ({ label: item.label, value: item.count, color: item.color, id: item.id, })) .filter(item => item.value > 0) .sort((a, b) => b.value - a.value) // Sort by count descending } const choreDuePieChartData = generateChoreDuePieChartData(filteredChores) setChoreDuePieChartData(choreDuePieChartData) setChoresPriorityChartData( generateChorePriorityPieChartData(filteredChores), ) setChoresLabelsChartData(generateChoreLabelsChartData(filteredChores)) setChoresAssigneeBreakdownChartData( generateChoreAssigneeBreakdownChartData(filteredChores), ) } }, [isChoresLoading, choresData, userProfile?.id, circleUsers, selectedUser]) const generateChoreLabelsWithDurationChartData = (chores, history) => { if (!chores || !history || chores.length === 0 || history.length === 0) { return [] } const labelDurations = {} let unlabeledDuration = 0 // Iterate through ChoreHistory to get actual time spent history.forEach(historyItem => { const duration = historyItem.duration || 0 // duration in seconds from ChoreHistory // Find the corresponding chore to get its labels const chore = chores.find(c => c.id === historyItem.choreId) if (chore && chore.labelsV2 && chore.labelsV2.length > 0) { // If chore has labels, add duration to each label chore.labelsV2.forEach(label => { if (labelDurations[label.id]) { labelDurations[label.id].duration += duration } else { labelDurations[label.id] = { label: label.name, duration: duration, color: label.color || TASK_COLOR.ANYTIME, id: label.id, } } }) } else { // If chore has no labels or chore not found, add to unlabeled unlabeledDuration += duration } }) // Convert seconds to hours for better readability const result = Object.values(labelDurations) .map(item => ({ label: item.label, value: Math.round((item.duration / 3600) * 10) / 10, // Convert to hours and round to 1 decimal color: item.color, id: item.id, })) .filter(item => item.value > 0) .sort((a, b) => b.value - a.value) // Sort by duration descending // Add unlabeled tasks duration if there is any if (unlabeledDuration > 0) { result.push({ label: 'No Labels', value: Math.round((unlabeledDuration / 3600) * 10) / 10, // Convert to hours and round to 1 decimal color: TASK_COLOR.ANYTIME, id: 'unlabeled', }) } return result } const generateTasksTimeChartData = history => { if (!history || history.length === 0) { return [] } const taskDurations = {} const colorValues = Object.values(COLORS) // Iterate through ChoreHistory to get actual time spent per task history.forEach(historyItem => { const duration = historyItem.duration || 0 // duration in seconds from ChoreHistory const taskName = historyItem.choreName || 'Unknown Task' if (taskDurations[taskName]) { taskDurations[taskName].duration += duration taskDurations[taskName].count += 1 } else { taskDurations[taskName] = { taskName: taskName, duration: duration, count: 1, } } }) // Convert seconds to hours and prepare chart data const result = Object.values(taskDurations) .map((item, index) => ({ label: item.taskName, value: Math.round((item.duration / 3600) * 10) / 10, // Convert to hours and round to 1 decimal count: item.count, color: colorValues[index % colorValues.length], id: item.taskName, })) .filter(item => item.value > 0) .sort((a, b) => b.value - a.value) // Sort by time spent descending .slice(0, 10) // Show top 10 tasks only return result } const generateChoreDuePieChartData = chores => { if (!chores || chores.length === 0) { return [] } const groups = ChoresGrouper('due_date', chores, null) return groups .map(group => { return { label: group.name, value: group.content.length, color: group.color, id: group.name, } }) .filter(item => item.value > 0) } const generateHistoryPieChartData = history => { if (!history || history.length === 0) { return [] } const totalCompleted = history.filter(item => item.dueDate > item.performedAt).length || 0 const totalLate = history.filter(item => item.dueDate < item.performedAt).length || 0 const totalNoDueDate = history.filter(item => !item.dueDate).length || 0 const result = [] if (totalCompleted > 0) { result.push({ label: `On time`, value: totalCompleted, color: TASK_COLOR.COMPLETED, id: 1, }) } if (totalLate > 0) { result.push({ label: `Late`, value: totalLate, color: TASK_COLOR.LATE, id: 2, }) } if (totalNoDueDate > 0) { result.push({ label: `Completed`, value: totalNoDueDate, color: TASK_COLOR.ANYTIME, id: 3, }) } return result } if (isChoresHistoryLoading || isChoresLoading) { return } const chartData = { history: { data: historyPieChartData || [], title: 'Status', description: 'Completed tasks status', }, due: { data: choreDuePieChartData || [], title: 'Due Date', description: 'Current tasks due date', }, // assigned: { // data: choresAssignedChartData, // title: 'Assigned to me', // description: 'Tasks assigned to you vs others', // }, priority: { data: choresPriorityChartData || [], title: 'Priority', description: 'Tasks by priority', }, labels: { data: choresLabelsChartData || [], title: 'Labels', description: 'Tasks by labels', }, labelsDuration: { data: choresLabelsDurationChartData || [], title: 'Labels (time)', description: 'Time spent by labels (hours)', }, tasksTime: { data: tasksTimeChartData || [], title: 'Tasks (time)', description: 'Time spent by individual tasks (hours)', }, assigneeBreakdown: { data: choresAssigneeBreakdownChartData || [], title: 'by Assignee', description: 'Tasks grouped by assignee', }, } if (!userProfile) { return } if (!choresData.res?.length > 0 || !choresHistory?.length > 0) { return ( No activities You have no activities for the selected period. ) } // Calculate activities analytics return ( {/* Main Content Area - Mobile: Stack vertically, Desktop: Side by side */} {/* Left Side - Timeline with Filters (Mobile: Full width, Desktop: Flexible) */} {/* Improved Filter Bar - Now above timeline */} Filter Activities {/* User Filter */} Show activities for: {/* Time Period Filter */} Time period: { setTabValue(tabValue) refetchHistory(tabValue) }} value={tabValue} sx={{ borderRadius: 8, backgroundColor: 'background.surface', border: '1px solid', borderColor: 'divider', }} > {[ { label: '7 Days', value: 7 }, { label: '30 Days', value: 30 }, { label: '90 Days', value: 90 }, { label: 'All Time', value: 365 }, ].map((tab, index) => ( {tab.label} ))} {/* Current Filter Summary */} Showing activities for{' '} {selectedUser === undefined || selectedUser === 'all' ? 'All Users' : circleUsers.find(user => user.userId === selectedUser) ?.displayName || 'Unknown User'} {' '} over the{' '} {tabValue === 365 ? 'All Time' : `Last ${tabValue} Days`} {/* Right Sidebar - Charts (Mobile: Full width, Desktop: Fixed width + sticky) */} {/* Charts Container */} {/* Main Chart */} {chartData[selectedChart].title} {chartData[selectedChart].description} {renderPieChart( chartData[selectedChart].data, 300, // Increased size for better chart container true, selectedChart, )} {/* Chart Selection Grid */} {Object.entries(chartData) .filter(([key]) => key !== selectedChart) .map(([key, { data, title }]) => ( setSelectedChart(key)} variant='plain' sx={{ cursor: 'pointer', p: 1, transition: 'all 0.2s ease-in-out', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', minHeight: 80, maxWidth: 90, '&:hover': { transform: 'scale(1.02)', boxShadow: 'sm', }, }} > {title} {renderPieChart(data, 70, false)} ))} ) } export default UserActivites