Add Points view, Don't allow completion outside the completion window, migrate more things to react query
This commit is contained in:
commit
c5e89c1fb6
14 changed files with 628 additions and 30 deletions
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "donetick",
|
"name": "donetick",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.85",
|
"version": "0.1.90",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"*.{js,jsx,ts,tsx}": [
|
"*.{js,jsx,ts,tsx}": [
|
||||||
|
|
|
@ -23,6 +23,7 @@ import TestView from '../views/TestView/Test'
|
||||||
import ThingsHistory from '../views/Things/ThingsHistory'
|
import ThingsHistory from '../views/Things/ThingsHistory'
|
||||||
import ThingsView from '../views/Things/ThingsView'
|
import ThingsView from '../views/Things/ThingsView'
|
||||||
import UserActivities from '../views/User/UserActivities'
|
import UserActivities from '../views/User/UserActivities'
|
||||||
|
import UserPoints from '../views/User/UserPoints'
|
||||||
const getMainRoute = () => {
|
const getMainRoute = () => {
|
||||||
if (import.meta.env.VITE_IS_LANDING_DEFAULT === 'true') {
|
if (import.meta.env.VITE_IS_LANDING_DEFAULT === 'true') {
|
||||||
return <Landing />
|
return <Landing />
|
||||||
|
@ -71,6 +72,10 @@ const Router = createBrowserRouter([
|
||||||
path: '/activities',
|
path: '/activities',
|
||||||
element: <UserActivities />,
|
element: <UserActivities />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/points',
|
||||||
|
element: <UserPoints />,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/login',
|
path: '/login',
|
||||||
element: <LoginView />,
|
element: <LoginView />,
|
||||||
|
|
|
@ -19,11 +19,11 @@ export const useCreateChore = () => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useChoresHistory = initialLimit => {
|
export const useChoresHistory = (initialLimit, includeMembers) => {
|
||||||
const [limit, setLimit] = useState(initialLimit) // Initially, no limit is selected
|
const [limit, setLimit] = useState(initialLimit) // Initially, no limit is selected
|
||||||
|
|
||||||
const { data, error, isLoading } = useQuery(['choresHistory', limit], () =>
|
const { data, error, isLoading } = useQuery(['choresHistory', limit], () =>
|
||||||
GetChoresHistory(limit),
|
GetChoresHistory(limit, includeMembers),
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleLimitChange = newLimit => {
|
const handleLimitChange = newLimit => {
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
import { useQuery } from 'react-query'
|
import { useQuery } from 'react-query'
|
||||||
import { GetAllUsers } from '../utils/Fetcher'
|
import { GetAllCircleMembers, GetAllUsers } from '../utils/Fetcher'
|
||||||
|
|
||||||
export const useAllUsers = () => {
|
export const useAllUsers = () => {
|
||||||
return useQuery('allUsers', GetAllUsers)
|
return useQuery('allUsers', GetAllUsers)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const useCircleMembers = () => {
|
||||||
|
return useQuery('allCircleMembers', GetAllCircleMembers)
|
||||||
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import moment from 'moment'
|
||||||
import { TASK_COLOR } from './Colors.jsx'
|
import { TASK_COLOR } from './Colors.jsx'
|
||||||
|
|
||||||
export const ChoresGrouper = (groupBy, chores) => {
|
export const ChoresGrouper = (groupBy, chores) => {
|
||||||
|
@ -158,3 +159,12 @@ export const ChoresGrouper = (groupBy, chores) => {
|
||||||
}
|
}
|
||||||
return groups
|
return groups
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const notInCompletionWindow = chore => {
|
||||||
|
return (
|
||||||
|
chore.completionWindow &&
|
||||||
|
chore.completionWindow > -1 &&
|
||||||
|
chore.nextDueDate &&
|
||||||
|
moment().add(chore.completionWindow, 'hours') < moment(chore.nextDueDate)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -11,7 +11,8 @@ const createChore = userID => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const signUp = (username, password, displayName, email) => {
|
const signUp = (username, password, displayName, email) => {
|
||||||
return fetch(`/auth/`, {
|
const baseURL = apiManager.getApiURL()
|
||||||
|
return fetch(`${baseURL}/auth/`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
@ -193,11 +194,12 @@ const UpdateChoreHistory = (choreId, id, choreHistory) => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const GetAllCircleMembers = () => {
|
const GetAllCircleMembers = async () => {
|
||||||
return Fetch(`/circles/members`, {
|
const resp = await Fetch(`/circles/members`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: HEADERS(),
|
headers: HEADERS(),
|
||||||
})
|
})
|
||||||
|
return resp.json()
|
||||||
}
|
}
|
||||||
|
|
||||||
const GetUserProfile = () => {
|
const GetUserProfile = () => {
|
||||||
|
@ -420,11 +422,16 @@ const RefreshToken = () => {
|
||||||
headers: HEADERS(),
|
headers: HEADERS(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
const GetChoresHistory = async limit => {
|
const GetChoresHistory = async (limit, includeMembers) => {
|
||||||
var url = `/chores/history`
|
var url = `/chores/history`
|
||||||
|
if (!limit) limit = 7
|
||||||
|
|
||||||
if (limit) {
|
if (limit) {
|
||||||
url += `?limit=${limit}`
|
url += `?limit=${limit}`
|
||||||
}
|
}
|
||||||
|
if (includeMembers) {
|
||||||
|
url += `&members=true`
|
||||||
|
}
|
||||||
const resp = await Fetch(url, {
|
const resp = await Fetch(url, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: HEADERS(),
|
headers: HEADERS(),
|
||||||
|
|
|
@ -75,6 +75,7 @@ const ChoreEdit = () => {
|
||||||
const [labels, setLabels] = useState([])
|
const [labels, setLabels] = useState([])
|
||||||
const [labelsV2, setLabelsV2] = useState([])
|
const [labelsV2, setLabelsV2] = useState([])
|
||||||
const [points, setPoints] = useState(-1)
|
const [points, setPoints] = useState(-1)
|
||||||
|
const [completionWindow, setCompletionWindow] = useState(-1)
|
||||||
const [allUserThings, setAllUserThings] = useState([])
|
const [allUserThings, setAllUserThings] = useState([])
|
||||||
const [thingTrigger, setThingTrigger] = useState(null)
|
const [thingTrigger, setThingTrigger] = useState(null)
|
||||||
const [isThingValid, setIsThingValid] = useState(false)
|
const [isThingValid, setIsThingValid] = useState(false)
|
||||||
|
@ -200,6 +201,7 @@ const ChoreEdit = () => {
|
||||||
notificationMetadata: notificationMetadata,
|
notificationMetadata: notificationMetadata,
|
||||||
thingTrigger: thingTrigger,
|
thingTrigger: thingTrigger,
|
||||||
points: points < 0 ? null : points,
|
points: points < 0 ? null : points,
|
||||||
|
completionWindow: completionWindow < 0 ? null : completionWindow,
|
||||||
}
|
}
|
||||||
let SaveFunction = CreateChore
|
let SaveFunction = CreateChore
|
||||||
if (choreId > 0) {
|
if (choreId > 0) {
|
||||||
|
@ -216,11 +218,9 @@ const ChoreEdit = () => {
|
||||||
}
|
}
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
//fetch performers:
|
//fetch performers:
|
||||||
GetAllCircleMembers()
|
GetAllCircleMembers().then(data => {
|
||||||
.then(response => response.json())
|
setPerformers(data.res)
|
||||||
.then(data => {
|
})
|
||||||
setPerformers(data.res)
|
|
||||||
})
|
|
||||||
GetThings().then(response => {
|
GetThings().then(response => {
|
||||||
response.json().then(data => {
|
response.json().then(data => {
|
||||||
setAllUserThings(data.res)
|
setAllUserThings(data.res)
|
||||||
|
@ -254,7 +254,11 @@ const ChoreEdit = () => {
|
||||||
setPoints(
|
setPoints(
|
||||||
data.res.points && data.res.points > -1 ? data.res.points : -1,
|
data.res.points && data.res.points > -1 ? data.res.points : -1,
|
||||||
)
|
)
|
||||||
// setLabels(data.res.labels ? data.res.labels.split(',') : [])
|
setCompletionWindow(
|
||||||
|
data.res.completionWindow && data.res.completionWindow > -1
|
||||||
|
? data.res.completionWindow
|
||||||
|
: -1,
|
||||||
|
)
|
||||||
|
|
||||||
setLabelsV2(data.res.labelsV2)
|
setLabelsV2(data.res.labelsV2)
|
||||||
setAssignStrategy(
|
setAssignStrategy(
|
||||||
|
@ -570,6 +574,70 @@ const ChoreEdit = () => {
|
||||||
<FormHelperText>{errors.dueDate}</FormHelperText>
|
<FormHelperText>{errors.dueDate}</FormHelperText>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<FormControl
|
||||||
|
orientation='horizontal'
|
||||||
|
sx={{ width: 400, justifyContent: 'space-between' }}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
{/* <FormLabel>Completion window (hours)</FormLabel> */}
|
||||||
|
<Typography level='h5'>Completion window (hours)</Typography>
|
||||||
|
|
||||||
|
<FormHelperText sx={{ mt: 0 }}>
|
||||||
|
{"Set a time window that task can't be completed before"}
|
||||||
|
</FormHelperText>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={completionWindow != -1}
|
||||||
|
onClick={event => {
|
||||||
|
event.preventDefault()
|
||||||
|
if (completionWindow != -1) {
|
||||||
|
setCompletionWindow(-1)
|
||||||
|
} else {
|
||||||
|
setCompletionWindow(1)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
color={completionWindow !== -1 ? 'success' : 'neutral'}
|
||||||
|
variant={completionWindow !== -1 ? 'solid' : 'outlined'}
|
||||||
|
// endDecorator={points !== -1 ? 'On' : 'Off'}
|
||||||
|
slotProps={{
|
||||||
|
endDecorator: {
|
||||||
|
sx: {
|
||||||
|
minWidth: 24,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
{completionWindow != -1 && (
|
||||||
|
<Card variant='outlined'>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
mt: 0,
|
||||||
|
ml: 4,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography level='body-sm'>Hours:</Typography>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
type='number'
|
||||||
|
value={completionWindow}
|
||||||
|
sx={{ maxWidth: 100 }}
|
||||||
|
// add min points is 0 and max is 1000
|
||||||
|
slotProps={{
|
||||||
|
input: {
|
||||||
|
min: 0,
|
||||||
|
max: 24 * 7,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
placeholder='Hours'
|
||||||
|
onChange={e => {
|
||||||
|
setCompletionWindow(parseInt(e.target.value))
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
{!['once', 'no_repeat'].includes(frequencyType) && (
|
{!['once', 'no_repeat'].includes(frequencyType) && (
|
||||||
<Box mt={2}>
|
<Box mt={2}>
|
||||||
|
|
|
@ -36,6 +36,7 @@ import { Divider } from '@mui/material'
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useNavigate, useParams, useSearchParams } from 'react-router-dom'
|
import { useNavigate, useParams, useSearchParams } from 'react-router-dom'
|
||||||
|
import { notInCompletionWindow } from '../../utils/Chores.jsx'
|
||||||
import { getTextColorFromBackgroundColor } from '../../utils/Colors.jsx'
|
import { getTextColorFromBackgroundColor } from '../../utils/Colors.jsx'
|
||||||
import {
|
import {
|
||||||
GetAllUsers,
|
GetAllUsers,
|
||||||
|
@ -563,7 +564,7 @@ const ChoreView = () => {
|
||||||
fullWidth
|
fullWidth
|
||||||
size='lg'
|
size='lg'
|
||||||
onClick={handleTaskCompletion}
|
onClick={handleTaskCompletion}
|
||||||
disabled={isPendingCompletion}
|
disabled={isPendingCompletion || notInCompletionWindow(chore)}
|
||||||
color={isPendingCompletion ? 'danger' : 'success'}
|
color={isPendingCompletion ? 'danger' : 'success'}
|
||||||
startDecorator={<Check />}
|
startDecorator={<Check />}
|
||||||
sx={{
|
sx={{
|
||||||
|
|
|
@ -43,6 +43,7 @@ import moment from 'moment'
|
||||||
import React, { useEffect } from 'react'
|
import React, { useEffect } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { UserContext } from '../../contexts/UserContext'
|
import { UserContext } from '../../contexts/UserContext'
|
||||||
|
import { notInCompletionWindow } from '../../utils/Chores.jsx'
|
||||||
import { getTextColorFromBackgroundColor } from '../../utils/Colors.jsx'
|
import { getTextColorFromBackgroundColor } from '../../utils/Colors.jsx'
|
||||||
import {
|
import {
|
||||||
ArchiveChore,
|
ArchiveChore,
|
||||||
|
@ -412,7 +413,6 @@ const ChoreCard = ({
|
||||||
}
|
}
|
||||||
return name
|
return name
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box key={chore.id + '-box'}>
|
<Box key={chore.id + '-box'}>
|
||||||
<Chip
|
<Chip
|
||||||
|
@ -571,7 +571,7 @@ const ChoreCard = ({
|
||||||
variant='solid'
|
variant='solid'
|
||||||
color='success'
|
color='success'
|
||||||
onClick={handleTaskCompletion}
|
onClick={handleTaskCompletion}
|
||||||
disabled={isPendingCompletion}
|
disabled={isPendingCompletion || notInCompletionWindow(chore)}
|
||||||
sx={{
|
sx={{
|
||||||
borderRadius: '50%',
|
borderRadius: '50%',
|
||||||
minWidth: 50,
|
minWidth: 50,
|
||||||
|
|
|
@ -14,14 +14,12 @@ import {
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { Link, useParams } from 'react-router-dom'
|
import { Link, useParams } from 'react-router-dom'
|
||||||
import { API_URL } from '../../Config'
|
|
||||||
import {
|
import {
|
||||||
DeleteChoreHistory,
|
DeleteChoreHistory,
|
||||||
GetAllCircleMembers,
|
GetAllCircleMembers,
|
||||||
GetChoreHistory,
|
GetChoreHistory,
|
||||||
UpdateChoreHistory,
|
UpdateChoreHistory,
|
||||||
} from '../../utils/Fetcher'
|
} from '../../utils/Fetcher'
|
||||||
import { Fetch } from '../../utils/TokenManager'
|
|
||||||
import LoadingComponent from '../components/Loading'
|
import LoadingComponent from '../components/Loading'
|
||||||
import EditHistoryModal from '../Modals/EditHistoryModal'
|
import EditHistoryModal from '../Modals/EditHistoryModal'
|
||||||
import HistoryCard from './HistoryCard'
|
import HistoryCard from './HistoryCard'
|
||||||
|
@ -42,7 +40,7 @@ const ChoreHistory = () => {
|
||||||
|
|
||||||
Promise.all([
|
Promise.all([
|
||||||
GetChoreHistory(choreId).then(res => res.json()),
|
GetChoreHistory(choreId).then(res => res.json()),
|
||||||
GetAllCircleMembers().then(res => res.json()),
|
GetAllCircleMembers(),
|
||||||
])
|
])
|
||||||
.then(([historyData, usersData]) => {
|
.then(([historyData, usersData]) => {
|
||||||
setChoresHistory(historyData.res)
|
setChoresHistory(historyData.res)
|
||||||
|
|
|
@ -54,11 +54,9 @@ const Settings = () => {
|
||||||
setCircleMemberRequests(data.res ? data.res : [])
|
setCircleMemberRequests(data.res ? data.res : [])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
GetAllCircleMembers()
|
GetAllCircleMembers().then(data => {
|
||||||
.then(res => res.json())
|
setCircleMembers(data.res ? data.res : [])
|
||||||
.then(data => {
|
})
|
||||||
setCircleMembers(data.res ? data.res : [])
|
|
||||||
})
|
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
@ -3,14 +3,16 @@ import CheckCircleIcon from '@mui/icons-material/CheckCircle'
|
||||||
import CircleIcon from '@mui/icons-material/Circle'
|
import CircleIcon from '@mui/icons-material/Circle'
|
||||||
import { Cell, Legend, Pie, PieChart, Tooltip } from 'recharts'
|
import { Cell, Legend, Pie, PieChart, Tooltip } from 'recharts'
|
||||||
|
|
||||||
import { Toll } from '@mui/icons-material'
|
import { EventBusy, Toll } from '@mui/icons-material'
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
|
Button,
|
||||||
Card,
|
Card,
|
||||||
Chip,
|
Chip,
|
||||||
Container,
|
Container,
|
||||||
Divider,
|
Divider,
|
||||||
Grid,
|
Grid,
|
||||||
|
Link,
|
||||||
Stack,
|
Stack,
|
||||||
Tab,
|
Tab,
|
||||||
TabList,
|
TabList,
|
||||||
|
@ -173,7 +175,7 @@ const UserActivites = () => {
|
||||||
data: choresHistory,
|
data: choresHistory,
|
||||||
isChoresHistoryLoading,
|
isChoresHistoryLoading,
|
||||||
handleLimitChange: refetchHistory,
|
handleLimitChange: refetchHistory,
|
||||||
} = useChoresHistory(tabValue ? tabValue : 30)
|
} = useChoresHistory(tabValue ? tabValue : 30, false)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isChoresHistoryLoading && !isChoresLoading && choresHistory) {
|
if (!isChoresHistoryLoading && !isChoresLoading && choresHistory) {
|
||||||
const enrichedHistory = choresHistory.res.map(item => {
|
const enrichedHistory = choresHistory.res.map(item => {
|
||||||
|
@ -306,6 +308,40 @@ const UserActivites = () => {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!choresData.res?.length > 0 || !choresHistory?.res?.length > 0) {
|
||||||
|
return (
|
||||||
|
<Container
|
||||||
|
maxWidth='md'
|
||||||
|
sx={{
|
||||||
|
textAlign: 'center',
|
||||||
|
display: 'flex',
|
||||||
|
// make sure the content is centered vertically:
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
flexDirection: 'column',
|
||||||
|
height: '50vh',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<EventBusy
|
||||||
|
sx={{
|
||||||
|
fontSize: '6rem',
|
||||||
|
mb: 1,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Typography level='h3' gutterBottom>
|
||||||
|
No activities
|
||||||
|
</Typography>
|
||||||
|
<Typography level='body1'>
|
||||||
|
You have no activities for the selected period.
|
||||||
|
</Typography>
|
||||||
|
<Button variant='soft' sx={{ mt: 2 }}>
|
||||||
|
<Link to='/my/chores'>Go back to chores</Link>
|
||||||
|
</Button>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container
|
<Container
|
||||||
maxWidth='md'
|
maxWidth='md'
|
||||||
|
|
465
src/views/User/UserPoints.jsx
Normal file
465
src/views/User/UserPoints.jsx
Normal file
|
@ -0,0 +1,465 @@
|
||||||
|
import {
|
||||||
|
Bar,
|
||||||
|
BarChart,
|
||||||
|
CartesianGrid,
|
||||||
|
ResponsiveContainer,
|
||||||
|
XAxis,
|
||||||
|
YAxis,
|
||||||
|
} from 'recharts'
|
||||||
|
|
||||||
|
import { Toll } from '@mui/icons-material'
|
||||||
|
import {
|
||||||
|
Avatar,
|
||||||
|
Box,
|
||||||
|
Card,
|
||||||
|
Chip,
|
||||||
|
Container,
|
||||||
|
Option,
|
||||||
|
Select,
|
||||||
|
Tab,
|
||||||
|
TabList,
|
||||||
|
Tabs,
|
||||||
|
Typography,
|
||||||
|
} from '@mui/joy'
|
||||||
|
import { useContext, useEffect, useState } from 'react'
|
||||||
|
import { UserContext } from '../../contexts/UserContext.js'
|
||||||
|
import LoadingComponent from '../components/Loading.jsx'
|
||||||
|
|
||||||
|
import { useChoresHistory } from '../../queries/ChoreQueries.jsx'
|
||||||
|
import { useCircleMembers } from '../../queries/UserQueries.jsx'
|
||||||
|
const UserPoints = () => {
|
||||||
|
const [tabValue, setTabValue] = useState(7)
|
||||||
|
|
||||||
|
const { data: circleMembersData, isLoading: isCircleMembersLoading } =
|
||||||
|
useCircleMembers()
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: choresHistoryData,
|
||||||
|
isLoading: isChoresHistoryLoading,
|
||||||
|
handleLimitChange: handleChoresHistoryLimitChange,
|
||||||
|
} = useChoresHistory(7)
|
||||||
|
|
||||||
|
const { userProfile } = useContext(UserContext)
|
||||||
|
const [selectedUser, setSelectedUser] = useState(userProfile?.id)
|
||||||
|
const [circleUsers, setCircleUsers] = useState([])
|
||||||
|
const [selectedHistory, setSelectedHistory] = useState([])
|
||||||
|
const [userPointsBarChartData, setUserPointsBarChartData] = useState([])
|
||||||
|
|
||||||
|
const [choresHistory, setChoresHistory] = useState([])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (circleMembersData && choresHistoryData && userProfile) {
|
||||||
|
setCircleUsers(circleMembersData.res)
|
||||||
|
setSelectedHistory(generateWeeklySummary(choresHistory, userProfile?.id))
|
||||||
|
}
|
||||||
|
}, [circleMembersData, choresHistoryData])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (choresHistoryData) {
|
||||||
|
var history
|
||||||
|
if (tabValue === 7) {
|
||||||
|
history = generateWeeklySummary(choresHistoryData.res, selectedUser)
|
||||||
|
} else if (tabValue === 30) {
|
||||||
|
history = generateMonthSummary(choresHistoryData.res, selectedUser)
|
||||||
|
} else if (tabValue === 6 * 30) {
|
||||||
|
history = generateMonthlySummary(choresHistoryData.res, selectedUser)
|
||||||
|
} else {
|
||||||
|
history = generateYearlySummary(choresHistoryData.res, selectedUser)
|
||||||
|
}
|
||||||
|
setSelectedHistory(history)
|
||||||
|
}
|
||||||
|
}, [selectedUser, choresHistoryData])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setSelectedUser(userProfile?.id)
|
||||||
|
}, [userProfile])
|
||||||
|
|
||||||
|
const generateUserPointsHistory = history => {
|
||||||
|
const userPoints = {}
|
||||||
|
for (let i = 0; i < history.length; i++) {
|
||||||
|
const chore = history[i]
|
||||||
|
if (!userPoints[chore.completedBy]) {
|
||||||
|
userPoints[chore.completedBy] = chore.points ? chore.points : 0
|
||||||
|
} else {
|
||||||
|
userPoints[chore.completedBy] += chore.points ? chore.points : 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return userPoints
|
||||||
|
}
|
||||||
|
|
||||||
|
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.completedAt).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.completedAt).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.completedAt).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.completedAt).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) {
|
||||||
|
return <LoadingComponent />
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container
|
||||||
|
maxWidth='md'
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
mb: 4,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography level='h4'>Points Overview</Typography>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
gap: 1,
|
||||||
|
my: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
sx={{
|
||||||
|
width: 200,
|
||||||
|
}}
|
||||||
|
variant='soft'
|
||||||
|
label='User'
|
||||||
|
value={selectedUser}
|
||||||
|
onChange={(e, selected) => {
|
||||||
|
setSelectedUser(selected)
|
||||||
|
setSelectedHistory(generateWeeklySummary(choresHistory, selected))
|
||||||
|
}}
|
||||||
|
renderValue={selected => (
|
||||||
|
<Typography
|
||||||
|
startDecorator={
|
||||||
|
<Avatar color='primary' m={0} size='sm'>
|
||||||
|
{
|
||||||
|
circleUsers.find(user => user.userId === selectedUser)
|
||||||
|
?.displayName[0]
|
||||||
|
}
|
||||||
|
</Avatar>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
circleUsers.find(user => user.userId === selectedUser)
|
||||||
|
?.displayName
|
||||||
|
}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{circleUsers.map(user => (
|
||||||
|
<Option key={user.userId} value={user.userId}>
|
||||||
|
<Typography>{user.displayName}</Typography>
|
||||||
|
<Chip
|
||||||
|
color='success'
|
||||||
|
size='sm'
|
||||||
|
variant='soft'
|
||||||
|
startDecorator={<Toll />}
|
||||||
|
>
|
||||||
|
{user.points - user.pointsRedeemed}
|
||||||
|
</Chip>
|
||||||
|
</Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
// resposive width based on parent available space:
|
||||||
|
width: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-evenly',
|
||||||
|
gap: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{[
|
||||||
|
{
|
||||||
|
title: 'Total',
|
||||||
|
value: circleMembersData.res.find(
|
||||||
|
user => user.userId === selectedUser,
|
||||||
|
)?.points,
|
||||||
|
color: 'primary',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Available',
|
||||||
|
value: (function () {
|
||||||
|
const user = circleMembersData.res.find(
|
||||||
|
user => user.userId === selectedUser,
|
||||||
|
)
|
||||||
|
if (!user) return 0
|
||||||
|
return user.points - user.pointsRedeemed
|
||||||
|
})(),
|
||||||
|
|
||||||
|
color: 'success',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Redeemed',
|
||||||
|
value: circleMembersData.res.find(
|
||||||
|
user => user.userId === selectedUser,
|
||||||
|
)?.pointsRedeemed,
|
||||||
|
color: 'warning',
|
||||||
|
},
|
||||||
|
].map(card => (
|
||||||
|
<Card
|
||||||
|
key={card.title}
|
||||||
|
sx={{
|
||||||
|
p: 2,
|
||||||
|
mb: 1,
|
||||||
|
minWidth: 80,
|
||||||
|
width: '100%',
|
||||||
|
}}
|
||||||
|
variant='soft'
|
||||||
|
>
|
||||||
|
<Typography level='body-xs' textAlign='center' mb={-1}>
|
||||||
|
{card.title}
|
||||||
|
</Typography>
|
||||||
|
<Typography level='title-md' textAlign='center'>
|
||||||
|
{card.value}
|
||||||
|
</Typography>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
<Typography level='h4'>Points History</Typography>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
// center vertically:
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'left',
|
||||||
|
gap: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Tabs
|
||||||
|
onChange={(e, tabValue) => {
|
||||||
|
setTabValue(tabValue)
|
||||||
|
handleChoresHistoryLimitChange(tabValue)
|
||||||
|
}}
|
||||||
|
defaultValue={tabValue}
|
||||||
|
sx={{
|
||||||
|
py: 0.5,
|
||||||
|
borderRadius: 16,
|
||||||
|
maxWidth: 400,
|
||||||
|
mb: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TabList
|
||||||
|
disableUnderline
|
||||||
|
sx={{
|
||||||
|
borderRadius: 16,
|
||||||
|
backgroundColor: 'background.paper',
|
||||||
|
boxShadow: 1,
|
||||||
|
justifyContent: 'space-evenly',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{[
|
||||||
|
{ label: '7 Days', value: 7 },
|
||||||
|
// { label: '3 Month', value: 30 },
|
||||||
|
{ label: '6 Months', value: 6 * 30 },
|
||||||
|
{ label: 'All Time', value: 90 },
|
||||||
|
].map((tab, index) => (
|
||||||
|
<Tab
|
||||||
|
key={index}
|
||||||
|
sx={{
|
||||||
|
borderRadius: 16,
|
||||||
|
color: 'text.secondary',
|
||||||
|
'&.Mui-selected': {
|
||||||
|
color: 'text.primary',
|
||||||
|
backgroundColor: 'primary.light',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
disableIndicator
|
||||||
|
value={tab.value}
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
</Tab>
|
||||||
|
))}
|
||||||
|
</TabList>
|
||||||
|
</Tabs>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
// resposive width based on parent available space:
|
||||||
|
width: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'left',
|
||||||
|
gap: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{[
|
||||||
|
{
|
||||||
|
title: 'Points',
|
||||||
|
value: selectedHistory.reduce((acc, cur) => acc + cur.points, 0),
|
||||||
|
color: 'success',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Tasks',
|
||||||
|
value: selectedHistory.reduce((acc, cur) => acc + cur.tasks, 0),
|
||||||
|
color: 'primary',
|
||||||
|
},
|
||||||
|
].map(card => (
|
||||||
|
<Card
|
||||||
|
key={card.title}
|
||||||
|
sx={{
|
||||||
|
p: 2,
|
||||||
|
mb: 1,
|
||||||
|
width: 250,
|
||||||
|
}}
|
||||||
|
variant='soft'
|
||||||
|
>
|
||||||
|
<Typography level='body-xs' textAlign='center' mb={-1}>
|
||||||
|
{card.title}
|
||||||
|
</Typography>
|
||||||
|
<Typography level='title-md' textAlign='center'>
|
||||||
|
{card.value}
|
||||||
|
</Typography>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</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]}
|
||||||
|
>
|
||||||
|
{/* Rounded top corners, blue fill, set bar width */}
|
||||||
|
{/* Add a slightly darker top section to the 'Jul' bar */}
|
||||||
|
</Bar>
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UserPoints
|
|
@ -9,6 +9,7 @@ import {
|
||||||
Message,
|
Message,
|
||||||
SettingsOutlined,
|
SettingsOutlined,
|
||||||
ShareOutlined,
|
ShareOutlined,
|
||||||
|
Toll,
|
||||||
Widgets,
|
Widgets,
|
||||||
} from '@mui/icons-material'
|
} from '@mui/icons-material'
|
||||||
import {
|
import {
|
||||||
|
@ -43,15 +44,20 @@ const links = [
|
||||||
label: 'Things',
|
label: 'Things',
|
||||||
icon: <Widgets />,
|
icon: <Widgets />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
to: 'labels',
|
||||||
|
label: 'Labels',
|
||||||
|
icon: <ListAlt />,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
to: 'activities',
|
to: 'activities',
|
||||||
label: 'Activities',
|
label: 'Activities',
|
||||||
icon: <History />,
|
icon: <History />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
to: 'labels',
|
to: 'points',
|
||||||
label: 'Labels',
|
label: 'Points',
|
||||||
icon: <ListAlt />,
|
icon: <Toll />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
to: '/settings#sharing',
|
to: '/settings#sharing',
|
||||||
|
|
Loading…
Add table
Reference in a new issue