Refactor chore management components to centralize action handling
- Removed individual chore action handlers from ChoreCard and CompactChoreCard. - Introduced a unified `onAction` prop to handle various chore actions (approve, reject, complete, start, pause, delete, etc.) in a centralized manner. - Updated ChoreActionMenu to utilize the new `onAction` prop for performing actions on chores. - Simplified state management and notification handling for chore updates. - Cleaned up unused imports and state variables across affected components.
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
import {
|
||||
CancelScheduleSend,
|
||||
Check,
|
||||
Delete,
|
||||
Edit,
|
||||
Group,
|
||||
HourglassEmpty,
|
||||
Notifications,
|
||||
Pause,
|
||||
@@ -18,41 +18,26 @@ import {
|
||||
import {
|
||||
Avatar,
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
Checkbox,
|
||||
Chip,
|
||||
CircularProgress,
|
||||
Grid,
|
||||
IconButton,
|
||||
Snackbar,
|
||||
Typography,
|
||||
} from '@mui/joy'
|
||||
import { config } from 'dotenv'
|
||||
import moment from 'moment'
|
||||
import React from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useImpersonateUser } from '../../contexts/ImpersonateUserContext.jsx'
|
||||
import { usePauseChore, useStartChore } from '../../queries/TimeQueries'
|
||||
import { useUserProfile } from '../../queries/UserQueries.jsx'
|
||||
import { useNotification } from '../../service/NotificationProvider'
|
||||
import { notInCompletionWindow } from '../../utils/Chores.jsx'
|
||||
import { getTextColorFromBackgroundColor } from '../../utils/Colors.jsx'
|
||||
import { isOfficialDonetickInstanceSync } from '../../utils/FeatureToggle'
|
||||
import {
|
||||
ApproveChore,
|
||||
DeleteChore,
|
||||
MarkChoreComplete,
|
||||
RejectChore,
|
||||
} from '../../utils/Fetcher'
|
||||
import Priorities from '../../utils/Priorities'
|
||||
import ConfirmationModal from '../Modals/Inputs/ConfirmationModal'
|
||||
import ChoreActionMenu from '../components/ChoreActionMenu'
|
||||
const ChoreCard = ({
|
||||
chore,
|
||||
performers,
|
||||
onChoreUpdate,
|
||||
onChoreRemove,
|
||||
sx,
|
||||
viewOnly,
|
||||
onChipClick,
|
||||
@@ -62,21 +47,13 @@ const ChoreCard = ({
|
||||
isSelected = false,
|
||||
onSelectionToggle,
|
||||
}) => {
|
||||
const [confirmModelConfig, setConfirmModelConfig] = React.useState({})
|
||||
const [isOfficialInstance, setIsOfficialInstance] = React.useState(false)
|
||||
const navigate = useNavigate()
|
||||
|
||||
const [isPendingCompletion, setIsPendingCompletion] = React.useState(false)
|
||||
const [secondsLeftToCancel, setSecondsLeftToCancel] = React.useState(null)
|
||||
const [timeoutId, setTimeoutId] = React.useState(null)
|
||||
const { data: userProfile } = useUserProfile()
|
||||
|
||||
const { impersonatedUser } = useImpersonateUser()
|
||||
|
||||
const { showError, showNotification } = useNotification()
|
||||
const startChore = useStartChore()
|
||||
const pauseChore = usePauseChore()
|
||||
|
||||
// Swipe functionality state
|
||||
const [swipeTranslateX, setSwipeTranslateX] = React.useState(0)
|
||||
const [isDragging, setIsDragging] = React.useState(false)
|
||||
@@ -104,110 +81,6 @@ const ChoreCard = ({
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleDelete = () => {
|
||||
setConfirmModelConfig({
|
||||
isOpen: true,
|
||||
title: 'Delete Chore',
|
||||
confirmText: 'Delete',
|
||||
cancelText: 'Cancel',
|
||||
message: 'Are you sure you want to delete this chore?',
|
||||
onClose: isConfirmed => {
|
||||
if (isConfirmed === true) {
|
||||
DeleteChore(chore.id).then(response => {
|
||||
if (response.ok) {
|
||||
onChoreRemove(chore)
|
||||
}
|
||||
})
|
||||
}
|
||||
setConfirmModelConfig({})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
setIsPendingCompletion(false) // Reset the state
|
||||
}
|
||||
}, 1000)
|
||||
|
||||
const id = setTimeout(() => {
|
||||
MarkChoreComplete(
|
||||
chore.id,
|
||||
impersonatedUser ? { completedBy: impersonatedUser.userId } : null,
|
||||
null,
|
||||
null,
|
||||
)
|
||||
.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)
|
||||
})
|
||||
.catch(error => {
|
||||
if (error?.queued) {
|
||||
showError({
|
||||
title: 'Update Failed',
|
||||
message: 'Request will be reattempt when you are online',
|
||||
})
|
||||
} else {
|
||||
showError({
|
||||
title: 'Failed to update',
|
||||
message: error,
|
||||
})
|
||||
}
|
||||
|
||||
setIsPendingCompletion(false)
|
||||
clearTimeout(id)
|
||||
clearInterval(countdownInterval) // Ensure to clear this interval as well
|
||||
setTimeoutId(null)
|
||||
setSecondsLeftToCancel(null)
|
||||
})
|
||||
}, 2000)
|
||||
|
||||
setTimeoutId(id)
|
||||
}
|
||||
|
||||
|
||||
const handleApproveChore = () => {
|
||||
resetSwipe()
|
||||
ApproveChore(chore.id).then(response => {
|
||||
if (response.ok) {
|
||||
response.json().then(data => {
|
||||
onChoreUpdate(data.res, 'approved')
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleRejectChore = () => {
|
||||
resetSwipe()
|
||||
RejectChore(chore.id).then(response => {
|
||||
if (response.ok) {
|
||||
response.json().then(data => {
|
||||
onChoreUpdate(data.res, 'rejected')
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// Check if the current user can approve/reject (admin, manager, or task owner)
|
||||
const canApproveReject = () => {
|
||||
if (!performers || !chore) return false
|
||||
@@ -397,31 +270,6 @@ const ChoreCard = ({
|
||||
}
|
||||
}, [hoverTimer])
|
||||
|
||||
// Handlers for start/pause/complete functionality
|
||||
const handleChorePause = () => {
|
||||
pauseChore.mutate(chore.id, {
|
||||
onSuccess: data => {
|
||||
const newChore = {
|
||||
...chore,
|
||||
status: data.res.status,
|
||||
}
|
||||
onChoreUpdate(newChore, 'paused')
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const handleChoreStart = () => {
|
||||
startChore.mutate(chore.id, {
|
||||
onSuccess: data => {
|
||||
const newChore = {
|
||||
...chore,
|
||||
status: data.res.status,
|
||||
}
|
||||
onChoreUpdate(newChore, 'started')
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const getDueDateChipText = nextDueDate => {
|
||||
if (chore.nextDueDate === null) return 'No Due Date'
|
||||
// if due in next 48 hours, we should it in this format : Tomorrow 11:00 AM
|
||||
@@ -638,7 +486,11 @@ const ChoreCard = ({
|
||||
variant='soft'
|
||||
color='success'
|
||||
size='md'
|
||||
onClick={handleApproveChore}
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
resetSwipe()
|
||||
onAction('approve', chore)
|
||||
}}
|
||||
sx={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
@@ -651,7 +503,11 @@ const ChoreCard = ({
|
||||
variant='soft'
|
||||
color='danger'
|
||||
size='md'
|
||||
onClick={handleRejectChore}
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
resetSwipe()
|
||||
onAction('reject', chore)
|
||||
}}
|
||||
sx={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
@@ -686,9 +542,9 @@ const ChoreCard = ({
|
||||
resetSwipe()
|
||||
|
||||
if (chore.status !== 0) {
|
||||
handleTaskCompletion()
|
||||
onAction('complete', chore)
|
||||
} else {
|
||||
handleChoreStart()
|
||||
onAction('start', chore)
|
||||
}
|
||||
}}
|
||||
sx={{
|
||||
@@ -768,7 +624,7 @@ const ChoreCard = ({
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
resetSwipe()
|
||||
handleDelete()
|
||||
onAction('delete', chore)
|
||||
}}
|
||||
sx={{
|
||||
width: 40,
|
||||
@@ -874,7 +730,7 @@ const ChoreCard = ({
|
||||
<Typography level='title-md'>
|
||||
{getName(chore.name)}
|
||||
</Typography>
|
||||
{userProfile && chore.assignedTo !== userProfile.id && (
|
||||
{chore.assignedTo && chore.assignedTo !== userProfile?.id && (
|
||||
<Box display='flex' alignItems='center' gap={0.5}>
|
||||
<Chip
|
||||
variant='outlined'
|
||||
@@ -895,6 +751,13 @@ const ChoreCard = ({
|
||||
</Chip>
|
||||
</Box>
|
||||
)}
|
||||
{chore.assignedTo === null && (
|
||||
<Box display='flex' alignItems='center' gap={0.5}>
|
||||
<Chip variant='outlined' startDecorator={<Group />}>
|
||||
Anyone
|
||||
</Chip>
|
||||
</Box>
|
||||
)}
|
||||
<Box key={`${chore.id}-labels`}>
|
||||
{chore.priority > 0 && (
|
||||
<Chip
|
||||
@@ -1014,7 +877,7 @@ const ChoreCard = ({
|
||||
color='success'
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
handleApproveChore()
|
||||
onAction('approve', chore)
|
||||
}}
|
||||
sx={{
|
||||
borderRadius: '50%',
|
||||
@@ -1041,7 +904,7 @@ const ChoreCard = ({
|
||||
color='danger'
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
handleRejectChore()
|
||||
onAction('reject', chore)
|
||||
}}
|
||||
sx={{
|
||||
borderRadius: '50%',
|
||||
@@ -1084,21 +947,19 @@ const ChoreCard = ({
|
||||
e.stopPropagation()
|
||||
switch (chore.status) {
|
||||
case 0: // Not started
|
||||
handleTaskCompletion()
|
||||
onAction('complete', chore)
|
||||
break
|
||||
case 1: // In progress
|
||||
handleChorePause()
|
||||
onAction('pause', chore)
|
||||
break
|
||||
case 2: // Paused
|
||||
handleChoreStart()
|
||||
onAction('start', chore)
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
}}
|
||||
disabled={
|
||||
isPendingCompletion || notInCompletionWindow(chore)
|
||||
}
|
||||
disabled={notInCompletionWindow(chore)}
|
||||
sx={{
|
||||
borderRadius: '50%',
|
||||
minWidth: 50,
|
||||
@@ -1118,41 +979,28 @@ const ChoreCard = ({
|
||||
}}
|
||||
>
|
||||
<div className='relative grid place-items-center'>
|
||||
{isPendingCompletion ? (
|
||||
<CircularProgress size='md' />
|
||||
) : chore.status === 0 ? (
|
||||
{chore.status === 0 ? (
|
||||
<Check />
|
||||
) : chore.status === 1 ? (
|
||||
<Pause />
|
||||
) : (
|
||||
<PlayArrow />
|
||||
)}
|
||||
{isPendingCompletion && (
|
||||
<CircularProgress
|
||||
variant='solid'
|
||||
color='success'
|
||||
size='md'
|
||||
sx={{
|
||||
color: 'success.main',
|
||||
position: 'absolute',
|
||||
zIndex: 0,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</IconButton>
|
||||
)}
|
||||
<ChoreActionMenu
|
||||
chore={chore}
|
||||
onChoreUpdate={onChoreUpdate}
|
||||
onChoreRemove={onChoreRemove}
|
||||
onCompleteWithNote={() => onAction('completeWithNote', chore)}
|
||||
onCompleteWithPastDate={() => onAction('completeWithPastDate', chore)}
|
||||
onCompleteWithPastDate={() =>
|
||||
onAction('completeWithPastDate', chore)
|
||||
}
|
||||
onAction={type => onAction(type, chore)}
|
||||
onChangeAssignee={() => onAction('changeAssignee', chore)}
|
||||
onChangeDueDate={() => onAction('changeDueDate', chore)}
|
||||
onWriteNFC={() => onAction('writeNFC', chore)}
|
||||
onNudge={() => onAction('nudge', chore)}
|
||||
onDelete={handleDelete}
|
||||
onDelete={() => onAction('delete', chore)}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onOpen={() => {
|
||||
// Clear any pending hide timer when menu opens
|
||||
@@ -1165,36 +1013,8 @@ const ChoreCard = ({
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
{confirmModelConfig?.isOpen && (
|
||||
<ConfirmationModal config={confirmModelConfig} />
|
||||
)}
|
||||
</Card>
|
||||
</Box>
|
||||
<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>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {
|
||||
CancelScheduleSend,
|
||||
Check,
|
||||
Delete,
|
||||
Edit,
|
||||
@@ -14,43 +13,23 @@ import {
|
||||
TimesOneMobiledata,
|
||||
Webhook,
|
||||
} from '@mui/icons-material'
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Checkbox,
|
||||
Chip,
|
||||
CircularProgress,
|
||||
IconButton,
|
||||
Snackbar,
|
||||
Typography,
|
||||
} from '@mui/joy'
|
||||
import { Box, Checkbox, Chip, IconButton, Typography } from '@mui/joy'
|
||||
import moment from 'moment'
|
||||
import React from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useImpersonateUser } from '../../contexts/ImpersonateUserContext.jsx'
|
||||
import { useCircleMembers, useUserProfile } from '../../queries/UserQueries.jsx'
|
||||
import { useNotification } from '../../service/NotificationProvider'
|
||||
import { notInCompletionWindow } from '../../utils/Chores.jsx'
|
||||
import {
|
||||
getPriorityColor,
|
||||
getTextColorFromBackgroundColor,
|
||||
} from '../../utils/Colors.jsx'
|
||||
import { isOfficialDonetickInstanceSync } from '../../utils/FeatureToggle'
|
||||
import {
|
||||
ApproveChore,
|
||||
DeleteChore,
|
||||
MarkChoreComplete,
|
||||
RejectChore,
|
||||
} from '../../utils/Fetcher'
|
||||
import { usePauseChore, useStartChore } from '../../queries/TimeQueries'
|
||||
import ConfirmationModal from '../Modals/Inputs/ConfirmationModal'
|
||||
import ChoreActionMenu from '../components/ChoreActionMenu'
|
||||
|
||||
const CompactChoreCard = ({
|
||||
chore,
|
||||
performers,
|
||||
onChoreUpdate,
|
||||
onChoreRemove,
|
||||
sx,
|
||||
viewOnly,
|
||||
onChipClick,
|
||||
@@ -60,22 +39,14 @@ const CompactChoreCard = ({
|
||||
isSelected = false,
|
||||
onSelectionToggle,
|
||||
}) => {
|
||||
const [confirmModelConfig, setConfirmModelConfig] = React.useState({})
|
||||
const [isOfficialInstance, setIsOfficialInstance] = React.useState(false)
|
||||
const navigate = useNavigate()
|
||||
const startChore = useStartChore()
|
||||
const pauseChore = usePauseChore()
|
||||
|
||||
const [isPendingCompletion, setIsPendingCompletion] = React.useState(false)
|
||||
const [secondsLeftToCancel, setSecondsLeftToCancel] = React.useState(null)
|
||||
const [timeoutId, setTimeoutId] = React.useState(null)
|
||||
const { data: userProfile } = useUserProfile()
|
||||
const { data: circleMembersData } = useCircleMembers()
|
||||
|
||||
const { impersonatedUser } = useImpersonateUser()
|
||||
|
||||
const { showError } = useNotification()
|
||||
|
||||
// Swipe functionality state
|
||||
const [swipeTranslateX, setSwipeTranslateX] = React.useState(0)
|
||||
const [isDragging, setIsDragging] = React.useState(false)
|
||||
@@ -93,7 +64,7 @@ const CompactChoreCard = ({
|
||||
setIsTouchDevice('ontouchstart' in window || navigator.maxTouchPoints > 0)
|
||||
}
|
||||
checkTouchDevice()
|
||||
|
||||
|
||||
// Check if this is the official donetick.com instance
|
||||
try {
|
||||
setIsOfficialInstance(isOfficialDonetickInstanceSync())
|
||||
@@ -274,111 +245,6 @@ const CompactChoreCard = ({
|
||||
}
|
||||
}, [hoverTimer])
|
||||
|
||||
// All the existing handler methods (same as original ChoreCard)
|
||||
const handleDelete = () => {
|
||||
setConfirmModelConfig({
|
||||
isOpen: true,
|
||||
title: 'Delete Chore',
|
||||
confirmText: 'Delete',
|
||||
cancelText: 'Cancel',
|
||||
message: 'Are you sure you want to delete this chore?',
|
||||
onClose: isConfirmed => {
|
||||
if (isConfirmed === true) {
|
||||
DeleteChore(chore.id).then(response => {
|
||||
if (response.ok) {
|
||||
onChoreRemove(chore)
|
||||
}
|
||||
})
|
||||
}
|
||||
setConfirmModelConfig({})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const handleTaskCompletion = () => {
|
||||
setIsPendingCompletion(true)
|
||||
let seconds = 3
|
||||
setSecondsLeftToCancel(seconds)
|
||||
|
||||
const countdownInterval = setInterval(() => {
|
||||
seconds -= 1
|
||||
setSecondsLeftToCancel(seconds)
|
||||
|
||||
if (seconds <= 0) {
|
||||
clearInterval(countdownInterval)
|
||||
setIsPendingCompletion(false)
|
||||
}
|
||||
}, 1000)
|
||||
|
||||
const id = setTimeout(() => {
|
||||
MarkChoreComplete(
|
||||
chore.id,
|
||||
impersonatedUser ? { completedBy: impersonatedUser.userId } : null,
|
||||
null,
|
||||
null,
|
||||
)
|
||||
.then(resp => {
|
||||
if (resp.ok) {
|
||||
return resp.json().then(data => {
|
||||
onChoreUpdate(data.res, 'completed')
|
||||
})
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
setIsPendingCompletion(false)
|
||||
clearTimeout(id)
|
||||
clearInterval(countdownInterval)
|
||||
setTimeoutId(null)
|
||||
setSecondsLeftToCancel(null)
|
||||
})
|
||||
.catch(error => {
|
||||
if (error?.queued) {
|
||||
showError({
|
||||
title: 'Update Failed',
|
||||
message: 'Request will be reattempt when you are online',
|
||||
})
|
||||
} else {
|
||||
showError({
|
||||
title: 'Failed to update',
|
||||
message: error,
|
||||
})
|
||||
}
|
||||
|
||||
setIsPendingCompletion(false)
|
||||
clearTimeout(id)
|
||||
clearInterval(countdownInterval)
|
||||
setTimeoutId(null)
|
||||
setSecondsLeftToCancel(null)
|
||||
})
|
||||
}, 2000)
|
||||
|
||||
setTimeoutId(id)
|
||||
}
|
||||
|
||||
|
||||
const handleApproveChore = () => {
|
||||
resetSwipe()
|
||||
ApproveChore(chore.id).then(response => {
|
||||
if (response.ok) {
|
||||
response.json().then(data => {
|
||||
onChoreUpdate(data.res, 'approved')
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleRejectChore = () => {
|
||||
resetSwipe()
|
||||
RejectChore(chore.id).then(response => {
|
||||
if (response.ok) {
|
||||
response.json().then(data => {
|
||||
onChoreUpdate(data.res, 'rejected')
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// Check if the current user can approve/reject (admin, manager, or task owner)
|
||||
const canApproveReject = () => {
|
||||
if (!circleMembersData?.res || !chore) return false
|
||||
@@ -543,12 +409,15 @@ const CompactChoreCard = ({
|
||||
parts.push(getRecurrentText(chore))
|
||||
|
||||
// Assignee (if not current user)
|
||||
if (userProfile && chore.assignedTo !== userProfile.id) {
|
||||
if (chore.assignedTo && chore.assignedTo !== userProfile.id) {
|
||||
const assignee = performers.find(
|
||||
p => p.id === chore.assignedTo,
|
||||
)?.displayName
|
||||
if (assignee) parts.push(assignee)
|
||||
}
|
||||
if (chore.assignedTo === null) {
|
||||
parts.push('Anyone')
|
||||
}
|
||||
|
||||
// Points
|
||||
if (chore.points > 0) {
|
||||
@@ -558,29 +427,6 @@ const CompactChoreCard = ({
|
||||
return parts.join(' • ')
|
||||
}
|
||||
|
||||
const handleChorePause = () => {
|
||||
pauseChore.mutate(chore.id, {
|
||||
onSuccess: data => {
|
||||
const newChore = {
|
||||
...chore,
|
||||
...data.res,
|
||||
}
|
||||
onChoreUpdate(newChore, 'paused')
|
||||
},
|
||||
})
|
||||
}
|
||||
const handleChoreStart = () => {
|
||||
startChore.mutate(chore.id, {
|
||||
onSuccess: data => {
|
||||
const newChore = {
|
||||
...chore,
|
||||
...data.res,
|
||||
}
|
||||
onChoreUpdate(newChore, 'started')
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Box key={chore.id + '-compact-box'}>
|
||||
<Box
|
||||
@@ -634,7 +480,11 @@ const CompactChoreCard = ({
|
||||
variant='soft'
|
||||
color='danger'
|
||||
size='sm'
|
||||
onClick={handleRejectChore}
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
resetSwipe()
|
||||
onAction('reject', chore)
|
||||
}}
|
||||
sx={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
@@ -669,10 +519,9 @@ const CompactChoreCard = ({
|
||||
resetSwipe()
|
||||
|
||||
if (chore.status === 0 || chore.status === 2) {
|
||||
handleChoreStart()
|
||||
onAction('start', chore)
|
||||
} else {
|
||||
// handleChorePause()
|
||||
handleTaskCompletion()
|
||||
onAction('complete', chore)
|
||||
}
|
||||
}}
|
||||
sx={{
|
||||
@@ -735,7 +584,7 @@ const CompactChoreCard = ({
|
||||
<Edit sx={{ fontSize: 16 }} />
|
||||
</IconButton>
|
||||
|
||||
{isOfficialInstance && (
|
||||
{isOfficialInstance && (
|
||||
<IconButton
|
||||
variant='soft'
|
||||
color='warning'
|
||||
@@ -762,7 +611,7 @@ const CompactChoreCard = ({
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
resetSwipe()
|
||||
handleDelete()
|
||||
onAction('delete', chore)
|
||||
}}
|
||||
sx={{
|
||||
width: 40,
|
||||
@@ -888,7 +737,7 @@ const CompactChoreCard = ({
|
||||
size='sm'
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
handleApproveChore()
|
||||
onAction('approve', chore)
|
||||
}}
|
||||
sx={{
|
||||
width: 24,
|
||||
@@ -911,7 +760,7 @@ const CompactChoreCard = ({
|
||||
size='sm'
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
handleRejectChore()
|
||||
onAction('reject', chore)
|
||||
}}
|
||||
sx={{
|
||||
width: 24,
|
||||
@@ -953,14 +802,14 @@ const CompactChoreCard = ({
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
if (chore.status === 0) {
|
||||
handleTaskCompletion()
|
||||
onAction('complete', chore)
|
||||
} else if (chore.status === 1) {
|
||||
handleChorePause()
|
||||
onAction('pause', chore)
|
||||
} else {
|
||||
handleChoreStart()
|
||||
onAction('start', chore)
|
||||
}
|
||||
}}
|
||||
disabled={isPendingCompletion || notInCompletionWindow(chore)}
|
||||
disabled={notInCompletionWindow(chore)}
|
||||
sx={{
|
||||
width: 32,
|
||||
height: 32,
|
||||
@@ -979,9 +828,7 @@ const CompactChoreCard = ({
|
||||
},
|
||||
}}
|
||||
>
|
||||
{isPendingCompletion ? (
|
||||
<CircularProgress size='sm' />
|
||||
) : chore.status === 0 ? (
|
||||
{chore.status === 0 ? (
|
||||
<Check sx={{ fontSize: 16 }} />
|
||||
) : chore.status === 1 ? (
|
||||
<Pause sx={{ fontSize: 16 }} />
|
||||
@@ -1171,15 +1018,16 @@ const CompactChoreCard = ({
|
||||
<ChoreActionMenu
|
||||
variant='plain'
|
||||
chore={chore}
|
||||
onChoreUpdate={onChoreUpdate}
|
||||
onChoreRemove={onChoreRemove}
|
||||
onAction={onAction}
|
||||
onCompleteWithNote={() => onAction('completeWithNote', chore)}
|
||||
onCompleteWithPastDate={() => onAction('completeWithPastDate', chore)}
|
||||
onCompleteWithPastDate={() =>
|
||||
onAction('completeWithPastDate', chore)
|
||||
}
|
||||
onChangeAssignee={() => onAction('changeAssignee', chore)}
|
||||
onChangeDueDate={() => onAction('changeDueDate', chore)}
|
||||
onWriteNFC={() => onAction('writeNFC', chore)}
|
||||
onNudge={() => onAction('nudge', chore)}
|
||||
onDelete={handleDelete}
|
||||
onDelete={() => onAction('delete', chore)}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
// onMouseLeave={handleMouseLeave}
|
||||
sx={{
|
||||
@@ -1199,37 +1047,6 @@ const CompactChoreCard = ({
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{confirmModelConfig?.isOpen && (
|
||||
<ConfirmationModal config={confirmModelConfig} />
|
||||
)}
|
||||
|
||||
{/* Snackbar for pending completion */}
|
||||
<Snackbar
|
||||
open={isPendingCompletion}
|
||||
endDecorator={
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId)
|
||||
setIsPendingCompletion(false)
|
||||
setTimeoutId(null)
|
||||
setSecondsLeftToCancel(null)
|
||||
}
|
||||
}}
|
||||
size='sm'
|
||||
variant='outlined'
|
||||
color='primary'
|
||||
startDecorator={<CancelScheduleSend />}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Typography level='body-xs' textAlign={'center'}>
|
||||
Task will be marked as completed in {secondsLeftToCancel} seconds
|
||||
</Typography>
|
||||
</Snackbar>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -64,12 +64,15 @@ import { useMediaQuery } from '@mui/material'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import KeyboardShortcutHint from '../../components/common/KeyboardShortcutHint'
|
||||
import { useImpersonateUser } from '../../contexts/ImpersonateUserContext.jsx'
|
||||
import { usePauseChore, useStartChore } from '../../queries/TimeQueries'
|
||||
import { useCircleMembers, useUserProfile } from '../../queries/UserQueries'
|
||||
import { ChoreFilters, ChoresGrouper, ChoreSorter } from '../../utils/Chores'
|
||||
import {
|
||||
ApproveChore,
|
||||
DeleteChore,
|
||||
MarkChoreComplete,
|
||||
NudgeChore,
|
||||
RejectChore,
|
||||
SkipChore,
|
||||
UpdateChoreAssignee,
|
||||
UpdateDueDate,
|
||||
@@ -96,6 +99,8 @@ const MyChores = () => {
|
||||
const archiveChore = useArchiveChore()
|
||||
const unarchiveChore = useUnArchiveChore()
|
||||
const deleteChores = useDeleteChores()
|
||||
const startChore = useStartChore()
|
||||
const pauseChore = usePauseChore()
|
||||
const [chores, setChores] = useState([])
|
||||
const [filteredChores, setFilteredChores] = useState([])
|
||||
const [searchFilter, setSearchFilter] = useState('All')
|
||||
@@ -495,11 +500,328 @@ const MyChores = () => {
|
||||
}
|
||||
}, [isMultiSelectMode, selectedChores.size, addTaskModalOpen])
|
||||
|
||||
// Centralized modal handlers
|
||||
const handleChoreAction = (action, chore, extraData = {}) => {
|
||||
setModalChore(chore)
|
||||
setModalData(extraData)
|
||||
setActiveModal(action)
|
||||
// Helper function to update local state and show notifications
|
||||
const updateChoreInState = (updatedChore, event) => {
|
||||
let newChores = chores.map(c =>
|
||||
c.id === updatedChore.id ? updatedChore : c,
|
||||
)
|
||||
let newFilteredChores = filteredChores.map(c =>
|
||||
c.id === updatedChore.id ? updatedChore : c,
|
||||
)
|
||||
|
||||
// Remove from lists if archived or completed (once/trigger types)
|
||||
if (
|
||||
event === 'archive' ||
|
||||
(event === 'completed' && updatedChore.frequencyType === 'once') ||
|
||||
updatedChore.frequencyType === 'trigger'
|
||||
) {
|
||||
newChores = newChores.filter(c => c.id !== updatedChore.id)
|
||||
newFilteredChores = newFilteredChores.filter(
|
||||
c => c.id !== updatedChore.id,
|
||||
)
|
||||
}
|
||||
|
||||
setChores(newChores)
|
||||
setFilteredChores(newFilteredChores)
|
||||
setChoreSections(
|
||||
ChoresGrouper(
|
||||
selectedChoreSection,
|
||||
newChores,
|
||||
ChoreFilters(impersonatedUser?.userId || userProfile?.id)[
|
||||
selectedChoreFilter
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
// Show notification based on event type
|
||||
const notifications = {
|
||||
completed: {
|
||||
type: 'success',
|
||||
title: 'Task Completed',
|
||||
message: 'Great job! The task has been marked as completed.',
|
||||
},
|
||||
skipped: {
|
||||
type: 'success',
|
||||
title: 'Task Skipped',
|
||||
message: 'The task has been moved to the next due date.',
|
||||
},
|
||||
rescheduled: {
|
||||
type: 'success',
|
||||
title: 'Task Rescheduled',
|
||||
message: 'The task due date has been updated successfully.',
|
||||
},
|
||||
'due-date-removed': {
|
||||
type: 'success',
|
||||
title: 'Task Unplanned',
|
||||
message: 'The task is now unplanned and has no due date.',
|
||||
},
|
||||
unarchive: {
|
||||
type: 'success',
|
||||
title: 'Task Restored',
|
||||
message: 'The task has been restored and is now active.',
|
||||
},
|
||||
archive: {
|
||||
type: 'success',
|
||||
title: 'Task Archived',
|
||||
message: 'The task has been archived and hidden from the active list.',
|
||||
},
|
||||
started: {
|
||||
type: 'success',
|
||||
title: 'Task Started',
|
||||
message: 'The task has been marked as started.',
|
||||
},
|
||||
paused: {
|
||||
type: 'warning',
|
||||
title: 'Task Paused',
|
||||
message: 'The task has been paused.',
|
||||
},
|
||||
approved: {
|
||||
type: 'success',
|
||||
title: 'Task Approved',
|
||||
message: 'The task has been approved.',
|
||||
},
|
||||
rejected: {
|
||||
type: 'warning',
|
||||
title: 'Task Rejected',
|
||||
message: 'The task has been rejected.',
|
||||
},
|
||||
deleted: {
|
||||
type: 'success',
|
||||
title: 'Task Deleted',
|
||||
message: 'The task has been deleted.',
|
||||
},
|
||||
}
|
||||
|
||||
const notification = notifications[event]
|
||||
if (notification) {
|
||||
const notifyFn =
|
||||
notification.type === 'warning' ? showWarning : showSuccess
|
||||
notifyFn({ title: notification.title, message: notification.message })
|
||||
}
|
||||
}
|
||||
|
||||
// Centralized action handler for all chore operations
|
||||
const handleChoreAction = async (action, chore, extraData = {}) => {
|
||||
switch (action) {
|
||||
case 'complete':
|
||||
try {
|
||||
const response = await MarkChoreComplete(
|
||||
chore.id,
|
||||
impersonatedUser ? { completedBy: impersonatedUser.userId } : null,
|
||||
null,
|
||||
null,
|
||||
)
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
updateChoreInState(data.res, 'completed')
|
||||
}
|
||||
} catch (error) {
|
||||
if (error?.queued) {
|
||||
showError({
|
||||
title: 'Update Failed',
|
||||
message: 'Request will be reattempt when you are online',
|
||||
})
|
||||
} else {
|
||||
showError({
|
||||
title: 'Failed to update',
|
||||
message: error,
|
||||
})
|
||||
}
|
||||
}
|
||||
break
|
||||
|
||||
case 'start':
|
||||
startChore.mutate(chore.id, {
|
||||
onSuccess: async res => {
|
||||
const data = await res.json()
|
||||
const newChore = { ...chore, status: data.res.status }
|
||||
updateChoreInState(newChore, 'started')
|
||||
},
|
||||
onError: error => {
|
||||
showError({
|
||||
title: 'Failed to start',
|
||||
message: error.message || 'Unable to start chore',
|
||||
})
|
||||
},
|
||||
})
|
||||
break
|
||||
|
||||
case 'pause':
|
||||
pauseChore.mutate(chore.id, {
|
||||
onSuccess: async res => {
|
||||
const data = await res.json()
|
||||
const newChore = { ...chore, status: data.res.status }
|
||||
updateChoreInState(newChore, 'paused')
|
||||
},
|
||||
onError: error => {
|
||||
showError({
|
||||
title: 'Failed to pause',
|
||||
message: error.message || 'Unable to pause chore',
|
||||
})
|
||||
},
|
||||
})
|
||||
break
|
||||
|
||||
case 'approve':
|
||||
try {
|
||||
const response = await ApproveChore(chore.id)
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
updateChoreInState(data.res, 'approved')
|
||||
}
|
||||
} catch (error) {
|
||||
showError({
|
||||
title: 'Failed to approve',
|
||||
message: error.message || 'Unable to approve chore',
|
||||
})
|
||||
}
|
||||
break
|
||||
|
||||
case 'reject':
|
||||
try {
|
||||
const response = await RejectChore(chore.id)
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
updateChoreInState(data.res, 'rejected')
|
||||
}
|
||||
} catch (error) {
|
||||
showError({
|
||||
title: 'Failed to reject',
|
||||
message: error.message || 'Unable to reject chore',
|
||||
})
|
||||
}
|
||||
break
|
||||
|
||||
case 'delete':
|
||||
setConfirmModelConfig({
|
||||
isOpen: true,
|
||||
title: 'Delete Chore',
|
||||
confirmText: 'Delete',
|
||||
cancelText: 'Cancel',
|
||||
message: 'Are you sure you want to delete this chore?',
|
||||
onClose: async isConfirmed => {
|
||||
if (isConfirmed === true) {
|
||||
try {
|
||||
const response = await DeleteChore(chore.id)
|
||||
if (response.ok) {
|
||||
// Remove from state and show notification
|
||||
const newChores = chores.filter(c => c.id !== chore.id)
|
||||
const newFilteredChores = filteredChores.filter(
|
||||
c => c.id !== chore.id,
|
||||
)
|
||||
setChores(newChores)
|
||||
setFilteredChores(newFilteredChores)
|
||||
setChoreSections(
|
||||
ChoresGrouper(
|
||||
selectedChoreSection,
|
||||
newChores,
|
||||
ChoreFilters(impersonatedUser?.userId || userProfile?.id)[
|
||||
selectedChoreFilter
|
||||
],
|
||||
),
|
||||
)
|
||||
showSuccess({
|
||||
title: 'Task Deleted',
|
||||
message: 'The task has been deleted successfully.',
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
showError({
|
||||
title: 'Failed to delete',
|
||||
message: error,
|
||||
})
|
||||
}
|
||||
}
|
||||
setConfirmModelConfig({})
|
||||
},
|
||||
})
|
||||
break
|
||||
|
||||
case 'archive':
|
||||
try {
|
||||
await new Promise((resolve, reject) => {
|
||||
archiveChore.mutate(chore.id, {
|
||||
onSuccess: data => {
|
||||
updateChoreInState(data, 'archive')
|
||||
resolve(data)
|
||||
},
|
||||
onError: error => {
|
||||
showError({
|
||||
title: 'Failed to archive',
|
||||
message: error.message || 'Unable to archive chore',
|
||||
})
|
||||
reject(error)
|
||||
},
|
||||
})
|
||||
})
|
||||
} catch (error) {
|
||||
// Error already handled in onError callback
|
||||
}
|
||||
break
|
||||
|
||||
case 'skip':
|
||||
try {
|
||||
const response = await SkipChore(chore.id)
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
updateChoreInState(data.res, 'skipped')
|
||||
}
|
||||
} catch (error) {
|
||||
showError({
|
||||
title: 'Failed to skip',
|
||||
message: error,
|
||||
})
|
||||
}
|
||||
break
|
||||
|
||||
// Quick reschedule actions (with date provided)
|
||||
case 'changeDueDate':
|
||||
console.log('Reschedule response data111:', chore, extraData)
|
||||
|
||||
if (extraData && 'date' in extraData) {
|
||||
// Quick reschedule with specific date (including null for remove)
|
||||
try {
|
||||
const response = await UpdateDueDate(chore.id, extraData.date)
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
console.log('Reschedule response data:', data, chore, extraData)
|
||||
|
||||
chore.nextDueDate = extraData.date
|
||||
const eventType =
|
||||
extraData.date === null ? 'due-date-removed' : 'rescheduled'
|
||||
updateChoreInState(chore, eventType)
|
||||
}
|
||||
} catch (error) {
|
||||
showError({
|
||||
title:
|
||||
extraData.date === null
|
||||
? 'Failed to remove due date'
|
||||
: 'Failed to reschedule',
|
||||
message: error.message || 'Unable to update due date',
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// Open modal for custom date selection
|
||||
setModalChore(chore)
|
||||
setModalData(extraData)
|
||||
setActiveModal(action)
|
||||
}
|
||||
break
|
||||
|
||||
// Modal-based actions
|
||||
case 'completeWithNote':
|
||||
case 'completeWithPastDate':
|
||||
case 'changeAssignee':
|
||||
case 'writeNFC':
|
||||
case 'nudge':
|
||||
setModalChore(chore)
|
||||
setModalData(extraData)
|
||||
setActiveModal(action)
|
||||
break
|
||||
|
||||
default:
|
||||
console.warn('Unknown action:', action)
|
||||
}
|
||||
}
|
||||
|
||||
const closeModal = () => {
|
||||
@@ -513,8 +835,9 @@ const MyChores = () => {
|
||||
UpdateDueDate(modalChore.id, newDate).then(response => {
|
||||
if (response.ok) {
|
||||
response.json().then(data => {
|
||||
const newChore = data.res
|
||||
handleChoreUpdated(newChore, 'rescheduled')
|
||||
const newChore = modalChore
|
||||
newChore.nextDueDate = newDate
|
||||
updateChoreInState(newChore, 'rescheduled')
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -532,7 +855,7 @@ const MyChores = () => {
|
||||
if (response.ok) {
|
||||
response.json().then(data => {
|
||||
const newChore = data.res
|
||||
handleChoreUpdated(newChore, 'completed')
|
||||
updateChoreInState(newChore, 'completed')
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -545,7 +868,7 @@ const MyChores = () => {
|
||||
if (response.ok) {
|
||||
response.json().then(data => {
|
||||
const newChore = data.res
|
||||
handleChoreUpdated(newChore, 'assigned')
|
||||
updateChoreInState(newChore, 'assigned')
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -565,7 +888,7 @@ const MyChores = () => {
|
||||
if (response.ok) {
|
||||
response.json().then(data => {
|
||||
const newChore = data.res
|
||||
handleChoreUpdated(newChore, 'completed')
|
||||
updateChoreInState(newChore, 'completed')
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -699,8 +1022,6 @@ const MyChores = () => {
|
||||
<CardComponent
|
||||
key={key || chore.id}
|
||||
chore={chore}
|
||||
onChoreUpdate={handleChoreUpdated}
|
||||
onChoreRemove={handleChoreDeleted}
|
||||
performers={performers}
|
||||
userLabels={userLabels}
|
||||
onChipClick={handleLabelFiltering}
|
||||
@@ -830,119 +1151,6 @@ const MyChores = () => {
|
||||
setSelectedCalendarDate(null)
|
||||
}
|
||||
|
||||
const handleChoreUpdated = (updatedChore, event) => {
|
||||
var newChores = chores.map(chore => {
|
||||
if (chore.id === updatedChore.id) {
|
||||
return updatedChore
|
||||
}
|
||||
return chore
|
||||
})
|
||||
|
||||
var newFilteredChores = filteredChores.map(chore => {
|
||||
if (chore.id === updatedChore.id) {
|
||||
return updatedChore
|
||||
}
|
||||
return chore
|
||||
})
|
||||
if (
|
||||
event === 'archive' ||
|
||||
(event === 'completed' && updatedChore.frequencyType === 'once') ||
|
||||
updatedChore.frequencyType === 'trigger'
|
||||
) {
|
||||
newChores = newChores.filter(chore => chore.id !== updatedChore.id)
|
||||
newFilteredChores = newFilteredChores.filter(
|
||||
chore => chore.id !== updatedChore.id,
|
||||
)
|
||||
}
|
||||
|
||||
setChores(newChores)
|
||||
setFilteredChores(newFilteredChores)
|
||||
setChoreSections(
|
||||
ChoresGrouper(
|
||||
selectedChoreSection,
|
||||
newChores,
|
||||
ChoreFilters(impersonatedUser?.userId || userProfile?.id)[
|
||||
selectedChoreFilter
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
switch (event) {
|
||||
case 'completed':
|
||||
showSuccess({
|
||||
title: 'Task Completed',
|
||||
message: 'Great job! The task has been marked as completed.',
|
||||
})
|
||||
break
|
||||
case 'skipped':
|
||||
showSuccess({
|
||||
title: 'Task Skipped',
|
||||
message: 'The task has been moved to the next due date.',
|
||||
})
|
||||
break
|
||||
case 'rescheduled':
|
||||
showSuccess({
|
||||
title: 'Task Rescheduled',
|
||||
message: 'The task due date has been updated successfully.',
|
||||
})
|
||||
break
|
||||
case 'due-date-removed':
|
||||
showSuccess({
|
||||
title: 'Task Unplanned',
|
||||
message: 'The task is now unplanned and has no due date.',
|
||||
})
|
||||
break
|
||||
case 'unarchive':
|
||||
showSuccess({
|
||||
title: 'Task Restored',
|
||||
message: 'The task has been restored and is now active.',
|
||||
})
|
||||
break
|
||||
case 'archive':
|
||||
showSuccess({
|
||||
title: 'Task Archived',
|
||||
message:
|
||||
'The task has been archived and hidden from the active list.',
|
||||
})
|
||||
break
|
||||
case 'started':
|
||||
showSuccess({
|
||||
title: 'Task Started',
|
||||
message: 'The task has been marked as started.',
|
||||
})
|
||||
break
|
||||
case 'paused':
|
||||
showWarning({
|
||||
title: 'Task Paused',
|
||||
message: 'The task has been paused.',
|
||||
})
|
||||
break
|
||||
case 'deleted':
|
||||
default:
|
||||
showSuccess({
|
||||
title: 'Task Updated',
|
||||
message: 'Your changes have been saved successfully.',
|
||||
})
|
||||
}
|
||||
}
|
||||
const handleChoreDeleted = deletedChore => {
|
||||
const newChores = chores.filter(chore => chore.id !== deletedChore.id)
|
||||
const newFilteredChores = filteredChores.filter(
|
||||
chore => chore.id !== deletedChore.id,
|
||||
)
|
||||
setChores(newChores)
|
||||
setFilteredChores(newFilteredChores)
|
||||
setChoreSections(
|
||||
ChoresGrouper(
|
||||
selectedChoreSection,
|
||||
newChores,
|
||||
ChoreFilters(impersonatedUser?.userId || userProfile?.id)[
|
||||
selectedChoreFilter
|
||||
],
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
const searchOptions = useMemo(
|
||||
() => ({
|
||||
keys: ['name', 'raw_label'],
|
||||
@@ -2115,11 +2323,10 @@ const MyChores = () => {
|
||||
<CompactChoreCard
|
||||
key={`calendar-${chore.id}`}
|
||||
chore={chore}
|
||||
onChoreUpdate={handleChoreUpdated}
|
||||
onChoreRemove={handleChoreDeleted}
|
||||
performers={performers}
|
||||
userLabels={userLabels}
|
||||
onChipClick={handleLabelFiltering}
|
||||
onAction={handleChoreAction}
|
||||
// Multi-select props
|
||||
isMultiSelectMode={isMultiSelectMode}
|
||||
isSelected={selectedChores.has(chore.id)}
|
||||
|
||||
@@ -460,6 +460,7 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
|
||||
handleCloseModal(true)
|
||||
}
|
||||
})
|
||||
handleCloseModal(false)
|
||||
}
|
||||
if (userLabelsLoading || isCircleMembersLoading) {
|
||||
return <></>
|
||||
|
||||
@@ -23,19 +23,11 @@ import {
|
||||
import { Divider, IconButton, Menu, MenuItem, Tooltip } from '@mui/joy'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useNotification } from '../../service/NotificationProvider'
|
||||
import { isOfficialDonetickInstanceSync } from '../../utils/FeatureToggle'
|
||||
import {
|
||||
DeleteChore,
|
||||
SkipChore,
|
||||
UpdateDueDate,
|
||||
} from '../../utils/Fetcher'
|
||||
import { useArchiveChore, useUnArchiveChore } from '../../queries/ChoreQueries'
|
||||
|
||||
const ChoreActionMenu = ({
|
||||
chore,
|
||||
onChoreUpdate,
|
||||
onChoreRemove,
|
||||
onAction,
|
||||
onCompleteWithNote,
|
||||
onCompleteWithPastDate,
|
||||
onChangeAssignee,
|
||||
@@ -53,9 +45,6 @@ const ChoreActionMenu = ({
|
||||
const [isOfficialInstance, setIsOfficialInstance] = useState(false)
|
||||
const menuRef = React.useRef(null)
|
||||
const navigate = useNavigate()
|
||||
const { showError } = useNotification()
|
||||
const archiveChore = useArchiveChore()
|
||||
const unArchiveChore = useUnArchiveChore()
|
||||
|
||||
// Check if this is the official donetick.com instance
|
||||
useEffect(() => {
|
||||
@@ -79,13 +68,13 @@ const ChoreActionMenu = ({
|
||||
}
|
||||
|
||||
document.addEventListener('mousedown', handleMenuOutsideClick)
|
||||
if (anchorEl) {
|
||||
if (anchorEl && onOpen) {
|
||||
onOpen()
|
||||
}
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleMenuOutsideClick)
|
||||
}
|
||||
}, [anchorEl])
|
||||
}, [anchorEl, onOpen])
|
||||
|
||||
const handleMenuOpen = event => {
|
||||
event.stopPropagation()
|
||||
@@ -115,59 +104,23 @@ const ChoreActionMenu = ({
|
||||
if (onDelete) {
|
||||
onDelete()
|
||||
} else {
|
||||
// Default delete behavior
|
||||
DeleteChore(chore.id).then(response => {
|
||||
if (response.ok) {
|
||||
onChoreRemove?.(chore)
|
||||
}
|
||||
})
|
||||
onAction?.('delete', chore)
|
||||
}
|
||||
handleMenuClose()
|
||||
}
|
||||
|
||||
const handleArchive = () => {
|
||||
if (chore.isActive) {
|
||||
archiveChore.mutate(chore.id, {
|
||||
onSuccess: () => {
|
||||
const newChore = { ...chore, isActive: false }
|
||||
onChoreUpdate?.(newChore, 'archive')
|
||||
},
|
||||
})
|
||||
onAction?.('archive', chore)
|
||||
} else {
|
||||
unArchiveChore.mutate(chore.id, {
|
||||
onSuccess: () => {
|
||||
const newChore = { ...chore, isActive: true }
|
||||
onChoreUpdate?.(newChore, 'unarchive')
|
||||
},
|
||||
})
|
||||
onAction?.('unarchive', chore)
|
||||
}
|
||||
handleMenuClose()
|
||||
}
|
||||
|
||||
const handleSkip = () => {
|
||||
SkipChore(chore.id)
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
response.json().then(data => {
|
||||
const newChore = data.res
|
||||
onChoreUpdate?.(newChore, 'skipped')
|
||||
handleMenuClose()
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
if (error?.queued) {
|
||||
showError({
|
||||
title: 'Failed to update',
|
||||
message: 'Request will be processed when you are online',
|
||||
})
|
||||
} else {
|
||||
showError({
|
||||
title: 'Failed to update',
|
||||
message: error,
|
||||
})
|
||||
}
|
||||
})
|
||||
onAction?.('skip', chore)
|
||||
handleMenuClose()
|
||||
}
|
||||
|
||||
const handleHistory = () => {
|
||||
@@ -238,20 +191,7 @@ const ChoreActionMenu = ({
|
||||
|
||||
const handleQuickSchedule = option => {
|
||||
const date = option === 'remove' ? null : getQuickScheduleDate(option)
|
||||
UpdateDueDate(chore.id, date).then(response => {
|
||||
if (response.ok) {
|
||||
response.json().then(data => {
|
||||
const newChore = {
|
||||
...chore,
|
||||
nextDueDate: date ? date.toISOString() : null,
|
||||
}
|
||||
onChoreUpdate(
|
||||
newChore,
|
||||
option === 'remove' ? 'due-date-removed' : 'rescheduled',
|
||||
)
|
||||
})
|
||||
}
|
||||
})
|
||||
onAction?.('changeDueDate', chore, { date })
|
||||
handleMenuClose()
|
||||
}
|
||||
|
||||
@@ -326,7 +266,7 @@ const ChoreActionMenu = ({
|
||||
<RecordVoiceOver />
|
||||
Delegate to someone else
|
||||
</MenuItem>
|
||||
{isOfficialInstance && (
|
||||
{isOfficialInstance && (
|
||||
<MenuItem
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
|
||||
Reference in New Issue
Block a user