Improve MarkChoreComplete, allow complete in past ,etc

This commit is contained in:
Mo Tarbin 2024-07-07 00:25:11 -04:00
parent df83cc8948
commit e039b732a8
3 changed files with 228 additions and 92 deletions

View file

@ -57,11 +57,20 @@ const GetChoreDetailById = id => {
headers: HEADERS(), headers: HEADERS(),
}) })
} }
const MarkChoreComplete = (id, note, completedDate) => {
const MarkChoreComplete = id => { const body = {
return Fetch(`${API_URL}/chores/${id}/do`, { note,
}
let completedDateFormated = ''
if (completedDate) {
completedDateFormated = `?completedDate=${new Date(
completedDate,
).toISOString()}`
}
return Fetch(`${API_URL}/chores/${id}/do${completedDateFormated}`, {
method: 'POST', method: 'POST',
headers: HEADERS(), headers: HEADERS(),
body: JSON.stringify(body),
}) })
} }
const CreateChore = chore => { const CreateChore = chore => {

View file

@ -3,14 +3,19 @@ import {
CancelScheduleSend, CancelScheduleSend,
Check, Check,
Checklist, Checklist,
Note,
PeopleAlt, PeopleAlt,
Person, Person,
} from '@mui/icons-material' } from '@mui/icons-material'
import { import {
Box, Box,
Button, Button,
Checkbox,
Container, Container,
Divider,
FormControl,
Grid, Grid,
Input,
ListItem, ListItem,
ListItemContent, ListItemContent,
ListItemDecorator, ListItemDecorator,
@ -21,7 +26,7 @@ import {
} from '@mui/joy' } from '@mui/joy'
import moment from 'moment' import moment from 'moment'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useParams, useSearchParams } from 'react-router-dom' import { useNavigate, useParams, useSearchParams } from 'react-router-dom'
import { import {
GetAllUsers, GetAllUsers,
GetChoreDetailById, GetChoreDetailById,
@ -43,14 +48,16 @@ const ChoreView = () => {
const [performers, setPerformers] = useState([]) const [performers, setPerformers] = useState([])
const [infoCards, setInfoCards] = useState([]) const [infoCards, setInfoCards] = useState([])
const { choreId } = useParams() const { choreId } = useParams()
const [note, setNote] = useState(null)
// query param `complete=true` const Navigate = useNavigate()
const [searchParams] = useSearchParams() const [searchParams] = useSearchParams()
const [isPendingCompletion, setIsPendingCompletion] = useState(false) const [isPendingCompletion, setIsPendingCompletion] = useState(false)
const [timeoutId, setTimeoutId] = useState(null) const [timeoutId, setTimeoutId] = useState(null)
const [secondsLeftToCancel, setSecondsLeftToCancel] = useState(null) const [secondsLeftToCancel, setSecondsLeftToCancel] = useState(null)
const [completedDate, setCompletedDate] = useState(null)
useEffect(() => { useEffect(() => {
Promise.all([ Promise.all([
GetChoreDetailById(choreId).then(resp => { GetChoreDetailById(choreId).then(resp => {
@ -82,7 +89,7 @@ const ChoreView = () => {
{ {
icon: <CalendarMonth />, icon: <CalendarMonth />,
text: 'Due Date', text: 'Due Date',
subtext: moment(chore.dueDate).format('MM/DD/YYYY hh:mm A'), subtext: moment(chore.nextDueDate).format('MM/DD/YYYY hh:mm A'),
}, },
{ {
icon: <PeopleAlt />, icon: <PeopleAlt />,
@ -119,12 +126,21 @@ const ChoreView = () => {
subtext: chore.lastCompletedDate subtext: chore.lastCompletedDate
? `${ ? `${
chore.lastCompletedDate && chore.lastCompletedDate &&
moment(chore.lastCompletedDate).format('MM/DD/YYYY hh:mm A') moment(chore.lastCompletedDate).fromNow()
}(${ // moment(chore.lastCompletedDate).format('MM/DD/YYYY hh:mm A'))
performers.find(p => p.id === chore.lastCompletedBy)?.displayName }(
})` ${
performers.find(p => p.id === chore.lastCompletedBy)
?.displayName
}
)`
: 'Never', : 'Never',
}, },
{
icon: <Note />,
text: 'Recent Note',
subtext: chore.notes || '--',
},
] ]
setInfoCards(cards) setInfoCards(cards)
} }
@ -143,10 +159,11 @@ const ChoreView = () => {
}, 1000) }, 1000)
const id = setTimeout(() => { const id = setTimeout(() => {
MarkChoreComplete(choreId) MarkChoreComplete(choreId, note, completedDate)
.then(resp => { .then(resp => {
if (resp.ok) { if (resp.ok) {
return resp.json().then(data => { return resp.json().then(data => {
setNote(null)
setChore(data.res) setChore(data.res)
}) })
} }
@ -174,22 +191,18 @@ const ChoreView = () => {
} }
return ( return (
<Container maxWidth='sm'> <Container
<Sheet maxWidth='sm'
variant='plain'
sx={{ sx={{
borderRadius: 'sm',
p: 2,
boxShadow: 'md',
minHeight: '90vh',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
// space between :
justifyContent: 'space-between', justifyContent: 'space-between',
}} }}
> >
<Box> <Box>
<Typography <Typography
level='h4' level='h3'
textAlign={'center'} textAlign={'center'}
sx={{ sx={{
mt: 2, mt: 2,
@ -202,9 +215,7 @@ const ChoreView = () => {
<Grid container spacing={1}> <Grid container spacing={1}>
{infoCards.map((info, index) => ( {infoCards.map((info, index) => (
<Grid key={index} item xs={12} sm={6}> <Grid key={index} item xs={12} sm={6}>
<Sheet <Sheet sx={{ mb: 1, borderRadius: 'md', p: 1, boxShadow: 'sm' }}>
sx={{ mb: 1, borderRadius: 'md', p: 1, boxShadow: 'sm' }}
>
<ListItem> <ListItem>
<ListItemDecorator> <ListItemDecorator>
<IconCard>{info.icon}</IconCard> <IconCard>{info.icon}</IconCard>
@ -223,11 +234,72 @@ const ChoreView = () => {
))} ))}
</Grid> </Grid>
</Box> </Box>
<Divider
sx={{
my: 2,
}}
/>
<Box>
<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
}
setNote(e.target.value)
}}
size='md'
sx={{
my: 1,
}}
/>
<FormControl size='sm' sx={{ width: 400 }}>
<Checkbox
defaultChecked={completedDate !== null}
checked={completedDate !== null}
value={completedDate !== null}
onChange={e => {
if (e.target.checked) {
setCompletedDate(
moment(new Date()).format('YYYY-MM-DDTHH:00:00'),
)
} else {
setCompletedDate(null)
}
}}
overlay
sx={{
my: 1,
}}
label={<Typography level='body2'>Set completion date</Typography>}
/>
</FormControl>
{completedDate !== null && (
<Input
sx={{ mt: 1, mb: 1.5, width: 300 }}
type='datetime-local'
value={completedDate}
onChange={e => {
setCompletedDate(e.target.value)
}}
/>
)}
{completedDate === null && (
// placeholder for the completion date with margin:
<Box <Box
sx={{ sx={{
mt: 6, height: 56,
}} }}
> />
)}
<Button <Button
fullWidth fullWidth
size='lg' size='lg'
@ -259,7 +331,7 @@ const ChoreView = () => {
<Box>Mark as {isPendingCompletion ? 'completed' : 'done'}</Box> <Box>Mark as {isPendingCompletion ? 'completed' : 'done'}</Box>
</Button> */} </Button> */}
</Box> </Box>
</Sheet>
<Snackbar <Snackbar
open={isPendingCompletion} open={isPendingCompletion}
endDecorator={ endDecorator={

View file

@ -1,4 +1,5 @@
import { import {
CancelScheduleSend,
Check, Check,
Delete, Delete,
Edit, Edit,
@ -21,6 +22,7 @@ import {
import { import {
Avatar, Avatar,
Box, Box,
Button,
Card, Card,
Chip, Chip,
CircularProgress, CircularProgress,
@ -29,6 +31,7 @@ import {
IconButton, IconButton,
Menu, Menu,
MenuItem, MenuItem,
Snackbar,
Typography, Typography,
} from '@mui/joy' } from '@mui/joy'
import moment from 'moment' import moment from 'moment'
@ -59,6 +62,9 @@ const ChoreCard = ({ chore, performers, onChoreUpdate, onChoreRemove, sx }) => {
const navigate = useNavigate() const navigate = useNavigate()
const [isDisabled, setIsDisabled] = React.useState(false) const [isDisabled, setIsDisabled] = React.useState(false)
const [isPendingCompletion, setIsPendingCompletion] = React.useState(false)
const [secondsLeftToCancel, setSecondsLeftToCancel] = React.useState(null)
const [timeoutId, setTimeoutId] = React.useState(null)
// useEffect(() => { // useEffect(() => {
// GetAllUsers() // GetAllUsers()
// .then(response => response.json()) // .then(response => response.json())
@ -117,18 +123,41 @@ const ChoreCard = ({ chore, performers, onChoreUpdate, onChoreRemove, sx }) => {
}) })
} }
const handleCompleteChore = () => { const handleTaskCompletion = () => {
MarkChoreComplete(chore.id).then(response => { setIsPendingCompletion(true)
if (response.ok) { let seconds = 3 // Starting countdown from 3 seconds
response.json().then(data => { setSecondsLeftToCancel(seconds)
const newChore = data.res
onChoreUpdate(newChore, 'completed') const countdownInterval = setInterval(() => {
seconds -= 1
setSecondsLeftToCancel(seconds)
if (seconds <= 0) {
clearInterval(countdownInterval) // Stop the countdown when it reaches 0
}
}, 1000)
const id = setTimeout(() => {
MarkChoreComplete(chore.id)
.then(resp => {
if (resp.ok) {
return resp.json().then(data => {
onChoreUpdate(data.res, 'completed')
}) })
} }
}) })
setIsDisabled(true) .then(() => {
setTimeout(() => setIsDisabled(false), 3000) // Re-enable the button after 5 seconds setIsPendingCompletion(false)
clearTimeout(id)
clearInterval(countdownInterval) // Ensure to clear this interval as well
setTimeoutId(null)
setSecondsLeftToCancel(null)
})
}, 3000)
setTimeoutId(id)
} }
const handleChangeDueDate = newDate => { const handleChangeDueDate = newDate => {
if (activeUserId === null) { if (activeUserId === null) {
alert('Please select a performer') alert('Please select a performer')
@ -353,7 +382,7 @@ const ChoreCard = ({ chore, performers, onChoreUpdate, onChoreRemove, sx }) => {
{/* Box in top right with Chip showing next due date */} {/* Box in top right with Chip showing next due date */}
<Box display='flex' justifyContent='start' alignItems='center'> <Box display='flex' justifyContent='start' alignItems='center'>
<Avatar sx={{ mr: 1, fontSize: 22 }}> <Avatar sx={{ mr: 1, fontSize: 22 }}>
{chore.name.charAt(0).toUpperCase()} {Array.from(chore.name)[0]}
</Avatar> </Avatar>
<Box display='flex' flexDirection='column'> <Box display='flex' flexDirection='column'>
<Typography level='title-md'>{chore.name}</Typography> <Typography level='title-md'>{chore.name}</Typography>
@ -409,8 +438,8 @@ const ChoreCard = ({ chore, performers, onChoreUpdate, onChoreRemove, sx }) => {
<IconButton <IconButton
variant='solid' variant='solid'
color='success' color='success'
onClick={handleCompleteChore} onClick={handleTaskCompletion}
disabled={isDisabled} disabled={isPendingCompletion}
sx={{ sx={{
borderRadius: '50%', borderRadius: '50%',
minWidth: 50, minWidth: 50,
@ -420,7 +449,7 @@ const ChoreCard = ({ chore, performers, onChoreUpdate, onChoreRemove, sx }) => {
> >
<div className='relative grid place-items-center'> <div className='relative grid place-items-center'>
<Check /> <Check />
{isDisabled && ( {isPendingCompletion && (
<CircularProgress <CircularProgress
variant='solid' variant='solid'
color='success' color='success'
@ -596,6 +625,32 @@ const ChoreCard = ({ chore, performers, onChoreUpdate, onChoreRemove, sx }) => {
}, },
}} }}
/> />
<Snackbar
open={isPendingCompletion}
endDecorator={
<Button
onClick={() => {
if (timeoutId) {
clearTimeout(timeoutId)
setIsPendingCompletion(false)
setTimeoutId(null)
setSecondsLeftToCancel(null) // Reset or adjust as needed
}
}}
size='md'
variant='outlined'
color='primary'
startDecorator={<CancelScheduleSend />}
>
Cancel
</Button>
}
>
<Typography level='body2' textAlign={'center'}>
Task will be marked as completed in {secondsLeftToCancel} seconds
</Typography>
</Snackbar>
</Card> </Card>
</> </>
) )