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:
Mo Tarbin
2025-09-28 02:20:18 -04:00
parent bdd73135f0
commit c7c4a011ef
5 changed files with 406 additions and 621 deletions

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -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)}

View File

@@ -460,6 +460,7 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
handleCloseModal(true)
}
})
handleCloseModal(false)
}
if (userLabelsLoading || isCircleMembersLoading) {
return <></>

View File

@@ -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()