968 lines
31 KiB
JavaScript
968 lines
31 KiB
JavaScript
import {
|
|
Bar,
|
|
BarChart,
|
|
CartesianGrid,
|
|
ResponsiveContainer,
|
|
XAxis,
|
|
YAxis,
|
|
} from 'recharts'
|
|
|
|
import {
|
|
AccountBalanceWallet,
|
|
Analytics,
|
|
AssignmentTurnedIn,
|
|
CreditCard,
|
|
EmojiEvents,
|
|
MilitaryTech,
|
|
Redeem,
|
|
Star,
|
|
SwapHoriz,
|
|
Timeline,
|
|
Toll,
|
|
TrendingUp,
|
|
WorkspacePremium,
|
|
} from '@mui/icons-material'
|
|
import {
|
|
Avatar,
|
|
Box,
|
|
Button,
|
|
Card,
|
|
CardContent,
|
|
Chip,
|
|
Container,
|
|
Grid,
|
|
Option,
|
|
Select,
|
|
Stack,
|
|
Tab,
|
|
TabList,
|
|
Tabs,
|
|
Typography,
|
|
} from '@mui/joy'
|
|
import { useEffect, useState } from 'react'
|
|
import LoadingComponent from '../components/Loading.jsx'
|
|
|
|
import { useChoresHistory } from '../../queries/ChoreQueries.jsx'
|
|
import { useCircleMembers, useUserProfile } from '../../queries/UserQueries.jsx'
|
|
import { RedeemPoints } from '../../utils/Fetcher.jsx'
|
|
import { resolvePhotoURL } from '../../utils/Helpers.jsx'
|
|
import RedeemPointsModal from '../Modals/RedeemPointsModal'
|
|
const UserPoints = () => {
|
|
const [tabValue, setTabValue] = useState(7)
|
|
const [isRedeemModalOpen, setIsRedeemModalOpen] = useState(false)
|
|
const [leaderboardMode, setLeaderboardMode] = useState('points') // 'points' or 'tasks'
|
|
|
|
const {
|
|
data: circleMembersData,
|
|
isLoading: isCircleMembersLoading,
|
|
handleRefetch: handleCircleMembersRefetch,
|
|
} = useCircleMembers()
|
|
|
|
const {
|
|
data: choresHistoryData,
|
|
isLoading: isChoresHistoryLoading,
|
|
handleLimitChange: handleChoresHistoryLimitChange,
|
|
} = useChoresHistory(7)
|
|
|
|
const { data: userProfile } = useUserProfile()
|
|
const [selectedUser, setSelectedUser] = useState(userProfile?.id)
|
|
const [circleUsers, setCircleUsers] = useState([])
|
|
const [selectedHistory, setSelectedHistory] = useState([])
|
|
|
|
useEffect(() => {
|
|
if (circleMembersData && choresHistoryData && userProfile) {
|
|
setCircleUsers(circleMembersData.res)
|
|
setSelectedHistory(
|
|
generateWeeklySummary(choresHistoryData, userProfile?.id),
|
|
)
|
|
}
|
|
}, [circleMembersData, choresHistoryData, userProfile])
|
|
|
|
useEffect(() => {
|
|
if (choresHistoryData) {
|
|
var history
|
|
if (tabValue === 7) {
|
|
history = generateWeeklySummary(choresHistoryData, selectedUser)
|
|
} else if (tabValue === 30) {
|
|
history = generateMonthSummary(choresHistoryData, selectedUser)
|
|
} else if (tabValue === 6 * 30) {
|
|
history = generateMonthlySummary(choresHistoryData, selectedUser)
|
|
} else {
|
|
history = generateYearlySummary(choresHistoryData, selectedUser)
|
|
}
|
|
setSelectedHistory(history)
|
|
}
|
|
}, [selectedUser, choresHistoryData, tabValue])
|
|
|
|
useEffect(() => {
|
|
setSelectedUser(userProfile?.id)
|
|
}, [userProfile])
|
|
|
|
const generateWeeklySummary = (history, userId) => {
|
|
const daysAggregated = []
|
|
for (let i = 6; i > -1; i--) {
|
|
const currentDate = new Date()
|
|
currentDate.setDate(currentDate.getDate() - i)
|
|
daysAggregated.push({
|
|
label: currentDate.toLocaleString('en-US', { weekday: 'short' }),
|
|
points: 0,
|
|
tasks: 0,
|
|
})
|
|
}
|
|
history.forEach(chore => {
|
|
const dayName = new Date(chore.performedAt).toLocaleString('en-US', {
|
|
weekday: 'short',
|
|
})
|
|
|
|
const dayIndex = daysAggregated.findIndex(dayData => {
|
|
if (userId)
|
|
return dayData.label === dayName && chore.completedBy === userId
|
|
return dayData.label === dayName
|
|
})
|
|
if (dayIndex !== -1) {
|
|
if (chore.points) daysAggregated[dayIndex].points += chore.points
|
|
daysAggregated[dayIndex].tasks += 1
|
|
}
|
|
})
|
|
return daysAggregated
|
|
}
|
|
|
|
const generateMonthSummary = (history, userId) => {
|
|
const daysAggregated = []
|
|
for (let i = 29; i > -1; i--) {
|
|
const currentDate = new Date()
|
|
currentDate.setDate(currentDate.getDate() - i)
|
|
daysAggregated.push({
|
|
label: currentDate.toLocaleString('en-US', { day: 'numeric' }),
|
|
points: 0,
|
|
tasks: 0,
|
|
})
|
|
}
|
|
history.forEach(chore => {
|
|
const dayName = new Date(chore.performedAt).toLocaleString('en-US', {
|
|
day: 'numeric',
|
|
})
|
|
|
|
const dayIndex = daysAggregated.findIndex(dayData => {
|
|
if (userId)
|
|
return dayData.label === dayName && chore.completedBy === userId
|
|
return dayData.label === dayName
|
|
})
|
|
|
|
if (dayIndex !== -1) {
|
|
if (chore.points) daysAggregated[dayIndex].points += chore.points
|
|
daysAggregated[dayIndex].tasks += 1
|
|
}
|
|
})
|
|
|
|
return daysAggregated
|
|
}
|
|
|
|
const generateMonthlySummary = (history, userId) => {
|
|
const monthlyAggregated = []
|
|
for (let i = 5; i > -1; i--) {
|
|
const currentMonth = new Date()
|
|
currentMonth.setMonth(currentMonth.getMonth() - i)
|
|
monthlyAggregated.push({
|
|
label: currentMonth.toLocaleString('en-US', { month: 'short' }),
|
|
points: 0,
|
|
tasks: 0,
|
|
})
|
|
}
|
|
history.forEach(chore => {
|
|
const monthName = new Date(chore.performedAt).toLocaleString('en-US', {
|
|
month: 'short',
|
|
})
|
|
|
|
const monthIndex = monthlyAggregated.findIndex(monthData => {
|
|
if (userId)
|
|
return monthData.label === monthName && chore.completedBy === userId
|
|
return monthData.label === monthName
|
|
})
|
|
|
|
if (monthIndex !== -1) {
|
|
if (chore.points) monthlyAggregated[monthIndex].points += chore.points
|
|
monthlyAggregated[monthIndex].tasks += 1
|
|
}
|
|
})
|
|
return monthlyAggregated
|
|
}
|
|
|
|
const generateYearlySummary = (history, userId) => {
|
|
const yearlyAggregated = []
|
|
|
|
for (let i = 11; i > -1; i--) {
|
|
const currentYear = new Date()
|
|
currentYear.setFullYear(currentYear.getFullYear() - i)
|
|
yearlyAggregated.push({
|
|
label: currentYear.toLocaleString('en-US', { year: 'numeric' }),
|
|
points: 0,
|
|
tasks: 0,
|
|
})
|
|
}
|
|
history.forEach(chore => {
|
|
const yearName = new Date(chore.performedAt).toLocaleString('en-US', {
|
|
year: 'numeric',
|
|
})
|
|
|
|
const yearIndex = yearlyAggregated.findIndex(yearData => {
|
|
if (userId)
|
|
return yearData.label === yearName && chore.completedBy === userId
|
|
return yearData.label === yearName
|
|
})
|
|
|
|
if (yearIndex !== -1) {
|
|
if (chore.points) yearlyAggregated[yearIndex].points += chore.points
|
|
yearlyAggregated[yearIndex].tasks += 1
|
|
}
|
|
})
|
|
return yearlyAggregated
|
|
}
|
|
|
|
if (isChoresHistoryLoading || isCircleMembersLoading || !userProfile) {
|
|
return <LoadingComponent />
|
|
}
|
|
|
|
// Calculate leaderboard data for the current time period
|
|
const calculateLeaderboard = () => {
|
|
if (!choresHistoryData || !circleUsers.length) return []
|
|
|
|
// Calculate points for each user in the current time period
|
|
const userPeriodStats = {}
|
|
|
|
// Initialize stats for all users
|
|
circleUsers.forEach(user => {
|
|
userPeriodStats[user.userId] = {
|
|
userId: user.userId,
|
|
displayName: user.displayName,
|
|
image: user.image,
|
|
totalPoints: user.points || 0,
|
|
availablePoints: (user.points || 0) - (user.pointsRedeemed || 0),
|
|
periodPoints: 0,
|
|
periodTasks: 0,
|
|
}
|
|
})
|
|
|
|
// Calculate period-specific stats from history
|
|
choresHistoryData.forEach(historyEntry => {
|
|
const userId = historyEntry.completedBy
|
|
if (userPeriodStats[userId]) {
|
|
userPeriodStats[userId].periodPoints += historyEntry.points || 0
|
|
userPeriodStats[userId].periodTasks += 1
|
|
}
|
|
})
|
|
|
|
// Convert to array and sort by selected mode
|
|
const sortField =
|
|
leaderboardMode === 'points' ? 'periodPoints' : 'periodTasks'
|
|
return Object.values(userPeriodStats)
|
|
.sort((a, b) => b[sortField] - a[sortField])
|
|
.map((user, index) => ({
|
|
...user,
|
|
rank: index + 1,
|
|
avgPointsPerTask:
|
|
user.periodTasks > 0
|
|
? (user.periodPoints / user.periodTasks).toFixed(1)
|
|
: 0,
|
|
}))
|
|
}
|
|
|
|
const leaderboardData = calculateLeaderboard()
|
|
|
|
// Get trophy icons for top 3
|
|
const getTrophyIcon = rank => {
|
|
switch (rank) {
|
|
case 1:
|
|
return <EmojiEvents sx={{ color: '#FFD700', fontSize: '1.2rem' }} /> // Gold
|
|
case 2:
|
|
return (
|
|
<WorkspacePremium sx={{ color: '#C0C0C0', fontSize: '1.2rem' }} />
|
|
) // Silver
|
|
case 3:
|
|
return <MilitaryTech sx={{ color: '#CD7F32', fontSize: '1.2rem' }} /> // Bronze
|
|
default:
|
|
return <Star sx={{ color: 'text.secondary', fontSize: '1rem' }} />
|
|
}
|
|
}
|
|
|
|
return (
|
|
<Container
|
|
maxWidth='md'
|
|
sx={{
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
px: { xs: 2, sm: 3 },
|
|
}}
|
|
>
|
|
{/* Enhanced Leaderboard Header Section */}
|
|
<Box sx={{ mb: 4 }}>
|
|
<Stack spacing={2}>
|
|
{/* Title Row */}
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
|
<EmojiEvents sx={{ fontSize: '2rem', color: '#FFD700' }} />
|
|
<Stack sx={{ flex: 1 }}>
|
|
<Typography
|
|
level='h3'
|
|
sx={{ fontWeight: 'lg', color: 'text.primary' }}
|
|
>
|
|
{leaderboardMode === 'points' ? 'Points' : 'Tasks'} Leaderboard
|
|
</Typography>
|
|
<Typography level='body-sm' sx={{ color: 'text.secondary' }}>
|
|
Rankings based on{' '}
|
|
{leaderboardMode === 'points'
|
|
? 'points earned'
|
|
: 'tasks completed'}{' '}
|
|
during the selected time period
|
|
</Typography>
|
|
</Stack>
|
|
</Box>
|
|
|
|
{/* Filters Row - Responsive */}
|
|
<Box
|
|
sx={{
|
|
display: 'flex',
|
|
flexDirection: { xs: 'column', sm: 'row' },
|
|
gap: 2,
|
|
alignItems: { xs: 'stretch', sm: 'center' },
|
|
justifyContent: { xs: 'flex-start', sm: 'space-between' },
|
|
}}
|
|
>
|
|
{/* Time Period Filter */}
|
|
<Box
|
|
sx={{
|
|
display: 'flex',
|
|
justifyContent: { xs: 'center', sm: 'flex-start' },
|
|
}}
|
|
>
|
|
<Tabs
|
|
onChange={(e, tabValue) => {
|
|
setTabValue(tabValue)
|
|
handleChoresHistoryLimitChange(tabValue)
|
|
}}
|
|
value={tabValue}
|
|
size='sm'
|
|
sx={{
|
|
borderRadius: 6,
|
|
backgroundColor: 'background.surface',
|
|
border: '1px solid',
|
|
borderColor: 'divider',
|
|
}}
|
|
>
|
|
<TabList
|
|
disableUnderline
|
|
sx={{
|
|
borderRadius: 6,
|
|
backgroundColor: 'transparent',
|
|
p: 0.3,
|
|
gap: 0.3,
|
|
}}
|
|
>
|
|
{[
|
|
{ label: '7D', value: 7 },
|
|
{ label: '6M', value: 6 * 30 },
|
|
{ label: 'All', value: 24 * 30 },
|
|
].map((tab, index) => (
|
|
<Tab
|
|
key={index}
|
|
sx={{
|
|
borderRadius: 4,
|
|
minWidth: 'auto',
|
|
px: 1.5,
|
|
py: 0.5,
|
|
fontSize: 'xs',
|
|
fontWeight: 500,
|
|
color: 'text.secondary',
|
|
'&.Mui-selected': {
|
|
color: 'primary.plainColor',
|
|
backgroundColor: 'primary.softBg',
|
|
fontWeight: 600,
|
|
},
|
|
'&:hover': {
|
|
backgroundColor: 'neutral.softHoverBg',
|
|
},
|
|
}}
|
|
disableIndicator
|
|
value={tab.value}
|
|
>
|
|
{tab.label}
|
|
</Tab>
|
|
))}
|
|
</TabList>
|
|
</Tabs>
|
|
</Box>
|
|
|
|
{/* Toggle between points and tasks */}
|
|
<Box
|
|
sx={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: { xs: 'center', sm: 'flex-end' },
|
|
gap: 1,
|
|
mb: 1,
|
|
}}
|
|
>
|
|
<Chip
|
|
variant={leaderboardMode === 'points' ? 'solid' : 'outlined'}
|
|
color='primary'
|
|
size='sm'
|
|
sx={{ cursor: 'pointer' }}
|
|
onClick={() => setLeaderboardMode('points')}
|
|
>
|
|
Points
|
|
</Chip>
|
|
<SwapHoriz
|
|
sx={{ fontSize: '0.875rem', color: 'text.tertiary' }}
|
|
/>
|
|
<Chip
|
|
variant={leaderboardMode === 'tasks' ? 'solid' : 'outlined'}
|
|
color='primary'
|
|
size='sm'
|
|
sx={{ cursor: 'pointer' }}
|
|
onClick={() => setLeaderboardMode('tasks')}
|
|
>
|
|
Tasks
|
|
</Chip>
|
|
</Box>
|
|
</Box>
|
|
</Stack>
|
|
|
|
{/* Leaderboard Cards */}
|
|
<Card
|
|
variant='outlined'
|
|
sx={{ borderRadius: 'lg', overflow: 'hidden' }}
|
|
>
|
|
<Stack spacing={0}>
|
|
{leaderboardData.map((user, index) => (
|
|
<Box key={user.userId}>
|
|
<Box
|
|
sx={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
p: 2.5,
|
|
backgroundColor:
|
|
user.userId === userProfile.id
|
|
? 'primary.softBg'
|
|
: 'transparent',
|
|
position: 'relative',
|
|
'&:hover': {
|
|
backgroundColor:
|
|
user.userId === userProfile.id
|
|
? 'primary.softHoverBg'
|
|
: 'neutral.softHoverBg',
|
|
},
|
|
cursor:
|
|
user.userId === selectedUser ? 'default' : 'pointer',
|
|
transition: 'background-color 0.2s ease',
|
|
}}
|
|
onClick={() => {
|
|
if (user.userId !== selectedUser) {
|
|
setSelectedUser(user.userId)
|
|
setSelectedHistory(
|
|
generateWeeklySummary(choresHistoryData, user.userId),
|
|
)
|
|
}
|
|
}}
|
|
>
|
|
{/* Rank Badge */}
|
|
<Box
|
|
sx={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
minWidth: 40,
|
|
mr: 2,
|
|
}}
|
|
>
|
|
{getTrophyIcon(user.rank)}
|
|
<Typography
|
|
level='body-sm'
|
|
sx={{
|
|
ml: 0.5,
|
|
fontWeight: user.rank <= 3 ? 'bold' : 'normal',
|
|
color:
|
|
user.rank <= 3 ? 'text.primary' : 'text.secondary',
|
|
}}
|
|
>
|
|
#{user.rank}
|
|
</Typography>
|
|
</Box>
|
|
|
|
{/* User Avatar and Info */}
|
|
<Box sx={{ display: 'flex', alignItems: 'center', flex: 1 }}>
|
|
<Avatar
|
|
src={user.image ? resolvePhotoURL(user.image) : undefined}
|
|
sx={{ width: 40, height: 40, mr: 2 }}
|
|
>
|
|
{user.displayName?.charAt(0)}
|
|
</Avatar>
|
|
<Stack sx={{ flex: 1 }}>
|
|
<Typography
|
|
level='body-md'
|
|
sx={{
|
|
fontWeight:
|
|
user.userId === userProfile.id ? 'bold' : 'normal',
|
|
color: 'text.primary',
|
|
}}
|
|
>
|
|
{user.displayName}
|
|
{user.userId === userProfile.id && (
|
|
<Chip
|
|
size='sm'
|
|
variant='soft'
|
|
color='primary'
|
|
sx={{ ml: 1 }}
|
|
>
|
|
You
|
|
</Chip>
|
|
)}
|
|
</Typography>
|
|
<Typography
|
|
level='body-xs'
|
|
sx={{ color: 'text.secondary' }}
|
|
>
|
|
{user.periodTasks} tasks • {user.avgPointsPerTask} avg
|
|
per task
|
|
</Typography>
|
|
</Stack>
|
|
</Box>
|
|
|
|
{/* Metric Display */}
|
|
<Stack alignItems='flex-end' spacing={0.5}>
|
|
<Box
|
|
sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}
|
|
>
|
|
{leaderboardMode === 'points' ? (
|
|
<Toll sx={{ fontSize: '1rem', color: 'success.500' }} />
|
|
) : (
|
|
<AssignmentTurnedIn
|
|
sx={{ fontSize: '1rem', color: 'success.500' }}
|
|
/>
|
|
)}
|
|
<Typography
|
|
level='title-md'
|
|
sx={{
|
|
fontWeight: 'bold',
|
|
color: 'success.600',
|
|
}}
|
|
>
|
|
{leaderboardMode === 'points'
|
|
? user.periodPoints
|
|
: user.periodTasks}
|
|
</Typography>
|
|
</Box>
|
|
<Typography level='body-xs' sx={{ color: 'text.tertiary' }}>
|
|
{leaderboardMode === 'points'
|
|
? `${user.availablePoints} available`
|
|
: `${user.periodPoints} points`}
|
|
</Typography>
|
|
</Stack>
|
|
|
|
{/* Selection Indicator */}
|
|
{user.userId === selectedUser && (
|
|
<Box
|
|
sx={{
|
|
position: 'absolute',
|
|
left: 0,
|
|
top: 0,
|
|
bottom: 0,
|
|
width: 4,
|
|
backgroundColor: 'primary.500',
|
|
borderRadius: '0 4px 4px 0',
|
|
}}
|
|
/>
|
|
)}
|
|
</Box>
|
|
{index < leaderboardData.length - 1 && (
|
|
<Box
|
|
sx={{
|
|
height: 1,
|
|
backgroundColor: 'divider',
|
|
mx: 2.5,
|
|
}}
|
|
/>
|
|
)}
|
|
</Box>
|
|
))}
|
|
</Stack>
|
|
</Card>
|
|
</Box>
|
|
|
|
{/* Filters Section Header */}
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}>
|
|
<Analytics sx={{ fontSize: '1.5rem', color: 'primary.500' }} />
|
|
<Typography level='h4' sx={{ fontWeight: 'lg', color: 'text.primary' }}>
|
|
Filter & Analysis
|
|
</Typography>
|
|
</Box>
|
|
|
|
{/* Improved Filter Bar */}
|
|
<Card
|
|
variant='outlined'
|
|
sx={{
|
|
width: '100%',
|
|
p: 2,
|
|
mb: 3,
|
|
borderRadius: 12,
|
|
background:
|
|
'linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.05) 100%)',
|
|
backdropFilter: 'blur(10px)',
|
|
}}
|
|
>
|
|
<Stack spacing={2}>
|
|
<Typography level='title-sm' sx={{ color: 'text.secondary' }}>
|
|
Filter Points
|
|
</Typography>
|
|
|
|
<Stack
|
|
direction={{ xs: 'column', sm: 'row' }}
|
|
spacing={2}
|
|
alignItems={{ xs: 'stretch', sm: 'center' }}
|
|
>
|
|
{/* User Filter */}
|
|
<Box sx={{ flex: 1, minWidth: 200 }}>
|
|
<Typography level='body-sm' sx={{ mb: 1, fontWeight: 500 }}>
|
|
Show points for:
|
|
</Typography>
|
|
<Select
|
|
sx={{
|
|
width: '100%',
|
|
}}
|
|
variant='outlined'
|
|
value={selectedUser}
|
|
onChange={(e, selected) => {
|
|
setSelectedUser(selected)
|
|
setSelectedHistory(
|
|
generateWeeklySummary(choresHistoryData, selected),
|
|
)
|
|
}}
|
|
renderValue={() => {
|
|
return (
|
|
<Typography
|
|
startDecorator={
|
|
<Avatar
|
|
color='primary'
|
|
size='sm'
|
|
src={resolvePhotoURL(
|
|
circleUsers.find(
|
|
user => user.userId === selectedUser,
|
|
)?.image,
|
|
)}
|
|
>
|
|
{circleUsers
|
|
.find(user => user.userId === selectedUser)
|
|
?.displayName?.charAt(0)}
|
|
</Avatar>
|
|
}
|
|
>
|
|
{
|
|
circleUsers.find(user => user.userId === selectedUser)
|
|
?.displayName
|
|
}
|
|
</Typography>
|
|
)
|
|
}}
|
|
>
|
|
{circleUsers.map(user => (
|
|
<Option key={user.userId} value={user.userId}>
|
|
<Avatar
|
|
color='primary'
|
|
size='sm'
|
|
src={resolvePhotoURL(user.image)}
|
|
>
|
|
{user.displayName?.charAt(0)}
|
|
</Avatar>
|
|
<Typography>{user.displayName}</Typography>
|
|
<Chip
|
|
color='success'
|
|
size='sm'
|
|
variant='soft'
|
|
startDecorator={<Toll />}
|
|
>
|
|
{user.points - user.pointsRedeemed}
|
|
</Chip>
|
|
</Option>
|
|
))}
|
|
</Select>
|
|
</Box>
|
|
|
|
{/* Time Period Filter */}
|
|
<Box sx={{ flex: 1, minWidth: 200 }}>
|
|
<Typography level='body-sm' sx={{ mb: 1, fontWeight: 500 }}>
|
|
Time period:
|
|
</Typography>
|
|
<Tabs
|
|
onChange={(e, tabValue) => {
|
|
setTabValue(tabValue)
|
|
handleChoresHistoryLimitChange(tabValue)
|
|
}}
|
|
value={tabValue}
|
|
sx={{
|
|
borderRadius: 8,
|
|
backgroundColor: 'background.surface',
|
|
border: '1px solid',
|
|
borderColor: 'divider',
|
|
}}
|
|
>
|
|
<TabList
|
|
disableUnderline
|
|
sx={{
|
|
borderRadius: 8,
|
|
backgroundColor: 'transparent',
|
|
p: 0.5,
|
|
gap: 0.5,
|
|
}}
|
|
>
|
|
{[
|
|
{ label: '7 Days', value: 7 },
|
|
{ label: '6 Months', value: 6 * 30 },
|
|
{ label: 'All Time', value: 24 * 30 },
|
|
].map((tab, index) => (
|
|
<Tab
|
|
key={index}
|
|
sx={{
|
|
borderRadius: 6,
|
|
minWidth: 'auto',
|
|
px: 2,
|
|
py: 1,
|
|
fontSize: 'sm',
|
|
fontWeight: 500,
|
|
color: 'text.secondary',
|
|
'&.Mui-selected': {
|
|
color: 'primary.plainColor',
|
|
backgroundColor: 'primary.softBg',
|
|
fontWeight: 600,
|
|
},
|
|
'&:hover': {
|
|
backgroundColor: 'neutral.softHoverBg',
|
|
},
|
|
}}
|
|
disableIndicator
|
|
value={tab.value}
|
|
>
|
|
{tab.label}
|
|
</Tab>
|
|
))}
|
|
</TabList>
|
|
</Tabs>
|
|
</Box>
|
|
|
|
{/* Redeem Points Button */}
|
|
{circleUsers.find(user => user.userId === userProfile.id)?.role ===
|
|
'admin' && (
|
|
<Box sx={{ display: 'flex', alignItems: 'flex-end' }}>
|
|
<Button
|
|
variant='soft'
|
|
size='md'
|
|
startDecorator={<CreditCard />}
|
|
onClick={() => {
|
|
setIsRedeemModalOpen(true)
|
|
}}
|
|
sx={{ mt: 'auto' }}
|
|
>
|
|
Redeem Points
|
|
</Button>
|
|
</Box>
|
|
)}
|
|
</Stack>
|
|
</Stack>
|
|
</Card>
|
|
|
|
{/* Points Status Cards */}
|
|
<Grid container spacing={1} sx={{ mb: 3 }}>
|
|
{(() => {
|
|
const selectedUserData = circleUsers.find(
|
|
user => user.userId === selectedUser,
|
|
)
|
|
const totalPoints = selectedUserData?.points || 0
|
|
const redeemedPoints = selectedUserData?.pointsRedeemed || 0
|
|
const availablePoints = totalPoints - redeemedPoints
|
|
|
|
const periodStats = leaderboardData.find(
|
|
user => user.userId === selectedUser,
|
|
)
|
|
const periodPoints = selectedHistory.reduce(
|
|
(sum, item) => sum + (item.points || 0),
|
|
0,
|
|
)
|
|
|
|
const pointsCards = [
|
|
{
|
|
icon: <Toll />,
|
|
title: 'Available',
|
|
text: `${availablePoints} points`,
|
|
subtext: 'Ready to redeem',
|
|
},
|
|
{
|
|
icon: <Redeem />,
|
|
title: 'Redeemed',
|
|
text: `${redeemedPoints} points`,
|
|
subtext: 'Previously used',
|
|
},
|
|
{
|
|
icon: <AccountBalanceWallet />,
|
|
title: 'Total',
|
|
text: `${totalPoints} points`,
|
|
subtext: 'All time earned',
|
|
},
|
|
{
|
|
icon: <TrendingUp />,
|
|
title: 'Period Points',
|
|
text: `${periodPoints} points`,
|
|
subtext: `${tabValue === 24 * 30 ? 'All time' : tabValue === 6 * 30 ? 'Last 6 months' : `Last ${tabValue} days`}`,
|
|
},
|
|
]
|
|
|
|
return pointsCards.map((card, index) => (
|
|
<Grid item xs={6} sm={6} key={index}>
|
|
<Card
|
|
variant='soft'
|
|
sx={{
|
|
borderRadius: 'md',
|
|
boxShadow: 1,
|
|
px: 2,
|
|
py: 1,
|
|
minHeight: 90,
|
|
height: '100%',
|
|
justifyContent: 'start',
|
|
}}
|
|
>
|
|
<CardContent>
|
|
<Box
|
|
sx={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'start',
|
|
mb: 0.5,
|
|
}}
|
|
>
|
|
{card.icon}
|
|
<Typography
|
|
level='body-md'
|
|
sx={{
|
|
ml: 1,
|
|
fontWeight: '500',
|
|
color: 'text.primary',
|
|
}}
|
|
>
|
|
{card.title}
|
|
</Typography>
|
|
</Box>
|
|
<Box>
|
|
<Typography
|
|
level='body-sm'
|
|
sx={{ color: 'text.secondary', lineHeight: 1.5 }}
|
|
>
|
|
{card.text}
|
|
</Typography>
|
|
<Typography
|
|
level='body-sm'
|
|
sx={{ color: 'text.secondary', lineHeight: 1.5 }}
|
|
>
|
|
{card.subtext}
|
|
</Typography>
|
|
</Box>
|
|
</CardContent>
|
|
</Card>
|
|
</Grid>
|
|
))
|
|
})()}
|
|
</Grid>
|
|
|
|
{/* Current Filter Summary */}
|
|
<Box sx={{ mb: 3, textAlign: 'center' }}>
|
|
<Typography level='body-sm' sx={{ color: 'text.secondary' }}>
|
|
Showing points for{' '}
|
|
<Typography
|
|
component='span'
|
|
sx={{ fontWeight: 600, color: 'primary.500' }}
|
|
>
|
|
{circleUsers.find(user => user.userId === selectedUser)
|
|
?.displayName || 'Unknown User'}
|
|
</Typography>{' '}
|
|
over the{' '}
|
|
<Typography
|
|
component='span'
|
|
sx={{ fontWeight: 600, color: 'primary.500' }}
|
|
>
|
|
{tabValue === 24 * 30
|
|
? 'All Time'
|
|
: tabValue === 6 * 30
|
|
? 'Last 6 Months'
|
|
: `Last ${tabValue} Days`}
|
|
</Typography>
|
|
</Typography>
|
|
</Box>
|
|
|
|
<Box
|
|
sx={{
|
|
mb: 4,
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
gap: 3,
|
|
}}
|
|
>
|
|
{/* Chart Section Header */}
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}>
|
|
<Timeline sx={{ fontSize: '1.5rem', color: 'primary.500' }} />
|
|
<Typography
|
|
level='h4'
|
|
sx={{ fontWeight: 'lg', color: 'text.primary' }}
|
|
>
|
|
Points Trend
|
|
</Typography>
|
|
</Box>
|
|
|
|
{/* Bar Chart for points overtime */}
|
|
<Box sx={{ display: 'flex', justifyContent: 'center', gap: 1 }}>
|
|
<ResponsiveContainer height={300}>
|
|
<BarChart
|
|
data={selectedHistory}
|
|
margin={{ top: 5, left: -20, bottom: 5 }}
|
|
>
|
|
<CartesianGrid strokeDasharray={'3 3'} />
|
|
<XAxis dataKey='label' axisLine={false} tickLine={false} />
|
|
<YAxis axisLine={false} tickLine={false} />
|
|
<Bar
|
|
fill='#4183F2'
|
|
dataKey='points'
|
|
barSize={30}
|
|
radius={[5, 5, 0, 0]}
|
|
/>
|
|
</BarChart>
|
|
</ResponsiveContainer>
|
|
</Box>
|
|
</Box>
|
|
|
|
<RedeemPointsModal
|
|
config={(() => {
|
|
const user = circleUsers.find(u => u.userId === selectedUser)
|
|
const availablePoints = user
|
|
? (user.points || 0) - (user.pointsRedeemed || 0)
|
|
: 0
|
|
|
|
return {
|
|
onClose: () => {
|
|
setIsRedeemModalOpen(false)
|
|
},
|
|
isOpen: isRedeemModalOpen,
|
|
available: availablePoints,
|
|
user: user,
|
|
onSave: ({ userId, points }) => {
|
|
RedeemPoints(userId, points, userProfile.circleID)
|
|
.then(() => {
|
|
setIsRedeemModalOpen(false)
|
|
handleCircleMembersRefetch()
|
|
})
|
|
.catch(err => {
|
|
console.log(err)
|
|
})
|
|
},
|
|
}
|
|
})()}
|
|
/>
|
|
</Container>
|
|
)
|
|
}
|
|
|
|
export default UserPoints
|