Files
donetick/src/views/Chores/hooks/useChoreActions.js

739 lines
22 KiB
JavaScript

import { useQueryClient } from '@tanstack/react-query'
import { useCallback } from 'react'
import { useArchiveChore } from '../../../queries/ChoreQueries'
import { usePauseChore, useStartChore } from '../../../queries/TimeQueries'
import {
ApproveChore,
DeleteChore,
MarkChoreComplete,
NudgeChore,
RejectChore,
SkipChore,
UndoChoreAction,
UpdateChoreAssignee,
UpdateDueDate,
} from '../../../utils/Fetcher'
export const useChoreActions = ({
chores,
filteredChores,
setChores,
setFilteredChores,
userProfile,
impersonatedUser,
showSuccess,
showError,
showWarning,
showUndo,
refetchChores,
setConfirmModelConfig,
openModal,
closeModal,
modalChore,
getSelectedChoresData,
clearSelection,
}) => {
const queryClient = useQueryClient()
const archiveChore = useArchiveChore()
const startChore = useStartChore()
const pauseChore = usePauseChore()
const updateChoreInState = useCallback(
(updatedChore, event) => {
let newChores = chores.map(c =>
c.id === updatedChore.id ? updatedChore : c,
)
let newFilteredChores = filteredChores.map(c =>
c.id === updatedChore.id ? updatedChore : c,
)
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)
queryClient.invalidateQueries({ queryKey: ['chores'] })
const undoableActions = {
completed: 'Task completed',
approved: 'Task approved',
rejected: 'Task rejected',
skipped: 'Task skipped',
}
if (undoableActions[event]) {
showSuccess({
message: undoableActions[event],
undoAction: async () => {
try {
const undoResponse = await UndoChoreAction(updatedChore.id)
if (undoResponse.ok) {
refetchChores()
const undoMessages = {
completed: 'Task completion has been undone.',
approved: 'Task approval has been undone.',
rejected: 'Task rejection has been undone.',
skipped: 'Task skip has been undone.',
}
showUndo({
title: 'Undo Successful',
message: undoMessages[event],
})
} else {
throw new Error('Failed to undo')
}
} catch (error) {
showError({
title: 'Undo Failed',
message: 'Unable to undo the action. Please try again.',
})
}
},
})
return
}
const notifications = {
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.',
},
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 })
}
},
[chores, filteredChores, setChores, setFilteredChores, queryClient, showSuccess, showError, showWarning, showUndo, refetchChores],
)
const handleChoreAction = useCallback(
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) {
const newChores = chores.filter(c => c.id !== chore.id)
const newFilteredChores = filteredChores.filter(
c => c.id !== chore.id,
)
setChores(newChores)
setFilteredChores(newFilteredChores)
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) {
}
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
case 'changeDueDate':
if (extraData && 'date' in extraData) {
try {
const response = await UpdateDueDate(chore.id, extraData.date)
if (response.ok) {
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 {
openModal(action, chore, extraData)
}
break
case 'completeWithNote':
case 'completeWithPastDate':
case 'changeAssignee':
case 'writeNFC':
case 'nudge':
openModal(action, chore, extraData)
break
default:
console.warn('Unknown action:', action)
}
},
[
impersonatedUser,
chores,
filteredChores,
setChores,
setFilteredChores,
updateChoreInState,
showError,
showSuccess,
setConfirmModelConfig,
openModal,
archiveChore,
startChore,
pauseChore,
],
)
const handleChangeDueDate = useCallback(
newDate => {
if (!modalChore) return
UpdateDueDate(modalChore.id, newDate).then(response => {
if (response.ok) {
response.json().then(data => {
const newChore = modalChore
newChore.nextDueDate = newDate
updateChoreInState(newChore, 'rescheduled')
})
}
})
closeModal()
},
[modalChore, updateChoreInState, closeModal],
)
const handleCompleteWithPastDate = useCallback(
newDate => {
if (!modalChore) return
MarkChoreComplete(
modalChore.id,
impersonatedUser ? { completedBy: impersonatedUser.userId } : null,
new Date(newDate).toISOString(),
null,
).then(response => {
if (response.ok) {
response.json().then(data => {
const newChore = data.res
updateChoreInState(newChore, 'completed')
})
}
})
closeModal()
},
[modalChore, impersonatedUser, updateChoreInState, closeModal],
)
const handleAssigneeChange = useCallback(
assigneeId => {
if (!modalChore) return
UpdateChoreAssignee(modalChore.id, assigneeId).then(response => {
if (response.ok) {
response.json().then(data => {
const newChore = data.res
updateChoreInState(newChore, 'assigned')
})
}
})
closeModal()
},
[modalChore, updateChoreInState, closeModal],
)
const handleCompleteWithNote = useCallback(
note => {
if (!modalChore) return
MarkChoreComplete(
modalChore.id,
impersonatedUser
? { note, completedBy: impersonatedUser.userId }
: { note },
null,
null,
).then(response => {
if (response.ok) {
response.json().then(data => {
const newChore = data.res
updateChoreInState(newChore, 'completed')
})
}
})
closeModal()
},
[modalChore, impersonatedUser, updateChoreInState, closeModal],
)
const handleNudge = useCallback(
async ({ choreId, message, notifyAllAssignees }) => {
try {
const response = await NudgeChore(choreId, {
message,
notifyAllAssignees,
})
if (response.ok) {
const data = await response.json()
showSuccess({
title: 'Nudge Sent!',
message: data.message || 'Nudge sent successfully',
})
} else {
throw new Error('Failed to send nudge')
}
} catch (error) {
showError({
title: 'Failed to Send Nudge',
message: error.message || 'Unable to send nudge at this time',
})
} finally {
closeModal()
}
},
[showSuccess, showError, closeModal],
)
const handleBulkComplete = useCallback(async () => {
const selectedData = getSelectedChoresData()
if (selectedData.length === 0) return
setConfirmModelConfig({
isOpen: true,
title: 'Complete Tasks',
confirmText: 'Complete',
cancelText: 'Cancel',
message: `Mark ${selectedData.length} task${selectedData.length > 1 ? 's' : ''} as completed?`,
onClose: async isConfirmed => {
if (isConfirmed === true) {
try {
const completedTasks = []
const failedTasks = []
for (const chore of selectedData) {
try {
await MarkChoreComplete(
chore.id,
impersonatedUser
? { completedBy: impersonatedUser.userId }
: null,
null,
null,
)
completedTasks.push(chore)
} catch (error) {
failedTasks.push(chore)
}
}
if (completedTasks.length > 0) {
showSuccess({
title: '✅ Tasks Completed',
message: `Successfully completed ${completedTasks.length} task${completedTasks.length > 1 ? 's' : ''}.`,
})
}
if (failedTasks.length > 0) {
showError({
title: 'Some Tasks Failed',
message: `${failedTasks.length} task${failedTasks.length > 1 ? 's' : ''} could not be completed.`,
})
}
refetchChores()
clearSelection()
} catch (error) {
showError({
title: 'Bulk Complete Failed',
message: 'An unexpected error occurred. Please try again.',
})
}
}
setConfirmModelConfig({})
},
})
}, [getSelectedChoresData, impersonatedUser, showSuccess, showError, refetchChores, clearSelection, setConfirmModelConfig])
const handleBulkArchive = useCallback(async () => {
const selectedData = getSelectedChoresData()
if (selectedData.length === 0) return
setConfirmModelConfig({
isOpen: true,
title: 'Archive Tasks',
confirmText: 'Archive',
cancelText: 'Cancel',
message: `Archive ${selectedData.length} task${selectedData.length > 1 ? 's' : ''}?`,
onClose: async isConfirmed => {
if (isConfirmed === true) {
try {
const archivedTasks = []
const failedTasks = []
for (const chore of selectedData) {
try {
await new Promise((resolve, reject) => {
archiveChore.mutate(chore.id, {
onSuccess: data => {
archivedTasks.push(data)
setChores(prev => prev.filter(c => c.id !== chore.id))
setFilteredChores(prev =>
prev.filter(c => c.id !== chore.id),
)
resolve(data)
},
onError: error => {
failedTasks.push(chore)
reject(error)
},
})
})
} catch (error) {
}
}
if (archivedTasks.length > 0) {
showSuccess({
title: '📦 Tasks Archived',
message: `Successfully archived ${archivedTasks.length} task${archivedTasks.length > 1 ? 's' : ''}.`,
})
}
if (failedTasks.length > 0) {
showError({
title: 'Some Tasks Failed',
message: `${failedTasks.length} task${failedTasks.length > 1 ? 's' : ''} could not be archived.`,
})
}
refetchChores()
clearSelection()
} catch (error) {
showError({
title: 'Bulk Archive Failed',
message: 'An unexpected error occurred. Please try again.',
})
}
}
setConfirmModelConfig({})
},
})
}, [getSelectedChoresData, archiveChore, setChores, setFilteredChores, showSuccess, showError, refetchChores, clearSelection, setConfirmModelConfig])
const handleBulkDelete = useCallback(async () => {
const selectedData = getSelectedChoresData()
if (selectedData.length === 0) return
setConfirmModelConfig({
isOpen: true,
title: 'Delete Tasks',
confirmText: 'Delete',
cancelText: 'Cancel',
message: `Delete ${selectedData.length} task${selectedData.length > 1 ? 's' : ''}?\n\nThis action cannot be undone.`,
onClose: async isConfirmed => {
if (isConfirmed === true) {
try {
const deletedTasks = []
const failedTasks = []
for (const chore of selectedData) {
try {
await DeleteChore(chore.id)
deletedTasks.push(chore)
} catch (error) {
failedTasks.push(chore)
}
}
if (deletedTasks.length > 0) {
showSuccess({
title: '🗑️ Tasks Deleted',
message: `Successfully deleted ${deletedTasks.length} task${deletedTasks.length > 1 ? 's' : ''}.`,
})
const deletedIds = new Set(deletedTasks.map(c => c.id))
const newChores = chores.filter(c => !deletedIds.has(c.id))
const newFilteredChores = filteredChores.filter(
c => !deletedIds.has(c.id),
)
setChores(newChores)
setFilteredChores(newFilteredChores)
}
if (failedTasks.length > 0) {
showError({
title: 'Some Tasks Failed',
message: `${failedTasks.length} task${failedTasks.length > 1 ? 's' : ''} could not be deleted.`,
})
}
refetchChores()
clearSelection()
} catch (error) {
showError({
title: 'Bulk Delete Failed',
message: 'An unexpected error occurred. Please try again.',
})
}
}
setConfirmModelConfig({})
},
})
}, [getSelectedChoresData, chores, filteredChores, setChores, setFilteredChores, showSuccess, showError, refetchChores, clearSelection, setConfirmModelConfig])
const handleBulkSkip = useCallback(async () => {
const selectedData = getSelectedChoresData()
if (selectedData.length === 0) return
setConfirmModelConfig({
isOpen: true,
title: 'Skip Tasks',
confirmText: 'Skip',
cancelText: 'Cancel',
message: `Skip ${selectedData.length} task${selectedData.length > 1 ? 's' : ''} to next due date?`,
onClose: async isConfirmed => {
if (isConfirmed === true) {
try {
const skippedTasks = []
const failedTasks = []
for (const chore of selectedData) {
try {
await SkipChore(chore.id)
skippedTasks.push(chore)
} catch (error) {
failedTasks.push(chore)
}
}
if (skippedTasks.length > 0) {
showSuccess({
title: '⏭️ Tasks Skipped',
message: `Successfully skipped ${skippedTasks.length} task${skippedTasks.length > 1 ? 's' : ''}.`,
undoAction: async () => {
try {
for (const chore of skippedTasks) {
await UndoChoreAction(chore.id)
}
refetchChores()
showUndo({
title: 'Undo Successful',
message: `Undo skip for ${skippedTasks.length} task${skippedTasks.length > 1 ? 's' : ''}.`,
})
} catch (error) {
showError({
title: 'Undo Failed',
message: 'Unable to undo the action. Please try again.',
})
}
},
})
}
if (failedTasks.length > 0) {
showError({
title: 'Some Tasks Failed',
message: `${failedTasks.length > 1 ? 's' : ''} could not be skipped.`,
})
}
refetchChores()
clearSelection()
} catch (error) {
showError({
title: 'Bulk Skip Failed',
message: 'An unexpected error occurred. Please try again.',
})
}
}
setConfirmModelConfig({})
},
})
}, [getSelectedChoresData, showSuccess, showError, showUndo, refetchChores, clearSelection, setConfirmModelConfig])
return {
handleChoreAction,
handleChangeDueDate,
handleCompleteWithPastDate,
handleAssigneeChange,
handleCompleteWithNote,
handleNudge,
handleBulkComplete,
handleBulkArchive,
handleBulkDelete,
handleBulkSkip,
}
}