From c7c4a011ef6ce55157189eb39a3b53797b850cce Mon Sep 17 00:00:00 2001 From: Mo Tarbin Date: Sun, 28 Sep 2025 02:20:18 -0400 Subject: [PATCH] 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. --- src/views/Chores/ChoreCard.jsx | 248 ++---------- src/views/Chores/CompactChoreCard.jsx | 237 ++---------- src/views/Chores/MyChores.jsx | 461 ++++++++++++++++------- src/views/components/AddTaskModal.jsx | 1 + src/views/components/ChoreActionMenu.jsx | 80 +--- 5 files changed, 406 insertions(+), 621 deletions(-) diff --git a/src/views/Chores/ChoreCard.jsx b/src/views/Chores/ChoreCard.jsx index f7b1b53..bbe8757 100644 --- a/src/views/Chores/ChoreCard.jsx +++ b/src/views/Chores/ChoreCard.jsx @@ -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 = ({ {getName(chore.name)} - {userProfile && chore.assignedTo !== userProfile.id && ( + {chore.assignedTo && chore.assignedTo !== userProfile?.id && ( )} + {chore.assignedTo === null && ( + + }> + Anyone + + + )} {chore.priority > 0 && ( { 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 = ({ }} >
- {isPendingCompletion ? ( - - ) : chore.status === 0 ? ( + {chore.status === 0 ? ( ) : chore.status === 1 ? ( ) : ( )} - {isPendingCompletion && ( - - )}
)} 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 = ({
- {confirmModelConfig?.isOpen && ( - - )} - { - if (timeoutId) { - clearTimeout(timeoutId) - setIsPendingCompletion(false) - setTimeoutId(null) - setSecondsLeftToCancel(null) // Reset or adjust as needed - } - }} - size='md' - variant='outlined' - color='primary' - startDecorator={} - > - Cancel - - } - > - - Task will be marked as completed in {secondsLeftToCancel} seconds - - ) } diff --git a/src/views/Chores/CompactChoreCard.jsx b/src/views/Chores/CompactChoreCard.jsx index 5944d5f..1023411 100644 --- a/src/views/Chores/CompactChoreCard.jsx +++ b/src/views/Chores/CompactChoreCard.jsx @@ -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 ( { + 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 = ({ -{isOfficialInstance && ( + {isOfficialInstance && ( { 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 ? ( - - ) : chore.status === 0 ? ( + {chore.status === 0 ? ( ) : chore.status === 1 ? ( @@ -1171,15 +1018,16 @@ const CompactChoreCard = ({ 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 = ({ - - {confirmModelConfig?.isOpen && ( - - )} - - {/* Snackbar for pending completion */} - { - if (timeoutId) { - clearTimeout(timeoutId) - setIsPendingCompletion(false) - setTimeoutId(null) - setSecondsLeftToCancel(null) - } - }} - size='sm' - variant='outlined' - color='primary' - startDecorator={} - > - Cancel - - } - > - - Task will be marked as completed in {secondsLeftToCancel} seconds - - ) } diff --git a/src/views/Chores/MyChores.jsx b/src/views/Chores/MyChores.jsx index cf60bf6..7c3900e 100644 --- a/src/views/Chores/MyChores.jsx +++ b/src/views/Chores/MyChores.jsx @@ -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 = () => { { 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 = () => { { handleCloseModal(true) } }) + handleCloseModal(false) } if (userLabelsLoading || isCircleMembersLoading) { return <> diff --git a/src/views/components/ChoreActionMenu.jsx b/src/views/components/ChoreActionMenu.jsx index 8794074..fdd288f 100644 --- a/src/views/components/ChoreActionMenu.jsx +++ b/src/views/components/ChoreActionMenu.jsx @@ -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 = ({ Delegate to someone else -{isOfficialInstance && ( + {isOfficialInstance && ( { e.stopPropagation()