345 lines
11 KiB
React
345 lines
11 KiB
React
![]() |
import { Checklist, EventBusy, Timelapse } from '@mui/icons-material'
|
||
|
import {
|
||
|
Avatar,
|
||
|
Box,
|
||
|
Button,
|
||
|
Chip,
|
||
|
CircularProgress,
|
||
|
Container,
|
||
|
Grid,
|
||
|
List,
|
||
|
ListDivider,
|
||
|
ListItem,
|
||
|
ListItemContent,
|
||
|
ListItemDecorator,
|
||
|
Sheet,
|
||
|
Typography,
|
||
|
} from '@mui/joy'
|
||
|
import moment from 'moment'
|
||
|
import React, { useEffect, useState } from 'react'
|
||
|
import { Link, useParams } from 'react-router-dom'
|
||
|
import { API_URL } from '../../Config'
|
||
|
import { GetAllCircleMembers } from '../../utils/Fetcher'
|
||
|
import { Fetch } from '../../utils/TokenManager'
|
||
|
|
||
|
const ChoreHistory = () => {
|
||
|
const [choreHistory, setChoresHistory] = useState([])
|
||
|
const [userHistory, setUserHistory] = useState([])
|
||
|
const [performers, setPerformers] = useState([])
|
||
|
const [historyInfo, setHistoryInfo] = useState([])
|
||
|
|
||
|
const [isLoading, setIsLoading] = useState(true) // Add loading state
|
||
|
const { choreId } = useParams()
|
||
|
|
||
|
useEffect(() => {
|
||
|
setIsLoading(true) // Start loading
|
||
|
|
||
|
Promise.all([
|
||
|
Fetch(`${API_URL}/chores/${choreId}/history`).then(res => res.json()),
|
||
|
GetAllCircleMembers().then(res => res.json()),
|
||
|
])
|
||
|
.then(([historyData, usersData]) => {
|
||
|
setChoresHistory(historyData.res)
|
||
|
|
||
|
const newUserChoreHistory = {}
|
||
|
historyData.res.forEach(choreHistory => {
|
||
|
const userId = choreHistory.completedBy
|
||
|
newUserChoreHistory[userId] = (newUserChoreHistory[userId] || 0) + 1
|
||
|
})
|
||
|
setUserHistory(newUserChoreHistory)
|
||
|
|
||
|
setPerformers(usersData.res)
|
||
|
updateHistoryInfo(historyData.res, newUserChoreHistory, usersData.res)
|
||
|
})
|
||
|
.catch(error => {
|
||
|
console.error('Error fetching data:', error)
|
||
|
// Handle errors, e.g., show an error message to the user
|
||
|
})
|
||
|
.finally(() => {
|
||
|
setIsLoading(false) // Finish loading
|
||
|
})
|
||
|
}, [choreId])
|
||
|
|
||
|
const updateHistoryInfo = (histories, userHistories, performers) => {
|
||
|
// average delay for task completaion from due date:
|
||
|
|
||
|
const averageDelay =
|
||
|
histories.reduce((acc, chore) => {
|
||
|
if (chore.dueDate) {
|
||
|
// Only consider chores with a due date
|
||
|
return acc + moment(chore.completedAt).diff(chore.dueDate, 'hours')
|
||
|
}
|
||
|
return acc
|
||
|
}, 0) / histories.length
|
||
|
const averageDelayMoment = moment.duration(averageDelay, 'hours')
|
||
|
const maximumDelay = histories.reduce((acc, chore) => {
|
||
|
if (chore.dueDate) {
|
||
|
// Only consider chores with a due date
|
||
|
const delay = moment(chore.completedAt).diff(chore.dueDate, 'hours')
|
||
|
return delay > acc ? delay : acc
|
||
|
}
|
||
|
return acc
|
||
|
}, 0)
|
||
|
|
||
|
const maxDelayMoment = moment.duration(maximumDelay, 'hours')
|
||
|
|
||
|
// find max value in userHistories:
|
||
|
const userCompletedByMost = Object.keys(userHistories).reduce((a, b) =>
|
||
|
userHistories[a] > userHistories[b] ? a : b,
|
||
|
)
|
||
|
const userCompletedByLeast = Object.keys(userHistories).reduce((a, b) =>
|
||
|
userHistories[a] < userHistories[b] ? a : b,
|
||
|
)
|
||
|
|
||
|
const historyInfo = [
|
||
|
{
|
||
|
icon: (
|
||
|
<Avatar>
|
||
|
<Checklist />
|
||
|
</Avatar>
|
||
|
),
|
||
|
text: `${histories.length} completed`,
|
||
|
subtext: `${Object.keys(userHistories).length} users contributed`,
|
||
|
},
|
||
|
{
|
||
|
icon: (
|
||
|
<Avatar>
|
||
|
<Timelapse />
|
||
|
</Avatar>
|
||
|
),
|
||
|
text: `Completed within ${moment
|
||
|
.duration(averageDelayMoment)
|
||
|
.humanize()}`,
|
||
|
subtext: `Maximum delay was ${moment
|
||
|
.duration(maxDelayMoment)
|
||
|
.humanize()}`,
|
||
|
},
|
||
|
{
|
||
|
icon: <Avatar></Avatar>,
|
||
|
text: `${
|
||
|
performers.find(p => p.userId === Number(userCompletedByMost))
|
||
|
?.displayName
|
||
|
} completed most`,
|
||
|
subtext: `${userHistories[userCompletedByMost]} time/s`,
|
||
|
},
|
||
|
]
|
||
|
if (userCompletedByLeast !== userCompletedByMost) {
|
||
|
historyInfo.push({
|
||
|
icon: (
|
||
|
<Avatar>
|
||
|
{
|
||
|
performers.find(p => p.userId === userCompletedByLeast)
|
||
|
?.displayName
|
||
|
}
|
||
|
</Avatar>
|
||
|
),
|
||
|
text: `${
|
||
|
performers.find(p => p.userId === Number(userCompletedByLeast))
|
||
|
.displayName
|
||
|
} completed least`,
|
||
|
subtext: `${userHistories[userCompletedByLeast]} time/s`,
|
||
|
})
|
||
|
}
|
||
|
|
||
|
setHistoryInfo(historyInfo)
|
||
|
}
|
||
|
|
||
|
function formatTimeDifference(startDate, endDate) {
|
||
|
const diffInMinutes = moment(startDate).diff(endDate, 'minutes')
|
||
|
let timeValue = diffInMinutes
|
||
|
let unit = 'minute'
|
||
|
|
||
|
if (diffInMinutes >= 60) {
|
||
|
const diffInHours = moment(startDate).diff(endDate, 'hours')
|
||
|
timeValue = diffInHours
|
||
|
unit = 'hour'
|
||
|
|
||
|
if (diffInHours >= 24) {
|
||
|
const diffInDays = moment(startDate).diff(endDate, 'days')
|
||
|
timeValue = diffInDays
|
||
|
unit = 'day'
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return `${timeValue} ${unit}${timeValue !== 1 ? 's' : ''}`
|
||
|
}
|
||
|
if (isLoading) {
|
||
|
return <CircularProgress /> // Show loading indicator
|
||
|
}
|
||
|
if (!choreHistory.length) {
|
||
|
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',
|
||
|
// color: 'text.disabled',
|
||
|
mb: 1,
|
||
|
}}
|
||
|
/>
|
||
|
|
||
|
<Typography level='h3' gutterBottom>
|
||
|
No History Yet
|
||
|
</Typography>
|
||
|
<Typography level='body1'>
|
||
|
You haven't completed any tasks. Once you start finishing tasks,
|
||
|
they'll show up here.
|
||
|
</Typography>
|
||
|
<Button variant='soft' sx={{ mt: 2 }}>
|
||
|
<Link to='/my/chores'>Go back to chores</Link>
|
||
|
</Button>
|
||
|
</Container>
|
||
|
)
|
||
|
}
|
||
|
|
||
|
return (
|
||
|
<Container maxWidth='md'>
|
||
|
<Typography level='h3' mb={1.5}>
|
||
|
Summary:
|
||
|
</Typography>
|
||
|
{/* <Sheet sx={{ mb: 1, borderRadius: 'sm', p: 2, boxShadow: 'md' }}>
|
||
|
<ListItem sx={{ gap: 1.5 }}>
|
||
|
<ListItemDecorator>
|
||
|
<Avatar>
|
||
|
<AccountCircle />
|
||
|
</Avatar>
|
||
|
</ListItemDecorator>
|
||
|
<ListItemContent>
|
||
|
<Typography level='body1' sx={{ fontWeight: 'md' }}>
|
||
|
{choreHistory.length} completed
|
||
|
</Typography>
|
||
|
<Typography level='body2' color='text.tertiary'>
|
||
|
{Object.keys(userHistory).length} users contributed
|
||
|
</Typography>
|
||
|
</ListItemContent>
|
||
|
</ListItem>
|
||
|
</Sheet> */}
|
||
|
<Grid container>
|
||
|
{historyInfo.map((info, index) => (
|
||
|
<Grid key={index} item xs={12} sm={6}>
|
||
|
<Sheet sx={{ mb: 1, borderRadius: 'sm', p: 2, boxShadow: 'md' }}>
|
||
|
<ListItem sx={{ gap: 1.5 }}>
|
||
|
<ListItemDecorator>{info.icon}</ListItemDecorator>
|
||
|
<ListItemContent>
|
||
|
<Typography level='body1' sx={{ fontWeight: 'md' }}>
|
||
|
{info.text}
|
||
|
</Typography>
|
||
|
<Typography level='body1' color='text.tertiary'>
|
||
|
{info.subtext}
|
||
|
</Typography>
|
||
|
</ListItemContent>
|
||
|
</ListItem>
|
||
|
</Sheet>
|
||
|
</Grid>
|
||
|
))}
|
||
|
</Grid>
|
||
|
{/* User History Cards */}
|
||
|
<Typography level='h3' my={1.5}>
|
||
|
History:
|
||
|
</Typography>
|
||
|
<Box sx={{ borderRadius: 'sm', p: 2, boxShadow: 'md' }}>
|
||
|
{/* Chore History List (Updated Style) */}
|
||
|
|
||
|
<List sx={{ p: 0 }}>
|
||
|
{choreHistory.map((chore, index) => (
|
||
|
<>
|
||
|
<ListItem sx={{ gap: 1.5, alignItems: 'flex-start' }}>
|
||
|
{' '}
|
||
|
{/* Adjusted spacing and alignment */}
|
||
|
<ListItemDecorator>
|
||
|
<Avatar sx={{ mr: 1 }}>
|
||
|
{performers
|
||
|
.find(p => p.userId === chore.completedBy)
|
||
|
?.displayName?.charAt(0) || '?'}
|
||
|
</Avatar>
|
||
|
</ListItemDecorator>
|
||
|
<ListItemContent sx={{ my: 0 }}>
|
||
|
{' '}
|
||
|
{/* Removed vertical margin */}
|
||
|
<Box
|
||
|
sx={{
|
||
|
display: 'flex',
|
||
|
justifyContent: 'space-between',
|
||
|
alignItems: 'center',
|
||
|
}}
|
||
|
>
|
||
|
<Typography level='body1' sx={{ fontWeight: 'md' }}>
|
||
|
{moment(chore.completedAt).format('ddd MM/DD/yyyy HH:mm')}
|
||
|
</Typography>
|
||
|
|
||
|
<Chip>
|
||
|
{chore.dueDate && chore.completedAt > chore.dueDate
|
||
|
? 'Late'
|
||
|
: 'On Time'}
|
||
|
</Chip>
|
||
|
</Box>
|
||
|
<Typography level='body2' color='text.tertiary'>
|
||
|
<Chip>
|
||
|
{
|
||
|
performers.find(p => p.userId === chore.completedBy)
|
||
|
?.displayName
|
||
|
}
|
||
|
</Chip>{' '}
|
||
|
completed
|
||
|
{chore.completedBy !== chore.assignedTo && (
|
||
|
<>
|
||
|
{', '}
|
||
|
assigned to{' '}
|
||
|
<Chip>
|
||
|
{
|
||
|
performers.find(p => p.userId === chore.assignedTo)
|
||
|
?.displayName
|
||
|
}
|
||
|
</Chip>
|
||
|
</>
|
||
|
)}
|
||
|
</Typography>
|
||
|
{chore.dueDate && (
|
||
|
<Typography level='body2' color='text.tertiary'>
|
||
|
Due: {moment(chore.dueDate).format('ddd MM/DD/yyyy')}
|
||
|
</Typography>
|
||
|
)}
|
||
|
{chore.notes && (
|
||
|
<Typography level='body2' color='text.tertiary'>
|
||
|
Note: {chore.notes}
|
||
|
</Typography>
|
||
|
)}
|
||
|
</ListItemContent>
|
||
|
</ListItem>
|
||
|
{index < choreHistory.length - 1 && (
|
||
|
<>
|
||
|
<ListDivider component='li'>
|
||
|
{/* time between two completion: */}
|
||
|
{index < choreHistory.length - 1 &&
|
||
|
choreHistory[index + 1].completedAt && (
|
||
|
<Typography level='body3' color='text.tertiary'>
|
||
|
{formatTimeDifference(
|
||
|
chore.completedAt,
|
||
|
choreHistory[index + 1].completedAt,
|
||
|
)}{' '}
|
||
|
before
|
||
|
</Typography>
|
||
|
)}
|
||
|
</ListDivider>
|
||
|
</>
|
||
|
)}
|
||
|
</>
|
||
|
))}
|
||
|
</List>
|
||
|
</Box>
|
||
|
</Container>
|
||
|
)
|
||
|
}
|
||
|
|
||
|
export default ChoreHistory
|