aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLibravatar Mo Tarbin <mhed.t91@gmail.com>2024-07-07 00:25:11 -0400
committerLibravatar Mo Tarbin <mhed.t91@gmail.com>2024-07-07 00:25:11 -0400
commite039b732a8f74ae2d2c9ebaf9330c74c09502099 (patch)
tree1609806a6f26ebad1e76ad23f2e68a1ce74aab8d
parentdf83cc894889ff2c8a121d4cb621776930e220a6 (diff)
downloaddonetick-frontend-e039b732a8f74ae2d2c9ebaf9330c74c09502099.tar.gz
donetick-frontend-e039b732a8f74ae2d2c9ebaf9330c74c09502099.tar.bz2
donetick-frontend-e039b732a8f74ae2d2c9ebaf9330c74c09502099.zip
Improve MarkChoreComplete, allow complete in past ,etc
-rw-r--r--src/utils/Fetcher.jsx15
-rw-r--r--src/views/ChoreEdit/ChoreView.jsx214
-rw-r--r--src/views/Chores/ChoreCard.jsx83
3 files changed, 224 insertions, 88 deletions
diff --git a/src/utils/Fetcher.jsx b/src/utils/Fetcher.jsx
index 0acc91f..afa9235 100644
--- a/src/utils/Fetcher.jsx
+++ b/src/utils/Fetcher.jsx
@@ -57,11 +57,20 @@ const GetChoreDetailById = id => {
headers: HEADERS(),
})
}
-
-const MarkChoreComplete = id => {
- return Fetch(`${API_URL}/chores/${id}/do`, {
+const MarkChoreComplete = (id, note, completedDate) => {
+ const body = {
+ note,
+ }
+ let completedDateFormated = ''
+ if (completedDate) {
+ completedDateFormated = `?completedDate=${new Date(
+ completedDate,
+ ).toISOString()}`
+ }
+ return Fetch(`${API_URL}/chores/${id}/do${completedDateFormated}`, {
method: 'POST',
headers: HEADERS(),
+ body: JSON.stringify(body),
})
}
const CreateChore = chore => {
diff --git a/src/views/ChoreEdit/ChoreView.jsx b/src/views/ChoreEdit/ChoreView.jsx
index 8116270..29268bc 100644
--- a/src/views/ChoreEdit/ChoreView.jsx
+++ b/src/views/ChoreEdit/ChoreView.jsx
@@ -3,14 +3,19 @@ import {
CancelScheduleSend,
Check,
Checklist,
+ Note,
PeopleAlt,
Person,
} from '@mui/icons-material'
import {
Box,
Button,
+ Checkbox,
Container,
+ Divider,
+ FormControl,
Grid,
+ Input,
ListItem,
ListItemContent,
ListItemDecorator,
@@ -21,7 +26,7 @@ import {
} from '@mui/joy'
import moment from 'moment'
import { useEffect, useState } from 'react'
-import { useParams, useSearchParams } from 'react-router-dom'
+import { useNavigate, useParams, useSearchParams } from 'react-router-dom'
import {
GetAllUsers,
GetChoreDetailById,
@@ -43,14 +48,16 @@ const ChoreView = () => {
const [performers, setPerformers] = useState([])
const [infoCards, setInfoCards] = useState([])
const { choreId } = useParams()
+ const [note, setNote] = useState(null)
- // query param `complete=true`
+ const Navigate = useNavigate()
const [searchParams] = useSearchParams()
const [isPendingCompletion, setIsPendingCompletion] = useState(false)
const [timeoutId, setTimeoutId] = useState(null)
const [secondsLeftToCancel, setSecondsLeftToCancel] = useState(null)
+ const [completedDate, setCompletedDate] = useState(null)
useEffect(() => {
Promise.all([
GetChoreDetailById(choreId).then(resp => {
@@ -82,7 +89,7 @@ const ChoreView = () => {
{
icon: <CalendarMonth />,
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 />,
@@ -119,12 +126,21 @@ const ChoreView = () => {
subtext: chore.lastCompletedDate
? `${
chore.lastCompletedDate &&
- moment(chore.lastCompletedDate).format('MM/DD/YYYY hh:mm A')
- }(${
- performers.find(p => p.id === chore.lastCompletedBy)?.displayName
- })`
+ moment(chore.lastCompletedDate).fromNow()
+ // moment(chore.lastCompletedDate).format('MM/DD/YYYY hh:mm A'))
+ }(
+ ${
+ performers.find(p => p.id === chore.lastCompletedBy)
+ ?.displayName
+ }
+ )`
: 'Never',
},
+ {
+ icon: <Note />,
+ text: 'Recent Note',
+ subtext: chore.notes || '--',
+ },
]
setInfoCards(cards)
}
@@ -143,10 +159,11 @@ const ChoreView = () => {
}, 1000)
const id = setTimeout(() => {
- MarkChoreComplete(choreId)
+ MarkChoreComplete(choreId, note, completedDate)
.then(resp => {
if (resp.ok) {
return resp.json().then(data => {
+ setNote(null)
setChore(data.res)
})
}
@@ -174,75 +191,130 @@ const ChoreView = () => {
}
return (
- <Container maxWidth='sm'>
- <Sheet
- variant='plain'
+ <Container
+ maxWidth='sm'
+ sx={{
+ display: 'flex',
+ flexDirection: 'column',
+ // space between :
+ justifyContent: 'space-between',
+ }}
+ >
+ <Box>
+ <Typography
+ level='h3'
+ textAlign={'center'}
+ sx={{
+ mt: 2,
+ mb: 4,
+ }}
+ >
+ {chore.name}
+ </Typography>
+
+ <Grid container spacing={1}>
+ {infoCards.map((info, index) => (
+ <Grid key={index} item xs={12} sm={6}>
+ <Sheet sx={{ mb: 1, borderRadius: 'md', p: 1, boxShadow: 'sm' }}>
+ <ListItem>
+ <ListItemDecorator>
+ <IconCard>{info.icon}</IconCard>
+ </ListItemDecorator>
+ <ListItemContent>
+ <Typography level='body1' sx={{ fontWeight: 'md' }}>
+ {info.text}
+ </Typography>
+ <Typography level='body1' color='text.tertiary'>
+ {info.subtext ? info.subtext : '--'}
+ </Typography>
+ </ListItemContent>
+ </ListItem>
+ </Sheet>
+ </Grid>
+ ))}
+ </Grid>
+ </Box>
+ <Divider
sx={{
- borderRadius: 'sm',
- p: 2,
- boxShadow: 'md',
- minHeight: '90vh',
- display: 'flex',
- flexDirection: 'column',
- justifyContent: 'space-between',
+ my: 2,
}}
- >
- <Box>
- <Typography
- level='h4'
- textAlign={'center'}
+ />
+
+ <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={{
- mt: 2,
- mb: 4,
+ my: 1,
}}
- >
- {chore.name}
- </Typography>
+ 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
+ sx={{
+ height: 56,
+ }}
+ />
+ )}
- <Grid container spacing={1}>
- {infoCards.map((info, index) => (
- <Grid key={index} item xs={12} sm={6}>
- <Sheet
- sx={{ mb: 1, borderRadius: 'md', p: 1, boxShadow: 'sm' }}
- >
- <ListItem>
- <ListItemDecorator>
- <IconCard>{info.icon}</IconCard>
- </ListItemDecorator>
- <ListItemContent>
- <Typography level='body1' sx={{ fontWeight: 'md' }}>
- {info.text}
- </Typography>
- <Typography level='body1' color='text.tertiary'>
- {info.subtext ? info.subtext : '--'}
- </Typography>
- </ListItemContent>
- </ListItem>
- </Sheet>
- </Grid>
- ))}
- </Grid>
- </Box>
- <Box
+ <Button
+ fullWidth
+ size='lg'
sx={{
- mt: 6,
+ height: 50,
+ mb: 2,
}}
+ onClick={handleTaskCompletion}
+ disabled={isPendingCompletion}
+ color={isPendingCompletion ? 'danger' : 'success'}
+ startDecorator={<Check />}
>
- <Button
- fullWidth
- size='lg'
- sx={{
- height: 50,
- mb: 2,
- }}
- onClick={handleTaskCompletion}
- disabled={isPendingCompletion}
- color={isPendingCompletion ? 'danger' : 'success'}
- startDecorator={<Check />}
- >
- <Box>Mark as done</Box>
- </Button>
- {/* <Button
+ <Box>Mark as done</Box>
+ </Button>
+ {/* <Button
sx={{
borderRadius: '32px',
mt: 1,
@@ -258,8 +330,8 @@ const ChoreView = () => {
>
<Box>Mark as {isPendingCompletion ? 'completed' : 'done'}</Box>
</Button> */}
- </Box>
- </Sheet>
+ </Box>
+
<Snackbar
open={isPendingCompletion}
endDecorator={
diff --git a/src/views/Chores/ChoreCard.jsx b/src/views/Chores/ChoreCard.jsx
index 305f7c4..6a38e0b 100644
--- a/src/views/Chores/ChoreCard.jsx
+++ b/src/views/Chores/ChoreCard.jsx
@@ -1,4 +1,5 @@
import {
+ CancelScheduleSend,
Check,
Delete,
Edit,
@@ -21,6 +22,7 @@ import {
import {
Avatar,
Box,
+ Button,
Card,
Chip,
CircularProgress,
@@ -29,6 +31,7 @@ import {
IconButton,
Menu,
MenuItem,
+ Snackbar,
Typography,
} from '@mui/joy'
import moment from 'moment'
@@ -59,6 +62,9 @@ const ChoreCard = ({ chore, performers, onChoreUpdate, onChoreRemove, sx }) => {
const navigate = useNavigate()
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(() => {
// GetAllUsers()
// .then(response => response.json())
@@ -117,18 +123,41 @@ const ChoreCard = ({ chore, performers, onChoreUpdate, onChoreRemove, sx }) => {
})
}
- const handleCompleteChore = () => {
- MarkChoreComplete(chore.id).then(response => {
- if (response.ok) {
- response.json().then(data => {
- const newChore = data.res
- onChoreUpdate(newChore, 'completed')
- })
+ const handleTaskCompletion = () => {
+ setIsPendingCompletion(true)
+ let seconds = 3 // Starting countdown from 3 seconds
+ setSecondsLeftToCancel(seconds)
+
+ const countdownInterval = setInterval(() => {
+ seconds -= 1
+ setSecondsLeftToCancel(seconds)
+
+ if (seconds <= 0) {
+ clearInterval(countdownInterval) // Stop the countdown when it reaches 0
}
- })
- setIsDisabled(true)
- setTimeout(() => setIsDisabled(false), 3000) // Re-enable the button after 5 seconds
+ }, 1000)
+
+ const id = setTimeout(() => {
+ MarkChoreComplete(chore.id)
+ .then(resp => {
+ if (resp.ok) {
+ return resp.json().then(data => {
+ onChoreUpdate(data.res, 'completed')
+ })
+ }
+ })
+ .then(() => {
+ setIsPendingCompletion(false)
+ clearTimeout(id)
+ clearInterval(countdownInterval) // Ensure to clear this interval as well
+ setTimeoutId(null)
+ setSecondsLeftToCancel(null)
+ })
+ }, 3000)
+
+ setTimeoutId(id)
}
+
const handleChangeDueDate = newDate => {
if (activeUserId === null) {
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 display='flex' justifyContent='start' alignItems='center'>
<Avatar sx={{ mr: 1, fontSize: 22 }}>
- {chore.name.charAt(0).toUpperCase()}
+ {Array.from(chore.name)[0]}
</Avatar>
<Box display='flex' flexDirection='column'>
<Typography level='title-md'>{chore.name}</Typography>
@@ -409,8 +438,8 @@ const ChoreCard = ({ chore, performers, onChoreUpdate, onChoreRemove, sx }) => {
<IconButton
variant='solid'
color='success'
- onClick={handleCompleteChore}
- disabled={isDisabled}
+ onClick={handleTaskCompletion}
+ disabled={isPendingCompletion}
sx={{
borderRadius: '50%',
minWidth: 50,
@@ -420,7 +449,7 @@ const ChoreCard = ({ chore, performers, onChoreUpdate, onChoreRemove, sx }) => {
>
<div className='relative grid place-items-center'>
<Check />
- {isDisabled && (
+ {isPendingCompletion && (
<CircularProgress
variant='solid'
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>
</>
)