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