Add SkipChore function to Fetcher utility module, Update Loading Comp

This commit is contained in:
Mo Tarbin 2024-07-16 19:37:18 -04:00
parent 93512eb666
commit 7f4e592849
7 changed files with 296 additions and 171 deletions

View file

@ -26,6 +26,7 @@ import {
styled,
Typography,
} from '@mui/joy'
import { Divider } from '@mui/material'
import moment from 'moment'
import { useEffect, useState } from 'react'
import { useNavigate, useParams, useSearchParams } from 'react-router-dom'
@ -135,7 +136,7 @@ const ChoreView = () => {
? `${
performers.find(p => p.id === chore.lastCompletedBy)?.displayName
}`
: 'Never',
: '--',
},
{
size: 6,
@ -230,8 +231,8 @@ const ChoreView = () => {
level='h3'
// textAlign={'center'}
sx={{
mt: 2,
mb: 1,
mt: 1,
mb: 0.5,
}}
>
{chore.name}
@ -243,7 +244,7 @@ const ChoreView = () => {
</Chip>
</Box>
<Box>
<Typography level='title-md' sx={{ mb: 1 }}>
<Typography level='title-md' sx={{ mb: 0.5 }}>
Details
</Typography>
@ -265,9 +266,13 @@ const ChoreView = () => {
<Typography level='body-xs' sx={{ fontWeight: 'md' }}>
{detail.text}
</Typography>
<Typography level='body-sm' color='text.tertiary'>
<Chip
color='primary'
size='md'
startDecorator={detail.icon}
>
{detail.subtext ? detail.subtext : '--'}
</Typography>
</Chip>
</ListItemContent>
</ListItem>
</Grid>
@ -286,28 +291,6 @@ const ChoreView = () => {
</Sheet>
</>
)}
<Box
sx={{
display: 'flex',
flexDirection: 'row',
gap: 1,
alignContent: 'center',
justifyContent: 'center',
}}
>
<Button
startDecorator={<History />}
size='lg'
color='success'
variant='plain'
onClick={() => {
navigate(`/chores/${choreId}/history`)
}}
>
View History
</Button>
</Box>
</Box>
{/* <Divider
sx={{
@ -325,25 +308,63 @@ const ChoreView = () => {
mt: 2,
}}
>
<Typography level='title-md'>Additional Notes</Typography>
<Input
fullWidth
multiline
label='Additional Notes'
placeholder='note or information about the task'
value={note || ''}
onChange={e => {
if (e.target.value.trim() === '') {
setNote(null)
return
<Typography level='body-md' sx={{ mb: 1 }}>
Complete the task
</Typography>
<FormControl size='sm'>
<Checkbox
defaultChecked={note !== null}
checked={note !== null}
value={note !== null}
size='lg'
onChange={e => {
if (e.target.checked) {
setNote('')
} else {
setNote(null)
}
}}
overlay
sx={
{
// my: 1,
}
}
setNote(e.target.value)
}}
size='md'
sx={{
mb: 1,
}}
/>
label={
<Typography
level='body-sm'
sx={{
// center vertically
display: 'flex',
alignItems: 'center',
}}
>
Add Additional Notes
</Typography>
}
/>
</FormControl>
{note !== null && (
<Input
fullWidth
multiline
label='Additional Notes'
placeholder='note or information about the task'
value={note || ''}
onChange={e => {
if (e.target.value.trim() === '') {
setNote(null)
return
}
setNote(e.target.value)
}}
size='md'
sx={{
mb: 1,
}}
/>
)}
<FormControl size='sm'>
<Checkbox
@ -366,7 +387,18 @@ const ChoreView = () => {
// my: 1,
}
}
label={<Typography level='body2'>Set completion date</Typography>}
label={
<Typography
level='body-sm'
sx={{
// center vertically
display: 'flex',
alignItems: 'center',
}}
>
Specify completion date
</Typography>
}
/>
</FormControl>
{completedDate !== null && (
@ -390,30 +422,55 @@ const ChoreView = () => {
>
<Box>Mark as done</Box>
</Button>
<Button
fullWidth
size='lg'
onClick={() => {
setConfirmModelConfig({
isOpen: true,
title: 'Skip Task',
<Divider sx={{ my: 0.5 }}>or</Divider>
message: 'Are you sure you want to skip this task?',
confirmText: 'Skip',
cancelText: 'Cancel',
onClose: confirmed => {
if (confirmed) {
handleSkippingTask()
}
setConfirmModelConfig({})
},
})
<Box
sx={{
display: 'flex',
flexDirection: 'row',
gap: 1,
alignContent: 'center',
justifyContent: 'center',
}}
startDecorator={<SwitchAccessShortcut />}
>
<Box>Skip</Box>
</Button>
<Button
fullWidth
size='lg'
onClick={() => {
setConfirmModelConfig({
isOpen: true,
title: 'Skip Task',
message: 'Are you sure you want to skip this task?',
confirmText: 'Skip',
cancelText: 'Cancel',
onClose: confirmed => {
if (confirmed) {
handleSkippingTask()
}
setConfirmModelConfig({})
},
})
}}
startDecorator={<SwitchAccessShortcut />}
>
<Box>Skip</Box>
</Button>
<Button
startDecorator={<History />}
size='lg'
color='primary'
variant='outlined'
fullWidth
onClick={() => {
navigate(`/chores/${choreId}/history`)
}}
>
History
</Button>
</Box>
<Snackbar
open={isPendingCompletion}
endDecorator={

View file

@ -17,6 +17,7 @@ import {
SwitchAccessShortcut,
TimesOneMobiledata,
Update,
ViewCarousel,
Webhook,
} from '@mui/icons-material'
import {
@ -107,6 +108,9 @@ const ChoreCard = ({
const handleEdit = () => {
navigate(`/chores/${chore.id}/edit`)
}
const handleView = () => {
navigate(`/chores/${chore.id}`)
}
const handleDelete = () => {
setConfirmModelConfig({
isOpen: true,
@ -579,6 +583,10 @@ const ChoreCard = ({
<Edit />
Edit
</MenuItem>
<MenuItem onClick={handleView}>
<ViewCarousel />
View
</MenuItem>
<MenuItem onClick={handleDelete} color='danger'>
<Delete />
Delete

View file

@ -3,7 +3,6 @@ import {
Badge,
Box,
Checkbox,
CircularProgress,
Container,
IconButton,
Input,
@ -18,8 +17,8 @@ import Fuse from 'fuse.js'
import { useContext, useEffect, useRef, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { UserContext } from '../../contexts/UserContext'
import Logo from '../../Logo'
import { GetAllUsers, GetChores, GetUserProfile } from '../../utils/Fetcher'
import LoadingComponent from '../components/Loading'
import ChoreCard from './ChoreCard'
const MyChores = () => {
@ -184,18 +183,7 @@ const MyChores = () => {
}
if (userProfile === null) {
return (
<Container className='flex h-full items-center justify-center'>
<Box className='flex flex-col items-center justify-center'>
<CircularProgress
color='success'
sx={{ '--CircularProgress-size': '200px' }}
>
<Logo />
</CircularProgress>
</Box>
</Container>
)
return <LoadingComponent />
}
return (

View file

@ -1,15 +1,13 @@
import { Checklist, EventBusy, Timelapse } from '@mui/icons-material'
import { Checklist, EventBusy, Group, Timelapse } from '@mui/icons-material'
import {
Avatar,
Box,
Button,
CircularProgress,
Chip,
Container,
Grid,
List,
ListItem,
ListItemContent,
ListItemDecorator,
Sheet,
Typography,
} from '@mui/joy'
@ -19,6 +17,7 @@ import { Link, useParams } from 'react-router-dom'
import { API_URL } from '../../Config'
import { GetAllCircleMembers } from '../../utils/Fetcher'
import { Fetch } from '../../utils/TokenManager'
import LoadingComponent from '../components/Loading'
import HistoryCard from './HistoryCard'
const ChoreHistory = () => {
@ -92,59 +91,49 @@ const ChoreHistory = () => {
const historyInfo = [
{
icon: (
<Avatar>
<Checklist />
</Avatar>
),
text: `${histories.length} completed`,
subtext: `${Object.keys(userHistories).length} users contributed`,
icon: <Checklist />,
text: 'Total Completed',
subtext: `${histories.length} times`,
},
{
icon: (
<Avatar>
<Timelapse />
</Avatar>
),
text: `Completed within ${moment
.duration(averageDelayMoment)
.humanize()}`,
subtext: `Maximum delay was ${moment
.duration(maxDelayMoment)
.humanize()}`,
icon: <Timelapse />,
text: 'Usually Within',
subtext: moment.duration(averageDelayMoment).humanize(),
},
{
icon: <Avatar></Avatar>,
text: `${
icon: <Timelapse />,
text: 'Maximum Delay',
subtext: moment.duration(maxDelayMoment).humanize(),
},
{
icon: <Avatar />,
text: ' Completed Most',
subtext: `${
performers.find(p => p.userId === Number(userCompletedByMost))
?.displayName
} completed most`,
subtext: `${userHistories[userCompletedByMost]} time/s`,
} `,
},
// contributes:
{
icon: <Group />,
text: 'Total Performers',
subtext: `${Object.keys(userHistories).length} users`,
},
{
icon: <Avatar />,
text: 'Last Completed',
subtext: `${
performers.find(p => p.userId === Number(histories[0].completedBy))
?.displayName
}`,
},
]
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)
}
if (isLoading) {
return <CircularProgress /> // Show loading indicator
return <LoadingComponent />
}
if (!choreHistory.length) {
return (
@ -184,50 +173,43 @@ const ChoreHistory = () => {
return (
<Container maxWidth='md'>
<Typography level='h3' mb={1.5}>
<Typography level='title-md' 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>
<Sheet
// sx={{
// mb: 1,
// borderRadius: 'lg',
// p: 2,
// }}
sx={{ borderRadius: 'sm', p: 2 }}
variant='outlined'
>
<Grid container spacing={1}>
{historyInfo.map((info, index) => (
<Grid item xs={4} key={index}>
{/* divider between the list items: */}
<ListItem key={index}>
<ListItemContent>
<Typography level='body1' sx={{ fontWeight: 'md' }}>
<Typography level='body-xs' sx={{ fontWeight: 'md' }}>
{info.text}
</Typography>
<Typography level='body1' color='text.tertiary'>
{info.subtext}
</Typography>
<Chip color='primary' size='md' startDecorator={info.icon}>
{info.subtext ? info.subtext : '--'}
</Chip>
</ListItemContent>
</ListItem>
</Sheet>
</Grid>
))}
</Grid>
</Grid>
))}
</Grid>
</Sheet>
{/* User History Cards */}
<Typography level='h3' my={1.5}>
<Typography level='title-md' my={1.5}>
History:
</Typography>
<Box sx={{ borderRadius: 'sm', p: 2, boxShadow: 'md' }}>
<Sheet sx={{ borderRadius: 'sm', p: 2, boxShadow: 'md' }}>
{/* Chore History List (Updated Style) */}
<List sx={{ p: 0 }}>
@ -241,7 +223,7 @@ const ChoreHistory = () => {
/>
))}
</List>
</Box>
</Sheet>
</Container>
)
}

View file

@ -1,3 +1,4 @@
import { CalendarViewDay, Check, Timelapse } from '@mui/icons-material'
import {
Avatar,
Box,
@ -30,6 +31,50 @@ const HistoryCard = ({ allHistory, performers, historyEntry, index }) => {
return `${timeValue} ${unit}${timeValue !== 1 ? 's' : ''}`
}
const getCompletedChip = historyEntry => {
var text = 'No Due Date'
var color = 'info'
var icon = <CalendarViewDay />
// if completed few hours +-6 hours
if (
historyEntry.dueDate &&
historyEntry.completedAt > historyEntry.dueDate - 1000 * 60 * 60 * 6 &&
historyEntry.completedAt < historyEntry.dueDate + 1000 * 60 * 60 * 6
) {
text = 'On Time'
color = 'success'
icon = <Check />
} else if (
historyEntry.dueDate &&
historyEntry.completedAt < historyEntry.dueDate
) {
text = 'On Time'
color = 'success'
icon = <Check />
}
// if completed after due date then it's late
else if (
historyEntry.dueDate &&
historyEntry.completedAt > historyEntry.dueDate
) {
text = 'Late'
color = 'warning'
icon = <Timelapse />
} else {
text = 'No Due Date'
color = 'info'
icon = <CalendarViewDay />
}
return (
<Chip startDecorator={icon} color={color}>
{text}
</Chip>
)
}
return (
<>
<ListItem sx={{ gap: 1.5, alignItems: 'flex-start' }}>
@ -55,13 +100,7 @@ const HistoryCard = ({ allHistory, performers, historyEntry, index }) => {
<Typography level='body1' sx={{ fontWeight: 'md' }}>
{moment(historyEntry.completedAt).format('ddd MM/DD/yyyy HH:mm')}
</Typography>
<Chip>
{historyEntry.dueDate &&
historyEntry.completedAt > historyEntry.dueDate
? 'Late'
: 'On Time'}
</Chip>
{getCompletedChip(historyEntry)}
</Box>
<Typography level='body2' color='text.tertiary'>
<Chip>

View file

@ -7,7 +7,7 @@ const DemoHistory = () => {
{
id: 32,
choreId: 12,
completedAt: moment().format(),
completedAt: moment().hour(4).format(),
completedBy: 1,
assignedTo: 1,
notes: null,
@ -25,8 +25,8 @@ const DemoHistory = () => {
{
id: 31,
choreId: 12,
completedAt: moment().day(-10).format(),
completedBy: 1,
completedAt: moment().day(-10).hour(1).format(),
completedBy: 2,
assignedTo: 1,
notes: null,
dueDate: moment().day(-10).format(),

View file

@ -0,0 +1,51 @@
import { Box, CircularProgress, Container } from '@mui/joy'
import { Typography } from '@mui/material'
import { useEffect, useState } from 'react'
import Logo from '../../Logo'
const LoadingComponent = () => {
const [message, setMessage] = useState('Loading...')
const [subMessage, setSubMessage] = useState('')
useEffect(() => {
// if loading took more than 5 seconds update submessage to mention there might be an error:
const timeout = setTimeout(() => {
setSubMessage(
'This is taking longer than usual. There might be an issue.',
)
}, 5000)
return () => clearTimeout(timeout)
}, [])
return (
<Container className='flex h-full items-center justify-center'>
<Box
className='flex flex-col items-center justify-center'
sx={{
minHeight: '80vh',
}}
>
<CircularProgress
color='success'
sx={{ '--CircularProgress-size': '200px' }}
>
<Logo />
</CircularProgress>
<Box
className='flex items-center gap-2'
sx={{
fontWeight: 700,
fontSize: 24,
mt: 2,
}}
>
{message}
</Box>
<Typography level='h2' fontWeight={500} textAlign={'center'}>
{subMessage}
</Typography>
</Box>
</Container>
)
}
export default LoadingComponent