diff options
Diffstat (limited to '')
-rw-r--r-- | src/views/History/BigChip.jsx | 26 | ||||
-rw-r--r-- | src/views/History/ChoreHistory.jsx | 344 | ||||
-rw-r--r-- | src/views/History/InfoCard.jsx | 26 |
3 files changed, 396 insertions, 0 deletions
diff --git a/src/views/History/BigChip.jsx b/src/views/History/BigChip.jsx new file mode 100644 index 0000000..4987367 --- /dev/null +++ b/src/views/History/BigChip.jsx @@ -0,0 +1,26 @@ +import Chip from '@mui/joy/Chip' +import * as React from 'react' + +function BigChip(props) { + return ( + <Chip + variant='outlined' + color='primary' + size='lg' // Adjust to your desired size + sx={{ + fontSize: '1rem', // Example: Increase font size + padding: '1rem', // Example: Increase padding + height: '1rem', // Adjust to your desired height + // Add other custom styles as needed + }} + {...props} + > + {props.children} + </Chip> + ) +} + +export default BigChip +BigChip.propTypes = { + ...Chip.propTypes, +} diff --git a/src/views/History/ChoreHistory.jsx b/src/views/History/ChoreHistory.jsx new file mode 100644 index 0000000..22ea6a9 --- /dev/null +++ b/src/views/History/ChoreHistory.jsx @@ -0,0 +1,344 @@ +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 diff --git a/src/views/History/InfoCard.jsx b/src/views/History/InfoCard.jsx new file mode 100644 index 0000000..bf1ada8 --- /dev/null +++ b/src/views/History/InfoCard.jsx @@ -0,0 +1,26 @@ +import { AddTask } from '@mui/icons-material' +import { Box } from '@mui/joy' +import Card from '@mui/joy/Card' +import CardContent from '@mui/joy/CardContent' +import Typography from '@mui/joy/Typography' +import * as React from 'react' + +function InfoCard() { + return ( + <Card sx={{ minWidth: 200, maxWidth: 200 }}> + <CardContent> + <Box mb={2} sx={{ textAlign: 'left' }}> + <AddTask + sx={{ + fontSize: '2.5em' /* Increase the font size */, + }} + /> + </Box> + <Typography level='title-md'>You've completed</Typography> + <Typography level='body-sm'>12345 Chores</Typography> + </CardContent> + </Card> + ) +} + +export default InfoCard |